diff --git a/app/admin.py b/app/admin.py index cc210b2..ec68d10 100644 --- a/app/admin.py +++ b/app/admin.py @@ -2,8 +2,9 @@ import csv import io from pathlib import Path -from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile -from fastapi.responses import Response +from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, UploadFile +from fastapi.responses import HTMLResponse, Response +from fastapi.templating import Jinja2Templates from app.config import get_settings from app.db import list_leads, stats @@ -12,6 +13,31 @@ from app.security import require_admin router = APIRouter(prefix="/admin", tags=["admin"]) +_TEMPLATES_DIR = Path(__file__).parent / "templates" +templates = Jinja2Templates(directory=str(_TEMPLATES_DIR)) + + +@router.get("/leads.html", response_class=HTMLResponse) +@router.get("/leads", response_class=HTMLResponse) +def admin_leads_html( + request: Request, + _user: str = Depends(require_admin), + limit: int = Query(1000, ge=1, le=10000), +): + leads = list_leads(limit=limit, offset=0) + s = stats() + return templates.TemplateResponse( + "admin_leads.html", + { + "request": request, + "leads": leads, + "total": s["total_leads"], + "total_downloads": s["total_downloads"], + "top_empresas": s["top_5_empresas"], + "downloads_por_archivo": s["downloads_por_archivo"], + }, + ) + @router.get("/material-files") def admin_material_files(_user: str = Depends(require_admin)): diff --git a/app/templates/admin_leads.html b/app/templates/admin_leads.html new file mode 100644 index 0000000..9424464 --- /dev/null +++ b/app/templates/admin_leads.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} +{% block title %}Admin · Leads — taller-wox{% endblock %} + +{% block content %} +
+
+
+

ADMIN

+

Leads registrados

+

{{ total }} registros · {{ total_downloads }} descargas totales

+
+ +
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + {% for l in leads %} + + + + + + + + + + + + {% endfor %} + {% if not leads %} + + {% endif %} + +
#NombreEmailEmpresaReg.CreadoÚltimoIPConsent
{{ l.id }}{{ l.nombre }}{{ l.email }}{{ l.empresa }}{{ l.times_registered }}{{ l.created_at }}{{ l.last_seen }}{{ l.ip or '—' }}{% if l.consent %}{% else %}×{% endif %}
No hay leads registrados todavía.
+
+ + {% if top_empresas %} +
+

Top empresas

+ +
+ {% endif %} + + {% if downloads_por_archivo %} +
+

Descargas por archivo

+ +
+ {% endif %} +
+ + +{% endblock %} diff --git a/static/css/styles.css b/static/css/styles.css index 023c89c..9915dda 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -246,6 +246,107 @@ body { .footer-inner a:hover { color: var(--white); border-color: var(--slate-300); } .copyright { color: var(--slate-400); font-size: 12px; margin-top: 20px !important; } +/* ---------- Admin ---------- */ +.admin-section { + max-width: 1280px; margin: 0 auto; padding: 48px 32px 96px; +} +.admin-header { + display: flex; justify-content: space-between; align-items: flex-end; + gap: 16px; flex-wrap: wrap; margin-bottom: 32px; + border-bottom: 1px solid var(--slate-200); padding-bottom: 20px; +} +.admin-header h1 { + color: var(--navy); margin: 4px 0 6px; + font-size: 32px; font-weight: 700; letter-spacing: -0.015em; +} +.eyebrow-light { + color: var(--slate-600); font-weight: 600; font-size: 11px; + letter-spacing: 0.18em; margin: 0; text-transform: uppercase; +} +.admin-sub { color: var(--slate-600); margin: 0; font-size: 14px; } +.admin-actions { display: flex; gap: 10px; } +.btn-secondary { + background: var(--white); color: var(--navy); + border: 1.5px solid var(--slate-300); + padding: 10px 18px; font-size: 14px; border-radius: 6px; + font-weight: 600; transition: border-color 0.15s ease, background 0.15s ease; +} +.btn-secondary:hover { border-color: var(--navy); background: var(--slate-50); } + +.admin-filters { margin-bottom: 16px; } +.admin-filters input { + width: 100%; max-width: 420px; + padding: 10px 14px; border: 1.5px solid var(--slate-200); border-radius: 6px; + font-size: 14px; font-family: inherit; color: var(--slate-900); +} +.admin-filters input:focus { + outline: none; border-color: var(--teal); + box-shadow: 0 0 0 3px rgba(14, 116, 144, 0.1); +} + +.admin-table-wrap { + overflow-x: auto; border: 1px solid var(--slate-200); + border-radius: 8px; background: var(--white); +} +.admin-table { + width: 100%; border-collapse: collapse; font-size: 14px; +} +.admin-table thead th { + background: var(--slate-50); color: var(--slate-700); + text-align: left; padding: 12px 16px; font-weight: 600; + font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; + border-bottom: 1px solid var(--slate-200); +} +.admin-table thead th.num { text-align: right; } +.admin-table tbody td { + padding: 12px 16px; border-bottom: 1px solid var(--slate-100); + color: var(--slate-900); vertical-align: middle; +} +.admin-table tbody tr:hover { background: var(--slate-50); } +.admin-table tbody tr:last-child td { border-bottom: 0; } +.admin-table td.num { text-align: right; font-variant-numeric: tabular-nums; } +.admin-table td.muted { color: var(--slate-600); font-size: 13px; } +.admin-table td.mono { font-family: ui-monospace, Consolas, monospace; font-size: 12px; } +.admin-table td.empty { + text-align: center; color: var(--slate-600); padding: 48px 16px; font-style: italic; +} +.admin-table a { color: var(--teal); text-decoration: none; } +.admin-table a:hover { text-decoration: underline; } + +.pill { + display: inline-flex; align-items: center; justify-content: center; + width: 22px; height: 22px; border-radius: 50%; + font-size: 12px; font-weight: 700; +} +.pill-ok { background: rgba(14, 116, 144, 0.12); color: var(--teal); } +.pill-no { background: rgba(180, 83, 9, 0.12); color: var(--accent); } + +.admin-stats { margin-top: 40px; } +.admin-h2 { + color: var(--navy); font-size: 18px; font-weight: 600; + margin: 0 0 12px; letter-spacing: -0.005em; +} +.empresa-list { + list-style: none; padding: 0; margin: 0; + display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 8px; +} +.empresa-list li { + display: flex; justify-content: space-between; align-items: center; + padding: 12px 16px; background: var(--slate-50); + border: 1px solid var(--slate-200); border-radius: 6px; +} +.empresa-name { color: var(--slate-900); font-weight: 500; font-size: 14px; } +.empresa-count { + background: var(--navy); color: var(--white); + padding: 2px 10px; border-radius: 10px; font-size: 12px; font-weight: 600; + font-variant-numeric: tabular-nums; +} + +@media (max-width: 768px) { + .hide-sm { display: none; } +} + @media (max-width: 768px) { .site-header { padding: 14px 20px; } .powered-by { font-size: 11px; }