feat(5.12): modal editare + cont obligatoriu la import; design.md + PRD 5.13 revizuit (/autoplan)
5.12 (livrat): editare in modal a randurilor de preview, cont obligatoriu inainte de import, formular editare extras (_form_editare, _editare_preview_modal), plus suita de teste aferenta (preview edit/compact, mapare op, form editare, signup, admin panel). Design + planificare: - docs/design.md: sistem de design (tokeni, breakpoints, scara control, componente, a11y). - docs/prd/prd-5.12-* si prd-5.13-* (5.13 cu raport /autoplan: CEO+Design+Eng, audit trail). Curatare: sterse PNG-urile de test/mockup temporare din radacina. Nota: implementarea CSS 5.13 (responsive compact + sistem butoane) NU e inca facuta — planul revizuit cere refactorul testelor fragile din test_web_responsive.py INAINTE de CSS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -174,13 +174,16 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
|
||||
Scoped pe contul sesiunii — identic cu pattern-ul din restul rutelor (NULL->1).
|
||||
"""
|
||||
from ..mapping import account_or_default
|
||||
from ..accounts import account_is_complete as _acct_is_complete
|
||||
acct = account_or_default(account_id)
|
||||
|
||||
# Pas 1: are credentiale RAR configurate?
|
||||
# Pas 1: are credentiale RAR configurate? + metadate cont (pentru banner incomplet)
|
||||
row = conn.execute(
|
||||
"SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)
|
||||
"SELECT id, name, cui, email, rar_creds_enc FROM accounts WHERE id=?", (acct,)
|
||||
).fetchone()
|
||||
are_creds = bool(row and row["rar_creds_enc"])
|
||||
# Banner cont incomplet (US-002): contul nu are companie + email + CUI complete
|
||||
cont_incomplet = not _acct_is_complete(row) if row else False
|
||||
|
||||
# Pas 3: are cel putin un submission (trimis sau in coada)?
|
||||
row_sub = conn.execute(
|
||||
@@ -214,6 +217,8 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
|
||||
"versiune_trimiteri": _trimiteri_versiune(conn, account_id),
|
||||
# Acasa include caseta de upload -> are nevoie de csrf_token
|
||||
"csrf_token": get_csrf_token(request),
|
||||
# Banner ne-blocant (US-002): contul nu are identitate completa (companie+email+CUI)
|
||||
"cont_incomplet": cont_incomplet,
|
||||
}
|
||||
|
||||
|
||||
@@ -266,8 +271,11 @@ def _render_panel_cont(request: Request, conn, account_id: int) -> str:
|
||||
"""Randeaza panoul Cont ca string HTML."""
|
||||
from ..mapping import account_or_default
|
||||
acct = account_or_default(account_id)
|
||||
row = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone()
|
||||
row = conn.execute(
|
||||
"SELECT id, name, cui, email, rar_creds_enc FROM accounts WHERE id=?", (acct,)
|
||||
).fetchone()
|
||||
are_creds = bool(row and row["rar_creds_enc"])
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
return templates.get_template("_cont.html").render({
|
||||
"request": request,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
@@ -276,6 +284,9 @@ def _render_panel_cont(request: Request, conn, account_id: int) -> str:
|
||||
"creds_mesaj": None,
|
||||
"creds_eroare": None,
|
||||
"rot_eroare": None,
|
||||
"account_meta": account_meta,
|
||||
"date_firma_mesaj": None,
|
||||
"date_firma_eroare": None,
|
||||
})
|
||||
|
||||
|
||||
@@ -1865,7 +1876,8 @@ def _web_compute_preview(
|
||||
return "Batch de import inexistent sau inaccesibil."
|
||||
|
||||
raw_rows_db = conn.execute(
|
||||
"SELECT row_index, raw_json, override_json FROM import_rows WHERE batch_id=? ORDER BY row_index",
|
||||
"SELECT row_index, raw_json, override_json, reviewed FROM import_rows "
|
||||
"WHERE batch_id=? ORDER BY row_index",
|
||||
(import_id,),
|
||||
).fetchall()
|
||||
if not raw_rows_db:
|
||||
@@ -1874,6 +1886,7 @@ def _web_compute_preview(
|
||||
# Decripteaza randurile + override-urile editate
|
||||
rows: list[dict[str, Any]] = []
|
||||
overrides: list[dict[str, Any]] = []
|
||||
reviewed_flags: list[bool] = []
|
||||
for r in raw_rows_db:
|
||||
try:
|
||||
row_data = decrypt_creds(r["raw_json"]) or {}
|
||||
@@ -1885,6 +1898,7 @@ def _web_compute_preview(
|
||||
except Exception:
|
||||
ov = None
|
||||
overrides.append(ov or {})
|
||||
reviewed_flags.append(bool(r["reviewed"]))
|
||||
|
||||
col_names = list(rows[0].keys()) if rows else []
|
||||
sig = _signature(col_names)
|
||||
@@ -1952,6 +1966,7 @@ def _web_compute_preview(
|
||||
override=overrides[i] or None,
|
||||
valid_codes=valid_codes,
|
||||
text_rules=text_rules,
|
||||
reviewed=reviewed_flags[i],
|
||||
)
|
||||
|
||||
key: str | None = None
|
||||
@@ -2161,12 +2176,14 @@ async def web_upload_import(
|
||||
if sugg:
|
||||
fuzzy_suggestions[col] = sugg
|
||||
|
||||
_sample = parsed.rows[:3]
|
||||
return templates.TemplateResponse("_mapcoloane.html", {
|
||||
"request": request,
|
||||
"import_id": batch_id_int,
|
||||
"filename": filename,
|
||||
"columns": parsed.columns,
|
||||
"sample_rows": parsed.rows[:3],
|
||||
"sample_rows": _sample,
|
||||
"prima_inregistrare": _sample[0] if _sample else None,
|
||||
"fuzzy_suggestions": fuzzy_suggestions,
|
||||
"canonical_fields": _CANONICAL_FIELDS,
|
||||
"format_data": None,
|
||||
@@ -2379,23 +2396,63 @@ def _render_preview_rand(
|
||||
})
|
||||
|
||||
|
||||
@router.get("/_import/{import_id}/rand/{row_index}/editare", response_class=HTMLResponse)
|
||||
def web_rand_editare(request: Request, import_id: int, row_index: int) -> HTMLResponse:
|
||||
"""Intra in mod editare pe un rand de preview (randul devine FORM propriu)."""
|
||||
@router.get("/_import/{import_id}/rand/{row_index}/editare-modal", response_class=HTMLResponse)
|
||||
def web_rand_editare_modal(request: Request, import_id: int, row_index: int) -> HTMLResponse:
|
||||
"""Fragment editare rand preview in modalul global (#detaliu-modal-body).
|
||||
|
||||
US-006 (PRD 5.12): inlocuieste editarea inline (tr.preview-edit) care cauza
|
||||
colapsare vizuala si eroare JS la Anuleaza (R5). Randeaza _editare_preview_modal.html.
|
||||
Campurile vehicul/data/odometru sunt preluate din starea curenta (resolved + override).
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
conn = get_connection()
|
||||
try:
|
||||
result, row = _preview_one_row(conn, import_id, account_id, row_index)
|
||||
if row is None or isinstance(result, str):
|
||||
raise HTTPException(status_code=404, detail="rand de import inexistent")
|
||||
return _render_preview_rand(
|
||||
request, import_id=import_id, row=row, editing=True,
|
||||
include_oob=False, summary=result["summary"],
|
||||
)
|
||||
res = row.get("resolved") or {}
|
||||
err_map: dict[str, str] = {}
|
||||
fix_map: dict[str, str] = {}
|
||||
for e in (row.get("errors") or []):
|
||||
if isinstance(e, dict) and e.get("field"):
|
||||
err_map[e["field"]] = e.get("message") or e.get("msg") or ""
|
||||
if e.get("fix"):
|
||||
fix_map[e["field"]] = e["fix"]
|
||||
return templates.TemplateResponse("_editare_preview_modal.html", {
|
||||
"request": request,
|
||||
"import_id": import_id,
|
||||
"row_index": row_index,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
"vin": res.get("vin") or "",
|
||||
"stare_css": row.get("stare_css") or "",
|
||||
"stare_eticheta": row.get("stare_eticheta") or "",
|
||||
"form_nr": res.get("nr_inmatriculare") or "",
|
||||
"form_vin": res.get("vin") or "",
|
||||
"form_data": res.get("data_prestatie") or "",
|
||||
"form_odo_final": str(res.get("odometru_final") or ""),
|
||||
"form_odo_initial": str(res.get("odometru_initial") or ""),
|
||||
"err_map": err_map,
|
||||
"fix_map": fix_map,
|
||||
"vin_context": res.get("vin") or "",
|
||||
"btn_label": "Salveaza",
|
||||
"message": None,
|
||||
# T2 (US-007): butonul 'Confirma valorile' apare DOAR pe randurile needs_review.
|
||||
"is_needs_review": row.get("resolved_status") == "needs_review",
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/_import/{import_id}/rand/{row_index}/editare", response_class=HTMLResponse)
|
||||
def web_rand_editare(request: Request, import_id: int, row_index: int) -> HTMLResponse:
|
||||
"""Fragment editare rand preview in modal — alias al /editare-modal.
|
||||
|
||||
US-006: editarea inline eliminata; ruta pastrata pentru compatibilitate cu
|
||||
apeluri externe / teste existente. Delega la web_rand_editare_modal.
|
||||
"""
|
||||
return web_rand_editare_modal(request, import_id, row_index)
|
||||
|
||||
|
||||
@router.get("/_import/{import_id}/rand/{row_index}", response_class=HTMLResponse)
|
||||
def web_rand_display(request: Request, import_id: int, row_index: int) -> HTMLResponse:
|
||||
"""Iese din mod editare (Anuleaza) — re-randeaza randul read-only + OOB contoare."""
|
||||
@@ -2415,11 +2472,17 @@ def web_rand_display(request: Request, import_id: int, row_index: int) -> HTMLRe
|
||||
|
||||
@router.post("/_import/{import_id}/rand/{row_index}/editeaza", response_class=HTMLResponse)
|
||||
async def web_editeaza_rand(request: Request, import_id: int, row_index: int) -> HTMLResponse:
|
||||
"""Persista override (mutatie pura) + re-randeaza DOAR randul.
|
||||
"""Persista override (mutatie pura) + raspunde cu OOB rand+contoare sau erori in modal.
|
||||
|
||||
Statusul e rederivat prin `_resolve_row_for_preview`. Swap pe rand + OOB contoare.
|
||||
Daca raman erori de continut pe camp, randul ramane in editare cu valorile pastrate
|
||||
si mesajul pe campul vinovat."""
|
||||
US-006 (PRD 5.12):
|
||||
- Succes: raspuns cu OOB pe rand (#preview-row-N) + OOB contoare (#preview-rezumat) +
|
||||
header HX-Trigger-After-Settle:inchideModal (modalul se inchide, OOB se aplica).
|
||||
- Eroare camp: re-randeaza _editare_preview_modal.html cu valorile introduse + erorile
|
||||
per-camp; modalul RAMANE DESCHIS; NU se emite inchideModal.
|
||||
|
||||
INVARIANT CRITIC (R2): submissions NEATINS — override-only pe import_rows.override_json,
|
||||
NU re-queue, NU insereaza in submissions.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
form = await request.form()
|
||||
verify_csrf(request, str(form.get("csrf_token") or ""))
|
||||
@@ -2430,7 +2493,7 @@ async def web_editeaza_rand(request: Request, import_id: int, row_index: int) ->
|
||||
conn = get_connection()
|
||||
try:
|
||||
# Mutatie pura de stocare (404/409/422 -> propaga; htmx hx-on::response-error
|
||||
# pastreaza randul + valorile la 4xx/5xx).
|
||||
# pastreaza formularul modal cu valorile la 4xx/5xx).
|
||||
apply_row_override(
|
||||
conn, import_id=import_id, account_id=account_id,
|
||||
row_index=row_index, fields=fields,
|
||||
@@ -2443,15 +2506,120 @@ async def web_editeaza_rand(request: Request, import_id: int, row_index: int) ->
|
||||
if isinstance(e, dict) and e.get("field")
|
||||
]
|
||||
if field_errors:
|
||||
return _render_preview_rand(
|
||||
request, import_id=import_id, row=row, editing=True,
|
||||
include_oob=True, summary=result["summary"],
|
||||
message="Mai sunt valori invalide — corecteaza campurile marcate.",
|
||||
)
|
||||
return _render_preview_rand(
|
||||
request, import_id=import_id, row=row, editing=False,
|
||||
include_oob=True, summary=result["summary"],
|
||||
)
|
||||
# Eroare de validare: re-randeaza formularul in modal cu valorile introduse.
|
||||
# Modalul RAMANE DESCHIS (fara HX-Trigger-After-Settle:inchideModal).
|
||||
res = row.get("resolved") or {}
|
||||
err_map: dict[str, str] = {}
|
||||
fix_map: dict[str, str] = {}
|
||||
for e in field_errors:
|
||||
if e.get("field"):
|
||||
err_map[e["field"]] = e.get("message") or e.get("msg") or ""
|
||||
if e.get("fix"):
|
||||
fix_map[e["field"]] = e["fix"]
|
||||
return templates.TemplateResponse("_editare_preview_modal.html", {
|
||||
"request": request,
|
||||
"import_id": import_id,
|
||||
"row_index": row_index,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
"vin": res.get("vin") or "",
|
||||
"stare_css": row.get("stare_css") or "",
|
||||
"stare_eticheta": row.get("stare_eticheta") or "",
|
||||
# Valorile DIN FORM (pentru ca userul sa vada ce a introdus):
|
||||
"form_nr": str(form.get("nr_inmatriculare") or res.get("nr_inmatriculare") or ""),
|
||||
"form_vin": str(form.get("vin") or res.get("vin") or ""),
|
||||
"form_data": str(form.get("data_prestatie") or res.get("data_prestatie") or ""),
|
||||
"form_odo_final": str(form.get("odometru_final") or res.get("odometru_final") or ""),
|
||||
"form_odo_initial": str(form.get("odometru_initial") or res.get("odometru_initial") or ""),
|
||||
"err_map": err_map,
|
||||
"fix_map": fix_map,
|
||||
"vin_context": res.get("vin") or "",
|
||||
"btn_label": "Salveaza",
|
||||
"message": "Mai sunt valori invalide — corecteaza campurile marcate.",
|
||||
})
|
||||
|
||||
# Succes: OOB swap rand + contoare + inchideModal.
|
||||
# Continut primar (swap in #detaliu-modal-body): stub invizibil + script recalc.
|
||||
# OOB: <tr> actualizat + rezumat + contor.
|
||||
# HX-Trigger-After-Settle: inchideModal → base.html JS inchide modalul.
|
||||
oob_content = templates.get_template("_preview_rand.html").render({
|
||||
"request": request,
|
||||
"import_id": import_id,
|
||||
"row": row,
|
||||
"editing": False,
|
||||
"oob_tr": True,
|
||||
"include_oob": True,
|
||||
"summary": result["summary"],
|
||||
"message": None,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
})
|
||||
html_body = '<div style="display:none;"></div>' + oob_content
|
||||
resp = HTMLResponse(content=html_body)
|
||||
resp.headers["HX-Trigger-After-Settle"] = "inchideModal"
|
||||
return resp
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/_import/{import_id}/rand/{row_index}/confirma-review", response_class=HTMLResponse)
|
||||
async def web_confirma_review(
|
||||
request: Request,
|
||||
import_id: int,
|
||||
row_index: int,
|
||||
) -> HTMLResponse:
|
||||
"""Confirma explicit valorile unui rand needs_review → seteaza reviewed=1 in DB.
|
||||
|
||||
US-007 (PRD 5.12), T2: butonul 'Confirma valorile' din modal seteaza reviewed=1
|
||||
pentru randul indicat. La recalcul (_web_compute_preview), randul cu reviewed=1
|
||||
si fara erori de validare reale devine ok (nu mai e blocat de flaguri ambigue).
|
||||
|
||||
Guard: 404 cross-account (scoping JOIN), 409 batch committed.
|
||||
Raspunde OOB (rand + contoare) + HX-Trigger-After-Settle:inchideModal,
|
||||
identic cu web_editeaza_rand la succes.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
form = await request.form()
|
||||
verify_csrf(request, str(form.get("csrf_token") or ""))
|
||||
acct = account_or_default(account_id)
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
# Scoping: JOIN verifica ca batch-ul apartine contului si ca randul exista.
|
||||
# Acelasi tipar ca apply_row_override (404 cross-account, 409 committed).
|
||||
row_db = conn.execute(
|
||||
"SELECT r.id AS rid, b.status AS bstatus "
|
||||
"FROM import_rows r JOIN import_batches b ON b.id = r.batch_id "
|
||||
"WHERE b.id=? AND b.account_id=? AND r.row_index=?",
|
||||
(import_id, acct, row_index),
|
||||
).fetchone()
|
||||
if not row_db:
|
||||
raise HTTPException(status_code=404, detail="rand de import inexistent")
|
||||
if row_db["bstatus"] == "committed":
|
||||
raise HTTPException(status_code=409, detail="batch deja comis; confirmarea nu mai are efect")
|
||||
|
||||
# Seteaza reviewed=1 — marcaj separat, NU camp de continut (NU intra in override_json/payload).
|
||||
conn.execute("UPDATE import_rows SET reviewed=1 WHERE id=?", (row_db["rid"],))
|
||||
|
||||
# Recalculeaza preview: randul cu reviewed=1 + fara erori reale devine ok.
|
||||
result, row = _preview_one_row(conn, import_id, account_id, row_index)
|
||||
if row is None or isinstance(result, str):
|
||||
raise HTTPException(status_code=404, detail="rand de import inexistent")
|
||||
|
||||
# OOB: rand actualizat + rezumat + contor ok + inchideModal (identic cu succes editeaza)
|
||||
oob_content = templates.get_template("_preview_rand.html").render({
|
||||
"request": request,
|
||||
"import_id": import_id,
|
||||
"row": row,
|
||||
"editing": False,
|
||||
"oob_tr": True,
|
||||
"include_oob": True,
|
||||
"summary": result["summary"],
|
||||
"message": None,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
})
|
||||
html_body = '<div style="display:none;"></div>' + oob_content
|
||||
resp = HTMLResponse(content=html_body)
|
||||
resp.headers["HX-Trigger-After-Settle"] = "inchideModal"
|
||||
return resp
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -2499,6 +2667,82 @@ async def web_mapare_operatie(
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/_import/{import_id}/mapare-operatii", response_class=HTMLResponse)
|
||||
async def web_mapare_operatii(
|
||||
request: Request,
|
||||
import_id: int,
|
||||
) -> HTMLResponse:
|
||||
"""Un singur POST salveaza toate maparile de operatii (US-004).
|
||||
|
||||
Primeste perechi (cod_op_service, cod_prestatie) ca liste paralele din un singur
|
||||
<form> cu un select per operatie. Apeleaza save_mapping pentru fiecare pereche cu
|
||||
cod ales (reuse EXACT, fara logica noua). Perechile cu cod_prestatie gol sunt ignorate.
|
||||
D#12: per-item — cod invalid -> skip + sumar, restul salvate. O singura recompute
|
||||
_web_compute_preview + re-randare #import-section la final.
|
||||
CSRF + scoped sesiune (404 cross-account via _web_compute_preview) + guard committed 409.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
conn = get_connection()
|
||||
try:
|
||||
form = await request.form()
|
||||
verify_csrf(request, str(form.get("csrf_token") or ""))
|
||||
|
||||
# Guard batch committed (409)
|
||||
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; maparea nu mai are efect")
|
||||
|
||||
# Extrage listele paralele din form (getlist pentru valori multiple cu acelasi name)
|
||||
ops_list = form.getlist("cod_op_service")
|
||||
codes_list = form.getlist("cod_prestatie")
|
||||
|
||||
salvate: list[str] = []
|
||||
sarite_invalide: list[str] = []
|
||||
|
||||
for cod_op_service, cod_prestatie in zip(ops_list, codes_list):
|
||||
cod_op_service = str(cod_op_service or "").strip()
|
||||
cod_prestatie = str(cod_prestatie or "").strip().upper()
|
||||
|
||||
# Ignora perechile fara cod ales (selectul ramas pe "— alege cod RAR —")
|
||||
if not cod_op_service or not cod_prestatie:
|
||||
continue
|
||||
|
||||
# Validare per-item (D#12): cod invalid -> skip + sumar, nu all-or-nothing
|
||||
exists = conn.execute(
|
||||
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod_prestatie,)
|
||||
).fetchone()
|
||||
if not exists:
|
||||
sarite_invalide.append(f"{cod_op_service} ({cod_prestatie} necunoscut)")
|
||||
continue
|
||||
|
||||
save_mapping(conn, account_id, cod_op_service, cod_prestatie, auto_send=False)
|
||||
salvate.append(f"{cod_op_service} -> {cod_prestatie}")
|
||||
|
||||
# Compune mesajul sumar
|
||||
parts: list[str] = []
|
||||
if salvate:
|
||||
parts.append(f"Salvate: {', '.join(salvate)}.")
|
||||
if sarite_invalide:
|
||||
parts.append(f"Coduri necunoscute ignorate: {', '.join(sarite_invalide)}.")
|
||||
message = " ".join(parts) if parts else None
|
||||
error = bool(sarite_invalide) and not salvate
|
||||
|
||||
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, error=error, **result
|
||||
))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.get("/_import/reset", response_class=HTMLResponse)
|
||||
def web_import_reset(request: Request) -> HTMLResponse:
|
||||
"""Reseteaza sectiunea de import la starea initiala (drop zone gol)."""
|
||||
@@ -2532,14 +2776,11 @@ async def web_confirma_import(
|
||||
except (ValueError, TypeError):
|
||||
n_confirmat = 0
|
||||
|
||||
# Randuri needs_review bifate explicit
|
||||
reviewed_rows: set[int] = set()
|
||||
for v in form.getlist("reviewed_rows"):
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
reviewed_rows.add(int(v))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
# US-007: reviewed_rows (checkboxe vechi) NU mai este sursa de adevar pentru gate-ul
|
||||
# de commit pe canalul web. Gate-ul este derivat din DB import_rows.reviewed (D#8).
|
||||
# Randurile needs_review confirmate de operator via /confirma-review au resolved_status='ok'
|
||||
# in DB (recalculat de _web_compute_preview), asa ca interogarea de mai jos include corect
|
||||
# TOATE randurile gata de trimis.
|
||||
|
||||
confirmed_by = str(form.get("confirmed_by") or "").strip() or None
|
||||
|
||||
@@ -2559,10 +2800,19 @@ async def web_confirma_import(
|
||||
request, message="Acest batch a fost deja comis."
|
||||
))
|
||||
|
||||
# Incarca randurile cu stare ok si needs_review
|
||||
# Incarca DOAR randurile ok din DB.
|
||||
# D#8 (PRD 5.12): gate derivat din DB reviewed — randurile needs_review confirmate
|
||||
# de operator via /confirma-review au resolved_status='ok' (recalculat de
|
||||
# _web_compute_preview in calea /confirma-review). Randurile needs_review
|
||||
# neconfirmate sunt excluse (nu au reviewed=1 => raman needs_review in DB).
|
||||
# Fallback defensiv: includes si needs_review cu reviewed=1 (daca DB a ramas
|
||||
# neactualizat din vreun motiv — ex. restart intre confirma-review si preview refresh).
|
||||
ok_rows_db = conn.execute(
|
||||
"SELECT row_index, raw_json, override_json, resolved_status FROM import_rows "
|
||||
"WHERE batch_id=? AND resolved_status IN ('ok', 'needs_review') ORDER BY row_index",
|
||||
"SELECT row_index, raw_json, override_json, resolved_status, reviewed "
|
||||
"FROM import_rows "
|
||||
"WHERE batch_id=? AND (resolved_status='ok' OR "
|
||||
"(resolved_status='needs_review' AND reviewed=1)) "
|
||||
"ORDER BY row_index",
|
||||
(import_id,),
|
||||
).fetchall()
|
||||
|
||||
@@ -2584,28 +2834,18 @@ async def web_confirma_import(
|
||||
|
||||
# Decripteaza si construieste lista de randuri de trimis
|
||||
to_enqueue: list[dict[str, Any]] = []
|
||||
review_indices: set[int] = set()
|
||||
|
||||
for r in ok_rows_db:
|
||||
try:
|
||||
row_data = decrypt_creds(r["raw_json"]) or {}
|
||||
except Exception:
|
||||
continue
|
||||
if r["resolved_status"] == "ok":
|
||||
to_enqueue.append({"row_index": r["row_index"], "data": row_data,
|
||||
"override": _override_of(r), "status": "ok"})
|
||||
elif r["resolved_status"] == "needs_review":
|
||||
review_indices.add(r["row_index"])
|
||||
|
||||
# Adauga randurile needs_review bifate explicit
|
||||
for r in ok_rows_db:
|
||||
if r["resolved_status"] == "needs_review" and r["row_index"] in reviewed_rows:
|
||||
try:
|
||||
row_data = decrypt_creds(r["raw_json"]) or {}
|
||||
to_enqueue.append({"row_index": r["row_index"], "data": row_data,
|
||||
"override": _override_of(r), "status": "needs_review"})
|
||||
except Exception:
|
||||
pass
|
||||
to_enqueue.append({
|
||||
"row_index": r["row_index"],
|
||||
"data": row_data,
|
||||
"override": _override_of(r),
|
||||
"status": r["resolved_status"],
|
||||
})
|
||||
|
||||
n_total_ok = len(to_enqueue)
|
||||
|
||||
@@ -2816,6 +3056,9 @@ def _render_cont(
|
||||
creds_mesaj: str | None = None,
|
||||
creds_eroare: str | None = None,
|
||||
rot_eroare: str | None = None,
|
||||
account_meta: dict | None = None,
|
||||
date_firma_mesaj: str | None = None,
|
||||
date_firma_eroare: str | None = None,
|
||||
) -> HTMLResponse:
|
||||
"""Randeaza cardul 'Contul meu'. Parola niciodata in value=."""
|
||||
return templates.TemplateResponse(
|
||||
@@ -2827,22 +3070,41 @@ def _render_cont(
|
||||
creds_mesaj=creds_mesaj,
|
||||
creds_eroare=creds_eroare,
|
||||
rot_eroare=rot_eroare,
|
||||
account_meta=account_meta or {},
|
||||
date_firma_mesaj=date_firma_mesaj,
|
||||
date_firma_eroare=date_firma_eroare,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _fetch_account_meta(conn, acct: int) -> dict:
|
||||
"""Intoarce metadatele contului (id, name, cui, email) pentru sectiunea 'Date firma'."""
|
||||
row = conn.execute(
|
||||
"SELECT id, name, cui, email FROM accounts WHERE id=?", (acct,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
return {"id": acct, "name": "", "cui": "", "email": ""}
|
||||
return {
|
||||
"id": row["id"],
|
||||
"name": row["name"] or "",
|
||||
"cui": row["cui"] or "",
|
||||
"email": row["email"] or "",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/_fragments/cont", response_class=HTMLResponse)
|
||||
def fragment_cont(request: Request) -> HTMLResponse:
|
||||
"""Fragment HTMX card 'Contul meu': stare cheie + creds RAR (fara a le expune)."""
|
||||
"""Fragment HTMX card 'Contul meu': stare cheie + creds RAR + date firma."""
|
||||
account_id = require_login(request)
|
||||
acct = account_or_default(account_id)
|
||||
conn = get_connection()
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)
|
||||
"SELECT id, name, cui, email, rar_creds_enc FROM accounts WHERE id=?", (acct,)
|
||||
).fetchone()
|
||||
are_creds = bool(row and row["rar_creds_enc"])
|
||||
return _render_cont(request, are_creds=are_creds)
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
return _render_cont(request, are_creds=are_creds, account_meta=account_meta)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -2863,7 +3125,149 @@ def cont_roteste_cheie(
|
||||
"SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)
|
||||
).fetchone()
|
||||
are_creds = bool(row and row["rar_creds_enc"])
|
||||
return _render_cont(request, api_key=new_key, are_creds=are_creds)
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
return _render_cont(request, api_key=new_key, are_creds=are_creds, account_meta=account_meta)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/cont/date-firma", response_class=HTMLResponse)
|
||||
async def cont_date_firma(request: Request) -> HTMLResponse:
|
||||
"""Actualizeaza datele firmei (companie, email, CUI) pentru contul din sesiune.
|
||||
|
||||
Valideaza campurile (reuse _norm_cui / _norm_email din accounts.py), verifica
|
||||
unicitatea CUI-ului, actualizeaza accounts.name/email/cui. CSRF enforce.
|
||||
Scoped pe contul sesiunii (nu poate atinge alt cont).
|
||||
"""
|
||||
from ..accounts import _norm_cui, _norm_email
|
||||
|
||||
account_id = require_login(request)
|
||||
form = await request.form()
|
||||
verify_csrf(request, str(form.get("csrf_token") or ""))
|
||||
acct = account_or_default(account_id)
|
||||
|
||||
companie_raw = str(form.get("companie") or "").strip()
|
||||
email_raw = str(form.get("email") or "")
|
||||
cui_raw = str(form.get("cui") or "")
|
||||
|
||||
# Validare companie
|
||||
if not companie_raw:
|
||||
conn = get_connection()
|
||||
try:
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone()
|
||||
are_creds = bool(row_cr and row_cr["rar_creds_enc"])
|
||||
finally:
|
||||
conn.close()
|
||||
return _render_cont(
|
||||
request,
|
||||
are_creds=are_creds,
|
||||
account_meta=account_meta,
|
||||
date_firma_eroare="Compania (numele firmei) este obligatorie.",
|
||||
)
|
||||
|
||||
# Normalizare si validare email
|
||||
try:
|
||||
email_norm = _norm_email(email_raw)
|
||||
except ValueError as exc:
|
||||
conn = get_connection()
|
||||
try:
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone()
|
||||
are_creds = bool(row_cr and row_cr["rar_creds_enc"])
|
||||
finally:
|
||||
conn.close()
|
||||
return _render_cont(
|
||||
request,
|
||||
are_creds=are_creds,
|
||||
account_meta={"name": companie_raw, "email": email_raw, "cui": cui_raw},
|
||||
date_firma_eroare=f"Email invalid: {exc}",
|
||||
)
|
||||
|
||||
if not email_norm:
|
||||
conn = get_connection()
|
||||
try:
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone()
|
||||
are_creds = bool(row_cr and row_cr["rar_creds_enc"])
|
||||
finally:
|
||||
conn.close()
|
||||
return _render_cont(
|
||||
request,
|
||||
are_creds=are_creds,
|
||||
account_meta={"name": companie_raw, "email": email_raw, "cui": cui_raw},
|
||||
date_firma_eroare="Email-ul de contact este obligatoriu.",
|
||||
)
|
||||
|
||||
# Normalizare si validare CUI
|
||||
try:
|
||||
cui_norm = _norm_cui(cui_raw)
|
||||
except ValueError as exc:
|
||||
conn = get_connection()
|
||||
try:
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone()
|
||||
are_creds = bool(row_cr and row_cr["rar_creds_enc"])
|
||||
finally:
|
||||
conn.close()
|
||||
return _render_cont(
|
||||
request,
|
||||
are_creds=are_creds,
|
||||
account_meta={"name": companie_raw, "email": email_norm, "cui": cui_raw},
|
||||
date_firma_eroare=f"CUI invalid: {exc}",
|
||||
)
|
||||
|
||||
if not cui_norm:
|
||||
conn = get_connection()
|
||||
try:
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone()
|
||||
are_creds = bool(row_cr and row_cr["rar_creds_enc"])
|
||||
finally:
|
||||
conn.close()
|
||||
return _render_cont(
|
||||
request,
|
||||
are_creds=are_creds,
|
||||
account_meta={"name": companie_raw, "email": email_norm, "cui": cui_raw},
|
||||
date_firma_eroare="CUI-ul firmei este obligatoriu.",
|
||||
)
|
||||
|
||||
# Actualizare in DB
|
||||
conn = get_connection()
|
||||
try:
|
||||
try:
|
||||
conn.execute(
|
||||
"UPDATE accounts SET name=?, email=?, cui=? WHERE id=?",
|
||||
(companie_raw, email_norm, cui_norm, acct),
|
||||
)
|
||||
except sqlite3.IntegrityError:
|
||||
# CUI duplicat (index partial unic ux_accounts_cui)
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM accounts WHERE cui=? AND id!=?", (cui_norm, acct)
|
||||
).fetchone()
|
||||
owner = existing["id"] if existing else "?"
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone()
|
||||
are_creds = bool(row_cr and row_cr["rar_creds_enc"])
|
||||
return _render_cont(
|
||||
request,
|
||||
are_creds=are_creds,
|
||||
account_meta={"name": companie_raw, "email": email_norm, "cui": cui_norm},
|
||||
date_firma_eroare=(
|
||||
f"CUI-ul {cui_norm} este deja folosit de alt cont (id={owner}). "
|
||||
"Foloseste un CUI diferit sau contacteaza administratorul."
|
||||
),
|
||||
)
|
||||
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
row_cr = conn.execute("SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)).fetchone()
|
||||
are_creds = bool(row_cr and row_cr["rar_creds_enc"])
|
||||
return _render_cont(
|
||||
request,
|
||||
are_creds=are_creds,
|
||||
account_meta=account_meta,
|
||||
date_firma_mesaj="Datele firmei au fost salvate.",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -2949,12 +3353,14 @@ def cont_rar_creds(
|
||||
"SELECT rar_creds_enc FROM accounts WHERE id=?", (acct,)
|
||||
).fetchone()
|
||||
are_creds = bool(row and row["rar_creds_enc"])
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
finally:
|
||||
conn.close()
|
||||
return _render_cont(
|
||||
request,
|
||||
are_creds=are_creds,
|
||||
creds_eroare="Email si parola sunt obligatorii.",
|
||||
account_meta=account_meta,
|
||||
)
|
||||
|
||||
enc = encrypt_creds({"email": email, "password": parola})
|
||||
@@ -2964,10 +3370,12 @@ def cont_rar_creds(
|
||||
"UPDATE accounts SET rar_creds_enc=? WHERE id=?",
|
||||
(enc, acct),
|
||||
)
|
||||
account_meta = _fetch_account_meta(conn, acct)
|
||||
return _render_cont(
|
||||
request,
|
||||
are_creds=True,
|
||||
creds_mesaj="Credentialele RAR au fost salvate cu succes.",
|
||||
account_meta=account_meta,
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user