From 7b7db7cd738396f78e93440975fe6a729e7cdc61 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 01:13:55 +0000 Subject: [PATCH 1/2] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/index.tsx | 297 +++++++++++++++++++++++++++++++++---------- 1 file changed, 232 insertions(+), 65 deletions(-) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index cabc5e1..82274b6 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,14 +1,18 @@ import { createFileRoute, Link } from "@tanstack/react-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { ProtectedLayout } from "@/components/protected-layout"; import { PageContainer, PageHeader } from "@/components/app-shell"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; -import { Briefcase, Users, Receipt, Clock, ArrowRight } from "lucide-react"; +import { + Briefcase, Receipt, Clock, ArrowRight, CalendarDays, Plus, FileText, + CheckSquare, UserPlus, DollarSign, Wallet, AlertCircle, Activity, +} from "lucide-react"; import { formatCurrency, formatDate, statusBadgeClass } from "@/lib/format"; -import { Badge } from "@/components/ui/badge"; +import { differenceInCalendarDays, format, startOfDay, startOfMonth, endOfMonth, subDays } from "date-fns"; export const Route = createFileRoute("/")({ component: () => ( @@ -18,78 +22,253 @@ export const Route = createFileRoute("/")({ ), }); -interface Stats { - activeCases: number; - clients: number; - unpaidInvoices: number; - unbilledHours: number; +interface Financial { + trustBalance: number; + paidThisMonth: number; + overdueTotal: number; + unsentTotal: number; +} +interface MyTaskCounts { + dueToday: number; + overdue: number; + incomplete: number; +} +interface OpenCases { + open: number; + newLast30: number; + closedLast30: number; } function Dashboard() { const { user } = useAuth(); - const [stats, setStats] = useState({ activeCases: 0, clients: 0, unpaidInvoices: 0, unbilledHours: 0 }); + const [financial, setFinancial] = useState({ trustBalance: 0, paidThisMonth: 0, overdueTotal: 0, unsentTotal: 0 }); + const [myTasks, setMyTasks] = useState({ dueToday: 0, overdue: 0, incomplete: 0 }); + const [openCases, setOpenCases] = useState({ open: 0, newLast30: 0, closedLast30: 0 }); + const [todaysEvents, setTodaysEvents] = useState([]); const [recentCases, setRecentCases] = useState([]); const [recentInvoices, setRecentInvoices] = useState([]); + const [loading, setLoading] = useState(true); useEffect(() => { + if (!user?.id) return; (async () => { - const [casesRes, clientsRes, invoicesRes, timeRes, recentCasesRes, recentInvRes] = await Promise.all([ + setLoading(true); + const today = startOfDay(new Date()); + const todayStr = format(today, "yyyy-MM-dd"); + const monthStart = format(startOfMonth(today), "yyyy-MM-dd"); + const monthEnd = format(endOfMonth(today), "yyyy-MM-dd"); + const thirtyAgo = format(subDays(today, 30), "yyyy-MM-dd"); + + const [ + trustRes, paymentsRes, invoicesRes, + myAssigneeRes, + openCasesRes, newCasesRes, closedCasesRes, + tasksTodayRes, hearingsTodayRes, + recentCasesRes, recentInvRes, + ] = await Promise.all([ + supabase.from("trust_ledger_entries").select("entry_type, amount"), + supabase.from("invoice_payments").select("amount, paid_on").gte("paid_on", monthStart).lte("paid_on", monthEnd), + supabase.from("invoices").select("id, total, amount_paid, status, due_date"), + supabase.from("task_assignees").select("task_id, task:tasks(id, status, due_date)").eq("user_id", user.id), supabase.from("cases").select("id", { count: "exact", head: true }).in("status", ["intake", "active", "on_hold"]), - supabase.from("clients").select("id", { count: "exact", head: true }), - supabase.from("invoices").select("total, amount_paid").in("status", ["sent", "overdue"]), - supabase.from("time_entries").select("hours").is("invoice_id", null).eq("billable", true), + supabase.from("cases").select("id", { count: "exact", head: true }).gte("opened_at", thirtyAgo), + supabase.from("cases").select("id", { count: "exact", head: true }).gte("closed_at", thirtyAgo).not("closed_at", "is", null), + supabase.from("tasks").select("id, title, due_date, case_id, case:cases(case_number, title)").eq("status", "incomplete").eq("due_date", todayStr), + supabase.from("cases").select("id, title, case_number, next_hearing_date, next_hearing_notes").eq("next_hearing_date", todayStr), supabase.from("cases").select("id, case_number, title, status, updated_at, client:clients(name)").order("updated_at", { ascending: false }).limit(5), supabase.from("invoices").select("id, invoice_number, total, status, issue_date, client:clients(name)").order("created_at", { ascending: false }).limit(5), ]); - const unpaid = (invoicesRes.data ?? []).reduce( - (sum, i) => sum + (Number(i.total) - Number(i.amount_paid)), - 0, - ); - const hours = (timeRes.data ?? []).reduce((s, t) => s + Number(t.hours), 0); + // Trust balance + const trustBalance = (trustRes.data ?? []).reduce((s, e: any) => { + return s + (e.entry_type === "deposit" ? Number(e.amount) : -Number(e.amount)); + }, 0); - setStats({ - activeCases: casesRes.count ?? 0, - clients: clientsRes.count ?? 0, - unpaidInvoices: unpaid, - unbilledHours: hours, + // Paid this month + const paidThisMonth = (paymentsRes.data ?? []).reduce((s, p: any) => s + Number(p.amount), 0); + + // Overdue + unsent totals (remaining) + let overdueTotal = 0; + let unsentTotal = 0; + for (const inv of (invoicesRes.data ?? []) as any[]) { + const remaining = Number(inv.total) - Number(inv.amount_paid); + if (remaining <= 0) continue; + if (inv.status === "draft") unsentTotal += remaining; + if (inv.due_date && inv.due_date < todayStr && inv.status !== "paid") overdueTotal += remaining; + } + + // My tasks + const myTaskRows = (myAssigneeRes.data ?? []).map((r: any) => r.task).filter(Boolean); + let dueToday = 0, overdue = 0, incomplete = 0; + for (const t of myTaskRows) { + if (t.status !== "incomplete") continue; + incomplete++; + if (t.due_date) { + if (t.due_date === todayStr) dueToday++; + else if (t.due_date < todayStr) overdue++; + } + } + + // Today's events: combine tasks + hearings + const evts: any[] = [ + ...((tasksTodayRes.data ?? []) as any[]).map((t) => ({ + id: `task-${t.id}`, kind: "task", title: t.title, + subtitle: t.case ? `${t.case.case_number} · ${t.case.title}` : null, + to: t.case_id ? `/cases/${t.case_id}` : "/tasks", + })), + ...((hearingsTodayRes.data ?? []) as any[]).map((c) => ({ + id: `hearing-${c.id}`, kind: "hearing", title: `Hearing: ${c.title}`, + subtitle: c.case_number, to: `/cases/${c.id}`, + })), + ]; + + setFinancial({ trustBalance, paidThisMonth, overdueTotal, unsentTotal }); + setMyTasks({ dueToday, overdue, incomplete }); + setOpenCases({ + open: openCasesRes.count ?? 0, + newLast30: newCasesRes.count ?? 0, + closedLast30: closedCasesRes.count ?? 0, }); + setTodaysEvents(evts); setRecentCases(recentCasesRes.data ?? []); setRecentInvoices(recentInvRes.data ?? []); + setLoading(false); })(); }, [user?.id]); - const cards = [ - { label: "Active cases", value: stats.activeCases, icon: Briefcase, to: "/cases" }, - { label: "Clients", value: stats.clients, icon: Users, to: "/clients" }, - { label: "Unbilled hours", value: stats.unbilledHours.toFixed(1), icon: Clock, to: "/cases" }, - { label: "Outstanding A/R", value: formatCurrency(stats.unpaidInvoices), icon: Receipt, to: "/invoices" }, + const quickActions = useMemo(() => [ + { label: "Add task", icon: CheckSquare, to: "/tasks" }, + { label: "Add case", icon: Briefcase, to: "/cases/new" }, + { label: "Add contact", icon: UserPlus, to: "/contacts" }, + { label: "Create invoice", icon: Receipt, to: "/invoices" }, + { label: "Add time", icon: Clock, to: "/tasks" }, + { label: "Add expense", icon: DollarSign, to: "/cases" }, + ], []); + + const financialCards = [ + { label: "Trust account balance", value: formatCurrency(financial.trustBalance), tone: "bg-muted/40", icon: Wallet, to: "/clients" }, + { label: "Invoices paid this mo.", value: formatCurrency(financial.paidThisMonth), tone: "bg-emerald-500/10", icon: Receipt, to: "/invoices" }, + { label: "Overdue invoice total", value: formatCurrency(financial.overdueTotal), tone: "bg-destructive/10", icon: AlertCircle, to: "/invoices" }, + { label: "Unsent invoice total", value: formatCurrency(financial.unsentTotal), tone: "bg-muted/40", icon: FileText, to: "/invoices" }, ]; return ( -
- {cards.map((c) => ( - - - -
-
{c.label}
- -
-
{c.value}
-
-
- - ))} + {/* Quick actions */} + + +
Quick actions
+
+ {quickActions.map((a) => ( + + ))} +
+
+
+ + {/* Financial + My tasks */} +
+ + + Financial overview + + +
+ {financialCards.map((c) => ( + +
+
{c.label}
+
{loading ? "—" : c.value}
+
+ + ))} +
+
+
+ + + + My tasks + + +
+ +
Due today
+
{myTasks.dueToday}
+ + +
Overdue
+
{myTasks.overdue}
+ + +
Incomplete
+
{myTasks.incomplete}
+ +
+
+
-
- + {/* Open cases + Today's events */} +
+ - Recent cases + Open cases + + + +
{openCases.open}
+
+
+ New cases in last 30 days + {openCases.newLast30} +
+
+ Cases closed in last 30 days + {openCases.closedLast30} +
+
+
+
+ + + + Today's events + + + + {todaysEvents.length === 0 ? ( +

Nothing scheduled today.

+ ) : ( +
+ {todaysEvents.map((e) => ( + + +
+
{e.title}
+ {e.subtitle &&
{e.subtitle}
} +
+ + ))} +
+ )} +
+
+
+ + {/* Recent cases & invoices */} +
+ + + Recent cases @@ -97,17 +276,11 @@ function Dashboard() { {recentCases.length === 0 &&

No cases yet.

} {recentCases.map((c) => ( - +
{c.title}
-
- {c.case_number} · {c.client?.name} -
+
{c.case_number} · {c.client?.name}
{c.status.replace("_", " ")} @@ -115,9 +288,9 @@ function Dashboard() {
- + - Recent invoices + Recent invoices @@ -125,17 +298,11 @@ function Dashboard() { {recentInvoices.length === 0 &&

No invoices yet.

} {recentInvoices.map((i) => ( - +
{i.invoice_number}
-
- {i.client?.name} · {formatDate(i.issue_date)} -
+
{i.client?.name} · {formatDate(i.issue_date)}
{formatCurrency(i.total)} From a5b583691d412a1061038fe421f59bf032a1f840 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 18 Apr 2026 01:14:06 +0000 Subject: [PATCH 2/2] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 82274b6..86ec365 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -8,11 +8,11 @@ import { Badge } from "@/components/ui/badge"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/lib/auth"; import { - Briefcase, Receipt, Clock, ArrowRight, CalendarDays, Plus, FileText, - CheckSquare, UserPlus, DollarSign, Wallet, AlertCircle, Activity, + Briefcase, Receipt, Clock, ArrowRight, CalendarDays, FileText, + CheckSquare, UserPlus, DollarSign, Wallet, AlertCircle, } from "lucide-react"; import { formatCurrency, formatDate, statusBadgeClass } from "@/lib/format"; -import { differenceInCalendarDays, format, startOfDay, startOfMonth, endOfMonth, subDays } from "date-fns"; +import { format, startOfDay, startOfMonth, endOfMonth, subDays } from "date-fns"; export const Route = createFileRoute("/")({ component: () => (