Files
mylegal-stage-law/src/routes/status.index.tsx
T
2026-04-19 17:08:09 +00:00

202 lines
7.8 KiB
TypeScript

import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useMemo, useState } from "react";
import { ProtectedLayout } from "@/components/protected-layout";
import { PageContainer, PageHeader } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { Loader2, Send, ExternalLink } from "lucide-react";
import { formatDateTime } from "@/lib/format";
import { toast } from "sonner";
export const Route = createFileRoute("/status/")({
component: () => (
<ProtectedLayout>
<StatusUpdatesPage />
</ProtectedLayout>
),
});
function nowLocal() {
const d = new Date();
d.setMinutes(d.getMinutes() - d.getTimezoneOffset());
return d.toISOString().slice(0, 16);
}
function StatusUpdatesPage() {
const { user } = useAuth();
const [cases, setCases] = useState<any[]>([]);
const [recent, setRecent] = useState<any[]>([]);
const [caseId, setCaseId] = useState<string>("");
const [title, setTitle] = useState(nowLocal());
const [body, setBody] = useState("");
const [submitting, setSubmitting] = useState(false);
const [loading, setLoading] = useState(true);
const loadCases = async () => {
const { data, error } = await supabase
.from("cases")
.select("id, case_number, title, client:clients(name)")
.is("archived_at", null)
.order("opened_at", { ascending: false })
.limit(500);
if (error) toast.error("Could not load cases", { description: error.message });
setCases(data ?? []);
};
const loadRecent = async () => {
const { data, error } = await supabase
.from("status_updates")
.select("*, case:cases(id, case_number, title, client:clients(name))")
.order("created_at", { ascending: false })
.limit(25);
if (error) { toast.error("Could not load updates", { description: error.message }); setLoading(false); return; }
const ids = Array.from(new Set((data ?? []).map((d: any) => d.created_by).filter(Boolean)));
let profMap: Record<string, any> = {};
if (ids.length) {
const { data: profs } = await supabase.from("profiles").select("id, full_name, email").in("id", ids);
profMap = Object.fromEntries((profs ?? []).map((p: any) => [p.id, p]));
}
setRecent((data ?? []).map((u: any) => ({ ...u, user: u.created_by ? profMap[u.created_by] : null })));
setLoading(false);
};
useEffect(() => { loadCases(); loadRecent(); }, []);
const selectedCase = useMemo(() => cases.find((c) => c.id === caseId), [cases, caseId]);
const submit = async (e: React.FormEvent) => {
e.preventDefault();
if (!caseId) { toast.error("Select a case"); return; }
if (!title.trim() || !body.trim()) { toast.error("Date/time and details required"); return; }
setSubmitting(true);
const { error } = await supabase.from("status_updates").insert({
case_id: caseId,
title: title.trim(),
body: body.trim(),
created_by: user?.id,
});
setSubmitting(false);
if (error) { toast.error(error.message); return; }
toast.success("Update logged");
setBody("");
setTitle(nowLocal());
loadRecent();
};
return (
<PageContainer>
<PageHeader
title="Status Updates"
description="Quickly log a status update against any case without leaving this page."
/>
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.2fr)]">
{/* Form */}
<Card className="border-border/60">
<CardContent className="p-5">
<form onSubmit={submit} className="space-y-4">
<div className="space-y-1.5">
<Label>Case</Label>
<SearchableSelect
value={caseId}
onValueChange={setCaseId}
placeholder="Select a case…"
searchPlaceholder="Search cases…"
emptyText="No cases found."
options={cases.map((c) => ({
value: c.id,
label: `${c.case_number} — ${c.title}${c.client?.name ? ` · ${c.client.name}` : ""}`,
keywords: `${c.case_number} ${c.title} ${c.client?.name ?? ""}`,
}))}
/>
{selectedCase && (
<Link
to="/cases/$caseId"
params={{ caseId: selectedCase.id }}
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-primary mt-1"
>
Open case <ExternalLink className="h-3 w-3" />
</Link>
)}
</div>
<div className="space-y-1.5">
<Label>Date & time</Label>
<Input type="datetime-local" value={title} onChange={(e) => setTitle(e.target.value)} required />
</div>
<div className="space-y-1.5">
<Label>Details</Label>
<Textarea
rows={6}
value={body}
onChange={(e) => setBody(e.target.value)}
required
maxLength={5000}
placeholder="What happened?"
/>
</div>
<div className="flex justify-end">
<Button type="submit" disabled={submitting || !caseId}>
{submitting ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Send className="h-4 w-4 mr-2" />}
Log update
</Button>
</div>
</form>
</CardContent>
</Card>
{/* Recent */}
<div className="space-y-3">
<div className="text-xs uppercase tracking-wider text-muted-foreground px-1">
Recent updates (last 25)
</div>
{loading && (
<Card className="border-border/60"><CardContent className="p-6 text-center text-sm text-muted-foreground">Loading…</CardContent></Card>
)}
{!loading && recent.length === 0 && (
<Card className="border-border/60"><CardContent className="p-6 text-center text-sm text-muted-foreground">No updates yet.</CardContent></Card>
)}
{recent.map((u) => (
<Card key={u.id} className="border-border/60">
<CardContent className="p-4">
<div className="flex items-start justify-between gap-3 mb-1">
<div className="min-w-0">
<div className="font-serif text-base">{formatDateTime(u.title)}</div>
<div className="text-xs text-muted-foreground truncate">
{u.case ? (
<Link to="/cases/$caseId" params={{ caseId: u.case.id }} className="hover:text-primary">
{u.case.case_number} — {u.case.title}
{u.case.client?.name ? ` · ${u.case.client.name}` : ""}
</Link>
) : "—"}
</div>
</div>
<div className="text-[11px] text-muted-foreground whitespace-nowrap">
{u.user?.full_name || u.user?.email || "Unknown"} · {formatDateTime(u.created_at)}
</div>
</div>
<p className="text-sm whitespace-pre-wrap mt-2">{u.body}</p>
</CardContent>
</Card>
))}
</div>
</div>
</PageContainer>
);
}