Added litigation overview

X-Lovable-Edit-ID: edt-8351170c-d6dd-4396-a752-6df441f86dc9
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-17 00:04:58 +00:00
co-authored by renee-png
4 changed files with 348 additions and 2 deletions
+266
View File
@@ -0,0 +1,266 @@
import { useState } from "react";
import { supabase } from "@/integrations/supabase/client";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Separator } from "@/components/ui/separator";
import { Pencil, Save, X, Calendar, Gavel, Scale, FileSearch, Users, AlertCircle } from "lucide-react";
import { toast } from "sonner";
import { formatCurrency, formatDate } from "@/lib/format";
interface Props {
caseRecord: any;
canManage: boolean;
onSaved: () => void;
}
const FIELDS: Array<{ key: string; label: string; type?: string; full?: boolean }> = [
{ key: "case_caption", label: "Case caption", full: true },
{ key: "court_case_number", label: "Court case #" },
{ key: "litigation_stage", label: "Litigation stage" },
{ key: "court", label: "Court" },
{ key: "jurisdiction", label: "Jurisdiction" },
{ key: "judge", label: "Judge" },
{ key: "filing_date", label: "Filing date", type: "date" },
{ key: "next_hearing_date", label: "Next hearing", type: "date" },
{ key: "statute_of_limitations", label: "Statute of limitations", type: "date" },
{ key: "claim_amount", label: "Claim amount", type: "number" },
{ key: "settlement_amount", label: "Settlement amount", type: "number" },
];
const OPPOSING: Array<{ key: string; label: string }> = [
{ key: "opposing_party", label: "Opposing party" },
{ key: "opposing_counsel", label: "Opposing counsel" },
{ key: "opposing_counsel_firm", label: "Counsel firm" },
{ key: "opposing_counsel_email", label: "Counsel email" },
{ key: "opposing_counsel_phone", label: "Counsel phone" },
];
export function CaseLitigationTab({ caseRecord, canManage, onSaved }: Props) {
const [editing, setEditing] = useState(false);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState<any>(() => ({ ...caseRecord }));
const [keyDates, setKeyDates] = useState<Array<{ date: string; label: string }>>(
Array.isArray(caseRecord.key_dates) ? caseRecord.key_dates : [],
);
const startEdit = () => {
setForm({ ...caseRecord });
setKeyDates(Array.isArray(caseRecord.key_dates) ? caseRecord.key_dates : []);
setEditing(true);
};
const cancel = () => {
setEditing(false);
};
const save = async () => {
setSaving(true);
const payload: any = { key_dates: keyDates.filter((k) => k.date || k.label) };
const all: Array<{ key: string; type?: string }> = [
...FIELDS,
...OPPOSING,
{ key: "case_summary", type: "text" },
{ key: "next_hearing_notes", type: "text" },
];
all.forEach((f) => {
const v = form[f.key];
if (f.type === "number") payload[f.key] = v === "" || v == null ? null : Number(v);
else payload[f.key] = v === "" ? null : v ?? null;
});
const { error } = await supabase.from("cases").update(payload).eq("id", caseRecord.id);
setSaving(false);
if (error) {
toast.error("Could not save", { description: error.message });
return;
}
toast.success("Litigation details updated");
setEditing(false);
onSaved();
};
const renderValue = (key: string, type?: string) => {
const v = caseRecord[key];
if (v == null || v === "") return <span className="text-muted-foreground">—</span>;
if (type === "date") return formatDate(v);
if (type === "number") return formatCurrency(Number(v));
return String(v);
};
const updateKeyDate = (i: number, patch: Partial<{ date: string; label: string }>) => {
setKeyDates((prev) => prev.map((k, idx) => (idx === i ? { ...k, ...patch } : k)));
};
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="font-serif text-xl">Litigation overview</h2>
<p className="text-xs text-muted-foreground">Court details, opposing parties, and key dates.</p>
</div>
{canManage && !editing && (
<Button variant="outline" size="sm" onClick={startEdit}>
<Pencil className="h-3.5 w-3.5 mr-1.5" /> Edit
</Button>
)}
{editing && (
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={cancel} disabled={saving}>
<X className="h-3.5 w-3.5 mr-1.5" /> Cancel
</Button>
<Button size="sm" onClick={save} disabled={saving}>
<Save className="h-3.5 w-3.5 mr-1.5" /> {saving ? "Saving…" : "Save"}
</Button>
</div>
)}
</div>
<Card className="border-border/60">
<CardContent className="p-5 space-y-5">
<Section icon={<FileSearch className="h-4 w-4" />} title="Case summary">
{editing ? (
<Textarea
rows={5}
value={form.case_summary ?? ""}
onChange={(e) => setForm({ ...form, case_summary: e.target.value })}
placeholder="Facts, claims, theory of the case, posture…"
/>
) : caseRecord.case_summary ? (
<p className="text-sm whitespace-pre-wrap">{caseRecord.case_summary}</p>
) : (
<p className="text-sm text-muted-foreground">No summary yet.</p>
)}
</Section>
<Separator />
<Section icon={<Gavel className="h-4 w-4" />} title="Court & filing">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{FIELDS.map((f) => (
<Field key={f.key} field={f} editing={editing} form={form} setForm={setForm} renderValue={renderValue} />
))}
</div>
</Section>
<Separator />
<Section icon={<Users className="h-4 w-4" />} title="Opposing party & counsel">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{OPPOSING.map((f) => (
<Field key={f.key} field={f} editing={editing} form={form} setForm={setForm} renderValue={renderValue} />
))}
</div>
</Section>
<Separator />
<Section icon={<AlertCircle className="h-4 w-4" />} title="Next hearing notes">
{editing ? (
<Textarea
rows={3}
value={form.next_hearing_notes ?? ""}
onChange={(e) => setForm({ ...form, next_hearing_notes: e.target.value })}
placeholder="What's on calendar, prep needs, location…"
/>
) : caseRecord.next_hearing_notes ? (
<p className="text-sm whitespace-pre-wrap">{caseRecord.next_hearing_notes}</p>
) : (
<p className="text-sm text-muted-foreground">—</p>
)}
</Section>
<Separator />
<Section icon={<Calendar className="h-4 w-4" />} title="Key dates timeline">
{editing ? (
<div className="space-y-2">
{keyDates.map((k, i) => (
<div key={i} className="flex gap-2 items-center">
<Input
type="date"
value={k.date ?? ""}
onChange={(e) => updateKeyDate(i, { date: e.target.value })}
className="w-[170px]"
/>
<Input
placeholder="Event description"
value={k.label ?? ""}
onChange={(e) => updateKeyDate(i, { label: e.target.value })}
/>
<Button variant="ghost" size="icon" onClick={() => setKeyDates((p) => p.filter((_, idx) => idx !== i))}>
<X className="h-4 w-4" />
</Button>
</div>
))}
<Button variant="outline" size="sm" onClick={() => setKeyDates((p) => [...p, { date: "", label: "" }])}>
Add date
</Button>
</div>
) : keyDates.length === 0 ? (
<p className="text-sm text-muted-foreground">No key dates recorded.</p>
) : (
<ul className="space-y-1.5">
{[...keyDates]
.sort((a, b) => (a.date || "").localeCompare(b.date || ""))
.map((k, i) => (
<li key={i} className="flex gap-3 text-sm">
<Scale className="h-3.5 w-3.5 mt-0.5 text-muted-foreground" />
<span className="font-mono text-xs text-muted-foreground w-24 shrink-0">
{k.date ? formatDate(k.date) : "—"}
</span>
<span>{k.label || "—"}</span>
</li>
))}
</ul>
)}
</Section>
</CardContent>
</Card>
</div>
);
}
function Section({ icon, title, children }: { icon: React.ReactNode; title: string; children: React.ReactNode }) {
return (
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs uppercase tracking-wider text-muted-foreground">
{icon}
{title}
</div>
{children}
</div>
);
}
function Field({
field,
editing,
form,
setForm,
renderValue,
}: {
field: { key: string; label: string; type?: string };
editing: boolean;
form: any;
setForm: (v: any) => void;
renderValue: (key: string, type?: string) => React.ReactNode;
}) {
return (
<div>
<Label className="text-xs text-muted-foreground">{field.label}</Label>
{editing ? (
<Input
type={field.type ?? "text"}
step={field.type === "number" ? "0.01" : undefined}
value={form[field.key] ?? ""}
onChange={(e) => setForm({ ...form, [field.key]: e.target.value })}
className="mt-1"
/>
) : (
<div className="text-sm mt-1">{renderValue(field.key, field.type)}</div>
)}
</div>
);
}
+57
View File
@@ -17,49 +17,106 @@ export type Database = {
cases: {
Row: {
assigned_attorney_id: string | null
case_caption: string | null
case_number: string
case_summary: string | null
claim_amount: number | null
client_id: string
closed_at: string | null
court: string | null
court_case_number: string | null
created_at: string
created_by: string | null
default_hourly_rate: number | null
description: string | null
filing_date: string | null
id: string
judge: string | null
jurisdiction: string | null
key_dates: Json
litigation_stage: string | null
next_hearing_date: string | null
next_hearing_notes: string | null
opened_at: string
opposing_counsel: string | null
opposing_counsel_email: string | null
opposing_counsel_firm: string | null
opposing_counsel_phone: string | null
opposing_party: string | null
practice_area: string | null
settlement_amount: number | null
status: Database["public"]["Enums"]["case_status"]
statute_of_limitations: string | null
title: string
updated_at: string
}
Insert: {
assigned_attorney_id?: string | null
case_caption?: string | null
case_number: string
case_summary?: string | null
claim_amount?: number | null
client_id: string
closed_at?: string | null
court?: string | null
court_case_number?: string | null
created_at?: string
created_by?: string | null
default_hourly_rate?: number | null
description?: string | null
filing_date?: string | null
id?: string
judge?: string | null
jurisdiction?: string | null
key_dates?: Json
litigation_stage?: string | null
next_hearing_date?: string | null
next_hearing_notes?: string | null
opened_at?: string
opposing_counsel?: string | null
opposing_counsel_email?: string | null
opposing_counsel_firm?: string | null
opposing_counsel_phone?: string | null
opposing_party?: string | null
practice_area?: string | null
settlement_amount?: number | null
status?: Database["public"]["Enums"]["case_status"]
statute_of_limitations?: string | null
title: string
updated_at?: string
}
Update: {
assigned_attorney_id?: string | null
case_caption?: string | null
case_number?: string
case_summary?: string | null
claim_amount?: number | null
client_id?: string
closed_at?: string | null
court?: string | null
court_case_number?: string | null
created_at?: string
created_by?: string | null
default_hourly_rate?: number | null
description?: string | null
filing_date?: string | null
id?: string
judge?: string | null
jurisdiction?: string | null
key_dates?: Json
litigation_stage?: string | null
next_hearing_date?: string | null
next_hearing_notes?: string | null
opened_at?: string
opposing_counsel?: string | null
opposing_counsel_email?: string | null
opposing_counsel_firm?: string | null
opposing_counsel_phone?: string | null
opposing_party?: string | null
practice_area?: string | null
settlement_amount?: number | null
status?: Database["public"]["Enums"]["case_status"]
statute_of_limitations?: string | null
title?: string
updated_at?: string
}
+5 -2
View File
@@ -8,13 +8,14 @@ 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 { ArrowLeft, FileText, Clock, DollarSign, Activity, Receipt, Scale } 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 { CaseLitigationTab } from "@/components/cases/litigation-tab";
import { toast } from "sonner";
import { useAuth } from "@/lib/auth";
@@ -135,14 +136,16 @@ function CaseDetail() {
</Card>
)}
<Tabs defaultValue="overview">
<Tabs defaultValue="litigation">
<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="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="litigation"><CaseLitigationTab caseRecord={data} canManage={canManage} onSaved={load} /></TabsContent>
<TabsContent value="overview"><CaseStatusTab caseId={caseId} /></TabsContent>
<TabsContent value="documents"><CaseDocumentsTab caseId={caseId} /></TabsContent>
<TabsContent value="time"><CaseTimeTab caseRecord={data} /></TabsContent>
@@ -0,0 +1,20 @@
ALTER TABLE public.cases
ADD COLUMN IF NOT EXISTS case_summary text,
ADD COLUMN IF NOT EXISTS opposing_party text,
ADD COLUMN IF NOT EXISTS opposing_counsel text,
ADD COLUMN IF NOT EXISTS opposing_counsel_firm text,
ADD COLUMN IF NOT EXISTS opposing_counsel_email text,
ADD COLUMN IF NOT EXISTS opposing_counsel_phone text,
ADD COLUMN IF NOT EXISTS court text,
ADD COLUMN IF NOT EXISTS jurisdiction text,
ADD COLUMN IF NOT EXISTS judge text,
ADD COLUMN IF NOT EXISTS court_case_number text,
ADD COLUMN IF NOT EXISTS case_caption text,
ADD COLUMN IF NOT EXISTS filing_date date,
ADD COLUMN IF NOT EXISTS next_hearing_date date,
ADD COLUMN IF NOT EXISTS next_hearing_notes text,
ADD COLUMN IF NOT EXISTS statute_of_limitations date,
ADD COLUMN IF NOT EXISTS claim_amount numeric,
ADD COLUMN IF NOT EXISTS settlement_amount numeric,
ADD COLUMN IF NOT EXISTS litigation_stage text,
ADD COLUMN IF NOT EXISTS key_dates jsonb NOT NULL DEFAULT '[]'::jsonb;