Bills: editing a paid bill saves and keeps its payment in sync

Editing a paid bill now updates the linked payment along with the bill: when the
amount changes and the bill has a single bill payment, the bank transaction, the
check, and the paid amount are all updated to the new total. The whole save is
wrapped in error handling and the bill_items insert is now error-checked, so any
failure surfaces a clear toast instead of silently appearing to 'not save'.
Also refreshes the transactions/accounts caches after a bill edit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 16:59:06 -04:00
parent 9063e49389
commit 1370b98be9
+29 -2
View File
@@ -46,6 +46,7 @@ export default function AccountingBillsPage() {
const parseFn = parseBill; const parseFn = parseBill;
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [editId, setEditId] = useState<string | null>(null); const [editId, setEditId] = useState<string | null>(null);
const [editBill, setEditBill] = useState<any | null>(null);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<string>("all"); const [statusFilter, setStatusFilter] = useState<string>("all");
const [autoOnly, setAutoOnly] = useState(false); const [autoOnly, setAutoOnly] = useState(false);
@@ -130,7 +131,7 @@ export default function AccountingBillsPage() {
const total = +(subtotal + tax).toFixed(2); const total = +(subtotal + tax).toFixed(2);
const resetForm = () => { const resetForm = () => {
setEditId(null); setEditId(null); setEditBill(null);
setVendorId(""); setNumber(""); setDueDate(""); setTaxPct(0); setNotes(""); setVendorId(""); setNumber(""); setDueDate(""); setTaxPct(0); setNotes("");
setItems([{ description: "", quantity: 1, rate: 0, account_id: null }]); setItems([{ description: "", quantity: 1, rate: 0, account_id: null }]);
setFile(null); setFilePreview(null); setUploadedUrl(null); setFile(null); setFilePreview(null); setUploadedUrl(null);
@@ -139,6 +140,7 @@ export default function AccountingBillsPage() {
const openEdit = async (b: any) => { const openEdit = async (b: any) => {
setEditId(b.id); setEditId(b.id);
setEditBill(b);
// The dropdown is keyed by public vendor id; map the stored accounting // The dropdown is keyed by public vendor id; map the stored accounting
// vendor back to its source public vendor when one exists. // vendor back to its source public vendor when one exists.
let pubVendorId = ""; let pubVendorId = "";
@@ -261,6 +263,7 @@ export default function AccountingBillsPage() {
const save = async (keepOpen = false) => { const save = async (keepOpen = false) => {
if (!number.trim()) return toast.error("Bill number required"); if (!number.trim()) return toast.error("Bill number required");
if (!vendorId) return toast.error("Select a vendor (Pay to) before saving"); if (!vendorId) return toast.error("Select a vendor (Pay to) before saving");
try {
let attachmentUrl = uploadedUrl; let attachmentUrl = uploadedUrl;
if (file && !attachmentUrl) attachmentUrl = await uploadFileObj(file); if (file && !attachmentUrl) attachmentUrl = await uploadFileObj(file);
@@ -291,8 +294,27 @@ export default function AccountingBillsPage() {
}).eq("id", editId); }).eq("id", editId);
if (error) return toast.error(error.message); if (error) return toast.error(error.message);
await accounting.from("bill_items").delete().eq("bill_id", editId); await accounting.from("bill_items").delete().eq("bill_id", editId);
await accounting.from("bill_items").insert(itemRows(editId)); const { error: biErr } = await accounting.from("bill_items").insert(itemRows(editId));
if (biErr) return toast.error(biErr.message);
// Keep an already-paid bill's payment in sync with the edited amount. When
// the total changed and the bill has a single linked bill payment, update
// that payment (bank transaction + check) and the paid amount to match.
const wasPaid = Number(editBill?.paid_amount ?? 0) > 0;
const amountChanged = Math.abs(Number(editBill?.total ?? 0) - total) > 0.005;
if (wasPaid && amountChanged) {
const { data: pays } = await accounting.from("transactions").select("id").eq("bill_id", editId);
if ((pays?.length ?? 0) === 1) {
await accounting.from("transactions").update({ amount: total }).eq("bill_id", editId);
await accounting.from("checks").update({ amount: total }).eq("source_bill_id", editId);
await accounting.from("bills").update({ paid_amount: total, status: total > 0 ? "paid" : "open" }).eq("id", editId);
toast.success("Bill and payment updated");
} else {
toast.warning("Bill updated — it has split/partial payments, so review the payment(s) manually.");
}
} else {
toast.success("Bill updated"); toast.success("Bill updated");
}
} else { } else {
const { data: bill, error } = await accounting.from("bills").insert({ const { data: bill, error } = await accounting.from("bills").insert({
company_id: cid, vendor_id: acctVendorId, number, company_id: cid, vendor_id: acctVendorId, number,
@@ -308,6 +330,11 @@ export default function AccountingBillsPage() {
resetForm(); resetForm();
if (!keepOpen) setOpen(false); if (!keepOpen) setOpen(false);
qc.invalidateQueries({ queryKey: ["bills", cid] }); qc.invalidateQueries({ queryKey: ["bills", cid] });
qc.invalidateQueries({ queryKey: ["transactions", cid] });
qc.invalidateQueries({ queryKey: ["accounts", cid] });
} catch (e: any) {
toast.error(e?.message ?? "Failed to save bill");
}
}; };
// ── Read-only bill detail view ── // ── Read-only bill detail view ──