diff --git a/app/Http/Controllers/BLInventarioController.php b/app/Http/Controllers/BLInventarioController.php index 70ea462..e33f700 100644 --- a/app/Http/Controllers/BLInventarioController.php +++ b/app/Http/Controllers/BLInventarioController.php @@ -24,21 +24,44 @@ public function index() { $productos = BlProducto::with([ 'color', - 'empaques.inventarioDetalle.posicion.nivel.estanteria' + 'empaques.inventarioDetalle.zona.nivel.estanteria' ]) ->get() ->map(function ($producto) { // Obtener todas las ubicaciones donde está este producto $ubicaciones = []; + $zonasUnicas = []; + $estanteriasUnicas = []; // NUEVO: para almacenar códigos de estanterías + foreach ($producto->empaques as $empaque) { foreach ($empaque->inventarioDetalle as $inventario) { - if ($inventario->posicion && $inventario->posicion->nivel && $inventario->posicion->nivel->estanteria) { - $ubicaciones[] = $inventario->posicion->nivel->estanteria->nombre; + if ($inventario->zona && $inventario->zona->nivel && $inventario->zona->nivel->estanteria) { + $estanteria = $inventario->zona->nivel->estanteria; + $nivel = $inventario->zona->nivel; + $zona = $inventario->zona; + + // Crear ubicación legible + $ubicacionLegible = $estanteria->nombre . ' - ' . + $nivel->nivel . ' - ' . + 'Zona ' . $zona->zona; + + $ubicaciones[] = $ubicacionLegible; + + // Guardar información de zona única + $zonasUnicas[] = [ + 'estanteria_nombre' => $estanteria->nombre, + 'estanteria_codigo' => $estanteria->codigo, // EST-04, RACK-04, etc. + 'nivel_nombre' => $nivel->nivel, + 'zona_nombre' => $zona->zona, + 'codigo_completo' => $zona->codigo_completo + ]; + + // NUEVO: Guardar código de estantería para el plano + $estanteriasUnicas[] = $estanteria->codigo; } } } - // Si no tiene ubicación, mostrar "Sin ubicación" $estanteria = !empty($ubicaciones) ? implode(', ', array_unique($ubicaciones)) : 'Sin ubicación'; return [ @@ -49,9 +72,14 @@ public function index() 'descripcion' => $producto->descripcion, 'stock_total' => $producto->empaques ->where('estado', 'disponible') - ->sum('cantidad_unidades'), + ->sum('cantidad_por_empaque'), 'estanteria' => $estanteria, 'tiene_ubicacion' => !empty($ubicaciones), + 'ubicaciones_detalladas' => $zonasUnicas, + 'estanterias' => array_unique(array_column($zonasUnicas, 'estanteria_nombre')), + 'zonas_completas' => array_unique(array_column($zonasUnicas, 'codigo_completo')), + // NUEVO: Array de códigos de estantería para el plano + 'estanterias_codigos' => array_unique($estanteriasUnicas), ]; }); diff --git a/app/Models/BlInventarioDetalle.php b/app/Models/BlInventarioDetalle.php index 7be90aa..d32e460 100644 --- a/app/Models/BlInventarioDetalle.php +++ b/app/Models/BlInventarioDetalle.php @@ -11,8 +11,13 @@ class BlInventarioDetalle extends Model protected $table = 'bl_inventario_detalle'; protected $fillable = [ - 'empaque_id', 'posicion_id', 'cantidad_actual', - 'fecha_ubicacion', 'fecha_vencimiento', 'estado', 'notas' + 'empaque_id', + 'zona_id', // Cambiado: posicion_id → zona_id + 'cantidad_actual', + 'fecha_ubicacion', + 'fecha_vencimiento', + 'estado', + 'notas' ]; protected $dates = ['fecha_ubicacion', 'fecha_vencimiento']; @@ -23,10 +28,10 @@ public function empaque() return $this->belongsTo(BlEmpaque::class, 'empaque_id'); } - // Relación con posición - public function posicion() + // NUEVA RELACIÓN con zona (reemplaza posicion) + public function zona() { - return $this->belongsTo(BlPosicion::class, 'posicion_id'); + return $this->belongsTo(BlZonaNivel::class, 'zona_id'); } // Relación con producto (a través de empaque) @@ -42,7 +47,7 @@ public function producto() ); } - // Scopes útiles + // Scopes útiles ACTUALIZADOS public function scopeDisponibles($query) { return $query->where('estado', 'disponible'); @@ -50,15 +55,35 @@ public function scopeDisponibles($query) public function scopeEnEstanteria($query, $estanteriaId) { - return $query->whereHas('posicion.nivel.estanteria', function($q) use ($estanteriaId) { + return $query->whereHas('zona.nivel.estanteria', function($q) use ($estanteriaId) { $q->where('id', $estanteriaId); }); } + public function scopeEnZona($query, $zonaId) + { + return $query->where('zona_id', $zonaId); + } + public function scopeConProducto($query, $productoId) { return $query->whereHas('empaque', function($q) use ($productoId) { $q->where('producto_id', $productoId); }); } + + // Nuevo scope para buscar por código de zona + public function scopeEnZonaCodigo($query, $codigoZona) + { + return $query->whereHas('zona', function($q) use ($codigoZona) { + $q->where('codigo_completo', $codigoZona); + }); + } + + // Scope para productos próximos a vencer + public function scopeProximosAVencer($query, $dias = 30) + { + return $query->whereDate('fecha_vencimiento', '<=', now()->addDays($dias)) + ->where('estado', 'disponible'); + } } \ No newline at end of file diff --git a/app/Models/BlZonaNivel.php b/app/Models/BlZonaNivel.php new file mode 100644 index 0000000..56c3f01 --- /dev/null +++ b/app/Models/BlZonaNivel.php @@ -0,0 +1,65 @@ +belongsTo(BlNivelEstanteria::class, 'nivel_id'); + } + + // Relación con inventario detalle + public function inventarioDetalle() + { + return $this->hasMany(BlInventarioDetalle::class, 'zona_id'); + } + + // Relación con estantería (a través de nivel) + public function estanteria() + { + return $this->hasOneThrough( + BlEstanteria::class, + BlNivelEstanteria::class, + 'id', // Foreign key on bl_niveles_estanteria + 'id', // Foreign key on bl_estanterias + 'nivel_id', // Local key on bl_zonas_nivel + 'estanteria_id' // Local key on bl_niveles_estanteria + ); + } + + // Scope para zonas activas + public function scopeActivas($query) + { + return $query->where('activa', true); + } + + // Scope para zonas con capacidad disponible + public function scopeConCapacidad($query, $cantidad = 1) + { + return $query->whereRaw('(capacidad_maxima - productos_actuales) >= ?', [$cantidad]); + } + + // Método para calcular espacio disponible + public function getEspacioDisponibleAttribute() + { + return $this->capacidad_maxima - $this->productos_actuales; + } +} \ No newline at end of file diff --git a/database/migrations/2025_09_24_180852_bl_estanterias_table.php b/database/migrations/2025_09_24_180852_bl_estanterias_table.php index de7cc25..d7d6923 100644 --- a/database/migrations/2025_09_24_180852_bl_estanterias_table.php +++ b/database/migrations/2025_09_24_180852_bl_estanterias_table.php @@ -30,6 +30,6 @@ public function up(): void */ public function down(): void { - // + Schema::dropIfExists('bl_estanterias'); } }; diff --git a/database/migrations/2025_09_24_180942_bl_niveles_estanteria_table.php b/database/migrations/2025_09_24_180942_bl_niveles_estanteria_table.php index 2bde446..3f42702 100644 --- a/database/migrations/2025_09_24_180942_bl_niveles_estanteria_table.php +++ b/database/migrations/2025_09_24_180942_bl_niveles_estanteria_table.php @@ -30,6 +30,6 @@ public function up(): void */ public function down(): void { - // + Schema::dropIfExists('bl_niveles_estanteria'); } }; diff --git a/database/migrations/2025_09_24_181023_bl_posiciones_table.php b/database/migrations/2025_09_24_181023_bl_posiciones_table.php index e421490..0ee2c49 100644 --- a/database/migrations/2025_09_24_181023_bl_posiciones_table.php +++ b/database/migrations/2025_09_24_181023_bl_posiciones_table.php @@ -31,6 +31,6 @@ public function up(): void */ public function down(): void { - // + Schema::dropIfExists('bl_posiciones'); } }; diff --git a/database/migrations/2025_09_24_181042_bl_inventario_detalle_table.php b/database/migrations/2025_09_24_181042_bl_inventario_detalle_table.php index 4cdc5b3..69e359a 100644 --- a/database/migrations/2025_09_24_181042_bl_inventario_detalle_table.php +++ b/database/migrations/2025_09_24_181042_bl_inventario_detalle_table.php @@ -32,6 +32,6 @@ public function up(): void */ public function down(): void { - // + Schema::dropIfExists('bl_inventario_detalle'); } }; diff --git a/database/migrations/2025_09_28_152407_drop_bl_posiciones_table.php b/database/migrations/2025_09_28_152407_drop_bl_posiciones_table.php new file mode 100644 index 0000000..22de0f6 --- /dev/null +++ b/database/migrations/2025_09_28_152407_drop_bl_posiciones_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('nivel_id')->constrained('bl_niveles_estanteria'); + $table->string('posicion'); + $table->string('codigo_completo')->unique(); + $table->decimal('ancho', 8, 2)->nullable(); + $table->decimal('alto', 8, 2)->nullable(); + $table->decimal('profundidad', 8, 2)->nullable(); + $table->integer('capacidad_maxima')->default(1); + $table->boolean('ocupada')->default(false); + $table->boolean('activa')->default(true); + $table->timestamps(); + }); + } +}; diff --git a/database/migrations/2025_09_28_152633_bl_zonas_nivel_table.php b/database/migrations/2025_09_28_152633_bl_zonas_nivel_table.php new file mode 100644 index 0000000..cbf195a --- /dev/null +++ b/database/migrations/2025_09_28_152633_bl_zonas_nivel_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('nivel_id')->constrained('bl_niveles_estanteria'); + $table->string('zona'); // 'A', 'B' + $table->string('codigo_completo')->unique(); // Ej: 'RACK-01-B-A' + $table->integer('capacidad_maxima')->default(5000); // mitad de la capacidad del nivel + $table->integer('productos_actuales')->default(0); + $table->text('descripcion')->nullable(); // "Frente", "Fondo", etc. + $table->boolean('activa')->default(true); + $table->timestamps(); + + $table->unique(['nivel_id', 'zona']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('bl_zonas_nivel'); + } +}; diff --git a/database/migrations/2025_09_28_153005_change_posicion_to_zona_in_inventario_detalle.php b/database/migrations/2025_09_28_153005_change_posicion_to_zona_in_inventario_detalle.php new file mode 100644 index 0000000..447d164 --- /dev/null +++ b/database/migrations/2025_09_28_153005_change_posicion_to_zona_in_inventario_detalle.php @@ -0,0 +1,72 @@ +id(); + $table->foreignId('empaque_id')->constrained('bl_empaques'); + $table->foreignId('zona_id')->constrained('bl_zonas_nivel'); // NUEVO CAMPO + $table->integer('cantidad_actual')->default(0); + $table->date('fecha_ubicacion'); + $table->date('fecha_vencimiento')->nullable(); + $table->enum('estado', ['disponible', 'reservado', 'danado', 'caducado'])->default('disponible'); + $table->text('notas')->nullable(); + $table->timestamps(); + + $table->unique(['empaque_id', 'zona_id']); + $table->index(['zona_id', 'estado']); + $table->index('fecha_vencimiento'); + }); + + // 2️⃣ Copiar los datos desde la tabla antigua + // Aquí asumimos que existe un mapeo directo posicón_id → zona_id + // Si necesitas un mapeo distinto, ajusta el SELECT + DB::statement(' + INSERT INTO bl_inventario_detalle_new (id, empaque_id, zona_id, cantidad_actual, fecha_ubicacion, fecha_vencimiento, estado, notas, created_at, updated_at) + SELECT id, empaque_id, posicion_id, cantidad_actual, fecha_ubicacion, fecha_vencimiento, estado, notas, created_at, updated_at + FROM bl_inventario_detalle + '); + + // 3️⃣ Eliminar la tabla antigua + Schema::dropIfExists('bl_inventario_detalle'); + + // 4️⃣ Renombrar la nueva tabla con el nombre original + Schema::rename('bl_inventario_detalle_new', 'bl_inventario_detalle'); + } + + public function down(): void + { + // Revertir: volver a la tabla con posicion_id + Schema::create('bl_inventario_detalle_old', function (Blueprint $table) { + $table->id(); + $table->foreignId('empaque_id')->constrained('bl_empaques'); + $table->foreignId('posicion_id')->constrained('bl_posiciones'); + $table->integer('cantidad_actual')->default(0); + $table->date('fecha_ubicacion'); + $table->date('fecha_vencimiento')->nullable(); + $table->enum('estado', ['disponible', 'reservado', 'danado', 'caducado'])->default('disponible'); + $table->text('notas')->nullable(); + $table->timestamps(); + + $table->unique(['empaque_id', 'posicion_id']); + $table->index(['posicion_id', 'estado']); + }); + + DB::statement(' + INSERT INTO bl_inventario_detalle_old (id, empaque_id, posicion_id, cantidad_actual, fecha_ubicacion, fecha_vencimiento, estado, notas, created_at, updated_at) + SELECT id, empaque_id, zona_id, cantidad_actual, fecha_ubicacion, fecha_vencimiento, estado, notas, created_at, updated_at + FROM bl_inventario_detalle + '); + + Schema::dropIfExists('bl_inventario_detalle'); + Schema::rename('bl_inventario_detalle_old', 'bl_inventario_detalle'); + } +}; diff --git a/database/seeders/InventarioDetalleSeeder.php b/database/seeders/InventarioDetalleSeeder.php new file mode 100644 index 0000000..657d8d2 --- /dev/null +++ b/database/seeders/InventarioDetalleSeeder.php @@ -0,0 +1,137 @@ +where('estado', 'disponible') + ->get(); + + if ($empaques->isEmpty()) { + $this->command->warn('❌ No hay empaques disponibles para asignar a zonas.'); + return; + } + + // 2. Obtener zonas activas + $zonas = DB::table('bl_zonas_nivel') + ->where('activa', true) + ->get(); + + if ($zonas->isEmpty()) { + $this->command->warn('❌ No hay zonas activas disponibles.'); + return; + } + + $inventarioData = []; + $fechaBase = Carbon::now()->subMonths(6); // Fecha base para variar las fechas + + foreach ($empaques as $empaque) { + // Seleccionar una zona aleatoria + $zonaAleatoria = $zonas->random(); + + // Calcular fechas variadas + $fechaUbicacion = $fechaBase->copy()->addDays(rand(0, 180)); + $fechaVencimiento = $fechaUbicacion->copy()->addMonths(rand(6, 24)); + + // Determinar estado aleatorio (mayoría disponible) + $estados = ['disponible', 'disponible', 'disponible', 'reservado', 'disponible']; + $estado = $estados[array_rand($estados)]; + + // Cantidad aleatoria entre 1 y la capacidad máxima de la zona + $cantidadMaxima = min($empaque->cantidad_disponible ?? 100, $zonaAleatoria->capacidad_maxima); + $cantidadActual = rand(1, $cantidadMaxima); + + $inventarioData[] = [ + 'empaque_id' => $empaque->id, + 'zona_id' => $zonaAleatoria->id, + 'cantidad_actual' => $cantidadActual, + 'fecha_ubicacion' => $fechaUbicacion->format('Y-m-d'), + 'fecha_vencimiento' => $fechaVencimiento->format('Y-m-d'), + 'estado' => $estado, + 'notas' => $this->generarNotasAleatorias($estado), + 'created_at' => now(), + 'updated_at' => now(), + ]; + + // Limitar a 50 registros para no saturar (puedes ajustar) + if (count($inventarioData) >= 50) { + break; + } + } + + // 3. Insertar los datos en bl_inventario_detalle + DB::table('bl_inventario_detalle')->insert($inventarioData); + + // 4. Actualizar el contador de productos_actuales en las zonas + $this->actualizarContadorZonas(); + + $this->command->info('✅ Seeder de inventario detalle ejecutado correctamente:'); + $this->command->info(' - ' . count($inventarioData) . ' registros creados en bl_inventario_detalle'); + $this->command->info(' - Empaques asignados aleatoriamente a zonas disponibles'); + $this->command->info(' - Contadores de zonas actualizados'); + + // Mostrar algunos ejemplos + if (count($inventarioData) > 0) { + $this->command->info(''); + $this->command->info('📋 Ejemplos de registros creados:'); + $ejemplos = array_slice($inventarioData, 0, 3); + foreach ($ejemplos as $ejemplo) { + $zonaCodigo = DB::table('bl_zonas_nivel') + ->where('id', $ejemplo['zona_id']) + ->value('codigo_completo'); + + $empaqueNombre = DB::table('bl_empaques') + ->where('id', $ejemplo['empaque_id']) + ->value('nombre'); + + $this->command->info(" - {$empaqueNombre} → {$zonaCodigo} ({$ejemplo['cantidad_actual']} unidades)"); + } + } + } + + /** + * Generar notas aleatorias basadas en el estado + */ + private function generarNotasAleatorias(string $estado): ?string + { + $notas = [ + 'disponible' => [null, 'Stock nuevo', 'Recién ingresado', 'En óptimas condiciones'], + 'reservado' => ['Reservado para pedido #' . rand(1000, 9999), 'Cliente: Cliente ' . rand(1, 10)], + 'danado' => ['Daño en empaque', 'Producto golpeado', 'Requiere revisión'], + 'caducado' => ['Próximo a vencer', 'Requiere rotación'], + ]; + + $opciones = $notas[$estado] ?? [null]; + return $opciones[array_rand($opciones)]; + } + + /** + * Actualizar el contador de productos_actuales en las zonas + */ + private function actualizarContadorZonas(): void + { + // Obtener el conteo actual por zona + $conteoPorZona = DB::table('bl_inventario_detalle') + ->select('zona_id', DB::raw('SUM(cantidad_actual) as total_productos')) + ->where('estado', 'disponible') + ->groupBy('zona_id') + ->get(); + + foreach ($conteoPorZona as $conteo) { + DB::table('bl_zonas_nivel') + ->where('id', $conteo->zona_id) + ->update(['productos_actuales' => $conteo->total_productos]); + } + } +} \ No newline at end of file diff --git a/database/seeders/NivelesEstanteriaSeeder.php b/database/seeders/NivelesEstanteriaSeeder.php new file mode 100644 index 0000000..81e210b --- /dev/null +++ b/database/seeders/NivelesEstanteriaSeeder.php @@ -0,0 +1,66 @@ +get(); + + $nivelesData = []; + + foreach ($estanterias as $estanteria) { + // Crear 3 niveles para cada estantería + $niveles = [ + [ + 'estanteria_id' => $estanteria->id, + 'nivel' => 'Bajo', + 'codigo' => $estanteria->codigo . '-B', + 'capacidad_maxima' => 10000, + 'orden' => 1, + 'activo' => true, + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'estanteria_id' => $estanteria->id, + 'nivel' => 'Medio', + 'codigo' => $estanteria->codigo . '-M', + 'capacidad_maxima' => 10000, + 'orden' => 2, + 'activo' => true, + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'estanteria_id' => $estanteria->id, + 'nivel' => 'Alto', + 'codigo' => $estanteria->codigo . '-A', + 'capacidad_maxima' => 10000, + 'orden' => 3, + 'activo' => true, + 'created_at' => now(), + 'updated_at' => now(), + ], + ]; + + $nivelesData = array_merge($nivelesData, $niveles); + } + + // Insertar todos los niveles + DB::table('bl_niveles_estanteria')->insert($nivelesData); + + $this->command->info('Seeder de niveles ejecutado correctamente:'); + $this->command->info('- ' . count($estanterias) . ' estanterías procesadas'); + $this->command->info('- ' . count($nivelesData) . ' niveles creados'); + $this->command->info('- Capacidad máxima: 10,000 unidades por nivel'); + } +} \ No newline at end of file diff --git a/database/seeders/ZonasNivelSeeder.php b/database/seeders/ZonasNivelSeeder.php new file mode 100644 index 0000000..faa2044 --- /dev/null +++ b/database/seeders/ZonasNivelSeeder.php @@ -0,0 +1,80 @@ +get(); + + $zonasData = []; + + foreach ($niveles as $nivel) { + // Obtener información de la estantería padre para determinar el tipo + $estanteria = DB::table('bl_estanterias') + ->where('id', $nivel->estanteria_id) + ->first(); + + // Determinar descripción basada en el tipo de estantería + $descripcionZonaA = $estanteria->tipo === 'vertical' ? 'Lado izquierdo' : 'Zona frontal'; + $descripcionZonaB = $estanteria->tipo === 'vertical' ? 'Lado derecho' : 'Zona trasera'; + + // Calcular capacidad máxima por zona (mitad de la capacidad del nivel) + $capacidadPorZona = floor($nivel->capacidad_maxima / 2); + + // Crear 2 zonas (A y B) para cada nivel + $zonas = [ + [ + 'nivel_id' => $nivel->id, + 'zona' => 'A', + 'codigo_completo' => $nivel->codigo . '-A', + 'capacidad_maxima' => $capacidadPorZona, + 'productos_actuales' => 0, + 'descripcion' => $descripcionZonaA, + 'activa' => true, + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'nivel_id' => $nivel->id, + 'zona' => 'B', + 'codigo_completo' => $nivel->codigo . '-B', + 'capacidad_maxima' => $capacidadPorZona, + 'productos_actuales' => 0, + 'descripcion' => $descripcionZonaB, + 'activa' => true, + 'created_at' => now(), + 'updated_at' => now(), + ], + ]; + + $zonasData = array_merge($zonasData, $zonas); + } + + // Insertar todas las zonas + DB::table('bl_zonas_nivel')->insert($zonasData); + + $this->command->info('✅ Seeder de zonas de nivel ejecutado correctamente:'); + $this->command->info(' - ' . count($niveles) . ' niveles procesados'); + $this->command->info(' - ' . count($zonasData) . ' zonas creadas (A y B por cada nivel)'); + $this->command->info(' - Capacidad por zona: calculada automáticamente según el nivel'); + + // Mostrar algunos ejemplos de códigos generados + if (count($zonasData) > 0) { + $this->command->info(''); + $this->command->info('📋 Ejemplos de códigos generados:'); + $this->command->info(' - ' . $zonasData[0]['codigo_completo']); + $this->command->info(' - ' . $zonasData[1]['codigo_completo']); + $this->command->info(' - ' . $zonasData[4]['codigo_completo']); + $this->command->info(' - ' . $zonasData[5]['codigo_completo']); + } + } +} \ No newline at end of file diff --git a/resources/js/components/BL/ListaInventario.jsx b/resources/js/components/BL/ListaInventario.jsx index 9d68941..a1b7fad 100644 --- a/resources/js/components/BL/ListaInventario.jsx +++ b/resources/js/components/BL/ListaInventario.jsx @@ -14,6 +14,8 @@ const ListaInventario = ({ onLimpiarFiltros, onEstanteriaClick }) => { + console.log(productosFiltrados); + const [expandida, setExpandida] = useState(true); const toggleExpandida = () => { @@ -112,33 +114,51 @@ const ListaInventario = ({ {/* Lista de productos */}
Pasillo