Files
rar-autopass/app/web/templates/base.html
Claude Agent 5a964a1a8d feat(5.10): UX trimiteri (pill filtre, paginare, editare) + Mapari in meniu + branding ROMFAST
14 stories TDD prin echipa de workeri (lead orchestreaza, 3 teammates pe valuri cu fisiere disjuncte; routes.py + base.html serializate ca fisiere fierbinti).

- US-001 fix filtrare data (_iso_date_prefix pe garda+comparatie, prinde timestamp cu ora)
- US-002/007 operatie service distincta in payload_view + afisare in detaliu
- US-003 pill-uri categorii (button/aria-pressed; needs_mapping --warn, needs_data/error --err); fara lista ID-uri/dropdown
- US-004 paginare numerotata 25/pag (total ramificat SQL-COUNT vs fetch-all+slice, clamp page, poll pastreaza pagina)
- US-005 VIN block-level sub nr
- US-006/006b editare cod RAR + validare nomenclator + recalcul idempotency (needs_data/needs_mapping via /corecteaza, error via /repune)
- US-008 card eroare 3-niveluri doar pe read-only + rezumat top-of-form
- US-009 Mapari in meniu hamburger; scoatere tab-bar + role=tablist orfan
- US-010/011 pagina Mapari consolidata + butoane icon SVG + dirty-state (fara kebab/emoji)
- US-012/012b header centrat + logo ROMFAST (/static/romfast_logo.png) in header
- US-013 paleta azur ROMFAST (#2E74D6/#1F66C9) + IBM Plex Sans/Mono self-host (woff2 reale)
- US-014 selector tema ciclic Light/Dark/Petrol/Auto + anti-FOUC pe 4 stari

Backend trimitere (worker/masina stari/idempotenta/mapping) + schema NEATINSE (UI/UX pur + 1 fix de filtrare).
VERIFY context curat PASS; /code-review high: 1 finding material reparat (US-006b). Regresie 896 passed, 1 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 20:20:58 +00:00

831 lines
49 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, extins US-014 PRD 5.10): citeste preferinta tema din
// localStorage inainte de primul paint; seteaza data-theme pe <html> sincron, fara blink.
// Cunoaste toate cele 4 teme: light/dark/petrol/auto. Valoare legacy/necunoscuta -> auto.
// 'auto' se rezolva la 'light' sau 'dark' dupa prefers-color-scheme (fara blink).
(function() {
var VALID = {light:1, dark:1, petrol:1, auto:1};
try {
var t = localStorage.getItem('theme');
if (!t || !VALID[t]) t = 'auto'; // fallback: valoare lipsa sau legacy -> auto
if (t === 'auto') {
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>
/* US-013 (PRD 5.10): IBM Plex Sans + Mono self-hosted (latin-ext pentru diacritice romanesti).
font-display:swap permite text vizibil inainte de incarcare (FOUT system-ui->IBM Plex).
FOUT pe tabular-nums: IBM Plex Sans are metrici apropiate de system-ui; reflow-ul vizibil
pe VIN/coduri e acceptat explicit — fontul se incarca din /static/ (acelasi origin).
IBM Plex Sans/Mono self-host, subset latin + latin-ext de pe fontsource
(@fontsource/ibm-plex-sans + @fontsource/ibm-plex-mono, v5.0.8), woff2 valide. */
@font-face {
font-family: "IBM Plex Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/static/fonts/IBMPlexSans-Regular-latin-ext.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF;
}
@font-face {
font-family: "IBM Plex Sans";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/static/fonts/IBMPlexSans-Regular-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+20AC, U+2122, U+FEFF, U+FFFD;
}
@font-face {
font-family: "IBM Plex Sans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("/static/fonts/IBMPlexSans-Medium-latin-ext.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF;
}
@font-face {
font-family: "IBM Plex Sans";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("/static/fonts/IBMPlexSans-Medium-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+20AC, U+2122, U+FEFF, U+FFFD;
}
@font-face {
font-family: "IBM Plex Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/static/fonts/IBMPlexSans-Bold-latin-ext.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF;
}
@font-face {
font-family: "IBM Plex Sans";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/static/fonts/IBMPlexSans-Bold-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+20AC, U+2122, U+FEFF, U+FFFD;
}
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/static/fonts/IBMPlexMono-Regular-latin-ext.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF;
}
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/static/fonts/IBMPlexMono-Regular-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+20AC, U+2122, U+FEFF, U+FFFD;
}
/* Paleta dark (default) — accent azur ROMFAST conform DESIGN.md */
:root { --bg:#0f1218; --card:#181c24; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#2E74D6; }
/* Paleta light — accent azur inchis pentru contrast AA pe alb (#1F66C9: 5.51:1 pe alb) */
[data-theme="light"] { --bg:#f5f7fa; --card:#ffffff; --ink:#1a1d24; --muted:#5c6473; --line:#e2e5ea;
--ok:#15803d; --warn:#b45309; --err:#dc2626; --accent:#1F66C9; }
/* Paleta Petrol (US-014) — tema intunecata alternativa, accent teal #0E7C7B.
Wordmark-ul FAST #2E74D6 coexista armonios: ambele sunt reci/saturate, contrast AA pe --card #161e20. */
[data-theme="petrol"] { --bg:#0e1416; --card:#161e20; --ink:#e6e9ef; --muted:#8b93a7; --line:#232c2e;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#0E7C7B; }
* { box-sizing:border-box; }
/* PRD 5.9 US-006 — CONVENTIE BREAKPOINT: un singur prag mobil la 768px.
CSS custom properties NU functioneaza in `@media`, deci pragul nu poate fi o
variabila; folosim consecvent `@media (max-width:767px)` peste tot (mobil) si
`@media (max-width:1024px)` doar pentru densitatea tabelului. >=1024px = layout
desktop neschimbat (fara regresie). Orice regula mobila noua reutilizeaza 767px. */
body { margin:0; font:15px/1.5 "IBM Plex Sans",system-ui,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }
/* US-012 (PRD 5.10): grila 3 coloane — stanga (env badge echilibru) | centru (titlu+wordmark) | dreapta (controale). */
header { padding:16px 24px; border-bottom:1px solid var(--line);
display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; }
.header-left { display:flex; align-items:center; }
.header-center { display:flex; flex-direction:column; align-items:center; text-align:center; }
.header-right { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
/* US-012b: logo PNG ROMFAST sub titlu — 28px inaltime, centrat, fara filtre de culoare.
Logo are fundal transparent + culori proprii (ROM rosu + FAST albastru) -> ok pe toate temele. */
.brand-logo { height:28px; width:auto; display:block; margin:3px auto 0; }
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:"IBM Plex Mono",ui-monospace,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); }
/* US-011: variante icon-btn — dirty (modificari nesalvate) + danger (destructiv) */
.icon-btn.dirty { background:var(--accent); color:#fff; border-color:var(--accent); }
.icon-btn.dirty:hover { filter:brightness(0.9); }
.icon-btn.danger { color:var(--err); border-color:var(--err); }
.icon-btn.danger:hover, .icon-btn.danger:focus-visible { background:var(--err); color:#fff; }
.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; }
/* PRD 5.9 US-002: codul RAR pe linia 2 — chip discret, fara prefixul „cod RAR:". */
.tabel-trimiteri .cod-rar-cod { display:inline-block; font-family:"IBM Plex Mono",ui-monospace,monospace;
font-size:12px; padding:1px 7px; border:1px solid var(--line);
border-radius:99px; color:var(--muted); }
/* PRD 5.9 US-002 (R1): eticheta umana scurta sub pill — text mic; clasa `s-error`
o coloreaza (apare doar pe error/needs_*). Stare prin text, nu doar culoare. */
.tabel-trimiteri .eticheta-problema { font-size:12px; line-height:1.3; margin-top:3px; }
/* PRD 5.9 US-002 (R8): randul e clickabil (deschide modalul) -> tinta de atins >=44px
(touch) + afordanta hover/focus. Inlocuieste vechea regula `@media pointer:coarse
.chevron` (chevron eliminat); este SINGURA regula 44px pe rand. */
.tabel-trimiteri tr.trimitere-row { min-height:44px; }
.tabel-trimiteri tr.trimitere-row > td { padding-top:11px; padding-bottom:11px; }
.tabel-trimiteri tr.trimitere-row:hover { background:color-mix(in srgb, var(--accent) 6%, transparent); }
.tabel-trimiteri tr.trimitere-row:focus,
.tabel-trimiteri tr.trimitere-row:focus-visible { outline:2px solid var(--accent); outline-offset:-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: vezi blocul
`@media (max-width:767px)` US-006 de mai jos. === */
.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; }
/* === PRD 5.9 US-006: fundatie responsive mobil (<768px) ===
Breakpoint unic 767px (vezi conventia de sus). Cuprinde: card per rand pe tabelul
de trimiteri (5.8, pastrat), modal full-screen, header/nav colapsat cu tinte touch
>=44px. Desktop (>=1024px) ramane neschimbat — regulile de baza nu se modifica. */
@media (max-width:767px) {
/* Tabel trimiteri: card per rand (eticheta:valoare stivuit) -> fara scroll orizontal */
.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; }
/* Modal full-screen: ocupa tot ecranul, fara backdrop lateral (overlay fara
padding, dialog la latime/inaltime pline, fara colturi/umbra). Scroll intern
pe dialog; butonul `x` la >=44px (tinta touch). Desktop pastreaza varianta
centrata cu `max-width:680px` din regula de baza de mai sus. */
.modal-overlay { padding:0; align-items:stretch; justify-content:stretch; }
.modal-dialog { width:100vw; max-width:none; min-height:100vh; max-height:100vh;
border:none; border-radius:0; box-shadow:none;
padding:16px; padding-top:56px; overflow-y:auto; }
.modal-close { width:44px; height:44px; top:8px; right:8px; font-size:24px; }
/* US-004 (R11): actiunile de jos din detaliu (Re-pune / Sterge) full-width stivuit pe mobil. */
.detaliu-actiuni-jos button { width:100%; }
/* Header + nav colapsate: pe mobil trece de la grid la flex wrap.
Randul 1: [env badge stanga] [controale dreapta] (margin-left:auto pe .header-right).
Randul 2: [titlu + wordmark centrat, full-width]. Fara scroll orizontal, tinte >=44px. */
header { display:flex; flex-wrap:wrap; padding:12px 16px; gap:8px; align-items:center; }
.header-left { order:0; flex:0 0 auto; }
.header-center { order:2; width:100%; text-align:center; }
.header-right { order:1; margin-left:auto; flex:0 0 auto; }
header h1 { font-size:17px; }
main { padding:16px; }
.icon-btn { min-height:44px; min-width:44px; }
.tab-link { min-height:44px; padding:10px 14px; }
.cont-menu a, .cont-menu button { min-height:44px; }
/* === PRD 5.9 US-007 (R12): paginile de continut pe mobil ===
Tabele ACTIONABILE (Mapari) -> card per rand. Clasa proprie `.tabel-card`,
scopata SEPARAT de `.tabel-trimiteri` (5.8) ca sa NU strice cardurile de
trimiteri. Tabele DENSE read-only (Jurnal, Nomenclator) + Admin raman in
`.tablewrap` (scroll orizontal CONTAINED, definit global mai sus). */
.tabel-card table { table-layout:auto; }
.tabel-card thead { display:none; }
.tabel-card table, .tabel-card tbody, .tabel-card tr, .tabel-card td { display:block; width:auto; }
.tabel-card tr { border:1px solid var(--line); border-radius:8px; padding:10px 12px; margin-bottom:10px; }
.tabel-card td { border-bottom:none; padding:5px 0; }
.tabel-card td::before { content:attr(data-eticheta); display:block; color:var(--muted);
font-size:12px; margin-bottom:3px; }
/* Celulele fara eticheta (doar actiuni) nu primesc antet gol. */
.tabel-card td:not([data-eticheta])::before,
.tabel-card td[data-eticheta=""]::before { display:none; }
/* Controale full-width in card; butoanele primesc tinta touch >=44px. */
.tabel-card td select, .tabel-card td input[type=text],
.tabel-card td input[type=search] { width:100%; max-width:none; }
.tabel-card td button { width:100%; min-height:44px; }
/* Formulare de continut: o coloana, inputuri/selecturi full-width, butoane >=44px.
Scopat strict pe controalele de formular din sectiunile de continut (nu atinge
tabelul de trimiteri, modalul, taburile ARIA sau butoanele de copiere absolute). */
#card-cont input[type=email], #card-cont input[type=password],
#form-test-cheie input[type=password],
#jurnal-section select, #jurnal-section input[type=date],
#jurnal-section input[type=number] {
/* !important fiindca aceste inputuri au latimi inline (ex. width:280px / max-width:100px)
pe desktop; le suprascriem DOAR sub 767px, deci desktop ramane neschimbat. */
width:100% !important; max-width:none !important; box-sizing:border-box; }
#jurnal-section #filtre-jurnal > div { width:100%; }
#card-cont button, #form-test-cheie button,
#jurnal-section #filtre-jurnal button { min-height:44px; width:100%; }
/* === PRD 5.9 US-008: Acasa (upload, status, filtre) + login/signup pe mobil ===
Zona de upload, bara de status si bara de filtre (`_coada.html`) stiveaza pe O
coloana sub 767px; inputuri/butoane full-width cu tinta touch >=44px. Scopat pe
id-urile sectiunilor de pe Acasa ca sa NU atinga tabelul de trimiteri (5.8),
modalul sau paginile de continut (US-007). */
/* Bara de upload: zona slim (returning user) trece pe coloana; butonul full-width. */
#import-section .drop-zone { flex-direction:column; align-items:stretch; text-align:left; }
#import-section #upload-btn { width:100%; min-height:44px; }
/* Bara de status: contoarele/randurile raman aliniate la stanga, fara scroll orizontal. */
#status-bar > div { gap:10px; }
/* Bara de filtre trimiteri: o coloana, fiecare control full-width, buton >=44px.
!important suprascrie latimile inline (ex. max-width:180px pe vehicul) DOAR pe mobil. */
#filtre-trimiteri { flex-direction:column; align-items:stretch; }
#filtre-trimiteri > div { width:100%; }
#filtre-trimiteri select, #filtre-trimiteri input[type=text],
#filtre-trimiteri input[type=date] { width:100% !important; max-width:none !important; }
#filtre-trimiteri button { width:100%; min-height:44px; }
/* Card de autentificare (login/signup): centrat si nu depaseste viewport-ul pe mobil. */
.auth-card { max-width:100%; margin:24px auto; }
}
</style>
</head>
<body>
{# US-012 (PRD 5.10): grila 3 coloane — stanga (env badge) | centru (titlu+wordmark) | dreapta (controale). #}
<header>
{# Celula stanga: badge env (test/prod) — echilibru optic fata de controalele din dreapta #}
<div class="header-left">
<span class="env">{{ rar_env }}</span>
</div>
{# Celula centru: titlu + wordmark 'by ROMFAST' #}
<div class="header-center">
<h1>Gateway RAR AUTOPASS</h1>
{# US-012b (decizie user): logo PNG real in loc de wordmark text.
288x175 RGBA fundal transparent — lizibil pe light/dark/petrol fara filtre. #}
<img src="/static/romfast_logo.png" alt="ROMFAST" class="brand-logo">
</div>
{# Celula dreapta: comutator tema + versiune + meniu cont #}
<div class="header-right">
<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>
{# US-009 (PRD 5.10): Mapari mutat din tab-bar in meniu, cu badge needs_mapping. #}
{% set _mapari_badge = (badges.mapari if (badges is defined and badges and badges.mapari) else 0) %}
<a role="menuitem" href="/?tab=mapari">Mapari{% if _mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; margin-left:6px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ _mapari_badge }}</span>{% endif %}</a>
<hr>
<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>
{# aria-live pentru anuntarea schimbarilor de tema (US-014, accesibilitate) #}
<span id="tema-live" role="status" aria-live="polite"
style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;"></span>
<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>
// Comutator tema ciclic (US-014 PRD 5.10): click cicleaza Light->Dark->Petrol->Auto.
// Separare init (sincronizare iconita/label) de persistenta (doar la click explicit).
// 'auto' se rezolva la paint prin anti-FOUC; aici setam data-theme rezolvat.
(function() {
var btn = document.getElementById('tema-toggle');
if (!btn) return;
var CYCLE = ['light', 'dark', 'petrol', 'auto'];
var VALID = {light:1, dark:1, petrol:1, auto:1};
// Iconite per tema: ☀ Light, ☾ Dark, ◐ Petrol, ◉ Auto
var ICONS = {light:'&#9728;', dark:'&#9790;', petrol:'&#9680;', auto:'&#9689;'};
var LABELS = {light:'Light', dark:'Dark', petrol:'Petrol', auto:'Auto'};
var NEXT = {light:'Dark', dark:'Petrol', petrol:'Auto', auto:'Light'};
var TOOLTIP_CICLU = 'Ciclu: Light → Dark → Petrol → Auto';
function _stored() {
try { var v = localStorage.getItem('theme'); return (v && VALID[v]) ? v : 'auto'; } catch(e) { return 'auto'; }
}
function _resolved(stored) {
if (stored !== 'auto') return stored;
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
function _syncButton(stored) {
var s = VALID[stored] ? stored : 'auto';
btn.innerHTML = ICONS[s];
btn.setAttribute('aria-label', 'Tema: ' + LABELS[s] + ', apasa pentru ' + NEXT[s]);
btn.title = 'Tema: ' + LABELS[s] + '. ' + TOOLTIP_CICLU;
}
function _setTheme(t) {
document.documentElement.setAttribute('data-theme', _resolved(t));
try { localStorage.setItem('theme', t); } catch(e) {}
_syncButton(t);
var live = document.getElementById('tema-live');
if (live) live.textContent = 'Tema: ' + LABELS[t] + (t === 'auto' ? ' (urmeaza sistemul)' : '');
}
// Init: sincronizeaza iconita din starea stocata (fara a scrie in localStorage).
_syncButton(_stored());
btn.addEventListener('click', function() {
var cur = _stored();
var idx = CYCLE.indexOf(cur);
_setTheme(CYCLE[(idx + 1) % CYCLE.length]);
});
})();
</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>
// US-011: dirty state pentru butoanele de salvare din tabelele de mapari.
// Cand utilizatorul schimba un select dintr-un form de mapare, butonul de salvare
// legat prin data-dirty-form devine evidentiat (clasa "dirty" → fundal --accent).
// Starea "dirty" e efemera per-render: un swap outerHTML o reseteaza automat.
// Delegare pe document → supravietuieste swap-urilor HTMX (#mapari-section).
document.addEventListener('change', function(e) {
var el = e.target;
if (el.tagName !== 'SELECT' || !el.form || !el.form.id) return;
var saveBtn = document.querySelector('button[data-dirty-form="' + el.form.id + '"]');
if (saveBtn) saveBtn.classList.add('dirty');
});
</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>
<script>
// Poll-guard (PRD 5.9 US-005, R6). Inlocuieste vechea pauza pe „rand expandat" (5.8):
// randul-sibling de detaliu nu mai exista (US-003 l-a mutat in modalul global, care
// traieste in afara #submissions-wrap -> un swap de poll nu-l atinge). Aici oprim
// poll-ul de 15s de a REINCARCA lista cat timp (a) modalul e deschis SAU (b) exista
// cel putin un checkbox de bulk bifat — altfel modalul s-ar reseta / bifele s-ar sterge.
//
// CRITIC (F5): blocam DOAR trigger-ul periodic. In htmx `load`/`every 15s` declanseaza
// requestul FARA `triggeringEvent`; `trimiteriChanged` (HX-Trigger dupa corectie/stergere)
// si submit-ul/filtrul AU `triggeringEvent` -> TREC MEREU. Asa evitam blocajul permanent:
// daca randul bifat paraseste filtrul, pauza nu ramane lipita (pauza e legata strict de
// trigger-ul periodic, nu de o stare „sticky"). Anularea unui `htmx:beforeRequest` NU
// opreste timer-ul htmx (se reprogrameaza singur) -> poll-ul reia automat la urmatorul
// tic cand ambele conditii dispar; nu se pierde scroll, focus sau selectia de bife.
(function() {
function modalDeschis() {
var o = document.getElementById('modal-detaliu');
return !!(o && !o.hidden);
}
function existaBifa() {
return !!document.querySelector('#submissions-wrap input[name="submission_id"]:checked');
}
document.body.addEventListener('htmx:beforeRequest', function(evt) {
var d = evt.detail || {};
if (!d.elt || d.elt.id !== 'submissions-wrap') return; // doar poll-ul listei
var rc = d.requestConfig || {};
if (rc.triggeringEvent) return; // trimiteriChanged / filtru: TREC MEREU
if (modalDeschis() || existaBifa()) evt.preventDefault(); // pauza scopata pe periodic
});
// Resume pe checkbox `change`->gol: delegare pe body ca sa prinda si checkbox-urile
// randate dupa swap. Cand modalul e inchis si nu mai exista nicio bifa, fortam un
// refresh imediat (nu mai asteptam ticul de 15s) prin `trimiteriChanged from:body`,
// care pastreaza filtrul curent (hx-include #filtre-trimiteri) si trece de guard.
document.body.addEventListener('change', function(evt) {
var t = evt.target;
if (!(t && t.name === 'submission_id')) return;
if (!modalDeschis() && !existaBifa() && window.htmx) {
htmx.trigger(document.body, 'trimiteriChanged');
}
});
})();
</script>
</body>
</html>