diff --git a/src/components/trust/trust-account-panel.tsx b/src/components/trust/trust-account-panel.tsx new file mode 100644 index 0000000..a46df08 --- /dev/null +++ b/src/components/trust/trust-account-panel.tsx @@ -0,0 +1,421 @@ +import { useEffect, useState, useCallback } from "react"; +import { Link } from "@tanstack/react-router"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/lib/auth"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Wallet, Plus, ArrowDownToLine, ArrowUpFromLine, Trash2, Loader2 } from "lucide-react"; +import { formatCurrency, formatDate } from "@/lib/format"; +import { toast } from "sonner"; + +interface Entry { + id: string; + entry_date: string; + entry_type: "deposit" | "withdrawal"; + amount: number; + note: string | null; + source_invoice_id: string | null; + applied_invoice_id: string | null; + created_by: string | null; +} + +export function TrustAccountPanel({ clientId }: { clientId: string }) { + const { user, isAdmin } = useAuth(); + const [entries, setEntries] = useState([]); + const [loading, setLoading] = useState(true); + const [adjustOpen, setAdjustOpen] = useState(null); + + const load = useCallback(async () => { + setLoading(true); + const { data } = await supabase + .from("trust_ledger_entries") + .select("*") + .eq("client_id", clientId) + .order("entry_date", { ascending: false }) + .order("created_at", { ascending: false }); + setEntries((data ?? []) as Entry[]); + setLoading(false); + }, [clientId]); + + useEffect(() => { + load(); + const ch = supabase + .channel(`trust-${clientId}`) + .on( + "postgres_changes", + { event: "*", schema: "public", table: "trust_ledger_entries", filter: `client_id=eq.${clientId}` }, + () => load(), + ) + .subscribe(); + return () => { + supabase.removeChannel(ch); + }; + }, [clientId, load]); + + const balance = entries.reduce( + (sum, e) => sum + (e.entry_type === "deposit" ? Number(e.amount) : -Number(e.amount)), + 0, + ); + + const removeEntry = async (id: string) => { + if (!confirm("Remove this trust ledger entry? This will not reverse the related invoice payment.")) return; + const { error } = await supabase.from("trust_ledger_entries").delete().eq("id", id); + if (error) return toast.error(error.message); + toast.success("Entry removed"); + load(); + }; + + return ( + + +
+
+ +

Trust account

+
+
+ + +
+
+ +
+ Available balance + + {formatCurrency(balance)} + +
+ +
+
Ledger
+ {loading ? ( +

Loading…

+ ) : entries.length === 0 ? ( +

+ No activity yet. Mark an invoice as a retainer and record payment to fund this account. +

+ ) : ( +
+ {entries.map((e) => { + const isDeposit = e.entry_type === "deposit"; + const link = isDeposit ? e.source_invoice_id : e.applied_invoice_id; + const canDelete = isAdmin || e.created_by === user?.id; + return ( +
+
+
+ {isDeposit ? ( + + ) : ( + + )} +
+
+
+ {isDeposit ? "Deposit" : "Withdrawal"} + {e.note ? · {e.note} : null} +
+
+ {formatDate(e.entry_date)} + {link && ( + <> + {" · "} + + {isDeposit ? "from invoice" : "to invoice"} + + + )} +
+
+
+
+ + {isDeposit ? "+" : "−"} + {formatCurrency(e.amount)} + + {canDelete && ( + + )} +
+
+ ); + })} +
+ )} +
+
+ + !v && setAdjustOpen(null)} + clientId={clientId} + userId={user?.id ?? null} + balance={balance} + onSaved={load} + /> +
+ ); +} + +function ManualAdjustDialog({ + open, + type, + onOpenChange, + clientId, + userId, + balance, + onSaved, +}: { + open: boolean; + type: "deposit" | "withdrawal"; + onOpenChange: (b: boolean) => void; + clientId: string; + userId: string | null; + balance: number; + onSaved: () => void; +}) { + const [amount, setAmount] = useState(""); + const [date, setDate] = useState(new Date().toISOString().slice(0, 10)); + const [note, setNote] = useState(""); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (open) { + setAmount(""); + setDate(new Date().toISOString().slice(0, 10)); + setNote(""); + } + }, [open]); + + const save = async () => { + const amt = Number(amount); + if (!amt || amt <= 0) return toast.error("Enter a valid amount"); + if (type === "withdrawal" && amt > balance) { + if (!confirm(`This will overdraw the trust by ${formatCurrency(amt - balance)}. Continue?`)) return; + } + setSaving(true); + const { error } = await supabase.from("trust_ledger_entries").insert({ + client_id: clientId, + entry_date: date, + entry_type: type, + amount: amt, + note: note || (type === "deposit" ? "Manual deposit" : "Manual withdrawal"), + created_by: userId, + }); + setSaving(false); + if (error) return toast.error(error.message); + toast.success(type === "deposit" ? "Deposit recorded" : "Withdrawal recorded"); + onOpenChange(false); + onSaved(); + }; + + return ( + + + + {type === "deposit" ? "Record trust deposit" : "Record trust withdrawal"} + +
+
+
+ + setAmount(e.target.value)} /> +
+
+ + setDate(e.target.value)} /> +
+
+
+ +