diff --git a/docs/prd/prd-3.6-editare-preview-acasa-unificata.md b/docs/prd/prd-3.6-editare-preview-acasa-unificata.md new file mode 100644 index 0000000..ce8d5cf --- /dev/null +++ b/docs/prd/prd-3.6-editare-preview-acasa-unificata.md @@ -0,0 +1,543 @@ + +# PRD 3.6 — Editare celule in preview + Acasa unificata (Trimiteri inline, upload slim, Mapari tabelar) + +**Stare**: aprobat (post-autoplan 2026-06-19 — raport de review la finalul fisierului; stories revizuite cu fix-urile aplicate) + +> Proces complet: `docs/ROADMAP.md` §5. Contract RAR (sursa de adevar): `docs/api-rar-contract.md`. +> Starea trece: `draft → aprobat → in-executie → verify-pass → inchis` (actualizata de lead). +> Continua 3.5 ([prd-3.5](prd-3.5-dashboard-compact-trimiteri-mapari.md)). **Backend trimitere (worker, +> masina stari, idempotenta-logica, mapping-rezolvare) NU se atinge** — doar canalul de import si stratul web. +> Exceptie schema decisa la poarta autoplan: UNA coloana nullable `import_rows.override_json` (Approach B), cu +> migrare defensiva `_migrate` ca la 3.3b/3.5. Worker/idempotenta/mapare raman neatinse. + +## 1. Obiectiv + +Dupa upload, utilizatorul putea citi randurile cu probleme dar nu le putea **corecta inainte de trimitere** — +singura editare de continut era post-confirmare, in alt tab. Livram: (a) editare de celule direct in preview +(buton "Editeaza" pe rand), si (b) reunificarea fluxului pe pagina **Acasa** — Trimiterile devin o sectiune +permanenta sub upload (tab-ul "Trimiteri" dispare), zona de upload se comprima la o bara slim, iar "Mapari" +trece pe format tabelar compact cu eticheta auto-send reformulata ca un comutator Automat/Manual. + +Motivatie: microcopia spune "vezi mai jos trimiterile", dar trimiterile erau intr-un alt tab; cazul real de +utilizare (fisier cu o data/VIN lipsa sau gresit) cerea re-upload in loc de o corectie pe loc. + +## 2. Non-Goals (anti scope-creep) + +- **Fara editare a operatiei/codului RAR in celula** — codul de operatie ramane rezolvat prin panoul de mapare + existent (`needs_mapping`). Editarea de celule acopera campurile de continut: VIN, nr. inmatriculare, + data prestatie, odometru initial/final. (Operatia se schimba din panoul de mapare, nu din tabel.) +- **Fara editare in bloc / multi-rand** — un rand pe rand, mod editare explicit. +- **Schema: o singura coloana noua** — `import_rows.override_json` (nullable, criptat Fernet), patch CANONIC peste + randul mapat (Approach B). NU se modifica `raw_json` (ramane cheiat pe anteturile fisierului). Restul schemei neatins. +- **Fara atingerea logicii de idempotenta, validare, mapare sau a worker-ului** — editarea = mutatie pura de stocare; + recalculul de stare merge OBLIGATORIU prin `_resolve_row_for_preview` (un singur clasificator, fara drift). +- **Fara reorganizarea tab-urilor ramase** (Mapari/Cont/Nomenclator) dincolo de scoaterea tab-ului Trimiteri. +- **Fara paginare noua pe Trimiteri** — sectiunea de pe Acasa refoloseste filtrarea existenta (US-009 din 3.5). + +## 3. Stories atomice + +> Backend + UI pentru acelasi comportament = stories separate. Toate rutele web noi sunt sub `require_login` +> si **scoped pe contul din sesiune** (gard cross-account 404, identic cu rutele existente). CSRF pe toate POST-urile. + +### US-001: Backend — persista editarea unui rand de preview +**Ca** utilizator care vede un rand cu date gresite/lipsa in preview **vreau** sa salvez valori corectate +pentru acel rand **pentru ca** sa-l trimit fara re-upload de fisier. + +- **Depinde de**: — +- **Fisiere**: `app/schema.sql` (coloana `override_json` + `_migrate`), `app/api/v1/import_router.py` + (`_resolve_row_for_preview` + `commit_import` aplica override; ruta noua), `tests/test_import_edit_row.py`, + `tests/fixtures/import_antet_necanonic.csv` (NOU), `tests/fixtures/import_lipsa_coloana.csv` (NOU) (~5 fisiere) +- **Stocare (Approach B, decis la poarta autoplan):** editarea scrie un dict CANONIC in `import_rows.override_json` + (nullable, criptat Fernet). `_resolve_row_for_preview` si `commit_import` aplica `mapped.update(override_json)` + ULTIMUL, dupa maparea `json_mapare`. Astfel se poate completa si un camp a carui coloana LIPSESTE din fisier + (cazul "completez informatii lipsa"), iar `raw_json`/idempotency raman neatinse. +- **Test intai (RED)**: `tests/test_import_edit_row.py` — + `test_editeaza_rand_antet_necanonic_devine_ok` (fixture cu antet `Serie sasiu`/`Data` — prinde bug-ul de stocare), + `test_editeaza_completeaza_coloana_absenta` (fisier fara coloana data → editarea adauga data → ok), + `test_editeaza_status_identic_cu_GET_preview` (ruta editare NU re-deriva status; egal cu `GET /preview`), + `test_editeaza_rand_scoped_alt_cont_404`, `test_editeaza_batch_inexistent_404`, + `test_editeaza_row_index_invalid_pe_batch_valid_404`, + `test_editeaza_pastreaza_campuri_neatinse` (operatie/prestatii raman), + `test_editeaza_batch_committed_409` (guard post-commit), + `test_editeaza_raw_corupt_no_op` (decrypt fail → 422/no-op, fara crash), + `test_editeaza_empty_input_sterge_campul` (semantica empty = CLEAR, pentru cazul "corectez o valoare gresita"). +- **Acceptance criteria**: + - [ ] Migrare: `import_rows.override_json TEXT` (nullable), `_migrate` defensiv (idempotent, ca `is_admin` in 3.3b). + - [ ] Ruta `POST /v1/import/{import_id}/rand/{row_index}/editeaza` (`resolve_account_id`) + alias web + `POST /_import/{import_id}/rand/{row_index}/editeaza` (`require_login`). Campuri: `vin`, `nr_inmatriculare`, + `data_prestatie`, `odometru_initial`, `odometru_final`. + - [ ] **Mutatie pura**: decripteaza `override_json` curent (sau {}), aplica campurile (vezi semantica empty), re-cripteaza, + `UPDATE`. NU recalculeaza statusul in ruta — preview-ul il rederiva via `_resolve_row_for_preview`. + - [ ] **Semantica empty**: input gol = STERGE cheia din override (revine la valoarea din fisier daca exista). Documentat + testat. + - [ ] **Scoping intr-o singura interogare**: `import_rows r JOIN import_batches b ON b.id=r.batch_id + WHERE b.id=? AND b.account_id=? AND r.row_index=?` → 404 pe gol (acopera alt cont, batch inexistent, row_index invalid). + - [ ] **Guard committed**: batch cu `status='committed'` → 409 (editarea n-ar avea efect downstream). + - [ ] `decrypt_creds` → None/exceptie → 422/no-op defensiv (ca import_router.py:602-606), niciodata scriere goala. + - [ ] Coercion: nu se afirma `canonicalize_row` pe `odometru_initial` (normeaza doar `_final`); validarea (`_parse_int`) + tolereaza ".0" — testul verifica prin validare, nu prin canonicalize. + - [ ] NU atinge `submissions`. +- **Verificare E2E**: TestClient — (a) fixture cu antet ne-canonic, rand needs_data → editeaza → preview = `ok`; + (b) fixture fara coloana data → editeaza data → `ok`. + +### US-002: UI — buton "Editeaza" pe rand in tabelul de preview +**Ca** utilizator **vreau** sa pun un rand in mod editare si sa-i corectez celulele pe loc **pentru ca** +sa nu reincarc tot fisierul pentru o singura valoare. + +- **Depinde de**: US-001 +- **Fisiere**: `app/web/templates/_preview_import.html`, `app/web/templates/_preview_rand.html` (NOU — fragment rand), + `app/web/routes.py` (handler fragment rand), `tests/test_preview_edit_ui.py` +- **Test intai (RED)**: `tests/test_preview_edit_ui.py` — + `test_preview_are_buton_editeaza_pe_rand`, + `test_editeaza_intra_in_mod_editare_form_propriu` (randul devine FORM separat, NU in `#confirm-form`), + `test_salveaza_reda_doar_randul` (raspuns = fragment rand + OOB contoare, NU tot `#import-section`), + `test_enter_in_camp_editare_nu_declanseaza_confirm`, + `test_eroare_validare_pastreaza_valorile_introduse` (data invalida → ramane in editare, mesaj pe camp). +- **Acceptance criteria**: + - [ ] Fiecare rand are un buton "Editeaza" (coloana de actiuni la final). + - [ ] **Swap pe rand, NU pe sectiune (D-3.1):** Editeaza/Salveaza tintesc randul `