Files
rar-autopass/app/web/templates/admin.html
Claude Agent 1fbd894329 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>
2026-06-23 11:56:05 +00:00

153 lines
7.1 KiB
HTML

{% 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 }}">&#8943;</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 &#39; 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>
</div>
{% if error %}
<div class="banner" style="margin-bottom:16px;padding:10px 14px;">{{ error }}</div>
{% endif %}
{{ lifecycle_block("Conturi in asteptare", pending, "pending",
['activate', 'block', 'archive', 'delete'],
['activate', 'block', 'archive', 'delete']) }}
{{ lifecycle_block("Conturi active", active, "active",
['block', 'archive', 'delete'],
['block', 'archive', 'delete']) }}
{# 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 %}