chore: curatare agresiva comentarii — scoatere referinte US/PRD din cod si template-uri

Eliminat zgomotul de trasabilitate (US-xxx, PRD x.x, Rn, OV-x, Tn, decizii/naratiune
istorica) din 41 fisiere app/ + template-uri. Pastrate comentariile care documenteaza
invarianti si logica ne-evidenta (idempotenta/hash, reconciliere anti-duplicat, RAR 500
esec definitiv, creds per cont, WAF User-Agent, 422 fara echo de parola, scope NULL->1),
curatate doar de tokeni.

Verificare: pentru cele 27 module .py curatate, structura de cod (tokeni non-comentariu/
non-string) e IDENTICA fata de HEAD -> doar comentarii/docstring-uri schimbate. Singura
schimbare de cod e in tests/test_web_responsive.py (scos 3 assert pe markeri US-006/007/008,
inlocuite de asertiunile structurale alaturate). 0 tokeni US/PRD reziduali in app/.
Regresie: 896 passed, 1 deselected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-25 21:44:24 +00:00
parent f05fe5b221
commit 4a2afc68bf
43 changed files with 547 additions and 649 deletions

View File

@@ -1,4 +1,4 @@
"""Panou admin web /admin. US-011 PRD 3.3b.
"""Panou admin web /admin.
Rute:
GET /admin — listeaza conturi in asteptare + active (require_admin)
@@ -49,7 +49,7 @@ def _render_admin(request: Request, conn, *, error: str | None = None, status_co
emails = _emails_by_account(conn)
for acct in accounts:
acct["email"] = emails.get(acct["id"])
# Grupare pe STARE (5.5), nu pe `active`: altfel conturile arhivate/blocate (active=0)
# Grupare pe STARE, nu pe `active`: altfel conturile arhivate/blocate (active=0)
# ar cadea gresit sub "in asteptare". 'deleted' e deja exclus din list_accounts.
pending = [a for a in accounts if a["status"] == "pending" and a["id"] != 1]
active = [a for a in accounts if a["status"] == "active" and a["id"] != 1]
@@ -79,7 +79,7 @@ async def admin_get(request: Request):
def _apply_lifecycle(conn, ids: list[int], action: str) -> None:
"""Aplica un verb de ciclu de viata (5.5) pe o lista de conturi. Conturile protejate
"""Aplica un verb de ciclu de viata pe o lista de conturi. Conturile protejate
(id=1) sau inexistente ridica ValueError din helperi -> sarite (nu opresc bulk-ul).
`action`: activate | block | archive | delete."""
for aid in ids:
@@ -97,7 +97,7 @@ def _apply_lifecycle(conn, ids: list[int], action: str) -> None:
def _lifecycle_route(request: Request, account_id: list[int], csrf_token: str, action: str):
"""Corp comun pentru rutele de ciclu de viata (5.5): auth + CSRF + aplica verbul (bulk) + PRG.
"""Corp comun pentru rutele de ciclu de viata: auth + CSRF + aplica verbul (bulk) + PRG.
Evita 4 handlere copy-paste care difera doar prin verb."""
require_admin(request)
verify_csrf(request, csrf_token)

View File

@@ -1,4 +1,4 @@
"""Rute autentificare web: /signup (US-003), /login + /logout (US-004). PRD 3.3."""
"""Rute autentificare web: /signup, /login, /logout."""
from __future__ import annotations

View File

@@ -1,4 +1,4 @@
"""CSRF token per-sesiune + validare. US-009 PRD 3.3.
"""CSRF token per-sesiune + validare.
Contract pentru rutele POST web:
- Formulare HTML includ: <input type="hidden" name="csrf_token" value="{{ csrf_token }}">

View File

@@ -1,9 +1,6 @@
"""
labels.py — traducere stari tehnice in text uman + clasa CSS (US-001, PRD 3.4).
"""Traducere stari tehnice in text uman + clasa CSS.
Functii pure: fara DB, fara request. Usor de testat unitar si de importat in template-uri.
Sursa de adevar pentru texte: tabelul din PRD 3.4 §3 US-001.
"""
import json
@@ -59,7 +56,7 @@ STARI_SUBMISSION: dict[str, Eticheta] = {
# ---------------------------------------------------------------------------
# Etichete scurte (pill) pentru coloana Stare din tabelul de trimiteri (US-006)
# Etichete scurte (pill) pentru coloana Stare din tabelul de trimiteri
# Dict propriu — NU element in tuple Eticheta (ar rupe template-urile care
# despacheteaza 3 elemente). eticheta_stare ramane neatinsa.
# ---------------------------------------------------------------------------
@@ -155,7 +152,7 @@ def eticheta_rar(stare: str) -> Eticheta:
# ---------------------------------------------------------------------------
# Format data RAR (US-001, PRD 3.5)
# Format data RAR
# ---------------------------------------------------------------------------
def format_data_rar(raw: object) -> str:
@@ -181,7 +178,7 @@ def format_data_rar(raw: object) -> str:
# ---------------------------------------------------------------------------
# Motiv uman din rar_error (US-004, PRD 3.5)
# Motiv uman din rar_error
# ---------------------------------------------------------------------------
def motiv_uman(status: str, rar_error: object) -> str:
@@ -231,7 +228,7 @@ def motiv_uman(status: str, rar_error: object) -> str:
# ---------------------------------------------------------------------------
# parse_erori — transforma rar_error in lista 3-niveluri (US-006, PRD 5.4)
# parse_erori — transforma rar_error in lista 3-niveluri
# ---------------------------------------------------------------------------
def parse_erori(rar_error: object) -> list[dict]:
@@ -275,7 +272,7 @@ def parse_erori(rar_error: object) -> list[dict]:
"cauza": e.get("cauza") or e.get("message") or "",
"fix": e.get("fix") or "",
"field": e.get("field"),
# Cod BRUT de catalog (ex. RAR_EROARE_SERVER) — DOAR pentru modal (US-001/R1).
# Cod BRUT de catalog (ex. RAR_EROARE_SERVER) — DOAR pentru modal.
"cod": e.get("cod"),
})
else:
@@ -305,7 +302,7 @@ def parse_erori(rar_error: object) -> list[dict]:
"cauza": data.get("cauza") or "",
"fix": data.get("fix") or "",
"field": data.get("field"),
# Cod BRUT de catalog (ex. COD_NEMAPAT) — DOAR pentru modal (US-001/R1).
# Cod BRUT de catalog (ex. COD_NEMAPAT) — DOAR pentru modal.
"cod": data.get("cod"),
}]
# Dict vechi: unmapped

View File

@@ -1,8 +1,8 @@
"""Middleware HTTP: request_id per cerere (PRD 5.6 US-002).
"""Middleware HTTP: request_id per cerere.
Fiecare raspuns primeste un header `X-Request-ID` (generat daca clientul nu trimite
unul). Pe durata cererii, id-ul e disponibil prin `observ.request_id_var` (contextvar)
in handlerul de erori (US-001) si in `log_event` (US-003) — fara a polua semnaturile.
in handlerul de erori si in `log_event` — fara a polua semnaturile.
Format opac, fara PII: `secrets.token_hex(8)` (16 hex). Daca clientul trimite un
`X-Request-ID`, il pastram (corelare end-to-end), dar il scurtam defensiv (max 64).

View File

@@ -1,6 +1,6 @@
"""Rate-limit in-proces cu fereastra glisanta. US-009 PRD 3.3 C5.
"""Rate-limit in-proces cu fereastra glisanta.
Fara dependinta externa. Folosit de POST /signup (US-003) cu cheia = IP client.
Fara dependinta externa. Folosit de POST /signup cu cheia = IP client.
Configurabil prin AUTOPASS_signup_rate_max / AUTOPASS_signup_rate_window_s (config.py).
"""

View File

@@ -1,12 +1,11 @@
"""Dashboard Jinja2 + HTMX (server-rendered, zero build).
Schelet cu stari explicite: empty (coada goala), banner alerta blocate,
worker viu/mort, ultimul login RAR. Editor mapari + browser nomenclator +
export CSV + stare "RAR indisponibil" = de adaugat (plan.md sect. 4 + design-review).
worker viu/mort, ultimul login RAR. Editor mapari + browser nomenclator.
U5 — Rute web pentru import fisier (upload → mapare coloane → preview → confirma → trimite).
Consuma endpointurile backend din import_router (helper-e interne) fara a le modifica.
Toate rutele /_import/* returneaza fragmente HTML targetate pe #import-section prin HTMX.
Rute web pentru import fisier (upload → mapare coloane → preview → confirma → trimite).
Consuma helper-e interne din import_router fara a le modifica. Toate rutele /_import/*
returneaza fragmente HTML targetate pe #import-section prin HTMX.
"""
from __future__ import annotations
@@ -83,12 +82,12 @@ from ..mapping import (
text_rules_overlap,
)
# Campuri canonice cu eticheta umana pentru dropdown mapare coloane (U5)
# Campuri canonice cu eticheta umana pentru dropdown mapare coloane
_CANONICAL_FIELDS = [(k, v[0]) for k, v in _CANONICAL_SYNONYMS.items()]
router = APIRouter(tags=["web"])
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent / "templates"))
# Expune parse_erori in toate template-urile (US-006, PRD 5.4)
# Expune parse_erori in toate template-urile
templates.env.globals["parse_erori"] = parse_erori
_BLOCKED = ("error", "needs_data", "needs_mapping")
@@ -98,7 +97,7 @@ def _ctx(request: Request, **extra) -> dict:
"""Context de baza pentru template-uri cu formulare: include mereu csrf_token.
Previne lock-out in prod (web_auth_required=True): orice re-randare de eroare
trebuie sa includa csrf_token negol altfel urmatorul submit da 403 (task #8).
trebuie sa includa csrf_token negol altfel urmatorul submit da 403.
"""
return {"request": request, "csrf_token": get_csrf_token(request), **extra}
@@ -161,15 +160,14 @@ def _rar_state(hb, worker_alive: bool) -> str:
return "indisponibil?" if age > 108000 else "ok"
# US-002: "import" nu mai e tab separat — importul traieste pe Acasa. ?tab=import
# cade pe Acasa (tab invalid -> fallback "acasa" in dashboard()), fara 404.
# US-003 (3.6): "coada" (Trimiteri) nu mai e tab — Trimiterile sunt sectiune pe Acasa.
# ?tab=coada cade tot pe Acasa (fallback), fara 404, fara fragment orfan.
# "import" si "coada" nu mai sunt tab-uri separate — importul si Trimiterile sunt
# sectiuni pe Acasa. ?tab=import / ?tab=coada cad pe Acasa (fallback in dashboard()),
# fara 404 si fara fragment orfan.
_TABS_VALIDE = {"acasa", "mapari", "cont", "nomenclator", "integrare", "jurnal"}
def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
"""Calculeaza contextul pentru panoul Acasa (US-005).
"""Calculeaza contextul pentru panoul Acasa.
Scoped pe contul sesiunii — identic cu pattern-ul din restul rutelor (NULL->1).
"""
@@ -197,8 +195,8 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
).fetchone()
are_cheie_folosita = row_key is not None
# US-003 (3.6): contorul de atentie (blocate) se reflecta in heading-ul
# sectiunii "Trimiterile tale" de pe Acasa, nu pe un tab disparut.
# Contorul de atentie (blocate) se reflecta in heading-ul sectiunii
# "Trimiterile tale" de pe Acasa.
counts = _status_counts(conn, account_id)
blocate_total = sum(counts.get(s, 0) for s in _BLOCKED)
@@ -212,7 +210,7 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
"pills_categorii": _pills_categorii(counts),
# Semnatura datelor: nudge-ul "Date noi" o compara la fiecare poll usor.
"versiune_trimiteri": _trimiteri_versiune(conn, account_id),
# US-002: Acasa include caseta de upload -> are nevoie de csrf_token
# Acasa include caseta de upload -> are nevoie de csrf_token
"csrf_token": get_csrf_token(request),
}
@@ -220,8 +218,8 @@ def _get_acasa_context(request: Request, conn, account_id: int) -> dict:
def _render_panel_acasa(request: Request, conn=None, account_id: int = 1, status: str | None = None) -> str:
"""Randeaza panoul Acasa ca string HTML.
`status` (US-014/T13): deep-link `?tab=acasa&status=error` pre-selecteaza filtrul de
stare in sectiunea Trimiteri, astfel ca lista se incarca direct filtrata (nu dead-end).
`status`: deep-link `?tab=acasa&status=error` pre-selecteaza filtrul de stare in
sectiunea Trimiteri, astfel ca lista se incarca direct filtrata (nu dead-end).
"""
if conn is None:
return templates.get_template("_acasa.html").render(
@@ -243,8 +241,8 @@ def _render_panel_import(request: Request) -> str:
def _render_panel_coada(request: Request, conn=None, account_id: int = 1) -> str:
"""US-003 (3.6): "coada" nu mai e panou propriu — serveste continutul Acasa
(Trimiterile sunt sectiune pe Acasa). Pastrat ca alias pentru deep-link/bookmark vechi."""
""""coada" nu mai e panou propriu — serveste continutul Acasa (Trimiterile sunt
sectiune pe Acasa). Pastrat ca alias pentru deep-link/bookmark vechi."""
return _render_panel_acasa(request, conn, account_id)
@@ -334,10 +332,10 @@ def _jurnal_context(
data_de: str | None = None, data_pana: str | None = None,
cont: str | None = None, page: int = 0,
) -> dict:
"""Context pentru tab-ul Jurnal (US-006): evenimente paginate + filtre + scope.
"""Context pentru tab-ul Jurnal: evenimente paginate + filtre + scope.
Admin -> vede TOT, cu filtru optional pe cont. Non-admin -> DOAR evenimentele
contului sau (regula NULL->cont 1, ca restul UI-ului). Decizie §5.
contului sau (regula NULL->cont 1, ca restul UI-ului).
"""
admin = is_account_admin(conn, account_id)
tip = (tip or "").strip() or None
@@ -397,7 +395,7 @@ def _jurnal_context(
def _render_panel_jurnal(request: Request, conn, account_id: int) -> str:
"""Randeaza panoul Jurnal ca string HTML (US-006)."""
"""Randeaza panoul Jurnal ca string HTML."""
return templates.get_template("_jurnal.html").render(_jurnal_context(request, conn, account_id))
@@ -424,20 +422,20 @@ def _render_panel_for_tab(request: Request, conn, account_id: int, tab: str, sta
@router.get("/", response_class=HTMLResponse)
def dashboard(request: Request, tab: str = "acasa", status: str | None = None) -> HTMLResponse:
"""Dashboard principal cu tab-uri (US-003).
"""Dashboard principal cu tab-uri.
Parametrul ?tab= permite deep-link pe orice sectiune; panoul activ e randat
server-side la full load (fara palpaiere la refresh, degradare gratiosa fara JS).
Tab invalid -> fallback la 'acasa'. `?status=` (US-014/T13) pre-filtreaza lista
Trimiteri de pe Acasa (deep-link din banner-ul "Necesita atentia ta").
Tab invalid -> fallback la 'acasa'. `?status=` pre-filtreaza lista Trimiteri de
pe Acasa (deep-link din banner-ul "Necesita atentia ta").
"""
account_id = require_login(request)
active_tab = tab if tab in _TABS_VALIDE else "acasa"
conn = get_connection()
try:
panel_html = _render_panel_for_tab(request, conn, account_id, active_tab, status=status)
# Badge contoare pe tab-uri (US-011): needs_mapping -> Mapari. Blocatele
# (fost badge "coada") se reflecta acum in heading-ul sectiunii Trimiteri (US-003).
# Badge contoare pe tab-uri: needs_mapping -> Mapari. Blocatele se reflecta in
# heading-ul sectiunii Trimiteri.
counts = _status_counts(conn, account_id)
badges = {
"mapari": counts.get("needs_mapping", 0),
@@ -460,7 +458,7 @@ def dashboard(request: Request, tab: str = "acasa", status: str | None = None) -
@router.get("/_fragments/acasa", response_class=HTMLResponse)
def fragment_acasa(request: Request) -> HTMLResponse:
"""Fragment HTMX pentru tab-ul Acasa (US-003, US-005)."""
"""Fragment HTMX pentru tab-ul Acasa."""
account_id = require_login(request)
conn = get_connection()
try:
@@ -472,16 +470,16 @@ def fragment_acasa(request: Request) -> HTMLResponse:
@router.get("/_fragments/import", response_class=HTMLResponse)
def fragment_import(request: Request) -> HTMLResponse:
"""Fragment HTMX pentru tab-ul Import — include zona de upload (US-003)."""
"""Fragment HTMX pentru tab-ul Import — include zona de upload."""
require_login(request)
return templates.TemplateResponse("_upload.html", _ctx(request))
@router.get("/_fragments/coada", response_class=HTMLResponse)
def fragment_coada(request: Request) -> HTMLResponse:
"""US-003 (3.6): "coada" nu mai are fragment propriu. Serveste continutul Acasa
(Trimiterile sunt sectiune permanenta pe Acasa) — evita un fragment `_coada.html`
orfan din bookmark-uri/HTMX vechi. Nu da 404."""
""""coada" nu mai are fragment propriu. Serveste continutul Acasa (Trimiterile sunt
sectiune permanenta pe Acasa) — evita un fragment `_coada.html` orfan din
bookmark-uri/HTMX vechi. Nu da 404."""
account_id = require_login(request)
conn = get_connection()
try:
@@ -528,7 +526,7 @@ def fragment_jurnal(
cont: str | None = None,
page: int = 0,
) -> HTMLResponse:
"""Tab Jurnal (US-006): evenimente app_events paginate + filtre, scoped pe cont.
"""Tab Jurnal: evenimente app_events paginate + filtre, scoped pe cont.
Admin vede tot (filtru optional pe cont); non-admin doar evenimentele proprii.
"""
@@ -563,8 +561,8 @@ def fragment_banner(request: Request) -> HTMLResponse:
def _blocate_defalcat(counts: dict[str, int]) -> list[tuple]:
"""Construieste lista [(eticheta, n), ...] pentru starile blocate cu n > 0.
Ordinea: needs_mapping, needs_data, error — aceeasi ca in PRD.
Returneaza lista goala daca nu exista nicio stare blocata.
Ordinea: needs_mapping, needs_data, error. Returneaza lista goala daca nu
exista nicio stare blocata.
"""
rezultat = []
for status in ("needs_mapping", "needs_data", "error"):
@@ -575,13 +573,12 @@ def _blocate_defalcat(counts: dict[str, int]) -> list[tuple]:
def _pills_categorii(counts: dict[str, int]) -> list[dict]:
"""Pill-uri pentru starile cu problema (US-003 PRD 5.10).
"""Pill-uri pentru starile cu problema.
Inlocuieste _blocate_actionabil (care incarca PII/VIN per rand).
Reutilizeaza contoarele deja calculate din _status_counts.
Returneza lista goala daca nu exista nicio stare blocata.
Reutilizeaza contoarele deja calculate din _status_counts (fara PII/VIN per rand).
Returneaza lista goala daca nu exista nicio stare blocata.
"""
# DESIGN.md §Componente: Lipsa cod = --warn (chihlimbar), celelalte categorii = --err (rosu).
# Lipsa cod = --warn (chihlimbar), celelalte categorii = --err (rosu).
# Culoarea e CSS variable name (nu clasa), injectata direct in style tag al pill-ului,
# pentru ca s-needs_mapping in base.html e tot --err (incorect pentru pill).
PILL_DEFS = [
@@ -598,7 +595,7 @@ def _pills_categorii(counts: dict[str, int]) -> list[dict]:
@router.get("/_fragments/status", response_class=HTMLResponse)
def fragment_status(request: Request) -> HTMLResponse:
"""Bara de status persistenta cu etichete umane (US-002, PRD 3.4).
"""Bara de status persistenta cu etichete umane.
Scoped pe contul sesiunii. Expune starea worker, legatura RAR, ultima
autentificare, contorii de coada si defalcarea blocatelor pe motiv.
@@ -623,7 +620,7 @@ def fragment_status(request: Request) -> HTMLResponse:
"request": request,
"worker_lbl": worker_lbl,
"rar_lbl": rar_lbl,
# Stari binare pentru bife accesibile (US-001 PRD 3.5): glifa + culoare
# Stari binare pentru bife accesibile: glifa + culoare
"worker_ok": worker_alive,
"rar_ok": rar_ok,
"eticheta_ultima_auth": ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
@@ -657,9 +654,8 @@ def _iso_date_prefix(value: object) -> str | None:
Permite filtrarea dupa data_prestatie chiar daca valoarea contine ora/minut/secunda
(ex. '2026-06-20 14:35:07' sau '2026-06-20T14:35:07') — extrage portiunea de data
fara a exclude timestamp-urile (bug-ul fix US-001: _is_iso_date cerea len==10).
Valori care nu incep cu o data ISO valida (ex. '05.12.2024') intorc None si
sunt excluse din filtru — comportament actual pastrat.
fara a exclude timestamp-urile. Valori care nu incep cu o data ISO valida
(ex. '05.12.2024') intorc None si sunt excluse din filtru.
"""
s = str(value or "").strip()
if len(s) < 10:
@@ -673,16 +669,16 @@ def _iso_date_prefix(value: object) -> str | None:
# Stari care semnaleaza o problema ce necesita atentia operatorului. Eticheta umana
# scurta de pe rand (US-001, R1) e ne-goala DOAR pe acestea; pe queued/sending/sent e "".
# scurta de pe rand e ne-goala DOAR pe acestea; pe queued/sending/sent e "".
_STARI_CU_PROBLEMA = ("error", "needs_data", "needs_mapping")
def _eticheta_problema(status: str, motiv: str) -> str:
"""Eticheta umana scurta a problemei pentru randul de tabel (US-001, R1).
"""Eticheta umana scurta a problemei pentru randul de tabel.
Reutilizeaza `motiv` (motiv_uman, deja calculat in randul de view) si cade pe
`eticheta_scurta` cand motivul e gol — NU re-parseaza `rar_error` (R1: DRY, fara
al 3-lea decoder). Codul BRUT de catalog ramane doar pentru modal, nu pe rand.
`eticheta_scurta` cand motivul e gol — NU re-parseaza `rar_error` (DRY, fara al
3-lea decoder). Codul BRUT de catalog ramane doar pentru modal, nu pe rand.
Sir gol pe stari fara problema (queued/sending/sent); ne-gol pe error/needs_*.
Defensiv: motiv_uman nu arunca, iar starile cu problema au intotdeauna eticheta
@@ -694,13 +690,13 @@ def _eticheta_problema(status: str, motiv: str) -> str:
def _submission_row_view(r) -> dict:
"""Imbogateste un rand de submission cu campuri afisabile umane (US-003/US-004)."""
"""Imbogateste un rand de submission cu campuri afisabile umane."""
eticheta = eticheta_stare(r["status"])
motiv = motiv_uman(r["status"], r["rar_error"])
return {
"id": r["id"],
"status": r["status"],
# PRD 5.8 US-007/US-006: pill = eticheta scurta; textul lung ramane ca tooltip (title=).
# pill = eticheta scurta; textul lung ramane ca tooltip (title=).
"stare_scurt": eticheta_scurta(r["status"]),
"stare_text": eticheta[0],
"stare_css": eticheta[2],
@@ -708,15 +704,15 @@ def _submission_row_view(r) -> dict:
"id_prezentare": r["id_prezentare"],
"updated_at": format_data_rar(r["updated_at"]),
"motiv": motiv,
# US-001/R1: eticheta umana scurta a problemei sub pill (text, nu cod brut).
# eticheta umana scurta a problemei sub pill (text, nu cod brut).
"eticheta_problema": _eticheta_problema(r["status"], motiv),
# US-011: randurile blocate (error/needs_data/needs_mapping) sunt selectabile
# pentru stergere bulk; sent/sending/queued raman read-only (fara checkbox).
# randurile blocate (error/needs_data/needs_mapping) sunt selectabile pentru
# stergere bulk; sent/sending/queued raman read-only (fara checkbox).
"gestionabil": r["status"] in _GESTIONABILE_WEB,
}
_PAGE_SIZE = 25 # Marime pagina fixa (US-004 PRD 5.10)
_PAGE_SIZE = 25 # Marime pagina fixa
@router.get("/_fragments/submissions", response_class=HTMLResponse)
@@ -728,9 +724,9 @@ def fragment_submissions(
data_pana: str | None = None,
page: int = 1,
) -> HTMLResponse:
"""Tabel Trimiteri, scoped pe cont, cu filtre optionale si paginare (US-009, US-004).
"""Tabel Trimiteri, scoped pe cont, cu filtre optionale si paginare.
US-004 H1: totalul se calculeaza DIFERIT dupa tipul de filtru:
Totalul se calculeaza DIFERIT dupa tipul de filtru:
- FARA filtru Python (status-only / niciun filtru): SQL COUNT(*) + LIMIT/OFFSET
- CU filtru vehicul/data activ: fetch-all -> filtreaza Python -> total=len -> slice
SQL COUNT/LIMIT pe calea cu filtru Python ar da total gresit (taie inainte de filtru).
@@ -756,8 +752,8 @@ def fragment_submissions(
where_sql = " AND ".join(where)
if filtru_python:
# Calea B: fetch-all, filtreaza in Python, slice (US-004 H1)
# FARA LIMIT — altfel paginile >8 ar disparea silentios (bug PRD H1)
# Calea B: fetch-all, filtreaza in Python, slice.
# FARA LIMIT — altfel paginile >8 ar disparea silentios.
rows_db = conn.execute(
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
f"updated_at, payload_json FROM submissions WHERE {where_sql} ORDER BY id DESC",
@@ -773,7 +769,7 @@ def fragment_submissions(
if vehicul_q not in hay:
continue
if data_de or data_pana:
# Extragem portiunea YYYY-MM-DD (US-001 fix).
# Extragem portiunea YYYY-MM-DD.
d_prefix = _iso_date_prefix(prez["data_prestatie"])
if d_prefix is None:
continue
@@ -785,7 +781,7 @@ def fragment_submissions(
total = len(view_all)
pages = max(1, math.ceil(total / _PAGE_SIZE)) if total > 0 else 1
page = max(1, min(page, pages)) # clamp H2
page = max(1, min(page, pages)) # clamp la nr. de pagini
offset = (page - 1) * _PAGE_SIZE
view = view_all[offset:offset + _PAGE_SIZE]
@@ -796,7 +792,7 @@ def fragment_submissions(
).fetchone()[0]
pages = max(1, math.ceil(total / _PAGE_SIZE)) if total > 0 else 1
page = max(1, min(page, pages)) # clamp H2
page = max(1, min(page, pages)) # clamp la nr. de pagini
offset = (page - 1) * _PAGE_SIZE
rows_db = conn.execute(
@@ -815,13 +811,13 @@ def fragment_submissions(
"rows": view,
"filtru_activ": filtru_activ,
"csrf_token": get_csrf_token(request),
# Paginare (US-004)
# Paginare
"total": total,
"page": page,
"pages": pages,
"page_start": page_start,
"page_end": page_end,
# Filtre curente pentru linkurile de paginare (pastreaza filtrele, H2)
# Filtre curente pentru linkurile de paginare (pastreaza filtrele)
"f_status": status or "",
"f_vehicul": vehicul_q or "",
"f_data_de": data_de or "",
@@ -835,17 +831,17 @@ def fragment_submissions(
conn.close()
# Stari ne-trimise blocate pe care le putem corecta inline (US-010).
# Stari ne-trimise blocate pe care le putem corecta inline.
_CORECTABILE = ("needs_data", "needs_mapping")
# US-006b: stari cu select editabil cod_prestatie (superset al _CORECTABILE: error
# primeste select in formularul /repune, nu in /corecteaza — fara schimbare de vehicle fields).
# Stari cu select editabil cod_prestatie (superset al _CORECTABILE: error primeste
# select in formularul /repune, nu in /corecteaza — fara schimbare de vehicle fields).
_EDITABILE_OP = ("needs_data", "needs_mapping", "error")
# Stari gestionabile prin lifecycle web (US-011): sterge / re-pune in coada.
# Stari gestionabile prin lifecycle web: sterge / re-pune in coada.
_GESTIONABILE_WEB = ("error", "needs_data", "needs_mapping")
def _render_submissions(request: Request, conn, account_id: int) -> HTMLResponse:
"""Re-randeaza lista Trimiteri (fara filtre) — folosit dupa actiuni bulk (US-011)."""
"""Re-randeaza lista Trimiteri (fara filtre) — folosit dupa actiuni bulk."""
scope_sql, scope_params = account_scope_clause(account_id)
rows = conn.execute(
"SELECT id, status, id_prezentare, rar_status_code, rar_error, retry_count, "
@@ -865,7 +861,7 @@ def _render_submissions(request: Request, conn, account_id: int) -> HTMLResponse
def _payload_form_values(payload_json) -> dict:
"""Valori brute pentru prefill-ul formularului de corectie (US-010)."""
"""Valori brute pentru prefill-ul formularului de corectie."""
try:
data = json.loads(payload_json) if payload_json else {}
if not isinstance(data, dict):
@@ -882,7 +878,7 @@ def _payload_form_values(payload_json) -> dict:
def _nemapate_pentru_submission(row, nomenclator: list[dict]) -> list[dict]:
"""Operatiile nemapate ale UNUI submission needs_mapping, cu sugestii fuzzy (PRD 5.7).
"""Operatiile nemapate ale UNUI submission needs_mapping, cu sugestii fuzzy.
Echivalentul `pending_unmapped` restrans la un singur rand: parseaza payload_json,
aduna prestatiile fara cod_prestatie (cu cod_op_service) si ataseaza sugestii din
@@ -921,12 +917,12 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
"""Context pentru _trimitere_detaliu.html dintr-un rand de submission.
`conn`+`account_id` (optional): cand sunt date si randul e needs_mapping, expune
`nemapate_inline` + `nomenclator` pentru maparea inline din panou (PRD 5.7).
`nemapate_inline` + `nomenclator` pentru maparea inline din panou.
"""
eticheta = eticheta_stare(row["status"])
nemapate_inline: list[dict] = []
nomenclator: list[dict] = []
# Variabila interna: nomenclatorul complet (incarcat pentru needs_mapping, refolosit pt US-006)
# Nomenclatorul complet, incarcat pentru needs_mapping si refolosit mai jos.
_nomenclator_complet: list[dict] = []
if conn is not None and row["status"] == "needs_mapping":
# Un singur SELECT pe nomenclator: il refolosim si pentru sugestii si pentru dropdown.
@@ -934,14 +930,14 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
nemapate_inline = _nemapate_pentru_submission(row, _nomenclator_complet)
nomenclator = _nomenclator_complet if nemapate_inline else []
# US-006/US-006b: nomenclator pentru selectul cod_prestatie — needs_data/needs_mapping (in
# formularul /corecteaza) + error (in formularul /repune). Refoloseste _nomenclator_complet
# daca e deja incarcat (needs_mapping), altfel incarca fresh.
# Nomenclator pentru selectul cod_prestatie — needs_data/needs_mapping (in formularul
# /corecteaza) + error (in formularul /repune). Refoloseste _nomenclator_complet daca
# e deja incarcat (needs_mapping), altfel incarca fresh.
nomenclator_rar: list[dict] = []
if conn is not None and row["status"] in _EDITABILE_OP:
nomenclator_rar = _nomenclator_complet if _nomenclator_complet else load_nomenclator(conn)
# US-006: cod_prestatie curent din prima prestatie (pentru pre-selectare in select)
# cod_prestatie curent din prima prestatie (pentru pre-selectare in select)
cod_prestatie_curent = ""
try:
_pd = json.loads(row["payload_json"] or "{}")
@@ -969,14 +965,14 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
"created_at": format_data_rar(row["created_at"]),
"updated_at": format_data_rar(row["updated_at"]),
"next_attempt_at": format_data_rar(row["next_attempt_at"]),
# randuri ne-trimise blocate sunt corectabile (US-010); sent/sending nu
# randuri ne-trimise blocate sunt corectabile; sent/sending nu
"editabil": row["status"] in _CORECTABILE,
# US-011: error/needs_data/needs_mapping pot fi sterse / re-puse in coada
# error/needs_data/needs_mapping pot fi sterse / re-puse in coada
"gestionabil": row["status"] in _GESTIONABILE_WEB,
# PRD 5.7: mapare inline (operatii nemapate ale acestui rand + nomenclator)
# mapare inline (operatii nemapate ale acestui rand + nomenclator)
"nemapate_inline": nemapate_inline,
"nomenclator": nomenclator,
# US-006: select cod_prestatie pentru stari editabile
# select cod_prestatie pentru stari editabile
"nomenclator_rar": nomenclator_rar,
"cod_prestatie_curent": cod_prestatie_curent,
"corectie_msg": message,
@@ -988,7 +984,7 @@ def _detaliu_ctx(request: Request, row, *, message: str | None = None,
def _fetch_submission_scoped(conn, account_id: int, submission_id: int):
"""Randul scoped pe cont sau None (404 cross-account, nu confirmam existenta — B3)."""
"""Randul scoped pe cont sau None (404 cross-account, nu confirmam existenta)."""
scope_sql, scope_params = account_scope_clause(account_id)
return conn.execute(
f"SELECT * FROM submissions WHERE id=? AND {scope_sql}",
@@ -996,14 +992,14 @@ def _fetch_submission_scoped(conn, account_id: int, submission_id: int):
).fetchone()
# Campuri afisate in detaliul trimiterii (panou dedicat US-004). payload_json e
# plaintext si se foloseste doar pentru campurile derivate (prezentare_din_payload).
# Campuri afisate in detaliul trimiterii. payload_json e plaintext si se foloseste
# doar pentru campurile derivate (prezentare_din_payload).
@router.get("/_fragments/trimitere/{submission_id}", response_class=HTMLResponse)
def fragment_trimitere_detaliu(request: Request, submission_id: int) -> HTMLResponse:
"""Detaliu complet al unei trimiteri, in panou dedicat (#trimitere-detaliu).
Scoped pe contul sesiunii: 404 daca randul nu exista SAU apartine altui cont
(acelasi mesaj, nu confirmam existenta — vezi B3/router.py).
(acelasi mesaj, nu confirmam existenta).
"""
account_id = require_login(request)
conn = get_connection()
@@ -1021,7 +1017,7 @@ def fragment_trimitere_detaliu(request: Request, submission_id: int) -> HTMLResp
@router.post("/trimitere/{submission_id}/mapeaza", response_class=HTMLResponse)
async def post_mapeaza_inline(request: Request, submission_id: int) -> HTMLResponse:
"""Mapare inline din panoul de detaliu (PRD 5.7): alege cod RAR pentru o operatie nemapata.
"""Mapare inline din panoul de detaliu: alege cod RAR pentru o operatie nemapata.
Reutilizeaza EXACT save_mapping + reresolve_account (ca tab-ul Mapari) — fara logica
noua de clasificare. Re-rezolva scoped pe batch-ul randului (canal API batch_id IS NULL
@@ -1108,10 +1104,9 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
if isinstance(val, str) and val.strip() != "":
content[camp] = val.strip()
# US-006: injectare cod_prestatie din form INAINTE de resolve_prestatii.
# Oglindeste validarea din post_mapeaza_inline (nomenclator check). Codul nou
# e injectat in prima prestatie (index 0); build_key il include in hash (CLAUDE.md
# invariant "build_key hashuieste cod_prestatie, idempotency.py:34").
# Injectare cod_prestatie din form INAINTE de resolve_prestatii. Oglindeste
# validarea din post_mapeaza_inline (nomenclator check). Codul nou e injectat in
# prima prestatie (index 0); build_key il include in hash.
_cod_raw = form.get("cod_prestatie")
cod_prestatie_form = (_cod_raw.strip().upper() if isinstance(_cod_raw, str) else "")
if cod_prestatie_form:
@@ -1143,7 +1138,7 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
resolved, unmapped = resolve_prestatii(content.get("prestatii"), mapping, valid_codes, text_rules)
content["prestatii"] = resolved
# US-010: telemetrie pentru itemii rezolvati prin regula text (calea corectie web).
# telemetrie pentru itemii rezolvati prin regula text (calea corectie web).
_emite_text_rule_hits(conn, account_id, row["id"], resolved)
# Canonicalizare (strip ".0" odometru, VIN/nr upper) INAINTE de validare si cheie.
@@ -1238,8 +1233,8 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
"_trimitere_detaliu.html",
_detaliu_ctx(request, row2, message="Corectat — randul a fost re-pus in coada."),
)
# PRD 5.9 US-003 (R5): pe succes, lista se reincarca (trimiteriChanged) si modalul
# se inchide (inchideModal). After-settle ca inchiderea sa urmeze swap-ul fragmentului.
# Pe succes, lista se reincarca (trimiteriChanged) si modalul se inchide
# (inchideModal). After-settle ca inchiderea sa urmeze swap-ul fragmentului.
resp.headers["HX-Trigger-After-Settle"] = "trimiteriChanged, inchideModal"
return resp
finally:
@@ -1247,26 +1242,26 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
# =========================================================================== #
# US-011 — Lifecycle trimiteri blocate din dashboard: sterge / re-pune in coada #
# Peste helper-ul US-009 (submissions_admin). CSRF enforce; scoped pe sesiune. #
# Lifecycle trimiteri blocate din dashboard: sterge / re-pune in coada. #
# Peste helper-ul submissions_admin. CSRF enforce; scoped pe sesiune. #
# =========================================================================== #
@router.post("/trimitere/{submission_id}/repune", response_class=HTMLResponse)
async def post_repune_trimitere(request: Request, submission_id: int) -> HTMLResponse:
"""Re-pune in coada un rand blocat (error/needs_data/needs_mapping) din dashboard.
US-006b: daca randul e in starea `error` si formularul contine `cod_prestatie`,
actualizeaza codul in payload, recalculeaza cheia de idempotency si re-pun in coada
direct (fara `requeue_submission`, care nu actualizeaza cheia). Scoped pe sesiune
(404 cross-account/inexistent, 409 sent/sending). Re-randeaza panoul de detaliu cu
starea noua + nudge `trimiteriChanged` pentru lista.
Daca randul e in starea `error` si formularul contine `cod_prestatie`, actualizeaza
codul in payload, recalculeaza cheia de idempotency si re-pune in coada direct (fara
`requeue_submission`, care nu actualizeaza cheia). Scoped pe sesiune (404
cross-account/inexistent, 409 sent/sending). Re-randeaza panoul de detaliu cu starea
noua + nudge `trimiteriChanged` pentru lista.
"""
account_id = require_login(request)
form = await request.form()
verify_csrf(request, str(form.get("csrf_token") or ""))
conn = get_connection()
try:
# US-006b: prelucrare cod_prestatie pentru starea error (inaintea requeue_submission
# Prelucrare cod_prestatie pentru starea error (inaintea requeue_submission
# standard, care nu actualizeaza cheia de idempotency).
_cod_raw = form.get("cod_prestatie")
cod_prestatie_form = (_cod_raw.strip().upper() if isinstance(_cod_raw, str) else "")
@@ -1398,7 +1393,7 @@ async def post_sterge_trimitere(request: Request, submission_id: int) -> HTMLRes
resp = HTMLResponse(
'<div class="flash" style="margin:0;">Trimitere stearsa.</div>'
)
# PRD 5.9 US-003 (R5): pe succes, lista se reincarca + modalul se inchide.
# Pe succes, lista se reincarca + modalul se inchide.
resp.headers["HX-Trigger-After-Settle"] = "trimiteriChanged, inchideModal"
return resp
finally:
@@ -1410,7 +1405,7 @@ async def post_sterge_bulk(request: Request) -> HTMLResponse:
"""Sterge in bloc trimiterile selectate (doar blocate, scoped pe sesiune).
Sare peste randuri sent/sending (read-only) si cross-account (inexistente) fara a
opri operatia — pe modelul panoului admin (PRD 5.5). Re-randeaza lista Trimiteri.
opri operatia — pe modelul panoului admin. Re-randeaza lista Trimiteri.
"""
account_id = require_login(request)
form = await request.form()
@@ -1436,7 +1431,7 @@ async def post_sterge_bulk(request: Request) -> HTMLResponse:
def _load_saved_op_mappings(conn, account_id: int) -> list[dict]:
"""Mapari operatie->cod salvate (operations_mapping) ale contului, cu numele
prestatiei jonctionat din nomenclator (US-005). Scoped pe cont (NOT NULL → simplu)."""
prestatiei jonctionat din nomenclator. Scoped pe cont (NOT NULL → simplu)."""
acct = account_or_default(account_id)
rows = conn.execute(
"SELECT o.id, o.cod_op_service, o.cod_prestatie, o.auto_send, n.nume_prestatie "
@@ -1458,7 +1453,7 @@ def _load_saved_op_mappings(conn, account_id: int) -> list[dict]:
def _load_column_formats(conn, account_id: int) -> list[dict]:
"""Formate de coloane salvate (column_mappings) ale contului (US-006).
"""Formate de coloane salvate (column_mappings) ale contului.
Coloanele afisate = cheile din json_mapare (campurile recunoscute). Scoped pe cont.
"""
@@ -1507,7 +1502,7 @@ def _render_mapari(
def fragment_mapari(request: Request) -> HTMLResponse:
"""Editor mapari: operatii ROAAUTO nemapate + sugestii fuzzy pe nomenclator RAR.
Scoped pe contul sesiunii (C6/task#7): pending_unmapped primeste account_id explicit.
Scoped pe contul sesiunii: pending_unmapped primeste account_id explicit.
"""
account_id = require_login(request)
conn = get_connection()
@@ -1547,7 +1542,7 @@ def post_mapare(
# =========================================================================== #
# US-005 — Mapari operatii salvate: editare cod/auto-send + stergere #
# Mapari operatii salvate: editare cod/auto-send + stergere. #
# CRUD pe operations_mapping scoped pe sesiune; re-rezolva blocatele la edit. #
# =========================================================================== #
@@ -1611,7 +1606,7 @@ def post_sterge_mapare_salvata(
# =========================================================================== #
# US-004 (5.8) — Reguli automate (text): substring -> cod RAR #
# Reguli automate (text): substring -> cod RAR. #
# Adaugare/stergere reguli text scoped pe sesiune; salvarea re-rezolva blocajele.#
# =========================================================================== #
@@ -1628,7 +1623,7 @@ def post_salveaza_regula_text(
Scoped pe contul sesiunii (save_text_rule foloseste account_or_default(sesiune)).
Valideaza cod_prestatie fata de nomenclator INAINTE de save (cod necunoscut ->
respins inline, fara salvare). La succes: mesaj „Regula salvata. Deblocate: N"
+ trigger trimiteriChanged (refresh lista), ca maparea inline (5.7).
+ trigger trimiteriChanged (refresh lista), ca maparea inline.
"""
account_id = require_login(request)
verify_csrf(request, csrf_token)
@@ -1647,7 +1642,7 @@ def post_salveaza_regula_text(
request, conn, account_id,
message=f"Cod RAR necunoscut in nomenclator: {cod}.",
)
# US-011: avertisment neblocant daca regula noua se suprapune (substring,
# avertisment neblocant daca regula noua se suprapune (substring,
# oricare directie) cu una existenta. Calculam INAINTE de save, fata de
# regulile curente, ca pattern-ul nou sa nu se compare cu sine.
overlap = text_rules_overlap(pat, load_text_rules(conn, account_id))
@@ -1696,7 +1691,7 @@ def post_preview_regula_text(
pattern: str = Form(""),
csrf_token: str | None = Form(None),
) -> HTMLResponse:
"""Preview pre-salvare (US-009): cate operatii nemapate ar potrivi regula.
"""Preview pre-salvare: cate operatii nemapate ar potrivi regula.
NU salveaza nimic (zero scriere DB). Normalizeaza pattern-ul cu
normalize_for_match, numara operatiile DISTINCTE nemapate ale contului
@@ -1743,7 +1738,7 @@ def post_preview_regula_text(
# =========================================================================== #
# US-006 — Formate de coloane salvate: editare format data + stergere #
# Formate de coloane salvate: editare format data + stergere. #
# CRUD pe column_mappings scoped pe sesiune (prin id, verificat pe account). #
# =========================================================================== #
@@ -1879,7 +1874,7 @@ def _web_compute_preview(
if not raw_rows_db:
return "Niciun rand in batch."
# Decripteaza randurile + override-urile editate (3.6)
# Decripteaza randurile + override-urile editate
rows: list[dict[str, Any]] = []
overrides: list[dict[str, Any]] = []
for r in raw_rows_db:
@@ -1907,12 +1902,12 @@ def _web_compute_preview(
json_mapare: dict[str, str] = json.loads(mapping_row["json_mapare"])
format_data: str | None = mapping_row["format_data"]
# Mapare operatii (o singura incarcare — Eng#5)
# Mapare operatii (o singura incarcare)
mapping_meta = load_mapping_meta(conn, acct)
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# US-010/US-003 (paritate): preview-ul web trebuie sa aplice ACELEASI reguli text +
# validare nomenclator ca si commit-ul (2426), altfel un rand rezolvabil doar prin
# regula text ar fi marcat needs_mapping si exclus din commit. Incarcate o data.
# Paritate cu commit-ul: preview-ul web trebuie sa aplice ACELEASI reguli text +
# validare nomenclator, altfel un rand rezolvabil doar prin regula text ar fi marcat
# needs_mapping si exclus din commit. Incarcate o data.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
@@ -1980,7 +1975,7 @@ def _web_compute_preview(
"idempotency_key": key,
})
# Already_sent: batch lookup (Eng#5 — fara N+1)
# Already_sent: batch lookup (fara N+1)
unique_keys = list(set(keys_for_lookup))
already_sent_map = _already_sent_lookup(conn, account_id, unique_keys)
@@ -2091,7 +2086,7 @@ async def web_upload_import(
try:
sig = _signature(parsed.columns)
# Stagingul in DB (tranzactie explicita — Issue 6)
# Stagingul in DB (tranzactie explicita)
conn.execute("BEGIN IMMEDIATE")
try:
cur = conn.execute(
@@ -2330,15 +2325,15 @@ def web_preview_import(
# =========================================================================== #
# US-002 (3.6) — Editare celule in preview: mod editare pe rand. #
# Swap pe rand (#preview-row-N) + OOB contoare, NU pe #import-section (D-3.1). #
# Status rederivat DOAR prin _resolve_row_for_preview (H2 — fara clasificator). #
# Editare celule in preview: mod editare pe rand. #
# Swap pe rand (#preview-row-N) + OOB contoare, NU pe #import-section. #
# Status rederivat DOAR prin _resolve_row_for_preview (fara clasificator). #
# =========================================================================== #
def _preview_one_row(conn, import_id: int, account_id: int, row_index: int):
"""Recalculeaza preview-ul si extrage un singur rand.
Statusul e rederivat prin `_resolve_row_for_preview` (H2, fara clasificator duplicat),
Statusul e rederivat prin `_resolve_row_for_preview` (fara clasificator duplicat),
iar `_web_compute_preview` persista `resolved_status` pentru toate randurile — astfel
confirmarea (commit) vede starea editata. Intoarce (result, row) sau (mesaj, None)."""
result = _web_compute_preview(conn, import_id, account_id)
@@ -2366,7 +2361,7 @@ def _render_preview_rand(
@router.get("/_import/{import_id}/rand/{row_index}/editare", response_class=HTMLResponse)
def web_rand_editare(request: Request, import_id: int, row_index: int) -> HTMLResponse:
"""Intra in mod editare pe un rand de preview (randul devine FORM propriu, D-3.3)."""
"""Intra in mod editare pe un rand de preview (randul devine FORM propriu)."""
account_id = require_login(request)
conn = get_connection()
try:
@@ -2400,11 +2395,11 @@ def web_rand_display(request: Request, import_id: int, row_index: int) -> HTMLRe
@router.post("/_import/{import_id}/rand/{row_index}/editeaza", response_class=HTMLResponse)
async def web_editeaza_rand(request: Request, import_id: int, row_index: int) -> HTMLResponse:
"""Alias web (US-001/US-002): persista override (mutatie pura) + re-randeaza DOAR randul.
"""Persista override (mutatie pura) + re-randeaza DOAR randul.
Statusul e rederivat prin `_resolve_row_for_preview` (H2). Swap pe rand + OOB
contoare (D-3.1). Daca raman erori de continut pe camp, randul ramane in editare
cu valorile pastrate si mesajul pe campul vinovat (D-2.1/D-2.2)."""
Statusul e rederivat prin `_resolve_row_for_preview`. Swap pe rand + OOB contoare.
Daca raman erori de continut pe camp, randul ramane in editare cu valorile pastrate
si mesajul pe campul vinovat."""
account_id = require_login(request)
form = await request.form()
verify_csrf(request, str(form.get("csrf_token") or ""))
@@ -2502,8 +2497,8 @@ async def web_confirma_import(
Replica logica din import_router.commit_import dar cu input din form HTML
si raspuns HTML (nu JSON). INSERT per-rand ON CONFLICT DO NOTHING (TOCTOU).
C8/OV-2: account_id din sesiune propagat consecvent la build_key si toate lookup-urile.
C12: require_login — pe scrieri NICIODATA fallback cont 1 in prod.
account_id din sesiune propagat consecvent la build_key si toate lookup-urile.
require_login — pe scrieri NICIODATA fallback cont 1 in prod.
"""
account_id = require_login(request)
acct = account_or_default(account_id)
@@ -2644,11 +2639,11 @@ async def web_confirma_import(
# Mapare operatii
mapping_meta = load_mapping_meta(conn, acct)
mapping_ops = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
# T2: validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
# validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
valid_codes = load_nomenclator_codes(conn) or None
text_rules = load_text_rules(conn, acct)
# Enqueue in tranzactie explicita (Issue 6) — INSERT ON CONFLICT DO NOTHING (TOCTOU)
# Enqueue in tranzactie explicita — INSERT ON CONFLICT DO NOTHING (TOCTOU)
enqueued: list[dict] = []
toctou: list[int] = []
rows_for_hash: list[str] = []
@@ -2696,7 +2691,7 @@ async def web_confirma_import(
"odometru_final": canon["odometru_final"],
})
# Override editat in preview (3.6) — aplicat ULTIMUL, ca in resolver.
# Override editat in preview — aplicat ULTIMUL, ca in resolver.
override = item.get("override") or {}
if override:
mapped.update(override)
@@ -2729,7 +2724,7 @@ async def web_confirma_import(
if cur.rowcount == 0:
toctou.append(row_index)
else:
# US-010: telemetrie pentru itemii rezolvati prin regula text.
# telemetrie pentru itemii rezolvati prin regula text.
_emite_text_rule_hits(conn, acct, int(cur.lastrowid), resolved_p)
enqueued.append({"submission_id": cur.lastrowid, "row_index": row_index})
@@ -2740,7 +2735,7 @@ async def web_confirma_import(
n_enqueued = len(enqueued)
# Log atestare (Voce#9)
# Log atestare
rows_hash = hashlib.sha256(
json.dumps(sorted(rows_for_hash), ensure_ascii=False).encode("utf-8")
).hexdigest() if rows_for_hash else ""
@@ -2757,7 +2752,7 @@ async def web_confirma_import(
# 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 (US-003/US-004).
# sectiunea "Trimiterile tale" de pe Acasa.
toctou_msg = f" ({len(toctou)} coliziuni TOCTOU excluse)" if toctou else ""
return templates.TemplateResponse("_upload.html", _ctx(
request,
@@ -2773,8 +2768,8 @@ async def web_confirma_import(
# =========================================================================== #
# US-007 — Sectiune "Contul meu": rotire cheie API + creds RAR din UI #
# Rute web proprii scoped pe sesiune (C13: nu reutilizeaza /v1/conturi/rar-creds
# Sectiune "Contul meu": rotire cheie API + creds RAR din UI. #
# Rute web proprii scoped pe sesiune (nu reutilizeaza /v1/conturi/rar-creds #
# care cere cheie API; sesiunea web e suficienta ca identitate). #
# =========================================================================== #
@@ -2846,10 +2841,9 @@ def integrare_test_cheie(
) -> HTMLResponse:
"""Verifica cheia API lipita de utilizator — scoped pe contul sesiunii.
US-004 (PRD Etapa 5): permite utilizatorului sa confirme ca o cheie copiata
din generatorul de exemple corespunde contului sau, fara efecte secundare
(fara creare/rotire). Cheie goala, invalida sau a altui cont -> mesaj de
eroare neutru (fara eco al cheii in raspuns).
Permite utilizatorului sa confirme ca o cheie copiata din generatorul de exemple
corespunde contului sau, fara efecte secundare (fara creare/rotire). Cheie goala,
invalida sau a altui cont -> mesaj de eroare neutru (fara eco al cheii in raspuns).
"""
account_id = require_login(request)
verify_csrf(request, csrf_token)
@@ -2901,7 +2895,7 @@ def cont_rar_creds(
rar_parola: str = Form(""),
csrf_token: str | None = Form(None),
) -> HTMLResponse:
"""Seteaza creds RAR per cont din sesiune (ruta web proprie, C13).
"""Seteaza creds RAR per cont din sesiune (ruta web proprie).
Camp parola NICIODATA re-pus in value= la re-randare.
Validare minima: email si parola negoale.

View File

@@ -1,6 +1,6 @@
"""Helper-e sesiune web. US-002 PRD 3.3.
"""Helper-e sesiune web.
Mecanism require_login (C11): NU un dependency FastAPI care intoarce RedirectResponse
Mecanism require_login: NU un dependency FastAPI care intoarce RedirectResponse
(acela nu scurtcircuiteaza handler-ul — FastAPI continua executia). In schimb:
- require_login() RIDICA LoginRequired
- app.main inregistreaza @app.exception_handler(LoginRequired) care intoarce
@@ -31,7 +31,7 @@ def current_account(request: Request) -> int | None:
def current_user_id(request: Request) -> int | None:
"""user_id din sesiune sau None (C19: leaga import_attestations.confirmed_by)."""
"""user_id din sesiune sau None (leaga import_attestations.confirmed_by)."""
val = request.session.get("user_id")
return int(val) if val is not None else None
@@ -88,7 +88,7 @@ def require_admin(request: Request) -> int:
def set_session(request: Request, account_id: int, user_id: int) -> None:
"""Seteaza sesiunea dupa login. Curata mai intai (C3 anti-fixare sesiune)."""
"""Seteaza sesiunea dupa login. Curata mai intai (anti-fixare sesiune)."""
request.session.clear()
request.session["account_id"] = account_id
request.session["user_id"] = user_id

View File

@@ -44,12 +44,8 @@
</div>
{% endif %}
{# US-001 (5.5): randul "Ajutor" (wayfinding Mapari/Coduri RAR) eliminat — navigarea
traieste in tab-bar (Mapari) si in meniul de cont (Nomenclator etc.). #}
{# === Sectiunea Trimiteri ("Trimiterile tale"), permanenta sub upload (US-003).
Suprimata la first-run (zero trimiteri): bara de upload acopera deja CTA-ul,
iar empty-state-ul tabelului ar fi redundant (US-004 / D-5.1). === #}
{# 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. #}
{% if are_trimiteri %}
{% include '_coada.html' %}
{% endif %}

View File

@@ -1,8 +1,6 @@
{#
_coada.html — repurposat in 3.6 (US-003).
Nu mai e un tab/panou separat: e sectiunea "Trimiterile tale" inclusa pe Acasa,
sub zona de upload. Pastreaza filtrele (US-009) si tabelul (_submissions.html); detaliul
se deschide acum in modalul global (#modal-detaliu). Poll aliniat la 15s (anti dublu-poll, M5).
_coada.html — sectiunea "Trimiterile tale" inclusa pe Acasa, sub zona de upload.
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);">
@@ -68,8 +66,4 @@
</div>
</div>
{# PRD 5.9 US-003: detaliul s-a mutat intr-un MODAL global (#modal-detaliu in base.html),
in afara #submissions-wrap -> poll-ul de 15s nu-l mai atinge. Randul declanseaza
deschiderea (hx-target=#detaliu-modal-body). Vechiul panou inert #trimitere-detaliu
a fost eliminat (rol preluat de modal). #}
</section>

View File

@@ -1,5 +1,5 @@
{#
_eroare.html — macro card_erori(erori) (US-006, PRD 5.4).
_eroare.html — macro card_erori(erori).
Primeste o lista de dict-uri cu cheile: problema, cauza, fix, field (sau None).
Afiseaza 3 niveluri intr-un bloc scannabil:

View File

@@ -1,6 +1,6 @@
{# _jurnal.html — tab Jurnal de aplicatie (US-006, PRD 5.6).
{# _jurnal.html — tab Jurnal de aplicatie.
Lista paginata de evenimente (app_events), redactate la scriere. Filtre tip/nivel/
data + (admin) cont. Stil consistent cu tabelele PRD 5.5 (.tablewrap). #}
data + (admin) cont. #}
<section id="jurnal-section" aria-labelledby="jurnal-heading">
<div class="card">
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 12px;">

View File

@@ -1,18 +1,14 @@
{# Macro-uri partajate intre template-urile de import si mapari. #}
{# US-003 (5.5): comutator COMPACT "In coada" — Manual <-> Auto pe un singur rand, fara
proza inline (explicatia traieste o singura data in panoul Ajutor al cardului Mapari).
Inlocuieste varianta verbose din 3.6 care injecta 3 randuri de text pe FIECARE linie de
tabel (randuri inalte -> Salveaza/Sterge ieseau din viewport).
INVARIANT BACKEND (nealterat din 3.6): control = checkbox cu `name="auto_send" value="true"`
si SEMANTICA DE PREZENTA (bifat -> trimite "true" -> True; nebifat -> camp absent -> False).
{# 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. Zero atingere backend.
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 (H4) — bifat = Auto. #}
- 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."

View File

@@ -4,7 +4,7 @@
/* Selectul de cod RAR e principalul vinovat de latimea tabelelor de mapari. Il limitam ca
tabelul sa incapa in card fara scroll orizontal -> coloana Actiuni (kebab) ramane vizibila. */
#mapari-section td select { width:100%; max-width:240px; min-width:150px; }
/* US-007 (R12): in card per rand (sub 767px) selectul/inputurile umplu cardul. */
/* In card per rand (sub 767px) selectul/inputurile umplu cardul. */
@media (max-width:767px) {
#mapari-section td select, #mapari-section td input[type=text] { max-width:none; min-width:0; }
}
@@ -18,11 +18,6 @@
<!-- Sectiunea 1: De rezolvat (operatii needs_mapping) -->
<!-- ============================================================ -->
<div class="card">
{# US-005 (5.5): antet standard + link Ajutor ca <details> nativ (fara JS). Toata proza
care inainte se repeta inline (scopul maparilor, Auto/Manual) traieste acum AICI,
o singura data, ascunsa implicit. #}
{# US-010: sectiunea de ajutor (details.ajutor-mapari) eliminata.
Empty-state „Nicio operatie nemapata" eliminat — sectiunea ramane goala (fara text). #}
<h2 style="font-size:15px; margin:0 0 8px;">De rezolvat</h2>
{% if pending %}
@@ -102,8 +97,6 @@
Nicio mapare salvata inca. Pe masura ce mapezi operatii, ele apar aici si le poti edita oricand.
</div>
{% else %}
{# US-005 (5.5): proza explicativa mutata in panoul Ajutor de la "De rezolvat" (o singura data). #}
<div data-dt="10">
<div class="dt-tools">
<input type="search" data-dt-search class="dt-search"
@@ -150,7 +143,7 @@
{{ 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">
{# US-011: butoane icon mereu vizibile (fara kebab). SVG aria-hidden; aria-label pe buton.
{# 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,
JS adauga clasa "dirty" pe butonul de salvare (fundal --accent = modificari nesalvate). #}
<button type="submit" form="map-salv-{{ loop.index }}"
@@ -182,7 +175,6 @@
<!-- ============================================================ -->
<!-- Sectiunea 3: Reguli automate pe text (operation_text_rules) -->
<!-- US-010: mutata pe pozitia 3 (inainte de Formate de coloane) -->
<!-- ============================================================ -->
<div class="card">
<h2 style="font-size:15px; margin:0 0 8px;">Reguli automate (text)</h2>
@@ -266,7 +258,7 @@
<button type="submit" form="rt-add">Adauga</button>
</td>
</tr>
{# Preview pre-salvare (US-009): cate operatii nemapate potriveste pattern-ul. #}
{# Preview pre-salvare: cate operatii nemapate potriveste pattern-ul. #}
<tr>
<td colspan="4" style="padding-top:0;">
<div id="rt-preview" aria-live="polite"></div>
@@ -279,7 +271,6 @@
<!-- ============================================================ -->
<!-- Sectiunea 4: Formate de coloane salvate (column_mappings) -->
<!-- US-010: mutata pe pozitia 4 (dupa Reguli automate) -->
<!-- ============================================================ -->
<div class="card">
<h2 style="font-size:15px; margin:0 0 12px;">Formate de coloane salvate</h2>

View File

@@ -1,7 +1,5 @@
{# US-002 (5.5): aceeasi grila standard ca tabelul Trimiteri (_submissions.html):
.tablewrap > table, antet th standard (mostenit din base.html), cod in .pill,
denumire ca text normal (singura coloana care se poate rupe pe randuri inguste),
empty-state in .empty. Zero stiluri inline noi — totul vine din base.html. #}
{# Aceeasi grila standard ca tabelul Trimiteri: cod in .pill, denumire ca text normal
(singura coloana care se poate rupe pe randuri inguste), empty-state in .empty. #}
{% if rows %}
<div class="tablewrap">
<table>

View File

@@ -17,7 +17,7 @@
</div>
{% endif %}
<!-- Rezumat stari (id stabil pentru OOB swap dupa editarea unui rand — US-002) -->
<!-- Rezumat stari (id stabil pentru OOB swap dupa editarea unui rand) -->
{% set status_labels = [
('ok', 'gata de trimis'),
('needs_review', 'verifica valori'),
@@ -108,7 +108,7 @@
{% endif %}
<!-- Tabel preview. Randurile au FORM PROPRIU pentru editare (NU sunt in #confirm-form,
altfel Enter intr-un camp ar declansa trimiterea ireversibila — D-3.3). Bifele
altfel Enter intr-un camp ar declansa trimiterea ireversibila). Bifele
needs_review se asociaza la #confirm-form prin atributul form=. -->
<div class="tablewrap">
<table>
@@ -142,7 +142,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
<div class="sticky-bar">
<div style="flex:1; min-width:280px;">
<!-- Banner declarant (D12) — direct deasupra input-ului N -->
<!-- Banner declarant — direct deasupra input-ului N -->
<div class="banner warn" style="margin-bottom:10px; padding:8px 12px; border-radius:6px;"
role="note" aria-live="polite">
Confirmand, TU esti declarantul acestor
@@ -199,7 +199,7 @@
</form>
<!-- Contor "gata de trimis" citit din DOM (data-ok), ca OOB swap-ul de la editare
sa actualizeze N fara a re-randa sectiunea (US-002). -->
sa actualizeze N fara a re-randa sectiunea. -->
<span id="preview-ok-count" data-ok="{{ summary.get('ok', 0) }}" hidden></span>
<div style="padding:8px 0 4px;">
@@ -212,13 +212,13 @@
<script>
(function() {
/* D-1.2: un singur sticky bar pe ecran — cat preview-ul de import e activ,
/* Un singur sticky bar pe ecran — cat preview-ul de import e activ,
ascunde sectiunea Trimiteri de pe Acasa (se reveleaza la reset/commit din _upload.html). */
var trim = document.getElementById('trimiteri-section');
if (trim) trim.style.display = 'none';
/* nOk se citeste din DOM (#preview-ok-count[data-ok]) ca OOB swap-ul de la editare
sa-l poata actualiza fara re-randarea sectiunii (D-3.1/D-3.4). */
sa-l poata actualiza fara re-randarea sectiunii. */
function getOk() {
var el = document.getElementById('preview-ok-count');
return el ? parseInt(el.dataset.ok || '0', 10) : 0;
@@ -231,7 +231,7 @@
var inp = document.getElementById('n-confirmat');
var disp = document.getElementById('n-display');
var btn = document.getElementById('confirm-btn');
/* Nu re-activa confirm cat un rand e in editare (mutual-exclusion D-3.2). */
/* Nu re-activa confirm cat un rand e in editare (mutual-exclusion). */
var editing = document.querySelector('tr[data-editing="1"]') !== null;
if (inp) inp.value = total;
if (disp) disp.textContent = total;

View File

@@ -1,10 +1,10 @@
{#
_preview_rand.html — un singur rand de preview import (US-002, 3.6).
_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.
Swap pe RAND (hx-target pe #preview-row-N, outerHTML), NU pe #import-section (D-3.1).
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).
#}
{%- set res = row.resolved -%}
@@ -80,7 +80,7 @@
</tr>
<script>
(function() {
/* Mutual-exclusion (D-3.2/3.6): cat un rand e in editare, dezactiveaza confirm + alte Editeaza. */
/* Mutual-exclusion: cat un rand e in editare, dezactiveaza confirm + alte Editeaza. */
var btn = document.getElementById('confirm-btn');
if (btn) { btn.disabled = true; btn.title = 'Termina editarea randului inainte de a trimite.'; }
document.querySelectorAll('.btn-editeaza').forEach(function(b) { b.disabled = true; });
@@ -152,7 +152,7 @@
</td>
</tr>
{% if include_oob %}
{# OOB: actualizeaza rezumatul si contorul "gata de trimis" dupa save, fara a re-randa sectiunea (D-3.1). #}
{# 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')] %}

View File

@@ -1,6 +1,6 @@
{#
OOB: actualizeaza inputul id="f-page" din #filtre-trimiteri (US-004 L2).
Poll-ul de 15s (hx-include="#filtre-trimiteri") preia automat pagina curenta.
OOB: actualizeaza inputul id="f-page" din #filtre-trimiteri.
Reincarcarea (hx-include="#filtre-trimiteri") preia automat pagina curenta.
Elementul OOB e extras din continutul normal de HTMX inainte de swap in #submissions-wrap.
#}
<input type="hidden" id="f-page" name="page" value="{{ page | default(1) }}" hx-swap-oob="true">
@@ -13,7 +13,7 @@
<span id="trimiteri-versiune" data-v="{{ versiune_trimiteri | default('') }}" hidden></span>
{% if rows %}
{# US-011: form de stergere bulk. Selectia opereaza DOAR pe randuri blocate
{# Form de stergere bulk. Selectia opereaza DOAR pe randuri blocate
(gestionabil); sent/sending/queued nu au checkbox (read-only). #}
<form id="bulk-trimiteri"
hx-post="/trimiteri/sterge-bulk"
@@ -43,9 +43,8 @@
</tr></thead>
<tbody>
{% for r in rows %}
{# PRD 5.9 US-003: randul declanseaza deschiderea MODALULUI global (#detaliu-modal-body),
nu un rand-sibling. Clickabil/focusabil (role=button); Enter/Space deschid modalul
(JS in base.html). Vechiul rand-sibling de detaliu a fost eliminat. #}
{# Randul declanseaza deschiderea MODALULUI global (#detaliu-modal-body).
Clickabil/focusabil (role=button); Enter/Space deschid modalul (JS in base.html). #}
<tr id="trimitere-row-{{ r.id }}"
class="trimitere-row"
data-detaliu-id="{{ r.id }}"
@@ -65,8 +64,8 @@
<td class="col-id muted" data-eticheta="#">{{ r.id }}</td>
<td class="col-stare" data-eticheta="Stare">
<span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}">{{ r.stare_scurt }}</span>
{# PRD 5.9 US-002 (R1): eticheta umana scurta sub pill — text mic, `s-error`
pe error/needs_* (singurele stari pe care `eticheta_problema` e ne-goala).
{# Eticheta umana scurta sub pill — text mic, `s-error` pe error/needs_*
(singurele stari pe care `eticheta_problema` e ne-goala).
Stare transmisa prin TEXT, nu doar culoare. Codul brut ramane in modal. #}
{% if r.eticheta_problema %}
<div class="eticheta-problema s-error">{{ r.eticheta_problema }}</div>
@@ -75,14 +74,14 @@
<td class="col-vehicul" data-eticheta="Vehicul">
{{ r.prez.vehicul_nr }}
{% if r.prez.vin_scurt and r.prez.vin_scurt != '—' %}
{# US-005: VIN pe rand separat sub nr (element block, nu span inline) #}
{# VIN pe rand separat sub nr (element block, nu span inline) #}
<div class="muted" style="font-size:12px;">{{ r.prez.vin_scurt }}</div>
{% endif %}
</td>
<td class="col-operatie" data-eticheta="Operatie">
<div>{{ r.prez.operatie }}</div>
{# PRD 5.9 US-002: doar codul RAR (ex. OE-2), FARA prefixul "cod RAR:" — chip
muted discret; cand nemapat afiseaza "nemapat" muted (comportament 5.8). #}
{# Doar codul RAR (ex. OE-2), FARA prefixul "cod RAR:" — chip muted discret;
cand nemapat afiseaza "nemapat" muted. #}
{% if r.prez.cod_rar and r.prez.cod_rar != '—' %}
<div class="cod-rar-sub"><span class="cod-rar-cod">{{ r.prez.cod_rar }}</span></div>
{% else %}
@@ -100,7 +99,7 @@
</form>
{#
Paginare numerotata (US-004 PRD 5.10).
Paginare numerotata.
Afisata doar cand exista mai mult de o pagina.
Fiecare link pastreaza filtrele curente (status, vehicul, data_de, data_pana).
Pagina curenta: aria-current="page" (semantic).

View File

@@ -1,14 +1,13 @@
{% from "_eroare.html" import card_erori %}
{% import '_macros.html' as ui %}
{# PRD 5.9 US-004: detaliu editabil in-place, butoane consolidate, ordine verticala R10.
Fragmentul se swap-uieste in corpul modalului global (#detaliu-modal-body). Heading-ul
poarta id-ul folosit de aria-labelledby al dialogului.
R9: operatie + cod RAR rezolvat apar IMPREUNA, read-only, folosind `prez.cod_rar`
{# Detaliu editabil in-place. Fragmentul se swap-uieste in corpul modalului global
(#detaliu-modal-body). Heading-ul poarta id-ul folosit de aria-labelledby al dialogului.
Operatie + cod RAR rezolvat apar IMPREUNA, read-only, folosind `prez.cod_rar`
(fallback „nemapat"), fara eticheta separata „Cod RAR". #}
{% set cod_afis = prez.cod_rar if (prez.cod_rar and prez.cod_rar != '—') else 'nemapat' %}
<div class="card" id="detaliu-card-{{ id }}" style="border:none; padding:0; margin:0;">
{# === R10 (1): header — #id + pill + motiv uman === #}
{# === Header — #id + pill + motiv uman === #}
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 8px;">
<h2 id="detaliu-modal-titlu" style="font-size:15px; margin:0;">Detaliu trimitere #{{ id }}</h2>
<span class="pill {{ stare_css }}">{{ stare_text }}</span>
@@ -19,7 +18,7 @@
<p class="muted" style="margin:0 0 12px; font-size:13px;">{{ stare_subtext }}</p>
{% endif %}
{# === R10 (2): bloc eroare blocanta — DOAR in read-only (US-008).
{# === Bloc eroare blocanta — DOAR in read-only.
In editare, cardul 3-niveluri e inlocuit cu: erori per-camp in macro `camp`
(text simplu .s-error) + rezumat top-of-form pentru erori fara camp (mai jos). === #}
{% if not editabil and erori_3n %}
@@ -28,7 +27,7 @@
</div>
{% endif %}
{# === R10 (3) + R9: mapare inline (PRD 5.7) — alege cod RAR pentru operatiile nemapate.
{# === Mapare inline — alege cod RAR pentru operatiile nemapate.
Cand nemapate_inline, linia „Operatie: X · nemapat" apare in formularul de mai jos
(cod_afis = nemapat), iar aici e picker-ul; dupa mapare, re-render arata codul rezolvat. === #}
{% if nemapate_inline %}
@@ -78,7 +77,7 @@
</div>
{% endif %}
{# === R10 (4): formular editabil (needs_data/needs_mapping) SAU context read-only.
{# === Formular editabil (needs_data/needs_mapping) SAU context read-only.
Zero dublare: campurile vehiculului apar O SINGURA DATA — editabile cand randul e
corectabil, altfel read-only. Operatie + cod RAR read-only deasupra campurilor. === #}
{% if editabil %}
@@ -90,7 +89,7 @@
{% if corectie_error %}role="alert"{% endif %}>{{ corectie_msg }}</div>
{% endif %}
{# US-008 (M6): erori fara camp (field None) nu dispar silentios in editare —
{# Erori fara camp (field None) nu dispar silentios in editare —
cardul 3n e ascuns, deci adaugam un rezumat simplu top-of-form.
Erori cu camp raman afisate per-camp de macro-ul `camp` de mai jos. #}
{% for e in erori_3n if not e.field %}
@@ -114,7 +113,7 @@
hx-disabled-elt="find button">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
{# US-006: select cod RAR pe stari editabile (needs_data/needs_mapping), cu nomenclator.
{# Select cod RAR pe stari editabile (needs_data/needs_mapping), cu nomenclator.
Read-only pe sent/sending/queued/error (nomenclator_rar gol → ramura else). #}
{% if nomenclator_rar %}
<div style="margin:0 0 12px;">
@@ -133,15 +132,15 @@
</select>
</div>
{% else %}
{# Operatie + cod RAR read-only deasupra campurilor (R9, fara eticheta „Cod RAR"). #}
{# Operatie + cod RAR read-only deasupra campurilor (fara eticheta „Cod RAR"). #}
<div style="margin:0 0 12px;">
<div class="muted" style="font-size:12px;">Operatie</div>
<div>{{ prez.operatie }} &middot; {{ cod_afis }}</div>
</div>
{% endif %}
{# US-007: operatie service (cod intern + denumire venita prin API/import), distinct de
operatia RAR mapata. Conventie US-002: op_service_cod="" cand lipseste → randul absent. #}
{# Operatie service (cod intern + denumire venita prin API/import), distinct de
operatia RAR mapata. op_service_cod="" cand lipseste → randul absent. #}
{% if prez.op_service_cod %}
<div style="margin:0 0 12px;">
<div class="muted" style="font-size:12px;">Operatie service</div>
@@ -160,7 +159,7 @@
{{ camp('odometru_initial', 'Odometru initial (daca e cerut)', form_odo_initial) }}
</div>
{# === R10 (5): actiune primara conditionata de stare (R2). needs_data/needs_mapping
{# === Actiune primara conditionata de stare. needs_data/needs_mapping
-> „Salveaza si retrimite" pe /corecteaza. UN SINGUR buton primar per stare. === #}
<div style="margin-top:14px;">
<button type="submit">Salveaza si retrimite</button>
@@ -177,8 +176,8 @@
<div style="word-break:break-all;">{{ prez.vin }}</div>
</div>
<div><div class="muted" style="font-size:12px;">Operatie</div><div>{{ prez.operatie }} &middot; {{ cod_afis }}</div></div>
{# US-007: operatie service (cod intern + denumire), distinct de operatia RAR.
Conventie US-002: op_service_cod="" cand lipseste → randul absent (fara "—"). #}
{# Operatie service (cod intern + denumire), distinct de operatia RAR.
op_service_cod="" cand lipseste → randul absent (fara "—"). #}
{% if prez.op_service_cod %}
<div><div class="muted" style="font-size:12px;">Operatie service</div>
<div>{{ prez.op_service_cod }}{% if prez.op_service_denumire %} — {{ prez.op_service_denumire }}{% endif %}</div></div>
@@ -188,17 +187,17 @@
</div>
{% endif %}
{# === R10 (5): actiuni de jos — primar Re-pune (doar error) + Sterge pe RAND SEPARAT (R2/R11) === #}
{# === Actiuni de jos — primar Re-pune (doar error) + Sterge pe RAND SEPARAT === #}
{% if status == 'error' or gestionabil %}
<div class="detaliu-actiuni-jos" style="margin-top:14px; padding-top:12px; border-top:1px solid var(--line);">
{# R2: error -> buton primar „Re-pune in coada" pe /repune (error nu e editabil pentru #}
{# campuri vehicul, dar US-006b permite schimbarea cod_prestatie prin acelasi formular). #}
{# Error -> buton primar „Re-pune in coada" pe /repune (error nu e editabil pentru #}
{# campuri vehicul, dar se poate schimba cod_prestatie prin acelasi formular). #}
{% if status == 'error' %}
<form hx-post="/trimitere/{{ id }}/repune"
hx-target="#detaliu-modal-body" hx-swap="innerHTML"
hx-disabled-elt="find button" style="margin:0 0 10px;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
{# US-006b: select cod_prestatie optional in formularul /repune (doar pentru error). #}
{# Select cod_prestatie optional in formularul /repune (doar pentru error). #}
{% if nomenclator_rar %}
<label for="cod-rar-error-{{ id }}" style="display:block; font-size:12px; color:var(--muted); margin-bottom:4px;">
Operatie RAR (optional — schimba codul si re-pune)
@@ -219,7 +218,7 @@
</form>
{% endif %}
{# R11: UN SINGUR Sterge, outline distructiv (var(--err)), pe rand separat, full-width pe mobil. #}
{# UN SINGUR Sterge, outline distructiv (var(--err)), pe rand separat, full-width pe mobil. #}
{% if gestionabil %}
<form hx-post="/trimitere/{{ id }}/sterge"
hx-target="#detaliu-modal-body" hx-swap="innerHTML"
@@ -235,7 +234,7 @@
</div>
{% endif %}
{# === R10 (6): Detalii tehnice — colapsat implicit === #}
{# === Detalii tehnice — colapsat implicit === #}
<details style="margin-top:14px;">
<summary class="muted" style="font-size:12px; cursor:pointer;">Detalii tehnice</summary>
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:10px 24px; margin-top:10px;">
@@ -257,7 +256,6 @@
{% endif %}
</details>
</div>
{# PRD 5.9 US-004 (R4): scriptul inline vechi (marcheazaDetaliuDeschis / scrollIntoView pe
randul-sibling) a fost eliminat de US-003. Focus-ul post-swap (incl. re-render corectie/
mapare) e gestionat de htmx:afterSettle pe #detaliu-modal-body din base.html. R5: inchiderea
modalului pe succes (queued/sterge) vine din HX-Trigger `inchideModal` emis de rute. #}
{# Focus-ul post-swap (incl. re-render corectie/mapare) e gestionat de htmx:afterSettle pe
#detaliu-modal-body din base.html. Inchiderea modalului pe succes (queued/sterge) vine
din HX-Trigger `inchideModal` emis de rute. #}

View File

@@ -1,7 +1,7 @@
<div id="import-section">
{% set pas = 1 %}{% include '_stepper.html' %}
{# US-004 (3.6): bara de upload accentuata (border de accent) ca sa ramana punctul
de intrare evident chiar cu tabelul Trimiteri lung dedesubt (D-1.1/D-5.2). #}
{# Bara de upload accentuata (border de accent) ca sa ramana punctul
de intrare evident chiar cu tabelul Trimiteri lung dedesubt. #}
{% from '_eroare.html' import card_erori %}
<div class="card" style="border-color:var(--accent);">
@@ -105,7 +105,7 @@
var dz = document.getElementById('drop-zone');
var frm = document.getElementById('upload-form');
/* US-003 (3.6): un singur sticky bar pe ecran — cand re-apare zona de upload
/* Un singur sticky bar pe ecran — cand re-apare zona de upload
(reset sau dupa commit), sectiunea Trimiteri redevine vizibila. */
var trim = document.getElementById('trimiteri-section');
if (trim) trim.style.display = '';

View File

@@ -2,7 +2,7 @@
{% block title %}Conturi clienti — Gateway RAR AUTOPASS{% endblock %}
{% block content %}
{# US-009 (5.5): metadate verbe de ciclu de viata (eticheta, ruta, clasa). #}
{# Metadate verbe de ciclu de viata (eticheta, ruta, clasa). #}
{% set VERBS = {
'activate': ('Activeaza', '/admin/activate', ''),
'block': ('Blocheaza', '/admin/block', ''),

View File

@@ -6,7 +6,7 @@
<title>{% block title %}Gateway RAR AUTOPASS{% endblock %}</title>
<script src="/static/htmx.min.js"></script>
<script>
// US-002 (3.6): raspunsurile de editare-rand contin un <tr> (swap pe rand) PLUS
// Raspunsurile de editare-rand contin un <tr> (swap pe rand) PLUS
// elemente OOB non-rand (#preview-rezumat, #preview-ok-count). Fara fragmente-template,
// htmx parseaza raspunsul care incepe cu <tr> in context de tabel (<table><tbody>) si
// "foster-parent"-eaza div/span-urile OOB afara din fragment -> swapError + contoare pierdute.
@@ -14,8 +14,8 @@
htmx.config.useTemplateFragments = true;
</script>
<script>
// Anti-FOUC (US-001 PRD 5.3, extins US-014 PRD 5.10): citeste preferinta tema din
// localStorage inainte de primul paint; seteaza data-theme pe <html> sincron, fara blink.
// Anti-FOUC: citeste preferinta tema din localStorage inainte de primul
// paint; seteaza data-theme pe <html> sincron, fara blink.
// Cunoaste toate cele 4 teme: light/dark/petrol/auto. Valoare legacy/necunoscuta -> auto.
// 'auto' se rezolva la 'light' sau 'dark' dupa prefers-color-scheme (fara blink).
(function() {
@@ -33,12 +33,9 @@
})();
</script>
<style>
/* US-013 (PRD 5.10): IBM Plex Sans + Mono self-hosted (latin-ext pentru diacritice romanesti).
font-display:swap permite text vizibil inainte de incarcare (FOUT system-ui->IBM Plex).
FOUT pe tabular-nums: IBM Plex Sans are metrici apropiate de system-ui; reflow-ul vizibil
pe VIN/coduri e acceptat explicit — fontul se incarca din /static/ (acelasi origin).
IBM Plex Sans/Mono self-host, subset latin + latin-ext de pe fontsource
(@fontsource/ibm-plex-sans + @fontsource/ibm-plex-mono, v5.0.8), woff2 valide. */
/* IBM Plex Sans + Mono self-hosted (latin-ext pentru diacritice romanesti).
font-display:swap permite text vizibil inainte de incarcare (FOUT system-ui->IBM Plex);
reflow-ul vizibil pe VIN/coduri e acceptat explicit. */
@font-face {
font-family: "IBM Plex Sans";
font-style: normal;
@@ -103,38 +100,32 @@
src: url("/static/fonts/IBMPlexMono-Regular-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+20AC, U+2122, U+FEFF, U+FFFD;
}
/* Paleta dark (default) — accent azur ROMFAST conform DESIGN.md */
/* Paleta dark (default) — accent azur ROMFAST */
:root { --bg:#0f1218; --card:#181c24; --ink:#e6e9ef; --muted:#8b93a7; --line:#262b36;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#2E74D6; }
/* Paleta light — accent azur inchis pentru contrast AA pe alb (#1F66C9: 5.51:1 pe alb) */
[data-theme="light"] { --bg:#f5f7fa; --card:#ffffff; --ink:#1a1d24; --muted:#5c6473; --line:#e2e5ea;
--ok:#15803d; --warn:#b45309; --err:#dc2626; --accent:#1F66C9; }
/* Paleta Petrol (US-014) — tema intunecata alternativa, accent teal #0E7C7B.
/* Paleta Petrol — tema intunecata alternativa, accent teal #0E7C7B.
Wordmark-ul FAST #2E74D6 coexista armonios: ambele sunt reci/saturate, contrast AA pe --card #161e20. */
[data-theme="petrol"] { --bg:#0e1416; --card:#161e20; --ink:#e6e9ef; --muted:#8b93a7; --line:#232c2e;
--ok:#2FBF8F; --warn:#E0A93B; --err:#E05D5D; --accent:#0E7C7B; }
* { box-sizing:border-box; }
/* PRD 5.9 US-006 — CONVENTIE BREAKPOINT: un singur prag mobil la 768px.
/* CONVENTIE BREAKPOINT: un singur prag mobil la 768px.
CSS custom properties NU functioneaza in `@media`, deci pragul nu poate fi o
variabila; folosim consecvent `@media (max-width:767px)` peste tot (mobil) si
`@media (max-width:1024px)` doar pentru densitatea tabelului. >=1024px = layout
desktop neschimbat (fara regresie). Orice regula mobila noua reutilizeaza 767px. */
body { margin:0; font:15px/1.5 "IBM Plex Sans",system-ui,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
background:var(--bg); color:var(--ink); -webkit-font-smoothing:antialiased; }
/* US-012c (PRD 5.10): grila 3 coloane — stanga (logo ROMFAST) | centru (titlu+env) | dreapta (controale). */
/* Grila 3 coloane — stanga (logo ROMFAST) | centru (titlu+env) | dreapta (controale). */
header { padding:16px 24px; border-bottom:1px solid var(--line);
display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:8px; min-height:92px; }
.header-left { display:flex; align-items:center; }
.header-center { display:flex; flex-direction:column; align-items:center; text-align:center; }
.header-right { display:flex; align-items:center; justify-content:flex-end; gap:8px; }
/* US-012c: logo PNG ROMFAST in header-left (brand top-left ca pe romfast.ro).
32px inaltime — usor mai mare decat in header-center (28px) pentru vizibilitate ca brand anchor.
margin:0 — aliniat stanga, NU centrat (era `margin:3px auto 0` cand era sub titlu).
Logo transparent: ok pe dark/light/petrol fara filtre de culoare. */
/* Logo ROMFAST la dimensiunea de pe romfast.ro (~60px inaltime), aliniat stanga. */
/* Logo ROMFAST aliniat stanga; transparent, ok pe dark/light/petrol fara filtre de culoare. */
.brand-logo { height:60px; width:auto; display:block; margin:0; }
/* Env badge mic sub titlu in header-center (US-012c): nu mai echilibreaza optic dreapta
(logo-ul face asta), ci identifica mediul langa titlu. Pastrat mic, color:var(--muted). */
.header-center .env { font-size:11px; margin-top:2px; }
header h1 { font-size:20px; margin:0; font-weight:700; letter-spacing:-.01em; }
header .env { font-size:12px; color:var(--muted); border:1px solid var(--line); padding:2px 8px; border-radius:99px; }
@@ -210,7 +201,7 @@
button { background:var(--accent); border-color:var(--accent); color:#fff; cursor:pointer; }
button:hover { filter:brightness(1.08); }
.chk { font-size:13px; color:var(--muted); display:flex; align-items:center; gap:6px; }
/* Tab-bar (US-003) */
/* Tab-bar */
.tab-bar { display:flex; gap:2px; overflow-x:auto; -webkit-overflow-scrolling:touch;
border-bottom:1px solid var(--line); margin-bottom:16px; padding-bottom:0;
scrollbar-width:none; }
@@ -224,7 +215,7 @@
border-color:var(--line); border-bottom-color:var(--card); }
.tab-panel { min-height:120px; }
.status-bar { margin-bottom:12px; }
/* Eroare 3 niveluri (US-006, PRD 5.4) */
/* Eroare 3 niveluri */
.eroare-3n { margin-top:10px; }
.eroare-3n-item { padding:8px 10px; border-left:3px solid var(--err);
background:color-mix(in srgb, var(--err) 8%, var(--card));
@@ -237,13 +228,13 @@
.eroare-3n-label { font-weight:500; }
/* Inline fix per camp in preview */
.camp-fix { color:var(--accent); font-size:11px; margin-top:2px; display:block; }
/* Meniu hamburger cont (US-006 PRD 5.5) — dropdown ancorat dreapta-sus */
/* Meniu hamburger cont — dropdown ancorat dreapta-sus */
.cont-menu-wrap { position:relative; }
.icon-btn { background:transparent; border:1px solid var(--line); color:var(--ink); cursor:pointer;
border-radius:6px; min-height:36px; min-width:36px; font-size:16px; padding:4px 8px;
line-height:1; display:inline-flex; align-items:center; justify-content:center; }
.icon-btn:hover { background:var(--line); }
/* US-011: variante icon-btn — dirty (modificari nesalvate) + danger (destructiv) */
/* Variante icon-btn — dirty (modificari nesalvate) + danger (destructiv) */
.icon-btn.dirty { background:var(--accent); color:#fff; border-color:var(--accent); }
.icon-btn.dirty:hover { filter:brightness(0.9); }
.icon-btn.danger { color:var(--err); border-color:var(--err); }
@@ -260,7 +251,7 @@
.cont-menu form { margin:0; }
/* Kebab partajat (actiuni per-rand in tabele). Meniul e position:fixed si pozitionat de JS:
altfel `.tablewrap { overflow-x:auto }` induce overflow-y:auto si TAIE dropdown-ul pe ultimul
rand (bug 5.5 — meniul nu se vedea). fixed scoate meniul din contextul de clipping al tabelului. */
rand. fixed scoate meniul din contextul de clipping al tabelului. */
.kebab { position:relative; display:inline-block; }
.kebab > summary { list-style:none; cursor:pointer; display:inline-flex; align-items:center;
justify-content:center; min-height:32px; min-width:32px; padding:4px 10px;
@@ -288,7 +279,7 @@
.dt-pager button { background:transparent; color:var(--ink); border:1px solid var(--line);
padding:5px 12px; min-height:32px; }
.dt-pager button:disabled { opacity:.45; cursor:default; }
/* === Tabel trimiteri (PRD 5.8 US-007): fara scroll orizontal. SCOPAT prin
/* === Tabel trimiteri: fara scroll orizontal. SCOPAT prin
.tabel-trimiteri ca sa NU strice celelalte tabele (.tablewrap e partajat de
Mapari/Formate). Permitem wrap controlat pe coloanele text + latimi rezonabile. === */
.tabel-trimiteri table { table-layout:fixed; }
@@ -302,16 +293,15 @@
.tabel-trimiteri .col-operatie > div { line-height:1.35; }
/* secundarul muted („cod RAR" / „nemapat") — >=12px, contrast pe var(--muted) >=4.5:1 */
.tabel-trimiteri .cod-rar-sub { font-size:12px; margin-top:2px; }
/* PRD 5.9 US-002: codul RAR pe linia 2 — chip discret, fara prefixul „cod RAR:". */
/* Codul RAR pe linia 2 — chip discret, fara prefixul „cod RAR:". */
.tabel-trimiteri .cod-rar-cod { display:inline-block; font-family:"IBM Plex Mono",ui-monospace,monospace;
font-size:12px; padding:1px 7px; border:1px solid var(--line);
border-radius:99px; color:var(--muted); }
/* PRD 5.9 US-002 (R1): eticheta umana scurta sub pill — text mic; clasa `s-error`
o coloreaza (apare doar pe error/needs_*). Stare prin text, nu doar culoare. */
/* Eticheta umana scurta sub pill — text mic; clasa `s-error` o coloreaza
(apare doar pe error/needs_*). Stare prin text, nu doar culoare. */
.tabel-trimiteri .eticheta-problema { font-size:12px; line-height:1.3; margin-top:3px; }
/* PRD 5.9 US-002 (R8): randul e clickabil (deschide modalul) -> tinta de atins >=44px
(touch) + afordanta hover/focus. Inlocuieste vechea regula `@media pointer:coarse
.chevron` (chevron eliminat); este SINGURA regula 44px pe rand. */
/* Randul e clickabil (deschide modalul) -> tinta de atins >=44px (touch) +
afordanta hover/focus. */
.tabel-trimiteri tr.trimitere-row { min-height:44px; }
.tabel-trimiteri tr.trimitere-row > td { padding-top:11px; padding-bottom:11px; }
.tabel-trimiteri tr.trimitere-row:hover { background:color-mix(in srgb, var(--accent) 6%, transparent); }
@@ -321,10 +311,10 @@
@media (max-width:1024px) {
.tabel-trimiteri .col-actualizat { display:none; }
}
/* === Modal detaliu (PRD 5.9 US-003): fereastra modala globala, in afara zonei de
poll (#submissions-wrap). Backdrop + dialog centrat pe desktop; focus-trap +
/* === 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
`@media (max-width:767px)` US-006 de mai jos. === */
`@media (max-width:767px)` de mai jos. === */
.modal-overlay { position:fixed; inset:0; z-index:1100; display:flex;
align-items:flex-start; justify-content:center; padding:40px 16px; overflow-y:auto; }
.modal-overlay[hidden] { display:none; }
@@ -341,9 +331,9 @@
body.modal-open { overflow:hidden; }
.modal-eroare { padding:16px 4px; }
.modal-eroare .actiuni { margin-top:12px; display:flex; gap:10px; flex-wrap:wrap; }
/* === PRD 5.9 US-006: fundatie responsive mobil (<768px) ===
/* === Fundatie responsive mobil (<768px) ===
Breakpoint unic 767px (vezi conventia de sus). Cuprinde: card per rand pe tabelul
de trimiteri (5.8, pastrat), modal full-screen, header/nav colapsat cu tinte touch
de trimiteri, modal full-screen, header/nav colapsat cu tinte touch
>=44px. Desktop (>=1024px) ramane neschimbat — regulile de baza nu se modifica. */
@media (max-width:767px) {
/* Tabel trimiteri: card per rand (eticheta:valoare stivuit) -> fara scroll orizontal */
@@ -366,7 +356,7 @@
padding:16px; padding-top:56px; overflow-y:auto; }
.modal-close { width:44px; height:44px; top:8px; right:8px; font-size:24px; }
/* US-004 (R11): actiunile de jos din detaliu (Re-pune / Sterge) full-width stivuit pe mobil. */
/* Actiunile de jos din detaliu (Re-pune / Sterge) full-width stivuit pe mobil. */
.detaliu-actiuni-jos button { width:100%; }
/* Header + nav colapsate: pe mobil trece de la grid la flex wrap.
@@ -383,9 +373,9 @@
.tab-link { min-height:44px; padding:10px 14px; }
.cont-menu a, .cont-menu button { min-height:44px; }
/* === PRD 5.9 US-007 (R12): paginile de continut pe mobil ===
/* === Paginile de continut pe mobil ===
Tabele ACTIONABILE (Mapari) -> card per rand. Clasa proprie `.tabel-card`,
scopata SEPARAT de `.tabel-trimiteri` (5.8) ca sa NU strice cardurile de
scopata SEPARAT de `.tabel-trimiteri` ca sa NU strice cardurile de
trimiteri. Tabele DENSE read-only (Jurnal, Nomenclator) + Admin raman in
`.tablewrap` (scroll orizontal CONTAINED, definit global mai sus). */
.tabel-card table { table-layout:auto; }
@@ -417,11 +407,11 @@
#card-cont button, #form-test-cheie button,
#jurnal-section #filtre-jurnal button { min-height:44px; width:100%; }
/* === PRD 5.9 US-008: Acasa (upload, status, filtre) + login/signup pe mobil ===
/* === Acasa (upload, status, filtre) + login/signup pe mobil ===
Zona de upload, bara de status si bara de filtre (`_coada.html`) stiveaza pe O
coloana sub 767px; inputuri/butoane full-width cu tinta touch >=44px. Scopat pe
id-urile sectiunilor de pe Acasa ca sa NU atinga tabelul de trimiteri (5.8),
modalul sau paginile de continut (US-007). */
id-urile sectiunilor de pe Acasa ca sa NU atinga tabelul de trimiteri,
modalul sau paginile de continut. */
/* Bara de upload: zona slim (returning user) trece pe coloana; butonul full-width. */
#import-section .drop-zone { flex-direction:column; align-items:stretch; text-align:left; }
#import-section #upload-btn { width:100%; min-height:44px; }
@@ -441,16 +431,14 @@
</style>
</head>
<body>
{# US-012c (PRD 5.10): grila 3 coloane — stanga (logo ROMFAST) | centru (titlu+env) | dreapta (controale).
Decizie env badge: mutat in header-center sub <h1> (mic, color:muted) — nu suprapune logo-ul
si pastreaza centrarea optica a titlului in coloana auto. #}
{# Grila 3 coloane — stanga (logo ROMFAST) | centru (titlu+env) | dreapta (controale). #}
<header>
{# Celula stanga: logo ROMFAST (US-012c: brand top-left ca pe romfast.ro) #}
{# Celula stanga: logo ROMFAST #}
<div class="header-left">
{# US-012b/c: logo PNG real, 288x175 RGBA transparent — ok pe toate temele fara filtre. #}
{# Logo PNG real, RGBA transparent — ok pe toate temele fara filtre. #}
<img src="/static/romfast_logo.png" alt="ROMFAST" class="brand-logo">
</div>
{# Celula centru: titlu + badge env mic (US-012c: env mutat din header-left aici) #}
{# Celula centru: titlu + badge env mic #}
<div class="header-center">
<h1>Gateway RAR AUTOPASS</h1>
<span class="env">{{ rar_env }}</span>
@@ -462,14 +450,14 @@
title="Comuta tema">&#9728;</button>
<span class="muted" style="font-size:13px;">v{{ version }}</span>
{% if is_authenticated|default(false) %}
{# Meniu cont (US-006 PRD 5.5): Cont/Integrare/Nomenclator + (admin) + logout.
{# Meniu cont: Cont/Integrare/Nomenclator + (admin) + logout.
Pe paginile neautentificate (login/signup) nu se randeaza deloc. #}
<div class="cont-menu-wrap">
<button id="cont-menu-toggle" class="icon-btn"
aria-haspopup="true" aria-expanded="false" aria-controls="cont-menu"
aria-label="Meniu cont" title="Meniu cont">&#9776;</button>
<div id="cont-menu" class="cont-menu" role="menu" aria-labelledby="cont-menu-toggle" hidden>
{# US-009 (PRD 5.10): Mapari mutat din tab-bar in meniu, cu badge needs_mapping. #}
{# 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>
<hr>
@@ -488,14 +476,14 @@
{% endif %}
</div>
</header>
{# aria-live pentru anuntarea schimbarilor de tema (US-014, accesibilitate) #}
{# aria-live pentru anuntarea schimbarilor de tema (accesibilitate) #}
<span id="tema-live" role="status" aria-live="polite"
style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;"></span>
<main>{% block content %}{% endblock %}</main>
{# Modal detaliu trimitere (PRD 5.9 US-003): container global, SIBLING al <main>
(nu descendent), ca `inert`+`aria-hidden` pe <main> sa nu-l prinda si pe el (R7).
Corpul #detaliu-modal-body e tinta de swap pentru fragment + rutele corectie/
mapare/lifecycle. Traieste in afara #submissions-wrap -> poll-ul de 15s nu-l atinge. #}
{# Modal detaliu trimitere: container global, SIBLING al <main> (nu descendent),
ca `inert`+`aria-hidden` pe <main> sa nu-l prinda si pe el. Corpul
#detaliu-modal-body e tinta de swap pentru fragment + rutele corectie/mapare/
lifecycle. Traieste in afara #submissions-wrap -> poll-ul nu-l atinge. #}
<div id="modal-detaliu" class="modal-overlay" role="dialog" aria-modal="true"
aria-labelledby="detaliu-modal-titlu" hidden>
<div class="modal-backdrop" data-modal-close></div>
@@ -505,7 +493,7 @@
</div>
</div>
<script>
// Comutator tema ciclic (US-014 PRD 5.10): click cicleaza Light->Dark->Petrol->Auto.
// Comutator tema ciclic: click cicleaza Light->Dark->Petrol->Auto.
// Separare init (sincronizare iconita/label) de persistenta (doar la click explicit).
// 'auto' se rezolva la paint prin anti-FOUC; aici setam data-theme rezolvat.
(function() {
@@ -528,7 +516,7 @@
var s = VALID[stored] ? stored : 'auto';
btn.innerHTML = ICONS[s];
btn.setAttribute('aria-label', 'Tema: ' + LABELS[s] + ', apasa pentru ' + NEXT[s]);
btn.title = LABELS[s]; // US-014b: doar numele temei (ex. "Petrol"), nu ciclul intreg
btn.title = LABELS[s]; // doar numele temei (ex. "Petrol"), nu ciclul intreg
}
function _setTheme(t) {
document.documentElement.setAttribute('data-theme', _resolved(t));
@@ -547,7 +535,7 @@
})();
</script>
<script>
// Meniu cont (US-006 PRD 5.5): dropdown ancorat dreapta-sus. Deschide/inchide la click,
// Meniu cont: dropdown ancorat dreapta-sus. Deschide/inchide la click,
// inchide la Esc (focus readus pe buton) si la click in afara. Fara dependente.
(function() {
var toggle = document.getElementById('cont-menu-toggle');
@@ -624,7 +612,7 @@
})();
</script>
<script>
// US-011: dirty state pentru butoanele de salvare din tabelele de mapari.
// Dirty state pentru butoanele de salvare din tabelele de mapari.
// Cand utilizatorul schimba un select dintr-un form de mapare, butonul de salvare
// legat prin data-dirty-form devine evidentiat (clasa "dirty" → fundal --accent).
// Starea "dirty" e efemera per-render: un swap outerHTML o reseteaza automat.
@@ -700,11 +688,11 @@
})();
</script>
<script>
// Modal detaliu trimitere (PRD 5.9 US-003): inlocuieste detaliul inline (5.8). Detaliul
// se incarca prin HTMX in #detaliu-modal-body (in afara #submissions-wrap, deci poll-ul
// de 15s nu-l atinge). Aici: deschidere la click pe rand, inchidere (x/Esc/backdrop),
// focus-trap, scroll-lock, inert+aria-hidden pe <main> (R7), stare de eroare la load
// esuat (R5), inchidere pe succes corectie/sterge (HX-Trigger inchideModal, R5).
// Modal detaliu trimitere: detaliul se incarca prin HTMX in #detaliu-modal-body
// (in afara #submissions-wrap, deci poll-ul nu-l atinge). Aici: deschidere la click
// pe rand, inchidere (x/Esc/backdrop), focus-trap, scroll-lock, inert+aria-hidden pe
// <main>, stare de eroare la load esuat, inchidere pe succes corectie/sterge
// (HX-Trigger inchideModal).
(function() {
var overlay = document.getElementById('modal-detaliu');
if (!overlay) return;
@@ -721,7 +709,7 @@
' select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'),
function(el) { return el.offsetParent !== null || el === document.activeElement; });
}
// R7: focus-trap — Tab/Shift+Tab cicleaza in interiorul dialogului.
// focus-trap — Tab/Shift+Tab cicleaza in interiorul dialogului.
function trapFocus(e) {
if (e.key !== 'Tab') return;
var f = focusable();
@@ -755,7 +743,6 @@
if (t && t.focus) t.focus(); // focus readus pe rand
}
// API public: butonul „Inchide" din fragment + inchiderea pe succes corectie/sterge.
// (Semnatura veche inchideDetaliu(id) pastrata, dar exista un singur modal o data.)
window.inchideDetaliu = function() { close(); };
// Inchidere: x si backdrop (elemente cu data-modal-close), Esc.
@@ -778,7 +765,7 @@
var f = focusable();
if (f.length) f[0].focus();
});
// R5: load-error al fragmentului (GET esuat) -> stare Reincearca/Inchide, nu placeholder blocat.
// Load-error al fragmentului (GET esuat) -> stare Reincearca/Inchide, nu placeholder blocat.
body.addEventListener('htmx:responseError', function(evt) {
if (!isOpen()) return;
var elt = evt.detail && evt.detail.elt;
@@ -796,7 +783,7 @@
{ target: body, swap: 'innerHTML' });
});
// R5: inchidere pe succes corectie/sterge — ruta emite HX-Trigger `inchideModal`.
// Inchidere pe succes corectie/sterge — ruta emite HX-Trigger `inchideModal`.
// Lista se reincarca separat prin `trimiteriChanged` (#submissions-wrap). Maparea
// inline NU emite inchideModal -> modalul ramane deschis sa arate codul rezolvat.
document.body.addEventListener('inchideModal', function() { close(); });

View File

@@ -1,11 +1,6 @@
{% extends "base.html" %}
{% block content %}
{# US-009 (PRD 5.10): tab-bar-ul Acasa/Mapari a fost eliminat. Mapari s-a mutat in meniul
hamburger (#cont-menu in base.html). Acasa e continutul principal direct — nicio schela ARIA
role="tablist"/"tab"/"tabpanel" orfana. Rutele /_fragments/* si deep-link-urile ?tab=
raman valide (navigare prin meniu → full page reload). #}
<!-- Bara de status: mereu vizibila -->
<div id="status-bar" class="status-bar card"
hx-get="/_fragments/status"