import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { useEffect, useState } from "react"; import { ProtectedLayout } from "@/components/protected-layout"; import { PageContainer, PageHeader } from "@/components/app-shell"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { supabase } from "@/integrations/supabase/client"; import { ClientFormDialog } from "@/components/clients/client-form-dialog"; import { useAuth } from "@/lib/auth"; import { ArrowLeft, Edit, Plus, Building2, User, MapPin, Mail, Phone, Users, Activity, Briefcase, FileDown, Contact, Receipt, Archive, ArchiveRestore, ListPlus, Save } from "lucide-react"; import { ContactsLinkTab } from "@/components/contacts/contacts-link-tab"; import { ClientCustomFieldsTab } from "@/components/clients/client-custom-fields-tab"; import { formatCurrency, formatDate, formatDateTime, statusBadgeClass } from "@/lib/format"; import { toast } from "sonner"; import { downloadStatusReport, saveStatusReportToDb } from "@/lib/status-pdf"; import { TrustAccountPanel } from "@/components/trust/trust-account-panel"; import { setArchived } from "@/lib/archive"; export const Route = createFileRoute("/clients/$clientId")({ component: () => ( ), }); function ClientDetail() { const { clientId } = Route.useParams(); const navigate = useNavigate(); const { user, isAdmin } = useAuth(); const [client, setClient] = useState(null); const [cases, setCases] = useState([]); const [statusEntries, setStatusEntries] = useState([]); const [loading, setLoading] = useState(true); const [editOpen, setEditOpen] = useState(false); const load = async () => { setLoading(true); const [{ data: c, error: ce }, { data: cs }] = await Promise.all([ supabase.from("clients").select("*").eq("id", clientId).maybeSingle(), supabase.from("cases").select("id, case_number, title, status, opened_at").eq("client_id", clientId).order("opened_at", { ascending: false }), ]); if (ce) toast.error("Failed to load client", { description: ce.message }); const caseIds = (cs ?? []).map((x: any) => x.id); let totals: Record = {}; let updates: any[] = []; if (caseIds.length) { const [{ data: te }, { data: su }] = await Promise.all([ supabase .from("time_entries") .select("case_id, hours, hourly_rate, billable") .in("case_id", caseIds) .eq("billable", true), supabase .from("status_updates") .select("*") .in("case_id", caseIds) .order("title", { ascending: false }), ]); (te ?? []).forEach((t: any) => { const h = Number(t.hours) || 0; const r = Number(t.hourly_rate) || 0; if (!totals[t.case_id]) totals[t.case_id] = { hours: 0, amount: 0 }; totals[t.case_id].hours += h; totals[t.case_id].amount += h * r; }); const ids = Array.from(new Set((su ?? []).map((d: any) => d.created_by).filter(Boolean))); let profileMap: Record = {}; if (ids.length) { const { data: profs } = await supabase.from("profiles").select("id, full_name, email").in("id", ids); profileMap = Object.fromEntries((profs ?? []).map((p: any) => [p.id, p])); } const caseMap = Object.fromEntries((cs ?? []).map((x: any) => [x.id, x])); updates = (su ?? []).map((d: any) => ({ ...d, user: d.created_by ? profileMap[d.created_by] : null, case: caseMap[d.case_id], })); } setClient(c); setCases((cs ?? []).map((x: any) => ({ ...x, billed: totals[x.id] ?? { hours: 0, amount: 0 } }))); setStatusEntries(updates); setLoading(false); }; useEffect(() => { load(); }, [clientId]); if (loading) { return ( Loading… ); } if (!client) { return ( Client not found. Back to clients ); } const Icon = client.client_type === "hoa" || client.client_type === "condo" ? Building2 : User; const canEdit = isAdmin || client.created_by === user?.id; return ( All clients {client.archived_at && ( Archived )} {canEdit && ( setEditOpen(true)}> Edit )} {canEdit && ( { if (await setArchived("clients", client.id, !client.archived_at)) load(); }} > {client.archived_at ? ( <> Restore> ) : ( <> Archive> )} )} navigate({ to: "/invoices/new/$clientId", params: { clientId: client.id } })}> Generate invoice navigate({ to: "/cases/new", search: { clientId: client.id } })}> New case > } /> Details {(client.client_type === "hoa" || client.client_type === "condo") && ( <> > )} {client.notes && ( Notes {client.notes} )} {(client.client_type === "hoa" || client.client_type === "condo") && client.board_members?.length > 0 && ( Board members {client.board_members.map((m: any, i: number) => ( {m.name} {m.role && {m.role}} {m.email && {m.email}} {m.phone && {m.phone}} ))} )} Cases Status Contacts Fields {cases.length === 0 && No cases yet.} {cases.map((c) => ( {c.title} {c.status.replace("_", " ")} {c.case_number} · opened {formatDate(c.opened_at)} Billed: {c.billed.hours.toFixed(1)} hr · {formatCurrency(c.billed.amount)} ))} {statusEntries.length} update{statusEntries.length === 1 ? "" : "s"} across all cases { if (!user?.id) { toast.error("Not signed in"); return; } const opts = { title: "Client Status Report", subtitle: client.name, groupByCase: true, entries: statusEntries.map((u) => ({ title: u.title, body: u.body, created_at: u.created_at, author_name: u.user?.full_name, author_email: u.user?.email, case_number: u.case?.case_number, case_title: u.case?.title, })), }; const filename = `status-report-${client.name.replace(/[^a-z0-9]+/gi, "-")}-${new Date().toISOString().slice(0, 10)}.pdf`; try { await saveStatusReportToDb(opts, filename, { userId: user.id, payload: { client_id: client.id, client_name: client.name, entry_count: statusEntries.length, scope: "client" }, }); toast.success("Saved to Reports"); } catch (err: any) { toast.error(err?.message || "Could not save report"); } }} > Save to Reports { downloadStatusReport({ title: "Client Status Report", subtitle: client.name, groupByCase: true, entries: statusEntries.map((u) => ({ title: u.title, body: u.body, created_at: u.created_at, author_name: u.user?.full_name, author_email: u.user?.email, case_number: u.case?.case_number, case_title: u.case?.title, })), }, `status-report-${client.name.replace(/[^a-z0-9]+/gi, "-")}.pdf`); }} > Export {statusEntries.length === 0 && No status updates logged.} {statusEntries.map((u) => ( {formatDateTime(u.title)} {u.case?.case_number} · {u.user?.full_name || u.user?.email || "Unknown"} {u.body} ))} ); } function DetailRow({ label, value, icon: Icon }: { label: string; value: any; icon?: any }) { return ( <> {label} {Icon && value && } {value || —} > ); }
Loading…
Client not found.
{client.notes}
No cases yet.
{statusEntries.length} update{statusEntries.length === 1 ? "" : "s"} across all cases
No status updates logged.
{u.body}