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

@@ -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)
#

View File

@@ -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,
})

View File

@@ -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;">

View File

@@ -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» &rarr; 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">&rarr;</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>

View File

@@ -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>

View File

@@ -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