feat(orders): add resync and delete order buttons

Resync soft-deletes from Oracle then re-imports from GoMag with fresh
article data. Delete soft-deletes and marks DELETED_IN_ROA. Both have
invoice safety gates (refuse if invoiced or Oracle unavailable).

UI: split modal footer (Delete left, Resync+Close right), inline
confirm pattern (no native confirm()), dashboard row hover action
icons, disabled+tooltip for invoiced orders. 8 unit tests for safety
gates and happy paths.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-09 13:10:01 +00:00
parent 90a4906d87
commit 25f73db64d
8 changed files with 621 additions and 34 deletions

View File

@@ -367,6 +367,47 @@ input[type="checkbox"] {
border-color: var(--info-hover);
}
.btn-outline-warning {
color: var(--warning);
border-color: var(--warning);
}
.btn-outline-warning:hover {
background: var(--warning);
border-color: var(--warning);
color: #fff;
}
.btn-outline-danger {
color: var(--error);
border-color: var(--error);
}
.btn-outline-danger:hover {
background: var(--error);
border-color: var(--error);
color: #fff;
}
/* Compact button for dashboard row actions */
.btn-xs {
font-size: 0.75rem;
line-height: 1;
padding: 4px 8px;
}
/* Dashboard row hover actions */
#dashOrdersBody tr { position: relative; }
#dashOrdersBody tr .row-actions {
display: none;
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
gap: 4px;
z-index: 1;
}
#dashOrdersBody tr:hover .row-actions {
display: inline-flex;
}
/* ── Forms ───────────────────────────────────────── */
.form-control, .form-select {
font-size: 0.9375rem;

View File

@@ -375,7 +375,7 @@ async function loadDashOrders() {
<td>${o.items_count || 0}</td>
<td class="text-end text-muted">${fmtCost(o.delivery_cost)}</td>
<td class="text-end text-muted">${fmtCost(o.discount_total)}</td>
<td class="text-end fw-bold">${orderTotal}</td>
<td class="text-end fw-bold" style="position:relative">${orderTotal}${(o.status === 'IMPORTED' || o.status === 'ALREADY_IMPORTED') && !(o.invoice && o.invoice.facturat) ? '<span class="row-actions"><button class="btn btn-xs btn-outline-warning" aria-label="Resync comanda" title="Resync" onclick="event.stopPropagation(); dashResyncOrder(\'' + esc(o.order_number) + '\', this)"><i class="bi bi-arrow-repeat"></i></button><button class="btn btn-xs btn-outline-danger" aria-label="Sterge din ROA" title="Sterge din ROA" onclick="event.stopPropagation(); dashDeleteOrder(\'' + esc(o.order_number) + '\', this)"><i class="bi bi-trash"></i></button></span>' : ''}</td>
</tr>`;
}).join('');
}
@@ -595,3 +595,69 @@ function openDashQuickMap(sku, productName, orderNumber, itemIdx) {
});
}
// ── Dashboard row action handlers ────────────────
function dashResyncOrder(orderNumber, btn) {
inlineConfirmAction(btn, '?', async (b) => {
try {
const res = await fetch(`/api/orders/${encodeURIComponent(orderNumber)}/resync`, { method: 'POST' });
const data = await res.json();
if (data.success) {
b.innerHTML = '<i class="bi bi-check-circle"></i>';
b.className = 'btn btn-xs btn-success';
setTimeout(() => loadDashOrders(), 1500);
} else {
b.innerHTML = '<i class="bi bi-exclamation-triangle"></i>';
b.className = 'btn btn-xs btn-danger';
b.title = data.message || 'Eroare';
setTimeout(() => {
b.innerHTML = '<i class="bi bi-arrow-repeat"></i>';
b.className = 'btn btn-xs btn-outline-warning';
b.disabled = false;
b.title = 'Resync';
}, 3000);
}
} catch (err) {
b.innerHTML = '<i class="bi bi-exclamation-triangle"></i>';
b.disabled = false;
}
}, {
defaultHtml: '<i class="bi bi-arrow-repeat"></i>',
loadingText: '',
confirmClass: 'btn-warning',
defaultBtnClass: 'btn-outline-warning'
});
}
function dashDeleteOrder(orderNumber, btn) {
inlineConfirmAction(btn, '?', async (b) => {
try {
const res = await fetch(`/api/orders/${encodeURIComponent(orderNumber)}/delete`, { method: 'POST' });
const data = await res.json();
if (data.success) {
b.innerHTML = '<i class="bi bi-check-circle"></i>';
b.className = 'btn btn-xs btn-success';
setTimeout(() => loadDashOrders(), 1500);
} else {
b.innerHTML = '<i class="bi bi-exclamation-triangle"></i>';
b.className = 'btn btn-xs btn-danger';
b.title = data.message || 'Eroare';
setTimeout(() => {
b.innerHTML = '<i class="bi bi-trash"></i>';
b.className = 'btn btn-xs btn-outline-danger';
b.disabled = false;
b.title = 'Sterge din ROA';
}, 3000);
}
} catch (err) {
b.innerHTML = '<i class="bi bi-exclamation-triangle"></i>';
b.disabled = false;
}
}, {
defaultHtml: '<i class="bi bi-trash"></i>',
loadingText: '',
confirmClass: 'btn-danger',
defaultBtnClass: 'btn-outline-danger'
});
}

View File

@@ -735,7 +735,7 @@ async function renderOrderDetailModal(orderNumber, opts) {
// Retry button (only for ERROR/SKIPPED orders)
const retryBtn = document.getElementById('detailRetryBtn');
if (retryBtn) {
const canRetry = ['ERROR', 'SKIPPED'].includes((order.status || '').toUpperCase());
const canRetry = ['ERROR', 'SKIPPED', 'DELETED_IN_ROA'].includes((order.status || '').toUpperCase());
retryBtn.style.display = canRetry ? '' : 'none';
if (canRetry) {
retryBtn.onclick = async () => {
@@ -766,6 +766,106 @@ async function renderOrderDetailModal(orderNumber, opts) {
}
}
// Resync button (IMPORTED/ALREADY_IMPORTED only)
const resyncBtn = document.getElementById('detailResyncBtn');
if (resyncBtn) {
const canResync = ['IMPORTED', 'ALREADY_IMPORTED'].includes((order.status || '').toUpperCase());
resyncBtn.style.display = canResync ? '' : 'none';
if (canResync) {
const isInvoiced = !!(order.factura_numar);
if (isInvoiced) {
resyncBtn.disabled = true;
resyncBtn.style.opacity = '0.5';
resyncBtn.style.pointerEvents = 'none';
resyncBtn.title = 'Comanda facturata';
} else {
resyncBtn.disabled = false;
resyncBtn.style.opacity = '';
resyncBtn.style.pointerEvents = '';
resyncBtn.title = '';
resyncBtn.onclick = () => {
inlineConfirmAction(resyncBtn, 'Confirmi resync?', async (btn) => {
try {
const res = await fetch(`/api/orders/${encodeURIComponent(orderNumber)}/resync`, { method: 'POST' });
const data = await res.json();
if (data.success) {
btn.innerHTML = '<i class="bi bi-check-circle"></i> Reimportat';
btn.className = 'btn btn-sm btn-success';
setTimeout(() => renderOrderDetailModal(orderNumber, opts), 1500);
} else {
btn.innerHTML = '<i class="bi bi-exclamation-triangle"></i> ' + (data.message || 'Eroare');
btn.className = 'btn btn-sm btn-danger';
setTimeout(() => {
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i> Resync';
btn.className = 'btn btn-sm btn-outline-warning';
btn.disabled = false;
}, 3000);
}
} catch (err) {
btn.innerHTML = 'Eroare: ' + err.message;
btn.disabled = false;
}
}, {
defaultHtml: '<i class="bi bi-arrow-repeat"></i> Resync',
loadingText: 'Resync...',
confirmClass: 'btn-warning',
defaultBtnClass: 'btn-outline-warning'
});
};
}
}
}
// Delete button (IMPORTED/ALREADY_IMPORTED only)
const deleteBtn = document.getElementById('detailDeleteBtn');
if (deleteBtn) {
const canDelete = ['IMPORTED', 'ALREADY_IMPORTED'].includes((order.status || '').toUpperCase());
deleteBtn.style.display = canDelete ? '' : 'none';
if (canDelete) {
const isInvoiced = !!(order.factura_numar);
if (isInvoiced) {
deleteBtn.disabled = true;
deleteBtn.style.opacity = '0.5';
deleteBtn.style.pointerEvents = 'none';
deleteBtn.title = 'Comanda facturata';
} else {
deleteBtn.disabled = false;
deleteBtn.style.opacity = '';
deleteBtn.style.pointerEvents = '';
deleteBtn.title = '';
deleteBtn.onclick = () => {
inlineConfirmAction(deleteBtn, 'Confirmi stergerea?', async (btn) => {
try {
const res = await fetch(`/api/orders/${encodeURIComponent(orderNumber)}/delete`, { method: 'POST' });
const data = await res.json();
if (data.success) {
btn.innerHTML = '<i class="bi bi-check-circle"></i> Sters';
btn.className = 'btn btn-sm btn-success';
setTimeout(() => renderOrderDetailModal(orderNumber, opts), 1500);
} else {
btn.innerHTML = '<i class="bi bi-exclamation-triangle"></i> ' + (data.message || 'Eroare');
btn.className = 'btn btn-sm btn-danger';
setTimeout(() => {
btn.innerHTML = '<i class="bi bi-trash"></i> Sterge din ROA';
btn.className = 'btn btn-sm btn-outline-danger';
btn.disabled = false;
}, 3000);
}
} catch (err) {
btn.innerHTML = 'Eroare: ' + err.message;
btn.disabled = false;
}
}, {
defaultHtml: '<i class="bi bi-trash"></i> Sterge din ROA',
loadingText: 'Stergere...',
confirmClass: 'btn-danger',
defaultBtnClass: 'btn-outline-danger'
});
};
}
}
}
if (opts.onAfterRender) opts.onAfterRender(order, items);
} catch (err) {
document.getElementById('detailError').textContent = err.message;
@@ -779,6 +879,27 @@ function _sharedModalQuickMap(sku, productName, orderNumber, itemIdx) {
if (_sharedModalQuickMapFn) _sharedModalQuickMapFn(sku, productName, orderNumber, itemIdx);
}
// ── Inline confirm helper (shared: modal + dashboard) ──────
function inlineConfirmAction(btn, confirmText, actionFn, opts) {
if (btn.dataset.confirming === 'true') {
btn.dataset.confirming = 'false';
clearTimeout(btn._resetTimer);
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> ' + opts.loadingText;
actionFn(btn);
} else {
btn.dataset.confirming = 'true';
btn._origClass = btn.className;
btn.innerHTML = confirmText;
btn.className = btn.className.replace(/btn-outline-\w+/, opts.confirmClass);
btn._resetTimer = setTimeout(() => {
btn.dataset.confirming = 'false';
btn.innerHTML = opts.defaultHtml;
btn.className = btn._origClass;
}, 3000);
}
}
// ── Dot helper ────────────────────────────────────
function statusDot(status) {
switch ((status || '').toUpperCase()) {