From 538ee8ad2554d3783a09ed47a8b41c35a6af956c Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:10:59 -0500 Subject: [PATCH] Vincular turnos por muestra pendiente y precargar Datos Toma de Muestras MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El turno original no se modifica (queda finalizado igual que antes) — solo se agrega trazabilidad: turnero_muestras.recibida_en_turno_id registra en qué turno se completó una muestra que había quedado pendiente en otra visita. Historial, bandeja y lugar.php ahora muestran un chip/botón "Turno vinculado" en ambos sentidos (origen ⇄ seguimiento) con acceso directo. Además, al abrir un formulario "Datos Toma de Muestras" pendiente para una visita de seguimiento, aparece un botón para cargar las respuestas del último formulario firmado de ese mismo paciente (sin firma ni identidad del profesional anterior) — evita reescribir la historia clínica; la firma y fecha siguen siendo las de hoy. Co-Authored-By: Claude Sonnet 4.6 --- modules/turnero/api/get_bandeja.php | 27 +++++++++ .../turnero/api/get_formulario_anterior.php | 53 ++++++++++++++++ modules/turnero/api/get_historial.php | 37 ++++++++++++ modules/turnero/api/update_muestra_estado.php | 16 +++-- modules/turnero/views/bandeja.php | 17 +++++- modules/turnero/views/historial.php | 28 ++++++++- modules/turnero/views/lugar.php | 1 + ver_formulario_enviado.php | 60 +++++++++++++++++++ 8 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 modules/turnero/api/get_formulario_anterior.php diff --git a/modules/turnero/api/get_bandeja.php b/modules/turnero/api/get_bandeja.php index a017d53..00c5a9f 100644 --- a/modules/turnero/api/get_bandeja.php +++ b/modules/turnero/api/get_bandeja.php @@ -81,12 +81,39 @@ if (isset($_GET['turno_id'])) { $stmtCom->execute([$tid]); $comentarios = $stmtCom->fetchAll(PDO::FETCH_ASSOC); + // Turnos vinculados por muestras pendientes completadas en otra visita (ambos sentidos) + $relacionados = []; + $stmtRelFwd = $pdo->prepare(" + SELECT t2.id AS rel_id, t2.codigo AS rel_codigo, s2.fecha AS rel_fecha + FROM turnero_muestras tm + JOIN turnero_turnos t2 ON t2.id = tm.recibida_en_turno_id + JOIN turnero_sesiones s2 ON s2.id = t2.sesion_id + WHERE tm.solicitud_id = ? AND tm.recibida_en_turno_id IS NOT NULL + "); + $stmtRelFwd->execute([$solId]); + foreach ($stmtRelFwd->fetchAll(PDO::FETCH_ASSOC) as $r) { + $relacionados[] = ['turno_id' => (int)$r['rel_id'], 'codigo' => $r['rel_codigo'], 'fecha' => $r['rel_fecha'], 'direccion' => 'seguimiento']; + } + $stmtRelBack = $pdo->prepare(" + SELECT t1.id AS rel_id, t1.codigo AS rel_codigo, s1.fecha AS rel_fecha + FROM turnero_muestras tm + JOIN turnero_solicitudes ts1 ON ts1.id = tm.solicitud_id + JOIN turnero_turnos t1 ON t1.id = ts1.turno_id + JOIN turnero_sesiones s1 ON s1.id = t1.sesion_id + WHERE tm.recibida_en_turno_id = ? + "); + $stmtRelBack->execute([$tid]); + foreach ($stmtRelBack->fetchAll(PDO::FETCH_ASSOC) as $r) { + $relacionados[] = ['turno_id' => (int)$r['rel_id'], 'codigo' => $r['rel_codigo'], 'fecha' => $r['rel_fecha'], 'direccion' => 'origen']; + } + jsonOk([ 'turno' => $data, 'examenes_por_cat'=> $examenesPorCat, 'muestras' => $muestras, 'consentimientos' => $consentimientos, 'comentarios' => $comentarios, + 'relacionados' => $relacionados, ]); } diff --git a/modules/turnero/api/get_formulario_anterior.php b/modules/turnero/api/get_formulario_anterior.php new file mode 100644 index 0000000..89e7d96 --- /dev/null +++ b/modules/turnero/api/get_formulario_anterior.php @@ -0,0 +1,53 @@ +prepare( + "SELECT tc.datos_respuestas, tc.firmado_at, t.codigo AS turno_codigo + FROM turnero_consentimientos tc + JOIN turnero_turnos t ON t.id = tc.turno_id + LEFT JOIN turnero_solicitudes ts ON ts.turno_id = t.id + WHERE tc.formulario_id = ? + AND tc.estado = 'firmado' + AND COALESCE(ts.paciente_id, t.paciente_id) = ? + AND t.id != ? + ORDER BY tc.firmado_at DESC + LIMIT 1" +); +$stmt->execute([$formularioId, $pacienteId, $excludeTurno]); +$row = $stmt->fetch(PDO::FETCH_ASSOC); + +if (!$row) { + jsonOk(['encontrado' => false]); +} + +$datos = json_decode($row['datos_respuestas'] ?? '{}', true) ?: []; +// No traer firmas ni identidad del profesional anterior: cada visita se firma de nuevo +foreach (array_keys($datos) as $k) { + if (str_contains($k, 'firma') || $k === '_pro_nombre' || $k === '_pro_cedula') { + unset($datos[$k]); + } +} + +jsonOk([ + 'encontrado' => true, + 'datos_respuestas'=> $datos, + 'firmado_at' => $row['firmado_at'], + 'turno_codigo' => $row['turno_codigo'], +]); diff --git a/modules/turnero/api/get_historial.php b/modules/turnero/api/get_historial.php index 0d224b0..8e08e90 100644 --- a/modules/turnero/api/get_historial.php +++ b/modules/turnero/api/get_historial.php @@ -144,6 +144,7 @@ $turnoIds = array_column($turnos, 'id'); $examenes = []; $consentimientos = []; $comentarios = []; +$relacionados = []; if ($turnoIds) { $ph = implode(',', array_fill(0, count($turnoIds), '?')); @@ -194,6 +195,41 @@ if ($turnoIds) { } } + // Turnos vinculados por muestras pendientes completadas en otra visita — batch (ambos sentidos) + // Sentido "seguimiento": este turno tenía una muestra pendiente que se completó en OTRO turno + $stmtRelFwd = $pdo->prepare( + "SELECT DISTINCT ts.turno_id AS turno_id, t2.id AS rel_id, t2.codigo AS rel_codigo, s2.fecha AS rel_fecha + FROM turnero_muestras tm + JOIN turnero_solicitudes ts ON ts.id = tm.solicitud_id + JOIN turnero_turnos t2 ON t2.id = tm.recibida_en_turno_id + JOIN turnero_sesiones s2 ON s2.id = t2.sesion_id + WHERE ts.turno_id IN ($ph) AND tm.recibida_en_turno_id IS NOT NULL" + ); + $stmtRelFwd->execute($turnoIds); + foreach ($stmtRelFwd->fetchAll(PDO::FETCH_ASSOC) as $row) { + $tid = (int)$row['turno_id']; + $relacionados[$tid][] = [ + 'turno_id' => (int)$row['rel_id'], 'codigo' => $row['rel_codigo'], + 'fecha' => $row['rel_fecha'], 'direccion' => 'seguimiento', + ]; + } + // Sentido "origen": este turno completó una muestra pendiente de OTRO turno anterior + $stmtRelBack = $pdo->prepare( + "SELECT DISTINCT tm.recibida_en_turno_id AS turno_id, t1.id AS rel_id, t1.codigo AS rel_codigo, s1.fecha AS rel_fecha + FROM turnero_muestras tm + JOIN turnero_solicitudes ts ON ts.id = tm.solicitud_id + JOIN turnero_turnos t1 ON t1.id = ts.turno_id + JOIN turnero_sesiones s1 ON s1.id = t1.sesion_id + WHERE tm.recibida_en_turno_id IN ($ph)" + ); + $stmtRelBack->execute($turnoIds); + foreach ($stmtRelBack->fetchAll(PDO::FETCH_ASSOC) as $row) { + $tid = (int)$row['turno_id']; + $relacionados[$tid][] = [ + 'turno_id' => (int)$row['rel_id'], 'codigo' => $row['rel_codigo'], + 'fecha' => $row['rel_fecha'], 'direccion' => 'origen', + ]; + } } foreach ($turnos as &$turno) { @@ -201,6 +237,7 @@ foreach ($turnos as &$turno) { $turno['examenes'] = $examenes[$tid] ?? []; $turno['consentimientos'] = $consentimientos[$tid] ?? []; $turno['comentarios'] = $comentarios[$tid] ?? []; + $turno['relacionados'] = $relacionados[$tid] ?? []; } // ── Resumen por estado (del rango filtrado) ─────────────────── diff --git a/modules/turnero/api/update_muestra_estado.php b/modules/turnero/api/update_muestra_estado.php index 6d4509d..bd972f1 100644 --- a/modules/turnero/api/update_muestra_estado.php +++ b/modules/turnero/api/update_muestra_estado.php @@ -8,6 +8,8 @@ * muestra_id int requerido * estado string requerido 'recibida' | 'rechazada' | 'pendiente' * motivo_rechazo string opcional requerido si estado = 'rechazada' + * turno_id int opcional turno activo desde el que se recibe (para trazabilidad + * cuando la muestra es de una visita anterior) */ require_once __DIR__ . '/_helpers.php'; @@ -18,6 +20,7 @@ $datos = inputJson(); $muestraId = isset($datos['muestra_id']) ? (int)$datos['muestra_id'] : 0; $estado = trim($datos['estado'] ?? ''); $motivo = isset($datos['motivo_rechazo']) ? trim($datos['motivo_rechazo']) : null; +$turnoId = isset($datos['turno_id']) ? (int)$datos['turno_id'] : 0; if ($muestraId <= 0) jsonError('muestra_id inválido.'); if (!in_array($estado, ['recibida', 'rechazada', 'pendiente'], true)) { @@ -29,8 +32,9 @@ $pdo = db(); // Verificar que la muestra existe $stmt = $pdo->prepare( - "SELECT tm.id, tm.solicitud_id, tm.estado + "SELECT tm.id, tm.solicitud_id, tm.estado, ts.turno_id AS turno_origen_id FROM turnero_muestras tm + JOIN turnero_solicitudes ts ON ts.id = tm.solicitud_id WHERE tm.id = ?" ); $stmt->execute([$muestraId]); @@ -38,6 +42,10 @@ $muestra = $stmt->fetch(PDO::FETCH_ASSOC); if (!$muestra) jsonError('Muestra no encontrada.', 404); +// Solo registrar "recibida en otro turno" cuando de verdad es una visita distinta +// a la que generó la muestra (no tocar la solicitud/turno original). +$recibidaEnTurno = ($turnoId && $turnoId !== (int)$muestra['turno_origen_id']) ? $turnoId : null; + // Construir campos a actualizar $ahora = date('Y-m-d H:i:s'); $adminI = adminId(); @@ -46,9 +54,9 @@ if ($estado === 'recibida') { $pdo->prepare( "UPDATE turnero_muestras SET estado = 'recibida', recibida_por = ?, recibida_at = ?, - motivo_rechazo = NULL + recibida_en_turno_id = ?, motivo_rechazo = NULL WHERE id = ?" - )->execute([$adminI, $ahora, $muestraId]); + )->execute([$adminI, $ahora, $recibidaEnTurno, $muestraId]); } elseif ($estado === 'rechazada') { $pdo->prepare( "UPDATE turnero_muestras @@ -61,7 +69,7 @@ if ($estado === 'recibida') { $pdo->prepare( "UPDATE turnero_muestras SET estado = 'pendiente', recibida_por = NULL, - recibida_at = NULL, motivo_rechazo = NULL + recibida_at = NULL, recibida_en_turno_id = NULL, motivo_rechazo = NULL WHERE id = ?" )->execute([$muestraId]); } diff --git a/modules/turnero/views/bandeja.php b/modules/turnero/views/bandeja.php index 55caccf..c879631 100644 --- a/modules/turnero/views/bandeja.php +++ b/modules/turnero/views/bandeja.php @@ -642,6 +642,21 @@ function _renderDetalle(d) { } secM += ``; + // Turno vinculado (muestra pendiente completada en otra visita, o viceversa) + let secRel = ''; + if (d.relacionados?.length) { + secRel = `
Turno vinculado
`; + d.relacionados.forEach(r => { + const txt = r.direccion === 'seguimiento' + ? `Muestra completada en ${r.codigo}` + : `Completa muestra pendiente de ${r.codigo}`; + secRel += ``; + }); + secRel += `
`; + } + // Consentimientos let secC = `
Consentimientos
`; if (!d.consentimientos?.length) { @@ -687,7 +702,7 @@ function _renderDetalle(d) {
`; - document.getElementById('bnd-detail').innerHTML = hdr + secEx + secM + secC + secCom; + document.getElementById('bnd-detail').innerHTML = hdr + secEx + secM + secRel + secC + secCom; } // ── Marcar visto ────────────────────────────────────────────── diff --git a/modules/turnero/views/historial.php b/modules/turnero/views/historial.php index 1e16069..ad6a698 100644 --- a/modules/turnero/views/historial.php +++ b/modules/turnero/views/historial.php @@ -345,6 +345,15 @@ function buscar(pageNum) { cargarPagina(); } +function irATurno(codigo, fecha) { + document.getElementById('fDesde').value = fecha || hoy(); + document.getElementById('fHasta').value = fecha || hoy(); + document.getElementById('fQ').value = codigo; + document.getElementById('fEstado').value = ''; + buscar(); + window.scrollTo({top:0, behavior:'smooth'}); +} + async function cargarPagina() { mostrarSpinner(); const params = new URLSearchParams({ @@ -524,6 +533,23 @@ function renderDetalle(t) { { lbl:'Atendió muestras', val: esc(t.atendido_lugar_nombre || '—') }, { lbl:'Notas', val: esc(t.notas || '—') }, ]; + // Turnos vinculados (muestra pendiente completada en otra visita, o viceversa) + let relHtml = ''; + if (t.relacionados && t.relacionados.length) { + relHtml = `
+
Turno vinculado
+
${t.relacionados.map(r => { + const txt = r.direccion === 'seguimiento' + ? `Muestra completada en ${esc(r.codigo)}` + : `Completa muestra pendiente de ${esc(r.codigo)}`; + return ``; + }).join('')}
+
`; + } + // Exámenes let examHtml = ''; if (t.examenes && t.examenes.length) { @@ -555,7 +581,7 @@ function renderDetalle(t) {
${it.val}
`).join('')} - ${examHtml}${comHtml} + ${relHtml}${examHtml}${comHtml}
Documentos
Cargando… diff --git a/modules/turnero/views/lugar.php b/modules/turnero/views/lugar.php index 44c67ff..f52454d 100644 --- a/modules/turnero/views/lugar.php +++ b/modules/turnero/views/lugar.php @@ -2550,6 +2550,7 @@ async function marcarMuestra(muestraId, estado, motivo = null) { try { const body = { muestra_id: muestraId, estado }; if (motivo) body.motivo_rechazo = motivo; + if (turnoActivo?.id) body.turno_id = turnoActivo.id; const res = await fetch(API + 'update_muestra_estado.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/ver_formulario_enviado.php b/ver_formulario_enviado.php index 83992e2..6f3d339 100644 --- a/ver_formulario_enviado.php +++ b/ver_formulario_enviado.php @@ -34,6 +34,7 @@ if ($modoTurnero) { tc.siguiente_toma_at, tc.toma_inicio_at, f.nombre AS form_nombre, f.categoria, f.descripcion AS form_descripcion, f.esquema, f.es_toma_progresiva, f.solo_profesional, f.doc_encabezado, f.doc_subtitulo, f.doc_logo_base64, f.doc_color, f.doc_pie_pagina, + p.id AS paciente_id, p.nombre_completo AS paciente_nombre, p.numero_documento, p.tipo_documento, p.fecha_nacimiento, p.telefono AS paciente_telefono, p.eps, @@ -986,6 +987,15 @@ function _addExamWizardHtml(string $cid): string {
+ + + + ; window._profNombre = ; window._profDocumento = ; + +// ── Cargar datos de la visita anterior (mismo formulario, mismo paciente) ── +(function() { + var btn = document.getElementById('btnCargarAnterior'); + if (!btn) return; + btn.addEventListener('click', function() { + var base = window.location.href.split('/ver_formulario_enviado.php')[0]; + var msg = document.getElementById('cargarAnteriorMsg'); + btn.disabled = true; + btn.innerHTML = 'Buscando…'; + var params = new URLSearchParams({ + paciente_id: btn.dataset.paciente, + formulario_id: btn.dataset.formulario, + exclude_turno_id: btn.dataset.turno, + }); + fetch(base + '/modules/turnero/api/get_formulario_anterior.php?' + params) + .then(function(r) { return r.json(); }) + .then(function(json) { + btn.disabled = false; + btn.innerHTML = 'Cargar datos de la visita anterior'; + if (!json.ok || !json.encontrado) { + msg.style.display = ''; + msg.textContent = 'No hay una visita anterior con este formulario firmado.'; + return; + } + var datos = json.datos_respuestas || {}; + document.querySelectorAll('[name]').forEach(function(el) { + var raw = el.name, isArr = raw.slice(-2) === '[]', name = isArr ? raw.slice(0, -2) : raw; + if (!(name in datos)) return; + var val = datos[name]; + if (el.type === 'checkbox') { + var arr = Array.isArray(val) ? val : [val]; + el.checked = arr.includes(el.value); + } else if (el.type === 'radio') { + el.checked = (el.value === val); + } else { + el.value = val; + } + }); + msg.style.display = ''; + msg.textContent = 'Datos cargados de la visita ' + (json.turno_codigo || '') + '. Revise y firme.'; + }) + .catch(function() { + btn.disabled = false; + btn.innerHTML = 'Cargar datos de la visita anterior'; + msg.style.display = ''; + msg.textContent = 'Error de conexión.'; + }); + }); +})(); // Fix 1: recuperar countdown si el modal fue cerrado y reabierto mientras una toma estaba en curso (function() {