Student profile: read-only view by default, Edit button to edit
Profile / Family & pickup / Academics tabs now render a clean read-only view with an Edit toggle that reveals the editable form (Save/Cancel/Done). Guardian/pickup/login cards show summaries in view mode, inputs in edit mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||||
import { ArrowLeft, Trash2, Plus, FileText, Loader2, ImageUp, User } from "lucide-react";
|
import { ArrowLeft, Trash2, Plus, FileText, Loader2, ImageUp, User, Pencil, Check } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ const DISMISSAL_OPTIONS = [
|
|||||||
{ value: "rideshare", label: "Rideshare (Uber/Lyft)" },
|
{ value: "rideshare", label: "Rideshare (Uber/Lyft)" },
|
||||||
{ value: "other", label: "Other" },
|
{ value: "other", label: "Other" },
|
||||||
];
|
];
|
||||||
|
const dismissalLabel = (v: string) => DISMISSAL_OPTIONS.find((o) => o.value === v)?.label ?? v;
|
||||||
|
|
||||||
function Field({ label, children, className = "" }: { label: string; children: React.ReactNode; className?: string }) {
|
function Field({ label, children, className = "" }: { label: string; children: React.ReactNode; className?: string }) {
|
||||||
return <div className={className}><Label className="text-xs">{label}</Label>{children}</div>;
|
return <div className={className}><Label className="text-xs">{label}</Label>{children}</div>;
|
||||||
@@ -39,6 +40,20 @@ function Section({ title, children }: { title: string; children: React.ReactNode
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
function ViewRow({ label, value }: { label: string; value?: string | null }) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between gap-4 py-1.5 border-b last:border-0 text-sm">
|
||||||
|
<span className="text-muted-foreground shrink-0">{label}</span>
|
||||||
|
<span className="font-medium text-right break-words">{value || "—"}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function EditToggle({ editing, setEditing, canEdit }: { editing: boolean; setEditing: (v: boolean) => void; canEdit: boolean }) {
|
||||||
|
if (!canEdit) return null;
|
||||||
|
return editing
|
||||||
|
? <Button size="sm" variant="ghost" onClick={() => setEditing(false)}><Check className="h-4 w-4 mr-1" /> Done</Button>
|
||||||
|
: <Button size="sm" variant="outline" onClick={() => setEditing(true)}><Pencil className="h-4 w-4 mr-1" /> Edit</Button>;
|
||||||
|
}
|
||||||
|
|
||||||
function StudentDetail() {
|
function StudentDetail() {
|
||||||
const { id } = Route.useParams();
|
const { id } = Route.useParams();
|
||||||
@@ -97,7 +112,6 @@ function PhotoAvatar({ studentId, photoPath, canEdit }: { studentId: string; pho
|
|||||||
return data?.signedUrl ?? null;
|
return data?.signedUrl ?? null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onUpload = async (file: File) => {
|
const onUpload = async (file: File) => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
@@ -112,7 +126,6 @@ function PhotoAvatar({ studentId, photoPath, canEdit }: { studentId: string; pho
|
|||||||
toast.success("Photo updated");
|
toast.success("Photo updated");
|
||||||
} catch (e) { toast.error((e as Error).message); } finally { setUploading(false); }
|
} catch (e) { toast.error((e as Error).message); } finally { setUploading(false); }
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="h-20 w-20 rounded-full bg-muted border overflow-hidden flex items-center justify-center">
|
<div className="h-20 w-20 rounded-full bg-muted border overflow-hidden flex items-center justify-center">
|
||||||
@@ -128,9 +141,10 @@ function PhotoAvatar({ studentId, photoPath, canEdit }: { studentId: string; pho
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Profile (all single-value student fields) ────────────────────────────────
|
// ── Profile ──────────────────────────────────────────────────────────────────
|
||||||
function ProfileTab({ studentId, canEdit }: { studentId: string; canEdit: boolean }) {
|
function ProfileTab({ studentId, canEdit }: { studentId: string; canEdit: boolean }) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
const { data: s } = useQuery({
|
const { data: s } = useQuery({
|
||||||
queryKey: ["student", studentId],
|
queryKey: ["student", studentId],
|
||||||
queryFn: async () => (await supabase.from("students").select("*").eq("id", studentId).single()).data,
|
queryFn: async () => (await supabase.from("students").select("*").eq("id", studentId).single()).data,
|
||||||
@@ -151,7 +165,6 @@ function ProfileTab({ studentId, canEdit }: { studentId: string; canEdit: boolea
|
|||||||
const keys = [
|
const keys = [
|
||||||
"first_name", "last_name", "dob", "gender", "grade_level", "preferred_start_date", "class_id",
|
"first_name", "last_name", "dob", "gender", "grade_level", "preferred_start_date", "class_id",
|
||||||
"allergies", "chronic_conditions", "primary_physician", "physician_phone",
|
"allergies", "chronic_conditions", "primary_physician", "physician_phone",
|
||||||
"home_ed_eval_due", "previous_schools", "special_needs", "portfolio_keeper", "disciplinary_history", "custody_agreement",
|
|
||||||
"dismissal_other", "backup_transport_plan", "interests", "other_info", "agreement_signed_by", "agreement_signed_date", "notes",
|
"dismissal_other", "backup_transport_plan", "interests", "other_info", "agreement_signed_by", "agreement_signed_date", "notes",
|
||||||
];
|
];
|
||||||
const payload: Record<string, unknown> = { dismissal_methods: dismissal, photo_release: !!c.photo_release };
|
const payload: Record<string, unknown> = { dismissal_methods: dismissal, photo_release: !!c.photo_release };
|
||||||
@@ -161,16 +174,57 @@ function ProfileTab({ studentId, canEdit }: { studentId: string; canEdit: boolea
|
|||||||
const { error } = await supabase.from("students").update(payload as never).eq("id", studentId);
|
const { error } = await supabase.from("students").update(payload as never).eq("id", studentId);
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
},
|
},
|
||||||
onSuccess: () => { toast.success("Profile saved"); qc.invalidateQueries({ queryKey: ["student", studentId] }); setForm(null); },
|
onSuccess: () => { toast.success("Profile saved"); qc.invalidateQueries({ queryKey: ["student", studentId] }); setForm(null); setEditing(false); },
|
||||||
onError: (e: Error) => toast.error(e.message),
|
onError: (e: Error) => toast.error(e.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!c) return <div className="p-6"><Loader2 className="h-5 w-5 animate-spin text-muted-foreground" /></div>;
|
if (!c) return <div className="p-6"><Loader2 className="h-5 w-5 animate-spin text-muted-foreground" /></div>;
|
||||||
const val = (k: string) => (c[k] as string) ?? "";
|
const val = (k: string) => (c[k] as string) ?? "";
|
||||||
const T = (k: string, label: string, className = "") => <Field label={label} className={className}><Input disabled={!canEdit} value={val(k)} onChange={(e) => upd(k, e.target.value)} /></Field>;
|
const className = (classes ?? []).find((cl) => cl.id === c.class_id)?.name ?? null;
|
||||||
const D = (k: string, label: string) => <Field label={label}><Input type="date" disabled={!canEdit} value={val(k)} onChange={(e) => upd(k, e.target.value)} /></Field>;
|
|
||||||
const A = (k: string, label: string, rows = 2) => <Field label={label}><Textarea rows={rows} disabled={!canEdit} value={val(k)} onChange={(e) => upd(k, e.target.value)} /></Field>;
|
|
||||||
|
|
||||||
|
// ── View mode ──
|
||||||
|
if (!editing) {
|
||||||
|
return (
|
||||||
|
<div className="bg-card border rounded-lg p-6 space-y-6 mt-4">
|
||||||
|
<div className="flex justify-end"><EditToggle editing={editing} setEditing={setEditing} canEdit={canEdit} /></div>
|
||||||
|
<Section title="Student">
|
||||||
|
<ViewRow label="First name" value={val("first_name")} />
|
||||||
|
<ViewRow label="Last name" value={val("last_name")} />
|
||||||
|
<ViewRow label="Date of birth" value={val("dob")} />
|
||||||
|
<ViewRow label="Gender" value={val("gender") ? val("gender")[0].toUpperCase() + val("gender").slice(1) : ""} />
|
||||||
|
<ViewRow label="Grade level" value={val("grade_level")} />
|
||||||
|
<ViewRow label="Preferred start date" value={val("preferred_start_date")} />
|
||||||
|
<ViewRow label="Class" value={className} />
|
||||||
|
</Section>
|
||||||
|
<Section title="Health">
|
||||||
|
<ViewRow label="Allergies" value={val("allergies")} />
|
||||||
|
<ViewRow label="Chronic conditions" value={val("chronic_conditions")} />
|
||||||
|
<ViewRow label="Primary physician" value={val("primary_physician")} />
|
||||||
|
<ViewRow label="Physician phone" value={val("physician_phone")} />
|
||||||
|
</Section>
|
||||||
|
<Section title="Independent dismissal">
|
||||||
|
<ViewRow label="Allowed methods" value={dismissal.map(dismissalLabel).join(", ")} />
|
||||||
|
{dismissal.includes("other") && <ViewRow label="Other" value={val("dismissal_other")} />}
|
||||||
|
<ViewRow label="Backup transportation plan" value={val("backup_transport_plan")} />
|
||||||
|
</Section>
|
||||||
|
<Section title="Additional">
|
||||||
|
<ViewRow label="Interests / hobbies" value={val("interests")} />
|
||||||
|
<ViewRow label="Other notes" value={val("other_info")} />
|
||||||
|
<ViewRow label="Photo release" value={c.photo_release ? "Granted" : "Not granted"} />
|
||||||
|
<ViewRow label="Internal notes" value={val("notes")} />
|
||||||
|
</Section>
|
||||||
|
<Section title="Agreement">
|
||||||
|
<ViewRow label="Signed by" value={val("agreement_signed_by")} />
|
||||||
|
<ViewRow label="Date" value={val("agreement_signed_date")} />
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Edit mode ──
|
||||||
|
const T = (k: string, label: string) => <Field label={label}><Input value={val(k)} onChange={(e) => upd(k, e.target.value)} /></Field>;
|
||||||
|
const D = (k: string, label: string) => <Field label={label}><Input type="date" value={val(k)} onChange={(e) => upd(k, e.target.value)} /></Field>;
|
||||||
|
const A = (k: string, label: string, rows = 2) => <Field label={label}><Textarea rows={rows} value={val(k)} onChange={(e) => upd(k, e.target.value)} /></Field>;
|
||||||
return (
|
return (
|
||||||
<div className="bg-card border rounded-lg p-6 space-y-6 mt-4">
|
<div className="bg-card border rounded-lg p-6 space-y-6 mt-4">
|
||||||
<Section title="Student">
|
<Section title="Student">
|
||||||
@@ -179,7 +233,7 @@ function ProfileTab({ studentId, canEdit }: { studentId: string; canEdit: boolea
|
|||||||
{T("last_name", "Last name")}
|
{T("last_name", "Last name")}
|
||||||
{D("dob", "Date of birth")}
|
{D("dob", "Date of birth")}
|
||||||
<Field label="Gender">
|
<Field label="Gender">
|
||||||
<Select value={val("gender")} onValueChange={(v) => upd("gender", v)} disabled={!canEdit}>
|
<Select value={val("gender")} onValueChange={(v) => upd("gender", v)}>
|
||||||
<SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
|
<SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
|
||||||
<SelectContent><SelectItem value="male">Male</SelectItem><SelectItem value="female">Female</SelectItem><SelectItem value="other">Other</SelectItem></SelectContent>
|
<SelectContent><SelectItem value="male">Male</SelectItem><SelectItem value="female">Female</SelectItem><SelectItem value="other">Other</SelectItem></SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -187,58 +241,56 @@ function ProfileTab({ studentId, canEdit }: { studentId: string; canEdit: boolea
|
|||||||
{T("grade_level", "Grade level")}
|
{T("grade_level", "Grade level")}
|
||||||
{D("preferred_start_date", "Preferred start date")}
|
{D("preferred_start_date", "Preferred start date")}
|
||||||
<Field label="Class">
|
<Field label="Class">
|
||||||
<Select value={val("class_id")} onValueChange={(v) => upd("class_id", v)} disabled={!canEdit}>
|
<Select value={val("class_id")} onValueChange={(v) => upd("class_id", v)}>
|
||||||
<SelectTrigger><SelectValue placeholder="Assign class" /></SelectTrigger>
|
<SelectTrigger><SelectValue placeholder="Assign class" /></SelectTrigger>
|
||||||
<SelectContent>{(classes ?? []).map((cl) => <SelectItem key={cl.id} value={cl.id}>{cl.name}</SelectItem>)}</SelectContent>
|
<SelectContent>{(classes ?? []).map((cl) => <SelectItem key={cl.id} value={cl.id}>{cl.name}</SelectItem>)}</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="Health">
|
<Section title="Health">
|
||||||
{A("allergies", "Allergies (blank if none)")}
|
{A("allergies", "Allergies (blank if none)")}
|
||||||
{A("chronic_conditions", "Chronic conditions (blank if none)")}
|
{A("chronic_conditions", "Chronic conditions (blank if none)")}
|
||||||
<div className="grid grid-cols-2 gap-3">{T("primary_physician", "Primary physician")}{T("physician_phone", "Physician phone")}</div>
|
<div className="grid grid-cols-2 gap-3">{T("primary_physician", "Primary physician")}{T("physician_phone", "Physician phone")}</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="Independent dismissal">
|
<Section title="Independent dismissal">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
|
||||||
{DISMISSAL_OPTIONS.map((o) => (
|
{DISMISSAL_OPTIONS.map((o) => (
|
||||||
<label key={o.value} className="flex items-center gap-2 text-sm">
|
<label key={o.value} className="flex items-center gap-2 text-sm">
|
||||||
<Checkbox checked={dismissal.includes(o.value)} disabled={!canEdit} onCheckedChange={() => canEdit && toggleDismissal(o.value)} />{o.label}
|
<Checkbox checked={dismissal.includes(o.value)} onCheckedChange={() => toggleDismissal(o.value)} />{o.label}
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{dismissal.includes("other") && T("dismissal_other", "Other (describe)")}
|
{dismissal.includes("other") && T("dismissal_other", "Other (describe)")}
|
||||||
{A("backup_transport_plan", "Backup transportation plan")}
|
{A("backup_transport_plan", "Backup transportation plan")}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="Additional">
|
<Section title="Additional">
|
||||||
{A("interests", "Interests / hobbies / activities")}
|
{A("interests", "Interests / hobbies / activities")}
|
||||||
{A("other_info", "Anything else we should know")}
|
{A("other_info", "Anything else we should know")}
|
||||||
<div className="flex items-center justify-between max-w-sm"><Label className="text-xs">Photo release granted</Label><Switch disabled={!canEdit} checked={!!c.photo_release} onCheckedChange={(v) => upd("photo_release", v)} /></div>
|
<div className="flex items-center justify-between max-w-sm"><Label className="text-xs">Photo release granted</Label><Switch checked={!!c.photo_release} onCheckedChange={(v) => upd("photo_release", v)} /></div>
|
||||||
{A("notes", "Internal notes (staff only)")}
|
{A("notes", "Internal notes (staff only)")}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="Agreement">
|
<Section title="Agreement">
|
||||||
<div className="grid grid-cols-2 gap-3">{T("agreement_signed_by", "Signed by")}{D("agreement_signed_date", "Date")}</div>
|
<div className="grid grid-cols-2 gap-3">{T("agreement_signed_by", "Signed by")}{D("agreement_signed_date", "Date")}</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
<div className="flex gap-2">
|
||||||
{canEdit && <Button onClick={() => save.mutate()} disabled={save.isPending}>{save.isPending ? "Saving…" : "Save profile"}</Button>}
|
<Button onClick={() => save.mutate()} disabled={save.isPending}>{save.isPending ? "Saving…" : "Save profile"}</Button>
|
||||||
|
<Button variant="ghost" onClick={() => { setForm(null); setEditing(false); }}>Cancel</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Family & pickup (guardians, emergency contacts, authorized pickups) ───────
|
// ── Family & pickup ──────────────────────────────────────────────────────────
|
||||||
type GuardianRow = { id: string; is_primary: boolean; full_name: string; relationship: string | null; phone: string | null; address: string | null; city: string | null; state: string | null; zip: string | null; email: string | null; employer: string | null };
|
type GuardianRow = { id: string; is_primary: boolean; full_name: string; relationship: string | null; phone: string | null; address: string | null; city: string | null; state: string | null; zip: string | null; email: string | null; employer: string | null };
|
||||||
|
|
||||||
function FamilyTab({ studentId, canEdit }: { studentId: string; canEdit: boolean }) {
|
function FamilyTab({ studentId, canEdit }: { studentId: string; canEdit: boolean }) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
const { data: guardians } = useQuery({
|
const { data: guardians } = useQuery({
|
||||||
queryKey: ["guardians", studentId],
|
queryKey: ["guardians", studentId],
|
||||||
queryFn: async () => (await supabase.from("student_guardians").select("*").eq("student_id", studentId).order("is_primary", { ascending: false })).data ?? [],
|
queryFn: async () => (await supabase.from("student_guardians").select("*").eq("student_id", studentId).order("is_primary", { ascending: false })).data ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const addGuardian = useMutation({
|
const addGuardian = useMutation({
|
||||||
mutationFn: async (isPrimary: boolean) => {
|
mutationFn: async (isPrimary: boolean) => {
|
||||||
const { error } = await supabase.from("student_guardians").insert({ student_id: studentId, is_primary: isPrimary, full_name: "New guardian" });
|
const { error } = await supabase.from("student_guardians").insert({ student_id: studentId, is_primary: isPrimary, full_name: "New guardian" });
|
||||||
@@ -250,30 +302,26 @@ function FamilyTab({ studentId, canEdit }: { studentId: string; canEdit: boolean
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 mt-4">
|
<div className="space-y-6 mt-4">
|
||||||
|
<div className="flex justify-end"><EditToggle editing={editing} setEditing={setEditing} canEdit={canEdit} /></div>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between"><h3 className="font-semibold">Guardians</h3>
|
<div className="flex items-center justify-between"><h3 className="font-semibold">Guardians</h3>
|
||||||
{canEdit && <Button size="sm" variant="outline" onClick={() => addGuardian.mutate((guardians ?? []).length === 0)}><Plus className="h-4 w-4 mr-1" /> Add guardian</Button>}
|
{editing && <Button size="sm" variant="outline" onClick={() => addGuardian.mutate((guardians ?? []).length === 0)}><Plus className="h-4 w-4 mr-1" /> Add guardian</Button>}
|
||||||
</div>
|
</div>
|
||||||
{(guardians ?? []).map((g) => <GuardianCard key={g.id} g={g as GuardianRow} studentId={studentId} canEdit={canEdit} />)}
|
{(guardians ?? []).map((g) => <GuardianCard key={g.id} g={g as GuardianRow} studentId={studentId} editing={editing} />)}
|
||||||
{guardians?.length === 0 && <p className="text-sm text-muted-foreground">No guardians yet.</p>}
|
{guardians?.length === 0 && <p className="text-sm text-muted-foreground">No guardians yet.</p>}
|
||||||
</div>
|
</div>
|
||||||
|
<PickupList studentId={studentId} editing={editing} kind="pickup" title="Authorized pick-up" subtitle="Only these individuals may pick up the student (valid ID required)." />
|
||||||
<PickupList studentId={studentId} canEdit={canEdit} kind="pickup" title="Authorized pick-up" subtitle="Only these individuals may pick up the student (valid ID required)." />
|
<PickupList studentId={studentId} editing={editing} kind="emergency" title="Emergency contacts" subtitle="Contacted if guardians are unreachable." />
|
||||||
<PickupList studentId={studentId} canEdit={canEdit} kind="emergency" title="Emergency contacts" subtitle="Contacted if guardians are unreachable." />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GuardianCard({ g, studentId, canEdit }: { g: GuardianRow; studentId: string; canEdit: boolean }) {
|
function GuardianCard({ g, studentId, editing }: { g: GuardianRow; studentId: string; editing: boolean }) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const [row, setRow] = useState<GuardianRow>(g);
|
const [row, setRow] = useState<GuardianRow>(g);
|
||||||
const f = (k: keyof GuardianRow) => (e: React.ChangeEvent<HTMLInputElement>) => setRow({ ...row, [k]: e.target.value });
|
const f = (k: keyof GuardianRow) => (e: React.ChangeEvent<HTMLInputElement>) => setRow({ ...row, [k]: e.target.value });
|
||||||
const save = useMutation({
|
const save = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => { const { id, ...rest } = row; const { error } = await supabase.from("student_guardians").update(rest).eq("id", id); if (error) throw error; },
|
||||||
const { id, ...rest } = row;
|
|
||||||
const { error } = await supabase.from("student_guardians").update(rest).eq("id", id);
|
|
||||||
if (error) throw error;
|
|
||||||
},
|
|
||||||
onSuccess: () => { toast.success("Guardian saved"); qc.invalidateQueries({ queryKey: ["guardians", studentId] }); },
|
onSuccess: () => { toast.success("Guardian saved"); qc.invalidateQueries({ queryKey: ["guardians", studentId] }); },
|
||||||
onError: (e: Error) => toast.error(e.message),
|
onError: (e: Error) => toast.error(e.message),
|
||||||
});
|
});
|
||||||
@@ -281,31 +329,47 @@ function GuardianCard({ g, studentId, canEdit }: { g: GuardianRow; studentId: st
|
|||||||
mutationFn: async () => { const { error } = await supabase.from("student_guardians").delete().eq("id", g.id); if (error) throw error; },
|
mutationFn: async () => { const { error } = await supabase.from("student_guardians").delete().eq("id", g.id); if (error) throw error; },
|
||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["guardians", studentId] }),
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["guardians", studentId] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!editing) {
|
||||||
|
const loc = [g.city, g.state, g.zip].filter(Boolean).join(", ");
|
||||||
|
return (
|
||||||
|
<div className="bg-card border rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2"><span className="font-medium">{g.full_name}</span>{g.is_primary && <span className="text-[10px] uppercase tracking-wide bg-primary/10 text-primary rounded px-1.5 py-0.5">Primary</span>}</div>
|
||||||
|
<div className="text-sm text-muted-foreground mt-1 space-y-0.5">
|
||||||
|
{g.relationship && <div>{g.relationship}</div>}
|
||||||
|
{g.phone && <div>{g.phone}</div>}
|
||||||
|
{g.email && <div>{g.email}</div>}
|
||||||
|
{(g.address || loc) && <div>{[g.address, loc].filter(Boolean).join(" · ")}</div>}
|
||||||
|
{g.employer && <div>Employer: {g.employer}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="bg-card border rounded-lg p-4 space-y-3">
|
<div className="bg-card border rounded-lg p-4 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="flex items-center gap-2 text-sm font-medium"><Checkbox checked={row.is_primary} disabled={!canEdit} onCheckedChange={(v) => setRow({ ...row, is_primary: !!v })} /> Primary guardian</label>
|
<label className="flex items-center gap-2 text-sm font-medium"><Checkbox checked={row.is_primary} onCheckedChange={(v) => setRow({ ...row, is_primary: !!v })} /> Primary guardian</label>
|
||||||
{canEdit && <Button size="icon" variant="ghost" onClick={() => del.mutate()}><Trash2 className="h-4 w-4" /></Button>}
|
<Button size="icon" variant="ghost" onClick={() => del.mutate()}><Trash2 className="h-4 w-4" /></Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||||
<Field label="Full name"><Input disabled={!canEdit} value={row.full_name ?? ""} onChange={f("full_name")} /></Field>
|
<Field label="Full name"><Input value={row.full_name ?? ""} onChange={f("full_name")} /></Field>
|
||||||
<Field label="Relationship"><Input disabled={!canEdit} value={row.relationship ?? ""} onChange={f("relationship")} /></Field>
|
<Field label="Relationship"><Input value={row.relationship ?? ""} onChange={f("relationship")} /></Field>
|
||||||
<Field label="Phone"><Input disabled={!canEdit} value={row.phone ?? ""} onChange={f("phone")} /></Field>
|
<Field label="Phone"><Input value={row.phone ?? ""} onChange={f("phone")} /></Field>
|
||||||
<Field label="Address" className="col-span-2 md:col-span-3"><Input disabled={!canEdit} value={row.address ?? ""} onChange={f("address")} /></Field>
|
<Field label="Address" className="col-span-2 md:col-span-3"><Input value={row.address ?? ""} onChange={f("address")} /></Field>
|
||||||
<Field label="City"><Input disabled={!canEdit} value={row.city ?? ""} onChange={f("city")} /></Field>
|
<Field label="City"><Input value={row.city ?? ""} onChange={f("city")} /></Field>
|
||||||
<Field label="State"><Input disabled={!canEdit} value={row.state ?? ""} onChange={f("state")} /></Field>
|
<Field label="State"><Input value={row.state ?? ""} onChange={f("state")} /></Field>
|
||||||
<Field label="Zip"><Input disabled={!canEdit} value={row.zip ?? ""} onChange={f("zip")} /></Field>
|
<Field label="Zip"><Input value={row.zip ?? ""} onChange={f("zip")} /></Field>
|
||||||
<Field label="Email"><Input disabled={!canEdit} value={row.email ?? ""} onChange={f("email")} /></Field>
|
<Field label="Email"><Input value={row.email ?? ""} onChange={f("email")} /></Field>
|
||||||
<Field label="Employer"><Input disabled={!canEdit} value={row.employer ?? ""} onChange={f("employer")} /></Field>
|
<Field label="Employer"><Input value={row.employer ?? ""} onChange={f("employer")} /></Field>
|
||||||
</div>
|
</div>
|
||||||
{canEdit && <Button size="sm" onClick={() => save.mutate()} disabled={save.isPending}>Save</Button>}
|
<Button size="sm" onClick={() => save.mutate()} disabled={save.isPending}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type PickupRow = { id: string; name: string; relationship: string | null; phone: string | null; alt_phone: string | null; notes: string | null; kind: string };
|
type PickupRow = { id: string; name: string; relationship: string | null; phone: string | null; alt_phone: string | null; notes: string | null; kind: string };
|
||||||
|
|
||||||
function PickupList({ studentId, canEdit, kind, title, subtitle }: { studentId: string; canEdit: boolean; kind: string; title: string; subtitle: string }) {
|
function PickupList({ studentId, editing, kind, title, subtitle }: { studentId: string; editing: boolean; kind: string; title: string; subtitle: string }) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ["pickups", studentId],
|
queryKey: ["pickups", studentId],
|
||||||
@@ -320,23 +384,20 @@ function PickupList({ studentId, canEdit, kind, title, subtitle }: { studentId:
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between"><div><h3 className="font-semibold">{title}</h3><p className="text-xs text-muted-foreground">{subtitle}</p></div>
|
<div className="flex items-center justify-between"><div><h3 className="font-semibold">{title}</h3><p className="text-xs text-muted-foreground">{subtitle}</p></div>
|
||||||
{canEdit && <Button size="sm" variant="outline" onClick={() => add.mutate()}><Plus className="h-4 w-4 mr-1" /> Add</Button>}
|
{editing && <Button size="sm" variant="outline" onClick={() => add.mutate()}><Plus className="h-4 w-4 mr-1" /> Add</Button>}
|
||||||
</div>
|
</div>
|
||||||
{rows.map((p) => <PickupCard key={p.id} p={p as PickupRow} studentId={studentId} canEdit={canEdit} showNotes={kind === "pickup"} showAlt={kind === "emergency"} />)}
|
{rows.map((p) => <PickupCard key={p.id} p={p as PickupRow} studentId={studentId} editing={editing} showNotes={kind === "pickup"} showAlt={kind === "emergency"} />)}
|
||||||
{rows.length === 0 && <p className="text-sm text-muted-foreground">None added.</p>}
|
{rows.length === 0 && <p className="text-sm text-muted-foreground">None added.</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PickupCard({ p, studentId, canEdit, showNotes, showAlt }: { p: PickupRow; studentId: string; canEdit: boolean; showNotes: boolean; showAlt: boolean }) {
|
function PickupCard({ p, studentId, editing, showNotes, showAlt }: { p: PickupRow; studentId: string; editing: boolean; showNotes: boolean; showAlt: boolean }) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const [row, setRow] = useState<PickupRow>(p);
|
const [row, setRow] = useState<PickupRow>(p);
|
||||||
const f = (k: keyof PickupRow) => (e: React.ChangeEvent<HTMLInputElement>) => setRow({ ...row, [k]: e.target.value });
|
const f = (k: keyof PickupRow) => (e: React.ChangeEvent<HTMLInputElement>) => setRow({ ...row, [k]: e.target.value });
|
||||||
const save = useMutation({
|
const save = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => { const { error } = await supabase.from("authorized_pickups").update({ name: row.name, relationship: row.relationship, phone: row.phone, alt_phone: row.alt_phone, notes: row.notes }).eq("id", row.id); if (error) throw error; },
|
||||||
const { error } = await supabase.from("authorized_pickups").update({ name: row.name, relationship: row.relationship, phone: row.phone, alt_phone: row.alt_phone, notes: row.notes }).eq("id", row.id);
|
|
||||||
if (error) throw error;
|
|
||||||
},
|
|
||||||
onSuccess: () => { toast.success("Saved"); qc.invalidateQueries({ queryKey: ["pickups", studentId] }); },
|
onSuccess: () => { toast.success("Saved"); qc.invalidateQueries({ queryKey: ["pickups", studentId] }); },
|
||||||
onError: (e: Error) => toast.error(e.message),
|
onError: (e: Error) => toast.error(e.message),
|
||||||
});
|
});
|
||||||
@@ -344,25 +405,39 @@ function PickupCard({ p, studentId, canEdit, showNotes, showAlt }: { p: PickupRo
|
|||||||
mutationFn: async () => { const { error } = await supabase.from("authorized_pickups").delete().eq("id", p.id); if (error) throw error; },
|
mutationFn: async () => { const { error } = await supabase.from("authorized_pickups").delete().eq("id", p.id); if (error) throw error; },
|
||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["pickups", studentId] }),
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["pickups", studentId] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!editing) {
|
||||||
|
return (
|
||||||
|
<div className="bg-card border rounded-lg p-4">
|
||||||
|
<div className="font-medium">{p.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground mt-0.5 space-y-0.5">
|
||||||
|
{p.relationship && <div>{p.relationship}</div>}
|
||||||
|
{p.phone && <div>{p.phone}{p.alt_phone ? ` · alt ${p.alt_phone}` : ""}</div>}
|
||||||
|
{p.notes && <div>Note: {p.notes}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="bg-card border rounded-lg p-4 space-y-3">
|
<div className="bg-card border rounded-lg p-4 space-y-3">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
<Field label="Name"><Input disabled={!canEdit} value={row.name ?? ""} onChange={f("name")} /></Field>
|
<Field label="Name"><Input value={row.name ?? ""} onChange={f("name")} /></Field>
|
||||||
<Field label="Relationship"><Input disabled={!canEdit} value={row.relationship ?? ""} onChange={f("relationship")} /></Field>
|
<Field label="Relationship"><Input value={row.relationship ?? ""} onChange={f("relationship")} /></Field>
|
||||||
<Field label="Phone"><Input disabled={!canEdit} value={row.phone ?? ""} onChange={f("phone")} /></Field>
|
<Field label="Phone"><Input value={row.phone ?? ""} onChange={f("phone")} /></Field>
|
||||||
{showAlt && <Field label="Alternate phone"><Input disabled={!canEdit} value={row.alt_phone ?? ""} onChange={f("alt_phone")} /></Field>}
|
{showAlt && <Field label="Alternate phone"><Input value={row.alt_phone ?? ""} onChange={f("alt_phone")} /></Field>}
|
||||||
{showNotes && <Field label="Notes / restrictions"><Input disabled={!canEdit} value={row.notes ?? ""} onChange={f("notes")} /></Field>}
|
{showNotes && <Field label="Notes / restrictions"><Input value={row.notes ?? ""} onChange={f("notes")} /></Field>}
|
||||||
</div>
|
</div>
|
||||||
{canEdit && <div className="flex gap-2"><Button size="sm" onClick={() => save.mutate()} disabled={save.isPending}>Save</Button><Button size="sm" variant="ghost" onClick={() => del.mutate()}><Trash2 className="h-4 w-4" /></Button></div>}
|
<div className="flex gap-2"><Button size="sm" onClick={() => save.mutate()} disabled={save.isPending}>Save</Button><Button size="sm" variant="ghost" onClick={() => del.mutate()}><Trash2 className="h-4 w-4" /></Button></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Academics (curriculum logins + academic notes) ───────────────────────────
|
// ── Academics ────────────────────────────────────────────────────────────────
|
||||||
type LoginRow = { id: string; website: string | null; student_login: string | null; student_password: string | null; parent_account: string | null; parent_password: string | null };
|
type LoginRow = { id: string; website: string | null; student_login: string | null; student_password: string | null; parent_account: string | null; parent_password: string | null };
|
||||||
|
|
||||||
function AcademicsTab({ studentId, canEdit }: { studentId: string; canEdit: boolean }) {
|
function AcademicsTab({ studentId, canEdit }: { studentId: string; canEdit: boolean }) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
const { data: s } = useQuery({
|
const { data: s } = useQuery({
|
||||||
queryKey: ["student", studentId],
|
queryKey: ["student", studentId],
|
||||||
queryFn: async () => (await supabase.from("students").select("*").eq("id", studentId).single()).data,
|
queryFn: async () => (await supabase.from("students").select("*").eq("id", studentId).single()).data,
|
||||||
@@ -371,21 +446,16 @@ function AcademicsTab({ studentId, canEdit }: { studentId: string; canEdit: bool
|
|||||||
const c = (form ?? s) as Record<string, unknown> | null;
|
const c = (form ?? s) as Record<string, unknown> | null;
|
||||||
const upd = (k: string, v: unknown) => setForm({ ...(c as Record<string, unknown>), [k]: v });
|
const upd = (k: string, v: unknown) => setForm({ ...(c as Record<string, unknown>), [k]: v });
|
||||||
const val = (k: string) => (c?.[k] as string) ?? "";
|
const val = (k: string) => (c?.[k] as string) ?? "";
|
||||||
|
|
||||||
const { data: logins } = useQuery({
|
const { data: logins } = useQuery({
|
||||||
queryKey: ["curriculum", studentId],
|
queryKey: ["curriculum", studentId],
|
||||||
queryFn: async () => (await supabase.from("student_curriculum_logins").select("*").eq("student_id", studentId).order("created_at")).data ?? [],
|
queryFn: async () => (await supabase.from("student_curriculum_logins").select("*").eq("student_id", studentId).order("created_at")).data ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveAcademics = useMutation({
|
const saveAcademics = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const { error } = await supabase.from("students").update({
|
const { error } = await supabase.from("students").update({
|
||||||
home_ed_eval_due: val("home_ed_eval_due") || null,
|
home_ed_eval_due: val("home_ed_eval_due") || null, previous_schools: val("previous_schools") || null,
|
||||||
previous_schools: val("previous_schools") || null,
|
special_needs: val("special_needs") || null, portfolio_keeper: val("portfolio_keeper") || null,
|
||||||
special_needs: val("special_needs") || null,
|
disciplinary_history: val("disciplinary_history") || null, custody_agreement: val("custody_agreement") || null,
|
||||||
portfolio_keeper: val("portfolio_keeper") || null,
|
|
||||||
disciplinary_history: val("disciplinary_history") || null,
|
|
||||||
custody_agreement: val("custody_agreement") || null,
|
|
||||||
}).eq("id", studentId);
|
}).eq("id", studentId);
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
},
|
},
|
||||||
@@ -397,49 +467,60 @@ function AcademicsTab({ studentId, canEdit }: { studentId: string; canEdit: bool
|
|||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["curriculum", studentId] }),
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["curriculum", studentId] }),
|
||||||
onError: (e: Error) => toast.error(e.message),
|
onError: (e: Error) => toast.error(e.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!c) return <div className="p-6"><Loader2 className="h-5 w-5 animate-spin text-muted-foreground" /></div>;
|
if (!c) return <div className="p-6"><Loader2 className="h-5 w-5 animate-spin text-muted-foreground" /></div>;
|
||||||
|
const portfolio = val("portfolio_keeper") === "parent" ? "Parent/Guardian" : val("portfolio_keeper") === "bayside" ? "Bayside Academy" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 mt-4">
|
<div className="space-y-6 mt-4">
|
||||||
|
<div className="flex justify-end"><EditToggle editing={editing} setEditing={setEditing} canEdit={canEdit} /></div>
|
||||||
|
|
||||||
<div className="bg-card border rounded-lg p-6 space-y-4">
|
<div className="bg-card border rounded-lg p-6 space-y-4">
|
||||||
|
{!editing ? (
|
||||||
|
<>
|
||||||
|
<ViewRow label="Home Ed. eval due to BPS" value={val("home_ed_eval_due")} />
|
||||||
|
<ViewRow label="Previous school(s)" value={val("previous_schools")} />
|
||||||
|
<ViewRow label="Special needs / accommodations" value={val("special_needs")} />
|
||||||
|
<ViewRow label="Portfolio kept by" value={portfolio} />
|
||||||
|
<ViewRow label="Disciplinary history" value={val("disciplinary_history")} />
|
||||||
|
<ViewRow label="Custody agreement" value={val("custody_agreement")} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<Field label="Home Ed. Annual Evaluation due to BPS"><Input type="date" disabled={!canEdit} value={val("home_ed_eval_due")} onChange={(e) => upd("home_ed_eval_due", e.target.value)} /></Field>
|
<Field label="Home Ed. Annual Evaluation due to BPS"><Input type="date" value={val("home_ed_eval_due")} onChange={(e) => upd("home_ed_eval_due", e.target.value)} /></Field>
|
||||||
<Field label="Previous school(s)"><Input disabled={!canEdit} value={val("previous_schools")} onChange={(e) => upd("previous_schools", e.target.value)} /></Field>
|
<Field label="Previous school(s)"><Input value={val("previous_schools")} onChange={(e) => upd("previous_schools", e.target.value)} /></Field>
|
||||||
</div>
|
</div>
|
||||||
<Field label="Special academic needs / accommodations"><Textarea rows={2} disabled={!canEdit} value={val("special_needs")} onChange={(e) => upd("special_needs", e.target.value)} /></Field>
|
<Field label="Special academic needs / accommodations"><Textarea rows={2} value={val("special_needs")} onChange={(e) => upd("special_needs", e.target.value)} /></Field>
|
||||||
<Field label="Who keeps the portfolio">
|
<Field label="Who keeps the portfolio">
|
||||||
<Select value={val("portfolio_keeper")} onValueChange={(v) => upd("portfolio_keeper", v)} disabled={!canEdit}>
|
<Select value={val("portfolio_keeper")} onValueChange={(v) => upd("portfolio_keeper", v)}>
|
||||||
<SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
|
<SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
|
||||||
<SelectContent><SelectItem value="parent">Parent/Guardian</SelectItem><SelectItem value="bayside">Bayside Academy</SelectItem></SelectContent>
|
<SelectContent><SelectItem value="parent">Parent/Guardian</SelectItem><SelectItem value="bayside">Bayside Academy</SelectItem></SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Disciplinary history"><Textarea rows={2} disabled={!canEdit} value={val("disciplinary_history")} onChange={(e) => upd("disciplinary_history", e.target.value)} /></Field>
|
<Field label="Disciplinary history"><Textarea rows={2} value={val("disciplinary_history")} onChange={(e) => upd("disciplinary_history", e.target.value)} /></Field>
|
||||||
<Field label="Custody agreement"><Textarea rows={2} disabled={!canEdit} value={val("custody_agreement")} onChange={(e) => upd("custody_agreement", e.target.value)} /></Field>
|
<Field label="Custody agreement"><Textarea rows={2} value={val("custody_agreement")} onChange={(e) => upd("custody_agreement", e.target.value)} /></Field>
|
||||||
{canEdit && <Button onClick={() => saveAcademics.mutate()} disabled={saveAcademics.isPending}>Save academics</Button>}
|
<Button onClick={() => saveAcademics.mutate()} disabled={saveAcademics.isPending}>Save academics</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between"><h3 className="font-semibold">Curriculum platform logins</h3>
|
<div className="flex items-center justify-between"><h3 className="font-semibold">Curriculum platform logins</h3>
|
||||||
{canEdit && <Button size="sm" variant="outline" onClick={() => addLogin.mutate()}><Plus className="h-4 w-4 mr-1" /> Add platform</Button>}
|
{editing && <Button size="sm" variant="outline" onClick={() => addLogin.mutate()}><Plus className="h-4 w-4 mr-1" /> Add platform</Button>}
|
||||||
</div>
|
</div>
|
||||||
{(logins ?? []).map((l) => <LoginCard key={l.id} l={l as LoginRow} studentId={studentId} canEdit={canEdit} />)}
|
{(logins ?? []).map((l) => <LoginCard key={l.id} l={l as LoginRow} studentId={studentId} editing={editing} />)}
|
||||||
{logins?.length === 0 && <p className="text-sm text-muted-foreground">No logins added.</p>}
|
{logins?.length === 0 && <p className="text-sm text-muted-foreground">No logins added.</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoginCard({ l, studentId, canEdit }: { l: LoginRow; studentId: string; canEdit: boolean }) {
|
function LoginCard({ l, studentId, editing }: { l: LoginRow; studentId: string; editing: boolean }) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const [row, setRow] = useState<LoginRow>(l);
|
const [row, setRow] = useState<LoginRow>(l);
|
||||||
const f = (k: keyof LoginRow) => (e: React.ChangeEvent<HTMLInputElement>) => setRow({ ...row, [k]: e.target.value });
|
const f = (k: keyof LoginRow) => (e: React.ChangeEvent<HTMLInputElement>) => setRow({ ...row, [k]: e.target.value });
|
||||||
const save = useMutation({
|
const save = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => { const { id, ...rest } = row; const { error } = await supabase.from("student_curriculum_logins").update(rest).eq("id", id); if (error) throw error; },
|
||||||
const { id, ...rest } = row;
|
|
||||||
const { error } = await supabase.from("student_curriculum_logins").update(rest).eq("id", id);
|
|
||||||
if (error) throw error;
|
|
||||||
},
|
|
||||||
onSuccess: () => { toast.success("Saved"); qc.invalidateQueries({ queryKey: ["curriculum", studentId] }); },
|
onSuccess: () => { toast.success("Saved"); qc.invalidateQueries({ queryKey: ["curriculum", studentId] }); },
|
||||||
onError: (e: Error) => toast.error(e.message),
|
onError: (e: Error) => toast.error(e.message),
|
||||||
});
|
});
|
||||||
@@ -447,21 +528,32 @@ function LoginCard({ l, studentId, canEdit }: { l: LoginRow; studentId: string;
|
|||||||
mutationFn: async () => { const { error } = await supabase.from("student_curriculum_logins").delete().eq("id", l.id); if (error) throw error; },
|
mutationFn: async () => { const { error } = await supabase.from("student_curriculum_logins").delete().eq("id", l.id); if (error) throw error; },
|
||||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["curriculum", studentId] }),
|
onSuccess: () => qc.invalidateQueries({ queryKey: ["curriculum", studentId] }),
|
||||||
});
|
});
|
||||||
|
if (!editing) {
|
||||||
|
return (
|
||||||
|
<div className="bg-card border rounded-lg p-4">
|
||||||
|
<div className="font-medium">{l.website || "Platform"}</div>
|
||||||
|
<div className="text-sm text-muted-foreground mt-0.5 space-y-0.5">
|
||||||
|
{l.student_login && <div>Student: {l.student_login}{l.student_password ? ` / ${l.student_password}` : ""}</div>}
|
||||||
|
{l.parent_account && <div>Parent: {l.parent_account}{l.parent_password ? ` / ${l.parent_password}` : ""}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="bg-card border rounded-lg p-4 space-y-3">
|
<div className="bg-card border rounded-lg p-4 space-y-3">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||||
<Field label="Website"><Input disabled={!canEdit} value={row.website ?? ""} onChange={f("website")} /></Field>
|
<Field label="Website"><Input value={row.website ?? ""} onChange={f("website")} /></Field>
|
||||||
<Field label="Student login"><Input disabled={!canEdit} value={row.student_login ?? ""} onChange={f("student_login")} /></Field>
|
<Field label="Student login"><Input value={row.student_login ?? ""} onChange={f("student_login")} /></Field>
|
||||||
<Field label="Student password"><Input disabled={!canEdit} value={row.student_password ?? ""} onChange={f("student_password")} /></Field>
|
<Field label="Student password"><Input value={row.student_password ?? ""} onChange={f("student_password")} /></Field>
|
||||||
<Field label="Parent account"><Input disabled={!canEdit} value={row.parent_account ?? ""} onChange={f("parent_account")} /></Field>
|
<Field label="Parent account"><Input value={row.parent_account ?? ""} onChange={f("parent_account")} /></Field>
|
||||||
<Field label="Parent password"><Input disabled={!canEdit} value={row.parent_password ?? ""} onChange={f("parent_password")} /></Field>
|
<Field label="Parent password"><Input value={row.parent_password ?? ""} onChange={f("parent_password")} /></Field>
|
||||||
</div>
|
</div>
|
||||||
{canEdit && <div className="flex gap-2"><Button size="sm" onClick={() => save.mutate()} disabled={save.isPending}>Save</Button><Button size="sm" variant="ghost" onClick={() => del.mutate()}><Trash2 className="h-4 w-4" /></Button></div>}
|
<div className="flex gap-2"><Button size="sm" onClick={() => save.mutate()} disabled={save.isPending}>Save</Button><Button size="sm" variant="ghost" onClick={() => del.mutate()}><Trash2 className="h-4 w-4" /></Button></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Attendance / Ledger / Contracts (unchanged) ──────────────────────────────
|
// ── Attendance / Ledger / Contracts ──────────────────────────────────────────
|
||||||
function AttendanceTab({ studentId }: { studentId: string }) {
|
function AttendanceTab({ studentId }: { studentId: string }) {
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ["attendance", studentId],
|
queryKey: ["attendance", studentId],
|
||||||
|
|||||||
Reference in New Issue
Block a user