420 lines
20 KiB
PHP
420 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* modules/registro_exams/views/nueva_orden.php
|
|
* Formulario para crear una nueva orden de exámenes.
|
|
* URL: /erp.php?m=registro_exams&v=nueva_orden[&solicitud_id=N]
|
|
*/
|
|
|
|
require_once APP_ROOT . '/config/config.php';
|
|
|
|
// Pre-carga de solicitud del turnero (opcional)
|
|
$solicitudId = isset($_GET['solicitud_id']) ? (int) $_GET['solicitud_id'] : null;
|
|
$pacientePreId = null;
|
|
$pacientePreNom = '';
|
|
$pacientePreDoc = '';
|
|
|
|
if ($solicitudId) {
|
|
try {
|
|
$pdo = Database::getInstance()->getConnection();
|
|
$stmt = $pdo->prepare('
|
|
SELECT s.id AS sol_id,
|
|
p.id AS pac_id, p.nombre_completo, p.numero_documento
|
|
FROM turnero_solicitudes s
|
|
JOIN lab_pacientes p ON p.id = s.paciente_id
|
|
WHERE s.id = ?
|
|
');
|
|
$stmt->execute([$solicitudId]);
|
|
$pre = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
if ($pre) {
|
|
$pacientePreId = (int) $pre['pac_id'];
|
|
$pacientePreNom = $pre['nombre_completo'];
|
|
$pacientePreDoc = $pre['numero_documento'];
|
|
}
|
|
} catch (\Throwable $e) {
|
|
// continuar sin pre-cargar
|
|
}
|
|
}
|
|
|
|
// Cargar tipos de exámenes activos agrupados por categoría
|
|
$examTipos = [];
|
|
try {
|
|
$pdo = Database::getInstance()->getConnection();
|
|
$stmt = $pdo->query('
|
|
SELECT id, codigo, nombre, categoria, requiere_ayuno, horas_ayuno,
|
|
precio_base, instrucciones
|
|
FROM exam_tipos
|
|
WHERE activo = 1
|
|
ORDER BY categoria, nombre
|
|
');
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$cat = $row['categoria'] ?: 'General';
|
|
$examTipos[$cat][] = $row;
|
|
}
|
|
} catch (\Throwable $e) {}
|
|
|
|
Layout::open('Nueva Orden de Exámenes', 'fas fa-plus-circle');
|
|
?>
|
|
<div class="container-fluid py-3" style="max-width: 960px;">
|
|
|
|
<div class="d-flex 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"><i class="fas fa-plus-circle me-2 text-primary"></i>Nueva Orden de Exámenes</h4>
|
|
</div>
|
|
|
|
<form id="formOrden" novalidate>
|
|
<input type="hidden" id="ordenId" value="">
|
|
<input type="hidden" id="solicitudIdHidden" value="<?= (int) $solicitudId ?>">
|
|
|
|
<!-- ── SECCIÓN 1: Paciente ─────────────────────────── -->
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header bg-primary text-white py-2">
|
|
<i class="fas fa-user me-2"></i><strong>1. Datos del paciente</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
<input type="hidden" id="pacienteId" value="<?= (int) $pacientePreId ?>">
|
|
|
|
<?php if ($pacientePreId): ?>
|
|
<!-- Paciente pre-cargado desde turnero -->
|
|
<div id="pacienteSeleccionado" class="alert alert-success d-flex align-items-center gap-3 mb-2">
|
|
<i class="fas fa-user-check fs-4"></i>
|
|
<div>
|
|
<div class="fw-bold" id="pacNombre"><?= htmlspecialchars($pacientePreNom) ?></div>
|
|
<div class="small" id="pacDoc"><?= htmlspecialchars($pacientePreDoc) ?></div>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-outline-danger ms-auto" id="btnCambiarPaciente">
|
|
<i class="fas fa-times me-1"></i>Cambiar
|
|
</button>
|
|
</div>
|
|
<div id="buscadorPaciente" class="d-none">
|
|
<?php else: ?>
|
|
<div id="pacienteSeleccionado" class="alert alert-light border-dashed d-none">
|
|
<div class="fw-bold" id="pacNombre"></div>
|
|
<div class="small text-muted" id="pacDoc"></div>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" id="btnCambiarPaciente">Cambiar</button>
|
|
</div>
|
|
<div id="buscadorPaciente">
|
|
<?php endif; ?>
|
|
<label class="form-label fw-semibold">Buscar paciente</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
|
<input type="search" id="inputBuscarPac" class="form-control"
|
|
placeholder="Nombre o número de documento…" autocomplete="off">
|
|
</div>
|
|
<div id="resultadosPac" class="list-group mt-1 shadow-sm"></div>
|
|
</div><!-- /buscadorPaciente -->
|
|
</div><!-- /card-body -->
|
|
</div>
|
|
|
|
<!-- ── SECCIÓN 2: Datos médicos ───────────────────── -->
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header py-2">
|
|
<i class="fas fa-stethoscope me-2 text-secondary"></i><strong>2. Datos médicos</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-md-5">
|
|
<label class="form-label small fw-semibold">Médico solicitante</label>
|
|
<input type="text" id="medicoNombre" class="form-control form-control-sm"
|
|
placeholder="Nombre del médico">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-semibold">Registro médico</label>
|
|
<input type="text" id="medicoRegistro" class="form-control form-control-sm"
|
|
placeholder="No. registro">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label small fw-semibold">Prioridad</label>
|
|
<select id="prioridad" class="form-select form-select-sm">
|
|
<option value="normal" selected>Normal</option>
|
|
<option value="urgente">Urgente</option>
|
|
<option value="stat">STAT (inmediato)</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label small fw-semibold">Diagnóstico / indicación</label>
|
|
<textarea id="diagnostico" class="form-control form-control-sm" rows="2"
|
|
placeholder="CIE-10 o descripción clínica"></textarea>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label small fw-semibold">Notas internas</label>
|
|
<textarea id="notas" class="form-control form-control-sm" rows="2"
|
|
placeholder="Observaciones para el bacteriólogo…"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── SECCIÓN 3: Exámenes solicitados ───────────── -->
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header py-2">
|
|
<i class="fas fa-vials me-2 text-secondary"></i>
|
|
<strong>3. Exámenes solicitados</strong>
|
|
</div>
|
|
<div class="card-body">
|
|
|
|
<!-- Buscador de exámenes -->
|
|
<div class="input-group mb-3">
|
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
|
<input type="search" id="inputBuscarExam" class="form-control"
|
|
placeholder="Filtrar exámenes por nombre o código…">
|
|
</div>
|
|
|
|
<?php if (empty($examTipos)): ?>
|
|
<div class="alert alert-warning">
|
|
No hay tipos de examen configurados. Configure los exámenes en
|
|
<a href="<?= BASE_URL ?>/erp.php?m=turnero&v=configuracion">Configuración → Exámenes</a>.
|
|
</div>
|
|
<?php else: ?>
|
|
<div id="catalogoExams">
|
|
<?php foreach ($examTipos as $categoria => $tipos): ?>
|
|
<div class="exam-categoria mb-2">
|
|
<div class="fw-semibold text-uppercase text-muted small border-bottom pb-1 mb-2 categoria-header">
|
|
<?= htmlspecialchars($categoria) ?>
|
|
</div>
|
|
<div class="row g-2">
|
|
<?php foreach ($tipos as $t): ?>
|
|
<div class="col-sm-6 col-md-4 exam-item"
|
|
data-nombre="<?= htmlspecialchars(strtolower($t['nombre'])) ?>"
|
|
data-codigo="<?= htmlspecialchars(strtolower($t['codigo'])) ?>">
|
|
<label class="card h-100 border-0 bg-light p-2 cursor-pointer exam-card"
|
|
for="et<?= $t['id'] ?>">
|
|
<div class="d-flex align-items-start gap-2">
|
|
<input class="form-check-input mt-1 exam-checkbox flex-shrink-0"
|
|
type="checkbox"
|
|
id="et<?= $t['id'] ?>"
|
|
value="<?= $t['id'] ?>"
|
|
data-nombre="<?= htmlspecialchars($t['nombre']) ?>"
|
|
data-ayuno="<?= $t['requiere_ayuno'] ? '1' : '0' ?>"
|
|
data-horas-ayuno="<?= (int) $t['horas_ayuno'] ?>"
|
|
data-precio="<?= $t['precio_base'] !== null ? number_format((float)$t['precio_base'], 0, ',', '.') : '' ?>">
|
|
<div class="lh-sm">
|
|
<div class="fw-semibold small"><?= htmlspecialchars($t['nombre']) ?></div>
|
|
<div class="text-muted" style="font-size:.75rem"><?= htmlspecialchars($t['codigo']) ?></div>
|
|
<?php if ($t['requiere_ayuno']): ?>
|
|
<span class="badge bg-warning text-dark" style="font-size:.65rem">
|
|
<i class="fas fa-clock me-1"></i>Ayuno <?= $t['horas_ayuno'] ? $t['horas_ayuno'].'h' : '' ?>
|
|
</span>
|
|
<?php endif; ?>
|
|
<?php if ($t['precio_base']): ?>
|
|
<span class="badge bg-light text-secondary border" style="font-size:.65rem">
|
|
$<?= number_format((float)$t['precio_base'], 0, ',', '.') ?>
|
|
</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Resumen seleccionados -->
|
|
<div id="resumenExams" class="mt-3 d-none">
|
|
<div class="alert alert-primary py-2 mb-0">
|
|
<strong><i class="fas fa-check-circle me-1"></i>Seleccionados:</strong>
|
|
<span id="resumenLista"></span>
|
|
<span id="ayunoAviso" class="ms-2 badge bg-warning text-dark d-none">
|
|
<i class="fas fa-clock me-1"></i> Ayuno requerido
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Botones ───────────────────────────────────── -->
|
|
<div class="d-flex gap-2 justify-content-end mb-4">
|
|
<a href="<?= BASE_URL ?>/erp.php?m=registro_exams&v=lista"
|
|
class="btn btn-outline-secondary">
|
|
Cancelar
|
|
</a>
|
|
<button type="submit" class="btn btn-primary px-4" id="btnGuardar">
|
|
<i class="fas fa-save me-1"></i> Crear orden
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<style>
|
|
.exam-card { cursor: pointer; transition: border-color .15s; }
|
|
.exam-card:hover { border-color: #0d6efd !important; background: #e9f0ff !important; }
|
|
.exam-card:has(.exam-checkbox:checked) { background: #dbeafe !important; border: 1px solid #0d6efd !important; }
|
|
.cursor-pointer { cursor: pointer; }
|
|
</style>
|
|
|
|
<script>
|
|
(function () {
|
|
const BASE = '<?= BASE_URL ?>';
|
|
const API_PAC = BASE + '/api/lab/get_pacientes.php';
|
|
const API_SV = BASE + '/modules/registro_exams/api/save_orden.php';
|
|
let debounceTimer = null;
|
|
let examDebounce = null;
|
|
|
|
// ── Buscador de pacientes ──────────────────────────────────
|
|
const inputBuscarPac = document.getElementById('inputBuscarPac');
|
|
const resultadosPac = document.getElementById('resultadosPac');
|
|
const buscadorDiv = document.getElementById('buscadorPaciente');
|
|
const seleccionadoDiv = document.getElementById('pacienteSeleccionado');
|
|
const pacNombreEl = document.getElementById('pacNombre');
|
|
const pacDocEl = document.getElementById('pacDoc');
|
|
const pacienteIdEl = document.getElementById('pacienteId');
|
|
|
|
if (inputBuscarPac) {
|
|
inputBuscarPac.addEventListener('input', () => {
|
|
clearTimeout(debounceTimer);
|
|
const q = inputBuscarPac.value.trim();
|
|
if (q.length < 2) { resultadosPac.innerHTML = ''; return; }
|
|
debounceTimer = setTimeout(() => buscarPaciente(q), 300);
|
|
});
|
|
}
|
|
|
|
async function buscarPaciente(q) {
|
|
try {
|
|
const r = await fetch(`${API_PAC}?busqueda=${encodeURIComponent(q)}&limit=8`);
|
|
const j = await r.json();
|
|
if (!j.ok || !j.data?.length) {
|
|
resultadosPac.innerHTML = '<div class="list-group-item text-muted small">Sin resultados</div>';
|
|
return;
|
|
}
|
|
resultadosPac.innerHTML = j.data.map(p =>
|
|
`<button type="button" class="list-group-item list-group-item-action py-2"
|
|
data-id="${p.id}" data-nombre="${escAtt(p.nombre_completo)}" data-doc="${escAtt(p.numero_documento)} ${escAtt(p.tipo_documento||'')}">
|
|
<div class="fw-semibold lh-sm">${escHtml(p.nombre_completo)}</div>
|
|
<div class="small text-muted">${escHtml(p.tipo_documento||'')} ${escHtml(p.numero_documento)}</div>
|
|
</button>`
|
|
).join('');
|
|
|
|
resultadosPac.querySelectorAll('button').forEach(btn => {
|
|
btn.addEventListener('click', () => seleccionarPaciente(
|
|
parseInt(btn.dataset.id), btn.dataset.nombre, btn.dataset.doc
|
|
));
|
|
});
|
|
} catch { resultadosPac.innerHTML = ''; }
|
|
}
|
|
|
|
function seleccionarPaciente(id, nombre, doc) {
|
|
pacienteIdEl.value = id;
|
|
pacNombreEl.textContent = nombre;
|
|
pacDocEl.textContent = doc;
|
|
resultadosPac.innerHTML = '';
|
|
buscadorDiv.classList.add('d-none');
|
|
seleccionadoDiv.classList.remove('d-none');
|
|
seleccionadoDiv.classList.remove('alert-light');
|
|
seleccionadoDiv.classList.add('alert-success');
|
|
}
|
|
|
|
const btnCambiar = document.getElementById('btnCambiarPaciente');
|
|
if (btnCambiar) {
|
|
btnCambiar.addEventListener('click', () => {
|
|
pacienteIdEl.value = '';
|
|
buscadorDiv.classList.remove('d-none');
|
|
seleccionadoDiv.classList.add('d-none');
|
|
if (inputBuscarPac) { inputBuscarPac.value = ''; inputBuscarPac.focus(); }
|
|
});
|
|
}
|
|
|
|
// ── Buscador de exámenes ───────────────────────────────────
|
|
const inputBuscarExam = document.getElementById('inputBuscarExam');
|
|
if (inputBuscarExam) {
|
|
inputBuscarExam.addEventListener('input', () => {
|
|
clearTimeout(examDebounce);
|
|
examDebounce = setTimeout(() => filtrarExams(inputBuscarExam.value.trim().toLowerCase()), 200);
|
|
});
|
|
}
|
|
|
|
function filtrarExams(q) {
|
|
const items = document.querySelectorAll('.exam-item');
|
|
const cats = document.querySelectorAll('.exam-categoria');
|
|
items.forEach(el => {
|
|
const visible = !q || el.dataset.nombre.includes(q) || el.dataset.codigo.includes(q);
|
|
el.style.display = visible ? '' : 'none';
|
|
});
|
|
// Ocultar categorías vacías
|
|
cats.forEach(cat => {
|
|
const visibles = [...cat.querySelectorAll('.exam-item')].some(i => i.style.display !== 'none');
|
|
cat.style.display = visibles ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
// ── Resumen de exámenes seleccionados ──────────────────────
|
|
document.querySelectorAll('.exam-checkbox').forEach(cb => {
|
|
cb.addEventListener('change', actualizarResumen);
|
|
});
|
|
|
|
function actualizarResumen() {
|
|
const sel = [...document.querySelectorAll('.exam-checkbox:checked')];
|
|
const resumenDiv = document.getElementById('resumenExams');
|
|
const resumenLista = document.getElementById('resumenLista');
|
|
const ayunoAviso = document.getElementById('ayunoAviso');
|
|
|
|
if (!sel.length) {
|
|
resumenDiv.classList.add('d-none');
|
|
return;
|
|
}
|
|
resumenDiv.classList.remove('d-none');
|
|
resumenLista.textContent = sel.map(c => c.dataset.nombre).join(' · ');
|
|
const ayuno = sel.some(c => c.dataset.ayuno === '1');
|
|
ayunoAviso.classList.toggle('d-none', !ayuno);
|
|
}
|
|
|
|
// ── Enviar formulario ──────────────────────────────────────
|
|
document.getElementById('formOrden').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const pacId = parseInt(document.getElementById('pacienteId').value) || 0;
|
|
if (!pacId) {
|
|
alert('Seleccione un paciente antes de continuar.');
|
|
return;
|
|
}
|
|
|
|
const items = [...document.querySelectorAll('.exam-checkbox:checked')]
|
|
.map(c => parseInt(c.value));
|
|
if (!items.length) {
|
|
alert('Seleccione al menos un examen.');
|
|
return;
|
|
}
|
|
|
|
const btn = document.getElementById('btnGuardar');
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Guardando…';
|
|
|
|
const body = {
|
|
id: null,
|
|
paciente_id: pacId,
|
|
solicitud_id: parseInt(document.getElementById('solicitudIdHidden').value) || null,
|
|
medico_nombre: document.getElementById('medicoNombre').value.trim(),
|
|
medico_registro: document.getElementById('medicoRegistro').value.trim(),
|
|
diagnostico: document.getElementById('diagnostico').value.trim(),
|
|
prioridad: document.getElementById('prioridad').value,
|
|
notas: document.getElementById('notas').value.trim(),
|
|
items: items,
|
|
};
|
|
|
|
try {
|
|
const r = await fetch(API_SV, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const j = await r.json();
|
|
if (!j.ok) throw new Error(j.error);
|
|
// Redirigir a la orden creada
|
|
window.location.href = `${BASE}/erp.php?m=registro_exams&v=orden&id=${j.id}`;
|
|
} catch (err) {
|
|
alert('Error al guardar: ' + err.message);
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-save me-1"></i> Crear orden';
|
|
}
|
|
});
|
|
|
|
function escHtml(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
function escAtt(s) { return String(s||'').replace(/"/g,'"'); }
|
|
})();
|
|
</script>
|
|
<?php Layout::close(); ?>
|