feat: admin dashboard with stats and recent activity
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
192
admin/index.php
Normal file
192
admin/index.php
Normal 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ó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á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ó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ó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á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á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ó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'; ?>
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user