Added Hearing Timeline tab

X-Lovable-Edit-ID: edt-54ed7862-87b4-41f1-8786-c92f84edbbef
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-01 14:52:32 +00:00
co-authored by renee-png
2 changed files with 165 additions and 1 deletions
@@ -0,0 +1,161 @@
import { useEffect, useState, useCallback } from "react";
import { Link } from "@tanstack/react-router";
import { supabase } from "@/integrations/supabase/client";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Gavel, Loader2, MapPin, CalendarPlus, ExternalLink } from "lucide-react";
import { formatDateTime, formatDate } from "@/lib/format";
interface HearingEvent {
id: string;
title: string;
start_at: string;
end_at: string | null;
all_day: boolean;
location: string | null;
description: string | null;
event_type: string;
}
export function CaseHearingTimelineTab({ caseId, nextHearingDate, nextHearingNotes }: {
caseId: string;
nextHearingDate?: string | null;
nextHearingNotes?: string | null;
}) {
const [hearings, setHearings] = useState<HearingEvent[]>([]);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
setLoading(true);
const { data, error } = await supabase
.from("events")
.select("id,title,start_at,end_at,all_day,location,description,event_type")
.eq("case_id", caseId)
.eq("event_type", "hearing")
.order("start_at", { ascending: false });
if (!error) setHearings((data ?? []) as HearingEvent[]);
setLoading(false);
}, [caseId]);
useEffect(() => { load(); }, [load]);
const now = Date.now();
const upcoming = hearings.filter((h) => new Date(h.start_at).getTime() >= now);
const past = hearings.filter((h) => new Date(h.start_at).getTime() < now);
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
);
}
if (hearings.length === 0 && !nextHearingDate) {
return (
<Card>
<CardContent className="py-12 text-center text-muted-foreground space-y-3">
<Gavel className="h-8 w-8 mx-auto opacity-50" />
<p className="text-sm">No hearings scheduled for this case yet.</p>
<Button asChild variant="outline" size="sm">
<Link to="/calendar" search={{ caseId } as any}>
<CalendarPlus className="h-4 w-4 mr-1.5" />
Schedule on calendar
</Link>
</Button>
</CardContent>
</Card>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Hearings are sourced from the calendar. Add or edit them in the{" "}
<Link to="/calendar" className="text-primary underline-offset-2 hover:underline">
Calendar
</Link>.
</p>
<Button asChild variant="outline" size="sm">
<Link to="/calendar"><CalendarPlus className="h-4 w-4 mr-1.5" />Open calendar</Link>
</Button>
</div>
{nextHearingDate && (
<Section title="On case record">
<Card>
<CardContent className="py-4 flex items-start gap-3">
<Gavel className="h-5 w-5 mt-0.5 text-rose-500" />
<div className="flex-1">
<div className="font-medium">Next hearing</div>
<div className="text-sm text-muted-foreground">{formatDate(nextHearingDate)}</div>
{nextHearingNotes && (
<div className="text-sm mt-1 whitespace-pre-wrap">{nextHearingNotes}</div>
)}
</div>
</CardContent>
</Card>
</Section>
)}
{upcoming.length > 0 && (
<Section title={`Upcoming (${upcoming.length})`}>
<div className="space-y-2">
{upcoming.map((h) => <HearingRow key={h.id} h={h} />)}
</div>
</Section>
)}
{past.length > 0 && (
<Section title={`Past (${past.length})`}>
<div className="space-y-2">
{past.map((h) => <HearingRow key={h.id} h={h} muted />)}
</div>
</Section>
)}
</div>
);
}
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="space-y-2">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">{title}</h3>
{children}
</div>
);
}
function HearingRow({ h, muted }: { h: HearingEvent; muted?: boolean }) {
return (
<Card className={muted ? "opacity-75" : ""}>
<CardContent className="py-3 flex items-start gap-3">
<div className="mt-0.5">
<Badge variant="secondary" className="bg-rose-500/10 text-rose-700 dark:text-rose-300 border-rose-500/30">
<Gavel className="h-3 w-3 mr-1" />Hearing
</Badge>
</div>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{h.title}</div>
<div className="text-sm text-muted-foreground">
{h.all_day ? formatDate(h.start_at) : formatDateTime(h.start_at)}
{h.end_at && !h.all_day && ` – ${formatDateTime(h.end_at)}`}
</div>
{h.location && (
<div className="text-sm text-muted-foreground flex items-center gap-1 mt-0.5">
<MapPin className="h-3 w-3" />{h.location}
</div>
)}
{h.description && (
<div className="text-sm mt-1 whitespace-pre-wrap">{h.description}</div>
)}
</div>
<Button asChild variant="ghost" size="sm">
<Link to="/calendar"><ExternalLink className="h-3.5 w-3.5" /></Link>
</Button>
</CardContent>
</Card>
);
}
+4 -1
View File
@@ -10,7 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { supabase } from "@/integrations/supabase/client";
import { ArrowLeft, FileText, Clock, DollarSign, Activity, Receipt, Scale, Users, Contact, Phone, Tag, Archive, ArchiveRestore, ArrowRightLeft, Search, GitFork, Wallet, CheckSquare, Mail, CalendarClock } from "lucide-react";
import { ArrowLeft, FileText, Clock, DollarSign, Activity, Receipt, Scale, Users, Contact, Phone, Tag, Archive, ArchiveRestore, ArrowRightLeft, Search, GitFork, Wallet, CheckSquare, Mail, CalendarClock, Gavel } from "lucide-react";
import { TrustAccountPanel } from "@/components/trust/trust-account-panel";
import { ConvertToCollectionsDialog } from "@/components/cases/convert-to-collections-dialog";
import { ContactsLinkTab } from "@/components/contacts/contacts-link-tab";
@@ -27,6 +27,7 @@ import { CaseCustomFieldsTab } from "@/components/cases/custom-fields-tab";
import { CaseTasksTab } from "@/components/cases/tasks-tab";
import { CaseMessagesTab } from "@/components/cases/messages-tab";
import { CasePaymentPlansTab } from "@/components/cases/payment-plans-tab";
import { CaseHearingTimelineTab } from "@/components/cases/hearing-timeline-tab";
import { setArchived } from "@/lib/archive";
import { toast } from "sonner";
import { useAuth } from "@/lib/auth";
@@ -313,6 +314,7 @@ function CaseTabs({ data, caseId, canManage, load, tab, onTabChange }: { data: a
<Tabs value={tab} onValueChange={onTabChange}>
<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="hearings"><Gavel className="h-3.5 w-3.5 mr-1.5" />Hearing Timeline</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>
@@ -332,6 +334,7 @@ function CaseTabs({ data, caseId, canManage, load, tab, onTabChange }: { data: a
)}
</TabsList>
<TabsContent value="litigation"><CaseLitigationTab caseRecord={data} canManage={canManage} onSaved={load} /></TabsContent>
<TabsContent value="hearings"><CaseHearingTimelineTab caseId={caseId} nextHearingDate={data.next_hearing_date} nextHearingNotes={data.next_hearing_notes} /></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>