diff --git a/src/pages/accounting/AccountingBillsPage.tsx b/src/pages/accounting/AccountingBillsPage.tsx index c5285ec..775be71 100644 --- a/src/pages/accounting/AccountingBillsPage.tsx +++ b/src/pages/accounting/AccountingBillsPage.tsx @@ -253,7 +253,7 @@ export default function AccountingBillsPage() { } }; - const save = async () => { + const save = async (keepOpen = false) => { if (!number.trim()) return toast.error("Bill number required"); let attachmentUrl = uploadedUrl; if (file && !attachmentUrl) attachmentUrl = await uploadFileObj(file); @@ -299,11 +299,45 @@ export default function AccountingBillsPage() { await accounting.from("bill_items").insert(itemRows(bill.id)); toast.success("Bill recorded"); } - setOpen(false); resetForm(); + if (!keepOpen) setOpen(false); qc.invalidateQueries({ queryKey: ["bills", cid] }); }; + // ── Read-only bill detail view ── + const [viewBill, setViewBill] = useState(null); + const [viewItems, setViewItems] = useState([]); + const openView = async (b: any) => { + setViewBill(b); + const { data } = await accounting.from("bill_items").select("*").eq("bill_id", b.id); + setViewItems(data ?? []); + }; + + // Clone a bill into a fresh create form (today's date, blank number). + const duplicateBill = async (b: any) => { + setViewBill(null); + let pubVendorId = ""; + if (b.vendor_id) { + const { data: av } = await accounting.from("vendors").select("external_source, external_id").eq("id", b.vendor_id).maybeSingle(); + if (av?.external_source === "acmacc_vendor" && av?.external_id) pubVendorId = String(av.external_id); + } + const { data: its } = await accounting.from("bill_items").select("*").eq("bill_id", b.id); + setEditId(null); + setVendorId(pubVendorId); + setNumber(""); + setIssueDate(new Date().toLocaleDateString("en-CA", { timeZone: "America/New_York" })); + setDueDate(""); + const sub = Number(b.subtotal || 0); + setTaxPct(sub > 0 ? +((Number(b.tax || 0) / sub) * 100).toFixed(4) : 0); + setNotes(b.notes ?? ""); + setUploadedUrl(null); setFile(null); setFilePreview(null); + setAiFilled(new Set()); setMissingFields(new Set()); + setItems(its && its.length + ? its.map((i: any) => ({ description: i.description ?? "", quantity: Number(i.quantity ?? 1), rate: Number(i.rate ?? 0), account_id: i.account_id ?? null })) + : [{ description: "", quantity: 1, rate: 0, account_id: null }]); + setOpen(true); + }; + // Payment dialog state const [payBill, setPayBill] = useState(null); @@ -535,48 +569,67 @@ export default function AccountingBillsPage() {

Bills

{ setOpen(o); if (!o) resetForm(); }}> - - {editId ? "Edit bill" : "New bill"} + + + {editId ? "Edit bill" : "Record bill"} + - {aiFilled.size > 0 && ( -
- - AI has filled in this bill — please review all fields before saving. -
- )} +
+ {aiFilled.size > 0 && ( +
+ + AI has filled in this bill — please review highlighted fields before saving. +
+ )} -
- {/* Form */} -
- {/* Upload zone */} +
+ {/* ── Left: attachment panel ── */}
{ e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)} - onDrop={(e) => { - e.preventDefault(); setDragOver(false); - const f = e.dataTransfer.files?.[0]; if (f) handleFile(f); - }} + onDrop={(e) => { e.preventDefault(); setDragOver(false); const f = e.dataTransfer.files?.[0]; if (f) handleFile(f); }} className={cn( - "rounded-lg border-2 border-dashed p-4 text-center text-sm cursor-pointer transition-colors", - dragOver ? "border-primary bg-primary/5" : "border-border hover:bg-muted/50" + "relative rounded-lg border-2 border-dashed min-h-[420px] flex flex-col items-center justify-center text-center p-6 transition-colors", + dragOver ? "border-primary bg-primary/5" : "border-border", + !filePreview && !file && !uploadedUrl && "cursor-pointer hover:bg-muted/40", )} - onClick={() => fileRef.current?.click()} + onClick={() => { if (!filePreview && !file && !uploadedUrl) fileRef.current?.click(); }} > - { const f = e.target.files?.[0]; if (f) handleFile(f); }} - /> - -
- {parsing ? "Parsing with AI…" : file ? file.name : "Upload & Parse Bill"} -
-
- {parsing ? "Extracting fields from your bill…" : "Drag & drop or click — PDF, JPG, PNG, HEIC · auto-parses on drop"} -
- {file && !parsing && ( -
e.stopPropagation()}> - + )} +
-
+ {/* ── Right: form ── */} +
- - -
-
- - setNumber(e.target.value)} className={hl("number")} /> -
-
- - setIssueDate(e.target.value)} className={hl("issueDate")} /> -
-
- - setDueDate(e.target.value)} className={hl("dueDate")} /> -
-
- -
- - {items.map((it, idx) => ( -
- { const a = [...items]; a[idx] = { ...it, description: e.target.value }; setItems(a); }} /> - { const a = [...items]; a[idx] = { ...it, quantity: Number(e.target.value) }; setItems(a); }} /> - { const a = [...items]; a[idx] = { ...it, rate: Number(e.target.value) }; setItems(a); }} /> - setIssueDate(e.target.value)} className={hl("issueDate")} /> +
+
+ + setDueDate(e.target.value)} className={hl("dueDate")} /> +
+
+
+ + -
- ))} - -
- -
-
- - setTaxPct(Number(e.target.value))} className={hl("tax")} /> -
-
-
Subtotal: {money(subtotal, cur)}
-
Tax: {money(tax, cur)}
-
Total: {money(total, cur)}
-
-
- - {(notes || aiFilled.has("notes")) && ( -
- - setNotes(e.target.value)} className={hl("notes")} /> -
- )} -
- - {/* Preview pane */} -
- -
- {filePreview ? ( - Bill preview - ) : file ? ( -
- -
{file.name}
-
{(file.size / 1024).toFixed(1)} KB
+
+
+ + setNumber(e.target.value)} className={hl("number")} /> +
+
+ + setNotes(e.target.value)} className={hl("notes")} placeholder="Optional" /> +
- ) : ( -
No file uploaded
- )} +
+ +
+

Item Details

+
+
+
Account
+
Description
+
Qty
+
Rate
+
+
+ {items.map((it, idx) => ( +
+ + { const a = [...items]; a[idx] = { ...it, description: e.target.value }; setItems(a); }} /> + { const a = [...items]; a[idx] = { ...it, quantity: Number(e.target.value) }; setItems(a); }} /> + { const a = [...items]; a[idx] = { ...it, rate: Number(e.target.value) }; setItems(a); }} /> + +
+ ))} +
+ +
+ +
+
+ + setTaxPct(Number(e.target.value))} className={hl("tax")} /> +
+
+
Subtotal {money(subtotal, cur)}
+
Tax {money(tax, cur)}
+
+
- + {/* Total bar */} +
+ Total bill amount: {money(total, cur)} +
+ + + + {!editId && } + +
@@ -741,14 +798,14 @@ export default function AccountingBillsPage() { {!isLoading && filtered.map((b: any) => ( - {b.attachment_url ? ( - - {b.number} - - ) : b.number} + {b.auto_created && Auto} - {b.vendors?.name ?? "—"} + + + {fmtDate(b.issue_date)} {fmtDate(b.due_date)} {money(b.total, cur)} @@ -853,6 +910,117 @@ export default function AccountingBillsPage() { + + {/* Bill detail (read-only overview) */} + { if (!o) { setViewBill(null); setViewItems([]); } }}> + + {viewBill && (() => { + const acctName = (id: string | null) => { + const a = (expenseAccounts as any[]).find((x) => x.id === id); + return a ? `${a.code ? a.code + " " : ""}${a.name}` : "—"; + }; + const v = viewBill; + const canPay = v.derivedStatus !== "paid" && v.derivedStatus !== "void"; + return ( + <> + {/* Header */} +
+
+
+
+

{v.vendors?.name ?? "Vendor"}

+ +
+

+ Bill {v.number} · {money(v.total, cur)} · Due {fmtDate(v.due_date)} + +

+
+
+ {canPay && } + + +
+
+
+ +
+ {/* Bill details + items */} +
+ + Bill details + + + + + + + {v.attachment_url && ( +
+
Attachment
+ + View + +
+ )} +
+
+ + + Item details + + + + + Account + Description + Amount + + + + {viewItems.map((it: any) => ( + + {acctName(it.account_id)} + {it.description || "—"} + {money(Number(it.amount), cur)} + + ))} + + Total + {money(v.total, cur)} + + +
+
+
+
+ + {/* Amount sidebar */} +
+ + Bill amount + +
Total{money(v.total, cur)}
+
Paid{money(Number(v.paid_amount ?? 0), cur)}
+
Remaining{money(v.balance, cur)}
+
+
+
+
+ + ); + })()} +
+
+ + ); +} + +function Field({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
); }