feat(admin): HTML leads list with filter + top empresas + download stats

This commit is contained in:
2026-05-13 16:20:32 +00:00
parent b5aedb86b1
commit 7b1b28ead8
3 changed files with 220 additions and 2 deletions

View File

@@ -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)):

View File

@@ -0,0 +1,91 @@
{% extends "base.html" %}
{% block title %}Admin · Leads — taller-wox{% endblock %}
{% block content %}
<section class="admin-section">
<div class="admin-header">
<div>
<p class="eyebrow-light">ADMIN</p>
<h1>Leads registrados</h1>
<p class="admin-sub">{{ total }} registros · {{ total_downloads }} descargas totales</p>
</div>
<div class="admin-actions">
<a class="btn btn-secondary" href="/admin/leads.csv">Exportar CSV</a>
<a class="btn btn-secondary" href="/admin/stats">Ver stats JSON</a>
</div>
</div>
<div class="admin-filters">
<input type="text" id="filter" placeholder="Filtrar por nombre, email o empresa..." oninput="filterTable()">
</div>
<div class="admin-table-wrap">
<table class="admin-table" id="leads-table">
<thead>
<tr>
<th>#</th>
<th>Nombre</th>
<th>Email</th>
<th>Empresa</th>
<th class="num">Reg.</th>
<th>Creado</th>
<th>Último</th>
<th class="hide-sm">IP</th>
<th class="hide-sm">Consent</th>
</tr>
</thead>
<tbody>
{% for l in leads %}
<tr>
<td class="muted">{{ l.id }}</td>
<td><strong>{{ l.nombre }}</strong></td>
<td><a href="mailto:{{ l.email }}">{{ l.email }}</a></td>
<td>{{ l.empresa }}</td>
<td class="num">{{ l.times_registered }}</td>
<td class="muted">{{ l.created_at }}</td>
<td class="muted">{{ l.last_seen }}</td>
<td class="muted mono hide-sm">{{ l.ip or '—' }}</td>
<td class="hide-sm">{% if l.consent %}<span class="pill pill-ok"></span>{% else %}<span class="pill pill-no">×</span>{% endif %}</td>
</tr>
{% endfor %}
{% if not leads %}
<tr><td colspan="9" class="empty">No hay leads registrados todavía.</td></tr>
{% endif %}
</tbody>
</table>
</div>
{% if top_empresas %}
<div class="admin-stats">
<h2 class="admin-h2">Top empresas</h2>
<ul class="empresa-list">
{% for e in top_empresas %}
<li><span class="empresa-name">{{ e.empresa }}</span><span class="empresa-count">{{ e.count }}</span></li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if downloads_por_archivo %}
<div class="admin-stats">
<h2 class="admin-h2">Descargas por archivo</h2>
<ul class="empresa-list">
{% for fname, count in downloads_por_archivo.items() %}
<li><span class="empresa-name mono">{{ fname }}</span><span class="empresa-count">{{ count }}</span></li>
{% endfor %}
</ul>
</div>
{% endif %}
</section>
<script>
function filterTable() {
const q = document.getElementById('filter').value.toLowerCase();
const rows = document.querySelectorAll('#leads-table tbody tr');
rows.forEach(r => {
const t = r.textContent.toLowerCase();
r.style.display = t.includes(q) ? '' : 'none';
});
}
</script>
{% endblock %}