Skip to main content
The live form UI reads questions from a published FormTemplate. If Mongo has no published bank, getPublishedOrJson() loads src/data/formQuestions.json.

Template statuses

draft | published | archived version is unique. Publishing clones the draft into a new published version; existing forms keep questionBankVersion.

Super Admin routes

Authenticated users load the bank they should render: GET /api/question-bank?schoolYear=2026-2027&version=3

Seed a plan from the published bank

Empty plans do not copy the question schema onto FormSubmission. Super Admin can seed formData and pin questionBankVersion with POST /api/admin/forms/populate-bank. Filled seeds mark checkboxes true, yes/no yes (so gate questions stay visible), tables with two rows, and every step complete.
Dry-run is the default. --answers writes sample responses instead of empty shells. Load env with node --env-file=.env.local when running outside Next.

How to configure a table question

Set Type to Table (Excel paste) on the question. Staff then get a grid on the plan (how they fill it). Column definitions live on question.columns and survive publish because FormTemplate questions are strict: false.
1

Open the draft question

Super Admin → /admin/questions. Edit the step, find the question, set type to Table (Excel paste).
2

Start from a preset (optional)

Click a preset to lock headers (and dropdowns, when the preset includes them):
3

Name columns, one per line

Column names is the blueprint. One header per line (or a single comma-separated line). Reorder by moving lines. Clear columns empties the list so staff can paste their own Excel headers on the form.
4

Make a column a dropdown

Either:
  • In the blueprint, write Certified | Yes, No (name, pipe, choices). Use ; to separate choices that contain commas.
  • Or set Input to Dropdown and list options one per line under that column.
Text columns accept any string. Max 20 columns; dropdowns cap at 40 options.
5

Publish

Save the draft, then publish with { confirm: true } so live plans pick up the columns. Existing answers keep their question.id; changing headers on a locked table reshapes the grid on the next edit (extra cells are trimmed, missing cells pad empty).
Leave Table columns empty when schools truly have different spreadsheets. Lock a preset when you need comparable district data (staff lists, program hours) and clean PDF/DOCX tables.
Do not rename question.id after schools have answered. Column header edits are safer than id changes, but a published lock still overwrites staff-chosen headers on the next save.
parseColumnBlueprint in src/lib/tableAnswer.js is the same parser the admin editor and the form use.
Year setup includes Contact table migrate. Dry-run a question id for a school year, review the JSON diff, then apply. Use this after you change a free-text contact question to type table with a contact preset. Download the dry-run report before applying.

Safe edits vs breaking edits

Change title, placeholder, description, required, active, order, or table columns. Keep question.id unchanged.
Renaming id, deleting a question that already has answers, or reshuffling ids. Old formData[stepKey].data[oldId] would orphan.
Publish requires { "confirm": true }. Optionally pass schoolYear to pin that version on SchoolYearSettings.questionBankVersion.