fix(5.11): tabel trimiteri stabil — bug status=None, pills in bara de filtre, nudge "Date noi" in loc de poll 15s, logo ROMFAST marit

- Fix bug: campul hidden de filtru randa literal "None" (status_filtru None +
  Jinja default('')) -> poll-ul trimitea status=None -> tabel gol. status or "".
- Pills de stare mutate din bara de status in bara de filtre (filtreazaStare scrie
  campul hidden + re-trimite form-ul; filtrul persista la reincarcari). Re-randate
  OOB cu contoare proaspete la fiecare reincarcare a tabelului.
- Polling redesign: tabelul nu se mai reincarca singur (fara every 15s). Poller usor
  JSON (/_fragments/trimiteri-versiune) detecteaza schimbari -> nudge "Date noi —
  Reincarca". Reincarcarea (nudge / actiune) pastreaza filtrul+pagina. Scroll/selectia
  nu se mai pierd. Poll-guard eliminat (nu mai exista poll periodic de pauzat).
- Logo ROMFAST 32px -> 60px (ca pe romfast.ro), header min-height 92px, 44px pe mobil.

Regresie: 896 passed, 1 deselected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-25 21:13:42 +00:00
parent 074b6e7c8a
commit f05fe5b221
10 changed files with 224 additions and 199 deletions

View File

@@ -21,19 +21,16 @@
</span>
</div>
<!-- Filtre (US-009): reincarca tabelul; poll-ul re-trimite filtrul curent prin hx-include -->
<!-- Bara de filtre: vehicul/data + pill-uri de stare pe acelasi rand. Pill-urile scriu
campul hidden status si re-trimit form-ul (filtreazaStare) -> filtrul persista la reincarcari. -->
<form id="filtre-trimiteri"
hx-get="/_fragments/submissions"
hx-target="#submissions-wrap"
hx-swap="innerHTML"
hx-trigger="submit, change, keyup delay:400ms from:input[name='vehicul']"
style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end; margin-bottom:12px;">
{# US-003 (PRD 5.10): dropdown status eliminat — inlocuit cu pill-uri in bara de status.
Filtrul de stare vine de la pill-uri (/_fragments/submissions?status=X direct).
Camp hidden permite reset stare la submit manual din form (Filtreaza). #}
<input type="hidden" name="status" value="{{ status_filtru | default('') }}">
{# US-004 (PRD 5.10): pagina curenta — actualizata prin OOB swap din _submissions.html.
Poll-ul (hx-include="#filtre-trimiteri") include automat pagina curenta (L2 PRD). #}
style="display:flex; gap:10px 14px; flex-wrap:wrap; align-items:flex-end; margin-bottom:12px;">
<input type="hidden" id="f-status" name="status" value="{{ status_filtru | default('', true) }}">
{# Pagina curenta — actualizata prin OOB swap din _submissions.html; inclusa la reincarcari. #}
<input type="hidden" id="f-page" name="page" value="1">
<div>
<label for="f-vehicul" class="muted" style="display:block; font-size:12px;">Vehicul (nr/VIN)</label>
@@ -48,12 +45,24 @@
<input id="f-data-pana" type="date" name="data_pana">
</div>
<button type="submit">Filtreaza</button>
{# Pill-uri de stare pe acelasi rand cu filtrele; re-randate prin OOB la reincarcarea tabelului. #}
<span id="pills-categorii" class="pills-categorii" style="margin-left:auto;">
{% include '_pills.html' %}
</span>
</form>
<!-- Poll aliniat la 15s ca status-ul (M5: nu doua timere perpetue pe pagina mereu deschisa) -->
<!-- Nudge "Date noi": tabelul nu se reimprospateaza singur; bannerul apare doar cand
pollerul usor detecteaza schimbari, iar utilizatorul reincarca cand vrea. -->
<div id="nudge-trimiteri" hidden role="status" aria-live="polite">
<span>Sunt trimiteri actualizate.</span>
<button type="button" onclick="reincarcaTrimiteri()">Reincarca</button>
</div>
<!-- Tabelul se reincarca DOAR la: incarcarea paginii, actiunile tale (trimiteriChanged)
sau apasarea pe Reincarca (reincarcaTrimiteri). Fara poll periodic care sa-l reseteze. -->
<div id="submissions-wrap"
hx-get="/_fragments/submissions"
hx-trigger="load, every 15s, trimiteriChanged from:body"
hx-trigger="load, trimiteriChanged from:body, reincarcaTrimiteri"
hx-include="#filtre-trimiteri" hx-swap="innerHTML">
<div class="empty">se incarca…</div>
</div>

View File

@@ -0,0 +1,15 @@
{# Pill-uri de filtrare a starii, randate in bara de filtre (_coada.html) si re-randate
prin OOB la fiecare reincarcare a tabelului (_submissions.html). Stare activa =
status_filtru. "Toate" reseteaza filtrul; categoriile apar doar cand au n>0. #}
<button type="button" class="pill-cat pill-cat-reset" data-status=""
aria-pressed="{{ 'true' if not status_filtru else 'false' }}"
onclick="filtreazaStare(this, '')">Toate</button>
{% for pill in pills_categorii %}
<button type="button" class="pill-cat" data-status="{{ pill.status }}"
aria-pressed="{{ 'true' if status_filtru == pill.status else 'false' }}"
style="color:var({{ pill.color_var }}); border-color:var({{ pill.color_var }});"
onclick="filtreazaStare(this, '{{ pill.status }}')">
{{ pill.label }}
<span class="pill-cat-n" style="background:var({{ pill.color_var }});">{{ pill.n }}</span>
</button>
{% endfor %}

View File

@@ -1,15 +1,3 @@
<style>
/* Pill-uri categorii blocate (US-003 PRD 5.10)
Culoarea e injectata inline (color_var: --warn / --err) dupa DESIGN.md:
Lipsa cod = --warn (chihlimbar), Date incomplete + Eroare = --err (rosu).
Activ = fundal pe culoarea categoriei (NU accent albastru — S1/A5). */
.pill-cat { transition: background 0.15s, color 0.15s; }
.pill-cat:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Activ: background = culoarea categoriei (currentColor = var(--err/--warn) din inline style),
text = var(--card) (contrast AA). NU accent albastru (S1/A5 DESIGN.md). */
.pill-cat[aria-pressed="true"] { background: currentColor !important; color: var(--card) !important; border-color: currentColor !important; }
.pill-cat[aria-pressed="true"] span { background: var(--card) !important; color: currentColor; }
</style>
<div id="status-bar" class="status-bar card"
hx-get="/_fragments/status"
hx-trigger="every 15s"
@@ -59,51 +47,6 @@
</span>
</div>
<!-- Pill-uri categorii blocate (US-003 PRD 5.10): inlocuiesc lista de ID-uri.
<button> reale cu aria-pressed, focalizabile, activare Enter/Space.
Inactiv = contur+text pe culoarea categoriei; activ = umplere pe culoarea categoriei.
Pill ascuns cand n=0 (lista pills_categorii filtreaza deja). -->
{% if pills_categorii %}
<div style="margin-top:12px; padding-top:10px; border-top:1px solid var(--line);">
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
<span style="font-size:12px; color:var(--muted);">Necesita atentie:</span>
{% for pill in pills_categorii %}
<button type="button"
class="pill-cat"
aria-pressed="false"
hx-get="/_fragments/submissions?status={{ pill.status }}"
hx-target="#submissions-wrap"
hx-swap="innerHTML"
onclick="(function(b){
var pressed=b.getAttribute('aria-pressed')==='true';
document.querySelectorAll('.pill-cat').forEach(function(x){x.setAttribute('aria-pressed','false');});
if(!pressed){b.setAttribute('aria-pressed','true');}
var s=document.getElementById('trimiteri-section');
if(s){s.scrollIntoView({behavior:'smooth'});}
})(this)"
style="display:inline-flex; align-items:center; gap:5px;
padding:3px 10px; border-radius:99px; font-size:12px; font-weight:600;
cursor:pointer; border:1.5px solid var({{ pill.color_var }}); color:var({{ pill.color_var }});
background:transparent; transition:background 0.15s, color 0.15s;">
{{ pill.label }}
<span style="font-size:11px; font-weight:700; background:var({{ pill.color_var }}); color:var(--card);
padding:0 5px; border-radius:99px; min-width:18px; text-align:center;">{{ pill.n }}</span>
</button>
{% endfor %}
{# Buton "Toate" — reseteaza filtrul de categorie #}
<button type="button"
class="pill-cat-reset"
aria-label="Arata toate trimiterile"
hx-get="/_fragments/submissions"
hx-target="#submissions-wrap"
hx-swap="innerHTML"
onclick="document.querySelectorAll('.pill-cat').forEach(function(x){x.setAttribute('aria-pressed','false');})"
style="padding:3px 10px; border-radius:99px; font-size:12px; cursor:pointer;
border:1px solid var(--line); background:transparent; color:var(--muted);">
Toate
</button>
</div>
</div>
{% endif %}
{# Pill-urile de stare s-au mutat in bara de filtre din sectiunea Trimiteri (_coada.html). #}
</div>

View File

@@ -5,6 +5,13 @@
#}
<input type="hidden" id="f-page" name="page" value="{{ page | default(1) }}" hx-swap-oob="true">
{# OOB: re-randeaza pill-urile de stare (in bara de filtre, in afara #submissions-wrap) cu
contoarele si starea activa proaspete la fiecare reincarcare a tabelului. #}
<span hx-swap-oob="innerHTML:#pills-categorii">{% include '_pills.html' %}</span>
{# Versiunea datelor cu care s-a randat tabelul; pollerul "Date noi" o compara. #}
<span id="trimiteri-versiune" data-v="{{ versiune_trimiteri | default('') }}" hidden></span>
{% if rows %}
{# US-011: form de stergere bulk. Selectia opereaza DOAR pe randuri blocate
(gestionabil); sent/sending/queued nu au checkbox (read-only). #}

View File

@@ -123,7 +123,7 @@
background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }
/* US-012c (PRD 5.10): grila 3 coloane — stanga (logo ROMFAST) | centru (titlu+env) | 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; }
display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; min-height:92px; }
.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; }
@@ -131,7 +131,8 @@
32px inaltime — usor mai mare decat in header-center (28px) pentru vizibilitate ca brand anchor.
margin:0 — aliniat stanga, NU centrat (era `margin:3px auto 0` cand era sub titlu).
Logo transparent: ok pe dark/light/petrol fara filtre de culoare. */
.brand-logo { height:32px; width:auto; display:block; margin:0; }
/* Logo ROMFAST la dimensiunea de pe romfast.ro (~60px inaltime), aliniat stanga. */
.brand-logo { height:60px; width:auto; display:block; margin:0; }
/* Env badge mic sub titlu in header-center (US-012c): nu mai echilibreaza optic dreapta
(logo-ul face asta), ci identifica mediul langa titlu. Pastrat mic, color:var(--muted). */
.header-center .env { font-size:11px; margin-top:2px; }
@@ -149,6 +150,28 @@
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); }
/* Pill-uri de filtrare a starii (bara de filtre Trimiteri). Inactiv = contur+text pe
culoarea categoriei (injectata inline); activ = umplere pe acea culoare. */
.pills-categorii { display:inline-flex; gap:8px; flex-wrap:wrap; align-items:center; }
.pill-cat { display:inline-flex; align-items:center; gap:5px; padding:4px 11px; border-radius:99px;
font-size:12px; font-weight:600; cursor:pointer; background:transparent;
border:1.5px solid var(--line); color:var(--muted); min-height:30px;
transition:background .15s, color .15s; }
.pill-cat:hover { filter:brightness(1.1); }
.pill-cat:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
.pill-cat-n { font-size:11px; font-weight:700; color:var(--card); padding:0 5px;
border-radius:99px; min-width:18px; text-align:center; }
.pill-cat[aria-pressed="true"] { background:currentColor; color:var(--card); border-color:currentColor; }
.pill-cat[aria-pressed="true"] .pill-cat-n { background:var(--card) !important; color:currentColor; }
.pill-cat-reset[aria-pressed="true"] { background:var(--accent); color:#fff; border-color:var(--accent); }
/* Nudge "Date noi": apare doar cand pollerul usor detecteaza schimbari; tabelul nu se
schimba singur niciodata, utilizatorul reincarca cand vrea. */
#nudge-trimiteri { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin:0 0 12px;
padding:8px 12px; border-radius:8px; font-size:13px;
border:1px solid var(--accent);
background:color-mix(in srgb, var(--accent) 12%, var(--card)); }
#nudge-trimiteri[hidden] { display:none; }
#nudge-trimiteri button { font-size:13px; padding:5px 12px; min-height:32px; }
.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);}
@@ -349,7 +372,8 @@
/* Header + nav colapsate: pe mobil trece de la grid la flex wrap.
Randul 1: [logo ROMFAST stanga] [controale dreapta] (margin-left:auto pe .header-right).
Randul 2: [titlu + env mic centrat, full-width]. Fara scroll orizontal, tinte >=44px. */
header { display:flex; flex-wrap:wrap; padding:12px 16px; gap:8px; align-items:center; }
header { display:flex; flex-wrap:wrap; padding:12px 16px; gap:8px; align-items:center; min-height:0; }
.brand-logo { height:44px; }
.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; }
@@ -789,45 +813,49 @@
})();
</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.
// Filtrare stare prin pill-uri + reincarcare manuala a tabelului. Tabelul NU se mai
// schimba singur (fara poll periodic pe #submissions-wrap): un poller usor verifica
// doar versiunea datelor si arata nudge-ul "Date noi" cand difera. Reincarcarea
// (pill, nudge sau actiune) trece prin form -> pastreaza filtrul/pagina curenta.
(function() {
function modalDeschis() {
var o = document.getElementById('modal-detaliu');
return !!(o && !o.hidden);
// Pill de stare: scrie campul hidden, reseteaza pagina la 1 si re-trimite filtrul.
window.filtreazaStare = function(btn, status) {
var form = document.getElementById('filtre-trimiteri');
if (!form) return;
var hs = document.getElementById('f-status'); if (hs) hs.value = status || '';
var hp = document.getElementById('f-page'); if (hp) hp.value = '1';
document.querySelectorAll('#pills-categorii .pill-cat').forEach(function(b) {
b.setAttribute('aria-pressed', 'false');
});
if (btn) btn.setAttribute('aria-pressed', 'true');
if (form.requestSubmit) form.requestSubmit(); else form.submit();
};
// Reincarca tabelul pastrand filtrul curent (hx-include #filtre-trimiteri) si ascunde nudge-ul.
window.reincarcaTrimiteri = function() {
var n = document.getElementById('nudge-trimiteri'); if (n) n.hidden = true;
if (window.htmx) htmx.trigger('#submissions-wrap', 'reincarcaTrimiteri');
};
// Poller "Date noi": compara versiunea datelor cu cea cu care s-a randat tabelul.
// Daca difera, arata nudge-ul; daca nu, nu atinge nimic. JSON usor, fara re-render.
var INTERVAL = 20000;
function versiuneCurenta() {
var e = document.getElementById('trimiteri-versiune');
return e ? e.getAttribute('data-v') : null;
}
function existaBifa() {
return !!document.querySelector('#submissions-wrap input[name="submission_id"]:checked');
function verifica() {
if (versiuneCurenta() === null) return; // tabelul nu e pe ecran (alt tab)
var nudge = document.getElementById('nudge-trimiteri');
if (!nudge || !nudge.hidden) return; // deja afisat -> nu re-cere
fetch('/_fragments/trimiteri-versiune', { headers: { 'X-Requested-With': 'fetch' } })
.then(function(r) { return r.ok ? r.json() : null; })
.then(function(d) {
if (!d) return;
if (d.v !== versiuneCurenta()) nudge.hidden = false;
})
.catch(function() {});
}
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');
}
});
setInterval(verifica, INTERVAL);
})();
</script>
</body>