659 lines
33 KiB
PHP
659 lines
33 KiB
PHP
<?php
|
||
|
||
namespace App\Filament\Resources;
|
||
|
||
use App\Filament\Resources\ProductoResource\Pages;
|
||
use App\Filament\Resources\ProductoResource\RelationManagers;
|
||
use App\Models\Producto;
|
||
use Filament\Forms;
|
||
use Filament\Forms\Form;
|
||
use Filament\Resources\Resource;
|
||
use Filament\Tables;
|
||
use Filament\Tables\Table;
|
||
use Filament\Tables\Actions\DeleteAction;
|
||
use Illuminate\Database\Eloquent\Builder;
|
||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||
use Filament\Tables\Filters\SelectFilter;
|
||
use App\Filament\Resources\ProductoResource\RelationManagers\VariantesRelationManager;
|
||
use App\Filament\Resources\ProductoResource\RelationManagers\BodegasRelationManager;
|
||
use Filament\Forms\Components\TextInput;
|
||
use Filament\Forms\Components\Actions\Action;
|
||
use App\Models\ProductVariant;
|
||
use Illuminate\Validation\Rule;
|
||
use Filament\Notifications\Notification;
|
||
use App\Exports\ProductosExport;
|
||
use App\Imports\ProductosImport;
|
||
use Maatwebsite\Excel\Facades\Excel;
|
||
use Filament\Forms\Components\FileUpload;
|
||
|
||
|
||
|
||
|
||
class ProductoResource extends Resource
|
||
{
|
||
|
||
protected static ?string $navigationGroup = 'Inventario'; //
|
||
|
||
protected static ?string $model = Producto::class;
|
||
protected static string $relationship = 'variantes';
|
||
|
||
protected static ?string $navigationIcon = 'heroicon-o-cube';
|
||
|
||
public static function canViewAny(): bool
|
||
{
|
||
return auth()->user()->can('ver productos');
|
||
}
|
||
|
||
|
||
public static function form(Form $form): Form
|
||
{
|
||
return $form
|
||
->schema([
|
||
Forms\Components\TextInput::make('nombre')
|
||
->required()
|
||
->maxLength(100),
|
||
// Campo para subir imagen
|
||
Forms\Components\FileUpload::make('imagen')
|
||
->label('Imagen del Producto')
|
||
->image()
|
||
->directory(directory: 'productos')
|
||
->maxSize(10240)
|
||
->acceptedFileTypes(['image/jpeg', 'image/png', 'image/gif', 'image/webp']),
|
||
Forms\Components\Textarea::make('descripcion')
|
||
->columnSpanFull()
|
||
->required()
|
||
->placeholder('Ingrese una descripción del producto')
|
||
->default(''),
|
||
|
||
TextInput::make('precio_compra')
|
||
->required()
|
||
->numeric()
|
||
->maxLength(255)
|
||
->placeholder('Ej: 1000')
|
||
->lazy(), // Cambiar de reactive() a lazy()
|
||
|
||
TextInput::make('precio_venta')
|
||
->required()
|
||
->numeric()
|
||
->maxLength(255)
|
||
->lazy() // Cambiar de reactive() a lazy()
|
||
->placeholder('Ej: 1200')
|
||
->afterStateUpdated(function (callable $get, callable $set) {
|
||
$precioCompra = $get('precio_compra');
|
||
$precioVenta = $get('precio_venta');
|
||
|
||
// Solo validar si ambos valores están presentes y son significativos
|
||
if (!is_null($precioCompra) && !is_null($precioVenta) &&
|
||
$precioCompra > 0 && $precioVenta > 0 &&
|
||
$precioVenta < $precioCompra) {
|
||
Notification::make()
|
||
->warning()
|
||
->title('Advertencia')
|
||
->body('El precio de venta es menor al precio de compra.')
|
||
->persistent()
|
||
->send();
|
||
}
|
||
}),
|
||
|
||
Forms\Components\TextInput::make('stock')
|
||
->required()
|
||
->numeric()
|
||
->default(0)
|
||
->lazy() // Cambiar de live() a lazy()
|
||
->dehydrated()
|
||
->hidden(fn($get, $record) => $record && $record->variants()->exists()) // Oculta si hay variantes
|
||
->disabled(fn($get) => $get('producto_id') && \App\Models\Producto::find($get('producto_id'))->variants()->exists())
|
||
->visible(false) // Oculto - se maneja automáticamente
|
||
->label('Stock (Unidades Base)'),
|
||
|
||
Forms\Components\TextInput::make('stock_entrada')
|
||
->label('Stock Inicial')
|
||
->numeric()
|
||
->default(0)
|
||
->lazy() // Cambiar de live() a lazy() para evitar actualizaciones en tiempo real
|
||
->dehydrated(true) // Permitir que se envíe al servidor
|
||
->hidden(fn($get, $record) => $record && $record->variants()->exists())
|
||
->helperText(fn($get, $record) => $record && $record->variants()->exists()
|
||
? 'El stock se maneja a nivel de variantes'
|
||
: 'Ingrese el stock inicial del producto')
|
||
->afterStateUpdated(function (callable $get, callable $set) {
|
||
$stockEntrada = $get('stock_entrada') ?? 0;
|
||
$unidadMedida = $get('unidad_medida') ?? 'unidad';
|
||
// Asegurar que la cantidad sea float (Filament a veces pasa strings desde inputs)
|
||
$stockEntradaFloat = is_numeric($stockEntrada) ? (float) $stockEntrada : 0.0;
|
||
$stockUnidades = Producto::convertirAUnidades($stockEntradaFloat, $unidadMedida);
|
||
$set('stock', $stockUnidades);
|
||
})
|
||
->hint(function (callable $get) {
|
||
$unidadMedida = $get('unidad_medida') ?? 'unidad';
|
||
$factor = Producto::getFactorConversion($unidadMedida);
|
||
return $factor > 1 ? "Se convertirá automáticamente (×{$factor})" : null;
|
||
}),
|
||
|
||
Forms\Components\TextInput::make('stock_minimo')
|
||
->required()
|
||
->numeric()
|
||
->default(0)
|
||
->lazy() // Cambiar de live() a lazy()
|
||
->dehydrated()
|
||
->visible(false) // Oculto - se maneja automáticamente
|
||
->label('Stock Mínimo (Unidades Base)')
|
||
->afterStateUpdated(function (callable $get, callable $set) {
|
||
$stockMinimo = $get('stock_minimo');
|
||
$stockMaximo = $get('stock_maximo');
|
||
|
||
// Solo validar si hay valores significativos
|
||
if (!is_null($stockMinimo) && !is_null($stockMaximo) &&
|
||
$stockMinimo > 0 && $stockMaximo > 0 &&
|
||
$stockMaximo < $stockMinimo) {
|
||
Notification::make()
|
||
->warning()
|
||
->title('Advertencia')
|
||
->body('El stock máximo debe ser mayor al stock mínimo.')
|
||
->persistent()
|
||
->send();
|
||
}
|
||
}),
|
||
|
||
Forms\Components\TextInput::make('stock_minimo_entrada')
|
||
->label('Stock Mínimo')
|
||
->numeric()
|
||
->default(0)
|
||
->lazy() // Cambiar de live() a lazy() para evitar actualizaciones en tiempo real
|
||
->dehydrated(true) // Permitir que se envíe al servidor
|
||
->hidden(fn($get, $record) => $record && $record->variants()->exists())
|
||
->helperText(fn($get, $record) => $record && $record->variants()->exists()
|
||
? 'El stock mínimo se configura por variante'
|
||
: 'Configure el stock mínimo para alertas')
|
||
->afterStateUpdated(function (callable $get, callable $set) {
|
||
$stockMinimoEntrada = $get('stock_minimo_entrada') ?? 0;
|
||
$unidadMedida = $get('unidad_medida') ?? 'unidad';
|
||
$stockMinimoEntradaFloat = is_numeric($stockMinimoEntrada) ? (float) $stockMinimoEntrada : 0.0;
|
||
$stockMinimoUnidades = Producto::convertirAUnidades($stockMinimoEntradaFloat, $unidadMedida);
|
||
$set('stock_minimo', $stockMinimoUnidades);
|
||
|
||
// Solo validar si hay un valor significativo para evitar notificaciones innecesarias
|
||
if ($stockMinimoEntradaFloat > 0) {
|
||
$stockMaximo = $get('stock_maximo');
|
||
if (!is_null($stockMaximo) && $stockMaximo < $stockMinimoUnidades) {
|
||
Notification::make()
|
||
->warning()
|
||
->title('Advertencia')
|
||
->body('El stock máximo debe ser mayor al stock mínimo.')
|
||
->persistent()
|
||
->send();
|
||
}
|
||
}
|
||
})
|
||
->hint(function (callable $get) {
|
||
$unidadMedida = $get('unidad_medida') ?? 'unidad';
|
||
$factor = Producto::getFactorConversion($unidadMedida);
|
||
return $factor > 1 ? "Se convertirá automáticamente (×{$factor})" : null;
|
||
}),
|
||
|
||
Forms\Components\TextInput::make('stock_maximo')
|
||
->required()
|
||
->numeric()
|
||
->default(0)
|
||
->lazy() // Cambiar de live() a lazy()
|
||
->dehydrated()
|
||
->visible(false) // Oculto - se maneja automáticamente
|
||
->label('Stock Máximo (Unidades Base)')
|
||
->afterStateUpdated(function (callable $get, callable $set) {
|
||
$stockMinimo = $get('stock_minimo');
|
||
$stockMaximo = $get('stock_maximo');
|
||
|
||
// Solo validar si hay valores significativos
|
||
if (!is_null($stockMinimo) && !is_null($stockMaximo) &&
|
||
$stockMinimo > 0 && $stockMaximo > 0 &&
|
||
$stockMaximo < $stockMinimo) {
|
||
Notification::make()
|
||
->warning()
|
||
->title('Advertencia')
|
||
->body('El stock máximo debe ser mayor al stock mínimo.')
|
||
->persistent()
|
||
->send();
|
||
}
|
||
}),
|
||
|
||
Forms\Components\TextInput::make('stock_maximo_entrada')
|
||
->label('Stock Máximo')
|
||
->numeric()
|
||
->default(function (callable $get) {
|
||
$unidadMedida = $get('unidad_medida') ?? 'unidad';
|
||
return Producto::convertirDesdeUnidades(1000, $unidadMedida);
|
||
})
|
||
->lazy() // Cambiar de live() a lazy() para evitar actualizaciones en tiempo real
|
||
->dehydrated(true) // Permitir que se envíe al servidor
|
||
->hidden(fn($get, $record) => $record && $record->variants()->exists())
|
||
->helperText(fn($get, $record) => $record && $record->variants()->exists()
|
||
? 'El stock máximo se configura por variante'
|
||
: 'Configure el stock máximo recomendado')
|
||
->afterStateUpdated(function (callable $get, callable $set) {
|
||
$stockMaximoEntrada = $get('stock_maximo_entrada') ?? 0;
|
||
$unidadMedida = $get('unidad_medida') ?? 'unidad';
|
||
$stockMaximoEntradaFloat = is_numeric($stockMaximoEntrada) ? (float) $stockMaximoEntrada : 0.0;
|
||
$stockMaximoUnidades = Producto::convertirAUnidades($stockMaximoEntradaFloat, $unidadMedida);
|
||
$set('stock_maximo', $stockMaximoUnidades);
|
||
|
||
// Solo validar si hay un valor significativo para evitar notificaciones innecesarias
|
||
if ($stockMaximoEntradaFloat > 0) {
|
||
$stockMinimo = $get('stock_minimo');
|
||
if (!is_null($stockMinimo) && $stockMaximoUnidades < $stockMinimo) {
|
||
Notification::make()
|
||
->warning()
|
||
->title('Advertencia')
|
||
->body('El stock máximo debe ser mayor al stock mínimo.')
|
||
->persistent()
|
||
->send();
|
||
}
|
||
}
|
||
})
|
||
->hint(function (callable $get) {
|
||
$unidadMedida = $get('unidad_medida') ?? 'unidad';
|
||
$factor = Producto::getFactorConversion($unidadMedida);
|
||
return $factor > 1 ? "Se convertirá automáticamente (×{$factor})" : null;
|
||
}),
|
||
|
||
Forms\Components\Select::make('unidad_medida')
|
||
->label('Unidad de Medida')
|
||
->options(Producto::getUnidadesMedida())
|
||
->default('unidad')
|
||
->required()
|
||
->live()
|
||
->helperText('El sistema guardará todo en unidades individuales. Esta opción facilita la entrada de datos.')
|
||
->afterStateUpdated(function (callable $get, callable $set) {
|
||
$unidad = $get('unidad_medida');
|
||
$factor = Producto::getFactorConversion($unidad);
|
||
|
||
// Actualizar los campos auxiliares basados en los valores actuales en unidades
|
||
$stockActual = $get('stock') ?? 0;
|
||
$stockMinimo = $get('stock_minimo') ?? 0;
|
||
$stockMaximo = $get('stock_maximo') ?? 1000;
|
||
|
||
$set('stock_entrada', Producto::convertirDesdeUnidades($stockActual, $unidad));
|
||
$set('stock_minimo_entrada', Producto::convertirDesdeUnidades($stockMinimo, $unidad));
|
||
$set('stock_maximo_entrada', Producto::convertirDesdeUnidades($stockMaximo, $unidad));
|
||
|
||
if ($factor > 1) {
|
||
Notification::make()
|
||
->info()
|
||
->title('Conversión de Unidades')
|
||
->body("1 {$unidad} = {$factor} unidades individuales")
|
||
->send();
|
||
}
|
||
}),
|
||
|
||
Forms\Components\Select::make('categoria_id')
|
||
->relationship('categoria', 'nombre')
|
||
->searchable()
|
||
->required()
|
||
->lazy() // Cambiar de reactive() a lazy()
|
||
->afterStateUpdated(fn($set, $get) => ProductoResource::updateSkuAndBarcode($set, $get)),
|
||
|
||
TextInput::make('codigo_barras')
|
||
->label('Código de Barras')
|
||
->length(13)
|
||
->lazy() // Cambiar de live() a lazy()
|
||
->dehydrated()
|
||
->hidden(fn($get, $record) => $record && $record->variants()->exists()) // Oculta si hay variantes
|
||
->disabled(fn($get) => $get('producto_id') && \App\Models\Producto::find($get('producto_id'))->variants()->exists())
|
||
->suffixAction(
|
||
Action::make('imprimirQr')
|
||
->icon('heroicon-o-printer')
|
||
->color('primary')
|
||
->hidden(fn($record) => is_null($record)) // Oculta el botón si no hay un registro
|
||
->disabled(fn($get) => empty($get('codigo_barras'))) // Deshabilita si no hay código de barras
|
||
->action(fn($state) => redirect()->route('imprimir.barcode', ['barcode' => $state])) // Redirige a la impresión
|
||
->visible(fn($get, $record) => $record && !$record->variants()->exists()) // Muestra si el producto no tiene variantes
|
||
),
|
||
|
||
Forms\Components\Toggle::make('estado')
|
||
->label('Estado')
|
||
->default(true)
|
||
->required(),
|
||
|
||
Forms\Components\Hidden::make('producto_id')
|
||
->default(fn($record) => $record->id ?? null),
|
||
Forms\Components\Hidden::make('categoria_id')
|
||
->default(fn($record) => $record->categoria_id ?? null),
|
||
|
||
]);
|
||
}
|
||
|
||
protected static function updateSkuAndBarcode($set, $get)
|
||
{
|
||
$countryCode = '57'; // Código de país
|
||
$categoryId = $get('categoria_id') ?? '00';
|
||
|
||
// Obtener el siguiente ID de producto
|
||
$nextProductId = Producto::max('id') + 1;
|
||
|
||
$variantId = '00'; // Si no hay variante, usamos 00
|
||
|
||
// Generar código de barras estructurado
|
||
$eanService = app(\App\Services\EAN13Service::class);
|
||
$barcode = $eanService->generateUniqueBarcode($countryCode, $categoryId, $nextProductId, $variantId);
|
||
|
||
// Asignar el código de barras al formulario
|
||
$set('codigo_barras', $barcode);
|
||
}
|
||
|
||
|
||
public static function table(Table $table): Table
|
||
{
|
||
return $table
|
||
->columns([
|
||
Tables\Columns\TextColumn::make('nombre')
|
||
->searchable(),
|
||
Tables\Columns\TextColumn::make('codigo_barras')
|
||
->formatStateUsing(fn($record) => $record->variants()->exists() ? 'Tiene variantes' : $record->codigo_barras)
|
||
->action(fn($record) => !$record->variants()->exists() ? redirect()->route('imprimir.barcode', ['barcode' => $record->codigo_barras]) : null)
|
||
->tooltip(fn($record) => $record->variants()->exists() ? 'Este producto tiene variantes que contienen los Barcodes' : 'Imprimir código de barras'),
|
||
Tables\Columns\TextColumn::make('precio_compra')
|
||
->searchable(),
|
||
Tables\Columns\TextColumn::make('precio_venta')
|
||
->searchable(),
|
||
Tables\Columns\TextColumn::make('stock_total')
|
||
->label('Stock')
|
||
->getStateUsing(function ($record, \Livewire\Component $livewire) {
|
||
$filterState = $livewire->tableFilters ?? [];
|
||
$bodegaId = $filterState['bodegas']['value'] ?? null;
|
||
|
||
if ($bodegaId) {
|
||
return $record->getStockTotalEnBodega($bodegaId);
|
||
}
|
||
|
||
// Usar el método getStockEfectivo que considera bodegas, variantes y stock directo
|
||
return $record->getStockEfectivo();
|
||
})
|
||
->formatStateUsing(function ($state, $record) {
|
||
$totalStockUnidades = $state;
|
||
$ok = $totalStockUnidades >= $record->stock_minimo;
|
||
$icon = $ok ? '✅' : '⚠️';
|
||
|
||
// Mostrar stock en la unidad de medida configurada
|
||
$unidadMedida = $record->unidad_medida ?? 'unidad';
|
||
$stockEnUnidad = Producto::convertirDesdeUnidades($totalStockUnidades, $unidadMedida);
|
||
$nombreUnidad = Producto::getUnidadesMedida()[$unidadMedida] ?? 'Unidad (1)';
|
||
|
||
return "{$icon} {$stockEnUnidad} " . explode(' ', $nombreUnidad)[0];
|
||
})
|
||
->tooltip(function ($record, \Livewire\Component $livewire) {
|
||
$filterState = $livewire->tableFilters ?? [];
|
||
$bodegaId = $filterState['bodegas']['value'] ?? null;
|
||
|
||
if ($bodegaId) {
|
||
$totalStockUnidades = $record->getStockTotalEnBodega($bodegaId);
|
||
$bodegaNombre = \App\Models\Bodega::find($bodegaId)?->nombre ?? 'Bodega seleccionada';
|
||
$origen = "\nEn {$bodegaNombre}";
|
||
} else {
|
||
$totalStockUnidades = $record->getStockEfectivo();
|
||
|
||
$origen = '';
|
||
if ($record->bodegas()->exists()) {
|
||
$origen = "\nDistribuido en " . $record->bodegas()->count() . " bodega(s)";
|
||
} elseif ($record->variants()->exists()) {
|
||
$origen = "\nStock de variantes";
|
||
} else {
|
||
$origen = "\nStock directo";
|
||
}
|
||
}
|
||
|
||
$unidadMedida = $record->unidad_medida ?? 'unidad';
|
||
$stockEnUnidad = Producto::convertirDesdeUnidades($totalStockUnidades, $unidadMedida);
|
||
$nombreUnidad = Producto::getUnidadesMedida()[$unidadMedida] ?? 'Unidad (1)';
|
||
|
||
$mensaje = $totalStockUnidades < $record->stock_minimo
|
||
? "Stock por debajo del mínimo"
|
||
: "Stock suficiente";
|
||
|
||
return "{$mensaje}\nStock en {$nombreUnidad}: {$stockEnUnidad}\nStock en unidades: {$totalStockUnidades}{$origen}";
|
||
}),
|
||
|
||
|
||
Tables\Columns\TextColumn::make('stock_minimo')
|
||
->numeric()
|
||
->sortable(),
|
||
Tables\Columns\TextColumn::make('unidad_medida')
|
||
->label('Unidad')
|
||
->formatStateUsing(function ($state) {
|
||
$unidades = Producto::getUnidadesMedida();
|
||
return explode(' ', $unidades[$state ?? 'unidad'])[0];
|
||
})
|
||
->tooltip(function ($record) {
|
||
$unidades = Producto::getUnidadesMedida();
|
||
return $unidades[$record->unidad_medida ?? 'unidad'];
|
||
}),
|
||
Tables\Columns\TextColumn::make('categoria.nombre')
|
||
->searchable()
|
||
->sortable(),
|
||
Tables\Columns\IconColumn::make('estado')
|
||
->boolean(),
|
||
Tables\Columns\TextColumn::make('created_at')
|
||
->dateTime()
|
||
->sortable()
|
||
->toggleable(isToggledHiddenByDefault: true),
|
||
Tables\Columns\TextColumn::make('updated_at')
|
||
->dateTime()
|
||
->sortable()
|
||
->toggleable(isToggledHiddenByDefault: true),
|
||
])
|
||
->paginated(true)
|
||
//->paginationPageOptions([50])
|
||
->defaultSort('created_at', 'desc')
|
||
->filters([
|
||
SelectFilter::make('categoria_id')
|
||
->label('Categoría')
|
||
->relationship('categoria', 'nombre') // Relación con el modelo Categoría
|
||
->preload() // Precargar opciones
|
||
->searchable(), // Permitir búsqueda en el filtro
|
||
SelectFilter::make('bodegas')
|
||
->label('Bodega')
|
||
->relationship('bodegas', 'nombre')
|
||
->preload()
|
||
->searchable(),
|
||
])
|
||
->actions([
|
||
Tables\Actions\EditAction::make(),
|
||
|
||
Tables\Actions\DeleteAction::make()
|
||
->before(function (Producto $record) {
|
||
// Verificar si el producto está siendo usado en compras
|
||
$comprasCount = $record->detalleCompras()->count();
|
||
$ventasCount = $record->detalleVentas()->count();
|
||
|
||
if ($comprasCount > 0 || $ventasCount > 0) {
|
||
Notification::make()
|
||
->warning()
|
||
->title('No se puede eliminar')
|
||
->body("Este producto está siendo usado en {$comprasCount} compras y {$ventasCount} ventas. Los registros se mantendrán con información histórica.")
|
||
->persistent()
|
||
->send();
|
||
}
|
||
}),
|
||
|
||
Tables\Actions\Action::make('transferir')
|
||
->label('Transferir Stock')
|
||
->icon('heroicon-o-arrow-path')
|
||
->color('warning')
|
||
->form([
|
||
Forms\Components\Select::make('bodega_origen_id')
|
||
->label('Bodega Origen')
|
||
->options(function (Producto $record) {
|
||
return $record->bodegas()
|
||
->wherePivot('stock', '>', 0)
|
||
->pluck('nombre', 'bodegas.id');
|
||
})
|
||
->required()
|
||
->reactive()
|
||
->helperText(function (callable $get, Producto $record) {
|
||
$bodegaId = $get('bodega_origen_id');
|
||
if (!$bodegaId) return null;
|
||
|
||
$bodega = $record->bodegas()->where('bodega_id', $bodegaId)->first();
|
||
$stock = $bodega ? $bodega->pivot->stock : 0;
|
||
|
||
return "Stock disponible: {$stock} unidades";
|
||
}),
|
||
|
||
Forms\Components\Select::make('bodega_destino_id')
|
||
->label('Bodega Destino')
|
||
->options(function (callable $get) {
|
||
$bodegaOrigenId = $get('bodega_origen_id');
|
||
$bodegas = \App\Models\Bodega::all()->pluck('nombre', 'id');
|
||
|
||
if ($bodegaOrigenId) {
|
||
$bodegas = $bodegas->except($bodegaOrigenId);
|
||
}
|
||
|
||
return $bodegas;
|
||
})
|
||
->required(),
|
||
|
||
Forms\Components\TextInput::make('cantidad')
|
||
->label('Cantidad')
|
||
->numeric()
|
||
->required()
|
||
->minValue(1)
|
||
->maxValue(function (callable $get, Producto $record) {
|
||
$bodegaId = $get('bodega_origen_id');
|
||
if (!$bodegaId) return 999999;
|
||
|
||
$bodega = $record->bodegas()->where('bodega_id', $bodegaId)->first();
|
||
return $bodega ? $bodega->pivot->stock : 0;
|
||
}),
|
||
|
||
Forms\Components\Textarea::make('motivo')
|
||
->label('Motivo')
|
||
->placeholder('Opcional')
|
||
->rows(2),
|
||
])
|
||
->action(function (Producto $record, array $data) {
|
||
try {
|
||
$service = new \App\Services\TransferenciaBodegaService();
|
||
|
||
$service->transferir(
|
||
$record->id,
|
||
$data['bodega_origen_id'],
|
||
$data['bodega_destino_id'],
|
||
$data['cantidad'],
|
||
$data['motivo'] ?? null
|
||
);
|
||
|
||
Notification::make()
|
||
->title('Transferencia realizada')
|
||
->body("Se transfirieron {$data['cantidad']} unidades de {$record->nombre}")
|
||
->success()
|
||
->send();
|
||
|
||
} catch (\Exception $e) {
|
||
Notification::make()
|
||
->title('Error en transferencia')
|
||
->body($e->getMessage())
|
||
->danger()
|
||
->send();
|
||
}
|
||
})
|
||
->visible(fn (Producto $record) => $record->bodegas()->wherePivot('stock', '>', 0)->exists()),
|
||
])
|
||
->headerActions([
|
||
Tables\Actions\Action::make('exportar')
|
||
->label('Exportar a Excel')
|
||
->icon('heroicon-o-arrow-down-tray')
|
||
->color('success')
|
||
->action(function () {
|
||
return Excel::download(new ProductosExport, 'productos_' . date('Y-m-d_H-i-s') . '.xlsx');
|
||
}),
|
||
|
||
Tables\Actions\Action::make('importar')
|
||
->label('Importar desde Excel')
|
||
->icon('heroicon-o-arrow-up-tray')
|
||
->color('primary')
|
||
->form([
|
||
FileUpload::make('archivo')
|
||
->label('Archivo Excel')
|
||
->acceptedFileTypes([
|
||
'application/vnd.ms-excel',
|
||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
'text/csv',
|
||
])
|
||
->required()
|
||
->helperText('Formatos aceptados: .xlsx, .xls, .csv')
|
||
->disk('local')
|
||
->directory('imports'),
|
||
])
|
||
->action(function (array $data) {
|
||
try {
|
||
$import = new ProductosImport;
|
||
Excel::import($import, $data['archivo']);
|
||
|
||
$failures = $import->failures();
|
||
$errors = $import->errors();
|
||
|
||
if ($failures->isNotEmpty() || $errors->isNotEmpty()) {
|
||
$errorMessages = [];
|
||
|
||
foreach ($failures as $failure) {
|
||
$errorMessages[] = "Fila {$failure->row()}: " . implode(', ', $failure->errors());
|
||
}
|
||
|
||
foreach ($errors as $error) {
|
||
$errorMessages[] = $error->getMessage();
|
||
}
|
||
|
||
Notification::make()
|
||
->warning()
|
||
->title('Importación completada con errores')
|
||
->body('Algunos productos no se pudieron importar: ' . implode(' | ', array_slice($errorMessages, 0, 3)))
|
||
->persistent()
|
||
->send();
|
||
} else {
|
||
Notification::make()
|
||
->success()
|
||
->title('Importación exitosa')
|
||
->body('Los productos se importaron correctamente.')
|
||
->send();
|
||
}
|
||
|
||
} catch (\Exception $e) {
|
||
Notification::make()
|
||
->danger()
|
||
->title('Error en la importación')
|
||
->body('Ocurrió un error: ' . $e->getMessage())
|
||
->persistent()
|
||
->send();
|
||
}
|
||
}),
|
||
])
|
||
->bulkActions([
|
||
Tables\Actions\BulkActionGroup::make([
|
||
Tables\Actions\DeleteBulkAction::make(),
|
||
]),
|
||
]);
|
||
}
|
||
|
||
|
||
public static function getRelations(): array
|
||
{
|
||
return [
|
||
VariantesRelationManager::class,
|
||
BodegasRelationManager::class,
|
||
];
|
||
}
|
||
|
||
public static function getPages(): array
|
||
{
|
||
return [
|
||
'index' => Pages\ListProductos::route('/'),
|
||
'create' => Pages\CreateProducto::route('/create'),
|
||
'edit' => Pages\EditProducto::route('/{record}/edit'),
|
||
];
|
||
}
|
||
|
||
public static function getNavigationBadgeColor(): ?string
|
||
{
|
||
return 'success'; // Verde para productos
|
||
}
|
||
}
|