From 54793abda388aa33b9c9921ee1064ca11c9026c5 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 16 Apr 2026 22:50:27 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routeTree.gen.ts | 91 +++++++++++++- src/routes/cases.index.tsx | 112 ++++++++++++++++++ src/routes/cases.new.tsx | 192 ++++++++++++++++++++++++++++++ src/routes/clients.$clientId.tsx | 197 +++++++++++++++++++++++++++++++ 4 files changed, 589 insertions(+), 3 deletions(-) create mode 100644 src/routes/cases.index.tsx create mode 100644 src/routes/cases.new.tsx create mode 100644 src/routes/clients.$clientId.tsx diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 79b5271..752de02 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -13,6 +13,9 @@ import { Route as SetupRouteImport } from './routes/setup' import { Route as LoginRouteImport } from './routes/login' import { Route as IndexRouteImport } from './routes/index' import { Route as ClientsIndexRouteImport } from './routes/clients.index' +import { Route as CasesIndexRouteImport } from './routes/cases.index' +import { Route as ClientsClientIdRouteImport } from './routes/clients.$clientId' +import { Route as CasesNewRouteImport } from './routes/cases.new' const SetupRoute = SetupRouteImport.update({ id: '/setup', @@ -34,17 +37,38 @@ const ClientsIndexRoute = ClientsIndexRouteImport.update({ path: '/clients/', getParentRoute: () => rootRouteImport, } as any) +const CasesIndexRoute = CasesIndexRouteImport.update({ + id: '/cases/', + path: '/cases/', + getParentRoute: () => rootRouteImport, +} as any) +const ClientsClientIdRoute = ClientsClientIdRouteImport.update({ + id: '/clients/$clientId', + path: '/clients/$clientId', + getParentRoute: () => rootRouteImport, +} as any) +const CasesNewRoute = CasesNewRouteImport.update({ + id: '/cases/new', + path: '/cases/new', + getParentRoute: () => rootRouteImport, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/login': typeof LoginRoute '/setup': typeof SetupRoute + '/cases/new': typeof CasesNewRoute + '/clients/$clientId': typeof ClientsClientIdRoute + '/cases/': typeof CasesIndexRoute '/clients/': typeof ClientsIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/login': typeof LoginRoute '/setup': typeof SetupRoute + '/cases/new': typeof CasesNewRoute + '/clients/$clientId': typeof ClientsClientIdRoute + '/cases': typeof CasesIndexRoute '/clients': typeof ClientsIndexRoute } export interface FileRoutesById { @@ -52,20 +76,48 @@ export interface FileRoutesById { '/': typeof IndexRoute '/login': typeof LoginRoute '/setup': typeof SetupRoute + '/cases/new': typeof CasesNewRoute + '/clients/$clientId': typeof ClientsClientIdRoute + '/cases/': typeof CasesIndexRoute '/clients/': typeof ClientsIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/login' | '/setup' | '/clients/' + fullPaths: + | '/' + | '/login' + | '/setup' + | '/cases/new' + | '/clients/$clientId' + | '/cases/' + | '/clients/' fileRoutesByTo: FileRoutesByTo - to: '/' | '/login' | '/setup' | '/clients' - id: '__root__' | '/' | '/login' | '/setup' | '/clients/' + to: + | '/' + | '/login' + | '/setup' + | '/cases/new' + | '/clients/$clientId' + | '/cases' + | '/clients' + id: + | '__root__' + | '/' + | '/login' + | '/setup' + | '/cases/new' + | '/clients/$clientId' + | '/cases/' + | '/clients/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute LoginRoute: typeof LoginRoute SetupRoute: typeof SetupRoute + CasesNewRoute: typeof CasesNewRoute + ClientsClientIdRoute: typeof ClientsClientIdRoute + CasesIndexRoute: typeof CasesIndexRoute ClientsIndexRoute: typeof ClientsIndexRoute } @@ -99,6 +151,27 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ClientsIndexRouteImport parentRoute: typeof rootRouteImport } + '/cases/': { + id: '/cases/' + path: '/cases' + fullPath: '/cases/' + preLoaderRoute: typeof CasesIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/clients/$clientId': { + id: '/clients/$clientId' + path: '/clients/$clientId' + fullPath: '/clients/$clientId' + preLoaderRoute: typeof ClientsClientIdRouteImport + parentRoute: typeof rootRouteImport + } + '/cases/new': { + id: '/cases/new' + path: '/cases/new' + fullPath: '/cases/new' + preLoaderRoute: typeof CasesNewRouteImport + parentRoute: typeof rootRouteImport + } } } @@ -106,8 +179,20 @@ const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, LoginRoute: LoginRoute, SetupRoute: SetupRoute, + CasesNewRoute: CasesNewRoute, + ClientsClientIdRoute: ClientsClientIdRoute, + CasesIndexRoute: CasesIndexRoute, ClientsIndexRoute: ClientsIndexRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/src/routes/cases.index.tsx b/src/routes/cases.index.tsx new file mode 100644 index 0000000..42b5629 --- /dev/null +++ b/src/routes/cases.index.tsx @@ -0,0 +1,112 @@ +import { createFileRoute, Link } from "@tanstack/react-router"; +import { useEffect, useState } from "react"; +import { ProtectedLayout } from "@/components/protected-layout"; +import { PageContainer, PageHeader } from "@/components/app-shell"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { supabase } from "@/integrations/supabase/client"; +import { Plus, Search } from "lucide-react"; +import { formatDate, statusBadgeClass } from "@/lib/format"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +export const Route = createFileRoute("/cases/")({ + component: () => ( + + + + ), +}); + +function CasesList() { + const [cases, setCases] = useState([]); + const [q, setQ] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + + useEffect(() => { + (async () => { + const { data } = await supabase + .from("cases") + .select("*, client:clients(name), assignee:profiles!cases_assigned_attorney_id_fkey(full_name, email)") + .order("updated_at", { ascending: false }); + setCases(data ?? []); + })(); + }, []); + + const filtered = cases.filter((c) => { + const okStatus = statusFilter === "all" || c.status === statusFilter; + const okQ = [c.title, c.case_number, c.client?.name].filter(Boolean).join(" ").toLowerCase().includes(q.toLowerCase()); + return okStatus && okQ; + }); + + return ( + + + New case + + } + /> + +
+
+ + setQ(e.target.value)} placeholder="Search cases…" className="pl-9" /> +
+ +
+ + + + + + + + + + + + + + + {filtered.length === 0 && ( + + )} + {filtered.map((c) => ( + + + + + + + + ))} + +
CaseClientStatusAttorneyOpened
No cases.
+ + {c.title} + +
{c.case_number}
+
{c.client?.name ?? "—"} + {c.status.replace("_", " ")} + {c.assignee?.full_name || c.assignee?.email || "—"}{formatDate(c.opened_at)}
+
+
+
+ ); +} diff --git a/src/routes/cases.new.tsx b/src/routes/cases.new.tsx new file mode 100644 index 0000000..98b61da --- /dev/null +++ b/src/routes/cases.new.tsx @@ -0,0 +1,192 @@ +import { createFileRoute, useNavigate, Link } from "@tanstack/react-router"; +import { z } from "zod"; +import { useEffect, 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 { ArrowLeft, Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +const search = z.object({ clientId: z.string().optional() }); + +export const Route = createFileRoute("/cases/new")({ + validateSearch: search, + component: () => ( + + + + ), +}); + +function NewCase() { + const { clientId } = Route.useSearch(); + const navigate = useNavigate(); + const { user } = useAuth(); + const [clients, setClients] = useState([]); + const [users, setUsers] = useState([]); + const [submitting, setSubmitting] = useState(false); + + const [form, setForm] = useState({ + client_id: clientId ?? "", + case_number: "", + title: "", + practice_area: "", + description: "", + status: "intake" as const, + assigned_attorney_id: user?.id ?? "", + default_hourly_rate: "", + }); + + useEffect(() => { + (async () => { + const [{ data: cs }, { data: us }] = await Promise.all([ + supabase.from("clients").select("id, name").order("name"), + supabase.from("profiles").select("id, full_name, email").order("full_name"), + ]); + setClients(cs ?? []); + setUsers(us ?? []); + })(); + }, []); + + useEffect(() => { + if (user?.id && !form.assigned_attorney_id) { + setForm((f) => ({ ...f, assigned_attorney_id: user.id })); + } + }, [user?.id]); + + // Auto-generate case number suggestion + useEffect(() => { + if (!form.case_number) { + const yr = new Date().getFullYear(); + const rand = Math.floor(1000 + Math.random() * 9000); + setForm((f) => ({ ...f, case_number: `${yr}-${rand}` })); + } + }, []); + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!form.client_id || !form.title.trim() || !form.case_number.trim()) { + toast.error("Please fill in client, title, and case number."); + return; + } + setSubmitting(true); + const payload = { + client_id: form.client_id, + case_number: form.case_number.trim(), + title: form.title.trim(), + practice_area: form.practice_area.trim() || null, + description: form.description.trim() || null, + status: form.status, + assigned_attorney_id: form.assigned_attorney_id || null, + default_hourly_rate: form.default_hourly_rate ? parseFloat(form.default_hourly_rate) : null, + created_by: user?.id, + }; + const { data, error } = await supabase.from("cases").insert(payload).select("id").single(); + setSubmitting(false); + if (error) { + toast.error("Could not create case", { description: error.message }); + return; + } + toast.success("Case created"); + navigate({ to: "/cases/$caseId", params: { caseId: data.id } }); + }; + + return ( + + + + + + +
+
+
+ + +
+
+ + setForm({ ...form, case_number: e.target.value })} required maxLength={50} /> +
+
+
+ + setForm({ ...form, title: e.target.value })} required maxLength={200} /> +
+
+
+ + setForm({ ...form, practice_area: e.target.value })} + placeholder="HOA collections, Litigation, Transactional…" + maxLength={120} + /> +
+
+ + +
+
+
+
+ + +
+
+ + setForm({ ...form, default_hourly_rate: e.target.value })} + /> +
+
+
+ +