diff --git a/src/routes/_authenticated/billing.tsx b/src/routes/_authenticated/billing.tsx deleted file mode 100644 index 667d611..0000000 --- a/src/routes/_authenticated/billing.tsx +++ /dev/null @@ -1,402 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { supabase } from "@/integrations/supabase/client"; -import { useAuth, canBill } from "@/hooks/use-auth"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { money } from "@/lib/reports"; -import { Plus, Receipt, Eye, CheckCircle2, Loader2, AlertTriangle } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; - -export const Route = createFileRoute("/_authenticated/billing")({ - head: () => ({ meta: [{ title: "Billing — School Portal" }] }), - component: BillingPage, -}); - -type PreviewRow = { - student_id: string; - student_name: string; - campus_name: string; - scheduled_days: number; - subtotal_cents: number; - total_cents: number; -}; - -/** Monday–Sunday of the current week, as ISO dates. */ -function currentWeek(): { from: string; to: string } { - const now = new Date(); - const day = (now.getDay() + 6) % 7; // 0 = Monday - const mon = new Date(now); - mon.setDate(now.getDate() - day); - const sun = new Date(mon); - sun.setDate(mon.getDate() + 6); - const iso = (d: Date) => d.toISOString().slice(0, 10); - return { from: iso(mon), to: iso(sun) }; -} - -function BillingPage() { - const { user, roles } = useAuth(); - const qc = useQueryClient(); - const allowed = canBill(roles); - - const { data: campuses } = useQuery({ - queryKey: ["campuses"], - queryFn: async () => - (await supabase.from("campuses").select("id, name").order("name")).data ?? [], - }); - const { data: tiers } = useQuery({ - queryKey: ["tuition-tiers"], - queryFn: async () => - (await supabase.from("tuition_tiers").select("id, name").order("sort_order")).data ?? [], - }); - const { data: rates } = useQuery({ - queryKey: ["tuition-rates"], - enabled: allowed, - queryFn: async () => - ( - await supabase - .from("tuition_rates") - .select( - "id, amount_cents, rate_basis, attendance_basis, is_active, campuses(name), tuition_tiers(name)", - ) - .order("created_at", { ascending: false }) - ).data ?? [], - }); - const { data: batches } = useQuery({ - queryKey: ["invoice-batches"], - enabled: allowed, - queryFn: async () => - ( - await supabase - .from("invoice_batches") - .select( - "id, billing_period_start, billing_period_end, status, student_count, total_cents, confirmed_at", - ) - .order("created_at", { ascending: false }) - .limit(10) - ).data ?? [], - }); - - // ---- rate form ---- - const [rCampus, setRCampus] = useState(""); - const [rTier, setRTier] = useState(""); - const [rBasis, setRBasis] = useState("per_day"); - const [rAttendance, setRAttendance] = useState("part_time"); - const [rAmount, setRAmount] = useState(""); - - const addRate = useMutation({ - mutationFn: async () => { - const dollars = Number(rAmount); - if (!Number.isFinite(dollars) || dollars < 0) throw new Error("Enter a valid amount"); - const { error } = await supabase.from("tuition_rates").insert({ - campus_id: rCampus || null, - tier_id: rTier || null, - rate_basis: rBasis, - attendance_basis: rAttendance || null, - amount_cents: Math.round(dollars * 100), - priority: 200, - }); - if (error) throw error; - }, - onSuccess: () => { - setRAmount(""); - qc.invalidateQueries({ queryKey: ["tuition-rates"] }); - toast.success("Rate added"); - }, - onError: (e: Error) => toast.error(e.message), - }); - - // ---- batch flow ---- - const wk = currentWeek(); - const [from, setFrom] = useState(wk.from); - const [to, setTo] = useState(wk.to); - const [preview, setPreview] = useState<{ batchId: string; rows: PreviewRow[] } | null>(null); - - const createBatch = useMutation({ - mutationFn: async () => { - const { data, error } = await supabase - .from("invoice_batches") - .insert({ - billing_period_start: from, - billing_period_end: to, - criteria: {}, - created_by: user?.id ?? null, - }) - .select("id") - .single(); - if (error) throw error; - return data.id as string; - }, - onSuccess: (id) => { - qc.invalidateQueries({ queryKey: ["invoice-batches"] }); - toast.success("Batch created — preview it before issuing"); - runPreview.mutate(id); - }, - onError: (e: Error) => toast.error(e.message), - }); - - const runPreview = useMutation({ - mutationFn: async (batchId: string) => { - const { data, error } = await supabase.rpc("preview_invoice_batch", { _batch: batchId }); - if (error) throw error; - return { batchId, rows: (data ?? []) as PreviewRow[] }; - }, - onSuccess: (r) => { - setPreview(r); - qc.invalidateQueries({ queryKey: ["invoice-batches"] }); - }, - onError: (e: Error) => toast.error(e.message), - }); - - // The spec requires explicit administrative confirmation before bulk - // issuance; generate_invoice_batch() refuses without confirmed_at set. - const confirmAndIssue = useMutation({ - mutationFn: async (batchId: string) => { - const { error: upErr } = await supabase - .from("invoice_batches") - .update({ confirmed_at: new Date().toISOString(), confirmed_by: user?.id ?? null }) - .eq("id", batchId); - if (upErr) throw upErr; - const { data, error } = await supabase.rpc("generate_invoice_batch", { _batch: batchId }); - if (error) throw error; - return data as number; - }, - onSuccess: (n) => { - setPreview(null); - qc.invalidateQueries({ queryKey: ["invoice-batches"] }); - toast.success(`${n} invoice${n === 1 ? "" : "s"} issued`); - }, - onError: (e: Error) => toast.error(e.message), - }); - - if (!allowed) { - return ( -
You don't have billing access.
-Tuition rates and weekly invoice runs.
- - {/* ---------- rates ---------- */} -enrollment_statusis enrolled and they
- have a campus schedule row covering this period. Newly added columns default to
- prospective, so existing students need updating first.
- | Student | -Campus | -Days | -Subtotal | -Total | -
|---|---|---|---|---|
| {r.student_name} | -{r.campus_name ?? "—"} | -{Number(r.scheduled_days).toFixed(1)} | -{money(Number(r.subtotal_cents))} | -{money(Number(r.total_cents))} | -