Files
whatsapp/modules/turnero/api/_helpers.php
T
2026-06-16 13:04:59 -05:00

282 lines
8.3 KiB
PHP

<?php
/**
* Helpers compartidos para los endpoints del módulo Turnero.
* Incluido por cada endpoint de modules/turnero/api/.
*/
ob_start();
require_once __DIR__ . '/../../../config/config.php';
error_reporting(E_ERROR | E_PARSE);
ini_set('display_errors', '0');
ini_set('html_errors', '0');
register_shutdown_function(function () {
$err = error_get_last();
if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
ob_clean();
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'error' => 'Error interno: ' . $err['message']]);
}
});
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
// ── Funciones de respuesta ───────────────────────────────────
function jsonOk(array $payload = [], string $mensaje = ''): void
{
ob_clean();
$resp = ['ok' => true, 'success' => true];
if ($mensaje) {
$resp['message'] = $mensaje;
}
echo json_encode(array_merge($resp, $payload), JSON_UNESCAPED_UNICODE);
exit;
}
function jsonError(string $mensaje, int $code = 400): void
{
ob_clean();
http_response_code($code);
echo json_encode(['ok' => false, 'success' => false, 'error' => $mensaje], JSON_UNESCAPED_UNICODE);
exit;
}
function requireMethod(string $method): void
{
if ($_SERVER['REQUEST_METHOD'] !== strtoupper($method)) {
jsonError('Método no permitido', 405);
}
}
function inputJson(): array
{
static $parsed = null;
if ($parsed === null) {
$raw = file_get_contents('php://input');
$parsed = json_decode($raw, true) ?? [];
}
return $parsed;
}
// ── Autenticación (kiosko y display son públicos) ────────────
/**
* Exige sesión activa. Para endpoints de login requerido.
*/
function requireAuth(): void
{
if (empty($_SESSION['admin_user']['id'])) {
jsonError('No autenticado', 401);
}
}
/**
* Exige sesión + módulo turnero.
*/
function requireTurnero(): void
{
requireAuth();
$modules = $_SESSION['admin_user']['modules'] ?? [];
$roleId = $_SESSION['admin_user']['role_id'] ?? null;
// Superadmin / admin sin role_id = acceso total
if (empty($roleId)) {
return;
}
if (!in_array('turnero', (array) $modules, true)) {
jsonError('Sin acceso al módulo Turnero', 403);
}
}
function adminId(): ?int
{
return isset($_SESSION['admin_user']['id'])
? (int) $_SESSION['admin_user']['id']
: null;
}
// ── Helper BD ────────────────────────────────────────────────
function db(): PDO
{
return Database::getInstance()->getConnection();
}
// ── Motor de cola ────────────────────────────────────────────
/**
* Obtiene o crea la sesión del día actual.
* Devuelve el id de la sesión.
*/
function obtenerOCrearSesionHoy(): int
{
$pdo = db();
$hoy = date('Y-m-d');
$stmt = $pdo->prepare('SELECT id FROM turnero_sesiones WHERE fecha = ? LIMIT 1');
$stmt->execute([$hoy]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
return (int) $row['id'];
}
// Crear sesión del día
$stmt = $pdo->prepare(
'INSERT INTO turnero_sesiones (fecha, abierto_por, inicio_at) VALUES (?, ?, NOW())'
);
$stmt->execute([$hoy, adminId()]);
return (int) $pdo->lastInsertId();
}
/**
* Devuelve ['id', 'fin_at'] de la sesión de hoy si existe, o null.
* Para uso en pantallas: NO crea sesión nueva.
*/
function sesionHoy(): ?array
{
$pdo = db();
$stmt = $pdo->prepare('SELECT id, fin_at FROM turnero_sesiones WHERE fecha = CURDATE() LIMIT 1');
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
/**
* Genera el siguiente número correlativo de turno para una sesión.
* Usa bloqueo a nivel de fila para evitar duplicados en concurrencia.
*/
function siguienteNumero(int $sesionId): int
{
$pdo = db();
$stmt = $pdo->prepare(
'SELECT COALESCE(MAX(numero), 0) + 1 AS siguiente
FROM turnero_turnos
WHERE sesion_id = ?
FOR UPDATE'
);
$stmt->execute([$sesionId]);
return (int) $stmt->fetchColumn();
}
/**
* Genera el siguiente número correlativo de turno POR PRIORIDAD para una sesión.
* Cada prioridad (A, B, C, D, E, F) tiene su propia secuencia independiente.
* Usa bloqueo a nivel de fila para evitar duplicados en concurrencia.
*/
function siguienteNumeroPorPrioridad(int $sesionId, int $prioridadId): int
{
$pdo = db();
$stmt = $pdo->prepare(
'SELECT COALESCE(MAX(numero), 0) + 1 AS siguiente
FROM turnero_turnos
WHERE sesion_id = ?
AND prioridad_id = ?
FOR UPDATE'
);
$stmt->execute([$sesionId, $prioridadId]);
return (int) $stmt->fetchColumn();
}
/**
* Selecciona el siguiente turno a llamar según el motor de prioridades.
*
* @param string $estado Estado que deben tener los turnos en cola
* @param int|null $lugarId Si no es null, filtra por lugar_destino_id (o su grupo)
* @param array|null $lugarIds Lista explícita de lugar_ids (grupo completo)
* @return array|null Fila de turnero_turnos o null si cola vacía
*/
function siguienteTurnoEnCola(string $estado, ?int $lugarId = null, ?array $lugarIds = null): ?array
{
$pdo = db();
// Resolver grupo si se pasó un lugarId sin lista explícita
if ($lugarId !== null && $lugarIds === null) {
$lugarIds = lugarIdsDeGrupo($lugarId);
}
if (!empty($lugarIds)) {
$in = implode(',', array_map('intval', $lugarIds));
$sql = "
SELECT t.*, p.orden_peso, p.codigo AS prioridad_codigo, p.color AS prioridad_color
FROM turnero_turnos t
JOIN turnero_prioridades p ON p.id = t.prioridad_id
WHERE t.estado = ?
AND t.lugar_destino_id IN ($in)
ORDER BY p.orden_peso ASC, t.creado_at ASC
LIMIT 1
FOR UPDATE
";
$stmt = $pdo->prepare($sql);
$stmt->execute([$estado]);
} else {
$sql = '
SELECT t.*, p.orden_peso, p.codigo AS prioridad_codigo, p.color AS prioridad_color
FROM turnero_turnos t
JOIN turnero_prioridades p ON p.id = t.prioridad_id
WHERE t.estado = ?
ORDER BY p.orden_peso ASC, t.creado_at ASC
LIMIT 1
FOR UPDATE
';
$stmt = $pdo->prepare($sql);
$stmt->execute([$estado]);
}
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
/**
* Devuelve todos los lugar_ids que comparten grupo con $lugarId.
* Si no tiene grupo, devuelve [$lugarId].
*/
function lugarIdsDeGrupo(int $lugarId): array
{
$pdo = db();
$stmt = $pdo->prepare("SELECT grupo_id FROM turnero_lugares WHERE id = ?");
$stmt->execute([$lugarId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row || $row['grupo_id'] === null) {
return [$lugarId];
}
$grupoId = (int)$row['grupo_id'];
$stmt2 = $pdo->prepare("SELECT id FROM turnero_lugares WHERE grupo_id = ? AND activo = 1");
$stmt2->execute([$grupoId]);
$ids = $stmt2->fetchAll(PDO::FETCH_COLUMN);
return $ids ?: [$lugarId];
}
/**
* Emite un evento SSE a todos los clientes suscritos escribiendo
* un flag en la tabla turnero_sesiones (campo sse_ping_at).
* Los clientes hacen polling de ese campo o escuchan el SSE directamente.
*
* Implementación ligera: actualiza timestamp en sesión para que
* sse_turno.php lo detecte y reenvíe el estado.
*/
function notificarSSE(int $sesionId): void
{
try {
db()->prepare(
'UPDATE turnero_sesiones SET sse_ping_at = NOW() WHERE id = ?'
)->execute([$sesionId]);
} catch (\Throwable $e) {
// No es crítico si falla; el SSE continuará con polling normal
}
}