feat(web): UI compact Trimiteri + bara bulk contextuala + fix buton "+" editor (PRD 5.22)

Trimiteri desktop = tabel grid cu header, un rand per trimitere: status =
bulina cu tooltip, mediu = litera T/P, multi-cod = primul cod + badge +N,
sub-linie de eroare cu textul brut RAR doar pe error/needs_data. Mobil:
2 linii prin grid-areas, fara header. Actiunile bulk apar doar la selectie
(bara contextuala cu contor + texte explicite). Mapari: exemplu concret in
loc de descriere la Reguli automate.

Fix buton "+" chips (nu facea nimic, fara eroare): hx-disabled-elt="find
button" mostenit de la form-ul parinte crapa htmx inainte de request ->
hx-disabled-elt="this" explicit pe butoanele chips; separat, no-op-urile
silentioase din post_form_chips au acum mesaje (cod invalid/duplicat) si
confirmare cu chip evidentiat la succes.

Suita: 1622 passed. Verify E2E in browser (desktop 1280 + mobil 390).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-07 12:27:42 +00:00
parent 6674d0e3e1
commit 5a2c976d46
17 changed files with 912 additions and 169 deletions

View File

@@ -59,6 +59,19 @@ def _vin_scurt(vin: str) -> str:
return "" + vin[-6:] return "" + vin[-6:]
def _coduri_rar(prestatii: Any) -> list[str]:
"""Codurile RAR (cod_prestatie) din toate prestatiile: ordonate, dedup."""
coduri: list[str] = []
if isinstance(prestatii, list):
for item in prestatii:
if not isinstance(item, dict):
continue
cod = _clean_cod_rar(item.get("cod_prestatie"))
if cod and cod not in coduri:
coduri.append(cod)
return coduri
def _prima_prestatie(prestatii: Any) -> dict[str, Any]: def _prima_prestatie(prestatii: Any) -> dict[str, Any]:
"""Primul item de prestatie ca dict, defensiv (lista goala/non-dict -> {}).""" """Primul item de prestatie ca dict, defensiv (lista goala/non-dict -> {})."""
if isinstance(prestatii, list): if isinstance(prestatii, list):
@@ -83,10 +96,11 @@ def _payload_dict(payload: str | dict | None) -> dict[str, Any]:
return {} return {}
def prezentare_din_payload(payload: str | dict | None) -> dict[str, str]: def prezentare_din_payload(payload: str | dict | None) -> dict[str, Any]:
"""Campuri afisabile dintr-un payload de submission. """Campuri afisabile dintr-un payload de submission.
Intoarce un dict cu chei stabile (toate string-uri, EMPTY cand lipsesc): Intoarce un dict cu chei stabile (string-uri, EMPTY cand lipsesc; exceptie
`coduri_rar` = list[str]):
vehicul_nr, vin, vin_scurt, operatie, data_prestatie, odometru, cod. vehicul_nr, vin, vin_scurt, operatie, data_prestatie, odometru, cod.
`operatie` = denumire prestatiei daca exista, altfel codul; `cod` = codul RAR `operatie` = denumire prestatiei daca exista, altfel codul; `cod` = codul RAR
@@ -138,6 +152,9 @@ def prezentare_din_payload(payload: str | dict | None) -> dict[str, str]:
"odometru": odo or EMPTY, "odometru": odo or EMPTY,
"cod": cod or EMPTY, "cod": cod or EMPTY,
"cod_rar": cod_rar or EMPTY, "cod_rar": cod_rar or EMPTY,
# Lista ordonata, fara duplicate, a codurilor RAR din TOATE prestatiile
# (goala cand nimic nu e mapat). Randul compact afiseaza primul + "+N".
"coduri_rar": _coduri_rar(data.get("prestatii")),
# Chei cu conventie goala "" (nu EMPTY) — vezi comentariu de mai sus # Chei cu conventie goala "" (nu EMPTY) — vezi comentariu de mai sus
"op_service_cod": op_service_cod, "op_service_cod": op_service_cod,
"op_service_denumire": op_service_denumire, "op_service_denumire": op_service_denumire,

View File

@@ -359,6 +359,31 @@ def parse_erori(rar_error: object) -> list[dict]:
return [{"problema": str(data)[:200], "cauza": "", "fix": "", "field": None}] return [{"problema": str(data)[:200], "cauza": "", "fix": "", "field": None}]
# ---------------------------------------------------------------------------
# eroare_bruta — textul compact al erorii pentru sub-linia randului de tabel
# ---------------------------------------------------------------------------
# Stari cu sub-linie de eroare pe rand. needs_mapping e EXCLUS intentionat:
# "nemapat" e deja vizibil in coloana Cod RAR, sub-linia ar fi redundanta.
_STARI_CU_EROARE_BRUTA = ("error", "needs_data")
def eroare_bruta(status: str, rar_error: object) -> str:
"""Textul brut, compact, al primei erori — pentru sub-linia randului.
Prefera cauza tehnica (mesajul serverului RAR, ex. ORA-12899) si cade pe
problema. Sir gol in afara starilor error/needs_data. Nu arunca niciodata
(reuse parse_erori, degradare gratioasa).
"""
if status not in _STARI_CU_EROARE_BRUTA:
return ""
erori = parse_erori(rar_error)
if not erori:
return ""
e = erori[0]
return (str(e.get("cauza") or "").strip() or str(e.get("problema") or "").strip())[:200]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Etichete stari preview import (vocabular DIFERIT de starile de submission) # Etichete stari preview import (vocabular DIFERIT de starile de submission)
# #

View File

@@ -32,6 +32,7 @@ from ..web.csrf import get_csrf_token, verify_csrf
from .labels import ( from .labels import (
ETICHETA_ULTIMA_AUTENTIFICARE_RAR, ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
STARI_PREVIEW, STARI_PREVIEW,
eroare_bruta,
eticheta_env, eticheta_env,
eticheta_rar, eticheta_rar,
eticheta_scurta, eticheta_scurta,
@@ -1131,6 +1132,8 @@ def _submission_row_view(r) -> dict:
"motiv": motiv, "motiv": motiv,
# eticheta umana scurta a problemei sub pill (text, nu cod brut). # eticheta umana scurta a problemei sub pill (text, nu cod brut).
"eticheta_problema": _eticheta_problema(r["status"], motiv), "eticheta_problema": _eticheta_problema(r["status"], motiv),
# textul brut compact al erorii (mesaj RAR/validare) — sub-linia randului.
"eroare_bruta": eroare_bruta(r["status"], r["rar_error"]),
# randurile blocate (error/needs_data/needs_mapping) sunt selectabile pentru # randurile blocate (error/needs_data/needs_mapping) sunt selectabile pentru
# stergere bulk; sent/sending/queued raman read-only (fara checkbox). # stergere bulk; sent/sending/queued raman read-only (fara checkbox).
"gestionabil": r["status"] in _GESTIONABILE_WEB, "gestionabil": r["status"] in _GESTIONABILE_WEB,
@@ -2397,9 +2400,19 @@ async def post_form_chips(request: Request) -> HTMLResponse:
}) })
action = str(form.get("chips_action") or "").strip() action = str(form.get("chips_action") or "").strip()
chips_extra_error = False # T-C1/T-E4 (5.16): semnal pentru add_extra esuat # Orice adaugare are un efect vizibil: mesaj de refuz (chips_error) sau
# confirmare + chip evidentiat (chips_ok / chips_added_cod). Fara no-op silentios.
chips_error = ""
chips_ok = ""
chips_added_cod = ""
conn = get_connection() conn = get_connection()
def _cod_valid(cod: str) -> bool:
return conn.execute(
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod,)
).fetchone() is not None
try: try:
if action == "add": if action == "add":
# Adauga cod la operatia specificata prin chips_add_op_index # Adauga cod la operatia specificata prin chips_add_op_index
@@ -2408,44 +2421,37 @@ async def post_form_chips(request: Request) -> HTMLResponse:
except (ValueError, TypeError): except (ValueError, TypeError):
op_idx = 0 op_idx = 0
add_cod = str(form.get(f"chips_add_cod_{op_idx}") or "").strip().upper() add_cod = str(form.get(f"chips_add_cod_{op_idx}") or "").strip().upper()
if add_cod and 0 <= op_idx < len(chips): if not add_cod:
exists = conn.execute( chips_error = "Selecteaza un cod RAR din lista inainte de a adauga."
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (add_cod,) elif not (0 <= op_idx < len(chips)):
).fetchone() chips_error = "Operatia nu mai exista in formular — reincarca editarea."
if exists: elif not _cod_valid(add_cod):
chips[op_idx]["cod_prestatie"] = add_cod chips_error = f"Cod necunoscut in nomenclator: {add_cod}."
elif action == "add_flat":
# Adauga cod nou in lista plata (fara op_service)
add_cod_flat = str(form.get("chips_add_cod_flat") or "").strip().upper()
if add_cod_flat:
exists = conn.execute(
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (add_cod_flat,)
).fetchone()
if exists:
chips.append({"cod_prestatie": add_cod_flat, "cod_op_service": "", "denumire": ""})
elif action == "add_extra":
# Adauga cod RAR liber (extra, fara op_service) in modul operatii.
# Refoloseste `chips_add_cod_flat` (acelasi select; dedup per-item pastrat).
# Select gol sau cod invalid → chips_extra_error = True (semnal vizibil).
add_cod_extra = str(form.get("chips_add_cod_flat") or "").strip().upper()
if add_cod_extra:
exists = conn.execute(
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (add_cod_extra,)
).fetchone()
if exists:
# Dedup per-item (E4): nu adauga un chip extra identic deja existent
existing_pairs = {
(c.get("cod_op_service", ""), c.get("cod_prestatie", ""))
for c in chips
}
if ("", add_cod_extra) not in existing_pairs:
chips.append({"cod_prestatie": add_cod_extra, "cod_op_service": "", "denumire": ""})
else:
chips_extra_error = True # cod necunoscut in nomenclator
else: else:
chips_extra_error = True # select gol chips[op_idx]["cod_prestatie"] = add_cod
chips_ok = f"{add_cod} adaugat."
chips_added_cod = add_cod
elif action in ("add_flat", "add_extra"):
# Adauga cod RAR liber (fara op_service): add_flat = mod plat,
# add_extra = mod operatii. Acelasi select (chips_add_cod_flat),
# aceeasi validare si acelasi dedup per-item (E4).
add_cod_flat = str(form.get("chips_add_cod_flat") or "").strip().upper()
if not add_cod_flat:
chips_error = "Selecteaza un cod RAR din lista inainte de a adauga."
elif not _cod_valid(add_cod_flat):
chips_error = f"Cod necunoscut in nomenclator: {add_cod_flat}."
else:
existing_pairs = {
(c.get("cod_op_service", ""), c.get("cod_prestatie", ""))
for c in chips
}
if ("", add_cod_flat) in existing_pairs:
chips_error = f"{add_cod_flat} este deja in lista — nu a fost adaugat inca o data."
else:
chips.append({"cod_prestatie": add_cod_flat, "cod_op_service": "", "denumire": ""})
chips_ok = f"{add_cod_flat} adaugat."
chips_added_cod = add_cod_flat
elif action == "remove": elif action == "remove":
# Sterge codul de la indexul dat (lasa op_service intact -> operatie ramane nemapata) # Sterge codul de la indexul dat (lasa op_service intact -> operatie ramane nemapata)
@@ -2480,7 +2486,9 @@ async def post_form_chips(request: Request) -> HTMLResponse:
"has_r_odo": has_r_odo, "has_r_odo": has_r_odo,
"form_chips_url": "/form-chips", "form_chips_url": "/form-chips",
"chips_section_id": "chips-section", "chips_section_id": "chips-section",
"chips_extra_error": chips_extra_error, "chips_error": chips_error,
"chips_ok": chips_ok,
"chips_added_cod": chips_added_cod,
}) })

View File

@@ -46,6 +46,7 @@
{% if chip.cod_op_service %} {% if chip.cod_op_service %}
{% set _is_warn = chip.cod_prestatie in ('R-ODO', 'I-ODO') %} {% set _is_warn = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
{% set _nemapat = not chip.cod_prestatie %} {% set _nemapat = not chip.cod_prestatie %}
{% set _e_nou = (chips_added_cod is defined) and chips_added_cod and chip.cod_prestatie == chips_added_cod %}
<div class="op-row {% if _nemapat %}op-row-warn{% endif %}" style="margin-bottom:6px;"> <div class="op-row {% if _nemapat %}op-row-warn{% endif %}" style="margin-bottom:6px;">
<span class="op-row-name"> <span class="op-row-name">
{{ chip.cod_op_service }} {{ chip.cod_op_service }}
@@ -59,11 +60,12 @@
<span style="display:flex;align-items:center;gap:8px;"> <span style="display:flex;align-items:center;gap:8px;">
{% if chip.cod_prestatie %} {% if chip.cod_prestatie %}
{# ===== Operatie mapata: chip cu × ===== #} {# ===== Operatie mapata: chip cu × ===== #}
<span class="chip {% if _is_warn %}chip-warn{% endif %}" <span class="chip {% if _is_warn %}chip-warn{% endif %} {% if _e_nou %}chip-nou{% endif %}"
aria-label="Prestatie {{ chip.cod_prestatie }} adaugata pentru {{ chip.cod_op_service }}"> aria-label="Prestatie {{ chip.cod_prestatie }} adaugata pentru {{ chip.cod_op_service }}">
{{ chip.cod_prestatie }} {{ chip.cod_prestatie }}
<button type="button" class="chip-del" <button type="button" class="chip-del"
hx-post="{{ _chips_url }}" hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form" hx-include="closest form"
hx-target="#{{ _sec_id }}" hx-target="#{{ _sec_id }}"
hx-swap="outerHTML" hx-swap="outerHTML"
@@ -81,6 +83,7 @@
<button type="button" <button type="button"
style="font-size:10px;color:var(--muted);background:none;border:none;cursor:pointer;text-decoration:underline;padding:0;margin-left:4px;line-height:1;" style="font-size:10px;color:var(--muted);background:none;border:none;cursor:pointer;text-decoration:underline;padding:0;margin-left:4px;line-height:1;"
hx-post="/trimitere/{{ _sub_id }}/salveaza-regula-chip" hx-post="/trimitere/{{ _sub_id }}/salveaza-regula-chip"
hx-disabled-elt="this"
hx-include="closest form" hx-include="closest form"
hx-target="#detaliu-modal-body" hx-target="#detaliu-modal-body"
hx-swap="innerHTML" hx-swap="innerHTML"
@@ -104,6 +107,7 @@
<button type="button" <button type="button"
class="add-code" class="add-code"
hx-post="{{ _chips_url }}" hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form" hx-include="closest form"
hx-target="#{{ _sec_id }}" hx-target="#{{ _sec_id }}"
hx-swap="outerHTML" hx-swap="outerHTML"
@@ -126,11 +130,13 @@
<div class="chips" role="group" aria-label="Coduri RAR suplimentare" style="margin-top:4px;"> <div class="chips" role="group" aria-label="Coduri RAR suplimentare" style="margin-top:4px;">
{% for chip in _extra_chips %} {% for chip in _extra_chips %}
{% set _is_warn_extra = chip.cod_prestatie in ('R-ODO', 'I-ODO') %} {% set _is_warn_extra = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
<span class="chip {% if _is_warn_extra %}chip-warn{% endif %}" {% set _e_nou_extra = (chips_added_cod is defined) and chips_added_cod and chip.cod_prestatie == chips_added_cod %}
<span class="chip {% if _is_warn_extra %}chip-warn{% endif %} {% if _e_nou_extra %}chip-nou{% endif %}"
aria-label="Cod RAR suplimentar {{ chip.cod_prestatie }}"> aria-label="Cod RAR suplimentar {{ chip.cod_prestatie }}">
{{ chip.cod_prestatie }} {{ chip.cod_prestatie }}
<button type="button" class="chip-del" <button type="button" class="chip-del"
hx-post="{{ _chips_url }}" hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form" hx-include="closest form"
hx-target="#{{ _sec_id }}" hx-target="#{{ _sec_id }}"
hx-swap="outerHTML" hx-swap="outerHTML"
@@ -153,6 +159,7 @@
<button type="button" <button type="button"
class="add-code" class="add-code"
hx-post="{{ _chips_url }}" hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form" hx-include="closest form"
hx-target="#{{ _sec_id }}" hx-target="#{{ _sec_id }}"
hx-swap="outerHTML" hx-swap="outerHTML"
@@ -167,24 +174,19 @@
Nomenclator indisponibil — adaugarea de coduri suplimentare nu e posibila. Nomenclator indisponibil — adaugarea de coduri suplimentare nu e posibila.
</div> </div>
{% endif %} {% endif %}
{% if chips_extra_error %}
{# T-C1/T-E4 (5.16): semnal vizibil cand add_extra are select gol sau cod invalid #}
<div class="chips-extra-error" style="font-size:11px;color:var(--err);padding:2px 0;" role="alert">
Selecteaza un cod RAR din lista inainte de a adauga.
</div>
{% endif %}
{% else %} {% else %}
{# ===== Mod plat: lista de coduri libere (corectie pura, fara op_service) ===== #} {# ===== Mod plat: lista de coduri libere (corectie pura, fara op_service) ===== #}
<div class="chips" role="group" aria-label="Coduri RAR selectate"> <div class="chips" role="group" aria-label="Coduri RAR selectate">
{% for chip in _chips %} {% for chip in _chips %}
{% if chip.cod_prestatie %} {% if chip.cod_prestatie %}
{% set _is_warn_flat = chip.cod_prestatie in ('R-ODO', 'I-ODO') %} {% set _is_warn_flat = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
<span class="chip {% if _is_warn_flat %}chip-warn{% endif %}" {% set _e_nou_flat = (chips_added_cod is defined) and chips_added_cod and chip.cod_prestatie == chips_added_cod %}
<span class="chip {% if _is_warn_flat %}chip-warn{% endif %} {% if _e_nou_flat %}chip-nou{% endif %}"
aria-label="Prestatie {{ chip.cod_prestatie }}"> aria-label="Prestatie {{ chip.cod_prestatie }}">
{{ chip.cod_prestatie }} {{ chip.cod_prestatie }}
<button type="button" class="chip-del" <button type="button" class="chip-del"
hx-post="{{ _chips_url }}" hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form" hx-include="closest form"
hx-target="#{{ _sec_id }}" hx-target="#{{ _sec_id }}"
hx-swap="outerHTML" hx-swap="outerHTML"
@@ -207,6 +209,7 @@
<button type="button" <button type="button"
class="add-code" class="add-code"
hx-post="{{ _chips_url }}" hx-post="{{ _chips_url }}"
hx-disabled-elt="this"
hx-include="closest form" hx-include="closest form"
hx-target="#{{ _sec_id }}" hx-target="#{{ _sec_id }}"
hx-swap="outerHTML" hx-swap="outerHTML"
@@ -224,6 +227,18 @@
</div> </div>
{% endif %} {% endif %}
{# Rezultatul ultimei adaugari, in ambele moduri: refuz (rol=alert) sau confirmare.
Clasa chips-extra-error pastrata pe eroare (selector stabil pentru teste/QA). #}
{% if chips_error is defined and chips_error %}
<div class="chips-extra-error" style="font-size:11px;color:var(--err);padding:2px 0;" role="alert">
{{ chips_error }}
</div>
{% elif chips_ok is defined and chips_ok %}
<div class="chips-adaugat-ok" style="font-size:11px;color:var(--ok);padding:2px 0;" role="status">
{{ chips_ok }}
</div>
{% endif %}
{# Hint discret fara chips (debut) #} {# Hint discret fara chips (debut) #}
{% if not _chips %} {% if not _chips %}
<div style="font-size:10px;color:var(--muted);padding:4px 0;"> <div style="font-size:10px;color:var(--muted);padding:4px 0;">

View File

@@ -178,18 +178,19 @@
<!-- ============================================================ --> <!-- ============================================================ -->
<div class="card"> <div class="card">
<h2 style="font-size:15px; margin:0 0 8px;">Reguli automate (text)</h2> <h2 style="font-size:15px; margin:0 0 8px;">Reguli automate (text)</h2>
<p class="muted" style="margin:0 0 12px; font-size:13px; max-width:680px;"> {# Un exemplu concret in loc de descriere — arata direct formatul unei reguli. #}
O regula leaga orice operatie al carei text <strong>contine</strong> (nu egal, ci substring) <div class="regula-exemplu"
un cuvant de un cod RAR. Util pentru operatii fara cod intern: ex. orice operatie care style="margin:0 0 12px; font-size:13.5px; max-width:680px; padding:10px 12px;
<em>contine</em> „verificare" primeste codul ales. Match insensibil la majuscule/diacritice. border:1px dashed var(--line); border-radius:8px; background:var(--card2);">
</p> Daca operatia contine
<span style="font-family:var(--font-mono); font-size:12.5px; padding:1px 6px; border-radius:4px;
{% if not text_rules %} background:color-mix(in srgb, var(--warn) 18%, transparent); color:var(--warn);">verificare</span>
<div class="empty" style="margin-bottom:12px;"> <span class="muted" aria-hidden="true">&rarr;</span>
Inca nu ai reguli. Ex: operatia contine «verificare» &rarr; OE-2. Cod RAR =
Mapeaza automat operatii similare fara cod intern. Adauga prima regula mai jos. <span style="font-family:var(--font-mono); font-size:12.5px; font-weight:700; padding:1px 6px; border-radius:4px;
background:color-mix(in srgb, var(--accent) 16%, transparent); color:var(--accent);">OE-2</span>
Intretinere si revizie
</div> </div>
{% endif %}
<div class="tablewrap tabel-card"> <div class="tablewrap tabel-card">
<table> <table>

View File

@@ -60,45 +60,70 @@
hx-disinherit="hx-confirm" hx-disinherit="hx-confirm"
style="margin:0;"> style="margin:0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div style="display:flex; justify-content:flex-end; align-items:center;
gap:6px; margin-bottom:8px; flex-wrap:wrap;"> {# Bara contextuala bulk (5.22): randata inactiva (ascunsa prin CSS), afisata de
{# Bulk-fix: input cod + buton aplica #} JS (base.html) cand >= 1 checkbox e bifat. Contoarele .bulk-n / .bulk-n-txt
sunt actualizate client-side. Endpoint-urile si CSRF raman neschimbate;
"Sterge" pastreaza hx-confirm-ul form-ului, "Aplica" e exclus (hx-disinherit). #}
<div id="bulk-bar" class="bulk-bar" role="toolbar" aria-label="Actiuni pe randurile selectate">
<span style="font-weight:700;"><span class="bulk-n">0</span> selectate</span>
<span style="color:var(--muted);" aria-hidden="true">·</span>
<input type="text" name="cod_prestatie" id="bulk-fix-cod" <input type="text" name="cod_prestatie" id="bulk-fix-cod"
placeholder="Cod RAR (ex: OE-1)" placeholder="Cod RAR (ex: OE-1)"
autocomplete="off" autocapitalize="characters" autocomplete="off" autocapitalize="characters"
style="width:120px; font-size:12px; padding:3px 7px; style="width:120px; font-size:12px; padding:3px 7px;
border:1px solid var(--line); border-radius:5px; border:1px solid var(--line); border-radius:5px;
background:var(--card2); color:var(--ink);" background:var(--card2); color:var(--ink); font-family:var(--font-mono);"
aria-label="Cod RAR de aplicat la randurile selectate"> aria-label="Cod RAR de aplicat la randurile selectate">
<button type="button" <button type="button"
hx-post="/trimiteri/bulk-fix" hx-post="/trimiteri/bulk-fix"
hx-target="#submissions-wrap" hx-target="#submissions-wrap"
hx-swap="innerHTML" hx-swap="innerHTML"
style="background:var(--card); color:var(--accent); border-color:var(--accent); style="background:var(--card); color:var(--accent); border:1px solid var(--accent);
font-size:13px; padding:4px 10px; border-radius:5px; cursor:pointer;" font-size:12.5px; padding:4px 10px; border-radius:5px; cursor:pointer;"
aria-label="Aplica codul RAR la randurile blocate selectate"> aria-label="Aplica codul RAR la randurile blocate selectate">
Aplica cod Aplica acest cod la <span class="bulk-n">0</span> <span class="bulk-n-txt">randuri</span>
</button> </button>
{# Separator vizual #}
<span style="color:var(--muted); font-size:11px; padding:0 2px;" aria-hidden="true">|</span> <span style="color:var(--muted); font-size:11px; padding:0 2px;" aria-hidden="true">|</span>
{# Bulk-delete: pastreaza exact comportamentul existent #}
<button type="submit" id="bulk-sterge-btn" <button type="submit" id="bulk-sterge-btn"
style="background:var(--card); color:var(--err); border-color:var(--err); style="background:var(--card); color:var(--err); border:1px solid var(--err);
font-size:13px; padding:4px 10px; border-radius:5px; cursor:pointer;"> font-size:12.5px; padding:4px 10px; border-radius:5px; cursor:pointer;">
Sterge selectate Sterge <span class="bulk-n">0</span> <span class="bulk-n-txt">randuri</span>
</button>
<button type="button" id="bulk-deselect"
style="margin-left:auto; background:none; border:none; color:var(--muted);
cursor:pointer; text-decoration:underline; font-size:12px;">
Deselecteaza
</button> </button>
</div> </div>
<div class="bulk-hint">
Aplica codul RAR doar operatiilor fara cod din randurile selectate, apoi le repune in coada.
</div>
{# Lista slim trimiteri. {# Tabel compact trimiteri (grid CSS, PRD 5.22).
Inlocuieste tabelul cu randuri compacte: VIN mono + operatie·ora + pill. Desktop: un rand per trimitere sub un header de coloane; status = bulina cu
Nr. inmatriculare, data prestatie si nr. prezentare RAR raman accesibile tooltip, mediu = litera T/P, multi-cod = primul cod + badge +N.
pe linia meta discreta (linia 3) si in modalul de detaliu. #} Mobil (max-width:767px): header ascuns, randul revine la 2 linii
(placuta / cod·operatie·data) cu litera + bulina in dreapta. #}
<ul class="lista-trimiteri-slim" role="list" <ul class="lista-trimiteri-slim" role="list"
aria-label="Lista trimiteri"> aria-label="Lista trimiteri">
{# Header de coloane — doar desktop (CSS); numele starii/mediului stau aici,
celulele raman semnale compacte (bulina, litera). #}
<li class="trimiteri-head" aria-hidden="true">
<span></span>
<span>Vehicul</span>
<span>Cod RAR</span>
<span>Operatie</span>
<span>Data</span>
<span class="th-c">Status</span>
<span class="th-c">Mediu</span>
<span></span>
</li>
{% for r in rows %} {% for r in rows %}
{# Randul slim: stanga = VIN mono scurt (L1) + operatie·ora muted (L2) + meta (L3); {# Rand clickabil (role=button, Enter/Space in JS din base.html) — deschide
dreapta = pill de stare. Click deschide modalul global (#detaliu-modal-body). modalul global de detaliu (#detaliu-modal-body). #}
Clickabil/focusabil (role=button); Enter/Space deschid modalul (JS in base.html). #}
<li id="trimitere-row-{{ r.id }}" <li id="trimitere-row-{{ r.id }}"
class="trimitere-slim" class="trimitere-slim"
data-detaliu-id="{{ r.id }}" data-detaliu-id="{{ r.id }}"
@@ -110,66 +135,61 @@
style="cursor:pointer;" style="cursor:pointer;"
title="Click pentru detaliul complet"> title="Click pentru detaliul complet">
{# Zona checkbox — nu declanseaza modalul (stopPropagation). {# Checkbox — nu declanseaza modalul (stopPropagation). Vizibil DOAR pe
Vizibila DOAR pe randurile gestionabile (error/needs_data/needs_mapping). randurile gestionabile; latimea fixa previne reflow. #}
Latimea fixa previne reflow la prezenta/absenta checkbox-ului. #} <div class="c-check" onclick="event.stopPropagation();">
<div style="flex:0 0 22px; display:flex; align-items:center;" onclick="event.stopPropagation();">
{% if r.gestionabil %} {% if r.gestionabil %}
<input type="checkbox" name="submission_id" value="{{ r.id }}" <input type="checkbox" name="submission_id" value="{{ r.id }}"
aria-label="Selecteaza trimiterea #{{ r.id }} pentru stergere"> aria-label="Selecteaza trimiterea #{{ r.id }} pentru stergere">
{% endif %} {% endif %}
</div> </div>
{# Bloc text principal — stanga, ocupa spatiul ramas. Rand de 2 linii (spec 5.16): {# Vehicul: placuta (identificator primar); fallback VIN scurt / 'fara numar'
L1 = placuta (identificator primar); L2 = cod RAR · operatie · data prestatie. #} (niciodata em-dash izolat). #}
<div style="flex:1 1 auto; min-width:0;"> {% if r.prez.vehicul_nr and r.prez.vehicul_nr != '—' %}
<div class="slim-vin">{{ r.prez.vehicul_nr }}</div>
{% elif r.prez.vin_scurt and r.prez.vin_scurt != '—' %}
<div class="slim-vin muted">{{ r.prez.vin_scurt }}</div>
{% else %}
<div class="slim-vin muted">fara numar</div>
{% endif %}
{# Linia 1: nr. inmatriculare (placuta) — identificatorul primar pe care il {# Pe desktop .c-meta e display:contents (copiii devin celule de grid);
scaneaza operatorul. .slim-vin reumplut (acelasi nume de clasa, churn minim). pe mobil e linia 2 (flex) cu separatoare "·" din CSS. #}
Fallback cand placuta lipseste ('—'): VIN scurt, apoi mesaj neutru <div class="c-meta">
(nu randa em-dash izolat ca identificator). #} {% set _cods = r.prez.coduri_rar or [] %}
{% if r.prez.vehicul_nr and r.prez.vehicul_nr != '—' %} <span class="c-cod">
<div class="slim-vin">{{ r.prez.vehicul_nr }}</div> {% if _cods %}
{% elif r.prez.vin_scurt and r.prez.vin_scurt != '—' %} <span class="cod-rar-cod">{{ _cods[0] }}</span>
<div class="slim-vin muted">{{ r.prez.vin_scurt }}</div> {%- if _cods | length > 1 -%}
{% else %} <span class="cod-plus" title="{{ _cods | join(' · ') }}">+{{ _cods | length - 1 }}</span>
<div class="slim-vin muted">fara numar</div> {%- endif %}
{% endif %}
{# Linia 2: cod RAR (sau 'nemapat') · operatie (ink, ellipsis) · data prestatie.
Separatorul "·" e injectat prin CSS intre celule. Operatia primeste ellipsis
ca randul sa NU treaca pe a 3-a linie nici la 390px.
VIN integral, #id_prezentare si secundele traiesc in modalul de detaliu. #}
<div class="slim-meta slim-rand2">
{% if r.prez.cod_rar and r.prez.cod_rar != '—' %}
<span class="cod-rar-cod">{{ r.prez.cod_rar }}</span>
{% else %} {% else %}
<span class="cod-rar-cod cod-rar-sub muted">nemapat</span> <span class="cod-rar-cod cod-rar-sub muted">nemapat</span>
{% endif %} {% endif %}
<span class="slim-op">{{ r.prez.operatie }}</span> </span>
{% if r.prez.data_prestatie and r.prez.data_prestatie != '—' %} <span class="c-op">{{ r.prez.operatie }}</span>
<span class="slim-data muted">{{ r.prez.data_prestatie }}</span> <span class="c-data">{% if r.prez.data_prestatie and r.prez.data_prestatie != '—' %}{{ r.prez.data_prestatie }}{% endif %}</span>
{% endif %}
</div>
{# Micro-linie umana a problemei — text mic s-error, DOAR pe stari de problema
(loud-on-exception D6). Randul normal/finalizat ramane strict 2 linii.
Token tipografic --fs-xs (>=12px, scala 5.16). #}
{% if r.eticheta_problema and r.eticheta_problema != r.stare_scurt and r.eticheta_problema != r.stare_text %}
<div class="eticheta-problema s-error">{{ r.eticheta_problema }}</div>
{% endif %}
</div> </div>
{# Zona dreapta: pill stare + badge mediu RAR + buton Trimite #} {# Status: doar bulina colorata; numele complet in title + aria-label. #}
<div style="flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; gap:3px; white-space:nowrap;"> <span class="c-status">
<span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}">{{ r.stare_scurt }}</span> <span class="dot-stare {{ r.stare_css }}" title="{{ r.stare_text }}"
role="img" aria-label="{{ r.stare_text }}"></span>
</span>
{# Mediu: litera compacta — P fill atentie (productie, risc L.142),
T contur discret (testare). Numele complet in title. #}
<span class="c-env">
{% if r.rar_env %} {% if r.rar_env %}
{% set _eb = eticheta_env(r.rar_env) %} {% set _eb = eticheta_env(r.rar_env) %}
<span class="{{ _eb[1] }}">{{ _eb[0] }}</span> <span class="env-l {{ 'env-l-prod' if 'prod' in _eb[1] else 'env-l-test' }}"
title="{{ _eb[0] }}" role="img" aria-label="Mediu RAR: {{ _eb[0] }}">{{ _eb[0][0] }}</span>
{% endif %} {% endif %}
{# Buton "Trimite" DOAR pe randurile tinute (queued AND held=1). </span>
Afordanta dedicata cu stopPropagation (randul e role=button) — NU declanseaza modalul
de detaliu. Confirmare 1-linie prin hx-confirm (per rand); ireversibilitate in microcopy. #} {# Actiuni: buton "Trimite" DOAR pe randurile tinute (queued AND held=1). #}
<div class="c-act">
{% if r.tinut %} {% if r.tinut %}
<form hx-post="/trimitere/{{ r.id }}/trimite-acum" <form hx-post="/trimitere/{{ r.id }}/trimite-acum"
hx-target="#submissions-wrap" hx-swap="innerHTML" hx-target="#submissions-wrap" hx-swap="innerHTML"
@@ -180,7 +200,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn-trimite-rand" <button type="submit" class="btn-trimite-rand"
onclick="event.stopPropagation();" onclick="event.stopPropagation();"
style="margin-top:2px; padding:2px 10px; font-size:var(--fs-xs); style="padding:2px 10px; font-size:var(--fs-xs);
border:1px solid var(--accent); border-radius:99px; cursor:pointer; border:1px solid var(--accent); border-radius:99px; cursor:pointer;
background:var(--accent); color:#fff; font-weight:600;"> background:var(--accent); color:#fff; font-weight:600;">
Trimite Trimite
@@ -188,6 +208,12 @@
</form> </form>
{% endif %} {% endif %}
</div> </div>
{# Sub-linie de eroare: textul BRUT (mesaj RAR/validare), compact, un rand,
doar pe error/needs_data (eroare_bruta e "" in rest). #}
{% if r.eroare_bruta %}
<div class="c-err" title="{{ r.eroare_bruta }}">{{ r.eroare_bruta }}</div>
{% endif %}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -741,35 +741,68 @@
.compact-nr { font-size:var(--fs-xl); font-weight:700; line-height:1; } .compact-nr { font-size:var(--fs-xl); font-weight:700; line-height:1; }
.compact-lbl { font-size:10px; color:var(--muted); margin-top:3px; white-space:nowrap; } .compact-lbl { font-size:10px; color:var(--muted); margin-top:3px; white-space:nowrap; }
} }
/* .lista-trimiteri-slim + .trimitere-slim — lista compacta cu separator --line2. /* Bara contextuala bulk (5.22): ascunsa fara selectie; .activ (setat de JS
Randul e clickabil (rol button), tinta min-height:44px pe mobil. */ cand >=1 checkbox e bifat) o afiseaza impreuna cu hint-ul de sub ea. */
.bulk-bar { display:none; align-items:center; gap:8px; flex-wrap:wrap; font-size:13px;
background:color-mix(in srgb, var(--accent) 10%, var(--card));
border:1px solid color-mix(in srgb, var(--accent) 40%, transparent);
border-radius:8px; padding:8px 12px; margin-bottom:4px; }
.bulk-bar.activ { display:flex; }
.bulk-hint { display:none; font-size:12px; color:var(--muted); margin:0 0 8px; }
.bulk-bar.activ + .bulk-hint { display:block; }
/* Tabel compact trimiteri (grid CSS, 5.22).
Desktop: header de coloane + un rand per trimitere; sub-linia de eroare
(.c-err) ocupa coloanele 2..-1 pe un al doilea rand de grid.
Mobil (max-width:767px, mai jos): header ascuns, rand pe 2 linii. */
.lista-trimiteri-slim { list-style:none; margin:0; padding:0; } .lista-trimiteri-slim { list-style:none; margin:0; padding:0; }
.trimitere-slim { display:flex; align-items:center; justify-content:space-between; gap:12px; .trimiteri-head, .trimitere-slim {
padding:14px 16px; border-bottom:1px solid var(--line2); min-height:44px; cursor:pointer; } display:grid; align-items:center; column-gap:10px; row-gap:2px;
grid-template-columns:22px minmax(90px,150px) minmax(64px,auto) minmax(0,1fr) auto 46px 44px 84px; }
.trimiteri-head { padding:2px 16px 8px; border-bottom:1px solid var(--line);
font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase;
color:var(--muted); }
.trimiteri-head .th-c { text-align:center; }
.trimitere-slim { padding:9px 16px; border-bottom:1px solid var(--line2);
min-height:44px; cursor:pointer; }
.trimitere-slim:last-child { border-bottom:none; } .trimitere-slim:last-child { border-bottom:none; }
.trimitere-slim:hover { background:color-mix(in srgb, var(--accent) 6%, transparent); } .trimitere-slim:hover { background:color-mix(in srgb, var(--accent) 6%, transparent); }
.trimitere-slim:focus, .trimitere-slim:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; } .trimitere-slim:focus, .trimitere-slim:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.slim-vin { font-family:var(--font-mono); font-size:var(--fs-md); font-weight:600; color:var(--ink); } .trimitere-slim .c-check { display:flex; align-items:center; }
.slim-meta { font-size:var(--fs-sm); color:var(--muted); margin-top:3px; } .slim-vin { font-family:var(--font-mono); font-size:var(--fs-sm); font-weight:600; color:var(--ink);
/* Linia 2 a randului slim (5.16): cod RAR · operatie (ellipsis) · data, pe UN rand. white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
Ellipsis-ul pe operatie garanteaza 2 linii MAX si la 390px. */ /* Pe desktop copiii .c-meta devin celule de grid (cod / operatie / data). */
.slim-rand2 { display:flex; align-items:baseline; gap:6px; min-width:0; } .trimitere-slim .c-meta { display:contents; }
.slim-rand2 .cod-rar-cod { flex:0 0 auto; font-family:var(--font-mono); font-weight:600; .trimitere-slim .c-cod { white-space:nowrap; }
color:var(--accent); } .trimitere-slim .cod-rar-cod { font-family:var(--font-mono); font-weight:600;
.slim-rand2 .cod-rar-cod.muted { color:var(--muted); font-weight:500; } font-size:var(--fs-xs); color:var(--accent); }
.slim-rand2 .slim-op { flex:1 1 auto; min-width:0; white-space:nowrap; overflow:hidden; .trimitere-slim .cod-rar-cod.muted { color:var(--muted); font-weight:500; font-style:italic; }
text-overflow:ellipsis; color:var(--ink); } /* Badge +N: trimiterea are mai multe coduri RAR; toate stau in title. */
.slim-rand2 .slim-data { flex:0 0 auto; } .trimitere-slim .cod-plus { display:inline-block; font-family:var(--font-mono);
.slim-rand2 .slim-op::before, .slim-rand2 .slim-data::before { font-size:10px; font-weight:700; color:var(--muted); border:1px solid var(--line);
content:"·"; color:var(--muted); margin-right:6px; } border-radius:99px; padding:0 5px; margin-left:4px; vertical-align:1px; cursor:default; }
.lista-trimiteri-slim .eticheta-problema { font-size:var(--fs-xs); line-height:1.3; margin-top:2px; } .trimitere-slim .c-op { min-width:0; white-space:nowrap; overflow:hidden;
/* Pill slim (5.16): fill-tint + dot 7px + text colorat per stare (currentColor din .s-*). text-overflow:ellipsis; font-size:var(--fs-sm); color:var(--ink); }
Pastrat pe FIECARE rand inclusiv Finalizat (linistit dar prezent). */ .trimitere-slim .c-data { font-size:var(--fs-xs); color:var(--muted); white-space:nowrap;
.lista-trimiteri-slim .pill { display:inline-flex; align-items:center; gap:5px; font-weight:600; font-variant-numeric:tabular-nums; }
background:color-mix(in srgb, currentColor 14%, transparent); .trimitere-slim .c-status, .trimitere-slim .c-env { justify-self:center; line-height:1;
border-color:color-mix(in srgb, currentColor 35%, transparent); } display:inline-flex; align-items:center; }
.lista-trimiteri-slim .pill::before { content:""; width:7px; height:7px; border-radius:99px; /* Bulina de status: doar cerc colorat (currentColor din .s-*); numele starii
background:currentColor; flex-shrink:0; } traieste in headerul coloanei + title/aria-label. */
.dot-stare { display:inline-block; width:11px; height:11px; border-radius:99px;
background:currentColor; }
/* Litera de mediu: P = fill atentie (declaratie reala, ireversibila — L.142);
T = contur discret (mediu de proba). */
.env-l { display:inline-flex; align-items:center; justify-content:center;
width:20px; height:20px; border-radius:99px; font-size:11px; font-weight:800;
cursor:default; }
.env-l-prod { background:color-mix(in srgb, var(--err) 80%, var(--card)); color:var(--card); }
.env-l-test { color:var(--muted); border:1px solid var(--line); }
.trimitere-slim .c-act { justify-self:end; }
/* Sub-linia de eroare: text brut, mono, un singur rand cu ellipsis (title = complet). */
.trimitere-slim .c-err { grid-column:2 / -1; min-width:0; font-family:var(--font-mono);
font-size:var(--fs-xs); color:var(--err); white-space:nowrap; overflow:hidden;
text-overflow:ellipsis; }
/* .camp-slim — varianta compacta camp formular: label --fs-sm muted deasupra, input --fs-md, fundal --card2. /* .camp-slim — varianta compacta camp formular: label --fs-sm muted deasupra, input --fs-md, fundal --card2.
Mono pentru campuri VIN/odometru/nr: adauga clasa .camp-mono pe input. */ Mono pentru campuri VIN/odometru/nr: adauga clasa .camp-mono pe input. */
.camp-slim { margin-bottom:8px; } .camp-slim { margin-bottom:8px; }
@@ -793,6 +826,8 @@
.chip .chip-del:focus-visible { outline:2px solid var(--accent); outline-offset:1px; } .chip .chip-del:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }
/* Varianta chip warn (ex. R-ODO necesita odometruInitial) */ /* Varianta chip warn (ex. R-ODO necesita odometruInitial) */
.chip-warn { background:color-mix(in srgb, var(--warn) 22%, transparent); color:var(--warn); } .chip-warn { background:color-mix(in srgb, var(--warn) 22%, transparent); color:var(--warn); }
/* Chip abia adaugat via /form-chips — confirmare vizibila a actiunii "+" */
.chip-nou { outline:2px solid color-mix(in srgb, var(--ok) 45%, transparent); outline-offset:1px; }
/* .add-code — buton dashed pentru adaugare cod in chipbox */ /* .add-code — buton dashed pentru adaugare cod in chipbox */
.add-code { display:inline-flex; align-items:center; height:22px; padding:0 7px; background:transparent; .add-code { display:inline-flex; align-items:center; height:22px; padding:0 7px; background:transparent;
border:1px dashed color-mix(in srgb, var(--accent) 55%, var(--line)); border:1px dashed color-mix(in srgb, var(--accent) 55%, var(--line));
@@ -806,9 +841,30 @@
vizuala a randului de mapare op<->cod. */ vizuala a randului de mapare op<->cod. */
.op-row-name { font-size:var(--fs-sm); font-weight:700; color:var(--ink); } .op-row-name { font-size:var(--fs-sm); font-weight:700; color:var(--ink); }
.op-row-warn { border-color:color-mix(in srgb, var(--warn) 45%, var(--line)); } .op-row-warn { border-color:color-mix(in srgb, var(--warn) 45%, var(--line)); }
/* Mobil: tinta touch pentru trimitere-slim (deja garantata prin min-height:44px in regula de baza) */ /* Mobil: fara header, randul revine la 2 linii (placuta / cod·operatie·data),
cu actiune + litera mediu + bulina status in dreapta. Grid-areas peste
aceleasi celule; .c-meta redevine linie flex cu separatoare "·". */
@media (max-width:767px) { @media (max-width:767px) {
.trimitere-slim { padding:12px 14px; } .trimiteri-head { display:none; }
.trimitere-slim { padding:12px 14px; column-gap:8px;
grid-template-columns:22px minmax(0,1fr) auto auto auto;
grid-template-areas:
"check veh act env status"
"check meta act env status"
"err err err err err"; }
.trimitere-slim .c-check { grid-area:check; }
.trimitere-slim .slim-vin { grid-area:veh; font-size:var(--fs-md); }
.trimitere-slim .c-meta { grid-area:meta; display:flex; align-items:baseline;
gap:6px; min-width:0; margin-top:3px; }
.trimitere-slim .c-cod { flex:0 0 auto; }
.trimitere-slim .c-op { flex:1 1 auto; }
.trimitere-slim .c-data { flex:0 0 auto; }
.trimitere-slim .c-op::before, .trimitere-slim .c-data:not(:empty)::before {
content:"·"; color:var(--muted); margin-right:6px; }
.trimitere-slim .c-status { grid-area:status; }
.trimitere-slim .c-env { grid-area:env; }
.trimitere-slim .c-act { grid-area:act; }
.trimitere-slim .c-err { grid-area:err; }
} }
/* === SENTINEL-COMPONENTE-SLIM: sfarsit componente slim === */ /* === SENTINEL-COMPONENTE-SLIM: sfarsit componente slim === */
/* === Fix mobil Mapari === /* === Fix mobil Mapari ===
@@ -1222,6 +1278,34 @@
if (saveBtn) saveBtn.classList.add('dirty'); if (saveBtn) saveBtn.classList.add('dirty');
}); });
</script> </script>
<script>
// Bara contextuala bulk (trimiteri): vizibila DOAR cu selectie.
// Delegare pe document (supravietuieste swap-urilor HTMX); dupa un swap al
// listei checkbox-urile se reseteaza, deci sync() ascunde bara automat.
(function() {
function syncBulkBar() {
var form = document.getElementById('bulk-trimiteri');
var bar = document.getElementById('bulk-bar');
if (!form || !bar) return;
var n = form.querySelectorAll('input[name="submission_id"]:checked').length;
bar.classList.toggle('activ', n > 0);
bar.querySelectorAll('.bulk-n').forEach(function(el) { el.textContent = n; });
bar.querySelectorAll('.bulk-n-txt').forEach(function(el) {
el.textContent = (n === 1 ? 'rand' : 'randuri');
});
}
document.addEventListener('change', function(e) {
if (e.target && e.target.name === 'submission_id') syncBulkBar();
});
document.addEventListener('click', function(e) {
if (!(e.target.closest && e.target.closest('#bulk-deselect'))) return;
document.querySelectorAll('#bulk-trimiteri input[name="submission_id"]:checked')
.forEach(function(cb) { cb.checked = false; });
syncBulkBar();
});
document.body.addEventListener('htmx:afterSwap', syncBulkBar);
})();
</script>
<script> <script>
// Sub-taburi pagina Setari: comutare pur client-side, fara server. // Sub-taburi pagina Setari: comutare pur client-side, fara server.
// Delegare pe document -> supravietuieste swap-urilor HTMX ale #card-cont (dupa // Delegare pe document -> supravietuieste swap-urilor HTMX ale #card-cont (dupa

View File

@@ -48,7 +48,9 @@ Reguli de contract (detalii in `docs/api-rar-contract.md`): `FINALIZATA` e termi
> PRD-uri (`docs/prd/prd-X.Y-*.md`), linkate in coloana Detalii. La fiecare livrabila terminata: > PRD-uri (`docs/prd/prd-X.Y-*.md`), linkate in coloana Detalii. La fiecare livrabila terminata:
> schimba statusul + data + linkul PRD si actualizeaza "Ultima actualizare". > schimba statusul + data + linkul PRD si actualizeaza "Ultima actualizare".
**Ultima actualizare**: 2026-07-06**5.21 CACHE PERSISTENT EMBEDDINGS IN SQLITE — LIVRAT** (PRD: [prd-5.21](prd/prd-5.21-embedding-cache-sqlite.md); executie echipa de agenti Sonnet: cache-core T1+T2, wiring T3+T5, tests T4+T6, reviewer adversarial). Vectorii corpusului k-NN (17.181 exemple SILVER) persista in tabela noua `embedding_cache` (PK `(model, text_hash)`, blob float32 LE 384, `array('f')` end-to-end); la warmup se vectorizeaza DOAR miss-urile (hash sha256 pe `denumire_normalizata` din lista FILTRATA, identica cu ce intra in `index_corpus`), restarturile cu corpus neschimbat = `embed=0`. Modul nou `app/embedding_cache.py` (fara dependinte noi): `sync_corpus_vectors(conn, model, texts, embed_fn)` hash->load->embed(miss)->save; scrieri/stergeri in chunk-uri de 500 cu `BEGIN`/`COMMIT` EXPLICIT (conexiunile sunt autocommit — `conn.commit()` e no-op); validare dimensiune la scriere SI citire (blob corupt = miss re-vectorizat); purjare orfane + modele vechi DOAR dupa indexare reusita; toate erorile SQLite -> `log.warning` + fallback embed complet (degradare gratioasa, nimic propagat). `EmbeddingEngine.index_corpus(vectors=)` valideaza alinierea `len(vectors)==len(items)` + absenta None (mismatch -> fallback embed complet). Secventa protejata de `_embeddings_lock` (mapping.py); pe calea de request (`block=False`) acquire NE-blocant — daca warmup-ul tine lock-ul, return imediat (fix MAJOR din review: altfel primul request in fereastra de cold start ingheta ~1-2 min). Log observabilitate: `embeddings: warmup ok cache=N embed=M in Xs` (eticheta `corpus reindexat` pe calea de request). Semnatura corpusului (`_corpus_signature_silver`) = mecanism de decizie NESCHIMBAT; worker-ul nu atinge cache-ul. Review adversarial: 1 MAJOR + 2 MINOR + 1 NIT, toate reparate. Suita completa: **1596 passed, 1 skipped** (+31 teste noi: 15/15 cai din diagrama de acoperire, toate cu `AUTOPASS_EMBEDDINGS_ENABLED=1` explicit, backend mock). Crestere DB ~26-30MB acceptata; deferate in TODOS.md: CLI embcache, precompute la build, graceful reload, numpy la >50k randuri. **De verificat pe instanta reala (US-006)**: doua restarturi `./start.sh`, al doilea cu `embed=0` si sugestia "similaritate" in <30s. **Ultima actualizare**: 2026-07-07**5.22 UI COMPACT TRIMITERI + FIX BUTON "+" EDITOR — LIVRAT** (PRD: [prd-5.22](prd/prd-5.22-ui-compact-trimiteri-reguli-fix-chips.md); mockup aprobat de utilizator inainte de executie). Trimiteri pe desktop = tabel grid cu header (Vehicul/Cod RAR/Operatie/Data/Status/Mediu), un rand per trimitere: status = bulina colorata cu tooltip (`.dot-stare`), mediu = litera `P` (fill --err, risc L.142) / `T` (contur), multi-cod = primul cod + badge `+N` cu toate codurile in title (`payload_view.coduri_rar` nou), sub-linie de eroare = textul BRUT (mesaj RAR, ex. ORA-12899) mono trunchiat doar pe error/needs_data (`labels.eroare_bruta` nou; needs_mapping exclus — "nemapat" e in coloana Cod RAR). Mobil (<=767px): fara header, rand pe 2 linii prin grid-areas, T/P + bulina in dreapta. Actiunile bulk ("Aplica cod"/"Sterge") mutate intr-o **bara contextuala** vizibila doar la selectie, cu contor si texte explicite ("Aplica acest cod la N randuri") + hint; JS delegat + sync la `htmx:afterSwap`. Mapari: descrierea sectiunii "Reguli automate (text)" inlocuita cu un exemplu concret formatat. **Fix bug "+" editor prestatii** — cauza-radacina gasita LA VERIFY E2E (nu in analiza statica): `hx-disabled-elt="find button"` de pe form-urile parinte se mosteneste pe butoanele chips → `find button` relativ la "+" = null → TypeError intern htmx 1.9.12 INAINTE de request (nicio cerere, nicio eroare) → fix `hx-disabled-elt="this"` explicit pe toate butoanele hx-post din `_chips_prestatii.html`; separat, no-op-urile silentioase server-side din `post_form_chips` (cod invalid pe add/add_flat, duplicat pe toate ramurile) au acum mesaje explicite (`chips_error`) si confirmare la succes (chip `.chip-nou` evidentiat + "X adaugat."). Verify E2E in browser pe desktop 1280 + mobil 390 (screenshots), bulk-fix aplicat live. Suita completa: **1622 passed, 1 skipped** (+26 teste noi). Observatii deferate: `bulk_message` e suprascris imediat de reload-ul `trimiteriChanged` (pre-existent); operatia trunchiata agresiv pe mobil pe randurile cu buton "Trimite".
> 2026-07-06 — **5.21 CACHE PERSISTENT EMBEDDINGS IN SQLITE — LIVRAT** (PRD: [prd-5.21](prd/prd-5.21-embedding-cache-sqlite.md); executie echipa de agenti Sonnet: cache-core T1+T2, wiring T3+T5, tests T4+T6, reviewer adversarial). Vectorii corpusului k-NN (17.181 exemple SILVER) persista in tabela noua `embedding_cache` (PK `(model, text_hash)`, blob float32 LE 384, `array('f')` end-to-end); la warmup se vectorizeaza DOAR miss-urile (hash sha256 pe `denumire_normalizata` din lista FILTRATA, identica cu ce intra in `index_corpus`), restarturile cu corpus neschimbat = `embed=0`. Modul nou `app/embedding_cache.py` (fara dependinte noi): `sync_corpus_vectors(conn, model, texts, embed_fn)` hash->load->embed(miss)->save; scrieri/stergeri in chunk-uri de 500 cu `BEGIN`/`COMMIT` EXPLICIT (conexiunile sunt autocommit — `conn.commit()` e no-op); validare dimensiune la scriere SI citire (blob corupt = miss re-vectorizat); purjare orfane + modele vechi DOAR dupa indexare reusita; toate erorile SQLite -> `log.warning` + fallback embed complet (degradare gratioasa, nimic propagat). `EmbeddingEngine.index_corpus(vectors=)` valideaza alinierea `len(vectors)==len(items)` + absenta None (mismatch -> fallback embed complet). Secventa protejata de `_embeddings_lock` (mapping.py); pe calea de request (`block=False`) acquire NE-blocant — daca warmup-ul tine lock-ul, return imediat (fix MAJOR din review: altfel primul request in fereastra de cold start ingheta ~1-2 min). Log observabilitate: `embeddings: warmup ok cache=N embed=M in Xs` (eticheta `corpus reindexat` pe calea de request). Semnatura corpusului (`_corpus_signature_silver`) = mecanism de decizie NESCHIMBAT; worker-ul nu atinge cache-ul. Review adversarial: 1 MAJOR + 2 MINOR + 1 NIT, toate reparate. Suita completa: **1596 passed, 1 skipped** (+31 teste noi: 15/15 cai din diagrama de acoperire, toate cu `AUTOPASS_EMBEDDINGS_ENABLED=1` explicit, backend mock). Crestere DB ~26-30MB acceptata; deferate in TODOS.md: CLI embcache, precompute la build, graceful reload, numpy la >50k randuri. **De verificat pe instanta reala (US-006)**: doua restarturi `./start.sh`, al doilea cu `embed=0` si sugestia "similaritate" in <30s.
> 2026-07-06 — **HARDENING 80/20 LIVRAT + PUSH** (`main` la `63b6cbc`; plan /autoplan `main-hardening-8020-plan-20260703.md`, audit securitate 2026-07-03, inainte de expunerea publica autopass.romfast.ro). **P0**: compose fail-safe — `:?` obligatoriu pe `AUTOPASS_REQUIRE_API_KEY`/`AUTOPASS_WORKER_SEND_ENABLED`/`AUTOPASS_RAR_ENV` (split-brain api-prod/worker-test eliminat; post-5.20 variabila e doar ancora de fallback, nu tinta trimiterilor), `FORWARDED_ALLOW_IPS="*"` ca rate-limit-ul sa vada IP-ul real dupa Traefik (invariante in comentariu: api fara `ports:` pe host, Traefik fara `forwardedHeaders.insecure`), `AUTOPASS_SESSION_SECRET` obligatoriu + `SESSION_HTTPS_ONLY` default true; invarianta E1 fail-fast la boot (`validate_prod_invariants`): prod fara cheie API sau session secret -> RuntimeError cu mesaj actionabil. **P0-4 backup criptat SQLite**: `tools/backup_db.sh` (snapshot online stdlib + gpg AES256 + retentie + rclone optional), `tools/restore_check.sh` (integrity_check pe restore), `docs/backup.md`; trigger dur: configurat INAINTE de prima declaratie reala prod. **P1**: headers de securitate in-app (`SecurityHeadersMiddleware`, HSTS doar pe https) + body-cap global 10MB (`BodyCapMiddleware` ASGI pur, 413 inainte de parserul multipart/JSON; verificarea per-endpoint ramane strat 2) + imagine Docker non-root (uid 10001, `HOME=/home/app` pt fastembed, chown /data INAINTE de VOLUME, loguri pe `/data/logs`, port 8010 aliniat EXPOSE/CMD; migrare one-off volum existent: `docker compose run --rm --user root api chown -R app:app /data`). **P2**: print-uri cu email in stdout eliminate (signup + notify degradat din `app/email.py` -> `log_event` fara PII) + fix crestere monotona chei `ratelimit._hits`. Executie multi-agent (4 agenti implementare pe fisiere disjuncte + review dedicat: 0 constatari blocante, 3 nit-uri acceptate). Suita completa: 1557 passed, 1 skipped (3 rulari independente). **Post-deploy HOUR-1 (Dokploy)**: seteaza env-urile obligatorii INAINTE de redeploy (`:?` pica pornirea), creeaza cheia API + tier pro/trial pe contul propriu (altfel 403 PLAN_FARA_API), ruleaza migrarea chown, seteaza passphrase-ul de backup. > 2026-07-06 — **HARDENING 80/20 LIVRAT + PUSH** (`main` la `63b6cbc`; plan /autoplan `main-hardening-8020-plan-20260703.md`, audit securitate 2026-07-03, inainte de expunerea publica autopass.romfast.ro). **P0**: compose fail-safe — `:?` obligatoriu pe `AUTOPASS_REQUIRE_API_KEY`/`AUTOPASS_WORKER_SEND_ENABLED`/`AUTOPASS_RAR_ENV` (split-brain api-prod/worker-test eliminat; post-5.20 variabila e doar ancora de fallback, nu tinta trimiterilor), `FORWARDED_ALLOW_IPS="*"` ca rate-limit-ul sa vada IP-ul real dupa Traefik (invariante in comentariu: api fara `ports:` pe host, Traefik fara `forwardedHeaders.insecure`), `AUTOPASS_SESSION_SECRET` obligatoriu + `SESSION_HTTPS_ONLY` default true; invarianta E1 fail-fast la boot (`validate_prod_invariants`): prod fara cheie API sau session secret -> RuntimeError cu mesaj actionabil. **P0-4 backup criptat SQLite**: `tools/backup_db.sh` (snapshot online stdlib + gpg AES256 + retentie + rclone optional), `tools/restore_check.sh` (integrity_check pe restore), `docs/backup.md`; trigger dur: configurat INAINTE de prima declaratie reala prod. **P1**: headers de securitate in-app (`SecurityHeadersMiddleware`, HSTS doar pe https) + body-cap global 10MB (`BodyCapMiddleware` ASGI pur, 413 inainte de parserul multipart/JSON; verificarea per-endpoint ramane strat 2) + imagine Docker non-root (uid 10001, `HOME=/home/app` pt fastembed, chown /data INAINTE de VOLUME, loguri pe `/data/logs`, port 8010 aliniat EXPOSE/CMD; migrare one-off volum existent: `docker compose run --rm --user root api chown -R app:app /data`). **P2**: print-uri cu email in stdout eliminate (signup + notify degradat din `app/email.py` -> `log_event` fara PII) + fix crestere monotona chei `ratelimit._hits`. Executie multi-agent (4 agenti implementare pe fisiere disjuncte + review dedicat: 0 constatari blocante, 3 nit-uri acceptate). Suita completa: 1557 passed, 1 skipped (3 rulari independente). **Post-deploy HOUR-1 (Dokploy)**: seteaza env-urile obligatorii INAINTE de redeploy (`:?` pica pornirea), creeaza cheia API + tier pro/trial pe contul propriu (altfel 403 PLAN_FARA_API), ruleaza migrarea chown, seteaza passphrase-ul de backup.

View File

@@ -0,0 +1,209 @@
# PRD 5.22 — UI compact Trimiteri + bara contextuala "Aplica cod" + exemplu Reguli automate + fix buton "+" editor import
**Stare**: inchis
> Mockup aprobat de utilizator (artifact `6efc43e0`, v2, 2026-07-07). Decizii inchise:
> tabel desktop pe un rand cu header; status = bulina + tooltip; mediu = litera T/P;
> multi-cod = primul cod + badge `+N` cu tooltip; sub-linie de problema = textul brut
> al erorii, mono, trunchiat, doar pe erori; bara "Aplica cod" contextuala la selectie;
> Reguli automate = exemplu concret in loc de paragraf descriptiv; fix no-op silentios
> la butonul "+" din editorul de import.
## 1. Obiectiv
Patru imbunatatiri UI/UX aprobate pe mockup: (a) lista Trimiteri devine tabel compact
pe un singur rand pe desktop, cu header de coloane si semnalizare compacta
status/mediu; (b) actiunea bulk "Aplica cod" devine bara contextuala vizibila doar la
selectie; (c) sectiunea "Reguli automate (text)" din Mapari isi inlocuieste descrierea
cu un exemplu concret; (d) butonul "+" din editorul de prestatii (import) nu mai
esueaza silentios — feedback vizibil la succes si la refuz.
## 2. Non-Goals (anti scope-creep)
- NU se schimba logica backend a `bulk-fix` (`POST /trimiteri/bulk-fix`) — doar prezentarea.
- NU se schimba masina de stari `submissions`, filtrele, paginarea sau modalul de detaliu.
- NU se adauga functionalitate noua la regulile text (`/mapari/reguli-text`) — doar microcopy.
- NU se redeseneaza restul editorului de rand din import (doar zona chips "+").
- NU se ating API v1, worker-ul sau schema DB.
## 3. Stories atomice
### US-001: Fix buton "+" chips prestatii — feedback la refuz si la succes
**Ca** operator care editeaza un rand de import **vreau** ca apasarea pe "+" sa aiba
mereu un efect vizibil **pentru ca** azi codul invalid sau duplicat produce un raspuns
identic, fara niciun mesaj ("nu se intampla nimic").
- **Depinde de**: —
- **Fisiere**: `app/web/routes.py` (`post_form_chips`), `app/web/templates/_chips_prestatii.html`, `tests/test_web_corectie_prestatii.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_web_corectie_prestatii.py``test_add_cod_duplicat_afiseaza_mesaj`, `test_add_cod_invalid_afiseaza_mesaj`, `test_add_cod_reusit_marcheaza_chip_nou`
- **Acceptance criteria**:
- [ ] Ramurile `add`, `add_extra`, `add_flat` din `post_form_chips`: cod care nu trece
verificarea in `nomenclator_rar` → mesaj de eroare in fragmentul re-randat
("Cod necunoscut in nomenclator"), nu raspuns identic.
- [ ] Cod deja prezent in lista (dedup) → mesaj "X este deja in lista — nu a fost
adaugat inca o data", pe TOATE cele 3 ramuri.
- [ ] Adaugare reusita → chip-ul nou primeste clasa de evidentiere (`chip-nou`,
outline verde) + mesaj de confirmare "X adaugat" (aria-live pentru SR).
- [ ] Mesajele traiesc in fragmentul `_chips_prestatii.html` (swap `outerHTML` pe
`#chips-section`) si dispar la urmatoarea actiune reusita.
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser pe import → Editeaza rand → alege cod deja adaugat → "+"
→ mesaj rosu vizibil; alege cod nou → "+" → chip verde evidentiat + mesaj.
### US-002: Reguli automate — exemplu concret in loc de descriere
**Ca** utilizator al paginii Mapari **vreau** sa vad un exemplu de regula in loc de un
paragraf explicativ **pentru ca** exemplul arata direct formatul si scuteste cititul.
- **Depinde de**: —
- **Fisiere**: `app/web/templates/_mapari.html`, `tests/test_web_mapari_text_rules.py` (~2 fisiere)
- **Test intai (RED)**: `tests/test_web_mapari_text_rules.py``test_sectiune_reguli_afiseaza_exemplu_nu_descriere`
- **Acceptance criteria**:
- [ ] Textul descriptiv din capul sectiunii "Reguli automate (text)" (inclusiv
empty-state) e inlocuit cu un singur exemplu formatat:
`Daca operatia contine "verificare" → Cod RAR = OE-2 Intretinere si revizie`,
cu pattern-ul si codul evidentiate vizual (chip warn / chip accent, ca in mockup).
- [ ] Nicio schimbare functionala la tabel, adaugare, preview sau stergere reguli.
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser pe Mapari → sectiunea Reguli automate afiseaza exemplul.
### US-003: View-model rand trimitere — coduri multiple + eroare bruta
**Ca** dashboard **vreau** ca randul de trimitere sa expuna lista completa de coduri
RAR si textul brut al erorii **pentru ca** UI-ul compact afiseaza `primul cod +N`
(tooltip cu toate) si sub-linia de eroare cu mesajul RAR, nu eticheta explicativa.
- **Depinde de**: —
- **Fisiere**: `app/payload_view.py`, `app/web/routes.py` (`_submission_row_view`), `tests/test_payload_view.py`, `tests/test_web_submissions.py` (~4 fisiere)
- **Test intai (RED)**: `tests/test_payload_view.py``test_coduri_rar_lista_ordonata_dedup`, `test_coduri_rar_gol_fara_prestatii`; `tests/test_web_submissions.py``test_rand_expune_eroare_bruta_doar_pe_error`
- **Acceptance criteria**:
- [ ] `prezentare_din_payload` intoarce camp nou `coduri_rar`: lista ordonata,
dedup, a codurilor RAR (`cod_prestatie`) din TOATE prestatiile (lista goala
cand nu exista); campurile existente raman neschimbate (compatibilitate).
- [ ] `_submission_row_view` expune `eroare_bruta`: mesajul RAR brut cand exista
(`rar_error` → mesajul serverului, ex. `ORA-12899...`), altfel `motiv` scurt;
sir gol pe stari fara problema. `needs_mapping` NU produce `eroare_bruta`
(informatia "nemapat" traieste in coloana Cod RAR).
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: — (acoperit de US-004).
### US-004: Trimiteri desktop — tabel cu header, un rand per trimitere
**Ca** operator pe desktop **vreau** trimiterile intr-un tabel compact cu header de
coloane **pentru ca** este loc pe orizontala si scanez mai repede o lista pe un rand.
- **Depinde de**: US-003
- **Fisiere**: `app/web/templates/_submissions.html`, `app/web/templates/base.html` (CSS), `tests/test_web_submissions_layout.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_web_submissions_layout.py``test_desktop_header_coloane`, `test_status_bulina_cu_tooltip`, `test_mediu_litera_t_p`, `test_multicod_badge_plus_n`, `test_sublinie_eroare_bruta_doar_pe_error`
- **Acceptance criteria**:
- [ ] Pe desktop (>= breakpoint existent), lista devine tabel/grid cu header:
(checkbox) · Vehicul · Cod RAR · Operatie · Data · Status · Mediu · (actiuni).
Un rand per trimitere, fara wrap; operatia primeste ellipsis.
- [ ] Status = DOAR bulina colorata (culorile claselor `s-*` existente), nume stare
complet in `title` (tooltip); coloana are headerul "Status".
- [ ] Mediu = litera in coloana proprie cu header "Mediu": `P` alb pe fundal
`--err` (productie), `T` contur discret (testare); `title` cu numele complet.
- [ ] Mai multe coduri: primul cod + badge `+N` cu `title` care listeaza toate
codurile din `coduri_rar`; un singur cod → fara badge; niciun cod → `nemapat`.
- [ ] Sub-linia de problema apare DOAR cand `eroare_bruta` e ne-goala: text mono,
`--fs-xs`, culoare `--err`, un singur rand cu `text-overflow:ellipsis`,
`title` cu textul complet.
- [ ] Comportamente pastrate: click pe rand → modal detaliu; checkbox doar pe
`gestionabil`; buton "Trimite" doar pe `tinut`; paginare/filtre neatinse.
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser HTMX pe http://localhost:8010/ (desktop 1280px) —
randuri pe o singura linie, header vizibil, tooltip-uri corecte.
### US-005: Trimiteri mobil — bulina status + litera mediu
**Ca** operator pe mobil **vreau** aceleasi semnale compacte (bulina + T/P) pe lista
de 2 linii **pentru ca** pill-urile cu text ocupa spatiu inutil pe ecran mic.
- **Depinde de**: US-004
- **Fisiere**: `app/web/templates/_submissions.html`, `app/web/templates/base.html` (media query), `tests/test_web_submissions_layout.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_web_submissions_layout.py``test_mobil_pastreaza_doua_linii_cu_bulina_si_litera`
- **Acceptance criteria**:
- [ ] Sub breakpoint, randul revine la 2 linii (placuta / cod·operatie·data), iar in
dreapta stau litera de mediu + bulina de status (+ butonul "Trimite" pe tinute).
- [ ] Fara header de tabel pe mobil; fara scroll orizontal la 390px.
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser la 390px — lista lizibila, fara overflow orizontal.
### US-006: Bara contextuala "Aplica cod" — vizibila doar la selectie
**Ca** operator **vreau** ca actiunile bulk sa apara doar cand am selectat randuri,
cu text explicit **pentru ca** butonul permanent "Aplica cod" fara context e confuz.
- **Depinde de**: US-004
- **Fisiere**: `app/web/templates/_submissions.html`, `app/web/templates/base.html` (CSS + JS numarare selectie), `tests/test_web_bulk_fix.py` (~3 fisiere)
- **Test intai (RED)**: `tests/test_web_bulk_fix.py``test_bara_bulk_ascunsa_implicit`, `test_bara_bulk_texte_explicite`
- **Acceptance criteria**:
- [ ] Inputul de cod + "Aplica cod" + "Sterge selectate" dispar din capul listei;
apar intr-o bara contextuala (stil accent, ca in mockup) randata ascunsa si
afisata prin JS cand >= 1 checkbox e bifat; dispare la deselectare.
- [ ] Bara contine: contor "N selectate", input cod RAR, buton "Aplica acest cod la
N randuri", separator, buton "Sterge N randuri", actiune "Deselecteaza";
N se actualizeaza la fiecare bifare (JS, fara roundtrip).
- [ ] Sub bara: hint pe un rand — "Aplica codul RAR doar operatiilor fara cod din
randurile selectate, apoi le repune in coada."
- [ ] Endpoint-urile, CSRF, `hx-confirm` pe stergere si `hx-disinherit` raman
functional identice; `bulk_message` se afiseaza ca acum.
- [ ] Starea barei supravietuieste re-randarii fragmentului (re-evaluare la
`htmx:afterSwap` — dupa swap selectia se pierde, bara se ascunde).
- [ ] `python3 -m pytest -q` verde.
- **Verificare E2E**: browser — capul listei curat; bifez 2 randuri blocate → bara
apare cu "2 selectate"; aplic cod → lista se reincarca, bara dispare, sumar vizibil.
## 4. Riscuri
- **Tabel vs. lista pe acelasi fragment**: `_submissions.html` e folosit si de
paginare/filtre/OOB — orice schimbare de structura trebuie sa pastreze id-urile
(`#submissions-wrap`, `#bulk-trimiteri`, OOB-urile). Mitigare: pastram form-ul si
id-urile existente, schimbam doar interiorul listei; testele de layout existente
(`test_web_submissions_layout.py`) ruleaza inainte si dupa.
- **`rar_error` brut poate contine detalii tehnice lungi**: afisare pe un rand cu
ellipsis + `title`; nu se logheaza/expune nimic nou (textul exista deja in modal).
- **JS-ul de numarare selectie** ruleaza peste randuri re-randate de HTMX: legare prin
delegare pe container, nu pe noduri (altfel handler-ii mor la swap).
- **Doua semnale rotunde alaturate pe mobil** (bulina status + litera mediu): litera
are forma/continut diferit de bulina — validat in mockup; daca in practica se
confunda, fallback aprobat: mediu doar pe randurile de productie.
## 5. Intrebari deschise
— (toate inchise prin mockup v2 aprobat)
## 6. Valuri de executie (graful de dependente)
```
Val 1: [US-001] [US-002] [US-003] ← fara dependente, fisiere disjuncte → paralel
Val 2: [US-004] ← deblocat de US-003
Val 3: [US-005] [US-006] ← deblocate de US-004; ating aceleasi fisiere → secvential intre ele
```
---
## Raport VERIFY
**Verdict: PASS** — 2026-07-07, E2E in browser (Playwright, uvicorn pe DB temporar, 7 trimiteri
seedate: sent/queued/held/needs_mapping/error, test+prod, multi-cod).
- Desktop 1280px: header Vehicul/Cod RAR/Operatie/Data/Status/Mediu; un rand per trimitere;
bulina colorata cu title; `P` fill rosu / `T` contur; `OE-1 +2` cu tooltip; sub-linie
`ORA-12899...` mono trunchiata doar pe rândul error; `nemapat` italic fara sub-linie.
- Bara bulk: ascunsa implicit; la 2 checkbox-uri → "2 selectate · Aplica acest cod la 2 randuri |
Sterge 2 randuri | Deselecteaza" + hint; aplicat OE-2 → ambele randuri → `queued`, bara dispare.
- Mapari: exemplul "Daca operatia contine «verificare» → Cod RAR = OE-2 Intretinere si revizie"
inlocuieste descrierea.
- Chips "+": adaugare OE-1 → chip evidentiat (chip-nou) + "OE-1 adaugat."; duplicat → "OE-1 este
deja in lista — nu a fost adaugat inca o data."; select gol → "Selecteaza un cod RAR din lista".
- Mobil 390px: fara header, 2 linii, T/P + bulina in dreapta, fara scroll orizontal.
- Suita completa: **1622 passed, 1 skipped**.
**Cauza-radacina REALA a bug-ului "+" (gasita LA VERIFY, nu in analiza statica)**: form-urile
parinte din modalele de editare au `hx-disabled-elt="find button"`; atributul se MOSTENESTE pe
butoanele chips, iar `find button` rezolvat relativ la butonul "+" (fara butoane descendente) →
`null` → TypeError intern htmx 1.9.12 INAINTE de request → nicio cerere, nicio eroare vizibila.
Fix: `hx-disabled-elt="this"` explicit pe toate butoanele hx-post din `_chips_prestatii.html`
(care si dezactiveaza butonul cat cererea e in zbor). No-op-urile silentioase server-side
(cod invalid pe add/add_flat, duplicat) erau defecte reale suplimentare, reparate separat.
Observatii non-blocante: (1) `bulk_message` ("N reusite") dispare imediat — HX-Trigger
`trimiteriChanged` reincarca lista peste raspunsul cu mesaj (comportament pre-existent);
(2) pe mobil, operatia e trunchiata agresiv pe randurile cu buton "Trimite" (spatiul e
consumat de actiune + mediu + status) — acceptat, informatia completa e in modal.

View File

@@ -46,3 +46,39 @@ def test_eticheta_lunga_ramane_pentru_subtext():
assert isinstance(css_class, str) and css_class, ( assert isinstance(css_class, str) and css_class, (
f"eticheta_stare({status!r}) trebuie sa aiba css_class non-vida la pozitia 3" f"eticheta_stare({status!r}) trebuie sa aiba css_class non-vida la pozitia 3"
) )
def test_eroare_bruta_prefera_cauza_tehnica():
import json
from app.web.labels import eroare_bruta
rar_error = json.dumps({
"cod": "RAR_EROARE_SERVER",
"problema": "RAR a esuat la inregistrarea prezentarii",
"cauza": "ORA-12899: value too large for column PRESTATII.COD",
"fix": "",
"field": None,
"message": "ORA-12899: value too large for column PRESTATII.COD",
})
assert eroare_bruta("error", rar_error) == (
"ORA-12899: value too large for column PRESTATII.COD"
)
def test_eroare_bruta_goala_pe_needs_mapping_si_stari_ok():
from app.web.labels import eroare_bruta
assert eroare_bruta("needs_mapping", '{"unmapped": [{"cod_op_service": "X"}]}') == ""
assert eroare_bruta("sent", "orice") == ""
assert eroare_bruta("queued", None) == ""
def test_eroare_bruta_validare_needs_data():
import json
from app.web.labels import eroare_bruta
rar_error = json.dumps([{"field": "vin", "message": "VIN invalid (17 caractere)"}])
assert eroare_bruta("needs_data", rar_error) == "VIN invalid (17 caractere)"
def test_eroare_bruta_nu_arunca_pe_json_corupt():
from app.web.labels import eroare_bruta
assert eroare_bruta("error", "{corupt") == "{corupt"
assert eroare_bruta("error", None) == ""

View File

@@ -212,3 +212,23 @@ def test_fara_operatie_service_cand_lipseste():
d4 = prezentare_din_payload("nu-e-json") d4 = prezentare_din_payload("nu-e-json")
assert d4["op_service_cod"] == "" assert d4["op_service_cod"] == ""
assert d4["op_service_denumire"] == "" assert d4["op_service_denumire"] == ""
def test_coduri_rar_lista_ordonata_dedup():
from app.payload_view import prezentare_din_payload
p = prezentare_din_payload({
"vin": "WVWZZZ1JZXW000111",
"prestatii": [
{"cod_prestatie": "OE-1", "cod_op_service": "A"},
{"cod_prestatie": "oe-2", "cod_op_service": "B"},
{"cod_prestatie": "OE-1", "cod_op_service": "C"}, # duplicat
{"cod_op_service": "D"}, # nemapat, fara cod
],
})
assert p["coduri_rar"] == ["OE-1", "OE-2"]
def test_coduri_rar_gol_fara_prestatii():
from app.payload_view import prezentare_din_payload
assert prezentare_din_payload({"vin": "X"})["coduri_rar"] == []
assert prezentare_din_payload(None)["coduri_rar"] == []

View File

@@ -246,3 +246,34 @@ def test_bulk_scoped_cont(client):
# Randul lui B NEATINS # Randul lui B NEATINS
assert _get_status(sid_b) == "needs_mapping", \ assert _get_status(sid_b) == "needs_mapping", \
"Randul contului B a fost modificat de contul A — INCALCARE SCOPE!" "Randul contului B a fost modificat de contul A — INCALCARE SCOPE!"
def test_bara_bulk_ascunsa_implicit(client):
"""Controalele bulk stau intr-o bara contextuala ascunsa implicit (fara selectie)."""
acct = _create_account_user("bara.hidden@test.com")
_insert_submission(acct)
_login(client, "bara.hidden@test.com")
html = client.get("/_fragments/submissions").text
assert 'id="bulk-bar"' in html, "Bara contextuala bulk lipseste"
m = re.search(r'id="bulk-bar"[^>]*class="([^"]*)"', html) or \
re.search(r'class="([^"]*)"[^>]*id="bulk-bar"', html)
assert m and "bulk-bar" in m.group(1) and "activ" not in m.group(1), (
f"Bara bulk trebuie randata inactiva (ascunsa prin CSS): {m.group(0) if m else 'fara clasa'}"
)
# butonul vechi permanent 'Aplica cod' (fara context) a disparut
assert ">\n Aplica cod\n </button>" not in html
def test_bara_bulk_texte_explicite(client):
"""Bara contine contor, buton explicit 'Aplica acest cod la N randuri',
stergere cu N, 'Deselecteaza' si hint-ul cu ce face actiunea."""
acct = _create_account_user("bara.texte@test.com")
_insert_submission(acct)
_login(client, "bara.texte@test.com")
html = client.get("/_fragments/submissions").text
assert "selectate" in html
assert "Aplica acest cod la" in html
assert "Deselecteaza" in html
assert "operatiilor fara cod" in html, "Hint-ul explicativ lipseste"

View File

@@ -799,3 +799,128 @@ def test_add_extra_semnal_vizibil_cod_invalid(client):
assert "chips-extra-error" in resp.text, ( assert "chips-extra-error" in resp.text, (
f"Semnalul 'chips-extra-error' lipsa pentru cod invalid: {resp.text[:300]}" f"Semnalul 'chips-extra-error' lipsa pentru cod invalid: {resp.text[:300]}"
) )
def test_add_per_op_cod_invalid_afiseaza_mesaj(client):
"""Actiunea 'add' (picker per operatie) cu cod invalid da semnal vizibil."""
_create_account_user("add.op.err@test.com")
_login(client, "add.op.err@test.com")
_seed_cod("OE-1", "Schimb ulei")
csrf = _csrf(client)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": [""],
"chip_op_service": ["SchimbUlei"],
"chip_denumire": ["Schimb ulei"],
"chips_action": "add",
"chips_add_op_index": "0",
"chips_add_cod_0": "XX-99", # cod inexistent
},
)
assert resp.status_code == 200
assert "chips-extra-error" in resp.text, (
f"Semnal de eroare lipsa la add cu cod invalid: {resp.text[:300]}"
)
def test_add_flat_cod_invalid_afiseaza_mesaj(client):
"""Actiunea 'add_flat' cu cod invalid da semnal vizibil (nu no-op silentios)."""
_create_account_user("add.flat.err@test.com")
_login(client, "add.flat.err@test.com")
_seed_cod("OE-1", "Schimb ulei")
csrf = _csrf(client)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1"],
"chip_op_service": [""],
"chip_denumire": [""],
"chips_action": "add_flat",
"chips_add_cod_flat": "XX-99",
},
)
assert resp.status_code == 200
assert "chips-extra-error" in resp.text, (
f"Semnal de eroare lipsa la add_flat cu cod invalid: {resp.text[:300]}"
)
def test_add_flat_duplicat_afiseaza_mesaj(client):
"""add_flat cu un cod deja prezent NU dubleaza chip-ul si afiseaza mesaj."""
_create_account_user("add.flat.dup@test.com")
_login(client, "add.flat.dup@test.com")
_seed_cod("OE-1", "Schimb ulei")
csrf = _csrf(client)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1"],
"chip_op_service": [""],
"chip_denumire": [""],
"chips_action": "add_flat",
"chips_add_cod_flat": "OE-1", # deja in lista
},
)
assert resp.status_code == 200
import re as _re
hidden = _re.findall(r'<input[^>]+name="cod_prestatie"[^>]+value="OE-1"', resp.text)
assert len(hidden) == 1, f"Chip OE-1 duplicat: {len(hidden)} aparitii"
assert "deja in lista" in resp.text, (
f"Mesaj de duplicat lipsa la add_flat: {resp.text[:300]}"
)
def test_add_extra_duplicat_afiseaza_mesaj(client):
"""add_extra cu chip extra identic existent afiseaza mesaj (nu no-op)."""
_create_account_user("add.extra.dup@test.com")
_login(client, "add.extra.dup@test.com")
_seed_cod("OE-1", "Schimb ulei")
_seed_cod("FRN1", "Franare")
csrf = _csrf(client)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1", "FRN1"],
"chip_op_service": ["SchimbUlei", ""],
"chip_denumire": ["Schimb ulei", ""],
"chips_action": "add_extra",
"chips_add_cod_flat": "FRN1", # chip extra identic deja existent
},
)
assert resp.status_code == 200
assert "deja in lista" in resp.text, (
f"Mesaj de duplicat lipsa la add_extra: {resp.text[:300]}"
)
def test_add_reusit_marcheaza_chip_nou(client):
"""Adaugare reusita: chip-ul nou e evidentiat (chip-nou) + mesaj de confirmare."""
_create_account_user("add.ok.nou@test.com")
_login(client, "add.ok.nou@test.com")
_seed_cod("OE-1", "Schimb ulei")
_seed_cod("FRN1", "Franare")
csrf = _csrf(client)
resp = client.post(
"/form-chips",
data={
"csrf_token": csrf,
"cod_prestatie": ["OE-1"],
"chip_op_service": ["SchimbUlei"],
"chip_denumire": ["Schimb ulei"],
"chips_action": "add_extra",
"chips_add_cod_flat": "FRN1",
},
)
assert resp.status_code == 200
assert "chip-nou" in resp.text, f"Chip nou neevidentiat: {resp.text[:300]}"
assert "adaugat" in resp.text, f"Mesaj de confirmare lipsa: {resp.text[:300]}"

View File

@@ -198,3 +198,20 @@ def test_cod_invalid_respins(client):
assert resp.status_code == 200 assert resp.status_code == 200
assert "necunoscut" in resp.text.lower() assert "necunoscut" in resp.text.lower()
assert _text_rules(acct) == [] assert _text_rules(acct) == []
def test_sectiune_reguli_afiseaza_exemplu_nu_descriere(client):
"""Capul sectiunii = un exemplu concret de regula, nu paragraf descriptiv."""
_create_account_user("exemplu.regula@test.com")
_login(client, "exemplu.regula@test.com")
resp = client.get("/?tab=mapari")
assert resp.status_code == 200
html = resp.text
# Exemplul concret e prezent (pattern + cod, formatate)
assert "regula-exemplu" in html, "Blocul de exemplu (regula-exemplu) lipseste"
assert "verificare" in html
assert "OE-2" in html
# Paragraful descriptiv vechi a disparut
assert "nu egal, ci substring" not in html, "Descrierea veche inca prezenta"

View File

@@ -539,7 +539,7 @@ def test_clasa_contor_card(client):
def test_clasa_lista_slim(client): def test_clasa_lista_slim(client):
""".lista-trimiteri-slim + .trimitere-slim: separator --line2, padding, tinta min-height:44px. """.lista-trimiteri-slim + .trimitere-slim: separator --line2, padding, tinta min-height:44px.
Sub-elemente: .slim-vin (mono) si .slim-meta (muted 11px). Sub-elemente: .slim-vin (mono) si .c-meta (celulele cod/operatie/data).
""" """
_create_account_user("ls2@test.com") _create_account_user("ls2@test.com")
_login(client, "ls2@test.com") _login(client, "ls2@test.com")
@@ -552,8 +552,8 @@ def test_clasa_lista_slim(client):
assert "var(--line2)" in bloc, ".trimitere-slim nu foloseste var(--line2) ca separator" assert "var(--line2)" in bloc, ".trimitere-slim nu foloseste var(--line2) ca separator"
assert "min-height:44px" in bloc, ".trimitere-slim nu are tinta min-height:44px" assert "min-height:44px" in bloc, ".trimitere-slim nu are tinta min-height:44px"
assert ".slim-vin" in bloc, ".slim-vin lipseste din bloc" assert ".slim-vin" in bloc, ".slim-vin lipseste din bloc"
assert ".slim-meta" in bloc, ".slim-meta lipseste din bloc" assert ".c-meta" in bloc, ".c-meta lipseste din bloc"
assert "var(--muted)" in bloc, ".slim-meta nu foloseste var(--muted)" assert "var(--muted)" in bloc, "componentele slim nu folosesc var(--muted)"
def test_clasa_camp_slim(client): def test_clasa_camp_slim(client):

View File

@@ -186,15 +186,16 @@ def test_operatie_contine_cod_rar(client):
def test_pill_eticheta_scurta(client): def test_pill_eticheta_scurta(client):
"""Pill-ul de Stare foloseste eticheta scurta; textul lung in title.""" """Statusul e bulina colorata (dot-stare); textul complet al starii in title."""
acct = _create_account_user("pill@test.com") acct = _create_account_user("pill@test.com")
_insert_submission(acct, "sent", id_prezentare=70001) _insert_submission(acct, "sent", id_prezentare=70001)
_login(client, "pill@test.com") _login(client, "pill@test.com")
resp = client.get("/_fragments/submissions") resp = client.get("/_fragments/submissions")
assert resp.status_code == 200 assert resp.status_code == 200
html = resp.text html = resp.text
# eticheta scurta in pill # bulina cu clasa de stare, fara text in celula
assert ">Finalizat<" in html assert "dot-stare" in html
assert "s-sent" in html
# textul lung pastrat ca tooltip (title) # textul lung pastrat ca tooltip (title)
assert 'title="Declarate la RAR' in html assert 'title="Declarate la RAR' in html
@@ -229,8 +230,8 @@ def test_eticheta_umana_apare_sub_pill(client):
resp = client.get("/_fragments/submissions") resp = client.get("/_fragments/submissions")
assert resp.status_code == 200 assert resp.status_code == 200
html = resp.text html = resp.text
assert "eticheta-problema" in html # containerul textului uman sub pill assert "c-err" in html # sub-linia compacta de eroare
assert "s-error" in html # colorat ca problema (error/needs_*) assert "lipsa odometru" in html # textul brut al erorii, nu eticheta explicativa
# NU randeaza cod brut de catalog pe rand # NU randeaza cod brut de catalog pe rand
assert "COD_" not in html assert "COD_" not in html
assert "RAR_EROARE" not in html assert "RAR_EROARE" not in html
@@ -444,16 +445,17 @@ def test_rand_slim_vin_operatie_pill(client):
assert "slim-vin" in html, "slim-vin lipseste — linia 1 placuta" assert "slim-vin" in html, "slim-vin lipseste — linia 1 placuta"
assert "B777ZZZ" in html, "placuta (nr. inmatriculare) lipseste de pe rand" assert "B777ZZZ" in html, "placuta (nr. inmatriculare) lipseste de pe rand"
# L2: cod RAR · operatie · data (slim-meta / slim-rand2) # Celulele: cod RAR · operatie · data (c-meta cu c-cod/c-op/c-data)
assert "slim-meta" in html, "slim-meta lipseste — linia 2" assert "c-meta" in html, "c-meta lipseste — celulele cod/operatie/data"
assert "slim-rand2" in html, "slim-rand2 lipseste — linia 2 (cod RAR · operatie · data)" assert "c-cod" in html and "c-op" in html, "celulele cod/operatie lipsesc"
# VIN integral nu mai e pe rand — traieste in modalul de detaliu. # VIN integral nu mai e pe rand — traieste in modalul de detaliu.
assert "000777" not in html, "VIN scurt nu mai trebuie randat pe randul slim (2 linii)" assert "000777" not in html, "VIN scurt nu mai trebuie randat pe randul slim (2 linii)"
# Pill de stare: clasa CSS + eticheta scurta # Status: bulina cu clasa de stare; numele coloanei in header
assert "s-sent" in html, "clasa pill s-sent lipseste" assert "s-sent" in html, "clasa de stare s-sent lipseste"
assert "Finalizat" in html, "eticheta scurta stare_scurt lipseste" assert "dot-stare" in html, "bulina de status lipseste"
assert "Status" in html, "headerul coloanei Status lipseste"
def test_filtre_paginare_pastrate(client): def test_filtre_paginare_pastrate(client):

View File

@@ -136,3 +136,128 @@ def test_placuta_lipsa_nu_genereaza_rand_gol(client):
assert m.group(1).strip() != "", "slim-vin afiseaza '' izolat ca identificator" assert m.group(1).strip() != "", "slim-vin afiseaza '' izolat ca identificator"
assert "fara numar" in html, "fallback 'fara numar' lipseste cand placuta+VIN absente" assert "fara numar" in html, "fallback 'fara numar' lipseste cand placuta+VIN absente"
# --------------------------------------------------------------------------- #
# Tabel compact desktop (PRD 5.22): header coloane, bulina status, T/P, #
# badge +N multi-cod, sub-linie eroare bruta. #
# --------------------------------------------------------------------------- #
def _ins_full(acct: int, *, payload: dict, status: str = "queued",
rar_env: str = "test", rar_error: str | None = None) -> int:
from app.db import get_connection
conn = get_connection()
try:
cur = conn.execute(
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_env, rar_error) "
"VALUES (?, ?, ?, ?, ?, ?)",
(f"k-{os.urandom(5).hex()}", acct, status,
json.dumps(payload), rar_env, rar_error),
)
conn.commit()
return int(cur.lastrowid)
finally:
conn.close()
def _payload_min(coduri: list[str] | None = None) -> dict:
return {
"vin": "WVWZZZ1JZXW000002",
"nr_inmatriculare": "B22TST",
"data_prestatie": "2026-06-21",
"odometru_final": "200",
"prestatii": [{"cod_prestatie": c} for c in (coduri or ["R-X"])],
}
def test_desktop_header_coloane(client):
"""Lista are un rand de header cu numele coloanelor (Status, Mediu etc.)."""
acct = _create_account_user("head.col@test.com")
_ins(acct, nr="B77HDR")
_login(client, "head.col@test.com")
html = client.get("/_fragments/submissions").text
assert "trimiteri-head" in html, "Randul de header lipseste"
for col in ("Vehicul", "Cod RAR", "Operatie", "Status", "Mediu"):
assert col in html, f"Coloana '{col}' lipseste din header"
def test_status_bulina_cu_tooltip(client):
"""Statusul e o bulina colorata (dot-stare) cu numele starii in title."""
acct = _create_account_user("dot.status@test.com")
_ins_full(acct, payload=_payload_min(), status="sent")
_login(client, "dot.status@test.com")
html = client.get("/_fragments/submissions").text
m = re.search(r'<span[^>]*class="dot-stare[^"]*s-sent[^"]*"[^>]*title="([^"]+)"', html) or \
re.search(r'<span[^>]*class="dot-stare[^"]*"[^>]*class[^>]*>', html)
assert "dot-stare" in html, "Bulina de status (dot-stare) lipseste"
assert m, f"Bulina fara title (tooltip cu starea): {html[html.find('dot-stare')-100:html.find('dot-stare')+200]}"
def test_mediu_litera_t_p(client):
"""Mediul RAR e litera compacta: T (testare, contur) / P (productie, fill)."""
acct = _create_account_user("env.litera@test.com")
_ins_full(acct, payload=_payload_min(), rar_env="test")
_ins_full(acct, payload=_payload_min(), rar_env="prod")
_login(client, "env.litera@test.com")
html = client.get("/_fragments/submissions").text
assert re.search(r'class="env-l env-l-test"[^>]*>\s*T\s*<', html), "Litera T (testare) lipseste"
assert re.search(r'class="env-l env-l-prod"[^>]*>\s*P\s*<', html), "Litera P (productie) lipseste"
def test_multicod_badge_plus_n(client):
"""Mai multe coduri RAR: primul cod + badge +N cu toate codurile in title."""
acct = _create_account_user("multicod@test.com")
_ins_full(acct, payload=_payload_min(["OE-1", "OE-2", "OE-8"]))
_ins_full(acct, payload=_payload_min(["OE-4"]))
_login(client, "multicod@test.com")
html = client.get("/_fragments/submissions").text
m = re.search(r'<span class="cod-plus" title="([^"]+)">\+2</span>', html)
assert m, "Badge-ul +2 lipseste pentru trimiterea cu 3 coduri"
for cod in ("OE-1", "OE-2", "OE-8"):
assert cod in m.group(1), f"Codul {cod} lipseste din tooltip-ul badge-ului"
# un singur cod -> fara badge
assert not re.search(r'OE-4[^<]*</span><span class="cod-plus"', html)
def test_sublinie_eroare_bruta_doar_pe_error(client):
"""Sub-linia rosie contine textul brut al erorii RAR, doar pe error/needs_data."""
acct = _create_account_user("err.brut@test.com")
_ins_full(
acct, payload=_payload_min(), status="error",
rar_error=json.dumps({
"cod": "RAR_EROARE_SERVER",
"problema": "RAR a esuat la inregistrarea prezentarii",
"cauza": "ORA-12899: value too large for column PRESTATII.COD",
"fix": "x", "field": None,
"message": "ORA-12899: value too large for column PRESTATII.COD",
}),
)
_ins_full(
acct,
payload={**_payload_min(), "prestatii": [{"cod_op_service": "IGIENIZARE", "denumire": "Igienizare AC"}]},
status="needs_mapping",
rar_error=json.dumps({"unmapped": [{"cod_op_service": "IGIENIZARE"}]}),
)
_login(client, "err.brut@test.com")
html = client.get("/_fragments/submissions").text
err_cells = re.findall(r'class="c-err"[^>]*>([^<]*)<', html)
assert len(err_cells) == 1, f"Exact o sub-linie de eroare asteptata, gasite {len(err_cells)}"
assert "ORA-12899" in err_cells[0], f"Textul brut RAR lipseste: {err_cells[0]!r}"
def test_mobil_pastreaza_doua_linii_cu_bulina_si_litera(client):
"""CSS-ul mobil: header ascuns, rand pe 2 linii prin grid-areas (veh / meta),
cu actiune + mediu + status in dreapta."""
_create_account_user("mobil.css@test.com")
_login(client, "mobil.css@test.com")
html = client.get("/?tab=coada").text
assert re.search(r'\.trimiteri-head\s*\{\s*display\s*:\s*none', html), \
"Headerul de coloane nu e ascuns pe mobil"
m = re.search(r'grid-template-areas\s*:\s*"check veh\s+act env status"\s*"check meta act env status"', html)
assert m, "Grid-areas pe 2 linii (veh/meta) lipseste din CSS-ul mobil"