diff --git a/TODOS.md b/TODOS.md new file mode 100644 index 0000000..de00797 --- /dev/null +++ b/TODOS.md @@ -0,0 +1,18 @@ +# TODOS + +Elemente deferate din review-uri. Negrupte de un PRD curent; de promovat cand devin prioritare. + +## Din /autoplan PRD 5.11 (2026-06-26) + +- [ ] **E2E smoke de first-run ca poarta de release** — codifica scriptul de dogfooding + (import -> mapcoloane -> preview -> commit -> lista apare + contoare) ca test E2E care + trebuie sa treaca inainte de orice release. Motiv: cele 8 bug-uri din 5.11 sunt toate + first-run friction nedogfooded end-to-end; fara o poarta, reapar ca 8 tichete noi. + (CEO F2, severitate high.) + +- [ ] **Control compensator optional pe auto-trimitere unattended** — utilizatorul a ales + (2026-06-26) scoaterea completa a hold-ului auto_send. Risc rezidual acceptat: o regula + text gresita poate auto-trimite FINALIZATA (terminal, fara undo) pe randuri pe canalul API / + remapare inline (fara gate de preview). Daca apar integratori reali, evalueaza un throttle + „primele N auto-trimiteri pe o regula text noua cer confirmare" sau un kill-switch per cont. + (CEO F5/F6, severitate critical ca risc, dar pre-launch exposure ~zero acum.) diff --git a/app/api/v1/import_router.py b/app/api/v1/import_router.py index b509803..bd00161 100644 --- a/app/api/v1/import_router.py +++ b/app/api/v1/import_router.py @@ -221,7 +221,7 @@ def _resolve_row_for_preview( errors = validate_prezentare(mapped) if all_flags: - # needs_review: chiar daca validarea trece, flagurile blocheaza auto-send + # needs_review: validarea a trecut, dar flagurile (date ambigue, formule) cer confirmare manuala return { "resolved_status": "needs_review", "resolved": mapped, @@ -229,14 +229,7 @@ def _resolve_row_for_preview( "flags": all_flags, } - # auto_send gate - if has_no_auto_send(resolved, mapping_meta): - return { - "resolved_status": "needs_mapping", - "resolved": mapped, - "errors": [{"auto_send": "cod mapat cu auto_send=0; review manual inainte de trimitere"}], - "flags": all_flags, - } + # US-001 (PRD 5.11): ramura auto_send eliminata din preview. if errors: return { diff --git a/app/api/v1/router.py b/app/api/v1/router.py index 1166101..5474a9d 100644 --- a/app/api/v1/router.py +++ b/app/api/v1/router.py @@ -87,8 +87,10 @@ def _erori_nemapate(unmapped: list[dict]) -> list[dict]: def _motiv_clasificare(cl: dict) -> str | None: """Rezumat uman pe o linie pentru un rezultat de clasificare. - None cand status='queued'. Acopera toate ramurile de blocaj: erori de continut - (needs_data), coduri nemapate (needs_mapping) si auto_send oprit (needs_mapping). + None cand status='queued'. Acopera ramurile de blocaj: erori de continut + (needs_data) si coduri fara mapare RAR (needs_mapping). + Dupa US-001: needs_mapping apare EXCLUSIV cand unmapped e non-gol + (ramura auto_send_oprit era inaccesibila si a fost eliminata). """ if cl["status"] == "queued": return None @@ -99,8 +101,6 @@ def _motiv_clasificare(cl: dict) -> str | None: if cl["unmapped"]: coduri = ", ".join((u.get("cod_op_service") or "") for u in cl["unmapped"]) return f"Coduri fara mapare RAR: {coduri}" - if cl["status"] == "needs_mapping": - return "Cod cu trimitere automata oprita; confirmare manuala inainte de trimitere." return None @@ -156,7 +156,7 @@ def create_prezentari( conn = get_connection() results: list[SubmissionResult] = [] try: - # load_mapping_meta include auto_send per op (gate pentru coduri noi). + # load_mapping_meta incarca maparea op->cod RAR; dupa US-001, auto_send nu mai tine randuri. mapping_meta = load_mapping_meta(conn, acct) mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()} # Validare cod_prestatie fata de nomenclator + modul la cod necunoscut/nemapat. @@ -230,7 +230,7 @@ def create_prezentari( continue # Helper pur partajat cu dry-run: reproduce EXACT clasificarea - # (canonicalize + mapare op->cod + validare + auto_send gate). + # (canonicalize + mapare op->cod + validare; auto_send gate eliminat dupa US-001). cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules) if cl["blocked_error"]: # on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat). diff --git a/app/mapping.py b/app/mapping.py index 6a135af..47249cd 100644 --- a/app/mapping.py +++ b/app/mapping.py @@ -249,10 +249,8 @@ def resolve_prestatii( # Adnotare aditiva: marcheaza ca rezolvat-prin-regula cu pattern-ul # sursa. Payload-harmless (RAR citeste doar cod_prestatie). it["cod_sursa"] = f"{COD_SURSA_TEXT_RULE_PREFIX}{pattern_regula or ''}" - # Siguranta: regula cu auto_send=0 rezolva codul dar TINE randul - # pentru verificare umana (has_no_auto_send -> True). - if not auto_send_regula: - it["regula_fara_autosend"] = True + # US-001 (PRD 5.11): regula_fara_autosend nu se mai seteaza; + # auto_send nu mai tine randul (has_no_auto_send neutralizat). else: it["cod_prestatie"] = None unmapped.append({"cod_op_service": op, "denumire": it.get("denumire")}) @@ -410,14 +408,9 @@ def classify_prezentare( if errors: status = "needs_data" rar_error = json.dumps(errors, ensure_ascii=False) - elif has_no_auto_send(resolved, mapping_meta): - status = "needs_mapping" - mesaj = "cod mapat cu auto_send=0; review manual inainte de trimitere" - rar_error = json.dumps( - {"auto_send": mesaj, **err_mod.eroare("AUTO_SEND_OPRIT", cauza=mesaj)}, - ensure_ascii=False, - ) else: + # US-001 (PRD 5.11): ramura AUTO_SEND_OPRIT eliminata. + # Un cod rezolvat (mapare exacta sau regula text) -> queued direct. status = "queued" rar_error = None @@ -432,20 +425,14 @@ def classify_prezentare( def has_no_auto_send(resolved: list[dict], mapping_meta: dict[str, dict]) -> bool: - """Verifica daca vreun item rezolvat are auto_send=0 (mapare exacta SAU regula text). + """Neutralizat dupa US-001 (PRD 5.11): auto_send nu mai tine randuri in needs_mapping. - Un cod nou-mapat (operations_mapping) cu auto_send=0 nu trebuie trimis automat. - La fel pentru un item rezolvat printr-o REGULA TEXT cu auto_send=0 — marcat de - `resolve_prestatii` cu `regula_fara_autosend`. In ambele cazuri randul ramane - needs_mapping (review manual) pana cand operatorul activeaza „In coada". - Items cu cod_prestatie direct (nu via cod_op_service/regula) nu sunt afectate. + Simbolul este PASTRAT (importat in routes.py si import_router.py); stergerea + ar produce ImportError la boot. Functia intoarce mereu False — codul rezolvat + intra direct in queued, indiferent de valoarea auto_send din mapping_meta. + + Coloanele DB raman cu default=1 (migrare non-distructiva). """ - for item in resolved: - if item.get("regula_fara_autosend"): - return True - op = (item.get("cod_op_service") or "").strip() - if op and op in mapping_meta and not mapping_meta[op]["auto_send"]: - return True return False @@ -660,18 +647,8 @@ def reresolve_account(conn, account_id: int | None, batch_id: int | None = None) stats["still_blocked"] += 1 continue - # Verifica auto_send inainte de re-queuing. - if has_no_auto_send(resolved, mapping_meta): - conn.execute( - "UPDATE submissions SET payload_json=?, rar_error=?, updated_at=datetime('now') WHERE id=?", - ( - payload_json, - json.dumps({"auto_send": "cod mapat cu auto_send=0; review manual inainte de trimitere"}, ensure_ascii=False), - r["id"], - ), - ) - stats["review_manual"] += 1 - continue + # US-001 (PRD 5.11): ramura auto_send eliminata din reresolve. + # Un cod rezolvat -> queued direct (review_manual ramane 0). errors = validate_prezentare(content) if errors: diff --git a/app/web/labels.py b/app/web/labels.py index b1eb2be..daf9efc 100644 --- a/app/web/labels.py +++ b/app/web/labels.py @@ -327,6 +327,80 @@ def parse_erori(rar_error: object) -> list[dict]: return [{"problema": str(data)[:200], "cauza": "", "fix": "", "field": None}] +# --------------------------------------------------------------------------- +# Etichete stari preview import (vocabular DIFERIT de starile de submission) +# +# Starile de preview (ok/needs_review/already_sent/duplicate_in_file) NU +# exista in STARI_SUBMISSION — reutilizarea directa a eticheta_stare/eticheta_scurta +# ridica KeyError. Acest map este sursa de adevar pentru stratul de adaptare din +# _web_compute_preview (routes.py) si pentru template (_preview_rand.html). +# --------------------------------------------------------------------------- + +STARI_PREVIEW: dict[str, tuple[str, str]] = { + "ok": ("Gata de trimis", "s-ok"), + "needs_review": ("Verifica valori", "s-needs_review"), + "needs_mapping": ("Cod RAR lipsa", "s-needs_mapping"), + "needs_data": ("Date incomplete", "s-needs_data"), + "already_sent": ("Deja trimis", "s-already_sent"), + "duplicate_in_file": ("Duplicat in fisier", "s-duplicate_in_file"), +} + + +def nota_umana_preview(status: str, errors: list, flags: list) -> str: + """Formateaza mesajul uman pentru coloana Note din tabelul de preview import. + + Primeste ``errors`` ca lista Python (nu JSON string) — NU pasa la motiv_uman + sau parse_erori care asteapta un JSON string si ar produce repr Python brut + prin fallback ``raw[:160]`` (bug documentat in PRD 5.11 US-003). + + Logica de prioritate: + - already_sent / duplicate_in_file -> "" (template le afiseaza separat) + - needs_mapping -> unmapped INAINTE de flags (codul lipsa e motivul real) + - flags non-goale -> primul flag (needs_review: data ambigua etc.) + - errors cu cheie "unmapped" -> "Cod RAR lipsa pentru: COD1, COD2" + - errors cu field+message (needs_data) -> primul mesaj de validare + - altceva -> "" + + Fara exceptii. Trunchiat la 200 caractere. + """ + if status in ("already_sent", "duplicate_in_file"): + return "" + # needs_mapping: codul RAR lipseste — prioritizeaza 'unmapped' inaintea flags, + # altfel un rand cu si un flag (ex. VIN numeric) ar afisa textul flag-ului + # si ascunde motivul real (cod lipsa). + if status == "needs_mapping": + for e in errors: + if not isinstance(e, dict): + continue + if "unmapped" in e: + ops = e.get("unmapped") or [] + coduri = ", ".join( + o.get("cod_op_service", "") for o in ops if isinstance(o, dict) + ) + return ("Cod RAR lipsa pentru: " + coduri if coduri else "Cod RAR lipsa") + if flags: + return str(flags[0])[:200] + for e in errors: + if not isinstance(e, dict): + continue + if "unmapped" in e: + ops = e.get("unmapped") or [] + coduri = ", ".join( + o.get("cod_op_service", "") for o in ops if isinstance(o, dict) + ) + return (f"Cod RAR lipsa pentru: {coduri}" if coduri else "Cod RAR lipsa") + msg = ( + e.get("message") + or e.get("msg") + or e.get("problema") + or e.get("cauza") + or "" + ) + if msg: + return str(msg)[:200] + return "" + + # --------------------------------------------------------------------------- # Constante auxiliare (microcopy fix, fara logica) # --------------------------------------------------------------------------- diff --git a/app/web/routes.py b/app/web/routes.py index 17883ed..633cbbb 100644 --- a/app/web/routes.py +++ b/app/web/routes.py @@ -29,12 +29,14 @@ from ..payload_view import prezentare_din_payload from ..web.csrf import get_csrf_token, verify_csrf from .labels import ( ETICHETA_ULTIMA_AUTENTIFICARE_RAR, + STARI_PREVIEW, eticheta_rar, eticheta_scurta, eticheta_stare, eticheta_worker, format_data_rar, motiv_uman, + nota_umana_preview, parse_erori, ) from ..web.session import require_login @@ -593,6 +595,40 @@ def _pills_categorii(counts: dict[str, int]) -> list[dict]: ] +def _build_status_ctx(request: Request, conn, account_id: int, *, oob: bool = False, tab_activ: str = "acasa") -> dict: + """Construieste dictionarul de context pentru _status.html. + + Accepta o conexiune deja deschisa (nu deschide alta). Folosit de fragment_status + si de web_confirma_import (OOB swap dupa commit). + """ + counts = _status_counts(conn, account_id) + hb = read_heartbeat(conn) + worker_alive = _worker_alive(hb) + rar_state = _rar_state(hb, worker_alive) + worker_lbl = eticheta_worker(worker_alive) + rar_ok = rar_state == "ok" + rar_lbl = eticheta_rar("ok" if rar_ok else rar_state) + blocate_total = sum(counts.get(s, 0) for s in _BLOCKED) + return { + "request": request, + "worker_lbl": worker_lbl, + "rar_lbl": rar_lbl, + "worker_ok": worker_alive, + "rar_ok": rar_ok, + "eticheta_ultima_auth": ETICHETA_ULTIMA_AUTENTIFICARE_RAR, + "last_login": format_data_rar(hb["last_rar_login_ok"] if hb else None), + "counts_queued": counts.get("queued", 0), + "counts_sent": counts.get("sent", 0), + "blocate_total": blocate_total, + "blocate_defalcat": _blocate_defalcat(counts), + "pills_categorii": _pills_categorii(counts), + "account_active": _account_active(conn, account_id), + "tab_activ": tab_activ, + "mapari_badge": counts.get("needs_mapping", 0), + "oob": oob, + } + + @router.get("/_fragments/status", response_class=HTMLResponse) def fragment_status(request: Request) -> HTMLResponse: """Bara de status persistenta cu etichete umane. @@ -604,34 +640,9 @@ def fragment_status(request: Request) -> HTMLResponse: account_id = require_login(request) conn = get_connection() try: - counts = _status_counts(conn, account_id) - hb = read_heartbeat(conn) - worker_alive = _worker_alive(hb) - rar_state = _rar_state(hb, worker_alive) - - # Etichete umane pre-calculate (nu logica in template) - worker_lbl = eticheta_worker(worker_alive) - # eticheta_rar accepta "ok" sau orice alt string -> indisponibil/necunoscut - rar_ok = rar_state == "ok" - rar_lbl = eticheta_rar("ok" if rar_ok else rar_state) - blocate_total = sum(counts.get(s, 0) for s in _BLOCKED) - - return templates.TemplateResponse("_status.html", { - "request": request, - "worker_lbl": worker_lbl, - "rar_lbl": rar_lbl, - # Stari binare pentru bife accesibile: glifa + culoare - "worker_ok": worker_alive, - "rar_ok": rar_ok, - "eticheta_ultima_auth": ETICHETA_ULTIMA_AUTENTIFICARE_RAR, - "last_login": format_data_rar(hb["last_rar_login_ok"] if hb else None), - "counts_queued": counts.get("queued", 0), - "counts_sent": counts.get("sent", 0), - "blocate_total": blocate_total, - "blocate_defalcat": _blocate_defalcat(counts), - "pills_categorii": _pills_categorii(counts), - "account_active": _account_active(conn, account_id), - }) + tab_activ = request.query_params.get("tab", "acasa") + ctx = _build_status_ctx(request, conn, account_id, tab_activ=tab_activ) + return templates.TemplateResponse("_status.html", ctx) finally: conn.close() @@ -1163,21 +1174,7 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR message="Lipseste inca un cod RAR — alege-l mai jos sau in tab-ul Mapari."), ) - if has_no_auto_send(resolved, mapping_meta): - conn.execute( - "UPDATE submissions SET status='needs_mapping', payload_json=?, rar_error=?, " - "updated_at=datetime('now') WHERE id=?", - (payload_json, - json.dumps({"auto_send": "cod mapat cu auto_send=0; review manual inainte de trimitere"}, - ensure_ascii=False), - row["id"]), - ) - row2 = _fetch_submission_scoped(conn, account_id, submission_id) - return templates.TemplateResponse( - "_trimitere_detaliu.html", - _detaliu_ctx(request, row2, error=True, - message="Cod cu auto-send oprit — confirma manual din tab-ul Mapari."), - ) + # US-001 (PRD 5.11): ramura auto_send eliminata din corectie. errors = validate_prezentare(content) if errors: @@ -2025,6 +2022,29 @@ def _web_compute_preview( except Exception: conn.execute("ROLLBACK") + # Enrichment UI: adauga campuri pre-computate necesare template-ului. + # Toate consumatorii (preview complet, rand single via _preview_one_row) + # obtin automat campurile adaugate aici. + for row in preview_rows: + # view-model prez (vehicul/operatie/cod RAR) — prezentare_din_payload + # accepta dict direct (nu e nevoie de serializare/deserializare JSON). + row["prez"] = prezentare_din_payload(row["resolved"]) + # Eticheta umana + clasa CSS pentru pill — din STARI_PREVIEW, nu STARI_SUBMISSION + # (eticheta_stare ridica KeyError pe ok/already_sent/duplicate_in_file). + _etq, _css = STARI_PREVIEW.get( + row["resolved_status"], + (row["resolved_status"], f"s-{row['resolved_status']}"), + ) + row["stare_eticheta"] = _etq + row["stare_css"] = _css + # Nota umana formatata — errors e lista Python, NU JSON string; + # nota_umana_preview o trateaza corect (fara repr Python brut in Note). + row["nota_umana"] = nota_umana_preview( + row["resolved_status"], + row.get("errors") or [], + row.get("flags") or [], + ) + nomenclator = load_nomenclator(conn) return { "rows": preview_rows, @@ -2750,18 +2770,33 @@ async def web_confirma_import( (n_enqueued, import_id), ) - # Succes → bara de upload slim cu mesaj de confirmare. are_trimiteri=True: - # contul tocmai a pus randuri in coada -> bara ramane slim si dezvaluie - # sectiunea "Trimiterile tale" de pe Acasa. + # Succes → bara de upload slim cu mesaj de confirmare + OOB swap al + # #trimiteri-section (injecteaza _coada.html cu lista proaspata) + + # header HX-Trigger: trimiteriChanged (declanseza reincarcarea automata). toctou_msg = f" ({len(toctou)} coliziuni TOCTOU excluse)" if toctou else "" - return templates.TemplateResponse("_upload.html", _ctx( - request, - are_trimiteri=True, - message=( - f"S-au pus in coada {n_enqueued} prezentari{toctou_msg}. " - f"Procesarea incepe in cateva secunde — vezi mai jos, in Trimiterile tale." - ), - )) + succes_msg = ( + f"S-au pus in coada {n_enqueued} prezentari{toctou_msg}. " + f"Procesarea incepe in cateva secunde — vezi mai jos, in Trimiterile tale." + ) + + # Calculeaza contextele (necesita conn deschis) inainte de finally. + acasa_ctx = _get_acasa_context(request, conn, account_id) + acasa_ctx["status_filtru"] = "" + acasa_ctx["oob"] = True # adauga hx-swap-oob="outerHTML" la
+ + status_ctx = _build_status_ctx(request, conn, account_id, oob=True) + + # Randeaza imediat (conn inca deschis — query-urile s-au facut mai sus). + upload_html = templates.get_template("_upload.html").render( + _ctx(request, are_trimiteri=True, message=succes_msg) + ) + coada_html = templates.get_template("_coada.html").render(acasa_ctx) + status_html = templates.get_template("_status.html").render(status_ctx) + + return HTMLResponse( + content=upload_html + "\n" + coada_html + "\n" + status_html, + headers={"HX-Trigger": "trimiteriChanged"}, + ) finally: conn.close() diff --git a/app/web/templates/_acasa.html b/app/web/templates/_acasa.html index d41770b..302188c 100644 --- a/app/web/templates/_acasa.html +++ b/app/web/templates/_acasa.html @@ -1,7 +1,16 @@
- {# === Centru de greutate: bara de upload (importul e operatia principala) === #} - {% include '_upload.html' %} + {# === Container colapsabil: stepper + upload intr-un singur element
(US-006). + Serverul seteaza atributul `open` din are_trimiteri: + are_trimiteri=False (first-run) → open (importul e vizibil imediat, fara JS) + are_trimiteri=True (returning) → colapsat (nu ocupa ecranul, dar e accesibil la click) + Degradare fara JS: corecta pe ambele ramuri. + In timpul fluxului (mapcoloane/preview), HTMX face swap pe #import-section (descendentul + intern) →
ramane neatins → containerul ramane deschis intre pasi. === #} +
+ Importa un fisier + {% include '_upload.html' %} +
{# === Subordonat: primii pasi pe un singur rand compact === #} {% set toti_esentiali = are_creds and are_trimiteri %} @@ -44,10 +53,14 @@
{% endif %} - {# Sectiunea Trimiteri, permanenta sub upload. Suprimata la first-run (zero - trimiteri): bara de upload acopera deja CTA-ul, iar empty-state-ul ar fi redundant. #} + {# Sectiunea Trimiteri, permanenta sub upload. + La first-run (zero trimiteri), randam un placeholder
gol/ascuns — necesar + ca OOB swap-ul de la confirma sa gaseasca tinta valida in DOM si sa injecteze + _coada.html fara reload complet. Fara placeholder, HTMX ignora silentios OOB-ul. #} {% if are_trimiteri %} {% include '_coada.html' %} + {% else %} + {% endif %} diff --git a/app/web/templates/_coada.html b/app/web/templates/_coada.html index b3cf15a..b9ba351 100644 --- a/app/web/templates/_coada.html +++ b/app/web/templates/_coada.html @@ -3,7 +3,8 @@ Filtre + tabel (_submissions.html); detaliul se deschide in modalul global (#modal-detaliu). #}
+ style="margin-top:22px; padding-top:18px; border-top:2px solid var(--line);" + {% if oob %}hx-swap-oob="outerHTML"{% endif %}>

@@ -19,45 +20,66 @@

- +
+ style="display:flex; gap:8px 12px; flex-wrap:wrap; align-items:center; margin-bottom:12px;"> {# Pagina curenta — actualizata prin OOB swap din _submissions.html; inclusa la reincarcari. #} -
- - + + {# === STANGA: Quick-pills de data (preset interval) + buton Custom === + Azi / 7 zile / 30 zile → seteaza interval preset si submitr automat. + Custom → dezvaluie #custom-date-fields pentru introducere manuala (fara submit automat). #} +
+
+ + + + +
+ {# Campuri de data pentru modul Custom: ascunse pana la click pe „Custom". + type="date" (nu hidden) permite interactiunea utilizatorului. + Campul change pe form re-incarca automat lista via hx-trigger="change". #} +
-
- - + + {# === MIJLOC: cautare vehicul (nr/VIN) + buton Filtreaza === #} +
+ +
-
- - -
- - {# Pill-uri de stare pe acelasi rand cu filtrele; re-randate prin OOB la reincarcarea tabelului. #} - + + {# === DREAPTA: pill-uri de stare cu contoare; re-randate via OOB la reincarcarea tabelului === #} + {% include '_pills.html' %} - - - - +
Auto pe un singur rand. - INVARIANT BACKEND: control = checkbox cu `name="auto_send" value="true"` si - SEMANTICA DE PREZENTA (bifat -> trimite "true" -> True; nebifat -> camp absent -> False). - E singura forma compatibila cu AMBELE parsere: `Form(bool)` la /mapari SI `bool(form.get())` - la /_import/.../mapare-operatie. Radio Auto/Manual cu value="false" ar trimite campul prezent - pe "Manual" -> `bool("false")` = True la import (regresie tacuta). De aceea comutator vizual - Manual<->Auto peste checkbox, NU doua radio-uri. - - form_id: leaga input-ul de un
extern (necesar in celulele de tabel). - - checked: starea STOCATA per mapare — bifat = Auto. #} -{% macro autosend_toggle(form_id='', checked=True, label='') -%} - -{%- endmacro %} +{# US-002 (PRD 5.11): autosend_toggle neutralizat — auto_send nu mai tine randuri (US-001). + Simbolul pastrat (apelat in _mapari.html, _preview_import.html, _trimitere_detaliu.html) + dar intoarce string gol. Coloanele DB raman (default 1, ne-citite pentru hold). #} +{% macro autosend_toggle(form_id='', checked=True, label='') -%}{%- endmacro %} diff --git a/app/web/templates/_mapari.html b/app/web/templates/_mapari.html index 653c5e3..3ab039c 100644 --- a/app/web/templates/_mapari.html +++ b/app/web/templates/_mapari.html @@ -32,7 +32,6 @@ Operatie Sugestii Cod RAR - In coada @@ -69,9 +68,6 @@ {% endfor %} - - {{ ui.autosend_toggle(form_id="map-rez-" ~ loop.index, checked=True) }} - @@ -107,7 +103,6 @@ Operatie Cod RAR - In coada Actiuni @@ -139,9 +134,6 @@ {% endfor %} - - {{ ui.autosend_toggle(form_id="map-salv-" ~ loop.index, checked=m.auto_send) }} - {# 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, @@ -182,8 +174,6 @@ O regula leaga orice operatie al carei text contine (nu egal, ci substring) un cuvant de un cod RAR. Util pentru operatii fara cod intern: ex. orice operatie care contine „verificare" primeste codul ales. Match insensibil la majuscule/diacritice. - In coada: implicit oprit — regula rezolva codul dar tine randul pentru - verificare umana pana activezi „In coada".

{% if not text_rules %} @@ -198,7 +188,6 @@ Daca operatia contine Cod RAR - In coada Actiuni @@ -216,9 +205,6 @@ {{ r.cod_prestatie }} - - {% if r.auto_send %}Auto (in coada){% else %}Manual (verificare){% endif %} - {# Preview pre-salvare: cate operatii nemapate potriveste pattern-ul. #} - +
diff --git a/app/web/templates/_preview_import.html b/app/web/templates/_preview_import.html index 50358bd..f35e70b 100644 --- a/app/web/templates/_preview_import.html +++ b/app/web/templates/_preview_import.html @@ -17,25 +17,25 @@
{% endif %} - + {% set status_labels = [ - ('ok', 'gata de trimis'), - ('needs_review', 'verifica valori'), - ('needs_mapping', 'fara cod RAR'), - ('needs_data', 'date lipsa'), - ('already_sent', 'deja trimis'), - ('duplicate_in_file','dublicat in fisier'), + ('ok', 'Gata de trimis'), + ('needs_review', 'Verifica valori'), + ('needs_mapping', 'Cod RAR lipsa'), + ('needs_data', 'Date incomplete'), + ('already_sent', 'Deja trimis'), + ('duplicate_in_file','Duplicat in fisier'), ] %}
{% for status_key, label in status_labels %} {%- set cnt = summary.get(status_key, 0) -%} {% if cnt > 0 %} - {{ cnt }} {{ label }} + {{ cnt }} {{ label | lower }} {% endif %} {% endfor %}
- +
{% endif %} {% endfor %} @@ -96,9 +96,6 @@ {% endfor %}
-
- {{ ui.autosend_toggle(checked=True, label="In coada automat") }} -
@@ -107,23 +104,23 @@
{% endif %} - -
+
- - - - - - - - - - + + + + + + + + + @@ -132,6 +129,11 @@ {% endfor %}
#VINNr. Inm.DataKM finalOperatieStareNoteVerificat?Actiuni#StareVehiculOperatieDataKM finalNoteVerificat?Actiuni
+ +
@@ -240,11 +242,17 @@ if (btn) btn.disabled = (total === 0) || editing; } - /* Filtrare randuri dupa stare */ + /* Filtrare randuri dupa stare. + Cand niciun rand nu e vizibil, afiseaza mesajul #preview-zero-message. */ function filterRows(status) { + var visible = 0; document.querySelectorAll('tbody tr[data-status]').forEach(function(tr) { - tr.style.display = (status === 'all' || tr.dataset.status === status) ? '' : 'none'; + var show = status === 'all' || tr.dataset.status === status; + tr.style.display = show ? '' : 'none'; + if (show) visible++; }); + var zeroMsg = document.getElementById('preview-zero-message'); + if (zeroMsg) zeroMsg.style.display = (visible === 0) ? '' : 'none'; document.querySelectorAll('.filter-btn').forEach(function(b) { var active = b.dataset.filter === status; b.style.background = active ? 'var(--accent)' : ''; diff --git a/app/web/templates/_preview_rand.html b/app/web/templates/_preview_rand.html index e9c8363..0688078 100644 --- a/app/web/templates/_preview_rand.html +++ b/app/web/templates/_preview_rand.html @@ -1,34 +1,41 @@ {# _preview_rand.html — un singur rand de preview import. Doua moduri: - - display (editing falsy): normal + buton "Editeaza" pe coloana de actiuni. - - edit (editing truthy): cu un singur ce contine un FORM PROPRIU - (NU #confirm-form) cu grila responsiva refolosita din _trimitere_detaliu.html. + - display (editing falsy): normal cu 9 coloane in format .tabel-trimiteri. + - edit (editing truthy): (display:block) cu un singur + ce contine un FORM PROPRIU (NU #confirm-form). Escapa grila table-layout:fixed. Swap pe RAND (hx-target pe #preview-row-N, outerHTML), NU pe #import-section. La save, optional OOB pe rezumat + contor "gata de trimis" (include_oob). + + Campuri pre-computate de _web_compute_preview (NOT din template raw): + row.prez — prezentare_din_payload(resolved): vehicul_nr, vin_scurt, + operatie, cod_rar, data_prestatie, odometru + row.stare_eticheta — text uman (ex. "Gata de trimis"), din STARI_PREVIEW + row.stare_css — clasa CSS (ex. "s-ok"), din STARI_PREVIEW + row.nota_umana — mesaj uman formatat pentru coloana Note (fara repr Python) #} {%- set res = row.resolved -%} {%- set status = row.resolved_status -%} -{%- set prestatii = res.get('prestatii') or [] -%} -{%- set op = (prestatii[0].get('cod_prestatie') or prestatii[0].get('cod_op_service', '')) if prestatii else '' -%} {% if editing %} {%- set err_map = {} -%} {%- set fix_map = {} -%} {%- for e in row.errors -%}{%- if e is mapping and e.get('field') -%}{%- set _ = err_map.update({e.field: (e.get('message') or e.get('msg'))}) -%}{%- if e.get('fix') -%}{%- set _ = fix_map.update({e.field: e.get('fix')}) -%}{%- endif -%}{%- endif -%}{%- endfor -%} - - + + + hx-on::response-error="this.querySelector('.rand-eroare-banner').style.display='block';" + style="padding:12px; background:rgba(91,141,239,.06); border-radius:4px;">
Editare rand {{ row.row_index + 1 }} - {{ status }} + {{ row.stare_eticheta }}
{% if message %} @@ -91,22 +98,37 @@ {%- 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 -%} - {{ row.row_index + 1 }} - {{ res.get('vin') or '' | safe }} - {% if disp_fix_map.get('vin') %}{{ disp_fix_map.get('vin') }}{% endif %} + {{ row.row_index + 1 }} + + {{ row.stare_eticheta }} - {{ res.get('nr_inmatriculare') or '' }} + + {{ row.prez.vehicul_nr }} + {% 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 %} - {{ res.get('data_prestatie') or '' }} + +
{{ row.prez.operatie }}
+ {% if row.prez.cod_rar and row.prez.cod_rar != '—' %} +
{{ row.prez.cod_rar }}
+ {% else %} +
nemapat
+ {% endif %} + + + {{ row.prez.data_prestatie }} {% if disp_fix_map.get('data_prestatie') %}{{ disp_fix_map.get('data_prestatie') }}{% endif %} - {{ res.get('odometru_final') or '' }} + + {{ row.prez.odometru }} {% if disp_fix_map.get('odometru_final') %}{{ disp_fix_map.get('odometru_final') }}{% endif %} - {{ op or '' | safe }} - {{ status }} - + {% if status == 'already_sent' and row.get('already_sent_info') %} {% set ai = row.already_sent_info %} deja trimis {{ (ai.get('created_at') or '')[:10] }} @@ -114,20 +136,11 @@ {% elif status == 'duplicate_in_file' and row.get('duplicate_with') %} dubla cu randul {% for idx in row.duplicate_with %}{{ idx + 1 }}{% if not loop.last %}, {% endif %}{% endfor %} - {% elif row.flags %} - {{ row.flags[0] }} - {% elif row.errors %} - {%- for e in row.errors -%} - {%- if e is mapping -%} - {{ e.get('message') or e.get('msg') or (e.values() | list | first) }} - {%- else -%} - {{ e }} - {%- endif -%} - {%- if not loop.last %}; {% endif -%} - {%- endfor -%} + {% else %} + {{ row.nota_umana or '' }} {% endif %} - + {% if status == 'needs_review' %} {% endif %} - + {% if status not in ('already_sent', 'duplicate_in_file') %}
diff --git a/app/web/templates/_status.html b/app/web/templates/_status.html index f0ed3bc..6a44735 100644 --- a/app/web/templates/_status.html +++ b/app/web/templates/_status.html @@ -1,7 +1,8 @@
+ hx-get="/_fragments/status?tab={{ tab_activ | default('acasa') }}" + hx-trigger="every 15s, trimiteriChanged from:body" + hx-swap="outerHTML" + {% if oob %}hx-swap-oob="outerHTML"{% endif %}> {% if not account_active %} @@ -49,4 +50,20 @@ {# 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). + #} + {% set _tab = tab_activ | default('acasa') %} + +
diff --git a/app/web/templates/_trimitere_detaliu.html b/app/web/templates/_trimitere_detaliu.html index b1218da..71224d3 100644 --- a/app/web/templates/_trimitere_detaliu.html +++ b/app/web/templates/_trimitere_detaliu.html @@ -69,7 +69,6 @@ {% endfor %} - {{ ui.autosend_toggle(checked=True) }}
diff --git a/app/web/templates/base.html b/app/web/templates/base.html index e5e223e..f2baf62 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -148,21 +148,20 @@ font-size:12px; font-weight:600; cursor:pointer; background:transparent; border:1.5px solid var(--line); color:var(--muted); min-height:30px; transition:background .15s, color .15s; } - .pill-cat:hover { filter:brightness(1.1); } + /* Hover: color-mix pe culoarea curenta a pill-ului (categoria sa), nu filter:brightness + (care producea rosu plin ilizibil pe pill-uri colorate). Activ suprima hover-ul. */ + .pill-cat:hover { background:color-mix(in srgb, currentColor 12%, transparent); } .pill-cat:focus-visible { outline:2px solid var(--accent); outline-offset:2px; } .pill-cat-n { font-size:11px; font-weight:700; color:var(--card); padding:0 5px; border-radius:99px; min-width:18px; text-align:center; } + /* Activ categorie: umplere cu culoarea categoriei (currentColor = var injectat inline) */ .pill-cat[aria-pressed="true"] { background:currentColor; color:var(--card); border-color:currentColor; } .pill-cat[aria-pressed="true"] .pill-cat-n { background:var(--card) !important; color:currentColor; } + /* Activ suprima hover: pastram culoarea activa, nu o mixam din nou */ + .pill-cat[aria-pressed="true"]:hover { background:currentColor; } + /* Reset "Toate" activ = --accent plin (nu culoarea categoriei) */ .pill-cat-reset[aria-pressed="true"] { background:var(--accent); color:#fff; border-color:var(--accent); } - /* Nudge "Date noi": apare doar cand pollerul usor detecteaza schimbari; tabelul nu se - schimba singur niciodata, utilizatorul reincarca cand vrea. */ - #nudge-trimiteri { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin:0 0 12px; - padding:8px 12px; border-radius:8px; font-size:13px; - border:1px solid var(--accent); - background:color-mix(in srgb, var(--accent) 12%, var(--card)); } - #nudge-trimiteri[hidden] { display:none; } - #nudge-trimiteri button { font-size:13px; padding:5px 12px; min-height:32px; } + .pill-cat-reset[aria-pressed="true"]:hover { background:var(--accent); } .s-queued{color:var(--accent);} .s-sending{color:var(--warn);} .s-sent{color:var(--ok);} .s-error,.s-needs_data,.s-needs_mapping{color:var(--err);} .s-ok{color:var(--ok);} @@ -267,6 +266,41 @@ border-radius:6px; cursor:pointer; min-height:36px; white-space:nowrap; } .kebab-menu button:hover, .kebab-menu a:hover { background:var(--line); } .kebab-menu button.danger { color:var(--err); } + /* === Accordion import compact (US-006 — regiune CSS disjuncta) === +
invelete stepper + upload pe Acasa. + Atribut `open` setat de server din `are_trimiteri`: + False (first-run) → open; True (returning) → colapsat. + Degradare fara JS: returning-user colapsat, first-run deschis — ambele corecte + fara toggle JS (un toggle JS pur ar lasa returning-user fara-JS cu ecranul deschis). + aria-expanded + focus: native
le gestioneaza automat. */ + #import-details { margin-bottom:16px; border:1px solid var(--accent); border-radius:8px; + background:var(--card); overflow:hidden; } + #import-details > summary { display:flex; align-items:center; gap:8px; cursor:pointer; + user-select:none; list-style:none; padding:10px 16px; + font-weight:600; font-size:14px; color:var(--ink); } + #import-details > summary::-webkit-details-marker { display:none; } + #import-details > summary::marker { display:none; } + #import-details > summary::before { content:"▶"; font-size:10px; color:var(--accent); + flex-shrink:0; transition:transform .15s; } + #import-details[open] > summary::before { transform:rotate(90deg); } + #import-details[open] > summary { border-bottom:1px solid var(--line); } + #import-details > summary:hover { background:color-mix(in srgb, var(--accent) 8%, var(--card)); } + #import-details > summary:focus-visible { outline:2px solid var(--accent); outline-offset:2px; } + /* Continutul (stepper + card upload): padding si bordul cardului interior sunt suprastate + de bordul exterior al #import-details — scoatem border duplicat de pe .card intern. */ + #import-details #import-section { padding:0; } + #import-details #import-section > .card { border-left:none; border-right:none; + border-bottom:none; border-radius:0; + margin-bottom:0; } + /* === Sfarsit regiune accordion import === */ + /* === Inceput regiune nav links status-bar (US-005) === + Linkuri Trimiteri + Mapari sub contoare; marcaj activ cu aria-current/status-nav-activ. */ + .status-nav-link { color:var(--accent); text-decoration:none; padding:2px 6px; border-radius:4px; + transition:background .1s; } + .status-nav-link:hover { background:color-mix(in srgb, var(--accent) 10%, transparent); } + .status-nav-link.status-nav-activ { color:var(--ink); font-weight:600; } + .status-nav-link.status-nav-activ:hover { background:color-mix(in srgb, var(--ink) 8%, transparent); } + /* === Sfarsit regiune nav links status-bar === */ /* Tabel cu cautare + paginare client-side (data-dt). Maparile pot creste la sute de randuri; filtram/paginez DOM-ul deja randat, fara cereri suplimentare. Vezi scriptul din base.html. */ input[type=search] { font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line); @@ -311,6 +345,25 @@ @media (max-width:1024px) { .tabel-trimiteri .col-actualizat { display:none; } } + /* === 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). + Suma latimi fixe: col-id(48) + col-stare(104) + col-data(104) + + col-km(76) + col-note(176) + col-verificat(80) + col-actiuni(92) = 680px. + Restul (~600px la 1280px) revine lui col-vehicul + col-operatie (fluid). === */ + .tabel-trimiteri .col-km { width:76px; } + .tabel-trimiteri .col-note { width:176px; } + .tabel-trimiteri .col-verificat{ width:80px; } + .tabel-trimiteri .col-actiuni { width:92px; } + /* 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). */ + .tabel-trimiteri tr.preview-edit { display:block; } + .tabel-trimiteri tr.preview-edit > td { display:block; width:100%; box-sizing:border-box; padding:0; border:none; } + /* Pe mobil (<768px): pseudo-eticheta goala (data-eticheta="") nu lasa spatiu gol. */ + @media (max-width:767px) { + .tabel-trimiteri td[data-eticheta=""]::before { display:none; } + } /* === Modal detaliu: fereastra modala globala, in afara zonei de poll (#submissions-wrap). Backdrop + dialog centrat pe desktop; focus-trap + scroll-lock + inert pe
sunt in JS. Varianta full-screen mobil: vezi blocul @@ -435,12 +488,16 @@
{# Celula stanga: logo ROMFAST #}
- {# Logo PNG real, RGBA transparent — ok pe toate temele fara filtre. #} - + {# Logo PNG real, RGBA transparent — ok pe toate temele fara filtre. + Invelit in pentru a naviga la Trimiteri (Acasa) de pe orice pagina. #} + + +
- {# Celula centru: titlu + badge env mic #} + {# Celula centru: titlu + badge env mic. + Titlul linkeaza la / (Trimiteri) ca si logo-ul. #}
-

Gateway RAR AUTOPASS

+

Gateway RAR AUTOPASS

{{ rar_env }}
{# Celula dreapta: comutator tema + versiune + meniu cont #} @@ -457,6 +514,8 @@ aria-haspopup="true" aria-expanded="false" aria-controls="cont-menu" aria-label="Meniu cont" title="Meniu cont">☰