Files
whatsapp/lab_reportes.php
T
2026-03-10 18:42:51 -05:00

352 lines
17 KiB
PHP

<?php
/**
* Reportes y Trazabilidad — Módulo Administrativo de Laboratorio
*/
require_once 'config/config.php';
if (!isUserLoggedIn()) {
header('Location: login.php');
exit;
}
if (isEnfermero()) { header('Location: enfermero_portal.php'); exit; }
// ── Exportación CSV directa (sin cabeceras HTML) ──────────────────────────
$tipo = $_GET['export'] ?? '';
if ($tipo) {
require_once 'classes/Database.php';
$db = Database::getInstance();
$desde = $_GET['desde'] ?? date('Y-m-01');
$hasta = $_GET['hasta'] ?? date('Y-m-d');
switch ($tipo) {
case 'ordenes':
$rows = $db->fetchAll(
"SELECT o.id, p.nombre_completo AS paciente, p.numero_documento, p.eps,
o.estado, o.medico_nombre, o.fecha_orden, o.examenes_solicitados,
o.requiere_ayuno, o.horas_ayuno, o.created_at,
ar.full_name AS revisada_por, aa.full_name AS autorizada_por
FROM lab_ordenes_medicas o
JOIN lab_pacientes p ON p.id = o.paciente_id
LEFT JOIN admin_users ar ON ar.id = o.revisada_por
LEFT JOIN admin_users aa ON aa.id = o.autorizada_por
WHERE DATE(o.created_at) BETWEEN ? AND ?
ORDER BY o.created_at DESC",
[$desde, $hasta]
);
$filename = "ordenes_$desde\_$hasta.csv";
$headers = ['ID','Paciente','Documento','EPS','Estado','Médico','Fecha Orden','Exámenes','Ayuno','H.Ayuno','Creada','Revisada por','Autorizada por'];
break;
case 'domicilios':
$rows = $db->fetchAll(
"SELECT d.id, p.nombre_completo AS paciente, p.telefono AS tel_paciente,
d.direccion, d.barrio, d.ciudad, d.fecha_programada, d.hora_programada,
d.estado, COALESCE(e.nombre_completo,'Sin asignar') AS enfermera,
d.tipo_servicio, d.created_at
FROM lab_domicilios d
JOIN lab_pacientes p ON p.id = d.paciente_id
LEFT JOIN lab_asignaciones a ON a.domicilio_id = d.id AND a.estado NOT IN ('liberada')
LEFT JOIN lab_enfermeras e ON e.id = a.enfermera_id
WHERE DATE(d.fecha_programada) BETWEEN ? AND ?
ORDER BY d.fecha_programada, d.hora_programada",
[$desde, $hasta]
);
$filename = "domicilios_$desde\_$hasta.csv";
$headers = ['ID','Paciente','Teléfono','Dirección','Barrio','Ciudad','Fecha','Hora','Estado','Enfermera','Tipo Servicio','Registrado'];
break;
case 'pacientes':
$rows = $db->fetchAll(
"SELECT p.id, p.nombre_completo, p.tipo_documento, p.numero_documento,
p.telefono, p.email, p.eps, p.ciudad, p.barrio, p.is_active,
COUNT(o.id) AS total_ordenes, p.created_at
FROM lab_pacientes p
LEFT JOIN lab_ordenes_medicas o ON o.paciente_id = p.id
GROUP BY p.id
ORDER BY p.nombre_completo",
[]
);
$filename = "pacientes_" . date('Y-m-d') . ".csv";
$headers = ['ID','Nombre','Tipo Doc.','Documento','Teléfono','Email','EPS','Ciudad','Barrio','Activo','Total Órdenes','Registrado'];
break;
default:
http_response_code(400);
exit('Tipo de exportación no válido');
}
header('Content-Type: text/csv; charset=utf-8');
header("Content-Disposition: attachment; filename=\"$filename\"");
header('Pragma: no-cache');
$f = fopen('php://output', 'w');
fputs($f, "\xEF\xBB\xBF"); // BOM UTF-8 para Excel
fputcsv($f, $headers);
foreach ($rows as $row) {
fputcsv($f, array_values($row));
}
fclose($f);
exit;
}
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Admin';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reportes — Módulo Lab</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="./assets/css/styles.css?v=12" rel="stylesheet">
<style>
.log-item { font-size:.82rem; padding:6px 0; border-bottom:1px solid #f3f3f3; }
.log-item:last-child { border-bottom:none; }
.badge-modulo { font-size:.7rem; }
</style>
</head>
<body>
<nav class="sidebar">
<div class="sidebar-header"><h4><i class="fas fa-flask"></i> Módulo Lab</h4></div>
<ul class="sidebar-menu">
<li><a href="lab_dashboard.php" class="nav-link"><i class="fas fa-tachometer-alt"></i> Dashboard</a></li>
<li><a href="lab_ordenes.php" class="nav-link"><i class="fas fa-file-medical"></i> Órdenes</a></li>
<li><a href="lab_pacientes.php" class="nav-link"><i class="fas fa-users"></i> Pacientes</a></li>
<li><a href="lab_domicilios.php" class="nav-link"><i class="fas fa-house-medical"></i> Domicilios</a></li>
<li><a href="lab_enfermeras.php" class="nav-link"><i class="fas fa-user-nurse"></i> Enfermeras</a></li>
<li><a href="lab_formularios.php" class="nav-link"><i class="fas fa-wpforms"></i> Formularios</a></li>
<li><a href="lab_reportes.php" class="nav-link active"><i class="fas fa-chart-bar"></i> Reportes</a></li>
<li><a href="lab_configuracion.php" class="nav-link"><i class="fas fa-sliders-h"></i> Configuración</a></li>
<li><a href="lab_usuarios.php" class="nav-link"><i class="fas fa-users-cog"></i> Usuarios</a></li>
<li class="sidebar-divider"></li>
<li><a href="index.php" class="nav-link"><i class="fas fa-arrow-left"></i> Volver al Bot</a></li>
<li><a href="logout.php" class="nav-link logout-link" onclick="return confirm('¿Cerrar sesión?')"><i class="fas fa-sign-out-alt"></i> Cerrar Sesión</a></li>
</ul>
</nav>
<main class="main-content">
<header class="content-header d-flex align-items-center justify-content-between">
<div>
<h1><i class="fas fa-chart-bar text-primary"></i> Reportes y Trazabilidad</h1>
<small class="text-muted"><?= htmlspecialchars($adminNombre) ?></small>
</div>
</header>
<div class="container-fluid py-3">
<!-- ── Rango de fechas ──────────────────────────────────────────── -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body d-flex flex-wrap gap-3 align-items-end">
<div>
<label class="form-label small text-muted text-uppercase">Desde</label>
<input type="date" id="rpt-desde" class="form-control form-control-sm" value="<?= date('Y-m-01') ?>">
</div>
<div>
<label class="form-label small text-muted text-uppercase">Hasta</label>
<input type="date" id="rpt-hasta" class="form-control form-control-sm" value="<?= date('Y-m-d') ?>">
</div>
<button class="btn btn-primary btn-sm" onclick="cargarTodo()">
<i class="fas fa-sync-alt me-1"></i> Actualizar
</button>
<div class="ms-auto d-flex gap-2 flex-wrap">
<button class="btn btn-sm btn-outline-success" onclick="exportar('ordenes')">
<i class="fas fa-file-csv me-1"></i> Exportar Órdenes
</button>
<button class="btn btn-sm btn-outline-primary" onclick="exportar('domicilios')">
<i class="fas fa-file-csv me-1"></i> Exportar Domicilios
</button>
<button class="btn btn-sm btn-outline-secondary" onclick="exportar('pacientes')">
<i class="fas fa-file-csv me-1"></i> Exportar Pacientes
</button>
</div>
</div>
</div>
<div class="row g-3">
<!-- ── Resumen estadístico ──────────────────────────────────── -->
<div class="col-lg-8">
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-0">
<h6 class="fw-semibold mb-0"><i class="fas fa-chart-pie me-2 text-primary"></i>Resumen del período</h6>
</div>
<div class="card-body" id="resumen-stats">
<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin me-2"></i>Cargando...</div>
</div>
</div>
<!-- Órdenes por estado -->
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white border-0">
<h6 class="fw-semibold mb-0"><i class="fas fa-file-medical me-2 text-warning"></i>Órdenes por estado</h6>
</div>
<div class="card-body" id="ordenes-estado-chart">
<div class="text-center py-3 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
</div>
</div>
<!-- Domicilios por estado -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0">
<h6 class="fw-semibold mb-0"><i class="fas fa-house-medical me-2 text-success"></i>Domicilios por estado</h6>
</div>
<div class="card-body" id="domicilios-estado-chart">
<div class="text-center py-3 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
</div>
</div>
</div>
<!-- ── Log de actividad ─────────────────────────────────────── -->
<div class="col-lg-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white border-0 d-flex align-items-center justify-content-between">
<h6 class="fw-semibold mb-0"><i class="fas fa-history me-2 text-info"></i>Log de actividad</h6>
<div>
<select id="log-modulo" class="form-select form-select-sm" onchange="cargarActividad(1)">
<option value="">Todos los módulos</option>
<option value="ordenes">Órdenes</option>
<option value="domicilios">Domicilios</option>
<option value="pacientes">Pacientes</option>
<option value="enfermeras">Enfermeras</option>
</select>
</div>
</div>
<div class="card-body pb-0" id="log-body" style="max-height:520px;overflow-y:auto">
<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
</div>
<div class="card-footer bg-white border-0" id="log-paginacion"></div>
</div>
</div>
</div>
</div><!-- /container -->
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const COLOR = { pendiente:'warning', en_revision:'info', autorizada:'success', rechazada:'danger', en_domicilio:'primary', completada:'secondary', programado:'secondary', confirmado:'info', en_camino:'primary', cancelado:'danger' };
const MODULO_COLOR = { ordenes:'warning', domicilios:'success', pacientes:'primary', enfermeras:'info', asignaciones:'secondary' };
let logPag = 1;
async function cargarTodo() {
await Promise.all([cargarResumen(), cargarActividad(1)]);
}
async function cargarResumen() {
const r = await fetch(`api/lab/get_stats.php?desde=${fecha('rpt-desde')}&hasta=${fecha('rpt-hasta')}`);
const d = await r.json();
if (!d.success) return;
// Resumen general
const g = d.generales || {};
document.getElementById('resumen-stats').innerHTML = `
<div class="row g-3">
<div class="col-6 col-md-3 text-center">
<div class="fs-4 fw-bold text-warning">${d.ordenes?.total_periodo||g.total_ordenes||0}</div>
<div class="small text-muted">Órdenes</div>
</div>
<div class="col-6 col-md-3 text-center">
<div class="fs-4 fw-bold text-success">${d.ordenes?.autorizadas_periodo||0}</div>
<div class="small text-muted">Autorizadas</div>
</div>
<div class="col-6 col-md-3 text-center">
<div class="fs-4 fw-bold text-primary">${d.domicilios?.total_periodo||g.total_domicilios||0}</div>
<div class="small text-muted">Domicilios</div>
</div>
<div class="col-6 col-md-3 text-center">
<div class="fs-4 fw-bold text-info">${g.total_pacientes||0}</div>
<div class="small text-muted">Pacientes</div>
</div>
</div>`;
// Barras de órdenes por estado
renderBarras('ordenes-estado-chart', d.ordenes?.por_estado || {});
// Barras de domicilios por estado
renderBarras('domicilios-estado-chart', d.domicilios?.por_estado || {});
}
function renderBarras(elId, datos) {
const el = document.getElementById(elId);
const total = Object.values(datos).reduce((a,b) => a + (b||0), 0) || 1;
if (!Object.keys(datos).length) {
el.innerHTML = '<p class="text-muted small text-center">Sin datos</p>'; return;
}
el.innerHTML = Object.entries(datos).map(([estado, n]) => `
<div class="d-flex align-items-center gap-2 mb-2">
<div class="text-end" style="min-width:90px">
<span class="badge bg-${COLOR[estado]||'secondary'}">${esc(estado)}</span>
</div>
<div class="flex-grow-1 bg-light rounded" style="height:16px">
<div class="rounded h-100 bg-${COLOR[estado]||'secondary'}" style="width:${Math.max((n/total)*100,2)}%;transition:width .3s"></div>
</div>
<div style="min-width:28px" class="text-end fw-bold small">${n}</div>
</div>
`).join('');
}
async function cargarActividad(pag = 1) {
logPag = pag;
const modulo = document.getElementById('log-modulo').value;
const params = new URLSearchParams({
page: pag, limit: 30,
modulo,
desde: fecha('rpt-desde'),
hasta: fecha('rpt-hasta'),
});
const r = await fetch(`api/lab/get_actividad.php?${params}`);
const d = await r.json();
const el = document.getElementById('log-body');
if (!d.data?.length) {
el.innerHTML = '<p class="text-muted text-center py-3 small">Sin actividad en este período</p>';
document.getElementById('log-paginacion').innerHTML = '';
return;
}
el.innerHTML = d.data.map(a => `
<div class="log-item">
<div class="d-flex align-items-center gap-2">
<span class="badge bg-${MODULO_COLOR[a.modulo]||'secondary'} badge-modulo">${esc(a.modulo)}</span>
<span class="fw-semibold small">${esc(a.admin_nombre||a.admin_username||'Sistema')}</span>
</div>
<div class="text-muted" style="font-size:.78rem">${esc(a.accion)} ${a.entidad_id ? `#${a.entidad_id}` : ''}</div>
<div class="text-muted" style="font-size:.74rem">${formatFecha(a.created_at)}</div>
</div>
`).join('');
// Paginación mínima
const { paginas, total } = d;
const pagEl = document.getElementById('log-paginacion');
if (paginas > 1) {
pagEl.innerHTML = `
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">${total} eventos</small>
<div class="btn-group btn-group-sm">
${pag>1?`<button class="btn btn-outline-secondary" onclick="cargarActividad(${pag-1})"><i class="fas fa-chevron-left"></i></button>`:''}
<button class="btn btn-secondary disabled">${pag}/${paginas}</button>
${pag<paginas?`<button class="btn btn-outline-secondary" onclick="cargarActividad(${pag+1})"><i class="fas fa-chevron-right"></i></button>`:''}
</div>
</div>`;
} else {
pagEl.innerHTML = `<small class="text-muted">${total} evento(s)</small>`;
}
}
function exportar(tipo) {
const url = `lab_reportes.php?export=${tipo}&desde=${fecha('rpt-desde')}&hasta=${fecha('rpt-hasta')}`;
window.open(url, '_blank');
}
// ── Utils ──────────────────────────────────────────────────────────────────
const esc = s => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
const fecha = id => document.getElementById(id)?.value || '';
const formatFecha = s => s ? new Date(s).toLocaleString('es-CO', {day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'}) : '—';
cargarTodo();
</script>
</body>
</html>