- lab_domicilios.php: sidebar filtrado por hasModule() - lab_ordenes.php: sidebar filtrado por hasModule() - login.php: redirige a lab_domicilios.php si módulos son [lab_domicilios, lab_ordenes] - migration: elimina lab_dashboard y lab_pacientes del rol lab_readonly - DB producción: rol lab_readonly queda con lab_domicilios(read) + lab_ordenes(read)
486 lines
24 KiB
PHP
486 lines
24 KiB
PHP
<?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';
|
||
$puedeEscribir = hasModuleWrite('lab_ordenes');
|
||
$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">
|
||
<?php if (hasModule('lab_dashboard')): ?>
|
||
<li><a href="lab_dashboard.php" class="nav-link"><i class="fas fa-tachometer-alt"></i> Dashboard</a></li>
|
||
<?php endif; ?>
|
||
<li><a href="lab_ordenes.php" class="nav-link active"><i class="fas fa-file-medical"></i> Órdenes</a></li>
|
||
<?php if (hasModule('lab_pacientes')): ?>
|
||
<li><a href="lab_pacientes.php" class="nav-link"><i class="fas fa-users"></i> Pacientes</a></li>
|
||
<?php endif; ?>
|
||
<?php if (hasModule('lab_domicilios')): ?>
|
||
<li><a href="lab_domicilios.php" class="nav-link"><i class="fas fa-house-medical"></i> Domicilios</a></li>
|
||
<?php endif; ?>
|
||
<?php if (hasModule('lab_enfermeras')): ?>
|
||
<li><a href="lab_enfermeras.php" class="nav-link"><i class="fas fa-user-nurse"></i> Enfermeras</a></li>
|
||
<?php endif; ?>
|
||
<?php if (hasModule('lab_formularios')): ?>
|
||
<li><a href="lab_formularios.php" class="nav-link"><i class="fas fa-wpforms"></i> Formularios</a></li>
|
||
<?php endif; ?>
|
||
<?php if (hasModule('lab_reportes')): ?>
|
||
<li><a href="lab_reportes.php" class="nav-link"><i class="fas fa-chart-bar"></i> Reportes</a></li>
|
||
<?php endif; ?>
|
||
<?php if (hasModule('lab_configuracion')): ?>
|
||
<li><a href="lab_configuracion.php" class="nav-link"><i class="fas fa-sliders-h"></i> Configuración</a></li>
|
||
<?php endif; ?>
|
||
<?php if (hasModule('lab_usuarios')): ?>
|
||
<li><a href="lab_usuarios.php" class="nav-link"><i class="fas fa-users-cog"></i> Usuarios</a></li>
|
||
<?php endif; ?>
|
||
<li class="sidebar-divider"></li>
|
||
<?php if (hasModule('lab_dashboard')): ?>
|
||
<li><a href="index.php" class="nav-link"><i class="fas fa-arrow-left"></i> Volver al Bot</a></li>
|
||
<?php endif; ?>
|
||
<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' };
|
||
const PUEDE_ESCRIBIR = <?= $puedeEscribir ? 'true' : 'false' ?>;
|
||
|
||
// ── 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
|
||
// local_file puede venir con la ruta ya incluida (ej. "uploads/media/2026/02/x.jpg")
|
||
// o solo con el nombre de archivo — normalizamos para evitar ruta duplicada
|
||
function _resolveImg(f) {
|
||
if (!f) return null;
|
||
if (/^https?:\/\//i.test(f)) return f;
|
||
if (f.startsWith('uploads/')) return f;
|
||
return `uploads/media/${f}`;
|
||
}
|
||
const imgSrc = _resolveImg(o.local_file) || _resolveImg(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.domicilio_id ? `
|
||
<div class="mb-3">
|
||
<label class="form-label small text-muted text-uppercase">Domicilio vinculado</label>
|
||
<a href="lab_domicilios.php?id=${o.domicilio_id}" class="btn btn-sm btn-outline-primary w-100">
|
||
<i class="fas fa-house-medical me-1"></i>Ver domicilio #${o.domicilio_id}
|
||
</a>
|
||
</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>
|
||
— <span class="badge bg-${COLOR[h.accion]||'secondary'} text-uppercase">${esc(h.accion)}</span>
|
||
${h.comentario ? ` — <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' && PUEDE_ESCRIBIR ? `
|
||
<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) {
|
||
if (!PUEDE_ESCRIBIR) return '';
|
||
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 => ({'<':'<','>':'>','&':'&','"':'"'}[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>
|
||
<script src="assets/js/lab-sidebar.js"></script>
|
||
</body>
|
||
</html>
|