feat(5.8): reguli mapare pe text (substring/cont) + UX tabel trimiteri (detaliu inline, fara scroll, cod RAR)

Reguli text per cont (operation_text_rules), resolve_prestatii cu param aditiv
text_rules + precedenta stricta, threadat pe toate cele 6 callsite-uri + valid_codes
+ seam classify_prezentare. UI Mapari: sectiune reguli + preview pre-salvare + overlap
+ telemetrie text_rule_hit. UX tabel: cod_rar sub operatie, pill eticheta scurta, fara
scroll orizontal (scopat .tabel-trimiteri + carduri <768px), detaliu inline expandabil
(a11y + pauza poll). code-review: reparat regula auto_send=0 care trimitea automat la RAR
in loc sa tina randul pentru review. 814 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-24 12:47:37 +00:00
parent c80c79462c
commit 51dc504f1d
28 changed files with 3023 additions and 61 deletions

View File

@@ -30,6 +30,7 @@ from ..web.csrf import get_csrf_token, verify_csrf
from .labels import (
ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
eticheta_rar,
eticheta_scurta,
eticheta_stare,
eticheta_worker,
format_data_rar,
@@ -62,16 +63,23 @@ from ..submissions_admin import (
)
from ..mapping import (
DEFAULT_ACCOUNT_ID,
_emite_text_rule_hits,
account_or_default,
account_scope_clause,
delete_text_rule,
has_no_auto_send,
load_mapping_meta,
load_nomenclator,
load_nomenclator_codes,
load_text_rules,
normalize_for_match,
pending_unmapped,
reresolve_account,
resolve_prestatii,
save_mapping,
save_text_rule,
suggest_codes,
text_rules_overlap,
)
# Campuri canonice cu eticheta umana pentru dropdown mapare coloane (U5)
@@ -228,6 +236,7 @@ def _render_panel_mapari(request: Request, conn, account_id: int) -> str:
"pending": pending_unmapped(conn, account_id),
"saved_mappings": _load_saved_op_mappings(conn, account_id),
"column_formats": _load_column_formats(conn, account_id),
"text_rules": load_text_rules(conn, account_id),
"nomenclator": load_nomenclator(conn),
"message": None,
"csrf_token": get_csrf_token(request),
@@ -646,6 +655,8 @@ def _submission_row_view(r) -> dict:
return {
"id": r["id"],
"status": r["status"],
# PRD 5.8 US-007/US-006: pill = eticheta scurta; textul lung ramane ca tooltip (title=).
"stare_scurt": eticheta_scurta(r["status"]),
"stare_text": eticheta[0],
"stare_css": eticheta[2],
"prez": prezentare_din_payload(r["payload_json"]),
@@ -981,9 +992,14 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
# la RAR. Corectia campurilor de continut nu poate deebloca o operatie nemapata.
mapping_meta = load_mapping_meta(conn, account_id)
mapping = {op: m["cod_prestatie"] for op, m in mapping_meta.items()}
resolved, unmapped = resolve_prestatii(content.get("prestatii"), mapping)
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
# US-010: telemetrie pentru itemii rezolvati prin regula text (calea corectie web).
_emite_text_rule_hits(conn, account_id, row["id"], resolved)
# Canonicalizare (strip ".0" odometru, VIN/nr upper) INAINTE de validare si cheie.
canon = canonicalize_row(content)
content.update({
@@ -1228,6 +1244,7 @@ def _render_mapari(
"pending": pending_unmapped(conn, account_id),
"saved_mappings": _load_saved_op_mappings(conn, account_id),
"column_formats": _load_column_formats(conn, account_id),
"text_rules": load_text_rules(conn, account_id),
"nomenclator": load_nomenclator(conn),
"message": message,
"csrf_token": get_csrf_token(request),
@@ -1342,6 +1359,138 @@ def post_sterge_mapare_salvata(
conn.close()
# =========================================================================== #
# US-004 (5.8) — Reguli automate (text): substring -> cod RAR #
# Adaugare/stergere reguli text scoped pe sesiune; salvarea re-rezolva blocajele.#
# =========================================================================== #
@router.post("/mapari/reguli-text", response_class=HTMLResponse)
def post_salveaza_regula_text(
request: Request,
pattern: str = Form(...),
cod_prestatie: str = Form(...),
csrf_token: str | None = Form(None),
auto_send: bool = Form(False),
) -> HTMLResponse:
"""Salveaza o regula text (substring -> cod RAR) + re-rezolva submission-urile blocate.
Scoped pe contul sesiunii (save_text_rule foloseste account_or_default(sesiune)).
Valideaza cod_prestatie fata de nomenclator INAINTE de save (cod necunoscut ->
respins inline, fara salvare). La succes: mesaj „Regula salvata. Deblocate: N"
+ trigger trimiteriChanged (refresh lista), ca maparea inline (5.7).
"""
account_id = require_login(request)
verify_csrf(request, csrf_token)
conn = get_connection()
try:
pat = (pattern or "").strip()
cod = (cod_prestatie or "").strip().upper()
if not pat or not cod:
return _render_mapari(
request, conn, account_id,
message="Completeaza textul cautat si codul RAR.",
)
valid_codes = load_nomenclator_codes(conn)
if valid_codes and cod not in valid_codes:
return _render_mapari(
request, conn, account_id,
message=f"Cod RAR necunoscut in nomenclator: {cod}.",
)
# US-011: avertisment neblocant daca regula noua se suprapune (substring,
# oricare directie) cu una existenta. Calculam INAINTE de save, fata de
# regulile curente, ca pattern-ul nou sa nu se compare cu sine.
overlap = text_rules_overlap(pat, load_text_rules(conn, account_id))
save_text_rule(conn, account_id, pat, cod, auto_send)
stats = reresolve_account(conn, account_id)
msg = f"Regula salvata. Deblocate: {stats['requeued']}."
if overlap:
parti = "; ".join(
f"«{r.get('pattern')}» -> {r.get('cod_prestatie')}" for r in overlap
)
msg += (
f" Se suprapune cu regula {parti}; "
"ordinea (priority, id) decide care se aplica prima."
)
resp = _render_mapari(request, conn, account_id, message=msg)
resp.headers["HX-Trigger"] = "trimiteriChanged"
return resp
finally:
conn.close()
@router.post("/mapari/reguli-text/sterge", response_class=HTMLResponse)
def post_sterge_regula_text(
request: Request,
pattern: str = Form(...),
csrf_token: str | None = Form(None),
) -> HTMLResponse:
"""Sterge o regula text. Scoped pe contul sesiunii."""
account_id = require_login(request)
verify_csrf(request, csrf_token)
conn = get_connection()
try:
pat = (pattern or "").strip()
delete_text_rule(conn, account_id, pat)
return _render_mapari(
request, conn, account_id,
message=f"Regula stearsa: «{pat}».",
)
finally:
conn.close()
@router.post("/mapari/reguli-text/preview", response_class=HTMLResponse)
def post_preview_regula_text(
request: Request,
pattern: str = Form(""),
csrf_token: str | None = Form(None),
) -> HTMLResponse:
"""Preview pre-salvare (US-009): cate operatii nemapate ar potrivi regula.
NU salveaza nimic (zero scriere DB). Normalizeaza pattern-ul cu
normalize_for_match, numara operatiile DISTINCTE nemapate ale contului
(needs_mapping, reuse pending_unmapped) al caror text (denumire sau
cod_op_service, normalizat) CONTINE pattern-ul + intoarce pana la 3 exemple.
Pattern gol -> fragment gol (nu numara „tot"). Scoped pe contul sesiunii.
"""
from markupsafe import escape
account_id = require_login(request)
verify_csrf(request, csrf_token)
pat = normalize_for_match(pattern)
if not pat:
return HTMLResponse(content="")
conn = get_connection()
try:
pending = pending_unmapped(conn, account_id)
finally:
conn.close()
matches = []
for entry in pending:
text = normalize_for_match(entry.get("denumire") or entry.get("cod_op_service"))
if pat in text:
matches.append(entry)
if not matches:
return HTMLResponse(
content='<span class="muted" style="font-size:12px;">Nicio potrivire acum.</span>'
)
exemple = ", ".join(
f"«{escape((e.get('denumire') or e.get('cod_op_service') or '').strip())}»"
for e in matches[:3]
)
n = len(matches)
plural = "operatie nemapata" if n == 1 else "operatii nemapate"
return HTMLResponse(
content=(
f'<span class="muted" style="font-size:12px;">'
f'Potriveste {n} {plural}: {exemple}.</span>'
)
)
# =========================================================================== #
# US-006 — Formate de coloane salvate: editare format data + stergere #
# CRUD pe column_mappings scoped pe sesiune (prin id, verificat pe account). #
@@ -1510,6 +1659,11 @@ def _web_compute_preview(
# Mapare operatii (o singura incarcare — Eng#5)
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# US-010/US-003 (paritate): preview-ul web trebuie sa aplice ACELEASI reguli text +
# validare nomenclator ca si commit-ul (2426), altfel un rand rezolvabil doar prin
# regula text ar fi marcat needs_mapping si exclus din commit. Incarcate o data.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
# Detectie coercion flags din valorile stocate (VIN numeric)
coercion_flags_map: dict[int, list[str]] = {}
@@ -1553,6 +1707,8 @@ def _web_compute_preview(
mapping_meta=mapping_meta,
formula_columns=formula_columns,
override=overrides[i] or None,
valid_codes=valid_codes,
text_rules=text_rules,
)
key: str | None = None
@@ -2237,6 +2393,9 @@ async def web_confirma_import(
# Mapare operatii
mapping_meta = load_mapping_meta(conn, acct)
mapping_ops = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# T2: validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
# Enqueue in tranzactie explicita (Issue 6) — INSERT ON CONFLICT DO NOTHING (TOCTOU)
enqueued: list[dict] = []
@@ -2275,7 +2434,7 @@ async def web_confirma_import(
# Rezolva prestatii
prestatii = mapped.get("prestatii") or []
resolved_p, _ = resolve_prestatii(prestatii, mapping_ops)
resolved_p, _ = resolve_prestatii(prestatii, mapping_ops, valid_codes, text_rules)
mapped["prestatii"] = resolved_p
# Canonicalizare
@@ -2319,6 +2478,8 @@ async def web_confirma_import(
if cur.rowcount == 0:
toctou.append(row_index)
else:
# US-010: telemetrie pentru itemii rezolvati prin regula text.
_emite_text_rule_hits(conn, acct, int(cur.lastrowid), resolved_p)
enqueued.append({"submission_id": cur.lastrowid, "row_index": row_index})
conn.execute("COMMIT")