Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-17 01:45:54 +00:00
co-authored by renee-png
parent 2236a45d91
commit 3483e065ec
7 changed files with 869 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { supabase } from "@/integrations/supabase/client";
import { formatDate } from "@/lib/format";
import { FilePlus2, FileText, Gavel, LayoutTemplate, Plus, Download, Trash2 } from "lucide-react";
import { toast } from "sonner";
export const Route = createFileRoute("/documents/")({
component: DocumentsIndex,
});
function DocumentsIndex() {
const [generated, setGenerated] = useState<any[]>([]);
const [templates, setTemplates] = useState<any[]>([]);
const load = async () => {
const [g, t] = await Promise.all([
supabase.from("generated_documents").select("*").order("created_at", { ascending: false }).limit(20),
supabase.from("document_templates").select("*").order("name"),
]);
setGenerated(g.data ?? []);
setTemplates(t.data ?? []);
};
useEffect(() => { load(); }, []);
const removeGenerated = async (row: any) => {
if (!confirm(`Delete ${row.name}?`)) return;
if (row.storage_path) await supabase.storage.from("generated-documents").remove([row.storage_path]);
const { error } = await supabase.from("generated_documents").delete().eq("id", row.id);
if (error) toast.error(error.message); else { toast.success("Deleted"); load(); }
};
const downloadGenerated = async (row: any) => {
if (!row.storage_path) { toast.error("No file stored for this document"); return; }
const { data, error } = await supabase.storage.from("generated-documents").createSignedUrl(row.storage_path, 60);
if (error) { toast.error(error.message); return; }
window.open(data.signedUrl, "_blank");
};
return (
<ProtectedLayout>
<PageContainer>
<PageHeader
title="Documents"
description="Create pleadings and reusable document templates."
actions={
<>
<Button asChild variant="outline">
<Link to="/documents/templates">
<LayoutTemplate className="h-4 w-4 mr-2" /> Templates
</Link>
</Button>
<Button asChild>
<Link to="/documents/pleading/new">
<Gavel className="h-4 w-4 mr-2" /> New Pleading
</Link>
</Button>
</>
}
/>
<div className="grid md:grid-cols-3 gap-4 mb-8">
<Link to="/documents/pleading/new">
<Card className="hover:border-primary/40 transition-colors cursor-pointer h-full">
<CardContent className="p-5 flex items-start gap-3">
<div className="h-10 w-10 rounded-md bg-primary/10 text-primary flex items-center justify-center"><Gavel className="h-5 w-5" /></div>
<div>
<div className="font-medium">Florida Pleading</div>
<div className="text-xs text-muted-foreground mt-1">Court caption header in Bookman Old Style 12pt.</div>
</div>
</CardContent>
</Card>
</Link>
<Link to="/documents/templates/new">
<Card className="hover:border-primary/40 transition-colors cursor-pointer h-full">
<CardContent className="p-5 flex items-start gap-3">
<div className="h-10 w-10 rounded-md bg-primary/10 text-primary flex items-center justify-center"><FilePlus2 className="h-5 w-5" /></div>
<div>
<div className="font-medium">New template</div>
<div className="text-xs text-muted-foreground mt-1">Define custom merge fields and reuse them.</div>
</div>
</CardContent>
</Card>
</Link>
<Link to="/documents/templates">
<Card className="hover:border-primary/40 transition-colors cursor-pointer h-full">
<CardContent className="p-5 flex items-start gap-3">
<div className="h-10 w-10 rounded-md bg-primary/10 text-primary flex items-center justify-center"><LayoutTemplate className="h-5 w-5" /></div>
<div>
<div className="font-medium">Template library</div>
<div className="text-xs text-muted-foreground mt-1">{templates.length} template{templates.length === 1 ? "" : "s"} available.</div>
</div>
</CardContent>
</Card>
</Link>
</div>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">Recent documents</CardTitle>
</CardHeader>
<CardContent className="p-0">
{generated.length === 0 ? (
<div className="text-center py-12 text-muted-foreground text-sm">No documents yet. Create a pleading or use a template.</div>
) : (
<table className="w-full text-sm">
<thead className="bg-muted/40 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">Type</th>
<th className="text-left px-4 py-3 font-medium">Created</th>
<th className="text-right px-4 py-3 font-medium">Actions</th>
</tr>
</thead>
<tbody>
{generated.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"><Badge variant="outline" className="capitalize">{d.kind}</Badge></td>
<td className="px-4 py-3 text-muted-foreground">{formatDate(d.created_at)}</td>
<td className="px-4 py-3 text-right">
{d.storage_path && (
<Button variant="ghost" size="icon" onClick={() => downloadGenerated(d)}><Download className="h-4 w-4" /></Button>
)}
<Button variant="ghost" size="icon" onClick={() => removeGenerated(d)}><Trash2 className="h-4 w-4 text-destructive" /></Button>
</td>
</tr>
))}
</tbody>
</table>
)}
</CardContent>
</Card>
</PageContainer>
</ProtectedLayout>
);
}