Modified by www.SourceFiles.app
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
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 { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { GraduationCap, Loader2, CheckCircle2, Plus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { getIntakeToken, submitIntake, type IntakePayload } from "@/lib/intake.functions";
|
||||
|
||||
export const Route = createFileRoute("/intake/$token")({
|
||||
head: () => ({ meta: [{ title: "Student intake — Bayside Academy" }] }),
|
||||
component: IntakePage,
|
||||
});
|
||||
|
||||
const emptyGuardian = { is_primary: false, full_name: "", relationship: "", phone: "", address: "", city: "", state: "", zip: "", email: "", employer: "" };
|
||||
const emptyLogin = { website: "", student_login: "", student_password: "", parent_account: "", parent_password: "" };
|
||||
const DISMISSAL = [
|
||||
{ value: "not_allowed", label: "NOT ALLOWED" }, { value: "bicycle", label: "Bicycle" }, { value: "scooter", label: "Scooter" },
|
||||
{ value: "walking", label: "Walking" }, { value: "rideshare", label: "Rideshare (Uber/Lyft)" }, { value: "other", label: "Other" },
|
||||
];
|
||||
|
||||
function Field({ label, children, className = "" }: { label: string; children: React.ReactNode; className?: string }) {
|
||||
return <div className={className}><Label className="text-xs">{label}</Label>{children}</div>;
|
||||
}
|
||||
function Section({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) {
|
||||
return <section className="bg-card border rounded-lg p-5 space-y-4"><div><h2 className="font-semibold">{title}</h2>{description && <p className="text-xs text-muted-foreground mt-0.5">{description}</p>}</div>{children}</section>;
|
||||
}
|
||||
|
||||
// Top-level so the inputs don't remount / lose focus on each keystroke.
|
||||
function GuardianFields({ g, setG }: { g: typeof emptyGuardian; setG: (v: typeof emptyGuardian) => void }) {
|
||||
const f = (k: keyof typeof emptyGuardian) => (e: React.ChangeEvent<HTMLInputElement>) => setG({ ...g, [k]: e.target.value });
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<Field label="Full name" className="col-span-2 md:col-span-1"><Input value={g.full_name} onChange={f("full_name")} /></Field>
|
||||
<Field label="Relationship"><Input value={g.relationship} onChange={f("relationship")} /></Field>
|
||||
<Field label="Phone"><Input value={g.phone} onChange={f("phone")} /></Field>
|
||||
<Field label="Address" className="col-span-2 md:col-span-3"><Input value={g.address} onChange={f("address")} /></Field>
|
||||
<Field label="City"><Input value={g.city} onChange={f("city")} /></Field>
|
||||
<Field label="State"><Input value={g.state} onChange={f("state")} /></Field>
|
||||
<Field label="Zip"><Input value={g.zip} onChange={f("zip")} /></Field>
|
||||
<Field label="Email"><Input type="email" value={g.email} onChange={f("email")} /></Field>
|
||||
<Field label="Employer"><Input value={g.employer} onChange={f("employer")} /></Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IntakePage() {
|
||||
const { token } = Route.useParams();
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
const { data: info, isLoading } = useQuery({
|
||||
queryKey: ["intake-token", token],
|
||||
queryFn: async () => getIntakeToken({ data: { token } }),
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const [s, setS] = useState({
|
||||
dob: "", gender: "", grade_level: "", preferred_start_date: "", 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: "",
|
||||
});
|
||||
const set = (k: keyof typeof s) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setS((f) => ({ ...f, [k]: e.target.value }));
|
||||
const [primary, setPrimary] = useState({ ...emptyGuardian, is_primary: true });
|
||||
const [secondary, setSecondary] = useState({ ...emptyGuardian });
|
||||
const [contacts, setContacts] = useState([{ name: "", relationship: "", phone: "", alt_phone: "" }, { name: "", relationship: "", phone: "", alt_phone: "" }]);
|
||||
const [pickups, setPickups] = useState([{ name: "", relationship: "", phone: "", notes: "" }, { name: "", relationship: "", phone: "", notes: "" }, { name: "", relationship: "", phone: "", notes: "" }]);
|
||||
const [logins, setLogins] = useState([{ ...emptyLogin }]);
|
||||
const [dismissal, setDismissal] = useState<string[]>([]);
|
||||
const [agree, setAgree] = useState(false);
|
||||
|
||||
const submit = useMutation({
|
||||
mutationFn: async () => {
|
||||
const payload: IntakePayload = {
|
||||
student: { ...s, dismissal_methods: dismissal },
|
||||
guardians: [primary, secondary],
|
||||
contacts, pickups, logins,
|
||||
};
|
||||
return submitIntake({ data: { token, payload } });
|
||||
},
|
||||
onSuccess: () => setDone(true),
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
if (isLoading) return <Centered><Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /></Centered>;
|
||||
if (!info || info.status !== "ok") {
|
||||
const msg = info?.status === "used" ? "This intake link has already been used." : info?.status === "expired" ? "This intake link has expired. Please ask the school for a new one." : "This intake link is not valid.";
|
||||
return <Centered><div className="text-center max-w-sm"><h1 className="text-lg font-semibold">Link unavailable</h1><p className="text-sm text-muted-foreground mt-1">{msg}</p></div></Centered>;
|
||||
}
|
||||
if (done) return <Centered><div className="text-center max-w-sm"><CheckCircle2 className="h-10 w-10 text-green-600 mx-auto" /><h1 className="text-lg font-semibold mt-3">Thank you!</h1><p className="text-sm text-muted-foreground mt-1">{info.firstName}'s information has been submitted to Bayside Academy.</p></div></Centered>;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-muted/20">
|
||||
<div className="max-w-3xl mx-auto p-6 md:p-8 pb-24">
|
||||
<img src="/bayside-logo.png" alt="Bayside Academy" className="h-10 w-auto mb-3" />
|
||||
<h1 className="text-2xl font-semibold">Student intake</h1>
|
||||
<p className="text-muted-foreground text-sm mb-6">For <span className="font-medium">{info.firstName} {info.lastName}</span> — Bayside Academy. Please complete and submit.</p>
|
||||
|
||||
<form onSubmit={(e) => { e.preventDefault(); submit.mutate(); }} className="space-y-5">
|
||||
<Section title="Student information">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<Field label="Date of birth"><Input type="date" defaultValue={s.dob} onChange={set("dob")} /></Field>
|
||||
<Field label="Gender"><Select value={s.gender} onValueChange={(v) => setS((f) => ({ ...f, gender: v }))}><SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger><SelectContent><SelectItem value="male">Male</SelectItem><SelectItem value="female">Female</SelectItem><SelectItem value="other">Other</SelectItem></SelectContent></Select></Field>
|
||||
<Field label="Grade level applying for"><Input value={s.grade_level} onChange={set("grade_level")} /></Field>
|
||||
<Field label="Preferred start date"><Input type="date" defaultValue={s.preferred_start_date} onChange={set("preferred_start_date")} /></Field>
|
||||
</div>
|
||||
</Section>
|
||||
<Section title="Primary parent / guardian"><GuardianFields g={primary} setG={setPrimary} /></Section>
|
||||
<Section title="Secondary parent / guardian" description="Optional"><GuardianFields g={secondary} setG={setSecondary} /></Section>
|
||||
<Section title="Health">
|
||||
<Field label="Allergies (blank if none)"><Textarea rows={2} value={s.allergies} onChange={set("allergies")} /></Field>
|
||||
<Field label="Chronic conditions (blank if none)"><Textarea rows={2} value={s.chronic_conditions} onChange={set("chronic_conditions")} /></Field>
|
||||
<div className="grid grid-cols-2 gap-3"><Field label="Primary physician"><Input value={s.primary_physician} onChange={set("primary_physician")} /></Field><Field label="Physician phone"><Input value={s.physician_phone} onChange={set("physician_phone")} /></Field></div>
|
||||
</Section>
|
||||
<Section title="Emergency contacts" description="If different from the guardians above.">
|
||||
{contacts.map((c, i) => (
|
||||
<div key={i} className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Field label="Name"><Input value={c.name} onChange={(e) => setContacts(contacts.map((x, j) => j === i ? { ...x, name: e.target.value } : x))} /></Field>
|
||||
<Field label="Relationship"><Input value={c.relationship} onChange={(e) => setContacts(contacts.map((x, j) => j === i ? { ...x, relationship: e.target.value } : x))} /></Field>
|
||||
<Field label="Phone"><Input value={c.phone} onChange={(e) => setContacts(contacts.map((x, j) => j === i ? { ...x, phone: e.target.value } : x))} /></Field>
|
||||
<Field label="Alt phone"><Input value={c.alt_phone} onChange={(e) => setContacts(contacts.map((x, j) => j === i ? { ...x, alt_phone: e.target.value } : x))} /></Field>
|
||||
</div>
|
||||
))}
|
||||
</Section>
|
||||
<Section title="Authorized persons for pick-up" description="Only these individuals may pick up the student (valid ID required).">
|
||||
{pickups.map((p, i) => (
|
||||
<div key={i} className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<Field label="Name"><Input value={p.name} onChange={(e) => setPickups(pickups.map((x, j) => j === i ? { ...x, name: e.target.value } : x))} /></Field>
|
||||
<Field label="Relationship"><Input value={p.relationship} onChange={(e) => setPickups(pickups.map((x, j) => j === i ? { ...x, relationship: e.target.value } : x))} /></Field>
|
||||
<Field label="Phone"><Input value={p.phone} onChange={(e) => setPickups(pickups.map((x, j) => j === i ? { ...x, phone: e.target.value } : x))} /></Field>
|
||||
<Field label="Notes / restrictions"><Input value={p.notes} onChange={(e) => setPickups(pickups.map((x, j) => j === i ? { ...x, notes: e.target.value } : x))} /></Field>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" size="sm" variant="outline" onClick={() => setPickups([...pickups, { name: "", relationship: "", phone: "", notes: "" }])}><Plus className="h-4 w-4 mr-1" /> Add another</Button>
|
||||
</Section>
|
||||
<Section title="Academics">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Curriculum platform logins</Label>
|
||||
{logins.map((l, i) => (
|
||||
<div key={i} className="grid grid-cols-2 md:grid-cols-3 gap-2 border rounded-md p-3">
|
||||
<Field label="Website"><Input value={l.website} onChange={(e) => setLogins(logins.map((x, j) => j === i ? { ...x, website: e.target.value } : x))} /></Field>
|
||||
<Field label="Student login"><Input value={l.student_login} onChange={(e) => setLogins(logins.map((x, j) => j === i ? { ...x, student_login: e.target.value } : x))} /></Field>
|
||||
<Field label="Student password"><Input value={l.student_password} onChange={(e) => setLogins(logins.map((x, j) => j === i ? { ...x, student_password: e.target.value } : x))} /></Field>
|
||||
<Field label="Parent account"><Input value={l.parent_account} onChange={(e) => setLogins(logins.map((x, j) => j === i ? { ...x, parent_account: e.target.value } : x))} /></Field>
|
||||
<Field label="Parent password"><Input value={l.parent_password} onChange={(e) => setLogins(logins.map((x, j) => j === i ? { ...x, parent_password: e.target.value } : x))} /></Field>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" size="sm" variant="outline" onClick={() => setLogins([...logins, { ...emptyLogin }])}><Plus className="h-4 w-4 mr-1" /> Add platform</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3"><Field label="Home Ed. Annual Evaluation due to BPS"><Input type="date" defaultValue={s.home_ed_eval_due} onChange={set("home_ed_eval_due")} /></Field><Field label="Previous school(s)"><Input value={s.previous_schools} onChange={set("previous_schools")} /></Field></div>
|
||||
<Field label="Special academic needs / accommodations"><Textarea rows={2} value={s.special_needs} onChange={set("special_needs")} /></Field>
|
||||
<Field label="Who keeps the portfolio"><Select value={s.portfolio_keeper} onValueChange={(v) => setS((f) => ({ ...f, portfolio_keeper: v }))}><SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger><SelectContent><SelectItem value="parent">Parent/Guardian</SelectItem><SelectItem value="bayside">Bayside Academy</SelectItem></SelectContent></Select></Field>
|
||||
<Field label="Disciplinary history (blank if none)"><Textarea rows={2} value={s.disciplinary_history} onChange={set("disciplinary_history")} /></Field>
|
||||
<Field label="Custody agreement (blank if none)"><Textarea rows={2} value={s.custody_agreement} onChange={set("custody_agreement")} /></Field>
|
||||
</Section>
|
||||
<Section title="Independent dismissal" description="Check all methods by which the student may leave campus independently.">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">{DISMISSAL.map((o) => <label key={o.value} className="flex items-center gap-2 text-sm"><Checkbox checked={dismissal.includes(o.value)} onCheckedChange={() => setDismissal((d) => d.includes(o.value) ? d.filter((x) => x !== o.value) : [...d, o.value])} />{o.label}</label>)}</div>
|
||||
{dismissal.includes("other") && <Field label="Other (describe)"><Input value={s.dismissal_other} onChange={set("dismissal_other")} /></Field>}
|
||||
<Field label="Backup transportation plan"><Textarea rows={2} value={s.backup_transport_plan} onChange={set("backup_transport_plan")} /></Field>
|
||||
</Section>
|
||||
<Section title="Additional information">
|
||||
<Field label="Interests / hobbies / activities"><Textarea rows={2} value={s.interests} onChange={set("interests")} /></Field>
|
||||
<Field label="Anything else we should know"><Textarea rows={2} value={s.other_info} onChange={set("other_info")} /></Field>
|
||||
</Section>
|
||||
<Section title="Agreement & signature">
|
||||
<label className="flex items-start gap-2 text-sm"><Checkbox checked={agree} onCheckedChange={(v) => setAgree(!!v)} className="mt-0.5" /><span>I certify that the information provided is accurate and complete to the best of my knowledge.</span></label>
|
||||
<div className="grid grid-cols-2 gap-3"><Field label="Signed by (parent/guardian name)"><Input value={s.agreement_signed_by} onChange={set("agreement_signed_by")} /></Field><Field label="Date"><Input type="date" defaultValue={s.agreement_signed_date} onChange={set("agreement_signed_date")} /></Field></div>
|
||||
</Section>
|
||||
|
||||
<div className="sticky bottom-0 -mx-6 md:-mx-8 px-6 md:px-8 py-3 bg-background/90 backdrop-blur border-t flex items-center gap-3">
|
||||
<Button type="submit" disabled={!agree || submit.isPending}>{submit.isPending ? <><Loader2 className="h-4 w-4 mr-1 animate-spin" /> Submitting…</> : "Submit intake"}</Button>
|
||||
{!agree && <span className="text-xs text-muted-foreground">Check the agreement box to submit.</span>}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Centered({ children }: { children: React.ReactNode }) {
|
||||
return <div className="min-h-screen flex items-center justify-center bg-muted/20 p-6">{children}</div>;
|
||||
}
|
||||
Reference in New Issue
Block a user