# PRD 5.15 — Propagare design landing in aplicatie (dashboard compact + editare slim, VIN unic, prestatii multi-select) **Stare**: draft > Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`. > Sistemul de design al landing-ului: `app/web/templates/landing.html` (commit 41aa385), `DESIGN.md`. > Starea trece: `draft -> aprobat -> in-executie -> verify-pass -> inchis`. ## 1. Obiectiv Propagam sistemul de design al landing-ului comercial (carduri/liste/formulare compacte, slim, si cele 4 teme grafit/cobalt/cupru/hartie) in aplicatia reala. Concret: dashboard-ul Acasa primeste cardurile-contor + lista de trimiteri slim din mockup-ul hero, iar formularul de editare trimitere primeste designul compact din mockup-ul "prestatie noua", cu **un singur camp VIN**, **Observatii** ca text liber pentru operatiile de service si **prestatii ca chips multi-select** de coduri RAR. Userul a cerut explicit replicarea acestor doua mockup-uri pentru ca ii place cat de compacte/slim sunt. Decizii de produs confirmate cu userul (poarta de aprobare a acestui PRD): - **D1**: cardurile-contor INLOCUIESC bara de status actuala (`_status.html`); pastram doar indicatorii de sanatate worker/RAR intr-o forma compacta. - **D2**: temele sunt ADITIVE — pastram light/dark/petrol + Auto SI adaugam cele 4 din landing (grafit/cobalt/cupru/hartie). Selectorul ciclic le parcurge pe toate. (grafit ~ dark si hartie ~ light raman optiuni separate, la cererea userului.) - **D3**: prestatiile sunt chips reale multi-select — utilizatorul poate adauga mai multe coduri din nomenclatorul RAR si poate sterge oricare; se trimite lista `prestatii` completa (RAR accepta lista `{codPrestatie, idPrezentare:null}` — `docs/api-rar-contract.md` §payload). - **D4** (contor Trimise): cardul "Trimise" arata trei valori temporale — **all-time** (principal) + **luna asta** + **azi** (secundar). Necesita extinderea numaratorilor cu `sent_today`/`sent_month`. - **D5** (Observatii = operatii service): in API-ul RAR, campul `obs` e DE FAPT denumirea operatiilor din service. Deci `obs` = text liber cu operatiile efectuate; la import, daca fisierul nu are coloana Observatii, **concatenam denumirea operatiei de service in `obs`**. `obs` ramane in `payload_json` (camp din contractul RAR), fara coloana noua. Decizii din /plan-ceo-review (2026-06-28, mod SELECTIVE EXPANSION): - **D6** (sanatate mereu-vizibila): cardurile-contor inlocuiesc bara de status, DAR sanatatea (worker viu? RAR accesibil? ultima autentificare) ramane intr-un **strip mereu-vizibil, colorat, deasupra contoarelor** (verde "declaratiile curg" / rosu "blocat: worker oprit / RAR inaccesibil"). Invariant: zero-silent-failures — semnalul critic NU se ingroapa sub volum. (Rafineaza D1.) - **D7** (operatie -> obs, fara regresie de mapare): la import, denumirea operatiei RAMANE in `op_service` (sursa pentru maparea op->cod) SI se COPIAZA in `obs`. `obs` e sink aditional, nu mutare; fluxul needs_mapping ramane neatins. (Rafineaza D5.) - **D8** (idempotenta obs): `obs` e EXCLUS din cheia de idempotenta (`idempotency.py:98`). Deci editarea `obs` NU schimba cheia si NU poate crea duplicate — corecteaza AC-ul gresit din US-005. `prestatii` ESTE in cheie (sortat dupa cod) — multi-select re-cheieaza randul (US-006). - **D9** (secventiere): 5.15 INAINTE de 5.14 (mapare LLM). Editorul manual defineste forma listei `prestatii` si UX-ul de confirmare; 5.14 umple codurile peste aceeasi forma. - **D10** (extinderi acceptate, SELECTIVE EXPANSION): toate 4 intra in scope — (a) salvare mapare din chip (US-009), (b) bulk-fix din lista (US-010), (c) require dinamic odometruInitial la chip R-ODO/I-ODO (US-007), (d) editare keyboard-first in form slim (US-007). Fapte verificate care fundamenteaza scope-ul (nu presupuneri): - `vin` la RAR e **un singur camp** (17 car., MAJUSCULE, fara O/I/Q) — cerinta "fara 2 campuri VIN" e deja respectata azi (`_form_editare.html` are un singur `vin`); ramane sa NU regresam. - `prestatii` e deja **lista** in modelul intern (`mapping.resolve_prestatii(prestatii: list[dict])`) si in contractul RAR — multi-select nu cere model nou, ci editor nou. - `obs` exista deja ca alias de coloana la import (`import_router.py:71` — Observatii/Obs/Mentiuni/Note) si ca text liber optional in contractul RAR (`obs`); azi NU e editabil in formular. ## 2. Non-Goals (anti scope-creep) - Fara modificari pe backend-ul de trimitere: worker, masina de stari, idempotenta-logica (`build_key`), reconciliere, contract RAR. Recalcularea idempotentei la editare foloseste mecanismul EXISTENT (ca la 3.5/5.10), nu unul nou. - Fara migrare de schema decat daca strict necesar. `obs` si `prestatii` traiesc in `submissions.payload_json` (de confirmat la US-005) — fara coloane noi daca payload-ul le poarta. - Fara stergerea functionalitatii listei de trimiteri: filtre (data/vehicul/stare), paginare, bulk-delete pe randuri blocate, click->detaliu raman; se schimba DOAR aspectul randului (slim). - Fara schimbarea regulilor de mapare operatie->cod sau a validarii nomenclatorului RAR (`mapping.py`, `validation.py` raman ca atare; doar callsite-urile de editare le folosesc cu lista). - Fara redesign al landing-ului (deja livrat in 5.x); aici doar IMPORTAM stilul lui in app. ## 3. Stories atomice > Backend + UI pentru acelasi comportament = 2 stories. `base.html` e fisier FIERBINTE > (serializat intre valuri — un singur autor pe val). Toate UI verificate pe un esantion de teme (o tema luminoasa + una intunecata). ### US-001: Teme aditive (light/dark/petrol + grafit/cobalt/cupru/hartie) + tokeni `--card2`/`--line2` **Ca** operator de service **vreau** aceleasi teme ca pe landing **pentru ca** aplicatia sa para acelasi produs, coerent vizual. - **Depinde de**: — - **Fisiere**: `app/web/templates/base.html`, `DESIGN.md`, `tests/test_tema.py` (~3 fisiere) - **Test intai (RED)**: `tests/test_tema.py` — `test_cele_4_teme_definite`, `test_tokeni_card2_line2_in_toate_temele`, `test_anti_fouc_4_stari`, `test_migrare_localStorage_legacy` - **Acceptance criteria**: - [ ] Pastram temele EXISTENTE light/dark/petrol si ADAUGAM 4 teme noi grafit/cobalt/cupru/hartie, definite prin token-urile EXISTENTE (`--bg/--card/--ink/--muted/--line/--ok/--warn/--err/--accent`) + DOUA noi `--card2` (fundal input/contor) si `--line2` (separator subtire). `--card2`/`--line2` primesc valori si in light/dark/petrol (fallback rezonabil). Maparea landing->app pentru cele 4 noi: `--text->--ink`, `--sub->--muted`, `--okt->--ok`, `--errt->--err`, `--infot->--accent`. - [ ] Selectorul ciclic parcurge TOATE: light -> dark -> petrol -> grafit -> cobalt -> cupru -> hartie -> Auto, afiseaza eticheta temei curente, persistenta `localStorage` (D2). - [ ] "Auto" pastrat: urmeaza `prefers-color-scheme`, rezolva la dark/grafit sau light/hartie (decizie minora: Auto -> dark + hartie pentru light, sau dark/grafit — aliniaza cu I2). - [ ] Script anti-FOUC in `` seteaza `data-theme` sincron pre-paint pentru toate starile; valoare necunoscuta -> Auto, fara blink. Valorile vechi raman valide (nu se mapeaza fortat). - [ ] Contrast AA pentru text principal in toate temele (light + hartie sunt cele luminoase). - [ ] `DESIGN.md` actualizat: sectiunea cromatica + selector tema reflecta toate temele. - **Verificare E2E**: browser pe `/` (dashboard logat) — ciclare prin toate temele, persistenta la refresh, fara FOUC; toate temele selectabile. ### US-002: Componente de design slim in `base.html` (CSS, fara consumatori inca) **Ca** dezvoltator **vreau** clase reutilizabile pentru carduri-contor, lista slim, campuri slim si chips **pentru ca** dashboard-ul si formularul sa le consume DRY, identic cu mockup-ul. - **Depinde de**: US-001 (foloseste `--card2`/`--line2`) - **Fisiere**: `app/web/templates/base.html`, `DESIGN.md`, `tests/test_web_responsive.py` (~3 fisiere) - **Test intai (RED)**: `tests/test_web_responsive.py` — `test_clasa_contor_card`, `test_clasa_lista_slim`, `test_clasa_camp_slim`, `test_clasa_chips` - **Acceptance criteria**: - [ ] `.contor-card` (sau nume aliniat conventiei): cifra mare bold + eticheta mica muted, fundal `--card2`, bordura `--line`, radius 8px, padding 10-12px; variante de culoare a cifrei prin `.s-*` existente (verde/accent/rosu). - [ ] `.lista-trimiteri-slim` cu rand `.trimitere-slim`: stanga = VIN mono (linia 1) + operatie·ora muted (linia 2, 11px); dreapta = pill de stare; separator `--line2`; padding 10-14px. Randul ramane clickabil (rol button) si pastreaza tinta 44px pe mobil. - [ ] Varianta slim de camp formular: label 11px muted deasupra, input ~30px inaltime, fundal `--card2`, mono pentru VIN/odometru/nr; integrata in macro-ul `camp` din `_macros.html` printr-un flag (`slim=True`), fara a rupe randarea actuala (default neschimbat). - [ ] `.chips` + `.chip` (cu buton `×` de stergere) pentru prestatii multi-select; accesibil (buton real cu `aria-label`), stilat ca in mockup (accent 18%, font 10-11px). - [ ] Zero regresie vizuala pe componentele existente (`.card/.pill/.act/.tabel-trimiteri`). - **Verificare E2E**: pagina de proba/sandbox sau direct in US-003/004/007; vizual pe un esantion de teme + 390/1280. ### US-003: Dashboard Acasa — carduri-contor inlocuiesc bara de status **Ca** operator **vreau** cele 3 carduri-contor compacte (Trimise / In coada / De corectat) **pentru ca** sa vad starea dintr-o privire, ca in mockup. - **Depinde de**: US-002 - **Fisiere**: `app/web/templates/_status.html`, `app/web/templates/_acasa.html`, `app/web/routes.py` (`_status_counts` extins cu `sent_today`/`sent_month`), `tests/test_web_status.py`, `tests/test_web_dashboard.py` (~5 fisiere) - **Test intai (RED)**: `tests/test_web_status.py` — `test_strip_sanatate_mereu_vizibil`, `test_strip_rosu_worker_oprit`, `test_trei_contoare_card`, `test_trimise_all_time_luna_azi`, `test_fara_bara_veche` - **Acceptance criteria**: - [ ] **Strip de sanatate mereu-vizibil, DEASUPRA contoarelor** (D6): o linie compacta colorata — verde "declaratiile curg" cand worker viu + RAR ok; **rosu** + text explicit cand worker oprit SAU RAR inaccesibil ("Blocat: worker oprit" / "Blocat: RAR inaccesibil"), cu ultima autentificare RAR. Glife accesibile ✓/✗ (nu doar culoare). Invariant zero-silent-failures: semnalul "declaratiile NU pleaca" e imposibil de ratat, NU ingropat sub volum. - [ ] Sub strip: card "Trimiteri RAR AUTOPASS" cu 3 contoare slim: **In coada** (queued, accent), **Trimise** (sent, verde), **De corectat** (blocate = needs_data + needs_mapping + error, rosu). - [ ] Cardul **Trimise** afiseaza trei valori temporale (D4): all-time (cifra principala) + "luna asta" + "azi" (sub-linie secundara). `_status_counts` extins cu `sent_today`/`sent_month`. **Sursa de timp**: NU exista coloana `sent_at`; folosim `status='sent' AND date(updated_at)=...`. Justificare (verificat): un rand `sent` nu mai primeste scrieri ulterioare pana la purge-delete la +90z (`purge_after` se seteaza in ACEEASI scriere care marcheaza `sent`), deci `updated_at` == momentul trimiterii pentru randurile `sent` -> fara migrare de coloana (respecta Non-Goal). Daca pe viitor apar scrieri post-`sent`, reevalueaza o coloana `sent_at` dedicata. - [ ] Navigarea existenta (Trimiteri/Mapari + badge needs_mapping) se pastreaza. Click pe contorul **De corectat** deep-link-eaza in lista filtrata pe blocate (`?status=` existent din 5.x), nu intr-o pagina noua. - [ ] Scoped pe cont; poll-ul existent (`/_fragments/status`) randeaza noul antet fara a pierde tab-ul. - [ ] Responsive: cele 3 contoare pe un rand pe desktop, stivuite/2-pe-rand pe mobil, fara overflow. - **Verificare E2E**: browser pe `/` — contoare corecte vs date din DB, sanatate worker mort/viu, poll pastreaza starea. ### US-004: Lista de trimiteri — rand slim (VIN + operatie·ora + pill) **Ca** operator **vreau** lista de trimiteri in stil slim ca in mockup **pentru ca** e mai compacta si mai usor de scanat, pastrand filtrele si actiunile. - **Depinde de**: US-002 - **Fisiere**: `app/web/templates/_submissions.html`, `app/web/templates/_coada.html` (filtre raman), `tests/test_web_submissions.py`, `tests/test_web_responsive.py` (~4 fisiere) - **Test intai (RED)**: `tests/test_web_submissions.py` — `test_rand_slim_vin_operatie_pill`, `test_filtre_paginare_pastrate`, `test_bulk_doar_blocate`, `test_click_deschide_detaliu` - **Acceptance criteria**: - [ ] Fiecare rand: stanga VIN mono scurt (`vin_scurt`) linia 1 + operatie + ora/data muted linia 2; dreapta pill de stare (`stare_css`/`stare_scurt`). Nr. inmatriculare, data completa si nr. prezentare RAR raman accesibile (linie meta discreta si/sau in modalul de detaliu). - [ ] Filtre (data/vehicul/stare — `_coada.html`), paginarea numerotata si bulk-delete pe randuri blocate (checkbox doar pe `gestionabil`) raman FUNCTIONALE. - [ ] Click pe rand deschide `/_fragments/trimitere/{id}` in modal (neschimbat). - [ ] Slim layout consistent desktop si <=1024px (cardurile responsive existente nu regreseaza). - [ ] Pill-urile de stare folosesc maparea din `labels.py` (zero etichete noi). - **Verificare E2E**: browser — filtrare + paginare + click detaliu + bulk pe blocate, pe 4 teme, pe 390/820/1280. ### US-005: Backend — `obs` (Observatii) editabil si persistat **Ca** operator **vreau** sa editez Observatiile (operatiile de service in text liber) **pentru ca** sa corectez/completez ce s-a facut, separat de codurile RAR. - **Depinde de**: — - **Fisiere**: `app/web/routes.py` (`/trimitere/{id}/corecteaza`), `app/api/v1/import_router.py` (`/_import/{id}/rand/{row}/editeaza`, `EDIT_FIELDS`), `app/validation.py` (obs optional), `app/payload_view.py` (echo obs), `tests/test_web_corectie*.py`, `tests/test_import_review.py` (~6 fisiere) - **Test intai (RED)**: `tests/test_web_corectie_obs.py` — `test_obs_editabil_persistat_corecteaza`, `test_obs_persistat_preview_editeaza`, `test_obs_optional_gol_ok`, `test_import_concateneaza_operatie_in_obs` - **Acceptance criteria**: - [ ] `obs` traieste in `payload_json` (camp `obs` din contractul RAR); fara coloana noua / migrare (D5). - [ ] `obs` adaugat in `EDIT_FIELDS`; `corecteaza` si `editeaza` (preview) accepta si persista `obs`. - [ ] `obs` optional (text liber, fara validare de continut, doar trim); apare in `payload_view`. - [ ] `obs` se include in payload-ul trimis la RAR (camp `obs`). **`obs` e EXCLUS din cheia de idempotenta** (`idempotency.py:98`) — deci editarea DOAR a `obs` NU schimba cheia si NU poate crea duplicat (D8). NU recalcula/forta cheia pe baza `obs`. (Corecteaza formularea anterioara.) - [ ] **La import** (D7): denumirea operatiei RAMANE in `op_service` (sursa pentru maparea op->cod); daca fisierul NU are coloana Observatii, denumirea operatiei se **COPIAZA** (nu se muta) si in `obs`; daca are coloana Observatii, se pastreaza textul ei. Format de concatenare definit (denumiri separate prin "; "). Fluxul needs_mapping ramane neatins. - **Verificare E2E**: `POST /trimitere/{id}/corecteaza` cu `obs` -> persistat -> vizibil in detaliu; optional proba live RAR ca `obs` apare in FINALIZATA. ### US-006: Backend — prestatii multi-cod (lista) la editare/corectie **Ca** operator **vreau** sa adaug/sterg mai multe coduri RAR pe o trimitere **pentru ca** o comanda poate avea mai multe prestatii, asa cum accepta RAR. - **Depinde de**: — - **Fisiere**: `app/web/routes.py` (`/corecteaza`, `/repune` — **rescrie logica single-`prestatii[0]`** de azi: `cod_prestatie_curent` la `routes.py:977-982` + injectia la `1146-1164`/`1288-1324` presupun UN cod; multi-select cere pre-fill din lista intreaga + scriere pe toti itemii), `app/api/v1/import_router.py` (`/editeaza`, idem), `app/mapping.py` (NEATINS — deja accepta lista), `app/validation.py` (fiecare cod in nomenclator), `tests/test_web_corectie*.py`, `tests/test_mapping*.py` (~6 fisiere). Nota: `mapping.py` e neatins, dar call-site-urile din handler-e cer un rewrite real (nu "fara schimbare de logica"). - **Test intai (RED)**: `tests/test_web_corectie_prestatii.py` — `test_mai_multe_coduri_acceptate`, `test_cod_invalid_respins`, `test_lista_goala_needs_mapping`, `test_idempotency_recalculat`, `test_odometru_initial_conditionat_R_ODO` - **Acceptance criteria**: - [ ] Handler-ele de editare accepta o LISTA de `cod_prestatie`, inlocuind selectul unic. **NU reconstrui lista cu itemi goi**: handler-ele de azi injecteaza codul DOAR in `prestatii[0]` (`routes.py:1146-1164`, `1288-1324`) — multi-select le rescrie ca: pastreaza itemii existenti cu `cod_op_service`/`denumire` (invariant D7) si seteaza/adauga `cod_prestatie` pe ei. `idPrezentare:null` se adauga in `payload.py` la construirea payload-ului, NU in itemul intern. - [ ] **Pereche operatie<->cod definita**: cand exista operatii (`cod_op_service`), fiecare cod-chip se ataseaza unei operatii (1 operatie -> 1 cod, ca azi, dar acum N operatii -> N coduri); cand NU exista operatie (cod direct, ex. corectie pura), chip-urile sunt coduri libere intr-o lista fara `op_service`. Aceasta pereche e ce consuma US-009 (salvare mapare op->cod). - [ ] Fiecare cod e validat fata de nomenclator (`valid_codes`); cod necunoscut -> respins cu mesaj (NU se trimite raw — invariant ORA-12899 din CLAUDE.md/contract). - [ ] Lista goala de coduri -> ramane `needs_mapping` (nu se trimite fara cod). - [ ] **Coduri duplicate** (acelasi cod adaugat de 2x) -> deduplicate inainte de persistare (cheia sorteaza deja dupa identitate, dar lista persistata nu trebuie sa contina duplicate). - [ ] Recalcul idempotenta dupa editare (mecanism existent), cu prinderea coliziunii ca azi. - [ ] Se pastreaza regula `odometruInitial` obligatoriu cand lista contine `R-ODO`/`I-ODO` (contract §payload) — validare existenta, doar verificata pe lista. - **Verificare E2E**: `POST /corecteaza` cu 2 coduri valide -> `queued` cu `prestatii` de lungime 2; cu un cod invalid -> respins; optional live RAR cu 2 prestatii -> FINALIZATA. ### US-007: UI — formular editare slim (VIN unic, Observatii, chips prestatii) **Ca** operator **vreau** formularul de editare in design slim cu chips de prestatii **pentru ca** e compact si imi arata clar codurile RAR si observatiile, ca in mockup. - **Depinde de**: US-002, US-005, US-006 - **Fisiere**: `app/web/templates/_form_editare.html`, `app/web/templates/_macros.html`, `app/web/templates/_trimitere_detaliu.html`, `app/web/templates/_editare_preview_modal.html`, `tests/test_web_preview_edit.py`, `tests/test_web_detaliu*.py` (~6 fisiere) - **Test intai (RED)**: `tests/test_web_form_editare_slim.py` — `test_un_singur_vin`, `test_camp_observatii_prezent`, `test_chips_multi_select_prestatii`, `test_adauga_sterge_chip`, `test_form_slim_in_ambele_modale` - **Acceptance criteria**: - [ ] Formularul foloseste varianta slim de camp (US-002): VIN, Data prestatiei, Nr. inmatriculare, Observatii (textarea), prestatii (chips), Odometru — un SINGUR camp VIN (fara "Confirma VIN"). - [ ] Observatii = textarea liber, legat de `obs` (US-005). - [ ] Prestatii = chips multi-select: fiecare cod ca chip cu `×`; un picker (dropdown din nomenclator) adauga un cod nou; lista se trimite ca `cod_prestatie` multiplu (US-006). - [ ] Acelasi `_form_editare.html` slujeste ambele modale (detaliu `/corecteaza` si preview `/editeaza`), fara duplicare; degradare fara JS rezonabila (chips ca lista, picker = select). - [ ] **Require dinamic odometruInitial** (D10c): cand lista de chips contine `R-ODO` sau `I-ODO`, formularul DEZVALUIE si cere `odometru_initial` (contract §payload), previne 400 RAR si un drum `needs_data`. Cand niciun chip R-ODO/I-ODO -> campul ramane optional/ascuns. - [ ] **Editare keyboard-first** (D10d): in picker, Enter adauga chip-ul selectat; sageti navigheaza optiunile; Esc inchide modalul; focus-ul revine logic dupa adaugare/stergere. - [ ] Stilizare fidela mockup-ului pe toate temele; tinte 44px pe mobil; a11y (label-uri, aria, anunt de chip adaugat/sters pentru screen-reader). - [ ] **Suprafata JS reala** (nota de efort): chips add/remove client-side + picker navigabil cu tastatura + management focus + reveal conditional odometruInitial = JS ne-trivial intr-un app HTMX/minimal-JS. Fallback fara JS: picker = `