From 8d4ff3400e2a76dbc5bf18fdbdb9ff0d008cb778 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sat, 27 Jun 2026 23:34:33 +0000 Subject: [PATCH] feat(5.13): carduri compacte mobil/tableta + fix editare preview (OOB tr) + toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dogfood pe import + Trimiteri (mobil/tableta <1024px), pur CSS + markup, backend trimitere neatins: - Card compact real pentru .tabel-trimiteri (preview + Trimiteri): vehicul=titlu, stare=pill dreapta-sus, operatie+cod, meta data/km muted, nota mica. Inlocuieste stiva generica eticheta+valoare (carduri de ~450px -> ~135px). Anuleaza regula desktop tr.trimitere-row > td{padding:11px} in blocul compact. - FIX editare preview: OOB swap pe esua tacit in htmx 1.9 (un brut se pierde la parsarea unui fragment fara context de tabel) -> randul ramanea cu starea veche dupa salvare. Inlocuit cu reload complet al preview-ului prin HX-Trigger:reincarcaPreview + detalii randSalvat. /editeaza si /confirma-review folosesc helper-ul _raspuns_rand_salvat. - Feedback post-salvare: toast global "Randul N actualizat · " + scroll + flash pe randul actualizat (base.html window.arataToast + listener randSalvat). - Modal editare: Salveaza + Anuleaza pe acelasi rand (sistem .act): desktop text, mobil doua iconite Lucide 44px alaturate (save/x). Macro icon('x') + .act-primary. - Randuri deja-trimise/duplicate colapsate implicit in preview + toggle "Arata N". - Select "Operatii de mapat" full-width pe mobil (nu mai iese din viewport). - Bara de filtre Trimiteri adaptata mobil: pills pe banda cu scroll orizontal, cautare vehicul proeminenta (nu 8 butoane full-width stivuite). - Nota preview = culoarea camp-fix (accent) ca sa atraga atentia; hint-urile camp-fix per-camp scoase (campul Note e self-explanatory). - Confirmare trimitere: scos campul email (Declarant); text mai clar ("Confirma numarul din N gata de trimis"). Backend confirmed_by ramane optional. Teste: contractul OOB (rupt in browser) inlocuit cu noul contract (reincarcaPreview + randSalvat) in test_web_preview_edit / test_preview_edit_ui / test_import_review. Suita: 992 passed (exclus live). Co-Authored-By: Claude Opus 4.8 (1M context) --- app/web/routes.py | 66 +++-- app/web/templates/_editare_preview_modal.html | 10 +- app/web/templates/_form_editare.html | 16 +- app/web/templates/_macros.html | 22 ++ app/web/templates/_mapari.html | 28 +- app/web/templates/_preview_import.html | 74 ++++-- app/web/templates/_preview_rand.html | 21 +- app/web/templates/_stepper.html | 112 ++------ app/web/templates/_submissions.html | 2 +- app/web/templates/base.html | 245 +++++++++++++++++- docs/ROADMAP.md | 3 +- docs/design.md | 7 +- tests/test_import_review.py | 48 ++-- tests/test_preview_edit_ui.py | 20 +- tests/test_web_import_stepper.py | 18 +- tests/test_web_mapari_actiuni.py | 42 ++- tests/test_web_preview_edit.py | 49 ++-- tests/test_web_responsive.py | 153 ++++++++++- 18 files changed, 633 insertions(+), 303 deletions(-) diff --git a/app/web/routes.py b/app/web/routes.py index 7f3749e..0cd93a5 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -2380,6 +2380,33 @@ def _preview_one_row(conn, import_id: int, account_id: int, row_index: int): return result, row +def _raspuns_rand_salvat(import_id: int, row: dict) -> HTMLResponse: + """Raspuns dupa salvarea/confirmarea unui rand de preview. + + NU mai face OOB swap pe `` (htmx 1.9 pierde un `` brut la parsarea unui + fragment care nu incepe cu context de tabel -> swap-ul esua tacit, randul ramanea + cu starea veche; cauza confirmata a bug-ului 'editez si ramane la fel'). In schimb: + - `reincarcaPreview` (HX-Trigger) -> #import-section isi reincarca preview-ul complet + (rand + contoare + colaps deja-trimise, toate corecte, fara fragilitatea OOB). + - `randSalvat` (HX-Trigger, cu detalii) -> base.html arata un toast cu numarul randului + si noua stare, apoi evidentiaza randul dupa reload (feedback vizual clar: ce s-a salvat). + - `inchideModal` (HX-Trigger-After-Settle) -> inchide modalul. + """ + payload = { + "reincarcaPreview": True, + "randSalvat": { + "nr": row["row_index"] + 1, + "rowIndex": row["row_index"], + "stare": row.get("stare_eticheta") or "", + "stareCss": row.get("stare_css") or "", + }, + } + resp = HTMLResponse(content='
') + resp.headers["HX-Trigger"] = json.dumps(payload) + resp.headers["HX-Trigger-After-Settle"] = "inchideModal" + return resp + + def _render_preview_rand( request: Request, *, import_id: int, row: dict, editing: bool, include_oob: bool, summary: dict, message: str | None = None, @@ -2537,25 +2564,8 @@ async def web_editeaza_rand(request: Request, import_id: int, row_index: int) -> "message": "Mai sunt valori invalide — corecteaza campurile marcate.", }) - # Succes: OOB swap rand + contoare + inchideModal. - # Continut primar (swap in #detaliu-modal-body): stub invizibil + script recalc. - # OOB: actualizat + rezumat + contor. - # HX-Trigger-After-Settle: inchideModal → base.html JS inchide modalul. - oob_content = templates.get_template("_preview_rand.html").render({ - "request": request, - "import_id": import_id, - "row": row, - "editing": False, - "oob_tr": True, - "include_oob": True, - "summary": result["summary"], - "message": None, - "csrf_token": get_csrf_token(request), - }) - html_body = '
' + oob_content - resp = HTMLResponse(content=html_body) - resp.headers["HX-Trigger-After-Settle"] = "inchideModal" - return resp + # Succes: reincarca preview-ul complet + toast + inchide modal (vezi helper). + return _raspuns_rand_salvat(import_id, row) finally: conn.close() @@ -2604,22 +2614,8 @@ async def web_confirma_review( if row is None or isinstance(result, str): raise HTTPException(status_code=404, detail="rand de import inexistent") - # OOB: rand actualizat + rezumat + contor ok + inchideModal (identic cu succes editeaza) - oob_content = templates.get_template("_preview_rand.html").render({ - "request": request, - "import_id": import_id, - "row": row, - "editing": False, - "oob_tr": True, - "include_oob": True, - "summary": result["summary"], - "message": None, - "csrf_token": get_csrf_token(request), - }) - html_body = '
' + oob_content - resp = HTMLResponse(content=html_body) - resp.headers["HX-Trigger-After-Settle"] = "inchideModal" - return resp + # Reincarca preview-ul complet + toast + inchide modal (identic cu succes editeaza). + return _raspuns_rand_salvat(import_id, row) finally: conn.close() diff --git a/app/web/templates/_editare_preview_modal.html b/app/web/templates/_editare_preview_modal.html index 363edc9..5418b00 100644 --- a/app/web/templates/_editare_preview_modal.html +++ b/app/web/templates/_editare_preview_modal.html @@ -45,14 +45,10 @@ Salvarea nu a reusit (retea / sesiune). Valorile introduse sunt pastrate — reincearca. + {# with_cancel=True: _form_editare.html randeaza Salveaza + Anuleaza pe acelasi + rand (sistemul .act: desktop text, mobil iconite Lucide 44px alaturate). #} + {% set with_cancel = true %} {% include "_form_editare.html" %} - -
- -
{% if is_needs_review %} diff --git a/app/web/templates/_form_editare.html b/app/web/templates/_form_editare.html index a7264f6..f10b527 100644 --- a/app/web/templates/_form_editare.html +++ b/app/web/templates/_form_editare.html @@ -17,7 +17,7 @@ vin_context — string VIN pentru aria-label (poate fi '') btn_label — eticheta butonului primar (ex. 'Salveaza si retrimite') #} -{% from "_macros.html" import camp %} +{% from "_macros.html" import camp, icon %} {# Nr. inmatriculare pe rand propriu, VIN dedesubt — ambele latime plina. #} {{ camp('nr_inmatriculare', 'Numar inmatriculare', form_nr, @@ -35,7 +35,19 @@ err_map=err_map, fix_map=fix_map, vin_context=vin_context) }} -{# Buton primar parametrizat. #} +{# 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. #} +{% if with_cancel %} +
+ + +
+{% else %}
+{% endif %} diff --git a/app/web/templates/_macros.html b/app/web/templates/_macros.html index 793e6d0..e756118 100644 --- a/app/web/templates/_macros.html +++ b/app/web/templates/_macros.html @@ -50,3 +50,25 @@ {% endif %} {% endmacro %} + +{# PRD 5.13 — sistem butoane de actiune responsive. + CSS-ul aferent (.act, .act-tx, .act-ic, .act-save, .act-del, .act-group) + este definit in base.html. + Desktop: se afiseaza textul (.act-tx); mobil: se afiseaza iconita (.act-ic). #} + +{% macro icon(name) -%} + +{%- endmacro %} + +{% macro act_btn(label, ic, kind='', attrs='') -%} + +{%- endmacro %} diff --git a/app/web/templates/_mapari.html b/app/web/templates/_mapari.html index 3ab039c..7d7264c 100644 --- a/app/web/templates/_mapari.html +++ b/app/web/templates/_mapari.html @@ -135,24 +135,11 @@ - {# Butoane icon mereu vizibile (fara kebab). SVG aria-hidden; aria-label pe buton. + {# Butoane act_btn (desktop: text; mobil: iconita 44px). 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). #} - - + {{ ui.act_btn('Salveaza', 'save', 'save', 'type="submit" form="map-salv-' ~ loop.index ~ '" data-dirty-form="map-salv-' ~ loop.index ~ '"') }} + {{ ui.act_btn('Sterge', 'trash', 'del', 'type="submit" form="map-del-' ~ loop.index ~ '"') }} {% endfor %} @@ -206,10 +193,7 @@ {{ r.cod_prestatie }} - + {{ ui.act_btn('Sterge', 'trash', 'del', 'type="submit" form="rt-del-' ~ loop.index ~ '"') }} {% endfor %} @@ -308,9 +292,7 @@ hx-confirm="Stergi acest format de coloane?"> - + {{ ui.act_btn('Sterge', 'trash', 'del', 'type="submit"') }} diff --git a/app/web/templates/_preview_import.html b/app/web/templates/_preview_import.html index 1d2875a..3c8383e 100644 --- a/app/web/templates/_preview_import.html +++ b/app/web/templates/_preview_import.html @@ -1,5 +1,12 @@ {% import '_macros.html' as ui %} -
+{# reincarcaPreview (emis de /editeaza si /confirma-review prin HX-Trigger): preview-ul + se reincarca COMPLET (rand + contoare + colaps deja-trimise corecte) in loc de OOB swap + pe (fragil in htmx 1.9). Evidentierea + toast-ul randului salvat: base.html. #} +
{% set pas = 3 %}{% include '_stepper.html' %}
@@ -123,10 +130,22 @@ {% endif %}
+ + {% set _n_trimise = summary.get('already_sent', 0) + summary.get('duplicate_in_file', 0) %} + {% if _n_trimise %} +
+ +
+ {% endif %} + -
+
@@ -169,31 +188,19 @@ prezentari la RAR (ireversibil). -
-
- - - -({{ summary.get('ok', 0) }} ok) - -
- -
- - -
+
+ + + +din {{ summary.get('ok', 0) }} gata de trimis +
@@ -281,5 +288,18 @@ /* Filtru implicit "Toate" activ la incarcare */ filterRows('all'); updateN(); + + /* Evidentiere rand dupa reincarcarea preview-ului (window.__randSalvat setat de + listener-ul 'randSalvat' din base.html): scroll + flash, ca userul sa vada CARE + rand s-a schimbat si sa nu ramana cu impresia ca "nu s-a intamplat nimic". */ + if (window.__randSalvat) { + var d = window.__randSalvat; window.__randSalvat = null; + var r = document.getElementById('preview-row-' + d.rowIndex); + if (r) { + r.scrollIntoView({block:'center', behavior:'smooth'}); + void r.offsetWidth; + r.classList.add('rand-actualizat'); + } + } })(); diff --git a/app/web/templates/_preview_rand.html b/app/web/templates/_preview_rand.html index ce4e0c4..81e215c 100644 --- a/app/web/templates/_preview_rand.html +++ b/app/web/templates/_preview_rand.html @@ -18,11 +18,11 @@ #} {%- set res = row.resolved -%} {%- set status = row.resolved_status -%} -{%- set disp_fix_map = {} -%} -{%- for e in row.errors -%}{%- if e is mapping and e.get('field') and e.get('fix') -%}{%- set _ = disp_fix_map.update({e.field: e.get('fix')}) -%}{%- endif -%}{%- endfor -%} +{%- set _sent_dup = status in ('already_sent', 'duplicate_in_file') -%} + style="{% if status == 'needs_review' %}background:rgba(230,179,74,.04);{% elif _sent_dup %}opacity:.6;{% endif %}"> - - + + diff --git a/app/web/templates/base.html b/app/web/templates/base.html index 2a556f7..af39485 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -177,7 +177,7 @@ .banner.warn { border-left-color:var(--warn); background:color-mix(in srgb, var(--warn) 12%, var(--card)); } /* Bara confirmare sticky */ .sticky-bar { position:sticky; bottom:0; background:var(--card); border-top:1px solid var(--line); - padding:12px 16px; display:flex; align-items:flex-start; gap:16px; + padding:10px 14px; display:flex; align-items:flex-start; gap:12px; flex-wrap:wrap; z-index:10; } /* Indicator HTMX — ascuns pana la request */ .htmx-indicator { display:none; } @@ -199,6 +199,83 @@ select { max-width:340px; } button { background:var(--accent); border-color:var(--accent); color:#fff; cursor:pointer; } button:hover { filter:brightness(1.08); } + /* Sistem butoane unificat (design.md §5.1). Primarul = `button`/`.btn` (deja stilat). */ + .btn-secondary { background:transparent; color:var(--ink); border:1px solid var(--line); border-radius:6px; + padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px; + display:inline-flex; align-items:center; justify-content:center; gap:6px; } + .btn-secondary:hover { background:var(--line); filter:none; } + .btn-ghost { background:transparent; color:var(--accent); border:1px solid transparent; border-radius:6px; + padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px; + display:inline-flex; align-items:center; justify-content:center; gap:6px; } + .btn-ghost:hover { background:var(--line); filter:none; } + .btn-danger { background:transparent; color:var(--err); border:1px solid var(--err); border-radius:6px; + padding:8px 14px; font:inherit; font-weight:500; cursor:pointer; min-height:36px; + display:inline-flex; align-items:center; justify-content:center; gap:6px; } + .btn-danger:hover, .btn-danger:focus-visible { background:var(--err); color:#fff; filter:none; } + .btn-sm { padding:5px 10px; min-height:32px; font-size:13px; } + button:focus-visible, .btn-secondary:focus-visible, .btn-ghost:focus-visible, .btn-danger:focus-visible { + outline:2px solid var(--accent); outline-offset:2px; } + /* Actiuni de rand (design.md §5.1): desktop = text, mobil = iconita patrata 44px. + act_btn randeaza si .act-tx (text) si .act-ic (svg); CSS ascunde unul per breakpoint. */ + .act { display:inline-flex; align-items:center; justify-content:center; gap:6px; font:inherit; font-weight:500; + border-radius:7px; padding:6px 12px; min-height:36px; cursor:pointer; background:transparent; + border:1px solid var(--line); color:var(--ink); } + .act:hover { background:var(--line); filter:none; } + .act:focus-visible { outline:2px solid var(--accent); outline-offset:2px; } + .act .act-ic { width:18px; height:18px; display:none; } + .act .act-tx { display:inline; } + .act-save.dirty { background:var(--accent); color:#fff; border-color:var(--accent); } + .act-save.dirty:hover { filter:brightness(.92); } + /* Variant primar mereu-accent (ex. Salveaza in modalul de editare). */ + .act-primary { background:var(--accent); color:#fff; border-color:var(--accent); } + .act-primary:hover { filter:brightness(.92); background:var(--accent); } + .act-del { color:var(--err); border-color:var(--err); } + .act-del:hover, .act-del:focus-visible { background:var(--err); color:#fff; } + .act-group { display:inline-flex; gap:8px; align-items:center; } + .btn-editeaza { white-space:nowrap; } + /* Toast global (feedback tranzitoriu post-actiune). */ + #toast { position:fixed; left:50%; bottom:24px; transform:translateX(-50%) translateY(8px); + z-index:1300; max-width:90vw; padding:11px 18px; border-radius:10px; + background:var(--ink); color:var(--card); font-size:14px; font-weight:500; + box-shadow:0 8px 28px rgba(0,0,0,.28); display:flex; align-items:center; gap:9px; + opacity:0; pointer-events:none; transition:opacity .2s, transform .2s; } + #toast[hidden] { display:none; } + #toast.show { opacity:1; transform:translateX(-50%) translateY(0); } + #toast::before { content:""; width:9px; height:9px; border-radius:50%; background:var(--ok); flex:0 0 auto; } + #toast.t-err::before, #toast.t-s-error::before, #toast.t-s-needs_data::before, + #toast.t-s-needs_mapping::before { background:var(--err); } + #toast.t-warn::before, #toast.t-s-needs_review::before { background:var(--warn); } + /* Rand de preview tocmai actualizat: flash scurt ca userul sa-l localizeze. */ + @keyframes rand-flash { 0% { background:color-mix(in srgb, var(--accent) 26%, var(--card)); } + 100% { background:transparent; } } + .tabel-trimiteri tr.rand-actualizat { animation:rand-flash 1.6s ease-out; } + /* Randuri deja-trimise / duplicate in preview: colapsate implicit (nu ocupa loc). + Reafisate cand userul apasa toggle-ul -> .preview-arata-trimise pe container. + !important fiindca regulile compacte mobil/tableta seteaza `tr{display:flex}`. */ + .tabel-trimiteri tr.preview-sent-row { display:none !important; } + .tabel-trimiteri.preview-arata-trimise tr.preview-sent-row { display:table-row !important; } + /* Stepper import compact (design.md §5.4). >=1024px: bara slim. <1024px: "Pasul N din 4" + progres. */ + .stepper { margin-bottom:16px; } + .stepper-track { display:flex; align-items:stretch; border:1px solid var(--line); border-radius:8px; + overflow:hidden; background:var(--card); } + .stepper-step { flex:1; display:flex; align-items:center; gap:8px; padding:10px 14px; min-height:44px; + border-right:1px solid var(--line); } + .stepper-step:last-child { border-right:none; } + .stepper-nr { display:inline-flex; align-items:center; justify-content:center; width:20px; height:20px; + border-radius:50%; font-size:11px; font-weight:700; flex-shrink:0; + background:var(--line); color:var(--muted); } + .stepper-tx { font-size:13px; color:var(--muted); white-space:nowrap; } + .stepper-step.is-active { background:color-mix(in srgb, var(--accent) 10%, transparent); } + .stepper-step.is-active .stepper-nr { background:var(--accent); color:#fff; } + .stepper-step.is-active .stepper-tx { color:var(--ink); font-weight:600; } + .stepper-step.is-done .stepper-nr { background:var(--ok); color:#fff; } + .stepper-step.is-done .stepper-tx { color:var(--ink); } + .stepper-help { margin:6px 2px 0; font-size:12px; color:var(--muted); } + .stepper-collapsed { display:none; } + .stepper-current { font-size:14px; font-weight:600; margin-bottom:6px; } + .stepper-current .muted { font-weight:400; } + .stepper-progress { height:5px; border-radius:99px; background:var(--line); overflow:hidden; } + .stepper-progress > span { display:block; height:100%; background:var(--accent); border-radius:99px; } .chk { font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px; } /* Tab-bar */ .tab-bar { display:flex; gap:2px; overflow-x:auto; -webkit-overflow-scrolling:touch; @@ -341,9 +418,12 @@ .tabel-trimiteri tr.trimitere-row:hover { background:color-mix(in srgb, var(--accent) 6%, transparent); } .tabel-trimiteri tr.trimitere-row:focus, .tabel-trimiteri tr.trimitere-row:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; } - /* 768-1024px: ascunde Actualizat (e in detaliu) -> 7 coloane, fara scroll */ + /* col-actualizat ca linie meta mica in carduri (decizie 5.13 #8). */ + .tabel-trimiteri td.col-actualizat { font-size:12px; } + /* Stepper: sub 1024px ascunde track-ul slim, arata forma colapsata (decizie 5.13 #11). */ @media (max-width:1024px) { - .tabel-trimiteri .col-actualizat { display:none; } + .stepper-track { display:none; } + .stepper-collapsed { display:block; } } /* Tableta (768–1024px): header compact fara suprapuneri. Grila 3-coloane se pastreaza; logo si titlu mai mici; versiunea ascunsa @@ -358,6 +438,43 @@ ascunsa pentru a elibera spatiu in celula dreapta. */ .header-right > .muted { display:none; } } + /* Tableta 768-1024px: listele actionabile raman O COLOANA, cardificate (nu tabel storcit, + nu 2/rand). Decizie 5.13 (premisa user). Tabelele dense read-only raman .tablewrap. */ + @media (min-width:768px) and (max-width:1024px) { + .tabel-trimiteri table, .tabel-card table { table-layout:auto; } + .tabel-trimiteri thead, .tabel-card thead { display:none; } + .tabel-trimiteri table, .tabel-trimiteri tbody, .tabel-trimiteri tr, .tabel-trimiteri td, + .tabel-card table, .tabel-card tbody, .tabel-card tr, .tabel-card td { display:block; width:auto; } + .tabel-trimiteri tr, .tabel-card tr { border:1px solid var(--line); border-radius:8px; + padding:10px 12px; margin-bottom:10px; } + .tabel-trimiteri td, .tabel-card td { border-bottom:none; padding:3px 0; } + .tabel-trimiteri td::before, .tabel-card td::before { content:attr(data-eticheta); display:block; + color:var(--muted); font-size:12px; margin-bottom:2px; } + .tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; } + .tabel-trimiteri td[data-eticheta=""]::before, .tabel-card td[data-eticheta=""]::before, + .tabel-card td:not([data-eticheta])::before { display:none; } + .tabel-card td select, .tabel-card td input[type=text], .tabel-card td input[type=search] { + width:100%; max-width:none; } + /* Card compact si pe tableta (acelasi layout ca pe mobil) pentru `.tabel-trimiteri`. */ + .tabel-trimiteri tr { display:flex; flex-wrap:wrap; align-items:baseline; gap:1px 8px; } + .tabel-trimiteri td { padding:0; } + /* Regula desktop `tr.trimitere-row > td { padding:11px }` e mai specifica -> o anulam + in cardul compact, altfel randurile de Trimiteri raman inalte/aerisite. */ + .tabel-trimiteri tr.trimitere-row > td { padding-top:0; padding-bottom:0; } + .tabel-trimiteri td::before { display:none; } + .tabel-trimiteri td.col-vehicul { order:1; flex:1 1 55%; min-width:0; font-weight:600; font-size:15px; line-height:1.25; } + .tabel-trimiteri td.col-vehicul .muted { font-weight:400; } + .tabel-trimiteri td.col-stare { order:2; flex:0 0 auto; margin-left:auto; align-self:flex-start; } + .tabel-trimiteri td.col-operatie { order:3; flex:1 1 100%; font-size:13px; line-height:1.3; margin-top:1px; } + .tabel-trimiteri td.col-data, .tabel-trimiteri td.col-km, .tabel-trimiteri td.col-rar { font-size:12px; color:var(--muted); } + .tabel-trimiteri td.col-data { order:4; } .tabel-trimiteri td.col-km { order:5; } .tabel-trimiteri td.col-rar { order:6; } + .tabel-trimiteri td.col-km::before { content:"· "; display:inline; color:var(--muted); } + .tabel-trimiteri td.col-actualizat { order:7; flex:1 1 100%; font-size:12px; color:var(--muted); } + .tabel-trimiteri td.col-note { order:8; flex:1 1 100%; font-size:12px; color:var(--accent); line-height:1.3; margin-top:1px; } + .tabel-trimiteri td.col-actiuni { order:9; flex:0 0 auto; margin-left:auto; margin-top:4px; text-align:right; } + .tabel-trimiteri td.col-actiuni button, .tabel-trimiteri td.col-actiuni .act { width:auto; min-height:32px; padding:5px 14px; } + .tabel-trimiteri.preview-arata-trimise tr.preview-sent-row { display:flex !important; } + } /* === Preview import: coloane extra fata de tabelul Trimiteri. SCOPAT prin .tabel-trimiteri (clasa partajata). Regiune separata — nu atinge coloanele existente (col-chk/id/stare/data/rar/actualizat). @@ -367,7 +484,12 @@ Restul (~680px la 1280px) revine lui col-vehicul + col-operatie (fluid). === */ .tabel-trimiteri .col-km { width:76px; } .tabel-trimiteri .col-note { width:176px; } - .tabel-trimiteri .col-actiuni { width:92px; } + /* Nota preview = culoarea camp-fix (accent), ca sa atraga atentia (dogfood 5.13): + campul Note e self-explanatory, asa ca hint-urile per-camp au fost scoase. */ + .tabel-trimiteri td.col-note { color:var(--accent); } + /* Pill-ul de stare nu se rupe pe doua randuri in cardul compact. */ + .tabel-trimiteri td.col-stare .pill { white-space:nowrap; } + .tabel-trimiteri .col-actiuni { width:104px; } /* Randul de editare inline iese din grila table-layout:fixed (display:block), astfel formularul nu e constrans de latimile coloanelor individuale. Salveaza/Anuleaza sunt mereu vizibile (overflow:visible, nu clip). */ @@ -401,16 +523,51 @@ Breakpoint unic 767px (vezi conventia de sus). Cuprinde: card per rand pe tabelul de trimiteri, modal full-screen, header/nav colapsat cu tinte touch >=44px. Desktop (>=1024px) ramane neschimbat — regulile de baza nu se modifica. */ + /* SENTINEL-TESTE-MOBIL: blocul mobil principal incepe mai jos; testele ancoreaza pe acest marker si feliaza pana la sfarsitul stilului. NU muta/sterge. */ @media (max-width:767px) { /* Tabel trimiteri: card per rand (eticheta:valoare stivuit) -> fara scroll orizontal */ .tabel-trimiteri table { table-layout:auto; } .tabel-trimiteri thead { display:none; } .tabel-trimiteri table, .tabel-trimiteri tbody, .tabel-trimiteri tr, .tabel-trimiteri td { display:block; width:auto; } .tabel-trimiteri tr { border:1px solid var(--line); border-radius:8px; padding:8px 12px; margin-bottom:10px; } - .tabel-trimiteri td { border-bottom:none; padding:4px 0; display:flex; gap:10px; align-items:baseline; } - .tabel-trimiteri td::before { content:attr(data-eticheta); color:var(--muted); font-size:12px; - flex:0 0 auto; min-width:120px; } - .tabel-trimiteri td.col-chk { display:none; } + .tabel-trimiteri td { border-bottom:none; padding:3px 0; display:block; } + .tabel-trimiteri td::before { content:attr(data-eticheta); display:block; color:var(--muted); + font-size:12px; margin-bottom:2px; } + .tabel-trimiteri td.col-chk, .tabel-trimiteri td.col-id { display:none; } + + /* === Card COMPACT (PRD 5.13, corectie dogfood) === + Inlocuieste stiva generica eticheta+valoare (prea inalta) cu un card + scanabil la prima vedere: vehicul = titlu, stare = pill dreapta-sus, + operatie+cod pe rand, meta (data/km/rar) muted mic, nota mica. Fara + etichete-zgomot. Override DUPA regulile de baza (cascada: ultimul castiga). */ + .tabel-trimiteri tr { display:flex; flex-wrap:wrap; align-items:baseline; + gap:1px 8px; padding:9px 12px; } + .tabel-trimiteri td { display:block; padding:0; } + .tabel-trimiteri tr.trimitere-row > td { padding-top:0; padding-bottom:0; } + .tabel-trimiteri td::before { display:none; } /* compact: fara etichete */ + .tabel-trimiteri td.col-vehicul { order:1; flex:1 1 55%; min-width:0; + font-weight:600; font-size:15px; line-height:1.25; } + .tabel-trimiteri td.col-vehicul .muted { font-weight:400; } + .tabel-trimiteri td.col-stare { order:2; flex:0 0 auto; margin-left:auto; + align-self:flex-start; } + .tabel-trimiteri td.col-operatie { order:3; flex:1 1 100%; font-size:13px; + line-height:1.3; margin-top:1px; } + .tabel-trimiteri td.col-data, + .tabel-trimiteri td.col-km, + .tabel-trimiteri td.col-rar { font-size:12px; color:var(--muted); } + .tabel-trimiteri td.col-data { order:4; } + .tabel-trimiteri td.col-km { order:5; } + .tabel-trimiteri td.col-km::before { content:"· "; display:inline; color:var(--muted); } + .tabel-trimiteri td.col-rar { order:6; } + .tabel-trimiteri td.col-actualizat { order:7; flex:1 1 100%; font-size:12px; + color:var(--muted); } + .tabel-trimiteri td.col-note { order:8; flex:1 1 100%; font-size:12px; + color:var(--accent); line-height:1.3; margin-top:1px; } + .tabel-trimiteri td.col-actiuni { order:9; flex:0 0 auto; margin-left:auto; + margin-top:4px; text-align:right; } + .tabel-trimiteri td.col-actiuni button, + .tabel-trimiteri td.col-actiuni .act { width:auto; min-height:32px; padding:5px 14px; } + .tabel-trimiteri.preview-arata-trimise tr.preview-sent-row { display:flex !important; } /* Modal full-screen: ocupa tot ecranul, fara backdrop lateral (overlay fara padding, dialog la latime/inaltime pline, fara colturi/umbra). Scroll intern @@ -483,16 +640,40 @@ #import-section #upload-btn { width:100%; min-height:44px; } /* Bara de status: contoarele/randurile raman aliniate la stanga, fara scroll orizontal. */ #status-bar > div { gap:10px; } - /* Bara de filtre trimiteri: o coloana, fiecare control full-width, buton >=44px. - !important suprascrie latimile inline (ex. max-width:180px pe vehicul) DOAR pe mobil. */ - #filtre-trimiteri { flex-direction:column; align-items:stretch; } + /* Bara de filtre trimiteri ADAPTATA pentru mobil (nu doar stivuita): + - cautarea vehicul = rand propriu prioritar (input + buton pe acelasi rand); + - grupurile de pill-uri (data + stare) = scroll orizontal, compacte (nu 8 butoane + full-width unul sub altul). !important suprascrie latimile inline doar pe mobil. */ + #filtre-trimiteri { flex-direction:column; align-items:stretch; gap:8px; } #filtre-trimiteri > div { width:100%; } - #filtre-trimiteri select, #filtre-trimiteri input[type=text], - #filtre-trimiteri input[type=date] { width:100% !important; max-width:none !important; } - #filtre-trimiteri button { width:100%; min-height:44px; } + /* Cautarea vehicul: input creste, butonul Filtreaza compact langa el. */ + #filtre-trimiteri input[type=text] { flex:1 1 auto; width:auto !important; max-width:none !important; min-height:44px; } + #filtre-trimiteri input[type=date] { width:100% !important; max-width:none !important; min-height:44px; } + #filtre-trimiteri button[type=submit] { flex:0 0 auto; width:auto; min-height:44px; } + /* Grupurile de pill-uri: o singura banda scrolabila orizontal, compacta. */ + #filtre-trimiteri .pills-categorii { margin-left:0 !important; flex-wrap:nowrap; + overflow-x:auto; -webkit-overflow-scrolling:touch; padding-bottom:2px; } + #filtre-trimiteri .pill-cat { flex:0 0 auto; } + + /* Operatii de mapat (preview import): randul de mapare stiva pe o coloana, + select-ul full-width (altfel max-width:340px global il scoate din viewport). */ + .maprow { gap:6px 12px; padding:10px 0; } + .maprow .mapcol { flex:1 1 100%; min-width:0; } + .maprow select { width:100% !important; max-width:none !important; } + .maprow button { width:100%; min-height:44px; } /* Card de autentificare (login/signup): centrat si nu depaseste viewport-ul pe mobil. */ .auth-card { max-width:100%; margin:24px auto; } + /* Versiunea ascunsa pe mobil (la fel ca pe tableta). */ + .header-right > .muted { display:none; } + /* Actiuni .act pe mobil: iconita patrata 44px, textul ascuns. */ + .act { min-width:44px; min-height:44px; width:44px; padding:0; } + .act .act-tx { display:none; } + .act .act-ic { display:inline-block; } + .act-group { gap:10px; } + /* Bara confirmare compacta pe mobil. */ + .sticky-bar { padding:10px 12px; gap:10px; } + .sticky-bar button { width:100%; min-height:44px; } } @@ -551,6 +732,9 @@ {# aria-live pentru anuntarea schimbarilor de tema (accesibilitate) #} + {# Toast global: feedback tranzitoriu (ex. dupa salvarea unui rand de import). + aria-live=polite -> citit de screen-reader. window.arataToast(text, stareCss). #} +
{% block content %}{% endblock %}
{# Modal detaliu trimitere: container global, SIBLING al
(nu descendent), ca `inert`+`aria-hidden` pe
sa nu-l prinda si pe el. Corpul @@ -696,6 +880,39 @@ if (saveBtn) saveBtn.classList.add('dirty'); }); +
{{ row.row_index + 1 }} {{ row.stare_eticheta }} @@ -32,9 +32,6 @@ {% if row.prez.vin_scurt and row.prez.vin_scurt != '—' %}
{{ row.prez.vin_scurt }}
{% endif %} - {# Fix-uri de validare pe vehicul #} - {% if disp_fix_map.get('vin') %}{{ disp_fix_map.get('vin') }}{% endif %} - {% if disp_fix_map.get('nr_inmatriculare') %}{{ disp_fix_map.get('nr_inmatriculare') }}{% endif %}
{{ row.prez.operatie }}
@@ -44,14 +41,8 @@
nemapat
{% endif %}
- {{ row.prez.data_prestatie }} - {% if disp_fix_map.get('data_prestatie') %}{{ disp_fix_map.get('data_prestatie') }}{% endif %} - - {{ row.prez.odometru }} - {% if disp_fix_map.get('odometru_final') %}{{ disp_fix_map.get('odometru_final') }}{% endif %} - {{ row.prez.data_prestatie }}{{ row.prez.odometru }} {% if status == 'already_sent' and row.get('already_sent_info') %} @@ -68,7 +59,7 @@ {% if status not in ('already_sent', 'duplicate_in_file') %}