feat(5.8): reguli mapare pe text (substring/cont) + UX tabel trimiteri (detaliu inline, fara scroll, cod RAR)
Reguli text per cont (operation_text_rules), resolve_prestatii cu param aditiv text_rules + precedenta stricta, threadat pe toate cele 6 callsite-uri + valid_codes + seam classify_prezentare. UI Mapari: sectiune reguli + preview pre-salvare + overlap + telemetrie text_rule_hit. UX tabel: cod_rar sub operatie, pill eticheta scurta, fara scroll orizontal (scopat .tabel-trimiteri + carduri <768px), detaliu inline expandabil (a11y + pauza poll). code-review: reparat regula auto_send=0 care trimitea automat la RAR in loc sa tina randul pentru review. 814 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -58,6 +58,38 @@ STARI_SUBMISSION: dict[str, Eticheta] = {
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Etichete scurte (pill) pentru coloana Stare din tabelul de trimiteri (US-006)
|
||||
# Dict propriu — NU element in tuple Eticheta (ar rupe template-urile care
|
||||
# despacheteaza 3 elemente). eticheta_stare ramane neatinsa.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ETICHETE_SCURTE: dict[str, str] = {
|
||||
"queued": "In coada",
|
||||
"sending": "Se trimite",
|
||||
"sent": "Finalizat",
|
||||
"needs_mapping": "De mapat",
|
||||
"needs_data": "Date lipsa",
|
||||
"error": "Eroare",
|
||||
}
|
||||
|
||||
|
||||
def eticheta_scurta(status: str) -> str:
|
||||
"""
|
||||
Returneaza eticheta compacta (pill) pentru o stare de submission.
|
||||
|
||||
Arunca KeyError daca starea nu este mapata — intentionat, ca sa prinda
|
||||
stari noi adaugate in schema fara mapare corespunzatoare.
|
||||
"""
|
||||
try:
|
||||
return ETICHETE_SCURTE[status]
|
||||
except KeyError:
|
||||
raise KeyError(
|
||||
f"Starea de submission {status!r} nu are eticheta scurta in labels.py. "
|
||||
"Adauga-o in ETICHETE_SCURTE."
|
||||
)
|
||||
|
||||
|
||||
def eticheta_stare(status: str) -> Eticheta:
|
||||
"""
|
||||
Returneaza (text, subtext, css_class) pentru o stare de submission.
|
||||
|
||||
@@ -30,6 +30,7 @@ from ..web.csrf import get_csrf_token, verify_csrf
|
||||
from .labels import (
|
||||
ETICHETA_ULTIMA_AUTENTIFICARE_RAR,
|
||||
eticheta_rar,
|
||||
eticheta_scurta,
|
||||
eticheta_stare,
|
||||
eticheta_worker,
|
||||
format_data_rar,
|
||||
@@ -62,16 +63,23 @@ from ..submissions_admin import (
|
||||
)
|
||||
from ..mapping import (
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
_emite_text_rule_hits,
|
||||
account_or_default,
|
||||
account_scope_clause,
|
||||
delete_text_rule,
|
||||
has_no_auto_send,
|
||||
load_mapping_meta,
|
||||
load_nomenclator,
|
||||
load_nomenclator_codes,
|
||||
load_text_rules,
|
||||
normalize_for_match,
|
||||
pending_unmapped,
|
||||
reresolve_account,
|
||||
resolve_prestatii,
|
||||
save_mapping,
|
||||
save_text_rule,
|
||||
suggest_codes,
|
||||
text_rules_overlap,
|
||||
)
|
||||
|
||||
# Campuri canonice cu eticheta umana pentru dropdown mapare coloane (U5)
|
||||
@@ -228,6 +236,7 @@ def _render_panel_mapari(request: Request, conn, account_id: int) -> str:
|
||||
"pending": pending_unmapped(conn, account_id),
|
||||
"saved_mappings": _load_saved_op_mappings(conn, account_id),
|
||||
"column_formats": _load_column_formats(conn, account_id),
|
||||
"text_rules": load_text_rules(conn, account_id),
|
||||
"nomenclator": load_nomenclator(conn),
|
||||
"message": None,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
@@ -646,6 +655,8 @@ def _submission_row_view(r) -> dict:
|
||||
return {
|
||||
"id": r["id"],
|
||||
"status": r["status"],
|
||||
# PRD 5.8 US-007/US-006: pill = eticheta scurta; textul lung ramane ca tooltip (title=).
|
||||
"stare_scurt": eticheta_scurta(r["status"]),
|
||||
"stare_text": eticheta[0],
|
||||
"stare_css": eticheta[2],
|
||||
"prez": prezentare_din_payload(r["payload_json"]),
|
||||
@@ -981,9 +992,14 @@ async def post_corectie_trimitere(request: Request, submission_id: int) -> HTMLR
|
||||
# la RAR. Corectia campurilor de continut nu poate deebloca o operatie nemapata.
|
||||
mapping_meta = load_mapping_meta(conn, account_id)
|
||||
mapping = {op: m["cod_prestatie"] for op, m in mapping_meta.items()}
|
||||
resolved, unmapped = resolve_prestatii(content.get("prestatii"), mapping)
|
||||
valid_codes = load_nomenclator_codes(conn) or None
|
||||
text_rules = load_text_rules(conn, account_id)
|
||||
resolved, unmapped = resolve_prestatii(content.get("prestatii"), mapping, valid_codes, text_rules)
|
||||
content["prestatii"] = resolved
|
||||
|
||||
# US-010: telemetrie pentru itemii rezolvati prin regula text (calea corectie web).
|
||||
_emite_text_rule_hits(conn, account_id, row["id"], resolved)
|
||||
|
||||
# Canonicalizare (strip ".0" odometru, VIN/nr upper) INAINTE de validare si cheie.
|
||||
canon = canonicalize_row(content)
|
||||
content.update({
|
||||
@@ -1228,6 +1244,7 @@ def _render_mapari(
|
||||
"pending": pending_unmapped(conn, account_id),
|
||||
"saved_mappings": _load_saved_op_mappings(conn, account_id),
|
||||
"column_formats": _load_column_formats(conn, account_id),
|
||||
"text_rules": load_text_rules(conn, account_id),
|
||||
"nomenclator": load_nomenclator(conn),
|
||||
"message": message,
|
||||
"csrf_token": get_csrf_token(request),
|
||||
@@ -1342,6 +1359,138 @@ def post_sterge_mapare_salvata(
|
||||
conn.close()
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-004 (5.8) — Reguli automate (text): substring -> cod RAR #
|
||||
# Adaugare/stergere reguli text scoped pe sesiune; salvarea re-rezolva blocajele.#
|
||||
# =========================================================================== #
|
||||
|
||||
@router.post("/mapari/reguli-text", response_class=HTMLResponse)
|
||||
def post_salveaza_regula_text(
|
||||
request: Request,
|
||||
pattern: str = Form(...),
|
||||
cod_prestatie: str = Form(...),
|
||||
csrf_token: str | None = Form(None),
|
||||
auto_send: bool = Form(False),
|
||||
) -> HTMLResponse:
|
||||
"""Salveaza o regula text (substring -> cod RAR) + re-rezolva submission-urile blocate.
|
||||
|
||||
Scoped pe contul sesiunii (save_text_rule foloseste account_or_default(sesiune)).
|
||||
Valideaza cod_prestatie fata de nomenclator INAINTE de save (cod necunoscut ->
|
||||
respins inline, fara salvare). La succes: mesaj „Regula salvata. Deblocate: N"
|
||||
+ trigger trimiteriChanged (refresh lista), ca maparea inline (5.7).
|
||||
"""
|
||||
account_id = require_login(request)
|
||||
verify_csrf(request, csrf_token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
pat = (pattern or "").strip()
|
||||
cod = (cod_prestatie or "").strip().upper()
|
||||
if not pat or not cod:
|
||||
return _render_mapari(
|
||||
request, conn, account_id,
|
||||
message="Completeaza textul cautat si codul RAR.",
|
||||
)
|
||||
valid_codes = load_nomenclator_codes(conn)
|
||||
if valid_codes and cod not in valid_codes:
|
||||
return _render_mapari(
|
||||
request, conn, account_id,
|
||||
message=f"Cod RAR necunoscut in nomenclator: {cod}.",
|
||||
)
|
||||
# US-011: avertisment neblocant daca regula noua se suprapune (substring,
|
||||
# oricare directie) cu una existenta. Calculam INAINTE de save, fata de
|
||||
# regulile curente, ca pattern-ul nou sa nu se compare cu sine.
|
||||
overlap = text_rules_overlap(pat, load_text_rules(conn, account_id))
|
||||
save_text_rule(conn, account_id, pat, cod, auto_send)
|
||||
stats = reresolve_account(conn, account_id)
|
||||
msg = f"Regula salvata. Deblocate: {stats['requeued']}."
|
||||
if overlap:
|
||||
parti = "; ".join(
|
||||
f"«{r.get('pattern')}» -> {r.get('cod_prestatie')}" for r in overlap
|
||||
)
|
||||
msg += (
|
||||
f" Se suprapune cu regula {parti}; "
|
||||
"ordinea (priority, id) decide care se aplica prima."
|
||||
)
|
||||
resp = _render_mapari(request, conn, account_id, message=msg)
|
||||
resp.headers["HX-Trigger"] = "trimiteriChanged"
|
||||
return resp
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/mapari/reguli-text/sterge", response_class=HTMLResponse)
|
||||
def post_sterge_regula_text(
|
||||
request: Request,
|
||||
pattern: str = Form(...),
|
||||
csrf_token: str | None = Form(None),
|
||||
) -> HTMLResponse:
|
||||
"""Sterge o regula text. Scoped pe contul sesiunii."""
|
||||
account_id = require_login(request)
|
||||
verify_csrf(request, csrf_token)
|
||||
conn = get_connection()
|
||||
try:
|
||||
pat = (pattern or "").strip()
|
||||
delete_text_rule(conn, account_id, pat)
|
||||
return _render_mapari(
|
||||
request, conn, account_id,
|
||||
message=f"Regula stearsa: «{pat}».",
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/mapari/reguli-text/preview", response_class=HTMLResponse)
|
||||
def post_preview_regula_text(
|
||||
request: Request,
|
||||
pattern: str = Form(""),
|
||||
csrf_token: str | None = Form(None),
|
||||
) -> HTMLResponse:
|
||||
"""Preview pre-salvare (US-009): cate operatii nemapate ar potrivi regula.
|
||||
|
||||
NU salveaza nimic (zero scriere DB). Normalizeaza pattern-ul cu
|
||||
normalize_for_match, numara operatiile DISTINCTE nemapate ale contului
|
||||
(needs_mapping, reuse pending_unmapped) al caror text (denumire sau
|
||||
cod_op_service, normalizat) CONTINE pattern-ul + intoarce pana la 3 exemple.
|
||||
Pattern gol -> fragment gol (nu numara „tot"). Scoped pe contul sesiunii.
|
||||
"""
|
||||
from markupsafe import escape
|
||||
|
||||
account_id = require_login(request)
|
||||
verify_csrf(request, csrf_token)
|
||||
pat = normalize_for_match(pattern)
|
||||
if not pat:
|
||||
return HTMLResponse(content="")
|
||||
conn = get_connection()
|
||||
try:
|
||||
pending = pending_unmapped(conn, account_id)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
matches = []
|
||||
for entry in pending:
|
||||
text = normalize_for_match(entry.get("denumire") or entry.get("cod_op_service"))
|
||||
if pat in text:
|
||||
matches.append(entry)
|
||||
|
||||
if not matches:
|
||||
return HTMLResponse(
|
||||
content='<span class="muted" style="font-size:12px;">Nicio potrivire acum.</span>'
|
||||
)
|
||||
|
||||
exemple = ", ".join(
|
||||
f"«{escape((e.get('denumire') or e.get('cod_op_service') or '').strip())}»"
|
||||
for e in matches[:3]
|
||||
)
|
||||
n = len(matches)
|
||||
plural = "operatie nemapata" if n == 1 else "operatii nemapate"
|
||||
return HTMLResponse(
|
||||
content=(
|
||||
f'<span class="muted" style="font-size:12px;">'
|
||||
f'Potriveste {n} {plural}: {exemple}.</span>'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# =========================================================================== #
|
||||
# US-006 — Formate de coloane salvate: editare format data + stergere #
|
||||
# CRUD pe column_mappings scoped pe sesiune (prin id, verificat pe account). #
|
||||
@@ -1510,6 +1659,11 @@ def _web_compute_preview(
|
||||
# Mapare operatii (o singura incarcare — Eng#5)
|
||||
mapping_meta = load_mapping_meta(conn, acct)
|
||||
mapping = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
|
||||
# US-010/US-003 (paritate): preview-ul web trebuie sa aplice ACELEASI reguli text +
|
||||
# validare nomenclator ca si commit-ul (2426), altfel un rand rezolvabil doar prin
|
||||
# regula text ar fi marcat needs_mapping si exclus din commit. Incarcate o data.
|
||||
valid_codes = load_nomenclator_codes(conn) or None
|
||||
text_rules = load_text_rules(conn, acct)
|
||||
|
||||
# Detectie coercion flags din valorile stocate (VIN numeric)
|
||||
coercion_flags_map: dict[int, list[str]] = {}
|
||||
@@ -1553,6 +1707,8 @@ def _web_compute_preview(
|
||||
mapping_meta=mapping_meta,
|
||||
formula_columns=formula_columns,
|
||||
override=overrides[i] or None,
|
||||
valid_codes=valid_codes,
|
||||
text_rules=text_rules,
|
||||
)
|
||||
|
||||
key: str | None = None
|
||||
@@ -2237,6 +2393,9 @@ async def web_confirma_import(
|
||||
# Mapare operatii
|
||||
mapping_meta = load_mapping_meta(conn, acct)
|
||||
mapping_ops = {op: meta["cod_prestatie"] for op, meta in mapping_meta.items()}
|
||||
# T2: validare nomenclator + reguli text incarcate O DATA, inainte de bucla pe randuri.
|
||||
valid_codes = load_nomenclator_codes(conn) or None
|
||||
text_rules = load_text_rules(conn, acct)
|
||||
|
||||
# Enqueue in tranzactie explicita (Issue 6) — INSERT ON CONFLICT DO NOTHING (TOCTOU)
|
||||
enqueued: list[dict] = []
|
||||
@@ -2275,7 +2434,7 @@ async def web_confirma_import(
|
||||
|
||||
# Rezolva prestatii
|
||||
prestatii = mapped.get("prestatii") or []
|
||||
resolved_p, _ = resolve_prestatii(prestatii, mapping_ops)
|
||||
resolved_p, _ = resolve_prestatii(prestatii, mapping_ops, valid_codes, text_rules)
|
||||
mapped["prestatii"] = resolved_p
|
||||
|
||||
# Canonicalizare
|
||||
@@ -2319,6 +2478,8 @@ async def web_confirma_import(
|
||||
if cur.rowcount == 0:
|
||||
toctou.append(row_index)
|
||||
else:
|
||||
# US-010: telemetrie pentru itemii rezolvati prin regula text.
|
||||
_emite_text_rule_hits(conn, acct, int(cur.lastrowid), resolved_p)
|
||||
enqueued.append({"submission_id": cur.lastrowid, "row_index": row_index})
|
||||
|
||||
conn.execute("COMMIT")
|
||||
|
||||
@@ -67,7 +67,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Panou dedicat pentru detaliul trimiterii (NU inline in tabel: poll-ul din tabel
|
||||
ar sterge un expand inline). Gol pana la click pe un rand. -->
|
||||
<div id="trimitere-detaliu"></div>
|
||||
<!-- US-008: detaliul traieste acum INLINE, ca rand-sibling expandabil sub randul
|
||||
selectat (#detaliu-{id} in _submissions.html); poll-ul de 15s se pune pe pauza
|
||||
cat un rand e deschis (base.html). Acest div global e golit de rol (nu mai e
|
||||
tinta de swap), pastrat doar ca ancora inerta. -->
|
||||
<div id="trimitere-detaliu" hidden></div>
|
||||
</section>
|
||||
|
||||
@@ -254,4 +254,100 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- Sectiunea 4: Reguli automate pe text (operation_text_rules) -->
|
||||
<!-- ============================================================ -->
|
||||
<div class="card">
|
||||
<h2 style="font-size:15px; margin:0 0 8px;">Reguli automate (text)</h2>
|
||||
<p class="muted" style="margin:0 0 12px; font-size:13px; max-width:680px;">
|
||||
O regula leaga orice operatie al carei text <strong>contine</strong> (nu egal, ci substring)
|
||||
un cuvant de un cod RAR. Util pentru operatii fara cod intern: ex. orice operatie care
|
||||
<em>contine</em> „verificare" primeste codul ales. Match insensibil la majuscule/diacritice.
|
||||
<strong>In coada</strong>: implicit oprit — regula rezolva codul dar tine randul pentru
|
||||
verificare umana pana activezi „In coada".
|
||||
</p>
|
||||
|
||||
{% if not text_rules %}
|
||||
<div class="empty" style="margin-bottom:12px;">
|
||||
Inca nu ai reguli. Ex: operatia contine «verificare» → OE-2.
|
||||
Mapeaza automat operatii similare fara cod intern. Adauga prima regula mai jos.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tablewrap">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Daca operatia contine</th>
|
||||
<th>Cod RAR</th>
|
||||
<th>In coada</th>
|
||||
<th>Actiuni</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for r in text_rules %}
|
||||
<tr>
|
||||
<td>
|
||||
<form id="rt-del-{{ loop.index }}" hx-post="/mapari/reguli-text/sterge"
|
||||
hx-target="#mapari-section" hx-swap="outerHTML"
|
||||
hx-confirm="Stergi regula «{{ r.pattern }}»?">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||
<input type="hidden" name="pattern" value="{{ r.pattern }}">
|
||||
</form>
|
||||
<div>contine <strong>«{{ r.pattern }}»</strong></div>
|
||||
</td>
|
||||
<td class="muted" style="font-size:12px;">
|
||||
{{ r.cod_prestatie }}
|
||||
</td>
|
||||
<td class="muted" style="font-size:12px;">
|
||||
{% if r.auto_send %}Auto (in coada){% else %}Manual (verificare){% endif %}
|
||||
</td>
|
||||
<td style="text-align:right; white-space:nowrap;">
|
||||
<button type="submit" form="rt-del-{{ loop.index }}"
|
||||
style="background:var(--card); color:var(--err); border-color:var(--err);">
|
||||
Sterge
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{# Rand de adaugare (mereu prezent ca placeholder, inclusiv in empty state). #}
|
||||
<tr>
|
||||
<td>
|
||||
<form id="rt-add" hx-post="/mapari/reguli-text" hx-target="#mapari-section" hx-swap="outerHTML">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token or '' }}">
|
||||
<input type="text" name="pattern" required
|
||||
placeholder="ex. verificare"
|
||||
aria-label="Text continut in operatie"
|
||||
style="width:100%; max-width:240px;"
|
||||
hx-post="/mapari/reguli-text/preview"
|
||||
hx-trigger="keyup delay:400ms"
|
||||
hx-target="#rt-preview"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="#rt-add">
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<select name="cod_prestatie" form="rt-add" required aria-label="Cod RAR pentru regula text">
|
||||
<option value="">— alege cod RAR —</option>
|
||||
{% for n in nomenclator %}
|
||||
<option value="{{ n.cod_prestatie }}">{{ n.cod_prestatie }} — {{ n.nume_prestatie }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
{{ ui.autosend_toggle(form_id="rt-add", checked=False) }}
|
||||
</td>
|
||||
<td style="text-align:right; white-space:nowrap;">
|
||||
<button type="submit" form="rt-add">Adauga</button>
|
||||
</td>
|
||||
</tr>
|
||||
{# Preview pre-salvare (US-009): cate operatii nemapate potriveste pattern-ul. #}
|
||||
<tr>
|
||||
<td colspan="4" style="padding-top:0;">
|
||||
<div id="rt-preview" aria-live="polite"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -15,46 +15,71 @@
|
||||
Sterge selectate
|
||||
</button>
|
||||
</div>
|
||||
<div class="tablewrap">
|
||||
<div class="tablewrap tabel-trimiteri">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="width:28px;"><span class="muted" title="Selecteaza randuri blocate">✓</span></th>
|
||||
<th>#</th>
|
||||
<th>Stare</th>
|
||||
<th>Vehicul</th>
|
||||
<th>Operatie</th>
|
||||
<th>Data prestatie</th>
|
||||
<th>Nr. prezentare RAR</th>
|
||||
<th>Actualizat</th>
|
||||
<th>Motiv</th>
|
||||
<th class="col-chk"><span class="muted" title="Selecteaza randuri blocate">✓</span></th>
|
||||
<th class="col-id">#</th>
|
||||
<th class="col-stare">Stare</th>
|
||||
<th class="col-vehicul">Vehicul</th>
|
||||
<th class="col-operatie">Operatie</th>
|
||||
<th class="col-data">Data prestatie</th>
|
||||
<th class="col-rar">Nr. prezentare RAR</th>
|
||||
<th class="col-actualizat">Actualizat</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
{# US-008: detaliul apare ca rand-sibling expandabil SUB acest rand (#detaliu-{id}),
|
||||
nu in panoul global de la baza. Randul e clickabil/focusabil (toggle prin JS in
|
||||
base.html: single-open + pauza poll). #}
|
||||
<tr id="trimitere-row-{{ r.id }}"
|
||||
class="trimitere-row"
|
||||
data-detaliu-id="{{ r.id }}"
|
||||
hx-get="/_fragments/trimitere/{{ r.id }}"
|
||||
hx-target="#trimitere-detaliu"
|
||||
hx-target="#detaliu-{{ r.id }}"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#ind-{{ r.id }}"
|
||||
role="button" tabindex="0" aria-expanded="false"
|
||||
aria-controls="detaliu-{{ r.id }}"
|
||||
style="cursor:pointer;"
|
||||
title="Click pentru detaliul complet">
|
||||
<td onclick="event.stopPropagation();">
|
||||
<td class="col-chk" onclick="event.stopPropagation();">
|
||||
{% if r.gestionabil %}
|
||||
<input type="checkbox" name="submission_id" value="{{ r.id }}"
|
||||
aria-label="Selecteaza trimiterea #{{ r.id }} pentru stergere">
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="muted">{{ r.id }}</td>
|
||||
<td><span class="pill {{ r.stare_css }}">{{ r.stare_text }}</span></td>
|
||||
<td>
|
||||
<td class="col-id muted" data-eticheta="#">
|
||||
<span class="chevron" aria-hidden="true">▸</span>{{ r.id }}</td>
|
||||
<td class="col-stare" data-eticheta="Stare">
|
||||
<span class="pill {{ r.stare_css }}" title="{{ r.stare_text }}">{{ r.stare_scurt }}</span>
|
||||
</td>
|
||||
<td class="col-vehicul" data-eticheta="Vehicul">
|
||||
{{ r.prez.vehicul_nr }}
|
||||
{% if r.prez.vin_scurt and r.prez.vin_scurt != '—' %}
|
||||
<span class="muted" style="font-size:12px;">{{ r.prez.vin_scurt }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ r.prez.operatie }}</td>
|
||||
<td>{{ r.prez.data_prestatie }}</td>
|
||||
<td>{{ r.id_prezentare or '—' }}</td>
|
||||
<td class="muted">{{ r.updated_at }}</td>
|
||||
<td class="muted" style="white-space:normal; max-width:280px;">{{ r.motiv }}</td>
|
||||
<td class="col-operatie" data-eticheta="Operatie">
|
||||
<div>{{ r.prez.operatie }}</div>
|
||||
{% if r.prez.cod_rar and r.prez.cod_rar != '—' %}
|
||||
<div class="muted cod-rar-sub">cod RAR: {{ r.prez.cod_rar }}</div>
|
||||
{% else %}
|
||||
<div class="muted cod-rar-sub">nemapat</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="col-data" data-eticheta="Data prestatie">{{ r.prez.data_prestatie }}</td>
|
||||
<td class="col-rar" data-eticheta="Nr. prezentare RAR">{{ r.id_prezentare or '—' }}</td>
|
||||
<td class="col-actualizat muted" data-eticheta="Actualizat">{{ r.updated_at }}</td>
|
||||
</tr>
|
||||
{# US-008: rand-sibling de detaliu, ascuns pana la deschidere. Placeholder „Se
|
||||
incarca…" prin hx-indicator cat raspunde HTMX. #}
|
||||
<tr class="detaliu-rand" hidden>
|
||||
<td colspan="8">
|
||||
<span id="ind-{{ r.id }}" class="htmx-indicator muted"
|
||||
style="padding:8px 4px;">Se incarca…</span>
|
||||
<div id="detaliu-{{ r.id }}"></div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{% from "_eroare.html" import card_erori %}
|
||||
{% import '_macros.html' as ui %}
|
||||
<div class="card" id="detaliu-card-{{ id }}" style="border-color:var(--accent);">
|
||||
{# US-008: conectorul detaliului = fundal subtil + border-top pe randul-sibling
|
||||
(.detaliu-rand, base.html), NU border-left accent (evita AI-slop). #}
|
||||
<div class="card" id="detaliu-card-{{ id }}" style="border-color:var(--line);">
|
||||
<div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin:0 0 12px;">
|
||||
<h2 style="font-size:15px; margin:0;">Detaliu trimitere #{{ id }}</h2>
|
||||
<span class="pill {{ stare_css }}">{{ stare_text }}</span>
|
||||
<button type="button" style="margin-left:auto; background:var(--card); color:var(--muted); border-color:var(--line);"
|
||||
onclick="document.getElementById('trimitere-detaliu').innerHTML='';">
|
||||
onclick="window.inchideDetaliu && window.inchideDetaliu('{{ id }}');">
|
||||
Inchide
|
||||
</button>
|
||||
</div>
|
||||
@@ -51,12 +53,12 @@
|
||||
{% if gestionabil %}
|
||||
<div style="margin-top:14px; padding-top:12px; border-top:1px solid var(--line); display:flex; gap:10px; flex-wrap:wrap;">
|
||||
<form hx-post="/trimitere/{{ id }}/repune"
|
||||
hx-target="#trimitere-detaliu" hx-swap="innerHTML" style="margin:0;">
|
||||
hx-target="#detaliu-{{ id }}" hx-swap="innerHTML" style="margin:0;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit">Re-pune in coada</button>
|
||||
</form>
|
||||
<form hx-post="/trimitere/{{ id }}/sterge"
|
||||
hx-target="#trimitere-detaliu" hx-swap="innerHTML"
|
||||
hx-target="#detaliu-{{ id }}" hx-swap="innerHTML"
|
||||
hx-confirm="Stergi definitiv aceasta trimitere din coada?" style="margin:0;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" style="background:var(--card); color:var(--err); border-color:var(--err);">
|
||||
@@ -77,7 +79,7 @@
|
||||
{% for op in nemapate_inline %}
|
||||
{% set top = op.suggestions[0] if op.suggestions else None %}
|
||||
{% set preselect = top.cod_prestatie if (top and top.score >= 60) else '' %}
|
||||
<form hx-post="/trimitere/{{ id }}/mapeaza" hx-target="#trimitere-detaliu" hx-swap="innerHTML"
|
||||
<form hx-post="/trimitere/{{ id }}/mapeaza" hx-target="#detaliu-{{ id }}" hx-swap="innerHTML"
|
||||
style="margin:0 0 12px; padding:10px; border:1px solid var(--line); border-radius:8px;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="cod_op_service" value="{{ op.cod_op_service }}">
|
||||
@@ -126,7 +128,7 @@
|
||||
{% endif %}
|
||||
|
||||
<form hx-post="/trimitere/{{ id }}/corecteaza"
|
||||
hx-target="#trimitere-detaliu" hx-swap="innerHTML">
|
||||
hx-target="#detaliu-{{ id }}" hx-swap="innerHTML">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:10px 16px;">
|
||||
|
||||
@@ -158,13 +160,17 @@
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
/* Vizibilitate (design review): scroll la panou + evidentiaza randul selectat. */
|
||||
var panou = document.getElementById('trimitere-detaliu');
|
||||
if (panou) panou.scrollIntoView({behavior: 'smooth', block: 'nearest'});
|
||||
document.querySelectorAll('tr[id^="trimitere-row-"]').forEach(function(tr) {
|
||||
tr.style.outline = '';
|
||||
});
|
||||
/* US-008: detaliul traieste acum in randul-sibling #detaliu-{id}. Asiguram ca randul
|
||||
de detaliu e vizibil (la re-swap dupa corectie/mapare HTMX poate readuce continut
|
||||
intr-un container ascuns) si ca randul declansator e marcat ca deschis. Single-open
|
||||
+ pauza poll sunt gestionate global in base.html. */
|
||||
var cont = document.getElementById('detaliu-{{ id }}');
|
||||
if (cont) {
|
||||
var detRow = cont.closest('tr.detaliu-rand');
|
||||
if (detRow) detRow.hidden = false;
|
||||
cont.scrollIntoView({behavior: 'smooth', block: 'nearest'});
|
||||
}
|
||||
var rand = document.getElementById('trimitere-row-{{ id }}');
|
||||
if (rand) rand.style.outline = '2px solid var(--accent)';
|
||||
if (rand && window.marcheazaDetaliuDeschis) window.marcheazaDetaliuDeschis(rand);
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -162,6 +162,52 @@
|
||||
.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 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; }
|
||||
.tabel-trimiteri th, .tabel-trimiteri td { white-space:normal; word-break:break-word; vertical-align:top; }
|
||||
.tabel-trimiteri .col-chk { width:30px; }
|
||||
.tabel-trimiteri .col-id { width:48px; }
|
||||
.tabel-trimiteri .col-stare { width:104px; }
|
||||
.tabel-trimiteri .col-data { width:104px; }
|
||||
.tabel-trimiteri .col-rar { width:96px; }
|
||||
.tabel-trimiteri .col-actualizat { width:128px; }
|
||||
.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; }
|
||||
/* === Detaliu inline (PRD 5.8 US-008): rand-sibling expandabil sub randul selectat. === */
|
||||
/* Chevron de stare (▸ inchis / ▾ deschis), rotit prin schimbarea glifei in JS. */
|
||||
.tabel-trimiteri .chevron { display:inline-block; color:var(--muted); font-size:11px;
|
||||
width:1.1em; text-align:center; margin-right:2px; }
|
||||
/* Randul deschis: fundal evidentiat (nu doar culoare de text -> a11y). */
|
||||
.tabel-trimiteri tr.rand-deschis > td { background:#1d212b; }
|
||||
[data-theme="light"] .tabel-trimiteri tr.rand-deschis > td { background:#eef1f6; }
|
||||
/* Conectorul detaliului = fundal subtil + border-top (NU border-left accent / slop). */
|
||||
.tabel-trimiteri tr.detaliu-rand > td { padding:0; border-top:2px solid var(--accent);
|
||||
background:color-mix(in srgb, var(--accent) 6%, var(--card)); }
|
||||
.tabel-trimiteri tr.detaliu-rand .card { margin:10px; }
|
||||
/* `hidden` trebuie sa invinga `display:block` din banda <768 (specificitate). */
|
||||
.tabel-trimiteri tr.detaliu-rand[hidden] { display:none; }
|
||||
/* 768-1024px: ascunde Actualizat (e in detaliu) -> 7 coloane, fara scroll */
|
||||
@media (max-width:1024px) {
|
||||
.tabel-trimiteri .col-actualizat { display:none; }
|
||||
}
|
||||
/* Tinta de atins >=44px pe touch (chevron-ul e ancora de toggle). */
|
||||
@media (pointer:coarse) {
|
||||
.tabel-trimiteri .chevron { min-width:44px; min-height:44px; line-height:44px; }
|
||||
}
|
||||
/* <768px: card per rand (eticheta:valoare stivuit), nu tabel -> fara scroll orizontal */
|
||||
@media (max-width:767px) {
|
||||
.tabel-trimiteri table { table-layout:auto; }
|
||||
.tabel-trimiteri thead { display:none; }
|
||||
.tabel-trimiteri table, .tabel-trimiteri tbody, .tabel-trimiteri tr, .tabel-trimiteri td { display:block; width:auto; }
|
||||
.tabel-trimiteri tr { border:1px solid var(--line); border-radius:8px; padding:8px 12px; margin-bottom:10px; }
|
||||
.tabel-trimiteri td { border-bottom:none; padding:4px 0; display:flex; gap:10px; align-items:baseline; }
|
||||
.tabel-trimiteri td::before { content:attr(data-eticheta); color:var(--muted); font-size:12px;
|
||||
flex:0 0 auto; min-width:120px; }
|
||||
.tabel-trimiteri td.col-chk { display:none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -371,5 +417,94 @@
|
||||
document.body.addEventListener('htmx:afterSettle', function(e) { enhance(e.target); });
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Detaliu trimitere INLINE (PRD 5.8 US-008): randul de detaliu (#detaliu-{id}) e
|
||||
// un <tr class="detaliu-rand"> sibling, ascuns pana la deschidere. La click pe rand:
|
||||
// - se inchid celelalte detalii (un singur rand deschis o data);
|
||||
// - se arata randul-sibling (placeholder „Se incarca…" prin hx-indicator);
|
||||
// - chevron ▸/▾ + fundal evidentiat + aria-expanded sincronizate.
|
||||
// Re-click pe acelasi rand inchide fara re-fetch. Cat un detaliu e deschis, poll-ul
|
||||
// de 15s (#submissions-wrap) e pus pe pauza (D-eng-2) ca lista sa nu se miste sub
|
||||
// operator. Delegare pe document.body -> supravietuieste swap-urilor HTMX ale listei.
|
||||
(function() {
|
||||
function chevron(row, on) {
|
||||
var c = row.querySelector('.chevron');
|
||||
if (c) c.innerHTML = on ? '▾' : '▸'; // ▾ / ▸
|
||||
}
|
||||
function setExpanded(row, on) {
|
||||
row.setAttribute('aria-expanded', on ? 'true' : 'false');
|
||||
if (on) row.classList.add('rand-deschis'); else row.classList.remove('rand-deschis');
|
||||
chevron(row, on);
|
||||
}
|
||||
function detRowFor(id) {
|
||||
var cont = document.getElementById('detaliu-' + id);
|
||||
return cont ? cont.closest('tr.detaliu-rand') : null;
|
||||
}
|
||||
function closeOne(row) {
|
||||
var id = row.getAttribute('data-detaliu-id');
|
||||
var cont = document.getElementById('detaliu-' + id);
|
||||
if (cont) cont.innerHTML = '';
|
||||
var dr = detRowFor(id);
|
||||
if (dr) dr.hidden = true;
|
||||
setExpanded(row, false);
|
||||
}
|
||||
function closeAllDetalii(except) {
|
||||
document.querySelectorAll('tr.trimitere-row[aria-expanded="true"]').forEach(function(r) {
|
||||
if (r !== except) closeOne(r);
|
||||
});
|
||||
}
|
||||
// Expus pentru butonul „Inchide" din _trimitere_detaliu.html: goleste containerul
|
||||
// randului CURENT si readuce focusul pe randul declansator.
|
||||
window.inchideDetaliu = function(id) {
|
||||
var row = document.getElementById('trimitere-row-' + id);
|
||||
if (row) { closeOne(row); row.focus(); }
|
||||
else {
|
||||
var cont = document.getElementById('detaliu-' + id);
|
||||
if (cont) cont.innerHTML = '';
|
||||
var dr = detRowFor(id);
|
||||
if (dr) dr.hidden = true;
|
||||
}
|
||||
};
|
||||
// Expus pentru scriptul fragmentului: marcheaza randul ca deschis dupa un re-swap
|
||||
// (corectie/mapare inline), inchizand orice alt detaliu ramas deschis.
|
||||
window.marcheazaDetaliuDeschis = function(row) {
|
||||
closeAllDetalii(row);
|
||||
setExpanded(row, true);
|
||||
};
|
||||
// htmx:beforeRequest — single point: pauza poll + toggle deschidere/inchidere.
|
||||
document.body.addEventListener('htmx:beforeRequest', function(evt) {
|
||||
var elt = evt.detail && evt.detail.elt;
|
||||
if (!elt) return;
|
||||
// Pauza poll periodic cat un detaliu e deschis (cererea vine chiar de pe wrap).
|
||||
if (elt.id === 'submissions-wrap' &&
|
||||
document.querySelector('tr.detaliu-rand:not([hidden])')) {
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (!(elt.classList && elt.classList.contains('trimitere-row'))) return;
|
||||
var id = elt.getAttribute('data-detaliu-id');
|
||||
if (elt.getAttribute('aria-expanded') === 'true') {
|
||||
// Re-click pe randul deschis -> inchide, fara re-fetch.
|
||||
evt.preventDefault();
|
||||
window.inchideDetaliu(id);
|
||||
return;
|
||||
}
|
||||
// Deschidere: inchide celelalte, arata randul-sibling (placeholder loading).
|
||||
closeAllDetalii(elt);
|
||||
var dr = detRowFor(id);
|
||||
if (dr) dr.hidden = false;
|
||||
setExpanded(elt, true);
|
||||
});
|
||||
// Tastatura (role=button): Enter/Space deschid/inchid randul focusat.
|
||||
document.body.addEventListener('keydown', function(evt) {
|
||||
var t = evt.target;
|
||||
if (!(t && t.classList && t.classList.contains('trimitere-row'))) return;
|
||||
if (evt.key === 'Enter' || evt.key === ' ' || evt.key === 'Spacebar') {
|
||||
evt.preventDefault();
|
||||
t.click();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user