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:13:05 +00:00
co-authored by renee-png
parent 8074288b1d
commit cc3aec5ef8
2 changed files with 190 additions and 0 deletions
+30
View File
@@ -14,6 +14,7 @@ import { Route as SettingsRouteImport } from './routes/settings'
import { Route as LoginRouteImport } from './routes/login'
import { Route as IndexRouteImport } from './routes/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'
import { Route as CollectionsIndexRouteImport } from './routes/collections.index'
import { Route as ClientsIndexRouteImport } from './routes/clients.index'
@@ -55,6 +56,11 @@ const SettingsIndexRoute = SettingsIndexRouteImport.update({
path: '/',
getParentRoute: () => SettingsRoute,
} as any)
const FilesIndexRoute = FilesIndexRouteImport.update({
id: '/files/',
path: '/files/',
getParentRoute: () => rootRouteImport,
} as any)
const DocumentsIndexRoute = DocumentsIndexRouteImport.update({
id: '/documents/',
path: '/documents/',
@@ -148,6 +154,7 @@ export interface FileRoutesByFullPath {
'/clients/': typeof ClientsIndexRoute
'/collections/': typeof CollectionsIndexRoute
'/documents/': typeof DocumentsIndexRoute
'/files/': typeof FilesIndexRoute
'/settings/': typeof SettingsIndexRoute
'/documents/pleading/new': typeof DocumentsPleadingNewRoute
'/documents/templates/$templateId': typeof DocumentsTemplatesTemplateIdRoute
@@ -169,6 +176,7 @@ export interface FileRoutesByTo {
'/clients': typeof ClientsIndexRoute
'/collections': typeof CollectionsIndexRoute
'/documents': typeof DocumentsIndexRoute
'/files': typeof FilesIndexRoute
'/settings': typeof SettingsIndexRoute
'/documents/pleading/new': typeof DocumentsPleadingNewRoute
'/documents/templates/$templateId': typeof DocumentsTemplatesTemplateIdRoute
@@ -192,6 +200,7 @@ export interface FileRoutesById {
'/clients/': typeof ClientsIndexRoute
'/collections/': typeof CollectionsIndexRoute
'/documents/': typeof DocumentsIndexRoute
'/files/': typeof FilesIndexRoute
'/settings/': typeof SettingsIndexRoute
'/documents/pleading/new': typeof DocumentsPleadingNewRoute
'/documents/templates/$templateId': typeof DocumentsTemplatesTemplateIdRoute
@@ -216,6 +225,7 @@ export interface FileRouteTypes {
| '/clients/'
| '/collections/'
| '/documents/'
| '/files/'
| '/settings/'
| '/documents/pleading/new'
| '/documents/templates/$templateId'
@@ -237,6 +247,7 @@ export interface FileRouteTypes {
| '/clients'
| '/collections'
| '/documents'
| '/files'
| '/settings'
| '/documents/pleading/new'
| '/documents/templates/$templateId'
@@ -259,6 +270,7 @@ export interface FileRouteTypes {
| '/clients/'
| '/collections/'
| '/documents/'
| '/files/'
| '/settings/'
| '/documents/pleading/new'
| '/documents/templates/$templateId'
@@ -280,6 +292,7 @@ export interface RootRouteChildren {
ClientsIndexRoute: typeof ClientsIndexRoute
CollectionsIndexRoute: typeof CollectionsIndexRoute
DocumentsIndexRoute: typeof DocumentsIndexRoute
FilesIndexRoute: typeof FilesIndexRoute
DocumentsPleadingNewRoute: typeof DocumentsPleadingNewRoute
DocumentsTemplatesTemplateIdRoute: typeof DocumentsTemplatesTemplateIdRoute
DocumentsTemplatesNewRoute: typeof DocumentsTemplatesNewRoute
@@ -323,6 +336,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SettingsIndexRouteImport
parentRoute: typeof SettingsRoute
}
'/files/': {
id: '/files/'
path: '/files'
fullPath: '/files/'
preLoaderRoute: typeof FilesIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/documents/': {
id: '/documents/'
path: '/documents'
@@ -461,6 +481,7 @@ const rootRouteChildren: RootRouteChildren = {
ClientsIndexRoute: ClientsIndexRoute,
CollectionsIndexRoute: CollectionsIndexRoute,
DocumentsIndexRoute: DocumentsIndexRoute,
FilesIndexRoute: FilesIndexRoute,
DocumentsPleadingNewRoute: DocumentsPleadingNewRoute,
DocumentsTemplatesTemplateIdRoute: DocumentsTemplatesTemplateIdRoute,
DocumentsTemplatesNewRoute: DocumentsTemplatesNewRoute,
@@ -469,3 +490,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>>
}
}
+160
View File
@@ -0,0 +1,160 @@
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 { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { supabase } from "@/integrations/supabase/client";
import { Download, FileText, Search, Folder } from "lucide-react";
import { formatDate } from "@/lib/format";
import { toast } from "sonner";
export const Route = createFileRoute("/files/")({
component: () => (
<ProtectedLayout>
<FilesPage />
</ProtectedLayout>
),
});
function FilesPage() {
const [docs, setDocs] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [q, setQ] = useState("");
const load = async () => {
setLoading(true);
const { data, error } = await supabase
.from("documents")
.select("*")
.order("created_at", { ascending: false })
.limit(1000);
if (error) { toast.error("Could not load files", { description: error.message }); setLoading(false); return; }
const caseIds = Array.from(new Set((data ?? []).map((d: any) => d.case_id).filter(Boolean)));
const uploaderIds = Array.from(new Set((data ?? []).map((d: any) => d.uploaded_by).filter(Boolean)));
const [{ data: cases }, { data: profs }] = await Promise.all([
caseIds.length
? supabase
.from("cases")
.select("id, case_number, title, client:clients(id, name)")
.in("id", caseIds)
: Promise.resolve({ data: [] as any[] }),
uploaderIds.length
? supabase.from("profiles").select("id, full_name, email").in("id", uploaderIds)
: Promise.resolve({ data: [] as any[] }),
]);
const caseMap = Object.fromEntries((cases ?? []).map((c: any) => [c.id, c]));
const profMap = Object.fromEntries((profs ?? []).map((p: any) => [p.id, p]));
setDocs(
(data ?? []).map((d: any) => ({
...d,
case: caseMap[d.case_id],
uploader: d.uploaded_by ? profMap[d.uploaded_by] : null,
})),
);
setLoading(false);
};
useEffect(() => { load(); }, []);
const filtered = useMemo(() => {
const s = q.trim().toLowerCase();
if (!s) return docs;
return docs.filter((d) =>
[d.name, d.folder, d.case?.case_number, d.case?.title, d.case?.client?.name, d.uploader?.full_name, d.uploader?.email]
.filter(Boolean)
.some((v: string) => String(v).toLowerCase().includes(s)),
);
}, [docs, q]);
const download = async (doc: any) => {
const { data, error } = await supabase.storage.from("case-documents").createSignedUrl(doc.storage_path, 60);
if (error) { toast.error(error.message); return; }
window.open(data.signedUrl, "_blank");
};
return (
<PageContainer>
<PageHeader
title="Files"
description="All documents uploaded across the cases you can access."
/>
<Card className="border-border/60 mb-4">
<CardContent className="p-3 flex items-center gap-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="Search by file, folder, case, client, or uploader…"
className="border-0 focus-visible:ring-0 shadow-none"
/>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{filtered.length} of {docs.length}
</span>
</CardContent>
</Card>
<Card className="border-border/60 overflow-hidden">
<CardContent className="p-0">
<table className="w-full text-sm">
<thead className="bg-muted/50 text-xs uppercase tracking-wider text-muted-foreground">
<tr>
<th className="text-left px-4 py-3 font-medium">Name</th>
<th className="text-left px-4 py-3 font-medium">Case</th>
<th className="text-left px-4 py-3 font-medium">Folder</th>
<th className="text-left px-4 py-3 font-medium">Uploaded by</th>
<th className="text-left px-4 py-3 font-medium">Date</th>
<th className="text-right px-4 py-3 font-medium">Size</th>
<th></th>
</tr>
</thead>
<tbody>
{loading && (
<tr><td colSpan={7} className="text-center py-12 text-muted-foreground">Loading…</td></tr>
)}
{!loading && filtered.length === 0 && (
<tr><td colSpan={7} className="text-center py-12 text-muted-foreground">No files found.</td></tr>
)}
{filtered.map((d) => (
<tr key={d.id} className="border-t hover:bg-muted/30">
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<FileText className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{d.name}</span>
</div>
</td>
<td className="px-4 py-3">
{d.case ? (
<Link to="/cases/$caseId" params={{ caseId: d.case.id }} className="hover:text-primary">
<div className="text-sm">{d.case.title}</div>
<div className="text-[11px] text-muted-foreground">
{d.case.case_number}{d.case.client ? ` · ${d.case.client.name}` : ""}
</div>
</Link>
) : <span className="text-muted-foreground">—</span>}
</td>
<td className="px-4 py-3 text-muted-foreground">
{d.folder ? (
<span className="inline-flex items-center gap-1.5"><Folder className="h-3.5 w-3.5" />{d.folder}</span>
) : <span className="text-xs">root</span>}
</td>
<td className="px-4 py-3 text-muted-foreground">{d.uploader?.full_name || d.uploader?.email || "—"}</td>
<td className="px-4 py-3 text-muted-foreground whitespace-nowrap">{formatDate(d.created_at)}</td>
<td className="px-4 py-3 text-right tabular-nums text-muted-foreground">
{d.size_bytes ? `${(d.size_bytes / 1024).toFixed(1)} KB` : "—"}
</td>
<td className="px-4 py-3 text-right">
<Button variant="ghost" size="icon" onClick={() => download(d)}><Download className="h-4 w-4" /></Button>
</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
</PageContainer>
);
}