352 lines
17 KiB
PHP
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 => ({'<':'<','>':'>','&':'&','"':'"'}[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>
|