From c10c8727323b6edd54de71c85bb4f79ebc1dcc14 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 22 Aug 2026 23:10:21 +0000 Subject: [PATCH] Terminal --- src/routes/_authenticated/invoice.$id.tsx | 288 ---------------------- 1 file changed, 288 deletions(-) delete mode 100644 src/routes/_authenticated/invoice.$id.tsx diff --git a/src/routes/_authenticated/invoice.$id.tsx b/src/routes/_authenticated/invoice.$id.tsx deleted file mode 100644 index ca6054e..0000000 --- a/src/routes/_authenticated/invoice.$id.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { createFileRoute, Link } from "@tanstack/react-router"; -import { useQuery } 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 { ArrowLeft, Loader2, Mail, Printer } from "lucide-react"; -import { useState } from "react"; -import { DocHeader, SignatureLines } from "@/components/reports"; -import { SCHOOL_NAME, money, monthToDateRange, schoolYearRange } from "@/lib/reports"; -import { formatDate } from "@/lib/plans"; - -export const Route = createFileRoute("/_authenticated/invoice/$id")({ - head: () => ({ meta: [{ title: "Invoice — School Portal" }] }), - component: InvoicePage, -}); - -const CATEGORY_LABEL: Record = { - tuition: "Tuition", - late_pickup: "Late pickup", - activity: "Activity", - other: "Other", -}; - -function InvoicePage() { - const { id } = Route.useParams(); - const initial = monthToDateRange(); - const [from, setFrom] = useState(initial.from); - const [to, setTo] = useState(initial.to); - - const { data: student } = useQuery({ - queryKey: ["invoice-student", id], - queryFn: async () => - ( - await supabase - .from("students") - .select("id, first_name, last_name, grade_level, daily_tuition_cents, classes(name)") - .eq("id", id) - .single() - ).data, - }); - - // Every entry, so the opening balance can be derived from activity before `from`. - const { data: entries } = useQuery({ - queryKey: ["invoice-entries", id], - queryFn: async () => - ( - await supabase - .from("ledger_entries") - .select("id, date, kind, category, amount_cents, note, attendance_id") - .eq("student_id", id) - .order("date") - ).data ?? [], - }); - - // Guardians from the intake form, plus any linked parent login accounts. - const { data: recipients } = useQuery({ - queryKey: ["invoice-recipients", id], - queryFn: async () => { - const [guardians, links] = await Promise.all([ - supabase.from("student_guardians").select("full_name, email").eq("student_id", id), - supabase.from("parent_students").select("parent_id").eq("student_id", id), - ]); - const emails = new Map(); - for (const g of guardians.data ?? []) - if (g.email) emails.set(g.email.toLowerCase(), g.full_name ?? g.email); - const ids = (links.data ?? []).map((l) => l.parent_id); - if (ids.length) { - // profiles is admin-readable only; non-admins simply get fewer recipients. - const { data: profs } = await supabase - .from("profiles") - .select("full_name, email") - .in("id", ids); - for (const p of profs ?? []) - if (p.email) emails.set(p.email.toLowerCase(), p.full_name ?? p.email); - } - return [...emails.entries()].map(([email, name]) => ({ email, name })); - }, - }); - - if (!student) { - return ( -
- Loading invoice… -
- ); - } - - const all = entries ?? []; - const signed = (e: { kind: string; amount_cents: number }) => - e.kind === "charge" ? e.amount_cents : -e.amount_cents; - - const opening = all.filter((e) => e.date < from).reduce((n, e) => n + signed(e), 0); - const period = all.filter((e) => e.date >= from && e.date <= to); - const charges = period.filter((e) => e.kind === "charge").reduce((n, e) => n + e.amount_cents, 0); - const payments = period - .filter((e) => e.kind === "payment") - .reduce((n, e) => n + e.amount_cents, 0); - const balanceDue = opening + charges - payments; - - const name = `${student.first_name} ${student.last_name}`; - const lines = period - .map( - (e) => - `${formatDate(e.date)} ${CATEGORY_LABEL[e.category] ?? e.category}${e.note ? ` — ${e.note}` : ""} ${ - e.kind === "charge" ? money(e.amount_cents) : `(${money(e.amount_cents)})` - }`, - ) - .join("\n"); - - // mailto: is the same mechanism the intake-link share uses — it opens the - // staff member's own mail client with a prefilled draft. It cannot attach a - // file, so the itemisation goes in the body as text. - const mailto = - `mailto:${(recipients ?? []).map((r) => r.email).join(",")}` + - `?subject=${encodeURIComponent(`${SCHOOL_NAME} — invoice for ${name}`)}` + - `&body=${encodeURIComponent( - `Hello,\n\nHere is the tuition statement for ${name} covering ${formatDate(from)} to ${formatDate(to)}.\n\n` + - `Balance carried forward: ${money(opening)}\n` + - `${lines ? `${lines}\n\n` : "\n"}` + - `Charges this period: ${money(charges)}\n` + - `Payments this period: ${money(payments)}\n` + - `Balance due: ${money(balanceDue)}\n\n` + - `Please reply to this email with any questions.\n\nThank you,\n${SCHOOL_NAME}`, - )}`; - - return ( -
-
- - Tuition ledger - -
-
- - setFrom(e.target.value)} - /> -
-
- - setTo(e.target.value)} - /> -
- -
- - {(recipients ?? []).length > 0 ? ( - - - - ) : ( - - )} -
- {(recipients ?? []).length > 0 && ( -

- Will draft to: {(recipients ?? []).map((r) => r.email).join(", ")} -

- )} -
- -
- - -
- {[ - ["Student", name], - ["Grade level", student.grade_level || "—"], - ["Class", (student.classes as { name: string } | null)?.name ?? "—"], - [ - "Daily rate", - student.daily_tuition_cents ? money(student.daily_tuition_cents) : "Not set", - ], - ].map(([k, v]) => ( -
- {k} - {v} -
- ))} -
- -
-

Account activity

- - - - - - - - - - - - - - - - - {period.map((e) => ( - - - - - - - ))} - {period.length === 0 && ( - - - - )} - - - - - - - - - - - - - - - -
DateDescriptionChargePayment
{formatDate(from)}Balance carried forward{opening !== 0 ? money(opening) : "—"}—
{formatDate(e.date)} - {CATEGORY_LABEL[e.category] ?? e.category} - {e.note ? — {e.note} : null} - - {e.kind === "charge" ? money(e.amount_cents) : "—"} - - {e.kind === "payment" ? money(e.amount_cents) : "—"} -
- No activity in this period. -
- Charges this period - {money(charges)} -
- Payments this period - - {money(payments)}
- Balance due - - {money(balanceDue)} -
- {balanceDue < 0 && ( -

- A negative balance is a credit on the account. -

- )} -
- - -
-
- ); -}