Files
mylegal-stage-law/src/routes/cases.$caseId.tsx
T
2026-04-17 02:23:58 +00:00

191 lines
9.2 KiB
TypeScript

import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useState, useCallback } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { supabase } from "@/integrations/supabase/client";
import { ArrowLeft, FileText, Clock, DollarSign, Activity, Receipt, Scale, Users, Contact } from "lucide-react";
import { ContactsLinkTab } from "@/components/contacts/contacts-link-tab";
import { formatCurrency, formatDate, statusBadgeClass } from "@/lib/format";
import { CaseDocumentsTab } from "@/components/cases/documents-tab";
import { CaseTimeTab } from "@/components/cases/time-tab";
import { CaseExpensesTab } from "@/components/cases/expenses-tab";
import { CaseStatusTab } from "@/components/cases/status-tab";
import { CaseInvoicesTab } from "@/components/cases/invoices-tab";
import { CaseLitigationTab } from "@/components/cases/litigation-tab";
import { CaseCollectionsTab } from "@/components/cases/collections-tab";
import { toast } from "sonner";
import { useAuth } from "@/lib/auth";
export const Route = createFileRoute("/cases/$caseId")({
component: () => (
<ProtectedLayout>
<CaseDetail />
</ProtectedLayout>
),
});
function CaseDetail() {
const { caseId } = Route.useParams();
const { user, isAdmin } = useAuth();
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [users, setUsers] = useState<any[]>([]);
const load = useCallback(async () => {
const [{ data: c, error }, { data: us }] = await Promise.all([
supabase
.from("cases")
.select("*, client:clients(id, name, client_type, annual_interest_rate), assignee:profiles!cases_assigned_attorney_id_fkey(id, full_name, email)")
.eq("id", caseId)
.maybeSingle(),
supabase.from("profiles").select("id, full_name, email"),
]);
if (error) toast.error("Failed to load case", { description: error.message });
setData(c);
setUsers(us ?? []);
setLoading(false);
}, [caseId]);
useEffect(() => {
load();
}, [load]);
const updateStatus = async (status: string) => {
const { error } = await supabase
.from("cases")
.update({ status: status as "active" | "closed" | "closed_lost" | "closed_won" | "intake" | "on_hold", closed_at: status.startsWith("closed") ? new Date().toISOString().slice(0, 10) : null })
.eq("id", caseId);
if (error) toast.error("Could not update", { description: error.message });
else {
toast.success("Status updated");
load();
}
};
const updateAssignee = async (assigned_attorney_id: string) => {
const { error } = await supabase.from("cases").update({ assigned_attorney_id }).eq("id", caseId);
if (error) toast.error(error.message);
else { toast.success("Assignee updated"); load(); }
};
if (loading) return <PageContainer><p className="text-muted-foreground">Loading…</p></PageContainer>;
if (!data) {
return (
<PageContainer>
<p className="text-muted-foreground">Case not found or you don't have access.</p>
<Button variant="outline" className="mt-3" asChild>
<Link to="/cases"><ArrowLeft className="h-4 w-4 mr-2" /> All cases</Link>
</Button>
</PageContainer>
);
}
const canManage = isAdmin || data.assigned_attorney_id === user?.id || data.created_by === user?.id;
return (
<PageContainer>
<Button variant="ghost" size="sm" asChild className="mb-3 -ml-2">
<Link to="/cases"><ArrowLeft className="h-4 w-4 mr-1" /> All cases</Link>
</Button>
<div className="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-4 mb-6">
<div>
<div className="flex items-center gap-3 mb-1">
<span className="text-xs uppercase tracking-widest text-muted-foreground">{data.case_number}</span>
<Badge variant="outline" className={statusBadgeClass(data.status)}>{data.status.replace("_", " ")}</Badge>
</div>
<h1 className="font-serif text-3xl">{data.title}</h1>
<p className="text-sm text-muted-foreground mt-1">
<Link to="/clients/$clientId" params={{ clientId: data.client.id }} className="hover:text-primary underline-offset-4 hover:underline">
{data.client.name}
</Link>
{" · "}{data.practice_area || "—"}{" · "}opened {formatDate(data.opened_at)}
</p>
</div>
{canManage && (
<div className="flex flex-wrap gap-2">
<Select value={data.assigned_attorney_id ?? ""} onValueChange={updateAssignee}>
<SelectTrigger className="w-[200px]"><SelectValue placeholder="Assign attorney" /></SelectTrigger>
<SelectContent>
{users.map((u) => <SelectItem key={u.id} value={u.id}>{u.full_name || u.email}</SelectItem>)}
</SelectContent>
</Select>
<Select value={data.status} onValueChange={updateStatus}>
<SelectTrigger className="w-[160px]"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="intake">Intake</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="on_hold">On hold</SelectItem>
<SelectItem value="closed_won">Closed — won</SelectItem>
<SelectItem value="closed_lost">Closed — lost</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent>
</Select>
</div>
)}
</div>
{data.description && (
<Card className="mb-6 border-border/60">
<CardContent className="p-4">
<p className="text-sm whitespace-pre-wrap">{data.description}</p>
</CardContent>
</Card>
)}
<CaseTabs data={data} caseId={caseId} canManage={canManage} load={load} />
<div className="mt-8 grid grid-cols-2 sm:grid-cols-4 gap-3 text-center text-xs">
<SmallStat label="Default rate" value={data.default_hourly_rate ? formatCurrency(data.default_hourly_rate) + "/hr" : "—"} />
<SmallStat label="Attorney" value={data.assignee?.full_name || data.assignee?.email || "—"} />
<SmallStat label="Closed" value={data.closed_at ? formatDate(data.closed_at) : "—"} />
<SmallStat label="Updated" value={formatDate(data.updated_at)} />
</div>
</PageContainer>
);
}
function CaseTabs({ data, caseId, canManage, load }: { data: any; caseId: string; canManage: boolean; load: () => void }) {
const [tab, setTab] = useState("litigation");
return (
<Tabs value={tab} onValueChange={setTab}>
<TabsList className="mb-4 flex-wrap h-auto">
<TabsTrigger value="litigation"><Scale className="h-3.5 w-3.5 mr-1.5" />Litigation</TabsTrigger>
<TabsTrigger value="overview"><Activity className="h-3.5 w-3.5 mr-1.5" />Status log</TabsTrigger>
<TabsTrigger value="documents"><FileText className="h-3.5 w-3.5 mr-1.5" />Documents</TabsTrigger>
<TabsTrigger value="contacts"><Contact className="h-3.5 w-3.5 mr-1.5" />Contacts</TabsTrigger>
<TabsTrigger value="time"><Clock className="h-3.5 w-3.5 mr-1.5" />Time</TabsTrigger>
<TabsTrigger value="expenses"><DollarSign className="h-3.5 w-3.5 mr-1.5" />Expenses</TabsTrigger>
<TabsTrigger value="invoices"><Receipt className="h-3.5 w-3.5 mr-1.5" />Invoices</TabsTrigger>
{(data.client?.client_type === "hoa" || data.client?.client_type === "condo") && (
<TabsTrigger value="collections"><Users className="h-3.5 w-3.5 mr-1.5" />Collections</TabsTrigger>
)}
</TabsList>
<TabsContent value="litigation"><CaseLitigationTab caseRecord={data} canManage={canManage} onSaved={load} /></TabsContent>
<TabsContent value="overview"><CaseStatusTab caseId={caseId} caseLabel={`${data.case_number} — ${data.title}`} /></TabsContent>
<TabsContent value="documents"><CaseDocumentsTab caseId={caseId} /></TabsContent>
<TabsContent value="contacts"><ContactsLinkTab parentId={caseId} parentTable="case_contacts" /></TabsContent>
<TabsContent value="time"><CaseTimeTab caseRecord={data} onInvoice={() => setTab("invoices")} /></TabsContent>
<TabsContent value="expenses"><CaseExpensesTab caseId={caseId} /></TabsContent>
<TabsContent value="invoices"><CaseInvoicesTab caseRecord={data} /></TabsContent>
{(data.client?.client_type === "hoa" || data.client?.client_type === "condo") && (
<TabsContent value="collections"><CaseCollectionsTab caseRecord={data} /></TabsContent>
)}
</Tabs>
);
}
function SmallStat({ label, value }: { label: string; value: string }) {
return (
<div className="border rounded-md py-2 px-3">
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">{label}</div>
<div className="text-sm font-medium mt-0.5 truncate">{value}</div>
</div>
);
}