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:
@@ -120,6 +120,7 @@ templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent / "tem
|
||||
# Expune parse_erori si eticheta_env in toate template-urile
|
||||
templates.env.globals["parse_erori"] = parse_erori
|
||||
templates.env.globals["eticheta_env"] = eticheta_env
|
||||
templates.env.globals["EXCLUDE_SENTINEL"] = EXCLUDE_SENTINEL
|
||||
|
||||
|
||||
def _mediu_instanta() -> str:
|
||||
@@ -1336,8 +1337,10 @@ def _payload_form_values(payload_json) -> dict:
|
||||
def _prestatii_chips_from_payload(payload_json) -> list[dict]:
|
||||
"""Extrage lista de chips prestatii din payload_json pentru _form_editare.html.
|
||||
|
||||
Returneaza lista de dicts {cod_prestatie, cod_op_service, denumire}.
|
||||
Returneaza lista de dicts {cod_prestatie, cod_op_service, denumire, exclus}.
|
||||
Itemele fara cod_prestatie (operatii nemapate) sunt incluse cu cod_prestatie=''.
|
||||
`exclus` propaga adnotarea din payload (chip Nedeclarat la redeschidere);
|
||||
default 0 pentru payload-uri vechi fara camp.
|
||||
"""
|
||||
try:
|
||||
data = json.loads(payload_json) if payload_json else {}
|
||||
@@ -1353,6 +1356,7 @@ def _prestatii_chips_from_payload(payload_json) -> list[dict]:
|
||||
"cod_prestatie": (item.get("cod_prestatie") or "").strip().upper(),
|
||||
"cod_op_service": (item.get("cod_op_service") or "").strip(),
|
||||
"denumire": (item.get("denumire") or "").strip(),
|
||||
"exclus": 1 if item.get("exclus") else 0,
|
||||
})
|
||||
return chips
|
||||
|
||||
@@ -1501,6 +1505,15 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
|
||||
ctx["prestatii_chips"] = prestatii_chips
|
||||
ctx["has_r_odo"] = _has_r_odo_chips(prestatii_chips)
|
||||
ctx["form_chips_url"] = "/form-chips"
|
||||
# Tinta implicita + sugestii la randarea INITIALA (inainte de orice /form-chips) —
|
||||
# helper partajat cu post_form_chips: cat exista operatii nemapate, EXISTA tinta.
|
||||
# Fara conn nu se pot calcula sugestiile (enrich_suggestions).
|
||||
ctx["chips_target_index"] = None
|
||||
ctx["sugestii_tinta"] = []
|
||||
if conn is not None:
|
||||
ctx["chips_target_index"], ctx["sugestii_tinta"] = _chips_target_si_sugestii(
|
||||
conn, prestatii_chips, nomenclator_rar
|
||||
)
|
||||
# submission_id pentru butonul "salveaza ca regula" din _chips_prestatii.html.
|
||||
# Cand chips sunt rerandate via /form-chips (stateless), chips_submission_id lipseste
|
||||
# → butonul nu apare (corect: /form-chips nu are scop de submission).
|
||||
@@ -1656,73 +1669,108 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
|
||||
if isinstance(obs_val, str):
|
||||
content["obs"] = obs_val.strip()
|
||||
|
||||
# Injectare coduri_prestatie din form (lista multi-select) INAINTE de resolve_prestatii.
|
||||
# form.getlist permite N coduri; fiecare se ataseaza itemului corespondent din
|
||||
# prestatii (by index), pastrand cod_op_service/denumire. Form-ul slim trimite
|
||||
# TOATE itemele (inclusiv "" pentru nemapate), permitand 1-1 aliniere by-index
|
||||
# chiar cand un item de mijloc ramane nemapat.
|
||||
# Cod necunoscut in nomenclator -> respins imediat (invariant ORA-12899).
|
||||
codes_raw = form.getlist("cod_prestatie")
|
||||
# Acceptam lista cu "" pentru pozitii nemapate; filtrare doar pt detectia
|
||||
# "fara niciun cod trimis" (cazul in care form-ul nu a inclus deloc cod_prestatie).
|
||||
codes_positional = [
|
||||
c.strip().upper() if isinstance(c, str) else ""
|
||||
for c in codes_raw
|
||||
]
|
||||
# Codul ales in picker dar ne-aprobat prin '+' se aplica implicit la salvare.
|
||||
# Picker flat (chips_add_cod_flat): cod ales dar neselectat ca chip → adaugat la sfarsit.
|
||||
# Picker per-operatie (chips_add_cod_{i}): cod ales pe pozitia i dar ne-aprobat → adaugat pozitional.
|
||||
# Ambele validate fata de nomenclator in bucla de validare de mai jos (invariant ORA-12899).
|
||||
_flat_picker = str(form.get("chips_add_cod_flat") or "").strip().upper()
|
||||
if _flat_picker and _flat_picker not in codes_positional:
|
||||
codes_positional.append(_flat_picker)
|
||||
for _i in range(len(codes_positional)):
|
||||
if not codes_positional[_i]:
|
||||
_op_val = str(form.get(f"chips_add_cod_{_i}") or "").strip().upper()
|
||||
if _op_val:
|
||||
codes_positional[_i] = _op_val
|
||||
# Verifica daca cel putin un cod non-gol a fost trimis
|
||||
codes_nonempty = [c for c in codes_positional if c]
|
||||
if codes_nonempty:
|
||||
# Valideaza FIECARE cod non-gol fata de nomenclator (ORA-12899: RAR accepta NUMAI coduri valide)
|
||||
for cod in codes_nonempty:
|
||||
exists_nom = conn.execute(
|
||||
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod,)
|
||||
).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}. "
|
||||
"Alege un cod valid din lista.",
|
||||
),
|
||||
)
|
||||
# Pereche operatie<->cod (E4): fiecare cod se ataseaza itemului by index.
|
||||
# Itemii existenti cu cod_op_service/denumire sunt PASTRATI (D7, E1 IRON RULE).
|
||||
# Coduri "" (pozitii nemapate) lasa itemul fara cod_prestatie -> needs_mapping.
|
||||
existing = content.get("prestatii") or []
|
||||
new_prestatii = []
|
||||
for i, cod in enumerate(codes_positional):
|
||||
if i >= len(existing) and not cod:
|
||||
continue # extra pozitii goale fara item corespondent — sarite
|
||||
item = dict(existing[i]) if i < len(existing) else {}
|
||||
if cod:
|
||||
item["cod_prestatie"] = cod
|
||||
# E1: cod_op_service/denumire NU se sterg; perechea op<->cod ramane intacta
|
||||
new_prestatii.append(item)
|
||||
# Dedup per-item (E4): (cod_op_service, cod_prestatie) identice -> pastreaza primul.
|
||||
# Doua operatii DIFERITE cu acelasi cod RAR sunt legitime si NU se dedupeaza.
|
||||
seen_pairs: set = set()
|
||||
deduped: list = []
|
||||
for item in new_prestatii:
|
||||
pair = (item.get("cod_op_service"), item.get("cod_prestatie"))
|
||||
if pair not in seen_pairs:
|
||||
seen_pairs.add(pair)
|
||||
deduped.append(item)
|
||||
content["prestatii"] = deduped
|
||||
# else: fara coduri trimise -> content["prestatii"] neatins; resolve_prestatii
|
||||
# detecteaza operatii nemapate si randul ramane needs_mapping.
|
||||
# Stare chips: editorul unificat trimite UN SINGUR hidden `chips_state`
|
||||
# (JSON versionat, cu `exclus` per item — vezi _chips_state_from_form). Cand e
|
||||
# prezent, INLOCUIESTE integral prestatiile, cu round-trip complet pe exclus.
|
||||
# Cod necunoscut in nomenclator -> respins (invariant ORA-12899);
|
||||
# sentinelul __NEDECLARAT__ nu e niciodata in nomenclator, deci pica in aceeasi
|
||||
# verificare daca ajunge (fabricat) direct ca cod_prestatie in starea hidden.
|
||||
_chips_state_raw = form.get("chips_state")
|
||||
_are_chips_state = _chips_state_raw is not None and str(_chips_state_raw).strip() != ""
|
||||
if _are_chips_state:
|
||||
_chips_din_stare = _chips_from_state_json(str(_chips_state_raw))
|
||||
if _chips_din_stare is None:
|
||||
return templates.TemplateResponse(
|
||||
"_trimitere_detaliu.html",
|
||||
_detaliu_ctx(
|
||||
request, row, conn=conn, account_id=account_id, error=True,
|
||||
message="Stare chips invalida — reincarca formularul si reintroduce codurile.",
|
||||
),
|
||||
)
|
||||
valid_codes_now = load_nomenclator_codes(conn) or set()
|
||||
necunoscute = sorted(
|
||||
{c["cod_prestatie"] for c in _chips_din_stare if c["cod_prestatie"]} - valid_codes_now
|
||||
)
|
||||
if necunoscute:
|
||||
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: {', '.join(necunoscute)}. "
|
||||
"Alege un cod valid din lista.",
|
||||
),
|
||||
)
|
||||
content["prestatii"] = _chips_din_stare
|
||||
else:
|
||||
# Compat: formular vechi (peste deploy) cu listele paralele
|
||||
# cod_prestatie/chip_op_service/chip_denumire + pickere ne-aprobate.
|
||||
# Injectare coduri_prestatie din form (lista multi-select) INAINTE de resolve_prestatii.
|
||||
# form.getlist permite N coduri; fiecare se ataseaza itemului corespondent din
|
||||
# prestatii (by index), pastrand cod_op_service/denumire. Form-ul slim trimite
|
||||
# TOATE itemele (inclusiv "" pentru nemapate), permitand 1-1 aliniere by-index
|
||||
# chiar cand un item de mijloc ramane nemapat.
|
||||
# Cod necunoscut in nomenclator -> respins imediat (invariant ORA-12899).
|
||||
codes_raw = form.getlist("cod_prestatie")
|
||||
# Acceptam lista cu "" pentru pozitii nemapate; filtrare doar pt detectia
|
||||
# "fara niciun cod trimis" (cazul in care form-ul nu a inclus deloc cod_prestatie).
|
||||
codes_positional = [
|
||||
c.strip().upper() if isinstance(c, str) else ""
|
||||
for c in codes_raw
|
||||
]
|
||||
# Codul ales in picker dar ne-aprobat prin '+' se aplica implicit la salvare.
|
||||
# Picker flat (chips_add_cod_flat): cod ales dar neselectat ca chip → adaugat la sfarsit.
|
||||
# Picker per-operatie (chips_add_cod_{i}): cod ales pe pozitia i dar ne-aprobat → adaugat pozitional.
|
||||
# Ambele validate fata de nomenclator in bucla de validare de mai jos (invariant ORA-12899).
|
||||
_flat_picker = str(form.get("chips_add_cod_flat") or "").strip().upper()
|
||||
if _flat_picker and _flat_picker not in codes_positional:
|
||||
codes_positional.append(_flat_picker)
|
||||
for _i in range(len(codes_positional)):
|
||||
if not codes_positional[_i]:
|
||||
_op_val = str(form.get(f"chips_add_cod_{_i}") or "").strip().upper()
|
||||
if _op_val:
|
||||
codes_positional[_i] = _op_val
|
||||
# Verifica daca cel putin un cod non-gol a fost trimis
|
||||
codes_nonempty = [c for c in codes_positional if c]
|
||||
if codes_nonempty:
|
||||
# Valideaza FIECARE cod non-gol fata de nomenclator (ORA-12899: RAR accepta NUMAI coduri valide)
|
||||
for cod in codes_nonempty:
|
||||
exists_nom = conn.execute(
|
||||
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod,)
|
||||
).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}. "
|
||||
"Alege un cod valid din lista.",
|
||||
),
|
||||
)
|
||||
# Pereche operatie<->cod (E4): fiecare cod se ataseaza itemului by index.
|
||||
# Itemii existenti cu cod_op_service/denumire sunt PASTRATI (D7, E1 IRON RULE).
|
||||
# Coduri "" (pozitii nemapate) lasa itemul fara cod_prestatie -> needs_mapping.
|
||||
existing = content.get("prestatii") or []
|
||||
new_prestatii = []
|
||||
for i, cod in enumerate(codes_positional):
|
||||
if i >= len(existing) and not cod:
|
||||
continue # extra pozitii goale fara item corespondent — sarite
|
||||
item = dict(existing[i]) if i < len(existing) else {}
|
||||
if cod:
|
||||
item["cod_prestatie"] = cod
|
||||
# E1: cod_op_service/denumire NU se sterg; perechea op<->cod ramane intacta
|
||||
new_prestatii.append(item)
|
||||
# Dedup per-item (E4): (cod_op_service, cod_prestatie) identice -> pastreaza primul.
|
||||
# Doua operatii DIFERITE cu acelasi cod RAR sunt legitime si NU se dedupeaza.
|
||||
seen_pairs: set = set()
|
||||
deduped: list = []
|
||||
for item in new_prestatii:
|
||||
pair = (item.get("cod_op_service"), item.get("cod_prestatie"))
|
||||
if pair not in seen_pairs:
|
||||
seen_pairs.add(pair)
|
||||
deduped.append(item)
|
||||
content["prestatii"] = deduped
|
||||
# else: fara coduri trimise -> content["prestatii"] neatins; resolve_prestatii
|
||||
# detecteaza operatii nemapate si randul ramane needs_mapping.
|
||||
|
||||
# Re-rezolva prestatiile cu maparea curenta (ca reresolve_account): NU re-pune
|
||||
# niciodata in coada un cod nemapat (codPrestatie null) — FINALIZATA e ireversibil
|
||||
@@ -1733,19 +1781,23 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
|
||||
text_rules = load_text_rules(conn, account_id)
|
||||
excluded_ops = load_excluded_ops(conn, account_id)
|
||||
resolved, unmapped = resolve_prestatii(content.get("prestatii"), mapping, valid_codes, text_rules, excluded_ops)
|
||||
# Persistam TOATE itemii (inclusiv cei exclusi, adnotati exclus=1) — round-trip
|
||||
# complet in editor (chip Nedeclarat reapare la redeschidere, x il readuce la
|
||||
# warning). Excluderea din payload-ul RAR/cheia idempotentei se face DOAR local,
|
||||
# mai jos, prin canon; content["prestatii"] NU se ingusteaza niciodata.
|
||||
content["prestatii"] = resolved
|
||||
|
||||
# telemetrie pentru itemii rezolvati prin regula text (calea corectie web).
|
||||
_emite_text_rule_hits(conn, account_id, row["id"], resolved)
|
||||
|
||||
# Prestatiile excluse de la declarare ies din payload-ul trimis (ca in
|
||||
# reresolve_account); toate excluse -> needs_data cu motiv explicit mai jos.
|
||||
# Prestatiile excluse de la declarare NU intra in payload-ul trimis la RAR nici
|
||||
# in cheia de idempotenta (ca in reresolve_account) — dar RAMAN in payload_json
|
||||
# persistat mai sus. Toate excluse -> needs_data cu motiv explicit mai jos.
|
||||
declarabile, excluse = split_prestatii_excluse(resolved)
|
||||
if not unmapped and excluse and declarabile:
|
||||
content["prestatii"] = declarabile
|
||||
|
||||
# Canonicalizare (strip ".0" odometru, VIN/nr upper) INAINTE de validare si cheie.
|
||||
canon = canonicalize_row(content)
|
||||
# Foloseste DOAR partea declarabila pentru cheie (exclusii nu schimba hash-ul).
|
||||
canon = canonicalize_row({**content, "prestatii": declarabile})
|
||||
content.update({
|
||||
"vin": canon["vin"],
|
||||
"nr_inmatriculare": canon["nr_inmatriculare"],
|
||||
@@ -2392,6 +2444,71 @@ async def post_bulk_fix(request: Request) -> HTMLResponse:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _sugestii_select_pentru_tinta(conn, denumire: str | None, nomenclator: list[dict]) -> list[dict]:
|
||||
"""Sugestii pentru optgroup "Sugestii" din selectul unic.
|
||||
|
||||
Refoloseste EXACT sursele din `_preview_import.html:89-121` (fara drum nou de cod):
|
||||
`sugestie_principala` (GOLD/SILVER/embedding, `enrich_suggestions`) prima, apoi
|
||||
top-3 fuzzy (`suggest_codes`), dedupliate, pastrand doar coduri care exista in
|
||||
nomenclator. Fara memoizare — se recalculeaza la fiecare re-render.
|
||||
|
||||
Degradare gratioasa: motor de sugestii rece/dezactivat/exceptie -> lista goala
|
||||
(selectul cade pe nomenclatorul complet, fara optgroup). `ensure_embeddings_corpus`
|
||||
ruleaza cu `block=False` implicit — calea de request nu asteapta warmup-ul.
|
||||
|
||||
Fara denumire (tinta neidentificata) fuzzy match-ul ar intoarce primele coduri
|
||||
din nomenclator ca sugestii irelevante — golim explicit.
|
||||
"""
|
||||
if not (denumire or "").strip():
|
||||
return []
|
||||
try:
|
||||
ensure_embeddings_corpus(conn, nomenclator)
|
||||
by_cod = {n["cod_prestatie"]: n.get("nume_prestatie") for n in (nomenclator or [])}
|
||||
out: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
enriched = enrich_suggestions(conn, denumire)
|
||||
principala = enriched.get("sugestie_principala") if enriched else None
|
||||
if principala and principala.get("cod_prestatie") in by_cod:
|
||||
cod = principala["cod_prestatie"]
|
||||
out.append({"cod_prestatie": cod, "nume_prestatie": by_cod.get(cod)})
|
||||
seen.add(cod)
|
||||
|
||||
for s in suggest_codes(denumire, nomenclator, limit=3):
|
||||
cod = s.get("cod_prestatie")
|
||||
if cod and cod not in seen and cod in by_cod:
|
||||
out.append({"cod_prestatie": cod, "nume_prestatie": by_cod.get(cod)})
|
||||
seen.add(cod)
|
||||
return out
|
||||
except Exception:
|
||||
return [] # degradare gratioasa: selectul ramane cu nomenclatorul complet
|
||||
|
||||
|
||||
def _chips_target_si_sugestii(
|
||||
conn, chips: list[dict], nomenclator: list[dict],
|
||||
) -> tuple[int | None, list[dict]]:
|
||||
"""Tinta implicita (prima operatie nemapata ne-exclusa, identificabila) + sugestii.
|
||||
|
||||
Sursa unica pentru `post_form_chips` SI randarea INITIALA a modalului (inainte
|
||||
de orice /form-chips): cat exista operatii nemapate, EXISTA tinta. Fara acest
|
||||
helper partajat, cele doua cai ar putea diverge.
|
||||
|
||||
Necesita `cod_op_service` truthy: template-ul randeaza chip-ul warning (tinta
|
||||
vizibila) doar pentru operatii identificabile; un item raw fara cod_op_service
|
||||
n-are ce sa arate ca tinta si ar ramane invizibil.
|
||||
"""
|
||||
target_index = next(
|
||||
(i for i, c in enumerate(chips)
|
||||
if not c.get("cod_prestatie") and not c.get("exclus") and c.get("cod_op_service")),
|
||||
None,
|
||||
)
|
||||
sugestii: list[dict] = []
|
||||
if target_index is not None:
|
||||
_den_tinta = chips[target_index].get("denumire") or chips[target_index].get("cod_op_service")
|
||||
sugestii = _sugestii_select_pentru_tinta(conn, _den_tinta, nomenclator)
|
||||
return target_index, sugestii
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# Endpoint /form-chips — re-randare chips prestatii. Preia starea curenta din #
|
||||
# form + actiunea (add/remove) si re-randeaza _chips_prestatii.html. Fara #
|
||||
@@ -2403,37 +2520,25 @@ async def post_bulk_fix(request: Request) -> HTMLResponse:
|
||||
async def post_form_chips(request: Request) -> HTMLResponse:
|
||||
"""Re-randeaza sectiunea chips prestatii (HTMX server-driven).
|
||||
|
||||
Primeste starea curenta a chip-urilor (3 liste paralele: cod_prestatie,
|
||||
chip_op_service, chip_denumire) + actiunea (add/remove) si returneaza
|
||||
_chips_prestatii.html actualizat. Fara scriere in DB (stateless mid-edit).
|
||||
Auth: sesiune activa; CSRF verificat.
|
||||
Stare unica: hidden `chips_state` (JSON versionat, vezi _chips_state_from_form).
|
||||
DOUA actiuni: add (leaga codul de operatia tinta prin chips_target_index, sau
|
||||
adauga cod liber fara tinta; sentinelul __NEDECLARAT__ + tinta marcheaza
|
||||
exclus=1 pe item) si remove (chips_remove_index, index unic — acopera si fostul
|
||||
remove_flat). Fara scriere in DB (stateless mid-edit). Auth: sesiune activa;
|
||||
CSRF verificat.
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
form = await request.form()
|
||||
verify_csrf(request, str(form.get("csrf_token") or ""))
|
||||
|
||||
# Reconstruct current chips state from parallel hidden inputs (emise de _chips_prestatii.html).
|
||||
# Toate cele 3 liste sunt aceeasi lungime (emise index-by-index in template).
|
||||
cod_list = [c.strip().upper() if isinstance(c, str) else "" for c in form.getlist("cod_prestatie")]
|
||||
op_list = [o.strip() if isinstance(o, str) else "" for o in form.getlist("chip_op_service")]
|
||||
den_list = [d.strip() if isinstance(d, str) else "" for d in form.getlist("chip_denumire")]
|
||||
|
||||
# Aliniaza listele la lungimea maxima (defensive)
|
||||
n = max(len(cod_list), len(op_list), len(den_list)) if (cod_list or op_list or den_list) else 0
|
||||
chips: list[dict] = []
|
||||
for i in range(n):
|
||||
chips.append({
|
||||
"cod_prestatie": cod_list[i] if i < len(cod_list) else "",
|
||||
"cod_op_service": op_list[i] if i < len(op_list) else "",
|
||||
"denumire": den_list[i] if i < len(den_list) else "",
|
||||
})
|
||||
|
||||
chips, chips_error = _chips_state_from_form(form)
|
||||
if chips is None:
|
||||
chips = []
|
||||
action = str(form.get("chips_action") or "").strip()
|
||||
# Orice adaugare are un efect vizibil: mesaj de refuz (chips_error) sau
|
||||
# confirmare + chip evidentiat (chips_ok / chips_added_cod). Fara no-op silentios.
|
||||
chips_error = ""
|
||||
# confirmare + chip evidentiat (chips_ok / chips_added_index). Fara no-op silentios.
|
||||
chips_ok = ""
|
||||
chips_added_cod = ""
|
||||
chips_added_index: int | None = None
|
||||
|
||||
conn = get_connection()
|
||||
|
||||
@@ -2442,71 +2547,93 @@ async def post_form_chips(request: Request) -> HTMLResponse:
|
||||
"SELECT 1 FROM nomenclator_rar WHERE cod_prestatie=?", (cod,)
|
||||
).fetchone() is not None
|
||||
|
||||
def _target_valid(idx: int) -> bool:
|
||||
return 0 <= idx < len(chips) and not chips[idx].get("cod_prestatie") and not chips[idx].get("exclus")
|
||||
|
||||
try:
|
||||
if action == "add":
|
||||
# Adauga cod la operatia specificata prin chips_add_op_index
|
||||
try:
|
||||
op_idx = int(str(form.get("chips_add_op_index") or 0))
|
||||
except (ValueError, TypeError):
|
||||
op_idx = 0
|
||||
add_cod = str(form.get(f"chips_add_cod_{op_idx}") or "").strip().upper()
|
||||
if not add_cod:
|
||||
chips_error = "Selecteaza un cod RAR din lista inainte de a adauga."
|
||||
elif not (0 <= op_idx < len(chips)):
|
||||
chips_error = "Operatia nu mai exista in formular — reincarca editarea."
|
||||
elif not _cod_valid(add_cod):
|
||||
chips_error = f"Cod necunoscut in nomenclator: {add_cod}."
|
||||
else:
|
||||
chips[op_idx]["cod_prestatie"] = add_cod
|
||||
chips_ok = f"{add_cod} adaugat."
|
||||
chips_added_cod = add_cod
|
||||
if not chips_error and action == "add":
|
||||
raw_target = str(form.get("chips_target_index") or "").strip()
|
||||
target_idx: int | None = None
|
||||
if raw_target != "":
|
||||
try:
|
||||
target_idx = int(raw_target)
|
||||
except (ValueError, TypeError):
|
||||
target_idx = -1 # index nenumeric -> forteaza "tinta invalida" mai jos
|
||||
|
||||
elif action in ("add_flat", "add_extra"):
|
||||
# Adauga cod RAR liber (fara op_service): add_flat = mod plat,
|
||||
# add_extra = mod operatii. Acelasi select (chips_add_cod_flat),
|
||||
# aceeasi validare si acelasi dedup per-item (E4).
|
||||
add_cod_flat = str(form.get("chips_add_cod_flat") or "").strip().upper()
|
||||
if not add_cod_flat:
|
||||
# Mai multe <select name="chips_pick"> pot coexista pe pagina (un picker
|
||||
# per operatie nemapata + unul flat); doar cel interactionat are valoare.
|
||||
pick = next(
|
||||
(v.strip().upper() for v in form.getlist("chips_pick") if isinstance(v, str) and v.strip()),
|
||||
"",
|
||||
)
|
||||
|
||||
if not pick:
|
||||
chips_error = "Selecteaza un cod RAR din lista inainte de a adauga."
|
||||
elif not _cod_valid(add_cod_flat):
|
||||
chips_error = f"Cod necunoscut in nomenclator: {add_cod_flat}."
|
||||
else:
|
||||
existing_pairs = {
|
||||
(c.get("cod_op_service", ""), c.get("cod_prestatie", ""))
|
||||
for c in chips
|
||||
}
|
||||
if ("", add_cod_flat) in existing_pairs:
|
||||
chips_error = f"{add_cod_flat} este deja in lista — nu a fost adaugat inca o data."
|
||||
elif pick == EXCLUDE_SENTINEL:
|
||||
# "Nu se declara la RAR": cere OBLIGATORIU o tinta valida (operatie
|
||||
# nemapata, ne-exclusa) — sentinelul nu are sens pe cod liber.
|
||||
if target_idx is None or not _target_valid(target_idx):
|
||||
chips_error = "\"Nu se declara la RAR\" cere o operatie tinta valida."
|
||||
else:
|
||||
chips.append({"cod_prestatie": add_cod_flat, "cod_op_service": "", "denumire": ""})
|
||||
chips_ok = f"{add_cod_flat} adaugat."
|
||||
chips_added_cod = add_cod_flat
|
||||
chips[target_idx]["exclus"] = 1
|
||||
chips[target_idx]["cod_prestatie"] = ""
|
||||
op_nume = chips[target_idx].get("cod_op_service") or "Operatia"
|
||||
chips_ok = f"{op_nume} nu se va declara la RAR."
|
||||
chips_added_index = target_idx
|
||||
elif not _cod_valid(pick):
|
||||
chips_error = f"Cod necunoscut in nomenclator: {pick}."
|
||||
elif target_idx is not None and not _target_valid(target_idx):
|
||||
chips_error = "Operatia tinta nu mai exista sau e deja rezolvata — reincarca editarea."
|
||||
elif target_idx is not None:
|
||||
# Leaga codul de operatia tinta (E4 binding).
|
||||
op_tinta = chips[target_idx].get("cod_op_service")
|
||||
dedup = any(c.get("cod_op_service") == op_tinta and c.get("cod_prestatie") == pick for c in chips)
|
||||
if dedup:
|
||||
chips_error = f"{pick} este deja in lista pentru aceasta operatie."
|
||||
else:
|
||||
chips[target_idx]["cod_prestatie"] = pick
|
||||
chips[target_idx]["exclus"] = 0
|
||||
chips_ok = f"{pick} adaugat."
|
||||
chips_added_index = target_idx
|
||||
else:
|
||||
# Fara tinta -> chip liber (fara operatie sursa).
|
||||
dedup = any(not c.get("cod_op_service") and c.get("cod_prestatie") == pick for c in chips)
|
||||
if dedup:
|
||||
chips_error = f"{pick} este deja in lista — nu a fost adaugat inca o data."
|
||||
else:
|
||||
chips.append({"cod_prestatie": pick, "cod_op_service": "", "denumire": "", "exclus": 0})
|
||||
chips_ok = f"{pick} adaugat."
|
||||
chips_added_index = len(chips) - 1
|
||||
|
||||
elif action == "remove":
|
||||
# Sterge codul de la indexul dat (lasa op_service intact -> operatie ramane nemapata)
|
||||
elif not chips_error and action == "remove":
|
||||
# Index unic (acopera si fostul remove_flat): chip legat de operatie ->
|
||||
# codul se sterge (operatia redevine nemapata/warning, inclusiv Nedeclarat
|
||||
# -> warning); chip liber -> iese complet din lista.
|
||||
try:
|
||||
remove_idx = int(str(form.get("chips_remove_index") or 0))
|
||||
remove_idx = int(str(form.get("chips_remove_index") or ""))
|
||||
except (ValueError, TypeError):
|
||||
remove_idx = 0
|
||||
remove_idx = -1
|
||||
if 0 <= remove_idx < len(chips):
|
||||
chips[remove_idx]["cod_prestatie"] = ""
|
||||
item = chips[remove_idx]
|
||||
if item.get("cod_op_service"):
|
||||
item["cod_prestatie"] = ""
|
||||
item["exclus"] = 0
|
||||
else:
|
||||
chips.pop(remove_idx)
|
||||
|
||||
elif action == "remove_flat":
|
||||
# Sterge un chip plat dupa cod (in mod fara op_service)
|
||||
remove_cod = str(form.get("chips_remove_cod") or "").strip().upper()
|
||||
chips = [
|
||||
c for c in chips
|
||||
if not (not c.get("cod_op_service") and c.get("cod_prestatie") == remove_cod)
|
||||
]
|
||||
|
||||
# Compute has_r_odo dupa actiune
|
||||
has_r_odo = _has_r_odo_chips(chips)
|
||||
|
||||
# Incarca nomenclatorul pentru picker
|
||||
nomenclator_rar = load_nomenclator(conn)
|
||||
|
||||
# Tinta implicita dupa actiune (prima operatie nemapata) + sugestii pentru ea
|
||||
# (optgroup "Sugestii" din select) — helper partajat cu randarea initiala.
|
||||
chips_target_index, sugestii_tinta = _chips_target_si_sugestii(conn, chips, nomenclator_rar)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# Ecou (nu re-derivat): permite butonului "salveaza ca regula" sa ramana
|
||||
# disponibil dupa re-randari succesive via /form-chips.
|
||||
chips_submission_id = str(form.get("chips_submission_id") or "").strip()
|
||||
|
||||
return templates.TemplateResponse("_chips_prestatii.html", {
|
||||
"request": request,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
@@ -2517,7 +2644,10 @@ async def post_form_chips(request: Request) -> HTMLResponse:
|
||||
"chips_section_id": "chips-section",
|
||||
"chips_error": chips_error,
|
||||
"chips_ok": chips_ok,
|
||||
"chips_added_cod": chips_added_cod,
|
||||
"chips_added_index": chips_added_index,
|
||||
"chips_target_index": chips_target_index,
|
||||
"chips_submission_id": chips_submission_id,
|
||||
"sugestii_tinta": sugestii_tinta,
|
||||
})
|
||||
|
||||
|
||||
@@ -3235,7 +3365,12 @@ def _web_compute_preview(
|
||||
key: str | None = None
|
||||
if info["resolved_status"] in ("ok", "needs_review", "needs_data"):
|
||||
try:
|
||||
key = _build_idempotency_key(account_id, info["resolved"], preview_env)
|
||||
# Cheia foloseste DOAR partea declarabila (itemii exclusi nu intra in
|
||||
# hash) — identica cu cea calculata la commit, altfel un rand cu
|
||||
# prestatii excluse arata alt duplicat decat cel real.
|
||||
decl, _exc = split_prestatii_excluse(info["resolved"].get("prestatii"))
|
||||
key_resolved = {**info["resolved"], "prestatii": decl} if _exc else info["resolved"]
|
||||
key = _build_idempotency_key(account_id, key_resolved, preview_env)
|
||||
keys_for_lookup.append(key)
|
||||
key_to_indices.setdefault(key, []).append(i)
|
||||
except Exception:
|
||||
@@ -3796,12 +3931,16 @@ def web_rand_editare_modal(request: Request, import_id: int, row_index: int) ->
|
||||
"cod_prestatie": (p.get("cod_prestatie") or "").strip().upper(),
|
||||
"cod_op_service": (p.get("cod_op_service") or "").strip(),
|
||||
"denumire": (p.get("denumire") or "").strip(),
|
||||
"exclus": 1 if p.get("exclus") else 0,
|
||||
}
|
||||
for p in (res.get("prestatii") or [])
|
||||
if isinstance(p, dict)
|
||||
]
|
||||
_preview_has_r_odo = _has_r_odo_chips(_preview_chips)
|
||||
_preview_nomenclator = load_nomenclator(conn)
|
||||
_preview_target_idx, _preview_sugestii = _chips_target_si_sugestii(
|
||||
conn, _preview_chips, _preview_nomenclator
|
||||
)
|
||||
return templates.TemplateResponse("_editare_preview_modal.html", {
|
||||
"request": request,
|
||||
"import_id": import_id,
|
||||
@@ -3827,6 +3966,8 @@ def web_rand_editare_modal(request: Request, import_id: int, row_index: int) ->
|
||||
"obs_val": (res.get("obs") or "").strip(),
|
||||
"nomenclator_rar": _preview_nomenclator,
|
||||
"form_chips_url": "/form-chips",
|
||||
"chips_target_index": _preview_target_idx,
|
||||
"sugestii_tinta": _preview_sugestii,
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -3859,25 +4000,77 @@ def web_rand_display(request: Request, import_id: int, row_index: int) -> HTMLRe
|
||||
conn.close()
|
||||
|
||||
|
||||
def _chips_state_from_form(form) -> list[dict[str, str]] | None:
|
||||
"""Reconstruieste starea chips prestatii din inputurile paralele ale formularului.
|
||||
CHIPS_STATE_VERSION = 1
|
||||
|
||||
Aceeasi conventie ca /form-chips (_chips_prestatii.html): 3 liste paralele
|
||||
cod_prestatie / chip_op_service / chip_denumire. Codurile alese in pickere dar
|
||||
ne-aprobate prin '+' (chips_add_cod_{i}, chips_add_cod_flat) se aplica implicit,
|
||||
ca in post_corectie_trimitere. Dedup pe perechea (op, cod).
|
||||
|
||||
Intoarce None cand formularul nu contine deloc stare de chips (form fara
|
||||
sectiunea de prestatii) — apelantul lasa prestatiile neatinse.
|
||||
def _parse_chip_exclus(raw) -> int:
|
||||
"""Parsare stricta a flag-ului exclus: DOAR '1' -> 1, orice altceva -> 0."""
|
||||
return 1 if str(raw).strip() == "1" else 0
|
||||
|
||||
|
||||
def _chips_state_to_json(chips: list[dict]) -> str:
|
||||
"""Serializeaza chips (lista interna) in JSON versionat pentru hidden `chips_state`."""
|
||||
return json.dumps({
|
||||
"v": CHIPS_STATE_VERSION,
|
||||
"items": [
|
||||
{
|
||||
"cod": c.get("cod_prestatie") or "",
|
||||
"op": c.get("cod_op_service") or "",
|
||||
"den": c.get("denumire") or "",
|
||||
"exclus": 1 if c.get("exclus") else 0,
|
||||
}
|
||||
for c in chips
|
||||
],
|
||||
}, ensure_ascii=False)
|
||||
|
||||
|
||||
# Expune serializarea chips_state in template (randare initiala, fara trecere prin
|
||||
# post_form_chips) -- sursa unica de adevar pentru hidden `chips_state`.
|
||||
templates.env.globals["chips_state_json"] = _chips_state_to_json
|
||||
|
||||
|
||||
def _chips_from_state_json(raw: str) -> list[dict] | None:
|
||||
"""Decodeaza hidden `chips_state` (JSON versionat) in lista interna de chips.
|
||||
|
||||
Intoarce None cand JSON-ul e invalid sau structura nu se potriveste
|
||||
(fara cheie 'items' lista) — apelantul trateaza asta ca eroare vizibila,
|
||||
NU ca stare goala (altfel o golire accidentala trece neobservata).
|
||||
"""
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
if not isinstance(parsed, dict) or not isinstance(parsed.get("items"), list):
|
||||
return None
|
||||
chips: list[dict] = []
|
||||
for it in parsed["items"]:
|
||||
if not isinstance(it, dict):
|
||||
return None
|
||||
chips.append({
|
||||
"cod_prestatie": str(it.get("cod") or "").strip().upper(),
|
||||
"cod_op_service": str(it.get("op") or "").strip(),
|
||||
"denumire": str(it.get("den") or "").strip(),
|
||||
"exclus": _parse_chip_exclus(it.get("exclus")),
|
||||
})
|
||||
return chips
|
||||
|
||||
|
||||
def _chips_from_legacy_lists(form) -> list[dict]:
|
||||
"""Compat tranzitorie: reconstruieste chips din listele paralele vechi.
|
||||
|
||||
cod_prestatie / chip_op_service / chip_denumire (+ chip_exclus optional,
|
||||
parsat strict). Codurile alese in pickere dar ne-aprobate prin '+' (E.g.
|
||||
chips_add_cod_{i}, chips_add_cod_flat) se aplica implicit — comportament
|
||||
pastrat identic cu forma veche (form-uri deschise peste deploy). Dedup pe
|
||||
perechea (op, cod). Se sterge in release-ul urmator.
|
||||
"""
|
||||
flat_picker = str(form.get("chips_add_cod_flat") or "").strip().upper()
|
||||
if "cod_prestatie" not in form and "chip_op_service" not in form and not flat_picker:
|
||||
return None
|
||||
cod_list = [c.strip().upper() if isinstance(c, str) else "" for c in form.getlist("cod_prestatie")]
|
||||
op_list = [o.strip() if isinstance(o, str) else "" for o in form.getlist("chip_op_service")]
|
||||
den_list = [d.strip() if isinstance(d, str) else "" for d in form.getlist("chip_denumire")]
|
||||
excl_list = [str(e) for e in form.getlist("chip_exclus")]
|
||||
n = max(len(cod_list), len(op_list), len(den_list))
|
||||
chips: list[dict[str, str]] = []
|
||||
chips: list[dict] = []
|
||||
for i in range(n):
|
||||
cod = cod_list[i] if i < len(cod_list) else ""
|
||||
if not cod:
|
||||
@@ -3887,12 +4080,13 @@ def _chips_state_from_form(form) -> list[dict[str, str]] | None:
|
||||
"cod_prestatie": cod,
|
||||
"cod_op_service": op_list[i] if i < len(op_list) else "",
|
||||
"denumire": den_list[i] if i < len(den_list) else "",
|
||||
"exclus": _parse_chip_exclus(excl_list[i]) if i < len(excl_list) else 0,
|
||||
})
|
||||
pairs = {(c["cod_op_service"], c["cod_prestatie"]) for c in chips}
|
||||
if flat_picker and ("", flat_picker) not in pairs:
|
||||
chips.append({"cod_prestatie": flat_picker, "cod_op_service": "", "denumire": ""})
|
||||
chips.append({"cod_prestatie": flat_picker, "cod_op_service": "", "denumire": "", "exclus": 0})
|
||||
seen: set = set()
|
||||
out: list[dict[str, str]] = []
|
||||
out: list[dict] = []
|
||||
for c in chips:
|
||||
if not (c["cod_prestatie"] or c["cod_op_service"] or c["denumire"]):
|
||||
continue
|
||||
@@ -3904,6 +4098,33 @@ def _chips_state_from_form(form) -> list[dict[str, str]] | None:
|
||||
return out
|
||||
|
||||
|
||||
def _chips_state_from_form(form) -> tuple[list[dict] | None, str]:
|
||||
"""Reconstruieste starea chips prestatii din formular.
|
||||
|
||||
Sursa unica: hidden `chips_state` (JSON versionat {"v":1,"items":[...]});
|
||||
compat tranzitorie pe listele paralele vechi (cod_prestatie/chip_op_service/
|
||||
chip_denumire) cand `chips_state` lipseste (form deschis peste deploy).
|
||||
|
||||
Intoarce (chips, error):
|
||||
- error non-gol -> JSON invalid; apelantul arata chips_error si NU aplica
|
||||
nicio schimbare (NU stare goala silentioasa).
|
||||
- chips=None + error gol -> formularul nu are deloc sectiune de chips;
|
||||
prestatiile raman neatinse.
|
||||
- chips=[] + error gol -> golire intentionata (chips_state prezent, items:[]);
|
||||
se persista la salvare.
|
||||
"""
|
||||
raw_state = form.get("chips_state")
|
||||
if raw_state is not None and str(raw_state).strip() != "":
|
||||
chips = _chips_from_state_json(str(raw_state))
|
||||
if chips is None:
|
||||
return None, "Stare chips invalida — reincarca formularul si reintroduce codurile."
|
||||
return chips, ""
|
||||
flat_picker = str(form.get("chips_add_cod_flat") or "").strip().upper()
|
||||
if "cod_prestatie" not in form and "chip_op_service" not in form and not flat_picker:
|
||||
return None, ""
|
||||
return _chips_from_legacy_lists(form), ""
|
||||
|
||||
|
||||
@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) + raspunde cu OOB rand+contoare sau erori in modal.
|
||||
@@ -3924,9 +4145,55 @@ async def web_editeaza_rand(request: Request, import_id: int, row_index: int) ->
|
||||
for camp in EDIT_FIELDS
|
||||
}
|
||||
# Chips prestatii din modal: None = form fara sectiune de chips (neatins).
|
||||
chips_state = _chips_state_from_form(form)
|
||||
chips_state, chips_parse_error = _chips_state_from_form(form)
|
||||
conn = get_connection()
|
||||
try:
|
||||
if chips_parse_error:
|
||||
# JSON chips_state corupt -> eroare vizibila, NU stare goala silentioasa;
|
||||
# override NEATINS (nicio prestatie nu se schimba).
|
||||
_nom_err = load_nomenclator(conn)
|
||||
_cur_result, _cur_row = _preview_one_row(conn, import_id, account_id, row_index)
|
||||
_cur_chips = (
|
||||
(_cur_row.get("resolved") or {}).get("prestatii")
|
||||
if _cur_row and not isinstance(_cur_result, str) else None
|
||||
) or []
|
||||
_cur_chips = [
|
||||
{
|
||||
"cod_prestatie": (p.get("cod_prestatie") or "").strip().upper(),
|
||||
"cod_op_service": (p.get("cod_op_service") or "").strip(),
|
||||
"denumire": (p.get("denumire") or "").strip(),
|
||||
"exclus": 1 if p.get("exclus") else 0,
|
||||
}
|
||||
for p in _cur_chips if isinstance(p, dict)
|
||||
]
|
||||
_cur_target_idx, _cur_sugestii = _chips_target_si_sugestii(conn, _cur_chips, _nom_err)
|
||||
return templates.TemplateResponse("_editare_preview_modal.html", {
|
||||
"request": request,
|
||||
"import_id": import_id,
|
||||
"row_index": row_index,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
"vin": str(form.get("vin") or ""),
|
||||
"stare_css": "",
|
||||
"stare_eticheta": "",
|
||||
"form_nr": str(form.get("nr_inmatriculare") or ""),
|
||||
"form_vin": str(form.get("vin") or ""),
|
||||
"form_data": str(form.get("data_prestatie") or ""),
|
||||
"form_odo_final": str(form.get("odometru_final") or ""),
|
||||
"form_odo_initial": str(form.get("odometru_initial") or ""),
|
||||
"err_map": {},
|
||||
"fix_map": {},
|
||||
"vin_context": str(form.get("vin") or ""),
|
||||
"btn_label": "Salveaza",
|
||||
"message": chips_parse_error,
|
||||
"chips_error": chips_parse_error,
|
||||
"prestatii_chips": _cur_chips,
|
||||
"has_r_odo": _has_r_odo_chips(_cur_chips),
|
||||
"obs_val": str(form.get("obs") or "").strip(),
|
||||
"nomenclator_rar": _nom_err,
|
||||
"form_chips_url": "/form-chips",
|
||||
"chips_target_index": _cur_target_idx,
|
||||
"sugestii_tinta": _cur_sugestii,
|
||||
})
|
||||
# Invariant ORA-12899: orice cod trimis din form se valideaza fata de
|
||||
# nomenclator INAINTE de persistare (RAR accepta NUMAI coduri valide).
|
||||
if chips_state is not None:
|
||||
@@ -3936,6 +4203,7 @@ async def web_editeaza_rand(request: Request, import_id: int, row_index: int) ->
|
||||
)
|
||||
if necunoscute:
|
||||
_nom = load_nomenclator(conn)
|
||||
_nec_target_idx, _nec_sugestii = _chips_target_si_sugestii(conn, chips_state, _nom)
|
||||
return templates.TemplateResponse("_editare_preview_modal.html", {
|
||||
"request": request,
|
||||
"import_id": import_id,
|
||||
@@ -3960,6 +4228,8 @@ async def web_editeaza_rand(request: Request, import_id: int, row_index: int) ->
|
||||
"obs_val": str(form.get("obs") or "").strip(),
|
||||
"nomenclator_rar": _nom,
|
||||
"form_chips_url": "/form-chips",
|
||||
"chips_target_index": _nec_target_idx,
|
||||
"sugestii_tinta": _nec_sugestii,
|
||||
})
|
||||
|
||||
# Mutatie pura de stocare (404/409/422 -> propaga; htmx hx-on::response-error
|
||||
@@ -3992,11 +4262,13 @@ async def web_editeaza_rand(request: Request, import_id: int, row_index: int) ->
|
||||
"cod_prestatie": (p.get("cod_prestatie") or "").strip().upper(),
|
||||
"cod_op_service": (p.get("cod_op_service") or "").strip(),
|
||||
"denumire": (p.get("denumire") or "").strip(),
|
||||
"exclus": 1 if p.get("exclus") else 0,
|
||||
}
|
||||
for p in (res.get("prestatii") or [])
|
||||
if isinstance(p, dict)
|
||||
]
|
||||
_err_nomenclator = load_nomenclator(conn)
|
||||
_err_target_idx, _err_sugestii = _chips_target_si_sugestii(conn, _err_chips, _err_nomenclator)
|
||||
return templates.TemplateResponse("_editare_preview_modal.html", {
|
||||
"request": request,
|
||||
"import_id": import_id,
|
||||
@@ -4021,6 +4293,8 @@ async def web_editeaza_rand(request: Request, import_id: int, row_index: int) ->
|
||||
"obs_val": str(form.get("obs") or res.get("obs") or "").strip(),
|
||||
"nomenclator_rar": _err_nomenclator,
|
||||
"form_chips_url": "/form-chips",
|
||||
"chips_target_index": _err_target_idx,
|
||||
"sugestii_tinta": _err_sugestii,
|
||||
})
|
||||
|
||||
# Succes: reincarca preview-ul complet + toast + inchide modal (vezi helper).
|
||||
@@ -4532,15 +4806,18 @@ async def web_confirma_import(
|
||||
# Rezolva prestatii
|
||||
prestatii = mapped.get("prestatii") or []
|
||||
resolved_p, _ = resolve_prestatii(prestatii, mapping_ops, valid_codes, text_rules, excluded_ops)
|
||||
# Prestatiile excluse NU pleaca la RAR (ca in preview); rand fara nicio
|
||||
# prestatie declarabila = 'excluded' in preview -> defensiv skip.
|
||||
resolved_p, _excluse_p = split_prestatii_excluse(resolved_p)
|
||||
if not resolved_p:
|
||||
# Prestatiile excluse NU pleaca la RAR si nu intra in cheia de
|
||||
# idempotenta, dar RAMAN in payload_json persistat (round-trip in
|
||||
# editor) — ingustarea la declarabile se face doar local, pentru
|
||||
# canon/cheie, si la worker, la momentul trimiterii.
|
||||
declarabile_p, _excluse_p = split_prestatii_excluse(resolved_p)
|
||||
# rand fara nicio prestatie declarabila = 'excluded' in preview -> defensiv skip.
|
||||
if not declarabile_p:
|
||||
continue
|
||||
mapped["prestatii"] = resolved_p
|
||||
|
||||
# Canonicalizare (cheia identica cu preview: canonicalize_row + build_key)
|
||||
canon = canonicalize_row(mapped)
|
||||
canon = canonicalize_row({**mapped, "prestatii": declarabile_p})
|
||||
mapped.update({
|
||||
"vin": canon["vin"],
|
||||
"nr_inmatriculare": canon["nr_inmatriculare"],
|
||||
@@ -4556,7 +4833,7 @@ async def web_confirma_import(
|
||||
"odometru_final": mapped.get("odometru_final"),
|
||||
"prestatii": [
|
||||
str(p.get("cod_prestatie") or p.get("cod_op_service") or "")
|
||||
for p in resolved_p
|
||||
for p in declarabile_p
|
||||
],
|
||||
}, sort_keys=True, ensure_ascii=False))
|
||||
|
||||
|
||||
@@ -1,233 +1,175 @@
|
||||
{#
|
||||
_chips_prestatii.html — sectiunea de prestatii chips (E4, server-driven via /form-chips).
|
||||
_chips_prestatii.html — control unificat chips prestatii.
|
||||
|
||||
Re-randata de endpoint-ul /form-chips la fiecare add/remove de chip.
|
||||
Inclusa si din _form_editare.html pentru randarea initiala.
|
||||
UN singur container `.chips`: chips cod (legate de operatie sau libere), chips
|
||||
warning pentru operatii nemapate, chip `Nedeclarat` pentru itemi exclusi, si
|
||||
selectul unic `chips_pick` la final. Fara `.op-row`, fara butoane Adauga/+ —
|
||||
selectul adauga instant la `change` (POST /form-chips, chips_action=add).
|
||||
|
||||
Starea chip-urilor traieste in input-uri hidden din form (NU in DB mid-edit).
|
||||
Fiecare operatie are un picker propriu cand e nemapata (E4 binding op<->cod).
|
||||
Reveal odometru initial semnalat prin data-has-r-odo="true" si chip-warn pe R-ODO/I-ODO.
|
||||
Diagrama starii per item (chips_state.items[i]):
|
||||
cod="" & exclus=0 -> chip warning (warn OP-X . fara cod), click = tinta
|
||||
cod="" & exclus=1 -> chip Nedeclarat (line-through), x -> readuce warning
|
||||
cod!="" (op="" sau op!="") -> chip cod (eticheta COD, tooltip = operatia sursa
|
||||
sau "cod adaugat manual")
|
||||
|
||||
Stare persistata intr-UN SINGUR hidden `chips_state` (JSON versionat)
|
||||
+ hidden `chips_target_index` (tinta curenta; comutata CLIENT-SIDE la click pe
|
||||
un chip warning, fara round-trip — indexul pleaca la server abia la urmatorul
|
||||
add) + hidden `chips_submission_id` (ecouat ca sa "salveaza ca regula" ramana
|
||||
disponibil dupa re-randari via /form-chips).
|
||||
|
||||
Context vars (toate cu defaults):
|
||||
prestatii_chips — list of {cod_prestatie, cod_op_service, denumire}
|
||||
nomenclator_rar — list of {cod_prestatie, nume_prestatie} pentru picker
|
||||
has_r_odo — True daca orice chip e R-ODO sau I-ODO (server-computed)
|
||||
form_chips_url — URL pentru HTMX; default '/form-chips'
|
||||
chips_section_id — ID div (default 'chips-section')
|
||||
csrf_token — CSRF (trecut prin hx-include din form parinte)
|
||||
prestatii_chips — list of {cod_prestatie, cod_op_service, denumire, exclus}
|
||||
nomenclator_rar — list of {cod_prestatie, nume_prestatie} pentru select
|
||||
has_r_odo — True daca vreun chip e R-ODO/I-ODO (server-computed)
|
||||
chips_added_index — index-ul chip-ului tocmai adaugat (emfaza chip-nou)
|
||||
chips_target_index — index-ul operatiei tinta curente (sau none)
|
||||
chips_submission_id — id submission cand chips sunt in modalul de detaliu
|
||||
(activeaza butonul "salveaza ca regula")
|
||||
form_chips_url — URL pentru HTMX; default '/form-chips'
|
||||
chips_section_id — ID div (default 'chips-section')
|
||||
csrf_token — CSRF (trecut prin hx-include din form parinte)
|
||||
#}
|
||||
{% set _chips_url = form_chips_url or '/form-chips' %}
|
||||
{% set _sec_id = chips_section_id or 'chips-section' %}
|
||||
{% set _chips = prestatii_chips or [] %}
|
||||
{% set _has_ops = _chips | selectattr('cod_op_service') | list | length > 0 %}
|
||||
{# chips_submission_id e setat din _detaliu_ctx cand chips sunt randate in modalul de detaliu.
|
||||
Lipseste cand _chips_prestatii.html e rerandat via /form-chips (stateless, fara submission). #}
|
||||
{% set _sub_id = chips_submission_id if chips_submission_id is defined else none %}
|
||||
{% set _sub_id = chips_submission_id if (chips_submission_id is defined and chips_submission_id) else '' %}
|
||||
{% set _target_idx = chips_target_index if (chips_target_index is defined and chips_target_index is not none) else none %}
|
||||
{% set _target_op = (_chips[_target_idx].cod_op_service or '') if (_target_idx is not none and _target_idx < _chips | length) else '' %}
|
||||
|
||||
<div id="{{ _sec_id }}" data-has-r-odo="{{ 'true' if has_r_odo else 'false' }}"
|
||||
<div id="{{ _sec_id }}" class="chips-editor" data-has-r-odo="{{ 'true' if has_r_odo else 'false' }}"
|
||||
aria-live="polite" aria-label="Prestatii cod RAR">
|
||||
|
||||
{# ===== Input-uri hidden pentru starea curenta a chip-urilor =====
|
||||
TOATE itemele emit 3 hidden inputs (cod poate fi "" pentru unmapped).
|
||||
Paralele index-by-index: cod_prestatie[i], chip_op_service[i], chip_denumire[i].
|
||||
Filtrate la submit de post_corectie_trimitere (coduri goale = neschimbate). #}
|
||||
{% for chip in _chips %}
|
||||
<input type="hidden" name="cod_prestatie" value="{{ chip.cod_prestatie or '' }}">
|
||||
<input type="hidden" name="chip_op_service" value="{{ chip.cod_op_service or '' }}">
|
||||
<input type="hidden" name="chip_denumire" value="{{ chip.denumire or '' }}">
|
||||
{% endfor %}
|
||||
{# ===== Stare: UN SINGUR hidden JSON versionat + tinta + submission id ===== #}
|
||||
<input type="hidden" name="chips_state" value='{{ chips_state_json(_chips) }}'>
|
||||
<input type="hidden" name="chips_target_index" id="chips-target-index"
|
||||
value="{{ _target_idx if _target_idx is not none else '' }}">
|
||||
<input type="hidden" name="chips_submission_id" value="{{ _sub_id }}">
|
||||
|
||||
<div class="camp-slim" style="margin-bottom:8px;">
|
||||
<label>Prestatii — cod RAR pe fiecare operatie</label>
|
||||
<label>Prestatii — cod RAR</label>
|
||||
|
||||
{% if _has_ops %}
|
||||
{# ===== Mod operatii: UN picker PE operatie (E4 binding) ===== #}
|
||||
{% for chip in _chips %}
|
||||
{% if chip.cod_op_service %}
|
||||
{% set _is_warn = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
|
||||
{% set _nemapat = not chip.cod_prestatie %}
|
||||
{% set _e_nou = (chips_added_cod is defined) and chips_added_cod and chip.cod_prestatie == chips_added_cod %}
|
||||
<div class="op-row {% if _nemapat %}op-row-warn{% endif %}" style="margin-bottom:6px;">
|
||||
<span class="op-row-name">
|
||||
{{ chip.cod_op_service }}
|
||||
{% if chip.denumire and chip.denumire != chip.cod_op_service %}
|
||||
<span class="muted" style="font-weight:400;font-size:11px;"> — {{ chip.denumire }}</span>
|
||||
{% endif %}
|
||||
{% if _nemapat %}
|
||||
<span style="color:var(--warn);font-size:10px;font-weight:400;"> · lipsa cod</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span style="display:flex;align-items:center;gap:8px;">
|
||||
{% if chip.cod_prestatie %}
|
||||
{# ===== Operatie mapata: chip cu × ===== #}
|
||||
<span class="chip {% if _is_warn %}chip-warn{% endif %} {% if _e_nou %}chip-nou{% endif %}"
|
||||
aria-label="Prestatie {{ chip.cod_prestatie }} adaugata pentru {{ chip.cod_op_service }}">
|
||||
{{ chip.cod_prestatie }}
|
||||
<button type="button" class="chip-del"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-vals='{"chips_action":"remove","chips_remove_index":{{ loop.index0 }}}'
|
||||
aria-label="Sterge codul {{ chip.cod_prestatie }} pentru {{ chip.cod_op_service }}">
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
{# "salveaza ca regula op->cod" — apare doar cand submission_id e cunoscut
|
||||
(in modalul de detaliu, nu la re-randarea stateless via /form-chips).
|
||||
Reuse EXACT save_mapping + reresolve_account via endpoint dedicat.
|
||||
hx-include="closest form" propaga csrf_token din form-ul parinte. #}
|
||||
<span id="save-rule-slot-{{ loop.index0 }}" class="save-rule-slot">
|
||||
{% if _sub_id and chip.cod_op_service and chip.cod_prestatie %}
|
||||
<button type="button"
|
||||
style="font-size:10px;color:var(--muted);background:none;border:none;cursor:pointer;text-decoration:underline;padding:0;margin-left:4px;line-height:1;"
|
||||
hx-post="/trimitere/{{ _sub_id }}/salveaza-regula-chip"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#detaliu-modal-body"
|
||||
hx-swap="innerHTML"
|
||||
hx-vals='{"salveaza_op":{{ chip.cod_op_service | tojson }},"salveaza_cod":{{ chip.cod_prestatie | tojson }}}'
|
||||
aria-label="Salveaza regula {{ chip.cod_op_service }} -> {{ chip.cod_prestatie }}">
|
||||
salveaza ca regula
|
||||
</button>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% else %}
|
||||
{# ===== Operatie nemapata: picker galben cu "alege cod RAR" ===== #}
|
||||
<select name="chips_add_cod_{{ loop.index0 }}"
|
||||
id="picker-op-{{ loop.index0 }}"
|
||||
aria-label="Alege cod RAR pentru {{ chip.cod_op_service }}"
|
||||
style="min-width:160px;font-size:11px;height:26px;">
|
||||
<option value="">— alege cod RAR —</option>
|
||||
{% for n in (nomenclator_rar or []) %}
|
||||
<option value="{{ n.cod_prestatie }}">{{ n.cod_prestatie }} — {{ n.nume_prestatie }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button"
|
||||
class="add-code"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-vals='{"chips_action":"add","chips_add_op_index":{{ loop.index0 }}}'
|
||||
aria-label="Adauga cod RAR pentru {{ chip.cod_op_service }}">
|
||||
+ Adauga
|
||||
</button>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<div class="chips" role="group" aria-label="Prestatii cod RAR">
|
||||
{% for chip in _chips %}
|
||||
{% set _idx = loop.index0 %}
|
||||
{% set _is_target = _target_idx is not none and _idx == _target_idx %}
|
||||
|
||||
{# ===== Chips extra + picker '+ Adauga alta operatie / cod RAR' in mod operatii ===== #}
|
||||
{# Chips extra: cod_op_service gol, cod_prestatie setat — afisate flat cu × (reuse remove_flat).
|
||||
Containerul .chips se randeaza DOAR cand exista chips extra — altfel ramanea
|
||||
un chenar gol nefinisat sub randurile de operatie. #}
|
||||
{% set _extra_chips = _chips | rejectattr('cod_op_service') | selectattr('cod_prestatie') | list %}
|
||||
{% if _extra_chips %}
|
||||
<div class="chips" role="group" aria-label="Coduri RAR suplimentare" style="margin-top:4px;">
|
||||
{% for chip in _extra_chips %}
|
||||
{% set _is_warn_extra = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
|
||||
{% set _e_nou_extra = (chips_added_cod is defined) and chips_added_cod and chip.cod_prestatie == chips_added_cod %}
|
||||
<span class="chip {% if _is_warn_extra %}chip-warn{% endif %} {% if _e_nou_extra %}chip-nou{% endif %}"
|
||||
aria-label="Cod RAR suplimentar {{ chip.cod_prestatie }}">
|
||||
{{ chip.cod_prestatie }}
|
||||
{% if chip.cod_op_service and not chip.cod_prestatie and not chip.exclus %}
|
||||
{# ===== Chip warning: operatie nemapata — click comuta tinta client-side ===== #}
|
||||
{% set _label = chip.cod_op_service %}
|
||||
{% set _label_scurt = (_label[:18] ~ '…') if _label | length > 18 else _label %}
|
||||
{% set _title = chip.cod_op_service ~ (' — ' ~ chip.denumire if chip.denumire and chip.denumire != chip.cod_op_service else '') %}
|
||||
<button type="button"
|
||||
class="chip chip-op-warn {% if _is_target %}is-target{% endif %}"
|
||||
data-chip-idx="{{ _idx }}"
|
||||
data-op-nume="{{ chip.cod_op_service }}"
|
||||
title="{{ _title }}"
|
||||
onclick="window.chipsSetTarget && window.chipsSetTarget(this, {{ _idx }})"
|
||||
aria-pressed="{{ 'true' if _is_target else 'false' }}"
|
||||
aria-label="Operatia {{ chip.cod_op_service }} nu are cod RAR{% if _is_target %} (tinta curenta){% endif %}">
|
||||
⚠ {{ _label_scurt }} · fara cod
|
||||
</button>
|
||||
|
||||
{% elif chip.exclus %}
|
||||
{# ===== Chip Nedeclarat: vizibil (line-through), x readuce warning-ul ===== #}
|
||||
<span class="chip chip-nedeclarat"
|
||||
title="{{ chip.cod_op_service or 'Cod liber' }} — nu se declara la RAR"
|
||||
aria-label="{{ chip.cod_op_service or 'Operatia' }} nu se declara la RAR">
|
||||
Nedeclarat
|
||||
<button type="button" class="chip-del"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-indicator="#{{ _sec_id }}"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-vals='{"chips_action":"remove_flat","chips_remove_cod":"{{ chip.cod_prestatie }}"}'
|
||||
aria-label="Sterge codul suplimentar {{ chip.cod_prestatie }}">×</button>
|
||||
hx-vals='{"chips_action":"remove","chips_remove_index":{{ _idx }}}'
|
||||
aria-label="Anuleaza Nedeclarat pentru {{ chip.cod_op_service or 'cod liber' }} (readuce avertismentul)">
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
|
||||
{% elif chip.cod_prestatie %}
|
||||
{# ===== Chip cod: legat de operatie sau liber ===== #}
|
||||
{% set _is_warn = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
|
||||
{% set _e_nou = (chips_added_index is defined) and chips_added_index is not none and _idx == chips_added_index %}
|
||||
{% set _tooltip = (chip.cod_op_service ~ (' — ' ~ chip.denumire if chip.denumire and chip.denumire != chip.cod_op_service else '')) if chip.cod_op_service else 'cod adaugat manual' %}
|
||||
<span class="chip {% if _is_warn %}chip-warn{% endif %} {% if _e_nou %}chip-nou{% endif %}"
|
||||
title="{{ _tooltip }}"
|
||||
aria-label="Prestatie {{ chip.cod_prestatie }}{% if chip.cod_op_service %} pentru {{ chip.cod_op_service }}{% endif %}">
|
||||
<span class="chip-cod-label">COD</span> {{ chip.cod_prestatie }}
|
||||
<button type="button" class="chip-del"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-indicator="#{{ _sec_id }}"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-vals='{"chips_action":"remove","chips_remove_index":{{ _idx }}}'
|
||||
aria-label="Sterge codul {{ chip.cod_prestatie }}{% if chip.cod_op_service %} pentru {{ chip.cod_op_service }}{% endif %}">
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
{# "salveaza ca regula op->cod" — apare doar cand submission_id e cunoscut
|
||||
(modalul de detaliu, nu la re-randarea stateless via /form-chips fara ecou). #}
|
||||
{% if _sub_id and chip.cod_op_service and chip.cod_prestatie %}
|
||||
<span id="save-rule-slot-{{ _idx }}" class="save-rule-slot">
|
||||
<button type="button"
|
||||
style="font-size:10px;color:var(--muted);background:none;border:none;cursor:pointer;text-decoration:underline;padding:0;margin-left:-2px;line-height:1;"
|
||||
hx-post="/trimitere/{{ _sub_id }}/salveaza-regula-chip"
|
||||
hx-disabled-elt="this"
|
||||
hx-indicator="#{{ _sec_id }}"
|
||||
hx-include="closest form"
|
||||
hx-target="#detaliu-modal-body"
|
||||
hx-swap="innerHTML"
|
||||
hx-vals='{"salveaza_op":{{ chip.cod_op_service | tojson }},"salveaza_cod":{{ chip.cod_prestatie | tojson }}}'
|
||||
aria-label="Salveaza regula {{ chip.cod_op_service }} -> {{ chip.cod_prestatie }}">
|
||||
salveaza ca regula
|
||||
</button>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if nomenclator_rar %}
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;margin-top:4px;">
|
||||
<select name="chips_add_cod_flat"
|
||||
aria-label="Adauga cod RAR suplimentar"
|
||||
style="min-width:160px;font-size:11px;height:26px;border:1px dashed color-mix(in srgb,var(--accent) 55%,var(--line));border-radius:5px;background:transparent;color:var(--accent);">
|
||||
<option value="">+ Adauga alta operatie / cod RAR</option>
|
||||
{% for n in nomenclator_rar %}
|
||||
|
||||
{# ===== Selectul unic: adauga instant la schimbare, placeholder numeste tinta ===== #}
|
||||
{% if nomenclator_rar %}
|
||||
<select name="chips_pick"
|
||||
id="chips-picker"
|
||||
class="chips-picker"
|
||||
hx-trigger="change"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-include="closest form"
|
||||
hx-disabled-elt="this"
|
||||
hx-indicator="#{{ _sec_id }}"
|
||||
hx-vals='{"chips_action":"add"}'
|
||||
aria-label="{% if _target_op %}Alege cod RAR pentru {{ _target_op }}{% else %}Adauga cod RAR{% endif %}">
|
||||
<option value="">{% if _target_op %}Cod pentru {{ _target_op }} — alege{% else %}— adauga cod RAR —{% endif %}</option>
|
||||
{% if _target_idx is not none %}
|
||||
{% set _sugestii = sugestii_tinta if (sugestii_tinta is defined and sugestii_tinta) else [] %}
|
||||
{% if _sugestii %}
|
||||
<optgroup label="Sugestii">
|
||||
{% for s in _sugestii %}
|
||||
<option value="{{ s.cod_prestatie }}">{{ s.cod_prestatie }} — {{ s.nume_prestatie }}</option>
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endif %}
|
||||
<option value="{{ EXCLUDE_SENTINEL }}">Nu se declara la RAR (exclude operatia)</option>
|
||||
{% endif %}
|
||||
{% for n in (nomenclator_rar or []) %}
|
||||
<option value="{{ n.cod_prestatie }}">{{ n.cod_prestatie }} — {{ n.nume_prestatie }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button"
|
||||
class="add-code"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-vals='{"chips_action":"add_extra"}'
|
||||
aria-label="Adauga cod RAR suplimentar la trimitere">
|
||||
+
|
||||
</button>
|
||||
</span>
|
||||
{% else %}
|
||||
{# T-D1/T-E5 (5.16): empty state in mod operatii cand nomenclatorul lipseste #}
|
||||
<div class="chips-nom-gol" style="font-size:11px;color:var(--warn);padding:4px 0;margin-top:4px;">
|
||||
Nomenclator indisponibil — adaugarea de coduri suplimentare nu e posibila.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{# ===== Mod plat: lista de coduri libere (corectie pura, fara op_service) ===== #}
|
||||
<div class="chips" role="group" aria-label="Coduri RAR selectate">
|
||||
{% for chip in _chips %}
|
||||
{% if chip.cod_prestatie %}
|
||||
{% set _is_warn_flat = chip.cod_prestatie in ('R-ODO', 'I-ODO') %}
|
||||
{% set _e_nou_flat = (chips_added_cod is defined) and chips_added_cod and chip.cod_prestatie == chips_added_cod %}
|
||||
<span class="chip {% if _is_warn_flat %}chip-warn{% endif %} {% if _e_nou_flat %}chip-nou{% endif %}"
|
||||
aria-label="Prestatie {{ chip.cod_prestatie }}">
|
||||
{{ chip.cod_prestatie }}
|
||||
<button type="button" class="chip-del"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-vals='{"chips_action":"remove_flat","chips_remove_cod":"{{ chip.cod_prestatie }}"}'
|
||||
aria-label="Sterge codul {{ chip.cod_prestatie }}">×</button>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{# Picker adaugare cod nou in mod plat #}
|
||||
{% if nomenclator_rar %}
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;">
|
||||
<select name="chips_add_cod_flat"
|
||||
aria-label="Adauga cod RAR nou"
|
||||
style="font-size:11px;height:22px;border:1px dashed color-mix(in srgb,var(--accent) 55%,var(--line));border-radius:5px;background:transparent;color:var(--accent);">
|
||||
<option value="">+ cod</option>
|
||||
{% for n in nomenclator_rar %}
|
||||
<option value="{{ n.cod_prestatie }}">{{ n.cod_prestatie }} — {{ n.nume_prestatie }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="button"
|
||||
class="add-code"
|
||||
hx-post="{{ _chips_url }}"
|
||||
hx-disabled-elt="this"
|
||||
hx-include="closest form"
|
||||
hx-target="#{{ _sec_id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-vals='{"chips_action":"add_flat"}'
|
||||
aria-label="Adauga cod RAR selectat in lista">
|
||||
+
|
||||
</button>
|
||||
</span>
|
||||
{% else %}
|
||||
{# T-D1/T-E5 (5.16): empty state in mod plat cand nomenclatorul lipseste #}
|
||||
{# T-D1/T-E5 (5.16): empty state cand nomenclatorul lipseste #}
|
||||
<div class="chips-nom-gol" style="font-size:11px;color:var(--warn);padding:4px 0;">
|
||||
Nomenclator indisponibil — nu se pot adauga coduri RAR momentan.
|
||||
Nomenclator indisponibil — adaugarea de coduri RAR nu e posibila.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Rezultatul ultimei adaugari, in ambele moduri: refuz (rol=alert) sau confirmare.
|
||||
{# Rezultatul ultimei adaugari: refuz (rol=alert) sau confirmare.
|
||||
Clasa chips-extra-error pastrata pe eroare (selector stabil pentru teste/QA). #}
|
||||
{% if chips_error is defined and chips_error %}
|
||||
<div class="chips-extra-error" style="font-size:11px;color:var(--err);padding:2px 0;" role="alert">
|
||||
@@ -238,12 +180,40 @@
|
||||
{{ chips_ok }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Hint discret fara chips (debut) #}
|
||||
{% if not _chips %}
|
||||
<div style="font-size:10px;color:var(--muted);padding:4px 0;">
|
||||
Niciun cod RAR inca — alege din picker (sus) sau adauga prin mapare.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
if (window.__chipsUnifiedBound) { return; }
|
||||
window.__chipsUnifiedBound = true;
|
||||
|
||||
// Click pe chip warning -> comuta tinta CLIENT-SIDE (fara round-trip). Click
|
||||
// pe tinta activa = no-op (nu se poate "deselecta" tinta cat exista warnings).
|
||||
window.chipsSetTarget = function (btn, idx) {
|
||||
var hidden = document.getElementById('chips-target-index');
|
||||
if (!hidden) { return; }
|
||||
if (String(idx) === hidden.value) { return; }
|
||||
hidden.value = String(idx);
|
||||
document.querySelectorAll('.chip-op-warn.is-target').forEach(function (el) {
|
||||
el.classList.remove('is-target');
|
||||
el.setAttribute('aria-pressed', 'false');
|
||||
});
|
||||
btn.classList.add('is-target');
|
||||
btn.setAttribute('aria-pressed', 'true');
|
||||
var picker = document.getElementById('chips-picker');
|
||||
if (picker && picker.options.length) {
|
||||
picker.options[0].textContent = 'Cod pentru ' + (btn.dataset.opNume || '') + ' — alege';
|
||||
}
|
||||
};
|
||||
|
||||
// Focus restaurat pe select dupa fiecare swap outerHTML al sectiunii.
|
||||
document.body.addEventListener('htmx:afterSettle', function (evt) {
|
||||
var t = evt.target;
|
||||
if (t && t.classList && t.classList.contains('chips-editor')) {
|
||||
var el = document.getElementById('chips-picker');
|
||||
if (el) { el.focus(); }
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -828,19 +828,22 @@
|
||||
.chip-warn { background:color-mix(in srgb, var(--warn) 22%, transparent); color:var(--warn); }
|
||||
/* Chip abia adaugat via /form-chips — confirmare vizibila a actiunii "+" */
|
||||
.chip-nou { outline:2px solid color-mix(in srgb, var(--ok) 45%, transparent); outline-offset:1px; }
|
||||
/* .add-code — buton dashed pentru adaugare cod in chipbox */
|
||||
.add-code { display:inline-flex; align-items:center; height:22px; padding:0 7px; background:transparent;
|
||||
border:1px dashed color-mix(in srgb, var(--accent) 55%, var(--line));
|
||||
border-radius:5px; color:var(--accent); font:500 10px var(--font-ui); cursor:pointer; }
|
||||
.add-code:hover, .add-code:focus-visible { border-style:solid; }
|
||||
/* .op-row — rand operatie cu picker op<->cod (E4): operatie + chip cod + picker */
|
||||
.op-row { display:flex; align-items:center; justify-content:space-between; gap:10px;
|
||||
padding:8px 10px; border:1px solid var(--line); border-radius:6px;
|
||||
background:var(--card2); margin-bottom:8px; }
|
||||
/* Nume operatie emfatic (T-9 5.16): proeminent (bold) ca in mockup — e ancora
|
||||
vizuala a randului de mapare op<->cod. */
|
||||
.op-row-name { font-size:var(--fs-sm); font-weight:700; color:var(--ink); }
|
||||
.op-row-warn { border-color:color-mix(in srgb, var(--warn) 45%, var(--line)); }
|
||||
/* Eticheta mono "COD" pe chip-ul de cod */
|
||||
.chip-cod-label { font-size:9px; font-weight:800; opacity:.65; letter-spacing:.03em; }
|
||||
/* Chip warning operatie nemapata: dashed implicit; tinta activa = solid + inel —
|
||||
singurul tratament "tare" din rand. */
|
||||
.chip-op-warn { background:color-mix(in srgb, var(--warn) 16%, transparent); color:var(--warn);
|
||||
border:1px dashed var(--warn); cursor:pointer; font-weight:600; }
|
||||
.chip-op-warn.is-target { border-style:solid; outline:2px solid color-mix(in srgb, var(--warn) 40%, transparent);
|
||||
outline-offset:1px; }
|
||||
.chip-op-warn:focus-visible { outline:2px solid var(--warn); outline-offset:1px; }
|
||||
/* Chip Nedeclarat: vizibil (line-through), NU mut/invizibil — cea mai riscanta
|
||||
actiune legala (exclude de la declararea RAR) nu poate fi cea mai discreta. */
|
||||
.chip-nedeclarat { background:var(--card2); color:var(--muted); border:1px dashed var(--line);
|
||||
text-decoration:line-through; }
|
||||
/* Sectiunea chips inerta pe durata unui request /form-chips in zbor:
|
||||
click pe x/warning mid-flight nu se pierde, doar se ignora. */
|
||||
.chips-editor.htmx-request { pointer-events:none; opacity:.7; }
|
||||
/* Mobil: fara header, randul revine la 2 linii (placuta / cod·operatie·data),
|
||||
cu actiune + litera mediu + bulina status in dreapta. Grid-areas peste
|
||||
aceleasi celule; .c-meta redevine linie flex cu separatoare "·". */
|
||||
|
||||
Reference in New Issue
Block a user