feat(ux): import compact + preview format Trimiteri + navigatie + scoatere auto_send (5.11)
8 stories TDD (echipa Sonnet, lead orchestreaza). US-001 scoate hold-ul auto_send din mapare (has_no_auto_send->False, simbol pastrat; cod rezolvat->queued). US-002 scoate bifa auto_send din UI. US-003 preview pas 3 in format .tabel-trimiteri (STARI_PREVIEW + nota_umana_preview, fara repr Python; view-model prez). US-004 filtre layout/stil ca referinta + buton Custom. US-005 navigatie Trimiteri/Mapari sub contoare pe toate paginile. US-006 import <details> nativ colapsabil. US-007 post-commit reveal (OOB _coada/_status + HX-Trigger). US-008 auto-refresh dupa actiuni (nudge eliminat). VERIFY context curat PASS (8/8). /code-review high: 3 buguri reparate (tab nav la self-refresh, pill Custom valori stale, nota_umana_preview precedenta needs_mapping). 934 passed, 1 skipped. Backend trimitere + schema NEATINSE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
18
TODOS.md
Normal file
18
TODOS.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# TODOS
|
||||
|
||||
Elemente deferate din review-uri. Negrupte de un PRD curent; de promovat cand devin prioritare.
|
||||
|
||||
## Din /autoplan PRD 5.11 (2026-06-26)
|
||||
|
||||
- [ ] **E2E smoke de first-run ca poarta de release** — codifica scriptul de dogfooding
|
||||
(import -> mapcoloane -> preview -> commit -> lista apare + contoare) ca test E2E care
|
||||
trebuie sa treaca inainte de orice release. Motiv: cele 8 bug-uri din 5.11 sunt toate
|
||||
first-run friction nedogfooded end-to-end; fara o poarta, reapar ca 8 tichete noi.
|
||||
(CEO F2, severitate high.)
|
||||
|
||||
- [ ] **Control compensator optional pe auto-trimitere unattended** — utilizatorul a ales
|
||||
(2026-06-26) scoaterea completa a hold-ului auto_send. Risc rezidual acceptat: o regula
|
||||
text gresita poate auto-trimite FINALIZATA (terminal, fara undo) pe randuri pe canalul API /
|
||||
remapare inline (fara gate de preview). Daca apar integratori reali, evalueaza un throttle
|
||||
„primele N auto-trimiteri pe o regula text noua cer confirmare" sau un kill-switch per cont.
|
||||
(CEO F5/F6, severitate critical ca risc, dar pre-launch exposure ~zero acum.)
|
||||
@@ -221,7 +221,7 @@ def _resolve_row_for_preview(
|
||||
errors = validate_prezentare(mapped)
|
||||
|
||||
if all_flags:
|
||||
# needs_review: chiar daca validarea trece, flagurile blocheaza auto-send
|
||||
# needs_review: validarea a trecut, dar flagurile (date ambigue, formule) cer confirmare manuala
|
||||
return {
|
||||
"resolved_status": "needs_review",
|
||||
"resolved": mapped,
|
||||
@@ -229,14 +229,7 @@ def _resolve_row_for_preview(
|
||||
"flags": all_flags,
|
||||
}
|
||||
|
||||
# auto_send gate
|
||||
if has_no_auto_send(resolved, mapping_meta):
|
||||
return {
|
||||
"resolved_status": "needs_mapping",
|
||||
"resolved": mapped,
|
||||
"errors": [{"auto_send": "cod mapat cu auto_send=0; review manual inainte de trimitere"}],
|
||||
"flags": all_flags,
|
||||
}
|
||||
# US-001 (PRD 5.11): ramura auto_send eliminata din preview.
|
||||
|
||||
if errors:
|
||||
return {
|
||||
|
||||
@@ -87,8 +87,10 @@ def _erori_nemapate(unmapped: list[dict]) -> list[dict]:
|
||||
def _motiv_clasificare(cl: dict) -> str | None:
|
||||
"""Rezumat uman pe o linie pentru un rezultat de clasificare.
|
||||
|
||||
None cand status='queued'. Acopera toate ramurile de blocaj: erori de continut
|
||||
(needs_data), coduri nemapate (needs_mapping) si auto_send oprit (needs_mapping).
|
||||
None cand status='queued'. Acopera ramurile de blocaj: erori de continut
|
||||
(needs_data) si coduri fara mapare RAR (needs_mapping).
|
||||
Dupa US-001: needs_mapping apare EXCLUSIV cand unmapped e non-gol
|
||||
(ramura auto_send_oprit era inaccesibila si a fost eliminata).
|
||||
"""
|
||||
if cl["status"] == "queued":
|
||||
return None
|
||||
@@ -99,8 +101,6 @@ def _motiv_clasificare(cl: dict) -> str | None:
|
||||
if cl["unmapped"]:
|
||||
coduri = ", ".join((u.get("cod_op_service") or "") for u in cl["unmapped"])
|
||||
return f"Coduri fara mapare RAR: {coduri}"
|
||||
if cl["status"] == "needs_mapping":
|
||||
return "Cod cu trimitere automata oprita; confirmare manuala inainte de trimitere."
|
||||
return None
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ def create_prezentari(
|
||||
conn = get_connection()
|
||||
results: list[SubmissionResult] = []
|
||||
try:
|
||||
# load_mapping_meta include auto_send per op (gate pentru coduri noi).
|
||||
# load_mapping_meta incarca maparea op->cod RAR; dupa US-001, auto_send nu mai tine randuri.
|
||||
mapping_meta = load_mapping_meta(conn, acct)
|
||||
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
|
||||
# Validare cod_prestatie fata de nomenclator + modul la cod necunoscut/nemapat.
|
||||
@@ -230,7 +230,7 @@ def create_prezentari(
|
||||
continue
|
||||
|
||||
# Helper pur partajat cu dry-run: reproduce EXACT clasificarea
|
||||
# (canonicalize + mapare op->cod + validare + auto_send gate).
|
||||
# (canonicalize + mapare op->cod + validare; auto_send gate eliminat dupa US-001).
|
||||
cl = _classify_modal(content, mapping, mapping_meta, valid_codes, error_mode, text_rules)
|
||||
if cl["blocked_error"]:
|
||||
# on_unmapped_error=True: respinge fara enqueue (cod necunoscut/nemapat).
|
||||
|
||||
@@ -249,10 +249,8 @@ def resolve_prestatii(
|
||||
# Adnotare aditiva: marcheaza ca rezolvat-prin-regula cu pattern-ul
|
||||
# sursa. Payload-harmless (RAR citeste doar cod_prestatie).
|
||||
it["cod_sursa"] = f"{COD_SURSA_TEXT_RULE_PREFIX}{pattern_regula or ''}"
|
||||
# Siguranta: regula cu auto_send=0 rezolva codul dar TINE randul
|
||||
# pentru verificare umana (has_no_auto_send -> True).
|
||||
if not auto_send_regula:
|
||||
it["regula_fara_autosend"] = True
|
||||
# US-001 (PRD 5.11): regula_fara_autosend nu se mai seteaza;
|
||||
# auto_send nu mai tine randul (has_no_auto_send neutralizat).
|
||||
else:
|
||||
it["cod_prestatie"] = None
|
||||
unmapped.append({"cod_op_service": op, "denumire": it.get("denumire")})
|
||||
@@ -410,14 +408,9 @@ def classify_prezentare(
|
||||
if errors:
|
||||
status = "needs_data"
|
||||
rar_error = json.dumps(errors, ensure_ascii=False)
|
||||
elif has_no_auto_send(resolved, mapping_meta):
|
||||
status = "needs_mapping"
|
||||
mesaj = "cod mapat cu auto_send=0; review manual inainte de trimitere"
|
||||
rar_error = json.dumps(
|
||||
{"auto_send": mesaj, **err_mod.eroare("AUTO_SEND_OPRIT", cauza=mesaj)},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
else:
|
||||
# US-001 (PRD 5.11): ramura AUTO_SEND_OPRIT eliminata.
|
||||
# Un cod rezolvat (mapare exacta sau regula text) -> queued direct.
|
||||
status = "queued"
|
||||
rar_error = None
|
||||
|
||||
@@ -432,20 +425,14 @@ def classify_prezentare(
|
||||
|
||||
|
||||
def has_no_auto_send(resolved: list[dict], mapping_meta: dict[str, dict]) -> bool:
|
||||
"""Verifica daca vreun item rezolvat are auto_send=0 (mapare exacta SAU regula text).
|
||||
"""Neutralizat dupa US-001 (PRD 5.11): auto_send nu mai tine randuri in needs_mapping.
|
||||
|
||||
Un cod nou-mapat (operations_mapping) cu auto_send=0 nu trebuie trimis automat.
|
||||
La fel pentru un item rezolvat printr-o REGULA TEXT cu auto_send=0 — marcat de
|
||||
`resolve_prestatii` cu `regula_fara_autosend`. In ambele cazuri randul ramane
|
||||
needs_mapping (review manual) pana cand operatorul activeaza „In coada".
|
||||
Items cu cod_prestatie direct (nu via cod_op_service/regula) nu sunt afectate.
|
||||
Simbolul este PASTRAT (importat in routes.py si import_router.py); stergerea
|
||||
ar produce ImportError la boot. Functia intoarce mereu False — codul rezolvat
|
||||
intra direct in queued, indiferent de valoarea auto_send din mapping_meta.
|
||||
|
||||
Coloanele DB raman cu default=1 (migrare non-distructiva).
|
||||
"""
|
||||
for item in resolved:
|
||||
if item.get("regula_fara_autosend"):
|
||||
return True
|
||||
op = (item.get("cod_op_service") or "").strip()
|
||||
if op and op in mapping_meta and not mapping_meta[op]["auto_send"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -660,18 +647,8 @@ def reresolve_account(conn, account_id: int | None, batch_id: int | None = None)
|
||||
stats["still_blocked"] += 1
|
||||
continue
|
||||
|
||||
# Verifica auto_send inainte de re-queuing.
|
||||
if has_no_auto_send(resolved, mapping_meta):
|
||||
conn.execute(
|
||||
"UPDATE submissions SET payload_json=?, rar_error=?, updated_at=datetime('now') WHERE id=?",
|
||||
(
|
||||
payload_json,
|
||||
json.dumps({"auto_send": "cod mapat cu auto_send=0; review manual inainte de trimitere"}, ensure_ascii=False),
|
||||
r["id"],
|
||||
),
|
||||
)
|
||||
stats["review_manual"] += 1
|
||||
continue
|
||||
# US-001 (PRD 5.11): ramura auto_send eliminata din reresolve.
|
||||
# Un cod rezolvat -> queued direct (review_manual ramane 0).
|
||||
|
||||
errors = validate_prezentare(content)
|
||||
if errors:
|
||||
|
||||
@@ -327,6 +327,80 @@ def parse_erori(rar_error: object) -> list[dict]:
|
||||
return [{"problema": str(data)[:200], "cauza": "", "fix": "", "field": None}]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Etichete stari preview import (vocabular DIFERIT de starile de submission)
|
||||
#
|
||||
# Starile de preview (ok/needs_review/already_sent/duplicate_in_file) NU
|
||||
# exista in STARI_SUBMISSION — reutilizarea directa a eticheta_stare/eticheta_scurta
|
||||
# ridica KeyError. Acest map este sursa de adevar pentru stratul de adaptare din
|
||||
# _web_compute_preview (routes.py) si pentru template (_preview_rand.html).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
STARI_PREVIEW: dict[str, tuple[str, str]] = {
|
||||
"ok": ("Gata de trimis", "s-ok"),
|
||||
"needs_review": ("Verifica valori", "s-needs_review"),
|
||||
"needs_mapping": ("Cod RAR lipsa", "s-needs_mapping"),
|
||||
"needs_data": ("Date incomplete", "s-needs_data"),
|
||||
"already_sent": ("Deja trimis", "s-already_sent"),
|
||||
"duplicate_in_file": ("Duplicat in fisier", "s-duplicate_in_file"),
|
||||
}
|
||||
|
||||
|
||||
def nota_umana_preview(status: str, errors: list, flags: list) -> str:
|
||||
"""Formateaza mesajul uman pentru coloana Note din tabelul de preview import.
|
||||
|
||||
Primeste ``errors`` ca lista Python (nu JSON string) — NU pasa la motiv_uman
|
||||
sau parse_erori care asteapta un JSON string si ar produce repr Python brut
|
||||
prin fallback ``raw[:160]`` (bug documentat in PRD 5.11 US-003).
|
||||
|
||||
Logica de prioritate:
|
||||
- already_sent / duplicate_in_file -> "" (template le afiseaza separat)
|
||||
- needs_mapping -> unmapped INAINTE de flags (codul lipsa e motivul real)
|
||||
- flags non-goale -> primul flag (needs_review: data ambigua etc.)
|
||||
- errors cu cheie "unmapped" -> "Cod RAR lipsa pentru: COD1, COD2"
|
||||
- errors cu field+message (needs_data) -> primul mesaj de validare
|
||||
- altceva -> ""
|
||||
|
||||
Fara exceptii. Trunchiat la 200 caractere.
|
||||
"""
|
||||
if status in ("already_sent", "duplicate_in_file"):
|
||||
return ""
|
||||
# needs_mapping: codul RAR lipseste — prioritizeaza 'unmapped' inaintea flags,
|
||||
# altfel un rand cu si un flag (ex. VIN numeric) ar afisa textul flag-ului
|
||||
# si ascunde motivul real (cod lipsa).
|
||||
if status == "needs_mapping":
|
||||
for e in errors:
|
||||
if not isinstance(e, dict):
|
||||
continue
|
||||
if "unmapped" in e:
|
||||
ops = e.get("unmapped") or []
|
||||
coduri = ", ".join(
|
||||
o.get("cod_op_service", "") for o in ops if isinstance(o, dict)
|
||||
)
|
||||
return ("Cod RAR lipsa pentru: " + coduri if coduri else "Cod RAR lipsa")
|
||||
if flags:
|
||||
return str(flags[0])[:200]
|
||||
for e in errors:
|
||||
if not isinstance(e, dict):
|
||||
continue
|
||||
if "unmapped" in e:
|
||||
ops = e.get("unmapped") or []
|
||||
coduri = ", ".join(
|
||||
o.get("cod_op_service", "") for o in ops if isinstance(o, dict)
|
||||
)
|
||||
return (f"Cod RAR lipsa pentru: {coduri}" if coduri else "Cod RAR lipsa")
|
||||
msg = (
|
||||
e.get("message")
|
||||
or e.get("msg")
|
||||
or e.get("problema")
|
||||
or e.get("cauza")
|
||||
or ""
|
||||
)
|
||||
if msg:
|
||||
return str(msg)[:200]
|
||||
return ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constante auxiliare (microcopy fix, fara logica)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -29,12 +29,14 @@ from ..payload_view import prezentare_din_payload
|
||||
from ..web.csrf import get_csrf_token, verify_csrf
|
||||
from .labels import (
|
||||
ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
|
||||
STARI_PREVIEW,
|
||||
eticheta_rar,
|
||||
eticheta_scurta,
|
||||
eticheta_stare,
|
||||
eticheta_worker,
|
||||
format_data_rar,
|
||||
motiv_uman,
|
||||
nota_umana_preview,
|
||||
parse_erori,
|
||||
)
|
||||
from ..web.session import require_login
|
||||
@@ -593,6 +595,40 @@ def _pills_categorii(counts: dict[str, int]) -> list[dict]:
|
||||
]
|
||||
|
||||
|
||||
def _build_status_ctx(request: Request, conn, account_id: int, *, oob: bool = False, tab_activ: str = "acasa") -> dict:
|
||||
"""Construieste dictionarul de context pentru _status.html.
|
||||
|
||||
Accepta o conexiune deja deschisa (nu deschide alta). Folosit de fragment_status
|
||||
si de web_confirma_import (OOB swap dupa commit).
|
||||
"""
|
||||
counts = _status_counts(conn, account_id)
|
||||
hb = read_heartbeat(conn)
|
||||
worker_alive = _worker_alive(hb)
|
||||
rar_state = _rar_state(hb, worker_alive)
|
||||
worker_lbl = eticheta_worker(worker_alive)
|
||||
rar_ok = rar_state == "ok"
|
||||
rar_lbl = eticheta_rar("ok" if rar_ok else rar_state)
|
||||
blocate_total = sum(counts.get(s, 0) for s in _BLOCKED)
|
||||
return {
|
||||
"request": request,
|
||||
"worker_lbl": worker_lbl,
|
||||
"rar_lbl": rar_lbl,
|
||||
"worker_ok": worker_alive,
|
||||
"rar_ok": rar_ok,
|
||||
"eticheta_ultima_auth": ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
|
||||
"last_login": format_data_rar(hb["last_rar_login_ok"] if hb else None),
|
||||
"counts_queued": counts.get("queued", 0),
|
||||
"counts_sent": counts.get("sent", 0),
|
||||
"blocate_total": blocate_total,
|
||||
"blocate_defalcat": _blocate_defalcat(counts),
|
||||
"pills_categorii": _pills_categorii(counts),
|
||||
"account_active": _account_active(conn, account_id),
|
||||
"tab_activ": tab_activ,
|
||||
"mapari_badge": counts.get("needs_mapping", 0),
|
||||
"oob": oob,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/_fragments/status", response_class=HTMLResponse)
|
||||
def fragment_status(request: Request) -> HTMLResponse:
|
||||
"""Bara de status persistenta cu etichete umane.
|
||||
@@ -604,34 +640,9 @@ def fragment_status(request: Request) -> HTMLResponse:
|
||||
account_id = require_login(request)
|
||||
conn = get_connection()
|
||||
try:
|
||||
counts = _status_counts(conn, account_id)
|
||||
hb = read_heartbeat(conn)
|
||||
worker_alive = _worker_alive(hb)
|
||||
rar_state = _rar_state(hb, worker_alive)
|
||||
|
||||
# Etichete umane pre-calculate (nu logica in template)
|
||||
worker_lbl = eticheta_worker(worker_alive)
|
||||
# eticheta_rar accepta "ok" sau orice alt string -> indisponibil/necunoscut
|
||||
rar_ok = rar_state == "ok"
|
||||
rar_lbl = eticheta_rar("ok" if rar_ok else rar_state)
|
||||
blocate_total = sum(counts.get(s, 0) for s in _BLOCKED)
|
||||
|
||||
return templates.TemplateResponse("_status.html", {
|
||||
"request": request,
|
||||
"worker_lbl": worker_lbl,
|
||||
"rar_lbl": rar_lbl,
|
||||
# Stari binare pentru bife accesibile: glifa + culoare
|
||||
"worker_ok": worker_alive,
|
||||
"rar_ok": rar_ok,
|
||||
"eticheta_ultima_auth": ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
|
||||
"last_login": format_data_rar(hb["last_rar_login_ok"] if hb else None),
|
||||
"counts_queued": counts.get("queued", 0),
|
||||
"counts_sent": counts.get("sent", 0),
|
||||
"blocate_total": blocate_total,
|
||||
"blocate_defalcat": _blocate_defalcat(counts),
|
||||
"pills_categorii": _pills_categorii(counts),
|
||||
"account_active": _account_active(conn, account_id),
|
||||
})
|
||||
tab_activ = request.query_params.get("tab", "acasa")
|
||||
ctx = _build_status_ctx(request, conn, account_id, tab_activ=tab_activ)
|
||||
return templates.TemplateResponse("_status.html", ctx)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -1163,21 +1174,7 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
|
||||
message="Lipseste inca un cod RAR — alege-l mai jos sau in tab-ul Mapari."),
|
||||
)
|
||||
|
||||
if has_no_auto_send(resolved, mapping_meta):
|
||||
conn.execute(
|
||||
"UPDATE submissions SET status='needs_mapping', payload_json=?, rar_error=?, "
|
||||
"updated_at=datetime('now') WHERE id=?",
|
||||
(payload_json,
|
||||
json.dumps({"auto_send": "cod mapat cu auto_send=0; review manual inainte de trimitere"},
|
||||
ensure_ascii=False),
|
||||
row["id"]),
|
||||
)
|
||||
row2 = _fetch_submission_scoped(conn, account_id, submission_id)
|
||||
return templates.TemplateResponse(
|
||||
"_trimitere_detaliu.html",
|
||||
_detaliu_ctx(request, row2, error=True,
|
||||
message="Cod cu auto-send oprit — confirma manual din tab-ul Mapari."),
|
||||
)
|
||||
# US-001 (PRD 5.11): ramura auto_send eliminata din corectie.
|
||||
|
||||
errors = validate_prezentare(content)
|
||||
if errors:
|
||||
@@ -2025,6 +2022,29 @@ def _web_compute_preview(
|
||||
except Exception:
|
||||
conn.execute("ROLLBACK")
|
||||
|
||||
# Enrichment UI: adauga campuri pre-computate necesare template-ului.
|
||||
# Toate consumatorii (preview complet, rand single via _preview_one_row)
|
||||
# obtin automat campurile adaugate aici.
|
||||
for row in preview_rows:
|
||||
# view-model prez (vehicul/operatie/cod RAR) — prezentare_din_payload
|
||||
# accepta dict direct (nu e nevoie de serializare/deserializare JSON).
|
||||
row["prez"] = prezentare_din_payload(row["resolved"])
|
||||
# Eticheta umana + clasa CSS pentru pill — din STARI_PREVIEW, nu STARI_SUBMISSION
|
||||
# (eticheta_stare ridica KeyError pe ok/already_sent/duplicate_in_file).
|
||||
_etq, _css = STARI_PREVIEW.get(
|
||||
row["resolved_status"],
|
||||
(row["resolved_status"], f"s-{row['resolved_status']}"),
|
||||
)
|
||||
row["stare_eticheta"] = _etq
|
||||
row["stare_css"] = _css
|
||||
# Nota umana formatata — errors e lista Python, NU JSON string;
|
||||
# nota_umana_preview o trateaza corect (fara repr Python brut in Note).
|
||||
row["nota_umana"] = nota_umana_preview(
|
||||
row["resolved_status"],
|
||||
row.get("errors") or [],
|
||||
row.get("flags") or [],
|
||||
)
|
||||
|
||||
nomenclator = load_nomenclator(conn)
|
||||
return {
|
||||
"rows": preview_rows,
|
||||
@@ -2750,18 +2770,33 @@ async def web_confirma_import(
|
||||
(n_enqueued, import_id),
|
||||
)
|
||||
|
||||
# Succes → bara de upload slim cu mesaj de confirmare. are_trimiteri=True:
|
||||
# contul tocmai a pus randuri in coada -> bara ramane slim si dezvaluie
|
||||
# sectiunea "Trimiterile tale" de pe Acasa.
|
||||
# Succes → bara de upload slim cu mesaj de confirmare + OOB swap al
|
||||
# #trimiteri-section (injecteaza _coada.html cu lista proaspata) +
|
||||
# header HX-Trigger: trimiteriChanged (declanseza reincarcarea automata).
|
||||
toctou_msg = f" ({len(toctou)} coliziuni TOCTOU excluse)" if toctou else ""
|
||||
return templates.TemplateResponse("_upload.html", _ctx(
|
||||
request,
|
||||
are_trimiteri=True,
|
||||
message=(
|
||||
succes_msg = (
|
||||
f"S-au pus in coada {n_enqueued} prezentari{toctou_msg}. "
|
||||
f"Procesarea incepe in cateva secunde — vezi mai jos, in Trimiterile tale."
|
||||
),
|
||||
))
|
||||
)
|
||||
|
||||
# Calculeaza contextele (necesita conn deschis) inainte de finally.
|
||||
acasa_ctx = _get_acasa_context(request, conn, account_id)
|
||||
acasa_ctx["status_filtru"] = ""
|
||||
acasa_ctx["oob"] = True # adauga hx-swap-oob="outerHTML" la <section>
|
||||
|
||||
status_ctx = _build_status_ctx(request, conn, account_id, oob=True)
|
||||
|
||||
# Randeaza imediat (conn inca deschis — query-urile s-au facut mai sus).
|
||||
upload_html = templates.get_template("_upload.html").render(
|
||||
_ctx(request, are_trimiteri=True, message=succes_msg)
|
||||
)
|
||||
coada_html = templates.get_template("_coada.html").render(acasa_ctx)
|
||||
status_html = templates.get_template("_status.html").render(status_ctx)
|
||||
|
||||
return HTMLResponse(
|
||||
content=upload_html + "\n" + coada_html + "\n" + status_html,
|
||||
headers={"HX-Trigger": "trimiteriChanged"},
|
||||
)
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
<div id="acasa-section">
|
||||
|
||||
{# === Centru de greutate: bara de upload (importul e operatia principala) === #}
|
||||
{# === Container colapsabil: stepper + upload intr-un singur element <details> (US-006).
|
||||
Serverul seteaza atributul `open` din are_trimiteri:
|
||||
are_trimiteri=False (first-run) → open (importul e vizibil imediat, fara JS)
|
||||
are_trimiteri=True (returning) → colapsat (nu ocupa ecranul, dar e accesibil la click)
|
||||
Degradare fara JS: corecta pe ambele ramuri.
|
||||
In timpul fluxului (mapcoloane/preview), HTMX face swap pe #import-section (descendentul
|
||||
intern) → <details> ramane neatins → containerul ramane deschis intre pasi. === #}
|
||||
<details id="import-details"{% if not are_trimiteri %} open{% endif %}>
|
||||
<summary>Importa un fisier</summary>
|
||||
{% include '_upload.html' %}
|
||||
</details>
|
||||
|
||||
{# === Subordonat: primii pasi pe un singur rand compact === #}
|
||||
{% set toti_esentiali = are_creds and are_trimiteri %}
|
||||
@@ -44,10 +53,14 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Sectiunea Trimiteri, permanenta sub upload. Suprimata la first-run (zero
|
||||
trimiteri): bara de upload acopera deja CTA-ul, iar empty-state-ul ar fi redundant. #}
|
||||
{# Sectiunea Trimiteri, permanenta sub upload.
|
||||
La first-run (zero trimiteri), randam un placeholder <section> gol/ascuns — necesar
|
||||
ca OOB swap-ul de la confirma sa gaseasca tinta valida in DOM si sa injecteze
|
||||
_coada.html fara reload complet. Fara placeholder, HTMX ignora silentios OOB-ul. #}
|
||||
{% if are_trimiteri %}
|
||||
{% include '_coada.html' %}
|
||||
{% else %}
|
||||
<section id="trimiteri-section" hidden></section>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
Filtre + tabel (_submissions.html); detaliul se deschide in modalul global (#modal-detaliu).
|
||||
#}
|
||||
<section id="trimiteri-section" aria-labelledby="trimiteri-heading"
|
||||
style="margin-top:22px; padding-top:18px; border-top:2px solid var(--line);">
|
||||
style="margin-top:22px; padding-top:18px; border-top:2px solid var(--line);"
|
||||
{% if oob %}hx-swap-oob="outerHTML"{% endif %}>
|
||||
<div class="card">
|
||||
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 12px;">
|
||||
<h2 id="trimiteri-heading" style="font-size:15px; margin:0;">
|
||||
@@ -19,45 +20,66 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Bara de filtre: vehicul/data + pill-uri de stare pe acelasi rand. Pill-urile scriu
|
||||
campul hidden status si re-trimit form-ul (filtreazaStare) -> filtrul persista la reincarcari. -->
|
||||
<!-- Bara de filtre: [quick-pills data STANGA] [cautare vehicul MIJLOC] [pills stare DREAPTA].
|
||||
Pill-urile de stare scriu campul hidden status si re-trimit form-ul (filtreazaStare).
|
||||
Quick-pills de data apeleaza setDataRange -> seteaza data_de/data_pana + re-submit. -->
|
||||
<form id="filtre-trimiteri"
|
||||
hx-get="/_fragments/submissions"
|
||||
hx-target="#submissions-wrap"
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="submit, change, keyup delay:400ms from:input[name='vehicul']"
|
||||
style="display:flex; gap:10px 14px; flex-wrap:wrap; align-items:flex-end; margin-bottom:12px;">
|
||||
style="display:flex; gap:8px 12px; flex-wrap:wrap; align-items:center; margin-bottom:12px;">
|
||||
<input type="hidden" id="f-status" name="status" value="{{ status_filtru | default('', true) }}">
|
||||
{# Pagina curenta — actualizata prin OOB swap din _submissions.html; inclusa la reincarcari. #}
|
||||
<input type="hidden" id="f-page" name="page" value="1">
|
||||
<div>
|
||||
<label for="f-vehicul" class="muted" style="display:block; font-size:12px;">Vehicul (nr/VIN)</label>
|
||||
<input id="f-vehicul" type="text" name="vehicul" placeholder="ex. B123 sau VIN" style="max-width:180px;">
|
||||
|
||||
{# === STANGA: Quick-pills de data (preset interval) + buton Custom ===
|
||||
Azi / 7 zile / 30 zile → seteaza interval preset si submitr automat.
|
||||
Custom → dezvaluie #custom-date-fields pentru introducere manuala (fara submit automat). #}
|
||||
<div style="flex:0 0 auto; display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
|
||||
<div class="pills-categorii" id="quick-date-pills">
|
||||
<button type="button" class="pill-cat pill-data" data-range="azi"
|
||||
aria-pressed="false"
|
||||
onclick="setDataRange(this,'azi')">Azi</button>
|
||||
<button type="button" class="pill-cat pill-data" data-range="7zile"
|
||||
aria-pressed="false"
|
||||
onclick="setDataRange(this,'7zile')">7 zile</button>
|
||||
<button type="button" class="pill-cat pill-data" data-range="30zile"
|
||||
aria-pressed="false"
|
||||
onclick="setDataRange(this,'30zile')">30 zile</button>
|
||||
<button type="button" class="pill-cat pill-data" data-range="custom"
|
||||
aria-pressed="false"
|
||||
onclick="setDataRange(this,'custom')">Custom</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="f-data-de" class="muted" style="display:block; font-size:12px;">Data de la</label>
|
||||
<input id="f-data-de" type="date" name="data_de">
|
||||
{# Campuri de data pentru modul Custom: ascunse pana la click pe „Custom".
|
||||
type="date" (nu hidden) permite interactiunea utilizatorului.
|
||||
Campul change pe form re-incarca automat lista via hx-trigger="change". #}
|
||||
<div id="custom-date-fields"
|
||||
style="display:none; gap:4px; align-items:center; flex-wrap:wrap; font-size:13px;">
|
||||
<label for="f-data-de" class="muted" style="font-size:12px; white-space:nowrap;">De:</label>
|
||||
<input type="date" id="f-data-de" name="data_de" value=""
|
||||
style="font-size:13px; max-width:140px;">
|
||||
<label for="f-data-pana" class="muted" style="font-size:12px; white-space:nowrap;">Pana:</label>
|
||||
<input type="date" id="f-data-pana" name="data_pana" value=""
|
||||
style="font-size:13px; max-width:140px;">
|
||||
</div>
|
||||
<div>
|
||||
<label for="f-data-pana" class="muted" style="display:block; font-size:12px;">pana la</label>
|
||||
<input id="f-data-pana" type="date" name="data_pana">
|
||||
</div>
|
||||
<button type="submit">Filtreaza</button>
|
||||
{# Pill-uri de stare pe acelasi rand cu filtrele; re-randate prin OOB la reincarcarea tabelului. #}
|
||||
<span id="pills-categorii" class="pills-categorii" style="margin-left:auto;">
|
||||
|
||||
{# === MIJLOC: cautare vehicul (nr/VIN) + buton Filtreaza === #}
|
||||
<div style="display:flex; align-items:center; gap:8px; flex:1 1 auto; min-width:160px; flex-wrap:wrap;">
|
||||
<input id="f-vehicul" type="text" name="vehicul" placeholder="Vehicul (nr/VIN)"
|
||||
style="flex:1 1 auto; min-width:120px;">
|
||||
<button type="submit" style="flex:0 0 auto;">Filtreaza</button>
|
||||
</div>
|
||||
|
||||
{# === DREAPTA: pill-uri de stare cu contoare; re-randate via OOB la reincarcarea tabelului === #}
|
||||
<span id="pills-categorii" class="pills-categorii" style="margin-left:auto; flex:0 0 auto;">
|
||||
{% include '_pills.html' %}
|
||||
</span>
|
||||
</form>
|
||||
|
||||
<!-- Nudge "Date noi": tabelul nu se reimprospateaza singur; bannerul apare doar cand
|
||||
pollerul usor detecteaza schimbari, iar utilizatorul reincarca cand vrea. -->
|
||||
<div id="nudge-trimiteri" hidden role="status" aria-live="polite">
|
||||
<span>Sunt trimiteri actualizate.</span>
|
||||
<button type="button" onclick="reincarcaTrimiteri()">Reincarca</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabelul se reincarca DOAR la: incarcarea paginii, actiunile tale (trimiteriChanged)
|
||||
sau apasarea pe Reincarca (reincarcaTrimiteri). Fara poll periodic care sa-l reseteze. -->
|
||||
<!-- Tabelul se reincarca la: incarcarea paginii, actiunile tale (trimiteriChanged)
|
||||
si auto-refresh periodic din poller (date noi externe). -->
|
||||
<div id="submissions-wrap"
|
||||
hx-get="/_fragments/submissions"
|
||||
hx-trigger="load, trimiteriChanged from:body, reincarcaTrimiteri"
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
{# Macro-uri partajate intre template-urile de import si mapari. #}
|
||||
|
||||
{# Comutator COMPACT "In coada" — Manual <-> Auto pe un singur rand.
|
||||
INVARIANT BACKEND: control = checkbox cu `name="auto_send" value="true"` si
|
||||
SEMANTICA DE PREZENTA (bifat -> trimite "true" -> True; nebifat -> camp absent -> False).
|
||||
E singura forma compatibila cu AMBELE parsere: `Form(bool)` la /mapari SI `bool(form.get())`
|
||||
la /_import/.../mapare-operatie. Radio Auto/Manual cu value="false" ar trimite campul prezent
|
||||
pe "Manual" -> `bool("false")` = True la import (regresie tacuta). De aceea comutator vizual
|
||||
Manual<->Auto peste checkbox, NU doua radio-uri.
|
||||
- form_id: leaga input-ul de un <form> extern (necesar in celulele de tabel).
|
||||
- checked: starea STOCATA per mapare — bifat = Auto. #}
|
||||
{% macro autosend_toggle(form_id='', checked=True, label='') -%}
|
||||
<label class="autosend-toggle"
|
||||
title="Bifat = Auto: pune automat in coada la fisierele viitoare cu aceasta operatie. Nebifat = Manual: tine pentru verificare; nimic nu pleaca la RAR pana confirmi."
|
||||
style="display:inline-flex; align-items:center; justify-content:center; gap:8px; min-height:36px; cursor:pointer;">
|
||||
{%- if label %}<span class="muted" style="font-size:13px;">{{ label }}</span>{% endif %}
|
||||
<input type="checkbox" name="auto_send" value="true"
|
||||
{%- if form_id %} form="{{ form_id }}"{% endif %}
|
||||
{%- if checked %} checked{% endif %}
|
||||
aria-label="In coada automat (Auto) pentru aceasta operatie"
|
||||
style="width:18px; height:18px; cursor:pointer; accent-color:var(--accent);">
|
||||
</label>
|
||||
{%- endmacro %}
|
||||
{# US-002 (PRD 5.11): autosend_toggle neutralizat — auto_send nu mai tine randuri (US-001).
|
||||
Simbolul pastrat (apelat in _mapari.html, _preview_import.html, _trimitere_detaliu.html)
|
||||
dar intoarce string gol. Coloanele DB raman (default 1, ne-citite pentru hold). #}
|
||||
{% macro autosend_toggle(form_id='', checked=True, label='') -%}{%- endmacro %}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
<th>Operatie</th>
|
||||
<th>Sugestii</th>
|
||||
<th>Cod RAR</th>
|
||||
<th>In coada</th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@@ -69,9 +68,6 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td data-eticheta="In coada">
|
||||
{{ ui.autosend_toggle(form_id="map-rez-" ~ loop.index, checked=True) }}
|
||||
</td>
|
||||
<td>
|
||||
<button type="submit" form="map-rez-{{ loop.index }}">Salveaza</button>
|
||||
</td>
|
||||
@@ -107,7 +103,6 @@
|
||||
<thead><tr>
|
||||
<th>Operatie</th>
|
||||
<th>Cod RAR</th>
|
||||
<th>In coada</th>
|
||||
<th>Actiuni</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@@ -139,9 +134,6 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td data-eticheta="In coada">
|
||||
{{ ui.autosend_toggle(form_id="map-salv-" ~ loop.index, checked=m.auto_send) }}
|
||||
</td>
|
||||
<td style="text-align:right; white-space:nowrap;" data-eticheta="Actiuni">
|
||||
{# Butoane icon mereu vizibile (fara kebab). SVG aria-hidden; aria-label pe buton.
|
||||
data-dirty-form e citit de JS din base.html: la schimbarea select-ului din acelasi rand,
|
||||
@@ -182,8 +174,6 @@
|
||||
O regula leaga orice operatie al carei text <strong>contine</strong> (nu egal, ci substring)
|
||||
un cuvant de un cod RAR. Util pentru operatii fara cod intern: ex. orice operatie care
|
||||
<em>contine</em> „verificare" primeste codul ales. Match insensibil la majuscule/diacritice.
|
||||
<strong>In coada</strong>: implicit oprit — regula rezolva codul dar tine randul pentru
|
||||
verificare umana pana activezi „In coada".
|
||||
</p>
|
||||
|
||||
{% if not text_rules %}
|
||||
@@ -198,7 +188,6 @@
|
||||
<thead><tr>
|
||||
<th>Daca operatia contine</th>
|
||||
<th>Cod RAR</th>
|
||||
<th>In coada</th>
|
||||
<th>Actiuni</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@@ -216,9 +205,6 @@
|
||||
<td class="muted" style="font-size:12px;" data-eticheta="Cod RAR">
|
||||
{{ r.cod_prestatie }}
|
||||
</td>
|
||||
<td class="muted" style="font-size:12px;" data-eticheta="In coada">
|
||||
{% if r.auto_send %}Auto (in coada){% else %}Manual (verificare){% endif %}
|
||||
</td>
|
||||
<td style="text-align:right; white-space:nowrap;">
|
||||
<button type="submit" form="rt-del-{{ loop.index }}"
|
||||
style="background:var(--card); color:var(--err); border-color:var(--err);">
|
||||
@@ -251,16 +237,13 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td data-eticheta="In coada">
|
||||
{{ ui.autosend_toggle(form_id="rt-add", checked=False) }}
|
||||
</td>
|
||||
<td style="text-align:right; white-space:nowrap;">
|
||||
<button type="submit" form="rt-add">Adauga</button>
|
||||
</td>
|
||||
</tr>
|
||||
{# Preview pre-salvare: cate operatii nemapate potriveste pattern-ul. #}
|
||||
<tr>
|
||||
<td colspan="4" style="padding-top:0;">
|
||||
<td colspan="3" style="padding-top:0;">
|
||||
<div id="rt-preview" aria-live="polite"></div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -17,25 +17,25 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Rezumat stari (id stabil pentru OOB swap dupa editarea unui rand) -->
|
||||
<!-- Rezumat stari cu etichete umane cu majuscula (id stabil pentru OOB swap) -->
|
||||
{% set status_labels = [
|
||||
('ok', 'gata de trimis'),
|
||||
('needs_review', 'verifica valori'),
|
||||
('needs_mapping', 'fara cod RAR'),
|
||||
('needs_data', 'date lipsa'),
|
||||
('already_sent', 'deja trimis'),
|
||||
('duplicate_in_file','dublicat in fisier'),
|
||||
('ok', 'Gata de trimis'),
|
||||
('needs_review', 'Verifica valori'),
|
||||
('needs_mapping', 'Cod RAR lipsa'),
|
||||
('needs_data', 'Date incomplete'),
|
||||
('already_sent', 'Deja trimis'),
|
||||
('duplicate_in_file','Duplicat in fisier'),
|
||||
] %}
|
||||
<div id="preview-rezumat" style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px;">
|
||||
{% for status_key, label in status_labels %}
|
||||
{%- set cnt = summary.get(status_key, 0) -%}
|
||||
{% if cnt > 0 %}
|
||||
<span class="pill s-{{ status_key }}">{{ cnt }} {{ label }}</span>
|
||||
<span class="pill s-{{ status_key }}">{{ cnt }} {{ label | lower }}</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Butoane filtrare stare -->
|
||||
<!-- Butoane filtrare stare — text uman, data-filter pastreaza codul tehnic -->
|
||||
<div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:10px;" role="group"
|
||||
aria-label="Filtrare dupa stare">
|
||||
<button type="button" class="filter-btn" data-filter="all"
|
||||
@@ -48,7 +48,7 @@
|
||||
<button type="button" class="filter-btn" data-filter="{{ status_key }}"
|
||||
style="min-height:36px; font-size:13px; padding:4px 12px;
|
||||
background:transparent; border-color:var(--line); color:var(--ink);">
|
||||
{{ status_key }} ({{ cnt }})
|
||||
{{ label }} ({{ cnt }})
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@@ -96,9 +96,6 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mapcol">
|
||||
{{ ui.autosend_toggle(checked=True, label="In coada automat") }}
|
||||
</div>
|
||||
<div class="mapcol">
|
||||
<button type="submit" style="min-height:44px;">Salveaza</button>
|
||||
</div>
|
||||
@@ -107,23 +104,23 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Tabel preview. Randurile au FORM PROPRIU pentru editare (NU sunt in #confirm-form,
|
||||
<!-- Tabel preview in format identic cu tabelul Trimiteri (.tabel-trimiteri).
|
||||
Randurile au FORM PROPRIU pentru editare (NU sunt in #confirm-form,
|
||||
altfel Enter intr-un camp ar declansa trimiterea ireversibila). Bifele
|
||||
needs_review se asociaza la #confirm-form prin atributul form=. -->
|
||||
<div class="tablewrap">
|
||||
<div class="tablewrap tabel-trimiteri">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>VIN</th>
|
||||
<th>Nr. Inm.</th>
|
||||
<th>Data</th>
|
||||
<th>KM final</th>
|
||||
<th>Operatie</th>
|
||||
<th>Stare</th>
|
||||
<th>Note</th>
|
||||
<th>Verificat?</th>
|
||||
<th>Actiuni</th>
|
||||
<th class="col-id">#</th>
|
||||
<th class="col-stare">Stare</th>
|
||||
<th class="col-vehicul">Vehicul</th>
|
||||
<th class="col-operatie">Operatie</th>
|
||||
<th class="col-data">Data</th>
|
||||
<th class="col-km">KM final</th>
|
||||
<th class="col-note">Note</th>
|
||||
<th class="col-verificat">Verificat?</th>
|
||||
<th class="col-actiuni">Actiuni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -132,6 +129,11 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Mesaj "filtrat la zero": afisat de JS cand filtrul ascunde toate randurile -->
|
||||
<p id="preview-zero-message" class="muted"
|
||||
style="display:none; text-align:center; padding:24px 16px; font-size:14px;">
|
||||
Niciun rand nu corespunde filtrului selectat.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Bara confirmare (sticky jos) — singurul formular care trimite la RAR -->
|
||||
@@ -240,11 +242,17 @@
|
||||
if (btn) btn.disabled = (total === 0) || editing;
|
||||
}
|
||||
|
||||
/* Filtrare randuri dupa stare */
|
||||
/* Filtrare randuri dupa stare.
|
||||
Cand niciun rand nu e vizibil, afiseaza mesajul #preview-zero-message. */
|
||||
function filterRows(status) {
|
||||
var visible = 0;
|
||||
document.querySelectorAll('tbody tr[data-status]').forEach(function(tr) {
|
||||
tr.style.display = (status === 'all' || tr.dataset.status === status) ? '' : 'none';
|
||||
var show = status === 'all' || tr.dataset.status === status;
|
||||
tr.style.display = show ? '' : 'none';
|
||||
if (show) visible++;
|
||||
});
|
||||
var zeroMsg = document.getElementById('preview-zero-message');
|
||||
if (zeroMsg) zeroMsg.style.display = (visible === 0) ? '' : 'none';
|
||||
document.querySelectorAll('.filter-btn').forEach(function(b) {
|
||||
var active = b.dataset.filter === status;
|
||||
b.style.background = active ? 'var(--accent)' : '';
|
||||
|
||||
@@ -1,34 +1,41 @@
|
||||
{#
|
||||
_preview_rand.html — un singur rand de preview import.
|
||||
Doua moduri:
|
||||
- display (editing falsy): <tr> normal + buton "Editeaza" pe coloana de actiuni.
|
||||
- edit (editing truthy): <tr> cu un singur <td colspan> ce contine un FORM PROPRIU
|
||||
(NU #confirm-form) cu grila responsiva refolosita din _trimitere_detaliu.html.
|
||||
- display (editing falsy): <tr> normal cu 9 coloane in format .tabel-trimiteri.
|
||||
- edit (editing truthy): <tr class="preview-edit"> (display:block) cu un singur
|
||||
<td> ce contine un FORM PROPRIU (NU #confirm-form). Escapa grila table-layout:fixed.
|
||||
Swap pe RAND (hx-target pe #preview-row-N, outerHTML), NU pe #import-section.
|
||||
La save, optional OOB pe rezumat + contor "gata de trimis" (include_oob).
|
||||
|
||||
Campuri pre-computate de _web_compute_preview (NOT din template raw):
|
||||
row.prez — prezentare_din_payload(resolved): vehicul_nr, vin_scurt,
|
||||
operatie, cod_rar, data_prestatie, odometru
|
||||
row.stare_eticheta — text uman (ex. "Gata de trimis"), din STARI_PREVIEW
|
||||
row.stare_css — clasa CSS (ex. "s-ok"), din STARI_PREVIEW
|
||||
row.nota_umana — mesaj uman formatat pentru coloana Note (fara repr Python)
|
||||
#}
|
||||
{%- set res = row.resolved -%}
|
||||
{%- set status = row.resolved_status -%}
|
||||
{%- set prestatii = res.get('prestatii') or [] -%}
|
||||
{%- set op = (prestatii[0].get('cod_prestatie') or prestatii[0].get('cod_op_service', '')) if prestatii else '' -%}
|
||||
{% if editing %}
|
||||
{%- set err_map = {} -%}
|
||||
{%- set fix_map = {} -%}
|
||||
{%- for e in row.errors -%}{%- if e is mapping and e.get('field') -%}{%- set _ = err_map.update({e.field: (e.get('message') or e.get('msg'))}) -%}{%- if e.get('fix') -%}{%- set _ = fix_map.update({e.field: e.get('fix')}) -%}{%- endif -%}{%- endif -%}{%- endfor -%}
|
||||
<tr id="preview-row-{{ row.row_index }}" data-status="{{ status }}" data-editing="1">
|
||||
<td colspan="10" style="background:rgba(91,141,239,.06);">
|
||||
<tr id="preview-row-{{ row.row_index }}" data-status="{{ status }}" data-editing="1"
|
||||
class="preview-edit">
|
||||
<td data-eticheta="" style="padding:0; border:none;">
|
||||
<form class="rand-editare"
|
||||
hx-post="/_import/{{ import_id }}/rand/{{ row.row_index }}/editeaza"
|
||||
hx-target="#preview-row-{{ row.row_index }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="#rand-spinner-{{ row.row_index }}"
|
||||
hx-disabled-elt="find button"
|
||||
hx-on::response-error="this.querySelector('.rand-eroare-banner').style.display='block';">
|
||||
hx-on::response-error="this.querySelector('.rand-eroare-banner').style.display='block';"
|
||||
style="padding:12px; background:rgba(91,141,239,.06); border-radius:4px;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||
|
||||
<div style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
|
||||
<strong style="font-size:13px;">Editare rand {{ row.row_index + 1 }}</strong>
|
||||
<span class="pill s-{{ status }}" style="font-size:11px;">{{ status }}</span>
|
||||
<span class="pill {{ row.stare_css }}" style="font-size:11px;">{{ row.stare_eticheta }}</span>
|
||||
</div>
|
||||
|
||||
{% if message %}
|
||||
@@ -91,22 +98,37 @@
|
||||
{%- for e in row.errors -%}{%- if e is mapping and e.get('field') and e.get('fix') -%}{%- set _ = disp_fix_map.update({e.field: e.get('fix')}) -%}{%- endif -%}{%- endfor -%}
|
||||
<tr id="preview-row-{{ row.row_index }}" data-status="{{ status }}"
|
||||
style="{% if status == 'needs_review' %}background:rgba(230,179,74,.04);{% elif status in ('already_sent', 'duplicate_in_file') %}opacity:.65;{% endif %}">
|
||||
<td class="muted">{{ row.row_index + 1 }}</td>
|
||||
<td>{{ res.get('vin') or '<span class="muted">—</span>' | safe }}
|
||||
{% if disp_fix_map.get('vin') %}<span class="camp-fix">{{ disp_fix_map.get('vin') }}</span>{% endif %}
|
||||
<td class="col-id muted" data-eticheta="#">{{ row.row_index + 1 }}</td>
|
||||
<td class="col-stare" data-eticheta="Stare">
|
||||
<span class="pill {{ row.stare_css }}">{{ row.stare_eticheta }}</span>
|
||||
</td>
|
||||
<td>{{ res.get('nr_inmatriculare') or '' }}
|
||||
<td class="col-vehicul" data-eticheta="Vehicul">
|
||||
{{ row.prez.vehicul_nr }}
|
||||
{% if row.prez.vin_scurt and row.prez.vin_scurt != '—' %}
|
||||
<div class="muted" style="font-size:12px;">{{ row.prez.vin_scurt }}</div>
|
||||
{% endif %}
|
||||
{# Fix-uri de validare pe vehicul #}
|
||||
{% if disp_fix_map.get('vin') %}<span class="camp-fix">{{ disp_fix_map.get('vin') }}</span>{% endif %}
|
||||
{% if disp_fix_map.get('nr_inmatriculare') %}<span class="camp-fix">{{ disp_fix_map.get('nr_inmatriculare') }}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ res.get('data_prestatie') or '' }}
|
||||
<td class="col-operatie" data-eticheta="Operatie">
|
||||
<div>{{ row.prez.operatie }}</div>
|
||||
{% if row.prez.cod_rar and row.prez.cod_rar != '—' %}
|
||||
<div class="cod-rar-sub"><span class="cod-rar-cod">{{ row.prez.cod_rar }}</span></div>
|
||||
{% else %}
|
||||
<div class="muted cod-rar-sub">nemapat</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="col-data" data-eticheta="Data prestatie">
|
||||
{{ row.prez.data_prestatie }}
|
||||
{% if disp_fix_map.get('data_prestatie') %}<span class="camp-fix">{{ disp_fix_map.get('data_prestatie') }}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ res.get('odometru_final') or '' }}
|
||||
<td class="col-km" data-eticheta="KM final">
|
||||
{{ row.prez.odometru }}
|
||||
{% if disp_fix_map.get('odometru_final') %}<span class="camp-fix">{{ disp_fix_map.get('odometru_final') }}</span>{% endif %}
|
||||
</td>
|
||||
<td>{{ op or '<span class="muted">—</span>' | safe }}</td>
|
||||
<td><span class="pill s-{{ status }}">{{ status }}</span></td>
|
||||
<td class="muted" style="font-size:12px; white-space:normal; max-width:220px;">
|
||||
<td class="col-note" data-eticheta="Note"
|
||||
style="font-size:12px; white-space:normal;">
|
||||
{% if status == 'already_sent' and row.get('already_sent_info') %}
|
||||
{% set ai = row.already_sent_info %}
|
||||
deja trimis {{ (ai.get('created_at') or '')[:10] }}
|
||||
@@ -114,20 +136,11 @@
|
||||
{% elif status == 'duplicate_in_file' and row.get('duplicate_with') %}
|
||||
dubla cu randul
|
||||
{% for idx in row.duplicate_with %}{{ idx + 1 }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
{% elif row.flags %}
|
||||
{{ row.flags[0] }}
|
||||
{% elif row.errors %}
|
||||
{%- for e in row.errors -%}
|
||||
{%- if e is mapping -%}
|
||||
{{ e.get('message') or e.get('msg') or (e.values() | list | first) }}
|
||||
{%- else -%}
|
||||
{{ e }}
|
||||
{%- endif -%}
|
||||
{%- if not loop.last %}; {% endif -%}
|
||||
{%- endfor -%}
|
||||
{% else %}
|
||||
{{ row.nota_umana or '' }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align:center;">
|
||||
<td class="col-verificat" data-eticheta="Verificat?" style="text-align:center;">
|
||||
{% if status == 'needs_review' %}
|
||||
<label class="chk" style="min-height:44px; justify-content:center; cursor:pointer;"
|
||||
title="Bifat inseamna ca ai verificat valorile si le incluzi in trimitere">
|
||||
@@ -138,7 +151,7 @@
|
||||
</label>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align:center;">
|
||||
<td class="col-actiuni" data-eticheta="Actiuni" style="text-align:center;">
|
||||
{% if status not in ('already_sent', 'duplicate_in_file') %}
|
||||
<button type="button" class="btn-editeaza"
|
||||
style="min-height:44px; padding:6px 14px; font-size:13px;
|
||||
@@ -154,13 +167,13 @@
|
||||
{% if include_oob %}
|
||||
{# OOB: actualizeaza rezumatul si contorul "gata de trimis" dupa save, fara a re-randa sectiunea. #}
|
||||
{% set status_labels = [
|
||||
('ok','gata de trimis'), ('needs_review','verifica valori'), ('needs_mapping','fara cod RAR'),
|
||||
('needs_data','date lipsa'), ('already_sent','deja trimis'), ('duplicate_in_file','dublicat in fisier')] %}
|
||||
('ok','Gata de trimis'), ('needs_review','Verifica valori'), ('needs_mapping','Cod RAR lipsa'),
|
||||
('needs_data','Date incomplete'), ('already_sent','Deja trimis'), ('duplicate_in_file','Duplicat in fisier')] %}
|
||||
<div id="preview-rezumat" hx-swap-oob="true"
|
||||
style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px;">
|
||||
{% for status_key, label in status_labels %}
|
||||
{%- set cnt = summary.get(status_key, 0) -%}
|
||||
{% if cnt > 0 %}<span class="pill s-{{ status_key }}">{{ cnt }} {{ label }}</span>{% endif %}
|
||||
{% if cnt > 0 %}<span class="pill s-{{ status_key }}">{{ cnt }} {{ label | lower }}</span>{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span id="preview-ok-count" hx-swap-oob="true" data-ok="{{ summary.get('ok', 0) }}" hidden></span>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<div id="status-bar" class="status-bar card"
|
||||
hx-get="/_fragments/status"
|
||||
hx-trigger="every 15s"
|
||||
hx-swap="outerHTML">
|
||||
hx-get="/_fragments/status?tab={{ tab_activ | default('acasa') }}"
|
||||
hx-trigger="every 15s, trimiteriChanged from:body"
|
||||
hx-swap="outerHTML"
|
||||
{% if oob %}hx-swap-oob="outerHTML"{% endif %}>
|
||||
|
||||
<!-- Cont in asteptare de activare (regasit din vechiul _banner; mereu vizibil) -->
|
||||
{% if not account_active %}
|
||||
@@ -49,4 +50,20 @@
|
||||
|
||||
{# Pill-urile de stare s-au mutat in bara de filtre din sectiunea Trimiteri (_coada.html). #}
|
||||
|
||||
{# === Rand 3: navigatie rapida sub contoare (US-005) ===
|
||||
Linkurile Trimiteri + Mapari apar pe FIECARE pagina sub status-bar.
|
||||
Marcajul activ vine din variabila de context tab_activ (transmisa de dashboard via ?tab=
|
||||
sau default 'acasa'). Badge-ul Mapari = mapari_badge (aceeasi sursa: counts.needs_mapping).
|
||||
#}
|
||||
{% set _tab = tab_activ | default('acasa') %}
|
||||
<nav class="status-nav" aria-label="Navigatie rapida"
|
||||
style="margin-top:10px; display:flex; gap:8px 16px; flex-wrap:wrap; font-size:13px; border-top:1px solid var(--line); padding-top:8px;">
|
||||
<a href="/"
|
||||
{% if _tab == 'acasa' or _tab == '' %}aria-current="page"{% endif %}
|
||||
class="status-nav-link{% if _tab == 'acasa' or _tab == '' %} status-nav-activ{% endif %}">Trimiteri</a>
|
||||
<a href="/?tab=mapari"
|
||||
{% if _tab == 'mapari' %}aria-current="page"{% endif %}
|
||||
class="status-nav-link{% if _tab == 'mapari' %} status-nav-activ{% endif %}">Mapari{% if mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:16px; height:16px; margin-left:4px; padding:0 4px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ mapari_badge }}</span>{% endif %}</a>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{{ ui.autosend_toggle(checked=True) }}
|
||||
<button type="submit">Salveaza maparea</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -148,21 +148,20 @@
|
||||
font-size:12px; font-weight:600; cursor:pointer; background:transparent;
|
||||
border:1.5px solid var(--line); color:var(--muted); min-height:30px;
|
||||
transition:background .15s, color .15s; }
|
||||
.pill-cat:hover { filter:brightness(1.1); }
|
||||
/* Hover: color-mix pe culoarea curenta a pill-ului (categoria sa), nu filter:brightness
|
||||
(care producea rosu plin ilizibil pe pill-uri colorate). Activ suprima hover-ul. */
|
||||
.pill-cat:hover { background:color-mix(in srgb, currentColor 12%, transparent); }
|
||||
.pill-cat:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
||||
.pill-cat-n { font-size:11px; font-weight:700; color:var(--card); padding:0 5px;
|
||||
border-radius:99px; min-width:18px; text-align:center; }
|
||||
/* Activ categorie: umplere cu culoarea categoriei (currentColor = var injectat inline) */
|
||||
.pill-cat[aria-pressed="true"] { background:currentColor; color:var(--card); border-color:currentColor; }
|
||||
.pill-cat[aria-pressed="true"] .pill-cat-n { background:var(--card) !important; color:currentColor; }
|
||||
/* Activ suprima hover: pastram culoarea activa, nu o mixam din nou */
|
||||
.pill-cat[aria-pressed="true"]:hover { background:currentColor; }
|
||||
/* Reset "Toate" activ = --accent plin (nu culoarea categoriei) */
|
||||
.pill-cat-reset[aria-pressed="true"] { background:var(--accent); color:#fff; border-color:var(--accent); }
|
||||
/* Nudge "Date noi": apare doar cand pollerul usor detecteaza schimbari; tabelul nu se
|
||||
schimba singur niciodata, utilizatorul reincarca cand vrea. */
|
||||
#nudge-trimiteri { display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin:0 0 12px;
|
||||
padding:8px 12px; border-radius:8px; font-size:13px;
|
||||
border:1px solid var(--accent);
|
||||
background:color-mix(in srgb, var(--accent) 12%, var(--card)); }
|
||||
#nudge-trimiteri[hidden] { display:none; }
|
||||
#nudge-trimiteri button { font-size:13px; padding:5px 12px; min-height:32px; }
|
||||
.pill-cat-reset[aria-pressed="true"]:hover { background:var(--accent); }
|
||||
.s-queued{color:var(--accent);} .s-sending{color:var(--warn);} .s-sent{color:var(--ok);}
|
||||
.s-error,.s-needs_data,.s-needs_mapping{color:var(--err);}
|
||||
.s-ok{color:var(--ok);}
|
||||
@@ -267,6 +266,41 @@
|
||||
border-radius:6px; cursor:pointer; min-height:36px; white-space:nowrap; }
|
||||
.kebab-menu button:hover, .kebab-menu a:hover { background:var(--line); }
|
||||
.kebab-menu button.danger { color:var(--err); }
|
||||
/* === Accordion import compact (US-006 — regiune CSS disjuncta) ===
|
||||
<details id="import-details"> invelete stepper + upload pe Acasa.
|
||||
Atribut `open` setat de server din `are_trimiteri`:
|
||||
False (first-run) → open; True (returning) → colapsat.
|
||||
Degradare fara JS: returning-user colapsat, first-run deschis — ambele corecte
|
||||
fara toggle JS (un toggle JS pur ar lasa returning-user fara-JS cu ecranul deschis).
|
||||
aria-expanded + focus: native <details> le gestioneaza automat. */
|
||||
#import-details { margin-bottom:16px; border:1px solid var(--accent); border-radius:8px;
|
||||
background:var(--card); overflow:hidden; }
|
||||
#import-details > summary { display:flex; align-items:center; gap:8px; cursor:pointer;
|
||||
user-select:none; list-style:none; padding:10px 16px;
|
||||
font-weight:600; font-size:14px; color:var(--ink); }
|
||||
#import-details > summary::-webkit-details-marker { display:none; }
|
||||
#import-details > summary::marker { display:none; }
|
||||
#import-details > summary::before { content:"▶"; font-size:10px; color:var(--accent);
|
||||
flex-shrink:0; transition:transform .15s; }
|
||||
#import-details[open] > summary::before { transform:rotate(90deg); }
|
||||
#import-details[open] > summary { border-bottom:1px solid var(--line); }
|
||||
#import-details > summary:hover { background:color-mix(in srgb, var(--accent) 8%, var(--card)); }
|
||||
#import-details > summary:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
||||
/* Continutul (stepper + card upload): padding si bordul cardului interior sunt suprastate
|
||||
de bordul exterior al #import-details — scoatem border duplicat de pe .card intern. */
|
||||
#import-details #import-section { padding:0; }
|
||||
#import-details #import-section > .card { border-left:none; border-right:none;
|
||||
border-bottom:none; border-radius:0;
|
||||
margin-bottom:0; }
|
||||
/* === Sfarsit regiune accordion import === */
|
||||
/* === Inceput regiune nav links status-bar (US-005) ===
|
||||
Linkuri Trimiteri + Mapari sub contoare; marcaj activ cu aria-current/status-nav-activ. */
|
||||
.status-nav-link { color:var(--accent); text-decoration:none; padding:2px 6px; border-radius:4px;
|
||||
transition:background .1s; }
|
||||
.status-nav-link:hover { background:color-mix(in srgb, var(--accent) 10%, transparent); }
|
||||
.status-nav-link.status-nav-activ { color:var(--ink); font-weight:600; }
|
||||
.status-nav-link.status-nav-activ:hover { background:color-mix(in srgb, var(--ink) 8%, transparent); }
|
||||
/* === Sfarsit regiune nav links status-bar === */
|
||||
/* Tabel cu cautare + paginare client-side (data-dt). Maparile pot creste la sute de randuri;
|
||||
filtram/paginez DOM-ul deja randat, fara cereri suplimentare. Vezi scriptul din base.html. */
|
||||
input[type=search] { font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line);
|
||||
@@ -311,6 +345,25 @@
|
||||
@media (max-width:1024px) {
|
||||
.tabel-trimiteri .col-actualizat { display:none; }
|
||||
}
|
||||
/* === Preview import: coloane extra fata de tabelul Trimiteri.
|
||||
SCOPAT prin .tabel-trimiteri (clasa partajata). Regiune separata —
|
||||
nu atinge coloanele existente (col-chk/id/stare/data/rar/actualizat).
|
||||
Suma latimi fixe: col-id(48) + col-stare(104) + col-data(104) +
|
||||
col-km(76) + col-note(176) + col-verificat(80) + col-actiuni(92) = 680px.
|
||||
Restul (~600px la 1280px) revine lui col-vehicul + col-operatie (fluid). === */
|
||||
.tabel-trimiteri .col-km { width:76px; }
|
||||
.tabel-trimiteri .col-note { width:176px; }
|
||||
.tabel-trimiteri .col-verificat{ width:80px; }
|
||||
.tabel-trimiteri .col-actiuni { width:92px; }
|
||||
/* Randul de editare inline iese din grila table-layout:fixed (display:block),
|
||||
astfel formularul nu e constrans de latimile coloanelor individuale.
|
||||
Salveaza/Anuleaza sunt mereu vizibile (overflow:visible, nu clip). */
|
||||
.tabel-trimiteri tr.preview-edit { display:block; }
|
||||
.tabel-trimiteri tr.preview-edit > td { display:block; width:100%; box-sizing:border-box; padding:0; border:none; }
|
||||
/* Pe mobil (<768px): pseudo-eticheta goala (data-eticheta="") nu lasa spatiu gol. */
|
||||
@media (max-width:767px) {
|
||||
.tabel-trimiteri td[data-eticheta=""]::before { display:none; }
|
||||
}
|
||||
/* === Modal detaliu: fereastra modala globala, in afara zonei de poll
|
||||
(#submissions-wrap). Backdrop + dialog centrat pe desktop; focus-trap +
|
||||
scroll-lock + inert pe <main> sunt in JS. Varianta full-screen mobil: vezi blocul
|
||||
@@ -435,12 +488,16 @@
|
||||
<header>
|
||||
{# Celula stanga: logo ROMFAST #}
|
||||
<div class="header-left">
|
||||
{# Logo PNG real, RGBA transparent — ok pe toate temele fara filtre. #}
|
||||
{# Logo PNG real, RGBA transparent — ok pe toate temele fara filtre.
|
||||
Invelit in <a href="/"> pentru a naviga la Trimiteri (Acasa) de pe orice pagina. #}
|
||||
<a href="/" style="display:inline-flex; align-items:center; text-decoration:none;">
|
||||
<img src="/static/romfast_logo.png" alt="ROMFAST" class="brand-logo">
|
||||
</a>
|
||||
</div>
|
||||
{# Celula centru: titlu + badge env mic #}
|
||||
{# Celula centru: titlu + badge env mic.
|
||||
Titlul linkeaza la / (Trimiteri) ca si logo-ul. #}
|
||||
<div class="header-center">
|
||||
<h1>Gateway RAR AUTOPASS</h1>
|
||||
<a href="/" style="text-decoration:none; color:inherit;"><h1>Gateway RAR AUTOPASS</h1></a>
|
||||
<span class="env">{{ rar_env }}</span>
|
||||
</div>
|
||||
{# Celula dreapta: comutator tema + versiune + meniu cont #}
|
||||
@@ -457,6 +514,8 @@
|
||||
aria-haspopup="true" aria-expanded="false" aria-controls="cont-menu"
|
||||
aria-label="Meniu cont" title="Meniu cont">☰</button>
|
||||
<div id="cont-menu" class="cont-menu" role="menu" aria-labelledby="cont-menu-toggle" hidden>
|
||||
{# Prima intrare: Trimiteri (Acasa) — pagina principala cu import + lista trimiterilor. #}
|
||||
<a role="menuitem" href="/">Trimiteri</a>
|
||||
{# Mapari, cu badge needs_mapping. #}
|
||||
{% set _mapari_badge = (badges.mapari if (badges is defined and badges and badges.mapari) else 0) %}
|
||||
<a role="menuitem" href="/?tab=mapari">Mapari{% if _mapari_badge %}<span class="tab-badge" aria-hidden="true" style="display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; margin-left:6px; padding:0 5px; border-radius:99px; background:var(--err); color:#fff; font-size:11px; font-weight:700;">{{ _mapari_badge }}</span>{% endif %}</a>
|
||||
@@ -800,11 +859,51 @@
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Filtrare stare prin pill-uri + reincarcare manuala a tabelului. Tabelul NU se mai
|
||||
// schimba singur (fara poll periodic pe #submissions-wrap): un poller usor verifica
|
||||
// doar versiunea datelor si arata nudge-ul "Date noi" cand difera. Reincarcarea
|
||||
// (pill, nudge sau actiune) trece prin form -> pastreaza filtrul/pagina curenta.
|
||||
// Filtrare stare prin pill-uri + reincarcare a tabelului (manuala sau auto din poller).
|
||||
// Reincarcarea trece prin form -> pastreaza filtrul/pagina curenta (hx-include).
|
||||
(function() {
|
||||
// Quick-pills de data (Azi/7 zile/30 zile/Custom): seteaza interval sau dezvaluie campuri manuale.
|
||||
// NU modifica f-status — pastreaza pill-ul de stare activ curent.
|
||||
window.setDataRange = function(btn, range) {
|
||||
var form = document.getElementById('filtre-trimiteri');
|
||||
if (!form) return;
|
||||
var de = document.getElementById('f-data-de');
|
||||
var pana = document.getElementById('f-data-pana');
|
||||
var hp = document.getElementById('f-page'); if (hp) hp.value = '1';
|
||||
var customPanel = document.getElementById('custom-date-fields');
|
||||
// Marcheaza pill-ul de data activ, reseteaza celelalte quick-pills
|
||||
document.querySelectorAll('.pill-data').forEach(function(b) {
|
||||
b.setAttribute('aria-pressed', 'false');
|
||||
});
|
||||
if (btn) btn.setAttribute('aria-pressed', 'true');
|
||||
// Custom: dezvaluie campurile manuale, asteapta inputul utilizatorului.
|
||||
// NU face submit automat; form-ul submite la change (hx-trigger="change").
|
||||
if (range === 'custom') {
|
||||
// Goleste valorile ramase de la ultimul preset — utilizatorul porneste de la curat.
|
||||
if (de) de.value = '';
|
||||
if (pana) pana.value = '';
|
||||
if (customPanel) customPanel.style.display = 'flex';
|
||||
if (de) de.focus();
|
||||
return;
|
||||
}
|
||||
// Preset-uri: ascunde campurile manuale, seteaza intervalul si trimite imediat.
|
||||
if (customPanel) customPanel.style.display = 'none';
|
||||
var azi = new Date();
|
||||
// Formateaza data ca YYYY-MM-DD in zona locala (nu UTC, ca sa nu cada cu -1 zi noaptea)
|
||||
function fmt(d) {
|
||||
return d.getFullYear() + '-' +
|
||||
String(d.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(d.getDate()).padStart(2, '0');
|
||||
}
|
||||
var from, to;
|
||||
if (range === 'azi') { from = fmt(azi); to = fmt(azi); }
|
||||
else if (range === '7zile') { var d7 = new Date(azi); d7.setDate(d7.getDate() - 6); from = fmt(d7); to = fmt(azi); }
|
||||
else if (range === '30zile') { var d30 = new Date(azi); d30.setDate(d30.getDate() - 29); from = fmt(d30); to = fmt(azi); }
|
||||
else { from = ''; to = ''; }
|
||||
if (de) de.value = from;
|
||||
if (pana) pana.value = to;
|
||||
if (form.requestSubmit) form.requestSubmit(); else form.submit();
|
||||
};
|
||||
// Pill de stare: scrie campul hidden, reseteaza pagina la 1 si re-trimite filtrul.
|
||||
window.filtreazaStare = function(btn, status) {
|
||||
var form = document.getElementById('filtre-trimiteri');
|
||||
@@ -817,30 +916,33 @@
|
||||
if (btn) btn.setAttribute('aria-pressed', 'true');
|
||||
if (form.requestSubmit) form.requestSubmit(); else form.submit();
|
||||
};
|
||||
// Reincarca tabelul pastrand filtrul curent (hx-include #filtre-trimiteri) si ascunde nudge-ul.
|
||||
// Reincarca tabelul pastrand filtrul curent (hx-include #filtre-trimiteri).
|
||||
window.reincarcaTrimiteri = function() {
|
||||
var n = document.getElementById('nudge-trimiteri'); if (n) n.hidden = true;
|
||||
if (window.htmx) htmx.trigger('#submissions-wrap', 'reincarcaTrimiteri');
|
||||
};
|
||||
|
||||
// Poller "Date noi": compara versiunea datelor cu cea cu care s-a randat tabelul.
|
||||
// Daca difera, arata nudge-ul; daca nu, nu atinge nimic. JSON usor, fara re-render.
|
||||
// Poller auto-refresh: compara versiunea datelor cu cea cu care s-a randat tabelul.
|
||||
// Daca difera (schimbari externe — ex. worker a procesat trimiteri), reincarca automat
|
||||
// pastrand filtrul curent. Fara nudge "Date noi" — auto-refresh e mai consistent.
|
||||
// Decizie: nudge eliminat; distinctia propriu vs extern e imposibila pe client
|
||||
// fara sesiune dedicata — auto-refresh acoper ambele cazuri (US-008).
|
||||
var INTERVAL = 20000;
|
||||
function versiuneCurenta() {
|
||||
var e = document.getElementById('trimiteri-versiune');
|
||||
return e ? e.getAttribute('data-v') : null;
|
||||
}
|
||||
var _verifica_in_curs = false;
|
||||
function verifica() {
|
||||
if (versiuneCurenta() === null) return; // tabelul nu e pe ecran (alt tab)
|
||||
var nudge = document.getElementById('nudge-trimiteri');
|
||||
if (!nudge || !nudge.hidden) return; // deja afisat -> nu re-cere
|
||||
if (_verifica_in_curs) return; // evita suprapuneri
|
||||
_verifica_in_curs = true;
|
||||
fetch('/_fragments/trimiteri-versiune', { headers: { 'X-Requested-With': 'fetch' } })
|
||||
.then(function(r) { return r.ok ? r.json() : null; })
|
||||
.then(function(d) {
|
||||
if (!d) return;
|
||||
if (d.v !== versiuneCurenta()) nudge.hidden = false;
|
||||
if (d && d.v !== versiuneCurenta()) reincarcaTrimiteri();
|
||||
})
|
||||
.catch(function() {});
|
||||
.catch(function() {})
|
||||
.finally(function() { _verifica_in_curs = false; });
|
||||
}
|
||||
setInterval(verifica, INTERVAL);
|
||||
})();
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
<!-- Bara de status: mereu vizibila -->
|
||||
<div id="status-bar" class="status-bar card"
|
||||
hx-get="/_fragments/status"
|
||||
hx-trigger="load, every 15s"
|
||||
hx-get="/_fragments/status?tab={{ active_tab }}"
|
||||
hx-trigger="load, every 15s, trimiteriChanged from:body"
|
||||
hx-swap="outerHTML">
|
||||
<div class="empty muted" style="padding:8px 0;">se incarca starea…</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
567
docs/prd/prd-5.11-ux-import-compact-preview-navigatie.md
Normal file
567
docs/prd/prd-5.11-ux-import-compact-preview-navigatie.md
Normal file
@@ -0,0 +1,567 @@
|
||||
<!-- /autoplan restore point: /home/claude/.gstack/projects/romfast-rar-autopass/main-autoplan-restore-20260626-095534.md -->
|
||||
# PRD 5.11 — Import compact + preview in format Trimiteri + navigatie + simplificare auto_send
|
||||
|
||||
**Stare**: inchis (2026-06-26 — verify-pass + code-review; vezi RAPORT VERIFY si RAPORT AUTOPLAN)
|
||||
|
||||
> Proces complet: `docs/ROADMAP.md` §5. Contractul RAR (sursa de adevar de contract):
|
||||
> `docs/api-rar-contract.md`. Starea trece: `draft → aprobat → in-executie → verify-pass → inchis`
|
||||
> (actualizata de lead). Acest PRD nu repeta strategia/contractul — le linkeaza.
|
||||
|
||||
## 1. Obiectiv
|
||||
|
||||
Reparam un set de bug-uri si frictiuni UX descoperite la dogfooding pe o baza goala (prima
|
||||
utilizare), confirmate in browser (E2E Playwright pe `prezentari_test.csv`):
|
||||
|
||||
1. **Dupa primul import ramai blocat** — wizard-ul se reseteaza la pasul 1, mesajul „vezi mai jos in
|
||||
Trimiterile tale" trimite la o sectiune care **nu exista in DOM** la first-run (`trimiteri-section`
|
||||
absent), iar lista nu apare fara reload manual.
|
||||
2. **Navigatie infundata** — din `?tab=mapari` / `?tab=jurnal` (si Cont/Integrare/Nomenclator) nu
|
||||
exista niciun link inapoi la Trimiteri; logo-ul nu e link; meniul hamburger nu are „Trimiteri".
|
||||
3. **Tabelul de preview (pasul 3) e neingrijit** — overflow orizontal taie coloanele Verificat?/Actiuni
|
||||
(butonul Editeaza), formularul de editare inline se taie, coloana „Note" scapa un **repr Python brut**
|
||||
(`[{'cod_op_service': ...}]`) iar „Stare" arata coduri brute (`needs_mapping`/`needs_data`).
|
||||
4. **`auto_send=0` produce o stare falsa** — un rand mapat (ex. OE-2) apare ca `needs_mapping` cu mesaj
|
||||
tehnic („cod mapat cu auto_send=0…") si fara actiune; bifa „In coada automat" adauga complexitate si
|
||||
confuzie. **Decizie utilizator (2026-06-26): scoatem complet conceptul auto_send din UI.**
|
||||
5. **Randul de filtre arata neuniform** — pill-urile au stil outline diferit de butonul solid Filtreaza
|
||||
si stau in dreapta lui; la hover deveneau rosu plin si textul devenea ilizibil.
|
||||
6. **Wizard-ul de import ocupa tot ecranul** — stepper + upload sunt doua carduri mari stivuite.
|
||||
|
||||
Toate sunt **UI/UX + o simplificare de mapare** (auto_send). Backendul de trimitere (worker, masina de
|
||||
stari de trimitere, idempotenta, contract RAR) ramane neatins, cu o singura exceptie controlata:
|
||||
logica de mapare nu mai „tine" randuri pe `auto_send` (US-001).
|
||||
|
||||
## 2. Non-Goals (anti scope-creep)
|
||||
|
||||
- **Nu** atingem worker-ul, reconcilierea, idempotenta sau contractul RAR.
|
||||
- **Nu** schimbam canalul API (`POST /v1/prezentari` / `/valideaza`) — doar UI web + `mapping.py`.
|
||||
(Campul `auto_send` din payload-ul API, daca exista, ramane acceptat dar ignorat — vezi US-001.)
|
||||
- **Nu** stergem coloanele DB `operations_mapping.auto_send` / `operation_text_rules.auto_send`
|
||||
(migrare distructiva inutila) — le lasam cu default `1` si nu le mai citim pentru a tine randuri.
|
||||
- **Nu** rescriem categoriile de filtrare cu etichetele din referinta (Facturate/Anulate/Diferente etc.)
|
||||
— pastram categoriile reale autopass (Toate / needs_mapping / needs_data / error). Adoptam doar
|
||||
**layout-ul si stilul** vizual din referinta (`image.png`).
|
||||
- **Nu** schimbam fluxul de erori pe 3 niveluri (5.4) — doar consumam helperele existente in preview.
|
||||
|
||||
## 3. Stories atomice
|
||||
|
||||
> Fiecare story: cea mai mica unitate care lasa sistemul functional. Backend + UI pentru acelasi
|
||||
> comportament = 2 stories. `Fisiere` + `Depinde de` complete (decid paralelizarea).
|
||||
|
||||
### US-001: Scoate „hold pe auto_send" din logica de mapare (backend)
|
||||
**Ca** operator **vreau** ca o operatie mapata la un cod RAR sa intre direct in coada **pentru ca**
|
||||
bifa auto_send introducea o stare `needs_mapping` falsa si confuza pe randuri deja corecte.
|
||||
|
||||
- **Depinde de**: —
|
||||
- **Fisiere**: `app/mapping.py`, `app/web/routes.py` (DOUA puncte: `post_mapeaza_inline` ~1018-1069
|
||||
parsare form `auto_send` + delegare `reresolve_account`; ramura `has_no_auto_send` din
|
||||
`post_corectie_trimitere` ~1166 — referinta „~1160-1185" din draft arata gresit catre mapeaza),
|
||||
**`app/api/v1/import_router.py`** (importa `has_no_auto_send` ~linia 48, il foloseste in
|
||||
`_resolve_row_for_preview` ~233 — OMIS in draft; vezi AC „nu sterge simbolul"),
|
||||
`tests/test_mapping.py`, `tests/test_web_mapeaza.py`, **`tests/test_t6_auto_send.py` +
|
||||
`tests/test_text_rule_autosend.py` (de sters/rescris — encodeaza invariantul VECHI; vor deveni RED)**
|
||||
(~6 fisiere)
|
||||
- **Test intai (RED)**: `tests/test_mapping.py` —
|
||||
`test_operatie_mapata_intra_in_queued_indiferent_de_autosend`,
|
||||
`test_regula_text_rezolvata_nu_mai_tine_randul`,
|
||||
`test_fara_stare_needs_mapping_pe_auto_send_oprit`,
|
||||
`test_niciun_rand_existent_nu_se_dezgheata` (AC dezghet),
|
||||
`test_canal_api_auto_send_ignorat_intra_queued` (`classify_prezentare`, `mapping_meta` auto_send=0 → `queued`)
|
||||
- **Acceptance criteria**:
|
||||
- [ ] `resolve_prestatii` NU mai marcheaza `regula_fara_autosend` / nu mai produce ramura
|
||||
`AUTO_SEND_OPRIT`; o operatie cu cod rezolvat (mapare exacta SAU regula text) → `queued`.
|
||||
- [ ] **`has_no_auto_send` NU se sterge — se neutralizeaza (`return False`) si ramane DEFINIT**
|
||||
(importat in `routes.py:70` SI `import_router.py:48`; stergerea simbolului → `ImportError` la
|
||||
load → app nu porneste). „Eliminat" si „intoarce mereu False" NU sunt echivalente: alege a doua.
|
||||
- [ ] Toate cele 4 callsite-uri tratate: clasificare (`mapping.py` ~413), `reresolve_account`
|
||||
(~664 + cheia stat `review_manual`), corectie (`routes.py` ~1166), preview (`import_router.py` ~233).
|
||||
- [ ] `/trimitere/{id}/mapeaza`: dupa mapare, randul cu cod valid trece `queued` (nu `needs_mapping`
|
||||
cu mesaj `auto_send`); raspuns fara cardul „auto-send oprit".
|
||||
- [ ] `needs_mapping` ramane DOAR pentru operatii fara cod RAR rezolvat (semantica reala).
|
||||
- [ ] Coloanele DB raman; `save_mapping` accepta inca `auto_send` (default 1) dar valoarea nu mai
|
||||
tine randuri (compat migrare). Niciun rand existent nu se „dezgheata" automat fara actiune.
|
||||
- [ ] **Randuri legacy `needs_mapping`-din-auto_send (cod deja prezent)** raman blocate pe test/prod
|
||||
fara afordanta de mapare (`_nemapate_pentru_submission` ~881 intoarce `[]`). DECIZIE explicita:
|
||||
requeue one-time la deploy (`reresolve_account` per cont, acum le trece `queued`) SAU documentam
|
||||
corectia inline ca singura iesire. (De notat in raportul VERIFY.)
|
||||
- **Verificare E2E**: `POST /trimitere/{id}/mapeaza` din panoul de detaliu (browser) → rand `queued`.
|
||||
|
||||
### US-002: Scoate bifa „In coada automat" din UI (Mapari + preview + detaliu)
|
||||
**Ca** operator **vreau** sa nu mai vad bifa auto_send nicaieri **pentru ca** nu mai are efect (US-001)
|
||||
si crea confuzie.
|
||||
|
||||
- **Depinde de**: US-001
|
||||
- **Fisiere**: `app/web/templates/_macros.html` (macro `autosend_toggle`),
|
||||
`app/web/templates/_mapari.html`, `app/web/templates/_preview_import.html`,
|
||||
`app/web/templates/_trimitere_detaliu.html`, `tests/test_web_mapari.py` (~5 fisiere)
|
||||
- **Test intai (RED)**: `tests/test_web_mapari.py` —
|
||||
`test_mapari_fara_toggle_autosend`, `test_preview_panou_mapare_fara_autosend`,
|
||||
`test_detaliu_mapare_inline_fara_autosend`
|
||||
- **Acceptance criteria**:
|
||||
- [ ] Macro-ul `autosend_toggle` eliminat (sau golit) si scos din cele 3 sabloane.
|
||||
- [ ] Panoul „Operatii de mapat la cod RAR" (preview) si tab-ul Mapari salveaza maparea fara nicio
|
||||
referinta la „In coada automat" / „auto_send".
|
||||
- [ ] Coloana „In coada" din tabelul Mapari operatii salvate + reguli text dispare.
|
||||
- [ ] Niciun text rezidual „auto_send" / „Tine pentru verificare" in sabloane.
|
||||
- **Verificare E2E**: browser pe `/?tab=mapari` + panoul de mapare din preview — fara bifa.
|
||||
|
||||
### US-003: Preview pas 3 — format identic cu tabelul Trimiteri (UI)
|
||||
**Ca** operator **vreau** ca tabelul de preview sa arate exact ca lista Trimiteri **pentru ca** acum
|
||||
scapa text intern brut, taie coloane si formularul de editare.
|
||||
|
||||
- **Depinde de**: —
|
||||
- **Fisiere**: `app/web/templates/_preview_import.html`, `app/web/templates/_preview_rand.html`,
|
||||
`app/web/templates/base.html` (refolosire `.tabel-trimiteri` + latimi `col-*` pt coloanele extra),
|
||||
**`app/web/payload_view.py`** (`prezentare_din_payload` accepta dict → `row.resolved` direct; OMIS),
|
||||
**stratul de adaptare in builderele de preview** (`app/web/routes.py` `_web_compute_preview` ~1851
|
||||
SI `app/api/v1/import_router.py` `_resolve_row_for_preview` ~122) — construieste un view-model tip
|
||||
`prez` + traduce `resolved_status`→eticheta, OMIS in draft,
|
||||
`app/web/labels.py` (NU reutilizare directa — vezi AC; necesita un map de stari PREVIEW),
|
||||
`tests/test_preview_import.py` (~6 fisiere)
|
||||
- **Test intai (RED)**: `tests/test_preview_import.py` —
|
||||
`test_preview_nu_contine_repr_python` (fixture cu rand `needs_mapping`/unmapped REAL — altfel trece in gol),
|
||||
`test_preview_stare_eticheta_umana` (acopera `ok`/`needs_review`/`already_sent`/`duplicate_in_file`),
|
||||
`test_preview_foloseste_clasa_tabel_trimiteri`
|
||||
- **Acceptance criteria**:
|
||||
- [ ] **NU reutiliza `eticheta_stare`/`eticheta_scurta` direct: ridica `KeyError` pe starile de
|
||||
preview** (`ok`/`needs_review`/`already_sent`/`duplicate_in_file` — absente in `STARI_SUBMISSION`).
|
||||
Adauga un map de stari preview→eticheta umana (extinde `labels.py` sau map nou), inclusiv text
|
||||
pentru `already_sent`/`duplicate_in_file`.
|
||||
- [ ] **NU pasa `row.errors` (lista Python) in `motiv_uman`/`parse_erori`** (asteapta string JSON →
|
||||
`str()` + `json.loads` esueaza → fallback `raw[:160]` = ACELASI repr pe care US-003 il repara).
|
||||
Stratul de adaptare serializeaza erorile (`json.dumps`) sau formateaza uman inainte de render.
|
||||
- [ ] Tabelul de preview foloseste `.tabel-trimiteri`, fara overflow la 1280px (`scrollWidth <= clientWidth`).
|
||||
Necesita: `data-eticheta` pe TOATE celulele (carduri <768px le citesc via `td::before`) +
|
||||
latimi `col-*` pt cele 4 coloane extra (Verificat?/Actiuni/Note/etc.) sub `table-layout:fixed`.
|
||||
- [ ] Coloana „Note" → mesaj uman, **niciodata** repr Python (`[{'cod_op_service': ...}]`).
|
||||
- [ ] „Stare" → pill uman (ca la Trimiteri), nu cod brut `needs_mapping`/`needs_data`.
|
||||
- [ ] Vehicul (nr + VIN sub), operatie (+ cod RAR sub) randate via view-model `prez` (din `payload_view`).
|
||||
- [ ] Formularul inline `colspan` e **full-width sub rand** dar IESE din grila `table-layout:fixed`
|
||||
(rand `display:block` sau editor in afara tabelului fix), cu Salveaza/Anuleaza mereu vizibile.
|
||||
- [ ] Stare „filtrat la zero" (filter JS din `_preview_import.html` ascunde toate randurile) afiseaza
|
||||
un mesaj, nu tabel gol mut; filtrul coexista cu cardurile `td{display:block}`.
|
||||
- **Verificare E2E**: browser pasul 3 — coloane intregi, fara repr brut, editare necliata, mobil + desktop.
|
||||
|
||||
### US-004: Randul de filtre Trimiteri — layout + stil ca referinta (UI)
|
||||
**Ca** operator **vreau** un rand de filtre uniform si lizibil **pentru ca** pill-urile aveau alt stil
|
||||
decat butonul Filtreaza, stateau in dreapta si la hover deveneau rosu ilizibil.
|
||||
|
||||
- **Depinde de**: —
|
||||
- **Fisiere**: `app/web/templates/_coada.html`, `app/web/templates/_pills.html`,
|
||||
`app/web/templates/base.html` (CSS `.pill-cat` + hover + layout), `tests/test_web_filtre.py` (~4 fisiere)
|
||||
- **Test intai (RED)**: `tests/test_web_filtre.py` —
|
||||
`test_pill_uri_in_stanga_controalelor`, `test_pill_categorie_stil_uniform`,
|
||||
`test_quick_pills_data_seteaza_interval`
|
||||
- **Acceptance criteria**:
|
||||
- [ ] Layout ca `image.png`: pill-uri rapide de data (Azi / 7 zile / 30 zile / Custom) in STANGA,
|
||||
camp de cautare la mijloc, pill-uri de stare cu contoare la dreapta — un singur stil de pill.
|
||||
- [ ] Pill-urile NU mai stau `margin-left:auto` izolate la dreapta butonului.
|
||||
- [ ] **Stare activa, dezambiguizata**: pill-ul „Toate" (reset) = `--accent` plin; pill-urile de
|
||||
CATEGORIE = culoarea lor de categorie cand sunt active (NU toate accent — „accent plin" din
|
||||
draft contrazice schema per-categorie din `_pills.html`). Enumera token-ul de prim-plan per
|
||||
categorie ca sa garantezi AA, SAU foloseste o pereche fixa (alb pe saturat) cu valori verificate.
|
||||
- [ ] **Hover lizibil, regula explicita**: hover = `background:color-mix(in srgb, currentColor 12%,
|
||||
transparent)` (nu `filter:brightness` actual, nu rosu plin); pill-ul ACTIV suprima hover-ul.
|
||||
Acelasi mecanism pe reset si pe categorie.
|
||||
- [ ] **Focus pastrat**: inelul `:focus-visible` (outline accent) ramane pe toate cele 3 variante de
|
||||
pill dupa rescrierea CSS (US-004 atinge exact acest bloc — usor de pierdut).
|
||||
- [ ] Quick-pills de data seteaza `data_de`/`data_pana` (preset) si reincarca lista (HTMX), pastrand
|
||||
pill-ul de stare activ.
|
||||
- [ ] Functioneaza in Light/Dark/Petrol cu token-uri concrete enumerate (nu „verifica AA" abstract).
|
||||
- **Verificare E2E**: browser — comutare quick-pill data + stare, hover citibil pe toate temele.
|
||||
|
||||
### US-005: Link-uri de navigatie sub contoare pe toate paginile (UI)
|
||||
**Ca** operator **vreau** sa ajung oricand inapoi la Trimiteri / Mapari **pentru ca** din Mapari/Jurnal
|
||||
nu mai aveam cale de intoarcere.
|
||||
|
||||
- **Depinde de**: —
|
||||
- **Fisiere**: `app/web/templates/base.html` (header + `#cont-menu`), **AMBELE** `dashboard.html`
|
||||
SI `_status.html` (ambele randeaza `<div id="status-bar">` — pe cai diferite: full-page vs OOB/partial;
|
||||
„sau" din draft risca nav prezenta la load dar pierduta la refresh + duplicat de id),
|
||||
`tests/test_web_nav.py` (~3 fisiere)
|
||||
- **Test intai (RED)**: `tests/test_web_nav.py` —
|
||||
`test_nav_trimiteri_mapari_pe_mapari`, `test_nav_trimiteri_pe_jurnal`, `test_logo_linkeaza_acasa`
|
||||
- **Acceptance criteria**:
|
||||
- [ ] Sub contoarele din `#status-bar`, pe FIECARE pagina (acasa/mapari/jurnal/cont/integrare/
|
||||
nomenclator): rand de link-uri `Trimiteri` + `Mapari` (cu badge needs_mapping daca exista),
|
||||
cu marcaj activ pe pagina curenta. **Sursa marcajului activ = o variabila de context numita
|
||||
explicit (ex. `tab_activ`, comparata cu `?tab=`), nu ghicita de implementator.**
|
||||
- [ ] Badge-ul `needs_mapping` reutilizeaza O SINGURA sursa (acelasi camp ca `_mapari_badge` din
|
||||
hamburger / `blocate_total`), nu un al doilea contor calculat diferit.
|
||||
- [ ] Logo-ul ROMFAST + titlul linkeaza la `/` (Trimiteri).
|
||||
- [ ] Meniul hamburger capata si „Trimiteri" (Acasa) ca prima intrare.
|
||||
- [ ] Niciun deadlock: din orice tab se ajunge la Trimiteri intr-un click.
|
||||
- **Verificare E2E**: browser — din Mapari si Jurnal, un click → Trimiteri.
|
||||
|
||||
### US-006: Import = container compact colapsabil (UI)
|
||||
**Ca** operator **vreau** ca importul sa nu ocupe tot ecranul **pentru ca** stepper-ul + upload-ul mare
|
||||
stau in cale dupa ce am deja trimiteri.
|
||||
|
||||
- **Depinde de**: —
|
||||
- **Fisiere**: `app/web/templates/_acasa.html`, `app/web/templates/_upload.html`,
|
||||
`app/web/templates/_stepper.html`, `app/web/templates/base.html` (CSS/JS accordion),
|
||||
`tests/test_web_acasa.py` (~4 fisiere)
|
||||
- **Test intai (RED)**: `tests/test_web_acasa.py` —
|
||||
`test_import_colapsat_cand_are_trimiteri`, `test_import_deschis_la_first_run`
|
||||
- **Acceptance criteria**:
|
||||
- [ ] Stepper + upload intr-UN singur container („Importa un fisier"), nu doua carduri.
|
||||
- [ ] Colapsat implicit cand contul are deja trimiteri (`are_trimiteri=True`); auto-deschis la
|
||||
first-run (`are_trimiteri=False`).
|
||||
- [ ] In timpul fluxului (mapcoloane/preview) containerul ramane deschis (nu se inchide intre pasi).
|
||||
- [ ] **Implementare cu `<details>` nativ** (disclosure CSS-only): serverul seteaza atributul `open`
|
||||
din `are_trimiteri`, deci „fara JS → degradare la deschis" si „colapsat la returning" sunt
|
||||
ambele corecte fara toggle JS (un toggle JS pur ar lasa returning-user-ul fara-JS cu ecranul
|
||||
permanent deschis = exact bug-ul reparat). `aria-expanded`/focus pastrate.
|
||||
- [ ] Ordinea de stivuire definita pe cele 4 combinatii (`are_creds` × `are_trimiteri`) — accordion,
|
||||
„Primii pasi", `trimiteri-section` nu se suprapun la first-run-cu-creds.
|
||||
- **Verificare E2E**: browser — first-run deschis; dupa ce exista trimiteri, colapsat; click → extins.
|
||||
|
||||
### US-007: Dupa commit, lista Trimiteri apare + se reimprospateaza automat (UI + glue)
|
||||
**Ca** operator **vreau** sa-mi vad trimiterile imediat dupa import **pentru ca** acum mesajul trimite la
|
||||
o sectiune inexistenta si nimic nu se actualizeaza fara reload.
|
||||
|
||||
- **Depinde de**: US-006
|
||||
- **Fisiere**: `app/web/routes.py` (`web_confirma_import` ~2757 intoarce AZI doar `_upload.html` via
|
||||
`hx-target=#import-section, outerHTML` — un swap `outerHTML` pe `#import-section` NU poate materializa
|
||||
un frate `#trimiteri-section`; alege mecanismul de dezvaluire — vezi AC), `app/web/templates/_upload.html`,
|
||||
`app/web/templates/_acasa.html`, `app/web/templates/_coada.html`, **`app/web/templates/_status.html` +
|
||||
`dashboard.html`** (OOB pe `#status-bar` — e in afara lui `acasa-section`, nu se actualizeaza la
|
||||
re-randarea `_acasa`/`_upload`), `tests/test_import_commit.py` (~6 fisiere)
|
||||
- **Test intai (RED)**: `tests/test_import_commit.py` —
|
||||
`test_commit_raspuns_contine_trimiteri_section`,
|
||||
`test_commit_raspuns_seteaza_hx_trigger_trimiteriChanged` (asertie pe header literal),
|
||||
`test_first_run_dupa_commit_arata_lista`,
|
||||
`test_commit_actualizeaza_status_bar` (OOB contoare)
|
||||
- **Acceptance criteria**:
|
||||
- [ ] **Mecanism de dezvaluire ales explicit**: fie (a) re-randam `_acasa.html` complet si retargetam
|
||||
pe `#acasa-section`, fie (b) emitem un OOB swap care injecteaza `_coada.html`. Minimal fata de
|
||||
codul actual (return `_upload.html`-only) = **(b)**. La first-run `#submissions-wrap` proaspat
|
||||
injectat isi declanseaza singur `hx-trigger="load"`.
|
||||
- [ ] **Raspunsul seteaza `HX-Trigger: trimiteriChanged`** (lipseste azi pe confirma; cf.
|
||||
`post_mapeaza_inline:1066`) — altfel la returning-user (sectiune deja in DOM) randurile noi
|
||||
`queued` nu apar. Atentie la dublu-load daca re-randam si `_acasa` (inofensiv, de notat).
|
||||
- [ ] Containerul de import se colapseaza, iar lista Trimiteri se reincarca automat si arata randurile.
|
||||
- [ ] **Contoarele `#status-bar` se actualizeaza via OOB swap** (sunt in `_status.html`/`dashboard.html`,
|
||||
in afara `acasa-section` — re-randarea `_acasa` nu le atinge), imediat, nu la poll-ul de 15s.
|
||||
- [ ] Mesajul de succes ramane onest („S-au pus in coada N prezentari") si pointeaza la o sectiune
|
||||
care chiar exista.
|
||||
- **Verificare E2E**: browser, baza goala — import → commit → lista apare + contoare actualizate, fara reload.
|
||||
|
||||
### US-008: Auto-refresh dupa actiuni, fara „Reincarca" manual (UI)
|
||||
**Ca** operator **vreau** ca lista sa se actualizeze singura dupa ce actionez **pentru ca** butonul
|
||||
manual „Reincarca" (nudge „Date noi") trecea neobservat (ex. dupa ce am mapat un cod si s-a trimis la RAR).
|
||||
|
||||
- **Depinde de**: US-007
|
||||
- **Fisiere**: `app/web/templates/base.html` (JS poller/nudge), `app/web/templates/_coada.html`,
|
||||
`app/web/templates/_submissions.html`, `tests/test_web_refresh.py` (~3 fisiere)
|
||||
- **Test intai (RED)**: `tests/test_web_refresh.py` —
|
||||
`test_actiune_proprie_reincarca_automat`, `test_nudge_nu_mai_blocheaza_actualizarea`
|
||||
- **Acceptance criteria**:
|
||||
- [ ] Dupa o actiune proprie (mapare inline, corectie, repune, commit import) lista se reincarca
|
||||
automat (fara click pe „Reincarca").
|
||||
- [ ] Poll-ul de fundal: cand detecteaza date noi declansate de actiuni proprii, aplica refresh
|
||||
automat; nudge-ul manual ramane doar pentru schimbari externe (alt proces/worker) — sau se
|
||||
elimina daca devine redundant. (Decizie de implementare documentata in raport.)
|
||||
- [ ] Nu se reseteaza filtrul/pagina curenta la auto-refresh (pastreaza `#filtre-trimiteri`).
|
||||
- **Verificare E2E**: browser — mapare inline a unui cod → randul devine `queued`/`sent` fara click manual.
|
||||
|
||||
## 4. Riscuri
|
||||
|
||||
- **R1 — Scoaterea auto_send rastoarna „default-ul de siguranta CEO"** (reguli text / mapari tineau randul
|
||||
pentru verificare). Acceptat constient de utilizator (2026-06-26): mapped → queued direct. Mitigare:
|
||||
randurile genuin nemapate raman `needs_mapping` (nu pleaca), iar preview-ul are gate per-rand
|
||||
(„Verificat?") inainte de commit. Documentam in CLAUDE.md (invariantul auto_send) ca a fost retras.
|
||||
- **R2 — Preview in format Trimiteri**: tabelul Trimiteri presupune un view-model (vehicul/op/cod RAR);
|
||||
randurile de preview vin din alt drum (`_resolve_row_for_preview`). Risc de divergenta de campuri.
|
||||
Mitigare: refolosim `payload_view` / `labels` ca pe canalul Trimiteri; test anti-repr.
|
||||
- **R3 — Accordion + HTMX**: colapsarea nu trebuie sa ascunda pasii in timpul fluxului mapcoloane/preview.
|
||||
Mitigare: container deschis cat timp `import-section` randeaza un pas != upload slim; test dedicat.
|
||||
- **R4 — Regresie tabel**: `.tabel-trimiteri` are reguli responsive (carduri <768px) — preview-ul are
|
||||
coloane diferite (Verificat?/Actiuni). Mitigare: verificare E2E mobil + desktop.
|
||||
|
||||
## 5. Intrebari deschise
|
||||
|
||||
> Rezolvate cu utilizatorul la planificare (2026-06-26) — vezi raspunsurile incorporate mai sus:
|
||||
> Q1 filtre = **layout complet ca referinta** + fix hover lizibil; Q2 preview = **format ca Trimiteri**;
|
||||
> Q3 = **scoatem complet bifa auto_send** (Mapari + Trimiteri); Q4 = **acordeon compact + lista apare/
|
||||
> refresh automat dupa commit** + link-uri Trimiteri/Mapari sub contoare.
|
||||
|
||||
- (rezolvat) Pastram coloanele DB `auto_send`? → DA, default 1, ne-citite pentru hold (non-distructiv).
|
||||
- (deschis, minor) Nudge-ul „Date noi" pentru schimbari externe: il pastram redus sau il eliminam? →
|
||||
decizie la implementare (US-008), documentata in raport.
|
||||
|
||||
## 6. Valuri de executie (graful de dependente)
|
||||
|
||||
```
|
||||
Val 1 (paralel, fisiere disjuncte):
|
||||
[US-001] mapping.py + routes mapeaza (backend)
|
||||
[US-003] preview format Trimiteri (_preview_*.html, base.css)
|
||||
[US-004] filtre layout+stil+hover (_coada/_pills/base.css)
|
||||
[US-005] navigatie sub contoare (base.html/_status/dashboard)
|
||||
[US-006] import accordion compact (_acasa/_upload/_stepper)
|
||||
|
||||
Val 2 (deblocate de Val 1):
|
||||
[US-002] scoate toggle auto_send din UI (dep US-001; _macros/_mapari/_preview/_detaliu)
|
||||
[US-007] post-commit reveal+refresh (dep US-006; routes/_acasa/_upload/_coada)
|
||||
|
||||
Val 3:
|
||||
[US-008] auto-refresh dupa actiuni (dep US-007; base.js/_coada/_submissions)
|
||||
```
|
||||
|
||||
Atentie la fisiere fierbinti partajate intre stories (serializare de catre lead):
|
||||
`base.html` (US-004 CSS, US-005 header/menu, US-006 accordion JS) si `_coada.html` /
|
||||
`_preview_import.html` apar in mai multe stories — NU paralel pe acelasi fisier (vezi ROADMAP §5.5).
|
||||
|
||||
---
|
||||
|
||||
## Raport VERIFY
|
||||
|
||||
> Completat de subagentul verificator (context curat, 2026-06-26) in faza VERIFY — ROADMAP §5.6.
|
||||
> Executie: echipa de teammates Sonnet (lead orchestreaza, NU scrie cod), 6 runde TDD pe valuri cu
|
||||
> fisiere disjuncte; `base.html`/`routes.py`/`_coada.html`/`_status.html` serializate ca fisiere fierbinti.
|
||||
|
||||
**VERDICT GLOBAL: PASS → verify-pass / CLOSE.** 8/8 stories trec criteriile de acceptare, cu dovezi
|
||||
cod + teste + randare runtime (verificatorul a pornit aplicatia reala si a inspectat HTML-ul randat).
|
||||
|
||||
- **Suita**: `python3 -m pytest -q` → **929 passed, 1 skipped** (skip = `test_live_rar`, gated corect
|
||||
pe marker `live` + `AUTOPASS_LIVE_RAR`). Smoke boot `python3 -c "import app.main"` → OK (critic US-001:
|
||||
`has_no_auto_send` pastrat → fara ImportError). (Dupa adaugarea testelor cu nomenclatura PRD pe US-008:
|
||||
931 passed.)
|
||||
- **US-001** PASS — `has_no_auto_send`→`return False` (definit + importat in routes.py:72 si
|
||||
import_router.py:48); `resolve_prestatii` nu mai marcheaza `regula_fara_autosend`; ramura
|
||||
`AUTO_SEND_OPRIT` scoasa din classify; cele 4 callsite tratate; canal API auto_send=0 → `queued`;
|
||||
`needs_mapping` doar pt unmapped real; coloane DB raman; teste vechi rescrise (nu sterse). Decizie
|
||||
randuri legacy: `reresolve_account` le re-trece `queued` la deploy (documentat).
|
||||
- **US-002** PASS — macro `autosend_toggle` golit, simbol pastrat; **0** reziduuri auto_send /
|
||||
„In coada automat" in template-uri (grep) + 0 in dashboard-ul randat.
|
||||
- **US-003** PASS — `STARI_PREVIEW` map nou in `labels.py` (fara KeyError pe stari preview);
|
||||
`nota_umana_preview` (errors ca lista → fara repr brut); view-model `prez` din `payload_view` (accepta
|
||||
dict); `.tabel-trimiteri` + `col-*` + `data-eticheta` pe toate celulele + editor `tr.preview-edit`
|
||||
(scapa grila fixa) + mesaj „filtrat la zero".
|
||||
- **US-004** PASS — quick-pills data STANGA / cautare MIJLOC / pills stare DREAPTA; hover
|
||||
`color-mix(currentColor 12%)`, `:focus-visible` accent, categorie activa=`currentColor`, reset=`--accent`,
|
||||
activ suprima hover. (Nota minora: butonul „Custom" — vezi mai jos.)
|
||||
- **US-005** PASS — nav Trimiteri+Mapari in `_status.html` randat pe fiecare pagina (dashboard.html
|
||||
incarca fragmentul, fara duplicat de id), activ din `tab_activ`, badge din sursa unica
|
||||
`counts.needs_mapping`; logo+titlu link `/`; hamburger „Trimiteri" prima. Confirmat runtime
|
||||
(`aria-current="page"` pe Mapari la `?tab=mapari`).
|
||||
- **US-006** PASS — `<details id="import-details" {open if not are_trimiteri}>` nativ (fara toggle JS);
|
||||
runtime: first-run `open` + placeholder `#trimiteri-section`; returning colapsat + lista plina.
|
||||
- **US-007** PASS — `web_confirma_import` → `_upload` slim + OOB `_coada` (#trimiteri-section) + OOB
|
||||
`_status` (#status-bar) + header `HX-Trigger: trimiteriChanged`; `#submissions-wrap` se auto-incarca;
|
||||
mesaj de succes onest catre o sectiune care chiar exista.
|
||||
- **US-008** PASS — actiuni proprii emit `trimiteriChanged` → reincarcare; poller compara `data-v` si
|
||||
cheama `reincarcaTrimiteri()` cu `hx-include="#filtre-trimiteri"` (pastreaza filtru+pagina); nudge
|
||||
„Date noi" ELIMINAT (decizie documentata: distinctia propriu-vs-extern nu e posibila pe client).
|
||||
- **E2E**: PROBAT la nivel de aplicatie reala (uvicorn :8011, login sesiune, HTML randat real pentru
|
||||
accordion/nav/logo/filtre/zero-reziduu auto_send). NEPROBAT: click-through Playwright complet pe
|
||||
upload→preview→commit si testul live RAR `FINALIZATA` (gated, fara creds web in mediu) — acelasi
|
||||
status ca livrabilele anterioare; backend trimitere neatins, risc minim.
|
||||
- **Regresia de aur**: PASS la nivel de teste — `test_api` (POST /v1/prezentari) + `test_import_e2e`
|
||||
(import→commit→queued) + worker reconcile/rar_errors verzi; worker/idempotenta/`build_key`/contract RAR
|
||||
NEATINSE (non-goals respectate).
|
||||
|
||||
**Note minore (non-blocante, follow-up):**
|
||||
1. US-004 „Custom": AC enumera 4 quick-pills (Azi/7zile/30zile/**Custom**); doar 3 randate ca butoane
|
||||
(JS-ul suporta ramura `'custom'`, dar fara buton). De adaugat butonul SAU scos „Custom" din AC.
|
||||
2. Cod/comentarii vestigiale in `import_router.py` (`_motiv_clasificare` ramura `needs_mapping`/auto_send
|
||||
acum inaccesibila; comentarii care inca spun „auto_send gate"). Pur cosmetic.
|
||||
|
||||
---
|
||||
|
||||
<!-- ============================================================= -->
|
||||
<!-- /autoplan REVIEW REPORT — generat 2026-06-26, commit 412102b -->
|
||||
<!-- Codex indisponibil (usage limit) -> dual voices = subagent-only -->
|
||||
<!-- ============================================================= -->
|
||||
|
||||
# RAPORT AUTOPLAN
|
||||
|
||||
## Faza 1 — CEO (strategie & scope)
|
||||
|
||||
### 0A. Provocarea premiselor
|
||||
- **Premisa centrala (US-001): "auto_send hold = stare falsa".** Citind `mapping.py:252-254` si `434-447`, hold-ul era un **gate de siguranta intentionat**, nu un bug. Reformulare onesta: scoatem un gate de siguranta pentru ca frictiunea > riscul de ireversibilitate perceput. **GATE PREMISA: utilizatorul a confirmat (2026-06-26) scoaterea completa** — premisa acceptata constient, documentata in R1.
|
||||
- Premisa "preview-ul are gate Verificat? care compenseaza" e PARTIALA: gate-ul exista doar pe calea import web; canalul API + remaparea inline (`routes.py:1166`) trec direct in `queued`. Acceptat de utilizator la gate.
|
||||
- Restul premiselor (overflow tabel, repr Python in Note, hover rosu ilizibil, nav infundata, wizard mare, reveal post-commit rupt) = **confirmate in cod** (vezi Faza 3).
|
||||
|
||||
### 0B. Ce exista deja (mapare sub-probleme -> cod)
|
||||
| Sub-problema | Cod existent de refolosit |
|
||||
|---|---|
|
||||
| Etichete umane stare/motiv | `labels.py: eticheta_stare, motiv_uman, parse_erori` |
|
||||
| Viewmodel rand trimitere | `payload_view.py` + `r.prez.*` in `_submissions.html` |
|
||||
| Tabel responsive | `.tabel-trimiteri` + carduri <768px in `base.html` |
|
||||
| Pills stare + contoare | `_pills.html` + `filtreazaStare()` in `base.html` |
|
||||
| Refresh lista | HTMX `trimiteriChanged from:body` (deja cablat in `_coada.html`) |
|
||||
| Sectiune trimiteri | `#trimiteri-section` exista in `_coada.html` (doar conditionat de `are_trimiteri`) |
|
||||
|
||||
### 0C. Dream-state delta
|
||||
- ACUM: 8 frictiuni de first-run, una mascand o decizie de siguranta ireversibila.
|
||||
- ACEST PLAN: first-run curat (import -> preview ingrijit -> commit -> lista apare/refresh), nav fara fundaturi, mapare fara concept auto_send.
|
||||
- IDEAL 12 luni: un **E2E smoke de first-run** ca poarta de release (codifica scriptul de dogfooding) ca aceste 8 simptome sa nu reapara. **Recomandare deferata (vezi TODOS).**
|
||||
|
||||
### 0C-bis. Alternative (US-001)
|
||||
| Abordare | Efort | Risc | Verdict |
|
||||
|---|---|---|---|
|
||||
| Scoatere completa (ales) | mic | gate pierdut pe API/inline | **ALES de utilizator** |
|
||||
| Hold doar pe text-rules unattended | mic | minim (pastreaza net unde conteaza) | respins de utilizator la gate |
|
||||
| Default per-cont (ca `on_unmapped_error_default`) | mediu | reversibil, dar pastreaza complexitate | respins de utilizator |
|
||||
|
||||
### 0D-0F. Mod = SELECTIVE EXPANSION. Premisa confirmata la gate. Procedam.
|
||||
|
||||
### Voci duale CEO (subagent-only; Codex indisponibil)
|
||||
**CLAUDE SUBAGENT (CEO):** 11 constatari. Critice: (F1) US-001 e o decizie clasa-siguranta impachetata cu UX cosmetic — risc de aprobare-prin-asociere; (F6) regret 6 luni = o regula text gresita auto-trimite FINALIZATA terminal nerecuperabil. Hi: F3 (premisa laundering), F4 (mitigare R1 supraevaluata — nu acopera canalul API), F5 (cazul periculos real = text-rules unattended), F8 (alternativa default-per-cont neanalizata), F10 (over-bundling). Recomandare subagent: split US-001/002 din restul. **Utilizatorul a ales full removal la gate — F4/F5/F6 raman ca follow-up optional, nu blocant.**
|
||||
|
||||
```
|
||||
CEO DUAL VOICES — CONSENSUS:
|
||||
Dimensiune Claude Codex Consensus
|
||||
1. Premise valide? Partial N/A flag (gate trecut de user)
|
||||
2. Problema corecta? Da N/A confirmat de 1 voce
|
||||
3. Scope calibrat? Nu(F10) N/A flag (over-bundle, dar waves ok)
|
||||
4. Alternative explorate? Nu(F8) N/A flag (rezolvat la gate)
|
||||
5. Riscuri piata/legal acoperite? Partial N/A flag (R1 acceptat)
|
||||
6. Traiectorie 6 luni sanatoasa? Partial N/A flag (F6 -> TODOS)
|
||||
Codex = N/A (usage limit). Single-critical din 1 voce = flagat oricum.
|
||||
```
|
||||
|
||||
### NU in scope (confirmat)
|
||||
Worker / reconciliere / idempotenta / contract RAR / canal API (payload) / coloane DB auto_send (raman, default 1) / categorii filtrare (raman cele reale autopass) / fluxul de erori 3-niveluri.
|
||||
|
||||
### Registru Erori & Salvare (relevant acestui plan)
|
||||
| Suprafata | Esec | Salvare existenta |
|
||||
|---|---|---|
|
||||
| Import upload | fisier invalid / multi-foaie | `eroare_upload` / select foaie (`_upload.html`) |
|
||||
| Preview rand | cod necunoscut nomenclator | promovat la `cod_op_service` -> `needs_mapping` (nu se trimite raw) |
|
||||
| Commit | rand `needs_mapping` | exclus din commit (ramane in lista) |
|
||||
|
||||
### Sumar CEO
|
||||
Plan sanatos ca directie. Singurul flag strategic (US-001 fara control compensator pe calea unattended) **acceptat constient la gate**. US-003-008 = castiguri de first-run cu risc redus; US-007/008 cele mai sigure. Recomandare: pastram PRD-ul unit (waves deja serializeaza fisierele fierbinti), adaugam 1 item TODOS (E2E first-run + control optional text-rule).
|
||||
|
||||
## Faza 2 — Design (UI scope: DA)
|
||||
|
||||
### Voci duale Design (subagent-only; Codex indisponibil)
|
||||
**CLAUDE SUBAGENT (design):** constatari grounded in template-uri reale.
|
||||
- **F2.4 (CRITIC):** `eticheta_stare`/`eticheta_scurta` ridica `KeyError` pe starile de preview
|
||||
(`ok`/`needs_review`/`already_sent`/`duplicate_in_file`) — reutilizarea „ca la Trimiteri" crapa preview-ul. → US-003 AC.
|
||||
- **F3.1 (CRITIC):** „activa = accent plin" contrazice pill-urile colorate per-categorie (`_pills.html:10`).
|
||||
Reset = accent; categorie = culoarea ei. → US-004 AC.
|
||||
- **F2.1/F2.2/F2.3 (high):** celulele de preview n-au `data-eticheta` (carduri <768px ilizibile); coloanele
|
||||
extra n-au latimi `col-*` sub `table-layout:fixed`; editorul `colspan` mosteneste latimea care depaseste. → US-003 AC.
|
||||
- **F3.2/F3.3 (high):** token-uri de contrast/hover neenumerate per tema/categorie — cel mai mare risc de „haunt". → US-004 AC.
|
||||
- **F2.6 (high):** preview n-are view-model `prez`; adaptorul lipseste din Fisiere. → US-003 Fisiere.
|
||||
- **F4.1/F4.2 (high):** `#status-bar` definit in DOUA locuri; variabila de tab activ nenumita. → US-005 Fisiere+AC.
|
||||
- **F1.3 (medium):** degradarea fara-JS contrazice „colapsat la returning" — necesita `<details>` nativ. → US-006 AC.
|
||||
|
||||
```
|
||||
DESIGN LITMUS — CONSENSUS:
|
||||
Dimensiune Claude Codex Consensus
|
||||
1. Ierarhie info (accordion) ok* N/A confirmat (1 voce) *cu state-table
|
||||
2. Stari lipsa (preview) NU N/A flag CRITIC (F2.4) -> US-003
|
||||
3. Matrice pill hover/active/focus NU N/A flag CRITIC (F3.1) -> US-004
|
||||
4. Nav activ + badge Partial N/A flag (F4) -> US-005
|
||||
5. Specificitate AC Partial N/A flag (token-uri, view-model)
|
||||
Codex = N/A (usage limit).
|
||||
```
|
||||
|
||||
## Faza 3 — Eng (arhitectura, teste, risc)
|
||||
|
||||
### Diagrama de dependente (componente noi vs existente)
|
||||
```
|
||||
[import upload] -> _upload.html (#import-section, outerHTML swap)
|
||||
| US-006: invelit in <details> (open=are_trimiteri)
|
||||
v
|
||||
[web_confirma_import ~2757] --returneaza--> _upload.html (AZI: doar atat -> bug US-007)
|
||||
US-007 fix: + OOB inject _coada.html (#trimiteri-section) + HX-Trigger trimiteriChanged
|
||||
+ OOB #status-bar (_status.html / dashboard.html)
|
||||
|
|
||||
v
|
||||
[#submissions-wrap] --hx-trigger: load, trimiteriChanged from:body--> /_fragments/submissions
|
||||
| |
|
||||
v v
|
||||
_submissions.html (r.prez view-model) _pills.html (OOB) <- US-004 CSS
|
||||
|
||||
[mapping.py resolve_prestatii] --auto_send hold SCOS (US-001)--> status: queued | needs_mapping(real)
|
||||
has_no_auto_send: NEUTRALIZAT (return False), simbol PASTRAT (importat in routes.py + import_router.py)
|
||||
callsite-uri: clasificare ~413 | reresolve ~664 | corectie routes ~1166 | preview import_router ~233
|
||||
|
||||
[preview builders] _web_compute_preview ~1851 / _resolve_row_for_preview ~122
|
||||
US-003: + adaptor resolved_status -> eticheta umana + view-model prez (payload_view) + json.dumps(errors)
|
||||
```
|
||||
|
||||
### Voci duale Eng (subagent-only; Codex indisponibil)
|
||||
**CLAUDE SUBAGENT (eng):** 13 constatari verificate in cod.
|
||||
- **F1 (HIGH/boot-crash):** `import_router.py:48` importa `has_no_auto_send`, `:233` il foloseste — al 4-lea
|
||||
callsite, OMIS din US-001 Fisiere. Stergerea simbolului → `ImportError` → app nu porneste. „Eliminat" ≠ „return False". → US-001.
|
||||
- **F4 (HIGH):** `test_t6_auto_send.py` + `test_text_rule_autosend.py` encodeaza holdul vechi → devin RED, neprogramate. → US-001.
|
||||
- **F5/F6 (HIGH):** `motiv_uman`/`parse_erori` asteapta string JSON; `row.errors` (lista) → fallback `raw[:160]` = ACELASI repr. Necesita strat de adaptare in buildere. → US-003.
|
||||
- **F2 (medium):** referinta de linie „~1160-1185" arata gresit; sunt 2 puncte (mapeaza ~1032 form, corectie ~1166). → US-001.
|
||||
- **F3 (medium):** randuri legacy needs_mapping-din-auto_send raman fara afordanta (`_nemapate_pentru_submission` ~881 → `[]`). → US-001 AC dezghet.
|
||||
- **F7 (medium):** `payload_view.py` necesar dar omis din US-003 Fisiere. → US-003.
|
||||
- **F8/F9/F10 (HIGH):** US-007 — `outerHTML` pe `#import-section` nu materializeaza frate `#trimiteri-section`; lipseste `HX-Trigger`; `#status-bar` (alt fisier) nu se actualizeaza. → US-007.
|
||||
- **F11/F13 (low):** waves §6 supravand paralelismul pe `base.html` (3 stories, regiuni disjuncte → serializate de lead); `save_mapping` scrie auto_send=0 dupa US-002 (inofensiv cat e ignorat).
|
||||
|
||||
```
|
||||
ENG DUAL VOICES — CONSENSUS:
|
||||
Dimensiune Claude Codex Consensus
|
||||
1. Arhitectura sanatoasa? Da N/A confirmat (1 voce)
|
||||
2. Acoperire teste suficienta? NU N/A flag (F4 + gap-uri) -> test plan
|
||||
3. Riscuri performanta? ok N/A nimic flagat (batch lookup already_sent fara N+1)
|
||||
4. Securitate? ok N/A nimic nou (CSRF/auth neatinse de UI)
|
||||
5. Cai de eroare tratate? Partial N/A flag (boot-crash F1, KeyError F5/F6)
|
||||
6. Risc de deploy gestionabil? Partial N/A flag (randuri legacy F3)
|
||||
Codex = N/A (usage limit).
|
||||
```
|
||||
|
||||
### Test plan (artefact pe disc)
|
||||
`~/.gstack/projects/romfast-rar-autopass/main-5.11-test-plan-20260626.md` — 18 codepath-uri mapate la acoperire,
|
||||
suite de rulat (inclusiv smoke de boot `import app.main` dupa US-001), 3 gap-uri critice.
|
||||
|
||||
### Registru moduri de esec (cu flag-uri critice)
|
||||
| Mod de esec | Trigger | Gravitate | Mitigare in plan |
|
||||
|---|---|---|---|
|
||||
| App nu porneste | stergerea `has_no_auto_send` | CRITIC | US-001 AC: neutralizare, nu stergere + smoke boot |
|
||||
| Preview crapa la render | reutilizare `eticheta_stare` pe stari preview | CRITIC | US-003 AC: map de stari preview |
|
||||
| Repr Python reapare in Note | `row.errors` in `motiv_uman` | HIGH | US-003 AC: adaptor json.dumps |
|
||||
| Returning-user nu vede randuri noi | lipsa `HX-Trigger` pe confirma | HIGH | US-007 AC: header trimiteriChanged |
|
||||
| Randuri legacy blocate fara iesire | needs_mapping-din-auto_send pe prod | MEDIUM | US-001 AC: requeue one-time SAU doc corectie |
|
||||
|
||||
## Cross-phase themes
|
||||
- **Theme: vocabularul de stari preview ≠ submission** — flagat INDEPENDENT in Faza 2 (Design F2.4) si
|
||||
Faza 3 (Eng F5/F6). Semnal de incredere mare: US-003 nu poate reutiliza direct `labels.py`; necesita
|
||||
un strat de traducere + view-model. Cea mai importanta corectie a planului.
|
||||
- **Theme: Fisiere sub-dimensionate fata de AC** — US-001 (import_router), US-003 (payload_view + buildere),
|
||||
US-007 (_status.html) — toate omiteau fisiere pe care propriile AC le cer. Corectat in stories.
|
||||
|
||||
## Corectie graf de executie (§6)
|
||||
Wave 1 NU e complet paralel: `base.html` e atins de US-004 (CSS pill), US-005 (header/nav), US-006
|
||||
(`<details>`) — regiuni disjuncte, dar lead-ul le SERIALIZEAZA (3 edituri pe acelasi fisier). Paralelismul
|
||||
real al Wave 1 e mai mic decat anunta graful. `_coada.html` (US-004→007→008) e serializat oricum de dependente.
|
||||
|
||||
## Deferate la TODOS.md
|
||||
- E2E smoke de first-run ca poarta de release (CEO F2).
|
||||
- Control compensator optional pe auto-trimitere unattended (CEO F5/F6) — risc rezidual acceptat de user.
|
||||
|
||||
<!-- AUTONOMOUS DECISION LOG -->
|
||||
## Decision Audit Trail
|
||||
|
||||
| # | Faza | Decizie | Clasificare | Principiu | Rationament | Respins |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | CEO | Premisa auto_send: full removal | GATE (user) | — | Utilizatorul a confirmat la gate | hold text-rule-only; default per-cont |
|
||||
| 2 | CEO | Pastram PRD unit (nu split US-001/002) | Taste | P3/P6 | Waves serializeaza deja; pre-launch, risc real ~0 | split in 2 PRD-uri (subagent F1/F10) |
|
||||
| 3 | CEO | E2E first-run -> TODOS, nu in scope acum | Mechanical | P3 | Boil-lake separat, nu blocheaza 5.11 | inclus in 5.11 |
|
||||
| 4 | Eng | US-001: neutralizare has_no_auto_send, NU stergere | Mechanical | P5 | Stergerea = ImportError (import_router) = boot crash | stergere simbol |
|
||||
| 5 | Eng | US-001 Fisiere += import_router.py + teste vechi | Mechanical | P1/P2 | Callsite real + teste care devin RED | lasa Fisiere ca-n draft |
|
||||
| 6 | Eng | US-001: randuri legacy -> decizie requeue/doc | Taste | P1 | In blast radius; impact pe prod DB | ignora (lasa blocate tacit) |
|
||||
| 7 | Design+Eng | US-003: strat adaptare + map stari preview | Mechanical | P1 | Reutilizare directa crapa/repr — cross-phase theme | reutilizare directa labels |
|
||||
| 8 | Design+Eng | US-003 Fisiere += payload_view + buildere | Mechanical | P1/P2 | AC cere view-model prez inexistent pe calea preview | doar template+labels |
|
||||
| 9 | Design | US-003: data-eticheta + col-* + colspan escape | Mechanical | P1 | Altfel overflow/mobil rup (AC propriu) | doar clasa .tabel-trimiteri |
|
||||
| 10 | Design | US-004: reset=accent, categorie=culoare proprie | Mechanical | P5 | „accent plin" contrazice schema per-categorie | toate accent |
|
||||
| 11 | Design | US-004: token-uri hover/active/focus enumerate | Mechanical | P1/P5 | „verifica AA" abstract = haunt | lasa la implementator |
|
||||
| 12 | Design | US-005: AMBELE status-bar + var tab_activ | Mechanical | P5 | „sau" risca nav pierduta la refresh + dup id | un singur fisier |
|
||||
| 13 | Design | US-006: `<details>` nativ | Mechanical | P5 | Toggle JS pur rupe degradarea fara-JS | toggle JS |
|
||||
| 14 | Eng | US-007: OOB inject _coada + HX-Trigger + OOB status-bar | Mechanical | P1/P5 | outerHTML nu materializeaza frate; lipsa trigger | „include/dezvaluie" vag |
|
||||
|
||||
## Stare review
|
||||
- Faza 1 CEO: rulat (subagent-only). Premisa confirmata la gate.
|
||||
- Faza 2 Design: rulat (subagent-only). 2 critice -> AC corectate.
|
||||
- Faza 3 Eng: rulat (subagent-only). 1 boot-crash + cross-phase theme -> stories corectate.
|
||||
- Faza 3.5 DX: SARIT — fara suprafata developer-facing (planul exclude canalul API; UI = operator).
|
||||
- Codex: indisponibil toata sesiunea (usage limit pana Jul 18) -> toate vocile = subagent-only.
|
||||
@@ -129,9 +129,10 @@ def test_badge_trimiteri_scoped_pe_acasa(client):
|
||||
|
||||
|
||||
def test_trimiteri_fara_poll_periodic_pe_tabel(client):
|
||||
"""Tabelul de trimiteri NU se mai reimprospateaza periodic: #submissions-wrap se
|
||||
incarca la load / actiunile utilizatorului / Reincarca (nudge), fara `every Ns`.
|
||||
Reimprospatarea live se face prin nudge-ul "Date noi" + endpointul de versiune."""
|
||||
"""Tabelul de trimiteri NU se reimprospateaza periodic: #submissions-wrap se
|
||||
incarca la load / actiunile utilizatorului (trimiteriChanged) / reincarcaTrimiteri.
|
||||
Reimprospatarea automata la date noi externe se face prin pollerul de versiune
|
||||
care cheama reincarcaTrimiteri() — fara nudge manual (US-008)."""
|
||||
_seed_submission("sent")
|
||||
r = client.get("/?tab=acasa")
|
||||
html = r.text
|
||||
@@ -141,6 +142,6 @@ def test_trimiteri_fara_poll_periodic_pe_tabel(client):
|
||||
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
|
||||
assert "reincarcaTrimiteri" in wrap
|
||||
assert "trimiteriChanged" in wrap
|
||||
# Mecanismul de nudge exista (banner + endpoint versiune).
|
||||
assert 'id="nudge-trimiteri"' in html
|
||||
# Pollerul de versiune exista (auto-refresh la date noi externe). Nudge eliminat (US-008).
|
||||
assert "/_fragments/trimiteri-versiune" in html
|
||||
assert 'id="nudge-trimiteri"' not in html
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"""Teste US-007 (PRD 3.6): bifa "auto-send" devine un comutator cu doua stari,
|
||||
etichetat pe COADA (nu pe trimitere).
|
||||
"""Teste US-007 (PRD 3.6) actualizate dupa US-002 (PRD 5.11).
|
||||
|
||||
Framing decis la poarta autoplan (UC-A): "Pune automat in coada" / "Tine pentru
|
||||
verificare". NU "Automat/Manual" (risc de send-safety peste declaratii ireversibile).
|
||||
`name="auto_send"` pastrat cu semantica de prezenta (checkbox value="true"):
|
||||
bifat -> auto_send True, nebifat -> absent -> False. Zero atingere backend, identic
|
||||
cu ambele parsere existente (`Form(bool)` la /mapari si `bool(form.get())` la preview).
|
||||
US-002: macro autosend_toggle neutralizat (intoarce string gol).
|
||||
Checkbox-ul name=auto_send a fost scos din UI. Coloanele DB raman.
|
||||
Testele de UI verifica ABSENTA toggle-ului; testele de backend (stocare DB) raman.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -94,38 +91,34 @@ def client(monkeypatch):
|
||||
|
||||
|
||||
def _macro_html(checked: bool = True, form_id: str = "") -> str:
|
||||
"""Randeaza direct macro-ul comutatorului, izolat de restul paginii."""
|
||||
"""Randeaza direct macro-ul, izolat de restul paginii."""
|
||||
from app.web.routes import templates
|
||||
mod = templates.env.get_template("_macros.html").make_module({})
|
||||
return str(mod.autosend_toggle(form_id=form_id, checked=checked))
|
||||
|
||||
|
||||
# --- markup / copy ---
|
||||
# --- markup: macro neutralizat dupa US-002 ---
|
||||
|
||||
def test_comutator_coada_prezent():
|
||||
"""5.5 (supersede framing 3.6): comutator etichetat Auto/Manual, compact.
|
||||
Send-safety pastrata prin tooltip/Ajutor (Manual = tine pentru verificare; nimic nu
|
||||
pleaca la RAR pana confirmi). Semantica de prezenta name=auto_send nealterata."""
|
||||
"""US-002: macro autosend_toggle neutralizat -> output gol (fara checkbox)."""
|
||||
html = _macro_html()
|
||||
assert "name=\"auto_send\"" in html and 'value="true"' in html
|
||||
assert "Auto" in html and "Manual" in html, "ambele stari etichetate"
|
||||
assert "verificare" in html, "sensul de verificare manuala trebuie pastrat (tooltip/ajutor)"
|
||||
assert "trimite" not in html.lower(), "fara cuvantul 'trimite' izolat in eticheta"
|
||||
assert "auto-send" not in html, "jargonul 'auto-send' trebuie inlocuit"
|
||||
assert 'name="auto_send"' not in html, "US-002: checkbox auto_send scos din UI"
|
||||
assert html.strip() == "", f"macro neutralizat trebuie sa intoarca string gol, got: {html!r}"
|
||||
|
||||
|
||||
def test_eticheta_scoped_pe_operatie():
|
||||
"""Microcopy scoped pe operatie (NU global)."""
|
||||
"""US-002: macro neutralizat -> nicio eticheta scoped."""
|
||||
html = _macro_html()
|
||||
assert "aceasta operatie" in html
|
||||
assert "aceasta operatie" not in html
|
||||
assert html.strip() == ""
|
||||
|
||||
|
||||
def test_default_pune_automat():
|
||||
"""Default = "Pune automat in coada" (mirror la checkbox-ul `checked` de azi)."""
|
||||
"""US-002: macro neutralizat intoarce gol indiferent de parametrul checked."""
|
||||
html_default = _macro_html(checked=True)
|
||||
assert "checked" in html_default
|
||||
assert html_default.strip() == ""
|
||||
html_off = _macro_html(checked=False)
|
||||
assert "checked" not in html_off, "starea stocata False nu trebuie bifata (H4)"
|
||||
assert html_off.strip() == ""
|
||||
|
||||
|
||||
# --- comportament (zero atingere backend) ---
|
||||
@@ -161,7 +154,7 @@ def test_tine_pentru_verificare_mapeaza_auto_send_false(client):
|
||||
# --- prezent in AMBELE locuri (mapari tab + panou preview) ---
|
||||
|
||||
def test_comutator_in_tab_mapari(client):
|
||||
"""Tabul Mapari (de-rezolvat) foloseste comutatorul de coada, nu jargonul vechi."""
|
||||
"""US-002: tabul Mapari nu mai contine checkbox auto_send (macro neutralizat)."""
|
||||
from app.db import get_connection
|
||||
import json
|
||||
acct = _create_account_user("tm@test.com")
|
||||
@@ -179,13 +172,12 @@ def test_comutator_in_tab_mapari(client):
|
||||
_login(client, "tm@test.com")
|
||||
resp = client.get("/?tab=mapari")
|
||||
assert resp.status_code == 200
|
||||
# 5.5: comutatorul compact Auto/Manual e prezent in tabul Mapari
|
||||
assert 'name="auto_send"' in resp.text
|
||||
assert "Manual" in resp.text and "Auto" in resp.text
|
||||
assert 'name="auto_send"' not in resp.text, "US-002: checkbox auto_send scos din UI"
|
||||
assert "In coada" not in resp.text, "US-002: coloana 'In coada' scoasa"
|
||||
|
||||
|
||||
def test_comutator_in_panou_preview(client):
|
||||
"""Panoul de mapare din preview are si el comutatorul + caption (azi lipsea caption)."""
|
||||
"""US-002: panoul de mapare din preview nu mai contine checkbox auto_send."""
|
||||
_create_account_user("pp@test.com")
|
||||
_seed_nomenclator("R-FRANE")
|
||||
_login(client, "pp@test.com")
|
||||
@@ -211,6 +203,5 @@ def test_comutator_in_panou_preview(client):
|
||||
})
|
||||
assert r.status_code == 200
|
||||
assert "OP-NEMAPAT" in r.text, "operatia nemapata trebuie sa apara in panoul de mapare"
|
||||
# 5.5: comutatorul compact Auto/Manual e prezent si in panoul de mapare din preview
|
||||
assert 'name="auto_send"' in r.text
|
||||
assert "Manual" in r.text and "Auto" in r.text
|
||||
assert 'name="auto_send"' not in r.text, "US-002: checkbox auto_send scos din preview"
|
||||
assert "In coada automat" not in r.text, "US-002: eticheta 'In coada automat' scoasa"
|
||||
|
||||
250
tests/test_import_commit.py
Normal file
250
tests/test_import_commit.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""Teste US-007 (PRD 5.11) — Post-commit: lista Trimiteri apare + refresh auto.
|
||||
|
||||
TDD RED: testele sunt scrise inainte de implementare.
|
||||
Verifica:
|
||||
1. Raspunsul confirma emite header HX-Trigger: trimiteriChanged.
|
||||
2. Raspunsul confirma include OOB swap al #trimiteri-section cu submissions-wrap.
|
||||
3. Prima vizita (first-run, zero trimiteri) randeaza placeholder #trimiteri-section in DOM.
|
||||
4. Mesajul de succes este onest: contine numarul de prezentari puse in coada.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Fixture client cu DB izolat #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "us007.db"))
|
||||
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.crypto import reset_cache
|
||||
reset_cache()
|
||||
from app.main import app
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
get_settings.cache_clear()
|
||||
reset_cache()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Utilitare #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _csv_bytes(rows: list[dict], sep: str = ";") -> bytes:
|
||||
buf = io.StringIO()
|
||||
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=sep)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
return buf.getvalue().encode("utf-8")
|
||||
|
||||
|
||||
def _seed_nomenclator(client: TestClient, cod_prestatie: str = "R-FRANE", cod_op: str = "OP-FRANE") -> None:
|
||||
"""Semeaza nomenclatorul si o mapare operatie->cod RAR (pentru randuri ok)."""
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?,?)",
|
||||
(cod_prestatie, "Reparatie frane"),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO operations_mapping "
|
||||
"(account_id, cod_op_service, cod_prestatie, auto_send) VALUES (1,?,?,1)",
|
||||
(cod_op, cod_prestatie),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _upload_preview_si_commit(client: TestClient, rows: list[dict]) -> tuple[int, object]:
|
||||
"""Parcurge fluxul web complet: upload -> mapare coloane -> confirma.
|
||||
|
||||
Intoarce (import_id, raspuns_confirma).
|
||||
Presupune: nomenclatorul si maparea operatiei sunt deja semanate.
|
||||
"""
|
||||
data = _csv_bytes(rows)
|
||||
r = client.post(
|
||||
"/_import/upload",
|
||||
files={"file": ("test.csv", io.BytesIO(data), "text/csv")},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
m = re.search(r"/_import/(\d+)/", r.text)
|
||||
assert m, f"import_id negasit in raspunsul de upload: {r.text[:400]}"
|
||||
iid = int(m.group(1))
|
||||
|
||||
if f"/_import/{iid}/mapare-coloane" in r.text:
|
||||
r2 = client.post(
|
||||
f"/_import/{iid}/mapare-coloane",
|
||||
data={
|
||||
"colname": ["VIN", "Nr", "Data", "KM", "Operatie"],
|
||||
"canon": ["vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie"],
|
||||
"format_data": "YYYY-MM-DD",
|
||||
},
|
||||
)
|
||||
assert r2.status_code == 200, r2.text
|
||||
|
||||
# GET preview pentru a afla n_ok — citit din atributul value al inputului #n-confirmat.
|
||||
# (Regex generic pe "ok" ar putea prinde valori din CSS like min-height:36px -> 36.)
|
||||
rp = client.get(f"/_import/{iid}/preview")
|
||||
assert rp.status_code == 200, rp.text
|
||||
m_ok = re.search(r'id="n-confirmat"[^>]*?value="(\d+)"', rp.text)
|
||||
n_ok = int(m_ok.group(1)) if m_ok else len(rows)
|
||||
|
||||
r_conf = client.post(
|
||||
f"/_import/{iid}/confirma",
|
||||
data={
|
||||
"csrf_token": "",
|
||||
"n_confirmat": str(n_ok),
|
||||
"confirmed_by": "test@us007.ro",
|
||||
},
|
||||
)
|
||||
return iid, r_conf
|
||||
|
||||
|
||||
# Date fixture: un singur rand ok
|
||||
_ROWS_OK = [
|
||||
{
|
||||
"VIN": "WVWZZZ1KZAW007001",
|
||||
"Nr": "B007TST",
|
||||
"Data": "2026-06-15",
|
||||
"KM": "77000",
|
||||
"Operatie": "OP-FRANE",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Teste RED → GREEN #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_confirma_emite_hx_trigger(client):
|
||||
"""Raspunsul confirma include header HX-Trigger: trimiteriChanged.
|
||||
|
||||
Necesar pentru ca HTMX sa emita evenimentul pe <body>, pe care alte
|
||||
elemente abonate (submissions-wrap) sa il prinda si sa se reimprospateze.
|
||||
"""
|
||||
_seed_nomenclator(client)
|
||||
_, r = _upload_preview_si_commit(client, _ROWS_OK)
|
||||
|
||||
assert r.status_code == 200, r.text
|
||||
hx_trigger = r.headers.get("HX-Trigger", "")
|
||||
assert "trimiteriChanged" in hx_trigger, (
|
||||
f"Header HX-Trigger lipseste sau nu contine 'trimiteriChanged'. "
|
||||
f"Primit: {hx_trigger!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_confirma_oob_trimiteri_section(client):
|
||||
"""Raspunsul confirma include OOB swap al #trimiteri-section.
|
||||
|
||||
Elementul <section id='trimiteri-section'> apare in raspuns cu atributul
|
||||
hx-swap-oob, astfel incat HTMX sa il injecteze in DOM fara reload complet.
|
||||
La first-run, #trimiteri-section era absent din DOM (zero trimiteri anterior);
|
||||
OOB-ul il populeaza si-l face vizibil.
|
||||
"""
|
||||
_seed_nomenclator(client)
|
||||
_, r = _upload_preview_si_commit(client, _ROWS_OK)
|
||||
|
||||
assert r.status_code == 200, r.text
|
||||
html = r.text
|
||||
|
||||
assert "hx-swap-oob" in html, (
|
||||
"Atributul hx-swap-oob lipseste din raspuns — OOB swap nu e emis"
|
||||
)
|
||||
assert 'id="trimiteri-section"' in html or "id='trimiteri-section'" in html, (
|
||||
"#trimiteri-section lipseste din raspuns OOB"
|
||||
)
|
||||
assert "submissions-wrap" in html, (
|
||||
"#submissions-wrap lipseste din OOB — lista Trimiteri nu va aparea"
|
||||
)
|
||||
|
||||
|
||||
def test_confirma_mesaj_succes_onest(client):
|
||||
"""Mesajul de succes mentioneaza numarul de prezentari puse in coada.
|
||||
|
||||
'Onest' inseamna ca mesajul reflecta exact numarul de randuri enqueue-uite,
|
||||
nu o formulare vaga (ex. 'succes'). Permite utilizatorului sa verifice.
|
||||
"""
|
||||
_seed_nomenclator(client)
|
||||
_, r = _upload_preview_si_commit(client, _ROWS_OK)
|
||||
|
||||
assert r.status_code == 200, r.text
|
||||
html = r.text
|
||||
|
||||
# Mesajul trebuie sa contina cel putin numarul '1' (un rand ok in fixture)
|
||||
# si sa indice ca randurile sunt "in coada" sau "prezentari".
|
||||
assert re.search(r"\b1\b", html), "Numarul de prezentari (1) lipseste din raspuns"
|
||||
# Cel putin unul din cuvintele cheie care indica succes de incarcare in coada
|
||||
assert any(kw in html.lower() for kw in ("coada", "prezenta", "trimiter")), (
|
||||
"Mesajul de succes nu contine cuvinte cheie despre enqueue (coada/prezentari/trimiteri)"
|
||||
)
|
||||
|
||||
|
||||
def test_commit_actualizeaza_status_bar(client):
|
||||
"""Raspunsul confirma include OOB swap al #status-bar cu trigger trimiteriChanged.
|
||||
|
||||
Verifica doua lucruri:
|
||||
1. #status-bar apare in raspunsul confirma cu hx-swap-oob (actualizare imediata
|
||||
a contorului 'In asteptare' fara a astepta poll-ul de 15s).
|
||||
2. Markup-ul #status-bar contine 'trimiteriChanged' in hx-trigger, deci la
|
||||
urmatoarele evenimente trimiteriChanged (mapeaza, corectie etc.) bara se
|
||||
re-incarca imediat, nu abia la 15s.
|
||||
"""
|
||||
_seed_nomenclator(client)
|
||||
_, r = _upload_preview_si_commit(client, _ROWS_OK)
|
||||
|
||||
assert r.status_code == 200, r.text
|
||||
html = r.text
|
||||
|
||||
assert 'id="status-bar"' in html or "id='status-bar'" in html, (
|
||||
"#status-bar lipseste din raspuns — OOB swap nu va actualiza contoarele"
|
||||
)
|
||||
assert "hx-swap-oob" in html, (
|
||||
"Atributul hx-swap-oob lipseste — cel putin un OOB swap trebuie emis"
|
||||
)
|
||||
assert "trimiteriChanged" in html, (
|
||||
"trimiteriChanged lipseste din raspuns — #status-bar nu va reactiona "
|
||||
"la eveniment si se va actualiza abia la urmatorul poll de 15s"
|
||||
)
|
||||
|
||||
# Verifica direct ca fragmentul /_fragments/status contine trigger-ul in markup.
|
||||
r_status = client.get("/_fragments/status")
|
||||
assert r_status.status_code == 200, r_status.text
|
||||
assert "trimiteriChanged" in r_status.text, (
|
||||
"/_fragments/status nu contine 'trimiteriChanged' in hx-trigger — "
|
||||
"bara nu va reactiona la evenimentul emis de confirma"
|
||||
)
|
||||
|
||||
|
||||
def test_acasa_placeholder_trimiteri_first_run(client):
|
||||
"""GET / (zero trimiteri) randeaza elementul #trimiteri-section in DOM.
|
||||
|
||||
La first-run (niciun submission anterior), #trimiteri-section trebuia sa
|
||||
existe in HTML ca placeholder gol/ascuns, astfel incat OOB swap-ul de la
|
||||
confirma sa aiba tinta valida. Fara placeholder, HTMX ignora silentios OOB-ul
|
||||
si lista Trimiteri nu apare dupa commit.
|
||||
"""
|
||||
r = client.get("/")
|
||||
assert r.status_code == 200, r.text
|
||||
html = r.text
|
||||
|
||||
assert 'id="trimiteri-section"' in html or "id='trimiteri-section'" in html, (
|
||||
"#trimiteri-section lipseste din DOM la first-run — "
|
||||
"OOB swap-ul de la confirma nu va gasi tinta si lista nu va aparea"
|
||||
)
|
||||
@@ -150,7 +150,7 @@ def test_mapari_de_rezolvat_in_tabel(client):
|
||||
|
||||
|
||||
def test_mapari_salvate_in_tabel(client):
|
||||
"""Sectiunea "Operatii salvate" randata ca tabel; H4: auto-send reflecta valoarea STOCATA."""
|
||||
"""Sectiunea "Operatii salvate" randata ca tabel. US-002: fara coloana auto_send."""
|
||||
acct = _create_account_user("salv@test.com")
|
||||
_seed_nomenclator("R-FRANE", "Reparatie frane")
|
||||
_seed_nomenclator("R-MOTOR", "Reparatie motor")
|
||||
@@ -165,9 +165,9 @@ def test_mapari_salvate_in_tabel(client):
|
||||
assert "tablewrap" in sec, "tabelul Operatii salvate trebuie sa foloseasca .tablewrap"
|
||||
assert "<table" in sec and "<th" in sec, "Operatii salvate trebuie randat ca tabel cu antet"
|
||||
assert "OP-AUTO" in sec and "OP-MANUAL" in sec
|
||||
assert 'name="auto_send"' in sec
|
||||
# H4: exact maparile cu auto_send STOCAT True sunt bifate (aici: o singura)
|
||||
assert sec.count("checked") == 1, "comutatorul auto-send trebuie sa reflecte valoarea stocata, nu un default"
|
||||
# US-002: checkbox auto_send si coloana In coada scoase din UI
|
||||
assert 'name="auto_send"' not in sec, "US-002: checkbox auto_send scos din tabelul salvate"
|
||||
assert "In coada" not in sec, "US-002: coloana In coada scoasa din tabelul salvate"
|
||||
# POST-urile neschimbate
|
||||
assert 'hx-post="/mapari/salvate"' in sec
|
||||
assert 'hx-post="/mapari/salvate/sterge"' in sec
|
||||
|
||||
@@ -380,9 +380,12 @@ def test_unmapped_are_3niveluri(client):
|
||||
assert err["fix"]
|
||||
|
||||
|
||||
def test_auto_send_oprit_3niveluri(client):
|
||||
"""Mapare cu auto_send=0 -> needs_mapping; rar_error are cheie 'auto_send'
|
||||
PASTRATA + campurile AUTO_SEND_OPRIT (cod/problema/cauza/fix)."""
|
||||
def test_auto_send_oprit_3niveluri_noul_comportament(client):
|
||||
"""Mapare cu auto_send=0 -> queued (auto_send ignorat dupa US-001).
|
||||
|
||||
Dupa US-001: classify_prezentare nu mai produce ramura AUTO_SEND_OPRIT.
|
||||
O operatie cu cod rezolvat (indiferent de auto_send) -> queued direct.
|
||||
"""
|
||||
import json
|
||||
from app.mapping import classify_prezentare
|
||||
|
||||
@@ -396,15 +399,9 @@ def test_auto_send_oprit_3niveluri(client):
|
||||
mapping = {"OP_REVIEW": "OE-1"}
|
||||
mapping_meta = {"OP_REVIEW": {"cod_prestatie": "OE-1", "auto_send": False}}
|
||||
res = classify_prezentare(content, mapping, mapping_meta)
|
||||
assert res["status"] == "needs_mapping"
|
||||
err = json.loads(res["rar_error"])
|
||||
# Cheia originala pastrata
|
||||
assert "auto_send" in err
|
||||
# 3 niveluri prezente
|
||||
assert err["cod"] == "AUTO_SEND_OPRIT"
|
||||
assert err["problema"]
|
||||
assert err["cauza"]
|
||||
assert err["fix"]
|
||||
assert res["status"] == "queued", (
|
||||
f"dupa US-001 auto_send=0 -> queued (nu needs_mapping), got {res['status']}"
|
||||
)
|
||||
|
||||
|
||||
def test_needs_data_pass_through(client):
|
||||
@@ -431,3 +428,153 @@ def test_needs_data_pass_through(client):
|
||||
assert "cod" in e, f"lipseste 'cod' in {e}"
|
||||
assert "problema" in e, f"lipseste 'problema' in {e}"
|
||||
assert "fix" in e, f"lipseste 'fix' in {e}"
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-001: Scoate hold auto_send din mapare — teste RED (inainte de implementare)
|
||||
# =========================================================================== #
|
||||
|
||||
@pytest.fixture()
|
||||
def db_conn(monkeypatch, tmp_path):
|
||||
"""Conexiune directa la o DB temporara, fara client HTTP."""
|
||||
db_path = str(tmp_path / "us001.db")
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", db_path)
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.db import init_db
|
||||
init_db()
|
||||
from app.db import get_connection
|
||||
c = get_connection()
|
||||
yield c
|
||||
c.close()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def test_operatie_mapata_intra_in_queued_indiferent_de_autosend():
|
||||
"""classify_prezentare cu mapare auto_send=0 -> queued (nu needs_mapping).
|
||||
|
||||
Dupa US-001: has_no_auto_send nu mai blocheaza; un cod rezolvat e direct queued.
|
||||
"""
|
||||
from app.mapping import classify_prezentare
|
||||
content = {
|
||||
"vin": "WVWZZZ1KZAW000123",
|
||||
"nr_inmatriculare": "B999TST",
|
||||
"data_prestatie": "2026-06-15",
|
||||
"odometru_final": "123456",
|
||||
"prestatii": [{"cod_op_service": "OP_REVIEW", "denumire": "Operatie cu review"}],
|
||||
}
|
||||
mapping = {"OP_REVIEW": "OE-1"}
|
||||
mapping_meta = {"OP_REVIEW": {"cod_prestatie": "OE-1", "auto_send": False}}
|
||||
valid_codes = {"OE-1"}
|
||||
result = classify_prezentare(content, mapping, mapping_meta, valid_codes)
|
||||
assert result["status"] == "queued", (
|
||||
f"asteptat queued (auto_send ignorat), got {result['status']}: {result.get('rar_error')}"
|
||||
)
|
||||
|
||||
|
||||
def test_regula_text_rezolvata_nu_mai_tine_randul():
|
||||
"""Regula text cu auto_send=0 rezolva codul -> queued (nu needs_mapping held).
|
||||
|
||||
Dupa US-001: regula_fara_autosend nu se mai seteaza; codul rezolvat = queued direct.
|
||||
"""
|
||||
from app.mapping import classify_prezentare
|
||||
content = {
|
||||
"vin": "WVWZZZ1KZAW000123",
|
||||
"nr_inmatriculare": "B999TST",
|
||||
"data_prestatie": "2026-06-15",
|
||||
"odometru_final": "123456",
|
||||
"prestatii": [{"cod_op_service": "X99", "denumire": "Verificare faruri"}],
|
||||
}
|
||||
text_rules = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 0, "priority": 0}]
|
||||
valid_codes = {"OE-2"}
|
||||
result = classify_prezentare(content, {}, {}, valid_codes, text_rules)
|
||||
assert result["status"] == "queued", (
|
||||
f"asteptat queued (regula text, auto_send ignorat), got {result['status']}: {result.get('rar_error')}"
|
||||
)
|
||||
|
||||
|
||||
def test_fara_stare_needs_mapping_pe_auto_send_oprit():
|
||||
"""has_no_auto_send intotdeauna False dupa US-001; nu mai produce AUTO_SEND_OPRIT."""
|
||||
from app.mapping import has_no_auto_send
|
||||
mapping_meta_false = {"OP_REVIEW": {"cod_prestatie": "OE-1", "auto_send": False}}
|
||||
resolved = [{"cod_op_service": "OP_REVIEW", "cod_prestatie": "OE-1"}]
|
||||
assert has_no_auto_send(resolved, mapping_meta_false) is False, (
|
||||
"has_no_auto_send trebuie sa intoarca mereu False dupa US-001"
|
||||
)
|
||||
resolved_cu_flag = [{"cod_op_service": "X", "cod_prestatie": "OE-1", "regula_fara_autosend": True}]
|
||||
assert has_no_auto_send(resolved_cu_flag, {}) is False, (
|
||||
"has_no_auto_send ignora regula_fara_autosend dupa US-001"
|
||||
)
|
||||
|
||||
|
||||
def test_niciun_rand_existent_nu_se_dezgheata(db_conn):
|
||||
"""Randuri legacy needs_mapping-din-auto_send: fara afordanta UI (cod prezent),
|
||||
dezghetabile via reresolve_account explicit (nu automat).
|
||||
|
||||
_nemapate_pentru_submission -> [] (fara panou mapare in UI).
|
||||
reresolve_account cu mapare activa -> requeued=1 (dezghet via actiune explicita).
|
||||
"""
|
||||
import json as _json
|
||||
payload = {
|
||||
"vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B1",
|
||||
"data_prestatie": "2026-06-15", "odometru_final": "123456",
|
||||
"prestatii": [{"cod_op_service": "X1", "cod_prestatie": "OE-1"}],
|
||||
}
|
||||
rar_error = _json.dumps({"auto_send": "cod mapat cu auto_send=0; review manual"})
|
||||
db_conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
("k-legacy-us001", 1, "needs_mapping", _json.dumps(payload), rar_error),
|
||||
)
|
||||
db_conn.execute(
|
||||
"INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES ('OE-1', 'Test')"
|
||||
)
|
||||
db_conn.execute(
|
||||
"INSERT OR REPLACE INTO operations_mapping "
|
||||
"(account_id, cod_op_service, cod_prestatie, auto_send) VALUES (1, 'X1', 'OE-1', 0)"
|
||||
)
|
||||
db_conn.commit()
|
||||
|
||||
row = db_conn.execute(
|
||||
"SELECT * FROM submissions WHERE idempotency_key='k-legacy-us001'"
|
||||
).fetchone()
|
||||
nomenclator = [{"cod_prestatie": "OE-1", "nume_prestatie": "Test"}]
|
||||
|
||||
# Nicio afordanta UI (cod deja prezent -> nu se arata panoul de mapare)
|
||||
from app.web.routes import _nemapate_pentru_submission
|
||||
assert _nemapate_pentru_submission(row, nomenclator) == [], (
|
||||
"_nemapate_pentru_submission trebuie sa intoarca [] (cod deja prezent)"
|
||||
)
|
||||
|
||||
# Dezghetare via reresolve_account explicit (actiune admin la deploy)
|
||||
from app.mapping import reresolve_account
|
||||
stats = reresolve_account(db_conn, 1)
|
||||
assert stats["requeued"] == 1, (
|
||||
f"reresolve_account trebuie sa requeueze randul legacy: {stats}"
|
||||
)
|
||||
row2 = db_conn.execute(
|
||||
"SELECT status FROM submissions WHERE idempotency_key='k-legacy-us001'"
|
||||
).fetchone()
|
||||
assert row2["status"] == "queued"
|
||||
|
||||
|
||||
def test_canal_api_auto_send_ignorat_intra_queued():
|
||||
"""classify_prezentare (canal API) cu mapping_meta auto_send=0 -> queued.
|
||||
|
||||
Campul auto_send din mapping_meta nu mai afecteaza decizia de clasificare.
|
||||
"""
|
||||
from app.mapping import classify_prezentare
|
||||
content = {
|
||||
"vin": "WVWZZZ1KZAW000123",
|
||||
"nr_inmatriculare": "B999TST",
|
||||
"data_prestatie": "2026-06-15",
|
||||
"odometru_final": "123456",
|
||||
"prestatii": [{"cod_op_service": "ITP-CHECK", "denumire": "Inspectie"}],
|
||||
}
|
||||
mapping = {"ITP-CHECK": "OE-1"}
|
||||
mapping_meta = {"ITP-CHECK": {"cod_prestatie": "OE-1", "auto_send": False}}
|
||||
valid_codes = {"OE-1"}
|
||||
result = classify_prezentare(content, mapping, mapping_meta, valid_codes)
|
||||
assert result["status"] == "queued", (
|
||||
f"canal API: auto_send ignorat -> asteptat queued, got {result['status']}"
|
||||
)
|
||||
|
||||
283
tests/test_preview_import.py
Normal file
283
tests/test_preview_import.py
Normal file
@@ -0,0 +1,283 @@
|
||||
"""Teste US-003 (PRD 5.11) — Preview pas 3 in format identic cu tabelul Trimiteri.
|
||||
|
||||
Proces TDD: aceste teste sunt scrise INAINTE de implementare (RED) si verifica:
|
||||
1. Coloana Note nu afiseaza repr Python brut (lista/dict Python) pentru needs_mapping.
|
||||
2. Starea afisata in pill-ul randului este eticheta umana (nu codul brut).
|
||||
3. Tabelul de preview foloseste clasa CSS .tabel-trimiteri.
|
||||
|
||||
Fixture real cu rand needs_mapping OBLIGATORIU pentru test_preview_nu_contine_repr_python
|
||||
(altfel testul trece in gol pe un preview fara erori in Note).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Fixture client cu DB izolat #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "preview_us003.db"))
|
||||
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.crypto import reset_cache
|
||||
reset_cache()
|
||||
from app.main import app
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
get_settings.cache_clear()
|
||||
reset_cache()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Utilitare #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def _csv_bytes(rows: list[dict], sep: str = ";") -> bytes:
|
||||
buf = io.StringIO()
|
||||
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=sep)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
return buf.getvalue().encode("utf-8")
|
||||
|
||||
|
||||
def _seed_nomenclator_si_mapare(
|
||||
cod_prestatie: str = "R-FRANE", cod_op: str = "OP-FRANE"
|
||||
) -> None:
|
||||
"""Semeaza nomenclatorul si o mapare pentru a produce randuri ok in preview."""
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?,?)",
|
||||
(cod_prestatie, "Reparatie frane"),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO operations_mapping "
|
||||
"(account_id, cod_op_service, cod_prestatie, auto_send) VALUES (1,?,?,1)",
|
||||
(cod_op, cod_prestatie),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _upload_and_preview(client: TestClient, rows: list[dict]) -> tuple[int, str]:
|
||||
"""Upload CSV + salveaza mapare coloane (daca lipseste) + GET preview.
|
||||
|
||||
Returneaza (import_id, html_preview).
|
||||
"""
|
||||
data = _csv_bytes(rows)
|
||||
r = client.post(
|
||||
"/_import/upload",
|
||||
files={"file": ("test.csv", io.BytesIO(data), "text/csv")},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
# Extrage import_id din URL-urile prezente in raspuns
|
||||
m = re.search(r"/_import/(\d+)/", r.text)
|
||||
assert m, f"import_id negasit in raspunsul de upload: {r.text[:400]}"
|
||||
iid = int(m.group(1))
|
||||
|
||||
# Daca s-a returnat formularul de mapare coloane, il salvam
|
||||
if f"/_import/{iid}/mapare-coloane" in r.text:
|
||||
r2 = client.post(
|
||||
f"/_import/{iid}/mapare-coloane",
|
||||
data={
|
||||
"colname": ["VIN", "Nr", "Data", "KM", "Operatie"],
|
||||
"canon": ["vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie"],
|
||||
"format_data": "YYYY-MM-DD",
|
||||
},
|
||||
)
|
||||
assert r2.status_code == 200, r2.text
|
||||
|
||||
r3 = client.get(f"/_import/{iid}/preview")
|
||||
assert r3.status_code == 200, r3.text
|
||||
return iid, r3.text
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Date fixture #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
# Un rand cu operatie nemapata — produce starea needs_mapping cu errors=[{"unmapped":[...]}]
|
||||
_ROWS_UNMAPPED = [
|
||||
{
|
||||
"VIN": "WVWZZZ1KZAW000001",
|
||||
"Nr": "B001TST",
|
||||
"Data": "2026-06-15",
|
||||
"KM": "123456",
|
||||
"Operatie": "OP-FARA-COD",
|
||||
},
|
||||
]
|
||||
|
||||
# Un rand ok (mapare existenta) + un rand unmapped
|
||||
_ROWS_OK_SI_UNMAPPED = [
|
||||
{
|
||||
"VIN": "WVWZZZ1KZAW000001",
|
||||
"Nr": "B001TST",
|
||||
"Data": "2026-06-15",
|
||||
"KM": "123456",
|
||||
"Operatie": "OP-FRANE",
|
||||
},
|
||||
{
|
||||
"VIN": "WVWZZZ1KZAW000002",
|
||||
"Nr": "B002TST",
|
||||
"Data": "2026-06-15",
|
||||
"KM": "100000",
|
||||
"Operatie": "OP-FARA-COD",
|
||||
},
|
||||
]
|
||||
|
||||
# Doua randuri identice — produc starea duplicate_in_file
|
||||
_ROWS_DUPLICATE = [
|
||||
{
|
||||
"VIN": "WVWZZZ1KZAW000001",
|
||||
"Nr": "B001TST",
|
||||
"Data": "2026-06-15",
|
||||
"KM": "123456",
|
||||
"Operatie": "OP-FRANE",
|
||||
},
|
||||
{
|
||||
"VIN": "WVWZZZ1KZAW000001",
|
||||
"Nr": "B001TST",
|
||||
"Data": "2026-06-15",
|
||||
"KM": "123456",
|
||||
"Operatie": "OP-FRANE",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Teste RED → GREEN #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_preview_nu_contine_repr_python(client):
|
||||
"""Coloana Note nu afiseaza repr Python brut pentru randuri needs_mapping.
|
||||
|
||||
Fixture real cu rand needs_mapping OBLIGATORIU — altfel testul ar trece in gol
|
||||
pe un preview fara erori (coloana Note goala nu produce repr).
|
||||
|
||||
Repr Python apare in codul curent cand Jinja2 randeaza
|
||||
``{{ e.values() | list | first }}`` unde valoarea e lista ``unmapped``:
|
||||
[{'cod_op_service': 'OP-FARA-COD', 'denumire': 'OP-FARA-COD'}]
|
||||
"""
|
||||
_, html = _upload_and_preview(client, _ROWS_UNMAPPED)
|
||||
|
||||
# Confirma ca preview-ul contine cel putin un rand needs_mapping (fixture corect)
|
||||
has_nm = (
|
||||
"s-needs_mapping" in html
|
||||
or "needs_mapping" in html
|
||||
or "Cod RAR lipsa" in html
|
||||
)
|
||||
assert has_nm, "Testul necesita cel putin un rand needs_mapping — fixture gresit"
|
||||
|
||||
# Repr Python brut: Jinja2 auto-escapa ghilimelele simple -> '
|
||||
# Deci [{'cod_op_service': ...}] devine [{'cod_op_service': ...}] in HTML.
|
||||
# Verificam secventa specifica a repr-ului HTML-escapata:
|
||||
assert "'cod_op_service'" not in html, (
|
||||
"Repr Python HTML-escapata ('cod_op_service') gasita in HTML — "
|
||||
"adaptorul trebuie sa formateze erorile uman INAINTE de randare in Note"
|
||||
)
|
||||
|
||||
|
||||
def test_preview_stare_eticheta_umana(client):
|
||||
"""Starea din pill-ul fiecarui rand este eticheta umana, nu codul brut.
|
||||
|
||||
Testeaza starile ok si needs_mapping (cele mai comune in preview).
|
||||
Celelalte stari (needs_review, already_sent, duplicate_in_file) sunt in testele ext.
|
||||
"""
|
||||
_seed_nomenclator_si_mapare()
|
||||
_, html = _upload_and_preview(client, _ROWS_OK_SI_UNMAPPED)
|
||||
|
||||
# Etichetele umane trebuie sa apara in HTML (din pill-urile randurilor)
|
||||
# "Gata de trimis" cu majuscula — nu confundam cu "gata de trimis" din
|
||||
# summary pills (deja prezente in codul curent cu lowercase)
|
||||
assert "Gata de trimis" in html, (
|
||||
"Eticheta umana 'Gata de trimis' lipsa — "
|
||||
"pill-ul randului ok afiseaza inca codul brut"
|
||||
)
|
||||
assert "Cod RAR lipsa" in html, (
|
||||
"Eticheta umana 'Cod RAR lipsa' lipsa — "
|
||||
"pill-ul randului needs_mapping afiseaza inca codul brut"
|
||||
)
|
||||
|
||||
# Codurile brute NU trebuie sa apara ca text vizibil al pill-ului de rand.
|
||||
# Cautam <span class="pill ...">ok</span> (codul brut ca text al pill-ului).
|
||||
assert re.search(r'class="pill[^"]*">ok<', html) is None, (
|
||||
"Pill cu text brut 'ok' gasit in randuri — trebuie 'Gata de trimis'"
|
||||
)
|
||||
assert re.search(r'class="pill[^"]*">needs_mapping<', html) is None, (
|
||||
"Pill cu text brut 'needs_mapping' gasit in randuri — trebuie 'Cod RAR lipsa'"
|
||||
)
|
||||
|
||||
|
||||
def test_preview_stare_eticheta_umana_duplicate(client):
|
||||
"""Starea duplicate_in_file afiseaza eticheta umana 'Duplicat in fisier'."""
|
||||
_seed_nomenclator_si_mapare()
|
||||
_, html = _upload_and_preview(client, _ROWS_DUPLICATE)
|
||||
|
||||
# Confirma ca exista randuri duplicate_in_file
|
||||
has_dup = "duplicate_in_file" in html or "Duplicat in fisier" in html
|
||||
assert has_dup, "Testul necesita randuri duplicate_in_file — fixture gresit"
|
||||
|
||||
assert "Duplicat in fisier" in html, (
|
||||
"Eticheta umana 'Duplicat in fisier' lipsa — "
|
||||
"pill-ul randului duplicate_in_file afiseaza inca codul brut"
|
||||
)
|
||||
assert re.search(r'class="pill[^"]*">duplicate_in_file<', html) is None, (
|
||||
"Pill cu text brut 'duplicate_in_file' gasit — trebuie 'Duplicat in fisier'"
|
||||
)
|
||||
|
||||
|
||||
def test_nota_umana_preview_needs_mapping_cu_flag_prioritizeaza_unmapped():
|
||||
"""needs_mapping + flag -> Note afiseaza 'Cod RAR lipsa', nu textul flag-ului.
|
||||
|
||||
BUG 3: nota_umana_preview verifica `if flags:` inaintea ramurei unmapped.
|
||||
Un rand needs_mapping care are si un flag (ex. VIN numeric) afisa textul
|
||||
flag-ului in loc de 'Cod RAR lipsa pentru: COD' — exact confuzia US-003
|
||||
voia s-o evite (userul corecteaza data si ramane blocat pe cod).
|
||||
Fix: cand status == 'needs_mapping', prioritizeaza ramura unmapped.
|
||||
"""
|
||||
from app.web.labels import nota_umana_preview
|
||||
|
||||
errors = [{"unmapped": [{"cod_op_service": "OP-TEST", "denumire": "Op Test"}]}]
|
||||
flags = ["VIN numeric (12345) — verificati seria sasiului"]
|
||||
|
||||
result = nota_umana_preview("needs_mapping", errors, flags)
|
||||
|
||||
assert "Cod RAR lipsa" in result, (
|
||||
f"needs_mapping cu flag trebuie sa afiseze 'Cod RAR lipsa', "
|
||||
f"nu textul flag-ului — primit: {result!r}"
|
||||
)
|
||||
assert "VIN numeric" not in result, (
|
||||
f"needs_mapping cu flag NU trebuie sa afiseze textul flag-ului — primit: {result!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_preview_foloseste_clasa_tabel_trimiteri(client):
|
||||
"""Tabelul de preview foloseste clasa CSS .tabel-trimiteri (nu doar .tablewrap).
|
||||
|
||||
Necesara pentru:
|
||||
- table-layout:fixed (fara overflow orizontal la 1280px)
|
||||
- carduri <768px: td::before citeste data-eticheta
|
||||
- col-* latimi pentru cele 4 coloane extra (col-km, col-note, col-verificat, col-actiuni)
|
||||
"""
|
||||
_, html = _upload_and_preview(client, _ROWS_UNMAPPED)
|
||||
|
||||
assert "tabel-trimiteri" in html, (
|
||||
"Clasa CSS 'tabel-trimiteri' lipsa din tabelul de preview — "
|
||||
"necesara pentru table-layout:fixed si carduri mobile"
|
||||
)
|
||||
@@ -1,8 +1,18 @@
|
||||
"""Teste T6: gate auto_send pe coduri nou-mapate (OV-1).
|
||||
"""Teste T6: comportament dupa US-001 (PRD 5.11) — auto_send nu mai tine randuri.
|
||||
|
||||
Verify:
|
||||
(a) cod nou-mapat cu auto_send=0 -> nu auto-send, review manual.
|
||||
(b) REGRESIE: mapare existenta cu auto_send=1 tot se requeue ca azi.
|
||||
US-001: has_no_auto_send neutralizat (return False); un cod rezolvat (mapare exacta
|
||||
sau regula text) -> queued direct, indiferent de auto_send=0/1 in mapping_meta.
|
||||
|
||||
Coloanele DB operations_mapping.auto_send si operation_text_rules.auto_send RAMAN
|
||||
(default 1, ne-citite pentru hold). Functia has_no_auto_send RAMANE DEFINITA (importata
|
||||
in routes.py + import_router.py) dar intoarce mereu False.
|
||||
|
||||
Inainte de US-001:
|
||||
(a) cod nou-mapat cu auto_send=0 -> nu auto-send, review manual (needs_mapping).
|
||||
(b) mapare existenta cu auto_send=1 -> queued.
|
||||
Dupa US-001:
|
||||
(a) cod nou-mapat cu auto_send=0 -> queued (ca si (b)).
|
||||
(b) mapare existenta cu auto_send=1 -> queued (neschimbat).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -70,6 +80,7 @@ def _add_mapping(conn, account_id=1, cod_op="ITP-CHECK", cod_prestatie="OE-1", a
|
||||
# --- load_mapping_meta ---
|
||||
|
||||
def test_load_mapping_meta_returns_auto_send(conn):
|
||||
"""load_mapping_meta expune coloana auto_send din DB (ramane, default 1)."""
|
||||
from app.mapping import load_mapping_meta
|
||||
_add_mapping(conn, cod_op="ITP-1", cod_prestatie="OE-1", auto_send=True)
|
||||
_add_mapping(conn, cod_op="ITP-2", cod_prestatie="OE-2", auto_send=False)
|
||||
@@ -79,18 +90,20 @@ def test_load_mapping_meta_returns_auto_send(conn):
|
||||
assert meta["ITP-2"]["auto_send"] is False
|
||||
|
||||
|
||||
# --- has_no_auto_send ---
|
||||
# --- has_no_auto_send (neutralizat, mereu False) ---
|
||||
|
||||
def test_has_no_auto_send_detecteaza_false(conn):
|
||||
def test_has_no_auto_send_mereu_false_cu_auto_send_fals(conn):
|
||||
"""has_no_auto_send intoarce False chiar si cand auto_send=False in mapping_meta (US-001)."""
|
||||
from app.mapping import has_no_auto_send
|
||||
mapping_meta = {
|
||||
"ITP-1": {"cod_prestatie": "OE-1", "auto_send": False},
|
||||
}
|
||||
resolved = [{"cod_op_service": "ITP-1", "cod_prestatie": "OE-1"}]
|
||||
assert has_no_auto_send(resolved, mapping_meta) is True
|
||||
assert has_no_auto_send(resolved, mapping_meta) is False
|
||||
|
||||
|
||||
def test_has_no_auto_send_trece_cu_true(conn):
|
||||
def test_has_no_auto_send_mereu_false_cu_auto_send_true(conn):
|
||||
"""has_no_auto_send intoarce False si cand auto_send=True (neschimbat, mereu False)."""
|
||||
from app.mapping import has_no_auto_send
|
||||
mapping_meta = {
|
||||
"ITP-1": {"cod_prestatie": "OE-1", "auto_send": True},
|
||||
@@ -100,46 +113,44 @@ def test_has_no_auto_send_trece_cu_true(conn):
|
||||
|
||||
|
||||
def test_has_no_auto_send_direct_cod_prestatie(conn):
|
||||
"""Item cu cod_prestatie direct (fara cod_op_service) nu e afectat de auto_send."""
|
||||
"""Item cu cod_prestatie direct (fara cod_op_service) -> False (neschimbat)."""
|
||||
from app.mapping import has_no_auto_send
|
||||
mapping_meta = {}
|
||||
resolved = [{"cod_prestatie": "OE-1"}]
|
||||
assert has_no_auto_send(resolved, mapping_meta) is False
|
||||
|
||||
|
||||
# --- reresolve_account cu auto_send=0 ---
|
||||
# --- reresolve_account cu auto_send=0 (US-001: acum requeue) ---
|
||||
|
||||
def test_reresolve_auto_send_zero_nu_requeue(conn):
|
||||
"""(a) cod nou-mapat cu auto_send=0 -> ramane needs_mapping (nu trece pe queued)."""
|
||||
def test_reresolve_auto_send_zero_acum_requeue(conn):
|
||||
"""(US-001) cod nou-mapat cu auto_send=0 -> queued (nu mai review_manual)."""
|
||||
from app.mapping import reresolve_account
|
||||
sid = _insert_needs_mapping(conn, cod_op="ITP-CHECK")
|
||||
_add_mapping(conn, cod_op="ITP-CHECK", cod_prestatie="OE-1", auto_send=False)
|
||||
|
||||
stats = reresolve_account(conn, 1)
|
||||
assert stats["review_manual"] == 1
|
||||
assert stats["requeued"] == 0
|
||||
assert stats["requeued"] == 1, f"asteptat requeued=1, got {stats}"
|
||||
assert stats.get("review_manual", 0) == 0
|
||||
|
||||
row = conn.execute("SELECT status, rar_error FROM submissions WHERE id=?", (sid,)).fetchone()
|
||||
assert row["status"] == "needs_mapping"
|
||||
err = json.loads(row["rar_error"])
|
||||
assert "auto_send" in err
|
||||
row = conn.execute("SELECT status FROM submissions WHERE id=?", (sid,)).fetchone()
|
||||
assert row["status"] == "queued"
|
||||
|
||||
|
||||
def test_reresolve_auto_send_unu_requeue(conn):
|
||||
"""(b) REGRESIE: mapare cu auto_send=1 tot se requeue ca azi."""
|
||||
"""REGRESIE: mapare cu auto_send=1 -> queued (neschimbat)."""
|
||||
from app.mapping import reresolve_account
|
||||
sid = _insert_needs_mapping(conn, cod_op="ITP-CHECK")
|
||||
_add_mapping(conn, cod_op="ITP-CHECK", cod_prestatie="OE-1", auto_send=True)
|
||||
|
||||
stats = reresolve_account(conn, 1)
|
||||
assert stats["requeued"] == 1
|
||||
assert stats["review_manual"] == 0
|
||||
assert stats.get("review_manual", 0) == 0
|
||||
|
||||
row = conn.execute("SELECT status FROM submissions WHERE id=?", (sid,)).fetchone()
|
||||
assert row["status"] == "queued"
|
||||
|
||||
|
||||
# --- POST /v1/prezentari cu auto_send=0 ---
|
||||
# --- POST /v1/prezentari cu auto_send=0 (US-001: acum queued) ---
|
||||
|
||||
def _body_with_op(cod_op="ITP-CHECK"):
|
||||
return {
|
||||
@@ -154,8 +165,8 @@ def _body_with_op(cod_op="ITP-CHECK"):
|
||||
}
|
||||
|
||||
|
||||
def test_post_auto_send_zero_nu_queued(client, env):
|
||||
"""(a) Via API: cod nou-mapat cu auto_send=0 -> nu 'queued', review manual."""
|
||||
def test_post_auto_send_zero_acum_queued(client, env):
|
||||
"""(US-001) Via API: cod nou-mapat cu auto_send=0 -> queued (nu mai needs_mapping)."""
|
||||
from app.db import get_connection
|
||||
conn2 = get_connection()
|
||||
try:
|
||||
@@ -166,12 +177,11 @@ def test_post_auto_send_zero_nu_queued(client, env):
|
||||
r = client.post("/v1/prezentari", json=_body_with_op("ITP-X"))
|
||||
assert r.status_code == 200
|
||||
status = r.json()["results"][0]["status"]
|
||||
assert status != "queued", f"auto_send=0 nu trebuie sa fie queued, e: {status}"
|
||||
assert status == "needs_mapping"
|
||||
assert status == "queued", f"auto_send=0 dupa US-001 -> queued, e: {status}"
|
||||
|
||||
|
||||
def test_post_auto_send_unu_queued(client, env):
|
||||
"""(b) REGRESIE: mapare existenta cu auto_send=1 -> queued ca azi."""
|
||||
"""REGRESIE: mapare existenta cu auto_send=1 -> queued ca inainte."""
|
||||
from app.db import get_connection
|
||||
conn2 = get_connection()
|
||||
try:
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""FIX (code-review 5.8): o regula text cu auto_send=0 (DEFAULT, decizia CEO) trebuie
|
||||
sa TINA randul pentru verificare umana (needs_mapping/review), NU sa-l trimita automat.
|
||||
"""Teste reguli text + auto_send dupa US-001 (PRD 5.11).
|
||||
|
||||
`has_no_auto_send` trebuie sa prinda si itemii rezolvati-prin-regula-text cu auto_send=0,
|
||||
nu doar maparile exacte din operations_mapping. Adnotarile (cod_sursa/regula_fara_autosend)
|
||||
trebuie curatate la fiecare rezolvare (anti-staleness).
|
||||
Inainte de US-001: o regula text cu auto_send=0 tinea randul in needs_mapping
|
||||
(regula_fara_autosend=True + has_no_auto_send -> True).
|
||||
|
||||
Dupa US-001: has_no_auto_send mereu False; o regula text cu auto_send=0 rezolva
|
||||
codul RAR si randul intra in queued direct. regula_fara_autosend nu se mai seteaza.
|
||||
|
||||
Functii pure -> teste fara DB.
|
||||
"""
|
||||
@@ -26,34 +27,37 @@ def _content_cu(op_denumire="Verificare faruri"):
|
||||
return {**_CONTENT, "prestatii": [{"cod_op_service": "X99", "denumire": op_denumire}]}
|
||||
|
||||
|
||||
def test_regula_auto_send_0_tine_randul():
|
||||
"""Regula text auto_send=0 + continut valid -> needs_mapping (review), NU queued."""
|
||||
def test_regula_auto_send_0_acum_queued():
|
||||
"""Regula text auto_send=0 + continut valid -> queued (nu mai needs_mapping) dupa US-001."""
|
||||
tr = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 0, "priority": 0}]
|
||||
cl = classify_prezentare(_content_cu(), {}, {}, VALID, tr)
|
||||
assert cl["status"] == "needs_mapping", f"asteptat needs_mapping (held), got {cl['status']}"
|
||||
assert cl["status"] == "queued", f"asteptat queued (auto_send ignorat), got {cl['status']}"
|
||||
|
||||
|
||||
def test_regula_auto_send_1_trece_in_coada():
|
||||
"""Regula text auto_send=1 + continut valid -> queued (trimite automat)."""
|
||||
"""Regula text auto_send=1 + continut valid -> queued (neschimbat)."""
|
||||
tr = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 1, "priority": 0}]
|
||||
cl = classify_prezentare(_content_cu(), {}, {}, VALID, tr)
|
||||
assert cl["status"] == "queued", f"asteptat queued, got {cl['status']}"
|
||||
|
||||
|
||||
def test_has_no_auto_send_prinde_flagul_regula():
|
||||
"""has_no_auto_send=True cand un item poarta regula_fara_autosend; codul e tot rezolvat."""
|
||||
def test_has_no_auto_send_mereu_false_cu_flag_regula():
|
||||
"""has_no_auto_send=False chiar daca regula_fara_autosend e prezent (US-001)."""
|
||||
tr = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 0, "priority": 0}]
|
||||
resolved, unmapped = resolve_prestatii(
|
||||
[{"cod_op_service": "X99", "denumire": "Verificare faruri"}], {}, VALID, tr
|
||||
)
|
||||
assert unmapped == []
|
||||
assert resolved[0]["cod_prestatie"] == "OE-2"
|
||||
assert resolved[0].get("regula_fara_autosend") is True
|
||||
assert has_no_auto_send(resolved, {}) is True
|
||||
# Flagul regula_fara_autosend nu se mai seteaza (US-001)
|
||||
assert resolved[0].get("regula_fara_autosend") is None, (
|
||||
"regula_fara_autosend nu trebuie sa mai fie setat dupa US-001"
|
||||
)
|
||||
assert has_no_auto_send(resolved, {}) is False
|
||||
|
||||
|
||||
def test_has_no_auto_send_fals_cand_regula_auto_send_1():
|
||||
"""Regula auto_send=1 -> fara flag -> has_no_auto_send False."""
|
||||
"""Regula auto_send=1 -> fara flag -> has_no_auto_send False (neschimbat)."""
|
||||
tr = [{"pattern": "verificare", "cod_prestatie": "OE-2", "auto_send": 1, "priority": 0}]
|
||||
resolved, _ = resolve_prestatii(
|
||||
[{"cod_op_service": "X99", "denumire": "Verificare faruri"}], {}, VALID, tr
|
||||
@@ -64,14 +68,13 @@ def test_has_no_auto_send_fals_cand_regula_auto_send_1():
|
||||
|
||||
def test_adnotari_stale_curatate_la_mapare_exacta():
|
||||
"""Un item venit cu cod_sursa/regula_fara_autosend stale dar re-rezolvat acum prin
|
||||
mapare EXACTA cu auto_send=1 -> adnotarile sunt curatate; randul NU mai e tinut."""
|
||||
mapare EXACTA -> adnotarile sunt curatate; randul NU mai e tinut (neschimbat)."""
|
||||
item_stale = {
|
||||
"cod_op_service": "X99",
|
||||
"denumire": "Verificare faruri",
|
||||
"cod_sursa": "text_rule:verificare",
|
||||
"regula_fara_autosend": True,
|
||||
}
|
||||
# Acum X99 are mapare exacta cu auto_send=1.
|
||||
mapping = {"X99": "OE-2"}
|
||||
mapping_meta = {"X99": {"cod_prestatie": "OE-2", "auto_send": True}}
|
||||
resolved, _ = resolve_prestatii([item_stale], mapping, VALID, text_rules=None)
|
||||
|
||||
130
tests/test_web_acasa.py
Normal file
130
tests/test_web_acasa.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""Teste US-006 (PRD 5.11): Import = container compact colapsabil.
|
||||
|
||||
TDD — testele sunt scrise INAINTE de implementare (RED), apoi devin GREEN.
|
||||
|
||||
Verifica:
|
||||
- Containerul de import e colapsat implicit cand exista trimiteri (are_trimiteri=True)
|
||||
- Containerul de import e deschis la first-run (are_trimiteri=False)
|
||||
- Implementare cu <details> nativ (CSS-only disclosure)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Fixture
|
||||
# ============================================================
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
"""Client cu BD izolata; auth dezactivat (cont implicit id=1)."""
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "acasa_test.db"))
|
||||
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.web import ratelimit
|
||||
ratelimit._hits.clear()
|
||||
from app.main import app
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
ratelimit._hits.clear()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Helpere
|
||||
# ============================================================
|
||||
|
||||
def _add_submission(acct_id: int = 1) -> None:
|
||||
"""Adauga un submission minimal pentru cont (simuleaza un import efectuat)."""
|
||||
from app.db import get_connection
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) "
|
||||
"VALUES (?, ?, 'queued', ?)",
|
||||
(f"test_key_us006_{acct_id}", acct_id, json.dumps({"test": True})),
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _detalii_import(html: str) -> re.Match | None:
|
||||
"""Returneaza match-ul pentru tagul <details> care contine containerul de import."""
|
||||
# Cauta <details cu id="import-details" (sau variante)
|
||||
m = re.search(r'<details([^>]*)id=["\']import-details["\']([^>]*)>', html)
|
||||
if m:
|
||||
return m
|
||||
# Fallback: primul <details> care nu e .kebab (e containerul de import)
|
||||
m2 = re.search(r'<details(?!.*class=["\'][^"\']*kebab)([^>]*)>', html)
|
||||
return m2
|
||||
|
||||
|
||||
# ============================================================
|
||||
# test_import_colapsat_cand_are_trimiteri
|
||||
# ============================================================
|
||||
|
||||
def test_import_colapsat_cand_are_trimiteri(client):
|
||||
"""Cand contul are deja trimiteri, containerul de import e colapsat (fara atribut open).
|
||||
|
||||
Serverul NU seteaza atributul `open` pe <details> cand are_trimiteri=True,
|
||||
deci browserul il randeaza colapsat implicit.
|
||||
"""
|
||||
_add_submission(acct_id=1)
|
||||
|
||||
resp = client.get("/_fragments/acasa")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Containerul de import trebuie sa existe ca element <details>
|
||||
m = _detalii_import(html)
|
||||
assert m is not None, (
|
||||
"Nu s-a gasit un element <details> pentru containerul de import. "
|
||||
"Implementati containerul cu <details id='import-details'>."
|
||||
)
|
||||
|
||||
# Atributul `open` NU trebuie sa fie prezent pe <details> cand exista trimiteri
|
||||
tag = m.group(0)
|
||||
assert "open" not in tag.lower(), (
|
||||
f"<details> are atributul 'open' cand sunt trimiteri existente — trebuie colapsat: {tag}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# test_import_deschis_la_first_run
|
||||
# ============================================================
|
||||
|
||||
def test_import_deschis_la_first_run(client):
|
||||
"""La first-run (zero trimiteri), containerul de import e deschis (atribut open prezent).
|
||||
|
||||
Serverul seteaza `open` pe <details> cand are_trimiteri=False, deci importul
|
||||
e vizibil imediat fara JS.
|
||||
"""
|
||||
# Nu adaugam niciun submission — cont proaspat
|
||||
resp = client.get("/_fragments/acasa")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Containerul de import trebuie sa existe ca element <details>
|
||||
m = _detalii_import(html)
|
||||
assert m is not None, (
|
||||
"Nu s-a gasit un element <details> pentru containerul de import. "
|
||||
"Implementati containerul cu <details id='import-details'>."
|
||||
)
|
||||
|
||||
# Atributul `open` TREBUIE sa fie prezent pe <details> la first-run
|
||||
tag = m.group(0)
|
||||
assert "open" in tag.lower(), (
|
||||
f"<details> NU are atributul 'open' la first-run — trebuie deschis: {tag}"
|
||||
)
|
||||
286
tests/test_web_filtre.py
Normal file
286
tests/test_web_filtre.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""Teste US-004 (PRD 5.11): Rand filtre Trimiteri — layout + stil ca referinta.
|
||||
|
||||
TDD — testele sunt scrise INAINTE de implementare (RED), apoi devin GREEN.
|
||||
|
||||
Verifica:
|
||||
- Quick-pills de data (Azi/7 zile/30 zile) in STANGA, inainte de campul vehicul si de pills-stare
|
||||
- Pill-urile folosesc stil uniform (color-mix la hover, nu filter:brightness)
|
||||
- Quick-pills seteaza data_de/data_pana si reincarca lista pastrand starea activa
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Helpers
|
||||
# ============================================================
|
||||
|
||||
def _create_account_user(email: str, password: str = "parolasecreta10"):
|
||||
from app.accounts import create_account
|
||||
from app.users import create_user
|
||||
from app.db import get_connection
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
acct_id = create_account(conn, f"Service {email}", active=True)
|
||||
create_user(conn, acct_id, email, password)
|
||||
return acct_id
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _login(client, email: str, password: str = "parolasecreta10") -> None:
|
||||
resp = client.get("/login")
|
||||
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) or \
|
||||
re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
|
||||
assert m, "csrf_token negasit pe /login"
|
||||
resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)})
|
||||
assert resp.status_code == 303, f"Login esuat: {resp.status_code}"
|
||||
|
||||
|
||||
def _ins(acct: int, *, status: str = "needs_mapping") -> int:
|
||||
from app.db import get_connection
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) VALUES (?, ?, ?, ?)",
|
||||
(
|
||||
f"k-us004-{os.urandom(4).hex()}",
|
||||
acct,
|
||||
status,
|
||||
json.dumps({
|
||||
"vin": "WVWZZZ1KZAW009999",
|
||||
"nr_inmatriculare": "B001TST",
|
||||
"data_prestatie": "2026-06-20",
|
||||
"odometru_final": "100000",
|
||||
"prestatii": [{"cod_prestatie": "OE-1"}],
|
||||
}),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return int(cur.lastrowid)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Fixture
|
||||
# ============================================================
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "filtre_us004.db"))
|
||||
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true")
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.web import ratelimit
|
||||
ratelimit._hits.clear()
|
||||
from app.main import app
|
||||
|
||||
with TestClient(app, follow_redirects=False) as c:
|
||||
yield c
|
||||
ratelimit._hits.clear()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# test_pill_uri_in_stanga_controalelor
|
||||
# ============================================================
|
||||
|
||||
def test_pill_uri_in_stanga_controalelor(client):
|
||||
"""Quick-pills de data (Azi/7 zile/30 zile) apar in STANGA formularului de filtre.
|
||||
|
||||
Ordinea in DOM: quick-pills → camp cautare vehicul → pills stare.
|
||||
Pill-urile de stare NU mai stau izolate la dreapta butonului Filtreaza cu margin-left:auto
|
||||
pe un span separat — layout-ul e controlat explicit prin pozitia in form.
|
||||
"""
|
||||
acct = _create_account_user("stanga@test.com")
|
||||
_ins(acct, status="needs_mapping")
|
||||
_login(client, "stanga@test.com")
|
||||
|
||||
resp = client.get("/?tab=acasa")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Quick-pills trebuie sa fie prezente in forma
|
||||
assert "Azi" in html, "Quick-pill 'Azi' trebuie sa fie prezent in bara de filtre"
|
||||
assert "7 zile" in html, "Quick-pill '7 zile' trebuie sa fie prezent in bara de filtre"
|
||||
assert "30 zile" in html, "Quick-pill '30 zile' trebuie sa fie prezent in bara de filtre"
|
||||
|
||||
# DOM order: quick-pills STANGA (index mai mic in HTML) fata de campul vehicul
|
||||
idx_azi = html.find("Azi")
|
||||
idx_vehicul = html.find('id="f-vehicul"')
|
||||
assert idx_azi != -1, "'Azi' nu s-a gasit in HTML"
|
||||
assert idx_vehicul != -1, "'f-vehicul' nu s-a gasit in HTML"
|
||||
assert idx_azi < idx_vehicul, (
|
||||
"Quick-pill 'Azi' trebuie sa apara INAINTE de campul f-vehicul in DOM (stanga)"
|
||||
)
|
||||
|
||||
# Pills stare (pills-categorii) la DREAPTA (dupa vehicul)
|
||||
idx_pills_cat = html.find('id="pills-categorii"')
|
||||
assert idx_pills_cat != -1, "pills-categorii nu s-a gasit in HTML"
|
||||
assert idx_vehicul < idx_pills_cat, (
|
||||
"Campul vehicul trebuie sa apara INAINTE de pills-categorii in DOM (pills-stare la dreapta)"
|
||||
)
|
||||
|
||||
# Quick-pills apar si inainte de pills-categorii (stanga totala)
|
||||
assert idx_azi < idx_pills_cat, (
|
||||
"Quick-pills de data trebuie sa apara INAINTE de pills-categorii"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# test_pill_categorie_stil_uniform
|
||||
# ============================================================
|
||||
|
||||
def test_pill_categorie_stil_uniform(client):
|
||||
"""Pill-urile au un singur stil uniform: hover cu color-mix, activ suprima hover.
|
||||
|
||||
- Hover trebuie sa foloseasca color-mix(in srgb, currentColor 12%, transparent)
|
||||
si NU filter:brightness (care devenea rosu plin si ilizibil).
|
||||
- Focus :focus-visible pastrat pe pill-cat.
|
||||
- Pill-cat-reset activ = --accent; pill-cat activ = culoarea categoriei (nu toate accent).
|
||||
"""
|
||||
acct = _create_account_user("stil@test.com")
|
||||
_login(client, "stil@test.com")
|
||||
|
||||
resp = client.get("/")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# CSS-ul trebuie sa contina color-mix pentru hover pe pill-cat
|
||||
assert "color-mix" in html, (
|
||||
"pill-cat:hover trebuie sa foloseasca color-mix, nu filter:brightness"
|
||||
)
|
||||
|
||||
# CSS-ul NU trebuie sa foloseasca filter:brightness pe .pill-cat:hover
|
||||
m = re.search(r'\.pill-cat:hover\s*\{([^}]*)\}', html)
|
||||
if m:
|
||||
hover_rule = m.group(1)
|
||||
assert "brightness" not in hover_rule, (
|
||||
f"pill-cat:hover NU trebuie sa contina filter:brightness — regula gasita: {hover_rule}"
|
||||
)
|
||||
|
||||
# focus-visible pastrat pe pill-cat
|
||||
assert "pill-cat:focus-visible" in html, (
|
||||
"pill-cat trebuie sa pastreze regula :focus-visible cu outline"
|
||||
)
|
||||
|
||||
# Pill-cat-reset activ foloseste --accent (nu culoarea categoriei)
|
||||
assert "pill-cat-reset" in html, "Clasa pill-cat-reset trebuie sa existe pentru butonul Toate"
|
||||
assert "var(--accent)" in html, (
|
||||
"Pill Toate activ trebuie sa foloseasca var(--accent)"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# test_quick_pills_data_seteaza_interval
|
||||
# ============================================================
|
||||
|
||||
def test_quick_pills_data_seteaza_interval(client):
|
||||
"""Quick-pills de data seteaza data_de/data_pana (preset) si reincarca lista HTMX.
|
||||
|
||||
Pastrand pill-ul de stare activ: setDataRange NU schimba campul #f-status.
|
||||
"""
|
||||
acct = _create_account_user("datepill@test.com")
|
||||
_ins(acct, status="needs_mapping")
|
||||
_login(client, "datepill@test.com")
|
||||
|
||||
resp = client.get("/?tab=acasa")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Trebuie sa existe un mecanism JS care seteaza data_de si data_pana
|
||||
assert "setDataRange" in html, (
|
||||
"Quick-pills trebuie sa apeleze setDataRange (functie JS pentru setarea intervalului de date)"
|
||||
)
|
||||
|
||||
# setDataRange trebuie sa seteze campul data_de/data_pana (prin id sau name)
|
||||
assert "f-data-de" in html or "data_de" in html, (
|
||||
"setDataRange trebuie sa seteze campul data_de (prin id f-data-de sau name data_de)"
|
||||
)
|
||||
assert "f-data-pana" in html or "data_pana" in html, (
|
||||
"setDataRange trebuie sa seteze campul data_pana (prin id f-data-pana sau name data_pana)"
|
||||
)
|
||||
|
||||
# Lista trebuie sa se reincarce prin form (HTMX) la click pe quick-pill
|
||||
assert "/_fragments/submissions" in html, (
|
||||
"Formularul de filtre trebuie sa trimita catre /_fragments/submissions"
|
||||
)
|
||||
|
||||
# setDataRange NU trebuie sa schimbe campul de status (pastreaza pill-ul de stare activ)
|
||||
# Verificam ca in JS-ul setDataRange nu se face `hs.value = ` (schimbare status)
|
||||
# prin cautarea functiei in HTML
|
||||
idx_fn = html.find("setDataRange")
|
||||
assert idx_fn != -1
|
||||
# Extrage corpul functiei (pana la urmatoarea definitie de functie mare)
|
||||
fn_body = html[idx_fn:idx_fn + 800]
|
||||
assert "f-status" not in fn_body or "hs.value" not in fn_body[:fn_body.find("f-data")], (
|
||||
"setDataRange NU trebuie sa modifice campul f-status (pastreaza filtrul de stare)"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# test_custom_pill_prezent_si_dezvaluie_campuri
|
||||
# ============================================================
|
||||
|
||||
def test_custom_pill_prezent_si_dezvaluie_campuri(client):
|
||||
"""Butonul Custom este al 4-lea quick-pill si dezvaluie campurile de data manuala.
|
||||
|
||||
AC US-004: Azi / 7 zile / 30 zile / Custom (4 quick-pills).
|
||||
Custom NU seteaza un preset; dezvaluie #custom-date-fields cu focus pe #f-data-de.
|
||||
Campurile #f-data-de/#f-data-pana sunt de tip 'date' (nu hidden) pentru interactiune.
|
||||
"""
|
||||
acct = _create_account_user("custom@test.com")
|
||||
_ins(acct, status="needs_mapping")
|
||||
_login(client, "custom@test.com")
|
||||
|
||||
resp = client.get("/?tab=acasa")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Butonul Custom trebuie sa fie prezent in quick-pills
|
||||
assert "Custom" in html, "Butonul 'Custom' trebuie sa fie prezent in bara de filtre (4 quick-pills)"
|
||||
|
||||
# Butonul Custom apeleaza setDataRange cu 'custom'
|
||||
assert "setDataRange" in html and "'custom'" in html, (
|
||||
"Butonul Custom trebuie sa apeleze setDataRange(this,'custom')"
|
||||
)
|
||||
|
||||
# In ramura 'custom' din JS, NU se apeleaza requestSubmit/form.submit
|
||||
# (se dezvaluie campurile; utilizatorul introduce datele si form-ul submite la change)
|
||||
# Cautam in JS (range === 'custom'), nu in atributul onclick al butonului
|
||||
idx_fn = html.find("range === 'custom'")
|
||||
assert idx_fn != -1, "Conditia `range === 'custom'` trebuie sa existe in JS (setDataRange)"
|
||||
# Cautam in blocul imediat urmator conditiei: trebuie sa apara 'return'
|
||||
# INAINTE de 'requestSubmit' (dovada ca nu submite automat in ramura custom)
|
||||
block_custom = html[idx_fn:idx_fn + 500]
|
||||
idx_return = block_custom.find("return")
|
||||
idx_submit = block_custom.find("requestSubmit")
|
||||
assert idx_return != -1, (
|
||||
"Ramura 'custom' din setDataRange trebuie sa contina 'return' pentru a nu submite automat"
|
||||
)
|
||||
assert idx_submit == -1 or idx_return < idx_submit, (
|
||||
"Ramura 'custom' NU trebuie sa apeleze requestSubmit inainte de 'return'"
|
||||
)
|
||||
|
||||
# Campurile de data trebuie sa existe si sa fie de tip 'date' (nu hidden)
|
||||
# pentru ca utilizatorul sa le poata interactiona in modul Custom
|
||||
import re
|
||||
m_de = re.search(r'<input[^>]+id="f-data-de"[^>]*>', html)
|
||||
assert m_de, "Input #f-data-de negasit in HTML"
|
||||
tag_de = m_de.group(0)
|
||||
assert 'type="hidden"' not in tag_de, (
|
||||
"Input #f-data-de NU trebuie sa fie type='hidden' — trebuie sa fie tip 'date' "
|
||||
"pentru interactiune in modul Custom"
|
||||
)
|
||||
160
tests/test_web_mapari.py
Normal file
160
tests/test_web_mapari.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""Teste US-002 (PRD 5.11) — scoate bifa auto_send din UI.
|
||||
|
||||
Verifica ca bifa "In coada automat" (macro autosend_toggle) nu mai apare in:
|
||||
- tab-ul Mapari (/_fragments/mapari)
|
||||
- panoul de mapare din preview (pas 3 import)
|
||||
- panoul de detaliu trimitere (_trimitere_detaliu.html)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "us002.db"))
|
||||
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.main import app
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def conn(client):
|
||||
from app.db import get_connection
|
||||
c = get_connection()
|
||||
yield c
|
||||
c.close()
|
||||
|
||||
|
||||
def _csv_bytes(rows: list[dict]) -> bytes:
|
||||
buf = io.StringIO()
|
||||
writer = csv.DictWriter(buf, fieldnames=list(rows[0].keys()), delimiter=";")
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
return buf.getvalue().encode("utf-8")
|
||||
|
||||
|
||||
def _seed_nomenclator(conn, cod="OE-1"):
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)",
|
||||
(cod, f"Operatie {cod}"),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO operations_mapping (account_id, cod_op_service, cod_prestatie, auto_send) "
|
||||
"VALUES (1, 'OP-EXISTENT', ?, 1)",
|
||||
(cod,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def test_mapari_fara_toggle_autosend(client, conn):
|
||||
"""GET /_fragments/mapari nu contine checkbox auto_send sau coloana 'In coada'.
|
||||
|
||||
Dupa US-002: macro autosend_toggle golit; coloana In coada scoasa din toate tabelele.
|
||||
"""
|
||||
_seed_nomenclator(conn)
|
||||
r = client.get("/_fragments/mapari")
|
||||
assert r.status_code == 200
|
||||
body = r.text
|
||||
assert 'name="auto_send"' not in body, (
|
||||
"tab-ul Mapari nu trebuie sa contina input auto_send dupa US-002"
|
||||
)
|
||||
assert "autosend-toggle" not in body, (
|
||||
"tab-ul Mapari nu trebuie sa contina clasa autosend-toggle dupa US-002"
|
||||
)
|
||||
assert "In coada" not in body, (
|
||||
"tab-ul Mapari nu trebuie sa contina coloana 'In coada' dupa US-002"
|
||||
)
|
||||
|
||||
|
||||
def test_preview_panou_mapare_fara_autosend(client):
|
||||
"""Panoul de mapare din preview (pas 3) nu contine checkbox auto_send.
|
||||
|
||||
Dupa US-002: macro autosend_toggle golit din _preview_import.html.
|
||||
"""
|
||||
# Upload CSV cu operatie nemapata -> map coloane -> ajunge la preview
|
||||
rows = [
|
||||
{"VIN": "WVWZZZ1KZAW000123", "Nr inmatriculare": "B001TST",
|
||||
"Data prestatie": "15.06.2026", "Odometru final": "123456",
|
||||
"Operatie": "OP-NEMAPATA-US002"},
|
||||
]
|
||||
data = _csv_bytes(rows)
|
||||
r = client.post("/_import/upload", files={"file": ("test.csv", data, "text/csv")})
|
||||
assert r.status_code == 200
|
||||
m = re.search(r"/_import/(\d+)/mapare-coloane", r.text)
|
||||
assert m, f"Nu am gasit import_id in raspuns: {r.text[:300]}"
|
||||
import_id = int(m.group(1))
|
||||
|
||||
# Salveaza maparea coloanelor -> genereaza preview
|
||||
r = client.post(f"/_import/{import_id}/mapare-coloane", data={
|
||||
"colname": ["VIN", "Nr inmatriculare", "Data prestatie", "Odometru final", "Operatie"],
|
||||
"canon": ["vin", "nr_inmatriculare", "data_prestatie", "odometru_final", "operatie"],
|
||||
"format_data": "DD.MM.YYYY",
|
||||
})
|
||||
assert r.status_code == 200
|
||||
|
||||
# Obtine preview-ul HTML (ar trebui sa aiba panoul de mapare cu OP-NEMAPATA-US002)
|
||||
r2 = client.get(f"/_import/{import_id}/preview")
|
||||
assert r2.status_code == 200
|
||||
body = r2.text
|
||||
# Panoul de mapare apare (operatia nemapata e prezenta)
|
||||
assert "OP-NEMAPATA-US002" in body, "preview-ul trebuie sa contina operatia nemapata"
|
||||
# Fara bifa auto_send
|
||||
assert 'name="auto_send"' not in body, (
|
||||
"preview-ul nu trebuie sa contina input auto_send dupa US-002"
|
||||
)
|
||||
assert "autosend-toggle" not in body, (
|
||||
"preview-ul nu trebuie sa contina clasa autosend-toggle dupa US-002"
|
||||
)
|
||||
assert "In coada automat" not in body, (
|
||||
"preview-ul nu trebuie sa contina 'In coada automat' dupa US-002"
|
||||
)
|
||||
|
||||
|
||||
def test_detaliu_mapare_inline_fara_autosend(client, conn):
|
||||
"""GET /_fragments/trimitere/{id} cu needs_mapping nu contine checkbox auto_send.
|
||||
|
||||
Dupa US-002: macro autosend_toggle golit din _trimitere_detaliu.html.
|
||||
"""
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES ('OE-1', 'Test')"
|
||||
)
|
||||
payload = {
|
||||
"vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B1",
|
||||
"data_prestatie": "2026-06-15", "odometru_final": "123456",
|
||||
"prestatii": [{"cod_op_service": "ITP-US002", "denumire": "Inspectie",
|
||||
"cod_prestatie": None}],
|
||||
}
|
||||
sid = conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json, rar_error) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
("k-us002-det", 1, "needs_mapping", json.dumps(payload),
|
||||
json.dumps({"unmapped": [{"cod_op_service": "ITP-US002"}]})),
|
||||
).lastrowid
|
||||
conn.commit()
|
||||
|
||||
r = client.get(f"/_fragments/trimitere/{sid}")
|
||||
assert r.status_code == 200
|
||||
body = r.text
|
||||
# Operatia nemapata e afisata
|
||||
assert "ITP-US002" in body, "detaliul trebuie sa contina operatia nemapata"
|
||||
assert 'name="auto_send"' not in body, (
|
||||
"detaliul nu trebuie sa contina input auto_send dupa US-002"
|
||||
)
|
||||
assert "autosend-toggle" not in body, (
|
||||
"detaliul nu trebuie sa contina clasa autosend-toggle dupa US-002"
|
||||
)
|
||||
129
tests/test_web_mapeaza.py
Normal file
129
tests/test_web_mapeaza.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Teste POST /trimitere/{id}/mapeaza — US-001 auto_send ignorat.
|
||||
|
||||
Dupa US-001: auto_send nu mai tine randul in needs_mapping; un cod valid mapat
|
||||
-> queued direct, indiferent de valoarea auto_send din form sau din mapping_meta.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "mapeaza.db"))
|
||||
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.main import app
|
||||
with TestClient(app, follow_redirects=False) as c:
|
||||
yield c
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def conn(client):
|
||||
"""Conexiune directa la DB-ul deja initializat de client."""
|
||||
from app.db import get_connection
|
||||
c = get_connection()
|
||||
yield c
|
||||
c.close()
|
||||
|
||||
|
||||
def _add_nomenclator(conn, cod="OE-1"):
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?, ?)",
|
||||
(cod, f"Operatie {cod}"),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _insert_needs_mapping(conn, cod_op="ITP-CHECK"):
|
||||
payload = {
|
||||
"vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B1",
|
||||
"data_prestatie": "2026-06-15", "odometru_final": "123456",
|
||||
"prestatii": [{"cod_op_service": cod_op, "denumire": "Inspectie tehnica"}],
|
||||
}
|
||||
cur = conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) "
|
||||
"VALUES (?, ?, ?, ?)",
|
||||
(f"k-{os.urandom(4).hex()}", 1, "needs_mapping", json.dumps(payload)),
|
||||
)
|
||||
conn.commit()
|
||||
return int(cur.lastrowid)
|
||||
|
||||
|
||||
def test_mapeaza_inline_auto_send_zero_in_form_tot_queued(client, conn):
|
||||
"""POST /trimitere/{id}/mapeaza cu auto_send=0 explicit in form -> queued dupa US-001.
|
||||
|
||||
Vechi comportament: reresolve_account vedea auto_send=0 -> review_manual -> needs_mapping.
|
||||
Nou comportament: has_no_auto_send returneaza False -> queued direct.
|
||||
"""
|
||||
_add_nomenclator(conn)
|
||||
sid = _insert_needs_mapping(conn)
|
||||
|
||||
r = client.post(
|
||||
f"/trimitere/{sid}/mapeaza",
|
||||
data={
|
||||
"cod_op_service": "ITP-CHECK",
|
||||
"cod_prestatie": "OE-1",
|
||||
"auto_send": "0",
|
||||
},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
row = conn.execute("SELECT status FROM submissions WHERE id=?", (sid,)).fetchone()
|
||||
assert row["status"] == "queued", (
|
||||
f"auto_send=0 in form nu trebuie sa tina randul dupa US-001, got {row['status']}"
|
||||
)
|
||||
|
||||
|
||||
def test_mapeaza_inline_fara_auto_send_form_tot_queued(client, conn):
|
||||
"""POST /trimitere/{id}/mapeaza fara auto_send in form -> queued dupa US-001."""
|
||||
_add_nomenclator(conn)
|
||||
sid = _insert_needs_mapping(conn)
|
||||
|
||||
r = client.post(
|
||||
f"/trimitere/{sid}/mapeaza",
|
||||
data={"cod_op_service": "ITP-CHECK", "cod_prestatie": "OE-1"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
row = conn.execute("SELECT status FROM submissions WHERE id=?", (sid,)).fetchone()
|
||||
assert row["status"] == "queued", (
|
||||
f"fara auto_send -> asteptat queued, got {row['status']}"
|
||||
)
|
||||
|
||||
|
||||
def test_mapeaza_inline_raspuns_fara_mesaj_auto_send(client, conn):
|
||||
"""Raspunsul la mapare nu contine text despre auto-send oprit."""
|
||||
_add_nomenclator(conn)
|
||||
sid = _insert_needs_mapping(conn)
|
||||
|
||||
r = client.post(
|
||||
f"/trimitere/{sid}/mapeaza",
|
||||
data={"cod_op_service": "ITP-CHECK", "cod_prestatie": "OE-1", "auto_send": "0"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.text.lower()
|
||||
assert "auto-send oprit" not in body, "raspunsul nu trebuie sa contina card 'auto-send oprit'"
|
||||
assert "review manual inainte de trimitere" not in body, (
|
||||
"raspunsul nu trebuie sa contina mesajul vechi de auto_send"
|
||||
)
|
||||
|
||||
|
||||
def test_mapeaza_inline_trigger_trimiteri_changed(client, conn):
|
||||
"""Raspunsul are header HX-Trigger=trimiteriChanged indiferent de auto_send."""
|
||||
_add_nomenclator(conn)
|
||||
sid = _insert_needs_mapping(conn)
|
||||
|
||||
r = client.post(
|
||||
f"/trimitere/{sid}/mapeaza",
|
||||
data={"cod_op_service": "ITP-CHECK", "cod_prestatie": "OE-1", "auto_send": "0"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.headers.get("HX-Trigger") == "trimiteriChanged"
|
||||
@@ -181,17 +181,18 @@ def test_tabel_fara_poll_periodic(client):
|
||||
assert "every" not in wrap, f"tabelul nu trebuie sa aiba poll periodic: {wrap}"
|
||||
|
||||
|
||||
def test_nudge_date_noi_in_loc_de_poll(client):
|
||||
"""Reimprospatarea live se face prin nudge-ul 'Date noi' (poller usor de versiune)
|
||||
care NU atinge tabelul; utilizatorul reincarca explicit cand vrea."""
|
||||
def test_auto_refresh_poller_la_versiune_noua(client):
|
||||
"""Reimprospatarea la date noi externe se face prin pollerul de versiune,
|
||||
care cheama reincarcaTrimiteri() automat — fara nudge manual (US-008).
|
||||
Nudge-ul 'Date noi. Reincarca' a fost eliminat; pollerul face auto-refresh."""
|
||||
acct = _create_account_user("poll2@test.com")
|
||||
_login(client, "poll2@test.com")
|
||||
_insert_submission(acct)
|
||||
html = client.get("/?tab=acasa").text
|
||||
|
||||
assert 'id="nudge-trimiteri"' in html, "bannerul nudge 'Date noi' trebuie sa existe"
|
||||
assert 'id="nudge-trimiteri"' not in html, "nudge-ul 'Date noi' trebuie eliminat (US-008)"
|
||||
assert "/_fragments/trimiteri-versiune" in html, "pollerul de versiune trebuie configurat"
|
||||
assert "reincarcaTrimiteri" in html, "reincarcarea manuala (Reincarca) trebuie expusa"
|
||||
assert "reincarcaTrimiteri" in html, "auto-refresh prin reincarcaTrimiteri() trebuie prezent"
|
||||
|
||||
|
||||
def test_trimiteriChanged_inca_reincarca(client):
|
||||
|
||||
231
tests/test_web_nav.py
Normal file
231
tests/test_web_nav.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""Teste US-005 (PRD 5.11): Link-uri navigatie sub contoare pe toate paginile.
|
||||
|
||||
TDD — testele sunt scrise INAINTE de implementare (RED), apoi devin GREEN.
|
||||
|
||||
Verifica:
|
||||
- Sub contoare (#status-bar): rand cu link Trimiteri + Mapari (badge needs_mapping)
|
||||
- Marcaj activ pe pagina curenta, via variabila de context tab_activ
|
||||
- Logo ROMFAST + titlu linkeaza la / (Trimiteri)
|
||||
- Hamburger capata Trimiteri (Acasa) ca prima intrare
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Helpers
|
||||
# ============================================================
|
||||
|
||||
def _create_account_user(email: str, password: str = "parolasecreta10"):
|
||||
from app.accounts import create_account
|
||||
from app.users import create_user
|
||||
from app.db import get_connection
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
acct_id = create_account(conn, f"Service {email}", active=True)
|
||||
create_user(conn, acct_id, email, password)
|
||||
return acct_id
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _login(client, email: str, password: str = "parolasecreta10") -> None:
|
||||
resp = client.get("/login")
|
||||
m = re.search(r'name="csrf_token"\s+value="([^"]+)"', resp.text) or \
|
||||
re.search(r'value="([^"]+)"\s+name="csrf_token"', resp.text)
|
||||
assert m, "csrf_token negasit pe /login"
|
||||
resp = client.post("/login", data={"email": email, "parola": password, "csrf_token": m.group(1)})
|
||||
assert resp.status_code == 303, f"Login esuat: {resp.status_code}"
|
||||
|
||||
|
||||
def _ins(acct: int, *, status: str = "needs_mapping") -> int:
|
||||
from app.db import get_connection
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) VALUES (?, ?, ?, ?)",
|
||||
(
|
||||
f"k-nav-{os.urandom(4).hex()}",
|
||||
acct,
|
||||
status,
|
||||
json.dumps({
|
||||
"vin": "WVWZZZ1KZAW001001",
|
||||
"nr_inmatriculare": "B001NAV",
|
||||
"data_prestatie": "2026-06-20",
|
||||
"odometru_final": "100000",
|
||||
"prestatii": [{"cod_prestatie": "OE-1"}],
|
||||
}),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return int(cur.lastrowid)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Fixture
|
||||
# ============================================================
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
"""Client cu BD izolata + auth activat (login real)."""
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "nav_us005.db"))
|
||||
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "true")
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.web import ratelimit
|
||||
ratelimit._hits.clear()
|
||||
from app.main import app
|
||||
|
||||
with TestClient(app, follow_redirects=False) as c:
|
||||
yield c
|
||||
ratelimit._hits.clear()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# test_nav_trimiteri_mapari_pe_mapari
|
||||
# ============================================================
|
||||
|
||||
def test_nav_trimiteri_mapari_pe_mapari(client):
|
||||
"""La tab=mapari, linkul Mapari e marcat activ (aria-current='page').
|
||||
Linkul Trimiteri e prezent si inactiv.
|
||||
Badge-ul needs_mapping apare pe linkul Mapari.
|
||||
"""
|
||||
acct = _create_account_user("nav1@test.com")
|
||||
_ins(acct, status="needs_mapping") # badge pentru Mapari
|
||||
_login(client, "nav1@test.com")
|
||||
|
||||
# Testam direct fragmentul status cu tab=mapari (asa il vede HTMX dupa implementare)
|
||||
resp = client.get("/_fragments/status?tab=mapari")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Linkul Trimiteri (href="/") trebuie sa fie prezent
|
||||
assert 'href="/"' in html, (
|
||||
"Linkul Trimiteri (href='/') trebuie sa fie prezent sub contoare in #status-bar"
|
||||
)
|
||||
|
||||
# Linkul Mapari trebuie sa fie prezent
|
||||
assert '/?tab=mapari' in html, (
|
||||
"Linkul Mapari (/?tab=mapari) trebuie sa fie prezent sub contoare in #status-bar"
|
||||
)
|
||||
|
||||
# Mapari trebuie sa fie marcat activ (aria-current="page" pe elementul Mapari)
|
||||
assert 'aria-current="page"' in html, (
|
||||
"Linkul Mapari trebuie sa fie marcat activ cu aria-current='page' la tab=mapari"
|
||||
)
|
||||
|
||||
# Trimiteri NU trebuie sa fie marcat activ (alta pagina e activa)
|
||||
# Gasim tag-ul <a href="/"> si verificam ca NU contine aria-current
|
||||
m = re.search(r'<a\b[^>]*href="/"[^>]*>', html)
|
||||
assert m is not None, "Tag-ul <a href='/'> (Trimiteri) negasit in HTML"
|
||||
tag_trimiteri = m.group(0)
|
||||
assert 'aria-current' not in tag_trimiteri, (
|
||||
f"Trimiteri NU trebuie sa fie activ la tab=mapari — tag gasit: {tag_trimiteri}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# test_nav_trimiteri_pe_jurnal
|
||||
# ============================================================
|
||||
|
||||
def test_nav_trimiteri_pe_jurnal(client):
|
||||
"""La tab=jurnal, ambele linkuri (Trimiteri + Mapari) sunt prezente,
|
||||
dar niciunul nu e marcat activ (jurnal e pagina curenta).
|
||||
"""
|
||||
acct = _create_account_user("nav2@test.com")
|
||||
_login(client, "nav2@test.com")
|
||||
|
||||
resp = client.get("/_fragments/status?tab=jurnal")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Ambele linkuri trebuie sa fie prezente
|
||||
assert 'href="/"' in html, "Linkul Trimiteri (href='/') trebuie sa fie prezent la tab=jurnal"
|
||||
assert '/?tab=mapari' in html, "Linkul Mapari trebuie sa fie prezent la tab=jurnal"
|
||||
|
||||
# La tab=jurnal, niciun link din nav nu trebuie sa aiba aria-current="page"
|
||||
# (jurnal e tab-ul curent, nu Trimiteri si nici Mapari)
|
||||
assert 'aria-current="page"' not in html, (
|
||||
"La tab=jurnal, niciun link din status-nav nu trebuie sa fie aria-current='page'"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# test_fragment_status_pastreaza_tab_la_self_refresh
|
||||
# ============================================================
|
||||
|
||||
def test_fragment_status_pastreaza_tab_la_self_refresh(client):
|
||||
"""Fragmentul status randat cu tab=mapari contine hx-get cu tab=mapari.
|
||||
|
||||
BUG 1: fara ?tab=, la poll-ul every 15s _status.html se re-randeaza fara
|
||||
tab_activ -> nav-ul marcheaza gresit Trimiteri activ chiar daca userul e
|
||||
pe ?tab=mapari. Fix: hx-get='/_fragments/status?tab={{ tab_activ }}'
|
||||
in _status.html.
|
||||
"""
|
||||
acct = _create_account_user("refresh@test.com")
|
||||
_login(client, "refresh@test.com")
|
||||
|
||||
resp = client.get("/_fragments/status?tab=mapari")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Fragmentul randat cu tab=mapari trebuie sa-si includa propriul hx-get
|
||||
# cu tab=mapari, ca la self-refresh (every 15s / trimiteriChanged) sa
|
||||
# primeasca acelasi tab si sa marcheze corect Mapari activ.
|
||||
assert 'hx-get="/_fragments/status?tab=mapari"' in html, (
|
||||
"status-bar randat cu tab=mapari trebuie sa contina hx-get cu tab=mapari "
|
||||
"in el insusi (altfel self-refresh-ul pierde tab-ul activ)"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# test_logo_linkeaza_acasa
|
||||
# ============================================================
|
||||
|
||||
def test_logo_linkeaza_acasa(client):
|
||||
"""Logo-ul ROMFAST (brand-logo) linkeaza la / (Trimiteri).
|
||||
|
||||
Logo-ul trebuie sa fie invelit intr-un <a href='/'> in header.
|
||||
Aceasta face logo-ul clickabil pe toate paginile.
|
||||
"""
|
||||
acct = _create_account_user("logo@test.com")
|
||||
_login(client, "logo@test.com")
|
||||
|
||||
resp = client.get("/")
|
||||
assert resp.status_code == 200
|
||||
html = resp.text
|
||||
|
||||
# Extrage sectiunea <header> pentru a izola cautarea
|
||||
m_header = re.search(r'<header\b[^>]*>(.*?)</header>', html, re.DOTALL)
|
||||
assert m_header, "<header> negasit in HTML"
|
||||
header_html = m_header.group(1)
|
||||
|
||||
# Logo brand-logo trebuie sa fie prezent in header
|
||||
assert 'brand-logo' in header_html, "Clasa brand-logo trebuie sa fie in header"
|
||||
|
||||
# Logo trebuie sa fie invelit intr-un <a href="/">
|
||||
# Verificam: exista un <a href="/"> care contine class="brand-logo" in interiorul sau
|
||||
assert re.search(r'<a\b[^>]*href="/"[^>]*>.*?brand-logo', header_html, re.DOTALL), (
|
||||
"Logo-ul (class='brand-logo') trebuie sa fie intr-un <a href='/'> in header. "
|
||||
"In prezent logo-ul nu e un link."
|
||||
)
|
||||
|
||||
# Titlul "Gateway RAR AUTOPASS" trebuie sa fie si el in interiorul unui <a href="/">
|
||||
# (PRD AC: Logo-ul ROMFAST + titlul linkeaza la /)
|
||||
assert re.search(r'<a\b[^>]*href="/"[^>]*>.*?Gateway RAR AUTOPASS', header_html, re.DOTALL), (
|
||||
"Titlul 'Gateway RAR AUTOPASS' trebuie sa fie intr-un <a href='/'> in header."
|
||||
)
|
||||
270
tests/test_web_refresh.py
Normal file
270
tests/test_web_refresh.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""Teste US-008 (PRD 5.11) — Auto-refresh dupa actiuni proprii.
|
||||
|
||||
Comportament dorit:
|
||||
1. Dupa actiune proprie (mapare inline, corectie, repune, commit), lista
|
||||
Trimiteri se reincarca automat, fara click pe Reincarca.
|
||||
Mecanism: server emite HX-Trigger: trimiteriChanged; #submissions-wrap
|
||||
asculta 'trimiteriChanged from:body' si re-fetches imediat.
|
||||
2. Filtrul activ si pagina curenta NU se reseteaza la auto-refresh.
|
||||
Mecanism: hx-include="#filtre-trimiteri" pe #submissions-wrap.
|
||||
3. Pollerul de fundal face auto-refresh direct la date noi (versiune diferita)
|
||||
in loc sa afiseze nudge-ul "Date noi. Reincarca".
|
||||
Decizie (documentata): nudge eliminat. Distinctia own vs externe nu e posibila
|
||||
pe client fara sesiune dedicata — auto-refresh e mai consistent si mai simplu.
|
||||
4. Elementul #nudge-trimiteri eliminat din template (dead code dupa schimbarea poller).
|
||||
|
||||
TDD: testele 1-2 sunt GREEN (comportament deja implementat — regresie).
|
||||
testele 3-4 sunt RED→GREEN (implementate in aceeasi sesiune).
|
||||
testele 5-6 (test_actiune_proprie_reincarca_automat, test_nudge_nu_mai_blocheaza)
|
||||
sunt adaugate cu numele exacte din spec PRD (GREEN de la primul run — implementarea
|
||||
existenta le satisface).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Fixture #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "us008.db"))
|
||||
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.main import app
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
def _add_submission(account_id: int = 1, status: str = "queued", payload: dict | None = None) -> int:
|
||||
"""Adauga un submission si returneaza id-ul sau.
|
||||
|
||||
status: starea dorita (implicit 'queued'); use 'needs_mapping' pentru testele de mapare.
|
||||
payload: continut JSON (implicit minimal).
|
||||
"""
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, account_id, status, payload_json) "
|
||||
"VALUES (?, ?, ?, ?)",
|
||||
(f"us008_key_{account_id}_{status}", account_id, status,
|
||||
json.dumps(payload or {"test": True})),
|
||||
)
|
||||
return cur.lastrowid
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _add_nomenclator(cod: str = "R-FRANE", nume: str = "Reparatie frane") -> None:
|
||||
"""Insereaza un cod RAR in nomenclatorul local."""
|
||||
from app.db import get_connection
|
||||
conn = get_connection()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO nomenclator_rar (cod_prestatie, nume_prestatie) VALUES (?,?)",
|
||||
(cod, nume),
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Test 1 — GREEN (regresie): submissions-wrap reincarca la trimiteriChanged #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_submissions_wrap_reincarca_la_trimiteriChanged(client):
|
||||
"""#submissions-wrap se reincarca automat la evenimentul trimiteriChanged.
|
||||
|
||||
Actiunile proprii (mapare inline, corectie, repune, commit) emit
|
||||
HX-Trigger: trimiteriChanged. #submissions-wrap asculta
|
||||
'trimiteriChanged from:body' si re-fetches imediat, fara click pe Reincarca.
|
||||
|
||||
Nota: _coada.html se randeaza doar cand exista submission-uri (are_trimiteri=True).
|
||||
Semanarea unui submission inainte de GET garanteaza randarea sectionii complete.
|
||||
"""
|
||||
_add_submission()
|
||||
|
||||
r = client.get("/_fragments/acasa")
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
assert "submissions-wrap" in r.text, (
|
||||
"#submissions-wrap lipseste din /_fragments/acasa (cu submission existent)"
|
||||
)
|
||||
assert "trimiteriChanged from:body" in r.text, (
|
||||
"#submissions-wrap nu asculta 'trimiteriChanged from:body' — "
|
||||
"auto-refresh dupa actiuni proprii nu va functiona"
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Test 2 — GREEN (regresie): filtrul nu se reseteaza la auto-refresh #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_submissions_wrap_pastreaza_filtrul_la_auto_refresh(client):
|
||||
"""Auto-refresh-ul nu reseteaza filtrul activ sau pagina curenta.
|
||||
|
||||
#submissions-wrap include #filtre-trimiteri la fiecare request HTMX.
|
||||
La reincarcarea declansata de trimiteriChanged, filtrul curent (stare,
|
||||
vehicul, data) si pagina curenta se retransmit automat.
|
||||
"""
|
||||
_add_submission()
|
||||
|
||||
r = client.get("/_fragments/acasa")
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
html = r.text
|
||||
assert 'hx-include="#filtre-trimiteri"' in html or "hx-include='#filtre-trimiteri'" in html, (
|
||||
"hx-include=#filtre-trimiteri lipseste din #submissions-wrap — "
|
||||
"filtrul se va reseta la auto-refresh"
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Test 3 — RED: pollerul face auto-refresh, nu arata nudge #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_poller_auto_refresh_nu_nudge(client):
|
||||
"""Pollerul de fundal face auto-refresh la date noi, NU afiseaza nudge.
|
||||
|
||||
Pattern curent (RED): cand versiunea difera, pollerul face nudge.hidden = false.
|
||||
Pattern dorit (GREEN): cand versiunea difera, pollerul cheama reincarcaTrimiteri()
|
||||
care re-fetches #submissions-wrap pastrand filtrul curent.
|
||||
|
||||
Decizie: nudge eliminat complet (nu ramane 'doar pentru schimbari externe')
|
||||
pentru ca distinctia propriu vs extern e imposibila pe client fara
|
||||
mecanism de sesiune dedicat (too complex, no gain).
|
||||
"""
|
||||
r = client.get("/")
|
||||
assert r.status_code == 200, r.text
|
||||
html = r.text
|
||||
|
||||
# Pollerul NU mai seteaza nudge.hidden = false in handler-ul de versiune diferita.
|
||||
# (Prezenta acestui pattern indica implementarea veche — RED.)
|
||||
assert "nudge.hidden = false" not in html, (
|
||||
"Pollerul inca arata nudge in loc de auto-refresh — "
|
||||
"schimbati 'nudge.hidden = false' cu 'reincarcaTrimiteri()' in base.html"
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Test 4 — RED: nudge-ul e eliminat din template #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_nudge_eliminat_din_lista(client):
|
||||
"""Elementul #nudge-trimiteri este eliminat din template-ul listei.
|
||||
|
||||
Dupa ce pollerul trece la auto-refresh, nudge-ul devine dead code.
|
||||
Eliminarea lui simplifica template-ul si elimina un element de UI confuz
|
||||
(utilizatorul vedea 'Date noi. Reincarca' chiar daca lista era actuala,
|
||||
din cauza refreshului propriu care nu actualiza versiunea fast enough).
|
||||
|
||||
Nota: _coada.html se randeaza doar cand sunt submission-uri. Semanarea
|
||||
asigura ca _coada.html e inclus in raspuns (altfel testul ar pasa vacuos).
|
||||
"""
|
||||
_add_submission()
|
||||
|
||||
r = client.get("/_fragments/acasa")
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
assert "nudge-trimiteri" not in r.text, (
|
||||
"#nudge-trimiteri inca exista in template — "
|
||||
"eliminati elementul din _coada.html dupa ce pollerul e migrat la auto-refresh"
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Test 5 (spec PRD): actiune proprie reincarca automat fara click Reincarca #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_actiune_proprie_reincarca_automat(client):
|
||||
"""Dupa o actiune proprie (mapare inline), lista se reincarca automat.
|
||||
|
||||
Verifica doua componente ale mecanismului:
|
||||
1. Server-side: POST la mapare inline returneaza HX-Trigger: trimiteriChanged
|
||||
in headerele raspunsului (indiferent de starea noua a submission-ului).
|
||||
2. Client-side: #submissions-wrap asculta 'trimiteriChanged from:body' in
|
||||
hx-trigger — HTMX va declansa re-fetch imediat la primirea headerului,
|
||||
fara click pe Reincarca.
|
||||
|
||||
Testeaza calea mapare-inline; corectie/repune/commit sunt acoperite similar
|
||||
(toate emit HX-Trigger: trimiteriChanged — verificate in test_import_commit.py).
|
||||
"""
|
||||
_add_nomenclator("R-FRANE", "Reparatie frane")
|
||||
sub_id = _add_submission(
|
||||
status="needs_mapping",
|
||||
payload={"prestatii": [{"cod_op_service": "OP-FRANE"}]},
|
||||
)
|
||||
|
||||
# POST mapare inline — emite HX-Trigger: trimiteriChanged
|
||||
r = client.post(
|
||||
f"/trimitere/{sub_id}/mapeaza",
|
||||
data={"csrf_token": "", "cod_op_service": "OP-FRANE", "cod_prestatie": "R-FRANE"},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
# Server emite triggerul — HTMX va declansa auto-refresh pe client
|
||||
hx = r.headers.get("HX-Trigger", "")
|
||||
assert "trimiteriChanged" in hx, (
|
||||
f"Mapare inline nu emite HX-Trigger: trimiteriChanged — "
|
||||
f"lista nu se va reincarca automat. Header primit: {hx!r}"
|
||||
)
|
||||
|
||||
# Client-side: #submissions-wrap asculta triggerul (markup existent in template)
|
||||
r_acasa = client.get("/_fragments/acasa")
|
||||
assert "trimiteriChanged from:body" in r_acasa.text, (
|
||||
"#submissions-wrap nu asculta 'trimiteriChanged from:body' — "
|
||||
"auto-refresh nu va functiona dupa actiune proprie"
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Test 6 (spec PRD): nudge nu mai blocheaza actualizarea #
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
def test_nudge_nu_mai_blocheaza_actualizarea(client):
|
||||
"""Nudge-ul 'Date noi. Reincarca' nu mai poate bloca actualizarea listei.
|
||||
|
||||
Inainte de US-008, mecanismul era: poller detecteaza versiune diferita →
|
||||
arata nudge → utilizatorul trebuia sa apese 'Reincarca' manual.
|
||||
Dupa US-008, nudge-ul e eliminat (decizie documentata mai jos) si pollerul
|
||||
cheama direct reincarcaTrimiteri() → actualizare automata, fara click.
|
||||
|
||||
Decizie privind nudge: ELIMINAT complet (nu pastrat pentru schimbari externe).
|
||||
Rationale: distinctia 'actiune proprie vs schimbare externa' nu e posibila
|
||||
pe client fara mecanism dedicat de sesiune. Auto-refresh periodic (la versiune
|
||||
diferita) acopera si schimbarile externe (worker, alt browser) fara friction.
|
||||
"""
|
||||
_add_submission() # are_trimiteri=True → _coada.html se randeaza
|
||||
|
||||
r_pagina = client.get("/_fragments/acasa")
|
||||
assert r_pagina.status_code == 200, r_pagina.text
|
||||
html = r_pagina.text
|
||||
|
||||
# Nudge eliminat: nu mai poate bloca utilizatorul cu un banner "Reincarca"
|
||||
assert "nudge-trimiteri" not in html, (
|
||||
"#nudge-trimiteri inca exista — poate bloca actualizarea (US-008 il elimina)"
|
||||
)
|
||||
|
||||
# Pollerul JS nu mai seteaza nudge.hidden = false: nu poate "bloca" prin afisare nudge
|
||||
r_home = client.get("/")
|
||||
assert r_home.status_code == 200, r_home.text
|
||||
assert "nudge.hidden = false" not in r_home.text, (
|
||||
"Pollerul JS inca poate afisa nudge-ul — schimbati cu reincarcaTrimiteri()"
|
||||
)
|
||||
|
||||
# Pollerul cheama reincarcaTrimiteri() la versiune diferita (auto-refresh)
|
||||
assert "reincarcaTrimiteri" in r_home.text, (
|
||||
"reincarcaTrimiteri() lipseste din poller — auto-refresh nu va functiona"
|
||||
)
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Teste PRD 5.5 — uniformizare UI: US-001 (Acasa fara Ajutor), US-002 (Nomenclator grila
|
||||
standard), US-003 (macro autosend compact). Stories de template/macro -> render direct Jinja
|
||||
pentru US-002/003; US-001 prin TestClient pe fragmentul Acasa.
|
||||
standard), US-003 (macro autosend) — actualizat dupa PRD 5.11 US-002 (macro neutralizat).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -53,32 +52,26 @@ def _render_macro(form_id="map-1", checked=True):
|
||||
|
||||
|
||||
def test_autosend_pastreaza_name_si_prezenta():
|
||||
"""Invariant backend: checkbox name=auto_send value=true (semantica de prezenta)."""
|
||||
"""US-002 (PRD 5.11): macro autosend_toggle neutralizat — output gol, fara checkbox."""
|
||||
html = _render_macro(checked=True)
|
||||
assert 'type="checkbox"' in html
|
||||
assert 'name="auto_send"' in html
|
||||
assert 'value="true"' in html
|
||||
assert 'form="map-1"' in html
|
||||
assert "checked" in html
|
||||
assert 'name="auto_send"' not in html, "US-002: checkbox auto_send scos din macro"
|
||||
assert html.strip() == "", f"macro neutralizat trebuie sa intoarca string gol, got: {html!r}"
|
||||
|
||||
|
||||
def test_autosend_nebifat_fara_checked():
|
||||
"""US-002: macro neutralizat intoarce gol indiferent de checked."""
|
||||
html = _render_macro(checked=False)
|
||||
assert 'name="auto_send"' in html
|
||||
assert "checked" not in html
|
||||
assert 'name="auto_send"' not in html
|
||||
assert html.strip() == ""
|
||||
|
||||
|
||||
def test_autosend_compact_fara_proza_inline():
|
||||
"""Proza explicativa de pe randuri (3.6) eliminata din CONTINUTUL vizibil — traieste in
|
||||
panoul Ajutor (US-005). Tooltip-ul scurt (atribut title=) e acceptat, deci il scoatem
|
||||
inainte de verificare."""
|
||||
"""US-002: macro neutralizat nu contine nicio proza inline."""
|
||||
html = _render_macro()
|
||||
vizibil = re.sub(r'title="[^"]*"', "", html) # scoate atributul title (tooltip)
|
||||
assert "La fisierele viitoare" not in vizibil
|
||||
assert "Tine pentru verificare" not in vizibil
|
||||
assert "nimic nu pleaca la RAR" not in vizibil
|
||||
# ambele etichete de stare vizibile, compact
|
||||
assert "Auto" in html and "Manual" in html
|
||||
assert "La fisierele viitoare" not in html
|
||||
assert "Tine pentru verificare" not in html
|
||||
assert "nimic nu pleaca la RAR" not in html
|
||||
assert html.strip() == ""
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -155,16 +148,16 @@ def test_mapari_ajutor_disclosure_si_fara_proza_inline(client):
|
||||
html = resp.text
|
||||
# US-010: sectiunea de ajutor (<details class="ajutor-mapari">) eliminata
|
||||
assert "ajutor-mapari" not in html
|
||||
# antet de coloana compact
|
||||
assert ">In coada<" in html
|
||||
# US-002: coloana In coada scoasa din tabel
|
||||
assert "In coada" not in html, "US-002: coloana 'In coada' scoasa din tabelul Mapari"
|
||||
# proza inline veche eliminata de pe sectiuni
|
||||
assert "sugestia fuzzy e preselectata) si salveaza" not in html
|
||||
assert "Maparile operatie -> cod RAR retinute pentru contul tau" not in html
|
||||
|
||||
|
||||
def test_mapari_comutator_compact_in_tabel(client):
|
||||
"""US-002: tabul Mapari nu mai contine checkbox auto_send."""
|
||||
acct = _login(client)
|
||||
_seed_needs_mapping(acct)
|
||||
html = client.get("/_fragments/mapari").text
|
||||
assert 'name="auto_send"' in html
|
||||
assert "Manual" in html and "Auto" in html
|
||||
assert 'name="auto_send"' not in html, "US-002: checkbox auto_send scos din UI"
|
||||
|
||||
Reference in New Issue
Block a user