feat(5.10): UX trimiteri (pill filtre, paginare, editare) + Mapari in meniu + branding ROMFAST
14 stories TDD prin echipa de workeri (lead orchestreaza, 3 teammates pe valuri cu fisiere disjuncte; routes.py + base.html serializate ca fisiere fierbinti). - US-001 fix filtrare data (_iso_date_prefix pe garda+comparatie, prinde timestamp cu ora) - US-002/007 operatie service distincta in payload_view + afisare in detaliu - US-003 pill-uri categorii (button/aria-pressed; needs_mapping --warn, needs_data/error --err); fara lista ID-uri/dropdown - US-004 paginare numerotata 25/pag (total ramificat SQL-COUNT vs fetch-all+slice, clamp page, poll pastreaza pagina) - US-005 VIN block-level sub nr - US-006/006b editare cod RAR + validare nomenclator + recalcul idempotency (needs_data/needs_mapping via /corecteaza, error via /repune) - US-008 card eroare 3-niveluri doar pe read-only + rezumat top-of-form - US-009 Mapari in meniu hamburger; scoatere tab-bar + role=tablist orfan - US-010/011 pagina Mapari consolidata + butoane icon SVG + dirty-state (fara kebab/emoji) - US-012/012b header centrat + logo ROMFAST (/static/romfast_logo.png) in header - US-013 paleta azur ROMFAST (#2E74D6/#1F66C9) + IBM Plex Sans/Mono self-host (woff2 reale) - US-014 selector tema ciclic Light/Dark/Petrol/Auto + anti-FOUC pe 4 stari Backend trimitere (worker/masina stari/idempotenta/mapping) + schema NEATINSE (UI/UX pur + 1 fix de filtrare). VERIFY context curat PASS; /code-review high: 1 finding material reparat (US-006b). Regresie 896 passed, 1 skipped, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,21 +28,13 @@
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="submit, change, keyup delay:400ms from:input[name='vehicul']"
|
||||
style="display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end; margin-bottom:12px;">
|
||||
<div>
|
||||
<label for="f-status" class="muted" style="display:block; font-size:12px;">Stare</label>
|
||||
{# US-014/T13: status_filtru (din deep-link ?tab=acasa&status=) pre-selecteaza
|
||||
starea, iar submissions-wrap (hx-include #filtre-trimiteri) o incarca filtrat. #}
|
||||
{% set sf = status_filtru | default('') %}
|
||||
<select id="f-status" name="status">
|
||||
<option value="" {% if not sf %}selected{% endif %}>toate</option>
|
||||
<option value="queued" {% if sf == 'queued' %}selected{% endif %}>in asteptare</option>
|
||||
<option value="sent" {% if sf == 'sent' %}selected{% endif %}>declarate la RAR</option>
|
||||
<option value="needs_mapping" {% if sf == 'needs_mapping' %}selected{% endif %}>lipsa cod</option>
|
||||
<option value="needs_data" {% if sf == 'needs_data' %}selected{% endif %}>date incomplete</option>
|
||||
<option value="error" {% if sf == 'error' %}selected{% endif %}>eroare</option>
|
||||
<option value="sending" {% if sf == 'sending' %}selected{% endif %}>se trimite</option>
|
||||
</select>
|
||||
</div>
|
||||
{# US-003 (PRD 5.10): dropdown status eliminat — inlocuit cu pill-uri in bara de status.
|
||||
Filtrul de stare vine de la pill-uri (/_fragments/submissions?status=X direct).
|
||||
Camp hidden permite reset stare la submit manual din form (Filtreaza). #}
|
||||
<input type="hidden" name="status" value="{{ status_filtru | default('') }}">
|
||||
{# US-004 (PRD 5.10): pagina curenta — actualizata prin OOB swap din _submissions.html.
|
||||
Poll-ul (hx-include="#filtre-trimiteri") include automat pagina curenta (L2 PRD). #}
|
||||
<input type="hidden" id="f-page" name="page" value="1">
|
||||
<div>
|
||||
<label for="f-vehicul" class="muted" style="display:block; font-size:12px;">Vehicul (nr/VIN)</label>
|
||||
<input id="f-vehicul" type="text" name="vehicul" placeholder="ex. B123 sau VIN" style="max-width:180px;">
|
||||
|
||||
@@ -21,26 +21,11 @@
|
||||
{# US-005 (5.5): antet standard + link Ajutor ca <details> nativ (fara JS). Toata proza
|
||||
care inainte se repeta inline (scopul maparilor, Auto/Manual) traieste acum AICI,
|
||||
o singura data, ascunsa implicit. #}
|
||||
{# US-010: sectiunea de ajutor (details.ajutor-mapari) eliminata.
|
||||
Empty-state „Nicio operatie nemapata" eliminat — sectiunea ramane goala (fara text). #}
|
||||
<h2 style="font-size:15px; margin:0 0 8px;">De rezolvat</h2>
|
||||
<details class="ajutor-mapari" style="margin:0 0 12px;">
|
||||
<summary class="cardlink" style="display:inline-flex; color:var(--accent); cursor:pointer; padding:4px 0;">Ajutor</summary>
|
||||
<div class="muted" style="font-size:13px; margin-top:8px; max-width:680px;">
|
||||
Maparile leaga o operatie din softul tau (cod intern ROAAUTO) de un cod RAR oficial.
|
||||
Operatiile necunoscute raman blocate in <span class="s-needs_mapping">needs_mapping</span>
|
||||
si NU pleaca la RAR pana le mapezi. Sugestiile (%) vin din potrivire fuzzy pe denumire —
|
||||
verifica-le inainte sa salvezi. <strong>In coada</strong>: <strong>Auto</strong> = la
|
||||
urmatoarele fisiere cu aceasta operatie randurile intra automat in coada;
|
||||
<strong>Manual</strong> = raman pentru verificare, nimic nu pleaca la RAR pana confirmi.
|
||||
La schimbarea unui cod, submission-urile blocate pe acea operatie se re-rezolva automat.
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{% if not pending %}
|
||||
<div class="empty">
|
||||
Nicio operatie nemapata — tot ce a venit s-a tradus in coduri RAR.
|
||||
<a href="/?tab=acasa">Importa un fisier nou</a> daca vrei sa adaugi prezentari.
|
||||
</div>
|
||||
{% else %}
|
||||
{% if pending %}
|
||||
<div data-dt="10">
|
||||
<div class="dt-tools">
|
||||
<input type="search" data-dt-search class="dt-search"
|
||||
@@ -165,15 +150,24 @@
|
||||
{{ ui.autosend_toggle(form_id="map-salv-" ~ loop.index, checked=m.auto_send) }}
|
||||
</td>
|
||||
<td style="text-align:right; white-space:nowrap;" data-eticheta="Actiuni">
|
||||
{# Salveaza/Sterge in meniu contextual (kebab) — randul ramane ingust. Butoanele se
|
||||
leaga prin form= de cele doua form-uri hx-post definite in prima celula a randului. #}
|
||||
<details class="kebab">
|
||||
<summary aria-label="Actiuni pentru {{ m.cod_op_service }}">⋯</summary>
|
||||
<div class="kebab-menu">
|
||||
<button type="submit" form="map-salv-{{ loop.index }}">Salveaza</button>
|
||||
<button type="submit" form="map-del-{{ loop.index }}" class="danger">Sterge</button>
|
||||
</div>
|
||||
</details>
|
||||
{# US-011: butoane icon mereu vizibile (fara kebab). SVG aria-hidden; aria-label pe buton.
|
||||
data-dirty-form e citit de JS din base.html: la schimbarea select-ului din acelasi rand,
|
||||
JS adauga clasa "dirty" pe butonul de salvare (fundal --accent = modificari nesalvate). #}
|
||||
<button type="submit" form="map-salv-{{ loop.index }}"
|
||||
class="icon-btn"
|
||||
data-dirty-form="map-salv-{{ loop.index }}"
|
||||
aria-label="Salveaza maparea pentru {{ m.cod_op_service }}">
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
<path d="M2 2a1 1 0 011-1h7.5L13 3.5V14a1 1 0 01-1 1H3a1 1 0 01-1-1V2zm5 10a2 2 0 100-4 2 2 0 000 4zM3 3v3h6V3H3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="submit" form="map-del-{{ loop.index }}"
|
||||
class="icon-btn danger"
|
||||
aria-label="Sterge maparea pentru {{ m.cod_op_service }}">
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
||||
<path d="M5.5 5.5A.5.5 0 016 6v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm2.5 0a.5.5 0 01.5.5v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm3 .5a.5.5 0 00-1 0v6a.5.5 0 001 0V6zM14 3a1 1 0 01-1 1H3a1 1 0 110-2h3.5l1-1h2l1 1H13a1 1 0 011 1zm-1 1H3v9a1 1 0 001 1h8a1 1 0 001-1V4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -187,79 +181,8 @@
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Sectiunea 3: Formate de coloane salvate (column_mappings) -->
|
||||
<!-- ============================================================ -->
|
||||
<div class="card">
|
||||
<h2 style="font-size:15px; margin:0 0 12px;">Formate de coloane salvate</h2>
|
||||
|
||||
{% if not column_formats %}
|
||||
<div class="empty">
|
||||
Niciun format de coloane salvat inca. La primul import, maparea coloanelor fisierului
|
||||
se retine aici si se reaplica automat la fisierele cu acelasi antet.
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted" style="margin:0 0 12px; font-size:13px;">
|
||||
Antetele de fisier recunoscute. Un fisier cu alte coloane = format nou separat.
|
||||
</p>
|
||||
|
||||
<div data-dt="10">
|
||||
<div class="dt-tools">
|
||||
<input type="search" data-dt-search class="dt-search"
|
||||
placeholder="Cauta coloana sau camp..." aria-label="Cauta in formatele de coloane">
|
||||
</div>
|
||||
<div class="tablewrap tabel-card">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Coloane</th>
|
||||
<th>Mapari (coloana → camp)</th>
|
||||
<th>Format data</th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for f in column_formats %}
|
||||
<tr>
|
||||
<td style="white-space:nowrap;" data-eticheta="Coloane">
|
||||
<strong>{{ f.columns | length }} coloane</strong>
|
||||
</td>
|
||||
<td class="muted" style="font-size:12px; white-space:normal; max-width:340px;" data-eticheta="Mapari (coloana → camp)">
|
||||
{% for col, camp in f.mappings.items() %}
|
||||
<span class="sugg">{{ col }}</span> → {{ camp }}{% if not loop.last %}; {% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td data-eticheta="Format data">
|
||||
<form id="fmt-edit-{{ loop.index }}" hx-post="/formate-coloane/editeaza"
|
||||
hx-target="#mapari-section" hx-swap="outerHTML"
|
||||
style="display:flex; gap:6px; align-items:center;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||
<input type="hidden" name="format_id" value="{{ f.id }}">
|
||||
<input type="text" name="format_data" value="{{ f.format_data or '' }}"
|
||||
placeholder="ex. DD.MM.YYYY" aria-label="Format data" style="max-width:130px;">
|
||||
<button type="submit">Salveaza data</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form hx-post="/formate-coloane/sterge" hx-target="#mapari-section" hx-swap="outerHTML"
|
||||
hx-confirm="Stergi acest format de coloane?">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||
<input type="hidden" name="format_id" value="{{ f.id }}">
|
||||
<button type="submit" style="background:var(--card); color:var(--err); border-color:var(--err);">
|
||||
Sterge
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dt-empty" data-dt-empty style="display:none;">Niciun format nu se potriveste cautarii.</div>
|
||||
<div class="dt-pager" data-dt-pager></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Sectiunea 4: Reguli automate pe text (operation_text_rules) -->
|
||||
<!-- Sectiunea 3: Reguli automate pe text (operation_text_rules) -->
|
||||
<!-- US-010: mutata pe pozitia 3 (inainte de Formate de coloane) -->
|
||||
<!-- ============================================================ -->
|
||||
<div class="card">
|
||||
<h2 style="font-size:15px; margin:0 0 8px;">Reguli automate (text)</h2>
|
||||
@@ -354,4 +277,77 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Sectiunea 4: Formate de coloane salvate (column_mappings) -->
|
||||
<!-- US-010: mutata pe pozitia 4 (dupa Reguli automate) -->
|
||||
<!-- ============================================================ -->
|
||||
<div class="card">
|
||||
<h2 style="font-size:15px; margin:0 0 12px;">Formate de coloane salvate</h2>
|
||||
|
||||
{% if not column_formats %}
|
||||
<div class="empty">
|
||||
Niciun format de coloane salvat inca. La primul import, maparea coloanelor fisierului
|
||||
se retine aici si se reaplica automat la fisierele cu acelasi antet.
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted" style="margin:0 0 12px; font-size:13px;">
|
||||
Antetele de fisier recunoscute. Un fisier cu alte coloane = format nou separat.
|
||||
</p>
|
||||
|
||||
<div data-dt="10">
|
||||
<div class="dt-tools">
|
||||
<input type="search" data-dt-search class="dt-search"
|
||||
placeholder="Cauta coloana sau camp..." aria-label="Cauta in formatele de coloane">
|
||||
</div>
|
||||
<div class="tablewrap tabel-card">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Coloane</th>
|
||||
<th>Mapari (coloana → camp)</th>
|
||||
<th>Format data</th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for f in column_formats %}
|
||||
<tr>
|
||||
<td style="white-space:nowrap;" data-eticheta="Coloane">
|
||||
<strong>{{ f.columns | length }} coloane</strong>
|
||||
</td>
|
||||
<td class="muted" style="font-size:12px; white-space:normal; max-width:340px;" data-eticheta="Mapari (coloana → camp)">
|
||||
{% for col, camp in f.mappings.items() %}
|
||||
<span class="sugg">{{ col }}</span> → {{ camp }}{% if not loop.last %}; {% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td data-eticheta="Format data">
|
||||
<form id="fmt-edit-{{ loop.index }}" hx-post="/formate-coloane/editeaza"
|
||||
hx-target="#mapari-section" hx-swap="outerHTML"
|
||||
style="display:flex; gap:6px; align-items:center;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||
<input type="hidden" name="format_id" value="{{ f.id }}">
|
||||
<input type="text" name="format_data" value="{{ f.format_data or '' }}"
|
||||
placeholder="ex. DD.MM.YYYY" aria-label="Format data" style="max-width:130px;">
|
||||
<button type="submit">Salveaza data</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form hx-post="/formate-coloane/sterge" hx-target="#mapari-section" hx-swap="outerHTML"
|
||||
hx-confirm="Stergi acest format de coloane?">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||
<input type="hidden" name="format_id" value="{{ f.id }}">
|
||||
<button type="submit" style="background:var(--card); color:var(--err); border-color:var(--err);">
|
||||
Sterge
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="dt-empty" data-dt-empty style="display:none;">Niciun format nu se potriveste cautarii.</div>
|
||||
<div class="dt-pager" data-dt-pager></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
<style>
|
||||
/* Pill-uri categorii blocate (US-003 PRD 5.10)
|
||||
Culoarea e injectata inline (color_var: --warn / --err) dupa DESIGN.md:
|
||||
Lipsa cod = --warn (chihlimbar), Date incomplete + Eroare = --err (rosu).
|
||||
Activ = fundal pe culoarea categoriei (NU accent albastru — S1/A5). */
|
||||
.pill-cat { transition: background 0.15s, color 0.15s; }
|
||||
.pill-cat:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
||||
/* Activ: background = culoarea categoriei (currentColor = var(--err/--warn) din inline style),
|
||||
text = var(--card) (contrast AA). NU accent albastru (S1/A5 DESIGN.md). */
|
||||
.pill-cat[aria-pressed="true"] { background: currentColor !important; color: var(--card) !important; border-color: currentColor !important; }
|
||||
.pill-cat[aria-pressed="true"] span { background: var(--card) !important; color: currentColor; }
|
||||
</style>
|
||||
<div id="status-bar" class="status-bar card"
|
||||
hx-get="/_fragments/status"
|
||||
hx-trigger="every 15s"
|
||||
@@ -47,36 +59,49 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Necesita atentia ta (US-014): categorii actionabile — link la lista filtrata
|
||||
+ identificatorii primelor randuri blocate. Se randeaza DOAR daca exista randuri
|
||||
blocate; cand contorul ajunge 0 (sters/re-pus/purjat), sectiunea dispare. -->
|
||||
{% if blocate_actionabil %}
|
||||
<!-- Pill-uri categorii blocate (US-003 PRD 5.10): inlocuiesc lista de ID-uri.
|
||||
<button> reale cu aria-pressed, focalizabile, activare Enter/Space.
|
||||
Inactiv = contur+text pe culoarea categoriei; activ = umplere pe culoarea categoriei.
|
||||
Pill ascuns cand n=0 (lista pills_categorii filtreaza deja). -->
|
||||
{% if pills_categorii %}
|
||||
<div style="margin-top:12px; padding-top:10px; border-top:1px solid var(--line);">
|
||||
<div style="font-size:13px; font-weight:600; margin-bottom:8px;">Necesita atentia ta</div>
|
||||
<div style="display:flex; gap:18px; flex-wrap:wrap;">
|
||||
{% for cat in blocate_actionabil %}
|
||||
<div style="min-width:200px;">
|
||||
{# Link: filtreaza lista Trimiteri pe aceasta stare (HTMX in-place) cu fallback
|
||||
deep-link server-side (?tab=acasa&status=...). #}
|
||||
<a class="{{ cat.eticheta[2] }}" style="font-size:13px; font-weight:600; text-decoration:none;"
|
||||
href="/?tab=acasa&status={{ cat.status }}"
|
||||
hx-get="/_fragments/submissions?status={{ cat.status }}"
|
||||
hx-target="#submissions-wrap" hx-swap="innerHTML"
|
||||
onclick="var s=document.getElementById('trimiteri-section'); if(s) s.scrollIntoView({behavior:'smooth'});">
|
||||
{{ cat.eticheta[0] }} ({{ cat.n }}) ›
|
||||
</a>
|
||||
<ul style="list-style:none; margin:6px 0 0; padding:0;">
|
||||
{% for r in cat.randuri %}
|
||||
<li class="muted" style="font-size:12px;">
|
||||
#{{ r.id }} {{ r.vin }}{% if r.nr %} / {{ r.nr }}{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if cat.rest %}
|
||||
<li class="muted" style="font-size:12px;">…si inca {{ cat.rest }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap; align-items:center;">
|
||||
<span style="font-size:12px; color:var(--muted);">Necesita atentie:</span>
|
||||
{% for pill in pills_categorii %}
|
||||
<button type="button"
|
||||
class="pill-cat"
|
||||
aria-pressed="false"
|
||||
hx-get="/_fragments/submissions?status={{ pill.status }}"
|
||||
hx-target="#submissions-wrap"
|
||||
hx-swap="innerHTML"
|
||||
onclick="(function(b){
|
||||
var pressed=b.getAttribute('aria-pressed')==='true';
|
||||
document.querySelectorAll('.pill-cat').forEach(function(x){x.setAttribute('aria-pressed','false');});
|
||||
if(!pressed){b.setAttribute('aria-pressed','true');}
|
||||
var s=document.getElementById('trimiteri-section');
|
||||
if(s){s.scrollIntoView({behavior:'smooth'});}
|
||||
})(this)"
|
||||
style="display:inline-flex; align-items:center; gap:5px;
|
||||
padding:3px 10px; border-radius:99px; font-size:12px; font-weight:600;
|
||||
cursor:pointer; border:1.5px solid var({{ pill.color_var }}); color:var({{ pill.color_var }});
|
||||
background:transparent; transition:background 0.15s, color 0.15s;">
|
||||
{{ pill.label }}
|
||||
<span style="font-size:11px; font-weight:700; background:var({{ pill.color_var }}); color:var(--card);
|
||||
padding:0 5px; border-radius:99px; min-width:18px; text-align:center;">{{ pill.n }}</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
{# Buton "Toate" — reseteaza filtrul de categorie #}
|
||||
<button type="button"
|
||||
class="pill-cat-reset"
|
||||
aria-label="Arata toate trimiterile"
|
||||
hx-get="/_fragments/submissions"
|
||||
hx-target="#submissions-wrap"
|
||||
hx-swap="innerHTML"
|
||||
onclick="document.querySelectorAll('.pill-cat').forEach(function(x){x.setAttribute('aria-pressed','false');})"
|
||||
style="padding:3px 10px; border-radius:99px; font-size:12px; cursor:pointer;
|
||||
border:1px solid var(--line); background:transparent; color:var(--muted);">
|
||||
Toate
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
{#
|
||||
OOB: actualizeaza inputul id="f-page" din #filtre-trimiteri (US-004 L2).
|
||||
Poll-ul de 15s (hx-include="#filtre-trimiteri") preia automat pagina curenta.
|
||||
Elementul OOB e extras din continutul normal de HTMX inainte de swap in #submissions-wrap.
|
||||
#}
|
||||
<input type="hidden" id="f-page" name="page" value="{{ page | default(1) }}" hx-swap-oob="true">
|
||||
|
||||
{% if rows %}
|
||||
{# US-011: form de stergere bulk. Selectia opereaza DOAR pe randuri blocate
|
||||
(gestionabil); sent/sending/queued nu au checkbox (read-only). #}
|
||||
@@ -61,7 +68,8 @@
|
||||
<td class="col-vehicul" data-eticheta="Vehicul">
|
||||
{{ r.prez.vehicul_nr }}
|
||||
{% if r.prez.vin_scurt and r.prez.vin_scurt != '—' %}
|
||||
<span class="muted" style="font-size:12px;">{{ r.prez.vin_scurt }}</span>
|
||||
{# US-005: 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">
|
||||
@@ -83,6 +91,105 @@
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{#
|
||||
Paginare numerotata (US-004 PRD 5.10).
|
||||
Afisata doar cand exista mai mult de o pagina.
|
||||
Fiecare link pastreaza filtrele curente (status, vehicul, data_de, data_pana).
|
||||
Pagina curenta: aria-current="page" (semantic).
|
||||
#}
|
||||
{% if total is defined %}
|
||||
<div aria-live="polite"
|
||||
style="font-size:12px; color:var(--muted); text-align:right; margin-top:6px; margin-bottom:2px;">
|
||||
{% if total == 0 %}
|
||||
0 trimiteri
|
||||
{% else %}
|
||||
{{ page_start }}–{{ page_end }} din {{ total }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if pages is defined and pages > 1 %}
|
||||
{#
|
||||
Construim param-string pentru filtrele curente (fara page) — refolosit in fiecare link.
|
||||
Filtrul status vine din pill-uri (nu din form); il pastram in URL.
|
||||
#}
|
||||
{% set pq = "" %}
|
||||
{% if f_status %}{% set pq = pq + "&status=" + f_status %}{% endif %}
|
||||
{% if f_vehicul %}{% set pq = pq + "&vehicul=" + f_vehicul %}{% endif %}
|
||||
{% if f_data_de %}{% set pq = pq + "&data_de=" + f_data_de %}{% endif %}
|
||||
{% if f_data_pana %}{% set pq = pq + "&data_pana=" + f_data_pana %}{% endif %}
|
||||
|
||||
<nav aria-label="Paginare trimiteri"
|
||||
style="display:flex; justify-content:center; gap:4px; flex-wrap:wrap; margin-top:10px;">
|
||||
|
||||
{# Buton Anterior #}
|
||||
{% if page > 1 %}
|
||||
<button type="button"
|
||||
hx-get="/_fragments/submissions?page={{ page - 1 }}{{ pq }}"
|
||||
hx-target="#submissions-wrap"
|
||||
hx-swap="innerHTML"
|
||||
style="padding:3px 10px; border-radius:6px; font-size:13px; cursor:pointer;
|
||||
border:1px solid var(--line); background:var(--card); color:var(--fg);"
|
||||
aria-label="Pagina anterioara">
|
||||
«
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button" disabled
|
||||
style="padding:3px 10px; border-radius:6px; font-size:13px;
|
||||
border:1px solid var(--line); background:var(--card); color:var(--muted);
|
||||
opacity:0.4; cursor:default;"
|
||||
aria-label="Pagina anterioara (indisponibila)">
|
||||
«
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{# Numerele de pagina #}
|
||||
{% for p in range(1, pages + 1) %}
|
||||
{% if p == page %}
|
||||
<button type="button"
|
||||
aria-current="page"
|
||||
style="padding:3px 10px; border-radius:6px; font-size:13px; cursor:default;
|
||||
border:1px solid var(--accent); background:var(--accent); color:#fff;
|
||||
font-weight:700;">
|
||||
{{ p }}
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button"
|
||||
hx-get="/_fragments/submissions?page={{ p }}{{ pq }}"
|
||||
hx-target="#submissions-wrap"
|
||||
hx-swap="innerHTML"
|
||||
style="padding:3px 10px; border-radius:6px; font-size:13px; cursor:pointer;
|
||||
border:1px solid var(--line); background:var(--card); color:var(--fg);">
|
||||
{{ p }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{# Buton Urmator #}
|
||||
{% if page < pages %}
|
||||
<button type="button"
|
||||
hx-get="/_fragments/submissions?page={{ page + 1 }}{{ pq }}"
|
||||
hx-target="#submissions-wrap"
|
||||
hx-swap="innerHTML"
|
||||
style="padding:3px 10px; border-radius:6px; font-size:13px; cursor:pointer;
|
||||
border:1px solid var(--line); background:var(--card); color:var(--fg);"
|
||||
aria-label="Pagina urmatoare">
|
||||
»
|
||||
</button>
|
||||
{% else %}
|
||||
<button type="button" disabled
|
||||
style="padding:3px 10px; border-radius:6px; font-size:13px;
|
||||
border:1px solid var(--line); background:var(--card); color:var(--muted);
|
||||
opacity:0.4; cursor:default;"
|
||||
aria-label="Pagina urmatoare (indisponibila)">
|
||||
»
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% elif filtru_activ %}
|
||||
<div class="empty">
|
||||
Nimic pe filtrul curent.
|
||||
|
||||
@@ -19,8 +19,10 @@
|
||||
<p class="muted" style="margin:0 0 12px; font-size:13px;">{{ stare_subtext }}</p>
|
||||
{% endif %}
|
||||
|
||||
{# === R10 (2): bloc eroare blocanta cand exista === #}
|
||||
{% if erori_3n %}
|
||||
{# === R10 (2): bloc eroare blocanta — DOAR in read-only (US-008).
|
||||
In editare, cardul 3-niveluri e inlocuit cu: erori per-camp in macro `camp`
|
||||
(text simplu .s-error) + rezumat top-of-form pentru erori fara camp (mai jos). === #}
|
||||
{% if not editabil and erori_3n %}
|
||||
<div style="margin:0 0 14px;">
|
||||
{{ card_erori(erori_3n) }}
|
||||
</div>
|
||||
@@ -88,6 +90,13 @@
|
||||
{% if corectie_error %}role="alert"{% endif %}>{{ corectie_msg }}</div>
|
||||
{% endif %}
|
||||
|
||||
{# US-008 (M6): erori fara camp (field None) nu dispar silentios in editare —
|
||||
cardul 3n e ascuns, deci adaugam un rezumat simplu top-of-form.
|
||||
Erori cu camp raman afisate per-camp de macro-ul `camp` de mai jos. #}
|
||||
{% for e in erori_3n if not e.field %}
|
||||
<div class="s-error" style="font-size:13px; margin:0 0 10px;" role="alert">{{ e.problema }}</div>
|
||||
{% endfor %}
|
||||
|
||||
{% macro camp(nume, eticheta, valoare, tip='text') %}
|
||||
<div style="margin-bottom:10px;">
|
||||
<label for="c-{{ nume }}" class="muted" style="font-size:12px; display:block;">{{ eticheta }}</label>
|
||||
@@ -105,11 +114,40 @@
|
||||
hx-disabled-elt="find button">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
{# US-006: select cod RAR pe stari editabile (needs_data/needs_mapping), cu nomenclator.
|
||||
Read-only pe sent/sending/queued/error (nomenclator_rar gol → ramura else). #}
|
||||
{% 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 (R9, fara eticheta „Cod RAR"). #}
|
||||
<div style="margin:0 0 12px;">
|
||||
<div class="muted" style="font-size:12px;">Operatie</div>
|
||||
<div>{{ prez.operatie }} · {{ cod_afis }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# US-007: operatie service (cod intern + denumire venita prin API/import), distinct de
|
||||
operatia RAR mapata. Conventie US-002: op_service_cod="" cand lipseste → randul absent. #}
|
||||
{% if prez.op_service_cod %}
|
||||
<div style="margin:0 0 12px;">
|
||||
<div class="muted" style="font-size:12px;">Operatie service</div>
|
||||
<div>{{ prez.op_service_cod }}{% if prez.op_service_denumire %} — {{ prez.op_service_denumire }}{% endif %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Nr. inmatriculare pe rand propriu, VIN dedesubt — ambele latime plina. #}
|
||||
{{ camp('nr_inmatriculare', 'Numar inmatriculare', form_nr) }}
|
||||
@@ -139,6 +177,12 @@
|
||||
<div style="word-break:break-all;">{{ prez.vin }}</div>
|
||||
</div>
|
||||
<div><div class="muted" style="font-size:12px;">Operatie</div><div>{{ prez.operatie }} · {{ cod_afis }}</div></div>
|
||||
{# US-007: operatie service (cod intern + denumire), distinct de operatia RAR.
|
||||
Conventie US-002: op_service_cod="" cand lipseste → randul absent (fara "—"). #}
|
||||
{% if prez.op_service_cod %}
|
||||
<div><div class="muted" style="font-size:12px;">Operatie service</div>
|
||||
<div>{{ prez.op_service_cod }}{% if prez.op_service_denumire %} — {{ prez.op_service_denumire }}{% endif %}</div></div>
|
||||
{% endif %}
|
||||
<div><div class="muted" style="font-size:12px;">Data prestatie</div><div>{{ prez.data_prestatie }}</div></div>
|
||||
<div><div class="muted" style="font-size:12px;">Odometru final</div><div>{{ prez.odometru }}</div></div>
|
||||
</div>
|
||||
@@ -147,12 +191,30 @@
|
||||
{# === R10 (5): actiuni de jos — primar Re-pune (doar error) + Sterge pe RAND SEPARAT (R2/R11) === #}
|
||||
{% if status == 'error' or gestionabil %}
|
||||
<div class="detaliu-actiuni-jos" style="margin-top:14px; padding-top:12px; border-top:1px solid var(--line);">
|
||||
{# R2: error -> buton primar „Re-pune in coada" pe /repune (error nu e editabil). #}
|
||||
{# R2: error -> buton primar „Re-pune in coada" pe /repune (error nu e editabil pentru #}
|
||||
{# campuri vehicul, dar US-006b permite schimbarea cod_prestatie prin acelasi formular). #}
|
||||
{% if status == 'error' %}
|
||||
<form hx-post="/trimitere/{{ id }}/repune"
|
||||
hx-target="#detaliu-modal-body" hx-swap="innerHTML"
|
||||
hx-disabled-elt="find button" style="margin:0 0 10px;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
{# US-006b: select cod_prestatie optional in formularul /repune (doar pentru error). #}
|
||||
{% if nomenclator_rar %}
|
||||
<label for="cod-rar-error-{{ id }}" style="display:block; font-size:12px; color:var(--muted); margin-bottom:4px;">
|
||||
Operatie RAR (optional — schimba codul si re-pune)
|
||||
</label>
|
||||
<select id="cod-rar-error-{{ id }}" name="cod_prestatie"
|
||||
aria-label="Alege operatia RAR din nomenclator"
|
||||
style="width:100%; margin-bottom:8px; font-size:13px;">
|
||||
<option value="">— pastrat ({{ cod_prestatie_curent }}) —</option>
|
||||
{% 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 }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<button type="submit">Re-pune in coada</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
@@ -14,12 +14,16 @@
|
||||
htmx.config.useTemplateFragments = true;
|
||||
</script>
|
||||
<script>
|
||||
// Anti-FOUC (US-001 PRD 5.3): citeste preferinta tema din localStorage inainte de
|
||||
// primul paint; seteaza data-theme pe <html> sincron, fara blink dark->light.
|
||||
// Anti-FOUC (US-001 PRD 5.3, extins US-014 PRD 5.10): 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).
|
||||
(function() {
|
||||
var VALID = {light:1, dark:1, petrol:1, auto:1};
|
||||
try {
|
||||
var t = localStorage.getItem('theme');
|
||||
if (!t) {
|
||||
if (!t || !VALID[t]) t = 'auto'; // fallback: valoare lipsa sau legacy -> auto
|
||||
if (t === 'auto') {
|
||||
t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
@@ -29,19 +33,103 @@
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
:root { --bg:#0f1115; --card:#181b22; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36;
|
||||
--ok:#3ecf8e; --warn:#e6b34a; --err:#e5605e; --accent:#5b8def; }
|
||||
[data-theme="light"] { --bg:#f6f7f9; --card:#ffffff; --ink:#1a1d24; --muted:#5c6473; --line:#e2e5ea;
|
||||
--ok:#15803d; --warn:#b45309; --err:#dc2626; --accent:#2563eb; }
|
||||
/* US-013 (PRD 5.10): IBM Plex Sans + Mono self-hosted (latin-ext pentru diacritice romanesti).
|
||||
font-display:swap permite text vizibil inainte de incarcare (FOUT system-ui->IBM Plex).
|
||||
FOUT pe tabular-nums: IBM Plex Sans are metrici apropiate de system-ui; reflow-ul vizibil
|
||||
pe VIN/coduri e acceptat explicit — fontul se incarca din /static/ (acelasi origin).
|
||||
IBM Plex Sans/Mono self-host, subset latin + latin-ext de pe fontsource
|
||||
(@fontsource/ibm-plex-sans + @fontsource/ibm-plex-mono, v5.0.8), woff2 valide. */
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/static/fonts/IBMPlexSans-Regular-latin-ext.woff2") format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/static/fonts/IBMPlexSans-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;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("/static/fonts/IBMPlexSans-Medium-latin-ext.woff2") format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("/static/fonts/IBMPlexSans-Medium-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;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("/static/fonts/IBMPlexSans-Bold-latin-ext.woff2") format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("/static/fonts/IBMPlexSans-Bold-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;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/static/fonts/IBMPlexMono-Regular-latin-ext.woff2") format("woff2");
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
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 conform DESIGN.md */
|
||||
:root { --bg:#0f1218; --card:#181c24; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36;
|
||||
--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;
|
||||
--ok:#15803d; --warn:#b45309; --err:#dc2626; --accent:#1F66C9; }
|
||||
/* Paleta Petrol (US-014) — 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;
|
||||
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#0E7C7B; }
|
||||
* { box-sizing:border-box; }
|
||||
/* PRD 5.9 US-006 — CONVENTIE BREAKPOINT: un singur prag mobil la 768px.
|
||||
CSS custom properties NU functioneaza in `@media`, deci pragul nu poate fi o
|
||||
variabila; folosim consecvent `@media (max-width:767px)` peste tot (mobil) si
|
||||
`@media (max-width:1024px)` doar pentru densitatea tabelului. >=1024px = layout
|
||||
desktop neschimbat (fara regresie). Orice regula mobila noua reutilizeaza 767px. */
|
||||
body { margin:0; font:15px/1.5 ui-sans-serif,system-ui,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
||||
body { margin:0; font:15px/1.5 "IBM Plex Sans",system-ui,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
|
||||
background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }
|
||||
header { padding:16px 24px; border-bottom:1px solid var(--line); display:flex; align-items:center; gap:12px; }
|
||||
/* US-012 (PRD 5.10): grila 3 coloane — stanga (env badge echilibru) | centru (titlu+wordmark) | dreapta (controale). */
|
||||
header { padding:16px 24px; border-bottom:1px solid var(--line);
|
||||
display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; }
|
||||
.header-left { display:flex; align-items:center; }
|
||||
.header-center { display:flex; flex-direction:column; align-items:center; text-align:center; }
|
||||
.header-right { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
|
||||
/* US-012b: logo PNG ROMFAST sub titlu — 28px inaltime, centrat, fara filtre de culoare.
|
||||
Logo are fundal transparent + culori proprii (ROM rosu + FAST albastru) -> ok pe toate temele. */
|
||||
.brand-logo { height:28px; width:auto; display:block; margin:3px auto 0; }
|
||||
header h1 { font-size:20px; margin:0; font-weight:700; letter-spacing:-.01em; }
|
||||
header .env { font-size:12px; color:var(--muted); border:1px solid var(--line); padding:2px 8px; border-radius:99px; }
|
||||
main { padding:24px; max-width:1100px; margin:0 auto; }
|
||||
@@ -115,7 +203,7 @@
|
||||
border-radius:0 6px 6px 0; }
|
||||
.eroare-3n-sep { margin-top:6px; }
|
||||
.eroare-3n-problema { font-weight:600; color:var(--err); font-size:13px; }
|
||||
.eroare-3n-camp { font-family:monospace; font-size:12px; opacity:.85; }
|
||||
.eroare-3n-camp { font-family:"IBM Plex Mono",ui-monospace,monospace; font-size:12px; opacity:.85; }
|
||||
.eroare-3n-cauza { color:var(--muted); font-size:12px; margin-top:3px; }
|
||||
.eroare-3n-fix { color:var(--accent); font-size:12px; margin-top:3px; }
|
||||
.eroare-3n-label { font-weight:500; }
|
||||
@@ -127,6 +215,11 @@
|
||||
border-radius:6px; min-height:36px; min-width:36px; font-size:16px; padding:4px 8px;
|
||||
line-height:1; display:inline-flex; align-items:center; justify-content:center; }
|
||||
.icon-btn:hover { background:var(--line); }
|
||||
/* US-011: variante icon-btn — dirty (modificari nesalvate) + danger (destructiv) */
|
||||
.icon-btn.dirty { background:var(--accent); color:#fff; border-color:var(--accent); }
|
||||
.icon-btn.dirty:hover { filter:brightness(0.9); }
|
||||
.icon-btn.danger { color:var(--err); border-color:var(--err); }
|
||||
.icon-btn.danger:hover, .icon-btn.danger:focus-visible { background:var(--err); color:#fff; }
|
||||
.cont-menu { position:absolute; right:0; top:calc(100% + 8px); min-width:180px; z-index:50;
|
||||
background:var(--card); border:1px solid var(--line); border-radius:8px; padding:6px;
|
||||
box-shadow:0 8px 24px rgba(0,0,0,.18); display:flex; flex-direction:column; gap:2px; }
|
||||
@@ -182,7 +275,7 @@
|
||||
/* secundarul muted („cod RAR" / „nemapat") — >=12px, contrast pe var(--muted) >=4.5:1 */
|
||||
.tabel-trimiteri .cod-rar-sub { font-size:12px; margin-top:2px; }
|
||||
/* PRD 5.9 US-002: codul RAR pe linia 2 — chip discret, fara prefixul „cod RAR:". */
|
||||
.tabel-trimiteri .cod-rar-cod { display:inline-block; font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
|
||||
.tabel-trimiteri .cod-rar-cod { display:inline-block; font-family:"IBM Plex Mono",ui-monospace,monospace;
|
||||
font-size:12px; padding:1px 7px; border:1px solid var(--line);
|
||||
border-radius:99px; color:var(--muted); }
|
||||
/* PRD 5.9 US-002 (R1): eticheta umana scurta sub pill — text mic; clasa `s-error`
|
||||
@@ -248,9 +341,13 @@
|
||||
/* US-004 (R11): actiunile de jos din detaliu (Re-pune / Sterge) full-width stivuit pe mobil. */
|
||||
.detaliu-actiuni-jos button { width:100%; }
|
||||
|
||||
/* Header + nav colapsate: header se rupe pe linii, fara scroll orizontal de pagina;
|
||||
tintele touch (toggle tema/cont, taburi, itemi meniu cont) cresc la >=44px. */
|
||||
header { padding:12px 16px; flex-wrap:wrap; gap:8px; }
|
||||
/* Header + nav colapsate: pe mobil trece de la grid la flex wrap.
|
||||
Randul 1: [env badge stanga] [controale dreapta] (margin-left:auto pe .header-right).
|
||||
Randul 2: [titlu + wordmark centrat, full-width]. Fara scroll orizontal, tinte >=44px. */
|
||||
header { display:flex; flex-wrap:wrap; padding:12px 16px; gap:8px; align-items:center; }
|
||||
.header-left { order:0; flex:0 0 auto; }
|
||||
.header-center { order:2; width:100%; text-align:center; }
|
||||
.header-right { order:1; margin-left:auto; flex:0 0 auto; }
|
||||
header h1 { font-size:17px; }
|
||||
main { padding:16px; }
|
||||
.icon-btn { min-height:44px; min-width:44px; }
|
||||
@@ -315,10 +412,21 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{# US-012 (PRD 5.10): grila 3 coloane — stanga (env badge) | centru (titlu+wordmark) | dreapta (controale). #}
|
||||
<header>
|
||||
<h1>Gateway RAR AUTOPASS</h1>
|
||||
<span class="env">{{ rar_env }}</span>
|
||||
<div style="margin-left:auto; display:flex; align-items:center; gap:8px;">
|
||||
{# Celula stanga: badge env (test/prod) — echilibru optic fata de controalele din dreapta #}
|
||||
<div class="header-left">
|
||||
<span class="env">{{ rar_env }}</span>
|
||||
</div>
|
||||
{# Celula centru: titlu + wordmark 'by ROMFAST' #}
|
||||
<div class="header-center">
|
||||
<h1>Gateway RAR AUTOPASS</h1>
|
||||
{# US-012b (decizie user): logo PNG real in loc de wordmark text.
|
||||
288x175 RGBA fundal transparent — lizibil pe light/dark/petrol fara filtre. #}
|
||||
<img src="/static/romfast_logo.png" alt="ROMFAST" class="brand-logo">
|
||||
</div>
|
||||
{# Celula dreapta: comutator tema + versiune + meniu cont #}
|
||||
<div class="header-right">
|
||||
<button id="tema-toggle" class="icon-btn"
|
||||
aria-label="Comuta tema (luminos/intunecat)"
|
||||
title="Comuta tema">☀</button>
|
||||
@@ -331,6 +439,10 @@
|
||||
aria-haspopup="true" aria-expanded="false" aria-controls="cont-menu"
|
||||
aria-label="Meniu cont" title="Meniu cont">☰</button>
|
||||
<div id="cont-menu" class="cont-menu" role="menu" aria-labelledby="cont-menu-toggle" hidden>
|
||||
{# US-009 (PRD 5.10): Mapari mutat din tab-bar in meniu, cu badge needs_mapping. #}
|
||||
{% set _mapari_badge = (badges.mapari if (badges is defined and badges and badges.mapari) else 0) %}
|
||||
<a role="menuitem" href="/?tab=mapari">Mapari{% if _mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; margin-left:6px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ _mapari_badge }}</span>{% endif %}</a>
|
||||
<hr>
|
||||
<a role="menuitem" href="/?tab=cont">Cont</a>
|
||||
<a role="menuitem" href="/?tab=integrare">Integrare</a>
|
||||
<a role="menuitem" href="/?tab=nomenclator">Nomenclator</a>
|
||||
@@ -346,6 +458,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
{# aria-live pentru anuntarea schimbarilor de tema (US-014, accesibilitate) #}
|
||||
<span id="tema-live" role="status" aria-live="polite"
|
||||
style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;"></span>
|
||||
<main>{% block content %}{% endblock %}</main>
|
||||
{# Modal detaliu trimitere (PRD 5.9 US-003): container global, SIBLING al <main>
|
||||
(nu descendent), ca `inert`+`aria-hidden` pe <main> sa nu-l prinda si pe el (R7).
|
||||
@@ -360,36 +475,46 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Handler comutator tema (US-002 PRD 5.3): click toggle light<->dark, persista in localStorage.
|
||||
// Separare init (doar sincronizare iconita) de persistenta (doar la click explicit).
|
||||
// Motivatie: scrierea in localStorage la init ar ingloba imediat preferinta OS-aware ca alegere
|
||||
// explicita, impiedicand urmarirea ulterioara a modificarilor de tema ale sistemului de operare.
|
||||
// Comutator tema ciclic (US-014 PRD 5.10): 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.
|
||||
(function() {
|
||||
var btn = document.getElementById('tema-toggle');
|
||||
if (!btn) return;
|
||||
// Sincronizeaza iconita si aria-label dupa tema curenta -- FARA efecte secundare in localStorage.
|
||||
function _syncIcon(t) {
|
||||
if (t === 'light') {
|
||||
btn.innerHTML = '☾';
|
||||
btn.setAttribute('aria-label', 'Comuta tema (intunecat)');
|
||||
btn.title = 'Comuta tema (intunecat)';
|
||||
} else {
|
||||
btn.innerHTML = '☀';
|
||||
btn.setAttribute('aria-label', 'Comuta tema (luminos)');
|
||||
btn.title = 'Comuta tema (luminos)';
|
||||
}
|
||||
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:'☀', dark:'☾', petrol:'◐', auto:'◙'};
|
||||
var LABELS = {light:'Light', dark:'Dark', petrol:'Petrol', auto:'Auto'};
|
||||
var NEXT = {light:'Dark', dark:'Petrol', petrol:'Auto', auto:'Light'};
|
||||
var TOOLTIP_CICLU = 'Ciclu: Light → Dark → Petrol → Auto';
|
||||
|
||||
function _stored() {
|
||||
try { var v = localStorage.getItem('theme'); return (v && VALID[v]) ? v : 'auto'; } catch(e) { return 'auto'; }
|
||||
}
|
||||
function _resolved(stored) {
|
||||
if (stored !== 'auto') return stored;
|
||||
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
}
|
||||
function _syncButton(stored) {
|
||||
var s = VALID[stored] ? stored : 'auto';
|
||||
btn.innerHTML = ICONS[s];
|
||||
btn.setAttribute('aria-label', 'Tema: ' + LABELS[s] + ', apasa pentru ' + NEXT[s]);
|
||||
btn.title = 'Tema: ' + LABELS[s] + '. ' + TOOLTIP_CICLU;
|
||||
}
|
||||
// Aplica o tema noua, seteaza data-theme si persista in localStorage -- apelat DOAR la click.
|
||||
function _setTheme(t) {
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
document.documentElement.setAttribute('data-theme', _resolved(t));
|
||||
try { localStorage.setItem('theme', t); } catch(e) {}
|
||||
_syncIcon(t);
|
||||
_syncButton(t);
|
||||
var live = document.getElementById('tema-live');
|
||||
if (live) live.textContent = 'Tema: ' + LABELS[t] + (t === 'auto' ? ' (urmeaza sistemul)' : '');
|
||||
}
|
||||
// La init: sincronizeaza doar iconita din data-theme curent (setat deja de scriptul anti-FOUC).
|
||||
_syncIcon(document.documentElement.getAttribute('data-theme') || 'dark');
|
||||
// Init: sincronizeaza iconita din starea stocata (fara a scrie in localStorage).
|
||||
_syncButton(_stored());
|
||||
btn.addEventListener('click', function() {
|
||||
var cur = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
_setTheme(cur === 'dark' ? 'light' : 'dark');
|
||||
var cur = _stored();
|
||||
var idx = CYCLE.indexOf(cur);
|
||||
_setTheme(CYCLE[(idx + 1) % CYCLE.length]);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -470,6 +595,19 @@
|
||||
window.addEventListener('resize', function() { closeAll(null); });
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// US-011: dirty state pentru butoanele de salvare din tabelele de mapari.
|
||||
// Cand utilizatorul schimba un select dintr-un form de mapare, butonul de salvare
|
||||
// legat prin data-dirty-form devine evidentiat (clasa "dirty" → fundal --accent).
|
||||
// Starea "dirty" e efemera per-render: un swap outerHTML o reseteaza automat.
|
||||
// Delegare pe document → supravietuieste swap-urilor HTMX (#mapari-section).
|
||||
document.addEventListener('change', function(e) {
|
||||
var el = e.target;
|
||||
if (el.tagName !== 'SELECT' || !el.form || !el.form.id) return;
|
||||
var saveBtn = document.querySelector('button[data-dirty-form="' + el.form.id + '"]');
|
||||
if (saveBtn) saveBtn.classList.add('dirty');
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// Cautare + paginare client-side pentru tabele mari (data-dt="<page_size>"). Filtreaza si
|
||||
// pagineaza DOM-ul deja randat (fara cereri server) — potrivit pentru maparile care pot creste
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
{# US-007 (5.5): nav-ul ad-hoc (Panou admin + logout) a fost mutat in meniul de cont (☰)
|
||||
din header (base.html). Aici raman doar bara de status + tab-bar-ul de lucru zilnic. #}
|
||||
{# US-009 (PRD 5.10): tab-bar-ul Acasa/Mapari a fost eliminat. Mapari s-a mutat in meniul
|
||||
hamburger (#cont-menu in base.html). Acasa e continutul principal direct — nicio schela ARIA
|
||||
role="tablist"/"tab"/"tabpanel" orfana. Rutele /_fragments/* si deep-link-urile ?tab=
|
||||
raman valide (navigare prin meniu → full page reload). #}
|
||||
|
||||
<!-- Bara de status (US-002): mereu vizibila, deasupra tab-bar-ului -->
|
||||
<!-- Bara de status: mereu vizibila -->
|
||||
<div id="status-bar" class="status-bar card"
|
||||
hx-get="/_fragments/status"
|
||||
hx-trigger="load, every 15s"
|
||||
@@ -12,80 +14,9 @@
|
||||
<div class="empty muted" style="padding:8px 0;">se incarca starea…</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab-bar: navigare intre sectiuni -->
|
||||
<div role="tablist" class="tab-bar" aria-label="Sectiuni dashboard">
|
||||
{# US-007 (5.5): tab-bar redus la suprafetele de LUCRU ZILNIC (Acasa·Mapari).
|
||||
Cont/Integrare/Nomenclator s-au mutat in meniul de cont (☰) din header — rutele
|
||||
`/_fragments/{cont,integrare,nomenclator}` + deep-link `?tab=` raman valide. #}
|
||||
{% set tabs = [
|
||||
("acasa", "Acasa", "tab-acasa"),
|
||||
("mapari", "Mapari", "tab-mapari")
|
||||
] %}
|
||||
{% for tab_id, tab_label, tab_elem_id in tabs %}
|
||||
{% set badge = (badges.get(tab_id, 0) if badges else 0) %}
|
||||
<a id="{{ tab_elem_id }}"
|
||||
role="tab"
|
||||
href="/?tab={{ tab_id }}"
|
||||
aria-selected="{{ 'true' if active_tab == tab_id else 'false' }}"
|
||||
aria-controls="tab-panel"
|
||||
{% if badge %}aria-label="{{ tab_label }}, {{ badge }} necesita atentie"{% endif %}
|
||||
class="tab-link{% if active_tab == tab_id %} tab-activ{% endif %}"
|
||||
tabindex="{{ '0' if active_tab == tab_id else '-1' }}"
|
||||
hx-get="/_fragments/{{ tab_id }}"
|
||||
hx-target="#tab-panel"
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url="/?tab={{ tab_id }}">{{ tab_label }}{% if badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; margin-left:6px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ badge }}</span>{% endif %}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Panou activ: randat server-side la full load; HTMX inlocuieste continutul la click pe tab -->
|
||||
<div id="tab-panel" role="tabpanel" aria-labelledby="tab-{{ active_tab }}" class="tab-panel">
|
||||
<!-- Panou activ: randat server-side la full load (Acasa implicit, sau ?tab= prin meniu) -->
|
||||
<div id="tab-panel" class="tab-panel">
|
||||
{{ panel_html | safe }}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
/* Navigare cu sageti intre tab-uri (ARIA pattern) — scoped pe fiecare tablist.
|
||||
Folosim querySelectorAll pentru a suporta multiple tablist-uri pe pagina
|
||||
(tab-bar principal + tab-urile interne din panoul Integrare). */
|
||||
document.querySelectorAll('[role="tablist"]').forEach(function(tablist) {
|
||||
var tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
|
||||
if (!tabs.length) return;
|
||||
|
||||
tablist.addEventListener('keydown', function(e) {
|
||||
var idx = tabs.indexOf(document.activeElement);
|
||||
if (idx === -1) return;
|
||||
var next = -1;
|
||||
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
||||
next = (idx + 1) % tabs.length;
|
||||
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
||||
next = (idx - 1 + tabs.length) % tabs.length;
|
||||
} else if (e.key === 'Home') {
|
||||
next = 0;
|
||||
} else if (e.key === 'End') {
|
||||
next = tabs.length - 1;
|
||||
}
|
||||
if (next !== -1) {
|
||||
e.preventDefault();
|
||||
tabs[next].focus();
|
||||
}
|
||||
});
|
||||
|
||||
/* La click pe tab: actualizeaza aria-selected + tabindex (scoped pe tablist-ul curent) */
|
||||
tabs.forEach(function(tab) {
|
||||
tab.addEventListener('click', function() {
|
||||
tabs.forEach(function(t) {
|
||||
t.setAttribute('aria-selected', 'false');
|
||||
t.setAttribute('tabindex', '-1');
|
||||
t.classList.remove('tab-activ');
|
||||
});
|
||||
tab.setAttribute('aria-selected', 'true');
|
||||
tab.setAttribute('tabindex', '0');
|
||||
tab.classList.add('tab-activ');
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user