Files
whatsapp/modules/turnero/api/create_turno.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 9a3cd24454 feat: block duplicate active turns for the same patient
Before creating a new turn, check if the patient (identified by
paciente_id) already has a turn in today's session that is not
finalizado/ausente/cancelado. Returns 409 with the existing turn
code so the receptionist or kiosko can show a clear message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 14:10:27 -05:00

272 lines
10 KiB
PHP

<?php
/**
* POST /modules/turnero/api/create_turno.php
* Genera un nuevo turno desde el kiosko o recepción.
* No requiere login (kiosko es público).
*
* Body JSON:
* prioridad_codigo string requerido "A"|"B"|"C"|"D"|"E"|"F"
* paciente_nombre string opcional
* paciente_cel string opcional
*/
require_once __DIR__ . '/_helpers.php';
requireMethod('POST');
// Este endpoint es público (kiosko no requiere login)
// Solo validamos CSRF básico: debe ser POST con JSON
$datos = inputJson();
// ── Validación ────────────────────────────────────────────────
$prioCodigo = strtoupper(trim($datos['prioridad_codigo'] ?? ''));
if (!in_array($prioCodigo, ['A', 'B', 'C', 'D', 'E', 'F'], true)) {
jsonError('prioridad_codigo inválido. Use A, B, C, D, E o F.');
}
$pacienteDoc = substr(trim($datos['paciente_nombre'] ?? ''), 0, 150);
$pacienteCel = preg_replace('/[^0-9+\- ]/', '', $datos['paciente_cel'] ?? '');
$pacienteCel = substr($pacienteCel, 0, 20);
// ── Lógica principal (dentro de transacción para evitar race conditions) ──
$pdo = db();
$pdo->beginTransaction();
try {
$sesionId = obtenerOCrearSesionHoy();
// Lock de sesión para serializar la creación de turnos
$pdo->prepare('SELECT id FROM turnero_sesiones WHERE id = ? FOR UPDATE')
->execute([$sesionId]);
// Buscar prioridad_id
$stmt = $pdo->prepare(
'SELECT id FROM turnero_prioridades WHERE codigo = ? AND activo = 1 LIMIT 1'
);
$stmt->execute([$prioCodigo]);
$prioridad = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$prioridad) {
$pdo->rollBack();
jsonError("La prioridad '$prioCodigo' no existe o no está activa.", 422);
}
$prioridadId = (int) $prioridad['id'];
// Buscar paciente por número de documento en lab_pacientes
$pacienteNombre = $pacienteDoc;
$pacienteId = null;
$pacienteTel = $pacienteCel; // fallback: celular del kiosko
if ($pacienteDoc) {
try {
$stmtP = $pdo->prepare(
'SELECT id, nombre_completo, telefono FROM lab_pacientes WHERE numero_documento = ? AND is_active = 1 LIMIT 1'
);
$stmtP->execute([$pacienteDoc]);
$pacRow = $stmtP->fetch(PDO::FETCH_ASSOC);
if ($pacRow) {
$pacienteNombre = $pacRow['nombre_completo'];
$pacienteId = (int) $pacRow['id'];
$pacienteTel = $pacRow['telefono'] ?: $pacienteCel;
}
} catch (\Throwable $e) {
$pacienteNombre = $pacienteDoc;
}
}
// Validar que el paciente no tenga un turno activo en la sesión de hoy
if ($pacienteId) {
$stmtDup = $pdo->prepare(
"SELECT codigo, estado FROM turnero_turnos
WHERE paciente_id = ? AND sesion_id = ?
AND estado NOT IN ('finalizado','ausente','cancelado')
LIMIT 1"
);
$stmtDup->execute([$pacienteId, $sesionId]);
$dupTurno = $stmtDup->fetch(PDO::FETCH_ASSOC);
if ($dupTurno) {
$pdo->rollBack();
jsonError(
"El paciente ya tiene un turno activo: {$dupTurno['codigo']} (estado: {$dupTurno['estado']}). " .
"Debe finalizar o cancelar el turno actual antes de tomar uno nuevo.",
409
);
}
}
// Correlativo independiente por prioridad (A-001, A-002... B-001, B-002... etc.)
$numero = siguienteNumeroPorPrioridad($sesionId, $prioridadId);
// Código visual: "A001", "B001", "E001", etc.
$codigo = $prioCodigo . str_pad($numero, 3, '0', STR_PAD_LEFT);
$stmt = $pdo->prepare(
'INSERT INTO turnero_turnos
(sesion_id, numero, codigo, prioridad_id, paciente_nombre, paciente_cel, paciente_id,
estado, creado_at)
VALUES
(?, ?, ?, ?, ?, ?, ?, "espera", NOW())'
);
$stmt->execute([
$sesionId,
$numero,
$codigo,
$prioridadId,
$pacienteNombre ?: null,
$pacienteCel ?: null,
$pacienteId,
]);
$turnoId = (int) $pdo->lastInsertId();
$pdo->commit();
notificarSSE($sesionId);
// ── Crear registro de consentimiento (siempre, con o sin teléfono) ──
$enlaceConsentimiento = null;
$esSoloMuestra = ($prioCodigo === 'F');
if (!$esSoloMuestra) {
try {
$stmtForms = $pdo->prepare(
"SELECT tlc.formulario_id, l.formulario_modo
FROM turnero_lugar_consentimientos tlc
JOIN lab_formularios f ON f.id = tlc.formulario_id AND f.is_active = 1
JOIN turnero_lugares l ON l.id = tlc.lugar_id AND l.tipo = 'recepcion' AND l.activo = 1
ORDER BY tlc.formulario_id ASC
LIMIT 1"
);
$stmtForms->execute();
$formRow = $stmtForms->fetch(\PDO::FETCH_ASSOC);
if ($formRow) {
$conToken = sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0,0xffff), mt_rand(0,0xffff),
mt_rand(0,0xffff),
mt_rand(0,0x0fff)|0x4000,
mt_rand(0,0x3fff)|0x8000,
mt_rand(0,0xffff), mt_rand(0,0xffff), mt_rand(0,0xffff)
);
$pdo->prepare(
"INSERT IGNORE INTO turnero_consentimientos
(turno_id, formulario_id, token, estado)
VALUES (?, ?, ?, 'pendiente')"
)->execute([$turnoId, (int)$formRow['formulario_id'], $conToken]);
// Solo generar enlace si el modo es 'link' (embebido no usa WA)
if (($formRow['formulario_modo'] ?? 'link') === 'link') {
$baseUrl = defined('BASE_URL') ? rtrim(BASE_URL, '/') : '';
$enlaceConsentimiento = $baseUrl . '/ver_formulario_enviado.php?token=' . urlencode($conToken);
}
}
} catch (\Throwable $e) {
// Sin consentimiento disponible, continuar sin link
}
}
// ── Enviar notificación WhatsApp ──
if ($pacienteTel) {
try {
$stmtCfg = $pdo->prepare(
"SELECT clave, valor FROM lab_config WHERE clave IN ('turnero_wa_template','turnero_wa_template_muestra','turnero_wa_lang','turnero_wa_kiosko_enabled')"
);
$stmtCfg->execute();
$cfgWA = $stmtCfg->fetchAll(PDO::FETCH_KEY_PAIR);
$waKioskoEnabled = ($cfgWA['turnero_wa_kiosko_enabled'] ?? '0') === '1';
$waTemplate = $esSoloMuestra
? ($cfgWA['turnero_wa_template_muestra'] ?? 'turno_muestra_pendiente')
: ($cfgWA['turnero_wa_template'] ?? 'consentimiento_turno');
// Usar idioma real de la plantilla aprobada (puede diferir del config)
$stmtLang = $pdo->prepare("SELECT language_code FROM message_templates WHERE template_name = ? LIMIT 1");
$stmtLang->execute([$waTemplate]);
$waLang = $stmtLang->fetchColumn() ?: ($cfgWA['turnero_wa_lang'] ?? 'es');
} catch (\Throwable $e) {
$waKioskoEnabled = true;
$waTemplate = 'consentimiento_turno';
$waLang = 'es';
}
}
if ($pacienteTel && $waKioskoEnabled) {
// WhatsAppService::formatPhoneNumber maneja el formato colombiano correctamente
$cel = preg_replace('/[^0-9]/', '', $pacienteTel);
// Marcar como enviado si vamos a mandar WA
if ($enlaceConsentimiento) {
try {
$pdo->prepare(
"UPDATE turnero_consentimientos SET estado='enviado', enviado_at=NOW()
WHERE turno_id=? AND estado='pendiente'"
)->execute([$turnoId]);
} catch (\Throwable $_) {}
}
require_once __DIR__ . '/../../../services/WhatsAppService.php';
$nombreWA = $pacienteNombre ?: 'Paciente';
$waMeta = ['canal' => 'turnero'];
try {
$wa = new WhatsAppService('turnero');
$rawComps = [
['type' => 'body', 'parameters' => [['type' => 'text', 'text' => $codigo]]]
];
if ($conToken) {
$rawComps[] = [
'type' => 'button',
'sub_type' => 'url',
'index' => '0',
'parameters' => [['type' => 'text', 'text' => $conToken]],
];
}
$wa->sendTemplateMessage($cel, $waTemplate, $waLang, [], [], $rawComps, $waMeta);
} catch (\Throwable $eTmpl) {
try {
$msg = "Hola {$nombreWA}, su turno *{$codigo}* ha sido registrado.";
if ($enlaceConsentimiento) {
$msg .= "\n\nPor favor firme su consentimiento informado antes de ser atendido:\n{$enlaceConsentimiento}";
}
$wa->sendTextMessage($cel, $msg, $waMeta);
} catch (\Throwable $eTxt) {
// Silenciar
}
}
}
// Contar posición en la cola
$stmt = $pdo->prepare(
'
SELECT COUNT(*) AS posicion
FROM turnero_turnos t
JOIN turnero_prioridades p ON p.id = t.prioridad_id
WHERE t.estado = "espera"
AND t.sesion_id = ?
AND (
p.orden_peso < (SELECT orden_peso FROM turnero_prioridades WHERE id = ?)
OR (
p.orden_peso = (SELECT orden_peso FROM turnero_prioridades WHERE id = ?)
AND t.creado_at < (SELECT creado_at FROM turnero_turnos WHERE id = ?)
)
)
'
);
$stmt->execute([$sesionId, $prioridadId, $prioridadId, $turnoId]);
$posicion = (int) $stmt->fetchColumn() + 1;
jsonOk([
'turno' => [
'id' => $turnoId,
'codigo' => $codigo,
'numero' => $numero,
'prioridad_codigo' => $prioCodigo,
'posicion_cola' => $posicion,
'sesion_id' => $sesionId,
'paciente_nombre' => $pacienteNombre ?: $pacienteDoc,
],
], "Turno $codigo asignado correctamente");
} catch (\Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
jsonError('Error al crear el turno: ' . $e->getMessage(), 500);
}