diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index febe8ae..3fdecba 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -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() + +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/files.index.tsx b/src/routes/files.index.tsx new file mode 100644 index 0000000..40ecdb6 --- /dev/null +++ b/src/routes/files.index.tsx @@ -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: () => ( + + + + ), +}); + +function FilesPage() { + const [docs, setDocs] = useState([]); + 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 ( + + + + + + + setQ(e.target.value)} + placeholder="Search by file, folder, case, client, or uploader…" + className="border-0 focus-visible:ring-0 shadow-none" + /> + + {filtered.length} of {docs.length} + + + + + + + + + + + + + + + + + + + + {loading && ( + + )} + {!loading && filtered.length === 0 && ( + + )} + {filtered.map((d) => ( + + + + + + + + + + ))} + +
NameCaseFolderUploaded byDateSize
Loading…
No files found.
+
+ + {d.name} +
+
+ {d.case ? ( + +
{d.case.title}
+
+ {d.case.case_number}{d.case.client ? ` · ${d.case.client.name}` : ""} +
+ + ) : —} +
+ {d.folder ? ( + {d.folder} + ) : root} + {d.uploader?.full_name || d.uploader?.email || "—"}{formatDate(d.created_at)} + {d.size_bytes ? `${(d.size_bytes / 1024).toFixed(1)} KB` : "—"} + + +
+
+
+
+ ); +}