recepcion: fix solo_muestras, reorder fields, duplicar visita, kiosko label

- create_solicitud.php: evitar SQL `IN ()` vacío cuando solo_muestras=1 sin exámenes
- recepcion.php: mover embarazada+médico después del historial del paciente
- recepcion.php: actualizar label kiosko con nombre real al vincular paciente
- recepcion.php: botón "Duplicar" en historial para copiar exámenes y pago de última visita
- recepcion.php: omitir validación de consentimientos pendientes cuando es solo_muestras
- recepcion.php: agregar alias esc() = escHtml() usado en buscarMedico

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-03 08:13:38 -05:00
co-authored by Claude Sonnet 4.6
parent d3826972ad
commit c511da18bd
2 changed files with 94 additions and 34 deletions
+9 -7
View File
@@ -94,13 +94,15 @@ try {
jsonError('Lugar destino no encontrado o inactivo.', 404);
}
// Verificar que todos los exam_tipo_ids existen
$in = implode(',', array_fill(0, count($examIds), '?'));
$stmt = $pdo->prepare("SELECT COUNT(*) FROM exam_tipos WHERE id IN ({$in}) AND activo = 1");
$stmt->execute($examIds);
if ((int)$stmt->fetchColumn() !== count($examIds)) {
$pdo->rollBack();
jsonError('Uno o más exámenes no existen o están inactivos.', 422);
// Verificar que todos los exam_tipo_ids existen (se omite si solo_muestras y sin exámenes)
if (!empty($examIds)) {
$in = implode(',', array_fill(0, count($examIds), '?'));
$stmt = $pdo->prepare("SELECT COUNT(*) FROM exam_tipos WHERE id IN ({$in}) AND activo = 1");
$stmt->execute($examIds);
if ((int)$stmt->fetchColumn() !== count($examIds)) {
$pdo->rollBack();
jsonError('Uno o más exámenes no existen o están inactivos.', 422);
}
}
// ── Insertar o reemplazar solicitud (DELETE + INSERT para poder re-guardar) ──
+85 -27
View File
@@ -404,7 +404,7 @@ document.addEventListener('DOMContentLoaded', function() {
<!-- Si el kiosko capturó un nombre -->
<div id="bloque-nombre-kiosko" class="d-none mb-2">
<small class="text-muted">Nombre del kiosko:</small>
<small class="text-muted" id="lbl-kiosko-label">Kiosko:</small>
<div class="fw-semibold" id="lbl-nombre-kiosko"></div>
</div>
@@ -438,6 +438,29 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
</div>
<!-- Historial colapsable del paciente -->
<div id="sec-historial-pac" class="d-none">
<button class="hist-toggle-btn" id="btn-hist-toggle" onclick="toggleHistorialPaciente()">
<i class="fas fa-history" style="color:#6366f1"></i>
Historial del paciente
<span id="btn-duplicar-wrap" class="ms-auto d-none">
<span class="btn btn-outline-indigo btn-sm py-0 px-2"
style="font-size:.7rem;border-color:#6366f1;color:#6366f1;background:#fff;border-radius:6px"
onmousedown="event.stopPropagation();duplicarUltimaVisita()">
<i class="fas fa-copy me-1"></i>Duplicar
</span>
</span>
<i class="fas fa-chevron-down hist-chev"></i>
</button>
<div id="hist-pac-body" class="hist-pac-body d-none">
<div id="hist-pac-content" class="pac-timeline">
<div class="text-center text-muted py-2 small">
<i class="fas fa-spinner fa-spin me-1"></i>Cargando…
</div>
</div>
</div>
</div>
<!-- Embarazo -->
<div class="mt-2">
<div class="form-check">
@@ -468,22 +491,6 @@ document.addEventListener('DOMContentLoaded', function() {
</div>
<input type="hidden" id="inp-medico-id">
</div>
<!-- Historial colapsable del paciente -->
<div id="sec-historial-pac" class="d-none">
<button class="hist-toggle-btn" id="btn-hist-toggle" onclick="toggleHistorialPaciente()">
<i class="fas fa-history" style="color:#6366f1"></i>
Historial del paciente
<i class="fas fa-chevron-down hist-chev"></i>
</button>
<div id="hist-pac-body" class="hist-pac-body d-none">
<div id="hist-pac-content" class="pac-timeline">
<div class="text-center text-muted py-2 small">
<i class="fas fa-spinner fa-spin me-1"></i>Cargando…
</div>
</div>
</div>
</div>
</div>
</div>
@@ -1038,10 +1045,11 @@ function abrirFicha(turno) {
document.getElementById('badge-turno-activo').classList.remove('d-none');
document.getElementById('badge-codigo').textContent = turno.codigo;
// Nombre del kiosko
// Nombre del kiosko (valor capturado en kiosko; se actualiza al vincular paciente)
if (turno.paciente_nombre) {
document.getElementById('bloque-nombre-kiosko').classList.remove('d-none');
document.getElementById('lbl-nombre-kiosko').textContent = turno.paciente_nombre;
document.getElementById('lbl-kiosko-label').textContent = 'Kiosko:';
document.getElementById('inp-buscar-pac').value = turno.paciente_nombre;
}
@@ -1154,13 +1162,19 @@ function seleccionarPaciente(pac) {
}
document.getElementById('bloque-pac-no-vinculado').classList.add('d-none');
document.getElementById('bloque-pac-seleccionado').classList.remove('d-none');
document.getElementById('lbl-pac-nombre').textContent =
(pac.full_name || (pac.nombre||'') + ' ' + (pac.apellido||'')).trim();
const pacNombre = (pac.full_name || pac.nombre_completo || (pac.nombre||'') + ' ' + (pac.apellido||'')).trim();
document.getElementById('lbl-pac-nombre').textContent = pacNombre;
document.getElementById('lbl-pac-doc').textContent =
(pac.tipo_documento||'') + ' ' + (pac.documento||pac.numero_documento||'');
document.getElementById('lbl-pac-cel').textContent =
pac.telefono || pac.celular || '';
document.getElementById('lista-pacientes-res').innerHTML = '';
// Actualizar etiqueta kiosko con el nombre real del paciente vinculado
const lblKiosko = document.getElementById('lbl-nombre-kiosko');
if (lblKiosko && document.getElementById('bloque-nombre-kiosko').classList.contains('d-none') === false) {
lblKiosko.textContent = pacNombre;
document.getElementById('lbl-kiosko-label').textContent = 'Cliente:';
}
// Mostrar historial del paciente
const secHist = document.getElementById('sec-historial-pac');
@@ -1168,6 +1182,7 @@ function seleccionarPaciente(pac) {
// Resetear estado colapsado
document.getElementById('hist-pac-body').classList.add('d-none');
document.getElementById('btn-hist-toggle').classList.remove('open');
document.getElementById('btn-duplicar-wrap').classList.remove('d-none');
// Precargar en background (usuario decide si abre)
_historialPacienteCargado = false;
_historialPacienteId = pac.id;
@@ -1203,6 +1218,7 @@ function desvincularPaciente() {
document.getElementById('sec-historial-pac').classList.add('d-none');
document.getElementById('hist-pac-body').classList.add('d-none');
document.getElementById('btn-hist-toggle').classList.remove('open');
document.getElementById('btn-duplicar-wrap').classList.add('d-none');
document.getElementById('lista-pacientes-res').innerHTML = '';
document.getElementById('inp-buscar-pac').value = '';
document.getElementById('chk-embarazada').checked = false;
@@ -1314,14 +1330,16 @@ async function guardarSolicitud() {
const lugarId = parseInt(document.getElementById('sel-lugar').value);
if (!lugarId) { mostrarError('Seleccione el lugar destino.'); return; }
// ✅ VALIDACIÓN: verificar que los consentimientos del lugar estén firmados
const consentPendientes = Array.from(document.querySelectorAll('.consent-item.pendiente, .consent-item.enviado, .consent-item.visto')).length;
if (consentPendientes > 0) {
mostrarError('⚠️ El paciente debe firmar los consentimientos del lugar antes de guardar la solicitud.');
return;
}
const soloMuestras = document.getElementById('chk-solo-muestras').checked;
// ✅ VALIDACIÓN: verificar que los consentimientos del lugar estén firmados (omitir en solo muestras)
if (!soloMuestras) {
const consentPendientes = Array.from(document.querySelectorAll('.consent-item.pendiente, .consent-item.enviado, .consent-item.visto')).length;
if (consentPendientes > 0) {
mostrarError('⚠️ El paciente debe firmar los consentimientos del lugar antes de guardar la solicitud.');
return;
}
}
const examIds = Array.from(document.querySelectorAll('.exam-chk:checked')).map(c => parseInt(c.value));
if (!soloMuestras && !examIds.length) { mostrarError('Seleccione al menos un examen.'); return; }
@@ -1739,6 +1757,45 @@ function resetMedico() {
quitarMedico();
}
async function duplicarUltimaVisita() {
if (!_historialPacienteId) { mostrarError('No hay paciente seleccionado.'); return; }
mostrarToast('Buscando última visita…', 'info', 1500);
try {
// 1. Obtener el turno más reciente del paciente
const rH = await fetch(`${API}get_historial.php?paciente_id=${_historialPacienteId}&per_page=1&page=1`);
const jH = await rH.json();
if (!jH.ok || !jH.turnos?.length) { mostrarError('Sin visitas anteriores para duplicar.'); return; }
const ultimoTurnoId = jH.turnos[0].id;
// 2. Obtener solicitud + exámenes de esa visita
const rS = await fetch(`${API}get_consentimientos.php?turno_id=${ultimoTurnoId}&incluir_solicitud=1`);
const jS = await rS.json();
if (!jS.ok || !jS.examenes?.length) { mostrarError('La última visita no tiene exámenes registrados.'); return; }
// 3. Pre-marcar checkboxes con los exam_tipo_ids
const ids = new Set(jS.examenes.map(e => String(e.id)));
let marcados = 0;
document.querySelectorAll('.exam-chk').forEach(c => {
const match = ids.has(c.value);
c.checked = match;
if (match) marcados++;
});
// 4. Pre-llenar pago si viene
if (jS.solicitud?.metodo_pago) {
document.getElementById('sel-pago').value = jS.solicitud.metodo_pago;
togglePagoCombinado();
}
if (jS.solicitud?.total_cobrado) {
document.getElementById('inp-total').value = jS.solicitud.total_cobrado;
}
mostrarToast(`Duplicado: ${marcados} exámen${marcados !== 1 ? 'es' : ''} copiado${marcados !== 1 ? 's' : ''}.`, 'success', 3000);
} catch (e) {
mostrarError('Error al duplicar: ' + e.message);
}
}
function escJs(s) { return String(s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'"); }
function toggleSoloMuestras() {
@@ -1766,6 +1823,7 @@ function escHtml(str) {
d.appendChild(document.createTextNode(String(str)));
return d.innerHTML;
}
const esc = escHtml;
function resetPlaceholder() {
const ph = document.getElementById('ficha-placeholder');