mesaRepository = $mesaRepository; $this->realtimeService = $realtimeService; } public function index(Request $request): JsonResponse { try { $sucursalId = $request->query('sucursal_id'); if (!$sucursalId) { return response()->json(['success' => false, 'message' => 'Falta sucursal_id'], 400); } $mesas = $this->mesaRepository->obtenerPorSucursal($sucursalId); return response()->json([ 'success' => true, 'data' => array_map(fn($m) => $m->toArray(), $mesas) ]); } catch (\Exception $e) { Log::error("❌ ResourceController Error: " . $e->getMessage(), ['trace' => $e->getTraceAsString(), 'url' => $request->fullUrl(), 'method' => $request->method(), 'body' => $request->all()]); return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } public function store(Request $request): JsonResponse { try { $validated = $request->validate([ 'sucursal_id' => 'required|string', 'nombre' => 'required|string', 'capacidad' => 'required|integer|min:1', 'ubicacion' => 'nullable|string', 'descripcion' => 'nullable|string', 'es_vip' => 'boolean', 'permite_union' => 'boolean', 'grupo_union_id' => 'nullable|string' ]); // Security Check: Table Limit $sucursal = \Illuminate\Support\Facades\DB::table('Nucleo_Sucursales') ->where('id_sucursal', $validated['sucursal_id']) ->first(); if ($sucursal && $sucursal->plan_id) { $plan = \Illuminate\Support\Facades\DB::table('SaaS_Gestion_Planes') ->where('plan_id', $sucursal->plan_id) ->first(); if ($plan && $plan->limite_mesas != -1) { $mesasActuales = \Illuminate\Support\Facades\DB::table('Nucleo_Recursos') ->where('sucursal_id', $validated['sucursal_id']) ->where('activo', 1) ->where('tipo', 'MESA') ->count(); if ($mesasActuales >= $plan->limite_mesas) { return response()->json([ 'success' => false, 'message' => "Límite de mesas alcanzado para tu plan ({$plan->limite_mesas}). Por favor, mejora tu plan." ], 403); } } } $id = (string) Str::uuid(); // Generación de QR con URL completa para soporte Web y App simultáneo $appUrl = rtrim(env('VISTAS_URL', 'http://piterpro.com'), '/'); $qrCode = $appUrl . "/m/" . $id; $mesa = new Mesa( id: $id, sucursalId: $validated['sucursal_id'], nombre: $validated['nombre'], tipo: 'MESA', estado: EstadoMesa::libre(), meseroAsignadoId: null, capacidad: $validated['capacidad'], ubicacion: $validated['ubicacion'] ?? 'General', qrCode: $qrCode, activo: true, descripcion: $validated['descripcion'] ?? null, esVip: $validated['es_vip'] ?? false, permiteUnion: $validated['permite_union'] ?? true, grupoUnionId: $validated['grupo_union_id'] ?? null ); $this->mesaRepository->guardar($mesa); // Emitir evento Realtime try { $this->realtimeService->mesaCreada($mesa->toArray()); } catch (\Exception $e) { // Log error without failing request \Illuminate\Support\Facades\Log::error("Error emitiendo mesa.creada: " . $e->getMessage()); } return response()->json(['success' => true, 'message' => 'Mesa creada', 'data' => $mesa->toArray()]); } catch (\Exception $e) { Log::error("❌ ResourceController Error: " . $e->getMessage(), ['trace' => $e->getTraceAsString(), 'url' => $request->fullUrl(), 'method' => $request->method(), 'body' => $request->all()]); return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } public function update(Request $request, string $id): JsonResponse { try { $validated = $request->validate([ 'nombre' => 'sometimes|string', 'capacidad' => 'sometimes|integer|min:1', 'estado' => 'sometimes|string', 'ubicacion' => 'nullable|string', 'sucursal_id' => 'sometimes|string', 'descripcion' => 'nullable|string', 'es_vip' => 'boolean', 'permite_union' => 'boolean', 'grupo_union_id' => 'nullable|string', 'iot_action' => 'nullable|string', 'iot_payload' => 'nullable|string' ]); $mesa = $this->mesaRepository->obtenerPorId($id); if (!$mesa) { return response()->json(['success' => false, 'message' => 'Mesa no encontrada'], 404); } // Validar que pertenezca a la sucursal (security check) if (isset($validated['sucursal_id']) && $mesa->sucursalId !== $validated['sucursal_id']) { return response()->json(['success' => false, 'message' => 'Acceso denegado a este recurso'], 403); } if (isset($validated['estado'])) { $mesa->estado = \App\Modules\Operations\Domain\ValueObjects\EstadoMesa::fromString($validated['estado']); if ($validated['estado'] === 'LIBRE') { $mesa->meseroAsignadoId = null; $mesa->meseroNombre = null; $mesa->pagoAdelantado = false; $mesa->pagoAdelantadoExpira = null; // 🧹 LIMPIEZA DE PEDIDOS ACTIVOS (Forzar Salida) // Cancelamos pedidos en preparación para limpiar Cocina/Bar y completamos los entregados/pagados try { app(\App\Modules\Orders\Domain\Repositories\PedidoRepositoryInterface::class)->limpiarPedidosAlLiberarMesa($id); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error("❌ Error llamando a limpiarPedidosAlLiberarMesa para mesa $id: " . $e->getMessage()); } } } $oldNombre = $mesa->nombre; $newNombre = $validated['nombre'] ?? $mesa->nombre; $mesa->nombre = $newNombre; $mesa->capacidad = $validated['capacidad'] ?? $mesa->capacidad; $mesa->ubicacion = $validated['ubicacion'] ?? $mesa->ubicacion; $mesa->descripcion = $validated['descripcion'] ?? $mesa->descripcion; $mesa->esVip = $validated['es_vip'] ?? $mesa->esVip; $mesa->permiteUnion = $validated['permite_union'] ?? $mesa->permiteUnion; $mesa->grupoUnionId = $validated['grupo_union_id'] ?? $mesa->grupoUnionId; if ($request->has('iot_action')) { $mesa->iotAction = $request->input('iot_action'); } if ($request->has('iot_payload')) { $mesa->iotPayload = $request->input('iot_payload'); } $this->mesaRepository->guardar($mesa); // 📡 SINCRONIZACIÓN IOT (Si el nombre cambió y hay un dispositivo vinculado) if ($oldNombre !== $newNombre && !empty($mesa->macAddress)) { try { $ruc = \Illuminate\Support\Facades\DB::table('Nucleo_Empresas as e') ->join('Nucleo_Sucursales as s', 'e.id_empresa', '=', 's.empresa_id') ->where('s.id_sucursal', $mesa->sucursalId) ->value('e.ruc_nit'); if ($ruc) { $iotController = app(\App\Modules\IoT\Empleado\Infrastructure\Controllers\RespuestaController::class); $syncRequest = new \Illuminate\Http\Request([ 'accion' => 'set_config', 'mesa' => $newNombre, 'id_sucursal' => $mesa->sucursalId, 'ruc' => $ruc ]); // Se envía el comando al tópico antiguo para que el equipo lo reciba $iotController->ejecutar($syncRequest, $ruc, $mesa->sucursalId, $oldNombre); \Illuminate\Support\Facades\Log::info("📡 IOT SYNC: Mesa '$oldNombre' renombrada a '$newNombre'. Comando sync enviado."); } } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error("❌ ERROR IOT SYNC: " . $e->getMessage()); } } // Emitir evento Realtime (Estado actualizado por si cambian nombre/capacidad visualmente) try { $this->realtimeService->mesaEstadoActualizado($mesa->toArray()); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error("Error emitiendo mesa.estado: " . $e->getMessage()); } return response()->json(['success' => true, 'message' => 'Mesa actualizada']); } catch (\Exception $e) { Log::error("❌ ResourceController Error: " . $e->getMessage(), ['trace' => $e->getTraceAsString(), 'url' => $request->fullUrl(), 'method' => $request->method(), 'body' => $request->all()]); return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } public function regenerateQR(Request $request, string $id): JsonResponse { try { $mesa = $this->mesaRepository->obtenerPorId($id); if (!$mesa) { return response()->json(['success' => false, 'message' => 'Mesa no encontrada'], 404); } $appUrl = env('VISTAS_URL'); if (!$appUrl) { // Si no hay VISTAS_URL definida (escenario local), usamos APP_URL // que suele ser http://:8000 o similar, lo cual permite // escanear en la misma red Wi-Fi sin obligar a usar HTTPS $appUrl = env('APP_URL', 'http://piterpro.com'); } $appUrl = rtrim($appUrl, '/'); $newQrCode = $appUrl . "/m/" . $id; \Illuminate\Support\Facades\DB::table('Nucleo_Recursos') ->where('id_recurso', $id) ->update(['qr_code' => $newQrCode]); return response()->json([ 'success' => true, 'message' => 'QR Regenerado exitosamente', 'data' => ['qr_code' => $newQrCode] ]); } catch (\Exception $e) { Log::error("❌ ResourceController RegenerateQR Error: " . $e->getMessage()); return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } public function destroy(Request $request, string $id): JsonResponse { try { $sucursalId = $request->query('sucursal_id'); $mesa = $this->mesaRepository->obtenerPorId($id); if (!$mesa || ($sucursalId && $mesa->sucursalId !== $sucursalId)) { return response()->json(['success' => false, 'message' => 'Recurso no accesible'], 404); } // Usar eliminación física o lógica mediada por el repositorio $this->mesaRepository->eliminar($id); // Emitir evento Realtime try { $this->realtimeService->mesaEliminada($mesa->toArray()); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error("Error emitiendo mesa.eliminada: " . $e->getMessage()); } return response()->json(['success' => true, 'message' => 'Mesa eliminada']); } catch (\Exception $e) { Log::error("❌ ResourceController Error: " . $e->getMessage(), ['trace' => $e->getTraceAsString(), 'url' => $request->fullUrl(), 'method' => $request->method(), 'body' => $request->all()]); return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } public function trash(Request $request): JsonResponse { try { $sucursalId = $request->query('sucursal_id'); if (!$sucursalId) { return response()->json(['success' => false, 'message' => 'Falta sucursal_id'], 400); } $mesas = $this->mesaRepository->obtenerInactivosPorSucursal($sucursalId); return response()->json([ 'success' => true, 'data' => array_map(fn($m) => $m->toArray(), $mesas) ]); } catch (\Exception $e) { Log::error("❌ ResourceController Error: " . $e->getMessage(), ['trace' => $e->getTraceAsString(), 'url' => $request->fullUrl(), 'method' => $request->method(), 'body' => $request->all()]); return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } public function restore(Request $request, string $id): JsonResponse { try { // 1️⃣ Obtener la mesa ANTES de restaurar para conocer su sucursal $mesa = $this->mesaRepository->obtenerPorId($id); if (!$mesa) { return response()->json(['success' => false, 'message' => 'Mesa no encontrada'], 404); } // 2️⃣ Validar límite del plan (igual que en store) $sucursal = \Illuminate\Support\Facades\DB::table('Nucleo_Sucursales') ->where('id_sucursal', $mesa->sucursalId) ->first(); if ($sucursal && $sucursal->plan_id) { $plan = \Illuminate\Support\Facades\DB::table('SaaS_Gestion_Planes') ->where('plan_id', $sucursal->plan_id) ->first(); if ($plan && $plan->limite_mesas != -1) { $mesasActuales = \Illuminate\Support\Facades\DB::table('Nucleo_Recursos') ->where('sucursal_id', $mesa->sucursalId) ->where('activo', 1) ->where('tipo', 'MESA') ->count(); if ($mesasActuales >= $plan->limite_mesas) { return response()->json([ 'success' => false, 'message' => "No puedes restaurar esta mesa: ya tienes el máximo de {$plan->limite_mesas} mesas activas permitidas por tu plan." ], 403); } } } // 3️⃣ Restaurar $this->mesaRepository->restaurar($id); // 4️⃣ Recargar para emitir evento realtime actualizado $mesaActualizada = $this->mesaRepository->obtenerPorId($id); if ($mesaActualizada) { $this->realtimeService->mesaCreada($mesaActualizada->toArray()); } return response()->json(['success' => true, 'message' => 'Recurso restaurado']); } catch (\Exception $e) { Log::error("❌ ResourceController Error: " . $e->getMessage(), ['trace' => $e->getTraceAsString(), 'url' => $request->fullUrl(), 'method' => $request->method(), 'body' => $request->all()]); return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } /** * Unir dos mesas (Master-Slave) * Transfiere pedidos de Slave a Master y marca la unión. */ public function unirMesas(Request $request): JsonResponse { try { $validated = $request->validate([ 'master_id' => 'required|string', 'slave_id' => 'required|string', 'sucursal_id' => 'required|string' ]); $masterId = $validated['master_id']; $slaveId = $validated['slave_id']; if ($masterId === $slaveId) { return response()->json(['success' => false, 'message' => 'No puedes unir una mesa consigo misma'], 400); } $master = $this->mesaRepository->obtenerPorId($masterId); $slave = $this->mesaRepository->obtenerPorId($slaveId); if (!$master || !$slave) { return response()->json(['success' => false, 'message' => 'Una de las mesas no existe'], 404); } if (!$master->permiteUnion || !$slave->permiteUnion) { return response()->json(['success' => false, 'message' => 'Una de las mesas no permite unión'], 400); } // 1. Fusionar Pedidos (Si ambas tienen cuenta abierta) // Table: Ventas_Ordenes, Column: recurso_id (instead of mesa_id) $pedidoMaster = \Illuminate\Support\Facades\DB::table('Ventas_Ordenes') ->where('recurso_id', $masterId) ->whereIn('estado', ['Pendiente', 'En Preparacion', 'Listo', 'Entregado']) // Active states ->first(); $pedidoSlave = \Illuminate\Support\Facades\DB::table('Ventas_Ordenes') ->where('recurso_id', $slaveId) ->whereIn('estado', ['Pendiente', 'En Preparacion', 'Listo', 'Entregado']) ->first(); if ($pedidoMaster && $pedidoSlave) { // Mover detalles del Slave al Master // Table: Ventas_Detalle_Orden, Column: orden_id (instead of pedido_id) \Illuminate\Support\Facades\DB::table('Ventas_Detalle_Orden') ->where('orden_id', $pedidoSlave->id_ordenes) ->update(['orden_id' => $pedidoMaster->id_ordenes]); // Cancelar pedido Slave vacío \Illuminate\Support\Facades\DB::table('Ventas_Ordenes') ->where('id_ordenes', $pedidoSlave->id_ordenes) ->update(['estado' => 'Cancelado', 'observaciones' => 'Fusionado con ' . $master->nombre]); } elseif (!$pedidoMaster && $pedidoSlave) { // Solo Slave tiene pedido -> Mover pedido a Master \Illuminate\Support\Facades\DB::table('Ventas_Ordenes') ->where('id_ordenes', $pedidoSlave->id_ordenes) ->update(['recurso_id' => $masterId]); // Actualizar estado de ocupación $master->estado = EstadoMesa::ocupada(); $this->mesaRepository->guardar($master); } // Liberar lógica del slave (aunque quedará unido visualmente) // $slave->liberar(); ... No, el slave queda ocupado visualmente como "Unido" // 2. Actualizar Estado de Unión $slave->grupoUnionId = $masterId; // El master es el líder, su grupoUnionId apunta a sí mismo o null (según convención) // Convención: Slave tiene grupoUnionId = MasterId. Master tiene null (es root) O MasterId (es root explícito). // Usaremos: Slave -> MasterId. Master -> MasterId (para indicar que ES master de un grupo). $master->grupoUnionId = $masterId; $this->mesaRepository->guardar($slave); $this->mesaRepository->guardar($master); // 3. Emitir Eventos $this->realtimeService->mesaActualizada($slave->toArray()); $this->realtimeService->mesaActualizada($master->toArray()); return response()->json(['success' => true, 'message' => 'Mesas unidas correctamente']); } catch (\Exception $e) { Log::error("❌ ResourceController Error: " . $e->getMessage(), ['trace' => $e->getTraceAsString(), 'url' => $request->fullUrl(), 'method' => $request->method(), 'body' => $request->all()]); return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } /** * Desvincular mesa de un grupo */ public function desunirMesa(Request $request): JsonResponse { try { $validated = $request->validate([ 'mesa_id' => 'required|string', 'sucursal_id' => 'required|string' ]); $mesaId = $validated['mesa_id']; $mesa = $this->mesaRepository->obtenerPorId($mesaId); if (!$mesa) { return response()->json(['success' => false, 'message' => 'Mesa no encontrada'], 404); } if (!$mesa->grupoUnionId) { return response()->json(['success' => false, 'message' => 'Esta mesa no está unida'], 400); } // Si es Master, desunimos a TODOS los esclavos primero? O solo se va él? // Si desunimos una Slave, solo esa se va. // Si desunimos Master, se rompe todo el grupo. $esMaster = $mesa->grupoUnionId === $mesa->id; if ($esMaster) { // Buscar esclavos y liberarlos $esclavos = \Illuminate\Support\Facades\DB::table('Nucleo_Recursos') ->where('grupo_union_id', $mesa->id) ->where('id_recurso', '!=', $mesa->id) ->get(); foreach ($esclavos as $slaveData) { $slave = $this->mesaRepository->obtenerPorId($slaveData->id_recurso); if ($slave) { $slave->nombre = $slaveData->nombre; // Restaurar nombre original $slave->grupoUnionId = null; $this->mesaRepository->guardar($slave); $this->realtimeService->mesaActualizada($slave->toArray()); } } // Finalmente el master se libera $masterData = \Illuminate\Support\Facades\DB::table('Nucleo_Recursos')->where('id_recurso', $mesa->id)->first(); if ($masterData) { $mesa->nombre = $masterData->nombre; // Restaurar nombre original } $mesa->grupoUnionId = null; } else { // Es esclavo, solo se libera él $slaveData = \Illuminate\Support\Facades\DB::table('Nucleo_Recursos')->where('id_recurso', $mesa->id)->first(); if ($slaveData) { $mesa->nombre = $slaveData->nombre; // Restaurar nombre original } $mesa->grupoUnionId = null; } $this->mesaRepository->guardar($mesa); $this->realtimeService->mesaActualizada($mesa->toArray()); return response()->json(['success' => true, 'message' => 'Mesa desvinculada']); } catch (\Exception $e) { Log::error("❌ ResourceController Error: " . $e->getMessage(), ['trace' => $e->getTraceAsString(), 'url' => $request->fullUrl(), 'method' => $request->method(), 'body' => $request->all()]); return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } /** * Activar/Desactivar Pago Adelantado en una mesa */ public function togglePagoAdelantado(Request $request, string $id): JsonResponse { try { $validated = $request->validate([ 'activar' => 'required|boolean', ]); $mesa = $this->mesaRepository->obtenerPorId($id); if (!$mesa) { return response()->json(['success' => false, 'message' => 'Mesa no encontrada'], 404); } if ($validated['activar']) { $this->mesaRepository->activarPagoAdelantado($id); return response()->json([ 'success' => true, 'message' => 'Pago Adelantado activado. La mesa no se liberará al pagar.', 'data' => ['pago_adelantado' => true] ]); } else { $this->mesaRepository->desactivarPagoAdelantado($id); return response()->json([ 'success' => true, 'message' => 'Pago Adelantado desactivado.', 'data' => ['pago_adelantado' => false] ]); } } catch (\Exception $e) { Log::error("❌ PagoAdelantado Error: " . $e->getMessage()); return response()->json(['success' => false, 'message' => $e->getMessage()], 500); } } }