feat(web): uniformizare/standardizare UI/UX + lifecycle conturi (PRD 5.5)
Aduce toate suprafetele dashboard-ului la grila tabelului Trimiteri, muta
navigarea intr-un meniu de cont (hamburger) si da panoului admin actiuni
reale de ciclu de viata. 9 stories, 3 valuri. UI pur (reskin + reasezare)
cu O SINGURA exceptie backend: modelul de stare a contului.
- US-001 sectiunea "Ajutor" eliminata din Acasa (wayfinding redundant).
- US-002 Nomenclator la grila standard (_submissions.html ca referinta).
- US-003 macro autosend compact (Manual<->Auto). Semantica de PREZENTA
`auto_send` (bifat->true, absent->false) NEALTERATA — compatibil cu ambele
parsere (Form(bool) la /mapari, bool(form.get()) la import). Zero backend.
- US-004 accounts.status (pending/active/blocked/archived/deleted), migrare
defensiva idempotenta derivata din `active`, gate worker claim_one pe
status='active' (echivalenta active=1 <=> status='active' pastrata).
- US-005 tabel Mapari compact + panou Ajutor (<details>, proza o singura data),
coloana "In coada".
- US-006 meniu hamburger dropdown (Cont/Integrare/Nomenclator/Admin/logout) +
context is_authenticated/is_admin/csrf_token defensiv in base.html.
- US-007 tab-bar redus la Acasa+Mapari; rutele /_fragments/{cont,integrare,
nomenclator} + deep-link ?tab= raman valide.
- US-008 rute admin block/archive/delete + bulk pe lista account_id,
require_admin + CSRF + PRG, dev id=1 sarit in bulk.
- US-009 admin UI: selectie bife + master + bara bulk + kebab per-rand,
grupare pe stare (bloc nou blocate/arhivate), nota "cont dev implicit" scoasa.
Stergere = SOFT: tombstone (status='deleted'), dar PII purjata IMEDIAT
(rar_creds_enc + chei API revocate + CUI eliberat pentru re-inregistrare),
GDPR/L.142.
VERIFY: 671 teste pass (+40). E2E browser (Playwright) a prins 2 bug-uri
invizibile la TestClient: bara bulk cu display:flex inline invingea [hidden]
(mutat in CSS .bulk-bar[hidden]); conturi arhivate cadeau sub "in asteptare"
(grupare pe status). /code-review high a prins 2 bug-uri reale: soft delete
pastra creds RAR + CUI la nesfarsit fara purjare accounts (GDPR neonorat);
apostrof in numele firmei rupea confirm() inline din kebab — ambele reparate,
plus cleanup boilerplate rute (_lifecycle_route).
Backend trimitere (worker masina stari/idempotenta/mapping) neatins, cu
exceptia gate-ului de cont. Design: docs/design/5.5-uniformizare-ui.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from .. import __version__
|
||||
from ..accounts import list_accounts, set_active
|
||||
from ..accounts import list_accounts, set_active, set_status, delete_account
|
||||
from ..config import get_settings
|
||||
from ..db import get_connection
|
||||
from ..web.csrf import get_csrf_token, verify_csrf
|
||||
@@ -49,16 +49,20 @@ 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"])
|
||||
pending = [a for a in accounts if not a["active"] and a["id"] != 1]
|
||||
active = [a for a in accounts if a["active"] and a["id"] != 1]
|
||||
default = next((a for a in accounts if a["id"] == 1), None)
|
||||
# Grupare pe STARE (5.5), 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]
|
||||
suspended = [a for a in accounts if a["status"] in ("blocked", "archived") and a["id"] != 1]
|
||||
return _TMPL.TemplateResponse(request, "admin.html", _ctx(
|
||||
request,
|
||||
csrf_token=get_csrf_token(request),
|
||||
pending=pending,
|
||||
active=active,
|
||||
default_account=default,
|
||||
suspended=suspended,
|
||||
error=error,
|
||||
is_authenticated=True,
|
||||
is_admin=True,
|
||||
), status_code=status_code)
|
||||
|
||||
|
||||
@@ -74,28 +78,66 @@ async def admin_get(request: Request):
|
||||
conn.close()
|
||||
|
||||
|
||||
@router.post("/admin/activate", response_class=HTMLResponse)
|
||||
async def admin_activate(
|
||||
request: Request,
|
||||
account_id: int = Form(...),
|
||||
csrf_token: str = Form(default=""),
|
||||
):
|
||||
"""Activeaza un cont. PRG: redirect 303 la /admin dupa succes."""
|
||||
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
|
||||
(id=1) sau inexistente ridica ValueError din helperi -> sarite (nu opresc bulk-ul).
|
||||
`action`: activate | block | archive | delete."""
|
||||
for aid in ids:
|
||||
try:
|
||||
if action == "activate":
|
||||
set_status(conn, aid, "active")
|
||||
elif action == "block":
|
||||
set_status(conn, aid, "blocked")
|
||||
elif action == "archive":
|
||||
set_status(conn, aid, "archived")
|
||||
elif action == "delete":
|
||||
delete_account(conn, aid)
|
||||
except ValueError:
|
||||
continue # cont de sistem / inexistent -> sarit
|
||||
|
||||
|
||||
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.
|
||||
Evita 4 handlere copy-paste care difera doar prin verb."""
|
||||
require_admin(request)
|
||||
verify_csrf(request, csrf_token)
|
||||
|
||||
conn = get_connection()
|
||||
try:
|
||||
try:
|
||||
set_active(conn, account_id, True)
|
||||
except ValueError as exc:
|
||||
return _render_admin(request, conn, error=str(exc), status_code=422)
|
||||
_apply_lifecycle(conn, account_id, action)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return RedirectResponse("/admin", status_code=303)
|
||||
|
||||
|
||||
@router.post("/admin/activate", response_class=HTMLResponse)
|
||||
async def admin_activate(request: Request, account_id: list[int] = Form(...),
|
||||
csrf_token: str = Form(default="")):
|
||||
"""Activeaza unul sau mai multe conturi (bulk). PRG 303."""
|
||||
return _lifecycle_route(request, account_id, csrf_token, "activate")
|
||||
|
||||
|
||||
@router.post("/admin/block", response_class=HTMLResponse)
|
||||
async def admin_block(request: Request, account_id: list[int] = Form(...),
|
||||
csrf_token: str = Form(default="")):
|
||||
"""Blocheaza (suspendare reversibila) unul sau mai multe conturi. PRG 303."""
|
||||
return _lifecycle_route(request, account_id, csrf_token, "block")
|
||||
|
||||
|
||||
@router.post("/admin/archive", response_class=HTMLResponse)
|
||||
async def admin_archive(request: Request, account_id: list[int] = Form(...),
|
||||
csrf_token: str = Form(default="")):
|
||||
"""Arhiveaza (scos din listele active, date read-only) unul sau mai multe conturi. PRG 303."""
|
||||
return _lifecycle_route(request, account_id, csrf_token, "archive")
|
||||
|
||||
|
||||
@router.post("/admin/delete", response_class=HTMLResponse)
|
||||
async def admin_delete(request: Request, account_id: list[int] = Form(...),
|
||||
csrf_token: str = Form(default="")):
|
||||
"""Stergere SOFT (tombstone + purjare PII imediata) a unuia sau mai multor conturi. PRG 303."""
|
||||
return _lifecycle_route(request, account_id, csrf_token, "delete")
|
||||
|
||||
|
||||
@router.post("/admin/deactivate", response_class=HTMLResponse)
|
||||
async def admin_deactivate(
|
||||
request: Request,
|
||||
|
||||
@@ -331,6 +331,7 @@ def dashboard(request: Request, tab: str = "acasa") -> HTMLResponse:
|
||||
"active_tab": active_tab,
|
||||
"panel_html": panel_html,
|
||||
"badges": badges,
|
||||
"is_authenticated": True,
|
||||
"is_admin": is_account_admin(conn, account_id),
|
||||
"csrf_token": get_csrf_token(request),
|
||||
}
|
||||
|
||||
@@ -44,15 +44,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# === Subordonat: ajutor rapid pe un rand discret ===
|
||||
US-003 (3.6): linkul redundant "Trimiteri" a fost scos (Trimiterile sunt mai jos
|
||||
pe aceeasi pagina). Wayfinding "Mapari"/"Coduri RAR" pastrat pentru operatori. #}
|
||||
<div style="margin-top:10px; font-size:13px; color:var(--muted);
|
||||
display:flex; gap:16px; flex-wrap:wrap; align-items:center;">
|
||||
<span>Ajutor:</span>
|
||||
<a href="/?tab=mapari">Mapari</a>
|
||||
<a href="/?tab=nomenclator">Coduri RAR</a>
|
||||
</div>
|
||||
{# 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,
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
{# Macro-uri partajate intre template-urile de import si mapari. #}
|
||||
|
||||
{# US-007: comutator pe COADA in loc de bifa "auto-send".
|
||||
Framing pe punerea in coada, NU pe trimitere (poarta autoplan UC-A): etichetele
|
||||
poarta singure sensul de send-safety. `name="auto_send" value="true"` pastrat cu
|
||||
semantica de prezenta (bifat -> True, nebifat -> absent -> False) ca sa produca
|
||||
bool corect cu AMBELE parsere backend (Form(bool) la /mapari, bool(form.get())
|
||||
la /_import/.../mapare-operatie). Zero atingere backend.
|
||||
{# 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).
|
||||
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.
|
||||
- form_id: leaga input-ul de un <form> extern (necesar in celulele de tabel).
|
||||
- checked: starea initiala (H4 - reflecta valoarea STOCATA per mapare). #}
|
||||
- checked: starea STOCATA per mapare (H4) — bifat = Auto. #}
|
||||
{% macro autosend_toggle(form_id='', checked=True) -%}
|
||||
<div class="autosend-toggle" style="display:flex; flex-direction:column; gap:4px;">
|
||||
<span class="muted" style="font-size:12px;">La fisierele viitoare cu aceasta operatie:</span>
|
||||
<label class="chk" style="display:inline-flex; align-items:center; gap:8px; min-height:44px;">
|
||||
<input type="checkbox" name="auto_send" value="true"
|
||||
{%- if form_id %} form="{{ form_id }}"{% endif %}
|
||||
{%- if checked %} checked{% endif %}
|
||||
aria-label="Pune automat in coada la fisierele viitoare cu aceasta operatie">
|
||||
<span><strong>Pune automat in coada</strong></span>
|
||||
</label>
|
||||
<span class="muted" style="font-size:11px;">
|
||||
Nebifat = "Tine pentru verificare". Doar pentru aceasta operatie;
|
||||
nimic nu pleaca la RAR pana confirmi.
|
||||
</span>
|
||||
</div>
|
||||
<label class="autosend-toggle"
|
||||
title="Auto = pune automat in coada la fisierele viitoare cu aceasta operatie. Manual = tine pentru verificare; nimic nu pleaca la RAR pana confirmi."
|
||||
style="display:inline-flex; align-items:center; gap:6px; white-space:nowrap; min-height:36px; cursor:pointer; font-size:13px;">
|
||||
<span class="muted">Manual</span>
|
||||
<input type="checkbox" name="auto_send" value="true"
|
||||
{%- if form_id %} form="{{ form_id }}"{% endif %}
|
||||
{%- if checked %} checked{% endif %}
|
||||
aria-label="In coada: Auto (bifat) sau Manual (nebifat), pentru aceasta operatie"
|
||||
style="width:32px; height:18px; cursor:pointer; accent-color:var(--accent);">
|
||||
<span><strong>Auto</strong></span>
|
||||
</label>
|
||||
{%- endmacro %}
|
||||
|
||||
@@ -9,7 +9,22 @@
|
||||
<!-- Sectiunea 1: De rezolvat (operatii needs_mapping) -->
|
||||
<!-- ============================================================ -->
|
||||
<div class="card">
|
||||
<h2 style="font-size:15px; margin:0 0 12px;">De rezolvat</h2>
|
||||
{# 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. #}
|
||||
<h2 style="font-size:15px; margin:0 0 8px;">De rezolvat</h2>
|
||||
<details class="ajutor-mapari" style="margin:0 0 12px;">
|
||||
<summary class="cardlink" style="display:inline-flex; color:var(--accent); cursor:pointer; padding:4px 0;">Ajutor</summary>
|
||||
<div class="muted" style="font-size:13px; margin-top:8px; max-width:680px;">
|
||||
Maparile leaga o operatie din softul tau (cod intern ROAAUTO) de un cod RAR oficial.
|
||||
Operatiile necunoscute raman blocate in <span class="s-needs_mapping">needs_mapping</span>
|
||||
si NU pleaca la RAR pana le mapezi. Sugestiile (%) vin din potrivire fuzzy pe denumire —
|
||||
verifica-le inainte sa salvezi. <strong>In coada</strong>: <strong>Auto</strong> = la
|
||||
urmatoarele fisiere cu aceasta operatie randurile intra automat in coada;
|
||||
<strong>Manual</strong> = raman pentru verificare, nimic nu pleaca la RAR pana confirmi.
|
||||
La schimbarea unui cod, submission-urile blocate pe acea operatie se re-rezolva automat.
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{% if not pending %}
|
||||
<div class="empty">
|
||||
@@ -17,18 +32,13 @@
|
||||
<a href="/?tab=acasa">Importa un fisier nou</a> daca vrei sa adaugi prezentari.
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted" style="margin:0 0 12px; font-size:13px;">
|
||||
Operatii ROAAUTO necunoscute, blocate in <span class="s-needs_mapping">needs_mapping</span>.
|
||||
Alege codul RAR (sugestia fuzzy e preselectata) si salveaza — submission-urile se deblocheaza automat.
|
||||
</p>
|
||||
|
||||
<div class="tablewrap">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Operatie</th>
|
||||
<th>Sugestii</th>
|
||||
<th>Cod RAR</th>
|
||||
<th>Punere in coada</th>
|
||||
<th>In coada</th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@@ -88,17 +98,14 @@
|
||||
Nicio mapare salvata inca. Pe masura ce mapezi operatii, ele apar aici si le poti edita oricand.
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted" style="margin:0 0 12px; font-size:13px;">
|
||||
Maparile operatie -> cod RAR retinute pentru contul tau. Schimba codul sau punerea in coada si salveaza;
|
||||
la schimbarea unui cod, submission-urile blocate pe acea operatie se re-rezolva automat.
|
||||
</p>
|
||||
{# US-005 (5.5): proza explicativa mutata in panoul Ajutor de la "De rezolvat" (o singura data). #}
|
||||
|
||||
<div class="tablewrap">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>Operatie</th>
|
||||
<th>Cod RAR</th>
|
||||
<th>Punere in coada</th>
|
||||
<th>In coada</th>
|
||||
<th>Actiuni</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@@ -160,7 +167,7 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="muted" style="margin:0 0 12px; font-size:13px;">
|
||||
Antetele de fisier recunoscute. Un fisier cu alte coloane = format nou separat (nu suprascrie).
|
||||
Antetele de fisier recunoscute. Un fisier cu alte coloane = format nou separat.
|
||||
</p>
|
||||
|
||||
<div class="tablewrap">
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
{# 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. #}
|
||||
{% if rows %}
|
||||
<div class="tablewrap">
|
||||
<table>
|
||||
<thead><tr><th>Cod</th><th>Denumire</th><th>Actualizat</th></tr></thead>
|
||||
<thead><tr>
|
||||
<th>Cod</th>
|
||||
<th>Denumire</th>
|
||||
<th>Actualizat</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td><span class="pill">{{ r.cod_prestatie }}</span></td>
|
||||
<td>{{ r.nume_prestatie }}</td>
|
||||
<td style="white-space:normal;">{{ r.nume_prestatie }}</td>
|
||||
<td class="muted">{{ r.updated_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,6 +1,104 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Panou admin — Gateway RAR AUTOPASS{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{# US-009 (5.5): metadate verbe de ciclu de viata (eticheta, ruta, clasa). #}
|
||||
{% set VERBS = {
|
||||
'activate': ('Activeaza', '/admin/activate', ''),
|
||||
'block': ('Blocheaza', '/admin/block', ''),
|
||||
'archive': ('Arhiveaza', '/admin/archive', ''),
|
||||
'delete': ('Sterge', '/admin/delete', 'danger')
|
||||
} %}
|
||||
|
||||
{% macro lifecycle_block(title, rows, block_id, bulk_verbs, row_verbs) %}
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0;">{{ title }} ({{ rows|length }})</h3>
|
||||
{% if rows %}
|
||||
{# Bara bulk: form propriu (id=bulk-<block>); checkbox-urile randurilor se leaga prin atributul
|
||||
HTML5 form= (fara form-uri imbricate). Ascunsa pana exista o selectie (JS). #}
|
||||
<form id="bulk-{{ block_id }}" method="post" class="bulk-form" data-block="{{ block_id }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="bulk-bar" hidden>
|
||||
<span class="bulk-count muted" style="font-size:13px;">0 selectate</span>
|
||||
{% for v in bulk_verbs %}
|
||||
{% set label, action, cls = VERBS[v] %}
|
||||
<button type="submit" formaction="{{ action }}"
|
||||
{% if v == 'delete' %}onclick="return confirm('Stergi conturile selectate? (stergere soft, datele se purjeaza)');"{% endif %}
|
||||
style="{% if cls == 'danger' %}background:var(--card); color:var(--err); border-color:var(--err);{% endif %}">{{ label }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="tablewrap">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th style="width:28px;"><input type="checkbox" class="master-check" data-block="{{ block_id }}"
|
||||
aria-label="Selecteaza tot"></th>
|
||||
<th>ID</th><th>Companie</th><th>CUI</th><th>Email</th><th>Stare</th><th>Inregistrat</th><th>Actiuni</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for acct in rows %}
|
||||
<tr>
|
||||
<td><input type="checkbox" name="account_id" value="{{ acct.id }}" form="bulk-{{ block_id }}"
|
||||
class="row-check" data-block="{{ block_id }}"
|
||||
aria-label="Selecteaza contul {{ acct.name }}"></td>
|
||||
<td class="muted">{{ acct.id }}</td>
|
||||
<td>{{ acct.name }}</td>
|
||||
<td class="muted">{{ acct.cui or "—" }}</td>
|
||||
<td>{{ acct.email or "—" }}</td>
|
||||
<td><span class="pill">{{ acct.status }}</span></td>
|
||||
<td class="muted">{{ acct.created_at or "—" }}</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<details class="kebab">
|
||||
<summary class="cardlink" style="list-style:none; cursor:pointer; display:inline-flex;
|
||||
padding:4px 10px;" aria-label="Actiuni pentru {{ acct.name }}">⋯</summary>
|
||||
<div class="kebab-menu">
|
||||
{% for v in row_verbs %}
|
||||
{% set label, action, cls = VERBS[v] %}
|
||||
{# Confirm fara nume interpolat: un apostrof in numele firmei (free-form) ar rupe
|
||||
string-ul JS din atributul inline (entitatea ' e decodata inainte de parse). #}
|
||||
<form method="post" action="{{ action }}"
|
||||
{% if v == 'delete' %}onsubmit="return confirm('Stergi acest cont? (stergere soft)');"{% endif %}>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="account_id" value="{{ acct.id }}">
|
||||
<button type="submit" {% if cls == 'danger' %}style="color:var(--err);"{% endif %}>{{ label }}</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty">Niciun cont.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<style>
|
||||
/* Bara de actiuni bulk — ascunsa pana exista selectie. `[hidden]` trebuie sa invinga
|
||||
display-ul, deci stilul sta in CSS (NU inline cu display:flex, care ar invinge [hidden]). */
|
||||
.bulk-bar { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:10px;
|
||||
padding:8px 10px; border:1px solid var(--line); border-radius:8px;
|
||||
background:color-mix(in srgb, var(--accent) 8%, var(--card)); }
|
||||
.bulk-bar[hidden] { display:none; }
|
||||
/* Kebab per-rand (reuseaza estetica meniului de cont) */
|
||||
.kebab { position:relative; display:inline-block; }
|
||||
.kebab > summary::-webkit-details-marker { display:none; }
|
||||
.kebab-menu { position:absolute; right:0; top:calc(100% + 4px); min-width:140px; z-index:40;
|
||||
background:var(--card); border:1px solid var(--line); border-radius:8px; padding:6px;
|
||||
box-shadow:0 8px 24px rgba(0,0,0,.18); display:flex; flex-direction:column; gap:2px; }
|
||||
.kebab[open] > summary { background:var(--line); }
|
||||
.kebab-menu form { margin:0; }
|
||||
.kebab-menu button { display:block; width:100%; text-align:left; background:transparent; border:none;
|
||||
color:var(--ink); font:inherit; padding:7px 10px; border-radius:6px; cursor:pointer;
|
||||
min-height:36px; }
|
||||
.kebab-menu button:hover { background:var(--line); }
|
||||
</style>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:16px;margin-bottom:20px;">
|
||||
<h2 style="margin:0;">Panou admin</h2>
|
||||
<a href="/" class="cardlink muted">Inapoi la dashboard</a>
|
||||
@@ -10,96 +108,45 @@
|
||||
<div class="banner" style="margin-bottom:16px;padding:10px 14px;">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Conturi in asteptare -->
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0;">Conturi in asteptare ({{ pending|length }})</h3>
|
||||
{% if pending %}
|
||||
<div class="tablewrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Companie</th>
|
||||
<th>CUI</th>
|
||||
<th>Email</th>
|
||||
<th>Inregistrat</th>
|
||||
<th>Actiune</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for acct in pending %}
|
||||
<tr>
|
||||
<td class="muted">{{ acct.id }}</td>
|
||||
<td>{{ acct.name }}</td>
|
||||
<td class="muted">{{ acct.cui or "—" }}</td>
|
||||
<td>{{ acct.email or "—" }}</td>
|
||||
<td class="muted">{{ acct.created_at or "—" }}</td>
|
||||
<td>
|
||||
<form method="post" action="/admin/activate" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="account_id" value="{{ acct.id }}">
|
||||
<button type="submit">Activeaza</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty">Niciun cont in asteptare.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ lifecycle_block("Conturi in asteptare", pending, "pending",
|
||||
['activate', 'block', 'archive', 'delete'],
|
||||
['activate', 'block', 'archive', 'delete']) }}
|
||||
|
||||
<!-- Conturi active -->
|
||||
<div class="card">
|
||||
<h3 style="margin-top:0;">Conturi active ({{ active|length }})</h3>
|
||||
{% if active %}
|
||||
<div class="tablewrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Companie</th>
|
||||
<th>CUI</th>
|
||||
<th>Email</th>
|
||||
<th>Inregistrat</th>
|
||||
<th>Actiune</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for acct in active %}
|
||||
<tr>
|
||||
<td class="muted">{{ acct.id }}</td>
|
||||
<td>{{ acct.name }}</td>
|
||||
<td class="muted">{{ acct.cui or "—" }}</td>
|
||||
<td>{{ acct.email or "—" }}</td>
|
||||
<td class="muted">{{ acct.created_at or "—" }}</td>
|
||||
<td>
|
||||
<form method="post" action="/admin/deactivate" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="account_id" value="{{ acct.id }}">
|
||||
<button type="submit" style="background:var(--err);border-color:var(--err);">Dezactiveaza</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty">Niciun cont activ (in afara de contul dev).</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ lifecycle_block("Conturi active", active, "active",
|
||||
['block', 'archive', 'delete'],
|
||||
['block', 'archive', 'delete']) }}
|
||||
|
||||
<!-- Contul dev default (id=1) -->
|
||||
{% if default_account %}
|
||||
<div class="card" style="border-color:var(--muted);">
|
||||
<p class="muted" style="margin:0;font-size:13px;">
|
||||
Cont dev implicit (id=1): <strong>{{ default_account.name }}</strong>
|
||||
— activ={{ default_account.active }} — fara buton de activare/dezactivare (cont de sistem).
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# Conturi suspendate (blocate/arhivate): reactivare sau stergere. Stare reala in pill. #}
|
||||
{{ lifecycle_block("Conturi blocate / arhivate", suspended, "suspended",
|
||||
['activate', 'delete'],
|
||||
['activate', 'delete']) }}
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Selectie + bara bulk, scoped pe fiecare bloc (pending/active) prin data-block.
|
||||
document.querySelectorAll('.master-check').forEach(function(master) {
|
||||
var block = master.getAttribute('data-block');
|
||||
var rows = Array.prototype.slice.call(
|
||||
document.querySelectorAll('.row-check[data-block="' + block + '"]'));
|
||||
var form = document.getElementById('bulk-' + block);
|
||||
var bar = form ? form.querySelector('.bulk-bar') : null;
|
||||
var count = form ? form.querySelector('.bulk-count') : null;
|
||||
|
||||
function refresh() {
|
||||
var n = rows.filter(function(r) { return r.checked; }).length;
|
||||
if (bar) bar.hidden = (n === 0);
|
||||
if (count) count.textContent = n + ' selectate';
|
||||
master.checked = (n > 0 && n === rows.length);
|
||||
master.indeterminate = (n > 0 && n < rows.length);
|
||||
}
|
||||
master.addEventListener('change', function() {
|
||||
rows.forEach(function(r) { r.checked = master.checked; });
|
||||
refresh();
|
||||
});
|
||||
rows.forEach(function(r) { r.addEventListener('change', refresh); });
|
||||
refresh();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -116,6 +116,22 @@
|
||||
.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 */
|
||||
.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); }
|
||||
.cont-menu { position:absolute; right:0; top:calc(100% + 8px); min-width:180px; z-index:50;
|
||||
background:var(--card); border:1px solid var(--line); border-radius:8px; padding:6px;
|
||||
box-shadow:0 8px 24px rgba(0,0,0,.18); display:flex; flex-direction:column; gap:2px; }
|
||||
.cont-menu[hidden] { display:none; }
|
||||
.cont-menu a, .cont-menu button { display:block; width:100%; text-align:left; background:transparent;
|
||||
border:none; color:var(--ink); text-decoration:none; font:inherit; padding:8px 10px;
|
||||
border-radius:6px; cursor:pointer; min-height:36px; }
|
||||
.cont-menu a:hover, .cont-menu button:hover { background:var(--line); }
|
||||
.cont-menu hr { border:none; border-top:1px solid var(--line); margin:4px 0; }
|
||||
.cont-menu form { margin:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -123,11 +139,30 @@
|
||||
<h1>Gateway RAR AUTOPASS</h1>
|
||||
<span class="env">{{ rar_env }}</span>
|
||||
<div style="margin-left:auto; display:flex; align-items:center; gap:8px;">
|
||||
<button id="tema-toggle"
|
||||
<button id="tema-toggle" class="icon-btn"
|
||||
aria-label="Comuta tema (luminos/intunecat)"
|
||||
title="Comuta tema"
|
||||
style="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;">☀</button>
|
||||
title="Comuta tema">☀</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.
|
||||
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">☰</button>
|
||||
<div id="cont-menu" class="cont-menu" role="menu" aria-labelledby="cont-menu-toggle" hidden>
|
||||
<a role="menuitem" href="/?tab=cont">Cont</a>
|
||||
<a role="menuitem" href="/?tab=integrare">Integrare</a>
|
||||
<a role="menuitem" href="/?tab=nomenclator">Nomenclator</a>
|
||||
{% if is_admin|default(false) %}<a role="menuitem" href="/admin">Panou admin</a>{% endif %}
|
||||
<hr>
|
||||
<form method="post" action="/logout">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token|default('') }}">
|
||||
<button role="menuitem" type="submit">Iesi din cont</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
<main>{% block content %}{% endblock %}</main>
|
||||
@@ -165,5 +200,37 @@
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
// Meniu cont (US-006 PRD 5.5): 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');
|
||||
var menu = document.getElementById('cont-menu');
|
||||
if (!toggle || !menu) return;
|
||||
function open() {
|
||||
menu.hidden = false;
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
document.addEventListener('click', onDocClick, true);
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
}
|
||||
function close(refocus) {
|
||||
menu.hidden = true;
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
document.removeEventListener('click', onDocClick, true);
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
if (refocus) toggle.focus();
|
||||
}
|
||||
function onDocClick(e) {
|
||||
if (!menu.contains(e.target) && e.target !== toggle) close(false);
|
||||
}
|
||||
function onKey(e) {
|
||||
if (e.key === 'Escape') { e.preventDefault(); close(true); }
|
||||
}
|
||||
toggle.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
if (menu.hidden) open(); else close(false);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<!-- Nav cont: link admin (doar pentru admini) + logout -->
|
||||
<div style="display:flex; gap:8px; justify-content:flex-end; margin-bottom:12px; flex-wrap:wrap;">
|
||||
{% if is_admin %}<a class="cardlink" href="/admin">Panou admin</a>{% endif %}
|
||||
<form method="post" action="/logout" style="display:inline; margin:0;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" style="background:var(--card); color:var(--muted); border-color:var(--line);">Iesi din cont</button>
|
||||
</form>
|
||||
</div>
|
||||
{# US-007 (5.5): nav-ul ad-hoc (Panou admin + logout) a fost mutat in meniul de cont (☰)
|
||||
din header (base.html). Aici raman doar bara de status + tab-bar-ul de lucru zilnic. #}
|
||||
|
||||
<!-- Bara de status (US-002): mereu vizibila, deasupra tab-bar-ului -->
|
||||
<div id="status-bar" class="status-bar card"
|
||||
@@ -20,14 +14,12 @@
|
||||
|
||||
<!-- Tab-bar: navigare intre sectiuni -->
|
||||
<div role="tablist" class="tab-bar" aria-label="Sectiuni dashboard">
|
||||
{# US-003 (3.6): tab-ul "Trimiteri" (coada) a fost eliminat — Trimiterile traiesc
|
||||
ca sectiune permanenta pe Acasa. Raman: Acasa·Mapari·Cont·Nomenclator·Integrare. #}
|
||||
{# US-007 (5.5): tab-bar redus la suprafetele de LUCRU ZILNIC (Acasa·Mapari).
|
||||
Cont/Integrare/Nomenclator s-au mutat in meniul de cont (☰) din header — rutele
|
||||
`/_fragments/{cont,integrare,nomenclator}` + deep-link `?tab=` raman valide. #}
|
||||
{% set tabs = [
|
||||
("acasa", "Acasa", "tab-acasa"),
|
||||
("mapari", "Mapari", "tab-mapari"),
|
||||
("cont", "Cont", "tab-cont"),
|
||||
("nomenclator", "Nomenclator", "tab-nomenclator"),
|
||||
("integrare", "Integrare", "tab-integrare")
|
||||
("mapari", "Mapari", "tab-mapari")
|
||||
] %}
|
||||
{% for tab_id, tab_label, tab_elem_id in tabs %}
|
||||
{% set badge = (badges.get(tab_id, 0) if badges else 0) %}
|
||||
|
||||
Reference in New Issue
Block a user