diff --git a/src/components/app-shell.tsx b/src/components/app-shell.tsx index 39bc88b..c60db26 100644 --- a/src/components/app-shell.tsx +++ b/src/components/app-shell.tsx @@ -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 }, diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index dfd6339..b84be35 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -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, diff --git a/src/routes/status.index.tsx b/src/routes/status.index.tsx new file mode 100644 index 0000000..bae6de9 --- /dev/null +++ b/src/routes/status.index.tsx @@ -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: () => ( + + + + ), +}); + +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([]); + const [recent, setRecent] = useState([]); + const [caseId, setCaseId] = useState(""); + 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 = {}; + 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 ( + + + +
+ {/* Form */} + + +
+
+ + + {selectedCase && ( + + Open case + + )} +
+ +
+ + setTitle(e.target.value)} required /> +
+ +
+ +