diff --git a/app/web/templates/_trimitere_detaliu.html b/app/web/templates/_trimitere_detaliu.html index 5afd3f8..aafbc81 100644 --- a/app/web/templates/_trimitere_detaliu.html +++ b/app/web/templates/_trimitere_detaliu.html @@ -1,72 +1,36 @@ {% from "_eroare.html" import card_erori %} {% import '_macros.html' as ui %} -{# PRD 5.9 US-003: fragmentul se swap-uieste in corpul modalului global - (#detaliu-modal-body). Heading-ul poarta id-ul folosit de aria-labelledby al dialogului. #} +{# PRD 5.9 US-004: detaliu editabil in-place, butoane consolidate, ordine verticala R10. + Fragmentul se swap-uieste in corpul modalului global (#detaliu-modal-body). Heading-ul + poarta id-ul folosit de aria-labelledby al dialogului. + R9: operatie + cod RAR rezolvat apar IMPREUNA, read-only, folosind `prez.cod_rar` + (fallback „nemapat"), fara eticheta separata „Cod RAR". #} +{% set cod_afis = prez.cod_rar if (prez.cod_rar and prez.cod_rar != '—') else 'nemapat' %}
-
+ + {# === R10 (1): header — #id + pill + motiv uman === #} +

Detaliu trimitere #{{ id }}

{{ stare_text }}
- - {% if stare_subtext %} + {% if motiv %} +

{{ motiv }}

+ {% elif stare_subtext %}

{{ stare_subtext }}

{% endif %} -
-
Numar inmatriculare
{{ prez.vehicul_nr }}
-
VIN (serie sasiu)
{{ prez.vin }}
-
Operatie
{{ prez.operatie }}
-
Cod RAR
{{ prez.cod }}
-
Data prestatie
{{ prez.data_prestatie }}
-
Odometru final
{{ prez.odometru }}
-
Nr. prezentare RAR
{{ id_prezentare or '—' }}
-
Cod HTTP RAR
{{ rar_status_code or '—' }}
-
Reincercari
{{ retry_count }}
-
Creat
{{ created_at }}
-
Actualizat
{{ updated_at }}
-
Urmatoarea incercare
{{ next_attempt_at }}
-
- + {# === R10 (2): bloc eroare blocanta cand exista === #} {% if erori_3n %} -
+
{{ card_erori(erori_3n) }}
- {% elif motiv %} -
-
Motiv
-
{{ motiv }}
-
{% endif %} - {% if rar_error %} -
- Mesaj tehnic RAR (integral) -
{{ rar_error }}
-
- {% endif %} - - {# === Lifecycle (US-011): sterge / re-pune in coada — doar randuri blocate === #} - {% if gestionabil %} -
-
- - -
-
- - -
-
- {% endif %} - - {# === Mapare inline (PRD 5.7): alege cod RAR pentru operatiile nemapate ale acestui rand === #} + {# === R10 (3) + R9: mapare inline (PRD 5.7) — alege cod RAR pentru operatiile nemapate. + Cand nemapate_inline, linia „Operatie: X · nemapat" apare in formularul de mai jos + (cod_afis = nemapat), iar aici e picker-ul; dupa mapare, re-render arata codul rezolvat. === #} {% if nemapate_inline %} -
+

Mapeaza codul operatiei

Alege codul RAR pentru fiecare operatie. La salvare, randul se re-rezolva pe loc @@ -76,6 +40,7 @@ {% set top = op.suggestions[0] if op.suggestions else None %} {% set preselect = top.cod_prestatie if (top and top.score >= 60) else '' %}

@@ -111,48 +76,126 @@
{% endif %} - {# === Corectie inline (US-010): doar randuri ne-trimise blocate === #} + {# === R10 (4): formular editabil (needs_data/needs_mapping) SAU context read-only. + Zero dublare: campurile vehiculului apar O SINGURA DATA — editabile cand randul e + corectabil, altfel read-only. Operatie + cod RAR read-only deasupra campurilor. === #} {% if editabil %} {% set err_map = {} %} {% for e in corectie_errors %}{% if e.field %}{% set _ = err_map.update({e.field: e.message}) %}{% endif %}{% endfor %} -
-

Corecteaza si re-trimite

- {% if corectie_msg %} -
{{ corectie_msg }}
+ {% if corectie_msg %} +
{{ corectie_msg }}
+ {% endif %} + + {% macro camp(nume, eticheta, valoare, tip='text') %} +
+ + + {% if err_map.get(nume) %} +
{{ err_map.get(nume) }}
{% endif %} +
+ {% endmacro %} - - -
+ + - {% macro camp(nume, eticheta, valoare, tip='text') %} -
- - - {% if err_map.get(nume) %} -
{{ err_map.get(nume) }}
- {% endif %} -
- {% endmacro %} + {# Operatie + cod RAR read-only deasupra campurilor (R9, fara eticheta „Cod RAR"). #} +
+
Operatie
+
{{ prez.operatie }} · {{ cod_afis }}
+
- {{ camp('nr_inmatriculare', 'Numar inmatriculare', form_nr) }} - {{ camp('vin', 'VIN (serie sasiu)', form_vin) }} - {{ camp('data_prestatie', 'Data prestatie (YYYY-MM-DD)', form_data) }} - {{ camp('odometru_final', 'Odometru final', form_odo_final) }} - {{ camp('odometru_initial', 'Odometru initial (daca e cerut)', form_odo_initial) }} -
-
- -
- + {# Nr. inmatriculare pe rand propriu, VIN dedesubt — ambele latime plina. #} + {{ camp('nr_inmatriculare', 'Numar inmatriculare', form_nr) }} + {{ camp('vin', 'VIN (serie sasiu)', form_vin) }} + + {# Restul campurilor in grila. #} +
+ {{ camp('data_prestatie', 'Data prestatie (YYYY-MM-DD)', form_data) }} + {{ camp('odometru_final', 'Odometru final', form_odo_final) }} + {{ camp('odometru_initial', 'Odometru initial (daca e cerut)', form_odo_initial) }} +
+ + {# === R10 (5): actiune primara conditionata de stare (R2). needs_data/needs_mapping + -> „Salveaza si retrimite" pe /corecteaza. UN SINGUR buton primar per stare. === #} +
+ +
+ + {% else %} + {# Context read-only pentru randuri ne-editabile (sent/sending/queued/error). #} +
+
+
Numar inmatriculare
{{ prez.vehicul_nr }}
+
+
+
VIN (serie sasiu)
+
{{ prez.vin }}
+
+
Operatie
{{ prez.operatie }} · {{ cod_afis }}
+
Data prestatie
{{ prez.data_prestatie }}
+
Odometru final
{{ prez.odometru }}
{% endif %} + + {# === R10 (5): actiuni de jos — primar Re-pune (doar error) + Sterge pe RAND SEPARAT (R2/R11) === #} + {% if status == 'error' or gestionabil %} +
+ {# R2: error -> buton primar „Re-pune in coada" pe /repune (error nu e editabil). #} + {% if status == 'error' %} +
+ + +
+ {% endif %} + + {# R11: UN SINGUR Sterge, outline distructiv (var(--err)), pe rand separat, full-width pe mobil. #} + {% if gestionabil %} +
+ + +
+ {% endif %} +
+ {% endif %} + + {# === R10 (6): Detalii tehnice — colapsat implicit === #} +
+ Detalii tehnice +
+
Nr. prezentare RAR
{{ id_prezentare or '—' }}
+
Cod HTTP RAR
{{ rar_status_code or '—' }}
+
Reincercari
{{ retry_count }}
+
Creat
{{ created_at }}
+
Actualizat
{{ updated_at }}
+
Urmatoarea incercare
{{ next_attempt_at }}
+ {% if erori_3n and erori_3n[0].cod %} +
Cod eroare (brut)
{{ erori_3n[0].cod }}
+ {% endif %} +
+ {% if rar_error %} +
+
Mesaj RAR (integral)
+
{{ rar_error }}
+
+ {% endif %} +
-{# PRD 5.9 US-003 (R4): focus-ul dupa swap (incl. re-render corectie/mapare) e mutat in - corpul modalului din base.html (htmx:afterSettle pe #detaliu-modal-body). Vechiul script - inline (marcheazaDetaliuDeschis / scrollIntoView pe randul-sibling) a fost eliminat. #} +{# PRD 5.9 US-004 (R4): scriptul inline vechi (marcheazaDetaliuDeschis / scrollIntoView pe + randul-sibling) a fost eliminat de US-003. Focus-ul post-swap (incl. re-render corectie/ + mapare) e gestionat de htmx:afterSettle pe #detaliu-modal-body din base.html. R5: inchiderea + modalului pe succes (queued/sterge) vine din HX-Trigger `inchideModal` emis de rute. #} diff --git a/app/web/templates/base.html b/app/web/templates/base.html index 07fa994..5459bae 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -245,6 +245,9 @@ padding:16px; padding-top:56px; overflow-y:auto; } .modal-close { width:44px; height:44px; top:8px; right:8px; font-size:24px; } + /* 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; } diff --git a/scripts/ralph/prd.json b/scripts/ralph/prd.json index 8ea6d02..6167854 100644 --- a/scripts/ralph/prd.json +++ b/scripts/ralph/prd.json @@ -159,12 +159,12 @@ ], "requiresBrowserCheck": true, "requiresDesignReview": true, - "passes": false, + "passes": true, "failed": false, "blocked": false, "retries": 0, "failureReason": "", - "notes": "" + "notes": "Implementat in _trimitere_detaliu.html (ordine R10): header+motiv, eroare blocanta, mapare inline, formular editabil cu campuri o singura data (nr rand propriu, VIN dedesubt) + operatie/cod read-only (prez.cod_rar, fallback nemapat), actiuni jos,
Detalii tehnice. R2/F7: buton primar conditionat de stare — error->Re-pune(/repune), needs_data/needs_mapping->Salveaza si retrimite(/corecteaza). R11: Sterge outline var(--err) pe rand separat, hx-confirm specific, full-width pe mobil (regula .detaliu-actiuni-jos in @media 767px). R5: hx-disabled-elt pe toate formele; inchidere pe succes prin HX-Trigger inchideModal. _detaliu_ctx si rutele NESCHIMBATE. 6 teste noi in test_web_corectie.py; suita 835 passed (-m 'not live'). Ramane VERIFY browser/design (requiresBrowserCheck/requiresDesignReview)." }, { "id": "US-007", diff --git a/scripts/ralph/progress.txt b/scripts/ralph/progress.txt index f662be9..4b6c184 100644 --- a/scripts/ralph/progress.txt +++ b/scripts/ralph/progress.txt @@ -96,3 +96,49 @@ Note: PRD APROBAT 2026-06-24 cu revizii obligatorii R1-R12 (raport AUTOPLAN). R1 - VERIFY: gstack browser la 375px pe `/` — fara scroll orizontal de pagina; ☰ deschide meniul cont; modal deschis ocupa tot ecranul si se inchide din `x` (requiresBrowserCheck + requiresDesignReview). --- + +## US-004: Detaliu editabil in-place + butoane consolidate (in modal), ordine verticala R10 + +### Ce s-a livrat: +- app/web/templates/_trimitere_detaliu.html REscris pe ordinea verticala R10: + (1) header #id + pill + motiv uman; (2) bloc eroare blocanta (card_erori) cand exista; + (3) mapare inline (5.7) cand nemapate_inline, tintind #detaliu-modal-body; (4) formular + editabil SAU context read-only; (5) actiuni jos; (6)
„Detalii tehnice" colapsat. +- Zero dublare: blocul read-only de grila (vechiul :19-32) si formularul de corectie s-au + contopit. Campurile editabile (nr_inmatriculare, vin, data_prestatie, odometru_final, + odometru_initial) apar O SINGURA DATA, ca inputuri pre-completate. Nr. pe rand propriu, + VIN dedesubt (ambele latime plina), restul in grila. +- R9: operatie + cod RAR rezolvat read-only deasupra campurilor, folosind prez.cod_rar + (fallback „nemapat"), fara eticheta separata „Cod RAR" (vechiul :23 eliminat). +- R2 (fix F7): buton primar CONDITIONAT DE STARE, unul singur per stare: + * error -> „Re-pune in coada" pe /repune (error NU e editabil, /corecteaza ar da 403); + * needs_data/needs_mapping -> „Salveaza si retrimite" pe /corecteaza (submit-ul formularului). + Butonul gol „Re-pune in coada" duplicat (vechiul :55-59) eliminat. +- R11: UN SINGUR „Sterge" (outline distructiv var(--err)) pe RAND SEPARAT (clasa + .detaliu-actiuni-jos, NU margin-left:auto), hx-confirm specific + „Stergi definitiv trimiterea #{id}? Nu se poate anula."; full-width stivuit pe mobil + prin regula noua `.detaliu-actiuni-jos button { width:100% }` in @media 767px (base.html). +- R5: hx-disabled-elt="find button" pe toate cele 3 forme (corectie/repune/sterge + mapare); + inchiderea modalului pe succes vine din HX-Trigger inchideModal emis de rute (neschimbat). +- R4: scriptul inline modal-appropriate (curatat deja de US-003) pastrat; comentariu actualizat. +- Detalii tehnice colapsate: Nr. prezentare RAR, Cod HTTP, Reincercari, timestamps, mesaj RAR + brut + cod brut (erori_3n[0].cod, doar cand exista). +- tests/test_web_corectie.py: 6 teste noi US-004 (test_camp_apare_o_singura_data, + test_nr_si_vin_pe_randuri_separate, test_un_singur_buton_primar_per_stare, + test_error_foloseste_repune, test_sterge_prezent_si_distinct, test_corectie_pastreaza_comportament). + +### Learnings: +- F7 (CRITICAL): consolidarea naiva pe /corecteaza ar fi scos retry-ul randurilor `error` + (_CORECTABILE=needs_data/needs_mapping; post_corectie da 403 pe error). Rezolvat prin buton + primar conditionat de stare: error pastreaza fluxul existent /repune (re-pune in coada), + needs_data/needs_mapping merg pe /corecteaza. Logica backend a rutelor NESCHIMBATA. +- _detaliu_ctx neatins (avea deja toate flag-urile necesare: editabil, gestionabil, status, + prez.cod_rar, erori_3n). Zero logica noua in routes.py. +- test_web_lifecycle.py::test_buton_sterge_doar_pe_blocate ramane verde (error arata + „Re-pune in coada", sent nu) — fluxul error->repune e pastrat. + +### Next: +- VERIFY (requiresBrowserCheck + requiresDesignReview): gstack browser — needs_data arata + fiecare camp o data (nr. rand propriu, VIN dedesubt), corectez data, „Salveaza si retrimite" + -> queued; error arata „Re-pune in coada"; „Sterge" clar separat, rosu, full-width pe mobil. +--- diff --git a/tests/test_web_corectie.py b/tests/test_web_corectie.py index 1ac96ec..f7ad34a 100644 --- a/tests/test_web_corectie.py +++ b/tests/test_web_corectie.py @@ -203,3 +203,100 @@ def test_corectie_cont_strain(client): }) assert resp.status_code == 404 assert _row(sid1)["status"] == "needs_data" # neatins + + +# =========================================================================== # +# US-004 (PRD 5.9): detaliu editabil in-place, zero dublare, butoane consolidate # +# =========================================================================== # + +def _fragment(client, sid: int) -> str: + resp = client.get(f"/_fragments/trimitere/{sid}") + assert resp.status_code == 200 + return resp.text + + +def test_camp_apare_o_singura_data(client): + """Zero dublare: fiecare camp editabil apare exact O DATA (input editabil pre-completat), + fara blocul read-only de grila duplicat deasupra formularului.""" + acct = _create_account_user("u1@test.com") + sid = _insert(acct, status="needs_data", payload=_payload("WVWZZZ1JZXW0U1001", odo="")) + _login(client, "u1@test.com") + html = _fragment(client, sid) + + # Fiecare camp editabil apare exact o data, ca input cu name="...". + for camp in ("nr_inmatriculare", "vin", "data_prestatie", "odometru_final", "odometru_initial"): + assert html.count(f'name="{camp}"') == 1, f"{camp} trebuie sa apara o singura data" + # Nu mai exista eticheta separata „Cod RAR". + assert "Cod RAR" not in html + + +def test_nr_si_vin_pe_randuri_separate(client): + """Nr. inmatriculare pe rand propriu, VIN dedesubt — ambele inputuri latime plina, + nr. inaintea VIN-ului in markup.""" + acct = _create_account_user("u2@test.com") + sid = _insert(acct, status="needs_data", payload=_payload("WVWZZZ1JZXW0U2001", odo="")) + _login(client, "u2@test.com") + html = _fragment(client, sid) + + poz_nr = html.find('name="nr_inmatriculare"') + poz_vin = html.find('name="vin"') + assert poz_nr != -1 and poz_vin != -1 + assert poz_nr < poz_vin # nr. apare inaintea VIN-ului (rand propriu, VIN dedesubt) + + +def test_un_singur_buton_primar_per_stare(client): + """R2: needs_data are UN SINGUR buton primar „Salveaza si retrimite" -> /corecteaza.""" + acct = _create_account_user("u3@test.com") + sid = _insert(acct, status="needs_data", payload=_payload("WVWZZZ1JZXW0U3001", odo="")) + _login(client, "u3@test.com") + html = _fragment(client, sid) + + assert "Salveaza si retrimite" in html + assert html.count("Salveaza si retrimite") == 1 + assert f"/trimitere/{sid}/corecteaza" in html + # needs_data NU ofera butonul de re-pune separat (acela e doar pentru error). + assert "Re-pune in coada" not in html + + +def test_error_foloseste_repune(client): + """R2 (fix F7): un rand `error` NU are formular de corectie; primarul „Re-pune in coada" + posteaza pe /repune (NU /corecteaza, care ar da 403).""" + acct = _create_account_user("u4@test.com") + sid = _insert(acct, status="error", payload=_payload("WVWZZZ1JZXW0U4001")) + _login(client, "u4@test.com") + html = _fragment(client, sid) + + assert "Re-pune in coada" in html + assert f"/trimitere/{sid}/repune" in html + # error nu e editabil -> fara post pe /corecteaza si fara butonul de salvare. + assert f"/trimitere/{sid}/corecteaza" not in html + assert "Salveaza si retrimite" not in html + + +def test_sterge_prezent_si_distinct(client): + """R11: UN SINGUR Sterge, outline distructiv (var(--err)), pe rand separat, cu hx-confirm + specific; posteaza pe /sterge.""" + acct = _create_account_user("u5@test.com") + sid = _insert(acct, status="needs_data", payload=_payload("WVWZZZ1JZXW0U5001", odo="")) + _login(client, "u5@test.com") + html = _fragment(client, sid) + + assert f"/trimitere/{sid}/sterge" in html + assert html.count(f"/trimitere/{sid}/sterge") == 1 + assert "var(--err)" in html # outline distructiv rosu + assert f"Stergi definitiv trimiterea #{sid}? Nu se poate anula." in html + + +def test_corectie_pastreaza_comportament(client): + """Regresie: retry pur (post pe /corecteaza fara modificari) pe needs_data valid ramane + idempotent — randul ajunge queued, ca azi (comportament ruta neschimbat).""" + acct = _create_account_user("u6@test.com") + # needs_data complet valid (toate campurile prezente) -> retry pur il trece in queued. + sid = _insert(acct, status="needs_data", payload=_payload("WVWZZZ1JZXW0U6001", odo="55000")) + _login(client, "u6@test.com") + csrf = _csrf(client) + + resp = client.post(f"/trimitere/{sid}/corecteaza", data={"csrf_token": csrf}) + assert resp.status_code == 200 + assert _row(sid)["status"] == "queued" + assert resp.headers.get("HX-Trigger-After-Settle") == "trimiteriChanged, inchideModal"