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:
Claude Agent
2026-06-27 18:52:20 +00:00
parent 283299ff20
commit b26dbb79e1
44 changed files with 4852 additions and 305 deletions

View File

@@ -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()