feat: admin dashboard with stats and recent activity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gestoría LP
2026-03-02 00:21:58 +00:00
parent fbf1b31dc0
commit 660d41fb31
2 changed files with 329 additions and 1 deletions

192
admin/index.php Normal file
View File

@@ -0,0 +1,192 @@
<?php
$pageTitle = 'Dashboard';
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/includes/admin-header.php';
$db = getDB();
// Stats
$solicitudesNuevas = $db->query("SELECT COUNT(*) FROM solicitudes WHERE estado='nueva'")->fetchColumn();
$tramitesEnProceso = $db->query("SELECT COUNT(*) FROM tramites WHERE estado='en_proceso'")->fetchColumn();
$tramitesCompletados = $db->query("SELECT COUNT(*) FROM tramites WHERE estado='completado'")->fetchColumn();
$totalClientes = $db->query("SELECT COUNT(*) FROM clientes")->fetchColumn();
// Latest requests
$ultimasSolicitudes = $db->query("SELECT * FROM solicitudes WHERE estado='nueva' ORDER BY created_at DESC LIMIT 5")->fetchAll();
// Upcoming reminders
$recordatorios = $db->query("SELECT r.*, c.nombre as cliente_nombre FROM recordatorios r LEFT JOIN clientes c ON r.cliente_id = c.id WHERE r.completado=0 AND r.fecha <= DATE_ADD(CURDATE(), INTERVAL 7 DAY) ORDER BY r.fecha ASC LIMIT 5")->fetchAll();
// Recent processes
$tramitesRecientes = $db->query("SELECT t.*, c.nombre as cliente_nombre FROM tramites t JOIN clientes c ON t.cliente_id = c.id ORDER BY t.updated_at DESC LIMIT 5")->fetchAll();
$tipoLabels = [
'visa' => 'Visa',
'sentri' => 'Sentri/Global',
'pasaporte' => 'Pasaporte',
'adelanto_cita' => 'Adelanto Cita',
'doble_nacionalidad' => 'Doble Nacionalidad',
];
$estadoLabels = [
'nuevo' => 'Nuevo',
'en_proceso' => 'En Proceso',
'en_revision' => 'En Revisi&oacute;n',
'completado' => 'Completado',
'cancelado' => 'Cancelado',
];
?>
<div class="admin-content__header">
<h1><i class="fas fa-tachometer-alt"></i> Dashboard</h1>
<p>Bienvenido, <?= htmlspecialchars($_SESSION['username'] ?? 'Admin') ?></p>
</div>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-card__icon stat-card__icon--primary">
<i class="fas fa-inbox"></i>
</div>
<div class="stat-card__info">
<span class="stat-card__number"><?= $solicitudesNuevas ?></span>
<span class="stat-card__label">Solicitudes Nuevas</span>
</div>
</div>
<div class="stat-card">
<div class="stat-card__icon stat-card__icon--warning">
<i class="fas fa-spinner"></i>
</div>
<div class="stat-card__info">
<span class="stat-card__number"><?= $tramitesEnProceso ?></span>
<span class="stat-card__label">Tr&aacute;mites en Proceso</span>
</div>
</div>
<div class="stat-card">
<div class="stat-card__icon stat-card__icon--success">
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-card__info">
<span class="stat-card__number"><?= $tramitesCompletados ?></span>
<span class="stat-card__label">Completados</span>
</div>
</div>
<div class="stat-card">
<div class="stat-card__icon stat-card__icon--info">
<i class="fas fa-users"></i>
</div>
<div class="stat-card__info">
<span class="stat-card__number"><?= $totalClientes ?></span>
<span class="stat-card__label">Total Clientes</span>
</div>
</div>
</div>
<!-- Recent Activity Grid -->
<div class="dashboard-grid">
<!-- Latest Requests -->
<div class="card">
<div class="card__header">
<h2><i class="fas fa-inbox"></i> Solicitudes Recientes</h2>
<a href="solicitudes.php" class="btn btn--sm btn--outline">Ver todas</a>
</div>
<div class="card__body">
<?php if (empty($ultimasSolicitudes)): ?>
<p class="text-muted text-center">No hay solicitudes nuevas</p>
<?php else: ?>
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>Nombre</th>
<th>Servicio</th>
<th>Fecha</th>
<th>Acci&oacute;n</th>
</tr>
</thead>
<tbody>
<?php foreach ($ultimasSolicitudes as $sol): ?>
<tr>
<td><?= htmlspecialchars($sol['nombre']) ?></td>
<td><?= htmlspecialchars($tipoLabels[$sol['servicio']] ?? $sol['servicio']) ?></td>
<td><?= date('d/m/Y', strtotime($sol['created_at'])) ?></td>
<td><a href="solicitud-detalle.php?id=<?= $sol['id'] ?>" class="btn btn--sm btn--primary">Ver</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- Upcoming Reminders -->
<div class="card">
<div class="card__header">
<h2><i class="fas fa-bell"></i> Recordatorios Pr&oacute;ximos</h2>
<a href="recordatorios.php" class="btn btn--sm btn--outline">Ver todos</a>
</div>
<div class="card__body">
<?php if (empty($recordatorios)): ?>
<p class="text-muted text-center">No hay recordatorios pendientes</p>
<?php else: ?>
<ul class="reminder-list">
<?php foreach ($recordatorios as $rec): ?>
<li class="reminder-item <?= strtotime($rec['fecha']) < strtotime('today') ? 'reminder-item--overdue' : (date('Y-m-d') === $rec['fecha'] ? 'reminder-item--today' : '') ?>">
<div class="reminder-item__date">
<span class="reminder-item__day"><?= date('d', strtotime($rec['fecha'])) ?></span>
<span class="reminder-item__month"><?= date('M', strtotime($rec['fecha'])) ?></span>
</div>
<div class="reminder-item__info">
<strong><?= htmlspecialchars($rec['titulo']) ?></strong>
<?php if ($rec['cliente_nombre']): ?>
<small><?= htmlspecialchars($rec['cliente_nombre']) ?></small>
<?php endif; ?>
</div>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
</div>
</div>
<!-- Recent Processes -->
<div class="card" style="margin-top: 1.5rem;">
<div class="card__header">
<h2><i class="fas fa-file-alt"></i> Tr&aacute;mites Recientes</h2>
<a href="tramites.php" class="btn btn--sm btn--outline">Ver todos</a>
</div>
<div class="card__body">
<?php if (empty($tramitesRecientes)): ?>
<p class="text-muted text-center">No hay tr&aacute;mites registrados</p>
<?php else: ?>
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>Cliente</th>
<th>Tipo</th>
<th>Estado</th>
<th>Fecha Solicitud</th>
<th>Acci&oacute;n</th>
</tr>
</thead>
<tbody>
<?php foreach ($tramitesRecientes as $tr): ?>
<tr>
<td><?= htmlspecialchars($tr['cliente_nombre']) ?></td>
<td><?= htmlspecialchars($tipoLabels[$tr['tipo']] ?? $tr['tipo']) ?></td>
<td><span class="badge badge--<?= $tr['estado'] ?>"><?= htmlspecialchars($estadoLabels[$tr['estado']] ?? $tr['estado']) ?></span></td>
<td><?= $tr['fecha_solicitud'] ? date('d/m/Y', strtotime($tr['fecha_solicitud'])) : 'N/A' ?></td>
<td><a href="tramite-detalle.php?id=<?= $tr['id'] ?>" class="btn btn--sm btn--primary">Ver</a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<?php require_once __DIR__ . '/includes/admin-footer.php'; ?>

View File

@@ -1625,7 +1625,143 @@ textarea.form-control {
} }
/* ========================================================== /* ==========================================================
23. PRINT STYLES 23. DASHBOARD
========================================================== */
/* stat-card__number — alias for stat-card__value used on dashboard */
.stat-card__number {
font-size: var(--admin-font-2xl);
font-weight: 700;
color: var(--admin-gray-900);
line-height: 1.2;
display: block;
}
/* Card body (generic content area inside .card) */
.card__body {
/* inherits padding from .card; no extra padding needed */
}
/* Card header headings */
.card__header h2 {
font-size: var(--admin-font-md);
font-weight: 600;
color: var(--admin-gray-900);
display: flex;
align-items: center;
gap: 0.5rem;
}
.card__header h2 i {
color: var(--admin-primary);
font-size: var(--admin-font-base);
}
/* Dashboard two-column grid */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
@media (max-width: 992px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
/* Reminder list */
.reminder-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.reminder-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
border-radius: var(--admin-radius);
background: var(--admin-gray-100);
transition: var(--admin-transition);
}
.reminder-item:hover {
background: var(--admin-gray-200);
}
.reminder-item--overdue {
background: var(--admin-danger-light);
border-left: 3px solid var(--admin-danger);
}
.reminder-item--overdue:hover {
background: #f5c6cb;
}
.reminder-item--today {
background: var(--admin-warning-light);
border-left: 3px solid var(--admin-warning);
}
.reminder-item--today:hover {
background: #ffe8a1;
}
.reminder-item__date {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-width: 48px;
height: 48px;
background: var(--admin-white);
border-radius: var(--admin-radius-sm);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
flex-shrink: 0;
}
.reminder-item__day {
font-size: var(--admin-font-lg);
font-weight: 700;
color: var(--admin-gray-900);
line-height: 1.1;
}
.reminder-item__month {
font-size: var(--admin-font-xs);
color: var(--admin-gray-600);
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
}
.reminder-item__info {
display: flex;
flex-direction: column;
gap: 0.125rem;
min-width: 0;
}
.reminder-item__info strong {
font-size: var(--admin-font-sm);
color: var(--admin-gray-800);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.reminder-item__info small {
font-size: var(--admin-font-xs);
color: var(--admin-gray-600);
}
/* ==========================================================
24. PRINT STYLES
========================================================== */ ========================================================== */
@media print { @media print {
.sidebar, .sidebar,