feat(editor): editor prestatii unificat in modal - un select, un rand de chips (PRD 5.23)

Inlocuieste cele 3 controale de asociere cod RAR cu un singur rand de chips
+ un singur select care adauga instant la change. Stare = un hidden chips_state
JSON versionat; post_form_chips redus la 2 actiuni (add/remove). Optgroup
Sugestii (fuzzy/k-NN) + optiune "Nu se declara la RAR" in select, cu tinta
implicita evidentiata si placeholder care o numeste. exclus persistat prin
payload_json (treapta noua de precedenta in resolve_prestatii, round-trip complet).

Siguranta: itemii exclusi sunt scosi din payload la momentul trimiterii
(worker split_prestatii_excluse inainte de build_rar_payload + filtru defensiv),
ca sa nu ajunga NICIODATA la RAR ca codPrestatie:null.

Suita: 1680 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-07-08 15:45:34 +00:00
parent 0bc618763e
commit 56ebe1f7cb
20 changed files with 2839 additions and 552 deletions

View File

@@ -233,8 +233,10 @@ def _resolve_row_for_preview(
"flags": all_flags,
}
# Operatii excluse de la declarare: ies din prestatiile trimise (si din cheia
# de idempotenta). Toate excluse -> rand 'excluded' (nu se trimite, nu blocheaza).
# Operatii excluse de la declarare: ies DOAR din payload-ul trimis la RAR (si din
# cheia de idempotenta), NU din `mapped["prestatii"]` persistat/afisat — editorul
# (chips modal) trebuie sa arate chip-ul Nedeclarat la redeschidere (round-trip).
# Toate excluse -> rand 'excluded' (nu se trimite, nu blocheaza).
declarabile, excluse = split_prestatii_excluse(resolved)
if excluse and not declarabile:
return {
@@ -243,11 +245,10 @@ def _resolve_row_for_preview(
"errors": [],
"flags": all_flags,
}
if excluse:
mapped["prestatii"] = declarabile
# Validare continut
errors = validate_prezentare(mapped)
# Validare continut: doar partea declarabila conteaza (validate_prezentare
# ignora oricum itemii fara cod_prestatie, dar `mapped` ramane intact pentru afisare).
errors = validate_prezentare({**mapped, "prestatii": declarabile} if excluse else mapped)
if all_flags and not reviewed:
# needs_review: validarea a trecut, dar flagurile (date ambigue, formule) cer confirmare manuala.
@@ -852,7 +853,15 @@ def preview_import(
key = None
if resolved_info["resolved_status"] in ("ok", "needs_review", "needs_data"):
try:
key = _build_idempotency_key(account_id, resolved_info["resolved"], preview_env)
# Cheia foloseste DOAR partea declarabila (itemii exclusi nu intra in
# hash) — identic cu cheia calculata la commit (linia cu
# split_prestatii_excluse din commit_import), altfel preview-ul
# arata alt duplicat decat cel real.
_key_resolved = resolved_info["resolved"]
_decl_key, _exc_key = split_prestatii_excluse(_key_resolved.get("prestatii"))
if _exc_key:
_key_resolved = {**_key_resolved, "prestatii": _decl_key}
key = _build_idempotency_key(account_id, _key_resolved, preview_env)
keys_for_lookup.append(key)
if key not in key_to_index:
key_to_index[key] = []
@@ -1204,16 +1213,19 @@ def commit_import(
# Rezolva prestatii INAINTE de canonicalizare (altfel cheia difera de cea din preview)
prestatii = mapped.get("prestatii") or []
resolved, _ = resolve_prestatii(prestatii, mapping, valid_codes, text_rules, excluded_ops)
# Prestatiile excluse de la declarare NU pleaca la RAR (ca in preview);
# Prestatiile excluse de la declarare NU pleaca la RAR si nu intra in
# cheia de idempotenta, dar RAMAN in payload_json persistat (round-trip
# in editor, ca in preview) — ingustarea la declarabile se face doar
# local, pentru canon/cheie, si la worker, la momentul trimiterii.
declarabile, _excluse = split_prestatii_excluse(resolved)
# rand fara nicio prestatie declarabila = 'excluded' in preview -> defensiv skip.
resolved, _excluse = split_prestatii_excluse(resolved)
if not resolved:
if not declarabile:
continue
mapped["prestatii"] = resolved
# Canonicalizare (dupa rezolvare prestatii -> cod_prestatie inclus in cheie;
# cheia IDENTICA cu preview: _build_idempotency_key = canonicalize_row + build_key).
canon = canonicalize_row(mapped)
canon = canonicalize_row({**mapped, "prestatii": declarabile})
mapped.update({
"vin": canon["vin"],
"nr_inmatriculare": canon["nr_inmatriculare"],
@@ -1223,13 +1235,13 @@ def commit_import(
# Cheia de idempotenta (identica cu cheia din preview — aceeasi ordine + env)
key = build_key(account_id, canon, env)
# Hash row pentru atestare (valori rezolvate)
# Hash row pentru atestare (valori rezolvate, doar partea declarabila)
rows_for_hash.append(json.dumps({
"row_index": row_index,
"vin": mapped.get("vin"),
"data_prestatie": mapped.get("data_prestatie"),
"odometru_final": mapped.get("odometru_final"),
"prestatii": [str(p.get("cod_prestatie") or p.get("cod_op_service") or "") for p in resolved],
"prestatii": [str(p.get("cod_prestatie") or p.get("cod_op_service") or "") for p in declarabile],
}, sort_keys=True, ensure_ascii=False))
payload_json = json.dumps(mapped, ensure_ascii=False)