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:
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user