fix(anaf-dedup): fix ANAF parsing, facturare addr, compact modal layout

- Fix ANAF API: extract CUI from date_generale (not top-level), fix
  notFound casing (capital F)
- Fix missing facturare address when same ID as livrare (copy instead
  of skip)
- Replace ANAF cache pre-population stub with real logic (3-month CUIs)
- Restructure order detail modal: inline 2-col GOMAG|ROA layout with
  compact address lines replacing collapsed sections
- Fix addrMatch() to use field-level comparison with Romanian
  abbreviation stripping (STR, NR, BL, SC, AP, ET, ETAJ, APART)
- Add dashboard "Diferente" filter pill for ANAF-adjusted orders
- Update e2e test for new modal structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-01 20:07:37 +00:00
parent 2f593c30f6
commit e8b42088e3
10 changed files with 308 additions and 243 deletions

View File

@@ -329,6 +329,7 @@ async function loadDashOrders() {
if (el('cntFact')) el('cntFact').textContent = c.facturate || 0;
if (el('cntNef')) el('cntNef').textContent = c.nefacturate || c.uninvoiced || 0;
if (el('cntCanc')) el('cntCanc').textContent = c.cancelled || 0;
if (el('cntDiff')) el('cntDiff').textContent = c.diffs || 0;
// Attention card
const attnEl = document.getElementById('attentionCard');
@@ -336,10 +337,11 @@ async function loadDashOrders() {
const errors = c.error || 0;
const unmapped = c.unresolved_skus || 0;
const nefact = c.nefacturate || 0;
const diffs = c.diffs || 0;
const incompleteAddr = c.incomplete_addresses || 0;
if (errors === 0 && unmapped === 0 && nefact === 0 && incompleteAddr === 0) {
if (errors === 0 && unmapped === 0 && nefact === 0 && incompleteAddr === 0 && diffs === 0) {
attnEl.innerHTML = '<div class="attention-card attention-ok"><i class="bi bi-check-circle"></i> Totul in ordine</div>';
} else {
let items = [];
@@ -347,6 +349,7 @@ async function loadDashOrders() {
if (unmapped > 0) items.push(`<span class="attention-item attention-warning" onclick="window.location='${window.ROOT_PATH||''}/missing-skus'"><i class="bi bi-puzzle"></i> ${unmapped} SKU-uri nemapate</span>`);
if (nefact > 0) items.push(`<span class="attention-item attention-warning" onclick="document.querySelector('.filter-pill[data-status=UNINVOICED]')?.click()"><i class="bi bi-receipt"></i> ${nefact} nefacturate</span>`);
if (c.incomplete_addresses > 0) items.push(`<span class="attention-item attention-warning"><i class="bi bi-geo-alt"></i> ${c.incomplete_addresses} adrese incomplete</span>`);
if (diffs > 0) items.push(`<span class="attention-item attention-warning" onclick="document.querySelector('.filter-pill[data-status=DIFFS]')?.click()"><i class="bi bi-exclamation-diamond"></i> ${diffs} diferente ANAF</span>`);
attnEl.innerHTML = '<div class="attention-card attention-alert">' + items.join('') + '</div>';
}
}
@@ -410,7 +413,8 @@ async function loadDashOrders() {
{ label: 'Erori', count: c.error || c.errors || 0, value: 'ERROR', active: activeStatus === 'ERROR', colorClass: 'fc-red' },
{ label: 'Fact.', count: c.facturate || 0, value: 'INVOICED', active: activeStatus === 'INVOICED', colorClass: 'fc-green' },
{ label: 'Nefact.', count: c.nefacturate || c.uninvoiced || 0, value: 'UNINVOICED', active: activeStatus === 'UNINVOICED', colorClass: 'fc-red' },
{ label: 'Anulate', count: c.cancelled || 0, value: 'CANCELLED', active: activeStatus === 'CANCELLED', colorClass: 'fc-dark' }
{ label: 'Anulate', count: c.cancelled || 0, value: 'CANCELLED', active: activeStatus === 'CANCELLED', colorClass: 'fc-dark' },
{ label: 'Dif.', count: c.diffs || 0, value: 'DIFFS', active: activeStatus === 'DIFFS', colorClass: 'fc-orange' }
], (val) => {
document.querySelectorAll('.filter-pill[data-status]').forEach(b => b.classList.remove('active'));
const pill = document.querySelector(`.filter-pill[data-status="${val}"]`);

View File

@@ -501,8 +501,6 @@ async function renderOrderDetailModal(orderNumber, opts) {
document.getElementById('detailStatus').innerHTML = '';
document.getElementById('detailIdComanda').textContent = '-';
document.getElementById('detailIdPartener').textContent = '-';
document.getElementById('detailIdAdresaFact').textContent = '-';
document.getElementById('detailIdAdresaLivr').textContent = '-';
document.getElementById('detailItemsBody').innerHTML = '<tr><td colspan="9" class="text-center">Se incarca...</td></tr>';
document.getElementById('detailError').style.display = 'none';
const retryBtn = document.getElementById('detailRetryBtn');
@@ -519,15 +517,21 @@ async function renderOrderDetailModal(orderNumber, opts) {
if (priceCheckEl) priceCheckEl.innerHTML = '';
const reconEl = document.getElementById('detailInvoiceRecon');
if (reconEl) { reconEl.innerHTML = ''; reconEl.style.display = 'none'; }
// Reset partner/address sections
const partnerSection = document.getElementById('detailPartnerSection');
if (partnerSection) partnerSection.style.display = 'none';
const addressSection = document.getElementById('detailAddressSection');
if (addressSection) addressSection.style.display = 'none';
const partnerBody = document.getElementById('partnerInfoBody');
if (partnerBody) partnerBody.innerHTML = '';
const addressBody = document.getElementById('addressInfoBody');
if (addressBody) addressBody.innerHTML = '';
// Reset compact header elements
const partenerRoa = document.getElementById('detailPartenerRoa');
if (partenerRoa) { partenerRoa.style.display = 'none'; partenerRoa.textContent = ''; }
const cuiGomag = document.getElementById('detailCuiGomag');
if (cuiGomag) cuiGomag.style.display = 'none';
const cuiRoa = document.getElementById('detailCuiRoa');
if (cuiRoa) cuiRoa.style.display = 'none';
const anafArea = document.getElementById('detailPartnerAnafArea');
if (anafArea) anafArea.innerHTML = '';
const denomMismatch = document.getElementById('detailDenomMismatch');
if (denomMismatch) { denomMismatch.style.display = 'none'; denomMismatch.innerHTML = ''; }
const addressBlock = document.getElementById('detailAddressBlock');
if (addressBlock) addressBlock.style.display = 'none';
const addressLines = document.getElementById('detailAddressLines');
if (addressLines) addressLines.innerHTML = '';
const modalEl = document.getElementById('orderDetailModal');
const existing = bootstrap.Modal.getInstance(modalEl);
@@ -563,8 +567,6 @@ async function renderOrderDetailModal(orderNumber, opts) {
document.getElementById('detailIdComanda').textContent = order.id_comanda || '-';
document.getElementById('detailIdPartener').textContent = order.id_partener || '-';
document.getElementById('detailIdAdresaFact').textContent = order.id_adresa_facturare || '-';
document.getElementById('detailIdAdresaLivr').textContent = order.id_adresa_livrare || '-';
// Invoice info
const inv = order.invoice;
@@ -592,9 +594,8 @@ async function renderOrderDetailModal(orderNumber, opts) {
reconEl.style.display = 'none';
}
// Render partner + address sections
_renderPartnerSection(order);
_renderAddressSection(order);
// Render compact header info (partner + addresses)
_renderHeaderInfo(order);
if (order.error_message) {
document.getElementById('detailError').textContent = order.error_message;
@@ -831,197 +832,161 @@ function statusDot(status) {
}
}
// ── Partner & Address Section Rendering ──────────
// ── Address helpers (module scope) ───────────────
function _renderPartnerSection(order) {
const section = document.getElementById('detailPartnerSection');
const body = document.getElementById('partnerInfoBody');
const alertEl = document.getElementById('partnerAlertCount');
if (!section || !body) return;
function fmtAddr(a) {
if (!a) return '\u2014';
if (typeof a === 'string') return a;
const parts = [a.address || a.strada || '', a.numar || ''].filter(Boolean);
const line1 = parts.join(' ').trim();
const line2 = [a.city || a.localitate || '', a.region || a.judet || ''].filter(Boolean).join(', ');
return [line1, line2].filter(Boolean).join(', ');
}
function addrMatch(gomag, roa) {
if (!gomag || !roa) return true; // can't compare
function norm(s) {
return (s || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.toUpperCase()
.replace(/\b(STR|NR|BL|SC|AP|ET|ETAJ|APART)\b/g, '')
.replace(/[^A-Z0-9]/g, '');
}
const gStreet = norm(gomag.address || gomag.strada || '');
const rStreet = norm((roa.strada || '') + (roa.numar || ''));
const gCity = norm(gomag.city || gomag.localitate || '');
const rCity = norm(roa.localitate || '');
const gRegion = norm(gomag.region || gomag.judet || '');
const rRegion = norm(roa.judet || '');
return gStreet === rStreet && gCity === rCity && gRegion === rRegion;
}
function hasEfacturaRisk(roa) {
if (!roa || typeof roa === 'string') return false;
return !roa.judet || !roa.localitate;
}
// ── Compact Header Info Rendering ────────────────
function _renderHeaderInfo(order) {
const pi = order.partner_info;
if (!pi || !pi.cod_fiscal_gomag) {
section.style.display = 'none';
return;
}
const isPJ = pi && pi.cod_fiscal_gomag;
section.style.display = '';
let alertCount = 0;
if (pi.anaf_cod_fiscal_adjusted) alertCount++;
if (pi.anaf_denumire_mismatch) alertCount++;
if (alertEl) {
if (alertCount > 0) {
alertEl.textContent = alertCount + (alertCount === 1 ? ' alerta' : ' alerte');
alertEl.style.display = '';
} else {
alertEl.style.display = 'none';
// GoMag CUI (PJ only)
if (isPJ) {
const cuiGomagEl = document.getElementById('detailCuiGomag');
if (cuiGomagEl) {
document.getElementById('detailCuiGomagVal').textContent = pi.cod_fiscal_gomag;
cuiGomagEl.style.display = '';
}
}
// ANAF badge
let anafBadge;
if (pi.anaf_platitor_tva === 1) {
anafBadge = '<span class="anaf-badge anaf-badge-ok">Platitor TVA</span>';
} else if (pi.anaf_platitor_tva === 0) {
anafBadge = '<span class="anaf-badge anaf-badge-warn">Neplatitor TVA</span>';
} else {
anafBadge = '<span class="anaf-badge anaf-badge-gray">Neverificat</span>';
}
// CUI correction badge
let cuiCorrBadge = '';
if (pi.anaf_cod_fiscal_adjusted) {
cuiCorrBadge = ' <span class="anaf-badge anaf-badge-warn"><i class="bi bi-arrow-left-right"></i> Corectat ANAF</span>';
}
// Denomination mismatch
let denomHtml = '';
if (pi.anaf_denumire_mismatch && pi.denumire_anaf) {
denomHtml = `<div style="background:var(--warning-light);padding:8px 12px;border-radius:var(--card-radius);margin-top:8px">
<span class="partner-label" style="color:var(--warning-text)"><i class="bi bi-exclamation-triangle"></i> Denumire diferita</span><br>
<span style="font-size:13px">GoMag: <strong>${esc(order.customer_name || '')}</strong></span><br>
<span style="font-size:13px">ANAF: <strong>${esc(pi.denumire_anaf)}</strong></span>
</div>`;
}
body.innerHTML = `
<div class="partner-row">
<div class="partner-field">
<div class="partner-label">CUI GoMag</div>
<div class="partner-value">${esc(pi.cod_fiscal_gomag)}</div>
</div>
<div class="partner-field">
<div class="partner-label">CUI ROA</div>
<div class="partner-value">${esc(pi.cod_fiscal_roa || '-')}${cuiCorrBadge}</div>
</div>
<div class="partner-field">
<div class="partner-label">Partener ROA</div>
<div style="font-family:var(--font-body);font-size:14px;font-weight:500">${esc(pi.denumire_roa || '-')}</div>
</div>
</div>
<div class="partner-row">
<div class="partner-field">
<div class="partner-label">ANAF</div>
<div>${anafBadge}</div>
</div>
</div>
${denomHtml}`;
// Auto-expand on mismatch
if (alertCount > 0) {
const collapseEl = document.getElementById('detailPartnerInfo');
if (collapseEl && !collapseEl.classList.contains('show')) {
new bootstrap.Collapse(collapseEl, { show: true });
// ROA column
if (isPJ && pi.denumire_roa) {
const partenerRoa = document.getElementById('detailPartenerRoa');
if (partenerRoa) {
partenerRoa.textContent = pi.denumire_roa;
partenerRoa.style.display = '';
}
}
}
function _renderAddressSection(order) {
const section = document.getElementById('detailAddressSection');
const body = document.getElementById('addressInfoBody');
const alertEl = document.getElementById('addressAlertCount');
if (!section || !body) return;
if (isPJ) {
const cuiRoaEl = document.getElementById('detailCuiRoa');
if (cuiRoaEl) {
document.getElementById('detailCuiRoaVal').textContent = pi.cod_fiscal_roa || '\u2014';
cuiRoaEl.style.display = '';
// CUI correction badge
let cuiCorrHtml = '';
if (pi.anaf_cod_fiscal_adjusted) {
cuiCorrHtml = ' <span class="anaf-badge anaf-badge-warn"><i class="bi bi-arrow-left-right"></i> Corectat</span>';
}
// ANAF badge
const anafArea = document.getElementById('detailPartnerAnafArea');
if (anafArea) {
let anafBadge;
if (pi.anaf_platitor_tva === 1) {
anafBadge = '<span class="anaf-badge anaf-badge-ok">Platitor TVA</span>';
} else if (pi.anaf_platitor_tva === 0) {
anafBadge = '<span class="anaf-badge anaf-badge-warn">Neplatitor TVA</span>';
} else {
anafBadge = '<span class="anaf-badge anaf-badge-gray">?</span>';
}
anafArea.innerHTML = cuiCorrHtml + ' ' + anafBadge;
}
}
}
// ERROR orders: muted dashes for ROA fields
if (order.status === 'ERROR' && !order.id_comanda) {
document.getElementById('detailIdComanda').innerHTML = '<span class="text-muted">\u2014</span>';
document.getElementById('detailIdPartener').innerHTML = '<span class="text-muted">\u2014</span>';
}
// Denomination mismatch alert
if (isPJ && pi.anaf_denumire_mismatch && pi.denumire_anaf) {
const denomEl = document.getElementById('detailDenomMismatch');
if (denomEl) {
denomEl.innerHTML = `<div style="background:var(--warning-light);padding:8px 12px;border-radius:var(--card-radius)">
<span style="font-size:12px;color:var(--warning-text);font-weight:500"><i class="bi bi-exclamation-triangle"></i> Denumire diferita</span><br>
<span style="font-size:13px">GoMag: <strong>${esc(order.customer_name || '')}</strong></span><br>
<span style="font-size:13px">ANAF: <strong>${esc(pi.denumire_anaf)}</strong></span>
</div>`;
denomEl.style.display = '';
}
}
// Compact address lines
const addr = order.addresses;
if (!addr || (!addr.livrare_gomag && !addr.facturare_gomag)) {
section.style.display = 'none';
return;
}
if (!addr || (!addr.livrare_gomag && !addr.facturare_gomag)) return;
section.style.display = '';
let mismatchCount = 0;
const addressBlock = document.getElementById('detailAddressBlock');
const addressLines = document.getElementById('detailAddressLines');
if (!addressBlock || !addressLines) return;
function fmtAddr(a) {
if (!a) return '-';
if (typeof a === 'string') return esc(a);
const parts = [a.address || a.strada || '', a.numar || ''].filter(Boolean);
const line1 = parts.join(' ').trim();
const line2 = [a.city || a.localitate || '', a.region || a.judet || ''].filter(Boolean).join(', ');
return esc(line1) + (line2 ? '<br>' + esc(line2) : '');
}
addressBlock.style.display = '';
let html = '';
function addrMatch(gomag, roa) {
if (!gomag || !roa) return true; // can't compare
const g = JSON.stringify(gomag).toUpperCase().replace(/[^A-Z0-9]/g, '');
const r = JSON.stringify(roa).toUpperCase().replace(/[^A-Z0-9]/g, '');
return g === r;
}
function hasEfacturaRisk(roa) {
if (!roa || typeof roa === 'string') return false;
return !roa.judet || !roa.localitate;
}
const livrMatch = addrMatch(addr.livrare_gomag, addr.livrare_roa);
const factMatch = addrMatch(addr.facturare_gomag, addr.facturare_roa);
if (!livrMatch) mismatchCount++;
if (!factMatch) mismatchCount++;
const livrRisk = hasEfacturaRisk(addr.livrare_roa);
const factRisk = hasEfacturaRisk(addr.facturare_roa);
if (alertEl) {
if (mismatchCount > 0) {
alertEl.textContent = mismatchCount + (mismatchCount === 1 ? ' diferenta' : ' diferente');
alertEl.style.display = '';
} else {
alertEl.style.display = 'none';
function addrLine(label, addrObj, matchIcon) {
const text = fmtAddr(addrObj);
const escaped = esc(text);
let icon = '';
if (matchIcon === 'match') {
icon = ' <i class="bi bi-check-lg addr-line-match" title="Adrese identice"></i>';
} else if (matchIcon === 'mismatch') {
icon = ' <i class="bi bi-exclamation-triangle" title="Adrese diferite"></i>';
} else if (matchIcon === 'risk') {
icon = ' <i class="bi bi-exclamation-triangle" title="Risc eFactura" style="color:var(--error-text)"></i>';
}
}
// Desktop: 2-column table
const livrClass = livrRisk ? 'addr-efactura-risk' : (!livrMatch ? 'addr-mismatch' : '');
const factClass = factRisk ? 'addr-efactura-risk' : (!factMatch ? 'addr-mismatch' : '');
const desktopHtml = `
<table class="addr-table d-none d-md-table">
<thead><tr><th></th><th>GOMAG</th><th>ROA</th></tr></thead>
<tbody>
<tr class="${livrClass}">
<td><span class="addr-label">LIVRARE</span>${livrRisk ? '<br><small style="color:var(--error-text)">⚠ Risc eFactura</small>' : ''}</td>
<td>${fmtAddr(addr.livrare_gomag)}</td>
<td>${fmtAddr(addr.livrare_roa)}</td>
</tr>
<tr class="${factClass}">
<td><span class="addr-label">FACTURARE</span>${factRisk ? '<br><small style="color:var(--error-text)">⚠ Risc eFactura</small>' : ''}</td>
<td>${fmtAddr(addr.facturare_gomag)}</td>
<td>${fmtAddr(addr.facturare_roa)}</td>
</tr>
</tbody>
</table>`;
// Mobile: stacked cards
function mobileCard(label, gomag, roa, isMatch, isRisk) {
const cls = isRisk ? ' addr-efactura-risk' : (!isMatch ? ' mismatch' : ' match');
const matchLabel = isMatch ? '<div class="addr-match-label">✓ Adrese identice</div>' : '';
const riskLabel = isRisk ? '<div style="font-size:11px;color:var(--error-text)">⚠ Risc eFactura</div>' : '';
return `<div class="addr-card${cls}">
<div class="addr-card-header">${label}</div>
<div class="addr-card-row">
<div class="addr-card-source">GoMag:</div>
<div class="addr-card-text">${fmtAddr(gomag)}</div>
</div>
<div class="addr-card-row">
<div class="addr-card-source">ROA:</div>
<div class="addr-card-text">${fmtAddr(roa)}</div>
</div>
${matchLabel}${riskLabel}
return `<div class="addr-line">
<span class="addr-line-label">${label}</span>
<span class="addr-line-text" title="${escaped}">${escaped}</span>${icon}
</div>`;
}
const mobileHtml = `<div class="d-md-none">
${mobileCard('LIVRARE', addr.livrare_gomag, addr.livrare_roa, livrMatch, livrRisk)}
${mobileCard('FACTURARE', addr.facturare_gomag, addr.facturare_roa, factMatch, factRisk)}
</div>`;
body.innerHTML = desktopHtml + mobileHtml;
// Auto-expand on mismatch
if (mismatchCount > 0) {
const collapseEl = document.getElementById('detailAddressInfo');
if (collapseEl && !collapseEl.classList.contains('show')) {
new bootstrap.Collapse(collapseEl, { show: true });
// Livrare
if (addr.livrare_gomag || addr.livrare_roa) {
html += addrLine('Livrare GoMag:', addr.livrare_gomag, null);
const livrRisk = hasEfacturaRisk(addr.livrare_roa);
const livrMatch = addrMatch(addr.livrare_gomag, addr.livrare_roa);
let matchType = null;
if (addr.livrare_roa) {
matchType = livrRisk ? 'risk' : (livrMatch ? 'match' : 'mismatch');
}
html += addrLine('Livrare ROA:', addr.livrare_roa, matchType);
}
// Facturare
if (addr.facturare_gomag || addr.facturare_roa) {
html += addrLine('Facturare GoMag:', addr.facturare_gomag, null);
const factRisk = hasEfacturaRisk(addr.facturare_roa);
const factMatch = addrMatch(addr.facturare_gomag, addr.facturare_roa);
let matchType = null;
if (addr.facturare_roa) {
matchType = factRisk ? 'risk' : (factMatch ? 'match' : 'mismatch');
}
html += addrLine('Facturare ROA:', addr.facturare_roa, matchType);
}
addressLines.innerHTML = html;
}