481 lines
18 KiB
PHP
481 lines
18 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Pages;
|
|
|
|
use App\Models\Caja;
|
|
use App\Models\Cliente;
|
|
use App\Models\MovimientoCaja;
|
|
use Filament\Pages\Page;
|
|
use Filament\Forms;
|
|
use Filament\Tables;
|
|
use App\Models\Producto;
|
|
use App\Models\ProductVariant;
|
|
use App\Models\Venta;
|
|
use Livewire\Component;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Mail;
|
|
|
|
class PuntoVenta extends Page
|
|
{
|
|
protected static ?string $navigationGroup = 'Operación';
|
|
protected static ?string $navigationIcon = 'heroicon-o-shopping-cart';
|
|
protected static string $view = 'filament.pages.punto-venta';
|
|
|
|
public $barcodeBusqueda = '';
|
|
|
|
public $productos = [];
|
|
public $carrito = [];
|
|
public $total = 0;
|
|
public $tipo_pago = 'Efectivo';
|
|
|
|
public $numeroDocumentoCliente = '';
|
|
public $datosCliente = [
|
|
'nombre' => 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();
|
|
}
|
|
|
|
|
|
}
|