feat(5.10): UX trimiteri (pill filtre, paginare, editare) + Mapari in meniu + branding ROMFAST
14 stories TDD prin echipa de workeri (lead orchestreaza, 3 teammates pe valuri cu fisiere disjuncte; routes.py + base.html serializate ca fisiere fierbinti). - US-001 fix filtrare data (_iso_date_prefix pe garda+comparatie, prinde timestamp cu ora) - US-002/007 operatie service distincta in payload_view + afisare in detaliu - US-003 pill-uri categorii (button/aria-pressed; needs_mapping --warn, needs_data/error --err); fara lista ID-uri/dropdown - US-004 paginare numerotata 25/pag (total ramificat SQL-COUNT vs fetch-all+slice, clamp page, poll pastreaza pagina) - US-005 VIN block-level sub nr - US-006/006b editare cod RAR + validare nomenclator + recalcul idempotency (needs_data/needs_mapping via /corecteaza, error via /repune) - US-008 card eroare 3-niveluri doar pe read-only + rezumat top-of-form - US-009 Mapari in meniu hamburger; scoatere tab-bar + role=tablist orfan - US-010/011 pagina Mapari consolidata + butoane icon SVG + dirty-state (fara kebab/emoji) - US-012/012b header centrat + logo ROMFAST (/static/romfast_logo.png) in header - US-013 paleta azur ROMFAST (#2E74D6/#1F66C9) + IBM Plex Sans/Mono self-host (woff2 reale) - US-014 selector tema ciclic Light/Dark/Petrol/Auto + anti-FOUC pe 4 stari Backend trimitere (worker/masina stari/idempotenta/mapping) + schema NEATINSE (UI/UX pur + 1 fix de filtrare). VERIFY context curat PASS; /code-review high: 1 finding material reparat (US-006b). Regresie 896 passed, 1 skipped, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
@@ -555,43 +556,26 @@ def _blocate_defalcat(counts: dict[str, int]) -> list[tuple]:
|
||||
return rezultat
|
||||
|
||||
|
||||
# Cate randuri blocate identificam nominal sub fiecare categorie din banner (US-014).
|
||||
_BLOCATE_SAMPLE = 3
|
||||
def _pills_categorii(counts: dict[str, int]) -> list[dict]:
|
||||
"""Pill-uri pentru starile cu problema (US-003 PRD 5.10).
|
||||
|
||||
|
||||
def _blocate_actionabil(conn, account_id: int) -> list[dict]:
|
||||
"""Categorii blocate cu identificatorii primelor randuri + deep-link (US-014).
|
||||
|
||||
Pentru fiecare stare blocata cu n>0: eticheta umana, contorul, primii N identificatori
|
||||
(VIN partial + nr inmatriculare + #id — PII doar partial, ca jurnalul) si cati raman.
|
||||
Scoped pe cont (regula NULL->1). Lista goala -> banner-ul nu se randeaza (se stinge).
|
||||
Inlocuieste _blocate_actionabil (care incarca PII/VIN per rand).
|
||||
Reutilizeaza contoarele deja calculate din _status_counts.
|
||||
Returneza lista goala daca nu exista nicio stare blocata.
|
||||
"""
|
||||
from ..security import vin_partial
|
||||
scope_sql, scope_params = account_scope_clause(account_id)
|
||||
out: list[dict] = []
|
||||
for status in ("needs_mapping", "needs_data", "error"):
|
||||
rows = conn.execute(
|
||||
f"SELECT id, payload_json FROM submissions WHERE {scope_sql} AND status=? ORDER BY id DESC",
|
||||
scope_params + [status],
|
||||
).fetchall()
|
||||
if not rows:
|
||||
continue
|
||||
sample = []
|
||||
for r in rows[:_BLOCATE_SAMPLE]:
|
||||
prez = prezentare_din_payload(r["payload_json"])
|
||||
sample.append({
|
||||
"id": r["id"],
|
||||
"vin": vin_partial(prez.get("vin") or ""),
|
||||
"nr": prez.get("vehicul_nr") or "",
|
||||
})
|
||||
out.append({
|
||||
"status": status,
|
||||
"eticheta": eticheta_stare(status),
|
||||
"n": len(rows),
|
||||
"randuri": sample,
|
||||
"rest": max(0, len(rows) - len(sample)),
|
||||
})
|
||||
return out
|
||||
# DESIGN.md §Componente: Lipsa cod = --warn (chihlimbar), celelalte categorii = --err (rosu).
|
||||
# Culoarea e CSS variable name (nu clasa), injectata direct in style tag al pill-ului,
|
||||
# pentru ca s-needs_mapping in base.html e tot --err (incorect pentru pill).
|
||||
PILL_DEFS = [
|
||||
("needs_mapping", "Lipsa cod", "--warn"),
|
||||
("needs_data", "Date incomplete", "--err"),
|
||||
("error", "Eroare", "--err"),
|
||||
]
|
||||
return [
|
||||
{"status": status, "label": label, "color_var": color_var, "n": counts.get(status, 0)}
|
||||
for status, label, color_var in PILL_DEFS
|
||||
if counts.get(status, 0) > 0
|
||||
]
|
||||
|
||||
|
||||
@router.get("/_fragments/status", response_class=HTMLResponse)
|
||||
@@ -630,23 +614,31 @@ def fragment_status(request: Request) -> HTMLResponse:
|
||||
"counts_sent": counts.get("sent", 0),
|
||||
"blocate_total": blocate_total,
|
||||
"blocate_defalcat": _blocate_defalcat(counts),
|
||||
"blocate_actionabil": _blocate_actionabil(conn, account_id),
|
||||
"pills_categorii": _pills_categorii(counts),
|
||||
"account_active": _account_active(conn, account_id),
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _is_iso_date(value: object) -> bool:
|
||||
"""True daca `value` e o data ISO YYYY-MM-DD (comparabila lexicografic corect)."""
|
||||
def _iso_date_prefix(value: object) -> str | None:
|
||||
"""Intoarce primele 10 caractere (YYYY-MM-DD) daca incep cu o data ISO valida, altfel None.
|
||||
|
||||
Permite filtrarea dupa data_prestatie chiar daca valoarea contine ora/minut/secunda
|
||||
(ex. '2026-06-20 14:35:07' sau '2026-06-20T14:35:07') — extrage portiunea de data
|
||||
fara a exclude timestamp-urile (bug-ul fix US-001: _is_iso_date cerea len==10).
|
||||
Valori care nu incep cu o data ISO valida (ex. '05.12.2024') intorc None si
|
||||
sunt excluse din filtru — comportament actual pastrat.
|
||||
"""
|
||||
s = str(value or "").strip()
|
||||
if len(s) != 10:
|
||||
return False
|
||||
if len(s) < 10:
|
||||
return None
|
||||
prefix = s[:10]
|
||||
try:
|
||||
datetime.strptime(s, "%Y-%m-%d")
|
||||
return True
|
||||
datetime.strptime(prefix, "%Y-%m-%d")
|
||||
return prefix
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
# Stari care semnaleaza o problema ce necesita atentia operatorului. Eticheta umana
|
||||
@@ -693,6 +685,9 @@ def _submission_row_view(r) -> dict:
|
||||
}
|
||||
|
||||
|
||||
_PAGE_SIZE = 25 # Marime pagina fixa (US-004 PRD 5.10)
|
||||
|
||||
|
||||
@router.get("/_fragments/submissions", response_class=HTMLResponse)
|
||||
def fragment_submissions(
|
||||
request: Request,
|
||||
@@ -700,12 +695,14 @@ def fragment_submissions(
|
||||
vehicul: str | None = None,
|
||||
data_de: str | None = None,
|
||||
data_pana: str | None = None,
|
||||
page: int = 1,
|
||||
) -> HTMLResponse:
|
||||
"""Tabel Trimiteri, scoped pe cont, cu filtre optionale (US-009).
|
||||
"""Tabel Trimiteri, scoped pe cont, cu filtre optionale si paginare (US-009, US-004).
|
||||
|
||||
Filtrarea pe stare se face in SQL (foloseste idx_submissions_account_status);
|
||||
filtrarea pe vehicul (nr/VIN, case-insensitive) si pe interval data_prestatie
|
||||
se face dupa parsarea payload_json in Python (plafon perf notat — eng review).
|
||||
US-004 H1: totalul se calculeaza DIFERIT dupa tipul de filtru:
|
||||
- FARA filtru Python (status-only / niciun filtru): SQL COUNT(*) + LIMIT/OFFSET
|
||||
- CU filtru vehicul/data activ: fetch-all -> filtreaza Python -> total=len -> slice
|
||||
SQL COUNT/LIMIT pe calea cu filtru Python ar da total gresit (taie inainte de filtru).
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
status = (status or "").strip() or None
|
||||
@@ -713,6 +710,9 @@ def fragment_submissions(
|
||||
data_de = (data_de or "").strip() or None
|
||||
data_pana = (data_pana or "").strip() or None
|
||||
filtru_activ = bool(status or vehicul_q or data_de or data_pana)
|
||||
filtru_python = bool(vehicul_q or data_de or data_pana) # filtru care necesita Python
|
||||
|
||||
page = max(1, page) # pre-clamp >= 1
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
@@ -722,45 +722,79 @@ def fragment_submissions(
|
||||
if status:
|
||||
where.append("status=?")
|
||||
params.append(status)
|
||||
# Filtrarea pe vehicul/data se face in Python (dupa parsarea payload). Daca am
|
||||
# taia la LIMIT inainte de filtru, am rata silentios randuri mai vechi care
|
||||
# potrivesc. Cand un filtru text/data e activ, scoatem LIMIT-ul din SQL si plafonam
|
||||
# afisarea dupa filtrare (OK la scara actuala — plafon perf notat, eng review).
|
||||
limit_sql = "" if (vehicul_q or data_de or data_pana) else " LIMIT 200"
|
||||
rows = conn.execute(
|
||||
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
|
||||
"updated_at, payload_json FROM submissions "
|
||||
f"WHERE {' AND '.join(where)} ORDER BY id DESC{limit_sql}",
|
||||
params,
|
||||
).fetchall()
|
||||
where_sql = " AND ".join(where)
|
||||
|
||||
view = []
|
||||
for r in rows:
|
||||
v = _submission_row_view(r)
|
||||
prez = v["prez"]
|
||||
if vehicul_q:
|
||||
hay = f"{prez['vehicul_nr']} {prez['vin']}".upper()
|
||||
if vehicul_q not in hay:
|
||||
continue
|
||||
if data_de or data_pana:
|
||||
d = prez["data_prestatie"]
|
||||
# Comparam doar date in format ISO (YYYY-MM-DD); altfel comparatia de string
|
||||
# ar fi gresita (ex. "05.12.2024"). Valori ne-ISO sunt excluse din filtru.
|
||||
if not _is_iso_date(d):
|
||||
continue
|
||||
if data_de and d < data_de:
|
||||
continue
|
||||
if data_pana and d > data_pana:
|
||||
continue
|
||||
view.append(v)
|
||||
if len(view) >= 200:
|
||||
break
|
||||
if filtru_python:
|
||||
# Calea B: fetch-all, filtreaza in Python, slice (US-004 H1)
|
||||
# FARA LIMIT — altfel paginile >8 ar disparea silentios (bug PRD H1)
|
||||
rows_db = conn.execute(
|
||||
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
|
||||
f"updated_at, payload_json FROM submissions WHERE {where_sql} ORDER BY id DESC",
|
||||
params,
|
||||
).fetchall()
|
||||
|
||||
view_all: list[dict] = []
|
||||
for r in rows_db:
|
||||
v = _submission_row_view(r)
|
||||
prez = v["prez"]
|
||||
if vehicul_q:
|
||||
hay = f"{prez['vehicul_nr']} {prez['vin']}".upper()
|
||||
if vehicul_q not in hay:
|
||||
continue
|
||||
if data_de or data_pana:
|
||||
# Extragem portiunea YYYY-MM-DD (US-001 fix).
|
||||
d_prefix = _iso_date_prefix(prez["data_prestatie"])
|
||||
if d_prefix is None:
|
||||
continue
|
||||
if data_de and d_prefix < data_de:
|
||||
continue
|
||||
if data_pana and d_prefix > data_pana:
|
||||
continue
|
||||
view_all.append(v)
|
||||
|
||||
total = len(view_all)
|
||||
pages = max(1, math.ceil(total / _PAGE_SIZE)) if total > 0 else 1
|
||||
page = max(1, min(page, pages)) # clamp H2
|
||||
offset = (page - 1) * _PAGE_SIZE
|
||||
view = view_all[offset:offset + _PAGE_SIZE]
|
||||
|
||||
else:
|
||||
# Calea A: SQL COUNT(*) + LIMIT/OFFSET (eficient, fara filtru Python activ)
|
||||
total = conn.execute(
|
||||
f"SELECT COUNT(*) FROM submissions WHERE {where_sql}", params
|
||||
).fetchone()[0]
|
||||
|
||||
pages = max(1, math.ceil(total / _PAGE_SIZE)) if total > 0 else 1
|
||||
page = max(1, min(page, pages)) # clamp H2
|
||||
offset = (page - 1) * _PAGE_SIZE
|
||||
|
||||
rows_db = conn.execute(
|
||||
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
|
||||
f"updated_at, payload_json FROM submissions WHERE {where_sql} ORDER BY id DESC "
|
||||
"LIMIT ? OFFSET ?",
|
||||
params + [_PAGE_SIZE, offset],
|
||||
).fetchall()
|
||||
view = [_submission_row_view(r) for r in rows_db]
|
||||
|
||||
page_start = (page - 1) * _PAGE_SIZE + 1 if total > 0 else 0
|
||||
page_end = min(page * _PAGE_SIZE, total)
|
||||
|
||||
return templates.TemplateResponse("_submissions.html", {
|
||||
"request": request,
|
||||
"rows": view,
|
||||
"filtru_activ": filtru_activ,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
# Paginare (US-004)
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pages": pages,
|
||||
"page_start": page_start,
|
||||
"page_end": page_end,
|
||||
# Filtre curente pentru linkurile de paginare (pastreaza filtrele, H2)
|
||||
"f_status": status or "",
|
||||
"f_vehicul": vehicul_q or "",
|
||||
"f_data_de": data_de or "",
|
||||
"f_data_pana": data_pana or "",
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -768,6 +802,9 @@ def fragment_submissions(
|
||||
|
||||
# Stari ne-trimise blocate pe care le putem corecta inline (US-010).
|
||||
_CORECTABILE = ("needs_data", "needs_mapping")
|
||||
# US-006b: stari cu select editabil cod_prestatie (superset al _CORECTABILE: error
|
||||
# primeste select in formularul /repune, nu in /corecteaza — fara schimbare de vehicle fields).
|
||||
_EDITABILE_OP = ("needs_data", "needs_mapping", "error")
|
||||
# Stari gestionabile prin lifecycle web (US-011): sterge / re-pune in coada.
|
||||
_GESTIONABILE_WEB = ("error", "needs_data", "needs_mapping")
|
||||
|
||||
@@ -851,12 +888,31 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
|
||||
eticheta = eticheta_stare(row["status"])
|
||||
nemapate_inline: list[dict] = []
|
||||
nomenclator: list[dict] = []
|
||||
# Variabila interna: nomenclatorul complet (incarcat pentru needs_mapping, refolosit pt US-006)
|
||||
_nomenclator_complet: list[dict] = []
|
||||
if conn is not None and row["status"] == "needs_mapping":
|
||||
# Un singur SELECT pe nomenclator: il refolosim si pentru sugestii si pentru dropdown.
|
||||
nomenclator = load_nomenclator(conn)
|
||||
nemapate_inline = _nemapate_pentru_submission(row, nomenclator)
|
||||
if not nemapate_inline:
|
||||
nomenclator = [] # nu expunem dropdown-ul cand nu exista operatii de mapat
|
||||
_nomenclator_complet = load_nomenclator(conn)
|
||||
nemapate_inline = _nemapate_pentru_submission(row, _nomenclator_complet)
|
||||
nomenclator = _nomenclator_complet if nemapate_inline else []
|
||||
|
||||
# US-006/US-006b: nomenclator pentru selectul cod_prestatie — needs_data/needs_mapping (in
|
||||
# formularul /corecteaza) + error (in formularul /repune). Refoloseste _nomenclator_complet
|
||||
# daca e deja incarcat (needs_mapping), altfel incarca fresh.
|
||||
nomenclator_rar: list[dict] = []
|
||||
if conn is not None and row["status"] in _EDITABILE_OP:
|
||||
nomenclator_rar = _nomenclator_complet if _nomenclator_complet else load_nomenclator(conn)
|
||||
|
||||
# US-006: cod_prestatie curent din prima prestatie (pentru pre-selectare in select)
|
||||
cod_prestatie_curent = ""
|
||||
try:
|
||||
_pd = json.loads(row["payload_json"] or "{}")
|
||||
_prestatii = (_pd.get("prestatii") or []) if isinstance(_pd, dict) else []
|
||||
if _prestatii and isinstance(_prestatii[0], dict):
|
||||
cod_prestatie_curent = (_prestatii[0].get("cod_prestatie") or "").strip().upper()
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
ctx = {
|
||||
"request": request,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
@@ -882,6 +938,9 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
|
||||
# PRD 5.7: mapare inline (operatii nemapate ale acestui rand + nomenclator)
|
||||
"nemapate_inline": nemapate_inline,
|
||||
"nomenclator": nomenclator,
|
||||
# US-006: select cod_prestatie pentru stari editabile
|
||||
"nomenclator_rar": nomenclator_rar,
|
||||
"cod_prestatie_curent": cod_prestatie_curent,
|
||||
"corectie_msg": message,
|
||||
"corectie_error": error,
|
||||
"corectie_errors": corectie_errors or [],
|
||||
@@ -1011,6 +1070,31 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
|
||||
if isinstance(val, str) and val.strip() != "":
|
||||
content[camp] = val.strip()
|
||||
|
||||
# US-006: injectare cod_prestatie din form INAINTE de resolve_prestatii.
|
||||
# Oglindeste validarea din post_mapeaza_inline (nomenclator check). Codul nou
|
||||
# e injectat in prima prestatie (index 0); build_key il include in hash (CLAUDE.md
|
||||
# invariant "build_key hashuieste cod_prestatie, idempotency.py:34").
|
||||
_cod_raw = form.get("cod_prestatie")
|
||||
cod_prestatie_form = (_cod_raw.strip().upper() if isinstance(_cod_raw, str) else "")
|
||||
if cod_prestatie_form:
|
||||
exists_nom = conn.execute(
|
||||
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod_prestatie_form,)
|
||||
).fetchone()
|
||||
if not exists_nom:
|
||||
return templates.TemplateResponse(
|
||||
"_trimitere_detaliu.html",
|
||||
_detaliu_ctx(
|
||||
request, row, conn=conn, account_id=account_id, error=True,
|
||||
message=f"Cod RAR necunoscut in nomenclator: {cod_prestatie_form}. "
|
||||
"Alege un cod valid din lista.",
|
||||
),
|
||||
)
|
||||
prestatii_form = content.get("prestatii")
|
||||
if isinstance(prestatii_form, list) and prestatii_form:
|
||||
p0 = dict(prestatii_form[0])
|
||||
p0["cod_prestatie"] = cod_prestatie_form
|
||||
content["prestatii"] = [p0] + list(prestatii_form[1:])
|
||||
|
||||
# Re-rezolva prestatiile cu maparea curenta (ca reresolve_account): NU re-pune
|
||||
# niciodata in coada un cod nemapat (codPrestatie null) — FINALIZATA e ireversibil
|
||||
# la RAR. Corectia campurilor de continut nu poate deebloca o operatie nemapata.
|
||||
@@ -1133,14 +1217,114 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
|
||||
async def post_repune_trimitere(request: Request, submission_id: int) -> HTMLResponse:
|
||||
"""Re-pune in coada un rand blocat (error/needs_data/needs_mapping) din dashboard.
|
||||
|
||||
Scoped pe sesiune (404 cross-account/inexistent, 409 sent/sending). Re-randeaza
|
||||
panoul de detaliu cu starea noua + nudge `trimiteriChanged` pentru lista.
|
||||
US-006b: daca randul e in starea `error` si formularul contine `cod_prestatie`,
|
||||
actualizeaza codul in payload, recalculeaza cheia de idempotency si re-pun in coada
|
||||
direct (fara `requeue_submission`, care nu actualizeaza cheia). Scoped pe sesiune
|
||||
(404 cross-account/inexistent, 409 sent/sending). Re-randeaza panoul de detaliu cu
|
||||
starea noua + nudge `trimiteriChanged` pentru lista.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
form = await request.form()
|
||||
verify_csrf(request, str(form.get("csrf_token") or ""))
|
||||
conn = get_connection()
|
||||
try:
|
||||
# US-006b: prelucrare cod_prestatie pentru starea error (inaintea requeue_submission
|
||||
# standard, care nu actualizeaza cheia de idempotency).
|
||||
_cod_raw = form.get("cod_prestatie")
|
||||
cod_prestatie_form = (_cod_raw.strip().upper() if isinstance(_cod_raw, str) else "")
|
||||
|
||||
if cod_prestatie_form:
|
||||
row = _fetch_submission_scoped(conn, account_id, submission_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="trimitere inexistenta")
|
||||
if row["status"] != "error":
|
||||
# cod_prestatie acceptat DOAR pentru starea error prin /repune
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="modificarea cod_prestatie prin repune e valida doar pentru starea error",
|
||||
)
|
||||
# Valideaza cod-ul fata de nomenclator
|
||||
exists_nom = conn.execute(
|
||||
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod_prestatie_form,)
|
||||
).fetchone()
|
||||
if not exists_nom:
|
||||
return templates.TemplateResponse(
|
||||
"_trimitere_detaliu.html",
|
||||
_detaliu_ctx(
|
||||
request, row, conn=conn, account_id=account_id,
|
||||
error=True,
|
||||
message=f"Cod RAR necunoscut: {cod_prestatie_form}. Alege un cod valid.",
|
||||
),
|
||||
)
|
||||
# Parseaza payload si injecteaza cod_prestatie
|
||||
try:
|
||||
content = json.loads(row["payload_json"]) if row["payload_json"] else {}
|
||||
if not isinstance(content, dict):
|
||||
content = {}
|
||||
except (ValueError, TypeError):
|
||||
content = {}
|
||||
prestatii = content.get("prestatii") or []
|
||||
if isinstance(prestatii, list) and prestatii:
|
||||
p0 = dict(prestatii[0])
|
||||
p0["cod_prestatie"] = cod_prestatie_form
|
||||
# sterge cod_op_service/denumire daca exista (codul direct preia prioritate)
|
||||
p0.pop("cod_op_service", None)
|
||||
content["prestatii"] = [p0] + list(prestatii[1:])
|
||||
# Re-rezolva prestatii cu noul cod
|
||||
mapping_meta = load_mapping_meta(conn, account_id)
|
||||
mapping = {op: m["cod_prestatie"] for op, m in mapping_meta.items()}
|
||||
valid_codes = load_nomenclator_codes(conn) or None
|
||||
text_rules = load_text_rules(conn, account_id)
|
||||
resolved, _unmapped = resolve_prestatii(content.get("prestatii"), mapping, valid_codes, text_rules)
|
||||
content["prestatii"] = resolved
|
||||
# Canonicalize + rebuild idempotency key
|
||||
canon = canonicalize_row(content)
|
||||
payload_json = json.dumps(content, ensure_ascii=False)
|
||||
new_key = build_key(account_id, canon)
|
||||
# Verifica coliziune (numai daca cheia s-a schimbat)
|
||||
if new_key != row["idempotency_key"]:
|
||||
dup = conn.execute(
|
||||
"SELECT id FROM submissions WHERE idempotency_key=? AND id<>?",
|
||||
(new_key, row["id"]),
|
||||
).fetchone()
|
||||
if dup:
|
||||
return templates.TemplateResponse(
|
||||
"_trimitere_detaliu.html",
|
||||
_detaliu_ctx(
|
||||
request, row, conn=conn, account_id=account_id,
|
||||
error=True,
|
||||
message=f"Exista deja o trimitere identica (rand #{dup['id']}).",
|
||||
),
|
||||
)
|
||||
try:
|
||||
conn.execute(
|
||||
"UPDATE submissions SET idempotency_key=?, status='queued', payload_json=?, "
|
||||
"rar_error=NULL, retry_count=0, next_attempt_at=datetime('now'), "
|
||||
"updated_at=datetime('now') WHERE id=? AND account_id=?",
|
||||
(new_key, payload_json, row["id"], account_id),
|
||||
)
|
||||
conn.commit()
|
||||
except sqlite3.IntegrityError:
|
||||
return templates.TemplateResponse(
|
||||
"_trimitere_detaliu.html",
|
||||
_detaliu_ctx(
|
||||
request, row, conn=conn, account_id=account_id,
|
||||
error=True,
|
||||
message="Exista deja o trimitere identica. Operatia a fost oprita.",
|
||||
),
|
||||
)
|
||||
row2 = _fetch_submission_scoped(conn, account_id, submission_id)
|
||||
resp = templates.TemplateResponse(
|
||||
"_trimitere_detaliu.html",
|
||||
_detaliu_ctx(
|
||||
request, row2, conn=conn, account_id=account_id,
|
||||
message="Cod actualizat — randul a fost re-pus in coada.",
|
||||
),
|
||||
)
|
||||
resp.headers["HX-Trigger"] = "trimiteriChanged"
|
||||
return resp
|
||||
|
||||
# Cale normala: fara cod_prestatie → delega la requeue_submission
|
||||
try:
|
||||
requeue_submission(conn, account_id, submission_id)
|
||||
except SubmissionNotFound:
|
||||
|
||||
Reference in New Issue
Block a user