This commit is contained in:
Lizandro Guarnizo
2026-04-18 23:41:01 -05:00
parent 876e935a66
commit 318a1742cc
37 changed files with 8838 additions and 89 deletions
+536
View File
@@ -0,0 +1,536 @@
<?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(); ?>