fix(address+ui): remove TIER 2 reuse, typed diff badges, false positive reduction

- Remove TIER 2 address lookup (county+city without street) from PL/SQL — creates
  new address when street differs instead of reusing wrong one
- Replace generic "N diferente" badge with typed micro-badges (CUI, Denumire, TVA,
  Adr. livr., Adr. fact., Preturi) with red/amber semantic colors
- Extend addrMatch() regex to strip full Romanian address words (STRADA, NUMAR, BLOC,
  COMUNA, SAT, MUNICIPIUL, etc.) — fixes "Strada X" vs "X" false positives
- Extend normalize_company_name() for II, PFA, INTREPRINDERE INDIVIDUALA legal forms
- Persist address_mismatch to SQLite so "Dif." filter includes address-only diffs
- Add red/amber indicator dots to desktop table and mobile list rows
- 12 unit tests for normalization and server-side address matching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-06 14:28:57 +00:00
parent fc1013bff6
commit 31095c07f7
11 changed files with 160 additions and 44 deletions

View File

@@ -1176,6 +1176,11 @@ tr.mapping-deleted td {
.anaf-badge-warn { background: var(--warning-light); color: var(--warning-text); }
.anaf-badge-gray { background: var(--cancelled-light); color: var(--text-muted); }
/* Diff-type badges (reuses .anaf-badge sizing per DESIGN.md type scale minimum) */
.diff-badge { display:inline-block; font-family:var(--font-body); font-size:12px; font-weight:500; padding:2px 8px; border-radius:9999px; margin-left:4px; vertical-align:middle; }
.diff-badge-anaf { background:var(--error-light); color:var(--error-text); }
.diff-badge-info { background:var(--warning-light); color:var(--warning-text); }
/* ── Compact order detail layout ──────────────── */
.detail-col-label {
font-family: var(--font-display);

View File

@@ -368,7 +368,7 @@ async function loadDashOrders() {
<td>${statusDot(o.status)}</td>
<td class="text-nowrap">${dateStr}</td>
${renderClientCell(o)}
<td><code>${esc(o.order_number)}</code></td>
<td><code>${esc(o.order_number)}</code>${(o.anaf_cod_fiscal_adjusted===1||o.anaf_denumire_mismatch===1||(o.cod_fiscal_gomag&&o.anaf_platitor_tva===0))?'<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--error);margin-left:4px;vertical-align:middle" title="Diferente ANAF"></span>':''}${(o.address_mismatch===1||o.price_match===false)?'<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--warning);margin-left:2px;vertical-align:middle" title="Diferente adresa/pret"></span>':''}</td>
<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>
@@ -394,12 +394,14 @@ async function loadDashOrders() {
}
const name = o.customer_name || o.shipping_name || o.billing_name || '\u2014';
const totalStr = o.order_total ? Number(o.order_total).toFixed(2) : '';
const anafDiffDot = (o.anaf_cod_fiscal_adjusted===1||o.anaf_denumire_mismatch===1||(o.cod_fiscal_gomag&&o.anaf_platitor_tva===0)) ? '<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--error);margin-right:2px;vertical-align:middle" title="ANAF"></span>' : '';
const addrDiffDot = o.address_mismatch===1 ? '<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--warning);margin-right:2px;vertical-align:middle" title="Adresa"></span>' : '';
const priceMismatch = o.price_match === false ? '<span class="dot dot-red" style="width:6px;height:6px" title="Pret!="></span> ' : '';
return `<div class="flat-row" onclick="openDashOrderDetail('${esc(o.order_number)}')" style="font-size:0.875rem">
${statusDot(o.status)}
<span style="color:var(--text-muted)" class="text-nowrap">${dateFmt}</span>
<span class="grow truncate fw-bold">${esc(name)}</span>
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · ' + priceMismatch + '<strong>' + totalStr + '</strong>' : ''}</span>
<span class="text-nowrap">x${o.items_count || 0}${totalStr ? ' · ' + anafDiffDot + addrDiffDot + priceMismatch + '<strong>' + totalStr + '</strong>' : ''}</span>
</div>`;
}).join('');
}

View File

@@ -856,7 +856,7 @@ function addrMatch(gomag, roa) {
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(/\b(STR|STRADA|NR|NUMAR|NUMARUL|BL|BLOC|SC|SCARA|AP|APART|APARTAMENT|ET|ETAJ|COM|COMUNA|SAT|MUN|MUNICIPIUL|JUD|JUDETUL|CARTIER|PARTER|SECTOR|ORAS)\b/g, '')
.replace(/[^A-Z0-9]/g, '');
}
const gStreet = norm(gomag.address || gomag.strada || '');
@@ -1013,22 +1013,25 @@ function _renderHeaderInfo(order) {
addressLines.innerHTML = html;
// Diff summary badge in modal header
let diffCount = 0;
if (isPJ && pi.anaf_denumire_mismatch) diffCount++;
if (order.price_check && order.price_check.mismatches > 0) diffCount += order.price_check.mismatches;
if (addr) {
if (addr.livrare_roa && !addrMatch(addr.livrare_gomag, addr.livrare_roa)) diffCount++;
if (addr.facturare_roa && !addrMatch(addr.facturare_gomag, addr.facturare_roa)) diffCount++;
}
// Typed diff badges in modal header
const orderNumEl = document.getElementById('detailOrderNumber');
if (orderNumEl && diffCount > 0) {
const existing = orderNumEl.querySelector('.diff-badge');
if (existing) existing.remove();
const badge = document.createElement('span');
badge.className = 'diff-badge badge ms-2';
badge.style.cssText = 'background:var(--warning-light);color:var(--warning-text);font-size:11px;vertical-align:middle';
badge.textContent = diffCount + ' diferente';
orderNumEl.parentNode.insertBefore(badge, orderNumEl.nextSibling);
if (orderNumEl) {
orderNumEl.parentNode.querySelectorAll('.diff-badge').forEach(b => b.remove());
const badges = [];
if (isPJ && pi.anaf_cod_fiscal_adjusted) badges.push({label:'CUI', cls:'diff-badge-anaf', aria:'CUI ajustat conform ANAF'});
if (isPJ && pi.anaf_denumire_mismatch) badges.push({label:'Denumire', cls:'diff-badge-anaf', aria:'Denumire diferita fata de ANAF'});
if (isPJ && pi.anaf_platitor_tva === 0) badges.push({label:'TVA', cls:'diff-badge-anaf', aria:'Neplatitor TVA conform ANAF'});
if (addr && addr.livrare_roa && !addrMatch(addr.livrare_gomag, addr.livrare_roa)) badges.push({label:'Adr. livr.', cls:'diff-badge-info', aria:'Adresa livrare diferita'});
if (addr && addr.facturare_roa && !addrMatch(addr.facturare_gomag, addr.facturare_roa)) badges.push({label:'Adr. fact.', cls:'diff-badge-info', aria:'Adresa facturare diferita'});
if (order.price_check && order.price_check.mismatches > 0) badges.push({label:'Preturi (' + order.price_check.mismatches + ')', cls:'diff-badge-info', aria:'Preturi diferite: ' + order.price_check.mismatches});
let insertAfter = orderNumEl;
badges.forEach(b => {
const el = document.createElement('span');
el.className = 'diff-badge ' + b.cls;
el.setAttribute('aria-label', b.aria);
el.textContent = b.label;
insertAfter.parentNode.insertBefore(el, insertAfter.nextSibling);
insertAfter = el;
});
}
}