Files
pos_heidiver/app/Filament/Pages/PuntoVenta.php
T
2026-01-06 15:35:59 -05:00

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();
}
}