Ticket: HEL-679
Date: 2026-04-07
Add a screens field to next_task_types so every form in the portal is configurable to appear in one or more named contexts (Onboarding, Session, Student Detail, etc.). Migrate the separate next_onboarding_steps system into task types. Refactor the onboarding route to read from task types. Introduce next_assigned_tasks for unified completion tracking. Add Supabase-backed file upload as a first-class task field type.
Onboarding and session forms are the same concept — task types with field schemas — but live in two separate systems with no shared configuration model. Merging them under screens makes onboarding steps configurable in the task type editor, creates a unified completion model that can power a future student progress bar, and eliminates a parallel admin UI that duplicates task type functionality.
| screen | persona | notes |
|---|---|---|
student_onboarding |
student | linear step flow on /onboarding |
parent_onboarding |
parent | isolated parent flow |
student_profile |
student | student-facing SP |
student_detail |
staff | staff-facing SD |
session |
staff | session logging modal; category = counseling/tutoring/essay |
exit_survey |
student | exit survey flow |
Add to next_task_types:
screens String[] @default([])is_required Boolean @default(false) — blocks task from being marked completeis_skippable Boolean @default(false)Create next_assigned_tasks:
model next_assigned_tasks {
id Int @id @default(autoincrement())
user_type String @default("student") @db.VarChar(20) // "student" | "parent"
user_id Int // student.id or parent.id
task_type_id Int
status String @default("pending") @db.VarChar(20) // pending|completed|skipped
assigned_by_id Int? // null = auto/system
completed_at DateTime?
skipped_at DateTime?
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @updatedAt @db.Timestamptz(6)
task_type next_task_types @relation(fields: [task_type_id], references: [id])
@@unique([user_type, user_id, task_type_id])
@@index([user_type, user_id])
}
Remove models: next_onboarding_steps, next_onboarding_completions
Keep: next_onboarding_settings (enforce_students/enforce_parents flags)
One migration file that:
ALTER TABLE next_task_types ADD COLUMN screens TEXT[] NOT NULL DEFAULT '{}', ADD COLUMN is_required BOOLEAN NOT NULL DEFAULT false, ADD COLUMN is_skippable BOOLEAN NOT NULL DEFAULT false
CREATE TABLE next_assigned_tasks (...) per schema above
Update existing session task types — set screens from departments:
UPDATE next_task_types
SET screens = ARRAY['session'],
category = CASE
WHEN 'counseling' = ANY(departments) THEN 'counseling'
WHEN 'tutoring' = ANY(departments) THEN 'tutoring'
WHEN 'essay' = ANY(departments) THEN 'essay'
ELSE category
END
WHERE is_active = true;
prisma/seed-onboarding.js:Student steps → screens: ["student_onboarding"]:
profile_contact, is_required=true, is_skippable=false, sort_order=2)profile_academic, sort_order=3)profile_major_career, sort_order=4)profile_activities, sort_order=5)profile_college_prefs, sort_order=6)profile_about_you, sort_order=7)custom_form, 5 boolean fields, sort_order=8)custom_form, 1 file-upload field, sort_order=9)Parent steps → screens: ["parent_onboarding"]:
parent_contact, is_required=true, sort_order=2)parent_background, sort_order=3)For notifications step: convert to custom_form with 5 boolean canonical fields (see Step 3).
For transcript step: convert to custom_form with 1 file-upload field, canonical_key: "documents.transcript_url".
DROP TABLE next_onboarding_completionsDROP TABLE next_onboarding_stepsAdd to next_field_registry (and prisma/seed-field-registry.js):
| field_key | display_name | data_type | notes |
|---|---|---|---|
prefs.sms_meeting_reminders |
SMS Meeting Reminders | boolean | replaces NotificationPrefsStep field |
prefs.sms_task_reminders |
SMS Task Reminders | boolean | |
prefs.email_meeting_reminders |
Email Meeting Reminders | boolean | |
prefs.email_weekly_summary |
Email Weekly Summary | boolean | |
prefs.email_newsletters |
Email Newsletters | boolean | |
documents.transcript_url |
Transcript File | text | Supabase storage path |
New: app/api/upload/student-file/route.ts
POST — multipart form: { file, studentId }student-files/students/{studentId}/{filename} via lib/supabase-storage.ts uploadFile(){ path, signedUrl } (signed URL TTL: 300s for immediate display)components/ui/TaskFormRenderer.tsx — wire existing file-upload FieldDef type:
FileUploadField component already in renderer)/api/upload/student-file, store returned path in form value/api/upload/student-file/signed-url?path=... to get download linkNew: app/api/upload/student-file/signed-url/route.ts
GET ?path=... — returns signed URL for a stored file path (TTL 300s)components/settings/TaskTypeEditor.tsx:
Screens multi-select — replace the category dropdown section with:
session is selected, show a category fieldcategory is still available as a free-form sub-groupingCategory → hashtag combobox — replace SelectField with a combobox that:
category values: GET /api/task-types/categoriesSubjectCombobox planned in HEL-667is_required toggle — checkbox below is_active: "Required (blocks completion)"
is_skippable toggle — checkbox: "Skippable by user"
file-upload added to FIELD_TYPES in the field builder dropdown
Filter UX — add screen filter chip row at top of task type list (alongside existing dept/cat filters)
app/api/task-types/route.ts — accept screens, is_required, is_skippable in POST/PUT body
New: app/api/task-types/categories/route.ts
GET — SELECT DISTINCT category FROM next_task_types WHERE category IS NOT NULL AND category != '' ORDER BY categorystring[]components/dashboard/TaskMilestoneModal.tsx (and anywhere else that filters category === "session"):
tt.category === "session" to tt.screens?.includes("session")GET /api/task-types?screen=session — add screen query param filter to the task types APIlib/onboarding-types.ts — replace OnboardingStep with TaskTypeStep:
export interface TaskTypeStep {
id: number;
name: string; // step_key equivalent — unique name
label: string; // display label
description: string | null;
field_schema: FieldDef[];
is_required: boolean;
is_skippable: boolean;
sort_order: number;
status: "pending" | "completed" | "skipped"; // from next_assigned_tasks
assigned_id: number; // next_assigned_tasks.id for writing completion
}
lib/onboarding-check.ts — rewrite:
async function checkOnboardingRequired(userId, userType, currentPath):
1. Skip if path starts with /onboarding
2. Read enforce flag from next_onboarding_settings
3. If not enforced → { required: false }
4. Query: next_task_types WHERE screens @> ['{userType}_onboarding'] AND is_active=true
5. Query: next_assigned_tasks WHERE user_type=userType AND user_id=userId
6. Pending = required task types with no completed assignment
7. Return { required: pending.length > 0, pendingSteps }
app/onboarding/page.tsx — rewrite:
student → student_onboarding, parent → parent_onboardingnext_assigned_tasks for this usernext_assigned_tasks (status=pending)TaskTypeStep[] with embedded statusOnboardingFlowcomponents/onboarding/OnboardingFlow.tsx — update:
steps: TaskTypeStep[] instead of OnboardingStep[]custom_form via TaskFormRenderer (no special step_type branching)profile_complete, notifications, transcript_upload step type handlersonCanonicalChange still writes to canonical store via /api/student/profile PATCH/api/onboarding/complete-step with assignedId (not stepKey)app/api/onboarding/complete-step/route.ts — rewrite:
{ assignedId: number }next_assigned_tasks SET status='completed', completed_at=NOW() WHERE id=$1 AND user_id=$2{ allDone: boolean }app/api/onboarding/skip-step/route.ts — rewrite:
{ assignedId: number }task_type.is_skippable = truenext_assigned_tasks SET status='skipped', skipped_at=NOW() WHERE id=$1Delete:
components/settings/OnboardingTab.tsxcomponents/onboarding/NotificationPrefsStep.tsxcomponents/onboarding/TranscriptUploadStep.tsxapp/api/admin/onboarding/steps/route.tsprisma/seed-onboarding.js (replaced by migration SQL)Modify:
components/settings/SettingsClient.tsx — remove OnboardingTab import and tab entryapp/(portal)/settings/page.tsx — remove onboarding steps fetch; keep next_onboarding_settings fetch, move enforcement toggles to an existing General or Portal Settings tabapp/api/admin/onboarding/settings/route.ts — keep but relocate to /api/admin/portal-settingse2e/task-type-editor.spec.ts:
e2e/onboarding-flow.spec.ts:
next_assigned_tasks (verify via DB or API)e2e/file-upload.spec.ts:
prisma/schema.prisma — schema changes aboveprisma/migrations/[ts]_task_type_screens.sql — full migrationprisma/seed-field-registry.js — add 6 new registry entriesapp/onboarding/page.tsx — full rewritelib/onboarding-check.ts — full rewritelib/onboarding-types.ts — replace OnboardingStep with TaskTypeStepcomponents/onboarding/OnboardingFlow.tsx — update props + remove special step_type branchesapp/api/onboarding/complete-step/route.ts — rewrite for next_assigned_tasksapp/api/onboarding/skip-step/route.ts — rewrite for next_assigned_tasksapp/api/task-types/route.ts — add screens/is_required/is_skippable; add ?screen= filtercomponents/settings/TaskTypeEditor.tsx — screens multi-select, hashtag category, toggles, file-upload in field typescomponents/settings/SettingsClient.tsx — remove OnboardingTabapp/(portal)/settings/page.tsx — remove onboarding steps fetchcomponents/dashboard/TaskMilestoneModal.tsx — filter by screens not categorycomponents/ui/TaskFormRenderer.tsx — wire file-upload to Supabaseprisma/migrations/[ts]_task_type_screens.sqlapp/api/task-types/categories/route.ts — distinct category valuesapp/api/upload/student-file/route.ts — multipart upload to Supabaseapp/api/upload/student-file/signed-url/route.ts — generate signed URLapp/api/admin/portal-settings/route.ts — replaces admin/onboarding/settingse2e/task-type-editor.spec.tse2e/onboarding-flow.spec.tse2e/file-upload.spec.tscomponents/settings/OnboardingTab.tsxcomponents/onboarding/NotificationPrefsStep.tsxcomponents/onboarding/TranscriptUploadStep.tsxapp/api/admin/onboarding/steps/route.tsprisma/seed-onboarding.jsassigned_tasks → next_assigned_tasks (follow-on)next_task_form_data still references legacy assigned_tasks for session logging — not changednext_task_types has screens, is_required, is_skippable columns in DBnext_assigned_tasks table exists with correct schemascreens = ["session"]next_onboarding_steps and next_onboarding_completions tables droppednext_assigned_tasksscreens not category)student-files/students/{id}/{filename}