From bedb93affece307a2406e6884f26a51a2f80a13d Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Thu, 19 Mar 2026 18:44:56 +0000 Subject: [PATCH] feat(dashboard): receipt-style order detail with inline transport and discount rows Replace totals bar + VAT subtotals table with transport/discount as table rows (with CODMAT from settings, proper VAT rate) and a single Total footer. Right-align qty/price/TVA columns, thousands separator (ro-RO), discount shown as qty=-1 price=positive. Co-Authored-By: Claude Opus 4.6 (1M context) --- api/app/routers/sync.py | 6 + api/app/static/js/dashboard.js | 188 ++++++++++++++++++++++++------- api/app/templates/dashboard.html | 15 +-- 3 files changed, 162 insertions(+), 47 deletions(-) diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py index 45338a8..7ca278d 100644 --- a/api/app/routers/sync.py +++ b/api/app/routers/sync.py @@ -416,6 +416,12 @@ async def order_detail(order_number: str): except (json.JSONDecodeError, TypeError): pass + # Add settings for receipt display + app_settings = await sqlite_service.get_app_settings() + order["transport_vat"] = app_settings.get("transport_vat") or "21" + order["transport_codmat"] = app_settings.get("transport_codmat") or "" + order["discount_codmat"] = app_settings.get("discount_codmat") or "" + return detail diff --git a/api/app/static/js/dashboard.js b/api/app/static/js/dashboard.js index 4079085..215b26d 100644 --- a/api/app/static/js/dashboard.js +++ b/api/app/static/js/dashboard.js @@ -522,14 +522,12 @@ async function openDashOrderDetail(orderNumber) { document.getElementById('detailIdPartener').textContent = '-'; document.getElementById('detailIdAdresaFact').textContent = '-'; document.getElementById('detailIdAdresaLivr').textContent = '-'; - document.getElementById('detailItemsBody').innerHTML = 'Se incarca...'; + document.getElementById('detailItemsBody').innerHTML = 'Se incarca...'; document.getElementById('detailError').style.display = 'none'; + document.getElementById('detailReceipt').innerHTML = ''; + document.getElementById('detailReceiptMobile').innerHTML = ''; const invInfo = document.getElementById('detailInvoiceInfo'); if (invInfo) invInfo.style.display = 'none'; - const detailItemsTotal = document.getElementById('detailItemsTotal'); - if (detailItemsTotal) detailItemsTotal.textContent = '-'; - const detailOrderTotal = document.getElementById('detailOrderTotal'); - if (detailOrderTotal) detailOrderTotal.textContent = '-'; const mobileContainer = document.getElementById('detailItemsMobile'); if (mobileContainer) mobileContainer.innerHTML = ''; @@ -574,46 +572,23 @@ async function openDashOrderDetail(orderNumber) { document.getElementById('detailError').style.display = ''; } - const dlvEl = document.getElementById('detailDeliveryCost'); - if (dlvEl) dlvEl.textContent = order.delivery_cost > 0 ? Number(order.delivery_cost).toFixed(2) + ' lei' : '–'; - - const dscEl = document.getElementById('detailDiscount'); - if (dscEl) { - if (order.discount_total > 0 && order.discount_split && typeof order.discount_split === 'object') { - const entries = Object.entries(order.discount_split); - if (entries.length > 1) { - const parts = entries.map(([vat, amt]) => `–${Number(amt).toFixed(2)} (TVA ${vat}%)`); - dscEl.innerHTML = parts.join('
'); - } else { - dscEl.textContent = '–' + Number(order.discount_total).toFixed(2) + ' lei'; - } - } else { - dscEl.textContent = order.discount_total > 0 ? '–' + Number(order.discount_total).toFixed(2) + ' lei' : '–'; - } - } - const items = data.items || []; if (items.length === 0) { - document.getElementById('detailItemsBody').innerHTML = 'Niciun articol'; + document.getElementById('detailItemsBody').innerHTML = 'Niciun articol'; return; } - // Update totals row - const itemsTotal = items.reduce((sum, item) => sum + (Number(item.price || 0) * Number(item.quantity || 0)), 0); - document.getElementById('detailItemsTotal').textContent = itemsTotal.toFixed(2) + ' lei'; - document.getElementById('detailOrderTotal').textContent = order.order_total != null ? Number(order.order_total).toFixed(2) + ' lei' : '-'; - // Store items for quick map pre-population window._detailItems = items; // Mobile article flat list const mobileContainer = document.getElementById('detailItemsMobile'); if (mobileContainer) { - mobileContainer.innerHTML = '
' + items.map((item, idx) => { + let mobileHtml = items.map((item, idx) => { const codmatText = item.codmat_details?.length ? item.codmat_details.map(d => `${esc(d.codmat)}${d.direct ? ' direct' : ''}`).join(' ') : `${esc(item.codmat || '–')}`; - const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2); + const valoare = (Number(item.price || 0) * Number(item.quantity || 0)); return `
${esc(item.sku)} @@ -622,29 +597,166 @@ async function openDashOrderDetail(orderNumber) {
${esc(item.product_name || '–')} x${item.quantity || 0} - ${valoare} lei + ${fmtNum(valoare)} lei + TVA ${item.vat != null ? Number(item.vat) : '?'}
`; - }).join('') + '
'; + }).join(''); + + // Transport row (mobile) + if (order.delivery_cost > 0) { + const tVat = order.transport_vat || '21'; + mobileHtml += `
+
+ Transport + x1 + ${fmtNum(order.delivery_cost)} lei + TVA ${tVat} +
+
`; + } + + // Discount rows (mobile) + if (order.discount_total > 0) { + const discSplit = computeDiscountSplit(items, order); + if (discSplit) { + Object.entries(discSplit) + .sort(([a], [b]) => Number(a) - Number(b)) + .forEach(([rate, amt]) => { + if (amt > 0) mobileHtml += `
+
+ Discount + x\u20131 + ${fmtNum(amt)} lei + TVA ${Number(rate)} +
+
`; + }); + } else { + mobileHtml += `
+
+ Discount + x\u20131 + ${fmtNum(order.discount_total)} lei +
+
`; + } + } + + mobileContainer.innerHTML = '
' + mobileHtml + '
'; } - document.getElementById('detailItemsBody').innerHTML = items.map((item, idx) => { - const valoare = (Number(item.price || 0) * Number(item.quantity || 0)).toFixed(2); + let tableHtml = items.map((item, idx) => { + const valoare = Number(item.price || 0) * Number(item.quantity || 0); return ` ${esc(item.sku)} ${esc(item.product_name || '-')} ${renderCodmatCell(item)} - ${item.quantity || 0} - ${item.price != null ? Number(item.price).toFixed(2) : '-'} - ${valoare} + ${item.quantity || 0} + ${item.price != null ? fmtNum(item.price) : '-'} + ${item.vat != null ? Number(item.vat) : '-'} + ${fmtNum(valoare)} `; }).join(''); + + // Transport row + if (order.delivery_cost > 0) { + const tVat = order.transport_vat || '21'; + const tCodmat = order.transport_codmat || ''; + tableHtml += ` + Transport + ${tCodmat ? '' + esc(tCodmat) + '' : ''} + 1${fmtNum(order.delivery_cost)} + ${tVat}${fmtNum(order.delivery_cost)} + `; + } + + // Discount rows (split by VAT rate) + if (order.discount_total > 0) { + const dCodmat = order.discount_codmat || ''; + const discSplit = computeDiscountSplit(items, order); + if (discSplit) { + Object.entries(discSplit) + .sort(([a], [b]) => Number(a) - Number(b)) + .forEach(([rate, amt]) => { + if (amt > 0) tableHtml += ` + Discount + ${dCodmat ? '' + esc(dCodmat) + '' : ''} + \u20131${fmtNum(amt)} + ${Number(rate)}\u2013${fmtNum(amt)} + `; + }); + } else { + tableHtml += ` + Discount + ${dCodmat ? '' + esc(dCodmat) + '' : ''} + \u20131${fmtNum(order.discount_total)} + -\u2013${fmtNum(order.discount_total)} + `; + } + } + + document.getElementById('detailItemsBody').innerHTML = tableHtml; + + // Receipt footer (just total) + renderReceipt(items, order); } catch (err) { document.getElementById('detailError').textContent = err.message; document.getElementById('detailError').style.display = ''; } } +function fmtNum(v) { + return Number(v).toLocaleString('ro-RO', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +} + +function computeDiscountSplit(items, order) { + if (order.discount_split && typeof order.discount_split === 'object') + return order.discount_split; + + // Compute proportionally from items by VAT rate + const byRate = {}; + items.forEach(item => { + const rate = item.vat != null ? Number(item.vat) : null; + if (rate === null) return; + if (!byRate[rate]) byRate[rate] = 0; + byRate[rate] += Number(item.price || 0) * Number(item.quantity || 0); + }); + const rates = Object.keys(byRate).sort((a, b) => Number(a) - Number(b)); + if (rates.length === 0) return null; + + const grandTotal = rates.reduce((s, r) => s + byRate[r], 0); + if (grandTotal <= 0) return null; + + const split = {}; + let remaining = order.discount_total; + rates.forEach((rate, i) => { + if (i === rates.length - 1) { + split[rate] = Math.round(remaining * 100) / 100; + } else { + const amt = Math.round(order.discount_total * byRate[rate] / grandTotal * 100) / 100; + split[rate] = amt; + remaining -= amt; + } + }); + return split; +} + +function renderReceipt(items, order) { + const desktop = document.getElementById('detailReceipt'); + const mobile = document.getElementById('detailReceiptMobile'); + if (!items.length) { + desktop.innerHTML = ''; + mobile.innerHTML = ''; + return; + } + + const total = order.order_total != null ? fmtNum(order.order_total) : '-'; + const html = `Total: ${total} lei`; + desktop.innerHTML = html; + mobile.innerHTML = html; +} + // ── Quick Map Modal ─────────────────────────────── function openQuickMap(sku, productName, orderNumber, itemIdx) { diff --git a/api/app/templates/dashboard.html b/api/app/templates/dashboard.html index ec1ac01..96af8a2 100644 --- a/api/app/templates/dashboard.html +++ b/api/app/templates/dashboard.html @@ -135,12 +135,6 @@
-
- Valoare: - - Discount: - - Transport: - - Total: - -
@@ -148,16 +142,19 @@ - - + + +
SKU Produs CODMATCant.PretCant.PretTVA% Valoare
+
+