Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
39b5036d0d
commit
e080623421
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
@@ -490,3 +511,12 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user