Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
616bc3b2ba
commit
8f67f02944
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { useAuth } from "@/lib/auth";
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Wallet, Plus, ArrowDownToLine, ArrowUpFromLine, Trash2, Loader2 } from "lucide-react";
|
||||
import { Wallet, Plus, ArrowDownToLine, ArrowUpFromLine, Trash2, Loader2, Briefcase } from "lucide-react";
|
||||
import { formatCurrency, formatDate } from "@/lib/format";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -31,33 +31,68 @@ interface Entry {
|
||||
entry_type: "deposit" | "withdrawal";
|
||||
amount: number;
|
||||
note: string | null;
|
||||
case_id: string | null;
|
||||
source_invoice_id: string | null;
|
||||
applied_invoice_id: string | null;
|
||||
created_by: string | null;
|
||||
}
|
||||
|
||||
export function TrustAccountPanel({ clientId }: { clientId: string }) {
|
||||
interface CaseOption {
|
||||
id: string;
|
||||
case_number: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const POOL_KEY = "_pool";
|
||||
|
||||
/**
|
||||
* Trust panel scoped to a client. If `caseId` is provided, the panel only
|
||||
* shows that case's trust activity (used on the case detail page).
|
||||
*/
|
||||
export function TrustAccountPanel({
|
||||
clientId,
|
||||
caseId,
|
||||
hideCasePicker,
|
||||
}: {
|
||||
clientId: string;
|
||||
caseId?: string;
|
||||
hideCasePicker?: boolean;
|
||||
}) {
|
||||
const { user, isAdmin } = useAuth();
|
||||
const [entries, setEntries] = useState<Entry[]>([]);
|
||||
const [cases, setCases] = useState<CaseOption[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [adjustOpen, setAdjustOpen] = useState<null | "deposit" | "withdrawal">(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const { data } = await supabase
|
||||
let q = supabase
|
||||
.from("trust_ledger_entries")
|
||||
.select("*")
|
||||
.eq("client_id", clientId)
|
||||
.order("entry_date", { ascending: false })
|
||||
.order("created_at", { ascending: false });
|
||||
setEntries((data ?? []) as Entry[]);
|
||||
if (caseId) q = q.eq("case_id", caseId);
|
||||
const [entriesRes, casesRes] = await Promise.all([
|
||||
q,
|
||||
caseId
|
||||
? Promise.resolve({ data: [] as any[] })
|
||||
: supabase
|
||||
.from("cases")
|
||||
.select("id, case_number, title")
|
||||
.eq("client_id", clientId)
|
||||
.is("archived_at", null)
|
||||
.order("opened_at", { ascending: false }),
|
||||
]);
|
||||
setEntries((entriesRes.data ?? []) as Entry[]);
|
||||
setCases(((casesRes.data ?? []) as CaseOption[]));
|
||||
setLoading(false);
|
||||
}, [clientId]);
|
||||
}, [clientId, caseId]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
const ch = supabase
|
||||
.channel(`trust-${clientId}`)
|
||||
.channel(`trust-${clientId}-${caseId ?? "all"}`)
|
||||
.on(
|
||||
"postgres_changes",
|
||||
{ event: "*", schema: "public", table: "trust_ledger_entries", filter: `client_id=eq.${clientId}` },
|
||||
@@ -67,13 +102,37 @@ export function TrustAccountPanel({ clientId }: { clientId: string }) {
|
||||
return () => {
|
||||
supabase.removeChannel(ch);
|
||||
};
|
||||
}, [clientId, load]);
|
||||
}, [clientId, caseId, load]);
|
||||
|
||||
const balance = entries.reduce(
|
||||
const totalBalance = entries.reduce(
|
||||
(sum, e) => sum + (e.entry_type === "deposit" ? Number(e.amount) : -Number(e.amount)),
|
||||
0,
|
||||
);
|
||||
|
||||
// Group entries by case bucket (POOL_KEY for null case_id) for the case-scoped breakdown
|
||||
const byCase = useMemo(() => {
|
||||
const map = new Map<string, { caseId: string | null; entries: Entry[]; balance: number }>();
|
||||
for (const e of entries) {
|
||||
const k = e.case_id ?? POOL_KEY;
|
||||
if (!map.has(k)) map.set(k, { caseId: e.case_id, entries: [], balance: 0 });
|
||||
const g = map.get(k)!;
|
||||
g.entries.push(e);
|
||||
g.balance += e.entry_type === "deposit" ? Number(e.amount) : -Number(e.amount);
|
||||
}
|
||||
// Order: client pool first, then named cases
|
||||
return Array.from(map.values()).sort((a, b) => {
|
||||
if (a.caseId == null) return -1;
|
||||
if (b.caseId == null) return 1;
|
||||
return 0;
|
||||
});
|
||||
}, [entries]);
|
||||
|
||||
const caseLookup = useMemo(() => {
|
||||
const m = new Map<string, CaseOption>();
|
||||
cases.forEach((c) => m.set(c.id, c));
|
||||
return m;
|
||||
}, [cases]);
|
||||
|
||||
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);
|
||||
@@ -82,13 +141,75 @@ export function TrustAccountPanel({ clientId }: { clientId: string }) {
|
||||
load();
|
||||
};
|
||||
|
||||
const renderEntryRow = (e: Entry) => {
|
||||
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 (
|
||||
<div key={e.id} className="flex items-start justify-between gap-3 py-2.5 text-sm">
|
||||
<div className="flex items-start gap-2 min-w-0">
|
||||
<div
|
||||
className={`mt-0.5 h-6 w-6 rounded-full flex items-center justify-center ${
|
||||
isDeposit
|
||||
? "bg-emerald-500/15 text-emerald-700 dark:text-emerald-400"
|
||||
: "bg-orange-500/15 text-orange-700 dark:text-orange-400"
|
||||
}`}
|
||||
>
|
||||
{isDeposit ? <ArrowDownToLine className="h-3 w-3" /> : <ArrowUpFromLine className="h-3 w-3" />}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium">
|
||||
{isDeposit ? "Deposit" : "Withdrawal"}
|
||||
{e.note ? <span className="text-muted-foreground font-normal"> · {e.note}</span> : null}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{formatDate(e.entry_date)}
|
||||
{link && (
|
||||
<>
|
||||
{" · "}
|
||||
<Link
|
||||
to="/invoices/$invoiceId"
|
||||
params={{ invoiceId: link }}
|
||||
className="hover:text-primary underline-offset-2 hover:underline"
|
||||
>
|
||||
{isDeposit ? "from invoice" : "to invoice"}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<span
|
||||
className={`tabular-nums font-medium ${
|
||||
isDeposit ? "text-emerald-700 dark:text-emerald-400" : "text-orange-700 dark:text-orange-400"
|
||||
}`}
|
||||
>
|
||||
{isDeposit ? "+" : "−"}
|
||||
{formatCurrency(e.amount)}
|
||||
</span>
|
||||
{canDelete && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => removeEntry(e.id)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border-border/60">
|
||||
<CardContent className="p-5 space-y-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wallet className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="font-serif text-lg">Trust account</h3>
|
||||
<h3 className="font-serif text-lg">{caseId ? "Case trust" : "Trust account"}</h3>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
<Button size="sm" variant="outline" onClick={() => setAdjustOpen("deposit")}>
|
||||
@@ -101,86 +222,71 @@ export function TrustAccountPanel({ clientId }: { clientId: string }) {
|
||||
</div>
|
||||
|
||||
<div className="rounded-md bg-muted/40 p-4 flex items-baseline justify-between">
|
||||
<span className="text-xs uppercase tracking-wider text-muted-foreground">Available balance</span>
|
||||
<span className="text-xs uppercase tracking-wider text-muted-foreground">
|
||||
{caseId ? "Case balance" : "Total balance"}
|
||||
</span>
|
||||
<span
|
||||
className={`font-serif text-2xl tabular-nums ${balance < 0 ? "text-destructive" : ""}`}
|
||||
className={`font-serif text-2xl tabular-nums ${totalBalance < 0 ? "text-destructive" : ""}`}
|
||||
>
|
||||
{formatCurrency(balance)}
|
||||
{formatCurrency(totalBalance)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Per-case breakdown (only on client view) */}
|
||||
{!caseId && byCase.length > 1 && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs uppercase tracking-wider text-muted-foreground">By case</div>
|
||||
<div className="rounded-md border divide-y">
|
||||
{byCase.map((g) => {
|
||||
const c = g.caseId ? caseLookup.get(g.caseId) : null;
|
||||
return (
|
||||
<div key={g.caseId ?? POOL_KEY} className="flex items-center justify-between px-3 py-2 text-sm">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Briefcase className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||
<div className="min-w-0">
|
||||
{g.caseId ? (
|
||||
c ? (
|
||||
<Link
|
||||
to="/cases/$caseId"
|
||||
params={{ caseId: g.caseId }}
|
||||
className="hover:text-primary truncate block"
|
||||
>
|
||||
<span className="font-medium">{c.case_number}</span>{" "}
|
||||
<span className="text-muted-foreground">— {c.title}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-muted-foreground italic">Case (archived)</span>
|
||||
)
|
||||
) : (
|
||||
<span className="font-medium">Client pool</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`tabular-nums font-medium ${g.balance < 0 ? "text-destructive" : ""}`}
|
||||
>
|
||||
{formatCurrency(g.balance)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="text-xs uppercase tracking-wider text-muted-foreground mb-2">Ledger</div>
|
||||
{loading ? (
|
||||
<p className="text-sm text-muted-foreground">Loading…</p>
|
||||
) : entries.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground italic">
|
||||
No activity yet. Mark an invoice as a retainer and record payment to fund this account.
|
||||
No activity yet.{" "}
|
||||
{caseId
|
||||
? "Push an invoice payment to trust to fund this case."
|
||||
: "Mark an invoice as a retainer or push a payment to trust to fund this account."}
|
||||
</p>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{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 (
|
||||
<div key={e.id} className="flex items-start justify-between gap-3 py-2.5 text-sm">
|
||||
<div className="flex items-start gap-2 min-w-0">
|
||||
<div
|
||||
className={`mt-0.5 h-6 w-6 rounded-full flex items-center justify-center ${
|
||||
isDeposit ? "bg-emerald-500/15 text-emerald-700 dark:text-emerald-400" : "bg-orange-500/15 text-orange-700 dark:text-orange-400"
|
||||
}`}
|
||||
>
|
||||
{isDeposit ? (
|
||||
<ArrowDownToLine className="h-3 w-3" />
|
||||
) : (
|
||||
<ArrowUpFromLine className="h-3 w-3" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium">
|
||||
{isDeposit ? "Deposit" : "Withdrawal"}
|
||||
{e.note ? <span className="text-muted-foreground font-normal"> · {e.note}</span> : null}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{formatDate(e.entry_date)}
|
||||
{link && (
|
||||
<>
|
||||
{" · "}
|
||||
<Link
|
||||
to="/invoices/$invoiceId"
|
||||
params={{ invoiceId: link }}
|
||||
className="hover:text-primary underline-offset-2 hover:underline"
|
||||
>
|
||||
{isDeposit ? "from invoice" : "to invoice"}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<span
|
||||
className={`tabular-nums font-medium ${isDeposit ? "text-emerald-700 dark:text-emerald-400" : "text-orange-700 dark:text-orange-400"}`}
|
||||
>
|
||||
{isDeposit ? "+" : "−"}
|
||||
{formatCurrency(e.amount)}
|
||||
</span>
|
||||
{canDelete && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => removeEntry(e.id)}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="divide-y">{entries.map(renderEntryRow)}</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -190,8 +296,11 @@ export function TrustAccountPanel({ clientId }: { clientId: string }) {
|
||||
type={adjustOpen ?? "deposit"}
|
||||
onOpenChange={(v) => !v && setAdjustOpen(null)}
|
||||
clientId={clientId}
|
||||
defaultCaseId={caseId ?? null}
|
||||
cases={cases}
|
||||
hideCasePicker={!!caseId || !!hideCasePicker}
|
||||
userId={user?.id ?? null}
|
||||
balance={balance}
|
||||
balance={totalBalance}
|
||||
onSaved={load}
|
||||
/>
|
||||
</Card>
|
||||
@@ -203,6 +312,9 @@ function ManualAdjustDialog({
|
||||
type,
|
||||
onOpenChange,
|
||||
clientId,
|
||||
defaultCaseId,
|
||||
cases,
|
||||
hideCasePicker,
|
||||
userId,
|
||||
balance,
|
||||
onSaved,
|
||||
@@ -211,6 +323,9 @@ function ManualAdjustDialog({
|
||||
type: "deposit" | "withdrawal";
|
||||
onOpenChange: (b: boolean) => void;
|
||||
clientId: string;
|
||||
defaultCaseId: string | null;
|
||||
cases: CaseOption[];
|
||||
hideCasePicker: boolean;
|
||||
userId: string | null;
|
||||
balance: number;
|
||||
onSaved: () => void;
|
||||
@@ -218,6 +333,7 @@ function ManualAdjustDialog({
|
||||
const [amount, setAmount] = useState("");
|
||||
const [date, setDate] = useState(new Date().toISOString().slice(0, 10));
|
||||
const [note, setNote] = useState("");
|
||||
const [caseChoice, setCaseChoice] = useState<string>(defaultCaseId ?? POOL_KEY);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -225,8 +341,9 @@ function ManualAdjustDialog({
|
||||
setAmount("");
|
||||
setDate(new Date().toISOString().slice(0, 10));
|
||||
setNote("");
|
||||
setCaseChoice(defaultCaseId ?? POOL_KEY);
|
||||
}
|
||||
}, [open]);
|
||||
}, [open, defaultCaseId]);
|
||||
|
||||
const save = async () => {
|
||||
const amt = Number(amount);
|
||||
@@ -237,6 +354,7 @@ function ManualAdjustDialog({
|
||||
setSaving(true);
|
||||
const { error } = await supabase.from("trust_ledger_entries").insert({
|
||||
client_id: clientId,
|
||||
case_id: caseChoice === POOL_KEY ? null : caseChoice,
|
||||
entry_date: date,
|
||||
entry_type: type,
|
||||
amount: amt,
|
||||
@@ -257,6 +375,22 @@ function ManualAdjustDialog({
|
||||
<DialogTitle>{type === "deposit" ? "Record trust deposit" : "Record trust withdrawal"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
{!hideCasePicker && (
|
||||
<div>
|
||||
<Label>Allocate to</Label>
|
||||
<Select value={caseChoice} onValueChange={setCaseChoice}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={POOL_KEY}>Client 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>Amount</Label>
|
||||
@@ -288,11 +422,16 @@ function ManualAdjustDialog({
|
||||
);
|
||||
}
|
||||
|
||||
/** Dialog used on an invoice detail page to apply trust funds toward its balance. */
|
||||
/**
|
||||
* Dialog used on an invoice detail page to apply trust funds toward its balance.
|
||||
* Funds are scoped to the invoice's own case (per-case trust). If the invoice
|
||||
* has no case, the client pool is used.
|
||||
*/
|
||||
export function ApplyTrustDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
clientId,
|
||||
caseId,
|
||||
invoiceId,
|
||||
invoiceNumber,
|
||||
balanceDue,
|
||||
@@ -302,6 +441,7 @@ export function ApplyTrustDialog({
|
||||
open: boolean;
|
||||
onOpenChange: (b: boolean) => void;
|
||||
clientId: string;
|
||||
caseId: string | null;
|
||||
invoiceId: string;
|
||||
invoiceNumber: string;
|
||||
balanceDue: number;
|
||||
@@ -316,10 +456,13 @@ export function ApplyTrustDialog({
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
(async () => {
|
||||
const { data } = await supabase
|
||||
let q = supabase
|
||||
.from("trust_ledger_entries")
|
||||
.select("entry_type, amount")
|
||||
.select("entry_type, amount, case_id")
|
||||
.eq("client_id", clientId);
|
||||
if (caseId) q = q.eq("case_id", caseId);
|
||||
else q = q.is("case_id", null);
|
||||
const { data } = await q;
|
||||
const bal = (data ?? []).reduce(
|
||||
(s: number, e: any) => s + (e.entry_type === "deposit" ? Number(e.amount) : -Number(e.amount)),
|
||||
0,
|
||||
@@ -329,7 +472,7 @@ export function ApplyTrustDialog({
|
||||
setAmount(suggest > 0 ? suggest.toFixed(2) : "");
|
||||
setDate(new Date().toISOString().slice(0, 10));
|
||||
})();
|
||||
}, [open, clientId, balanceDue]);
|
||||
}, [open, clientId, caseId, balanceDue]);
|
||||
|
||||
const apply = async () => {
|
||||
const amt = Number(amount);
|
||||
@@ -356,9 +499,10 @@ export function ApplyTrustDialog({
|
||||
setSaving(false);
|
||||
return toast.error(payErr?.message ?? "Could not record payment");
|
||||
}
|
||||
// 2) Add the matching trust ledger withdrawal pointing at this invoice.
|
||||
// 2) Add the matching trust ledger withdrawal scoped to this invoice's case.
|
||||
const { error: ledErr } = await supabase.from("trust_ledger_entries").insert({
|
||||
client_id: clientId,
|
||||
case_id: caseId,
|
||||
entry_date: date,
|
||||
entry_type: "withdrawal",
|
||||
amount: amt,
|
||||
@@ -383,7 +527,9 @@ export function ApplyTrustDialog({
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-md bg-muted/40 p-3 text-sm space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Trust balance</span>
|
||||
<span className="text-muted-foreground">
|
||||
{caseId ? "Case trust balance" : "Client-pool trust balance"}
|
||||
</span>
|
||||
<span className="tabular-nums font-medium">
|
||||
{trustBalance == null ? "…" : formatCurrency(trustBalance)}
|
||||
</span>
|
||||
@@ -419,3 +565,103 @@ export function ApplyTrustDialog({
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Small button shown on each invoice payment row that lets the user move
|
||||
* (some or all of) that payment into the case's trust account.
|
||||
*/
|
||||
export function PushPaymentToTrustButton({
|
||||
payment,
|
||||
invoice,
|
||||
userId,
|
||||
onPushed,
|
||||
}: {
|
||||
payment: { id: string; amount: number; paid_on: string; method: string | null; reference: string | null };
|
||||
invoice: { id: string; invoice_number: string; client_id: string; case_id: string | null };
|
||||
userId: string | null;
|
||||
onPushed: () => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [amount, setAmount] = useState(Number(payment.amount).toFixed(2));
|
||||
const [date, setDate] = useState(payment.paid_on);
|
||||
const [note, setNote] = useState(
|
||||
`From payment on inv ${invoice.invoice_number}${payment.reference ? ` (${payment.reference})` : ""}`,
|
||||
);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setAmount(Number(payment.amount).toFixed(2));
|
||||
setDate(payment.paid_on);
|
||||
}
|
||||
}, [open, payment.amount, payment.paid_on]);
|
||||
|
||||
const push = async () => {
|
||||
const amt = Number(amount);
|
||||
if (!amt || amt <= 0) return toast.error("Enter a valid amount");
|
||||
setSaving(true);
|
||||
const { error } = await supabase.from("trust_ledger_entries").insert({
|
||||
client_id: invoice.client_id,
|
||||
case_id: invoice.case_id,
|
||||
entry_date: date,
|
||||
entry_type: "deposit",
|
||||
amount: amt,
|
||||
note,
|
||||
source_invoice_id: invoice.id,
|
||||
source_payment_id: payment.id,
|
||||
created_by: userId,
|
||||
});
|
||||
setSaving(false);
|
||||
if (error) return toast.error(error.message);
|
||||
toast.success("Pushed to trust");
|
||||
setOpen(false);
|
||||
onPushed();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-muted-foreground hover:text-emerald-600"
|
||||
title="Push to trust"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Wallet className="h-3 w-3" />
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Push payment to trust</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Records a trust deposit{invoice.case_id ? " on this case" : " in the client pool"}. The original
|
||||
invoice payment is left untouched.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>Amount</Label>
|
||||
<Input type="number" step="0.01" value={amount} onChange={(e) => setAmount(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Date</Label>
|
||||
<Input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Note</Label>
|
||||
<Textarea rows={2} value={note} onChange={(e) => setNote(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
|
||||
<Button onClick={push} disabled={saving}>
|
||||
{saving && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}Push to trust
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user