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:
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user