293 lines
9.8 KiB
PHP

<?php
/**
* dashboard.php — Panel principal con KPIs y gráficos
*/
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/controllers/AuthController.php';
require_once __DIR__ . '/models/Oficio.php';
AuthController::requerirAuth();
$oficio = new OficioModel();
$kpis = $oficio->kpis();
$global = $kpis['global'];
$estados = $kpis['estados'];
$prioridades = $kpis['prioridades'];
$proximosVencer = $oficio->proximosAVencer(3);
$estadisticasMens = $oficio->estadisticasMensuales(6);
// Solo mis oficios si es usuario estándar
$esAdmin = AuthController::esAdmin();
$userId = $_SESSION['usuario_id'];
$pageTitle = 'Dashboard';
$activeNav = 'dashboard';
include __DIR__ . '/views/layout/header.php';
include __DIR__ . '/views/layout/sidebar.php';
include __DIR__ . '/views/layout/topbar.php';
?>
<div class="page-content">
<!-- Breadcrumb -->
<div class="breadcrumb">
<i class="fa-solid fa-house"></i>
<span>Dashboard</span>
</div>
<!-- Page header -->
<div class="page-header">
<div class="page-header-content">
<h1>Panel de Control</h1>
<p>Resumen general del sistema de gestión de oficios institucionales · <?= date('d/m/Y') ?></p>
</div>
<a href="<?= APP_URL ?>/views/oficios/crear.php" class="btn btn-primary">
<i class="fa-solid fa-plus"></i> Nuevo Oficio
</a>
</div>
<!-- KPI Cards -->
<div class="grid-4 mb-4">
<div class="kpi-card kpi-primary">
<div class="kpi-icon"><i class="fa-solid fa-folder-open"></i></div>
<div class="kpi-info">
<div class="kpi-value"><?= number_format((int)($global['total_oficios'] ?? 0)) ?></div>
<div class="kpi-label">Total Oficios</div>
</div>
</div>
<div class="kpi-card kpi-danger">
<div class="kpi-icon"><i class="fa-solid fa-circle-xmark"></i></div>
<div class="kpi-info">
<div class="kpi-value"><?= number_format((int)($global['total_vencidos'] ?? 0)) ?></div>
<div class="kpi-label">Vencidos</div>
</div>
</div>
<div class="kpi-card kpi-warning">
<div class="kpi-icon"><i class="fa-solid fa-clock"></i></div>
<div class="kpi-info">
<div class="kpi-value"><?= number_format((int)($global['total_por_vencer'] ?? 0)) ?></div>
<div class="kpi-label">Por Vencer (≤3 días)</div>
</div>
</div>
<div class="kpi-card kpi-success">
<div class="kpi-icon"><i class="fa-solid fa-circle-check"></i></div>
<div class="kpi-info">
<div class="kpi-value"><?= number_format((int)($global['completados_este_mes'] ?? 0)) ?></div>
<div class="kpi-label">Completados (este mes)</div>
</div>
</div>
</div>
<!-- Charts + Próximos a vencer -->
<div class="grid-2 mb-4" style="grid-template-columns:1.3fr 1fr">
<!-- Estado por mes (línea) -->
<div class="card">
<div class="card-header">
<i class="fa-solid fa-chart-line text-primary"></i>
<span class="card-title">Tendencia últimos 6 meses</span>
</div>
<div class="card-body">
<canvas id="chartLinea" height="180"></canvas>
</div>
</div>
<!-- Donut de estados -->
<div class="card">
<div class="card-header">
<i class="fa-solid fa-chart-pie text-primary"></i>
<span class="card-title">Oficios por Estado</span>
</div>
<div class="card-body" style="display:flex;flex-direction:column;align-items:center;">
<canvas id="chartDonut" height="180" style="max-width:240px"></canvas>
</div>
</div>
</div>
<div class="grid-2 mb-4" style="grid-template-columns:1fr 1fr">
<!-- Barras por prioridad -->
<div class="card">
<div class="card-header">
<i class="fa-solid fa-chart-bar text-warning"></i>
<span class="card-title">Oficios por Prioridad</span>
</div>
<div class="card-body">
<canvas id="chartPrioridad" height="160"></canvas>
</div>
</div>
<!-- Próximos a vencer -->
<div class="card">
<div class="card-header">
<i class="fa-solid fa-triangle-exclamation text-warning"></i>
<span class="card-title">Próximos a Vencer</span>
<a href="<?= APP_URL ?>/views/oficios/lista.php?semaforo=proximo" class="btn btn-sm btn-secondary">Ver todos</a>
</div>
<div class="card-body" style="padding:0">
<?php if (empty($proximosVencer)): ?>
<div style="padding:1.5rem;text-align:center;color:var(--text-muted);font-size:.83rem">
<i class="fa-solid fa-circle-check text-success" style="font-size:1.5rem;display:block;margin-bottom:.5rem"></i>
Sin oficios próximos a vencer
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table">
<thead>
<tr><th>Nº Oficio</th><th>Vence</th><th>Días</th><th>Estado</th></tr>
</thead>
<tbody>
<?php foreach (array_slice($proximosVencer, 0, 6) as $ov):
$dias = (int)((new DateTime($ov['fecha_vencimiento']))->diff(new DateTime())->days);
$semaforoClass = $dias <= 0 ? 'semaforo-vencido' : ($dias <= 1 ? 'semaforo-proximo' : 'semaforo-vigente');
?>
<tr>
<td>
<a href="<?= APP_URL ?>/views/oficios/detalle.php?id=<?= $ov['id'] ?>" class="fw-600" style="color:var(--primary);text-decoration:none;">
<?= htmlspecialchars($ov['numero_oficio']) ?>
</a>
<div class="fs-sm text-muted"><?= mb_strimwidth(htmlspecialchars($ov['asunto']), 0, 35, '…') ?></div>
</td>
<td><?= date('d/m/Y', strtotime($ov['fecha_vencimiento'])) ?></td>
<td><span class="badge <?= $semaforoClass ?>"><?= $dias ?> días</span></td>
<td><span class="badge badge-warning"><?= ucfirst($ov['estado']) ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
</div><!-- /.page-content -->
<?php
// Preparar datos para Chart.js
$meses = [];
$serieCreados = [];
$serieRespondidos = [];
foreach ($estadisticasMens as $row) {
if (!in_array($row['mes'], $meses)) {
$meses[] = $row['mes'];
$serieCreados[$row['mes']] = 0;
$serieRespondidos[$row['mes']] = 0;
}
if ($row['estado'] === 'recibido' || $row['estado'] === 'en_proceso') {
$serieCreados[$row['mes']] += $row['total'];
}
if ($row['estado'] === 'respondido') {
$serieRespondidos[$row['mes']] += $row['total'];
}
}
$estadosLabels = array_keys($estados);
$estadosData = array_values($estados);
$prioLabels = array_keys($prioridades);
$prioData = array_values($prioridades);
?>
<script>
const appUrl = '<?= APP_URL ?>';
// ── Línea tendencia ──────────────────────────────────────────────────────────
const meses = <?= json_encode(array_values($meses)) ?>;
const creados = <?= json_encode(array_values($serieCreados)) ?>;
const respondidos = <?= json_encode(array_values($serieRespondidos)) ?>;
new Chart(document.getElementById('chartLinea'), {
type: 'line',
data: {
labels: meses,
datasets: [
{
label: 'Activos',
data: creados,
borderColor: '#2e7d32',
backgroundColor: 'rgba(46, 125, 125, .1)',
fill: true,
tension: .4,
pointRadius: 4,
pointBackgroundColor: '#2e7d32',
},
{
label: 'Respondidos',
data: respondidos,
borderColor: '#0288d1',
backgroundColor: 'rgba(2, 136, 209, .08)',
fill: true,
tension: .4,
pointRadius: 4,
pointBackgroundColor: '#0288d1',
}
]
},
options: {
responsive: true,
plugins: { legend: { position: 'bottom' } },
scales: {
x: { grid: { color: 'rgba(0,0,0,.04)' } },
y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,.04)' }, ticks: { precision: 0 } }
}
}
});
// ── Donut estados ────────────────────────────────────────────────────────────
const estadosLabels = <?= json_encode($estadosLabels) ?>;
const estadosData = <?= json_encode($estadosData) ?>;
const estadosColors = {
'recibido': '#2e7d32',
'en_proceso': '#fbc02d',
'respondido': '#0288d1',
'vencido': '#d32f2f',
'archivado': '#94a3b8'
};
new Chart(document.getElementById('chartDonut'), {
type: 'doughnut',
data: {
labels: estadosLabels.map(e => e.charAt(0).toUpperCase() + e.slice(1).replace('_',' ')),
datasets: [{
data: estadosData,
backgroundColor: estadosLabels.map(e => estadosColors[e] || '#94a3b8'),
borderWidth: 2,
borderColor: 'var(--bg-card)',
}]
},
options: {
cutout: '65%',
responsive: true,
plugins: { legend: { position: 'bottom' } }
}
});
// ── Barras prioridad ─────────────────────────────────────────────────────────
const prioLabels = <?= json_encode($prioLabels) ?>;
const prioData = <?= json_encode($prioData) ?>;
const prioColors = { alta: '#d32f2f', media: '#fbc02d', baja: '#388e3c' };
new Chart(document.getElementById('chartPrioridad'), {
type: 'bar',
data: {
labels: prioLabels.map(p => p.charAt(0).toUpperCase() + p.slice(1)),
datasets: [{
label: 'Oficios',
data: prioData,
backgroundColor: prioLabels.map(p => prioColors[p] || '#4f46e5'),
borderRadius: 6,
borderSkipped: false,
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, ticks: { precision: 0 } }
}
}
});
</script>
<?php include __DIR__ . '/views/layout/footer.php'; ?>