diff --git a/src/components/plans.tsx b/src/components/plans.tsx
deleted file mode 100644
index 0483c6b..0000000
--- a/src/components/plans.tsx
+++ /dev/null
@@ -1,1654 +0,0 @@
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { supabase } from "@/integrations/supabase/client";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-import { Badge } from "@/components/ui/badge";
-import { Progress } from "@/components/ui/progress";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import {
- Plus,
- Trash2,
- Pencil,
- Check,
- Lock,
- FileText,
- Target,
- CalendarClock,
- ClipboardList,
- Loader2,
- Users,
-} from "lucide-react";
-import { useState } from "react";
-import { toast } from "sonner";
-import {
- ACCOMMODATION_CATEGORIES,
- GOAL_STATUS_LABEL,
- MEETING_TYPE_LABEL,
- PLAN_STATUS_LABEL,
- PLAN_TYPE_LABEL,
- COMPLIANCE_CLASS,
- accommodationLabel,
- complianceLabel,
- complianceState,
- daysUntil,
- defaultReviewDates,
- formatDate,
- todayISO,
- type GoalStatus,
- type MeetingType,
- type PlanStatus,
- type PlanType,
-} from "@/lib/plans";
-
-type PlanRow = {
- id: string;
- student_id: string;
- plan_type: PlanType;
- status: PlanStatus;
- case_manager_id: string | null;
- effective_date: string | null;
- end_date: string | null;
- next_annual_review_date: string | null;
- next_reevaluation_date: string | null;
-};
-
-const err = (e: unknown) => toast.error((e as Error).message);
-
-// ── Small shared bits ───────────────────────────────────────────────────────
-function SectionCard({
- title,
- icon: Icon,
- confidential,
- action,
- children,
-}: {
- title: string;
- icon: typeof Target;
- confidential?: boolean;
- action?: React.ReactNode;
- children: React.ReactNode;
-}) {
- return (
-
-
-
- {title}
- {confidential && (
-
- Confidential
-
- )}
-
- {action}
-
- {children}
-
- );
-}
-
-function Empty({ children }: { children: React.ReactNode }) {
- return {children}
;
-}
-
-export function ComplianceDate({ date, label }: { date: string | null; label: string }) {
- const state = complianceState(date);
- return (
-
-
{label}
-
{formatDate(date)}
- {state !== "none" && (
-
- {complianceLabel(state, daysUntil(date))}
-
- )}
-
- );
-}
-
-// Staff list for the case-manager picker. profiles/user_roles are admin-readable
-// only, so this returns empty for everyone else — the picker is admin-only UI.
-function useStaff() {
- return useQuery({
- queryKey: ["plan-staff"],
- queryFn: async () => {
- const { data: roles } = await supabase
- .from("user_roles")
- .select("user_id, role")
- .in("role", ["admin", "teacher"]);
- const ids = [...new Set((roles ?? []).map((r) => r.user_id))];
- if (!ids.length)
- return [] as { id: string; full_name: string | null; email: string | null }[];
- const { data } = await supabase.from("profiles").select("id, full_name, email").in("id", ids);
- return data ?? [];
- },
- });
-}
-
-// ── Entry point: the Plans tab on a student ─────────────────────────────────
-export function StudentPlansTab({ studentId, isAdmin }: { studentId: string; isAdmin: boolean }) {
- const qc = useQueryClient();
- const [adding, setAdding] = useState(false);
-
- const { data: plans, isLoading } = useQuery({
- queryKey: ["student-plans", studentId],
- queryFn: async () => {
- const { data, error } = await supabase
- .from("student_plans")
- .select("*")
- .eq("student_id", studentId)
- .order("created_at", { ascending: false });
- if (error) throw error;
- return (data ?? []) as PlanRow[];
- },
- });
-
- if (isLoading) {
- return (
-
- Loading plans…
-
- );
- }
-
- return (
-
- {isAdmin &&
- (adding ? (
-
{
- setAdding(false);
- qc.invalidateQueries({ queryKey: ["student-plans", studentId] });
- }}
- />
- ) : (
- setAdding(true)}>
- Add 504 / IEP plan
-
- ))}
- {(plans ?? []).map((p) => (
-
- ))}
- {plans?.length === 0 && No 504 or IEP plan on file for this student. }
-
- );
-}
-
-function NewPlanForm({ studentId, onDone }: { studentId: string; onDone: () => void }) {
- const { data: staff } = useStaff();
- const [saving, setSaving] = useState(false);
- const [f, setF] = useState({
- plan_type: "iep" as PlanType,
- status: "draft" as PlanStatus,
- case_manager_id: "",
- effective_date: todayISO(),
- next_annual_review_date: defaultReviewDates(todayISO()).annual,
- next_reevaluation_date: defaultReviewDates(todayISO()).reeval,
- });
-
- const save = async () => {
- setSaving(true);
- try {
- const { data: u } = await supabase.auth.getUser();
- const { error } = await supabase.from("student_plans").insert({
- student_id: studentId,
- plan_type: f.plan_type,
- status: f.status,
- case_manager_id: f.case_manager_id || null,
- effective_date: f.effective_date || null,
- next_annual_review_date: f.next_annual_review_date || null,
- next_reevaluation_date: f.next_reevaluation_date || null,
- created_by: u.user?.id,
- });
- if (error) throw error;
- toast.success("Plan created");
- onDone();
- } catch (e) {
- err(e);
- } finally {
- setSaving(false);
- }
- };
-
- return (
-
-
New plan
-
-
- Type
- setF({ ...f, plan_type: v as PlanType })}
- >
-
-
-
-
- {PLAN_TYPE_LABEL.iep}
- {PLAN_TYPE_LABEL.section_504}
-
-
-
-
- Status
- setF({ ...f, status: v as PlanStatus })}>
-
-
-
-
- {(Object.keys(PLAN_STATUS_LABEL) as PlanStatus[]).map((s) => (
-
- {PLAN_STATUS_LABEL[s]}
-
- ))}
-
-
-
-
- Case manager
- setF({ ...f, case_manager_id: v })}
- >
-
-
-
-
- {(staff ?? []).map((s) => (
-
- {s.full_name || s.email}
-
- ))}
-
-
-
-
-
-
- Review dates default to +1 year and +3 years from the effective date. Adjust to match the
- district's actual timeline.
-
-
-
- {saving ? "Saving…" : "Create plan"}
-
-
- Cancel
-
-
-
- );
-}
-
-// ── One plan ────────────────────────────────────────────────────────────────
-function PlanCard({ plan, isAdmin }: { plan: PlanRow; isAdmin: boolean }) {
- const qc = useQueryClient();
- const [editing, setEditing] = useState(false);
-
- // Ask the database who we are rather than re-deriving the tier rules here —
- // these are the same predicates the RLS policies use.
- const { data: access } = useQuery({
- queryKey: ["plan-access", plan.id],
- queryFn: async () => {
- const [full, edit] = await Promise.all([
- supabase.rpc("can_view_plan_full", { _plan: plan.id }),
- supabase.rpc("can_edit_plan", { _plan: plan.id }),
- ]);
- return { full: full.data === true, edit: edit.data === true };
- },
- });
- const canViewFull = access?.full ?? false;
- const canEdit = access?.edit ?? false;
-
- const { data: staff } = useStaff();
- const manager = (staff ?? []).find((s) => s.id === plan.case_manager_id);
-
- const invalidate = () => qc.invalidateQueries({ queryKey: ["student-plans", plan.student_id] });
-
- const remove = async () => {
- if (!confirm("Delete this plan and everything attached to it? This cannot be undone.")) return;
- const { error } = await supabase.from("student_plans").delete().eq("id", plan.id);
- if (error) return err(error);
- toast.success("Plan deleted");
- invalidate();
- };
-
- return (
-
-
-
-
-
- {PLAN_TYPE_LABEL[plan.plan_type]}
-
-
- {PLAN_STATUS_LABEL[plan.status]}
-
-
- {formatDate(plan.effective_date)}
- {plan.end_date ? ` – ${formatDate(plan.end_date)}` : ""}
-
-
-
- {canEdit &&
- (editing ? (
-
{
- setEditing(false);
- invalidate();
- }}
- >
- Done
-
- ) : (
-
setEditing(true)}>
- Edit
-
- ))}
- {isAdmin && (
-
-
-
- )}
-
-
-
-
-
-
-
-
Case manager
-
- {manager
- ? manager.full_name || manager.email
- : plan.case_manager_id
- ? "Assigned"
- : "Unassigned"}
-
-
-
-
- {editing &&
}
-
-
-
-
-
- {canViewFull ? (
- <>
-
-
-
-
- >
- ) : (
-
-
-
- Eligibility, goals, meeting notes and documents are restricted to the case manager,
- administrators, and the student's parents. You can see the accommodations and services
- you are responsible for implementing.
-
-
- )}
-
-
- );
-}
-
-function PlanHeaderEditor({
- plan,
- staff,
- onSaved,
-}: {
- plan: PlanRow;
- staff: { id: string; full_name: string | null; email: string | null }[];
- onSaved: () => void;
-}) {
- const [f, setF] = useState({
- status: plan.status,
- case_manager_id: plan.case_manager_id ?? "",
- effective_date: plan.effective_date ?? "",
- end_date: plan.end_date ?? "",
- next_annual_review_date: plan.next_annual_review_date ?? "",
- next_reevaluation_date: plan.next_reevaluation_date ?? "",
- });
- const [saving, setSaving] = useState(false);
-
- const save = async () => {
- setSaving(true);
- try {
- const { error } = await supabase
- .from("student_plans")
- .update({
- status: f.status,
- case_manager_id: f.case_manager_id || null,
- effective_date: f.effective_date || null,
- end_date: f.end_date || null,
- next_annual_review_date: f.next_annual_review_date || null,
- next_reevaluation_date: f.next_reevaluation_date || null,
- })
- .eq("id", plan.id);
- if (error) throw error;
- toast.success("Plan updated");
- onSaved();
- } catch (e) {
- err(e);
- } finally {
- setSaving(false);
- }
- };
-
- return (
-
-
-
- Status
- setF({ ...f, status: v as PlanStatus })}>
-
-
-
-
- {(Object.keys(PLAN_STATUS_LABEL) as PlanStatus[]).map((s) => (
-
- {PLAN_STATUS_LABEL[s]}
-
- ))}
-
-
-
-
- Case manager
- setF({ ...f, case_manager_id: v })}
- >
-
-
-
-
- {staff.map((s) => (
-
- {s.full_name || s.email}
-
- ))}
-
-
-
-
- Effective date
- setF({ ...f, effective_date: e.target.value })}
- />
-
-
- End date
- setF({ ...f, end_date: e.target.value })}
- />
-
-
- Next annual review
- setF({ ...f, next_annual_review_date: e.target.value })}
- />
-
-
- Next re-evaluation
- setF({ ...f, next_reevaluation_date: e.target.value })}
- />
-
-
-
- {saving ? "Saving…" : "Save plan details"}
-
-
- );
-}
-
-// ── Accommodations (visible to implementing teachers) ───────────────────────
-function AccommodationsSection({ planId, canEdit }: { planId: string; canEdit: boolean }) {
- const qc = useQueryClient();
- const key = ["plan-accommodations", planId];
- const { data } = useQuery({
- queryKey: key,
- queryFn: async () =>
- (
- await supabase
- .from("plan_accommodations")
- .select("*")
- .eq("plan_id", planId)
- .order("sort_order")
- ).data ?? [],
- });
- const [draft, setDraft] = useState({ category: "instructional", description: "", setting: "" });
- const [adding, setAdding] = useState(false);
-
- const add = async () => {
- if (!draft.description.trim()) return toast.error("Describe the accommodation");
- const { error } = await supabase.from("plan_accommodations").insert({
- plan_id: planId,
- category: draft.category,
- description: draft.description.trim(),
- setting: draft.setting || null,
- sort_order: data?.length ?? 0,
- });
- if (error) return err(error);
- setDraft({ category: "instructional", description: "", setting: "" });
- setAdding(false);
- qc.invalidateQueries({ queryKey: key });
- };
- const remove = async (id: string) => {
- const { error } = await supabase.from("plan_accommodations").delete().eq("id", id);
- if (error) return err(error);
- qc.invalidateQueries({ queryKey: key });
- };
-
- return (
- setAdding(true)}>
- Add
-
- ) : undefined
- }
- >
- {adding && (
-
-
- setDraft({ ...draft, category: v })}
- >
-
-
-
-
- {ACCOMMODATION_CATEGORIES.map((c) => (
-
- {c.label}
-
- ))}
-
-
- setDraft({ ...draft, setting: e.target.value })}
- />
-
-
- )}
- {(data ?? []).length === 0 && !adding && No accommodations recorded. }
-
- {(data ?? []).map((a) => (
-
-
-
{a.description}
-
- {accommodationLabel(a.category)}
- {a.setting ? ` · ${a.setting}` : ""}
-
-
- {canEdit && (
-
remove(a.id)}>
-
-
- )}
-
- ))}
-
-
- );
-}
-
-// ── Related services (visible to implementing teachers) ─────────────────────
-function ServicesSection({ planId, canEdit }: { planId: string; canEdit: boolean }) {
- const qc = useQueryClient();
- const key = ["plan-services", planId];
- const { data } = useQuery({
- queryKey: key,
- queryFn: async () =>
- (await supabase.from("plan_services").select("*").eq("plan_id", planId).order("created_at"))
- .data ?? [],
- });
- const [adding, setAdding] = useState(false);
- const [d, setD] = useState({
- service_type: "",
- provider_name: "",
- minutes_per_session: "",
- sessions_per_period: "",
- period: "week",
- location: "",
- delivery: "pull_out",
- });
-
- const add = async () => {
- if (!d.service_type.trim()) return toast.error("Name the service");
- const { error } = await supabase.from("plan_services").insert({
- plan_id: planId,
- service_type: d.service_type.trim(),
- provider_name: d.provider_name || null,
- minutes_per_session: d.minutes_per_session ? Number(d.minutes_per_session) : null,
- sessions_per_period: d.sessions_per_period ? Number(d.sessions_per_period) : null,
- period: d.period,
- location: d.location || null,
- delivery: d.delivery,
- });
- if (error) return err(error);
- setD({
- service_type: "",
- provider_name: "",
- minutes_per_session: "",
- sessions_per_period: "",
- period: "week",
- location: "",
- delivery: "pull_out",
- });
- setAdding(false);
- qc.invalidateQueries({ queryKey: key });
- };
- const remove = async (id: string) => {
- const { error } = await supabase.from("plan_services").delete().eq("id", id);
- if (error) return err(error);
- qc.invalidateQueries({ queryKey: key });
- };
-
- const summary = (s: {
- minutes_per_session: number | null;
- sessions_per_period: number | null;
- period: string;
- }) => {
- if (!s.minutes_per_session && !s.sessions_per_period) return null;
- const mins = s.minutes_per_session ? `${s.minutes_per_session} min` : "";
- const freq = s.sessions_per_period ? `${s.sessions_per_period}× / ${s.period}` : "";
- return [mins, freq].filter(Boolean).join(" · ");
- };
-
- return (
- setAdding(true)}>
- Add
-
- ) : undefined
- }
- >
- {adding && (
-
-
-
-
- Add
-
- setAdding(false)}>
- Cancel
-
-
-
- )}
- {(data ?? []).length === 0 && !adding && No related services recorded. }
-
- {(data ?? []).map((s) => (
-
-
-
- {s.service_type}
- {s.provider_name ? ` — ${s.provider_name}` : ""}
-
-
- {[summary(s), s.location, s.delivery === "push_in" ? "Push-in" : "Pull-out"]
- .filter(Boolean)
- .join(" · ")}
-
-
- {canEdit && (
-
remove(s.id)}>
-
-
- )}
-
- ))}
-
-
- );
-}
-
-// ── Eligibility / confidential detail ───────────────────────────────────────
-function DetailsSection({ planId, canEdit }: { planId: string; canEdit: boolean }) {
- const qc = useQueryClient();
- const key = ["plan-details", planId];
- const { data } = useQuery({
- queryKey: key,
- queryFn: async () =>
- (await supabase.from("plan_details").select("*").eq("plan_id", planId).maybeSingle()).data,
- });
- const [editing, setEditing] = useState(false);
- const [f, setF] = useState>({});
- const [saving, setSaving] = useState(false);
-
- const start = () => {
- setF({
- eligibility_category: data?.eligibility_category ?? "",
- eligibility_notes: data?.eligibility_notes ?? "",
- last_evaluation_date: data?.last_evaluation_date ?? "",
- consent_date: data?.consent_date ?? "",
- evaluation_summary: data?.evaluation_summary ?? "",
- student_strengths: data?.student_strengths ?? "",
- parent_concerns: data?.parent_concerns ?? "",
- confidential_notes: data?.confidential_notes ?? "",
- });
- setEditing(true);
- };
-
- const save = async () => {
- setSaving(true);
- try {
- const { error } = await supabase.from("plan_details").upsert(
- {
- plan_id: planId,
- eligibility_category: f.eligibility_category || null,
- eligibility_notes: f.eligibility_notes || null,
- last_evaluation_date: f.last_evaluation_date || null,
- consent_date: f.consent_date || null,
- evaluation_summary: f.evaluation_summary || null,
- student_strengths: f.student_strengths || null,
- parent_concerns: f.parent_concerns || null,
- confidential_notes: f.confidential_notes || null,
- },
- { onConflict: "plan_id" },
- );
- if (error) throw error;
- toast.success("Details saved");
- setEditing(false);
- qc.invalidateQueries({ queryKey: key });
- } catch (e) {
- err(e);
- } finally {
- setSaving(false);
- }
- };
-
- const Row = ({ label, value }: { label: string; value?: string | null }) => (
-
- {label}
- {value || "—"}
-
- );
-
- return (
- setEditing(false)}>
- Cancel
-
- ) : (
-
- Edit
-
- )
- ) : undefined
- }
- >
- {editing ? (
-
-
-
- Eligibility notes
-
-
- Evaluation summary
-
-
- Student strengths
-
-
- Parent concerns
-
-
- Confidential notes (staff only)
-
-
- {saving ? "Saving…" : "Save"}
-
-
- ) : (
-
-
-
-
-
-
-
-
-
-
- )}
-
- );
-}
-
-// ── Goals + progress monitoring ─────────────────────────────────────────────
-function GoalsSection({ planId, canEdit }: { planId: string; canEdit: boolean }) {
- const qc = useQueryClient();
- const key = ["plan-goals", planId];
- const { data } = useQuery({
- queryKey: key,
- queryFn: async () =>
- (await supabase.from("plan_goals").select("*").eq("plan_id", planId).order("sort_order"))
- .data ?? [],
- });
- const [adding, setAdding] = useState(false);
- const [g, setG] = useState({
- area: "",
- statement: "",
- baseline: "",
- target_criteria: "",
- target_value: "",
- unit: "",
- target_date: "",
- });
-
- const add = async () => {
- if (!g.statement.trim()) return toast.error("Write the goal statement");
- const { error } = await supabase.from("plan_goals").insert({
- plan_id: planId,
- area: g.area || null,
- statement: g.statement.trim(),
- baseline: g.baseline || null,
- target_criteria: g.target_criteria || null,
- target_value: g.target_value ? Number(g.target_value) : null,
- unit: g.unit || null,
- target_date: g.target_date || null,
- sort_order: data?.length ?? 0,
- });
- if (error) return err(error);
- setG({
- area: "",
- statement: "",
- baseline: "",
- target_criteria: "",
- target_value: "",
- unit: "",
- target_date: "",
- });
- setAdding(false);
- qc.invalidateQueries({ queryKey: key });
- };
-
- return (
- setAdding(true)}>
- Add goal
-
- ) : undefined
- }
- >
- {adding && (
-
- )}
- {(data ?? []).length === 0 && !adding && No goals recorded. }
-
- {(data ?? []).map((goal) => (
- qc.invalidateQueries({ queryKey: key })}
- />
- ))}
-
-
- );
-}
-
-type GoalRecord = {
- id: string;
- area: string | null;
- statement: string;
- baseline: string | null;
- target_criteria: string | null;
- target_value: number | null;
- unit: string | null;
- target_date: string | null;
- status: GoalStatus;
-};
-
-function GoalRow({
- goal,
- canEdit,
- onChanged,
-}: {
- goal: GoalRecord;
- canEdit: boolean;
- onChanged: () => void;
-}) {
- const qc = useQueryClient();
- const key = ["plan-goal-progress", goal.id];
- const [open, setOpen] = useState(false);
- const { data: progress } = useQuery({
- queryKey: key,
- enabled: open,
- queryFn: async () =>
- (
- await supabase
- .from("plan_goal_progress")
- .select("*")
- .eq("goal_id", goal.id)
- .order("date", { ascending: false })
- ).data ?? [],
- });
- const [p, setP] = useState({ date: todayISO(), value: "", narrative: "" });
-
- const latest = progress?.[0];
- const pct =
- goal.target_value && latest?.value != null
- ? Math.max(0, Math.min(100, (Number(latest.value) / Number(goal.target_value)) * 100))
- : null;
-
- const addProgress = async () => {
- const { data: u } = await supabase.auth.getUser();
- const { error } = await supabase.from("plan_goal_progress").insert({
- goal_id: goal.id,
- date: p.date || todayISO(),
- value: p.value ? Number(p.value) : null,
- narrative: p.narrative || null,
- recorded_by: u.user?.id,
- });
- if (error) return err(error);
- setP({ date: todayISO(), value: "", narrative: "" });
- qc.invalidateQueries({ queryKey: key });
- };
-
- const setStatus = async (status: GoalStatus) => {
- const { error } = await supabase.from("plan_goals").update({ status }).eq("id", goal.id);
- if (error) return err(error);
- onChanged();
- };
-
- const remove = async () => {
- if (!confirm("Delete this goal and its progress data?")) return;
- const { error } = await supabase.from("plan_goals").delete().eq("id", goal.id);
- if (error) return err(error);
- onChanged();
- };
-
- return (
-
-
-
-
{goal.statement}
-
- {[
- goal.area,
- goal.baseline ? `Baseline: ${goal.baseline}` : null,
- goal.target_value != null ? `Target: ${goal.target_value}${goal.unit ?? ""}` : null,
- goal.target_date ? `By ${formatDate(goal.target_date)}` : null,
- ]
- .filter(Boolean)
- .join(" · ")}
-
- {goal.target_criteria && (
-
Mastery: {goal.target_criteria}
- )}
-
-
- {canEdit ? (
- setStatus(v as GoalStatus)}>
-
-
-
-
- {(Object.keys(GOAL_STATUS_LABEL) as GoalStatus[]).map((s) => (
-
- {GOAL_STATUS_LABEL[s]}
-
- ))}
-
-
- ) : (
- {GOAL_STATUS_LABEL[goal.status]}
- )}
- {canEdit && (
-
-
-
- )}
-
-
-
- {pct !== null && (
-
-
-
- Latest {latest?.value}
- {goal.unit ?? ""} of {goal.target_value}
- {goal.unit ?? ""} target ({Math.round(pct)}%)
-
-
- )}
-
-
setOpen((o) => !o)}
- >
- {open ? "Hide" : "Show"} progress {progress ? `(${progress.length})` : ""}
-
-
- {open && (
-
- )}
-
- );
-}
-
-// ── Meetings + team ─────────────────────────────────────────────────────────
-function MeetingsSection({ planId, canEdit }: { planId: string; canEdit: boolean }) {
- const qc = useQueryClient();
- const key = ["plan-meetings", planId];
- const { data } = useQuery({
- queryKey: key,
- queryFn: async () =>
- (
- await supabase
- .from("plan_meetings")
- .select("*")
- .eq("plan_id", planId)
- .order("meeting_date", { ascending: false })
- ).data ?? [],
- });
- const [adding, setAdding] = useState(false);
- const [m, setM] = useState({
- meeting_type: "annual_review" as MeetingType,
- meeting_date: todayISO(),
- location: "",
- notes: "",
- outcome: "",
- });
-
- const add = async () => {
- const { data: u } = await supabase.auth.getUser();
- const { error } = await supabase.from("plan_meetings").insert({
- plan_id: planId,
- meeting_type: m.meeting_type,
- meeting_date: m.meeting_date || todayISO(),
- location: m.location || null,
- notes: m.notes || null,
- outcome: m.outcome || null,
- created_by: u.user?.id,
- });
- if (error) return err(error);
- setM({
- meeting_type: "annual_review",
- meeting_date: todayISO(),
- location: "",
- notes: "",
- outcome: "",
- });
- setAdding(false);
- qc.invalidateQueries({ queryKey: key });
- };
- const remove = async (id: string) => {
- const { error } = await supabase.from("plan_meetings").delete().eq("id", id);
- if (error) return err(error);
- qc.invalidateQueries({ queryKey: key });
- };
-
- return (
- setAdding(true)}>
- Add meeting
-
- ) : undefined
- }
- >
- {adding && (
-
- )}
- {(data ?? []).length === 0 && !adding && No meetings recorded. }
-
- {(data ?? []).map((mt) => (
-
-
-
-
- {MEETING_TYPE_LABEL[mt.meeting_type]} · {formatDate(mt.meeting_date)}
-
- {mt.location &&
{mt.location}
}
- {mt.notes &&
{mt.notes}
}
- {mt.outcome && (
-
Outcome: {mt.outcome}
- )}
-
- {canEdit && (
-
remove(mt.id)}>
-
-
- )}
-
-
-
- ))}
-
-
- );
-}
-
-function Attendees({ meetingId, canEdit }: { meetingId: string; canEdit: boolean }) {
- const qc = useQueryClient();
- const key = ["plan-meeting-attendees", meetingId];
- const { data } = useQuery({
- queryKey: key,
- queryFn: async () =>
- (
- await supabase
- .from("plan_meeting_attendees")
- .select("*")
- .eq("meeting_id", meetingId)
- .order("created_at")
- ).data ?? [],
- });
- const [a, setA] = useState({ name: "", team_role: "" });
-
- const add = async () => {
- if (!a.name.trim()) return;
- const { error } = await supabase.from("plan_meeting_attendees").insert({
- meeting_id: meetingId,
- name: a.name.trim(),
- team_role: a.team_role || null,
- });
- if (error) return err(error);
- setA({ name: "", team_role: "" });
- qc.invalidateQueries({ queryKey: key });
- };
- const remove = async (id: string) => {
- const { error } = await supabase.from("plan_meeting_attendees").delete().eq("id", id);
- if (error) return err(error);
- qc.invalidateQueries({ queryKey: key });
- };
-
- return (
-
-
Team members present
-
- {(data ?? []).map((at) => (
-
- {at.name}
- {at.team_role ? ` · ${at.team_role}` : ""}
- {canEdit && (
- remove(at.id)} className="ml-1 opacity-60 hover:opacity-100">
- ×
-
- )}
-
- ))}
- {(data ?? []).length === 0 && (
- None recorded.
- )}
-
- {canEdit && (
-
- setA({ ...a, name: e.target.value })}
- />
- setA({ ...a, team_role: e.target.value })}
- />
-
- Add
-
-
- )}
-
- );
-}
-
-// ── Signed documents ────────────────────────────────────────────────────────
-function DocumentsSection({ planId, canEdit }: { planId: string; canEdit: boolean }) {
- const qc = useQueryClient();
- const key = ["plan-documents", planId];
- const { data } = useQuery({
- queryKey: key,
- queryFn: async () =>
- (
- await supabase
- .from("plan_documents")
- .select("*")
- .eq("plan_id", planId)
- .order("created_at", { ascending: false })
- ).data ?? [],
- });
- const [title, setTitle] = useState("");
- const [uploading, setUploading] = useState(false);
-
- const onUpload = async (file: File) => {
- if (!file) return;
- setUploading(true);
- try {
- // Bucket policies key off the first path segment, so it must be the plan id.
- const path = `${planId}/${Date.now()}-${file.name}`;
- const { error: upErr } = await supabase.storage.from("plan-documents").upload(path, file);
- if (upErr) throw upErr;
- const { data: u } = await supabase.auth.getUser();
- const { error } = await supabase.from("plan_documents").insert({
- plan_id: planId,
- file_path: path,
- title: title || file.name,
- uploaded_by: u.user?.id,
- });
- if (error) throw error;
- setTitle("");
- qc.invalidateQueries({ queryKey: key });
- toast.success("Document uploaded");
- } catch (e) {
- err(e);
- } finally {
- setUploading(false);
- }
- };
-
- const download = async (path: string) => {
- const { data, error } = await supabase.storage.from("plan-documents").createSignedUrl(path, 60);
- if (error) return err(error);
- window.open(data.signedUrl, "_blank");
- };
-
- const remove = async (id: string, path: string) => {
- if (!confirm("Delete this document?")) return;
- await supabase.storage.from("plan-documents").remove([path]);
- const { error } = await supabase.from("plan_documents").delete().eq("id", id);
- if (error) return err(error);
- qc.invalidateQueries({ queryKey: key });
- };
-
- return (
-
- {canEdit && (
-
- setTitle(e.target.value)}
- />
- e.target.files && onUpload(e.target.files[0])}
- />
-
- )}
- {(data ?? []).length === 0 ? (
- No documents uploaded.
- ) : (
-
- {(data ?? []).map((doc) => (
-
-
download(doc.file_path)}
- className="flex items-center gap-2 text-sm hover:underline text-left"
- >
- {doc.title}
-
-
-
- {new Date(doc.created_at).toLocaleDateString()}
-
- {canEdit && (
- remove(doc.id, doc.file_path)}>
-
-
- )}
-
-
- ))}
-
- )}
-
- );
-}