feat(admin): HTML leads list with filter + top empresas + download stats
This commit is contained in:
30
app/admin.py
30
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)):
|
||||
|
||||
91
app/templates/admin_leads.html
Normal file
91
app/templates/admin_leads.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user