[estados_destino_permitidos]] const TRANSICIONES = [ 'espera' => ['en_recepcion', 'ausente', 'cancelado'], 'en_recepcion' => ['en_espera_lugar', 'espera', 'ausente', 'cancelado'], 'en_espera_lugar' => ['en_servicio', 'en_recepcion', 'ausente', 'cancelado'], 'en_servicio' => ['finalizado', 'ausente', 'cancelado', 'en_espera_lugar', 'en_recepcion'], ]; $pdo = db(); $pdo->beginTransaction(); try { // Leer turno actual con lock $stmt = $pdo->prepare( 'SELECT t.*, p.codigo AS prioridad_codigo FROM turnero_turnos t JOIN turnero_prioridades p ON p.id = t.prioridad_id WHERE t.id = ? FOR UPDATE' ); $stmt->execute([$turnoId]); $turno = $stmt->fetch(PDO::FETCH_ASSOC); if (!$turno) { $pdo->rollBack(); jsonError('Turno no encontrado.', 404); } $estadoActual = $turno['estado']; // Estados finales — solo admin (sin role_id) puede reabrir if (in_array($estadoActual, ['finalizado', 'ausente', 'cancelado'], true)) { $roleId = $_SESSION['admin_user']['role_id'] ?? null; if (!empty($roleId)) { $pdo->rollBack(); jsonError("El turno ya está en estado '{$estadoActual}' y no puede modificarse.", 422); } $permitidosAdmin = ['en_servicio', 'en_espera_lugar', 'en_recepcion', 'espera']; if (!in_array($nuevoEstado, $permitidosAdmin, true)) { $pdo->rollBack(); jsonError("Estado destino inválido para reapertura. Opciones: " . implode(', ', $permitidosAdmin), 422); } $sets = ['estado = ?']; $binds = [$nuevoEstado]; if ($nuevoEstado === 'en_servicio') { $sets[] = 'fin_lugar_at = NULL'; $sets[] = 'inicio_lugar_at = COALESCE(inicio_lugar_at, NOW())'; $sets[] = 'atendido_lugar_por = COALESCE(atendido_lugar_por, ?)'; $binds[] = adminId(); } elseif ($nuevoEstado === 'en_espera_lugar') { $sets[] = 'fin_lugar_at = NULL'; $sets[] = 'inicio_lugar_at = NULL'; $sets[] = 'llamado_lugar_at = NULL'; } elseif ($nuevoEstado === 'espera') { $sets[] = 'llamado_recepcion_at = NULL'; $sets[] = 'inicio_recepcion_at = NULL'; $sets[] = 'fin_recepcion_at = NULL'; $sets[] = 'llamado_lugar_at = NULL'; $sets[] = 'inicio_lugar_at = NULL'; $sets[] = 'fin_lugar_at = NULL'; $sets[] = 'atendido_recepcion_por = NULL'; $sets[] = 'atendido_lugar_por = NULL'; $sets[] = 'recepcion_desk_id = NULL'; } $sql = 'UPDATE turnero_turnos SET ' . implode(', ', $sets) . ' WHERE id = ?'; $binds[] = $turnoId; $pdo->prepare($sql)->execute($binds); $pdo->commit(); $stmt = $pdo->prepare( 'SELECT t.*, p.codigo AS prioridad_codigo, p.nombre AS prioridad_nombre, p.color AS prioridad_color FROM turnero_turnos t JOIN turnero_prioridades p ON p.id = t.prioridad_id WHERE t.id = ?' ); $stmt->execute([$turnoId]); $turnoActualizado = $stmt->fetch(PDO::FETCH_ASSOC); notificarSSE((int) $turnoActualizado['sesion_id']); jsonOk(['turno' => $turnoActualizado], "Turno reabierto: estado actualizado a '{$nuevoEstado}'"); } // Verificar que la transición sea válida $permitidos = TRANSICIONES[$estadoActual] ?? []; if (!in_array($nuevoEstado, $permitidos, true)) { $pdo->rollBack(); jsonError( "Transición '{$estadoActual}' → '{$nuevoEstado}' no está permitida. " . "Transiciones válidas: " . implode(', ', $permitidos), 422 ); } // ── Regla especial: en_espera_lugar → en_servicio ───────── if ($nuevoEstado === 'en_servicio') { $cStmt = $pdo->prepare( "SELECT COUNT(*) FROM turnero_consentimientos WHERE turno_id = ? AND estado NOT IN ('firmado', 'rechazado')" ); $cStmt->execute([$turnoId]); $pendientes = (int) $cStmt->fetchColumn(); if ($pendientes > 0) { $pdo->rollBack(); jsonError( "Hay {$pendientes} consentimiento(s) pendiente(s). " . "El turno no puede pasar a 'en_servicio' hasta que todos estén firmados o rechazados.", 422 ); } } // ── Construir UPDATE dinámico ───────────────────────────── $sets = ['estado = ?']; $binds = [$nuevoEstado]; switch ($nuevoEstado) { case 'en_recepcion': $sets[] = 'llamado_recepcion_at = NOW()'; $sets[] = 'inicio_recepcion_at = COALESCE(inicio_recepcion_at, NOW())'; $sets[] = 'atendido_recepcion_por = COALESCE(atendido_recepcion_por, ?)'; $binds[] = adminId(); if ($deskId) { $sets[] = 'recepcion_desk_id = ?'; $binds[] = $deskId; } // Viene de un lugar: limpiar campos del lugar para que quede reasignable if (in_array($estadoActual, ['en_espera_lugar', 'en_servicio'], true)) { $sets[] = 'lugar_destino_id = NULL'; $sets[] = 'llamado_lugar_at = NULL'; $sets[] = 'inicio_lugar_at = NULL'; $sets[] = 'fin_recepcion_at = NULL'; $sets[] = 'atendido_lugar_por = NULL'; } break; case 'en_espera_lugar': // Requiere lugar_id (al volver desde en_servicio se conserva lugar_destino_id actual) if ($lugarId) { $sets[] = 'lugar_destino_id = ?'; $binds[] = $lugarId; } $sets[] = 'llamado_lugar_at = NULL'; $sets[] = 'inicio_lugar_at = NULL'; $sets[] = 'fin_recepcion_at = COALESCE(fin_recepcion_at, NOW())'; $sets[] = 'atendido_recepcion_por = COALESCE(atendido_recepcion_por, ?)'; $binds[] = adminId(); break; case 'en_servicio': $sets[] = 'llamado_lugar_at = COALESCE(llamado_lugar_at, NOW())'; $sets[] = 'inicio_lugar_at = COALESCE(inicio_lugar_at, NOW())'; $sets[] = 'atendido_lugar_por = COALESCE(atendido_lugar_por, ?)'; $binds[] = adminId(); break; case 'finalizado': $sets[] = 'fin_lugar_at = COALESCE(fin_lugar_at, NOW())'; $sets[] = 'atendido_lugar_por = COALESCE(atendido_lugar_por, ?)'; $binds[] = adminId(); break; case 'espera': // Soltar: liberar el turno para que otro escritorio lo llame $sets[] = 'recepcion_desk_id = NULL'; $sets[] = 'inicio_recepcion_at = NULL'; $sets[] = 'atendido_recepcion_por = NULL'; break; case 'ausente': case 'cancelado': // Sin timestamps adicionales; simplemente cambio de estado break; } $sql = 'UPDATE turnero_turnos SET ' . implode(', ', $sets) . ' WHERE id = ?'; $binds[] = $turnoId; $pdo->prepare($sql)->execute($binds); $pdo->commit(); // Refrescar y devolver turno actualizado $stmt = $pdo->prepare( 'SELECT t.*, p.codigo AS prioridad_codigo, p.nombre AS prioridad_nombre, p.color AS prioridad_color FROM turnero_turnos t JOIN turnero_prioridades p ON p.id = t.prioridad_id WHERE t.id = ?' ); $stmt->execute([$turnoId]); $turnoActualizado = $stmt->fetch(PDO::FETCH_ASSOC); notificarSSE((int) $turnoActualizado['sesion_id']); jsonOk(['turno' => $turnoActualizado], "Estado actualizado a '{$nuevoEstado}'"); } catch (\Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } jsonError('Error al cambiar estado: ' . $e->getMessage(), 500); }