Files
2026-04-18 23:41:01 -05:00

537 lines
26 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* modules/registro_exams/views/orden.php
* Detalle de una orden: datos paciente, ítems, muestras y resultados.
* URL: /erp.php?m=registro_exams&v=orden&id=N
*/
require_once APP_ROOT . '/config/config.php';
$ordenId = isset($_GET['id']) ? (int) $_GET['id'] : 0;
if ($ordenId <= 0) {
header('Location: ' . BASE_URL . '/erp.php?m=registro_exams&v=lista');
exit;
}
Layout::open('Orden de Exámenes', 'fas fa-file-medical');
?>
<div class="container-fluid py-3" style="max-width: 1100px;">
<!-- Encabezado -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
<a href="<?= BASE_URL ?>/erp.php?m=registro_exams&v=lista" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Volver
</a>
<h4 class="mb-0" id="tituloOrden">
<i class="fas fa-file-medical me-2 text-primary"></i>
<span id="codigoOrden">Cargando…</span>
</h4>
<div class="ms-auto d-flex gap-2 flex-wrap">
<a href="<?= BASE_URL ?>/erp.php?m=registro_exams&v=etiqueta&id=<?= $ordenId ?>"
target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-print me-1"></i>Etiqueta
</a>
<button class="btn btn-sm btn-outline-success" id="btnMarcarEntregada">
<i class="fas fa-hand-holding me-1"></i>Marcar entregada
</button>
</div>
</div>
<!-- Spinner de carga -->
<div id="spinnerOrden" class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
<div class="mt-2 text-muted">Cargando orden…</div>
</div>
<!-- Contenido principal (oculto hasta carga) -->
<div id="contenidoOrden" class="d-none">
<!-- Fila 1: Paciente + Datos médicos -->
<div class="row g-3 mb-3">
<!-- Card Paciente -->
<div class="col-md-5">
<div class="card shadow-sm h-100">
<div class="card-header bg-primary text-white py-2">
<i class="fas fa-user me-2"></i><strong>Paciente</strong>
</div>
<div class="card-body py-2">
<div class="fs-5 fw-bold" id="detPacNombre"></div>
<div class="text-muted small mb-1" id="detPacDoc"></div>
<div class="row g-1 small">
<div class="col-6"><span class="text-muted">Edad:</span> <span id="detPacEdad"></span></div>
<div class="col-6"><span class="text-muted">Género:</span> <span id="detPacGenero"></span></div>
<div class="col-12"><span class="text-muted">EPS:</span> <span id="detPacEps"></span></div>
<div class="col-12"><span class="text-muted">Tel:</span> <span id="detPacTel"></span></div>
</div>
</div>
</div>
</div>
<!-- Card Orden -->
<div class="col-md-7">
<div class="card shadow-sm h-100">
<div class="card-header py-2">
<i class="fas fa-file-medical-alt me-2 text-secondary"></i>
<strong>Datos de la orden</strong>
<span class="ms-2" id="detBadgePrio"></span>
<span class="ms-1" id="detBadgeEstado"></span>
</div>
<div class="card-body py-2">
<div class="row g-1 small">
<div class="col-sm-6"><span class="text-muted">Médico:</span> <span id="detMedico"></span></div>
<div class="col-sm-6"><span class="text-muted">Registro:</span> <span id="detMedicoReg"></span></div>
<div class="col-12"><span class="text-muted">Diagnóstico:</span> <span id="detDiagnostico"></span></div>
<div class="col-12"><span class="text-muted">Notas:</span> <span id="detNotas"></span></div>
<div class="col-12"><span class="text-muted">Creado:</span> <span id="detFecha"></span></div>
</div>
</div>
</div>
</div>
</div>
<!-- Fila 2: Muestras -->
<div class="card shadow-sm mb-3">
<div class="card-header d-flex align-items-center justify-content-between py-2">
<span><i class="fas fa-tint me-2 text-danger"></i><strong>Muestras</strong></span>
<button class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#modalMuestra">
<i class="fas fa-plus me-1"></i>Registrar muestra
</button>
</div>
<div class="card-body p-0">
<table class="table table-sm mb-0" id="tablaMuestras">
<thead class="table-light">
<tr>
<th>Tipo</th>
<th>Código barras</th>
<th>Estado</th>
<th>Tomada por</th>
<th>Hora</th>
</tr>
</thead>
<tbody id="tbodyMuestras">
<tr><td colspan="5" class="text-muted text-center py-3">Sin muestras registradas</td></tr>
</tbody>
</table>
</div>
</div>
<!-- Fila 3: Ítems de examen con sección de resultados expandible -->
<div class="card shadow-sm mb-3">
<div class="card-header py-2">
<i class="fas fa-vials me-2 text-secondary"></i>
<strong>Exámenes solicitados</strong>
<span class="badge bg-secondary ms-2" id="contadorItems"></span>
</div>
<div class="card-body p-0" id="listaItems">
<div class="text-center text-muted py-3">Cargando ítems…</div>
</div>
</div>
</div><!-- /contenidoOrden -->
</div>
<!-- ── Modal: Registrar muestra ──────────────────────────── -->
<div class="modal fade" id="modalMuestra" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white py-2">
<h6 class="modal-title"><i class="fas fa-tint me-2"></i>Registrar muestra</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label small fw-semibold">Tipo de muestra</label>
<select id="mTipoMuestra" class="form-select form-select-sm">
<option value="sangre_venosa">Sangre venosa</option>
<option value="sangre_capilar">Sangre capilar</option>
<option value="orina_24h">Orina 24 h</option>
<option value="orina_espontanea">Orina espontánea</option>
<option value="heces">Heces</option>
<option value="esputo">Esputo</option>
<option value="hisopado">Hisopado</option>
<option value="otro">Otro</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small fw-semibold">Código de barras (opcional)</label>
<input type="text" id="mCodigoBarras" class="form-control form-control-sm"
placeholder="Escanear o ingresar código">
</div>
<div class="mb-0">
<label class="form-label small fw-semibold">Estado</label>
<select id="mEstadoMuestra" class="form-select form-select-sm">
<option value="tomada" selected>Tomada</option>
<option value="procesando">Procesando</option>
<option value="procesada">Procesada</option>
<option value="rechazada">Rechazada</option>
</select>
</div>
</div>
<div class="modal-footer py-2">
<button class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
<button class="btn btn-sm btn-danger" id="btnGuardarMuestra">
<i class="fas fa-save me-1"></i>Guardar muestra
</button>
</div>
</div>
</div>
</div>
<!-- ── Modal: Ingresar resultados ────────────────────────── -->
<div class="modal fade modal-xl" id="modalResultados" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-primary text-white py-2">
<h6 class="modal-title" id="modalResTitle"><i class="fas fa-flask me-2"></i>Ingresar resultados</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="resItemId">
<div id="tablaResultados">
<!-- Filas generadas dinámicamente -->
</div>
<button type="button" class="btn btn-sm btn-outline-secondary mt-2" id="btnAgregarFila">
<i class="fas fa-plus me-1"></i>Agregar campo
</button>
</div>
<div class="modal-footer py-2">
<button class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
<button class="btn btn-sm btn-primary" id="btnGuardarResultados">
<i class="fas fa-save me-1"></i>Guardar resultados
</button>
</div>
</div>
</div>
</div>
<script>
(function () {
const ORDEN_ID = <?= $ordenId ?>;
const BASE = '<?= BASE_URL ?>';
const API_GET = BASE + '/modules/registro_exams/api/get_orden.php';
const API_CAM = BASE + '/modules/registro_exams/api/cambiar_estado_item.php';
const API_RES = BASE + '/modules/registro_exams/api/save_resultado.php';
let ordenData = null;
// ── Carga inicial ─────────────────────────────────────────
async function cargarOrden() {
try {
const r = await fetch(`${API_GET}?id=${ORDEN_ID}`);
const j = await r.json();
if (!j.ok) throw new Error(j.error);
ordenData = j.data;
renderOrden(j.data);
} catch (e) {
document.getElementById('spinnerOrden').innerHTML =
`<div class="alert alert-danger m-4">Error al cargar: ${escHtml(e.message)}</div>`;
}
}
function renderOrden(o) {
document.getElementById('spinnerOrden').classList.add('d-none');
document.getElementById('contenidoOrden').classList.remove('d-none');
// Encabezado
document.getElementById('codigoOrden').textContent = o.codigo;
// Paciente
document.getElementById('detPacNombre').textContent = o.nombre_completo;
document.getElementById('detPacDoc').textContent = `${o.tipo_documento} ${o.numero_documento}`;
document.getElementById('detPacEdad').textContent = o.edad !== null ? `${o.edad} años` : '—';
document.getElementById('detPacGenero').textContent = o.genero || '—';
document.getElementById('detPacEps').textContent = o.eps || '—';
document.getElementById('detPacTel').textContent = o.telefono || '—';
// Orden
document.getElementById('detBadgePrio').innerHTML = badgePrio(o.prioridad);
document.getElementById('detBadgeEstado').innerHTML = badgeEstado(o.estado);
document.getElementById('detMedico').textContent = o.medico_nombre || '—';
document.getElementById('detMedicoReg').textContent = o.medico_registro || '—';
document.getElementById('detDiagnostico').textContent = o.diagnostico || '—';
document.getElementById('detNotas').textContent = o.notas || '—';
document.getElementById('detFecha').textContent = o.creado_at ? o.creado_at.substr(0, 16) : '—';
// Muestras
renderMuestras(o.muestras);
// Ítems
renderItems(o.items);
// Contador
document.getElementById('contadorItems').textContent = o.items.length;
// Botón marcar entregada (solo si está completa)
const btnEntregar = document.getElementById('btnMarcarEntregada');
btnEntregar.disabled = !['completa'].includes(o.estado);
}
// ── Muestras ──────────────────────────────────────────────
function renderMuestras(muestras) {
const tbody = document.getElementById('tbodyMuestras');
if (!muestras.length) {
tbody.innerHTML = '<tr><td colspan="5" class="text-muted text-center py-3">Sin muestras registradas</td></tr>';
return;
}
const labelTipo = {
sangre_venosa: 'Sangre venosa', sangre_capilar: 'Sangre capilar',
orina_24h: 'Orina 24h', orina_espontanea: 'Orina espontánea',
heces: 'Heces', esputo: 'Esputo', hisopado: 'Hisopado', otro: 'Otro'
};
const badgeEst = {
pendiente:'<span class="badge bg-light text-dark border">Pendiente</span>',
tomada: '<span class="badge bg-success">Tomada</span>',
procesando:'<span class="badge bg-info text-dark">Procesando</span>',
procesada: '<span class="badge bg-primary">Procesada</span>',
rechazada: '<span class="badge bg-danger">Rechazada</span>',
};
tbody.innerHTML = muestras.map(m =>
`<tr>
<td>${labelTipo[m.tipo_muestra] || m.tipo_muestra}</td>
<td>${escHtml(m.codigo_barras || '—')}</td>
<td>${badgeEst[m.estado] || m.estado}</td>
<td>${escHtml(m.tomada_nombre || '—')}</td>
<td>${m.tomada_at ? m.tomada_at.substr(11,5) : '—'}</td>
</tr>`
).join('');
}
// ── Ítems ─────────────────────────────────────────────────
function renderItems(items) {
const cont = document.getElementById('listaItems');
if (!items.length) {
cont.innerHTML = '<div class="text-muted text-center py-3">Sin ítems</div>';
return;
}
const estadoColor = {
pendiente: 'bg-light text-dark border',
muestra_tomada: 'bg-warning text-dark',
en_proceso: 'bg-info text-dark',
resultado_listo: 'bg-success',
entregado: 'bg-secondary',
};
cont.innerHTML = items.map(item => {
const badge = `<span class="badge ${estadoColor[item.estado] || 'bg-light'}">${labelEstItem(item.estado)}</span>`;
const nResultados = item.resultados?.length ?? 0;
const resHTML = nResultados > 0 ? renderTablaResultados(item.resultados) : '';
return `
<div class="border-bottom p-3" id="item-${item.id}">
<div class="d-flex flex-wrap align-items-start gap-2">
<div class="flex-grow-1">
<div class="fw-semibold">${escHtml(item.tipo_nombre)}
<small class="text-muted ms-1">${escHtml(item.tipo_codigo)}</small>
</div>
<div class="small text-muted">${escHtml(item.categoria || '')}</div>
${item.tipo_instrucciones
? `<div class="small text-info mt-1"><i class="fas fa-info-circle me-1"></i>${escHtml(item.tipo_instrucciones)}</div>`
: ''}
</div>
<div class="d-flex align-items-center gap-1 flex-wrap">
${badge}
<button class="btn btn-sm btn-outline-primary"
onclick="abrirResultados(${item.id}, '${escAtt(item.tipo_nombre)}', ${JSON.stringify(item.resultados || []).replace(/"/g,'&quot;')})">
<i class="fas fa-flask me-1"></i>${nResultados > 0 ? 'Editar resultados' : 'Ingresar resultados'}
</button>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
Estado
</button>
<ul class="dropdown-menu dropdown-menu-end">
${['pendiente','muestra_tomada','en_proceso','resultado_listo','entregado'].map(est =>
`<li><a class="dropdown-item small" href="#"
onclick="cambiarEstado('item',${item.id},'${est}');return false;">
${labelEstItem(est)}</a></li>`
).join('')}
</ul>
</div>
</div>
</div>
${resHTML ? `<div class="mt-2">${resHTML}</div>` : ''}
</div>`;
}).join('');
}
function renderTablaResultados(resultados) {
if (!resultados.length) return '';
const rows = resultados.map(r =>
`<tr class="${r.es_anormal ? 'table-danger' : ''}">
<td>${escHtml(r.campo_nombre)}</td>
<td class="fw-semibold">${escHtml(r.valor_texto || (r.valor_numerico !== null ? r.valor_numerico : '—'))}</td>
<td>${escHtml(r.unidad || '—')}</td>
<td>${escHtml(r.referencia_texto || (r.referencia_min !== null ? `${r.referencia_min} ${r.referencia_max}` : '—'))}</td>
<td>${r.es_anormal ? '<span class="badge bg-danger">ANORMAL</span>' : ''}</td>
</tr>`
).join('');
return `<table class="table table-sm table-bordered mb-0" style="font-size:.85rem">
<thead class="table-light">
<tr><th>Campo</th><th>Valor</th><th>Unidad</th><th>Referencia</th><th>Obs.</th></tr>
</thead>
<tbody>${rows}</tbody>
</table>`;
}
// ── Cambiar estado ────────────────────────────────────────
window.cambiarEstado = async function(tipo, id, estado) {
try {
const r = await fetch(API_CAM, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tipo, id, estado }),
});
const j = await r.json();
if (!j.ok) throw new Error(j.error);
await cargarOrden();
} catch (e) {
alert('Error: ' + e.message);
}
};
document.getElementById('btnMarcarEntregada').addEventListener('click', async () => {
if (!confirm('¿Marcar esta orden como entregada?')) return;
await cambiarEstado('orden', ORDEN_ID, 'entregada');
});
// ── Registrar muestra ─────────────────────────────────────
document.getElementById('btnGuardarMuestra').addEventListener('click', async () => {
const btn = document.getElementById('btnGuardarMuestra');
btn.disabled = true;
try {
const r = await fetch(API_CAM, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tipo: 'muestra',
id: null,
orden_id: ORDEN_ID,
tipo_muestra: document.getElementById('mTipoMuestra').value,
codigo_barras: document.getElementById('mCodigoBarras').value.trim(),
estado: document.getElementById('mEstadoMuestra').value,
}),
});
const j = await r.json();
if (!j.ok) throw new Error(j.error);
bootstrap.Modal.getInstance(document.getElementById('modalMuestra')).hide();
document.getElementById('mCodigoBarras').value = '';
await cargarOrden();
} catch (e) {
alert('Error: ' + e.message);
} finally {
btn.disabled = false;
}
});
// ── Modal Resultados ──────────────────────────────────────
window.abrirResultados = function(itemId, nombre, resultados) {
document.getElementById('resItemId').value = itemId;
document.getElementById('modalResTitle').innerHTML =
`<i class="fas fa-flask me-2"></i>Resultados — ${escHtml(nombre)}`;
renderFilasResultados(resultados);
bootstrap.Modal.getOrCreateInstance(document.getElementById('modalResultados')).show();
};
function renderFilasResultados(resultados) {
const cont = document.getElementById('tablaResultados');
const filas = resultados.length > 0 ? resultados : [{}];
cont.innerHTML = `
<table class="table table-sm table-bordered" id="tResultados">
<thead class="table-light">
<tr>
<th>Campo</th>
<th>Valor</th>
<th>Unidad</th>
<th>Referencia</th>
<th class="text-center">Anormal</th>
<th></th>
</tr>
</thead>
<tbody id="tbodyRes">${filas.map(r => buildFilaRes(r)).join('')}</tbody>
</table>`;
}
function buildFilaRes(r = {}) {
return `<tr>
<td><input class="form-control form-control-sm r-campo" value="${escAtt(r.campo_nombre||'')}" placeholder="Hemoglobina"></td>
<td><input class="form-control form-control-sm r-valor" value="${escAtt(r.valor_texto||r.valor_numerico||'')}" placeholder="14.5"></td>
<td><input class="form-control form-control-sm r-unidad" value="${escAtt(r.unidad||'')}" placeholder="g/dL" style="width:80px"></td>
<td><input class="form-control form-control-sm r-ref" value="${escAtt(r.referencia_texto || (r.referencia_min!=null ? r.referencia_min+' '+r.referencia_max : ''))}" placeholder="12.0 16.0"></td>
<td class="text-center"><input type="checkbox" class="form-check-input r-anormal" ${r.es_anormal ? 'checked' : ''}></td>
<td><button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('tr').remove()"><i class="fas fa-times"></i></button></td>
</tr>`;
}
document.getElementById('btnAgregarFila').addEventListener('click', () => {
document.getElementById('tbodyRes').insertAdjacentHTML('beforeend', buildFilaRes());
});
document.getElementById('btnGuardarResultados').addEventListener('click', async () => {
const itemId = parseInt(document.getElementById('resItemId').value);
const filas = [...document.querySelectorAll('#tbodyRes tr')];
const resultados = filas.map(tr => ({
campo_nombre: tr.querySelector('.r-campo').value.trim(),
valor_texto: tr.querySelector('.r-valor').value.trim(),
valor_numerico: parseFloat(tr.querySelector('.r-valor').value) || null,
unidad: tr.querySelector('.r-unidad').value.trim(),
referencia_texto: tr.querySelector('.r-ref').value.trim(),
es_anormal: tr.querySelector('.r-anormal').checked,
})).filter(r => r.campo_nombre !== '');
if (!resultados.length) { alert('Ingrese al menos un campo de resultado.'); return; }
const btn = document.getElementById('btnGuardarResultados');
btn.disabled = true;
try {
const r = await fetch(API_RES, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ item_id: itemId, resultados }),
});
const j = await r.json();
if (!j.ok) throw new Error(j.error);
bootstrap.Modal.getInstance(document.getElementById('modalResultados')).hide();
await cargarOrden();
} catch (e) {
alert('Error: ' + e.message);
} finally {
btn.disabled = false;
}
});
// ── Helpers ───────────────────────────────────────────────
function labelEstItem(e) {
return {
pendiente: 'Pendiente',
muestra_tomada: 'Muestra tomada',
en_proceso: 'En proceso',
resultado_listo: 'Resultado listo',
entregado: 'Entregado',
}[e] || e;
}
function badgePrio(p) {
return {stat:'<span class="badge bg-danger">STAT</span>',urgente:'<span class="badge bg-warning text-dark">Urgente</span>',normal:'<span class="badge bg-secondary">Normal</span>'}[p]||'';
}
function badgeEstado(e) {
return {pendiente:'<span class="badge bg-light text-dark border">Pendiente</span>',en_proceso:'<span class="badge bg-primary">En proceso</span>',completa:'<span class="badge bg-success">Completa</span>',entregada:'<span class="badge bg-info text-dark">Entregada</span>',cancelada:'<span class="badge bg-dark">Cancelada</span>'}[e]||e;
}
function escHtml(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function escAtt(s) { return String(s||'').replace(/"/g,'&quot;'); }
// ── Inicio ───────────────────────────────────────────────
cargarOrden();
})();
</script>
<?php Layout::close(); ?>