Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
54793abda3
commit
93a5d3d8ad
@@ -0,0 +1,170 @@
|
||||
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 } from "lucide-react";
|
||||
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 { 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), 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, 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>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue="overview">
|
||||
<TabsList className="mb-4 flex-wrap h-auto">
|
||||
<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="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>
|
||||
</TabsList>
|
||||
<TabsContent value="overview"><CaseStatusTab caseId={caseId} /></TabsContent>
|
||||
<TabsContent value="documents"><CaseDocumentsTab caseId={caseId} /></TabsContent>
|
||||
<TabsContent value="time"><CaseTimeTab caseRecord={data} /></TabsContent>
|
||||
<TabsContent value="expenses"><CaseExpensesTab caseId={caseId} /></TabsContent>
|
||||
<TabsContent value="invoices"><CaseInvoicesTab caseRecord={data} /></TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<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 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user