FormSubmission. Status enum:
draft → submitted → under_review → approved | rejected
Default step keys
Fallback keys insrc/lib/formSteps.js (order is the live bank when published):
tableOfContentschildAbuseInterventionsexualHarassmentrespectForAllsuicidePreventionattendancePlantemporaryHousingserviceInSchoolsplanningInterviewsmilitaryRecruitmentschoolCultureafterSchoolProgramscellPhonePolicycounselingPlan
principalLetter exists in some legacy maps but is not in the fallback key list. Do not revive it as a required step without a bank migration that keeps answer ids stable.
Form workspace
/form/[id] uses DashboardShell like the rest of the product.
- Header title is school + year. The body heading is the current section only.
- Status strip: editing/saved time (autosave ~3s), archived/view-only, optional due date, thin progress bar. There is no Save draft button.
- Header tools: Compare, Duplicate (level ≥ 4), Share (level 5), Comment (level 5), View all, Submit.
- Copied plans show unreviewed
needsUpdateflags (“Same as last year” / “New this year” / changed) with Mark reviewed. - Last step shows a submit summary; submit opens a confirm dialog, then attestation if the plan was copied.
/view/[id] (same shell, print/PDF/Word, jump list). New plans: /form/new.
How to fill a table question
Some questions are typetable (staff lists, program hours, contacts). Answers are stored as { headers, rows } on formData[stepKey].data[questionId], not as a single string. Super Admins configure columns in the question bank; you fill the grid here.
1
Open the section that has the table
Use the step list on
/form/[id]. A table question shows a grid (not a textarea) and a short hint under the prompt.2
Type in cells, or paste from Excel / Google Sheets
Click a cell and type. For dropdown columns, choose Select one or a listed option.To paste a spreadsheet: copy the range in Excel or Sheets, click anywhere on the table (not inside a single cell as if it were a paragraph), and paste. Tab-separated columns stay in columns. If the first pasted row matches the locked headers, that row is treated as a header and dropped.
3
Add or remove rows
Add row appends an empty row. The trash control on a row removes it (the last remaining row cannot be deleted). Clear table empties every cell but keeps the column headers the question bank defined.
4
Add a column only when headers are unlocked
If Super Admin left Table columns empty, you can Add column and edit header names. If they locked a preset (for example Staff contact), headers are fixed and Add column is hidden so every school uses the same shape.
What a saved table looks like
What a saved table looks like
Read-only and exports
Read-only and exports
/view/[id] and archived years show the same grid without edit controls. PDF and Word print real tables (headers + rows), not a joined paragraph.Editing a step
PUT /api/forms/[id]/step/[stepNumber]
- Resolves
stepNumber→stepKeyvia the question bank. - Rejects writes if the school year is archived.
- Acquires a 5-minute lock on
form:{id}:step:{stepKey}. - Rate-limits saves (
rl:save:{userId}:{formId}, 30 / 60s when Redis is up). - Writes only dirty fields on
formData[stepKey](revision409on conflict).
POST /api/forms/[id]/step/[stepNumber]/unlock.
Needs-update flags
Copied plans may includeneedsUpdate[]:
reason is new | changed | revisit. Clear a flag with POST /api/forms/[id]/review-flag { "questionId": "..." }.
Attestation
Copied drafts require principal sign-off before submit:POST /api/forms/[id]/attest { "name": "Jane Principal" }
Stores attestation.confirmed, name, signedAt, signedBy.