This commit is contained in:
Lizandro Guarnizo
2026-01-06 15:35:59 -05:00
commit a768146f65
602 changed files with 42505 additions and 0 deletions
@@ -0,0 +1,181 @@
<?php
namespace App\Filament\Resources\ProductoResource\Pages;
use App\Filament\Resources\ProductoResource;
use App\Models\Producto;
use App\Models\Bodega;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
use Filament\Notifications\Notification;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class CreateProducto extends CreateRecord
{
protected static string $resource = ProductoResource::class;
protected function mutateFormDataBeforeCreate(array $data): array
{
Log::info('=== CreateProducto DEBUG ===');
Log::info('Datos completos recibidos:', $data);
Log::info('Keys disponibles:', array_keys($data));
// Verificar presencia de campos específicos
Log::info('Campos de stock presentes:', [
'stock_entrada' => isset($data['stock_entrada']) ? $data['stock_entrada'] : 'NO PRESENTE',
'stock_minimo_entrada' => isset($data['stock_minimo_entrada']) ? $data['stock_minimo_entrada'] : 'NO PRESENTE',
'stock_maximo_entrada' => isset($data['stock_maximo_entrada']) ? $data['stock_maximo_entrada'] : 'NO PRESENTE',
]);
// Asegurar que descripcion no esté vacía
if (empty($data['descripcion']) || is_null($data['descripcion'])) {
$data['descripcion'] = 'Sin descripción';
}
// Procesar stock inicial
if (isset($data['stock_entrada'])) {
$stockEntrada = $data['stock_entrada'];
$unidadMedida = $data['unidad_medida'] ?? 'unidad';
// Convertir a unidades
$stockEntradaFloat = is_numeric($stockEntrada) ? (float) $stockEntrada : 0.0;
$data['stock'] = Producto::convertirAUnidades($stockEntradaFloat, $unidadMedida);
Log::info('Stock procesado:', [
'stock_entrada' => $stockEntrada,
'unidad_medida' => $unidadMedida,
'stock_final' => $data['stock']
]);
}
// Procesar stock mínimo
if (isset($data['stock_minimo_entrada'])) {
$stockMinimoEntrada = $data['stock_minimo_entrada'];
$unidadMedida = $data['unidad_medida'] ?? 'unidad';
// Convertir a unidades
$stockMinimoEntradaFloat = is_numeric($stockMinimoEntrada) ? (float) $stockMinimoEntrada : 0.0;
$data['stock_minimo'] = Producto::convertirAUnidades($stockMinimoEntradaFloat, $unidadMedida);
Log::info('Stock mínimo procesado:', [
'stock_minimo_entrada' => $stockMinimoEntrada,
'unidad_medida' => $unidadMedida,
'stock_minimo_final' => $data['stock_minimo']
]);
}
// Procesar stock máximo
if (isset($data['stock_maximo_entrada'])) {
$stockMaximoEntrada = $data['stock_maximo_entrada'];
$unidadMedida = $data['unidad_medida'] ?? 'unidad';
// Convertir a unidades
$stockMaximoEntradaFloat = is_numeric($stockMaximoEntrada) ? (float) $stockMaximoEntrada : 0.0;
$data['stock_maximo'] = Producto::convertirAUnidades($stockMaximoEntradaFloat, $unidadMedida);
Log::info('Stock máximo procesado:', [
'stock_maximo_entrada' => $stockMaximoEntrada,
'unidad_medida' => $unidadMedida,
'stock_maximo_final' => $data['stock_maximo']
]);
}
// Remover campos auxiliares que no deben guardarse
unset($data['stock_entrada'], $data['stock_minimo_entrada'], $data['stock_maximo_entrada']);
Log::info('Datos finales para crear producto:', $data);
return $data;
}
protected function afterCreate(): void
{
$producto = $this->record;
// Solo asignar stock a bodega principal si NO tiene variantes
if ($producto->stock > 0 && !$producto->variants()->exists()) {
try {
DB::beginTransaction();
// Buscar o crear la bodega principal
$bodegaPrincipal = Bodega::firstOrCreate([
'nombre' => 'Principal'
]);
Log::info("Asignando stock a bodega principal", [
'producto_id' => $producto->id,
'producto_nombre' => $producto->nombre,
'stock' => $producto->stock,
'bodega_id' => $bodegaPrincipal->id,
'bodega_nombre' => $bodegaPrincipal->nombre
]);
// Verificar si ya existe una relación con esta bodega
$existeRelacion = $producto->bodegas()->where('bodega_id', $bodegaPrincipal->id)->exists();
if (!$existeRelacion) {
// Asignar el stock a la bodega principal
$producto->bodegas()->attach($bodegaPrincipal->id, [
'stock' => $producto->stock
]);
Log::info("Stock asignado exitosamente a bodega principal");
// Mostrar notificación al usuario
Notification::make()
->success()
->title('Producto creado exitosamente')
->body("Stock inicial de {$producto->stock} unidades asignado a la bodega Principal")
->send();
} else {
Log::info("El producto ya tiene relación con la bodega principal, actualizando stock");
// Actualizar el stock en la bodega principal
$producto->bodegas()->updateExistingPivot($bodegaPrincipal->id, [
'stock' => $producto->stock
]);
Notification::make()
->success()
->title('Producto creado exitosamente')
->body("Stock actualizado en la bodega Principal: {$producto->stock} unidades")
->send();
}
DB::commit();
} catch (\Exception $e) {
DB::rollback();
Log::error("Error al asignar stock a bodega principal", [
'producto_id' => $producto->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// Mostrar notificación de error
Notification::make()
->warning()
->title('Producto creado con advertencia')
->body('El producto se creó correctamente, pero hubo un problema al asignar el stock a la bodega principal.')
->persistent()
->send();
}
} elseif ($producto->variants()->exists()) {
// Si tiene variantes, limpiar el stock del producto principal
$producto->update(['stock' => 0]);
Log::info("Producto con variantes creado - stock principal establecido en 0", [
'producto_id' => $producto->id,
'producto_nombre' => $producto->nombre
]);
Notification::make()
->info()
->title('Producto con variantes creado')
->body('El stock se manejará a nivel de cada variante individual')
->send();
}
}
}
@@ -0,0 +1,128 @@
<?php
namespace App\Filament\Resources\ProductoResource\Pages;
use App\Filament\Resources\ProductoResource;
use App\Models\Producto;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use Illuminate\Support\Facades\Log;
class EditProducto extends EditRecord
{
protected static string $resource = ProductoResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
protected function mutateFormDataBeforeFill(array $data): array
{
Log::info('Cargando datos para edición:', $data);
// Convertir valores base a campos auxiliares para mostrar en el formulario
$unidadMedida = $data['unidad_medida'] ?? 'unidad';
// Convertir stock base a campo auxiliar
if (isset($data['stock'])) {
$data['stock_entrada'] = Producto::convertirDesdeUnidades($data['stock'], $unidadMedida);
Log::info('Stock convertido para edición:', [
'stock_base' => $data['stock'],
'stock_entrada' => $data['stock_entrada'],
'unidad_medida' => $unidadMedida
]);
}
// Convertir stock mínimo base a campo auxiliar
if (isset($data['stock_minimo'])) {
$data['stock_minimo_entrada'] = Producto::convertirDesdeUnidades($data['stock_minimo'], $unidadMedida);
Log::info('Stock mínimo convertido para edición:', [
'stock_minimo_base' => $data['stock_minimo'],
'stock_minimo_entrada' => $data['stock_minimo_entrada'],
'unidad_medida' => $unidadMedida
]);
}
// Convertir stock máximo base a campo auxiliar
if (isset($data['stock_maximo'])) {
$data['stock_maximo_entrada'] = Producto::convertirDesdeUnidades($data['stock_maximo'], $unidadMedida);
Log::info('Stock máximo convertido para edición:', [
'stock_maximo_base' => $data['stock_maximo'],
'stock_maximo_entrada' => $data['stock_maximo_entrada'],
'unidad_medida' => $unidadMedida
]);
}
Log::info('Datos finales para formulario de edición:', $data);
return $data;
}
protected function mutateFormDataBeforeSave(array $data): array
{
Log::info('Datos recibidos en EditProducto:', $data);
// Asegurar que descripcion no esté vacía
if (empty($data['descripcion']) || is_null($data['descripcion'])) {
$data['descripcion'] = 'Sin descripción';
}
// Procesar stock inicial
if (isset($data['stock_entrada'])) {
$stockEntrada = $data['stock_entrada'];
$unidadMedida = $data['unidad_medida'] ?? 'unidad';
// Convertir a unidades
$stockEntradaFloat = is_numeric($stockEntrada) ? (float) $stockEntrada : 0.0;
$data['stock'] = Producto::convertirAUnidades($stockEntradaFloat, $unidadMedida);
Log::info('Stock procesado:', [
'stock_entrada' => $stockEntrada,
'unidad_medida' => $unidadMedida,
'stock_final' => $data['stock']
]);
}
// Procesar stock mínimo
if (isset($data['stock_minimo_entrada'])) {
$stockMinimoEntrada = $data['stock_minimo_entrada'];
$unidadMedida = $data['unidad_medida'] ?? 'unidad';
// Convertir a unidades
$stockMinimoEntradaFloat = is_numeric($stockMinimoEntrada) ? (float) $stockMinimoEntrada : 0.0;
$data['stock_minimo'] = Producto::convertirAUnidades($stockMinimoEntradaFloat, $unidadMedida);
Log::info('Stock mínimo procesado:', [
'stock_minimo_entrada' => $stockMinimoEntrada,
'unidad_medida' => $unidadMedida,
'stock_minimo_final' => $data['stock_minimo']
]);
}
// Procesar stock máximo
if (isset($data['stock_maximo_entrada'])) {
$stockMaximoEntrada = $data['stock_maximo_entrada'];
$unidadMedida = $data['unidad_medida'] ?? 'unidad';
// Convertir a unidades
$stockMaximoEntradaFloat = is_numeric($stockMaximoEntrada) ? (float) $stockMaximoEntrada : 0.0;
$data['stock_maximo'] = Producto::convertirAUnidades($stockMaximoEntradaFloat, $unidadMedida);
Log::info('Stock máximo procesado:', [
'stock_maximo_entrada' => $stockMaximoEntrada,
'unidad_medida' => $unidadMedida,
'stock_maximo_final' => $data['stock_maximo']
]);
}
// Remover campos auxiliares que no deben guardarse
unset($data['stock_entrada'], $data['stock_minimo_entrada'], $data['stock_maximo_entrada']);
Log::info('Datos finales para editar producto:', $data);
return $data;
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\ProductoResource\Pages;
use App\Filament\Resources\ProductoResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListProductos extends ListRecords
{
protected static string $resource = ProductoResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}
@@ -0,0 +1,308 @@
<?php
namespace App\Filament\Resources\ProductoResource\RelationManagers;
use App\Models\Bodega;
use App\Models\Producto;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Notifications\Notification;
class BodegasRelationManager extends RelationManager
{
protected static string $relationship = 'bodegas';
protected static ?string $title = 'Stock por Bodega';
protected static ?string $modelLabel = 'Bodega';
protected static ?string $pluralModelLabel = 'Bodegas';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Select::make('bodega_id')
->label('Bodega')
->options(\App\Models\Bodega::pluck('nombre', 'id'))
->required()
->searchable()
->preload(),
Forms\Components\TextInput::make('stock')
->label('Stock en esta Bodega')
->numeric()
->required()
->default(0)
->minValue(0)
->helperText('Cantidad de este producto en la bodega seleccionada'),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('nombre')
->columns([
Tables\Columns\TextColumn::make('nombre')
->label('Bodega')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('pivot.stock')
->label('Stock')
->alignCenter()
->sortable()
->formatStateUsing(function ($state, $record) {
$producto = $this->getOwnerRecord();
// Validación defensiva
if (!$producto || !$producto->getKey()) {
return "Error: Producto no encontrado";
}
$unidadMedida = $producto->unidad_medida ?? 'unidad';
$stockEnUnidad = Producto::convertirDesdeUnidades($state, $unidadMedida);
$nombreUnidad = explode(' ', Producto::getUnidadesMedida()[$unidadMedida])[0];
$estado = $state > 0 ? '✅' : '⚠️';
return "{$estado} {$stockEnUnidad} {$nombreUnidad}";
})
->tooltip(function ($state, $record) {
$producto = $this->getOwnerRecord();
// Validación defensiva
if (!$producto || !$producto->getKey()) {
return "Error: No se pudo cargar información del producto";
}
$unidadMedida = $producto->unidad_medida ?? 'unidad';
$stockEnUnidad = Producto::convertirDesdeUnidades($state, $unidadMedida);
$nombreUnidad = Producto::getUnidadesMedida()[$unidadMedida];
return "Stock en {$nombreUnidad}: {$stockEnUnidad}\nStock en unidades: {$state}";
}),
Tables\Columns\TextColumn::make('pivot.updated_at')
->label('Última Actualización')
->dateTime('d/m/Y H:i')
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\Filter::make('con_stock')
->label('Solo con Stock')
->query(fn ($query) => $query->where('producto_bodega.stock', '>', 0)),
])
->headerActions([
Tables\Actions\CreateAction::make()
->label('Asignar a Bodega')
->modalHeading('Asignar Producto a Bodega')
->mutateFormDataUsing(function (array $data): array {
// Asegurarse de que el bodega_id esté en el pivot
return $data;
})
->using(function (array $data): void {
$producto = $this->getOwnerRecord();
// Validación defensiva para evitar error getKey() on null
if (!$producto || !$producto->getKey()) {
Notification::make()
->danger()
->title('Error')
->body('No se pudo obtener la información del producto.')
->send();
return;
}
$bodegaId = $data['bodega_id'];
$stock = $data['stock'];
// Verificar si ya existe la relación
if ($producto->bodegas()->where('bodega_id', $bodegaId)->exists()) {
Notification::make()
->warning()
->title('Producto ya asignado')
->body('Este producto ya está asignado a esa bodega. Use la opción de editar.')
->send();
return;
}
// Crear la relación
$producto->bodegas()->attach($bodegaId, ['stock' => $stock]);
Notification::make()
->success()
->title('Producto asignado')
->body('El producto ha sido asignado a la bodega correctamente.')
->send();
}),
Tables\Actions\Action::make('distribuir_stock')
->label('Distribuir Stock Total')
->icon('heroicon-o-arrows-right-left')
->color('warning')
->form([
Forms\Components\Placeholder::make('info')
->label('Información')
->content(function () {
$producto = $this->getOwnerRecord();
// Validación defensiva
if (!$producto || !$producto->getKey()) {
return "Error: No se pudo cargar información del producto";
}
$stockTotal = $producto->getStockEfectivo();
$unidadMedida = $producto->unidad_medida ?? 'unidad';
$stockEnUnidad = Producto::convertirDesdeUnidades($stockTotal, $unidadMedida);
$nombreUnidad = Producto::getUnidadesMedida()[$unidadMedida];
return "Stock total disponible: {$stockEnUnidad} {$nombreUnidad} ({$stockTotal} unidades)";
}),
Forms\Components\Repeater::make('distribucion')
->label('Distribución por Bodega')
->schema([
Forms\Components\Select::make('bodega_id')
->label('Bodega')
->options(Bodega::pluck('nombre', 'id'))
->required(),
Forms\Components\TextInput::make('stock')
->label('Stock a Asignar')
->numeric()
->required()
->default(0)
->minValue(0),
])
->minItems(1)
->addActionLabel('Añadir Bodega'),
])
->action(function (array $data): void {
$producto = $this->getOwnerRecord();
// Validación defensiva
if (!$producto || !$producto->getKey()) {
Notification::make()
->danger()
->title('Error')
->body('No se pudo obtener la información del producto.')
->send();
return;
}
$distribucion = $data['distribucion'] ?? [];
// Validar que la suma no exceda el stock total
$stockAsignado = array_sum(array_column($distribucion, 'stock'));
$stockTotal = $producto->getStockEfectivo();
if ($stockAsignado > $stockTotal) {
Notification::make()
->danger()
->title('Error de Distribución')
->body("El stock asignado ({$stockAsignado}) excede el stock disponible ({$stockTotal})")
->send();
return;
}
// Distribuir el stock
foreach ($distribucion as $item) {
$producto->bodegas()->syncWithoutDetaching([
$item['bodega_id'] => ['stock' => $item['stock']]
]);
}
Notification::make()
->success()
->title('Stock Distribuido')
->body('El stock ha sido distribuido correctamente entre las bodegas.')
->send();
}),
])
->actions([
Tables\Actions\EditAction::make()
->label('Editar Stock')
->form([
Forms\Components\TextInput::make('stock')
->label('Stock en esta Bodega')
->numeric()
->required()
->minValue(0)
->helperText('Cantidad de este producto en la bodega'),
])
->fillForm(function ($record): array {
return [
'stock' => $record->pivot?->stock ?? 0,
];
})
->using(function (array $data, $record): void {
$record->pivot->update(['stock' => (int)$data['stock']]);
Notification::make()
->success()
->title('Stock Actualizado')
->body("El stock se actualizó a {$data['stock']} unidades.")
->send();
}),
Tables\Actions\Action::make('vaciar_stock')
->label('Vaciar Stock')
->icon('heroicon-o-minus-circle')
->color('warning')
->requiresConfirmation()
->modalHeading('Vaciar Stock de Bodega')
->modalDescription('¿Estás seguro de que quieres poner el stock en 0 para esta bodega? La bodega seguirá asignada al producto.')
->modalSubmitActionLabel('Sí, vaciar stock')
->action(function ($record): void {
$record->pivot->update(['stock' => 0]);
Notification::make()
->success()
->title('Stock Vaciado')
->body('El stock de la bodega ha sido puesto en 0.')
->send();
}),
/*
Tables\Actions\DeleteAction::make()
->label('Quitar Bodega')
->requiresConfirmation()
->modalHeading('Quitar Producto de Bodega')
->modalDescription('¿Estás seguro de que quieres quitar completamente este producto de la bodega? Se perderá toda la información de stock.')
->modalSubmitActionLabel('Sí, quitar completamente'), */
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\BulkAction::make('vaciar_stock_multiple')
->label('Vaciar Stock de Seleccionadas')
->icon('heroicon-o-minus-circle')
->color('warning')
->requiresConfirmation()
->modalHeading('Vaciar Stock de Bodegas Seleccionadas')
->modalDescription('¿Estás seguro de que quieres poner el stock en 0 para todas las bodegas seleccionadas?')
->action(function ($records): void {
foreach ($records as $record) {
$record->pivot->update(['stock' => 0]);
}
Notification::make()
->success()
->title('Stock Vaciado')
->body('El stock de las bodegas seleccionadas ha sido puesto en 0.')
->send();
}),
Tables\Actions\DeleteBulkAction::make()
->label('Quitar de Bodegas Seleccionadas')
->requiresConfirmation()
->modalHeading('Quitar Producto de Bodegas')
->modalDescription('¿Estás seguro de que quieres quitar completamente este producto de las bodegas seleccionadas? Se perderá toda la información de stock.')
->modalSubmitActionLabel('Sí, quitar completamente'),
]),
]);
}
}
@@ -0,0 +1,411 @@
<?php
namespace App\Filament\Resources\ProductoResource\RelationManagers;
use App\Models\ProductVariant;
use App\Models\Bodega;
use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Actions\Action;
use SimpleSoftwareIO\QrCode\Facades\QrCode;
use Filament\Notifications\Notification;
class VariantesRelationManager extends RelationManager
{
protected static string $relationship = 'variants';
public function form(Form $form): Form
{
return $form
->schema([
Forms\Components\Section::make('Información de la Variante')
->schema([
Forms\Components\Select::make('color_id')
->relationship('color', 'name')
->searchable()
->required()
->reactive()
->afterStateUpdated(fn($set, $get, $component) => $this->updateSkuAndBarcode($set, $get, $component)),
Forms\Components\Select::make('size_id')
->relationship('size', 'name')
->searchable()
->required()
->reactive()
->afterStateUpdated(fn($set, $get, $component) => $this->updateSkuAndBarcode($set, $get, $component)),
Forms\Components\TextInput::make('stock')
->label('Stock Inicial')
->required()
->numeric()
->default(0)
->helperText('Este será el stock inicial que se asignará a la bodega seleccionada'),
Forms\Components\TextInput::make('sku')
->label('SKU')
->required()
->maxLength(50)
->dehydrated(), // Se guarda en la base de datos
TextInput::make('barcode')
->label('Código de Barras')
->required()
->length(13)
->dehydrated()
->suffixAction(
Action::make('imprimirQr')
->icon('heroicon-o-printer')
->color('primary')
->hidden(fn($record) => is_null($record))
->action(fn($state) => redirect()->route('imprimir.barcode', ['barcode' => $state]))
->hidden(fn($record) => is_null($record))
)
])
->columns(2),
Forms\Components\Section::make('Asignación de Bodega')
->schema([
Forms\Components\Select::make('bodega_id')
->label('Bodega Inicial')
->options(Bodega::pluck('nombre', 'id'))
->required()
->searchable()
->preload()
->default(function () {
// Intentar obtener la bodega principal como default
$bodegaPrincipal = Bodega::where('nombre', 'Principal')->first();
return $bodegaPrincipal?->id;
})
->helperText('Seleccione la bodega donde se asignará el stock inicial de esta variante'),
Forms\Components\Placeholder::make('info_bodega')
->label('Información')
->content('La variante se creará con el stock especificado en la bodega seleccionada. Podrá distribuir a otras bodegas posteriormente.')
])
->columns(1),
]);
}
protected static function updateSkuAndBarcode($set, $get, $component)
{
$producto = $component->getLivewire()->ownerRecord;
$color = $get('color_id') ? \App\Models\Color::find($get('color_id')) : null;
$size = $get('size_id') ? \App\Models\Size::find($get('size_id')) : null;
$exists = ProductVariant::where('producto_id', $get('producto_id'))
->where('color_id', $get('color_id'))
->where('size_id', $get('size_id'))
->exists();
if ($exists) {
// Mostrar notificación en Filament
Notification::make()
->title('Error')
->body('Esta combinación de producto, color y talla ya existe.')
->danger()
->send();
}
// Generar SKU basado en el producto, color y talla
$baseSku = strtoupper(substr($producto?->nombre ?? 'XX', 0, 2)) . '-' .
strtoupper(substr($color?->name ?? 'XX', 0, 2)) . '-' .
strtoupper(substr($size?->name ?? 'XX', 0, 2));
// Asegurar SKU único
$sku = $baseSku;
$counter = 1;
while (ProductVariant::where('sku', $sku)->exists()) {
$sku = $baseSku . '-' . $counter;
$counter++;
}
// Obtener datos para el código de barras
$countryCode = '57'; // Código de país (puedes cambiarlo)
$categoryId = $producto?->categoria_id ?? 00;
$productId = $producto?->id ?? 00000;
$variantId = ProductVariant::where('producto_id',$productId)->count() ?? 0;
$variantId += 1;
// Generar código de barras estructurado
$eanService = app(\App\Services\EAN13Service::class);
$barcode = $eanService->generateUniqueBarcode($countryCode, $categoryId, $productId, $variantId);
// Asignar valores al formulario
$set('sku', $sku);
$set('barcode', $barcode);
}
/**
* Generar SKU y código de barras únicos para una variante
*/
private function generateUniqueSkuAndBarcode(ProductVariant $variante): void
{
$producto = $variante->producto;
$color = $variante->color;
$size = $variante->size;
// Generar SKU
$baseSku = strtoupper(substr($producto->nombre ?? 'XX', 0, 2)) . '-' .
strtoupper(substr($color->name ?? 'XX', 0, 2)) . '-' .
strtoupper(substr($size->name ?? 'XX', 0, 2));
$sku = $baseSku;
$counter = 1;
while (ProductVariant::where('sku', $sku)->exists()) {
$sku = $baseSku . '-' . $counter;
$counter++;
}
// Generar código de barras
$countryCode = '57';
$categoryId = $producto->categoria_id ?? 00;
$productId = $producto->id ?? 00000;
$variantId = ProductVariant::where('producto_id', $producto->id)->count() + 1;
$eanService = app(\App\Services\EAN13Service::class);
$barcode = $eanService->generateUniqueBarcode($countryCode, $categoryId, $productId, $variantId);
$variante->sku = $sku;
$variante->barcode = $barcode;
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('sku')
->columns([
Tables\Columns\TextColumn::make('sku')
->label('SKU')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('color.name')
->label('Color')
->badge()
->color('info'),
Tables\Columns\TextColumn::make('size.name')
->label('Talla')
->badge()
->color('warning'),
Tables\Columns\TextColumn::make('stock')
->label('Stock Directo')
->numeric()
->sortable()
->tooltip('Stock directo de la variante (sin considerar bodegas)'),
Tables\Columns\TextColumn::make('stock_efectivo')
->label('Stock Total')
->getStateUsing(function ($record) {
return $record->getStockEfectivo();
})
->badge()
->color(fn ($state) => $state > 0 ? 'success' : 'danger')
->tooltip('Stock total considerando todas las bodegas'),
Tables\Columns\TextColumn::make('bodegas_info')
->label('Distribución en Bodegas')
->getStateUsing(function ($record) {
$bodegas = $record->bodegas()->get();
if ($bodegas->count() === 0) {
return 'Sin asignar a bodegas';
}
$distribucion = $bodegas->map(function ($bodega) {
return "{$bodega->nombre}: {$bodega->pivot->stock}";
})->join(' | ');
return $distribucion;
})
->wrap()
->tooltip('Distribución de stock por bodega'),
Tables\Columns\TextColumn::make('barcode')
->label('Código de Barras')
->action(fn($record) => redirect()->route('imprimir.barcode', ['barcode' => $record->barcode]))
->tooltip('Click para imprimir código QR')
->copyable()
->copyMessage('Código copiado')
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Tables\Filters\SelectFilter::make('color_id')
->label('Color')
->relationship('color', 'name')
->searchable(),
Tables\Filters\SelectFilter::make('size_id')
->label('Talla')
->relationship('size', 'name')
->searchable(),
Tables\Filters\Filter::make('con_stock')
->label('Con Stock')
->query(fn (Builder $query): Builder =>
$query->where('stock', '>', 0)
->orWhereHas('bodegas', function ($q) {
$q->where('variante_bodega.stock', '>', 0);
})
),
Tables\Filters\Filter::make('sin_stock')
->label('Sin Stock')
->query(fn (Builder $query): Builder =>
$query->where('stock', '=', 0)
->whereDoesntHave('bodegas', function ($q) {
$q->where('variante_bodega.stock', '>', 0);
})
),
Tables\Filters\SelectFilter::make('bodega_id')
->label('En Bodega')
->options(Bodega::pluck('nombre', 'id'))
->query(function (Builder $query, array $data): Builder {
return $query->when(
$data['value'],
fn (Builder $query, $value): Builder => $query->whereHas('bodegas', function ($q) use ($value) {
$q->where('bodegas.id', $value);
})
);
}),
])
->headerActions([
Tables\Actions\CreateAction::make()
->label('Crear Variante')
->modalHeading('Crear Nueva Variante')
->modalDescription('Complete la información de la variante y seleccione la bodega inicial')
->modalWidth('3xl')
->using(function (array $data, string $model): ProductVariant {
// Extraer bodega_id de los datos antes de crear la variante
$bodegaId = $data['bodega_id'];
$stockInicial = $data['stock'];
// Remover bodega_id de los datos para evitar errores en la creación
unset($data['bodega_id']);
// Crear la variante
$variante = $this->getOwnerRecord()->variants()->create($data);
// Asignar a la bodega seleccionada
if ($bodegaId && $stockInicial > 0) {
$variante->bodegas()->attach($bodegaId, ['stock' => $stockInicial]);
$bodegaNombre = Bodega::find($bodegaId)->nombre;
Notification::make()
->success()
->title('Variante creada exitosamente')
->body("La variante {$variante->sku} se creó con {$stockInicial} unidades en la bodega {$bodegaNombre}")
->send();
} else if ($bodegaId) {
// Si no hay stock inicial pero se seleccionó bodega, crear la relación con stock 0
$variante->bodegas()->attach($bodegaId, ['stock' => 0]);
$bodegaNombre = Bodega::find($bodegaId)->nombre;
Notification::make()
->success()
->title('Variante creada')
->body("La variante {$variante->sku} se creó y asignó a la bodega {$bodegaNombre}")
->send();
}
return $variante;
}),
])
->actions([
Tables\Actions\EditAction::make()
->label('Editar')
->modalWidth('3xl'),
Tables\Actions\Action::make('gestionar_bodegas')
->label('Gestionar Bodegas')
->icon('heroicon-o-building-storefront')
->color('info')
->url(fn (ProductVariant $record): string =>
"/admin/product-variants/{$record->id}/edit"
)
->openUrlInNewTab()
->tooltip('Abrir gestión completa de bodegas para esta variante'),
Tables\Actions\Action::make('duplicar_variante')
->label('Duplicar')
->icon('heroicon-o-document-duplicate')
->color('warning')
->form([
Forms\Components\Select::make('color_id')
->label('Nuevo Color')
->relationship('color', 'name')
->required(),
Forms\Components\Select::make('size_id')
->label('Nueva Talla')
->relationship('size', 'name')
->required(),
Forms\Components\TextInput::make('stock_inicial')
->label('Stock Inicial')
->numeric()
->default(0),
Forms\Components\Select::make('bodega_id')
->label('Bodega Inicial')
->options(Bodega::pluck('nombre', 'id'))
->required(),
])
->action(function (ProductVariant $record, array $data): void {
// Verificar que no exista la combinación
$exists = ProductVariant::where('producto_id', $record->producto_id)
->where('color_id', $data['color_id'])
->where('size_id', $data['size_id'])
->exists();
if ($exists) {
Notification::make()
->danger()
->title('Error')
->body('Ya existe una variante con esa combinación de color y talla')
->send();
return;
}
// Crear nueva variante
$newVariant = $record->replicate([
'sku', 'barcode', 'color_id', 'size_id'
]);
$newVariant->color_id = $data['color_id'];
$newVariant->size_id = $data['size_id'];
$newVariant->stock = $data['stock_inicial'];
// Generar SKU y código de barras únicos
$this->generateUniqueSkuAndBarcode($newVariant);
$newVariant->save();
// Asignar a bodega
if ($data['bodega_id'] && $data['stock_inicial'] > 0) {
$newVariant->bodegas()->attach($data['bodega_id'], ['stock' => $data['stock_inicial']]);
}
Notification::make()
->success()
->title('Variante duplicada')
->body("Nueva variante {$newVariant->sku} creada exitosamente")
->send();
}),
Tables\Actions\DeleteAction::make()
->label('Eliminar'),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}