diff --git a/README.md b/README.md
index b8c3a64..50cfdde 100644
--- a/README.md
+++ b/README.md
@@ -38,10 +38,15 @@ ledgerflow_frontend/
### Invoices
- Create invoices linked to POs
+- Edit invoices (pre-filled modal with line items, totals recalculated in real-time)
+- Delete individual invoices from the detail view, or bulk-select and delete multiple at once
+- "Modified After Send" EDI status badge when an invoice is edited after EDI submission
+- Paid and EDI-submitted invoices are protected from selection/deletion
- Line items with quantity, unit price, and descriptions
- Payment terms: Net 15, Net 30, Net 45, Net 60, 90 NET, Due on Receipt
- Due date auto-calculates based on selected payment terms
- Tax rate from company settings
+- PO remaining balance updates in real-time during edit (accounts for current invoice)
- Save as draft or queue for EDI submission
### EDI (X12 810)
diff --git a/accounting-app.html b/accounting-app.html
index 8869e19..5e92bdc 100644
--- a/accounting-app.html
+++ b/accounting-app.html
@@ -298,6 +298,7 @@
.badge.open { background: rgba(0,102,255,0.15); color: #5b9bff; border: 1px solid rgba(0,102,255,0.2); }
.badge.pending { background: rgba(255,211,42,0.12); color: var(--yellow); border: 1px solid rgba(255,211,42,0.2); }
.badge.submitted { background: rgba(0,229,160,0.1); color: var(--accent); border: 1px solid rgba(0,229,160,0.2); }
+ .badge.modified { background: rgba(255,140,50,0.12); color: #ff8c32; border: 1px solid rgba(255,140,50,0.25); }
.badge.paid { background: rgba(0,229,160,0.05); color: #4a8; border: 1px solid rgba(0,229,160,0.1); }
.badge.overdue { background: rgba(255,71,87,0.12); color: var(--red); border: 1px solid rgba(255,71,87,0.2); }
.badge.partial { background: rgba(255,107,53,0.12); color: var(--accent3); border: 1px solid rgba(255,107,53,0.2); }
@@ -694,13 +695,16 @@
- Invoice # PO Reference Vendor Issued Due Amount EDI Status Status Actions
+ Invoice # PO Reference Vendor Issued Due Amount EDI Status Status Actions
- ๐งพ
No invoices yet. Create one against a PO.
+ ๐งพ
No invoices yet. Create one against a PO.
@@ -1141,11 +1145,11 @@
-
+
-
+
+
+
+
+
+
Are you sure you want to delete the following 0 invoice(s)? This cannot be undone.
+
+
+
+
+
+
โ
@@ -1342,7 +1366,7 @@ function navigate(view, el) {
// ============================================================
function openModal(id) {
document.getElementById(id).classList.add('open');
- if (id === 'newInvoiceModal') initNewInvoiceModal();
+ if (id === 'newInvoiceModal' && !editingInvoiceId) initNewInvoiceModal();
if (id === 'importPOModal') {
document.getElementById('imp-region').textContent = state.config.dynamo.region || 'โ';
document.getElementById('imp-table').textContent = state.config.dynamo.table || 'โ';
@@ -1747,9 +1771,23 @@ function updateDueDate() {
}
function initNewInvoiceModal() {
+ editingInvoiceId = null;
+
+ // Reset modal to create mode
+ document.getElementById('inv-modal-title').textContent = 'Create Invoice';
+ document.getElementById('inv-modal-footer').innerHTML = `
+ Cancel
+ Save Draft
+ Create & Queue for EDI
+ `;
+
+ document.getElementById('inv-number').disabled = false;
+ document.getElementById('inv-po-ref').disabled = false;
+
const prefix = state.config.company.invPrefix || 'INV-';
document.getElementById('inv-number').value = prefix + String(state.invCounter).padStart(4, '0');
document.getElementById('inv-issue').value = today();
+ document.getElementById('inv-notes').value = '';
updateDueDate();
document.getElementById('inv-tax-rate-label').textContent = state.config.company.taxRate || 0;
@@ -1821,7 +1859,10 @@ function calcTotals() {
if (poId) {
const po = state.pos.find(p => p.id === poId);
if (po) {
- const poRemaining = po.amount - (po.billed || 0) - total;
+ // When editing, the PO's billed amount already includes this invoice โ add it back
+ const editingInv = editingInvoiceId ? state.invoices.find(i => i.id === editingInvoiceId) : null;
+ const editingInvTotal = editingInv ? parseFloat(editingInv.total || 0) : 0;
+ const poRemaining = parseFloat(po.amount || 0) - parseFloat(po.billed || 0) + editingInvTotal - total;
remainEl.textContent = fmt(poRemaining);
remainEl.style.color = poRemaining < 0 ? 'var(--red, #e74c3c)' : 'var(--accent)';
}
@@ -1885,12 +1926,18 @@ async function saveInvoice(status) {
function refreshInvoiceTable() {
const tbody = document.getElementById('inv-table-body');
document.getElementById('inv-count').textContent = state.invoices.length;
+ document.getElementById('inv-select-all').checked = false;
+ document.getElementById('inv-bulk-delete-btn').style.display = 'none';
if (!state.invoices.length) {
- tbody.innerHTML = ' ';
+ tbody.innerHTML = ' ';
return;
}
+ const canSelect = inv => inv.status !== 'paid' && inv.ediStatus !== 'submitted';
tbody.innerHTML = state.invoices.map(inv => `
+
+ ${canSelect(inv) ? ` ` : ''}
+
${inv.number}
${inv.poNumber || 'โ'}
${inv.vendor}
@@ -1900,6 +1947,7 @@ function refreshInvoiceTable() {
${ediStatusLabel(inv.ediStatus)}
${inv.status}
+ ${inv.status !== 'paid' ? `Edit ` : ''}
โก EDI
${inv.status !== 'paid' ? `โ Paid ` : ''}
@@ -1932,9 +1980,214 @@ function viewInvoice(id) {
`;
document.getElementById('view-inv-edi-btn').onclick = () => { closeModal('viewInvoiceModal'); submitInvoiceEDI(id); };
document.getElementById('view-inv-paid-btn').onclick = () => { markPaid(id); closeModal('viewInvoiceModal'); };
+
+ // Edit button โ hidden for paid invoices
+ const editBtn = document.getElementById('view-inv-edit-btn');
+ editBtn.style.display = inv.status === 'paid' ? 'none' : '';
+ editBtn.onclick = () => { closeModal('viewInvoiceModal'); editInvoice(id); };
+
+ // Delete button โ hidden for paid invoices
+ const deleteBtn = document.getElementById('view-inv-delete-btn');
+ deleteBtn.style.display = inv.status === 'paid' ? 'none' : '';
+ deleteBtn.onclick = () => { deleteInvoice(id); };
+
openModal('viewInvoiceModal');
}
+let editingInvoiceId = null;
+
+function editInvoice(id) {
+ const inv = state.invoices.find(i => i.id === id);
+ if (!inv) return;
+
+ editingInvoiceId = id;
+
+ // Set modal to edit mode
+ document.getElementById('inv-modal-title').textContent = `Edit Invoice ${inv.number}`;
+ document.getElementById('inv-modal-footer').innerHTML = `
+ Cancel
+ Save Changes
+ `;
+
+ // Populate fields
+ document.getElementById('inv-number').value = inv.number;
+ document.getElementById('inv-number').disabled = true;
+ document.getElementById('inv-vendor').value = inv.vendor;
+ document.getElementById('inv-issue').value = inv.issueDate || '';
+ document.getElementById('inv-due').value = inv.dueDate || '';
+ document.getElementById('inv-terms').value = inv.terms || 'Net 30';
+ document.getElementById('inv-notes').value = inv.notes || '';
+ document.getElementById('inv-tax-rate-label').textContent = state.config.company.taxRate || 0;
+
+ // Populate PO ref
+ const sel = document.getElementById('inv-po-ref');
+ sel.innerHTML = 'โ Select PO (optional) โ ';
+ state.pos.filter(p => p.status !== 'closed' && p.status !== 'cancelled').forEach(po => {
+ const opt = document.createElement('option');
+ opt.value = po.id;
+ opt.textContent = `${po.poNumber} โ ${po.vendor} (${fmt(po.amount - (po.billed||0))} remaining)`;
+ sel.appendChild(opt);
+ });
+ sel.value = inv.poId || '';
+ sel.disabled = true; // PO linkage shouldn't change on edit
+
+ // Populate line items
+ document.getElementById('line-items-body').innerHTML = '';
+ inv.lineItems.forEach(li => {
+ lineItemCounter++;
+ const rowId = 'li-' + lineItemCounter;
+ const row = document.createElement('tr');
+ row.id = rowId;
+ row.innerHTML = `
+
+
+
+ ${fmt(li.total)}
+ ร `;
+ document.getElementById('line-items-body').appendChild(row);
+ });
+
+ if (inv.poId) {
+ document.getElementById('po-remaining-row').style.display = '';
+ } else {
+ document.getElementById('po-remaining-row').style.display = 'none';
+ }
+
+ calcTotals();
+ openModal('newInvoiceModal');
+}
+
+async function saveInvoiceEdit() {
+ if (!editingInvoiceId) return;
+
+ const vendor = document.getElementById('inv-vendor').value.trim();
+ if (!vendor) { toast('Vendor is required', 'warn'); return; }
+
+ const body = {
+ vendor,
+ issueDate: document.getElementById('inv-issue').value,
+ dueDate: document.getElementById('inv-due').value,
+ terms: document.getElementById('inv-terms').value,
+ lineItems: getLineItems(),
+ taxRate: state.config.company.taxRate || 0,
+ notes: document.getElementById('inv-notes').value,
+ };
+
+ const data = await api(`/invoices/${editingInvoiceId}`, { method: 'PUT', body });
+ if (!data) return;
+
+ // Update local state
+ const idx = state.invoices.findIndex(i => i.id === editingInvoiceId);
+ if (idx !== -1) state.invoices[idx] = data;
+ save();
+
+ // Refresh POs if linked
+ if (data.poId) await loadPOs();
+
+ closeModal('newInvoiceModal');
+ editingInvoiceId = null;
+ document.getElementById('inv-number').disabled = false;
+ document.getElementById('inv-po-ref').disabled = false;
+
+ refreshInvoiceTable();
+ refreshPOTable();
+ refreshDashboard();
+ addActivity(`Invoice ${data.number} updated`, 'blue');
+ toast(`Invoice ${data.number} updated`, 'ok');
+}
+
+async function deleteInvoice(id) {
+ const inv = state.invoices.find(i => i.id === id);
+ if (!inv) return;
+
+ if (!confirm(`Delete invoice ${inv.number}? This cannot be undone.`)) return;
+
+ const res = await fetch(`${API_URL}/invoices/${id}`, {
+ method: 'DELETE',
+ headers: { Authorization: `Bearer ${getToken()}` },
+ });
+ if (!res.ok && res.status !== 204) {
+ const err = await res.json().catch(() => ({}));
+ toast(err.message || err.error || 'Failed to delete invoice', 'err');
+ return;
+ }
+
+ state.invoices = state.invoices.filter(i => i.id !== id);
+ save();
+
+ if (inv.poId) await loadPOs();
+
+ closeModal('viewInvoiceModal');
+ refreshInvoiceTable();
+ refreshPOTable();
+ refreshDashboard();
+ addActivity(`Invoice ${inv.number} deleted`, 'red');
+ toast(`Invoice ${inv.number} deleted`, 'ok');
+}
+
+function toggleAllInvoices(checked) {
+ document.querySelectorAll('.inv-select-cb').forEach(cb => { cb.checked = checked; });
+ updateBulkDeleteBtn();
+}
+
+function updateBulkDeleteBtn() {
+ const selected = document.querySelectorAll('.inv-select-cb:checked');
+ const btn = document.getElementById('inv-bulk-delete-btn');
+ document.getElementById('inv-selected-count').textContent = selected.length;
+ btn.style.display = selected.length > 0 ? '' : 'none';
+
+ // Update select-all checkbox state
+ const all = document.querySelectorAll('.inv-select-cb');
+ document.getElementById('inv-select-all').checked = all.length > 0 && selected.length === all.length;
+}
+
+function confirmBulkDelete() {
+ const selectedIds = Array.from(document.querySelectorAll('.inv-select-cb:checked')).map(cb => cb.dataset.id);
+ if (!selectedIds.length) return;
+
+ const invoices = selectedIds.map(id => state.invoices.find(i => i.id === id)).filter(Boolean);
+ document.getElementById('bulk-del-count').textContent = invoices.length;
+ document.getElementById('bulk-del-list').innerHTML = invoices.map(inv =>
+ `${inv.number} โ ${inv.vendor} (${fmt(inv.total)})
`
+ ).join('');
+
+ openModal('bulkDeleteModal');
+}
+
+async function executeBulkDelete() {
+ const selectedIds = Array.from(document.querySelectorAll('.inv-select-cb:checked')).map(cb => cb.dataset.id);
+ if (!selectedIds.length) return;
+
+ closeModal('bulkDeleteModal');
+ let deleted = 0;
+ let hadPO = false;
+
+ for (const id of selectedIds) {
+ const inv = state.invoices.find(i => i.id === id);
+ if (!inv) continue;
+
+ const res = await fetch(`${API_URL}/invoices/${id}`, {
+ method: 'DELETE',
+ headers: { Authorization: `Bearer ${getToken()}` },
+ });
+ if (res.ok || res.status === 204) {
+ if (inv.poId) hadPO = true;
+ state.invoices = state.invoices.filter(i => i.id !== id);
+ deleted++;
+ }
+ }
+
+ if (deleted) {
+ save();
+ if (hadPO) await loadPOs();
+ refreshInvoiceTable();
+ refreshPOTable();
+ refreshDashboard();
+ addActivity(`Deleted ${deleted} invoice(s)`, 'red');
+ toast(`Deleted ${deleted} invoice(s)`, 'ok');
+ }
+}
+
async function markPaid(id) {
const inv = state.invoices.find(i => i.id === id);
if (!inv) return;
@@ -2186,8 +2439,8 @@ function fmt(n) {
}
function today() { return new Date().toISOString().split('T')[0]; }
function genId() { return Math.random().toString(36).slice(2, 10); }
-function ediStatusClass(s) { return s === 'submitted' ? 'submitted' : s === 'processing' ? 'pending' : 'open'; }
-function ediStatusLabel(s) { return s === 'submitted' ? 'Submitted' : s === 'processing' ? 'Processing' : 'Not Sent'; }
+function ediStatusClass(s) { return s === 'submitted' ? 'submitted' : s === 'modified_after_send' ? 'modified' : s === 'processing' ? 'pending' : 'open'; }
+function ediStatusLabel(s) { return s === 'submitted' ? 'Submitted' : s === 'modified_after_send' ? 'Modified After Send' : s === 'processing' ? 'Processing' : 'Not Sent'; }
function filterTable(tableId, query) {
const q = query.toLowerCase();