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:
@@ -39,9 +39,10 @@ def create_account(
|
||||
raise ValueError("name gol (un cont are nevoie de nume)")
|
||||
cui = _norm_cui(cui)
|
||||
try:
|
||||
# Invariant (5.5): active=1 <=> status='active'; cont creat inactiv = 'pending'.
|
||||
cur = conn.execute(
|
||||
"INSERT INTO accounts (name, cui, active) VALUES (?, ?, ?)",
|
||||
(name, cui, 1 if active else 0),
|
||||
"INSERT INTO accounts (name, cui, active, status) VALUES (?, ?, ?, ?)",
|
||||
(name, cui, 1 if active else 0, "active" if active else "pending"),
|
||||
)
|
||||
except sqlite3.IntegrityError:
|
||||
existing = conn.execute("SELECT id FROM accounts WHERE cui=?", (cui,)).fetchone()
|
||||
@@ -55,16 +56,72 @@ def create_account(
|
||||
|
||||
def set_active(conn: sqlite3.Connection, account_id: int, active: bool) -> None:
|
||||
"""Comuta `accounts.active`. Idempotent (set activ pe activ nu arunca).
|
||||
Cont inexistent -> ValueError."""
|
||||
Cont inexistent -> ValueError.
|
||||
|
||||
Mentine invariantul 5.5 active=1 <=> status='active': activarea -> 'active',
|
||||
dezactivarea -> 'pending' (legacy „in asteptare"). Pentru blocare/arhivare/stergere
|
||||
foloseste `set_status`/`delete_account`.
|
||||
"""
|
||||
row = conn.execute("SELECT 1 FROM accounts WHERE id=?", (account_id,)).fetchone()
|
||||
if not row:
|
||||
raise ValueError(f"cont inexistent: {account_id}")
|
||||
conn.execute("UPDATE accounts SET active=? WHERE id=?", (1 if active else 0, account_id))
|
||||
conn.execute(
|
||||
"UPDATE accounts SET active=?, status=? WHERE id=?",
|
||||
(1 if active else 0, "active" if active else "pending", account_id),
|
||||
)
|
||||
|
||||
|
||||
# Stari de ciclu de viata gestionate explicit (5.5). 'deleted' = stergere soft (purjata de
|
||||
# retentie); restul sunt reversibile.
|
||||
VALID_STATUSES = ("pending", "active", "blocked", "archived", "deleted")
|
||||
# Verbele care nu se pot aplica contului de sistem id=1 (protejat, ca la deactivate in 3.3b).
|
||||
_PROTECTED_ACCOUNT_ID = 1
|
||||
|
||||
|
||||
def set_status(conn: sqlite3.Connection, account_id: int, status: str) -> None:
|
||||
"""Seteaza `accounts.status` la una din `VALID_STATUSES`, mentinand mirror-ul `active`
|
||||
(active=1 doar pentru 'active', altfel 0).
|
||||
|
||||
Contul de sistem id=1 NU poate fi mutat din 'active' (cont default) -> ValueError.
|
||||
Status invalid sau cont inexistent -> ValueError.
|
||||
"""
|
||||
if status not in VALID_STATUSES:
|
||||
raise ValueError(f"status invalid: {status}")
|
||||
row = conn.execute("SELECT 1 FROM accounts WHERE id=?", (account_id,)).fetchone()
|
||||
if not row:
|
||||
raise ValueError(f"cont inexistent: {account_id}")
|
||||
if account_id == _PROTECTED_ACCOUNT_ID and status != "active":
|
||||
raise ValueError("Contul default (id=1) nu poate fi blocat/arhivat/sters (cont de sistem).")
|
||||
conn.execute(
|
||||
"UPDATE accounts SET active=?, status=? WHERE id=?",
|
||||
(1 if status == "active" else 0, status, account_id),
|
||||
)
|
||||
|
||||
|
||||
def delete_account(conn: sqlite3.Connection, account_id: int) -> None:
|
||||
"""Stergere SOFT: randul ramane ca tombstone (status='deleted', scos din liste), DAR datele
|
||||
sensibile se purjeaza IMEDIAT (GDPR/L.142): credentialele RAR criptate sterse, cheile API
|
||||
revocate si CUI-ul eliberat (ca acelasi CUI sa se poata re-inregistra — altfel indexul unic
|
||||
`ux_accounts_cui` l-ar tine blocat de un cont invizibil). Contul de sistem id=1 e protejat.
|
||||
|
||||
Nota: nu facem hard DELETE pe rand din cauza FK-urilor (submissions/api_keys/...); pastram
|
||||
tombstone-ul pentru audit, dar fara PII. Jobul de retentie T16 purjeaza `submissions`/batches,
|
||||
NU acest tombstone — de aceea purjam PII aici, la momentul stergerii."""
|
||||
set_status(conn, account_id, "deleted") # valideaza existenta + protejeaza id=1; seteaza status+active=0
|
||||
conn.execute(
|
||||
"UPDATE accounts SET rar_creds_enc=NULL, cui=NULL WHERE id=?", (account_id,)
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE api_keys SET active=0, revoked_at=datetime('now') WHERE account_id=? AND active=1",
|
||||
(account_id,),
|
||||
)
|
||||
|
||||
|
||||
def list_accounts(conn: sqlite3.Connection) -> list[dict]:
|
||||
"""Metadate conturi (FARA `rar_creds_enc`), ordonate dupa id."""
|
||||
"""Metadate conturi (FARA `rar_creds_enc`), ordonate dupa id. Exclude conturile 'deleted'
|
||||
(stergere soft -> invizibile in panou)."""
|
||||
rows = conn.execute(
|
||||
"SELECT id, name, cui, active, created_at FROM accounts ORDER BY id"
|
||||
"SELECT id, name, cui, active, status, created_at FROM accounts "
|
||||
"WHERE status != 'deleted' ORDER BY id"
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
Reference in New Issue
Block a user