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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user