Modified by www.SourceFiles.app
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
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 { ChevronRight, FileText } from "lucide-react";
|
||||||
|
import { money } from "@/lib/reports";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_authenticated/ledger")({
|
||||||
|
head: () => ({ meta: [{ title: "Tuition — School Portal" }] }),
|
||||||
|
component: LedgerListPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function LedgerListPage() {
|
||||||
|
const { data: students } = useQuery({
|
||||||
|
queryKey: ["students-with-balance"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data: studs } = await supabase
|
||||||
|
.from("students")
|
||||||
|
.select("id, first_name, last_name, daily_tuition_cents");
|
||||||
|
const { data: entries } = await supabase
|
||||||
|
.from("ledger_entries")
|
||||||
|
.select("student_id, amount_cents, kind");
|
||||||
|
const balances: Record<string, number> = {};
|
||||||
|
(entries ?? []).forEach((e) => {
|
||||||
|
balances[e.student_id] =
|
||||||
|
(balances[e.student_id] ?? 0) + (e.kind === "charge" ? e.amount_cents : -e.amount_cents);
|
||||||
|
});
|
||||||
|
return (studs ?? []).map((s) => ({ ...s, balance: balances[s.id] ?? 0 }));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 max-w-4xl">
|
||||||
|
<h1 className="text-2xl font-semibold mb-1">Tuition ledger</h1>
|
||||||
|
<p className="text-muted-foreground text-sm mb-6">
|
||||||
|
Click a student to view and manage their ledger, or build an invoice. Students with a daily
|
||||||
|
rate are charged automatically when marked present or late.
|
||||||
|
</p>
|
||||||
|
<div className="bg-card border rounded-lg divide-y">
|
||||||
|
{(students ?? []).map((s) => (
|
||||||
|
<div key={s.id} className="flex items-center gap-3 p-3 hover:bg-muted/50">
|
||||||
|
<Link
|
||||||
|
to="/students/$id"
|
||||||
|
params={{ id: s.id }}
|
||||||
|
className="flex-1 min-w-0 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span className="font-medium truncate">
|
||||||
|
{s.last_name}, {s.first_name}
|
||||||
|
</span>
|
||||||
|
{s.daily_tuition_cents ? (
|
||||||
|
<span className="text-xs text-muted-foreground shrink-0">
|
||||||
|
{money(s.daily_tuition_cents)}/day
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground shrink-0">no rate</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<span
|
||||||
|
className={`text-sm font-medium shrink-0 ${s.balance > 0 ? "text-destructive" : "text-green-700"}`}
|
||||||
|
>
|
||||||
|
{money(s.balance)}
|
||||||
|
</span>
|
||||||
|
<Link to="/invoice/$id" params={{ id: s.id }} className="shrink-0">
|
||||||
|
<Button size="sm" variant="outline">
|
||||||
|
<FileText className="h-4 w-4 mr-1" /> Invoice
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Link to="/students/$id" params={{ id: s.id }} className="shrink-0">
|
||||||
|
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{students?.length === 0 && (
|
||||||
|
<div className="p-6 text-center text-sm text-muted-foreground">No students.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user