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:
@@ -17,6 +17,7 @@ import { createUserFn } from "@/lib/user-admin.functions";
|
||||
import { createIntakeToken } from "@/lib/intake.functions";
|
||||
import { genTempPassword } from "@/lib/temp-password";
|
||||
import { StudentGradeReport } from "@/components/gradebook";
|
||||
import { StudentPlansTab } from "@/components/plans";
|
||||
|
||||
export const Route = createFileRoute("/_authenticated/students/$id")({
|
||||
head: () => ({ meta: [{ title: "Student — School Portal" }] }),
|
||||
@@ -90,6 +91,7 @@ function StudentDetail() {
|
||||
<TabsTrigger value="profile">Profile</TabsTrigger>
|
||||
<TabsTrigger value="family">Family & pickup</TabsTrigger>
|
||||
<TabsTrigger value="academics">Academics</TabsTrigger>
|
||||
<TabsTrigger value="plans">504 / IEP</TabsTrigger>
|
||||
<TabsTrigger value="grades">Grades</TabsTrigger>
|
||||
<TabsTrigger value="attendance">Attendance</TabsTrigger>
|
||||
<TabsTrigger value="ledger">Tuition</TabsTrigger>
|
||||
@@ -98,6 +100,7 @@ function StudentDetail() {
|
||||
<TabsContent value="profile"><ProfileTab studentId={id} canEdit={canEdit} isAdmin={isAdmin} /></TabsContent>
|
||||
<TabsContent value="family"><FamilyTab studentId={id} canEdit={canEdit} isAdmin={isAdmin} /></TabsContent>
|
||||
<TabsContent value="academics"><AcademicsTab studentId={id} canEdit={canEdit} /></TabsContent>
|
||||
<TabsContent value="plans"><StudentPlansTab studentId={id} isAdmin={isAdmin} /></TabsContent>
|
||||
<TabsContent value="grades" className="mt-4"><StudentGradeReport studentId={id} classId={(student?.class_id as string | null) ?? null} /></TabsContent>
|
||||
<TabsContent value="attendance"><AttendanceTab studentId={id} /></TabsContent>
|
||||
<TabsContent value="ledger"><LedgerTab studentId={id} canEdit={isAdmin} /></TabsContent>
|
||||
|
||||
Reference in New Issue
Block a user