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:
@@ -20,7 +20,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from .. import __version__
|
||||
@@ -113,6 +113,18 @@ def _status_counts(conn, account_id: int) -> dict[str, int]:
|
||||
return {r["status"]: int(r["n"]) for r in rows}
|
||||
|
||||
|
||||
def _trimiteri_versiune(conn, account_id: int) -> str:
|
||||
"""Semnatura ieftina a starii trimiterilor contului: numar randuri + cel mai recent
|
||||
updated_at. Se schimba la orice insert/update/delete -> nudge-ul "Date noi" o compara
|
||||
fara a re-randa tabelul."""
|
||||
row = conn.execute(
|
||||
"SELECT COUNT(*) AS n, COALESCE(MAX(updated_at), '') AS m FROM submissions "
|
||||
"WHERE (account_id = ? OR (? = 1 AND account_id IS NULL))",
|
||||
(account_id, account_id),
|
||||
).fetchone()
|
||||
return f"{row['n']}:{row['m']}"
|
||||
|
||||
|
||||
def _account_active(conn, account_id: int) -> bool:
|
||||
"""True daca contul e activ (sau legacy cu NULL/absent active)."""
|
||||
row = conn.execute("SELECT active FROM accounts WHERE id=?", (account_id,)).fetchone()
|
||||
@@ -196,6 +208,10 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
|
||||
"are_trimiteri": are_trimiteri,
|
||||
"are_cheie_folosita": are_cheie_folosita,
|
||||
"blocate_total": blocate_total,
|
||||
# Pill-uri de filtrare a starii, randate in bara de filtre (nu in bara de status).
|
||||
"pills_categorii": _pills_categorii(counts),
|
||||
# Semnatura datelor: nudge-ul "Date noi" o compara la fiecare poll usor.
|
||||
"versiune_trimiteri": _trimiteri_versiune(conn, account_id),
|
||||
# US-002: Acasa include caseta de upload -> are nevoie de csrf_token
|
||||
"csrf_token": get_csrf_token(request),
|
||||
}
|
||||
@@ -212,7 +228,9 @@ def _render_panel_acasa(request: Request, conn=None, account_id: int = 1, status
|
||||
{"request": request, "csrf_token": get_csrf_token(request)}
|
||||
)
|
||||
ctx = _get_acasa_context(request, conn, account_id)
|
||||
ctx["status_filtru"] = status
|
||||
# `status or ""`: campul hidden de filtru ar randa literal "None" cu un None Python
|
||||
# (Jinja `default('')` inlocuieste doar undefined), trimitand status=None la poll.
|
||||
ctx["status_filtru"] = status or ""
|
||||
return templates.get_template("_acasa.html").render(ctx)
|
||||
|
||||
|
||||
@@ -621,6 +639,19 @@ def fragment_status(request: Request) -> HTMLResponse:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/_fragments/trimiteri-versiune", response_class=JSONResponse)
|
||||
def fragment_trimiteri_versiune(request: Request) -> JSONResponse:
|
||||
"""Semnatura curenta a trimiterilor contului (JSON usor). Pollerul "Date noi" o
|
||||
compara cu versiunea cu care s-a randat tabelul; daca difera, arata nudge-ul de
|
||||
reincarcare — tabelul nu se mai schimba singur."""
|
||||
account_id = require_login(request)
|
||||
conn = get_connection()
|
||||
try:
|
||||
return JSONResponse({"v": _trimiteri_versiune(conn, account_id)})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _iso_date_prefix(value: object) -> str | None:
|
||||
"""Intoarce primele 10 caractere (YYYY-MM-DD) daca incep cu o data ISO valida, altfel None.
|
||||
|
||||
@@ -795,6 +826,10 @@ def fragment_submissions(
|
||||
"f_vehicul": vehicul_q or "",
|
||||
"f_data_de": data_de or "",
|
||||
"f_data_pana": data_pana or "",
|
||||
# Pill-uri (OOB) + stare activa + versiune pentru nudge-ul "Date noi".
|
||||
"pills_categorii": _pills_categorii(_status_counts(conn, account_id)),
|
||||
"status_filtru": status or "",
|
||||
"versiune_trimiteri": _trimiteri_versiune(conn, account_id),
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -823,6 +858,9 @@ def _render_submissions(request: Request, conn, account_id: int) -> HTMLResponse
|
||||
"rows": view,
|
||||
"filtru_activ": False,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
"pills_categorii": _pills_categorii(_status_counts(conn, account_id)),
|
||||
"status_filtru": "",
|
||||
"versiune_trimiteri": _trimiteri_versiune(conn, account_id),
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
15
app/web/templates/_pills.html
Normal file
15
app/web/templates/_pills.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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). #}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -128,10 +128,19 @@ def test_badge_trimiteri_scoped_pe_acasa(client):
|
||||
assert "2" in html[idx:idx + 400]
|
||||
|
||||
|
||||
def test_trimiteri_poll_aliniat_15s(client):
|
||||
"""Poll-ul de trimiteri e aliniat la 15s (anti dublu-poll M5), nu 10s."""
|
||||
def test_trimiteri_fara_poll_periodic_pe_tabel(client):
|
||||
"""Tabelul de trimiteri NU se mai reimprospateaza periodic: #submissions-wrap se
|
||||
incarca la load / actiunile utilizatorului / Reincarca (nudge), fara `every Ns`.
|
||||
Reimprospatarea live se face prin nudge-ul "Date noi" + endpointul de versiune."""
|
||||
_seed_submission("sent")
|
||||
r = client.get("/?tab=acasa")
|
||||
html = r.text
|
||||
assert "every 15s" in html
|
||||
assert "every 10s" not in html
|
||||
# Trigger-ul tabelului nu contine poll periodic.
|
||||
wrap = html[html.find('id="submissions-wrap"'):]
|
||||
wrap = wrap[:wrap.find(">") + 1]
|
||||
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
|
||||
assert "reincarcaTrimiteri" in wrap
|
||||
assert "trimiteriChanged" in wrap
|
||||
# Mecanismul de nudge exista (banner + endpoint versiune).
|
||||
assert 'id="nudge-trimiteri"' in html
|
||||
assert "/_fragments/trimiteri-versiune" in html
|
||||
|
||||
@@ -161,58 +161,48 @@ def test_modal_hookuri_js_prezente(client):
|
||||
assert "window.inchideDetaliu" in js
|
||||
|
||||
|
||||
# --- PRD 5.9 US-005 (R6): poll-guard ---------------------------------------
|
||||
# Modalul + selectia trebuie sa supravietuiasca poll-ului de 15s. Logica e JS in
|
||||
# base.html: testam la nivel de markup/handler ca guard-ul exista si distinge corect
|
||||
# sursa trigger-ului (periodic vs trimiteriChanged/filtru). Comportamentul runtime
|
||||
# efectiv (anularea propriu-zisa) e validat E2E (requiresBrowserCheck) — aici asertam
|
||||
# codul/atributele care il implementeaza.
|
||||
# --- Tabelul nu se reincarca singur: modalul + selectia sunt sigure ---------
|
||||
# Tabelul (#submissions-wrap) nu mai are poll periodic; se reincarca DOAR la load,
|
||||
# la actiunile utilizatorului (trimiteriChanged) sau la apasarea pe Reincarca (nudge).
|
||||
# Asa, modalul deschis si bifele de bulk nu pot fi sterse de un timer.
|
||||
|
||||
|
||||
def test_poll_pauzat_cat_modal_deschis(client):
|
||||
"""Guard-ul de poll exista si, cat modalul de detaliu e deschis, anuleaza
|
||||
reincarcarea periodica a listei (#submissions-wrap), nu pe restul."""
|
||||
_create_account_user("poll1@test.com")
|
||||
def test_tabel_fara_poll_periodic(client):
|
||||
"""#submissions-wrap nu are trigger periodic (`every Ns`) — niciun timer nu poate
|
||||
reseta modalul deschis sau selectia de bulk in timpul interactiunii."""
|
||||
acct = _create_account_user("poll1@test.com")
|
||||
_login(client, "poll1@test.com")
|
||||
js = client.get("/?tab=acasa").text
|
||||
_insert_submission(acct)
|
||||
html = client.get("/?tab=acasa").text
|
||||
|
||||
# Guard scopat la poll-ul listei, declansat pe htmx:beforeRequest.
|
||||
assert "htmx:beforeRequest" in js
|
||||
assert "d.elt.id !== 'submissions-wrap'" in js, "guard-ul trebuie scopat la #submissions-wrap"
|
||||
# Conditia (a): modal deschis -> pauza (preventDefault).
|
||||
assert "modalDeschis" in js
|
||||
assert "modal-detaliu" in js and "hidden" in js
|
||||
assert "evt.preventDefault()" in js, "pauza scopata se face prin preventDefault"
|
||||
assert 'id="submissions-wrap"' in html
|
||||
wrap = html[html.find('id="submissions-wrap"'):]
|
||||
wrap = wrap[:wrap.find(">") + 1]
|
||||
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
|
||||
|
||||
|
||||
def test_poll_pauzat_cat_exista_bifa(client):
|
||||
"""Conditia (b): macar un checkbox de bulk bifat -> poll-ul periodic e pus pe
|
||||
pauza. Resume pe checkbox `change` prin delegare pe body (prinde si bifele
|
||||
randate dupa swap)."""
|
||||
_create_account_user("poll2@test.com")
|
||||
def test_nudge_date_noi_in_loc_de_poll(client):
|
||||
"""Reimprospatarea live se face prin nudge-ul 'Date noi' (poller usor de versiune)
|
||||
care NU atinge tabelul; utilizatorul reincarca explicit cand vrea."""
|
||||
acct = _create_account_user("poll2@test.com")
|
||||
_login(client, "poll2@test.com")
|
||||
js = client.get("/?tab=acasa").text
|
||||
_insert_submission(acct)
|
||||
html = client.get("/?tab=acasa").text
|
||||
|
||||
# Detecteaza bifa de bulk in interiorul #submissions-wrap.
|
||||
assert "existaBifa" in js
|
||||
assert 'input[name="submission_id"]:checked' in js
|
||||
# Resume: delegare pe body pe evenimentul `change` al checkbox-ului de bulk.
|
||||
assert "addEventListener('change'" in js
|
||||
assert "t.name === 'submission_id'" in js
|
||||
assert 'id="nudge-trimiteri"' in html, "bannerul nudge 'Date noi' trebuie sa existe"
|
||||
assert "/_fragments/trimiteri-versiune" in html, "pollerul de versiune trebuie configurat"
|
||||
assert "reincarcaTrimiteri" in html, "reincarcarea manuala (Reincarca) trebuie expusa"
|
||||
|
||||
|
||||
def test_trimiteriChanged_inca_reincarca_cu_bifa(client):
|
||||
"""R6 (F5): guard-ul NU anuleaza request-urile cu `triggeringEvent`
|
||||
(trimiteriChanged / submit filtru) — acelea TREC MEREU, ca pauza sa nu ramana
|
||||
lipita permanent daca randul bifat paraseste filtrul."""
|
||||
_create_account_user("poll3@test.com")
|
||||
def test_trimiteriChanged_inca_reincarca(client):
|
||||
"""Actiunile utilizatorului (corectie / stergere) reincarca tabelul prin canalul
|
||||
`trimiteriChanged`, pastrand filtrul curent (hx-include #filtre-trimiteri)."""
|
||||
acct = _create_account_user("poll3@test.com")
|
||||
_login(client, "poll3@test.com")
|
||||
js = client.get("/?tab=acasa").text
|
||||
_insert_submission(acct)
|
||||
html = client.get("/?tab=acasa").text
|
||||
|
||||
# Numai trigger-ul periodic (fara triggeringEvent) e candidat la pauza;
|
||||
# orice request cu triggeringEvent iese devreme din guard.
|
||||
assert "triggeringEvent" in js
|
||||
assert "rc.triggeringEvent) return" in js, \
|
||||
"request-urile cu triggeringEvent (trimiteriChanged/filtru) trebuie sa treaca mereu"
|
||||
# Resume explicit reutilizeaza acelasi canal `trimiteriChanged` (pastreaza filtrul).
|
||||
assert "trimiteriChanged" in js
|
||||
wrap = html[html.find('id="submissions-wrap"'):]
|
||||
wrap = wrap[:wrap.find(">") + 1]
|
||||
assert "trimiteriChanged from:body" in wrap, "tabelul trebuie sa reincarce pe trimiteriChanged"
|
||||
assert 'hx-include="#filtre-trimiteri"' in wrap, "reincarcarea trebuie sa pastreze filtrul"
|
||||
|
||||
@@ -84,25 +84,24 @@ def test_pill_per_categorie_cu_numar(client):
|
||||
_ins(acct, status="sent", vin="WVIN_SE1_001", nr="BSE1")
|
||||
_login(client, "pill1@test.com")
|
||||
|
||||
resp = client.get("/_fragments/status")
|
||||
# Pill-urile traiesc in bara de filtre din sectiunea Trimiteri.
|
||||
resp = client.get("/?tab=acasa")
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
|
||||
# Pill-urile sunt elemente <button> (nu <span onclick>)
|
||||
# Pill-urile sunt elemente <button>
|
||||
assert "<button" in body, "Pill-urile trebuie sa fie elemente <button>"
|
||||
|
||||
# Fiecare categorie problemativa apare ca pill
|
||||
assert "needs_data" in body, "Pill needs_data trebuie sa apara"
|
||||
assert "needs_mapping" in body, "Pill needs_mapping trebuie sa apara"
|
||||
assert "error" in body, "Pill error trebuie sa apara (hx-get sau text)"
|
||||
assert 'data-status="needs_data"' in body, "Pill needs_data trebuie sa apara"
|
||||
assert 'data-status="needs_mapping"' in body, "Pill needs_mapping trebuie sa apara"
|
||||
assert 'data-status="error"' in body, "Pill error trebuie sa apara"
|
||||
|
||||
# Contoarele sunt afisate in pill-uri
|
||||
assert ">2<" in body or "2<" in body, "Contorul 2 pt needs_data trebuie vizibil in pill"
|
||||
|
||||
# Starea 'sent' nu produce pill (nu e categorie de problema)
|
||||
# (nu exista un pill cu status=sent in bara de status)
|
||||
pill_sent_count = body.count("status=sent")
|
||||
assert pill_sent_count == 0, "Nu trebuie pill pentru sent in bara de status"
|
||||
assert 'data-status="sent"' not in body, "Nu trebuie pill pentru sent"
|
||||
|
||||
|
||||
def test_pill_click_seteaza_status(client):
|
||||
@@ -113,21 +112,21 @@ def test_pill_click_seteaza_status(client):
|
||||
_ins(acct, status="error", vin="WVIN_ER2_001", nr="BER_P2a")
|
||||
_login(client, "pill2@test.com")
|
||||
|
||||
resp = client.get("/_fragments/status")
|
||||
resp = client.get("/?tab=acasa")
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
|
||||
# Fiecare pill are atribut hx-get cu parametrul status corespunzator
|
||||
assert "status=needs_data" in body, "Pill needs_data trebuie sa aiba ?status=needs_data in hx-get"
|
||||
assert "status=needs_mapping" in body, "Pill needs_mapping trebuie sa aiba ?status=needs_mapping in hx-get"
|
||||
assert "status=error" in body, "Pill error trebuie sa aiba ?status=error in hx-get"
|
||||
# Fiecare pill scrie campul de filtru prin filtreazaStare(this, 'X')
|
||||
assert "filtreazaStare(this, 'needs_data')" in body, "Pill needs_data trebuie sa apeleze filtreazaStare cu needs_data"
|
||||
assert "filtreazaStare(this, 'needs_mapping')" in body, "Pill needs_mapping trebuie sa apeleze filtreazaStare cu needs_mapping"
|
||||
assert "filtreazaStare(this, 'error')" in body, "Pill error trebuie sa apeleze filtreazaStare cu error"
|
||||
|
||||
# Pill-urile au aria-pressed pentru accesibilitate (WCAG)
|
||||
assert "aria-pressed" in body, "Pill-urile trebuie sa aiba atribut aria-pressed"
|
||||
|
||||
# Target-ul este tabelul de trimiteri
|
||||
assert "submissions-wrap" in body or "_fragments/submissions" in body, (
|
||||
"Pill-urile trebuie sa targeteze #submissions-wrap sau sa apeleze /_fragments/submissions"
|
||||
# Filtrarea trece prin form-ul care targeteaza tabelul de trimiteri
|
||||
assert "submissions-wrap" in body and "_fragments/submissions" in body, (
|
||||
"Form-ul de filtre trebuie sa targeteze #submissions-wrap prin /_fragments/submissions"
|
||||
)
|
||||
|
||||
|
||||
@@ -161,7 +160,7 @@ def test_pill_needs_mapping_culoare_warn(client):
|
||||
_ins(acct, status="needs_mapping", vin="WVIN_NM_WARN0001", nr="BNMW1")
|
||||
_login(client, "warn@test.com")
|
||||
|
||||
resp = client.get("/_fragments/status")
|
||||
resp = client.get("/?tab=acasa")
|
||||
assert resp.status_code == 200
|
||||
body = resp.text
|
||||
|
||||
@@ -169,4 +168,4 @@ def test_pill_needs_mapping_culoare_warn(client):
|
||||
assert "var(--warn)" in body, (
|
||||
"Pill needs_mapping trebuie sa foloseasca var(--warn) conform DESIGN.md §Componente"
|
||||
)
|
||||
assert "status=needs_mapping" in body, "Pill needs_mapping trebuie sa fie prezent in bara"
|
||||
assert 'data-status="needs_mapping"' in body, "Pill needs_mapping trebuie sa fie prezent in bara de filtre"
|
||||
|
||||
@@ -133,26 +133,16 @@ def test_status_blocate_defalcare(client):
|
||||
_insert_submission("needs_data", acct_id)
|
||||
_insert_submission("error", acct_id)
|
||||
|
||||
resp = client.get("/_fragments/status")
|
||||
# Pill-urile s-au mutat in bara de filtre din sectiunea Trimiteri (nu in bara de status).
|
||||
resp = client.get("/?tab=acasa")
|
||||
assert resp.status_code == 200
|
||||
|
||||
html = resp.text
|
||||
# US-003 (PRD 5.10): Blocatele apar ca pill-uri (nu ca lista cu ID-uri)
|
||||
assert "Necesita atentie" in html, (
|
||||
f"Fragmentul nu contine sectiunea 'Necesita atentie'. HTML: {html[:800]}"
|
||||
)
|
||||
# Pill-urile au etichetele scurte per categorie (nu etichetele lungi din eticheta_stare)
|
||||
assert "Lipsa cod" in html, (
|
||||
"Fragmentul nu arata pill-ul pentru needs_mapping"
|
||||
)
|
||||
assert "Date incomplete" in html, (
|
||||
"Fragmentul nu arata pill-ul pentru needs_data"
|
||||
)
|
||||
assert "Eroare" in html, (
|
||||
"Fragmentul nu arata pill-ul pentru error"
|
||||
)
|
||||
assert "Lipsa cod" in html, "Acasa nu arata pill-ul pentru needs_mapping"
|
||||
assert "Date incomplete" in html, "Acasa nu arata pill-ul pentru needs_data"
|
||||
assert "Eroare" in html, "Acasa nu arata pill-ul pentru error"
|
||||
# Pill-urile arata numarul total per categorie (2 needs_mapping, 1 needs_data, 1 error)
|
||||
assert "2" in html, "Pill-ul needs_mapping trebuie sa arate numarul 2"
|
||||
assert 'class="pill-cat"' in html, "Pill-urile trebuie sa fie elemente cu clasa pill-cat"
|
||||
assert "<button" in html, "Pill-urile trebuie sa fie elemente <button>"
|
||||
|
||||
|
||||
@@ -207,16 +197,15 @@ def _insert_submission_vehicul(status, account_id, vin, nr):
|
||||
|
||||
|
||||
def test_categorie_blocata_linkeaza_la_trimiteri_filtrate(client):
|
||||
"""US-003 (PRD 5.10): pill-ul error are hx-get cu ?status=error.
|
||||
Deep-link-ul tab=acasa&status=error a fost eliminat (pill inlocuieste link-ul vechi)."""
|
||||
"""Pill-ul error filtreaza tabelul prin filtreazaStare(this, 'error') in bara de filtre."""
|
||||
acct_id, _ = _create_account_user("link@test.com", "parolasecreta10")
|
||||
_login(client, "link@test.com", "parolasecreta10")
|
||||
_insert_submission("error", acct_id)
|
||||
|
||||
html = client.get("/_fragments/status").text
|
||||
# Pill-ul are hx-get cu status=error (filtrare directa submissions)
|
||||
assert "/_fragments/submissions?status=error" in html
|
||||
# Deep-link-ul tab=acasa&status=error nu mai exista — pill-uri inlocuiesc link-urile
|
||||
html = client.get("/?tab=acasa").text
|
||||
# Pill-ul scrie campul de filtru si re-trimite form-ul (nu mai navigheaza prin deep-link)
|
||||
assert "filtreazaStare(this, 'error')" in html
|
||||
assert 'data-status="error"' in html
|
||||
assert "tab=acasa&status=error" not in html
|
||||
|
||||
|
||||
@@ -228,12 +217,10 @@ def test_status_nu_arata_identificator_rand_blocat(client):
|
||||
_insert_submission_vehicul("error", acct_id, "WVWZZZ1KZAW000123", "B123ABC")
|
||||
|
||||
html = client.get("/_fragments/status").text
|
||||
# Bara de status arata pill cu count, nu lista cu VIN/nr per rand
|
||||
# Bara de status arata doar contoare, nu lista cu VIN/nr per rand (fara PII nominal)
|
||||
assert "B123ABC" not in html, "Nr inmatriculare nu trebuie sa mai apara in bara de status"
|
||||
assert "WVWZZZ1KZAW000123" not in html, "VIN integral nu trebuie expus"
|
||||
assert "0123" not in html, "VIN partial nu trebuie sa mai apara in bara de status"
|
||||
# Pill-ul cu count 1 apare in locul listei
|
||||
assert "status=error" in html, "Pill error trebuie sa aiba hx-get cu status=error"
|
||||
|
||||
|
||||
def test_scoped_pe_cont(client):
|
||||
|
||||
Reference in New Issue
Block a user