feat(import): confirmare valori needs_review in bloc + rapid din tabel (B1)

Randurile needs_review (flaguri de coercion: VIN citit ca numar, odometru
float, data ambigua) cereau confirmare umana DOAR prin modalul de editare,
un rand pe rand. Adauga doua cai care pastreaza omul in bucla (fara
auto-accept):

- Buton "Confirma toate valorile" (bulk): ruta noua
  POST /_import/{id}/confirma-toate-review marcheaza reviewed=1 pe TOATE
  randurile needs_review din batch cu un click. Scoped pe cont (404
  cross-account, 409 batch comis), o singura recompute + re-randare
  #import-section, dupa modelul web_mapare_operatii.
- Confirm rapid per-rand direct din tabel: buton in coloana Actiuni pe
  randurile needs_review, refoloseste ruta existenta /confirma-review
  (reviewed=1 pe un singur rand) cu hx-swap="none"; HX-Trigger
  reincarcaPreview reincarca sectiunea cu contoare/banner corecte.

Butonul bulk e randat in bannerul de discoverability (si in varianta OOB
din _preview_rand.html). Editarea unei valori reseteaza reviewed=0 ca
inainte (D#9, neschimbat).

Teste noi (tests/test_import_review.py): bulk marcheaza toate randurile,
guard committed 409, scoping 404 cross-account (fara efect pe randurile
altui cont), prezenta butonului rapid in tabel, confirm rapid per-rand
seteaza reviewed=1 fara a atinge alte randuri.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-03 13:25:48 +00:00
parent d3ebf4762d
commit a881b824bf
4 changed files with 338 additions and 14 deletions

View File

@@ -3611,6 +3611,63 @@ async def web_confirma_review(
conn.close()
@router.post("/_import/{import_id}/confirma-toate-review", response_class=HTMLResponse)
async def web_confirma_toate_review(
request: Request,
import_id: int,
) -> HTMLResponse:
"""Confirma in bloc TOATE randurile needs_review din batch → reviewed=1 (B1).
Un singur click marcheaza reviewed=1 pe toate randurile cu resolved_status='needs_review'
din batch-ul curent (om in bucla: operatorul confirma explicit intreg lotul, fara
auto-accept). Refoloseste EXACT logica din /confirma-review (reviewed=1 = marcaj separat,
NU camp de continut), aplicata in masa. La recalcul (_web_compute_preview) randurile cu
reviewed=1 si fara erori reale devin ok.
CSRF + scoped sesiune (404 cross-account) + guard committed (409). O singura recompute
+ re-randare #import-section la final (identic cu web_mapare_operatii).
"""
account_id = require_login(request)
conn = get_connection()
try:
form = await request.form()
verify_csrf(request, str(form.get("csrf_token") or ""))
# Guard batch: 404 cross-account (nu confirmam existenta), 409 committed.
acct = account_or_default(account_id)
batch = conn.execute(
"SELECT id, status FROM import_batches WHERE id=? AND account_id=?",
(import_id, acct),
).fetchone()
if not batch:
raise HTTPException(status_code=404, detail="batch de import inexistent sau inaccesibil")
if batch["status"] == "committed":
raise HTTPException(status_code=409, detail="batch deja comis; confirmarea nu mai are efect")
# Marcheaza reviewed=1 pe toate randurile needs_review ale batch-ului (scoped pe batch,
# deja verificat ca apartine contului). Acelasi marcaj ca /confirma-review, in masa.
cur = conn.execute(
"UPDATE import_rows SET reviewed=1 "
"WHERE batch_id=? AND resolved_status='needs_review'",
(import_id,),
)
n_confirmate = cur.rowcount if cur.rowcount is not None and cur.rowcount >= 0 else 0
message = (
f"Confirmate {n_confirmate} randuri cu valori de verificat."
if n_confirmate
else "Niciun rand de confirmat."
)
result = _web_compute_preview(conn, import_id, account_id)
if isinstance(result, str):
return templates.TemplateResponse("_upload.html", _ctx(request, error=result))
return templates.TemplateResponse("_preview_import.html", _ctx(
request, import_id=import_id, message=message, **result
))
finally:
conn.close()
@router.post("/_import/{import_id}/mapare-operatie", response_class=HTMLResponse)
async def web_mapare_operatie(
request: Request,

View File

@@ -132,9 +132,24 @@
style="margin-bottom:12px; padding:8px 14px; border-radius:6px;
background:color-mix(in srgb, var(--warn, #e6b34a) 12%, var(--card));
border:1px solid var(--warn, #e6b34a); font-size:13px;">
Randurile cu <span class="pill s-needs_review" style="font-size:11px;">Verifica valori</span>
nu pleaca la RAR pana le deschizi in modal si confirmi in modal
cu butonul <strong>Confirma valorile</strong>.
<div>
Randurile cu <span class="pill s-needs_review" style="font-size:11px;">Verifica valori</span>
nu pleaca la RAR pana confirmi valorile. Verifica-le (butonul <strong>Confirma valorile</strong>
de pe rand sau in modal) sau, daca lotul e in regula, confirma-le pe toate deodata.
</div>
{# B1: buton bulk — un click marcheaza reviewed=1 pe toate randurile needs_review.
hx-swap="none": raspunsul re-randeaza #import-section prin outerHTML pe tinta. #}
<button type="button"
hx-post="/_import/{{ import_id }}/confirma-toate-review"
hx-vals='{"csrf_token": "{{ csrf_token or '' }}"}'
hx-target="#import-section"
hx-swap="outerHTML"
hx-confirm="Confirmi valorile pentru toate cele {{ summary.get('needs_review', 0) }} randuri de verificat? Devin gata de trimis la RAR."
hx-disabled-elt="this"
style="margin-top:10px; min-height:40px; padding:8px 18px;
background:var(--ok, #2a7); color:#fff; border-color:transparent; font-size:13px;">
Confirma toate valorile ({{ summary.get('needs_review', 0) }})
</button>
</div>
{% endif %}
</div>

View File

@@ -62,14 +62,31 @@
<td class="col-data" data-eticheta="Data prestatie">{{ row.prez.data_prestatie }}</td>
<td class="col-actiuni" data-eticheta="Actiuni" style="text-align:center;">
{% if status not in ('already_sent', 'duplicate_in_file') %}
<button type="button" class="btn-editeaza"
style="min-height:36px; padding:6px 14px; font-size:13px;
background:transparent; border-color:var(--line); color:var(--ink);"
hx-get="/_import/{{ import_id }}/rand/{{ row.row_index }}/editare-modal"
hx-target="#detaliu-modal-body" hx-swap="innerHTML"
aria-label="Editeaza randul {{ row.row_index + 1 }} (VIN: {{ res.get('vin', '') }})">
Editeaza
</button>
<div style="display:inline-flex; gap:6px; flex-wrap:wrap; justify-content:center;">
{# B1: confirm rapid per-rand direct din tabel (fara a deschide modalul).
Refoloseste ruta /confirma-review (reviewed=1 pe un singur rand). hx-swap="none":
raspunsul (empty div) NU se insereaza nicaieri; HX-Trigger reincarcaPreview
reincarca sectiunea (contoare + banner corecte). Editarea reseteaza reviewed=0. #}
{% if status == 'needs_review' %}
<button type="button" class="btn-confirma-rapid"
style="min-height:36px; padding:6px 14px; font-size:13px;
background:var(--ok, #2a7); color:#fff; border-color:transparent;"
hx-post="/_import/{{ import_id }}/rand/{{ row.row_index }}/confirma-review"
hx-vals='{"csrf_token": "{{ csrf_token or '' }}"}'
hx-swap="none" hx-disabled-elt="this"
aria-label="Confirma valorile randului {{ row.row_index + 1 }} (VIN: {{ res.get('vin', '') }})">
Confirma valorile
</button>
{% endif %}
<button type="button" class="btn-editeaza"
style="min-height:36px; padding:6px 14px; font-size:13px;
background:transparent; border-color:var(--line); color:var(--ink);"
hx-get="/_import/{{ import_id }}/rand/{{ row.row_index }}/editare-modal"
hx-target="#detaliu-modal-body" hx-swap="innerHTML"
aria-label="Editeaza randul {{ row.row_index + 1 }} (VIN: {{ res.get('vin', '') }})">
Editeaza
</button>
</div>
{% endif %}
</td>
</tr>
@@ -93,9 +110,22 @@
style="margin-bottom:12px; padding:8px 14px; border-radius:6px;
background:color-mix(in srgb, var(--warn, #e6b34a) 12%, var(--card));
border:1px solid var(--warn, #e6b34a); font-size:13px;">
Randurile cu <span class="pill s-needs_review" style="font-size:11px;">Verifica valori</span>
nu pleaca la RAR pana le deschizi in modal si confirmi in modal
cu butonul <strong>Confirma valorile</strong>.
<div>
Randurile cu <span class="pill s-needs_review" style="font-size:11px;">Verifica valori</span>
nu pleaca la RAR pana confirmi valorile. Verifica-le (butonul <strong>Confirma valorile</strong>
de pe rand sau in modal) sau, daca lotul e in regula, confirma-le pe toate deodata.
</div>
<button type="button"
hx-post="/_import/{{ import_id }}/confirma-toate-review"
hx-vals='{"csrf_token": "{{ csrf_token or '' }}"}'
hx-target="#import-section"
hx-swap="outerHTML"
hx-confirm="Confirmi valorile pentru toate cele {{ summary.get('needs_review', 0) }} randuri de verificat? Devin gata de trimis la RAR."
hx-disabled-elt="this"
style="margin-top:10px; min-height:40px; padding:8px 18px;
background:var(--ok, #2a7); color:#fff; border-color:transparent; font-size:13px;">
Confirma toate valorile ({{ summary.get('needs_review', 0) }})
</button>
</div>
{% endif %}
</div>