Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
8074288b1d
commit
cc3aec5ef8
@@ -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>>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user