Files
rar-autopass/app/web/templates/base.html
Claude Agent fd4a05436d feat(5.9): US-003 - modal reutilizabil (overlay, focus-trap, a11y) + cleanup inline-expand 5.8
- base.html: #modal-detaliu (role=dialog, aria-modal) + #detaliu-modal-body swap target;
  focus-trap, inert+aria-hidden pe <main>, Esc/backdrop/x inchid, listener trimiteriChanged (R5/R7)
- _coada.html: ancora modal in afara #submissions-wrap; sters #trimitere-detaliu inert vechi
- _submissions.html: randul declanseaza modalul; sters tr.detaliu-rand sibling (R3)
- _trimitere_detaliu.html: script rescris pentru modal, fara marcheazaDetaliuDeschis/scrollIntoView (R4)
- teste: test_web_modal.py nou (3); test_web_detaliu_inline.py sters; test_acasa_trimiteri.py curatat (R3)
- gates: pytest PASS (suita completa 819). Browser E2E + design-review deferate la VERIFY.

Salvat manual: iteratiile Ralph 2-12 au ramas fara turns (30) inainte de commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:48:42 +00:00

549 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Gateway RAR AUTOPASS{% endblock %}</title>
<script src="/static/htmx.min.js"></script>
<script>
// US-002 (3.6): raspunsurile de editare-rand contin un <tr> (swap pe rand) PLUS
// elemente OOB non-rand (#preview-rezumat, #preview-ok-count). Fara fragmente-template,
// htmx parseaza raspunsul care incepe cu <tr> in context de tabel (<table><tbody>) si
// "foster-parent"-eaza div/span-urile OOB afara din fragment -> swapError + contoare pierdute.
// useTemplateFragments parseaza tot intr-un <template>, pastrand rand + OOB impreuna.
htmx.config.useTemplateFragments = true;
</script>
<script>
// Anti-FOUC (US-001 PRD 5.3): citeste preferinta tema din localStorage inainte de
// primul paint; seteaza data-theme pe <html> sincron, fara blink dark->light.
(function() {
try {
var t = localStorage.getItem('theme');
if (!t) {
t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
document.documentElement.setAttribute('data-theme', t);
} catch(e) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
<style>
:root { --bg:#0f1115; --card:#181b22; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36;
--ok:#3ecf8e; --warn:#e6b34a; --err:#e5605e; --accent:#5b8def; }
[data-theme="light"] { --bg:#f6f7f9; --card:#ffffff; --ink:#1a1d24; --muted:#5c6473; --line:#e2e5ea;
--ok:#15803d; --warn:#b45309; --err:#dc2626; --accent:#2563eb; }
* { box-sizing:border-box; }
body { margin:0; font:15px/1.5 ui-sans-serif,system-ui,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }
header { padding:16px 24px; border-bottom:1px solid var(--line); display:flex; align-items:center; gap:12px; }
header h1 { font-size:20px; margin:0; font-weight:700; letter-spacing:-.01em; }
header .env { font-size:12px; color:var(--muted); border:1px solid var(--line); padding:2px 8px; border-radius:99px; }
main { padding:24px; max-width:1100px; margin:0 auto; }
.card { background:var(--card); border:1px solid var(--line); border-radius:10px; padding:16px 20px; margin-bottom:16px; }
.banner { border-left:3px solid var(--err); background:color-mix(in srgb, var(--err) 12%, var(--card)); }
.banner.hidden { display:none; }
/* Tabelele de date au multe coloane; pe ecrane inguste scroll IN card, nu
impinge layout-ul paginii (altfel toata pagina scrolleaza orizontal). */
.tablewrap { overflow-x:auto; -webkit-overflow-scrolling:touch; }
table { width:100%; border-collapse:collapse; font-size:14px; font-variant-numeric:tabular-nums; }
th,td { text-align:left; padding:8px 10px; border-bottom:1px solid var(--line); white-space:nowrap; }
th { color:var(--muted); font-weight:500; font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
.empty { color:var(--muted); padding:24px; text-align:center; }
.pill { font-size:12px; padding:2px 8px; border-radius:99px; border:1px solid var(--line); }
.s-queued{color:var(--accent);} .s-sending{color:var(--warn);} .s-sent{color:var(--ok);}
.s-error,.s-needs_data,.s-needs_mapping{color:var(--err);}
.s-ok{color:var(--ok);}
.s-needs_review{color:var(--warn);}
.s-already_sent,.s-duplicate_in_file{color:var(--muted);}
.muted { color:var(--muted); }
a { color:var(--accent); }
/* Drop zone upload fisier */
.drop-zone { border:2px dashed var(--line); border-radius:8px; padding:32px 20px;
text-align:center; transition:border-color .15s,background .15s; }
.drop-zone.drag-over { border-color:var(--accent); background:rgba(91,141,239,.05); }
/* Banner varianta warn (nu eroare) */
.banner.warn { border-left-color:var(--warn); background:color-mix(in srgb, var(--warn) 12%, var(--card)); }
/* Bara confirmare sticky */
.sticky-bar { position:sticky; bottom:0; background:var(--card); border-top:1px solid var(--line);
padding:12px 16px; display:flex; align-items:flex-start; gap:16px;
flex-wrap:wrap; z-index:10; }
/* Indicator HTMX — ascuns pana la request */
.htmx-indicator { display:none; }
.htmx-indicator.htmx-request { display:inline; }
/* Link-uri de actiune in antetul cardurilor: zona de atins mai mare (>=36px) si
feedback la hover; pe ecrane inguste antetul se rupe curat sub titlu. */
.cardlink { font-size:13px; padding:7px 10px; border-radius:6px; display:inline-flex;
align-items:center; min-height:36px; white-space:nowrap; }
.cardlink:hover { background:var(--line); }
.flash { background:color-mix(in srgb, var(--ok) 12%, var(--card)); border-left:3px solid var(--ok); padding:8px 12px; border-radius:6px;
margin:0 0 12px; font-size:13px; }
.maprow { display:flex; gap:16px; align-items:center; padding:12px 0; border-bottom:1px solid var(--line);
flex-wrap:wrap; }
.maprow:last-child { border-bottom:0; }
.mapcol.grow { flex:1 1 280px; min-width:240px; }
.sugg { color:var(--accent); }
select, button, input[type=text] { font:inherit; background:var(--bg); color:var(--ink);
border:1px solid var(--line); border-radius:6px; padding:6px 10px; }
select { max-width:340px; }
button { background:var(--accent); border-color:var(--accent); color:#fff; cursor:pointer; }
button:hover { filter:brightness(1.08); }
.chk { font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px; }
/* Tab-bar (US-003) */
.tab-bar { display:flex; gap:2px; overflow-x:auto; -webkit-overflow-scrolling:touch;
border-bottom:1px solid var(--line); margin-bottom:16px; padding-bottom:0;
scrollbar-width:none; }
.tab-bar::-webkit-scrollbar { display:none; }
.tab-link { display:inline-flex; align-items:center; padding:8px 16px; font-size:14px;
font-weight:500; color:var(--muted); text-decoration:none; border-radius:6px 6px 0 0;
border:1px solid transparent; border-bottom:none; white-space:nowrap;
transition:color .12s, background .12s; margin-bottom:-1px; }
.tab-link:hover { color:var(--ink); background:var(--line); }
.tab-link.tab-activ { color:var(--ink); background:var(--card);
border-color:var(--line); border-bottom-color:var(--card); }
.tab-panel { min-height:120px; }
.status-bar { margin-bottom:12px; }
/* Eroare 3 niveluri (US-006, PRD 5.4) */
.eroare-3n { margin-top:10px; }
.eroare-3n-item { padding:8px 10px; border-left:3px solid var(--err);
background:color-mix(in srgb, var(--err) 8%, var(--card));
border-radius:0 6px 6px 0; }
.eroare-3n-sep { margin-top:6px; }
.eroare-3n-problema { font-weight:600; color:var(--err); font-size:13px; }
.eroare-3n-camp { font-family:monospace; font-size:12px; opacity:.85; }
.eroare-3n-cauza { color:var(--muted); font-size:12px; margin-top:3px; }
.eroare-3n-fix { color:var(--accent); font-size:12px; margin-top:3px; }
.eroare-3n-label { font-weight:500; }
/* Inline fix per camp in preview */
.camp-fix { color:var(--accent); font-size:11px; margin-top:2px; display:block; }
/* Meniu hamburger cont (US-006 PRD 5.5) — dropdown ancorat dreapta-sus */
.cont-menu-wrap { position:relative; }
.icon-btn { background:transparent; border:1px solid var(--line); color:var(--ink); cursor:pointer;
border-radius:6px; min-height:36px; min-width:36px; font-size:16px; padding:4px 8px;
line-height:1; display:inline-flex; align-items:center; justify-content:center; }
.icon-btn:hover { background:var(--line); }
.cont-menu { position:absolute; right:0; top:calc(100% + 8px); min-width:180px; z-index:50;
background:var(--card); border:1px solid var(--line); border-radius:8px; padding:6px;
box-shadow:0 8px 24px rgba(0,0,0,.18); display:flex; flex-direction:column; gap:2px; }
.cont-menu[hidden] { display:none; }
.cont-menu a, .cont-menu button { display:block; width:100%; text-align:left; background:transparent;
border:none; color:var(--ink); text-decoration:none; font:inherit; padding:8px 10px;
border-radius:6px; cursor:pointer; min-height:36px; }
.cont-menu a:hover, .cont-menu button:hover { background:var(--line); }
.cont-menu hr { border:none; border-top:1px solid var(--line); margin:4px 0; }
.cont-menu form { margin:0; }
/* Kebab partajat (actiuni per-rand in tabele). Meniul e position:fixed si pozitionat de JS:
altfel `.tablewrap { overflow-x:auto }` induce overflow-y:auto si TAIE dropdown-ul pe ultimul
rand (bug 5.5 — meniul nu se vedea). fixed scoate meniul din contextul de clipping al tabelului. */
.kebab { position:relative; display:inline-block; }
.kebab > summary { list-style:none; cursor:pointer; display:inline-flex; align-items:center;
justify-content:center; min-height:32px; min-width:32px; padding:4px 10px;
border-radius:6px; color:var(--ink); }
.kebab > summary::-webkit-details-marker { display:none; }
.kebab > summary:hover, .kebab[open] > summary { background:var(--line); }
.kebab-menu { position:fixed; z-index:1000; min-width:160px; background:var(--card);
border:1px solid var(--line); border-radius:8px; padding:6px;
box-shadow:0 8px 24px rgba(0,0,0,.18); display:flex; flex-direction:column; gap:2px; }
.kebab-menu form { margin:0; }
.kebab-menu button, .kebab-menu a { display:block; width:100%; text-align:left; background:transparent;
border:none; color:var(--ink); text-decoration:none; font:inherit; padding:7px 10px;
border-radius:6px; cursor:pointer; min-height:36px; white-space:nowrap; }
.kebab-menu button:hover, .kebab-menu a:hover { background:var(--line); }
.kebab-menu button.danger { color:var(--err); }
/* Tabel cu cautare + paginare client-side (data-dt). Maparile pot creste la sute de randuri;
filtram/paginez DOM-ul deja randat, fara cereri suplimentare. Vezi scriptul din base.html. */
input[type=search] { font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line);
border-radius:6px; padding:6px 10px; width:100%; }
.dt-tools { display:flex; align-items:center; gap:8px; margin:0 0 10px; }
.dt-search { flex:1 1 auto; max-width:320px; }
.dt-empty { color:var(--muted); padding:16px; text-align:center; font-size:13px; }
.dt-pager { display:flex; align-items:center; justify-content:flex-end; gap:10px;
margin-top:10px; font-size:13px; color:var(--muted); }
.dt-pager button { background:transparent; color:var(--ink); border:1px solid var(--line);
padding:5px 12px; min-height:32px; }
.dt-pager button:disabled { opacity:.45; cursor:default; }
/* === Tabel trimiteri (PRD 5.8 US-007): fara scroll orizontal. SCOPAT prin
.tabel-trimiteri ca sa NU strice celelalte tabele (.tablewrap e partajat de
Mapari/Formate). Permitem wrap controlat pe coloanele text + latimi rezonabile. === */
.tabel-trimiteri table { table-layout:fixed; }
.tabel-trimiteri th, .tabel-trimiteri td { white-space:normal; word-break:break-word; vertical-align:top; }
.tabel-trimiteri .col-chk { width:30px; }
.tabel-trimiteri .col-id { width:48px; }
.tabel-trimiteri .col-stare { width:104px; }
.tabel-trimiteri .col-data { width:104px; }
.tabel-trimiteri .col-rar { width:96px; }
.tabel-trimiteri .col-actualizat { width:128px; }
.tabel-trimiteri .col-operatie > div { line-height:1.35; }
/* secundarul muted („cod RAR" / „nemapat") — >=12px, contrast pe var(--muted) >=4.5:1 */
.tabel-trimiteri .cod-rar-sub { font-size:12px; margin-top:2px; }
/* 768-1024px: ascunde Actualizat (e in detaliu) -> 7 coloane, fara scroll */
@media (max-width:1024px) {
.tabel-trimiteri .col-actualizat { display:none; }
}
/* === Modal detaliu (PRD 5.9 US-003): fereastra modala globala, in afara zonei de
poll (#submissions-wrap). Backdrop + dialog centrat pe desktop; focus-trap +
scroll-lock + inert pe <main> sunt in JS. Varianta full-screen mobil vine in US-006. === */
.modal-overlay { position:fixed; inset:0; z-index:1100; display:flex;
align-items:flex-start; justify-content:center; padding:40px 16px; overflow-y:auto; }
.modal-overlay[hidden] { display:none; }
.modal-backdrop { position:fixed; inset:0; background:rgba(0,0,0,.55); }
.modal-dialog { position:relative; z-index:1; width:100%; max-width:680px;
background:var(--card); border:1px solid var(--line); border-radius:12px;
box-shadow:0 16px 48px rgba(0,0,0,.35); padding:18px 20px;
max-height:calc(100vh - 80px); overflow-y:auto; }
.modal-close { position:absolute; top:10px; right:10px; background:transparent;
border:1px solid var(--line); color:var(--muted); width:36px; height:36px;
border-radius:8px; font-size:20px; line-height:1; cursor:pointer;
display:inline-flex; align-items:center; justify-content:center; }
.modal-close:hover { background:var(--line); color:var(--ink); }
body.modal-open { overflow:hidden; }
.modal-eroare { padding:16px 4px; }
.modal-eroare .actiuni { margin-top:12px; display:flex; gap:10px; flex-wrap:wrap; }
/* <768px: card per rand (eticheta:valoare stivuit), nu tabel -> fara scroll orizontal */
@media (max-width:767px) {
.tabel-trimiteri table { table-layout:auto; }
.tabel-trimiteri thead { display:none; }
.tabel-trimiteri table, .tabel-trimiteri tbody, .tabel-trimiteri tr, .tabel-trimiteri td { display:block; width:auto; }
.tabel-trimiteri tr { border:1px solid var(--line); border-radius:8px; padding:8px 12px; margin-bottom:10px; }
.tabel-trimiteri td { border-bottom:none; padding:4px 0; display:flex; gap:10px; align-items:baseline; }
.tabel-trimiteri td::before { content:attr(data-eticheta); color:var(--muted); font-size:12px;
flex:0 0 auto; min-width:120px; }
.tabel-trimiteri td.col-chk { display:none; }
}
</style>
</head>
<body>
<header>
<h1>Gateway RAR AUTOPASS</h1>
<span class="env">{{ rar_env }}</span>
<div style="margin-left:auto; display:flex; align-items:center; gap:8px;">
<button id="tema-toggle" class="icon-btn"
aria-label="Comuta tema (luminos/intunecat)"
title="Comuta tema">&#9728;</button>
<span class="muted" style="font-size:13px;">v{{ version }}</span>
{% if is_authenticated|default(false) %}
{# Meniu cont (US-006 PRD 5.5): Cont/Integrare/Nomenclator + (admin) + logout.
Pe paginile neautentificate (login/signup) nu se randeaza deloc. #}
<div class="cont-menu-wrap">
<button id="cont-menu-toggle" class="icon-btn"
aria-haspopup="true" aria-expanded="false" aria-controls="cont-menu"
aria-label="Meniu cont" title="Meniu cont">&#9776;</button>
<div id="cont-menu" class="cont-menu" role="menu" aria-labelledby="cont-menu-toggle" hidden>
<a role="menuitem" href="/?tab=cont">Cont</a>
<a role="menuitem" href="/?tab=integrare">Integrare</a>
<a role="menuitem" href="/?tab=nomenclator">Nomenclator</a>
<a role="menuitem" href="/?tab=jurnal">Jurnal</a>
{% if is_admin|default(false) %}<a role="menuitem" href="/admin">Conturi clienti</a>{% endif %}
<hr>
<form method="post" action="/logout">
<input type="hidden" name="csrf_token" value="{{ csrf_token|default('') }}">
<button role="menuitem" type="submit">Iesi din cont</button>
</form>
</div>
</div>
{% endif %}
</div>
</header>
<main>{% block content %}{% endblock %}</main>
{# Modal detaliu trimitere (PRD 5.9 US-003): container global, SIBLING al <main>
(nu descendent), ca `inert`+`aria-hidden` pe <main> sa nu-l prinda si pe el (R7).
Corpul #detaliu-modal-body e tinta de swap pentru fragment + rutele corectie/
mapare/lifecycle. Traieste in afara #submissions-wrap -> poll-ul de 15s nu-l atinge. #}
<div id="modal-detaliu" class="modal-overlay" role="dialog" aria-modal="true"
aria-labelledby="detaliu-modal-titlu" hidden>
<div class="modal-backdrop" data-modal-close></div>
<div class="modal-dialog" role="document">
<button type="button" class="modal-close" data-modal-close aria-label="Inchide detaliul">&times;</button>
<div id="detaliu-modal-body"></div>
</div>
</div>
<script>
// Handler comutator tema (US-002 PRD 5.3): click toggle light<->dark, persista in localStorage.
// Separare init (doar sincronizare iconita) de persistenta (doar la click explicit).
// Motivatie: scrierea in localStorage la init ar ingloba imediat preferinta OS-aware ca alegere
// explicita, impiedicand urmarirea ulterioara a modificarilor de tema ale sistemului de operare.
(function() {
var btn = document.getElementById('tema-toggle');
if (!btn) return;
// Sincronizeaza iconita si aria-label dupa tema curenta -- FARA efecte secundare in localStorage.
function _syncIcon(t) {
if (t === 'light') {
btn.innerHTML = '&#9790;';
btn.setAttribute('aria-label', 'Comuta tema (intunecat)');
btn.title = 'Comuta tema (intunecat)';
} else {
btn.innerHTML = '&#9728;';
btn.setAttribute('aria-label', 'Comuta tema (luminos)');
btn.title = 'Comuta tema (luminos)';
}
}
// Aplica o tema noua, seteaza data-theme si persista in localStorage -- apelat DOAR la click.
function _setTheme(t) {
document.documentElement.setAttribute('data-theme', t);
try { localStorage.setItem('theme', t); } catch(e) {}
_syncIcon(t);
}
// La init: sincronizeaza doar iconita din data-theme curent (setat deja de scriptul anti-FOUC).
_syncIcon(document.documentElement.getAttribute('data-theme') || 'dark');
btn.addEventListener('click', function() {
var cur = document.documentElement.getAttribute('data-theme') || 'dark';
_setTheme(cur === 'dark' ? 'light' : 'dark');
});
})();
</script>
<script>
// Meniu cont (US-006 PRD 5.5): dropdown ancorat dreapta-sus. Deschide/inchide la click,
// inchide la Esc (focus readus pe buton) si la click in afara. Fara dependente.
(function() {
var toggle = document.getElementById('cont-menu-toggle');
var menu = document.getElementById('cont-menu');
if (!toggle || !menu) return;
function open() {
menu.hidden = false;
toggle.setAttribute('aria-expanded', 'true');
document.addEventListener('click', onDocClick, true);
document.addEventListener('keydown', onKey, true);
}
function close(refocus) {
menu.hidden = true;
toggle.setAttribute('aria-expanded', 'false');
document.removeEventListener('click', onDocClick, true);
document.removeEventListener('keydown', onKey, true);
if (refocus) toggle.focus();
}
function onDocClick(e) {
if (!menu.contains(e.target) && e.target !== toggle) close(false);
}
function onKey(e) {
if (e.key === 'Escape') { e.preventDefault(); close(true); }
}
toggle.addEventListener('click', function(e) {
e.stopPropagation();
if (menu.hidden) open(); else close(false);
});
})();
</script>
<script>
// Kebab partajat (actiuni per-rand). `<details class="kebab">` + `.kebab-menu` position:fixed.
// Pozitionarea se face in JS la deschidere (eveniment `toggle`, captat pe document fiindca nu
// bubble-uie), ancorat sub buton si aliniat la dreapta; flip in sus daca nu incape jos. Delegare
// pe document → supravietuieste swap-urilor HTMX (#mapari-section se re-randeaza la fiecare salvare).
(function() {
function position(d) {
var btn = d.querySelector('summary');
var menu = d.querySelector('.kebab-menu');
if (!btn || !menu) return;
var r = btn.getBoundingClientRect();
menu.style.visibility = 'hidden';
var mw = menu.offsetWidth, mh = menu.offsetHeight;
var left = Math.max(8, r.right - mw);
var top = (r.bottom + mh > window.innerHeight - 8 && r.top - mh - 4 > 8)
? r.top - mh - 4 : r.bottom + 4;
menu.style.left = left + 'px';
menu.style.top = top + 'px';
menu.style.visibility = '';
}
function closeAll(except) {
document.querySelectorAll('details.kebab[open]').forEach(function(d) {
if (d !== except) d.removeAttribute('open');
});
}
// `toggle` nu bubble-uie -> ascultam in faza de capturare pe document.
document.addEventListener('toggle', function(e) {
var d = e.target;
if (!d.classList || !d.classList.contains('kebab')) return;
if (d.open) { closeAll(d); position(d); }
}, true);
document.addEventListener('click', function(e) {
if (!e.target.closest('details.kebab')) closeAll(null);
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeAll(null);
});
// La scroll/resize repozitionam meniul deschis (position:fixed nu urmareste ancora singur).
window.addEventListener('scroll', function() {
var open = document.querySelector('details.kebab[open]');
if (open) position(open);
}, true);
window.addEventListener('resize', function() { closeAll(null); });
})();
</script>
<script>
// Cautare + paginare client-side pentru tabele mari (data-dt="<page_size>"). Filtreaza si
// pagineaza DOM-ul deja randat (fara cereri server) — potrivit pentru maparile care pot creste
// la sute de randuri. Re-init la full load SI dupa swap-urile HTMX (tab Mapari, salvare/stergere).
// Markup: <div data-dt="10"> [input data-dt-search] <table> [div data-dt-empty] [div data-dt-pager] </div>
(function() {
function enhance(scope) {
(scope || document).querySelectorAll('[data-dt]').forEach(function(wrap) {
if (wrap.__dt) return; // idempotent (afterSettle poate re-scana)
wrap.__dt = true;
var table = wrap.querySelector('table');
var tbody = table && table.tBodies[0];
if (!tbody) return;
var size = parseInt(wrap.getAttribute('data-dt'), 10) || 10;
var search = wrap.querySelector('[data-dt-search]');
var pager = wrap.querySelector('[data-dt-pager]');
var empty = wrap.querySelector('[data-dt-empty]');
var rows = Array.prototype.slice.call(tbody.rows);
var page = 1;
// Haystack-ul randului: atributul data-dt-row daca exista, altfel textContent. Necesar
// cand randul contine un <select> (optiunile lui ar pune tot nomenclatorul in textContent
// -> orice cautare ar potrivi orice rand). Vezi data-dt-row in _mapari.html.
function haystack(r) {
var h = r.getAttribute('data-dt-row');
return (h !== null ? h : r.textContent).toLowerCase();
}
function matched() {
var q = (search && search.value || '').trim().toLowerCase();
if (!q) return rows;
return rows.filter(function(r) { return haystack(r).indexOf(q) !== -1; });
}
function draw() {
var fr = matched();
var pages = Math.max(1, Math.ceil(fr.length / size));
if (page > pages) page = pages;
if (page < 1) page = 1;
var start = (page - 1) * size;
rows.forEach(function(r) { r.style.display = 'none'; });
fr.slice(start, start + size).forEach(function(r) { r.style.display = ''; });
if (empty) empty.style.display = fr.length ? 'none' : '';
if (!pager) return;
if (fr.length <= size && page === 1) { pager.style.display = 'none'; pager.innerHTML = ''; return; }
pager.style.display = '';
pager.innerHTML = '';
var info = document.createElement('span');
info.textContent = (fr.length ? start + 1 : 0) + '' +
Math.min(start + size, fr.length) + ' din ' + fr.length;
var prev = document.createElement('button');
prev.type = 'button'; prev.textContent = 'Inapoi'; prev.disabled = page <= 1;
prev.addEventListener('click', function() { page--; draw(); });
var next = document.createElement('button');
next.type = 'button'; next.textContent = 'Inainte'; next.disabled = page >= pages;
next.addEventListener('click', function() { page++; draw(); });
pager.appendChild(info); pager.appendChild(prev); pager.appendChild(next);
}
if (search) search.addEventListener('input', function() { page = 1; draw(); });
draw();
});
}
document.addEventListener('DOMContentLoaded', function() { enhance(document); });
document.body.addEventListener('htmx:afterSettle', function(e) { enhance(e.target); });
})();
</script>
<script>
// Modal detaliu trimitere (PRD 5.9 US-003): inlocuieste detaliul inline (5.8). Detaliul
// se incarca prin HTMX in #detaliu-modal-body (in afara #submissions-wrap, deci poll-ul
// de 15s nu-l atinge). Aici: deschidere la click pe rand, inchidere (x/Esc/backdrop),
// focus-trap, scroll-lock, inert+aria-hidden pe <main> (R7), stare de eroare la load
// esuat (R5), inchidere pe succes corectie/sterge (HX-Trigger inchideModal, R5).
(function() {
var overlay = document.getElementById('modal-detaliu');
if (!overlay) return;
var dialog = overlay.querySelector('.modal-dialog');
var body = document.getElementById('detaliu-modal-body');
var main = document.querySelector('main');
var trigger = null; // randul care a deschis modalul (focus return la inchidere)
var triggerId = null; // id-ul randului: re-query la inchidere daca poll-ul l-a re-swapuit
var onKeyTrap = null;
function focusable() {
return Array.prototype.filter.call(
dialog.querySelectorAll('a[href], button:not([disabled]), input:not([disabled]),' +
' select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'),
function(el) { return el.offsetParent !== null || el === document.activeElement; });
}
// R7: focus-trap — Tab/Shift+Tab cicleaza in interiorul dialogului.
function trapFocus(e) {
if (e.key !== 'Tab') return;
var f = focusable();
if (!f.length) { e.preventDefault(); return; }
var first = f[0], last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
function isOpen() { return !overlay.hidden; }
function open(triggerRow) {
trigger = triggerRow || null;
triggerId = (triggerRow && triggerRow.id) || null;
body.innerHTML = '<div class="empty muted" style="padding:24px;">Se incarca&hellip;</div>';
overlay.hidden = false;
document.body.classList.add('modal-open'); // scroll-lock pe body
if (main) { main.setAttribute('inert', ''); main.setAttribute('aria-hidden', 'true'); }
onKeyTrap = trapFocus;
document.addEventListener('keydown', onKeyTrap, true);
var x = overlay.querySelector('.modal-close');
if (x) x.focus(); // focus initial in modal
}
function close() {
if (!isOpen()) return;
overlay.hidden = true;
body.innerHTML = '';
document.body.classList.remove('modal-open');
if (main) { main.removeAttribute('inert'); main.removeAttribute('aria-hidden'); }
if (onKeyTrap) { document.removeEventListener('keydown', onKeyTrap, true); onKeyTrap = null; }
var t = trigger; trigger = null;
if (t && t.focus) t.focus(); // focus readus pe rand
}
// API public: butonul „Inchide" din fragment + inchiderea pe succes corectie/sterge.
// (Semnatura veche inchideDetaliu(id) pastrata, dar exista un singur modal o data.)
window.inchideDetaliu = function() { close(); };
// Inchidere: x si backdrop (elemente cu data-modal-close), Esc.
overlay.addEventListener('click', function(e) {
if (e.target && e.target.hasAttribute && e.target.hasAttribute('data-modal-close')) close();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && isOpen()) { e.preventDefault(); close(); }
});
// Deschidere la click pe rand (htmx:beforeRequest): arata modalul cu placeholder
// inainte ca raspunsul fragmentului sa fie swap-uit in corp.
document.body.addEventListener('htmx:beforeRequest', function(evt) {
var elt = evt.detail && evt.detail.elt;
if (elt && elt.classList && elt.classList.contains('trimitere-row')) open(elt);
});
// Dupa swap-ul fragmentului (sau re-render corectie/mapare): muta focusul in modal.
body.addEventListener('htmx:afterSettle', function() {
if (!isOpen()) return;
var f = focusable();
if (f.length) f[0].focus();
});
// R5: load-error al fragmentului (GET esuat) -> stare Reincearca/Inchide, nu placeholder blocat.
body.addEventListener('htmx:responseError', function(evt) {
if (!isOpen()) return;
var elt = evt.detail && evt.detail.elt;
var url = (elt && elt.getAttribute && elt.getAttribute('hx-get')) || '';
body.innerHTML = '<div class="modal-eroare"><p>Nu s-a putut incarca detaliul.</p>' +
'<div class="actiuni">' +
(url ? '<button type="button" data-modal-retry="' + url + '">Reincearca</button>' : '') +
'<button type="button" data-modal-close' +
' style="background:var(--card); color:var(--muted); border-color:var(--line);">Inchide</button>' +
'</div></div>';
});
body.addEventListener('click', function(e) {
var r = e.target.closest && e.target.closest('[data-modal-retry]');
if (r && window.htmx) htmx.ajax('GET', r.getAttribute('data-modal-retry'),
{ target: body, swap: 'innerHTML' });
});
// R5: inchidere pe succes corectie/sterge — ruta emite HX-Trigger `inchideModal`.
// Lista se reincarca separat prin `trimiteriChanged` (#submissions-wrap). Maparea
// inline NU emite inchideModal -> modalul ramane deschis sa arate codul rezolvat.
document.body.addEventListener('inchideModal', function() { close(); });
// Tastatura pe rand (role=button): Enter/Space deschid modalul.
document.body.addEventListener('keydown', function(evt) {
var t = evt.target;
if (!(t && t.classList && t.classList.contains('trimitere-row'))) return;
if (evt.key === 'Enter' || evt.key === ' ' || evt.key === 'Spacebar') {
evt.preventDefault();
t.click();
}
});
})();
</script>
</body>
</html>