Date: 2026-04-07
A Track is a named, ordered sequence of task types with grade/month timing that gets assigned to a student. Example tracks: Standard, Express, Student Athlete, BS/MD. Every student is on exactly one track at a time. When assigned, all task types in the track become next_assigned_tasks rows with computed due dates. The admin can build and edit tracks from Settings. Counselors can switch a student's track from Student Detail.
Currently, timing lives on task types themselves — meaning there's one global timeline for all students. Students on different programs (athlete recruiting timeline, BS/MD application windows, Express condensed sequence) need different task schedules. Tracks make the timeline configurable per-student-cohort and give admins a single place to manage what each program looks like end-to-end.
timing_grade + timing_month removed from next_task_types. They live on the track-task junction (next_track_task_types) instead.next_assigned_tasks rows with due_date computed from student grad year + timing_grade + timing_month. Tasks with no timing get due_date = null.function computeDueDate(gradYear: number, timingGrade: number, timingMonth: number): Date {
// Grade G school year starts in fall of: gradYear - (13 - G)
const fallYear = gradYear - 13 + timingGrade;
// Aug–Dec = fall semester; Jan–Jul = spring semester (next calendar year)
const calendarYear = timingMonth >= 8 ? fallYear : fallYear + 1;
return new Date(calendarYear, timingMonth - 1, 1);
}
// Example: grad 2026, grade 11, month 9 → Sep 2024
// Example: grad 2026, grade 12, month 1 → Jan 2026
If timing_grade or timing_month is null → due_date = null (no deadline on that task).
model next_tracks {
id Int @id @default(autoincrement())
name String @unique @db.VarChar(100)
description String?
is_default Boolean @default(false)
is_active Boolean @default(true)
sort_order Int @default(0)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @updatedAt @db.Timestamptz(6)
track_task_types next_track_task_types[]
student_tracks next_student_tracks[]
}
model next_track_task_types {
id Int @id @default(autoincrement())
track_id Int
task_type_id Int
timing_grade Int? // 8–12; null = no scheduled timing
timing_month Int? // 1–12; null = no scheduled timing
is_required Boolean @default(false)
sort_order Int @default(0)
track next_tracks @relation(fields: [track_id], references: [id], onDelete: Cascade)
task_type next_task_types @relation(fields: [task_type_id], references: [id], onDelete: Cascade)
@@unique([track_id, task_type_id])
@@index([track_id])
}
model next_student_tracks {
id Int @id @default(autoincrement())
student_id Int @unique // one active track per student
track_id Int
assigned_by_id Int?
assigned_at DateTime @default(now()) @db.Timestamptz(6)
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @updatedAt @db.Timestamptz(6)
track next_tracks @relation(fields: [track_id], references: [id])
student Student @relation(fields: [student_id], references: [id])
@@index([track_id])
}
next_task_typesRemove: timing_grade Int?, timing_month Int?
Add inverse relation:
track_task_types next_track_task_types[]
next_assigned_tasksAdd: due_date DateTime? @db.Timestamptz(6)
Also add inverse relation to Student model:
next_student_tracks next_student_tracks[]
-- ============================================================
-- PART A: New tables
-- ============================================================
CREATE TABLE next_tracks (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
is_default BOOLEAN NOT NULL DEFAULT false,
is_active BOOLEAN NOT NULL DEFAULT true,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE next_track_task_types (
id SERIAL PRIMARY KEY,
track_id INT NOT NULL REFERENCES next_tracks(id) ON DELETE CASCADE,
task_type_id INT NOT NULL REFERENCES next_task_types(id) ON DELETE CASCADE,
timing_grade INT,
timing_month INT,
is_required BOOLEAN NOT NULL DEFAULT false,
sort_order INT NOT NULL DEFAULT 0,
UNIQUE(track_id, task_type_id)
);
CREATE INDEX idx_track_task_types_track ON next_track_task_types(track_id);
CREATE TABLE next_student_tracks (
id SERIAL PRIMARY KEY,
student_id INT NOT NULL UNIQUE REFERENCES students(id),
track_id INT NOT NULL REFERENCES next_tracks(id),
assigned_by_id INT,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_student_tracks_track ON next_student_tracks(track_id);
-- ============================================================
-- PART B: Add due_date to next_assigned_tasks
-- ============================================================
ALTER TABLE next_assigned_tasks ADD COLUMN IF NOT EXISTS due_date TIMESTAMPTZ;
-- ============================================================
-- PART C: Seed the Standard (default) track
-- Migrate all timing from next_task_types → junction
-- ============================================================
INSERT INTO next_tracks (name, description, is_default, is_active, sort_order)
VALUES ('Standard', 'Default track for all students', true, true, 0);
-- Populate Standard track with all active task types, preserving their current timing
INSERT INTO next_track_task_types (track_id, task_type_id, timing_grade, timing_month, is_required, sort_order)
SELECT
(SELECT id FROM next_tracks WHERE name = 'Standard'),
id,
timing_grade,
timing_month,
is_required,
sort_order
FROM next_task_types
WHERE is_active = true;
-- ============================================================
-- PART D: Remove timing columns from next_task_types
-- ============================================================
ALTER TABLE next_task_types
DROP COLUMN IF EXISTS timing_grade,
DROP COLUMN IF EXISTS timing_month;
-- ============================================================
-- PART E: Verify
-- ============================================================
-- SELECT COUNT(*) FROM next_track_task_types; -- should match active task types count
-- SELECT * FROM next_tracks; -- should show Standard with is_default=true
app/api/tracks/route.ts (new)GET — list all tracks with task type count and student countPOST — create track { name, description }app/api/tracks/[id]/route.ts (new)GET — track detail with all task types + timingPATCH — update name, description, is_active, sort_orderDELETE — only if no students assigned; returns error otherwiseapp/api/tracks/[id]/task-types/route.ts (new)GET — all task types in this track with timing + sort_orderPOST — add a task type { task_type_id, timing_grade?, timing_month?, is_required?, sort_order? }PUT — bulk reorder: { items: [{ id, sort_order }] }app/api/tracks/[id]/task-types/[taskTypeId]/route.ts (new)PATCH — update timing/required for one track-task: { timing_grade?, timing_month?, is_required?, sort_order? }DELETE — remove task type from trackapp/api/students/[id]/track/route.ts (new)GET — student's current track + task completion summaryPOST — assign/change track { track_id, assigned_by_id }
next_student_tracks row (upsert)next_assigned_tasks with computed due_date. Skip if status is already completed/skipped.next_assigned_tasks rows that were in the old track but not the new one. (Add status='cancelled' as a valid value.)lib/track-utils.ts (new)export function computeTaskDueDate(
gradYear: number,
timingGrade: number | null,
timingMonth: number | null
): Date | null {
if (!timingGrade || !timingMonth) return null;
const fallYear = gradYear - 13 + timingGrade;
const calendarYear = timingMonth >= 8 ? fallYear : fallYear + 1;
return new Date(calendarYear, timingMonth - 1, 1);
}
export async function assignTrackToStudent(
studentId: number,
trackId: number,
assignedById: number | null,
gradYear: number
): Promise<void> {
// 1. Get student's current track (if any)
// 2. Get all task types in new track
// 3. Cancel pending assigned_tasks for old track tasks not in new track
// 4. Upsert next_student_tracks
// 5. For each task type in new track: upsert next_assigned_tasks with due_date
}
components/settings/SettingsClient.tsxAdd "tracks" to the Tab union and tabs array: { id: "tracks", label: "Tracks" }.
components/settings/TracksManager.tsx (new)Left panel: list of tracks
┌─────────────────────────────────────────────────────────┐
│ Tracks + New Track │
├───────────────────┬─────────────────────────────────────┤
│ Standard ★ │ [TrackEditor panel] │
│ 12 tasks · 847 │ │
│ │ │
│ Express │ │
│ 8 tasks · 34 │ │
│ │ │
│ Student Athlete │ │
│ 15 tasks · 28 │ │
│ │ │
│ BS/MD │ │
│ 18 tasks · 12 │ │
└───────────────────┴─────────────────────────────────────┘
components/settings/TrackEditor.tsx (new)Right panel content when a track is selected:
Header: Track name (editable inline), description, is_active toggle, student count badge, "Set as Default" button
Task list — drag-to-reorder (DndKit, same as TaskTypeEditor):
Each row shows:
"+ Add Tasks" button → opens a slide-over/modal with:
Timeline preview (collapsible) — group task rows by grade, sorted by month:
Grade 9
Aug Complete YouScience & Assessment
Sep Develop Test Prep Strategy
...
Grade 10
...
Grade 11
...
Unscheduled (no timing set)
Develop Resume
components/students/detail-sections/TrackSection.tsx (new)Follows DepartmentNotesSection pattern. Shows:
8 / 12 completeChange Track modal:
On confirm → POST /api/students/{id}/track → refresh section.
components/students/StudentDetail.tsx<TrackSection> to the right column (near TeamSection)app/(portal)/students/[id]/page.tsxprisma.next_student_tracks.findUnique({ where: { student_id }, include: { track: true } })components/dashboard/TaskMilestoneModal.tsxnext_assigned_tasks to get due_date per task typeDue Sep 2025 in muted textOverdue red badge on tasks where due_date < today && status === 'pending'app/api/task-types/route.tsAdd optional ?studentId= param — if provided, returns task types with their due dates for that student (joins through next_assigned_tasks).
prisma/seed-tracks.ts — runs via npx tsx prisma/seed-tracks.ts. Seeds the tracks below plus any task-type additions needed. Standard track is seeded in the migration (Part C above). The seed script adds the remaining tracks (empty shells + new task types).
Default track. All active task types from next_task_types with their existing timing. Every student who has no explicit track gets this one.
For students who start with HC in 11th or 12th grade — same core tasks as Standard but compressed into the remaining grade window. No 9th/10th grade tasks. Timeline is accelerated: major essay work starts in Grade 11 May (not Jun), college list locked by Grade 11 August, apps target Early Action. Omits tasks flagged for Gr 8–10.
Seed as empty shell (name + description). Admins populate via UI from Standard task list.
For students pursuing NCAA/NAIA recruiting (all divisions). Adds specialized milestones alongside the standard college counseling flow.
Additional task types to seed (department: athlete):
| Task Type Name | Category | Grade | Month |
|---|---|---|---|
| Create NCAA Eligibility Center Account | task | 9 | 9 |
| Build Student-Athlete Resume | task | 9 | 11 |
| Submit Transcript to NCAA Eligibility Center | task | 10 | 9 |
| Verify NCAA Core Course Compliance | task | 10 | 10 |
| Identify Target Colleges by Division Level | task | 10 | 11 |
| Audit Social Media for Coach Visibility | task | 11 | 8 |
| Email Coaches with Student-Athlete Resume | task | 11 | 9 |
| Attend NCAA Showcase / Camp | task | 11 | 10 |
| Create Highlight Video / NCSA Profile | task | 11 | 10 |
| Follow Up with Coaches (post-visit) | task | 11 | 11 |
| Official Campus Visits | task | 12 | 9 |
| Evaluate Scholarship Offers | task | 12 | 10 |
| Notify Non-Recruiting Coaches of Decision | task | 12 | 4 |
Note: Early recruiting timelines (Div I) compress most of this into Grade 10–11. The seed includes default timing above; admins adjust per student as needed.
For students pursuing 7–8 year direct-entry BS/MD programs. Requires early commitment and stronger preclinical profile than standard counseling. High selectivity: exceptional GPA, test scores, clinical and research experience required.
Additional task types to seed (department: bs-md):
| Task Type Name | Category | Grade | Month |
|---|---|---|---|
| Confirm BS/MD vs Standard Pre-Med Path | task | 9 | 9 |
| Research BS/MD Programs and Requirements | task | 10 | 1 |
| Begin Clinical Shadowing (hospital/clinic) | task | 10 | 9 |
| Log Clinical Shadowing Hours | task | 11 | 1 |
| Begin Research Experience | task | 11 | 1 |
| Build BS/MD Program Target List | task | 11 | 8 |
| Review Prerequisites (bio, chem, anatomy) | task | 11 | 9 |
| Write BS/MD Personal Statement | task | 11 | 10 |
| Prepare for BS/MD Interview | task | 12 | 9 |
| Submit BS/MD Applications (EA/ED priority) | task | 12 | 10 |
These supplement the Standard task types — the BS/MD track includes all Standard tasks plus these additions.
For students pursuing USMA (West Point), USNA (Naval Academy), USAFA (Air Force Academy), USCGA, USMMA, or ROTC scholarships. HC specialty track; Amanda Yoder leads this program ("Points For Patriots"). Congressional nominations are required for service academies and have October deadlines — outreach should start in June of the junior year.
Additional task types to seed (department: counseling, category: task):
| Task Type Name | Grade | Month |
|---|---|---|
| Confirm Service Academy vs ROTC vs Civilian Path | 9 | 9 |
| Research Service Academy Requirements | 10 | 1 |
| Begin Physical Fitness Training for CFA | 10 | 9 |
| Register with Service Academy Candidate Portal | 11 | 1 |
| Identify Congressional Representatives for Nominations | 11 | 3 |
| Request Congressional Nomination Applications | 11 | 6 |
| Request Letters of Recommendation (teachers + coach) | 11 | 6 |
| Submit Congressional Nomination Applications | 11 | 9 |
| Complete Academy Pre-Candidate Questionnaire | 11 | 9 |
| Pass Candidate Fitness Assessment (CFA) | 11 | 10 |
| Schedule Medical/DoDMERB Exam | 11 | 10 |
| Write Service Academy Essays (nomination + academy) | 11 | 10 |
| Academy Application: Submit Candidate Form | 12 | 8 |
| Blue & Gold / Liaison Officer Interview | 12 | 9 |
| Submit ROTC Scholarship Application | 12 | 10 |
| Track Nomination Decision | 12 | 11 |
Congressional nomination essays vary by representative (Senators + House Rep), each with slightly different prompts (why academy, character essay, etc.). Due dates cluster in October of senior year — apply by September to be safe.
scripts/seed-task-types.tsRemove all timing_grade and timing_month fields from every task type definition (they moved to tracks).
components/settings/TaskTypeEditor.tsxRemove the timing_grade/timing_month input fields from the editor UI.
e2e/tracks-admin.spec.tse2e/student-track-assignment.spec.tsnext_assigned_tasks rows created with correct due_datesprisma/migrations/[ts]_tracks.sqlprisma/seed-tracks.ts — seeds Standard + Express/Athlete/BSMD nameslib/track-utils.ts — computeTaskDueDate, assignTrackToStudentapp/api/tracks/route.tsapp/api/tracks/[id]/route.tsapp/api/tracks/[id]/task-types/route.tsapp/api/tracks/[id]/task-types/[taskTypeId]/route.tsapp/api/students/[id]/track/route.tscomponents/settings/TracksManager.tsxcomponents/settings/TrackEditor.tsxcomponents/students/detail-sections/TrackSection.tsxe2e/tracks-admin.spec.tse2e/student-track-assignment.spec.tsprisma/schema.prisma — new models, remove timing from task_types, add due_date to assigned_tasks, add relation to Studentscripts/seed-task-types.ts — remove timing_grade/timing_month from all entriescomponents/settings/SettingsClient.tsx — add "Tracks" tabcomponents/settings/TaskTypeEditor.tsx — remove timing fields from editorcomponents/students/StudentDetail.tsx — add TrackSection, pass track dataapp/(portal)/students/[id]/page.tsx — fetch student's current trackcomponents/dashboard/TaskMilestoneModal.tsx — show due dates, overdue badges, sorted by due dateapp/api/task-types/route.ts — optional ?studentId= param for due date join/home (follow-on)next_assigned_tasks and modifies next_task_types). If run before, the migration must be self-contained.next_assigned_tasks already exists in DB (confirmed in research). Migration adds due_date column only.next_tracks, next_track_task_types, next_student_tracks tables exist in DBnext_assigned_tasks.due_date column existsnext_task_types.timing_grade and timing_month columns removednext_assigned_tasks rows have correct due_date after track assignment