Files
rar-autopass/app/web/templates/base.html
Claude Agent 51dc504f1d feat(5.8): reguli mapare pe text (substring/cont) + UX tabel trimiteri (detaliu inline, fara scroll, cod RAR)
Reguli text per cont (operation_text_rules), resolve_prestatii cu param aditiv
text_rules + precedenta stricta, threadat pe toate cele 6 callsite-uri + valid_codes
+ seam classify_prezentare. UI Mapari: sectiune reguli + preview pre-salvare + overlap
+ telemetrie text_rule_hit. UX tabel: cod_rar sub operatie, pill eticheta scurta, fara
scroll orizontal (scopat .tabel-trimiteri + carduri <768px), detaliu inline expandabil
(a11y + pauza poll). code-review: reparat regula auto_send=0 care trimitea automat la RAR
in loc sa tina randul pentru review. 814 passed.

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

511 lines
28 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; }
/* === Detaliu inline (PRD 5.8 US-008): rand-sibling expandabil sub randul selectat. === */
/* Chevron de stare (▸ inchis / ▾ deschis), rotit prin schimbarea glifei in JS. */
.tabel-trimiteri .chevron { display:inline-block; color:var(--muted); font-size:11px;
width:1.1em; text-align:center; margin-right:2px; }
/* Randul deschis: fundal evidentiat (nu doar culoare de text -> a11y). */
.tabel-trimiteri tr.rand-deschis > td { background:#1d212b; }
[data-theme="light"] .tabel-trimiteri tr.rand-deschis > td { background:#eef1f6; }
/* Conectorul detaliului = fundal subtil + border-top (NU border-left accent / slop). */
.tabel-trimiteri tr.detaliu-rand > td { padding:0; border-top:2px solid var(--accent);
background:color-mix(in srgb, var(--accent) 6%, var(--card)); }
.tabel-trimiteri tr.detaliu-rand .card { margin:10px; }
/* `hidden` trebuie sa invinga `display:block` din banda <768 (specificitate). */
.tabel-trimiteri tr.detaliu-rand[hidden] { display:none; }
/* 768-1024px: ascunde Actualizat (e in detaliu) -> 7 coloane, fara scroll */
@media (max-width:1024px) {
.tabel-trimiteri .col-actualizat { display:none; }
}
/* Tinta de atins >=44px pe touch (chevron-ul e ancora de toggle). */
@media (pointer:coarse) {
.tabel-trimiteri .chevron { min-width:44px; min-height:44px; line-height:44px; }
}
/* <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>
<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>
// Detaliu trimitere INLINE (PRD 5.8 US-008): randul de detaliu (#detaliu-{id}) e
// un <tr class="detaliu-rand"> sibling, ascuns pana la deschidere. La click pe rand:
// - se inchid celelalte detalii (un singur rand deschis o data);
// - se arata randul-sibling (placeholder „Se incarca…" prin hx-indicator);
// - chevron ▸/▾ + fundal evidentiat + aria-expanded sincronizate.
// Re-click pe acelasi rand inchide fara re-fetch. Cat un detaliu e deschis, poll-ul
// de 15s (#submissions-wrap) e pus pe pauza (D-eng-2) ca lista sa nu se miste sub
// operator. Delegare pe document.body -> supravietuieste swap-urilor HTMX ale listei.
(function() {
function chevron(row, on) {
var c = row.querySelector('.chevron');
if (c) c.innerHTML = on ? '&#9662;' : '&#9656;'; // ▾ / ▸
}
function setExpanded(row, on) {
row.setAttribute('aria-expanded', on ? 'true' : 'false');
if (on) row.classList.add('rand-deschis'); else row.classList.remove('rand-deschis');
chevron(row, on);
}
function detRowFor(id) {
var cont = document.getElementById('detaliu-' + id);
return cont ? cont.closest('tr.detaliu-rand') : null;
}
function closeOne(row) {
var id = row.getAttribute('data-detaliu-id');
var cont = document.getElementById('detaliu-' + id);
if (cont) cont.innerHTML = '';
var dr = detRowFor(id);
if (dr) dr.hidden = true;
setExpanded(row, false);
}
function closeAllDetalii(except) {
document.querySelectorAll('tr.trimitere-row[aria-expanded="true"]').forEach(function(r) {
if (r !== except) closeOne(r);
});
}
// Expus pentru butonul „Inchide" din _trimitere_detaliu.html: goleste containerul
// randului CURENT si readuce focusul pe randul declansator.
window.inchideDetaliu = function(id) {
var row = document.getElementById('trimitere-row-' + id);
if (row) { closeOne(row); row.focus(); }
else {
var cont = document.getElementById('detaliu-' + id);
if (cont) cont.innerHTML = '';
var dr = detRowFor(id);
if (dr) dr.hidden = true;
}
};
// Expus pentru scriptul fragmentului: marcheaza randul ca deschis dupa un re-swap
// (corectie/mapare inline), inchizand orice alt detaliu ramas deschis.
window.marcheazaDetaliuDeschis = function(row) {
closeAllDetalii(row);
setExpanded(row, true);
};
// htmx:beforeRequest — single point: pauza poll + toggle deschidere/inchidere.
document.body.addEventListener('htmx:beforeRequest', function(evt) {
var elt = evt.detail && evt.detail.elt;
if (!elt) return;
// Pauza poll periodic cat un detaliu e deschis (cererea vine chiar de pe wrap).
if (elt.id === 'submissions-wrap' &&
document.querySelector('tr.detaliu-rand:not([hidden])')) {
evt.preventDefault();
return;
}
if (!(elt.classList && elt.classList.contains('trimitere-row'))) return;
var id = elt.getAttribute('data-detaliu-id');
if (elt.getAttribute('aria-expanded') === 'true') {
// Re-click pe randul deschis -> inchide, fara re-fetch.
evt.preventDefault();
window.inchideDetaliu(id);
return;
}
// Deschidere: inchide celelalte, arata randul-sibling (placeholder loading).
closeAllDetalii(elt);
var dr = detRowFor(id);
if (dr) dr.hidden = false;
setExpanded(elt, true);
});
// Tastatura (role=button): Enter/Space deschid/inchid randul focusat.
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>