feat(signup+admin): aliniere formular signup la landing + plan cerut, GDPR, control tier/trial in panou

Signup:
- /signup aliniat ca format la formularul din landing (campuri, etichete,
  placeholder-uri, select plan, checkbox GDPR, buton). Eticheta `name` = "Companie"
  (corecta: backendul salveaza nume de firma), uniform si in landing.
- Consimtamant GDPR validat server-side (functional, nu doar client-side) + salvat
  cu marca temporala (accounts.consent_at).
- Plan ales la signup salvat in accounts.requested_plan (intentie, NU drept): tier
  ramane sursa de adevar pentru gate-ul API; coloana pregateste integrarea platilor.
- landing: valorile `plan` = coduri tier (free/standard/pro/premium), data-plan
  sincronizat pe butoanele de pret; checkbox consimtamant primeste name.

Schema/DB:
- accounts: coloane noi requested_plan + consent_at (cu migrare aditiva in db.py).

Panou admin:
- Coloane noi: Plan curent (plan EFECTIV acum + zile trial ramase) si Plan cerut.
- Buton "Aplica" (POST /admin/set-tier): aloca plan real si INCHEIE trial-ul
  (efect imediat; altfel trial-ul Pro universal de 30z masca alegerea).
- Control "Trial Pro N zile" (POST /admin/set-trial via accounts.set_trial):
  acorda/prelungeste trial fara a schimba tier-ul de baza.

Teste: signup (consent obligatoriu, requested_plan persistat, tier ramane free),
panou admin (set-tier incheie trial, free opreste Pro imediat, set-trial, validari
+ CSRF). Call-site-urile existente POST /signup actualizate cu consent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-29 16:02:37 +00:00
parent a29896a790
commit 851f76ca16
14 changed files with 528 additions and 42 deletions

View File

@@ -8,6 +8,7 @@ Rute:
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from pathlib import Path
from fastapi import APIRouter, Form, Request
@@ -15,12 +16,42 @@ from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from .. import __version__
from ..accounts import account_is_complete, list_accounts, set_active, set_status, delete_account
from ..accounts import account_is_complete, list_accounts, set_active, set_status, set_tier, set_trial, delete_account
from ..config import get_settings
from ..db import get_connection
from ..plans import PLANS, effective_tier
from ..web.csrf import get_csrf_token, verify_csrf
from ..web.session import require_admin
def _plan_label(code: str | None) -> str:
"""Eticheta RO a unui cod de plan (din PLANS). None/necunoscut -> ''."""
if not code:
return ""
plan = PLANS.get(code)
return plan["label"] if plan else code
def _trial_zile_ramase(trial_until_str: str | None, now: datetime) -> int | None:
"""Zile ramase din trial (rotunjit in sus), sau None daca nu e trial activ/malformat.
Acelasi parsing tolerant ca plans.effective_tier (UTC implicit pe valori naive).
"""
if not trial_until_str:
return None
try:
tu = datetime.fromisoformat(trial_until_str.strip().replace(" ", "T"))
if tu.tzinfo is None:
tu = tu.replace(tzinfo=timezone.utc)
now_cmp = now if now.tzinfo else now.replace(tzinfo=timezone.utc)
secunde = (tu - now_cmp).total_seconds()
if secunde <= 0:
return None
# Rotunjire in sus la zile (o fractie de zi ramasa = inca 1 zi afisata).
return int(secunde // 86400) + (1 if secunde % 86400 else 0)
except (ValueError, AttributeError, TypeError):
return None
router = APIRouter()
_TMPL = Jinja2Templates(directory=str(Path(__file__).resolve().parent / "templates"))
@@ -47,10 +78,19 @@ def _render_admin(request: Request, conn, *, error: str | None = None, status_co
"""Randeaza pagina admin.html cu lista de conturi si optional un mesaj de eroare."""
accounts = list_accounts(conn)
emails = _emails_by_account(conn)
now = datetime.now(timezone.utc)
for acct in accounts:
# Computa is_complete INAINTE de a suprascrie accounts.email cu emailul de login al userului
acct["is_complete"] = account_is_complete(acct)
acct["email"] = emails.get(acct["id"])
# Plan EFECTIV (ce are contul acum): trial Pro activ ridica `free` la `pro`.
# `tier` ramane sursa de adevar pentru drepturi; `requested_plan` e doar intentia de la signup.
eff = effective_tier(acct, now)
acct["tier_label"] = _plan_label(acct.get("tier")) # tier de baza (post-trial)
acct["tier_efectiv_label"] = _plan_label(eff) # plan efectiv ACUM
acct["trial_activ"] = eff != (acct.get("tier") or "free")
acct["trial_zile"] = _trial_zile_ramase(acct.get("trial_until"), now)
acct["requested_plan_label"] = _plan_label(acct.get("requested_plan"))
# 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]
@@ -146,6 +186,73 @@ async def admin_delete(request: Request, account_id: list[int] = Form(...),
return _lifecycle_route(request, account_id, csrf_token, "delete")
@router.post("/admin/set-tier", response_class=HTMLResponse)
async def admin_set_tier(
request: Request,
account_id: int = Form(...),
tier: str = Form(...),
csrf_token: str = Form(default=""),
):
"""Schimba planul (tier) unui cont din panou. require_admin + CSRF, PRG 303.
Reuseaza accounts.set_tier (valideaza tier-ul, protejeaza id=1, logheaza schimbarea).
INCHEIE trial-ul (trial_until=NULL): alocarea manuala = plan real de-acum, cu efect
imediat — altfel trial-ul Pro universal (30z la signup) ar masca alegerea pana la
expirare (decizie user 2026-06-29). Tier invalid / cont protejat -> re-randare cu eroare.
"""
require_admin(request)
verify_csrf(request, csrf_token)
conn = get_connection()
try:
try:
# trial_until=None: alocarea manuala incheie trial-ul si aplica tier-ul ales acum.
set_tier(conn, account_id, tier, trial_until=None)
conn.commit()
except ValueError as exc:
return _render_admin(request, conn, error=str(exc), status_code=422)
finally:
conn.close()
return RedirectResponse("/admin", status_code=303)
@router.post("/admin/set-trial", response_class=HTMLResponse)
async def admin_set_trial(
request: Request,
account_id: int = Form(...),
trial_days: int = Form(...),
csrf_token: str = Form(default=""),
):
"""Acorda/prelungeste un trial Pro de N zile (de la acum), fara a schimba tier-ul de baza.
require_admin + CSRF, PRG 303. Reuseaza accounts.set_trial (protejeaza id=1, logheaza).
trial_days <= 0 sau peste plafon -> re-randare panou cu eroare (422). Plafon defensiv 3650z.
"""
require_admin(request)
verify_csrf(request, csrf_token)
conn = get_connection()
try:
if trial_days <= 0 or trial_days > 3650:
return _render_admin(
request, conn,
error="Numarul de zile pentru trial trebuie sa fie intre 1 si 3650.",
status_code=422,
)
try:
now = datetime.now(timezone.utc)
trial_until = (now + timedelta(days=trial_days)).strftime("%Y-%m-%d %H:%M:%S")
set_trial(conn, account_id, trial_until)
conn.commit()
except ValueError as exc:
return _render_admin(request, conn, error=str(exc), status_code=422)
finally:
conn.close()
return RedirectResponse("/admin", status_code=303)
@router.post("/admin/deactivate", response_class=HTMLResponse)
async def admin_deactivate(
request: Request,

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import datetime, timezone
from pathlib import Path
from fastapi import APIRouter, Form, Request
@@ -9,7 +10,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from .. import __version__
from ..accounts import create_account
from ..accounts import VALID_TIERS, create_account
from ..auth import create_api_key
from ..config import get_settings
from ..db import get_connection
@@ -47,10 +48,18 @@ async def signup_post(
cui: str = Form(default=""),
email: str = Form(default=""),
parola: str = Form(default=""),
plan: str = Form(default=""),
consent: str = Form(default=""),
csrf_token: str = Form(default=""),
):
verify_csrf(request, csrf_token)
# Planul CERUT (intentie, nu drept): pastram doar valori valide; orice altceva -> 'free'.
# `tier`-ul real ramane 'free' la creare; planul ales se onoreaza dupa plata (admin/webhook).
requested_plan = plan.strip().lower() if plan else ""
if requested_plan not in VALID_TIERS:
requested_plan = "free"
settings = get_settings()
ip = request.client.host if request.client else "unknown"
if not check_rate_limit(ip, settings.signup_rate_max, settings.signup_rate_window_s):
@@ -58,7 +67,7 @@ async def signup_post(
request,
csrf_token=get_csrf_token(request),
error=_RATE_MSG,
name=name, cui=cui, email=email,
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=429)
if len(parola) < _PASSWORD_MIN:
@@ -66,7 +75,7 @@ async def signup_post(
request,
csrf_token=get_csrf_token(request),
error=f"Parola trebuie sa aiba cel putin {_PASSWORD_MIN} caractere.",
name=name, cui=cui, email=email,
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=422)
# CUI obligatoriu la signup (US-001, PRD 5.12)
@@ -76,9 +85,19 @@ async def signup_post(
request,
csrf_token=get_csrf_token(request),
error="CUI-ul firmei este obligatoriu.",
name=name, cui=cui, email=email,
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=422)
# Consimtamant Termeni + GDPR obligatoriu (proba). Checkbox bifat -> valoare ne-goala.
if not (consent and consent.strip()):
return _TMPL.TemplateResponse(request, "signup.html", _ctx(
request,
csrf_token=get_csrf_token(request),
error="Trebuie sa accepti Termenii si prelucrarea datelor (GDPR) pentru a crea cont.",
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=422)
consent_at = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
# Bootstrap admin: count_admins se citeste INAUNTRUL tranzactiei BEGIN IMMEDIATE,
# astfel lock-ul RESERVED serializeaza scriitorii si al doilea signup vede count==1.
conn = get_connection()
@@ -86,7 +105,10 @@ async def signup_post(
conn.execute("BEGIN IMMEDIATE")
try:
is_first = count_admins(conn) == 0
account_id = create_account(conn, name, cui=cui_norm, email=email, active=False)
account_id = create_account(
conn, name, cui=cui_norm, email=email, active=False,
requested_plan=requested_plan, consent_at=consent_at,
)
user_id = create_user(conn, account_id, email, parola, is_admin=is_first)
api_key = create_api_key(conn, account_id)
conn.execute("COMMIT")
@@ -121,7 +143,7 @@ async def signup_post(
request,
csrf_token=get_csrf_token(request),
error=error_msg,
name=name, cui=cui, email=email,
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=422)
except Exception as exc:
conn.execute("ROLLBACK")
@@ -129,7 +151,7 @@ async def signup_post(
request,
csrf_token=get_csrf_token(request),
error=str(exc),
name=name, cui=cui, email=email,
name=name, cui=cui, email=email, plan=requested_plan,
), status_code=422)
finally:
conn.close()

View File

@@ -10,6 +10,9 @@
'delete': ('Sterge', '/admin/delete', 'danger')
} %}
{# Tier-uri selectabile in panou (cod, eticheta). Aliniat cu app/plans.py#PLANS. #}
{% set TIERS = [('free', 'Gratuit'), ('standard', 'Standard'), ('pro', 'Pro'), ('premium', 'Premium')] %}
{% macro lifecycle_block(title, rows, block_id, bulk_verbs, row_verbs) %}
<div class="card">
<h3 style="margin-top:0;">{{ title }} ({{ rows|length }})</h3>
@@ -34,7 +37,7 @@
<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>
<th>ID</th><th>Companie</th><th>CUI</th><th>Email</th><th>Plan curent</th><th>Plan cerut</th><th>Stare</th><th>Inregistrat</th><th>Actiuni</th>
</tr></thead>
<tbody>
{% for acct in rows %}
@@ -46,6 +49,45 @@
<td>{{ acct.name }}</td>
<td class="muted">{{ acct.cui or "—" }}</td>
<td>{{ acct.email or "—" }}</td>
<td style="white-space:nowrap;">
{# Plan EFECTIV acum (prominent): trial Pro activ ridica free->pro. #}
<div style="margin-bottom:5px;">
<span class="pill" style="font-weight:600;">{{ acct.tier_efectiv_label }}</span>
{% if acct.trial_activ %}
<span class="muted" style="font-size:11px;">
trial{% if acct.trial_zile %} · {{ acct.trial_zile }} {{ 'zi' if acct.trial_zile == 1 else 'zile' }} ramase{% endif %}
→ apoi {{ acct.tier_label }}
</span>
{% endif %}
</div>
{# Schimbare plan inline: select tier de baza + Aplica. Form propriu (nu imbricat in bulk-form).
Aplica INCHEIE trial-ul si seteaza planul ales ca real, cu efect imediat. #}
<form method="post" action="/admin/set-tier" class="tier-form"
style="display:flex;align-items:center;gap:6px;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="account_id" value="{{ acct.id }}">
<select name="tier" aria-label="Plan pentru {{ acct.name }}"
style="padding:4px 8px;min-height:32px;max-width:130px;">
{% for code, label in TIERS %}
<option value="{{ code }}"{% if acct.tier == code %} selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
<button type="submit" class="btn-sm"
title="Aplica planul ales ca plan real (incheie trial-ul daca e activ)">Aplica</button>
</form>
{# Acorda/prelungeste trial Pro de N zile, fara a schimba tier-ul de baza. #}
<form method="post" action="/admin/set-trial" class="trial-form"
style="display:flex;align-items:center;gap:6px;margin-top:5px;">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="account_id" value="{{ acct.id }}">
<input type="number" name="trial_days" value="30" min="1" max="3650"
aria-label="Zile trial Pro pentru {{ acct.name }}"
style="padding:4px 8px;min-height:32px;width:64px;">
<button type="submit" class="btn-sm"
title="Acorda/prelungeste trial Pro de la acum (nu schimba tier-ul de baza)">Trial Pro</button>
</form>
</td>
<td class="muted">{{ acct.requested_plan_label }}</td>
<td><span class="pill">{{ acct.status }}</span></td>
<td class="muted">{{ acct.created_at or "—" }}</td>
<td style="white-space:nowrap;">

View File

@@ -259,7 +259,7 @@
<div style="display:flex;gap:9px;font:400 13px/1.4 var(--font-ui);color:var(--sub,#8b93a7);"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="var(--sub,#8b93a7)" stroke-width="1.8" style="flex-shrink:0;margin-top:1px;"><path d="M4 12h16"/></svg>Fără import API</div>
<span style="display:none;"></span>
</div>
<button data-act="auth" data-tab="register" data-plan="Gratuit" style="width:100%;height:46px;border-radius:6px;background:#1F9D5C;border:1px solid #1F9D5C;color:#fff;font:700 14px var(--font-ui);cursor:pointer;transition:transform .2s cubic-bezier(.2,.8,.2,1), background .2s ease, box-shadow .2s ease;" style-hover="background:#16864a;border-color:#16864a;transform:translateY(-1px)">Creează cont gratuit</button>
<button data-act="auth" data-tab="register" data-plan="free" style="width:100%;height:46px;border-radius:6px;background:#1F9D5C;border:1px solid #1F9D5C;color:#fff;font:700 14px var(--font-ui);cursor:pointer;transition:transform .2s cubic-bezier(.2,.8,.2,1), background .2s ease, box-shadow .2s ease;" style-hover="background:#16864a;border-color:#16864a;transform:translateY(-1px)">Creează cont gratuit</button>
</div>
<!-- Standard -->
<div style="background:var(--card,#181c24);border:1px solid var(--line,#262b36);border-radius:12px;padding:26px 24px;">
@@ -273,7 +273,7 @@
<div style="display:flex;gap:9px;font:400 13px/1.4 var(--font-ui);color:var(--sub,#8b93a7);"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="var(--sub,#8b93a7)" stroke-width="1.8" style="flex-shrink:0;margin-top:1px;"><path d="M4 12h16"/></svg>Fără import API</div>
<span style="display:none;"></span>
</div>
<button style="width:100%;height:46px;border-radius:6px;background:transparent;border:1px solid var(--line,#262b36);color:var(--text,#e6e9ef);font:500 14px var(--font-ui);cursor:pointer;transition:transform .18s ease, background .18s ease, border-color .18s ease;" style-hover="background:#1F9D5C;border-color:#1F9D5C;color:#fff;transform:translateY(-1px)" data-act="auth" data-tab="register" data-plan="Standard">Creează cont gratuit</button>
<button style="width:100%;height:46px;border-radius:6px;background:transparent;border:1px solid var(--line,#262b36);color:var(--text,#e6e9ef);font:500 14px var(--font-ui);cursor:pointer;transition:transform .18s ease, background .18s ease, border-color .18s ease;" style-hover="background:#1F9D5C;border-color:#1F9D5C;color:#fff;transform:translateY(-1px)" data-act="auth" data-tab="register" data-plan="standard">Creează cont gratuit</button>
</div>
<!-- Pro -->
<div style="background:var(--card,#181c24);border:1px solid var(--line,#262b36);border-radius:12px;padding:26px 24px;position:relative;">
@@ -288,7 +288,7 @@
<div style="display:flex;gap:9px;font:400 13px/1.4 var(--font-ui);color:var(--text,#e6e9ef);"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#2FBF8F" stroke-width="2" style="flex-shrink:0;margin-top:1px;"><path d="M20 6L9 17l-5-5"/></svg>Categorisire automată, cu confirmare la operațiile incerte</div>
<span style="display:none;"></span>
</div>
<button style="width:100%;height:46px;border-radius:6px;background:transparent;border:1px solid var(--line,#262b36);color:var(--text,#e6e9ef);font:500 14px var(--font-ui);cursor:pointer;transition:transform .18s ease, background .18s ease, border-color .18s ease;" style-hover="background:#1F9D5C;border-color:#1F9D5C;color:#fff;transform:translateY(-1px)" data-act="auth" data-tab="register" data-plan="Pro">Creează cont gratuit</button>
<button style="width:100%;height:46px;border-radius:6px;background:transparent;border:1px solid var(--line,#262b36);color:var(--text,#e6e9ef);font:500 14px var(--font-ui);cursor:pointer;transition:transform .18s ease, background .18s ease, border-color .18s ease;" style-hover="background:#1F9D5C;border-color:#1F9D5C;color:#fff;transform:translateY(-1px)" data-act="auth" data-tab="register" data-plan="pro">Creează cont gratuit</button>
</div>
<!-- Premium -->
<div style="background:var(--card,#181c24);border:1px solid var(--line,#262b36);border-radius:12px;padding:26px 24px;">
@@ -301,7 +301,7 @@
<div style="display:flex;gap:9px;font:400 13px/1.4 var(--font-ui);color:var(--text,#e6e9ef);"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#2FBF8F" stroke-width="2" style="flex-shrink:0;margin-top:1px;"><path d="M20 6L9 17l-5-5"/></svg>Suport telefonic și online</div>
<div style="display:flex;gap:9px;font:400 13px/1.4 var(--font-ui);color:var(--text,#e6e9ef);"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#2FBF8F" stroke-width="2" style="flex-shrink:0;margin-top:1px;"><path d="M20 6L9 17l-5-5"/></svg>Asistență și onboarding dedicate</div>
</div>
<button style="width:100%;height:46px;border-radius:6px;background:transparent;border:1px solid var(--line,#262b36);color:var(--text,#e6e9ef);font:500 14px var(--font-ui);cursor:pointer;transition:transform .18s ease, background .18s ease, border-color .18s ease;" style-hover="background:#1F9D5C;border-color:#1F9D5C;color:#fff;transform:translateY(-1px)" data-act="auth" data-tab="register" data-plan="Premium">Creează cont gratuit</button>
<button style="width:100%;height:46px;border-radius:6px;background:transparent;border:1px solid var(--line,#262b36);color:var(--text,#e6e9ef);font:500 14px var(--font-ui);cursor:pointer;transition:transform .18s ease, background .18s ease, border-color .18s ease;" style-hover="background:#1F9D5C;border-color:#1F9D5C;color:#fff;transform:translateY(-1px)" data-act="auth" data-tab="register" data-plan="premium">Creează cont gratuit</button>
</div>
</div>
</div>
@@ -348,12 +348,12 @@
</div>
<form method="post" action="/signup" data-pane="register">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label style="display:block;margin-bottom:14px;"><span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--sub,#8b93a7);">Nume contact</span><input type="text" name="name" required placeholder="Ion Popescu" style="width:100%;height:44px;padding:0 12px;border:1px solid var(--line,#262b36);border-radius:6px;background:var(--card2,#0f1218);color:var(--text,#e6e9ef);font:400 14px var(--font-ui);outline:none;" /></label>
<label style="display:block;margin-bottom:14px;"><span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--sub,#8b93a7);">Companie</span><input type="text" name="name" required placeholder="SC Service Auto SRL" style="width:100%;height:44px;padding:0 12px;border:1px solid var(--line,#262b36);border-radius:6px;background:var(--card2,#0f1218);color:var(--text,#e6e9ef);font:400 14px var(--font-ui);outline:none;" /></label>
<label style="display:block;margin-bottom:14px;"><span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--sub,#8b93a7);">CUI</span><input type="text" name="cui" required placeholder="RO12345678" style="width:100%;height:44px;padding:0 12px;border:1px solid var(--line,#262b36);border-radius:6px;background:var(--card2,#0f1218);color:var(--text,#e6e9ef);font:400 14px var(--font-mono);outline:none;" /></label>
<label style="display:block;margin-bottom:14px;"><span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--sub,#8b93a7);">Email</span><input type="email" name="email" required placeholder="nume@service.ro" style="width:100%;height:44px;padding:0 12px;border:1px solid var(--line,#262b36);border-radius:6px;background:var(--card2,#0f1218);color:var(--text,#e6e9ef);font:400 14px var(--font-ui);outline:none;" /></label>
<label style="display:block;margin-bottom:14px;"><span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--sub,#8b93a7);">Parolă</span><input type="password" name="parola" required minlength="10" placeholder="Minim 10 caractere" style="width:100%;height:44px;padding:0 12px;border:1px solid var(--line,#262b36);border-radius:6px;background:var(--card2,#0f1218);color:var(--text,#e6e9ef);font:400 14px var(--font-ui);outline:none;" /></label>
<label style="display:block;margin-bottom:16px;"><span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--sub,#8b93a7);">Pachet ales</span><select id="plan-select" name="plan" style="width:100%;height:44px;padding:0 10px;border:1px solid var(--line,#262b36);border-radius:6px;background:var(--card2,#0f1218);color:var(--text,#e6e9ef);font:400 14px var(--font-ui);outline:none;cursor:pointer;"><option value="Gratuit" selected>Gratuit — 0 lei/lună</option><option value="Standard">Standard — 39 lei/lună</option><option value="Pro">Pro — 59 lei/lună</option><option value="Premium">Premium — la cerere</option></select></label>
<label style="display:flex;gap:9px;align-items:flex-start;margin-bottom:18px;font:400 13px/1.5 var(--font-ui);color:var(--sub,#8b93a7);cursor:pointer;"><input type="checkbox" required style="margin-top:2px;accent-color:var(--accent,#2E74D6);width:16px;height:16px;flex-shrink:0;" />Sunt de acord cu Termenii și cu prelucrarea datelor conform politicii de confidențialitate (GDPR).</label>
<label style="display:block;margin-bottom:16px;"><span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--sub,#8b93a7);">Pachet ales</span><select id="plan-select" name="plan" style="width:100%;height:44px;padding:0 10px;border:1px solid var(--line,#262b36);border-radius:6px;background:var(--card2,#0f1218);color:var(--text,#e6e9ef);font:400 14px var(--font-ui);outline:none;cursor:pointer;"><option value="free" selected>Gratuit — 0 lei/lună</option><option value="standard">Standard — 39 lei/lună</option><option value="pro">Pro — 59 lei/lună</option><option value="premium">Premium — la cerere</option></select></label>
<label style="display:flex;gap:9px;align-items:flex-start;margin-bottom:18px;font:400 13px/1.5 var(--font-ui);color:var(--sub,#8b93a7);cursor:pointer;"><input type="checkbox" name="consent" value="1" required style="margin-top:2px;accent-color:var(--accent,#2E74D6);width:16px;height:16px;flex-shrink:0;" />Sunt de acord cu Termenii și cu prelucrarea datelor conform politicii de confidențialitate (GDPR).</label>
<button type="submit" class="btn-green" style="width:100%;height:48px;border-radius:6px;background:#1F9D5C;border:1px solid #1F9D5C;color:#fff;font:700 15px var(--font-ui);cursor:pointer;transition:transform .2s cubic-bezier(.2,.8,.2,1), background .2s ease, box-shadow .2s ease;">Creează cont gratuit</button>
<div style="text-align:center;margin-top:14px;font:400 13px var(--font-ui);color:var(--sub,#8b93a7);">Ai deja cont? <a data-act="tab" data-tab="login" style="color:var(--accent,#2E74D6);font-weight:500;cursor:pointer;">Autentifică-te</a></div>
</form>

View File

@@ -37,33 +37,53 @@
});
</script>
{% else %}
<h2 style="margin-top:0;">Inregistrare cont nou</h2>
<h2 style="margin-top:0;">Creează cont nou</h2>
{% if error %}
<div class="banner" style="margin-bottom:12px;padding:8px 12px;">{{ error }}</div>
{% endif %}
{# Format aliniat la formularul de inregistrare din landing (#inregistrare): aceleasi campuri,
etichete, placeholder-uri si stil. Valorile `plan` = coduri tier (free/standard/pro/premium),
normalizate server-side. #}
<form method="post" action="/signup">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<p>
<label>Companie <span style="color:var(--err)">*</span></label><br>
<input type="text" name="name" value="{{ name or '' }}" required style="width:100%;">
</p>
<p>
<label>CUI <span style="color:var(--err)">*</span></label><br>
<input type="text" name="cui" value="{{ cui or '' }}" required style="width:100%;">
</p>
<p>
<label>Email <span style="color:var(--err)">*</span></label><br>
<input type="email" name="email" value="{{ email or '' }}" required style="width:100%;">
</p>
<p>
<label>Parola <span style="color:var(--err)">*</span>
<span style="color:var(--muted);font-size:12px;">(minim 10 caractere)</span>
</label><br>
<input type="password" name="parola" required style="width:100%;">
</p>
<button type="submit" style="width:100%;margin-top:8px;">Creeaza cont</button>
<label style="display:block;margin-bottom:14px;">
<span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--muted);">Companie</span>
<input type="text" name="name" value="{{ name or '' }}" required placeholder="SC Service Auto SRL"
style="width:100%;height:44px;padding:0 12px;border:1px solid var(--line);border-radius:6px;background:var(--card2);color:var(--ink);font:400 14px var(--font-ui);outline:none;">
</label>
<label style="display:block;margin-bottom:14px;">
<span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--muted);">CUI</span>
<input type="text" name="cui" value="{{ cui or '' }}" required placeholder="RO12345678"
style="width:100%;height:44px;padding:0 12px;border:1px solid var(--line);border-radius:6px;background:var(--card2);color:var(--ink);font:400 14px var(--font-mono);outline:none;">
</label>
<label style="display:block;margin-bottom:14px;">
<span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--muted);">Email</span>
<input type="email" name="email" value="{{ email or '' }}" required placeholder="nume@service.ro"
style="width:100%;height:44px;padding:0 12px;border:1px solid var(--line);border-radius:6px;background:var(--card2);color:var(--ink);font:400 14px var(--font-ui);outline:none;">
</label>
<label style="display:block;margin-bottom:14px;">
<span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--muted);">Parolă</span>
<input type="password" name="parola" required minlength="10" placeholder="Minim 10 caractere"
style="width:100%;height:44px;padding:0 12px;border:1px solid var(--line);border-radius:6px;background:var(--card2);color:var(--ink);font:400 14px var(--font-ui);outline:none;">
</label>
<label style="display:block;margin-bottom:16px;">
<span style="display:block;margin-bottom:6px;font:500 13px var(--font-ui);color:var(--muted);">Pachet ales</span>
<select name="plan"
style="width:100%;height:44px;padding:0 10px;border:1px solid var(--line);border-radius:6px;background:var(--card2);color:var(--ink);font:400 14px var(--font-ui);outline:none;cursor:pointer;">
<option value="free"{% if not plan or plan == 'free' %} selected{% endif %}>Gratuit — 0 lei/lună</option>
<option value="standard"{% if plan == 'standard' %} selected{% endif %}>Standard — 39 lei/lună</option>
<option value="pro"{% if plan == 'pro' %} selected{% endif %}>Pro — 59 lei/lună</option>
<option value="premium"{% if plan == 'premium' %} selected{% endif %}>Premium — la cerere</option>
</select>
</label>
<label style="display:flex;gap:9px;align-items:flex-start;margin-bottom:18px;font:400 13px/1.5 var(--font-ui);color:var(--muted);cursor:pointer;">
<input type="checkbox" name="consent" value="1" required style="margin-top:2px;accent-color:var(--accent);width:16px;height:16px;flex-shrink:0;">
Sunt de acord cu Termenii și cu prelucrarea datelor conform politicii de confidențialitate (GDPR).
</label>
<button type="submit"
style="width:100%;height:48px;border-radius:6px;background:#1F9D5C;border:1px solid #1F9D5C;color:#fff;font:700 15px var(--font-ui);cursor:pointer;">Creează cont gratuit</button>
</form>
<p style="text-align:center;font-size:13px;margin-top:16px;">
Ai deja cont? <a href="/login">Autentificare</a>