null, 'correo' => null, 'telefono' => null, ]; public $cliente_id = null; public $correoCotizacion = ''; public $bodega_id = null; // Bodega seleccionada para la venta public function mount() { $this->productos = Producto::where('estado', true) ->where(function ($query) { $query->where('stock', '>', 0) ->orWhereHas('bodegas', function ($q) { $q->where('producto_bodega.stock', '>', 0); }) ->orWhereHas('variants', function ($q) { $q->where('stock', '>', 0) ->orWhereHas('bodegas', function ($bq) { $bq->where('variante_bodega.stock', '>', 0); }); }); }) ->with('variants.color', 'variants.size', 'bodegas') ->limit(20) ->get(); // Seleccionar bodega "Principal" por defecto $bodegaPrincipal = \App\Models\Bodega::where('nombre', 'Principal')->first(); $this->bodega_id = $bodegaPrincipal ? $bodegaPrincipal->id : null; } public function agregarAlCarrito($productoId, $tipo) { if ($tipo == 'p') { $producto = Producto::find($productoId); if (!$producto) return; $key = 'p-' . $producto->id; if (!isset($this->carrito[$key])) { $this->carrito[$key] = [ 'id' => $producto->id, 'nombre' => $producto->nombre, 'precio_venta' => $producto->precio_venta, 'precio_original' => $producto->precio_venta, 'precio_modificado' => $producto->precio_venta, 'cantidad' => 1, 'tipo' => 'p', ]; } else { $this->carrito[$key]['cantidad']++; } } else { $producto = ProductVariant::find($productoId); if (!$producto) return; $key = 'v-' . $producto->id; if (!isset($this->carrito[$key])) { $this->carrito[$key] = [ 'id' => $producto->producto->id, 'nombre' => $producto->producto->nombre, 'precio_venta' => $producto->producto->precio_venta, 'precio_original' => $producto->producto->precio_venta, 'precio_modificado' => $producto->producto->precio_venta, 'variante' => 'Color: ' . $producto->color->name . ', Talla: ' . $producto->size->name, 'variante_id' => $producto->id, 'cantidad' => 1, 'tipo' => 'v', ]; } else { $this->carrito[$key]['cantidad']++; } } $this->calcularTotal(); } public function calcularTotal() { $this->total = collect($this->carrito)->sum(fn($item) => $item['precio_modificado'] * $item['cantidad']); } public function actualizarPrecio($key, $nuevoPrecio) { if (isset($this->carrito[$key])) { // Validar que el precio sea válido (mayor o igual a 0) $precio = floatval($nuevoPrecio); if ($precio >= 0) { $this->carrito[$key]['precio_modificado'] = $precio; $this->calcularTotal(); } } } public function procesarVenta() { if (empty($this->carrito)) { session()->flash('error', 'No se puede procesar la venta: el carrito está vacío.'); return; } DB::beginTransaction(); try { $clienteId = null; if ($this->numeroDocumentoCliente) { $cliente = Cliente::where('numero_documento', $this->numeroDocumentoCliente)->first(); if (!$cliente) { $datosCliente = [ 'nombre' => $this->datosCliente['nombre'] ?? "Cliente General", 'correo' => $this->datosCliente['correo'] ?? null, 'telefono' => $this->datosCliente['telefono'] ?? null, 'numero_documento' => $this->numeroDocumentoCliente, 'tipo_documento' => $this->datosCliente['tipo_documento'] ?? 'CC', ]; $cliente = Cliente::create($datosCliente); } $clienteId = $cliente->id; } elseif ($this->cliente_id) { $cliente = Cliente::find($this->cliente_id); if ($cliente) { $clienteId = $cliente->id; } else { $this->cliente_id = null; $clienteId = null; } } $venta = Venta::create([ 'cliente_id' => $clienteId, 'total' => $this->total, 'tipo_pago' => $this->tipo_pago, 'estado' => 'Pagado', ]); foreach ($this->carrito as $item) { $precioOriginal = $item['precio_original']; $precioModificado = $item['precio_modificado']; $descuentoAplicado = $precioOriginal - $precioModificado; $cantidadPendiente = $item['cantidad']; // Identificar el producto o variante real $producto = null; $variante = null; if (isset($item['variante_id'])) { $variante = ProductVariant::find($item['variante_id']); $producto = $variante->producto; } else { $producto = Producto::find($item['id']); } // Estrategia de deducción de stock: // 1. Buscar en Bodega Principal // 2. Buscar en otras bodegas con stock // 3. Si falta, descontar de stock directo (o dejar negativo en Principal si se prefiere, aquí usaremos Principal como fallback) $bodegas = \App\Models\Bodega::orderByRaw("CASE WHEN nombre = 'Principal' THEN 0 ELSE 1 END")->get(); foreach ($bodegas as $bodega) { if ($cantidadPendiente <= 0) break; $stockDisponible = 0; if ($variante) { $bodegaVariante = $variante->bodegas()->where('bodega_id', $bodega->id)->first(); $stockDisponible = $bodegaVariante ? $bodegaVariante->pivot->stock : 0; } else { $bodegaProducto = $producto->bodegas()->where('bodega_id', $bodega->id)->first(); $stockDisponible = $bodegaProducto ? $bodegaProducto->pivot->stock : 0; } if ($stockDisponible > 0) { $cantidadADescontar = min($cantidadPendiente, $stockDisponible); // Registrar detalle para esta bodega $venta->detalles()->create([ 'producto_id' => $item['id'], 'variante_id' => $item['variante_id'] ?? null, 'bodega_id' => $bodega->id, 'cantidad' => $cantidadADescontar, 'precio_unitario' => $precioModificado, 'precio_original' => $precioOriginal, 'descuento_aplicado' => $descuentoAplicado, 'usuario_modifico_precio_id' => ($descuentoAplicado != 0) ? auth()->id() : null, 'subtotal' => $cantidadADescontar * $precioModificado, ]); // Actualizar stock if ($variante) { $variante->bodegas()->updateExistingPivot($bodega->id, [ 'stock' => $stockDisponible - $cantidadADescontar ]); } else { $producto->bodegas()->updateExistingPivot($bodega->id, [ 'stock' => $stockDisponible - $cantidadADescontar ]); } $cantidadPendiente -= $cantidadADescontar; } } // Si aún queda cantidad pendiente (no había stock suficiente en ninguna bodega) // Lo asignamos a la Bodega Principal (o NULL si no hay bodegas) y dejamos que el stock se vaya a negativo o se descuente del directo if ($cantidadPendiente > 0) { $bodegaFallback = $bodegas->first(); // Principal por el ordenamiento $bodegaIdFallback = $bodegaFallback ? $bodegaFallback->id : null; $venta->detalles()->create([ 'producto_id' => $item['id'], 'variante_id' => $item['variante_id'] ?? null, 'bodega_id' => $bodegaIdFallback, 'cantidad' => $cantidadPendiente, 'precio_unitario' => $precioModificado, 'precio_original' => $precioOriginal, 'descuento_aplicado' => $descuentoAplicado, 'usuario_modifico_precio_id' => ($descuentoAplicado != 0) ? auth()->id() : null, 'subtotal' => $cantidadPendiente * $precioModificado, ]); // Intentar descontar del fallback if ($bodegaFallback) { if ($variante) { $bodegaVariante = $variante->bodegas()->where('bodega_id', $bodegaFallback->id)->first(); if ($bodegaVariante) { $variante->bodegas()->updateExistingPivot($bodegaFallback->id, [ 'stock' => $bodegaVariante->pivot->stock - $cantidadPendiente ]); } else { // Si no existe la relación, la creamos con stock negativo $variante->bodegas()->attach($bodegaFallback->id, ['stock' => -$cantidadPendiente]); } } else { $bodegaProducto = $producto->bodegas()->where('bodega_id', $bodegaFallback->id)->first(); if ($bodegaProducto) { $producto->bodegas()->updateExistingPivot($bodegaFallback->id, [ 'stock' => $bodegaProducto->pivot->stock - $cantidadPendiente ]); } else { $producto->bodegas()->attach($bodegaFallback->id, ['stock' => -$cantidadPendiente]); } } } else { // Si no hay bodegas en absoluto, descontar del stock directo if ($variante) { $variante->decrement('stock', $cantidadPendiente); } else { $producto->decrement('stock', $cantidadPendiente); } } } } $cajaAbierta = Caja::where('estado', 'Abierta') ->orderBy('fecha_apertura', 'desc') ->first(); if ($cajaAbierta) { MovimientoCaja::create([ 'caja_id' => $cajaAbierta->id, 'tipo' => 'Ingreso', 'monto' => $this->total, 'descripcion' => 'Venta realizada. Venta ID: ' . $venta->id, ]); } DB::commit(); $this->resetVentaState(); session()->flash('message', 'Venta realizada con éxito.'); $this->js("window.dispatchEvent(new CustomEvent('imprimir-recibo', { detail: { venta_id: {$venta->id} } }));"); } catch (\Exception $e) { DB::rollBack(); $this->resetVentaState(); session()->flash('error', 'Error al procesar la venta. ' . $e->getMessage()); } } /** * Resetea el estado de la venta para evitar inconsistencias */ private function resetVentaState() { $this->carrito = []; $this->total = 0; $this->numeroDocumentoCliente = ''; $this->datosCliente = [ 'nombre' => null, 'correo' => null, 'telefono' => null, ]; $this->cliente_id = null; $this->tipo_pago = 'Efectivo'; // Mantener la bodega seleccionada para la próxima venta // $this->bodega_id se mantiene } public function buscarProductoPorBarcode() { $barcode = trim($this->barcodeBusqueda); $producto = Producto::where('codigo_barras', $barcode)->first(); $tipo = 'p'; if (!$producto) { $producto = ProductVariant::where('barcode', $barcode)->first(); $tipo = 'v'; } if ($producto) { $this->agregarAlCarrito($producto->id, $tipo); $this->barcodeBusqueda = ''; } else { session()->flash('error', 'Producto no encontrado.'); } } public function incrementarCantidad($key) { if (isset($this->carrito[$key])) { $this->carrito[$key]['cantidad']++; $this->calcularTotal(); } } public function decrementarCantidad($key) { if (isset($this->carrito[$key])) { $this->carrito[$key]['cantidad']--; if ($this->carrito[$key]['cantidad'] <= 0) { unset($this->carrito[$key]); } $this->calcularTotal(); } } public function eliminarDelCarrito($key) { if (isset($this->carrito[$key])) { unset($this->carrito[$key]); $this->calcularTotal(); } } public function enviarCotizacion() { $correo = $this->correoCotizacion; if (!filter_var($correo, FILTER_VALIDATE_EMAIL)) { session()->flash('error', 'Correo inválido.'); return; } try { $carrito = $this->carrito; $total = $this->total; Mail::send('emails.cotizacion', compact('carrito', 'total'), function ($message) use ($correo) { $message->to($correo) ->subject('Cotización de productos'); }); session()->flash('message', 'Cotización enviada exitosamente.'); $this->correoCotizacion = ''; } catch (\Exception $e) { session()->flash('error', 'Error al enviar la cotización: ' . $e->getMessage()); } } public function buscarClientePorDocumento() { $documento = trim($this->numeroDocumentoCliente); if (!$documento) { // No se requiere documento $this->cliente_id = null; $this->datosCliente = [ 'nombre' => null, 'correo' => null, 'telefono' => null, ]; return; } $cliente = Cliente::where('numero_documento', $documento)->first(); if ($cliente) { // Verificar que el cliente encontrado existe realmente $clienteValidado = Cliente::find($cliente->id); if ($clienteValidado) { $this->cliente_id = $clienteValidado->id; $this->datosCliente = [ 'nombre' => $clienteValidado->nombre, 'correo' => $clienteValidado->correo, 'telefono' => $clienteValidado->telefono, ]; } else { // Cliente no válido, resetear $this->cliente_id = null; $this->datosCliente = [ 'nombre' => null, 'correo' => null, 'telefono' => null, ]; } } else { $this->cliente_id = null; $this->datosCliente = [ 'nombre' => null, 'correo' => null, 'telefono' => null, ]; } } public function cambiarBodega($bodegaId) { $this->bodega_id = $bodegaId; // Opcional: mostrar notificación de cambio session()->flash('message', 'Bodega cambiada. Las ventas se realizarán desde la nueva bodega seleccionada.'); } public function getBodegasProperty() { return \App\Models\Bodega::all(); } }