Add 504 / IEP plans with tiered confidentiality

Plans, accommodations, related services, annual goals with progress
monitoring, meetings/team, and signed documents — plus a compliance
list and dashboard alerts for annual-review and triennial re-evaluation
dates (overdue in red, due-within-30-days in amber).

Access is tiered because special-education records are need-to-know
under FERPA:

  FULL  admin, the plan's case manager, the student's parents
  IMPL  the above, plus any teacher of the student — accommodations
        and services only, never eligibility or meeting notes

RLS is row-level and every app role is the same Postgres role
(`authenticated`), so column grants cannot separate the tiers. The
split is therefore physical: confidential fields live in plan_details,
plan_goals, plan_meetings and plan_documents rather than as columns on
student_plans.

The UI asks the database which tier applies via the same predicates the
policies use (can_view_plan_full / can_edit_plan) instead of re-deriving
the rules client-side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 09:14:40 -04:00
co-authored by Claude Opus 5
parent 872eacefc5
commit 9d7d18669d
9 changed files with 2905 additions and 3 deletions
+21
View File
@@ -14,6 +14,7 @@ import { Route as AuthenticatedRouteRouteImport } from './routes/_authenticated/
import { Route as IndexRouteImport } from './routes/index'
import { Route as IntakeTokenRouteImport } from './routes/intake.$token'
import { Route as AuthenticatedStudentsRouteImport } from './routes/_authenticated/students'
import { Route as AuthenticatedPlansRouteImport } from './routes/_authenticated/plans'
import { Route as AuthenticatedMessagesRouteImport } from './routes/_authenticated/messages'
import { Route as AuthenticatedLedgerRouteImport } from './routes/_authenticated/ledger'
import { Route as AuthenticatedFormsRouteImport } from './routes/_authenticated/forms'
@@ -52,6 +53,11 @@ const AuthenticatedStudentsRoute = AuthenticatedStudentsRouteImport.update({
path: '/students',
getParentRoute: () => AuthenticatedRouteRoute,
} as any)
const AuthenticatedPlansRoute = AuthenticatedPlansRouteImport.update({
id: '/plans',
path: '/plans',
getParentRoute: () => AuthenticatedRouteRoute,
} as any)
const AuthenticatedMessagesRoute = AuthenticatedMessagesRouteImport.update({
id: '/messages',
path: '/messages',
@@ -132,6 +138,7 @@ export interface FileRoutesByFullPath {
'/forms': typeof AuthenticatedFormsRoute
'/ledger': typeof AuthenticatedLedgerRoute
'/messages': typeof AuthenticatedMessagesRoute
'/plans': typeof AuthenticatedPlansRoute
'/students': typeof AuthenticatedStudentsRouteWithChildren
'/intake/$token': typeof IntakeTokenRoute
'/classes/$id': typeof AuthenticatedClassesIdRoute
@@ -150,6 +157,7 @@ export interface FileRoutesByTo {
'/forms': typeof AuthenticatedFormsRoute
'/ledger': typeof AuthenticatedLedgerRoute
'/messages': typeof AuthenticatedMessagesRoute
'/plans': typeof AuthenticatedPlansRoute
'/intake/$token': typeof IntakeTokenRoute
'/classes/$id': typeof AuthenticatedClassesIdRoute
'/students/$id': typeof AuthenticatedStudentsIdRoute
@@ -170,6 +178,7 @@ export interface FileRoutesById {
'/_authenticated/forms': typeof AuthenticatedFormsRoute
'/_authenticated/ledger': typeof AuthenticatedLedgerRoute
'/_authenticated/messages': typeof AuthenticatedMessagesRoute
'/_authenticated/plans': typeof AuthenticatedPlansRoute
'/_authenticated/students': typeof AuthenticatedStudentsRouteWithChildren
'/intake/$token': typeof IntakeTokenRoute
'/_authenticated/classes/$id': typeof AuthenticatedClassesIdRoute
@@ -191,6 +200,7 @@ export interface FileRouteTypes {
| '/forms'
| '/ledger'
| '/messages'
| '/plans'
| '/students'
| '/intake/$token'
| '/classes/$id'
@@ -209,6 +219,7 @@ export interface FileRouteTypes {
| '/forms'
| '/ledger'
| '/messages'
| '/plans'
| '/intake/$token'
| '/classes/$id'
| '/students/$id'
@@ -228,6 +239,7 @@ export interface FileRouteTypes {
| '/_authenticated/forms'
| '/_authenticated/ledger'
| '/_authenticated/messages'
| '/_authenticated/plans'
| '/_authenticated/students'
| '/intake/$token'
| '/_authenticated/classes/$id'
@@ -281,6 +293,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedStudentsRouteImport
parentRoute: typeof AuthenticatedRouteRoute
}
'/_authenticated/plans': {
id: '/_authenticated/plans'
path: '/plans'
fullPath: '/plans'
preLoaderRoute: typeof AuthenticatedPlansRouteImport
parentRoute: typeof AuthenticatedRouteRoute
}
'/_authenticated/messages': {
id: '/_authenticated/messages'
path: '/messages'
@@ -414,6 +433,7 @@ interface AuthenticatedRouteRouteChildren {
AuthenticatedFormsRoute: typeof AuthenticatedFormsRoute
AuthenticatedLedgerRoute: typeof AuthenticatedLedgerRoute
AuthenticatedMessagesRoute: typeof AuthenticatedMessagesRoute
AuthenticatedPlansRoute: typeof AuthenticatedPlansRoute
AuthenticatedStudentsRoute: typeof AuthenticatedStudentsRouteWithChildren
}
@@ -426,6 +446,7 @@ const AuthenticatedRouteRouteChildren: AuthenticatedRouteRouteChildren = {
AuthenticatedFormsRoute: AuthenticatedFormsRoute,
AuthenticatedLedgerRoute: AuthenticatedLedgerRoute,
AuthenticatedMessagesRoute: AuthenticatedMessagesRoute,
AuthenticatedPlansRoute: AuthenticatedPlansRoute,
AuthenticatedStudentsRoute: AuthenticatedStudentsRouteWithChildren,
}