Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
82f943c308
commit
ece6b645af
@@ -0,0 +1,260 @@
|
||||
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) };
|
||||
[...FIELDS, ...OPPOSING, { key: "case_summary", label: "", type: "text" }, { key: "next_hearing_notes", label: "", type: "text" }].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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user