The tax-prep tracking, IRS transcript handpull, and bookkeeping work-log modals as server-rendered Django form specs: field tables (name, type, required, conditional-on, error copy), copy-paste modal markup on DaisyUI modal + native <dialog> (the §7 collapse, applied), and the HTMX interaction contract. Source of truth is ui_kits/product/app.js — every option list, default, required rule and error string below is lifted from it verbatim; do not invent fields. Companion: reference.html §5 (field vocabulary) · §6 (partial map) · §7 (class budget).
The shared modal + HTMX contract — all three forms follow it; per-form seams are noted on each block:
Shell: DaisyUI modal on a native <dialog> — .modal-box panel, thin .mdl-head / .mdl-body / .mdl-foot app scaffolding inside it, <form method="dialog" class="modal-backdrop"> for backdrop-click close. Escape is native (cancel). The interactive kit (index.html) now runs this exact markup; in real builds modal / modal-box / modal-backdrop come from the DaisyUI plugin (previewed here via daisy.css).
Open: trigger hx-get="/tax-prep/cases/{id}/forms/{form}/" → hx-target="#tp-modal" hx-swap="innerHTML"; the server returns the <dialog> fragment and hx-on::after-swap calls showModal(). #tp-modal is an empty container in the app base template.
Close: Escape · backdrop form · header × · Cancel — all through dialog.close(), no server round-trip, form state discarded.
Inline validation (delayed-trigger, matching the codebase's keyup pattern): text/number inputs hx-post …/validate/ hx-trigger="keyup changed delay:300ms" hx-target="closest .tp-fld" hx-swap="outerHTML"; discrete controls validate on change. The POST re-check in Form.clean() is authoritative.
Conditional sections: Alpine x-show — client-only visibility, per the HTMX-first/JS-last rule. The server enforces every required-when rule in clean() and discards values from hidden sections (e.g. tracking clears states unless Agency is State/Both — mirrors app.js).
Errors: invalid POST → 422 with the re-rendered form fragment swapped back into the dialog — .tp-fld.err + its .errmsg per field, footer .mdl-req.err reads “Please complete the highlighted fields” (default: “Fields marked * are required”).
Success: 204 + HX-Trigger: close the dialog, fire the toast (copy per form below), and swap the owning region out-of-band — tracking: #tp-year-table + the case row (_case_row.html, outerHTML); handpull: the transcripts card; bookkeeping: the bookkeeping card / inventory row.
Segmented choices, year toggles, option chips are button-look <label>s wrapping hidden <input type="radio|checkbox"> — the input is what posts; .on mirrors :checked (Alpine :class or CSS :has). Locked fields render .tp-readonly-inp display-only — their values come from the URL case server-side and are never trusted from the client.
Field specpartialpartials/_tracking_form.htmlsubmithx-post /tax-prep/cases/{id}/tracking/on success toast Tracking saved — {n} year(s) set to “{status}” · swap #tp-year-table + case row
Field
Control · type
Required
Conditional on
Error copy (app.js)
case
Locked display (.tp-readonly-inp) — from URL, server-side
—
—
—
years
Multi checkbox toggles (.tp-ytog), seeded from the case's scope years; “+ other…” select appends any year 2015–2026 not already a chip
Yes — ≥ 1
—
Select at least one year.
agency
Segmented radio (.tp-segchoice): Federal / State / Both — no default
Yes
—
Choose an agency.
states
Chip multi-select (.tp-msel), the 51 US states + DC; chips removable
When Agency is State or Both — ≥ 1; cleared server-side otherwise
agency ∈ {State, Both}
Add at least one state for {Agency} filings.
status
Select — the finalized-17 (FINAL_STATUSES, canonical order; placeholder “Select a status…”)
Yes
—
Select a status.
date_filed
Date input
No
—
—
applies_to
Segmented radio: Personal / Business / Both — default seeded from scope (Both if the case has both return types, else the one it has). Routes the status onto the return type(s) — and can add that return to a year not yet in scope
Always has a value
—
—
follow_up
Checkbox “Follow-up required?” (.tp-chkline)
No
—
—
assign_to
Select — active TP users (kit sample: Allen Rose, Cale Roy, Priya Shah, TPA Pool)
When follow_up
follow_up
Assign the follow-up.
follow_up_date
Date input
When follow_up
follow_up
Pick a date.
balance
Checkbox “Balance(s) owed?”
No
—
—
balance_notes
Textarea
No (submit without text records “Balance flagged”)
balance
—
What submit writes (submitTracking — the model contract): for each selected year × the return type(s) from Applies-to — if the year isn't in scope it is appended (flagged added, shows the “New” badge) — set the federal status when Agency is Federal/Both, and set/insert a per-state status for every selected state when Agency is State/Both. Also records the tracking caption (agency, states, date filed), the follow-up task, and the balance-owed note. This write is what retires the Zapier “TP Tracking” submission.
Copy-paste markupstate shown agency Both · follow-up + balance checked (conditional sections open) · open attr + .rx-dlg wrapper are specimen-only
Notes on the markup. The <form style="display:contents"> wraps body + footer so the submit button posts while .modal-box keeps its flex scroll chain. The cheat-sheet table is static server-rendered content; its search box filters rows client-side (Alpine) — no HTMX. The year “+ other…” select appends a checked toggle chip client-side; the posted field is always years. The “platform” tag on Applies-to marks a capability the Zapier form never had.
Locked constant: THS (“Transcripts are retrieved via the THS software.”)
—
—
—
transcript_types
Multi option chips (.tp-optchip) — HP_TYPES: Wage & Income (WITS), Account, Return, Record of Account, Non-filing. Default seeds Wage & Income checked
Yes — ≥ 1
—
Pick at least one transcript type
taxpayer
Segmented radio: Personal / Business / Both — default seeded from scope
Always has a value
—
—
notes
Textarea (“Optional — tax years needed, urgency, or anything the TP team should know”)
No
—
—
What submit writes: a request-log record (HP-{seq}, software THS, dept TP, requester + timestamp) in state pending. Lifecycle: pending → submitted → received, or failed — server-side transitions as the THS pull progresses; the case-detail transcripts card lists records newest-first with the state badge. In app.js the ≥1-types rule surfaces as a toast; server-rendered it is a normal field error on the chips group, same copy.
Request-log states (.hp-status, kit classes) + a log item
pendingsubmittedreceivedfailed
THSBothpending
W&I, Account · requested just now · Allen Rose · “Need 2022–2024 for the amended returns”
Copy-paste markupstate shown W&I + Account selected · taxpayer Both
8.3 · Bookkeeping work log (bk* — points formula moves server-side)
Field specpartialpartials/_bk_form.htmlsubmithx-post /tax-prep/cases/{id}/bookkeeping/ — upsert: posts the record id when editing (the form opens prefilled from the newest record; header sub shows “· editing BK-1001”)on success toast Bookkeeping saved — {pts} pts · {status} · swap the bookkeeping card / inventory row
Field
Control · type
Required
Conditional on
Error copy (app.js)
Core case info
case
Locked display — from URL (“Auto-linked to the case record.”)
—
—
—
years
Multi checkbox toggles, scope-seeded + “+ other…” (as tracking)
Yes — ≥ 1
—
Select at least one year.
status
Select — BK_STATUSES: Not Started, In Progress, Pending QA Review, QA Reviewed, Completed. Default Pending QA Review
Yes
—
Select a status.
Work product
bookkeeper
Select — BOOKKEEPERS (label: “Which bookkeeper did you review?”)
Yes
—
Select a bookkeeper.
total_businesses
Number, min 0 (“Total # of businesses (all years)”) — 0 is valid; blank is not
Yes
—
Enter a number.
types
Multi option chips — BK_TYPES: Bank Statements, Payroll, Financial Statements, Cleanup / Catch-up (“Metrics below appear for the types you pick.”)
Yes — ≥ 1
—
Select at least one type.
Bank statements — # of businesses by transaction volume
bs_simple · bs_standard · bs_complex
Number grid, min 0: ≤ 1,500 txns (simple) · 1,501–3,000 (standard) · ≥ 3,001 (complex)
Number grid, min 0: QB prepared # qtrs · Timecards # qtrs · Net checks # qtrs · 940(s) prepared · W2/W3 1–5 / 6–10 / 11–15 emp
No
Payroll type + payroll_prepared = Yes
—
Adjustments & notes
additional_points
Number, signed (“Additional points (manual +/−)”)
No
—
—
mistake_in_points
Checkbox “Mistake in points (correcting a prior entry)”
No
—
—
notes
Textarea (“Work completed, exceptions, context for the tax prep team…”)
Yes — non-blank
—
Notes are required.
Points move server-side. The client never computes points: the metric inputs hx-post …/bookkeeping/points/ hx-trigger="input delay:300ms" hx-target="#bk-points-bar" hx-swap="outerHTML" and the server returns the recomputed bar (partials/_bk_points_bar.html). The BK_PTS weights in app.js (bank 2/4/6 · payroll qtr 1.5 · 940 2 · W2 tiers 2/3/4) are sample values, backend-configured — label the bar “configured in backend” wherever the total is shown. Stored records keep the points as computed at save time.
Copy-paste markupstate shown edit mode (BK-1001) · Bank Statements + Payroll selected, payroll prepared — both metric grids open
8.4 · Partials, endpoints & error presentation
Piece
Suggested partial / endpoint
Notes
Modal container
<div id="tp-modal"></div> in the app base template
Every form trigger targets it; one dialog at a time
Upsert by record; success swaps the bookkeeping card / inventory row
Points bar
partials/_bk_points_bar.html · POST …/bookkeeping/points/
The #bk-points-bar recalc swap target — formula + weights live server-side only
Field wrapper
partials/_form_field.html
Renders label / .req / control / .hint / .errmsg + the .err state — shared by all three forms and the inline-validate swap unit (closest .tp-fld)
Error presentation — field level + form level (states from §5; copy verbatim from app.js)
Add at least one state for State filings.
Please complete the highlighted fields
Open decisions that touch these forms (flagged, not resolved — owners business-side): E-004.1 “via mailed” signature path — Date-filed / signature flows must not assume DocuSign-only; E-004.6/.7 Remind-Client / Request-Docs remain undefined (unrelated buttons stay disabled — these forms don't implement them); E-003.4 overdue threshold (14 vs 30 days) affects the row the tracking success-swap re-renders. Statuses: user-facing lists are the finalized-17 everywhere (E-001.7, reconciled July 17); WORKING_TO_FINAL and WORKING_GROUP in app.js stay intact as single re-mappable tables pending business sign-off. Points weights: backend-configured wherever shown.