up
This commit is contained in:
@@ -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'),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
+411
@@ -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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user