Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-25 18:51:47 +00:00
co-authored by renee-png
parent 17e23bc2c5
commit 4be52214a2
+36 -12
View File
@@ -37,7 +37,8 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) {
const [form, setForm] = useState({
expense_date: new Date().toISOString().slice(0, 10),
description: "",
amount: "",
quantity: "1",
unit_price: "",
billable: true,
});
const [receipt, setReceipt] = useState<File | null>(null);
@@ -102,8 +103,11 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) {
const submit = async (e: React.FormEvent) => {
e.preventDefault();
const amount = parseFloat(form.amount);
if (isNaN(amount) || amount < 0) { toast.error("Invalid amount"); return; }
const quantity = parseFloat(form.quantity);
const unit_price = parseFloat(form.unit_price);
if (isNaN(quantity) || quantity <= 0) { toast.error("Quantity must be greater than 0"); return; }
if (isNaN(unit_price) || unit_price < 0) { toast.error("Invalid unit price"); return; }
const amount = +(quantity * unit_price).toFixed(2);
if (!form.description.trim()) { toast.error("Description required"); return; }
setSubmitting(true);
let receipt_storage_path: string | null = null;
@@ -120,6 +124,8 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) {
expense_date: form.expense_date,
description: form.description.trim(),
amount,
quantity,
unit_price,
billable: form.billable,
receipt_storage_path,
});
@@ -129,7 +135,7 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) {
toast.success("Expense added");
setShowForm(false);
setReceipt(null);
setForm({ ...form, amount: "", description: "" });
setForm({ ...form, quantity: "1", unit_price: "", description: "" });
if (fileRef.current) fileRef.current.value = "";
load();
}
@@ -189,7 +195,8 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) {
if (fee) {
setForm((f) => ({
...f,
amount: String(fee.amount),
unit_price: String(fee.amount),
quantity: f.quantity || "1",
billable: fee.billable,
description: fee.description || fee.name,
}));
@@ -206,30 +213,43 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) {
/>
</div>
)}
<form onSubmit={submit} className="grid grid-cols-1 md:grid-cols-4 gap-3">
<form onSubmit={submit} className="grid grid-cols-2 md:grid-cols-6 gap-3">
<div className="space-y-1.5">
<Label className="text-xs">Date</Label>
<Input type="date" value={form.expense_date} onChange={(e) => setForm({ ...form, expense_date: e.target.value })} required />
</div>
<div className="space-y-1.5">
<Label className="text-xs">Amount ($)</Label>
<Input type="number" step="0.01" min="0" value={form.amount} onChange={(e) => setForm({ ...form, amount: e.target.value })} required />
<Label className="text-xs">Qty</Label>
<Input type="number" step="any" min="0" value={form.quantity} onChange={(e) => setForm({ ...form, quantity: e.target.value })} required />
</div>
<div className="space-y-1.5">
<Label className="text-xs">Unit price ($)</Label>
<Input type="number" step="0.01" min="0" value={form.unit_price} onChange={(e) => setForm({ ...form, unit_price: e.target.value })} required />
</div>
<div className="space-y-1.5">
<Label className="text-xs">Total</Label>
<Input
readOnly
tabIndex={-1}
value={`$${((parseFloat(form.quantity) || 0) * (parseFloat(form.unit_price) || 0)).toFixed(2)}`}
className="bg-muted/40"
/>
</div>
<div className="space-y-1.5 md:col-span-2">
<Label className="text-xs">Description</Label>
<Input value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} required maxLength={500} />
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 md:col-span-2">
<label className="flex items-center gap-2 text-sm">
<Checkbox checked={form.billable} onCheckedChange={(c) => setForm({ ...form, billable: !!c })} />
Billable
</label>
</div>
<div className="md:col-span-2">
<div className="md:col-span-4">
<Label className="text-xs">Receipt (optional)</Label>
<Input ref={fileRef} type="file" onChange={(e) => setReceipt(e.target.files?.[0] ?? null)} />
</div>
<div className="md:col-span-4 flex justify-end">
<div className="md:col-span-6 flex justify-end">
<Button type="submit" disabled={submitting}>
{submitting && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Save expense
@@ -248,13 +268,15 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) {
<th className="text-left px-4 py-3 font-medium">Date</th>
<th className="text-left px-4 py-3 font-medium">Description</th>
<th className="text-left px-4 py-3 font-medium">User</th>
<th className="text-right px-4 py-3 font-medium">Qty</th>
<th className="text-right px-4 py-3 font-medium">Unit</th>
<th className="text-right px-4 py-3 font-medium">Amount</th>
<th className="text-left px-4 py-3 font-medium">Status</th>
<th></th>
</tr>
</thead>
<tbody>
{items.length === 0 && <tr><td colSpan={6} className="text-center py-12 text-muted-foreground">No expenses.</td></tr>}
{items.length === 0 && <tr><td colSpan={8} className="text-center py-12 text-muted-foreground">No expenses.</td></tr>}
{items.map((e) => (
<tr key={e.id} className="border-t hover:bg-muted/30">
<td className="px-4 py-3 text-muted-foreground whitespace-nowrap">{formatDate(e.expense_date)}</td>
@@ -267,6 +289,8 @@ export function CaseExpensesTab({ caseId }: { caseId: string }) {
)}
</td>
<td className="px-4 py-3 text-muted-foreground">{e.user?.full_name || e.user?.email}</td>
<td className="px-4 py-3 text-right tabular-nums text-muted-foreground">{Number(e.quantity ?? 1)}</td>
<td className="px-4 py-3 text-right tabular-nums text-muted-foreground">{formatCurrency(Number(e.unit_price ?? e.amount))}</td>
<td className="px-4 py-3 text-right tabular-nums font-medium">{formatCurrency(e.amount)}</td>
<td className="px-4 py-3 text-xs text-muted-foreground">{!e.billable ? "Non-billable" : e.invoice_id ? "Invoiced" : "Unbilled"}</td>
<td className="px-4 py-3 text-right">