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.
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.