Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-18 01:09:04 +00:00
co-authored by renee-png
parent 6fa8e0fb63
commit b11e12f348
@@ -177,39 +177,27 @@ export function GenerateInvoiceDialog({ open, onOpenChange, clientId, clientName
<DialogTitle>Generate invoice — {clientName}</DialogTitle>
</DialogHeader>
{loading ? (
<div className="py-8 text-center text-muted-foreground">Loading unbilled work…</div>
) : cases.length === 0 ? (
<div className="py-8 text-center text-muted-foreground text-sm">
No unbilled time or expenses across this client's cases.
</div>
) : (
<div className="space-y-4">
<div>
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Cases to bill</Label>
<div className="mt-2 border rounded-md divide-y max-h-[260px] overflow-auto">
{cases.map((c) => (
<label key={c.id} className="flex items-start gap-3 p-3 cursor-pointer hover:bg-muted/40">
<Checkbox
checked={!!selected[c.id]}
onCheckedChange={(v) => setSelected((p) => ({ ...p, [c.id]: !!v }))}
className="mt-0.5"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<span className="font-medium text-sm truncate">{c.title}</span>
<span className="text-sm tabular-nums font-medium">{formatCurrency(c.total)}</span>
</div>
<div className="text-xs text-muted-foreground">
{c.case_number} · {c.timeCount} time entries ({formatCurrency(c.timeAmount)}) · {c.expenseCount} expenses ({formatCurrency(c.expenseAmount)})
</div>
</div>
</label>
))}
<div className="space-y-4">
<label className="flex items-start gap-3 p-3 rounded-md border cursor-pointer hover:bg-muted/40">
<Checkbox
checked={isRetainer}
onCheckedChange={(v) => setIsRetainer(!!v)}
className="mt-0.5"
/>
<div className="flex-1">
<div className="text-sm font-medium">Retainer / trust deposit invoice</div>
<div className="text-xs text-muted-foreground">
When paid, the amount is automatically deposited into this client's trust account.
</div>
</div>
</label>
{isRetainer ? (
<div className="grid grid-cols-3 gap-3">
<div>
<Label>Retainer amount</Label>
<Input id="retainer-amount" type="number" step="0.01" placeholder="0.00" />
</div>
<div>
<Label>Tax rate (%)</Label>
<Input type="number" step="0.01" value={taxPct} onChange={(e) => setTaxPct(e.target.value)} />
@@ -218,27 +206,73 @@ export function GenerateInvoiceDialog({ open, onOpenChange, clientId, clientName
<Label>Due in (days)</Label>
<Input type="number" value={dueDays} onChange={(e) => setDueDays(e.target.value)} />
</div>
<div className="col-span-1 flex items-end justify-end">
<div className="text-right">
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">Estimated total</div>
<div className="font-serif text-xl">{formatCurrency(total)}</div>
{tax > 0 && <div className="text-[11px] text-muted-foreground">{formatCurrency(subtotal)} + {formatCurrency(tax)} tax</div>}
</div>
) : loading ? (
<div className="py-8 text-center text-muted-foreground">Loading unbilled work…</div>
) : cases.length === 0 ? (
<div className="py-8 text-center text-muted-foreground text-sm">
No unbilled time or expenses across this client's cases.
</div>
) : (
<>
<div>
<Label className="text-xs uppercase tracking-wider text-muted-foreground">Cases to bill</Label>
<div className="mt-2 border rounded-md divide-y max-h-[260px] overflow-auto">
{cases.map((c) => (
<label key={c.id} className="flex items-start gap-3 p-3 cursor-pointer hover:bg-muted/40">
<Checkbox
checked={!!selected[c.id]}
onCheckedChange={(v) => setSelected((p) => ({ ...p, [c.id]: !!v }))}
className="mt-0.5"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<span className="font-medium text-sm truncate">{c.title}</span>
<span className="text-sm tabular-nums font-medium">{formatCurrency(c.total)}</span>
</div>
<div className="text-xs text-muted-foreground">
{c.case_number} · {c.timeCount} time entries ({formatCurrency(c.timeAmount)}) · {c.expenseCount} expenses ({formatCurrency(c.expenseAmount)})
</div>
</div>
</label>
))}
</div>
</div>
</div>
<div>
<Label>Notes (optional)</Label>
<Textarea rows={2} value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="Payment terms, thank-you message…" />
</div>
<div className="grid grid-cols-3 gap-3">
<div>
<Label>Tax rate (%)</Label>
<Input type="number" step="0.01" value={taxPct} onChange={(e) => setTaxPct(e.target.value)} />
</div>
<div>
<Label>Due in (days)</Label>
<Input type="number" value={dueDays} onChange={(e) => setDueDays(e.target.value)} />
</div>
<div className="col-span-1 flex items-end justify-end">
<div className="text-right">
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">Estimated total</div>
<div className="font-serif text-xl">{formatCurrency(total)}</div>
{tax > 0 && <div className="text-[11px] text-muted-foreground">{formatCurrency(subtotal)} + {formatCurrency(tax)} tax</div>}
</div>
</div>
</div>
</>
)}
<div>
<Label>Notes (optional)</Label>
<Textarea rows={2} value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="Payment terms, thank-you message…" />
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button onClick={submit} disabled={saving || cases.length === 0 || selectedIds.length === 0}>
<Button
onClick={submit}
disabled={saving || (!isRetainer && (cases.length === 0 || selectedIds.length === 0))}
>
{saving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <FilePlus className="h-4 w-4 mr-2" />}
Generate draft invoice
{isRetainer ? "Create retainer invoice" : "Generate draft invoice"}
</Button>
</DialogFooter>
</DialogContent>