false, 'message' => 'Método no permitido']); 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 // ============================================================ function asegurarTablaMovimientos($pdo) { $pdo->exec(" CREATE TABLE IF NOT EXISTS movimientos_caja ( id INT AUTO_INCREMENT PRIMARY KEY, fecha DATETIME NOT NULL, concepto VARCHAR(255) NOT NULL, tipo ENUM('entrada','salida') NOT NULL, importe DECIMAL(10,2) NOT NULL, categoria VARCHAR(50) DEFAULT 'Otros', nota TEXT NULL, reserva_id INT NULL, origen ENUM('manual','reserva') DEFAULT 'manual', fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci "); } function handleGet($pdo) { $periodo = isset($_GET['periodo']) ? $_GET['periodo'] : 'diario'; // Calcular rango de fechas según período $hoy = date('Y-m-d'); if ($periodo === 'diario') { $fechaInicio = $hoy . ' 00:00:00'; $fechaFin = $hoy . ' 23:59:59'; } elseif ($periodo === 'semanal') { $fechaInicio = date('Y-m-d', strtotime('monday this week')) . ' 00:00:00'; $fechaFin = date('Y-m-d', strtotime('sunday this week')) . ' 23:59:59'; } else { // mensual $fechaInicio = date('Y-m-01') . ' 00:00:00'; $fechaFin = date('Y-m-t') . ' 23:59:59'; } // Sincronizar reservas pagadas → movimientos_caja (solo del rango actual) sincronizarReservas($pdo, $fechaInicio, $fechaFin); // Obtener movimientos del rango $sql = "SELECT id, fecha, concepto, tipo, importe, categoria, nota, reserva_id, origen FROM movimientos_caja WHERE fecha BETWEEN :inicio AND :fin"; $params = [':inicio' => $fechaInicio, ':fin' => $fechaFin]; // Filtros opcionales if (isset($_GET['tipo']) && in_array($_GET['tipo'], ['entrada','salida'])) { $sql .= " AND tipo = :tipo"; $params[':tipo'] = $_GET['tipo']; } if (isset($_GET['categoria']) && $_GET['categoria'] !== '') { $sql .= " AND categoria = :categoria"; $params[':categoria'] = $_GET['categoria']; } $sql .= " ORDER BY fecha DESC, id DESC"; $stmt = $pdo->prepare($sql); $stmt->execute($params); $movimientos = $stmt->fetchAll(); // Calcular estadísticas del período $stmtStats = $pdo->prepare(" SELECT COALESCE(SUM(CASE WHEN tipo='entrada' THEN importe ELSE 0 END), 0) AS total_entradas, COALESCE(SUM(CASE WHEN tipo='salida' THEN importe ELSE 0 END), 0) AS total_salidas FROM movimientos_caja WHERE fecha BETWEEN :inicio AND :fin "); $stmtStats->execute([':inicio' => $fechaInicio, ':fin' => $fechaFin]); $stats = $stmtStats->fetch(); echo json_encode([ 'success' => true, 'message' => 'Movimientos obtenidos', 'data' => $movimientos, 'stats' => [ 'total_entradas' => floatval($stats['total_entradas']), 'total_salidas' => floatval($stats['total_salidas']), 'balance' => floatval($stats['total_entradas']) - floatval($stats['total_salidas']), 'periodo' => $periodo, 'fecha_inicio' => $fechaInicio, 'fecha_fin' => $fechaFin ], 'total' => count($movimientos) ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } function handlePost($pdo) { $input = json_decode(file_get_contents('php://input'), true); if (!$input) $input = $_POST; $campos_obligatorios = ['fecha', 'concepto', 'tipo', 'importe']; foreach ($campos_obligatorios as $campo) { if (!isset($input[$campo]) || $input[$campo] === '') { http_response_code(400); echo json_encode(['success' => false, 'message' => "Falta el campo obligatorio: $campo"]); return; } } if (!in_array($input['tipo'], ['entrada','salida'])) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Tipo no válido (debe ser entrada o salida)']); return; } $importe = floatval($input['importe']); if ($importe <= 0) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'El importe debe ser mayor que 0']); return; } $categoria = isset($input['categoria']) ? $input['categoria'] : 'Otros'; $nota = isset($input['nota']) ? $input['nota'] : null; $stmt = $pdo->prepare(" INSERT INTO movimientos_caja (fecha, concepto, tipo, importe, categoria, nota, origen) VALUES (:fecha, :concepto, :tipo, :importe, :categoria, :nota, 'manual') "); $stmt->execute([ ':fecha' => $input['fecha'], ':concepto' => trim($input['concepto']), ':tipo' => $input['tipo'], ':importe' => $importe, ':categoria' => $categoria, ':nota' => $nota ]); $nuevo_id = $pdo->lastInsertId(); $stmtGet = $pdo->prepare("SELECT * FROM movimientos_caja WHERE id = ?"); $stmtGet->execute([$nuevo_id]); echo json_encode([ 'success' => true, 'message' => 'Movimiento creado correctamente', 'data' => $stmtGet->fetch() ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } 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 (usar ?id=N)']); return; } // No permitir eliminar movimientos automáticos de reservas (por integridad) $stmtCheck = $pdo->prepare("SELECT id, concepto, origen FROM movimientos_caja WHERE id = ?"); $stmtCheck->execute([$id]); $mov = $stmtCheck->fetch(); if (!$mov) { http_response_code(404); echo json_encode(['success' => false, 'message' => 'Movimiento no encontrado']); return; } if ($mov['origen'] === 'reserva') { http_response_code(409); echo json_encode(['success' => false, 'message' => 'No se puede eliminar un movimiento automático de reserva']); return; } $stmt = $pdo->prepare("DELETE FROM movimientos_caja WHERE id = ?"); $stmt->execute([$id]); echo json_encode([ 'success' => true, 'message' => 'Movimiento eliminado correctamente', 'data' => ['id' => $id, 'concepto' => $mov['concepto']] ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } /** * Sincroniza las reservas pagadas en el rango de fechas con la tabla movimientos_caja. * Solo inserta si no existe ya un movimiento para esa reserva. */ function sincronizarReservas($pdo, $fechaInicio, $fechaFin) { // Solo fecha (sin hora) para comparar con r.fecha $fechaInicioDia = substr($fechaInicio, 0, 10); $fechaFinDia = substr($fechaFin, 0, 10); $sql = "SELECT r.id, r.codigo, r.fecha, r.importe, r.estado, u.nombre AS cliente_nombre, u.apellidos AS cliente_apellidos, c.codigo AS cabina_codigo FROM reservas r JOIN usuarios u ON r.usuario_id = u.id JOIN cabinas c ON r.cabina_id = c.id WHERE r.fecha BETWEEN :inicio AND :fin AND r.estado IN ('confirmada','activa','finalizada')"; $stmt = $pdo->prepare($sql); $stmt->execute([':inicio' => $fechaInicioDia, ':fin' => $fechaFinDia]); $reservas = $stmt->fetchAll(); $stmtCheck = $pdo->prepare("SELECT id FROM movimientos_caja WHERE reserva_id = ? AND origen = 'reserva'"); $stmtInsert = $pdo->prepare(" INSERT INTO movimientos_caja (fecha, concepto, tipo, importe, categoria, reserva_id, origen) VALUES (:fecha, :concepto, 'entrada', :importe, 'Reservas', :reserva_id, 'reserva') "); foreach ($reservas as $r) { $stmtCheck->execute([$r['id']]); if ($stmtCheck->fetch()) continue; // Ya existe, no duplicar $concepto = 'Reserva ' . ($r['codigo'] ?: '#' . $r['id']) . ' - ' . trim(($r['cliente_nombre'] ?: '') . ' ' . ($r['cliente_apellidos'] ?: '')); $fechaHora = $r['fecha'] . ' ' . ($r['hora_inicio'] ?? '00:00:00'); $stmtInsert->execute([ ':fecha' => $fechaHora, ':concepto' => $concepto, ':importe' => floatval($r['importe']), ':reserva_id' => $r['id'] ]); } } ?>