Added status update tracking

X-Lovable-Edit-ID: edt-478fbceb-6703-4c34-aaa7-18dcc2cdc724
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-17 02:15:04 +00:00
co-authored by renee-png
3 changed files with 223 additions and 0 deletions
+2
View File
@@ -15,6 +15,7 @@ import {
Wallet,
FileSignature,
FolderArchive,
ClipboardList,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { ReactNode } from "react";
@@ -30,6 +31,7 @@ const NAV: NavItem[] = [
{ to: "/", label: "Dashboard", icon: LayoutDashboard },
{ to: "/clients", label: "Clients", icon: Users },
{ to: "/cases", label: "Cases", icon: Briefcase },
{ to: "/status", label: "Status Updates", icon: ClipboardList },
{ to: "/collections", label: "Collections", icon: Wallet },
{ to: "/documents", label: "Documents", icon: FileSignature },
{ to: "/files", label: "Files", icon: FolderArchive },
+21
View File
@@ -13,6 +13,7 @@ import { Route as SetupRouteImport } from './routes/setup'
import { Route as SettingsRouteImport } from './routes/settings'
import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/index'
import { Route as StatusIndexRouteImport } from './routes/status.index'
import { Route as SettingsIndexRouteImport } from './routes/settings.index'
import { Route as FilesIndexRouteImport } from './routes/files.index'
import { Route as DocumentsIndexRouteImport } from './routes/documents.index'
@@ -51,6 +52,11 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
const StatusIndexRoute = StatusIndexRouteImport.update({
id: '/status/',
path: '/status/',
getParentRoute: () => rootRouteImport,
} as any)
const SettingsIndexRoute = SettingsIndexRouteImport.update({
id: '/',
path: '/',
@@ -156,6 +162,7 @@ export interface FileRoutesByFullPath {
'/documents/': typeof DocumentsIndexRoute
'/files/': typeof FilesIndexRoute
'/settings/': typeof SettingsIndexRoute
'/status/': typeof StatusIndexRoute
'/documents/pleading/new': typeof DocumentsPleadingNewRoute
'/documents/templates/$templateId': typeof DocumentsTemplatesTemplateIdRoute
'/documents/templates/new': typeof DocumentsTemplatesNewRoute
@@ -178,6 +185,7 @@ export interface FileRoutesByTo {
'/documents': typeof DocumentsIndexRoute
'/files': typeof FilesIndexRoute
'/settings': typeof SettingsIndexRoute
'/status': typeof StatusIndexRoute
'/documents/pleading/new': typeof DocumentsPleadingNewRoute
'/documents/templates/$templateId': typeof DocumentsTemplatesTemplateIdRoute
'/documents/templates/new': typeof DocumentsTemplatesNewRoute
@@ -202,6 +210,7 @@ export interface FileRoutesById {
'/documents/': typeof DocumentsIndexRoute
'/files/': typeof FilesIndexRoute
'/settings/': typeof SettingsIndexRoute
'/status/': typeof StatusIndexRoute
'/documents/pleading/new': typeof DocumentsPleadingNewRoute
'/documents/templates/$templateId': typeof DocumentsTemplatesTemplateIdRoute
'/documents/templates/new': typeof DocumentsTemplatesNewRoute
@@ -227,6 +236,7 @@ export interface FileRouteTypes {
| '/documents/'
| '/files/'
| '/settings/'
| '/status/'
| '/documents/pleading/new'
| '/documents/templates/$templateId'
| '/documents/templates/new'
@@ -249,6 +259,7 @@ export interface FileRouteTypes {
| '/documents'
| '/files'
| '/settings'
| '/status'
| '/documents/pleading/new'
| '/documents/templates/$templateId'
| '/documents/templates/new'
@@ -272,6 +283,7 @@ export interface FileRouteTypes {
| '/documents/'
| '/files/'
| '/settings/'
| '/status/'
| '/documents/pleading/new'
| '/documents/templates/$templateId'
| '/documents/templates/new'
@@ -293,6 +305,7 @@ export interface RootRouteChildren {
CollectionsIndexRoute: typeof CollectionsIndexRoute
DocumentsIndexRoute: typeof DocumentsIndexRoute
FilesIndexRoute: typeof FilesIndexRoute
StatusIndexRoute: typeof StatusIndexRoute
DocumentsPleadingNewRoute: typeof DocumentsPleadingNewRoute
DocumentsTemplatesTemplateIdRoute: typeof DocumentsTemplatesTemplateIdRoute
DocumentsTemplatesNewRoute: typeof DocumentsTemplatesNewRoute
@@ -329,6 +342,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/status/': {
id: '/status/'
path: '/status'
fullPath: '/status/'
preLoaderRoute: typeof StatusIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/settings/': {
id: '/settings/'
path: '/'
@@ -482,6 +502,7 @@ const rootRouteChildren: RootRouteChildren = {
CollectionsIndexRoute: CollectionsIndexRoute,
DocumentsIndexRoute: DocumentsIndexRoute,
FilesIndexRoute: FilesIndexRoute,
StatusIndexRoute: StatusIndexRoute,
DocumentsPleadingNewRoute: DocumentsPleadingNewRoute,
DocumentsTemplatesTemplateIdRoute: DocumentsTemplatesTemplateIdRoute,
DocumentsTemplatesNewRoute: DocumentsTemplatesNewRoute,
+200
View File
@@ -0,0 +1,200 @@
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 { 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)")
.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>
<Select value={caseId} onValueChange={setCaseId}>
<SelectTrigger>
<SelectValue placeholder="Select a case…" />
</SelectTrigger>
<SelectContent>
{cases.map((c) => (
<SelectItem key={c.id} value={c.id}>
{c.case_number} — {c.title}
{c.client?.name ? ` · ${c.client.name}` : ""}
</SelectItem>
))}
</SelectContent>
</Select>
{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>
);
}