Added trust entry dialog

X-Lovable-Edit-ID: edt-1794cffd-8a7e-454c-993b-1d1b553ca9dc
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-14 23:18:09 +00:00
co-authored by renee-png
2 changed files with 169 additions and 11 deletions
+14 -6
View File
@@ -497,9 +497,12 @@ function InvoiceDetail() {
</tbody>
</table>
{canModify && (
<div className="border-t px-3 py-2 bg-muted/20">
<Button variant="ghost" size="sm" onClick={() => addManualItem(g.caseRow?.id ?? null)} disabled={savingItem}>
<Plus className="h-3.5 w-3.5 mr-1.5" /> Add line to {g.caseRow?.case_number ?? "this group"}
<div className="border-t px-3 py-2 bg-muted/20 flex gap-2">
<Button variant="ghost" size="sm" onClick={() => addManualItem(g.caseRow?.id ?? null, "manual")} disabled={savingItem}>
<Plus className="h-3.5 w-3.5 mr-1.5" /> Add fee
</Button>
<Button variant="ghost" size="sm" onClick={() => addManualItem(g.caseRow?.id ?? null, "expense")} disabled={savingItem}>
<Plus className="h-3.5 w-3.5 mr-1.5" /> Add expense
</Button>
</div>
)}
@@ -508,9 +511,14 @@ function InvoiceDetail() {
))}
{canModify && groups.length === 0 && (
<Button variant="outline" onClick={() => addManualItem(null)}>
<Plus className="h-4 w-4 mr-2" /> Add first line item
</Button>
<div className="flex gap-2">
<Button variant="outline" onClick={() => addManualItem(null, "manual")}>
<Plus className="h-4 w-4 mr-2" /> Add fee line
</Button>
<Button variant="outline" onClick={() => addManualItem(null, "expense")}>
<Plus className="h-4 w-4 mr-2" /> Add expense line
</Button>
</div>
)}
<Card className="border-border/60">
+155 -5
View File
@@ -5,10 +5,20 @@ 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 { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from "@/components/ui/dialog";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
import { formatCurrency, formatDate } from "@/lib/format";
import { Search, Wallet, Download, ArrowDownToLine, ArrowUpFromLine } from "lucide-react";
import { Search, Wallet, Download, ArrowDownToLine, ArrowUpFromLine, Loader2 } from "lucide-react";
import { toast } from "sonner";
export const Route = createFileRoute("/trust/")({
component: () => (
@@ -47,6 +57,7 @@ function TrustLedgerPage() {
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [view, setView] = useState<"all" | "active" | "archived">("active");
const [entryDialog, setEntryDialog] = useState<null | "deposit" | "withdrawal">(null);
useEffect(() => {
(async () => {
@@ -142,12 +153,28 @@ function TrustLedgerPage() {
title="Trust accounting"
description="All client trust balances and ledger activity."
actions={
<Button variant="outline" size="sm" onClick={exportCsv}>
<Download className="h-3.5 w-3.5 mr-1.5" /> Export CSV
</Button>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => setEntryDialog("deposit")}>
<ArrowDownToLine className="h-3.5 w-3.5 mr-1.5" /> Record deposit
</Button>
<Button variant="outline" size="sm" onClick={() => setEntryDialog("withdrawal")}>
<ArrowUpFromLine className="h-3.5 w-3.5 mr-1.5" /> Record withdrawal
</Button>
<Button variant="outline" size="sm" onClick={exportCsv}>
<Download className="h-3.5 w-3.5 mr-1.5" /> Export CSV
</Button>
</div>
}
/>
<RecordEntryDialog
open={!!entryDialog}
type={entryDialog ?? "deposit"}
clients={Array.from(clients.values())}
onClose={() => setEntryDialog(null)}
onSaved={(e) => { setEntries((p) => [e, ...p]); setEntryDialog(null); }}
/>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<Card>
<CardContent className="p-4">
@@ -285,4 +312,127 @@ function TrustLedgerPage() {
</Card>
</PageContainer>
);
}
}
function RecordEntryDialog({
open, type, clients, onClose, onSaved,
}: {
open: boolean;
type: "deposit" | "withdrawal";
clients: ClientLite[];
onClose: () => void;
onSaved: (entry: Entry) => void;
}) {
const { user } = useAuth();
const [clientId, setClientId] = useState<string>("");
const [caseId, setCaseId] = useState<string>("_pool");
const [cases, setCases] = useState<{ id: string; case_number: string | null; title: string | null }[]>([]);
const [date, setDate] = useState<string>(new Date().toISOString().slice(0, 10));
const [amount, setAmount] = useState<string>("");
const [note, setNote] = useState<string>("");
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!open) {
setClientId(""); setCaseId("_pool"); setCases([]);
setAmount(""); setNote(""); setDate(new Date().toISOString().slice(0, 10));
}
}, [open]);
useEffect(() => {
if (!clientId) { setCases([]); return; }
(async () => {
const { data } = await supabase
.from("cases")
.select("id, case_number, title")
.eq("client_id", clientId)
.is("archived_at", null)
.order("opened_at", { ascending: false });
setCases(data ?? []);
})();
}, [clientId]);
const sortedClients = useMemo(
() => [...clients].filter((c) => !c.archived_at).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? "")),
[clients],
);
const save = async () => {
if (!clientId) return toast.error("Pick a client");
const amt = Number(amount);
if (!amt || amt <= 0) return toast.error("Enter a valid amount");
setSaving(true);
const { data, error } = await supabase
.from("trust_ledger_entries")
.insert({
client_id: clientId,
case_id: caseId === "_pool" ? null : caseId,
entry_date: date,
entry_type: type,
amount: amt,
note: note || null,
created_by: user?.id ?? null,
})
.select("*")
.single();
setSaving(false);
if (error) return toast.error(error.message);
toast.success(`${type === "deposit" ? "Deposit" : "Withdrawal"} recorded`);
onSaved(data as Entry);
};
return (
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
<DialogContent>
<DialogHeader>
<DialogTitle>Record {type}</DialogTitle>
</DialogHeader>
<div className="space-y-3">
<div>
<Label>Client</Label>
<Select value={clientId} onValueChange={setClientId}>
<SelectTrigger><SelectValue placeholder="Select client…" /></SelectTrigger>
<SelectContent>
{sortedClients.map((c) => (
<SelectItem key={c.id} value={c.id}>{c.name ?? "Unnamed"}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Case (optional)</Label>
<Select value={caseId} onValueChange={setCaseId} disabled={!clientId}>
<SelectTrigger><SelectValue placeholder="Client trust pool" /></SelectTrigger>
<SelectContent>
<SelectItem value="_pool">Client trust pool (no case)</SelectItem>
{cases.map((c) => (
<SelectItem key={c.id} value={c.id}>{c.case_number ?? c.title}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label>Date</Label>
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
</div>
<div>
<Label>Amount</Label>
<Input type="number" step="0.01" placeholder="0.00" value={amount} onChange={(e) => setAmount(e.target.value)} />
</div>
</div>
<div>
<Label>Note (optional)</Label>
<Textarea rows={2} value={note} onChange={(e) => setNote(e.target.value)} placeholder="Reference, source, purpose…" />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>Cancel</Button>
<Button onClick={save} disabled={saving}>
{saving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : null}
Record {type}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}