diff --git a/src/lib/plans.ts b/src/lib/plans.ts new file mode 100644 index 0000000..a1c6e93 --- /dev/null +++ b/src/lib/plans.ts @@ -0,0 +1,116 @@ +import type { Database } from "@/integrations/supabase/types"; + +export type PlanType = Database["public"]["Enums"]["plan_type"]; +export type PlanStatus = Database["public"]["Enums"]["plan_status"]; +export type GoalStatus = Database["public"]["Enums"]["plan_goal_status"]; +export type MeetingType = Database["public"]["Enums"]["plan_meeting_type"]; + +export const PLAN_TYPE_LABEL: Record = { + iep: "IEP", + section_504: "504 Plan", +}; + +export const PLAN_STATUS_LABEL: Record = { + draft: "Draft", + active: "Active", + archived: "Archived", +}; + +export const GOAL_STATUS_LABEL: Record = { + not_started: "Not started", + in_progress: "In progress", + met: "Met", + not_met: "Not met", + discontinued: "Discontinued", +}; + +export const MEETING_TYPE_LABEL: Record = { + initial: "Initial", + annual_review: "Annual review", + reevaluation: "Re-evaluation", + eligibility: "Eligibility", + amendment: "Amendment", + other: "Other", +}; + +export const ACCOMMODATION_CATEGORIES = [ + { value: "instructional", label: "Instructional" }, + { value: "environmental", label: "Environmental" }, + { value: "assessment", label: "Assessment / testing" }, + { value: "behavioral", label: "Behavioral" }, + { value: "other", label: "Other" }, +]; +export const accommodationLabel = (v: string) => + ACCOMMODATION_CATEGORIES.find((c) => c.value === v)?.label ?? v; + +// ── Compliance clocks ─────────────────────────────────────────────────────── +// A date within this many days counts as "due soon" rather than merely upcoming. +export const DUE_SOON_DAYS = 30; + +export type ComplianceState = "none" | "ok" | "due_soon" | "overdue"; + +// "YYYY-MM-DD" through `new Date()` parses as UTC midnight, which lands on the +// previous day for anyone west of Greenwich. Build a local date instead. +function parseISODate(d: string): Date { + const [y, m, day] = d.split("-").map(Number); + return new Date(y, m - 1, day); +} + +export function daysUntil(date: string | null | undefined, today = new Date()): number | null { + if (!date) return null; + const midnight = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + return Math.round((parseISODate(date).getTime() - midnight.getTime()) / 86_400_000); +} + +export function complianceState( + date: string | null | undefined, + today = new Date(), +): ComplianceState { + const d = daysUntil(date, today); + if (d === null) return "none"; + if (d < 0) return "overdue"; + if (d <= DUE_SOON_DAYS) return "due_soon"; + return "ok"; +} + +// EmbraceIEP shows overdue dates in red italics; due-soon gets an amber warning. +export const COMPLIANCE_CLASS: Record = { + none: "text-muted-foreground", + ok: "text-foreground", + due_soon: "text-amber-600 font-medium", + overdue: "text-destructive font-semibold italic", +}; + +export function complianceLabel(state: ComplianceState, days: number | null): string { + switch (state) { + case "overdue": + return `${Math.abs(days ?? 0)} day${Math.abs(days ?? 0) === 1 ? "" : "s"} overdue`; + case "due_soon": + return days === 0 ? "Due today" : `Due in ${days} day${days === 1 ? "" : "s"}`; + case "ok": + return "On track"; + default: + return "Not set"; + } +} + +export const formatDate = (d: string | null | undefined) => + d ? parseISODate(d).toLocaleDateString() : "—"; + +// Default compliance clocks when a plan takes effect: annual review at +1 year, +// re-evaluation at +3 years (the IDEA triennial). Both are editable afterwards. +export function defaultReviewDates(effective: string): { annual: string; reeval: string } { + const iso = (dt: Date) => + `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, "0")}-${String(dt.getDate()).padStart(2, "0")}`; + const base = parseISODate(effective); + const annual = new Date(base); + annual.setFullYear(annual.getFullYear() + 1); + const reeval = new Date(base); + reeval.setFullYear(reeval.getFullYear() + 3); + return { annual: iso(annual), reeval: iso(reeval) }; +} + +export const todayISO = () => { + const t = new Date(); + return `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, "0")}-${String(t.getDate()).padStart(2, "0")}`; +};