feat(ui): order totals, decimals, mobile modal cards, set editing
- Dashboard/Logs: Total column with 2 decimals (order_total) - Order detail modal: totals summary row (items total + order total) - Order detail modal mobile: compact article cards (d-md-none) - Mappings: openEditModal loads all CODMATs for SKU, saveMapping replaces entire set via delete-all + batch POST - Add project-specific team agents: ui-templates, ui-js, ui-verify, backend-api - CLAUDE.md: mandatory preview approval before implementation, fix-loop after verification, server must start via start.sh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -713,3 +713,26 @@ tr.mapping-deleted td {
|
||||
/* Hide per-page selector on mobile */
|
||||
.per-page-label { display: none; }
|
||||
}
|
||||
|
||||
/* Mobile article cards in order detail modal */
|
||||
.detail-item-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.detail-item-card .card-sku {
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
.detail-item-card .card-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.detail-item-card .card-details {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
@@ -295,7 +295,6 @@ async function loadDashOrders() {
|
||||
|
||||
// Invoice info
|
||||
let invoiceBadge = '';
|
||||
let invoiceTotal = '';
|
||||
if (o.status !== 'IMPORTED' && o.status !== 'ALREADY_IMPORTED') {
|
||||
invoiceBadge = '<span class="text-muted">-</span>';
|
||||
} else if (o.invoice && o.invoice.facturat) {
|
||||
@@ -303,11 +302,12 @@ async function loadDashOrders() {
|
||||
if (o.invoice.serie_act || o.invoice.numar_act) {
|
||||
invoiceBadge += `<br><small>${esc(o.invoice.serie_act || '')} ${esc(String(o.invoice.numar_act || ''))}</small>`;
|
||||
}
|
||||
invoiceTotal = o.invoice.total_cu_tva ? Number(o.invoice.total_cu_tva).toFixed(2) : '-';
|
||||
} else {
|
||||
invoiceBadge = `<span style="color:#dc2626">Nefacturat</span>`;
|
||||
}
|
||||
|
||||
const orderTotal = o.order_total != null ? Number(o.order_total).toFixed(2) : '-';
|
||||
|
||||
return `<tr style="cursor:pointer" onclick="openDashOrderDetail('${esc(o.order_number)}')">
|
||||
<td><code>${esc(o.order_number)}</code></td>
|
||||
<td>${dateStr}</td>
|
||||
@@ -316,7 +316,7 @@ async function loadDashOrders() {
|
||||
<td class="text-nowrap">${statusDot(o.status)} ${statusLabelText(o.status)}</td>
|
||||
<td>${o.id_comanda || '-'}</td>
|
||||
<td>${invoiceBadge}</td>
|
||||
<td>${invoiceTotal}</td>
|
||||
<td>${orderTotal}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -335,7 +335,7 @@ async function loadDashOrders() {
|
||||
if (d.length >= 16) dateFmt += ' ' + d.slice(11, 16);
|
||||
}
|
||||
const name = o.shipping_name || o.customer_name || o.billing_name || '\u2014';
|
||||
const totalStr = o.order_total ? Math.round(o.order_total) : '';
|
||||
const totalStr = o.order_total ? Number(o.order_total).toFixed(2) : '';
|
||||
return `<div class="flat-row" onclick="openDashOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem">
|
||||
${statusDot(o.status)}
|
||||
<span style="color:#6b7280" class="text-nowrap">${dateFmt}</span>
|
||||
@@ -475,6 +475,12 @@ async function openDashOrderDetail(orderNumber) {
|
||||
document.getElementById('detailIdAdresaLivr').textContent = '-';
|
||||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center">Se incarca...</td></tr>';
|
||||
document.getElementById('detailError').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 = '';
|
||||
|
||||
const modalEl = document.getElementById('orderDetailModal');
|
||||
const existing = bootstrap.Modal.getInstance(modalEl);
|
||||
@@ -510,6 +516,36 @@ async function openDashOrderDetail(orderNumber) {
|
||||
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' : '-';
|
||||
|
||||
// Mobile article cards
|
||||
const mobileContainer = document.getElementById('detailItemsMobile');
|
||||
if (mobileContainer) {
|
||||
mobileContainer.innerHTML = items.map(item => {
|
||||
let statusLabel = '';
|
||||
switch (item.mapping_status) {
|
||||
case 'mapped': statusLabel = '<span class="badge bg-success">Mapat</span>'; break;
|
||||
case 'direct': statusLabel = '<span class="badge bg-info">Direct</span>'; break;
|
||||
case 'missing': statusLabel = '<span class="badge bg-warning">Lipsa</span>'; break;
|
||||
default: statusLabel = '<span class="badge bg-secondary">?</span>';
|
||||
}
|
||||
const codmat = item.codmat || '-';
|
||||
return `<div class="detail-item-card">
|
||||
<div class="card-sku">${esc(item.sku)}</div>
|
||||
<div class="card-name">${esc(item.product_name || '-')}</div>
|
||||
<div class="card-details">
|
||||
<span>x${item.quantity || 0}</span>
|
||||
<span>${item.price != null ? Number(item.price).toFixed(2) : '-'} lei</span>
|
||||
<span><code>${esc(codmat)}</code></span>
|
||||
<span>${statusLabel}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
document.getElementById('detailItemsBody').innerHTML = items.map(item => {
|
||||
let statusBadge;
|
||||
switch (item.mapping_status) {
|
||||
|
||||
@@ -151,10 +151,11 @@ async function loadRunOrders(runId, statusFilter, page) {
|
||||
const orders = data.orders || [];
|
||||
|
||||
if (orders.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-3">Nicio comanda</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-3">Nicio comanda</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = orders.map((o, i) => {
|
||||
const dateStr = fmtDate(o.order_date);
|
||||
const orderTotal = o.order_total != null ? Number(o.order_total).toFixed(2) : '-';
|
||||
return `<tr style="cursor:pointer" onclick="openOrderDetail('${esc(o.order_number)}')">
|
||||
<td>${(ordersPage - 1) * 50 + i + 1}</td>
|
||||
<td>${dateStr}</td>
|
||||
@@ -162,6 +163,7 @@ async function loadRunOrders(runId, statusFilter, page) {
|
||||
<td>${esc(o.customer_name)}</td>
|
||||
<td>${o.items_count || 0}</td>
|
||||
<td class="text-nowrap">${statusDot(o.status)} ${logStatusText(o.status)}</td>
|
||||
<td>${orderTotal}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
@@ -179,7 +181,7 @@ async function loadRunOrders(runId, statusFilter, page) {
|
||||
dateFmt = d.slice(8, 10) + '.' + d.slice(5, 7) + '.' + d.slice(2, 4);
|
||||
if (d.length >= 16) dateFmt += ' ' + d.slice(11, 16);
|
||||
}
|
||||
const totalStr = o.order_total ? Math.round(o.order_total) : '';
|
||||
const totalStr = o.order_total ? Number(o.order_total).toFixed(2) : '';
|
||||
return `<div class="flat-row" onclick="openOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem">
|
||||
${statusDot(o.status)}
|
||||
<span style="color:#6b7280" class="text-nowrap">${dateFmt}</span>
|
||||
@@ -318,6 +320,12 @@ async function openOrderDetail(orderNumber) {
|
||||
document.getElementById('detailIdAdresaLivr').textContent = '-';
|
||||
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="8" class="text-center">Se incarca...</td></tr>';
|
||||
document.getElementById('detailError').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 = '';
|
||||
|
||||
const modalEl = document.getElementById('orderDetailModal');
|
||||
const existing = bootstrap.Modal.getInstance(modalEl);
|
||||
@@ -353,6 +361,36 @@ async function openOrderDetail(orderNumber) {
|
||||
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' : '-';
|
||||
|
||||
// Mobile article cards
|
||||
const mobileContainer = document.getElementById('detailItemsMobile');
|
||||
if (mobileContainer) {
|
||||
mobileContainer.innerHTML = items.map(item => {
|
||||
let statusLabel = '';
|
||||
switch (item.mapping_status) {
|
||||
case 'mapped': statusLabel = '<span class="badge bg-success">Mapat</span>'; break;
|
||||
case 'direct': statusLabel = '<span class="badge bg-info">Direct</span>'; break;
|
||||
case 'missing': statusLabel = '<span class="badge bg-warning">Lipsa</span>'; break;
|
||||
default: statusLabel = '<span class="badge bg-secondary">?</span>';
|
||||
}
|
||||
const codmat = item.codmat || '-';
|
||||
return `<div class="detail-item-card">
|
||||
<div class="card-sku">${esc(item.sku)}</div>
|
||||
<div class="card-name">${esc(item.product_name || '-')}</div>
|
||||
<div class="card-details">
|
||||
<span>x${item.quantity || 0}</span>
|
||||
<span>${item.price != null ? Number(item.price).toFixed(2) : '-'} lei</span>
|
||||
<span><code>${esc(codmat)}</code></span>
|
||||
<span>${statusLabel}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
document.getElementById('detailItemsBody').innerHTML = items.map(item => {
|
||||
let statusBadge;
|
||||
switch (item.mapping_status) {
|
||||
|
||||
@@ -276,23 +276,50 @@ function clearAddForm() {
|
||||
addCodmatLine();
|
||||
}
|
||||
|
||||
function openEditModal(sku, codmat, cantitate, procent) {
|
||||
async function openEditModal(sku, codmat, cantitate, procent) {
|
||||
editingMapping = { sku, codmat };
|
||||
document.getElementById('addModalTitle').textContent = 'Editare Mapare';
|
||||
document.getElementById('inputSku').value = sku;
|
||||
document.getElementById('inputSku').readOnly = false;
|
||||
document.getElementById('inputSku').readOnly = true;
|
||||
document.getElementById('pctWarning').style.display = 'none';
|
||||
|
||||
const container = document.getElementById('codmatLines');
|
||||
container.innerHTML = '';
|
||||
addCodmatLine();
|
||||
|
||||
// Pre-fill the CODMAT line
|
||||
const line = container.querySelector('.codmat-line');
|
||||
if (line) {
|
||||
line.querySelector('.cl-codmat').value = codmat;
|
||||
line.querySelector('.cl-cantitate').value = cantitate;
|
||||
line.querySelector('.cl-procent').value = procent;
|
||||
try {
|
||||
// Fetch all CODMATs for this SKU
|
||||
const res = await fetch(`/api/mappings?search=${encodeURIComponent(sku)}&per_page=100`);
|
||||
const data = await res.json();
|
||||
const allMappings = (data.mappings || []).filter(m => m.sku === sku && !m.sters);
|
||||
|
||||
if (allMappings.length === 0) {
|
||||
// Fallback to single line with passed values
|
||||
addCodmatLine();
|
||||
const line = container.querySelector('.codmat-line');
|
||||
if (line) {
|
||||
line.querySelector('.cl-codmat').value = codmat;
|
||||
line.querySelector('.cl-cantitate').value = cantitate;
|
||||
line.querySelector('.cl-procent').value = procent;
|
||||
}
|
||||
} else {
|
||||
for (const m of allMappings) {
|
||||
addCodmatLine();
|
||||
const lines = container.querySelectorAll('.codmat-line');
|
||||
const line = lines[lines.length - 1];
|
||||
line.querySelector('.cl-codmat').value = m.codmat;
|
||||
line.querySelector('.cl-cantitate').value = m.cantitate_roa;
|
||||
line.querySelector('.cl-procent').value = m.procent_pret;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback on error
|
||||
addCodmatLine();
|
||||
const line = container.querySelector('.codmat-line');
|
||||
if (line) {
|
||||
line.querySelector('.cl-codmat').value = codmat;
|
||||
line.querySelector('.cl-cantitate').value = cantitate;
|
||||
line.querySelector('.cl-procent').value = procent;
|
||||
}
|
||||
}
|
||||
|
||||
new bootstrap.Modal(document.getElementById('addModal')).show();
|
||||
@@ -395,17 +422,38 @@ async function saveMapping() {
|
||||
let res;
|
||||
|
||||
if (editingMapping) {
|
||||
// Edit mode: use PUT /api/mappings/{old_sku}/{old_codmat}/edit
|
||||
res = await fetch(`/api/mappings/${encodeURIComponent(editingMapping.sku)}/${encodeURIComponent(editingMapping.codmat)}/edit`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
new_sku: sku,
|
||||
new_codmat: mappings[0].codmat,
|
||||
cantitate_roa: mappings[0].cantitate_roa,
|
||||
procent_pret: mappings[0].procent_pret
|
||||
})
|
||||
});
|
||||
if (mappings.length === 1) {
|
||||
// Single CODMAT edit: use existing PUT endpoint
|
||||
res = await fetch(`/api/mappings/${encodeURIComponent(editingMapping.sku)}/${encodeURIComponent(editingMapping.codmat)}/edit`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
new_sku: sku,
|
||||
new_codmat: mappings[0].codmat,
|
||||
cantitate_roa: mappings[0].cantitate_roa,
|
||||
procent_pret: mappings[0].procent_pret
|
||||
})
|
||||
});
|
||||
} else {
|
||||
// Multi-CODMAT set: delete all existing then create new batch
|
||||
const existRes = await fetch(`/api/mappings?search=${encodeURIComponent(editingMapping.sku)}&per_page=100`);
|
||||
const existData = await existRes.json();
|
||||
const existing = (existData.mappings || []).filter(m => m.sku === editingMapping.sku && !m.sters);
|
||||
|
||||
// Delete each existing CODMAT
|
||||
for (const m of existing) {
|
||||
await fetch(`/api/mappings/${encodeURIComponent(m.sku)}/${encodeURIComponent(m.codmat)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
// Create new batch
|
||||
res = await fetch('/api/mappings/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sku, mappings })
|
||||
});
|
||||
}
|
||||
} else if (mappings.length === 1) {
|
||||
res = await fetch('/api/mappings', {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user