From e8b42088e340c42da2afa52a4c1c89a894b9dd60 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Wed, 1 Apr 2026 20:07:37 +0000 Subject: [PATCH] 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) --- api/app/services/anaf_service.py | 7 +- api/app/services/import_service.py | 2 + api/app/services/sqlite_service.py | 40 ++++ api/app/services/sync_service.py | 24 +- api/app/static/css/style.css | 61 ++++++ api/app/static/js/dashboard.js | 8 +- api/app/static/js/shared.js | 341 +++++++++++++---------------- api/app/templates/base.html | 59 +++-- api/app/templates/dashboard.html | 3 +- api/tests/e2e/test_order_detail.py | 6 +- 10 files changed, 308 insertions(+), 243 deletions(-) diff --git a/api/app/services/anaf_service.py b/api/app/services/anaf_service.py index d0c37f5..993bc08 100644 --- a/api/app/services/anaf_service.py +++ b/api/app/services/anaf_service.py @@ -82,8 +82,8 @@ async def _call_anaf_api(body: list[dict], retry: int = 0) -> dict[str, dict]: # Parse ANAF response found_list = data.get("found", []) for item in found_list: - cui_str = str(item.get("cui", "")) date_generals = item.get("date_generale", {}) + cui_str = str(date_generals.get("cui", "")) results[cui_str] = { "scpTVA": item.get("inregistrare_scop_Tva", {}).get("scpTVA"), "denumire_anaf": date_generals.get("denumire", ""), @@ -91,9 +91,10 @@ async def _call_anaf_api(body: list[dict], retry: int = 0) -> dict[str, dict]: } # Not found CUIs - notfound_list = data.get("notfound", []) + notfound_list = data.get("notFound", []) for item in notfound_list: - cui_str = str(item.get("cui", "")) + date_gen = item.get("date_generale", {}) + cui_str = str(date_gen.get("cui", item.get("cui", ""))) results[cui_str] = { "scpTVA": None, "denumire_anaf": "", diff --git a/api/app/services/import_service.py b/api/app/services/import_service.py index a8e132a..58afdef 100644 --- a/api/app/services/import_service.py +++ b/api/app/services/import_service.py @@ -365,6 +365,8 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se cur.execute("SELECT strada, numar, localitate, judet FROM vadrese_parteneri WHERE id_adresa = :1", [int(addr_fact_id)]) row = cur.fetchone() result["adresa_facturare_roa"] = {"strada": row[0], "numar": row[1], "localitate": row[2], "judet": row[3]} if row else None + elif addr_fact_id and addr_fact_id == addr_livr_id: + result["adresa_facturare_roa"] = result.get("adresa_livrare_roa") # Step 4: Build articles JSON and import order articles_json = build_articles_json(order.items, order, app_settings) diff --git a/api/app/services/sqlite_service.py b/api/app/services/sqlite_service.py index 059e216..f04a23d 100644 --- a/api/app/services/sqlite_service.py +++ b/api/app/services/sqlite_service.py @@ -696,6 +696,8 @@ async def get_orders(page: int = 1, per_page: int = 50, if status_filter and status_filter not in ("all", "UNINVOICED"): if status_filter.upper() == "IMPORTED": data_clauses.append("UPPER(status) IN ('IMPORTED', 'ALREADY_IMPORTED')") + elif status_filter.upper() == "DIFFS": + data_clauses.append("(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1)") else: data_clauses.append("UPPER(status) = ?") data_params.append(status_filter.upper()) @@ -749,6 +751,14 @@ async def get_orders(page: int = 1, per_page: int = 50, cursor = await db.execute(f"SELECT COUNT(*) FROM orders {uninv_old_where}", base_params) uninvoiced_old = (await cursor.fetchone())[0] + # Diffs count: orders with ANAF adjustments + diffs_clauses = list(base_clauses) + [ + "(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1)" + ] + diffs_where = "WHERE " + " AND ".join(diffs_clauses) + cursor = await db.execute(f"SELECT COUNT(*) FROM orders {diffs_where}", base_params) + diffs_count = (await cursor.fetchone())[0] + return { "orders": [dict(r) for r in rows], "total": total, @@ -765,6 +775,7 @@ async def get_orders(page: int = 1, per_page: int = 50, "total": sum(status_counts.values()), "uninvoiced_sqlite": uninvoiced_sqlite, "uninvoiced_old": uninvoiced_old, + "diffs": diffs_count, } } finally: @@ -1072,6 +1083,35 @@ async def bulk_populate_anaf_cache(results: dict[str, dict]): await db.close() +async def get_expired_cuis_for_prepopulate() -> list[str]: + """Get CUIs from recent orders that need ANAF cache refresh.""" + from ..services import anaf_service + db = await get_sqlite() + try: + cursor = await db.execute(""" + SELECT DISTINCT cod_fiscal_gomag FROM orders + WHERE cod_fiscal_gomag IS NOT NULL + AND cod_fiscal_gomag != '' + AND order_date >= date('now', '-3 months') + """) + rows = await cursor.fetchall() + + cuis_to_check = [] + for row in rows: + raw = row["cod_fiscal_gomag"] + bare = anaf_service.strip_ro_prefix(raw) + if not anaf_service.validate_cui(bare): + continue + # Check if cache is valid + cached = await get_anaf_cache(bare) + if cached is None: + cuis_to_check.append(bare) + + return cuis_to_check + finally: + await db.close() + + # ── Partner/Address Data on Orders ───────────────── async def update_order_partner_data(order_number: str, partner_data: dict): diff --git a/api/app/services/sync_service.py b/api/app/services/sync_service.py index 7f2eac4..c7a15f2 100644 --- a/api/app/services/sync_service.py +++ b/api/app/services/sync_service.py @@ -638,20 +638,20 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None 0, len(truly_importable), {"imported": 0, "skipped": skipped_count, "errors": 0, "already_imported": already_imported_count}) - # ANAF cache pre-population check + # ANAF cache pre-population: CUIs from last 3 months with expired/missing cache try: - db_check = await sqlite_service.get_sqlite() - try: - cursor = await db_check.execute("SELECT COUNT(*) FROM anaf_cache WHERE checked_at > datetime('now', '-7 days')") - row = await cursor.fetchone() - cache_count = row[0] if row else 0 - finally: - await db_check.close() - - if cache_count < 10: - _log_line(run_id, "ANAF pre-populare cache...") + prepop_cuis = await sqlite_service.get_expired_cuis_for_prepopulate() + if prepop_cuis: + _log_line(run_id, f"ANAF pre-populare: {len(prepop_cuis)} CUI-uri cu cache expirat") + prepop_results = await anaf_service.check_vat_status_batch(prepop_cuis) + if prepop_results: + await sqlite_service.bulk_populate_anaf_cache(prepop_results) + _log_line(run_id, f"ANAF pre-populare: {len(prepop_results)} rezultate stocate") + else: + _log_line(run_id, "ANAF pre-populare: cache complet") except Exception as e: - logger.warning(f"ANAF cache pre-population check failed: {e}") + _log_line(run_id, f"ANAF pre-populare eroare: {e}") + logger.warning(f"ANAF cache pre-population failed: {e}") # Step 4: ANAF batch verification for company CUIs company_cuis = set() diff --git a/api/app/static/css/style.css b/api/app/static/css/style.css index 1478e9c..958c000 100644 --- a/api/app/static/css/style.css +++ b/api/app/static/css/style.css @@ -451,6 +451,7 @@ input[type="checkbox"] { .fc-neutral { color: var(--text-muted); } .fc-blue { color: var(--info); } .fc-dark { color: var(--text-secondary); } +.fc-orange { color: var(--accent); } /* ── Log viewer (dark theme — keep as-is) ────────── */ .log-viewer { @@ -1155,6 +1156,66 @@ tr.mapping-deleted td { .anaf-badge-ok { background: var(--success-light); color: var(--success-text); } .anaf-badge-warn { background: var(--warning-light); color: var(--warning-text); } .anaf-badge-gray { background: var(--cancelled-light); color: var(--text-muted); } + +/* ── Compact order detail layout ──────────────── */ +.detail-col-label { + font-family: var(--font-display); + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-muted); + margin-bottom: 4px; +} +.detail-client-name { + font-family: var(--font-body); + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 2px; +} +.detail-cui-line { + font-size: 13px; + margin-bottom: 2px; +} +.detail-roa-id { + font-size: 13px; + color: var(--text-secondary); +} + +/* Address compact lines */ +.addr-line { + display: flex; + align-items: center; + gap: 8px; + padding: 2px 0; + font-size: 13px; + font-family: var(--font-body); +} +.addr-line-label { + flex-shrink: 0; + width: 120px; + font-size: 12px; + color: var(--text-muted); + font-family: var(--font-body); +} +.addr-line-text { + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text-primary); +} +.addr-line .bi-check-lg { + color: var(--success); + flex-shrink: 0; +} +.addr-line .bi-exclamation-triangle { + color: var(--warning); + flex-shrink: 0; + filter: drop-shadow(0 0 3px rgba(202,138,4,0.3)); +} .addr-table { width: 100%; border-collapse: collapse; font-size: 13px; } .addr-table th { font-family: var(--font-display); diff --git a/api/app/static/js/dashboard.js b/api/app/static/js/dashboard.js index c98ec26..9bb7a1a 100644 --- a/api/app/static/js/dashboard.js +++ b/api/app/static/js/dashboard.js @@ -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 = '
Totul in ordine
'; } else { let items = []; @@ -347,6 +349,7 @@ async function loadDashOrders() { if (unmapped > 0) items.push(` ${unmapped} SKU-uri nemapate`); if (nefact > 0) items.push(` ${nefact} nefacturate`); if (c.incomplete_addresses > 0) items.push(` ${c.incomplete_addresses} adrese incomplete`); + if (diffs > 0) items.push(` ${diffs} diferente ANAF`); attnEl.innerHTML = '
' + items.join('') + '
'; } } @@ -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}"]`); diff --git a/api/app/static/js/shared.js b/api/app/static/js/shared.js index 76d041b..460cb7f 100644 --- a/api/app/static/js/shared.js +++ b/api/app/static/js/shared.js @@ -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 = 'Se incarca...'; 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 = 'Platitor TVA'; - } else if (pi.anaf_platitor_tva === 0) { - anafBadge = 'Neplatitor TVA'; - } else { - anafBadge = 'Neverificat'; - } - - // CUI correction badge - let cuiCorrBadge = ''; - if (pi.anaf_cod_fiscal_adjusted) { - cuiCorrBadge = ' Corectat ANAF'; - } - - // Denomination mismatch - let denomHtml = ''; - if (pi.anaf_denumire_mismatch && pi.denumire_anaf) { - denomHtml = `
- Denumire diferita
- GoMag: ${esc(order.customer_name || '')}
- ANAF: ${esc(pi.denumire_anaf)} -
`; - } - - body.innerHTML = ` -
-
-
CUI GoMag
-
${esc(pi.cod_fiscal_gomag)}
-
-
-
CUI ROA
-
${esc(pi.cod_fiscal_roa || '-')}${cuiCorrBadge}
-
-
-
Partener ROA
-
${esc(pi.denumire_roa || '-')}
-
-
-
-
-
ANAF
-
${anafBadge}
-
-
- ${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 = ' Corectat'; + } + + // ANAF badge + const anafArea = document.getElementById('detailPartnerAnafArea'); + if (anafArea) { + let anafBadge; + if (pi.anaf_platitor_tva === 1) { + anafBadge = 'Platitor TVA'; + } else if (pi.anaf_platitor_tva === 0) { + anafBadge = 'Neplatitor TVA'; + } else { + anafBadge = '?'; + } + anafArea.innerHTML = cuiCorrHtml + ' ' + anafBadge; + } + } + } + + // ERROR orders: muted dashes for ROA fields + if (order.status === 'ERROR' && !order.id_comanda) { + document.getElementById('detailIdComanda').innerHTML = '\u2014'; + document.getElementById('detailIdPartener').innerHTML = '\u2014'; + } + + // Denomination mismatch alert + if (isPJ && pi.anaf_denumire_mismatch && pi.denumire_anaf) { + const denomEl = document.getElementById('detailDenomMismatch'); + if (denomEl) { + denomEl.innerHTML = `
+ Denumire diferita
+ GoMag: ${esc(order.customer_name || '')}
+ ANAF: ${esc(pi.denumire_anaf)} +
`; + 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 ? '
' + 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 = ' '; + } else if (matchIcon === 'mismatch') { + icon = ' '; + } else if (matchIcon === 'risk') { + icon = ' '; } - } - - // Desktop: 2-column table - const livrClass = livrRisk ? 'addr-efactura-risk' : (!livrMatch ? 'addr-mismatch' : ''); - const factClass = factRisk ? 'addr-efactura-risk' : (!factMatch ? 'addr-mismatch' : ''); - - const desktopHtml = ` - - - - - - - - - - - - - - -
GOMAGROA
LIVRARE${livrRisk ? '
⚠ Risc eFactura' : ''}
${fmtAddr(addr.livrare_gomag)}${fmtAddr(addr.livrare_roa)}
FACTURARE${factRisk ? '
⚠ Risc eFactura' : ''}
${fmtAddr(addr.facturare_gomag)}${fmtAddr(addr.facturare_roa)}
`; - - // Mobile: stacked cards - function mobileCard(label, gomag, roa, isMatch, isRisk) { - const cls = isRisk ? ' addr-efactura-risk' : (!isMatch ? ' mismatch' : ' match'); - const matchLabel = isMatch ? '
✓ Adrese identice
' : ''; - const riskLabel = isRisk ? '
⚠ Risc eFactura
' : ''; - return `
-
${label}
-
-
GoMag:
-
${fmtAddr(gomag)}
-
-
-
ROA:
-
${fmtAddr(roa)}
-
- ${matchLabel}${riskLabel} + return `
+ ${label} + ${escaped}${icon}
`; } - const mobileHtml = `
- ${mobileCard('LIVRARE', addr.livrare_gomag, addr.livrare_roa, livrMatch, livrRisk)} - ${mobileCard('FACTURARE', addr.facturare_gomag, addr.facturare_roa, factMatch, factRisk)} -
`; - - 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; } diff --git a/api/app/templates/base.html b/api/app/templates/base.html index 1c3037b..39c7ff8 100644 --- a/api/app/templates/base.html +++ b/api/app/templates/base.html @@ -19,7 +19,7 @@ {% set rp = request.scope.get('root_path', '') %} - + @@ -93,16 +93,26 @@