This commit is contained in:
Lizandro Guarnizo
2026-03-10 18:42:51 -05:00
parent b57fc16ab3
commit db469b2134
98 changed files with 16365 additions and 303 deletions
+449
View File
@@ -0,0 +1,449 @@
<?php
/**
* Gestión de Órdenes Médicas — 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; }
$adminNombre = $_SESSION['admin_user']['full_name'] ?? $_SESSION['admin_user']['username'] ?? 'Admin';
$ordenIdParam = (int)($_GET['id'] ?? 0);
$estadoParam = $_GET['estado'] ?? '';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Órdenes Médicas — 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>
.orden-row { cursor: pointer; }
.flujo-estado { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.flujo-paso { padding: 3px 10px; border-radius: 20px; font-size: .75rem; border: 1px solid #dee2e6; color: #6c757d; }
.flujo-paso.activo { background: var(--bs-primary); color: #fff; border-color: var(--bs-primary); font-weight: 600; }
.flujo-paso.completado { background: #d1e7dd; color: #0a3622; border-color: #a3cfbb; }
.img-orden { max-width: 100%; border-radius: 8px; border: 1px solid #dee2e6; cursor: zoom-in; }
.historial-item { font-size: .8rem; padding: 6px 0; border-bottom: 1px solid #f0f0f0; }
.historial-item: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 active"><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"><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-file-medical text-primary"></i> Órdenes Médicas</h1>
<small class="text-muted"><?= htmlspecialchars($adminNombre) ?></small>
</div>
</header>
<div class="container-fluid py-3">
<!-- Filtros + contadores -->
<div class="row g-2 mb-3" id="contadores-row">
<?php foreach(['pendiente'=>['warning','clock'],'en_revision'=>['info','magnifying-glass'],'autorizada'=>['success','check-circle'],'rechazada'=>['danger','times-circle'],'en_domicilio'=>['primary','house-medical'],'completada'=>['secondary','flag-checkered']] as $e=>[$color,$icon]): ?>
<div class="col-6 col-md-2">
<button class="btn btn-outline-<?=$color?> btn-sm w-100 filtro-estado <?= $estadoParam===$e?'active':'' ?>"
data-estado="<?=$e?>" onclick="filtrarEstado('<?=$e?>')">
<i class="fas fa-<?=$icon?> me-1"></i> <?=ucfirst(str_replace('_',' ',$e))?><br>
<strong id="cnt-<?=$e?>">—</strong>
</button>
</div>
<?php endforeach; ?>
</div>
<!-- Barra de búsqueda -->
<div class="row g-2 mb-3">
<div class="col-md-4">
<input type="text" id="buscador" class="form-control form-control-sm" placeholder="Buscar paciente…" oninput="debounce(cargarLista,400)()">
</div>
<div class="col-md-3">
<input type="date" id="filtro-desde" class="form-control form-control-sm" onchange="cargarLista()">
</div>
<div class="col-md-3">
<input type="date" id="filtro-hasta" class="form-control form-control-sm" onchange="cargarLista()">
</div>
<div class="col-md-2">
<button class="btn btn-sm btn-secondary w-100" onclick="limpiarFiltros()">
<i class="fas fa-eraser me-1"></i> Limpiar
</button>
</div>
</div>
<div class="row g-3">
<!-- Tabla de órdenes -->
<div class="col-lg-5">
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr><th>#</th><th>Paciente</th><th>Fecha</th><th>Estado</th></tr>
</thead>
<tbody id="tabla-body">
<tr><td colspan="4" class="text-center py-4 text-muted"><i class="fas fa-spinner fa-spin me-2"></i>Cargando...</td></tr>
</tbody>
</table>
</div>
<div class="d-flex align-items-center justify-content-between px-3 py-2 border-top" id="paginacion"></div>
</div>
</div>
</div>
<!-- Panel de detalle de orden -->
<div class="col-lg-7" id="detail-panel">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0 pb-0">
<div class="d-flex align-items-center justify-content-between">
<h6 class="mb-0 fw-semibold">Detalle de Orden <span id="detail-id" class="text-muted"></span></h6>
<button class="btn-close" onclick="cerrarDetalle()"></button>
</div>
<!-- Flujo de estados -->
<div class="flujo-estado mt-2" id="flujo-row"></div>
</div>
<div class="card-body" id="detail-body">
<div class="text-center py-5 text-muted">
<i class="fas fa-arrow-left me-2"></i>Selecciona una orden de la lista
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Modal Autorizar / Rechazar -->
<div class="modal fade" id="modalAccion" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modal-accion-titulo">Acción</h5>
<button class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="accion-id">
<input type="hidden" id="accion-tipo">
<div class="mb-3">
<label class="form-label" id="accion-label">Comentario</label>
<textarea class="form-control" id="accion-comentario" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
<button class="btn btn-primary" id="btn-confirmar-accion" onclick="ejecutarAccion()">Confirmar</button>
</div>
</div>
</div>
</div>
<!-- Modal visor de imagen -->
<div class="modal fade" id="modalImagen" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content bg-dark">
<div class="modal-header border-0 py-2">
<button class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center p-0">
<img id="img-visor" src="" class="img-fluid" style="max-height:80vh;object-fit:contain">
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let paginaActual = 1;
let filtroEstado = '<?= htmlspecialchars($estadoParam) ?>';
let ordenSeleccionada = <?= $ordenIdParam ?: 'null' ?>;
const modalAccion = new bootstrap.Modal('#modalAccion');
const modalImg = new bootstrap.Modal('#modalImagen');
const FLUJO = ['pendiente','en_revision','autorizada','rechazada','en_domicilio','completada'];
const COLOR = { pendiente:'warning', en_revision:'info', autorizada:'success', rechazada:'danger', en_domicilio:'primary', completada:'secondary' };
// ── Lista ──────────────────────────────────────────────────────────────────
async function cargarLista(pag = 1) {
paginaActual = pag;
const params = new URLSearchParams({
page: pag, limit: 20,
busqueda: document.getElementById('buscador').value.trim(),
desde: document.getElementById('filtro-desde').value,
hasta: document.getElementById('filtro-hasta').value,
estado: filtroEstado,
});
const r = await fetch(`api/lab/get_ordenes.php?${params}`);
const d = await r.json();
// Actualizar contadores
if (d.contadores) {
Object.entries(d.contadores).forEach(([e, n]) => {
const el = document.getElementById(`cnt-${e}`);
if (el) el.textContent = n;
});
}
const tbody = document.getElementById('tabla-body');
if (!d.data?.length) {
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4 text-muted">Sin resultados</td></tr>';
document.getElementById('paginacion').innerHTML = '';
return;
}
tbody.innerHTML = d.data.map(o => `
<tr class="orden-row ${ordenSeleccionada===o.id?'table-active':''}"
onclick="verOrden(${o.id})" id="row-${o.id}">
<td class="text-muted">#${o.id}</td>
<td>
<div class="fw-semibold">${esc(o.paciente_nombre)}</div>
<small class="text-muted">${esc(o.numero_documento||'')}</small>
</td>
<td>${formatFecha(o.created_at)}</td>
<td><span class="badge bg-${COLOR[o.estado]||'secondary'}">${esc(o.estado)}</span></td>
</tr>
`).join('');
renderPaginacion(d, pag);
if (ordenSeleccionada) verOrden(ordenSeleccionada);
}
function renderPaginacion(d, pag) {
const el = document.getElementById('paginacion');
if (d.paginas <= 1) { el.innerHTML = `<small class="text-muted">${d.total} orden(es)</small>`; return; }
el.innerHTML =
`<small class="text-muted">${((pag-1)*20)+1}${Math.min(pag*20,d.total)} de ${d.total}</small>
<div class="btn-group btn-group-sm">
${pag>1?`<button class="btn btn-outline-secondary" onclick="cargarLista(${pag-1})"><i class="fas fa-chevron-left"></i></button>`:''}
<button class="btn btn-secondary disabled">${pag}/${d.paginas}</button>
${pag<d.paginas?`<button class="btn btn-outline-secondary" onclick="cargarLista(${pag+1})"><i class="fas fa-chevron-right"></i></button>`:''}
</div>`;
}
// ── Detalle ────────────────────────────────────────────────────────────────
async function verOrden(id) {
ordenSeleccionada = id;
document.querySelectorAll('.orden-row').forEach(r => r.classList.remove('table-active'));
const rowEl = document.getElementById(`row-${id}`);
if (rowEl) rowEl.classList.add('table-active');
document.getElementById('detail-body').innerHTML = '<div class="text-center py-4"><i class="fas fa-spinner fa-spin"></i></div>';
document.getElementById('detail-id').textContent = `#${id}`;
const r = await fetch(`api/lab/get_ordenes.php?id=${id}&no_counts=1`);
const d = await r.json();
const o = d.orden;
if (!o) return;
// Flujo
const idx = FLUJO.indexOf(o.estado);
document.getElementById('flujo-row').innerHTML = FLUJO.map((e, i) => {
let cls = 'flujo-paso';
if (i < idx) cls += ' completado';
if (e === o.estado) cls += ' activo';
return `<span class="${cls}">${esc(e.replace('_',' '))}</span>${i < FLUJO.length-1 ? '<i class="fas fa-chevron-right text-muted" style="font-size:.65rem"></i>' : ''}`;
}).join('');
// Imagen de la orden
const imgSrc = o.local_file
? `uploads/media/${esc(o.local_file)}`
: (o.conv_local_file ? `uploads/media/${esc(o.conv_local_file)}` : null);
// Botones de acción según estado
const botonesAccion = renderBotones(o);
document.getElementById('detail-body').innerHTML = `
<div class="row g-3">
<div class="col-md-6">
${imgSrc ? `
<div class="mb-3">
<label class="form-label small text-muted text-uppercase">Imagen de la orden</label>
<img src="${imgSrc}" class="img-orden" onclick="verImagen('${imgSrc}')" title="Ver imagen completa">
</div>` : '<div class="alert alert-light small text-muted"><i class="fas fa-image me-2"></i>Sin imagen adjunta</div>'}
<dl class="row small">
<dt class="col-5 text-muted">Paciente</dt>
<dd class="col-7 fw-semibold"><a href="lab_pacientes.php" onclick="event.preventDefault()">${esc(o.paciente_nombre)}</a></dd>
<dt class="col-5 text-muted">Documento</dt><dd class="col-7">${esc(o.numero_documento||'—')}</dd>
<dt class="col-5 text-muted">Teléfono</dt><dd class="col-7">${esc(o.paciente_telefono||'—')}</dd>
<dt class="col-5 text-muted">EPS</dt><dd class="col-7">${esc(o.paciente_eps||'—')}</dd>
<dt class="col-5 text-muted">Médico</dt><dd class="col-7">${esc(o.medico_nombre||'—')}</dd>
<dt class="col-5 text-muted">Reg. médico</dt><dd class="col-7">${esc(o.medico_registro||'—')}</dd>
<dt class="col-5 text-muted">Fecha orden</dt><dd class="col-7">${esc(o.fecha_orden||'—')}</dd>
<dt class="col-5 text-muted">Ayuno</dt>
<dd class="col-7">${o.requiere_ayuno ? `<span class="badge bg-warning text-dark">${o.horas_ayuno||'?'}h ayuno</span>` : 'No requiere'}</dd>
</dl>
</div>
<div class="col-md-6">
${o.examenes_solicitados ? `
<div class="mb-3">
<label class="form-label small text-muted text-uppercase">Exámenes solicitados</label>
<div class="bg-light p-2 rounded small">${esc(o.examenes_solicitados).replace(/\n/g,'<br>')}</div>
</div>` : ''}
${o.indicaciones ? `
<div class="mb-3">
<label class="form-label small text-muted text-uppercase">Indicaciones</label>
<div class="bg-light p-2 rounded small">${esc(o.indicaciones).replace(/\n/g,'<br>')}</div>
</div>` : ''}
${o.comentario_revision ? `
<div class="mb-3">
<label class="form-label small text-muted text-uppercase">Comentario revisión</label>
<div class="alert alert-${o.estado==='rechazada'?'danger':'success'} py-2 small mb-0">${esc(o.comentario_revision)}</div>
</div>` : ''}
<!-- Botones de acción -->
<div class="d-flex gap-2 flex-wrap mb-3">${botonesAccion}</div>
<!-- Historial -->
<label class="form-label small text-muted text-uppercase">Historial de cambios</label>
<div class="border rounded p-2">
${(o.historial||[]).map(h => `
<div class="historial-item">
<span class="fw-semibold">${esc(h.admin_nombre||'Sistema')}</span>
&mdash; <span class="badge bg-${COLOR[h.accion]||'secondary'} text-uppercase">${esc(h.accion)}</span>
${h.comentario ? ` &mdash; <em class="text-muted">${esc(h.comentario)}</em>` : ''}
<br><span class="text-muted">${formatFecha(h.created_at)}</span>
</div>`).join('') || '<p class="text-muted small mb-0">Sin historial</p>'}
</div>
${o.estado === 'autorizada' ? `
<div class="mt-3">
<a href="lab_domicilios.php?orden_id=${o.id}&paciente_id=${o.paciente_id}" class="btn btn-success w-100 btn-sm">
<i class="fas fa-house-medical me-1"></i> Crear domicilio para esta orden
</a>
</div>` : ''}
</div>
</div>
`;
}
function renderBotones(o) {
const btns = [];
if (o.estado === 'pendiente')
btns.push(`<button class="btn btn-sm btn-info" onclick="accion(${o.id},'en_revision')"><i class="fas fa-magnifying-glass me-1"></i>Iniciar revisión</button>`);
if (o.estado === 'en_revision') {
btns.push(`<button class="btn btn-sm btn-success" onclick="accion(${o.id},'autorizada')"><i class="fas fa-check me-1"></i>Autorizar</button>`);
btns.push(`<button class="btn btn-sm btn-danger" onclick="accion(${o.id},'rechazada')"><i class="fas fa-times me-1"></i>Rechazar</button>`);
}
if (o.estado === 'en_domicilio')
btns.push(`<button class="btn btn-sm btn-secondary" onclick="accion(${o.id},'completada')"><i class="fas fa-flag-checkered me-1"></i>Completar</button>`);
return btns.join('');
}
function cerrarDetalle() {
ordenSeleccionada = null;
document.querySelectorAll('.orden-row').forEach(r => r.classList.remove('table-active'));
document.getElementById('detail-body').innerHTML = '<div class="text-center py-5 text-muted"><i class="fas fa-arrow-left me-2"></i>Selecciona una orden</div>';
document.getElementById('flujo-row').innerHTML = '';
document.getElementById('detail-id').textContent = '';
}
// ── Acciones ───────────────────────────────────────────────────────────────
function accion(id, tipo) {
document.getElementById('accion-id').value = id;
document.getElementById('accion-tipo').value = tipo;
document.getElementById('accion-comentario').value = '';
const titulos = {
en_revision: 'Iniciar revisión',
autorizada: 'Autorizar orden',
rechazada: 'Rechazar orden',
completada: 'Completar orden',
};
document.getElementById('modal-accion-titulo').textContent = titulos[tipo] || 'Confirmar acción';
document.getElementById('accion-label').textContent = tipo==='rechazada' ? 'Motivo del rechazo *' : 'Comentario (opcional)';
document.getElementById('accion-comentario').required = tipo === 'rechazada';
document.getElementById('btn-confirmar-accion').className = `btn btn-${tipo==='rechazada'?'danger':tipo==='autorizada'?'success':'primary'}`;
modalAccion.show();
}
async function ejecutarAccion() {
const id = parseInt(document.getElementById('accion-id').value);
const tipo = document.getElementById('accion-tipo').value;
const com = document.getElementById('accion-comentario').value.trim();
if (tipo === 'rechazada' && !com) {
document.getElementById('accion-comentario').focus();
return;
}
const r = await fetch('api/lab/autorizar_orden.php', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ id, accion: tipo, comentario: com }),
});
const d = await r.json();
modalAccion.hide();
if (d.success) {
mostrarToast(d.message, 'success');
await cargarLista(paginaActual);
if (ordenSeleccionada) verOrden(ordenSeleccionada);
} else {
mostrarToast(d.error || 'Error', 'danger');
}
}
function filtrarEstado(e) {
filtroEstado = (filtroEstado === e) ? '' : e;
document.querySelectorAll('.filtro-estado').forEach(b => {
b.classList.toggle('active', b.dataset.estado === filtroEstado);
});
cargarLista(1);
}
function limpiarFiltros() {
filtroEstado = '';
document.getElementById('buscador').value = '';
document.getElementById('filtro-desde').value = '';
document.getElementById('filtro-hasta').value = '';
document.querySelectorAll('.filtro-estado').forEach(b => b.classList.remove('active'));
cargarLista(1);
}
function verImagen(src) {
document.getElementById('img-visor').src = src;
modalImg.show();
}
// ── Utils ──────────────────────────────────────────────────────────────────
const esc = s => String(s||'').replace(/[<>&"]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]));
const formatFecha = s => s ? new Date(s).toLocaleString('es-CO', {day:'2-digit',month:'short',hour:'2-digit',minute:'2-digit'}) : '—';
function debounce(fn, ms) { let t; return (...a) => { clearTimeout(t); t = setTimeout(()=>fn(...a),ms); }; }
function mostrarToast(msg, tipo='success') {
const div = document.createElement('div');
div.className = `alert alert-${tipo} alert-dismissible position-fixed bottom-0 end-0 m-3`;
div.style.zIndex = 9999;
div.innerHTML = `${esc(msg)}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>`;
document.body.appendChild(div);
setTimeout(()=>div.remove(), 4000);
}
cargarLista();
</script>
</body>
</html>