diff --git a/src/components/invoices/generate-invoice-dialog.tsx b/src/components/invoices/generate-invoice-dialog.tsx index 0fa0f5e..e4ad656 100644 --- a/src/components/invoices/generate-invoice-dialog.tsx +++ b/src/components/invoices/generate-invoice-dialog.tsx @@ -41,6 +41,7 @@ export function GenerateInvoiceDialog({ open, onOpenChange, clientId, clientName const [taxPct, setTaxPct] = useState("0"); const [dueDays, setDueDays] = useState("30"); const [notes, setNotes] = useState(""); + const [isRetainer, setIsRetainer] = useState(false); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); @@ -104,9 +105,53 @@ export function GenerateInvoiceDialog({ open, onOpenChange, clientId, clientName const submit = async () => { if (!user?.id) return; - if (selectedIds.length === 0) return toast.error("Select at least one case"); + if (selectedIds.length === 0 && !isRetainer) return toast.error("Select at least one case"); setSaving(true); try { + if (isRetainer) { + // Retainer invoices have no time/expenses — create a single line item for the deposit. + const yr = new Date().getFullYear(); + const num = `INV-${yr}-${Math.floor(1000 + Math.random() * 9000)}`; + const due = new Date(); + due.setDate(due.getDate() + (Number(dueDays) || 30)); + const amt = Math.max(0, Number((document.getElementById("retainer-amount") as HTMLInputElement)?.value) || 0); + if (amt <= 0) { + setSaving(false); + return toast.error("Enter a retainer amount"); + } + const tax = +(amt * (Number(taxPct) || 0) / 100).toFixed(2); + const { data: inv, error: ie } = await supabase + .from("invoices") + .insert({ + invoice_number: num, + client_id: clientId, + status: "draft", + issue_date: new Date().toISOString().slice(0, 10), + due_date: due.toISOString().slice(0, 10), + subtotal: amt, + tax, + total: +(amt + tax).toFixed(2), + notes: notes || "Retainer deposit", + is_retainer: true, + created_by: user.id, + }) + .select("id, invoice_number") + .single(); + if (ie || !inv) throw ie ?? new Error("Failed to create"); + await supabase.from("invoice_line_items").insert({ + invoice_id: inv.id, + kind: "manual", + description: "Retainer deposit into trust account", + quantity: 1, + rate: amt, + amount: amt, + sort_order: 0, + }); + toast.success(`Retainer invoice ${inv.invoice_number} created`); + onOpenChange(false); + navigate({ to: "/invoices/$invoiceId", params: { invoiceId: inv.id } }); + return; + } const { invoiceId, invoiceNumber } = await generateInvoiceForClient({ clientId, caseIds: selectedIds, @@ -132,39 +177,27 @@ export function GenerateInvoiceDialog({ open, onOpenChange, clientId, clientName Generate invoice — {clientName} - {loading ? ( -
Loading unbilled work…
- ) : cases.length === 0 ? ( -
- No unbilled time or expenses across this client's cases. -
- ) : ( -
-
- -
- {cases.map((c) => ( - - ))} +
+ + {isRetainer ? (
+
+ + +
setTaxPct(e.target.value)} /> @@ -173,27 +206,73 @@ export function GenerateInvoiceDialog({ open, onOpenChange, clientId, clientName setDueDays(e.target.value)} />
-
-
-
Estimated total
-
{formatCurrency(total)}
- {tax > 0 &&
{formatCurrency(subtotal)} + {formatCurrency(tax)} tax
} +
+ ) : loading ? ( +
Loading unbilled work…
+ ) : cases.length === 0 ? ( +
+ No unbilled time or expenses across this client's cases. +
+ ) : ( + <> +
+ +
+ {cases.map((c) => ( + + ))}
-
-
- -