false, 'message' => 'Método no permitido. Soportados: GET, POST, PUT, DELETE' ]); break; } } catch (PDOException $e) { http_response_code(500); echo json_encode([ 'success' => false, 'message' => 'Error en el servidor', 'error' => $e->getMessage() ]); } catch (Exception $e) { http_response_code($e->getCode() ?: 500); echo json_encode([ 'success' => false, 'message' => $e->getMessage() ]); } // ============================================================ // FUNCIONES // ============================================================ /** * GET /cabinas.php * Filtros opcionales: tamanio, estado, bloque, esp32_id * O bien (disponibilidad por horario): fecha, hora_inicio, hora_fin, tamanio */ function handleGet($pdo) { // --- Caso 1: Buscar cabinas disponibles para un horario concreto --- if (isset($_GET['fecha']) && isset($_GET['hora_inicio']) && isset($_GET['hora_fin']) && isset($_GET['tamanio'])) { $fecha = $_GET['fecha']; $hora_inicio = $_GET['hora_inicio']; $hora_fin = $_GET['hora_fin']; $tamanio = $_GET['tamanio']; $stmt = $pdo->prepare(" SELECT c.id, c.codigo, c.fila, c.nivel, c.tamanio, c.ubicacion, c.bloque, c.esp32_id, c.temperatura_actual, c.humedad_actual, c.calidad_aire_actual, c.nivel_agua, c.ultima_telemetria, c.historial_cabinas, c.motivo_ultima_apertura FROM cabinas c WHERE c.estado = 'disponible' AND c.tamanio = :tamanio AND c.id NOT IN ( SELECT r.cabina_id FROM reservas r WHERE r.fecha = :fecha AND r.estado IN ('confirmada', 'activa', 'pagada_pendiente_aprobacion') AND (r.hora_inicio < :hora_fin AND r.hora_fin_prevista > :hora_inicio) ) ORDER BY c.bloque ASC, c.fila ASC, c.codigo ASC "); $stmt->execute([ ':tamanio' => $tamanio, ':fecha' => $fecha, ':hora_inicio' => $hora_inicio, ':hora_fin' => $hora_fin ]); $cabinas_disponibles = $stmt->fetchAll(); echo json_encode([ 'success' => true, 'message' => 'Cabinas disponibles para el horario solicitado', 'data' => $cabinas_disponibles, 'total' => count($cabinas_disponibles) ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); return; } // --- Caso 2: Listado general con filtros opcionales --- $sql = "SELECT id, codigo, fila, nivel, tamanio, estado, ubicacion, bloque, esp32_id, temperatura_actual, humedad_actual, calidad_aire_actual, nivel_agua, ultima_telemetria, historial_cabinas, motivo_ultima_apertura FROM cabinas WHERE 1=1"; $params = []; if (isset($_GET['tamanio'])) { $sql .= " AND tamanio = :tamanio"; $params[':tamanio'] = $_GET['tamanio']; } if (isset($_GET['estado'])) { $sql .= " AND estado = :estado"; $params[':estado'] = $_GET['estado']; } if (isset($_GET['bloque'])) { $sql .= " AND bloque = :bloque"; $params[':bloque'] = (int)$_GET['bloque']; } if (isset($_GET['esp32_id'])) { $sql .= " AND esp32_id = :esp32_id"; $params[':esp32_id'] = $_GET['esp32_id']; } // Filtro opcional para IoT: solo cabinas con telemetría reciente if (isset($_GET['incluir_telemetria']) && $_GET['incluir_telemetria'] == '1') { $sql .= " AND ultima_telemetria IS NOT NULL"; } $sql .= " ORDER BY bloque ASC, fila ASC, codigo ASC"; $stmt = $pdo->prepare($sql); $stmt->execute($params); $cabinas = $stmt->fetchAll(); echo json_encode([ 'success' => true, 'message' => 'Listado de cabinas', 'data' => $cabinas, 'total' => count($cabinas) ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } /** * POST /cabinas.php * Crea una nueva cabina. * Body: { codigo, fila, nivel, tamanio, estado, ubicacion, bloque, esp32_id } */ function handlePost($pdo) { $input = json_decode(file_get_contents('php://input'), true); if (!$input) { $input = $_POST; } // Validaciones obligatorias $campos_obligatorios = ['codigo', 'fila', 'nivel', 'estado']; foreach ($campos_obligatorios as $campo) { if (!isset($input[$campo]) || trim($input[$campo]) === '') { http_response_code(400); echo json_encode(['success' => false, 'message' => "Falta el campo obligatorio: $campo"]); return; } } $estados_permitidos = ['disponible', 'ocupada', 'reservada', 'mantenimiento']; if (!in_array($input['estado'], $estados_permitidos)) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Estado no válido']); return; } // Verificar que el código no exista ya $stmtCheck = $pdo->prepare("SELECT id FROM cabinas WHERE codigo = ?"); $stmtCheck->execute([$input['codigo']]); if ($stmtCheck->fetch()) { http_response_code(409); echo json_encode(['success' => false, 'message' => 'Ya existe una cabina con ese código']); return; } $timestamp = date('d/m/Y H:i'); $historial_inicial = "[{$timestamp}] - Cabina creada con estado \"{$input['estado']}\""; $stmt = $pdo->prepare(" INSERT INTO cabinas (codigo, fila, nivel, tamanio, estado, ubicacion, bloque, esp32_id, historial_cabinas) VALUES (:codigo, :fila, :nivel, :tamanio, :estado, :ubicacion, :bloque, :esp32_id, :historial) "); $stmt->execute([ ':codigo' => trim($input['codigo']), ':fila' => $input['fila'], ':nivel' => $input['nivel'], ':tamanio' => $input['tamanio'] ?? 'mediana', ':estado' => $input['estado'], ':ubicacion' => $input['ubicacion'] ?? null, ':bloque' => isset($input['bloque']) ? (int)$input['bloque'] : 1, ':esp32_id' => $input['esp32_id'] ?? null, ':historial' => $historial_inicial ]); $nuevo_id = $pdo->lastInsertId(); // Devolver la cabina recién creada $stmtGet = $pdo->prepare("SELECT * FROM cabinas WHERE id = ?"); $stmtGet->execute([$nuevo_id]); $cabina = $stmtGet->fetch(); echo json_encode([ 'success' => true, 'message' => 'Cabina creada correctamente', 'data' => $cabina ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } /** * PUT /cabinas.php * Actualiza una cabina existente. * Body: { id, estado?, observacion?|comentarios?, motivo_ultima_apertura?, operador? } */ function handlePut($pdo) { $input = json_decode(file_get_contents('php://input'), true); if (!isset($input['id'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Falta el campo obligatorio: id']); return; } $id = (int)$input['id']; if ($id <= 0) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID de cabina no válido']); return; } $estado = isset($input['estado']) ? $input['estado'] : null; // Aceptamos tanto "observacion" como "comentarios" (alias para compatibilidad con el frontend) $nueva_observacion = isset($input['observacion']) ? $input['observacion'] : (isset($input['comentarios']) ? $input['comentarios'] : null); $motivo_apertura = isset($input['motivo_ultima_apertura']) ? trim($input['motivo_ultima_apertura']) : null; $operador = isset($input['operador']) ? trim($input['operador']) : 'Sistema'; // Validar longitud del motivo de apertura if ($motivo_apertura !== null && mb_strlen($motivo_apertura) > 500) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'El motivo de apertura no puede superar 500 caracteres']); return; } // Validar estado if ($estado !== null) { $estados_permitidos = ['disponible', 'ocupada', 'reservada', 'mantenimiento']; if (!in_array($estado, $estados_permitidos)) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Estado no válido']); return; } } // --- Verificar que la cabina existe y obtener historial actual --- $stmtHistorial = $pdo->prepare("SELECT id, historial_cabinas FROM cabinas WHERE id = ?"); $stmtHistorial->execute([$id]); $row = $stmtHistorial->fetch(); if (!$row) { http_response_code(404); echo json_encode(['success' => false, 'message' => 'Cabina no encontrada']); return; } $historialActual = $row['historial_cabinas']; // --- Construir nueva entrada de historial --- $timestamp = date('d/m/Y H:i'); $nuevaEntrada = ''; if ($estado !== null) { $nuevaEntrada .= "[{$timestamp}] - Cambio de estado a \"{$estado}\""; } if ($nueva_observacion !== null && trim($nueva_observacion) !== '') { if ($nuevaEntrada) $nuevaEntrada .= " | "; else $nuevaEntrada = "[{$timestamp}] - "; $nuevaEntrada .= "Obs: " . trim($nueva_observacion); } if ($motivo_apertura !== null && $motivo_apertura !== '') { if ($nuevaEntrada) $nuevaEntrada .= " | "; else $nuevaEntrada = "[{$timestamp}] - "; $nuevaEntrada .= "Apertura manual por {$operador}: {$motivo_apertura}"; } // Añadir al historial existente (limitar a últimas 5000 caracteres para evitar crecimiento infinito) if ($nuevaEntrada) { $historialCompleto = $historialActual ? $historialActual . "\n" . $nuevaEntrada : $nuevaEntrada; if (mb_strlen($historialCompleto) > 5000) { // Conservar solo las entradas más recientes $lineas = explode("\n", $historialCompleto); $historialCompleto = implode("\n", array_slice($lineas, -50)); } } else { $historialCompleto = $historialActual; } // --- Construir UPDATE dinámico --- $campos = []; $params = [':id' => $id]; if ($estado !== null) { $campos[] = "estado = :estado"; $params[':estado'] = $estado; } if ($nueva_observacion !== null && trim($nueva_observacion) !== '') { // Si quieres persistir comentarios en una columna aparte, descomenta: // $campos[] = "comentarios = :comentarios"; // $params[':comentarios'] = trim($nueva_observacion); } if ($historialCompleto !== $historialActual) { $campos[] = "historial_cabinas = :historial"; $params[':historial'] = $historialCompleto; } if ($motivo_apertura !== null && $motivo_apertura !== '') { $campos[] = "motivo_ultima_apertura = :motivo"; $params[':motivo'] = $motivo_apertura; } if (empty($campos)) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'No se ha proporcionado ningún campo a actualizar']); return; } $sql = "UPDATE cabinas SET " . implode(', ', $campos) . " WHERE id = :id"; $stmt = $pdo->prepare($sql); $stmt->execute($params); // --- IMPORTANTE: no usar rowCount() --- // En MySQL, rowCount() en UPDATE devuelve filas "afectadas" (cuyo valor cambió), // no filas "coincididas". Si el nuevo estado es igual al actual, devuelve 0 // aunque la cabina exista. Por eso verificamos existencia con SELECT previo. // Devolver la cabina actualizada $stmtGet = $pdo->prepare("SELECT * FROM cabinas WHERE id = ?"); $stmtGet->execute([$id]); $cabinaActualizada = $stmtGet->fetch(); echo json_encode([ 'success' => true, 'message' => 'Cabina actualizada correctamente', 'data' => $cabinaActualizada ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } /** * DELETE /cabinas.php?id=123 * Elimina una cabina. Verifica que no tenga reservas activas asociadas. */ function handleDelete($pdo) { $id = isset($_GET['id']) ? (int)$_GET['id'] : 0; if ($id <= 0) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'ID no válido']); return; } // Verificar que exista $stmtCheck = $pdo->prepare("SELECT id, codigo FROM cabinas WHERE id = ?"); $stmtCheck->execute([$id]); $cabina = $stmtCheck->fetch(); if (!$cabina) { http_response_code(404); echo json_encode(['success' => false, 'message' => 'Cabina no encontrada']); return; } // Verificar que no tenga reservas activas $stmtReservas = $pdo->prepare(" SELECT COUNT(*) FROM reservas WHERE cabina_id = ? AND estado IN ('confirmada', 'activa', 'pagada_pendiente_aprobacion') "); $stmtReservas->execute([$id]); $reservasActivas = (int)$stmtReservas->fetchColumn(); if ($reservasActivas > 0) { http_response_code(409); echo json_encode([ 'success' => false, 'message' => "No se puede eliminar: hay {$reservasActivas} reserva(s) activa(s) en esta cabina" ]); return; } $stmt = $pdo->prepare("DELETE FROM cabinas WHERE id = ?"); $stmt->execute([$id]); echo json_encode([ 'success' => true, 'message' => "Cabina {$cabina['codigo']} eliminada correctamente" ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } ?>