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:
@@ -59,6 +59,19 @@ def _vin_scurt(vin: str) -> str:
|
||||
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]:
|
||||
"""Primul item de prestatie ca dict, defensiv (lista goala/non-dict -> {})."""
|
||||
if isinstance(prestatii, list):
|
||||
@@ -83,10 +96,11 @@ def _payload_dict(payload: str | dict | None) -> dict[str, Any]:
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
`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,
|
||||
"cod": cod 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
|
||||
"op_service_cod": op_service_cod,
|
||||
"op_service_denumire": op_service_denumire,
|
||||
|
||||
@@ -359,6 +359,31 @@ def parse_erori(rar_error: object) -> list[dict]:
|
||||
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)
|
||||
#
|
||||
|
||||
@@ -32,6 +32,7 @@ from ..web.csrf import get_csrf_token, verify_csrf
|
||||
from .labels import (
|
||||
ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
|
||||
STARI_PREVIEW,
|
||||
eroare_bruta,
|
||||
eticheta_env,
|
||||
eticheta_rar,
|
||||
eticheta_scurta,
|
||||
@@ -1131,6 +1132,8 @@ def _submission_row_view(r) -> dict:
|
||||
"motiv": motiv,
|
||||
# eticheta umana scurta a problemei sub pill (text, nu cod brut).
|
||||
"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
|
||||
# stergere bulk; sent/sending/queued raman read-only (fara checkbox).
|
||||
"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()
|
||||
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()
|
||||
|
||||
def _cod_valid(cod: str) -> bool:
|
||||
return conn.execute(
|
||||
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod,)
|
||||
).fetchone() is not None
|
||||
|
||||
try:
|
||||
if action == "add":
|
||||
# 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):
|
||||
op_idx = 0
|
||||
add_cod = str(form.get(f"chips_add_cod_{op_idx}") or "").strip().upper()
|
||||
if add_cod and 0 <= op_idx < len(chips):
|
||||
exists = conn.execute(
|
||||
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (add_cod,)
|
||||
).fetchone()
|
||||
if exists:
|
||||
chips[op_idx]["cod_prestatie"] = 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
|
||||
if not add_cod:
|
||||
chips_error = "Selecteaza un cod RAR din lista inainte de a adauga."
|
||||
elif not (0 <= op_idx < len(chips)):
|
||||
chips_error = "Operatia nu mai exista in formular — reincarca editarea."
|
||||
elif not _cod_valid(add_cod):
|
||||
chips_error = f"Cod necunoscut in nomenclator: {add_cod}."
|
||||
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":
|
||||
# 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,
|
||||
"form_chips_url": "/form-chips",
|
||||
"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,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
{% if chip.cod_op_service %}
|
||||
{% set _is_warn = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
|
||||
{% 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;">
|
||||
<span class="op-row-name">
|
||||
{{ chip.cod_op_service }}
|
||||
@@ -59,11 +60,12 @@
|
||||
<span style="display:flex;align-items:center;gap:8px;">
|
||||
{% if chip.cod_prestatie %}
|
||||
{# ===== 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 }}">
|
||||
{{ chip.cod_prestatie }}
|
||||
<button type="button" class="chip-del"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
@@ -81,6 +83,7 @@
|
||||
<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;"
|
||||
hx-post="/trimitere/{{ _sub_id }}/salveaza-regula-chip"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#detaliu-modal-body"
|
||||
hx-swap="innerHTML"
|
||||
@@ -104,6 +107,7 @@
|
||||
<button type="button"
|
||||
class="add-code"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
@@ -126,11 +130,13 @@
|
||||
<div class="chips" role="group" aria-label="Coduri RAR suplimentare" style="margin-top:4px;">
|
||||
{% for chip in _extra_chips %}
|
||||
{% 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 }}">
|
||||
{{ chip.cod_prestatie }}
|
||||
<button type="button" class="chip-del"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
@@ -153,6 +159,7 @@
|
||||
<button type="button"
|
||||
class="add-code"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
@@ -167,24 +174,19 @@
|
||||
Nomenclator indisponibil — adaugarea de coduri suplimentare nu e posibila.
|
||||
</div>
|
||||
{% 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 %}
|
||||
{# ===== Mod plat: lista de coduri libere (corectie pura, fara op_service) ===== #}
|
||||
<div class="chips" role="group" aria-label="Coduri RAR selectate">
|
||||
{% for chip in _chips %}
|
||||
{% if chip.cod_prestatie %}
|
||||
{% 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 }}">
|
||||
{{ chip.cod_prestatie }}
|
||||
<button type="button" class="chip-del"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
@@ -207,6 +209,7 @@
|
||||
<button type="button"
|
||||
class="add-code"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
@@ -224,6 +227,18 @@
|
||||
</div>
|
||||
{% 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) #}
|
||||
{% if not _chips %}
|
||||
<div style="font-size:10px;color:var(--muted);padding:4px 0;">
|
||||
|
||||
@@ -178,18 +178,19 @@
|
||||
<!-- ============================================================ -->
|
||||
<div class="card">
|
||||
<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;">
|
||||
O regula leaga orice operatie al carei text <strong>contine</strong> (nu egal, ci substring)
|
||||
un cuvant de un cod RAR. Util pentru operatii fara cod intern: ex. orice operatie care
|
||||
<em>contine</em> „verificare" primeste codul ales. Match insensibil la majuscule/diacritice.
|
||||
</p>
|
||||
|
||||
{% if not text_rules %}
|
||||
<div class="empty" style="margin-bottom:12px;">
|
||||
Inca nu ai reguli. Ex: operatia contine «verificare» → OE-2.
|
||||
Mapeaza automat operatii similare fara cod intern. Adauga prima regula mai jos.
|
||||
{# Un exemplu concret in loc de descriere — arata direct formatul unei reguli. #}
|
||||
<div class="regula-exemplu"
|
||||
style="margin:0 0 12px; font-size:13.5px; max-width:680px; padding:10px 12px;
|
||||
border:1px dashed var(--line); border-radius:8px; background:var(--card2);">
|
||||
Daca operatia contine
|
||||
<span style="font-family:var(--font-mono); font-size:12.5px; padding:1px 6px; border-radius:4px;
|
||||
background:color-mix(in srgb, var(--warn) 18%, transparent); color:var(--warn);">verificare</span>
|
||||
<span class="muted" aria-hidden="true">→</span>
|
||||
Cod RAR =
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
<div class="tablewrap tabel-card">
|
||||
<table>
|
||||
|
||||
@@ -60,45 +60,70 @@
|
||||
hx-disinherit="hx-confirm"
|
||||
style="margin:0;">
|
||||
<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;">
|
||||
{# Bulk-fix: input cod + buton aplica #}
|
||||
|
||||
{# Bara contextuala bulk (5.22): randata inactiva (ascunsa prin CSS), afisata de
|
||||
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"
|
||||
placeholder="Cod RAR (ex: OE-1)"
|
||||
autocomplete="off" autocapitalize="characters"
|
||||
style="width:120px; font-size:12px; padding:3px 7px;
|
||||
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">
|
||||
<button type="button"
|
||||
hx-post="/trimiteri/bulk-fix"
|
||||
hx-target="#submissions-wrap"
|
||||
hx-swap="innerHTML"
|
||||
style="background:var(--card); color:var(--accent); border-color:var(--accent);
|
||||
font-size:13px; padding:4px 10px; border-radius:5px; cursor:pointer;"
|
||||
style="background:var(--card); color:var(--accent); border:1px solid var(--accent);
|
||||
font-size:12.5px; padding:4px 10px; border-radius:5px; cursor:pointer;"
|
||||
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>
|
||||
{# Separator vizual #}
|
||||
<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"
|
||||
style="background:var(--card); color:var(--err); border-color:var(--err);
|
||||
font-size:13px; padding:4px 10px; border-radius:5px; cursor:pointer;">
|
||||
Sterge selectate
|
||||
style="background:var(--card); color:var(--err); border:1px solid var(--err);
|
||||
font-size:12.5px; padding:4px 10px; border-radius:5px; cursor:pointer;">
|
||||
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>
|
||||
</div>
|
||||
<div class="bulk-hint">
|
||||
Aplica codul RAR doar operatiilor fara cod din randurile selectate, apoi le repune in coada.
|
||||
</div>
|
||||
|
||||
{# Lista slim trimiteri.
|
||||
Inlocuieste tabelul cu randuri compacte: VIN mono + operatie·ora + pill.
|
||||
Nr. inmatriculare, data prestatie si nr. prezentare RAR raman accesibile
|
||||
pe linia meta discreta (linia 3) si in modalul de detaliu. #}
|
||||
{# Tabel compact trimiteri (grid CSS, PRD 5.22).
|
||||
Desktop: un rand per trimitere sub un header de coloane; status = bulina cu
|
||||
tooltip, mediu = litera T/P, multi-cod = primul cod + badge +N.
|
||||
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"
|
||||
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 %}
|
||||
{# Randul slim: stanga = VIN mono scurt (L1) + operatie·ora muted (L2) + meta (L3);
|
||||
dreapta = pill de stare. Click deschide modalul global (#detaliu-modal-body).
|
||||
Clickabil/focusabil (role=button); Enter/Space deschid modalul (JS in base.html). #}
|
||||
{# Rand clickabil (role=button, Enter/Space in JS din base.html) — deschide
|
||||
modalul global de detaliu (#detaliu-modal-body). #}
|
||||
<li id="trimitere-row-{{ r.id }}"
|
||||
class="trimitere-slim"
|
||||
data-detaliu-id="{{ r.id }}"
|
||||
@@ -110,66 +135,61 @@
|
||||
style="cursor:pointer;"
|
||||
title="Click pentru detaliul complet">
|
||||
|
||||
{# Zona checkbox — nu declanseaza modalul (stopPropagation).
|
||||
Vizibila DOAR pe randurile gestionabile (error/needs_data/needs_mapping).
|
||||
Latimea fixa previne reflow la prezenta/absenta checkbox-ului. #}
|
||||
<div style="flex:0 0 22px; display:flex; align-items:center;" onclick="event.stopPropagation();">
|
||||
{# Checkbox — nu declanseaza modalul (stopPropagation). Vizibil DOAR pe
|
||||
randurile gestionabile; latimea fixa previne reflow. #}
|
||||
<div class="c-check" onclick="event.stopPropagation();">
|
||||
{% if r.gestionabil %}
|
||||
<input type="checkbox" name="submission_id" value="{{ r.id }}"
|
||||
aria-label="Selecteaza trimiterea #{{ r.id }} pentru stergere">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Bloc text principal — stanga, ocupa spatiul ramas. Rand de 2 linii (spec 5.16):
|
||||
L1 = placuta (identificator primar); L2 = cod RAR · operatie · data prestatie. #}
|
||||
<div style="flex:1 1 auto; min-width:0;">
|
||||
{# Vehicul: placuta (identificator primar); fallback VIN scurt / 'fara numar'
|
||||
(niciodata em-dash izolat). #}
|
||||
{% 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
|
||||
scaneaza operatorul. .slim-vin reumplut (acelasi nume de clasa, churn minim).
|
||||
Fallback cand placuta lipseste ('—'): VIN scurt, apoi mesaj neutru
|
||||
(nu randa em-dash izolat ca identificator). #}
|
||||
{% 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 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>
|
||||
{# Pe desktop .c-meta e display:contents (copiii devin celule de grid);
|
||||
pe mobil e linia 2 (flex) cu separatoare "·" din CSS. #}
|
||||
<div class="c-meta">
|
||||
{% set _cods = r.prez.coduri_rar or [] %}
|
||||
<span class="c-cod">
|
||||
{% if _cods %}
|
||||
<span class="cod-rar-cod">{{ _cods[0] }}</span>
|
||||
{%- if _cods | length > 1 -%}
|
||||
<span class="cod-plus" title="{{ _cods | join(' · ') }}">+{{ _cods | length - 1 }}</span>
|
||||
{%- endif %}
|
||||
{% else %}
|
||||
<span class="cod-rar-cod cod-rar-sub muted">nemapat</span>
|
||||
{% endif %}
|
||||
<span class="slim-op">{{ r.prez.operatie }}</span>
|
||||
{% if r.prez.data_prestatie and r.prez.data_prestatie != '—' %}
|
||||
<span class="slim-data muted">{{ r.prez.data_prestatie }}</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 %}
|
||||
</span>
|
||||
<span class="c-op">{{ r.prez.operatie }}</span>
|
||||
<span class="c-data">{% if r.prez.data_prestatie and r.prez.data_prestatie != '—' %}{{ r.prez.data_prestatie }}{% endif %}</span>
|
||||
</div>
|
||||
|
||||
{# Zona dreapta: pill stare + badge mediu RAR + buton Trimite #}
|
||||
<div style="flex:0 0 auto; display:flex; flex-direction:column; align-items:flex-end; gap:3px; white-space:nowrap;">
|
||||
<span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}">{{ r.stare_scurt }}</span>
|
||||
{# Status: doar bulina colorata; numele complet in title + aria-label. #}
|
||||
<span class="c-status">
|
||||
<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 %}
|
||||
{% 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 %}
|
||||
{# Buton "Trimite" DOAR pe randurile tinute (queued AND held=1).
|
||||
Afordanta dedicata cu stopPropagation (randul e role=button) — NU declanseaza modalul
|
||||
de detaliu. Confirmare 1-linie prin hx-confirm (per rand); ireversibilitate in microcopy. #}
|
||||
</span>
|
||||
|
||||
{# Actiuni: buton "Trimite" DOAR pe randurile tinute (queued AND held=1). #}
|
||||
<div class="c-act">
|
||||
{% if r.tinut %}
|
||||
<form hx-post="/trimitere/{{ r.id }}/trimite-acum"
|
||||
hx-target="#submissions-wrap" hx-swap="innerHTML"
|
||||
@@ -180,7 +200,7 @@
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn-trimite-rand"
|
||||
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;
|
||||
background:var(--accent); color:#fff; font-weight:600;">
|
||||
Trimite
|
||||
@@ -188,6 +208,12 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
</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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -741,35 +741,68 @@
|
||||
.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; }
|
||||
}
|
||||
/* .lista-trimiteri-slim + .trimitere-slim — lista compacta cu separator --line2.
|
||||
Randul e clickabil (rol button), tinta min-height:44px pe mobil. */
|
||||
/* Bara contextuala bulk (5.22): ascunsa fara selectie; .activ (setat de JS
|
||||
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; }
|
||||
.trimitere-slim { display:flex; align-items:center; justify-content:space-between; gap:12px;
|
||||
padding:14px 16px; border-bottom:1px solid var(--line2); min-height:44px; cursor:pointer; }
|
||||
.trimiteri-head, .trimitere-slim {
|
||||
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: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; }
|
||||
.slim-vin { font-family:var(--font-mono); font-size:var(--fs-md); font-weight:600; color:var(--ink); }
|
||||
.slim-meta { font-size:var(--fs-sm); color:var(--muted); margin-top:3px; }
|
||||
/* Linia 2 a randului slim (5.16): cod RAR · operatie (ellipsis) · data, pe UN rand.
|
||||
Ellipsis-ul pe operatie garanteaza 2 linii MAX si la 390px. */
|
||||
.slim-rand2 { display:flex; align-items:baseline; gap:6px; min-width:0; }
|
||||
.slim-rand2 .cod-rar-cod { flex:0 0 auto; font-family:var(--font-mono); font-weight:600;
|
||||
color:var(--accent); }
|
||||
.slim-rand2 .cod-rar-cod.muted { color:var(--muted); font-weight:500; }
|
||||
.slim-rand2 .slim-op { flex:1 1 auto; min-width:0; white-space:nowrap; overflow:hidden;
|
||||
text-overflow:ellipsis; color:var(--ink); }
|
||||
.slim-rand2 .slim-data { flex:0 0 auto; }
|
||||
.slim-rand2 .slim-op::before, .slim-rand2 .slim-data::before {
|
||||
content:"·"; color:var(--muted); margin-right:6px; }
|
||||
.lista-trimiteri-slim .eticheta-problema { font-size:var(--fs-xs); line-height:1.3; margin-top:2px; }
|
||||
/* Pill slim (5.16): fill-tint + dot 7px + text colorat per stare (currentColor din .s-*).
|
||||
Pastrat pe FIECARE rand inclusiv Finalizat (linistit dar prezent). */
|
||||
.lista-trimiteri-slim .pill { display:inline-flex; align-items:center; gap:5px; font-weight:600;
|
||||
background:color-mix(in srgb, currentColor 14%, transparent);
|
||||
border-color:color-mix(in srgb, currentColor 35%, transparent); }
|
||||
.lista-trimiteri-slim .pill::before { content:""; width:7px; height:7px; border-radius:99px;
|
||||
background:currentColor; flex-shrink:0; }
|
||||
.trimitere-slim .c-check { display:flex; align-items:center; }
|
||||
.slim-vin { font-family:var(--font-mono); font-size:var(--fs-sm); font-weight:600; color:var(--ink);
|
||||
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
|
||||
/* Pe desktop copiii .c-meta devin celule de grid (cod / operatie / data). */
|
||||
.trimitere-slim .c-meta { display:contents; }
|
||||
.trimitere-slim .c-cod { white-space:nowrap; }
|
||||
.trimitere-slim .cod-rar-cod { font-family:var(--font-mono); font-weight:600;
|
||||
font-size:var(--fs-xs); color:var(--accent); }
|
||||
.trimitere-slim .cod-rar-cod.muted { color:var(--muted); font-weight:500; font-style:italic; }
|
||||
/* Badge +N: trimiterea are mai multe coduri RAR; toate stau in title. */
|
||||
.trimitere-slim .cod-plus { display:inline-block; font-family:var(--font-mono);
|
||||
font-size:10px; font-weight:700; color:var(--muted); border:1px solid var(--line);
|
||||
border-radius:99px; padding:0 5px; margin-left:4px; vertical-align:1px; cursor:default; }
|
||||
.trimitere-slim .c-op { min-width:0; white-space:nowrap; overflow:hidden;
|
||||
text-overflow:ellipsis; font-size:var(--fs-sm); color:var(--ink); }
|
||||
.trimitere-slim .c-data { font-size:var(--fs-xs); color:var(--muted); white-space:nowrap;
|
||||
font-variant-numeric:tabular-nums; }
|
||||
.trimitere-slim .c-status, .trimitere-slim .c-env { justify-self:center; line-height:1;
|
||||
display:inline-flex; align-items:center; }
|
||||
/* Bulina de status: doar cerc colorat (currentColor din .s-*); numele starii
|
||||
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.
|
||||
Mono pentru campuri VIN/odometru/nr: adauga clasa .camp-mono pe input. */
|
||||
.camp-slim { margin-bottom:8px; }
|
||||
@@ -793,6 +826,8 @@
|
||||
.chip .chip-del:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }
|
||||
/* Varianta chip warn (ex. R-ODO necesita odometruInitial) */
|
||||
.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 { 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));
|
||||
@@ -806,9 +841,30 @@
|
||||
vizuala a randului de mapare op<->cod. */
|
||||
.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)); }
|
||||
/* 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) {
|
||||
.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 === */
|
||||
/* === Fix mobil Mapari ===
|
||||
@@ -1222,6 +1278,34 @@
|
||||
if (saveBtn) saveBtn.classList.add('dirty');
|
||||
});
|
||||
</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>
|
||||
// Sub-taburi pagina Setari: comutare pur client-side, fara server.
|
||||
// Delegare pe document -> supravietuieste swap-urilor HTMX ale #card-cont (dupa
|
||||
|
||||
@@ -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:
|
||||
> 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.
|
||||
|
||||
|
||||
209
docs/prd/prd-5.22-ui-compact-trimiteri-reguli-fix-chips.md
Normal file
209
docs/prd/prd-5.22-ui-compact-trimiteri-reguli-fix-chips.md
Normal 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.
|
||||
@@ -46,3 +46,39 @@ def test_eticheta_lunga_ramane_pentru_subtext():
|
||||
assert isinstance(css_class, str) and css_class, (
|
||||
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) == ""
|
||||
|
||||
@@ -212,3 +212,23 @@ def test_fara_operatie_service_cand_lipseste():
|
||||
d4 = prezentare_din_payload("nu-e-json")
|
||||
assert d4["op_service_cod"] == ""
|
||||
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"] == []
|
||||
|
||||
@@ -246,3 +246,34 @@ def test_bulk_scoped_cont(client):
|
||||
# Randul lui B NEATINS
|
||||
assert _get_status(sid_b) == "needs_mapping", \
|
||||
"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"
|
||||
|
||||
@@ -799,3 +799,128 @@ def test_add_extra_semnal_vizibil_cod_invalid(client):
|
||||
assert "chips-extra-error" in resp.text, (
|
||||
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]}"
|
||||
|
||||
@@ -198,3 +198,20 @@ def test_cod_invalid_respins(client):
|
||||
assert resp.status_code == 200
|
||||
assert "necunoscut" in resp.text.lower()
|
||||
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"
|
||||
|
||||
@@ -539,7 +539,7 @@ def test_clasa_contor_card(client):
|
||||
|
||||
def test_clasa_lista_slim(client):
|
||||
""".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")
|
||||
_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 "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-meta" in bloc, ".slim-meta lipseste din bloc"
|
||||
assert "var(--muted)" in bloc, ".slim-meta nu foloseste var(--muted)"
|
||||
assert ".c-meta" in bloc, ".c-meta lipseste din bloc"
|
||||
assert "var(--muted)" in bloc, "componentele slim nu folosesc var(--muted)"
|
||||
|
||||
|
||||
def test_clasa_camp_slim(client):
|
||||
|
||||
@@ -186,15 +186,16 @@ def test_operatie_contine_cod_rar(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")
|
||||
_insert_submission(acct, "sent", id_prezentare=70001)
|
||||
_login(client, "pill@test.com")
|
||||
resp = client.get("/_fragments/submissions")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
# eticheta scurta in pill
|
||||
assert ">Finalizat<" in html
|
||||
# bulina cu clasa de stare, fara text in celula
|
||||
assert "dot-stare" in html
|
||||
assert "s-sent" in html
|
||||
# textul lung pastrat ca tooltip (title)
|
||||
assert 'title="Declarate la RAR' in html
|
||||
|
||||
@@ -229,8 +230,8 @@ def test_eticheta_umana_apare_sub_pill(client):
|
||||
resp = client.get("/_fragments/submissions")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
assert "eticheta-problema" in html # containerul textului uman sub pill
|
||||
assert "s-error" in html # colorat ca problema (error/needs_*)
|
||||
assert "c-err" in html # sub-linia compacta de eroare
|
||||
assert "lipsa odometru" in html # textul brut al erorii, nu eticheta explicativa
|
||||
# NU randeaza cod brut de catalog pe rand
|
||||
assert "COD_" 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 "B777ZZZ" in html, "placuta (nr. inmatriculare) lipseste de pe rand"
|
||||
|
||||
# L2: cod RAR · operatie · data (slim-meta / slim-rand2)
|
||||
assert "slim-meta" in html, "slim-meta lipseste — linia 2"
|
||||
assert "slim-rand2" in html, "slim-rand2 lipseste — linia 2 (cod RAR · operatie · data)"
|
||||
# Celulele: cod RAR · operatie · data (c-meta cu c-cod/c-op/c-data)
|
||||
assert "c-meta" in html, "c-meta lipseste — celulele cod/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.
|
||||
assert "000777" not in html, "VIN scurt nu mai trebuie randat pe randul slim (2 linii)"
|
||||
|
||||
# Pill de stare: clasa CSS + eticheta scurta
|
||||
assert "s-sent" in html, "clasa pill s-sent lipseste"
|
||||
assert "Finalizat" in html, "eticheta scurta stare_scurt lipseste"
|
||||
# Status: bulina cu clasa de stare; numele coloanei in header
|
||||
assert "s-sent" in html, "clasa de stare s-sent lipseste"
|
||||
assert "dot-stare" in html, "bulina de status lipseste"
|
||||
assert "Status" in html, "headerul coloanei Status lipseste"
|
||||
|
||||
|
||||
def test_filtre_paginare_pastrate(client):
|
||||
|
||||
@@ -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 "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"
|
||||
|
||||
Reference in New Issue
Block a user