Add admin-emailed one-time intake links
- intake_tokens table (server-only via service role) - Server functions: createIntakeToken (admin), getIntakeToken (public validate), submitIntake (public write + mark used, 14-day one-time tokens) - Public /intake/$token full intake form (no login) with valid/used/expired states - Student profile (admin): generate link, copy, and email-to-parent (mailto) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,11 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
import { ArrowLeft, Trash2, Plus, FileText, Loader2, ImageUp, User, Pencil, Check, UserPlus, Copy } from "lucide-react";
|
||||
import { ArrowLeft, Trash2, Plus, FileText, Loader2, ImageUp, User, Pencil, Check, UserPlus, Copy, Link2, Mail } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { createUserFn } from "@/lib/user-admin.functions";
|
||||
import { createIntakeToken } from "@/lib/intake.functions";
|
||||
import { genTempPassword } from "@/lib/temp-password";
|
||||
import { StudentGradeReport } from "@/components/gradebook";
|
||||
|
||||
@@ -323,6 +324,43 @@ function FamilyTab({ studentId, canEdit, isAdmin }: { studentId: string; canEdit
|
||||
<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} editing={editing} kind="emergency" title="Emergency contacts" subtitle="Contacted if guardians are unreachable." />
|
||||
{isAdmin && <ParentAccessSection studentId={studentId} />}
|
||||
{isAdmin && <IntakeLinkSection studentId={studentId} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IntakeLinkSection({ studentId }: { studentId: string }) {
|
||||
const { data: student } = useQuery({
|
||||
queryKey: ["student-name", studentId],
|
||||
queryFn: async () => (await supabase.from("students").select("first_name, last_name").eq("id", studentId).maybeSingle()).data,
|
||||
});
|
||||
const name = student ? `${student.first_name} ${student.last_name}` : "this student";
|
||||
const [link, setLink] = useState<string | null>(null);
|
||||
const gen = useMutation({
|
||||
mutationFn: async () => {
|
||||
const { token } = await createIntakeToken({ data: { studentId } });
|
||||
return `${window.location.origin}/intake/${token}`;
|
||||
},
|
||||
onSuccess: (url) => { setLink(url); toast.success("Intake link created"); },
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
const mailto = link
|
||||
? `mailto:?subject=${encodeURIComponent(`Student intake for ${name} — Bayside Academy`)}&body=${encodeURIComponent(`Please complete the student intake form for ${name} using this one-time link (valid 14 days):\n\n${link}\n\nThank you,\nBayside Academy`)}`
|
||||
: "#";
|
||||
return (
|
||||
<div className="space-y-3 border-t pt-6">
|
||||
<div><h3 className="font-semibold flex items-center gap-2"><Link2 className="h-4 w-4" /> Intake form link</h3><p className="text-xs text-muted-foreground">Generate a one-time link a parent can use to fill out this student's intake — no login required, valid 14 days.</p></div>
|
||||
<Button size="sm" variant="outline" onClick={() => gen.mutate()} disabled={gen.isPending}>{gen.isPending ? "Creating…" : link ? "Generate a new link" : "Generate intake link"}</Button>
|
||||
{link && (
|
||||
<div className="rounded-md border border-primary/30 bg-primary/5 p-3 text-sm space-y-2">
|
||||
<div className="font-mono text-xs break-all">{link}</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => { navigator.clipboard.writeText(link); toast.success("Copied"); }}><Copy className="h-4 w-4 mr-1" /> Copy link</Button>
|
||||
<a href={mailto}><Button size="sm"><Mail className="h-4 w-4 mr-1" /> Email to parent</Button></a>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Note: this link lets whoever opens it fill out {name}'s intake once. Share it only with the parent.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user