feat(5.15+5.14): CLOSE — fix-uri code-review + embeddings functional

5.15 (propagare design + dashboard editare) si 5.14 (mapare LLM distilata)
inchise dupa /code-review high. 8 buguri reparate TDD:

- HIGH modal nu se deschidea pe randul slim (base.html: trimitere-slim)
- HIGH /repune trunchia prestatii (declaratie incompleta la RAR) -> iterare
  peste existing, codes pozitional
- HIGH embeddings incarca model ~230MB degeaba pe corpus gol -> poarta has_corpus()
- HIGH picker chips gol pe re-render eroare -> conn/account_id pe toate ramurile
- MED obs re-derivat dupa stergere explicita -> _merge_override pastreaza obs=''
- MED mapare salvata fara denumire poluă GOLD -> _record_gold_validation guard
- MED typo nome_prestatie -> nume_prestatie in select /repune
- MED bucketare timp +3h gresita iarna -> SQLite localtime + TZ=Europe/Bucharest

Embeddings WIRE-uit functional (PRD #15, decizie user): ensure_embeddings_corpus
construieste corpus din nomenclator, gated pe AUTOPASS_EMBEDDINGS_ENABLED (default
off). Marime model corectata ~50MB->~230MB (estimare PRD gresita).

Cleanup: hoist load_* din bucla bulk-fix; import re la top.
Regresie: 1256 passed, 1 deselected (live), 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-28 20:48:34 +00:00
parent 9e42e7ed6f
commit 3fc53534e2
53 changed files with 9684 additions and 384 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,172 @@
{#
_chips_prestatii.html — sectiunea de prestatii chips (E4, server-driven via /form-chips).
Re-randata de endpoint-ul /form-chips la fiecare add/remove de chip.
Inclusa si din _form_editare.html pentru randarea initiala.
Starea chip-urilor traieste in input-uri hidden din form (NU in DB mid-edit).
Fiecare operatie are un picker propriu cand e nemapata (E4 binding op<->cod).
Reveal odometru initial semnalat prin data-has-r-odo="true" si chip-warn pe R-ODO/I-ODO.
Context vars (toate cu defaults):
prestatii_chips — list of {cod_prestatie, cod_op_service, denumire}
nomenclator_rar — list of {cod_prestatie, nume_prestatie} pentru picker
has_r_odo — True daca orice chip e R-ODO sau I-ODO (server-computed)
form_chips_url — URL pentru HTMX; default '/form-chips'
chips_section_id — ID div (default 'chips-section')
csrf_token — CSRF (trecut prin hx-include din form parinte)
#}
{% set _chips_url = form_chips_url or '/form-chips' %}
{% set _sec_id = chips_section_id or 'chips-section' %}
{% set _chips = prestatii_chips or [] %}
{% set _has_ops = _chips | selectattr('cod_op_service') | list | length > 0 %}
{# US-009: chips_submission_id e setat din _detaliu_ctx cand chips sunt randate in modalul de detaliu.
Lipseste cand _chips_prestatii.html e rerandat via /form-chips (stateless, fara submission). #}
{% set _sub_id = chips_submission_id if chips_submission_id is defined else none %}
<div id="{{ _sec_id }}" data-has-r-odo="{{ 'true' if has_r_odo else 'false' }}"
aria-live="polite" aria-label="Prestatii cod RAR">
{# ===== Input-uri hidden pentru starea curenta a chip-urilor =====
TOATE itemele emit 3 hidden inputs (cod poate fi "" pentru unmapped).
Paralele index-by-index: cod_prestatie[i], chip_op_service[i], chip_denumire[i].
Filtrate la submit de post_corectie_trimitere (coduri goale = neschimbate). #}
{% for chip in _chips %}
<input type="hidden" name="cod_prestatie" value="{{ chip.cod_prestatie or '' }}">
<input type="hidden" name="chip_op_service" value="{{ chip.cod_op_service or '' }}">
<input type="hidden" name="chip_denumire" value="{{ chip.denumire or '' }}">
{% endfor %}
<div class="camp-slim" style="margin-bottom:8px;">
<label>Prestatii — cod RAR pe fiecare operatie</label>
{% if _has_ops %}
{# ===== Mod operatii: UN picker PE operatie (E4 binding) ===== #}
{% for chip in _chips %}
{% if chip.cod_op_service %}
{% set _is_warn = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
{% set _nemapat = not chip.cod_prestatie %}
<div class="op-row {% if _nemapat %}op-row-warn{% endif %}" style="margin-bottom:6px;">
<span class="op-row-name">
{{ chip.cod_op_service }}
{% if chip.denumire and chip.denumire != chip.cod_op_service %}
<span class="muted" style="font-weight:400;font-size:11px;"> — {{ chip.denumire }}</span>
{% endif %}
{% if _nemapat %}
<span style="color:var(--warn);font-size:10px;font-weight:400;"> · lipsa cod</span>
{% endif %}
</span>
<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 %}"
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-include="closest form"
hx-target="#{{ _sec_id }}"
hx-swap="outerHTML"
hx-vals='{"chips_action":"remove","chips_remove_index":{{ loop.index0 }}}'
aria-label="Sterge codul {{ chip.cod_prestatie }} pentru {{ chip.cod_op_service }}">
&times;
</button>
</span>
{# US-009: "salveaza ca regula op->cod" — apare doar cand submission_id e cunoscut
(in modalul de detaliu, nu la re-randarea stateless via /form-chips).
Reuse EXACT save_mapping + reresolve_account via endpoint dedicat.
hx-include="closest form" propaga csrf_token din form-ul parinte. #}
<span id="save-rule-slot-{{ loop.index0 }}" class="save-rule-slot">
{% if _sub_id and chip.cod_op_service and chip.cod_prestatie %}
<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-include="closest form"
hx-target="#detaliu-modal-body"
hx-swap="innerHTML"
hx-vals='{"salveaza_op":{{ chip.cod_op_service | tojson }},"salveaza_cod":{{ chip.cod_prestatie | tojson }}}'
aria-label="Salveaza regula {{ chip.cod_op_service }} -> {{ chip.cod_prestatie }}">
salveaza ca regula
</button>
{% endif %}
</span>
{% else %}
{# ===== Operatie nemapata: picker galben cu "alege cod RAR" ===== #}
<select name="chips_add_cod_{{ loop.index0 }}"
id="picker-op-{{ loop.index0 }}"
aria-label="Alege cod RAR pentru {{ chip.cod_op_service }}"
style="min-width:160px;font-size:11px;height:26px;">
<option value="">— alege cod RAR —</option>
{% for n in (nomenclator_rar or []) %}
<option value="{{ n.cod_prestatie }}">{{ n.cod_prestatie }} — {{ n.nume_prestatie }}</option>
{% endfor %}
</select>
<button type="button"
class="add-code"
hx-post="{{ _chips_url }}"
hx-include="closest form"
hx-target="#{{ _sec_id }}"
hx-swap="outerHTML"
hx-vals='{"chips_action":"add","chips_add_op_index":{{ loop.index0 }}}'
aria-label="Adauga cod RAR pentru {{ chip.cod_op_service }}">
+ Adauga
</button>
{% endif %}
</span>
</div>
{% endif %}
{% endfor %}
{% 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 %}"
aria-label="Prestatie {{ chip.cod_prestatie }}">
{{ chip.cod_prestatie }}
<button type="button" class="chip-del"
hx-post="{{ _chips_url }}"
hx-include="closest form"
hx-target="#{{ _sec_id }}"
hx-swap="outerHTML"
hx-vals='{"chips_action":"remove_flat","chips_remove_cod":"{{ chip.cod_prestatie }}"}'
aria-label="Sterge codul {{ chip.cod_prestatie }}">&times;</button>
</span>
{% endif %}
{% endfor %}
{# Picker adaugare cod nou in mod plat #}
{% if nomenclator_rar %}
<span style="display:inline-flex;align-items:center;gap:4px;">
<select name="chips_add_cod_flat"
aria-label="Adauga cod RAR nou"
style="font-size:11px;height:22px;border:1px dashed color-mix(in srgb,var(--accent) 55%,var(--line));border-radius:5px;background:transparent;color:var(--accent);">
<option value="">+ cod</option>
{% for n in nomenclator_rar %}
<option value="{{ n.cod_prestatie }}">{{ n.cod_prestatie }}</option>
{% endfor %}
</select>
<button type="button"
class="add-code"
hx-post="{{ _chips_url }}"
hx-include="closest form"
hx-target="#{{ _sec_id }}"
hx-swap="outerHTML"
hx-vals='{"chips_action":"add_flat"}'
aria-label="Adauga cod RAR selectat in lista">
+
</button>
</span>
{% endif %}
</div>
{% endif %}
{# Hint discret fara chips (debut) #}
{% if not _chips %}
<div style="font-size:10px;color:var(--muted);padding:4px 0;">
Niciun cod RAR inca — alege din picker (sus) sau adauga prin mapare.
</div>
{% endif %}
</div>
</div>

View File

@@ -1,17 +1,22 @@
{# _form_editare.html — partial partajat: campurile vehicul/data/odometru.
US-005 (PRD 5.12): extras DRY din _trimitere_detaliu.html; refolosit si de
_preview_rand.html (US-006) pentru editarea randurilor de import in modal.
{# _form_editare.html — partial partajat slim: campurile vehicul/data/odo + obs + chips prestatii.
US-007 (PRD 5.15): redesign slim cu VIN unic, Observatii textarea, chips prestatii (E4),
si reveal dinamic odometru initial cand chips contin R-ODO/I-ODO (D10c, E6 server-driven).
Inclus cu {% include "_form_editare.html" %} INSIDE un <form> element al
template-ului parinte. Acel parinte pune form-ul, CSRF-ul si orice campuri
suplimentare (ex. select cod_prestatie din _trimitere_detaliu.html).
suplimentare.
Necesita din context (setate de parinte inainte de include):
Variabile necesare din context (setate de parinte inainte de include):
form_nr — valoare curenta nr_inmatriculare
form_vin — valoare curenta vin
form_data — valoare curenta data_prestatie (YYYY-MM-DD sau brut)
form_odo_final — valoare curenta odometru_final
form_odo_initial — valoare curenta odometru_initial
obs_val — valoare curenta obs (Observatii), text liber (default '')
prestatii_chips — list of {cod_prestatie, cod_op_service, denumire} (default [])
nomenclator_rar — list of {cod_prestatie, nume_prestatie} pentru picker (default [])
has_r_odo — True daca chips contin R-ODO/I-ODO (server-computed, default False)
form_chips_url — URL pentru HTMX chip endpoint (default '/form-chips')
err_map — dict {field_name: mesaj_eroare} (poate fi {})
fix_map — dict {field_name: hint_fix} (poate fi {})
vin_context — string VIN pentru aria-label (poate fi '')
@@ -19,23 +24,78 @@
#}
{% from "_macros.html" import camp, icon %}
{# Nr. inmatriculare pe rand propriu, VIN dedesubt — ambele latime plina. #}
{{ camp('nr_inmatriculare', 'Numar inmatriculare', form_nr,
err_map=err_map, fix_map=fix_map, vin_context=vin_context) }}
{{ camp('vin', 'VIN (serie sasiu)', form_vin,
{# === 1. VIN — camp unic (fara "Confirma VIN"; contractul RAR cere un singur VIN) === #}
{{ camp('vin', 'VIN (serie sasiu)', form_vin, slim=True, mono=True,
err_map=err_map, fix_map=fix_map, vin_context=vin_context) }}
{# Restul campurilor in grila responsiva existenta. #}
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:0 16px;">
{{ camp('data_prestatie', 'Data prestatie', form_data, tip='date',
{# === 2. Data prestatie + Nr. inmatriculare — grila 2 coloane === #}
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0 12px;">
{{ camp('data_prestatie', 'Data prestatiei', form_data, tip='date', slim=True,
err_map=err_map, fix_map=fix_map, vin_context=vin_context) }}
{{ camp('odometru_final', 'Odometru final', form_odo_final,
err_map=err_map, fix_map=fix_map, vin_context=vin_context) }}
{{ camp('odometru_initial', 'Odometru initial (daca e cerut)', form_odo_initial,
{{ camp('nr_inmatriculare', 'Numar inmatriculare', form_nr, slim=True, mono=True,
err_map=err_map, fix_map=fix_map, vin_context=vin_context) }}
</div>
{# Buton primar parametrizat.
{# === 3. Observatii (obs) — textarea liber, US-005 === #}
<div class="camp-slim">
<label for="c-obs">Observatii (operatiile efectuate)</label>
<textarea id="c-obs" name="obs" rows="2"
aria-label="Observatii (operatiile efectuate){% if vin_context %} (VIN: {{ vin_context }}){% endif %}"
placeholder="ex: Revizie; schimbare placute frana">{{ obs_val or '' }}</textarea>
</div>
{# === 4. Prestatii chips (E4 server-driven, US-007) === #}
{% set form_chips_url = form_chips_url or '/form-chips' %}
{% set chips_section_id = 'chips-section' %}
{% include "_chips_prestatii.html" %}
{# === 5. Odometru final — intotdeauna vizibil === #}
{{ camp('odometru_final', 'Odometru final (km)', form_odo_final, slim=True, mono=True,
err_map=err_map, fix_map=fix_map, vin_context=vin_context) }}
{# === 6. Odometru initial — reveal dinamic server cand chips contin R-ODO/I-ODO (D10c) ===
has_r_odo=True (server-computed din lista de chips): sectiune vizibila cu marcaj warn.
has_r_odo=False: hint discret, campul optional si vizual neutru. #}
{% if has_r_odo %}
<div class="odo-initial-warn"
style="border-left:2px solid var(--warn);padding-left:10px;margin-left:-2px;">
<div class="camp-slim">
<label for="c-odometru_initial" style="color:var(--warn);">
Odometru initial (km) · necesar pentru R-ODO
</label>
<input id="c-odometru_initial" type="text" name="odometru_initial"
value="{{ form_odo_initial or '' }}"
class="camp-mono"
required
aria-required="true"
style="border-color:color-mix(in srgb,var(--warn) 50%,var(--line));{% if err_map.get('odometru_initial') %}border-color:var(--err);{% endif %}"
aria-label="Odometru initial (VIN: {{ vin_context or '' }}) — necesar pentru R-ODO"
{% if err_map.get('odometru_initial') %}aria-invalid="true"{% endif %}>
{% if err_map.get('odometru_initial') %}
<div class="s-error" style="font-size:12px;margin-top:2px;">{{ err_map.get('odometru_initial') }}</div>
{% endif %}
</div>
</div>
{% else %}
{# Hint discret cand nu e necesar #}
<div class="camp-slim">
<label for="c-odometru_initial" style="color:var(--muted);">Odometru initial (km)</label>
<input id="c-odometru_initial" type="text" name="odometru_initial"
value="{{ form_odo_initial or '' }}"
class="camp-mono"
style="{% if err_map.get('odometru_initial') %}border-color:var(--err);{% endif %}"
aria-label="Odometru initial (optional){% if vin_context %} (VIN: {{ vin_context }}){% endif %}"
{% if err_map.get('odometru_initial') %}aria-invalid="true"{% endif %}>
{% if err_map.get('odometru_initial') %}
<div class="s-error" style="font-size:12px;margin-top:2px;">{{ err_map.get('odometru_initial') }}</div>
{% endif %}
<span style="font-size:10px;color:var(--muted);font-style:italic;">
Odometru initial se cere doar pentru coduri R-ODO / I-ODO.
</span>
</div>
{% endif %}
{# === 7. Buton primar parametrizat ===
with_cancel=True (modal editare preview): Salveaza + Anuleaza pe ACELASI rand,
sistemul .act (desktop = text alaturat; mobil = doua iconite Lucide 44px alaturate).
Implicit (ex. _trimitere_detaliu): un singur buton text, neschimbat. #}

View File

@@ -18,9 +18,13 @@
vin_context — string VIN pentru aria-label cu context (default '')
id_prefix — prefix pentru id="" al input-ului (default 'c'; preview poate folosi 'e-N')
#}
{% macro camp(nome, eticheta, valoare, tip='text', err_map={}, fix_map={}, vin_context='', id_prefix='c') %}
<div style="margin-bottom:10px;">
<label for="{{ id_prefix }}-{{ nome }}" class="muted" style="font-size:12px; display:block;">{{ eticheta }}</label>
{% macro camp(nome, eticheta, valoare, tip='text', err_map={}, fix_map={}, vin_context='', id_prefix='c', slim=False, mono=False) %}
{# slim=False: randare clasica (neschimbata). slim=True: varianta compacta (.camp-slim) din US-002 PRD 5.15:
label 11px muted deasupra, input ~30px, fundal --card2.
mono=True (valid numai cu slim=True): adauga clasa 'camp-mono' pe input pentru campuri
VIN/odometru/nr (IBM Plex Mono, prin .camp-slim .camp-mono din base.html). #}
<div {% if slim %}class="camp-slim"{% else %}style="margin-bottom:10px;"{% endif %}>
<label for="{{ id_prefix }}-{{ nome }}"{% if not slim %} class="muted" style="font-size:12px; display:block;"{% endif %}>{{ eticheta }}</label>
{% if tip == 'date' %}
{# D#10/R3: degradare grijulie pentru valori ne-YYYY-MM-DD.
Daca valoarea nu e in formatul corect, inputul ramane gol + hint + hidden cu valoarea bruta
@@ -28,7 +32,8 @@
{%- set _dp_ok = (valoare and valoare|length == 10 and valoare[4:5] == '-' and valoare[7:8] == '-') -%}
<input id="{{ id_prefix }}-{{ nome }}" type="date" name="{{ nome }}"
value="{{ valoare if _dp_ok else '' }}"
style="width:100%; {% if err_map.get(nome) %}border-color:var(--err);{% endif %}"
{% if slim and mono %}class="camp-mono"{% endif %}
style="{% if not slim %}width:100%; {% endif %}{% if err_map.get(nome) %}border-color:var(--err);{% endif %}"
aria-label="{{ eticheta }}{% if vin_context %} (VIN: {{ vin_context }}){% endif %}"
{% if err_map.get(nome) %}aria-invalid="true"{% endif %}>
{% if not _dp_ok and valoare %}
@@ -38,7 +43,8 @@
{% else %}
<input id="{{ id_prefix }}-{{ nome }}" type="{{ tip }}" name="{{ nome }}"
value="{{ valoare or '' }}"
style="width:100%; {% if err_map.get(nome) %}border-color:var(--err);{% endif %}"
{% if slim and mono %}class="camp-mono"{% endif %}
style="{% if not slim %}width:100%; {% endif %}{% if err_map.get(nome) %}border-color:var(--err);{% endif %}"
{% if vin_context %}aria-label="{{ eticheta }} (VIN: {{ vin_context }})"{% endif %}
{% if err_map.get(nome) %}aria-invalid="true"{% endif %}>
{% endif %}

View File

@@ -37,7 +37,8 @@
<tbody>
{% for e in pending %}
{% set top = e.suggestions[0] if e.suggestions else None %}
{% set preselect = top.cod_prestatie if (top and top.score >= 60) else '' %}
{# L14-S6: pre-selectare din sugestie_principala (GOLD/SILVER/embedding) > fuzzy #}
{% set preselect = e.sugestie_principala.cod_prestatie if e.sugestie_principala else (top.cod_prestatie if (top and top.score >= 60) else '') %}
{# data-dt-row = haystack de cautare (randul contine un <select> cu tot nomenclatorul). #}
<tr data-dt-row="{{ e.cod_op_service }} {{ e.denumire or '' }}
{%- for s in e.suggestions[:3] %} {{ s.cod_prestatie }}{% endfor %}">
@@ -45,6 +46,8 @@
<form id="map-rez-{{ loop.index }}" hx-post="/mapari" hx-target="#mapari-section" hx-swap="outerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
<input type="hidden" name="cod_op_service" value="{{ e.cod_op_service }}">
{# L14-S6: denumire pt record_human_validation in GOLD partajat #}
<input type="hidden" name="denumire" value="{{ e.denumire or '' }}">
</form>
<div><strong>{{ e.cod_op_service }}</strong>
<span class="pill" title="submission-uri blocate">{{ e.blocked }} blocate</span></div>

View File

@@ -4,7 +4,7 @@
hx-swap="outerHTML"
{% if oob %}hx-swap-oob="outerHTML"{% endif %}>
<!-- Cont in asteptare de activare (regasit din vechiul _banner; mereu vizibil) -->
{# Banner cont in asteptare de activare (mereu vizibil cand contul e inactiv) #}
{% if not account_active %}
<div style="margin-bottom:12px; padding:8px 10px; border-left:3px solid var(--warn);
background:color-mix(in srgb, var(--warn) 12%, var(--card)); border-radius:6px; font-size:13px;">
@@ -14,50 +14,68 @@
</div>
{% endif %}
<!-- Rand 1: doua bife binare + ultima autentificare -->
<div style="display:flex; gap:28px; flex-wrap:wrap; align-items:center; font-size:14px;">
{# Bifa: glifa (✓/✗) + culoare + text — accesibil (nu doar culoare, design review) #}
{% macro bifa(ok, text, tip) %}
<span title="{{ tip }}" style="display:inline-flex; align-items:center; gap:7px;">
{% if ok %}
<span class="s-sent" aria-hidden="true" style="font-weight:bold;">&#10003;</span>
<span class="s-sent">{{ text }}</span>
{% else %}
<span class="s-error" aria-hidden="true" style="font-weight:bold;">&#10007;</span>
<span class="s-error">{{ text }}</span>
{% endif %}
</span>
{% endmacro %}
{{ bifa(worker_ok, worker_lbl[0], worker_lbl[1]) }}
{{ bifa(rar_ok, rar_lbl[0], rar_lbl[1]) }}
<span style="display:inline-flex; align-items:center; gap:6px;">
<span class="muted">{{ eticheta_ultima_auth }}:</span>
<span>{{ last_login }}</span>
{# === D6: Strip sanatate mereu-vizibil DEASUPRA contoarelor ===
Verde: worker viu + RAR ok → "Declaratiile curg normal"
Rosu: worker oprit SAU RAR inaccesibil → "Blocat: ... — declaratiile NU pleaca"
Glife accesibile ✓/✗ (nu doar culoare). Layout: glifa+text stanga, ultima auth dreapta.
#}
<div id="strip-sanatate"
role="status"
aria-live="polite"
style="display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;
padding:10px 14px; border-radius:8px; margin-bottom:14px;
{% if sanatate_ok %}background:color-mix(in srgb, var(--ok) 13%, transparent); border:1px solid color-mix(in srgb, var(--ok) 30%, transparent);
{% else %}background:color-mix(in srgb, var(--err) 16%, var(--card)); border:1px solid color-mix(in srgb, var(--err) 40%, transparent);
{% endif %}">
<div style="display:flex; align-items:center; gap:9px;">
{% if sanatate_ok %}
<span aria-hidden="true" style="font-weight:700; font-size:15px; color:var(--ok);">&#10003;</span>
{% else %}
<span aria-hidden="true" style="font-weight:700; font-size:15px; color:var(--err);">&#10007;</span>
{% endif %}
<span style="font-weight:700; font-size:13px;">{{ sanatate_text }}</span>
</div>
<span style="font:400 11px/1.4 'IBM Plex Mono',ui-monospace,monospace; color:var(--muted); white-space:nowrap;">
{{ eticheta_ultima_auth }}: {{ last_login }}
</span>
</div>
<!-- Rand 2: contoare coada -->
<div style="margin-top:10px; display:flex; gap:20px; flex-wrap:wrap; font-size:14px;">
<span><span class="muted">In asteptare:</span> <span class="s-queued">{{ counts_queued }}</span></span>
<span><span class="muted">Declarate la RAR:</span> <span class="s-sent">{{ counts_sent }}</span></span>
<span><span class="muted">Blocate:</span>
<span class="{{ 's-error' if blocate_total else 'muted' }}">{{ blocate_total }}</span>
</span>
{# === D4: 3 carduri-contor (mockup exact: Trimise / In coada / De corectat) ===
Responsive: flex-wrap => 3 pe rand desktop, 2/stivuite pe mobil (min-width:120px).
Trimise: all-time (cifra mare) + sub-linie "luna N · azi N" (D4 + E7).
De corectat: rosu cand >0 (s-error), muted cand 0.
#}
<div style="display:flex; gap:12px; flex-wrap:wrap; margin-bottom:14px;">
{# Trimise (all-time principal, luna/azi secundar) #}
<div class="contor-card" style="flex:1; min-width:120px;">
<div class="contor-cifra">{{ counts_sent }}</div>
<div class="contor-label">Trimise (total)</div>
<div class="contor-sub">luna {{ sent_month }} &middot; azi {{ sent_today }}</div>
</div>
{# In coada (accent/albastru) #}
<div class="contor-card" style="flex:1; min-width:120px;">
<div class="contor-cifra s-queued">{{ counts_queued }}</div>
<div class="contor-label">In coada</div>
</div>
{# De corectat (rosu daca >0, muted la 0; link catre lista) #}
<a href="/" class="contor-card"
style="flex:1; min-width:120px; text-decoration:none; display:block; cursor:pointer;"
aria-label="De corectat: {{ blocate_total }} — click pentru lista de trimiteri">
<div class="contor-cifra {{ 's-error' if blocate_total else 'muted' }}">{{ blocate_total }}</div>
<div class="contor-label">De corectat</div>
</a>
</div>
{# Pill-urile de stare s-au mutat in bara de filtre din sectiunea Trimiteri (_coada.html). #}
{# === Rand 3: navigatie rapida sub contoare (US-005) ===
Linkurile Trimiteri + Mapari apar pe FIECARE pagina sub status-bar.
Marcajul activ vine din variabila de context tab_activ (transmisa de dashboard via ?tab=
sau default 'acasa'). Badge-ul Mapari = mapari_badge (aceeasi sursa: counts.needs_mapping).
{# === Navigatie rapida: Trimiteri + Mapari cu badge needs_mapping ===
Pastrata exact ca inainte (US-005): tab_activ determina marcajul activ.
#}
{% set _tab = tab_activ | default('acasa') %}
<nav class="status-nav" aria-label="Navigatie rapida"
style="margin-top:10px; display:flex; gap:8px 16px; flex-wrap:wrap; font-size:13px; border-top:1px solid var(--line); padding-top:8px;">
style="display:flex; gap:8px 16px; flex-wrap:wrap; font-size:13px; border-top:1px solid var(--line); padding-top:8px;">
<a href="/"
{% if _tab == 'acasa' or _tab == '' %}aria-current="page"{% endif %}
class="status-nav-link{% if _tab == 'acasa' or _tab == '' %} status-nav-activ{% endif %}">Trimiteri</a>

View File

@@ -12,9 +12,22 @@
{# Versiunea datelor cu care s-a randat tabelul; pollerul "Date noi" o compara. #}
<span id="trimiteri-versiune" data-v="{{ versiune_trimiteri | default('') }}" hidden></span>
{% if bulk_message %}
{# Sumar actiune bulk (US-010 PRD 5.15): afisat dupa bulk-fix, disparut la urmatoarea reincarcare. #}
<div class="bulk-message" role="status" aria-live="polite"
style="font-size:13px; color:var(--ink); background:var(--card2);
border:1px solid var(--line); border-radius:6px;
padding:6px 10px; margin-bottom:8px;">
{{ bulk_message }}
</div>
{% endif %}
{% if rows %}
{# Form de stergere bulk. Selectia opereaza DOAR pe randuri blocate
(gestionabil); sent/sending/queued nu au checkbox (read-only). #}
{# Form bulk cu DOUA actiuni: (1) aplica cod RAR la selectate (bulk-fix, US-010),
(2) sterge selectate (sterge-bulk). Selectia opereaza DOAR pe randuri blocate
(gestionabil); sent/sending/queued nu au checkbox (read-only).
Butonul "Aplica cod" foloseste hx-post propriu (override form action).
hx-disinherit="hx-confirm" pe form => butonul aplica-cod NU mosteneste confirmare. #}
<form id="bulk-trimiteri"
hx-post="/trimiteri/sterge-bulk"
hx-target="#submissions-wrap"
@@ -23,30 +36,47 @@
hx-disinherit="hx-confirm"
style="margin:0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div style="display:flex; justify-content:flex-end; margin-bottom:8px;">
<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 (US-010 PRD 5.15) #}
<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);"
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;"
aria-label="Aplica codul RAR la randurile blocate selectate">
Aplica cod
</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;">
style="background:var(--card); color:var(--err); border-color:var(--err);
font-size:13px; padding:4px 10px; border-radius:5px; cursor:pointer;">
Sterge selectate
</button>
</div>
<div class="tablewrap tabel-trimiteri">
<table>
<thead><tr>
<th class="col-chk"><span class="muted" title="Selecteaza randuri blocate">&#10003;</span></th>
<th class="col-id">#</th>
<th class="col-stare">Stare</th>
<th class="col-vehicul">Vehicul</th>
<th class="col-operatie">Operatie</th>
<th class="col-data">Data prestatie</th>
<th class="col-rar">Nr. prezentare RAR</th>
<th class="col-actualizat">Actualizat</th>
</tr></thead>
<tbody>
{# Lista slim trimiteri (US-004, PRD 5.15).
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. #}
<ul class="lista-trimiteri-slim" role="list"
aria-label="Lista trimiteri">
{% for r in rows %}
{# Randul declanseaza deschiderea MODALULUI global (#detaliu-modal-body).
{# 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). #}
<tr id="trimitere-row-{{ r.id }}"
class="trimitere-row"
<li id="trimitere-row-{{ r.id }}"
class="trimitere-slim"
data-detaliu-id="{{ r.id }}"
hx-get="/_fragments/trimitere/{{ r.id }}"
hx-target="#detaliu-modal-body"
@@ -55,47 +85,61 @@
aria-haspopup="dialog"
style="cursor:pointer;"
title="Click pentru detaliul complet">
<td class="col-chk" onclick="event.stopPropagation();">
{# 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();">
{% if r.gestionabil %}
<input type="checkbox" name="submission_id" value="{{ r.id }}"
aria-label="Selecteaza trimiterea #{{ r.id }} pentru stergere">
{% endif %}
</td>
<td class="col-id muted" data-eticheta="#">{{ r.id }}</td>
<td class="col-stare" data-eticheta="Stare">
<span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}">{{ r.stare_scurt }}</span>
{# Eticheta umana scurta sub pill — text mic, `s-error` pe error/needs_*
(singurele stari pe care `eticheta_problema` e ne-goala).
Stare transmisa prin TEXT, nu doar culoare. Codul brut ramane in modal. #}
{% 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 %}
</td>
<td class="col-vehicul" data-eticheta="Vehicul">
{{ r.prez.vehicul_nr }}
</div>
{# Bloc text principal — stanga, ocupa spatiul ramas #}
<div style="flex:1 1 auto; min-width:0;">
{# Linia 1: VIN mono scurt (slim-vin).
Guard: vin_scurt='—' inseamna VIN lipsa; fallback la vehicul_nr. #}
{% if r.prez.vin_scurt and r.prez.vin_scurt != '—' %}
{# VIN pe rand separat sub nr (element block, nu span inline) #}
<div class="muted" style="font-size:12px;">{{ r.prez.vin_scurt }}</div>
{% endif %}
</td>
<td class="col-operatie" data-eticheta="Operatie">
<div>{{ r.prez.operatie }}</div>
{# Doar codul RAR (ex. OE-2), FARA prefixul "cod RAR:" — chip muted discret;
cand nemapat afiseaza "nemapat" muted. #}
{% if r.prez.cod_rar and r.prez.cod_rar != '—' %}
<div class="cod-rar-sub"><span class="cod-rar-cod">{{ r.prez.cod_rar }}</span></div>
<div class="slim-vin">{{ r.prez.vin_scurt }}</div>
{% else %}
<div class="muted cod-rar-sub">nemapat</div>
<div class="slim-vin muted">{{ r.prez.vehicul_nr }}</div>
{% endif %}
</td>
<td class="col-data" data-eticheta="Data prestatie">{{ r.prez.data_prestatie }}</td>
<td class="col-rar" data-eticheta="Nr. prezentare RAR">{{ r.id_prezentare or '—' }}</td>
<td class="col-actualizat muted" data-eticheta="Actualizat">{{ r.updated_at }}</td>
</tr>
{# Linia 2: Operatie · ora/data (slim-meta muted) #}
<div class="slim-meta">{{ r.prez.operatie }} · {{ r.updated_at }}</div>
{# Cod RAR sau indicatorul 'nemapat': discret sub operatie.
Mentine compatibilitatea cu testele cod_rar: OE-2 vizibil, fara prefix 'cod RAR:'. #}
{% if r.prez.cod_rar and r.prez.cod_rar != '—' %}
<div class="slim-meta"><span class="cod-rar-cod">{{ r.prez.cod_rar }}</span></div>
{% else %}
<div class="slim-meta muted cod-rar-sub">nemapat</div>
{% endif %}
{# Linia meta discreta: nr inmatriculare · data prestatie · nr prezentare RAR.
Accesibila pe rand; informatia completa e in modalul de detaliu. #}
<div class="slim-meta" style="opacity:0.7;">
{{ r.prez.vehicul_nr -}}
{%- if r.prez.data_prestatie and r.prez.data_prestatie != '—' %} · {{ r.prez.data_prestatie }}{% endif -%}
{%- if r.id_prezentare %} · #{{ r.id_prezentare }}{% endif %}
</div>
{# Eticheta umana scurta sub pill — text mic, s-error pe error/needs_*.
Afisata DOAR pe randuri cu problema (eticheta_problema ne-goala).
Starea transmisa prin TEXT, nu doar culoare. #}
{% if r.eticheta_problema and r.eticheta_problema != r.stare_scurt and r.eticheta_problema != r.stare_text %}
<div class="eticheta-problema s-error" style="font-size:10px; margin-top:2px;">{{ r.eticheta_problema }}</div>
{% endif %}
</div>
{# Pill de stare — dreapta, flex:none #}
<span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}"
style="flex:0 0 auto; white-space:nowrap;">{{ r.stare_scurt }}</span>
</li>
{% endfor %}
</tbody>
</table>
</div>
</ul>
</form>
{#

View File

@@ -106,32 +106,10 @@
hx-disabled-elt="find button">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
{# Select cod RAR pe stari editabile (needs_data/needs_mapping), cu nomenclator.
Read-only pe sent/sending/queued/error (nomenclator_rar gol → ramura else).
RAMANE in _trimitere_detaliu.html (D#5 — logica specifica acestui modal). #}
{% if nomenclator_rar %}
<div style="margin:0 0 12px;">
<label for="c-cod-prestatie" class="muted" style="font-size:12px; display:block;">Operatie RAR (cod prestatie)</label>
{% if prez.operatie and prez.operatie != '—' %}
<div class="muted" style="font-size:12px; margin-bottom:4px;">{{ prez.operatie }}</div>
{% endif %}
<select id="c-cod-prestatie" name="cod_prestatie" style="max-width:380px; width:100%;"
aria-label="Alege operatia RAR din nomenclator">
<option value="">— pastrat ({{ cod_afis }}) —</option>
{% for n in nomenclator_rar %}
<option value="{{ n.cod_prestatie }}" {% if n.cod_prestatie == cod_prestatie_curent %}selected{% endif %}>
{{ n.cod_prestatie }} — {{ n.nume_prestatie }}
</option>
{% endfor %}
</select>
</div>
{% else %}
{# Operatie + cod RAR read-only deasupra campurilor (fara eticheta „Cod RAR"). #}
<div style="margin:0 0 12px;">
<div class="muted" style="font-size:12px;">Operatie</div>
<div>{{ prez.operatie }} &middot; {{ cod_afis }}</div>
</div>
{% endif %}
{# Cleanup B (US-009 PRD 5.15): vechiul <select name="cod_prestatie"> eliminat.
Chips din _form_editare.html (via _chips_prestatii.html) il inlocuiesc complet:
emit hidden inputs name="cod_prestatie" + picker per-operatie (E4, US-007).
post_corectie_trimitere foloseste form.getlist("cod_prestatie") → compatibil. #}
{# Operatie service (cod intern + denumire venita prin API/import), distinct de
operatia RAR mapata. op_service_cod="" cand lipseste → randul absent.
@@ -190,7 +168,7 @@
{% for item in nomenclator_rar %}
<option value="{{ item.cod_prestatie }}"
{% if item.cod_prestatie == cod_prestatie_curent %}selected{% endif %}>
{{ item.cod_prestatie }} — {{ item.nome_prestatie }}
{{ item.cod_prestatie }} — {{ item.nume_prestatie }}
</option>
{% endfor %}
</select>

View File

@@ -16,13 +16,16 @@
<script>
// Anti-FOUC: citeste preferinta tema din localStorage inainte de primul
// paint; seteaza data-theme pe <html> sincron, fara blink.
// Cunoaste toate cele 4 teme: light/dark/petrol/auto. Valoare legacy/necunoscuta -> auto.
// 'auto' se rezolva la 'light' sau 'dark' dupa prefers-color-scheme (fara blink).
// Cunoaste TOATE cele 7+1 teme: light/dark/petrol/grafit/cobalt/cupru/hartie + auto.
// Valori legacy (light/dark/petrol) raman valide — fara migrare fortata.
// Valoare lipsa/necunoscuta -> auto (fallback sigur, fara blink).
// 'auto' se rezolva la 'light' sau 'dark' dupa prefers-color-scheme (fara blink):
// auto + dark OS -> 'dark' | auto + light OS -> 'light' (comportament existent pastrat).
(function() {
var VALID = {light:1, dark:1, petrol:1, auto:1};
var VALID = {light:1, dark:1, petrol:1, grafit:1, cobalt:1, cupru:1, hartie:1, auto:1};
try {
var t = localStorage.getItem('theme');
if (!t || !VALID[t]) t = 'auto'; // fallback: valoare lipsa sau legacy -> auto
if (!t || !VALID[t]) t = 'auto'; // fallback: valoare lipsa sau necunoscuta -> auto
if (t === 'auto') {
t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
@@ -100,16 +103,32 @@
src: url("/static/fonts/IBMPlexMono-Regular-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+20AC, U+2122, U+FEFF, U+FFFD;
}
/* Paleta dark (default) — accent azur ROMFAST */
:root { --bg:#0f1218; --card:#181c24; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36;
/* Paleta dark (default) — accent azur ROMFAST.
--card2: fundal input/contor (= --bg, nivelul cel mai adanc).
--line2: separator subtire (intre --bg si --line). */
:root { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#2E74D6; }
/* Paleta light — accent azur inchis pentru contrast AA pe alb (#1F66C9: 5.51:1 pe alb) */
[data-theme="light"] { --bg:#f5f7fa; --card:#ffffff; --ink:#1a1d24; --muted:#5c6473; --line:#e2e5ea;
[data-theme="light"] { --bg:#f5f7fa; --card:#ffffff; --card2:#f5f7fa; --ink:#1a1d24; --muted:#5c6473; --line:#e2e5ea; --line2:#eaedf2;
--ok:#15803d; --warn:#b45309; --err:#dc2626; --accent:#1F66C9; }
/* Paleta Petrol — tema intunecata alternativa, accent teal #0E7C7B.
Wordmark-ul FAST #2E74D6 coexista armonios: ambele sunt reci/saturate, contrast AA pe --card #161e20. */
[data-theme="petrol"] { --bg:#0e1416; --card:#161e20; --ink:#e6e9ef; --muted:#8b93a7; --line:#232c2e;
[data-theme="petrol"] { --bg:#0e1416; --card:#161e20; --card2:#0e1416; --ink:#e6e9ef; --muted:#8b93a7; --line:#232c2e; --line2:#1c2426;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#0E7C7B; }
/* Paleta Grafit — similara cu dark, accent azur deschis (#6ea2ec = landing --infot).
Distinta de dark la cererea userului (D2 PRD 5.15). */
[data-theme="grafit"] { --bg:#0f1218; --card:#181c24; --card2:#0f1218; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36; --line2:#1f2530;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#6ea2ec; }
/* Paleta Cobalt — fundal bleumarin adanc, accent albastru viu (#8aa0ff = landing --infot). */
[data-theme="cobalt"] { --bg:#080d1c; --card:#111a33; --card2:#0b1226; --ink:#e9ecfb; --muted:#8a93b8; --line:#1d2747; --line2:#161f3a;
--ok:#2fd0a6; --warn:#E0A93B; --err:#f06a7a; --accent:#8aa0ff; }
/* Paleta Cupru — fundal cald ciocolata, accent chihlimbar (#dfa45c = landing --infot). */
[data-theme="cupru"] { --bg:#15110b; --card:#211a12; --card2:#15110b; --ink:#efe6d6; --muted:#a89a85; --line:#36291c; --line2:#281e14;
--ok:#67b98c; --warn:#c97d2e; --err:#e2685a; --accent:#dfa45c; }
/* Paleta Hartie — fundal crem cald, accent albastru clasic (#1F5FBF = landing --infot = --accent).
Similara cu light, distinta la cererea userului (D2 PRD 5.15). */
[data-theme="hartie"] { --bg:#f3efe6; --card:#fffdf7; --card2:#f3efe6; --ink:#1e1a13; --muted:#6a6052; --line:#e2dccc; --line2:#ece6d9;
--ok:#1c7d5d; --warn:#b45309; --err:#bd463c; --accent:#1F5FBF; }
* { box-sizing:border-box; }
/* CONVENTIE BREAKPOINT: un singur prag mobil la 768px.
CSS custom properties NU functioneaza in `@media`, deci pragul nu poate fi o
@@ -675,6 +694,62 @@
.sticky-bar { padding:10px 12px; gap:10px; }
.sticky-bar button { width:100%; min-height:44px; }
}
/* === SENTINEL-COMPONENTE-SLIM: inceput componente slim US-002 (PRD 5.15).
Testele ancoreaza pe acest marker. Nu muta/sterge. === */
/* .contor-card — card cifra contor: fundal --card2, bordura --line, radius 8px, padding 10-12px.
Variante de culoare a cifrei prin clasele .s-* existente (verde/accent/rosu). */
.contor-card { background:var(--card2); border:1px solid var(--line); border-radius:8px; padding:10px 12px; }
.contor-cifra { font-size:22px; font-weight:700; line-height:1; }
.contor-label { font-size:11px; color:var(--muted); margin-top:5px; }
.contor-sub { font-family:"IBM Plex Mono",ui-monospace,monospace; font-size:10px; color:var(--muted); margin-top:3px; }
/* .lista-trimiteri-slim + .trimitere-slim — lista compacta cu separator --line2.
Randul e clickabil (rol button), tinta min-height:44px pe mobil. */
.lista-trimiteri-slim { list-style:none; margin:0; padding:0; }
.trimitere-slim { display:flex; align-items:center; justify-content:space-between; gap:12px;
padding:11px 14px; 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:"IBM Plex Mono",ui-monospace,monospace; font-size:13px; font-weight:500; color:var(--ink); }
.slim-meta { font-size:11px; color:var(--muted); margin-top:3px; }
/* .camp-slim — varianta compacta camp formular: label 11px muted deasupra, input ~30px, fundal --card2.
Mono pentru campuri VIN/odometru/nr: adauga clasa .camp-mono pe input. */
.camp-slim { margin-bottom:8px; }
.camp-slim label { font-size:11px; color:var(--muted); display:block; margin-bottom:4px; }
.camp-slim input, .camp-slim textarea, .camp-slim select { background:var(--card2); height:30px; width:100%;
padding:0 10px; border:1px solid var(--line); border-radius:6px; font:inherit; color:var(--ink); }
.camp-slim textarea { height:auto; min-height:48px; padding:8px 10px; resize:vertical; }
.camp-slim .camp-mono { font-family:"IBM Plex Mono",ui-monospace,monospace; font-size:12px; }
/* .chips + .chip — prestatii multi-select cu buton de stergere accesibil (.chip-del).
Fundal accent 18%, font IBM Plex Mono 11px. */
.chips { min-height:30px; display:flex; align-items:center; gap:6px; flex-wrap:wrap;
padding:4px 8px; border:1px solid var(--line); border-radius:6px; background:var(--card2); }
.chip { display:inline-flex; align-items:center; gap:5px; padding:3px 8px; border-radius:5px;
background:color-mix(in srgb, var(--accent) 18%, transparent); color:var(--accent);
font-family:"IBM Plex Mono",ui-monospace,monospace; font-size:11px; font-weight:600; }
.chip .chip-del { background:transparent; border:none; color:inherit; opacity:.7; cursor:pointer;
padding:0; font-size:13px; line-height:1; display:inline-flex;
align-items:center; justify-content:center; min-width:16px; min-height:16px; }
.chip .chip-del:hover, .chip .chip-del:focus-visible { opacity:1; }
.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); }
/* .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));
border-radius:5px; color:var(--accent); font:500 10px inherit; cursor:pointer; }
.add-code:hover, .add-code:focus-visible { border-style:solid; }
/* .op-row — rand operatie cu picker op<->cod (E4): operatie + chip cod + picker */
.op-row { display:flex; align-items:center; justify-content:space-between; gap:10px;
padding:8px 10px; border:1px solid var(--line); border-radius:6px;
background:var(--card2); margin-bottom:8px; }
.op-row-name { font-size:12px; font-weight:500; 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) */
@media (max-width:767px) {
.trimitere-slim { padding:12px 14px; }
}
/* === SENTINEL-COMPONENTE-SLIM: sfarsit componente slim US-002 === */
</style>
</head>
<body>
@@ -749,18 +824,36 @@
</div>
</div>
<script>
// Comutator tema ciclic: click cicleaza Light->Dark->Petrol->Auto.
// Separare init (sincronizare iconita/label) de persistenta (doar la click explicit).
// 'auto' se rezolva la paint prin anti-FOUC; aici setam data-theme rezolvat.
// Comutator tema ciclic (DRY E2 — PRD 5.15): config traieste intr-o singura structura
// sursa-de-adevar THEMES din care se DERIVA CYCLE/VALID/ICONS/LABELS/NEXT.
// Adaugarea unei teme noi = O singura intrare in THEMES.
// Ciclu: Light->Dark->Petrol->Grafit->Cobalt->Cupru->Hartie->Auto->(inapoi la Light).
// 'auto' se rezolva la paint prin anti-FOUC (dark OS -> 'dark', light OS -> 'light').
(function() {
var btn = document.getElementById('tema-toggle');
if (!btn) return;
var CYCLE = ['light', 'dark', 'petrol', 'auto'];
var VALID = {light:1, dark:1, petrol:1, auto:1};
// Iconite per tema: ☀ Light, ☾ Dark, ◐ Petrol, ◉ Auto
var ICONS = {light:'&#9728;', dark:'&#9790;', petrol:'&#9680;', auto:'&#9689;'};
var LABELS = {light:'Light', dark:'Dark', petrol:'Petrol', auto:'Auto'};
var NEXT = {light:'Dark', dark:'Petrol', petrol:'Auto', auto:'Light'};
// SURSA DE ADEVAR UNICA: adaugarea unei teme = o singura intrare aici.
// Iconite: ☀ Light | ☾ Dark | ◐ Petrol | ◑ Grafit | ◆ Cobalt | ◇ Cupru | ○ Hartie | ◉ Auto
var THEMES = [
{id:'light', label:'Light', icon:'&#9728;'},
{id:'dark', label:'Dark', icon:'&#9790;'},
{id:'petrol', label:'Petrol', icon:'&#9680;'},
{id:'grafit', label:'Grafit', icon:'&#9681;'},
{id:'cobalt', label:'Cobalt', icon:'&#9670;'},
{id:'cupru', label:'Cupru', icon:'&#9671;'},
{id:'hartie', label:'Hartie', icon:'&#9675;'},
{id:'auto', label:'Auto', icon:'&#9689;'},
];
// Derivate din THEMES (nu literali separati — DRY E2):
var CYCLE = THEMES.map(function(t) { return t.id; });
var VALID = THEMES.reduce(function(a, t) { a[t.id] = 1; return a; }, {});
var ICONS = THEMES.reduce(function(a, t) { a[t.id] = t.icon; return a; }, {});
var LABELS = THEMES.reduce(function(a, t) { a[t.id] = t.label; return a; }, {});
var NEXT = (function() {
var n = {};
THEMES.forEach(function(t, i) { n[t.id] = THEMES[(i + 1) % THEMES.length].label; });
return n;
})();
function _stored() {
try { var v = localStorage.getItem('theme'); return (v && VALID[v]) ? v : 'auto'; } catch(e) { return 'auto'; }
}
@@ -1049,7 +1142,7 @@
document.body.addEventListener('htmx:beforeRequest', function(evt) {
var elt = evt.detail && evt.detail.elt;
if (!elt || !elt.classList) return;
if (elt.classList.contains('trimitere-row') || elt.classList.contains('btn-editeaza')) open(elt);
if (elt.classList.contains('trimitere-row') || elt.classList.contains('trimitere-slim') || elt.classList.contains('btn-editeaza')) open(elt);
});
// Dupa swap-ul fragmentului (sau re-render corectie/mapare): muta focusul in modal.
body.addEventListener('htmx:afterSettle', function() {
@@ -1083,7 +1176,7 @@
// Tastatura pe rand (role=button): Enter/Space deschid modalul.
document.body.addEventListener('keydown', function(evt) {
var t = evt.target;
if (!(t && t.classList && t.classList.contains('trimitere-row'))) return;
if (!(t && t.classList && (t.classList.contains('trimitere-row') || t.classList.contains('trimitere-slim')))) return;
if (evt.key === 'Enter' || evt.key === ' ' || evt.key === 'Spacebar') {
evt.preventDefault();
t.click();