Compare commits

..
9 Commits
Author SHA1 Message Date
Lizandro GuarnizoandClaude Sonnet 4.6 8bc9cbdd35 feat(turnero): 4 campos antecedentes en un solo renglón (F-LAB-08)
Gesta/Partos/Cesáreas/Abortos ahora caben en una fila usando CSS :has(.campo-cuarto)
que activa grid de 4 columnas solo cuando el campo tiene clase campo-cuarto.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 20:25:55 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 b1264da979 create_turno: priority F skips recepcion, goes direct to toma de muestras
When a patient selects 'Muestra pendiente' (prioridad F) at the kiosk,
the turn is immediately set to en_espera_lugar pointing to the first
active muestras location. Recepcion never sees it; toma de muestras
picks it up from its queue with no consent forms required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 17:53:48 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 17c9e1a1f2 ver_formulario_enviado: auto-fill hora on Minuto 0 (fresh form)
_mpAutoFillHora was gated behind hayFirmaIniciada, so it never ran
on a fresh form where no signatures exist yet. Now always fills the
hora field of the first visible firma widget; scroll only when at
least one signature has already been saved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 17:36:01 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 7a08e63e44 send_consentimiento: read wa template from lab_config instead of hardcoding
Was sending 'consentimiento_turno' (plain text fallback) while the
configured template is 'consentimiento_turno_v2'. Now reads
turnero_wa_template and turnero_wa_lang from lab_config, matching
the same logic used in create_turno.php (kiosk).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 17:18:24 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 26bb9e1844 rips: prevent reuse of already-consumed exam records
- Add turno_id column to rips_examenes_pendientes (already migrated)
- Filter turno_id IS NULL in get_examenes_rips.php query
- New marcar_rips_usado.php marks record as consumed when loaded
- cargarExamenesRips() calls marker after loading exams into the turn

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 16:39:22 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 d46c983776 recepcion: auto-check RIPS when turn has a cedula as patient name
If the kiosk captured a numeric document number as paciente_nombre,
fire consultarExamenesRips immediately on turn open without waiting
for the receptionist to manually search the patient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 16:35:29 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 44e9b86b27 recepcion: revert patient history to collapsed on load
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 16:30:50 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 89b407f5cc recepcion: auto-expand patient history with exams on patient select
Previously the history section required a manual click to expand and load.
Now it opens and fetches automatically when a patient is selected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 16:30:05 -05:00
Lizandro GuarnizoandClaude Sonnet 4.6 7842de0d21 kiosk: remove --disable-print-preview to unblock silent printing
--disable-print-preview conflicted with --kiosk-printing, routing to
the OS print dialog instead of printing silently. Also added AHK
fallback script for edge cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 15:30:07 -05:00
8 changed files with 105 additions and 9 deletions
+1
View File
@@ -20,6 +20,7 @@ $cacheRow = db()->prepare(
WHERE numero_documento = ?
AND DATE(created_at) = CURDATE()
AND created_at >= NOW() - INTERVAL 30 MINUTE
AND turno_id IS NULL
ORDER BY created_at DESC
LIMIT 1"
);
+26
View File
@@ -0,0 +1,26 @@
<?php
/**
* POST /api/lab/marcar_rips_usado.php
* Marca el registro RIPS de una cédula como consumido por un turno.
* Body: { cedula, turno_id }
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
$body = json_decode(file_get_contents('php://input'), true) ?? [];
$cedula = trim($body['cedula'] ?? '');
$turnoId = (int)($body['turno_id'] ?? 0);
if (!$cedula || !$turnoId) jsonError('cedula y turno_id requeridos', 400);
db()->prepare(
"UPDATE rips_examenes_pendientes
SET turno_id = ?
WHERE numero_documento = ?
AND DATE(created_at) = CURDATE()
AND turno_id IS NULL
ORDER BY created_at DESC
LIMIT 1"
)->execute([$turnoId, $cedula]);
jsonOk(['marcado' => true]);
+20
View File
@@ -0,0 +1,20 @@
; kiosko_autoprint.ahk
; Detecta el diálogo de impresión y lo confirma automáticamente.
; Requiere AutoHotkey v2: https://www.autohotkey.com/
#Persistent
SetTitleMatchMode 2
Loop {
; Esperar cualquier ventana de diálogo de impresión (Chrome, Edge, Windows)
if WinExist("Imprimir") or WinExist("Print") {
WinActivate
Sleep 400
; Intentar presionar el botón Imprimir / OK / Enter
ControlClick "Button1"
Sleep 200
Send "{Enter}"
Sleep 1000
}
Sleep 500
}
+1 -1
View File
@@ -2,4 +2,4 @@
taskkill /F /IM msedge.exe >nul 2>&1
timeout /t 2 /nobreak >nul
start "" "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --kiosk "https://erp.laboratorioximenacaicedo.com/erp.php?m=turnero&v=kiosko&autoprint=1" --edge-kiosk-type=fullscreen --kiosk-printing --disable-print-preview
start "" "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe" --kiosk "https://erp.laboratorioximenacaicedo.com/erp.php?m=turnero&v=kiosko&autoprint=1" --edge-kiosk-type=fullscreen --kiosk-printing
+22 -1
View File
@@ -120,11 +120,32 @@ try {
$turnoId = (int) $pdo->lastInsertId();
$pdo->commit();
// ── Prioridad F: saltar recepción, ir directo a toma de muestras ──
$esSoloMuestra = ($prioCodigo === 'F');
if ($esSoloMuestra) {
try {
$stmtLugar = $pdo->prepare(
"SELECT id FROM turnero_lugares WHERE tipo = 'muestras' AND activo = 1 ORDER BY sort_order ASC LIMIT 1"
);
$stmtLugar->execute();
$lugarMuestras = $stmtLugar->fetchColumn();
if ($lugarMuestras) {
$pdo->prepare(
"UPDATE turnero_turnos
SET estado = 'en_espera_lugar',
lugar_destino_id = ?,
fin_recepcion_at = NOW()
WHERE id = ?"
)->execute([(int)$lugarMuestras, $turnoId]);
notificarSSE($sesionId);
}
} catch (\Throwable $_) {}
}
notificarSSE($sesionId);
// ── Crear registro de consentimiento (siempre, con o sin teléfono) ──
$enlaceConsentimiento = null;
$esSoloMuestra = ($prioCodigo === 'F');
if (!$esSoloMuestra) {
try {
$stmtForms = $pdo->prepare(
+11 -2
View File
@@ -84,6 +84,15 @@ function generarUuid(): string
);
}
// ── Leer config de WhatsApp (igual que create_turno.php) ─────
$stmtCfg = $pdo->prepare(
"SELECT clave, valor FROM lab_config WHERE clave IN ('turnero_wa_template','turnero_wa_lang')"
);
$stmtCfg->execute();
$cfgWA = $stmtCfg->fetchAll(PDO::FETCH_KEY_PAIR);
$waTemplateName = $cfgWA['turnero_wa_template'] ?? 'consentimiento_turno';
$waLangFallback = $cfgWA['turnero_wa_lang'] ?? 'es';
$pdo->beginTransaction();
$consentimientosResultado = [];
$erroresEnvio = [];
@@ -105,10 +114,10 @@ try {
$wa = new WhatsAppService('turnero');
$waMeta = ['canal' => 'turnero'];
$waTemplate = 'consentimiento_turno';
$waTemplate = $waTemplateName;
$stmtLang = $pdo->prepare("SELECT language_code FROM message_templates WHERE template_name = ? LIMIT 1");
$stmtLang->execute([$waTemplate]);
$waLang = $stmtLang->fetchColumn() ?: 'es';
$waLang = $stmtLang->fetchColumn() ?: $waLangFallback;
$baseUrl = defined('BASE_URL') ? rtrim(BASE_URL, '/') : (
(isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http')
+13
View File
@@ -1515,6 +1515,9 @@ function abrirFicha(turno) {
if (data && !pacienteActivo) seleccionarPaciente(data);
})
.catch(() => {});
} else if (turno.paciente_nombre && /^\d{5,15}$/.test(turno.paciente_nombre.trim())) {
// El kiosko capturó una cédula como nombre — consultar RIPS de inmediato
consultarExamenesRips(turno.paciente_nombre.trim());
}
// Cargar consentimientos del desk actual para este turno
@@ -1704,6 +1707,16 @@ async function cargarExamenesRips() {
if (_ripsData.valor_total > 0) {
document.getElementById('inp-total').value = Math.round(_ripsData.valor_total);
}
// Marcar registro RIPS como consumido para no reutilizarlo en otro turno
const cedula = pacienteActivo?.numero_documento || pacienteActivo?.documento
|| turnoActivo?.paciente_nombre || '';
if (cedula && turnoActivo?.id) {
fetch(`${BASE_WA}api/lab/marcar_rips_usado.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cedula, turno_id: turnoActivo.id }),
}).catch(() => {});
}
descartarRips();
}
+11 -5
View File
@@ -546,6 +546,9 @@ function _addExamWizardHtml(string $cid): string {
@media (min-width: 480px) {
.campos-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 6px 14px; align-items: start; }
.campos-grid .campo-full { grid-column: span 3; }
/* ponytail: :has() override — 4-col only when campo-cuarto present */
.campos-grid:has(.campo-cuarto) { grid-template-columns: 1fr 1fr 1fr 1fr; }
.campos-grid:has(.campo-cuarto) .campo-full { grid-column: span 4; }
}
/* ── Checkboxes y radios con área táctil mayor ── */
.campo-edit .form-check {
@@ -1201,8 +1204,9 @@ function _addExamWizardHtml(string $cid): string {
};
// texto libre ocupa fila completa; tipos cortos caben 3 por fila
$inputFullClass = ($compact && $tipo === 'texto') ? ' campo-full' : '';
$extraClase = htmlspecialchars($campo['clase'] ?? '', ENT_QUOTES);
?>
<div class="campo-edit<?= $inputFullClass ?>" data-campo-id="<?= esc2($cid) ?>">
<div class="campo-edit<?= $inputFullClass ?><?= $extraClase ? ' '.$extraClase : '' ?>" data-campo-id="<?= esc2($cid) ?>">
<label><?= $label ?></label>
<input type="<?= $inputType ?>" class="form-control form-control-sm"
name="<?= esc2($cid) ?>" value="<?= esc2($prefill) ?>"<?= $req ?>
@@ -1963,13 +1967,15 @@ function _mpRefreshCards(signedFid, nextFid) {
document.addEventListener('DOMContentLoaded', function() {
_mpRenderCards();
setTimeout(function() {
var fw = document.querySelector('.firma-pro-widget:not([style*="display:none"])');
if (!fw) return;
// Siempre auto-llenar la hora (incluso en Minuto 0 sin firmas previas)
_mpAutoFillHora(fw.dataset.campo);
// Solo hacer scroll si ya hay alguna firma iniciada
var hayFirmaIniciada = Object.keys(window._mpSavedFirmas || {}).length > 0
|| !!document.querySelector('.firma-box img');
if (!hayFirmaIniciada) return;
var fw = document.querySelector('.firma-pro-widget:not([style*="display:none"])');
if (fw) {
if (hayFirmaIniciada) {
fw.scrollIntoView({ behavior: 'smooth', block: 'start' });
_mpAutoFillHora(fw.dataset.campo);
}
}, 350);
});