884 lines
47 KiB
PHP
884 lines
47 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':
|
||
// Soporta filtros extra enviados desde lab_domicilios.php
|
||
$sqlWhere = 'DATE(d.fecha_programada) BETWEEN ? AND ?';
|
||
$sqlParams = [$desde, $hasta];
|
||
// Si viene fecha única (filtro de día), tiene prioridad sobre desde/hasta
|
||
if (!empty($_GET['fecha'])) {
|
||
$sqlWhere = 'd.fecha_programada = ?';
|
||
$sqlParams = [$_GET['fecha']];
|
||
}
|
||
if (!empty($_GET['estado'])) {
|
||
$sqlWhere .= ' AND d.estado = ?';
|
||
$sqlParams[] = $_GET['estado'];
|
||
}
|
||
if (!empty($_GET['enfermera_id'])) {
|
||
$sqlWhere .= ' AND a.enfermera_id = ?';
|
||
$sqlParams[] = (int)$_GET['enfermera_id'];
|
||
}
|
||
$rows = $db->fetchAll(
|
||
"SELECT d.id,
|
||
p.nombre_completo AS paciente,
|
||
p.numero_documento,
|
||
p.telefono AS tel_paciente,
|
||
p.eps,
|
||
d.fecha_programada,
|
||
d.hora_programada,
|
||
d.hora_llegada,
|
||
d.hora_salida,
|
||
CASE
|
||
WHEN d.hora_llegada IS NOT NULL AND d.hora_salida IS NOT NULL
|
||
THEN CONCAT(TIMESTAMPDIFF(MINUTE, d.hora_llegada, d.hora_salida), ' min')
|
||
ELSE ''
|
||
END AS duracion_domicilio,
|
||
d.estado,
|
||
COALESCE(e.nombre_completo,'Sin asignar') AS enfermera,
|
||
d.tipo_servicio,
|
||
d.tipo_cliente,
|
||
COALESCE(d.seguro_nombre,'') AS seguro,
|
||
d.autorizacion,
|
||
d.examenes_solicitados,
|
||
d.direccion,
|
||
d.barrio,
|
||
d.ciudad,
|
||
COALESCE(d.valor_domicilio,0) AS valor_domicilio,
|
||
COALESCE(d.valor_copago,0) AS valor_copago,
|
||
COALESCE(d.copago_laboratorio,0) AS copago_laboratorio,
|
||
d.pago_estado,
|
||
d.pago_modo,
|
||
COALESCE(d.pago_monto,0) AS monto_recibido,
|
||
d.observaciones,
|
||
d.created_at,
|
||
COALESCE(nc.antecedentes,'') AS antecedentes,
|
||
COALESCE(nc.medicamentos,'') AS medicamentos,
|
||
COALESCE(nc.acudiente_nombre,'') AS acudiente_nombre,
|
||
COALESCE(nc.acudiente_documento,'') AS acudiente_documento,
|
||
COALESCE(
|
||
(SELECT GROUP_CONCAT(CONCAT(COALESCE(nl.titulo,''), ': ', COALESCE(nl.cuerpo,''))
|
||
ORDER BY nl.created_at ASC SEPARATOR ' | ')
|
||
FROM lab_domicilio_notas nl
|
||
WHERE nl.domicilio_id = d.id AND nl.tipo = 'libre'),
|
||
'') AS notas_libres
|
||
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
|
||
LEFT JOIN lab_domicilio_notas nc ON nc.domicilio_id = d.id AND nc.tipo = 'clinica'
|
||
WHERE $sqlWhere
|
||
ORDER BY d.fecha_programada ASC, d.hora_programada ASC",
|
||
$sqlParams
|
||
);
|
||
$rangoLabel = !empty($_GET['fecha']) ? $_GET['fecha'] : "{$desde}_{$hasta}";
|
||
$filename = "domicilios_{$rangoLabel}.csv";
|
||
$headers = [
|
||
'ID','Paciente','Documento','Teléfono','EPS',
|
||
'Fecha','Hora programada','Hora llegada','Hora salida','Duración',
|
||
'Estado','Enfermera','Tipo servicio','Tipo cliente','Seguro','Autorización',
|
||
'Exámenes','Dirección','Barrio','Ciudad',
|
||
'Valor domicilio','Valor copago','Copago lab',
|
||
'Pago estado','Pago modo','Monto recibido',
|
||
'Observaciones','Registrado',
|
||
'Antecedentes','Medicamentos','Acudiente nombre','Acudiente documento','Notas libres',
|
||
];
|
||
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; }
|
||
.kpi-card { border-radius:12px; padding:16px 14px; text-align:center; }
|
||
.kpi-num { font-size:1.9rem; font-weight:700; line-height:1.1; }
|
||
.kpi-lbl { font-size:.78rem; color:#6c757d; margin-top:4px; }
|
||
.bar-row { display:flex; align-items:center; gap:8px; margin-bottom:8px; }
|
||
.bar-label { min-width:110px; font-size:.8rem; text-align:right; white-space:nowrap;overflow:hidden;text-overflow:ellipsis; }
|
||
.bar-track { flex:1; background:#f1f3f5; border-radius:4px; height:16px; }
|
||
.bar-fill { height:100%; border-radius:4px; transition:width .4s; }
|
||
.bar-val { min-width:36px; font-size:.8rem; font-weight:600; text-align:right; }
|
||
.trend-row { display:flex; align-items:flex-end; gap:3px; height:60px; }
|
||
.trend-bar { flex:1; border-radius:3px 3px 0 0; min-width:6px; transition:height .4s; }
|
||
.trend-bar:hover { opacity:.8; }
|
||
.ranking-row { display:flex; align-items:center; gap:10px; padding:6px 0; border-bottom:1px solid #f5f5f5; }
|
||
.ranking-row:last-child { border-bottom:none; }
|
||
</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_actividad.php" class="nav-link"><i class="fas fa-history"></i> Actividad</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 Métricas</h1>
|
||
<small class="text-muted"><?= htmlspecialchars($adminNombre) ?></small>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="container-fluid py-3">
|
||
|
||
<!-- ── Rango de fechas + exportaciones ──────────────────────────── -->
|
||
<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('domicilios')">
|
||
<i class="fas fa-file-excel me-1"></i> Excel Domicilios
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-warning" onclick="exportar('ordenes')">
|
||
<i class="fas fa-file-excel me-1"></i> Excel Órdenes
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-secondary" onclick="exportar('pacientes')">
|
||
<i class="fas fa-file-excel me-1"></i> Excel Pacientes
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<!-- Reporte domicilios por enfermero -->
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<div class="card border-0 shadow-sm mb-4">
|
||
<div class="card-header bg-white border-0 d-flex flex-wrap gap-2 align-items-end">
|
||
<h6 class="fw-semibold mb-0 me-auto">
|
||
<i class="fas fa-user-nurse me-2 text-success"></i>Domicilios por enfermero
|
||
</h6>
|
||
<div class="d-flex flex-wrap gap-2 align-items-end">
|
||
<div>
|
||
<label class="form-label small text-muted text-uppercase mb-1">Año</label>
|
||
<input type="number" id="enf-anio" class="form-control form-control-sm"
|
||
value="<?= date('Y') ?>" min="2020" max="2099" style="width:90px">
|
||
</div>
|
||
<div>
|
||
<label class="form-label small text-muted text-uppercase mb-1">Mes</label>
|
||
<select id="enf-mes" class="form-select form-select-sm" style="width:100px">
|
||
<option value="">Todos</option>
|
||
<option value="1">Enero</option><option value="2">Febrero</option>
|
||
<option value="3">Marzo</option><option value="4">Abril</option>
|
||
<option value="5">Mayo</option><option value="6">Junio</option>
|
||
<option value="7">Julio</option><option value="8">Agosto</option>
|
||
<option value="9">Septiembre</option><option value="10">Octubre</option>
|
||
<option value="11">Noviembre</option><option value="12">Diciembre</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="form-label small text-muted text-uppercase mb-1">Enfermero/a</label>
|
||
<select id="enf-selector" class="form-select form-select-sm" style="min-width:160px">
|
||
<option value="">Todos</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" onclick="cargarReporteEnfermero()">
|
||
<i class="fas fa-sync-alt me-1"></i>Filtrar
|
||
</button>
|
||
<button class="btn btn-sm btn-outline-success" onclick="exportarReporteEnfermero()">
|
||
<i class="fas fa-file-excel me-1"></i>Excel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<div id="enf-tabla-wrap" style="overflow-x:auto">
|
||
<div class="text-center py-4 text-muted">
|
||
<i class="fas fa-spinner fa-spin"></i> Cargando…
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<!-- KPIs principales -->
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<div class="row g-3 mb-3" id="kpis-row">
|
||
<div class="col-6 col-md-3">
|
||
<div class="kpi-card" style="background:#fff8e1;border:1px solid #ffe082">
|
||
<div class="kpi-num text-warning" id="kpi-total">—</div>
|
||
<div class="kpi-lbl">Total domicilios</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-3">
|
||
<div class="kpi-card" style="background:#e8f5e9;border:1px solid #a5d6a7">
|
||
<div class="kpi-num text-success" id="kpi-tasa">—</div>
|
||
<div class="kpi-lbl">Tasa de éxito</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-3">
|
||
<div class="kpi-card" style="background:#e3f2fd;border:1px solid #90caf9">
|
||
<div class="kpi-num text-primary" id="kpi-duracion">—</div>
|
||
<div class="kpi-lbl">Duración promedio</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-3">
|
||
<div class="kpi-card" style="background:#f3e5f5;border:1px solid #ce93d8">
|
||
<div class="kpi-num" style="color:#7b1fa2" id="kpi-cobrado">—</div>
|
||
<div class="kpi-lbl">Total cobrado</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<!-- Fila 1: Tendencia por día + Tasa de éxito -->
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-lg-8">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-header bg-white border-0 d-flex justify-content-between align-items-center">
|
||
<h6 class="fw-semibold mb-0"><i class="fas fa-chart-bar me-2 text-primary"></i>Domicilios por día</h6>
|
||
<small class="text-muted" id="trend-subtitulo"></small>
|
||
</div>
|
||
<div class="card-body" id="trend-chart">
|
||
<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-header bg-white border-0">
|
||
<h6 class="fw-semibold mb-0"><i class="fas fa-chart-pie me-2 text-success"></i>Estado de domicilios</h6>
|
||
</div>
|
||
<div class="card-body" id="dom-estado-chart">
|
||
<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<!-- Fila 2: Tiempo en domicilio + Hora pico -->
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-lg-4">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-header bg-white border-0">
|
||
<h6 class="fw-semibold mb-0"><i class="fas fa-stopwatch me-2 text-info"></i>Tiempo en domicilio</h6>
|
||
</div>
|
||
<div class="card-body" id="tiempos-card">
|
||
<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-header bg-white border-0">
|
||
<h6 class="fw-semibold mb-0"><i class="fas fa-clock me-2 text-warning"></i>Horas pico del día</h6>
|
||
</div>
|
||
<div class="card-body" id="hora-chart">
|
||
<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<!-- Fila 3: Ranking enfermeras + Financiero + Zonas -->
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<div class="row g-3 mb-3">
|
||
<div class="col-lg-4">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-header bg-white border-0">
|
||
<h6 class="fw-semibold mb-0"><i class="fas fa-user-nurse me-2 text-success"></i>Ranking enfermeras</h6>
|
||
</div>
|
||
<div class="card-body" id="ranking-enf">
|
||
<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-header bg-white border-0">
|
||
<h6 class="fw-semibold mb-0"><i class="fas fa-dollar-sign me-2" style="color:#7b1fa2"></i>Resumen financiero</h6>
|
||
</div>
|
||
<div class="card-body" id="financiero-card">
|
||
<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-header bg-white border-0">
|
||
<h6 class="fw-semibold mb-0"><i class="fas fa-map-marker-alt me-2 text-danger"></i>Top zonas</h6>
|
||
</div>
|
||
<div class="card-body" id="zonas-chart">
|
||
<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<!-- Fila 4: Top exámenes + Tipo cliente + Log actividad -->
|
||
<!-- ══════════════════════════════════════════════════════════════ -->
|
||
<div class="row g-3">
|
||
<div class="col-lg-4">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-header bg-white border-0">
|
||
<h6 class="fw-semibold mb-0"><i class="fas fa-vials me-2 text-info"></i>Top exámenes solicitados</h6>
|
||
</div>
|
||
<div class="card-body" id="examenes-chart">
|
||
<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="card border-0 shadow-sm h-100">
|
||
<div class="card-header bg-white border-0">
|
||
<h6 class="fw-semibold mb-0"><i class="fas fa-shield-alt me-2 text-primary"></i>Tipo de cliente</h6>
|
||
</div>
|
||
<div class="card-body" id="tipo-cliente-chart">
|
||
<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<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-secondary"></i>Log de actividad</h6>
|
||
<select id="log-modulo" class="form-select form-select-sm w-auto" onchange="cargarActividad(1)">
|
||
<option value="">Todos</option>
|
||
<option value="ordenes">Órdenes</option>
|
||
<option value="domicilios">Domicilios</option>
|
||
<option value="pacientes">Pacientes</option>
|
||
<option value="enfermeras">Enfermeras</option>
|
||
</select>
|
||
</div>
|
||
<div class="card-body pb-0" id="log-body" style="max-height:380px;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_EST = {
|
||
pendiente:'warning', en_revision:'info', autorizada:'success', rechazada:'danger',
|
||
en_domicilio:'primary', completada:'secondary', completado:'success',
|
||
programado:'secondary', confirmado:'info', en_camino:'primary', cancelado:'danger', reprogramado:'dark',
|
||
};
|
||
const MODULO_COLOR = { ordenes:'warning', domicilios:'success', pacientes:'primary', enfermeras:'info', asignaciones:'secondary' };
|
||
let logPag = 1;
|
||
|
||
// ── Reporte por enfermero ───────────────────────────────────────────────────
|
||
async function cargarReporteEnfermero() {
|
||
const wrap = document.getElementById('enf-tabla-wrap');
|
||
wrap.innerHTML = '<div class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin"></i></div>';
|
||
|
||
const anio = document.getElementById('enf-anio').value;
|
||
const mes = document.getElementById('enf-mes').value;
|
||
const enf = document.getElementById('enf-selector').value;
|
||
|
||
const params = new URLSearchParams();
|
||
if (anio) params.set('anio', anio);
|
||
if (mes) params.set('mes', mes);
|
||
if (enf) params.set('enfermera_id', enf);
|
||
|
||
try {
|
||
const r = await fetch(`api/lab/get_reporte_enfermero.php?${params}`);
|
||
const d = await r.json();
|
||
if (!d.success) throw new Error(d.error || 'Error');
|
||
|
||
// Poblar selector de enfermeras (sólo la primera vez)
|
||
const sel = document.getElementById('enf-selector');
|
||
if (sel.options.length === 1 && d.enfermeras?.length) {
|
||
d.enfermeras.forEach(e => {
|
||
const opt = document.createElement('option');
|
||
opt.value = e.id;
|
||
opt.textContent = e.nombre_completo;
|
||
sel.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
if (!d.filas?.length) {
|
||
wrap.innerHTML = '<p class="text-muted text-center py-3 small">Sin datos para los filtros seleccionados.</p>';
|
||
return;
|
||
}
|
||
|
||
wrap.innerHTML = `
|
||
<table class="table table-sm table-hover mb-0" style="font-size:.85rem">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Enfermero/a</th>
|
||
<th class="text-center">Período</th>
|
||
<th class="text-center">Total</th>
|
||
<th class="text-center text-success">Completados</th>
|
||
<th class="text-center text-danger">Cancelados</th>
|
||
<th class="text-center text-warning">En proceso</th>
|
||
<th class="text-center">Tasa éxito</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${d.filas.map(f => {
|
||
const color = f.tasa_pct >= 80 ? '#43a047' : f.tasa_pct >= 50 ? '#fb8c00' : '#e53935';
|
||
return `<tr>
|
||
<td class="fw-semibold">${esc(f.enfermera)}</td>
|
||
<td class="text-center text-muted">${esc(f.mes_nombre)} ${f.anio}</td>
|
||
<td class="text-center">${f.total}</td>
|
||
<td class="text-center text-success">${f.completados}</td>
|
||
<td class="text-center text-danger">${f.cancelados}</td>
|
||
<td class="text-center text-warning">${f.en_proceso}</td>
|
||
<td class="text-center">
|
||
<span class="fw-bold" style="color:${color}">${f.tasa_pct}%</span>
|
||
<div style="width:60px;height:5px;background:#f0f0f0;border-radius:3px;margin:2px auto 0">
|
||
<div style="width:${f.tasa_pct}%;height:100%;background:${color};border-radius:3px"></div>
|
||
</div>
|
||
</td>
|
||
</tr>`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>`;
|
||
} catch (e) {
|
||
wrap.innerHTML = `<div class="alert alert-danger m-3 small">${esc(e.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
function exportarReporteEnfermero() {
|
||
const anio = document.getElementById('enf-anio').value;
|
||
const mes = document.getElementById('enf-mes').value;
|
||
const enf = document.getElementById('enf-selector').value;
|
||
const params = new URLSearchParams({ export: 'domicilios', desde: `${anio || '2020'}-${mes ? String(mes).padStart(2,'0') : '01'}-01` });
|
||
if (mes) params.set('hasta', `${anio || new Date().getFullYear()}-${String(mes).padStart(2,'0')}-31`);
|
||
else params.set('hasta', `${anio || new Date().getFullYear()}-12-31`);
|
||
if (enf) params.set('enfermera_id', enf);
|
||
window.open(`lab_reportes.php?${params}`, '_blank');
|
||
}
|
||
|
||
// ── Carga todo en paralelo ──────────────────────────────────────────────────
|
||
async function cargarTodo() {
|
||
const desde = fecha('rpt-desde');
|
||
const hasta = fecha('rpt-hasta');
|
||
await Promise.all([
|
||
cargarMetricas(desde, hasta),
|
||
cargarResumenOrdenes(desde, hasta),
|
||
cargarActividad(1),
|
||
cargarReporteEnfermero(),
|
||
]);
|
||
}
|
||
|
||
// ── Métricas de domicilios ──────────────────────────────────────────────────
|
||
async function cargarMetricas(desde, hasta) {
|
||
const r = await fetch(`api/lab/get_metricas.php?desde=${desde}&hasta=${hasta}`);
|
||
const d = await r.json();
|
||
if (!d.success) return;
|
||
const m = d; // jsonOk() hace array_merge → datos en nivel raíz
|
||
|
||
// ── KPIs ──
|
||
const kpi = m.kpi || {};
|
||
const total = parseInt(kpi.total) || 0;
|
||
const comp = parseInt(kpi.completados) || 0;
|
||
const canc = parseInt(kpi.cancelados) || 0;
|
||
const tasa = total ? Math.round(comp / total * 100) : 0;
|
||
document.getElementById('kpi-total').textContent = total;
|
||
document.getElementById('kpi-tasa').textContent = tasa + '%';
|
||
// Duración
|
||
const t = m.tiempos || {};
|
||
const prom = parseFloat(t.promedio_min) || 0;
|
||
document.getElementById('kpi-duracion').textContent = prom ? fmtMin(prom) : '—';
|
||
// Financiero KPI
|
||
const fin = m.financiero || {};
|
||
document.getElementById('kpi-cobrado').textContent = fmtMoney(fin.cobrado);
|
||
|
||
// ── Tendencia por día ──
|
||
renderTrend(m.por_dia || []);
|
||
|
||
// ── Estado domicilios ──
|
||
const estMap = {};
|
||
(m.kpi ? [] : []).forEach(() => {});
|
||
// Reconstruir desde por_dia
|
||
if (m.kpi) {
|
||
estMap.completado = comp;
|
||
estMap.cancelado = canc;
|
||
estMap.reprogramado = parseInt(kpi.reprogramados) || 0;
|
||
estMap.en_curso = parseInt(kpi.en_curso) || 0;
|
||
}
|
||
renderBarras('dom-estado-chart', estMap);
|
||
|
||
// ── Tiempos ──
|
||
renderTiempos(t, m.tiempo_agendamiento || {});
|
||
|
||
// ── Hora pico ──
|
||
renderHoras(m.por_hora || []);
|
||
|
||
// ── Ranking enfermeras ──
|
||
renderRanking(m.ranking_enfermeras || []);
|
||
|
||
// ── Financiero ──
|
||
renderFinanciero(fin, m.por_modo_pago || []);
|
||
|
||
// ── Zonas ──
|
||
renderBarrasSimples('zonas-chart', (m.top_zonas||[]).map(z => [z.zona, z.n]), '#e53935');
|
||
|
||
// ── Exámenes ──
|
||
renderBarrasSimples('examenes-chart', (m.top_examenes||[]).map(e => [e.examen, e.n]), '#0288d1');
|
||
|
||
// ── Tipo cliente ──
|
||
const tcMap = {};
|
||
(m.por_tipo_cliente || []).forEach(tc => {
|
||
const k = tc.tipo === 'seguro' && tc.seguro ? `seguro: ${tc.seguro}` : tc.tipo;
|
||
tcMap[k] = (tcMap[k] || 0) + parseInt(tc.n);
|
||
});
|
||
renderBarrasSimples('tipo-cliente-chart', Object.entries(tcMap), '#6d4c41');
|
||
}
|
||
|
||
// ── Órdenes (usa get_stats existente) ──────────────────────────────────────
|
||
async function cargarResumenOrdenes(desde, hasta) {
|
||
const r = await fetch(`api/lab/get_stats.php?desde=${desde}&hasta=${hasta}`);
|
||
const d = await r.json();
|
||
if (!d.success) return;
|
||
renderBarras('ordenes-estado-chart', d.ordenes?.por_estado || {});
|
||
}
|
||
|
||
// ── Render helpers ──────────────────────────────────────────────────────────
|
||
function renderTrend(dias) {
|
||
const el = document.getElementById('trend-chart');
|
||
if (!dias.length) { el.innerHTML = '<p class="text-muted small text-center py-3">Sin datos en el período</p>'; return; }
|
||
|
||
const maxN = Math.max(...dias.map(d => parseInt(d.total) || 0)) || 1;
|
||
document.getElementById('trend-subtitulo').textContent =
|
||
`${dias.length} día(s) · total ${dias.reduce((s,d) => s + parseInt(d.total||0), 0)}`;
|
||
|
||
el.innerHTML = `
|
||
<div style="overflow-x:auto">
|
||
<div style="display:flex;align-items:flex-end;gap:4px;min-height:90px;padding-bottom:20px;min-width:${Math.max(dias.length*26, 200)}px">
|
||
${dias.map(d => {
|
||
const tot = parseInt(d.total) || 0;
|
||
const com = parseInt(d.completados) || 0;
|
||
const can = parseInt(d.cancelados) || 0;
|
||
const h = Math.max(Math.round((tot / maxN) * 80), 4);
|
||
const pct = tot ? Math.round(com/tot*100) : 0;
|
||
return `<div style="flex:1;min-width:22px;display:flex;flex-direction:column;align-items:center;gap:2px"
|
||
title="${d.dia} Total: ${tot} Completados: ${com} Cancelados: ${can}">
|
||
<div style="font-size:.65rem;color:#666">${tot}</div>
|
||
<div style="height:${h}px;width:100%;border-radius:3px 3px 0 0;
|
||
background:${pct>=80?'#43a047':pct>=50?'#fb8c00':'#e53935'};opacity:.85"></div>
|
||
<div style="font-size:.6rem;color:#aaa;writing-mode:vertical-rl;transform:rotate(180deg);white-space:nowrap">
|
||
${d.dia?.slice(5) || ''}
|
||
</div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>
|
||
<div class="d-flex gap-3 justify-content-center mt-1" style="font-size:.75rem">
|
||
<span><span style="display:inline-block;width:10px;height:10px;background:#43a047;border-radius:2px"></span> ≥80% éxito</span>
|
||
<span><span style="display:inline-block;width:10px;height:10px;background:#fb8c00;border-radius:2px"></span> 50–79%</span>
|
||
<span><span style="display:inline-block;width:10px;height:10px;background:#e53935;border-radius:2px"></span> <50%</span>
|
||
</div>`;
|
||
}
|
||
|
||
function renderTiempos(t, ta) {
|
||
const el = document.getElementById('tiempos-card');
|
||
const prom = parseFloat(t.promedio_min);
|
||
const dias = parseFloat(ta.dias_promedio);
|
||
el.innerHTML = `
|
||
<div class="row g-2 text-center mb-3">
|
||
<div class="col-4">
|
||
<div class="fw-bold fs-5 text-info">${isNaN(prom) ? '—' : fmtMin(prom)}</div>
|
||
<div class="small text-muted">Promedio</div>
|
||
</div>
|
||
<div class="col-4">
|
||
<div class="fw-bold fs-5 text-success">${t.min_min != null ? fmtMin(t.min_min) : '—'}</div>
|
||
<div class="small text-muted">Mínimo</div>
|
||
</div>
|
||
<div class="col-4">
|
||
<div class="fw-bold fs-5 text-warning">${t.max_min != null ? fmtMin(t.max_min) : '—'}</div>
|
||
<div class="small text-muted">Máximo</div>
|
||
</div>
|
||
</div>
|
||
<div class="small text-muted text-center mb-2">
|
||
<i class="fas fa-info-circle me-1"></i>${t.con_tiempos || 0} domicilio(s) con hora llegada/salida registrada
|
||
</div>
|
||
<hr class="my-2">
|
||
<div class="text-center">
|
||
<div class="fw-bold fs-5 text-primary">${isNaN(dias) ? '—' : dias + ' días'}</div>
|
||
<div class="small text-muted">Promedio agendamiento → realización</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderHoras(horas) {
|
||
const el = document.getElementById('hora-chart');
|
||
if (!horas.length) { el.innerHTML = '<p class="text-muted small text-center py-3">Sin datos</p>'; return; }
|
||
const maxH = Math.max(...horas.map(h => parseInt(h.n) || 0)) || 1;
|
||
const allHours = Array.from({length:24}, (_, i) => i);
|
||
el.innerHTML = `
|
||
<div style="display:flex;align-items:flex-end;gap:2px;height:70px;margin-bottom:4px">
|
||
${allHours.map(h => {
|
||
const found = horas.find(r => parseInt(r.hora) === h);
|
||
const n = found ? parseInt(found.n) : 0;
|
||
const height = n ? Math.max(Math.round((n/maxH)*60), 4) : 2;
|
||
return `<div style="flex:1;height:${height}px;background:${n?'#1976d2':'#e0e0e0'};border-radius:2px 2px 0 0;opacity:.85"
|
||
title="${h}:00 — ${n} domicilio(s)"></div>`;
|
||
}).join('')}
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;font-size:.65rem;color:#aaa">
|
||
<span>00h</span><span>06h</span><span>12h</span><span>18h</span><span>23h</span>
|
||
</div>
|
||
<p class="text-muted small text-center mt-2">Pasa el cursor sobre las barras para ver detalle</p>`;
|
||
}
|
||
|
||
function renderRanking(enfs) {
|
||
const el = document.getElementById('ranking-enf');
|
||
if (!enfs.length) { el.innerHTML = '<p class="text-muted small text-center py-3">Sin datos</p>'; return; }
|
||
el.innerHTML = enfs.map((e, i) => {
|
||
const tasa = parseFloat(e.tasa_pct) || 0;
|
||
const color = tasa >= 80 ? '#43a047' : tasa >= 50 ? '#fb8c00' : '#e53935';
|
||
return `<div class="ranking-row">
|
||
<div style="font-size:.85rem;font-weight:700;color:#999;min-width:18px">${i+1}</div>
|
||
<div class="flex-grow-1 overflow-hidden">
|
||
<div class="fw-semibold small text-truncate">${esc(e.nombre_completo)}</div>
|
||
<div class="text-muted" style="font-size:.75rem">${e.asignados} asignados · ${e.completados} ✅ · ${e.cancelados} ❌</div>
|
||
</div>
|
||
<div class="text-end">
|
||
<div class="fw-bold small" style="color:${color}">${tasa}%</div>
|
||
<div style="width:50px;height:5px;background:#f0f0f0;border-radius:3px;margin-top:2px">
|
||
<div style="width:${tasa}%;height:100%;background:${color};border-radius:3px"></div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderFinanciero(fin, modos) {
|
||
const el = document.getElementById('financiero-card');
|
||
const bruto = parseFloat(fin.total_bruto) || 0;
|
||
const cobrado = parseFloat(fin.cobrado) || 0;
|
||
const pendiente= parseFloat(fin.pendiente) || 0;
|
||
const exento = parseFloat(fin.exento) || 0;
|
||
const pctCob = bruto ? Math.round(cobrado/bruto*100) : 0;
|
||
|
||
el.innerHTML = `
|
||
<div class="row g-2 text-center mb-3">
|
||
<div class="col-6">
|
||
<div class="fw-bold" style="color:#7b1fa2">${fmtMoney(cobrado)}</div>
|
||
<div class="small text-muted">Cobrado (${fin.n_pagados||0})</div>
|
||
</div>
|
||
<div class="col-6">
|
||
<div class="fw-bold text-warning">${fmtMoney(pendiente)}</div>
|
||
<div class="small text-muted">Pendiente (${fin.n_pendientes||0})</div>
|
||
</div>
|
||
</div>
|
||
<div style="background:#f3e5f5;border-radius:6px;padding:4px 8px;margin-bottom:10px;font-size:.8rem;text-align:center">
|
||
${pctCob}% cobrado de ${fmtMoney(bruto)} total facturado
|
||
</div>
|
||
${modos.length ? `<div class="small text-muted text-uppercase mb-1">Por modo de pago</div>
|
||
${modos.map(m => `
|
||
<div class="d-flex justify-content-between small mb-1">
|
||
<span>${esc(m.modo)}</span>
|
||
<span class="fw-semibold">${m.n} · ${fmtMoney(m.total)}</span>
|
||
</div>`).join('')}` : ''}`;
|
||
}
|
||
|
||
function renderBarras(elId, datos) {
|
||
const el = document.getElementById(elId);
|
||
if (!el) return;
|
||
const entries = Object.entries(datos).filter(([,v]) => v > 0);
|
||
const total = entries.reduce((a,[,b]) => a + (parseInt(b)||0), 0) || 1;
|
||
if (!entries.length) { el.innerHTML = '<p class="text-muted small text-center">Sin datos</p>'; return; }
|
||
el.innerHTML = entries.map(([estado, n]) => `
|
||
<div class="bar-row">
|
||
<div class="bar-label"><span class="badge bg-${COLOR_EST[estado]||'secondary'}">${esc(estado)}</span></div>
|
||
<div class="bar-track">
|
||
<div class="bar-fill bg-${COLOR_EST[estado]||'secondary'}" style="width:${Math.max((parseInt(n)/total)*100,2)}%"></div>
|
||
</div>
|
||
<div class="bar-val">${n}</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
function renderBarrasSimples(elId, entries, color) {
|
||
const el = document.getElementById(elId);
|
||
if (!el) return;
|
||
if (!entries.length) { el.innerHTML = '<p class="text-muted small text-center py-3">Sin datos</p>'; return; }
|
||
const max = Math.max(...entries.map(([,v]) => parseInt(v)||0)) || 1;
|
||
el.innerHTML = entries.slice(0, 10).map(([label, n]) => `
|
||
<div class="bar-row">
|
||
<div class="bar-label text-truncate" title="${esc(label)}">${esc(label)}</div>
|
||
<div class="bar-track">
|
||
<div class="bar-fill" style="width:${Math.max((parseInt(n)/max)*100,2)}%;background:${color};opacity:.8"></div>
|
||
</div>
|
||
<div class="bar-val">${n}</div>
|
||
</div>`).join('');
|
||
}
|
||
|
||
// ── Log de actividad ────────────────────────────────────────────────────────
|
||
async function cargarActividad(pag = 1) {
|
||
logPag = pag;
|
||
const params = new URLSearchParams({
|
||
page: pag, limit: 25,
|
||
modulo: document.getElementById('log-modulo').value,
|
||
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">
|
||
<span class="badge bg-${MODULO_COLOR[a.modulo]||'secondary'} badge-modulo">${esc(a.modulo)}</span>
|
||
<span class="fw-semibold small ms-1">${esc(a.admin_nombre||a.admin_username||'Sistema')}</span>
|
||
<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('');
|
||
|
||
const { paginas, total } = d;
|
||
const pagEl = document.getElementById('log-paginacion');
|
||
pagEl.innerHTML = paginas > 1
|
||
? `<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>`
|
||
: `<small class="text-muted">${total} evento(s)</small>`;
|
||
}
|
||
|
||
// ── Exportar ────────────────────────────────────────────────────────────────
|
||
function exportar(tipo) {
|
||
window.open(`lab_reportes.php?export=${tipo}&desde=${fecha('rpt-desde')}&hasta=${fecha('rpt-hasta')}`, '_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'}) : '—';
|
||
function fmtMin(m) {
|
||
m = parseFloat(m);
|
||
if (isNaN(m)) return '—';
|
||
if (m < 60) return Math.round(m) + ' min';
|
||
return Math.floor(m/60) + 'h ' + Math.round(m%60) + 'min';
|
||
}
|
||
function fmtMoney(v) {
|
||
v = parseFloat(v) || 0;
|
||
return '$' + v.toLocaleString('es-CO', {maximumFractionDigits:0});
|
||
}
|
||
|
||
cargarTodo();
|
||
</script>
|
||
<script src="assets/js/lab-sidebar.js"></script>
|
||
</body>
|
||
</html>
|