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:
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user