save before clean
This commit is contained in:
33
assets/referencia/fn_racks_con_componentes.txt
Normal file
33
assets/referencia/fn_racks_con_componentes.txt
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION nethive.fn_racks_con_componentes(p_negocio_id uuid)
|
||||||
|
RETURNS jsonb
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN (
|
||||||
|
SELECT jsonb_agg(rack_info)
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
rack.id AS rack_id,
|
||||||
|
rack.nombre AS nombre_rack,
|
||||||
|
rack.ubicacion AS ubicacion_rack,
|
||||||
|
jsonb_agg(
|
||||||
|
jsonb_build_object(
|
||||||
|
'componente_id', comp.id,
|
||||||
|
'nombre', comp.nombre,
|
||||||
|
'categoria_id', comp.categoria_id,
|
||||||
|
'descripcion', comp.descripcion,
|
||||||
|
'ubicacion', comp.ubicacion,
|
||||||
|
'imagen_url', comp.imagen_url,
|
||||||
|
'en_uso', comp.en_uso,
|
||||||
|
'activo', comp.activo
|
||||||
|
)
|
||||||
|
) AS componentes
|
||||||
|
FROM nethive.componente rack
|
||||||
|
JOIN nethive.componente_en_rack cer ON rack.id = cer.rack_id
|
||||||
|
JOIN nethive.componente comp ON cer.componente_id = comp.id
|
||||||
|
WHERE rack.negocio_id = p_negocio_id AND rack.categoria_id = 4 -- RACK
|
||||||
|
GROUP BY rack.id
|
||||||
|
) rack_info
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
@@ -181,6 +181,15 @@ descripcion (text)
|
|||||||
id (serial) (PK),
|
id (serial) (PK),
|
||||||
nombre (text)
|
nombre (text)
|
||||||
|
|
||||||
|
|
||||||
|
-- componente_en_rack --
|
||||||
|
|
||||||
|
id UUID (PK)
|
||||||
|
rack_id (UUID) (FK de nethive.componente.id),
|
||||||
|
componente_id (UUID) (FK de nethive.componente.id),
|
||||||
|
posicion_u (INT4),
|
||||||
|
fecha_registro (TIMESTAMP)
|
||||||
|
|
||||||
******* VISTAS: *******
|
******* VISTAS: *******
|
||||||
|
|
||||||
-- vista_cables_en_uso --
|
-- vista_cables_en_uso --
|
||||||
|
|||||||
45
lib/models/nethive/componente_en_rack_function_model.dart
Normal file
45
lib/models/nethive/componente_en_rack_function_model.dart
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
class ComponenteEnRack {
|
||||||
|
final String componenteId;
|
||||||
|
final String nombre;
|
||||||
|
final int categoriaId;
|
||||||
|
final String? descripcion;
|
||||||
|
final String? ubicacion;
|
||||||
|
final String? imagenUrl;
|
||||||
|
final bool enUso;
|
||||||
|
final bool activo;
|
||||||
|
|
||||||
|
ComponenteEnRack({
|
||||||
|
required this.componenteId,
|
||||||
|
required this.nombre,
|
||||||
|
required this.categoriaId,
|
||||||
|
this.descripcion,
|
||||||
|
this.ubicacion,
|
||||||
|
this.imagenUrl,
|
||||||
|
required this.enUso,
|
||||||
|
required this.activo,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ComponenteEnRack.fromMap(Map<String, dynamic> map) {
|
||||||
|
return ComponenteEnRack(
|
||||||
|
componenteId: map['componente_id'] ?? '',
|
||||||
|
nombre: map['nombre'] ?? '',
|
||||||
|
categoriaId: map['categoria_id'] ?? 0,
|
||||||
|
descripcion: map['descripcion'],
|
||||||
|
ubicacion: map['ubicacion'],
|
||||||
|
imagenUrl: map['imagen_url'],
|
||||||
|
enUso: map['en_uso'] ?? false,
|
||||||
|
activo: map['activo'] ?? false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() => {
|
||||||
|
'componente_id': componenteId,
|
||||||
|
'nombre': nombre,
|
||||||
|
'categoria_id': categoriaId,
|
||||||
|
'descripcion': descripcion,
|
||||||
|
'ubicacion': ubicacion,
|
||||||
|
'imagen_url': imagenUrl,
|
||||||
|
'en_uso': enUso,
|
||||||
|
'activo': activo,
|
||||||
|
};
|
||||||
|
}
|
||||||
155
lib/models/nethive/rack_con_componentes_model.dart
Normal file
155
lib/models/nethive/rack_con_componentes_model.dart
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class RackConComponentes {
|
||||||
|
final String rackId;
|
||||||
|
final String nombreRack;
|
||||||
|
final String? ubicacionRack;
|
||||||
|
final List<ComponenteEnRackDetalle> componentes;
|
||||||
|
|
||||||
|
RackConComponentes({
|
||||||
|
required this.rackId,
|
||||||
|
required this.nombreRack,
|
||||||
|
this.ubicacionRack,
|
||||||
|
required this.componentes,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory RackConComponentes.fromMap(Map<String, dynamic> map) {
|
||||||
|
return RackConComponentes(
|
||||||
|
rackId: map['rack_id'] ?? '',
|
||||||
|
nombreRack: map['nombre_rack'] ?? '',
|
||||||
|
ubicacionRack: map['ubicacion_rack'],
|
||||||
|
componentes: (map['componentes'] as List<dynamic>? ?? [])
|
||||||
|
.map((comp) => ComponenteEnRackDetalle.fromMap(comp))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'rack_id': rackId,
|
||||||
|
'nombre_rack': nombreRack,
|
||||||
|
'ubicacion_rack': ubicacionRack,
|
||||||
|
'componentes': componentes.map((comp) => comp.toMap()).toList(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory RackConComponentes.fromJson(String source) =>
|
||||||
|
RackConComponentes.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
// Métodos de utilidad
|
||||||
|
int get cantidadComponentes => componentes.length;
|
||||||
|
|
||||||
|
int get componentesActivos => componentes.where((c) => c.activo).length;
|
||||||
|
|
||||||
|
int get componentesEnUso => componentes.where((c) => c.enUso).length;
|
||||||
|
|
||||||
|
double get porcentajeOcupacion {
|
||||||
|
// Asumiendo racks estándar de 42U
|
||||||
|
const int alturaMaximaU = 42;
|
||||||
|
final posicionesOcupadas = componentes
|
||||||
|
.where((c) => c.posicionU != null)
|
||||||
|
.map((c) => c.posicionU!)
|
||||||
|
.toSet()
|
||||||
|
.length;
|
||||||
|
|
||||||
|
return posicionesOcupadas / alturaMaximaU * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ComponenteEnRackDetalle> get componentesOrdenadosPorPosicion {
|
||||||
|
final componentesConPosicion = componentes
|
||||||
|
.where((c) => c.posicionU != null)
|
||||||
|
.toList()
|
||||||
|
..sort((a, b) => (a.posicionU ?? 0).compareTo(b.posicionU ?? 0));
|
||||||
|
|
||||||
|
final componentesSinPosicion =
|
||||||
|
componentes.where((c) => c.posicionU == null).toList();
|
||||||
|
|
||||||
|
return [...componentesConPosicion, ...componentesSinPosicion];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComponenteEnRackDetalle {
|
||||||
|
final String componenteId;
|
||||||
|
final String nombre;
|
||||||
|
final int categoriaId;
|
||||||
|
final String? descripcion;
|
||||||
|
final String? ubicacion;
|
||||||
|
final String? imagenUrl;
|
||||||
|
final bool enUso;
|
||||||
|
final bool activo;
|
||||||
|
final int? posicionU;
|
||||||
|
|
||||||
|
ComponenteEnRackDetalle({
|
||||||
|
required this.componenteId,
|
||||||
|
required this.nombre,
|
||||||
|
required this.categoriaId,
|
||||||
|
this.descripcion,
|
||||||
|
this.ubicacion,
|
||||||
|
this.imagenUrl,
|
||||||
|
required this.enUso,
|
||||||
|
required this.activo,
|
||||||
|
this.posicionU,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ComponenteEnRackDetalle.fromMap(Map<String, dynamic> map) {
|
||||||
|
return ComponenteEnRackDetalle(
|
||||||
|
componenteId: map['componente_id'] ?? '',
|
||||||
|
nombre: map['nombre'] ?? '',
|
||||||
|
categoriaId: map['categoria_id'] ?? 0,
|
||||||
|
descripcion: map['descripcion'],
|
||||||
|
ubicacion: map['ubicacion'],
|
||||||
|
imagenUrl: map['imagen_url'],
|
||||||
|
enUso: map['en_uso'] ?? false,
|
||||||
|
activo: map['activo'] ?? false,
|
||||||
|
posicionU: map['posicion_u'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'componente_id': componenteId,
|
||||||
|
'nombre': nombre,
|
||||||
|
'categoria_id': categoriaId,
|
||||||
|
'descripcion': descripcion,
|
||||||
|
'ubicacion': ubicacion,
|
||||||
|
'imagen_url': imagenUrl,
|
||||||
|
'en_uso': enUso,
|
||||||
|
'activo': activo,
|
||||||
|
'posicion_u': posicionU,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String get estadoTexto {
|
||||||
|
if (!activo) return 'Inactivo';
|
||||||
|
if (enUso) return 'En Uso';
|
||||||
|
return 'Disponible';
|
||||||
|
}
|
||||||
|
|
||||||
|
Color get colorEstado {
|
||||||
|
if (!activo) return Colors.grey;
|
||||||
|
if (enUso) return Colors.green;
|
||||||
|
return Colors.orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData get iconoCategoria {
|
||||||
|
// Mapeo básico de categorías a iconos
|
||||||
|
switch (categoriaId) {
|
||||||
|
case 1: // Switch
|
||||||
|
return Icons.router;
|
||||||
|
case 2: // Server
|
||||||
|
return Icons.dns;
|
||||||
|
case 3: // Patch Panel
|
||||||
|
return Icons.view_module;
|
||||||
|
case 5: // UPS
|
||||||
|
return Icons.battery_charging_full;
|
||||||
|
case 6: // Router/Firewall
|
||||||
|
return Icons.security;
|
||||||
|
default:
|
||||||
|
return Icons.memory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import 'package:flutter_animate/flutter_animate.dart';
|
|||||||
import 'package:nethive_neo/theme/theme.dart';
|
import 'package:nethive_neo/theme/theme.dart';
|
||||||
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
|
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
|
||||||
import 'package:nethive_neo/models/nethive/topologia_completa_model.dart';
|
import 'package:nethive_neo/models/nethive/topologia_completa_model.dart';
|
||||||
|
import 'package:nethive_neo/pages/infrastructure/pages/widgets/topologia_page_widgets/rack_view_widget.dart';
|
||||||
|
import 'package:nethive_neo/pages/infrastructure/pages/widgets/topologia_page_widgets/floor_plan_view_widget.dart';
|
||||||
|
|
||||||
class TopologiaPage extends StatefulWidget {
|
class TopologiaPage extends StatefulWidget {
|
||||||
const TopologiaPage({Key? key}) : super(key: key);
|
const TopologiaPage({Key? key}) : super(key: key);
|
||||||
@@ -1195,69 +1197,17 @@ class _TopologiaPageState extends State<TopologiaPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRackView(bool isMediumScreen, ComponentesProvider provider) {
|
Widget _buildRackView(bool isMediumScreen, ComponentesProvider provider) {
|
||||||
return Container(
|
return RackViewWidget(
|
||||||
padding: const EdgeInsets.all(24),
|
isMediumScreen: isMediumScreen,
|
||||||
child: Center(
|
provider: provider,
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.dns,
|
|
||||||
size: 80,
|
|
||||||
color: Colors.white.withOpacity(0.7),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
const Text(
|
|
||||||
'Vista de Racks',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Próximamente: Visualización detallada de racks\ncon ${provider.componentesTopologia.where((c) => c.esRack).length} racks detectados',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style:
|
|
||||||
TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFloorPlanView(
|
Widget _buildFloorPlanView(
|
||||||
bool isMediumScreen, ComponentesProvider provider) {
|
bool isMediumScreen, ComponentesProvider provider) {
|
||||||
return Container(
|
return FloorPlanViewWidget(
|
||||||
padding: const EdgeInsets.all(24),
|
isMediumScreen: isMediumScreen,
|
||||||
child: Center(
|
provider: provider,
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.map,
|
|
||||||
size: 80,
|
|
||||||
color: Colors.white.withOpacity(0.7),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
const Text(
|
|
||||||
'Plano de Planta',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
'Próximamente: Plano de distribución física\ncon ubicaciones de ${provider.componentesTopologia.length} componentes',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style:
|
|
||||||
TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 16),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
|
||||||
|
|
||||||
|
class FloorPlanViewWidget extends StatelessWidget {
|
||||||
|
final bool isMediumScreen;
|
||||||
|
final ComponentesProvider provider;
|
||||||
|
|
||||||
|
const FloorPlanViewWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.isMediumScreen,
|
||||||
|
required this.provider,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.map,
|
||||||
|
size: 80,
|
||||||
|
color: Colors.white.withOpacity(0.7),
|
||||||
|
).animate().scale(duration: 600.ms),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
'Plano de Planta',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 300.ms),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Próximamente: Distribución geográfica de componentes\ncon ${_getUbicacionesUnicas().length} ubicaciones identificadas',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.7),
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 500.ms),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Panel de información adicional para planos
|
||||||
|
if (isMediumScreen) _buildFloorPlanInfoPanel(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _getUbicacionesUnicas() {
|
||||||
|
final ubicaciones = provider.componentesTopologia
|
||||||
|
.where((c) => c.ubicacion != null && c.ubicacion!.trim().isNotEmpty)
|
||||||
|
.map((c) => c.ubicacion!)
|
||||||
|
.toSet()
|
||||||
|
.toList();
|
||||||
|
return ubicaciones;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFloorPlanInfoPanel() {
|
||||||
|
final ubicaciones = _getUbicacionesUnicas();
|
||||||
|
final componentesPorPiso = _agruparComponentesPorPiso();
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.4),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Información del Plano',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (ubicaciones.isNotEmpty) ...[
|
||||||
|
Text(
|
||||||
|
'Ubicaciones detectadas: ${ubicaciones.length}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...ubicaciones.take(4).map((ubicacion) {
|
||||||
|
final componentesEnUbicacion = provider.componentesTopologia
|
||||||
|
.where((c) => c.ubicacion == ubicacion)
|
||||||
|
.length;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Text(
|
||||||
|
'• $ubicacion ($componentesEnUbicacion componentes)',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.7),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
if (ubicaciones.length > 4)
|
||||||
|
Text(
|
||||||
|
'... y ${ubicaciones.length - 4} más',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.5),
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
Text(
|
||||||
|
'No se encontraron ubicaciones específicas',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.7),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (componentesPorPiso.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'Distribución por niveles:',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.cyan,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
...componentesPorPiso.entries.take(3).map((entry) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Text(
|
||||||
|
'${entry.key}: ${entry.value} componentes',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.6),
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'Funcionalidades planificadas:',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.cyan,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
...[
|
||||||
|
'• Mapa interactivo de ubicaciones',
|
||||||
|
'• Vista por pisos y áreas',
|
||||||
|
'• Trazado de rutas de cableado',
|
||||||
|
'• Ubicación GPS de componentes',
|
||||||
|
].map((feature) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Text(
|
||||||
|
feature,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.6),
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 700.ms).slideY(begin: 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, int> _agruparComponentesPorPiso() {
|
||||||
|
Map<String, int> pisos = {};
|
||||||
|
|
||||||
|
for (var componente in provider.componentesTopologia) {
|
||||||
|
if (componente.ubicacion != null) {
|
||||||
|
String ubicacion = componente.ubicacion!.toLowerCase();
|
||||||
|
String piso = 'Otros';
|
||||||
|
|
||||||
|
if (ubicacion.contains('piso') || ubicacion.contains('planta')) {
|
||||||
|
// Extraer número de piso
|
||||||
|
RegExp regex = RegExp(r'(piso|planta)\s*(\d+)', caseSensitive: false);
|
||||||
|
var match = regex.firstMatch(ubicacion);
|
||||||
|
if (match != null) {
|
||||||
|
piso = 'Piso ${match.group(2)}';
|
||||||
|
}
|
||||||
|
} else if (ubicacion.contains('pb') ||
|
||||||
|
ubicacion.contains('planta baja')) {
|
||||||
|
piso = 'Planta Baja';
|
||||||
|
} else if (ubicacion.contains('sotano') ||
|
||||||
|
ubicacion.contains('sótano')) {
|
||||||
|
piso = 'Sótano';
|
||||||
|
} else if (ubicacion.contains('azotea') ||
|
||||||
|
ubicacion.contains('terraza')) {
|
||||||
|
piso = 'Azotea';
|
||||||
|
}
|
||||||
|
|
||||||
|
pisos[piso] = (pisos[piso] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pisos;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,863 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import 'package:nethive_neo/helpers/constants.dart';
|
||||||
|
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
|
||||||
|
import 'package:nethive_neo/models/nethive/rack_con_componentes_model.dart';
|
||||||
|
|
||||||
|
class RackViewWidget extends StatelessWidget {
|
||||||
|
final bool isMediumScreen;
|
||||||
|
final ComponentesProvider provider;
|
||||||
|
|
||||||
|
const RackViewWidget({
|
||||||
|
Key? key,
|
||||||
|
required this.isMediumScreen,
|
||||||
|
required this.provider,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (provider.isLoadingRacks) {
|
||||||
|
return _buildLoadingView();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (provider.racksConComponentes.isEmpty) {
|
||||||
|
return _buildEmptyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header con estadísticas
|
||||||
|
_buildRackSummaryHeader(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Vista principal de racks
|
||||||
|
Expanded(
|
||||||
|
child: isMediumScreen
|
||||||
|
? _buildDesktopRackView()
|
||||||
|
: _buildMobileRackView(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingView() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 3,
|
||||||
|
).animate().scale(duration: 600.ms),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
'Cargando vista de racks...',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 300.ms),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Obteniendo componentes de cada rack',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 500.ms),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyView() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.dns,
|
||||||
|
size: 80,
|
||||||
|
color: Colors.white.withOpacity(0.5),
|
||||||
|
).animate().scale(duration: 600.ms),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
'Sin Racks Detectados',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 300.ms),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'No se encontraron racks registrados\nen este negocio',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 500.ms),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.4),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Para ver racks aquí:',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'1. Cree componentes de tipo "Rack"',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Text(
|
||||||
|
'2. Asigne otros componentes a los racks',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
const Text(
|
||||||
|
'3. Configure posiciones U si es necesario',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 700.ms),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRackSummaryHeader() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.blue.withOpacity(0.8),
|
||||||
|
Colors.blue.withOpacity(0.6),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue.withOpacity(0.3),
|
||||||
|
blurRadius: 15,
|
||||||
|
offset: const Offset(0, 5),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.dns,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
).animate().scale(duration: 600.ms),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Vista de Racks - ${provider.negocioSeleccionadoNombre}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 300.ms),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${provider.totalRacks} racks • ${provider.totalComponentesEnRacks} componentes • ${provider.porcentajeOcupacionPromedio.toStringAsFixed(1)}% ocupación promedio',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 500.ms),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (provider.racksConProblemas.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.orange.withOpacity(0.3),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.warning, color: Colors.white, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'${provider.racksConProblemas.length} alertas',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 700.ms),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).animate().fadeIn().slideY(begin: -0.3, end: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDesktopRackView() {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Lista de racks
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: _buildRacksList(),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
// Panel de información
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: _buildRackInfoPanel(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMobileRackView() {
|
||||||
|
return _buildRacksList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRacksList() {
|
||||||
|
return GridView.builder(
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: isMediumScreen ? 2 : 1,
|
||||||
|
crossAxisSpacing: 16,
|
||||||
|
mainAxisSpacing: 16,
|
||||||
|
childAspectRatio: isMediumScreen ? 1.2 : 1.5,
|
||||||
|
),
|
||||||
|
itemCount: provider.racksConComponentes.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final rack = provider.racksConComponentes[index];
|
||||||
|
return _buildRackCard(rack, index);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRackCard(RackConComponentes rack, int index) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
Colors.black.withOpacity(0.8),
|
||||||
|
Colors.black.withOpacity(0.6),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white.withOpacity(0.3),
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.blue.withOpacity(0.2),
|
||||||
|
blurRadius: 15,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.4),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: () => _showRackDetails(rack),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Imagen del rack más grande
|
||||||
|
Container(
|
||||||
|
width: 90,
|
||||||
|
height: 90,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.blue.withOpacity(0.4),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.3),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: _buildRackImage(rack),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 20),
|
||||||
|
|
||||||
|
// Información principal
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header del rack
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.blue.withOpacity(0.4),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.dns,
|
||||||
|
color: Colors.blue,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
rack.nombreRack,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (rack.ubicacionRack != null)
|
||||||
|
Text(
|
||||||
|
rack.ubicacionRack!,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.7),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Estadísticas mejoradas
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildEnhancedStatItem(
|
||||||
|
rack.cantidadComponentes.toString(),
|
||||||
|
'Total',
|
||||||
|
Colors.blue,
|
||||||
|
Icons.memory,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildEnhancedStatItem(
|
||||||
|
rack.componentesActivos.toString(),
|
||||||
|
'Activos',
|
||||||
|
Colors.green,
|
||||||
|
Icons.check_circle,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildEnhancedStatItem(
|
||||||
|
'${rack.porcentajeOcupacion.toStringAsFixed(0)}%',
|
||||||
|
'Ocupación',
|
||||||
|
rack.porcentajeOcupacion > 80
|
||||||
|
? Colors.red
|
||||||
|
: rack.porcentajeOcupacion > 60
|
||||||
|
? Colors.orange
|
||||||
|
: Colors.green,
|
||||||
|
Icons.dashboard,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Barra de ocupación mejorada
|
||||||
|
_buildEnhancedOccupationBar(rack),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Componentes preview mejorado
|
||||||
|
if (rack.componentes.isNotEmpty)
|
||||||
|
_buildEnhancedComponentsPreview(rack),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: (100 * index).ms).slideY(begin: 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRackImage(RackConComponentes rack) {
|
||||||
|
// Buscar la imagen del rack en los componentes
|
||||||
|
final rackComponent = provider.componentesTopologia
|
||||||
|
.where((c) => c.id == rack.rackId)
|
||||||
|
.firstOrNull;
|
||||||
|
|
||||||
|
final imagenUrl = rackComponent?.imagenUrl;
|
||||||
|
|
||||||
|
if (imagenUrl != null && imagenUrl.isNotEmpty) {
|
||||||
|
// Construir URL completa de Supabase
|
||||||
|
final fullImageUrl =
|
||||||
|
"$supabaseUrl/storage/v1/object/public/nethive/componentes/$imagenUrl?${DateTime.now().millisecondsSinceEpoch}";
|
||||||
|
|
||||||
|
return Image.network(
|
||||||
|
fullImageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: 90,
|
||||||
|
height: 90,
|
||||||
|
loadingBuilder: (context, child, loadingProgress) {
|
||||||
|
if (loadingProgress == null) return child;
|
||||||
|
return Container(
|
||||||
|
width: 90,
|
||||||
|
height: 90,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.withOpacity(0.3),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.blue,
|
||||||
|
strokeWidth: 2,
|
||||||
|
value: loadingProgress.expectedTotalBytes != null
|
||||||
|
? loadingProgress.cumulativeBytesLoaded /
|
||||||
|
loadingProgress.expectedTotalBytes!
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Image.asset(
|
||||||
|
'assets/images/placeholder_no_image.jpg',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: 90,
|
||||||
|
height: 90,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Usar imagen placeholder local
|
||||||
|
return Image.asset(
|
||||||
|
'assets/images/placeholder_no_image.jpg',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
width: 90,
|
||||||
|
height: 90,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEnhancedStatItem(
|
||||||
|
String value, String label, Color color, IconData icon) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
color.withOpacity(0.2),
|
||||||
|
color.withOpacity(0.1),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: color.withOpacity(0.4), width: 1),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: color.withOpacity(0.2),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
color: color,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color.withOpacity(0.8),
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEnhancedOccupationBar(RackConComponentes rack) {
|
||||||
|
final ocupacion = rack.porcentajeOcupacion;
|
||||||
|
final color = ocupacion > 80
|
||||||
|
? Colors.red
|
||||||
|
: ocupacion > 60
|
||||||
|
? Colors.orange
|
||||||
|
: Colors.green;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Ocupación del Rack',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: color.withOpacity(0.4)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'${ocupacion.toStringAsFixed(1)}%',
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: ocupacion / 100,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(color),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEnhancedComponentsPreview(RackConComponentes rack) {
|
||||||
|
final componentesOrdenados = rack.componentesOrdenadosPorPosicion;
|
||||||
|
final maxPreview = 3;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.list_alt,
|
||||||
|
color: Colors.white.withOpacity(0.8),
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
'Componentes principales:',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...componentesOrdenados.take(maxPreview).map((comp) => Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 6),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.05),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(
|
||||||
|
color: comp.colorEstado.withOpacity(0.3),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: comp.colorEstado.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
comp.iconoCategoria,
|
||||||
|
size: 14,
|
||||||
|
color: comp.colorEstado,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
comp.nombre,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (comp.posicionU != null)
|
||||||
|
Text(
|
||||||
|
'Posición U${comp.posicionU}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.6),
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: comp.colorEstado.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
comp.estadoTexto,
|
||||||
|
style: TextStyle(
|
||||||
|
color: comp.colorEstado,
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
if (componentesOrdenados.length > maxPreview)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 4),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.more_horiz,
|
||||||
|
color: Colors.blue,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'+${componentesOrdenados.length - maxPreview} componentes más',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.blue,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRackInfoPanel() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.4),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Resumen de Racks',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildInfoRow('Total de Racks', provider.totalRacks.toString()),
|
||||||
|
_buildInfoRow('Componentes Totales',
|
||||||
|
provider.totalComponentesEnRacks.toString()),
|
||||||
|
_buildInfoRow(
|
||||||
|
'Racks Activos', provider.racksConComponentesActivos.toString()),
|
||||||
|
_buildInfoRow('Ocupación Promedio',
|
||||||
|
'${provider.porcentajeOcupacionPromedio.toStringAsFixed(1)}%'),
|
||||||
|
if (provider.racksConProblemas.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Divider(color: Colors.white24),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
'Racks con Alertas',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.orange,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
...provider.racksConProblemas.map((rack) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Text(
|
||||||
|
'• ${rack.nombreRack}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.7),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
const Spacer(),
|
||||||
|
const Text(
|
||||||
|
'Funcionalidades disponibles:',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.cyan,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
...[
|
||||||
|
'• Vista detallada de cada rack',
|
||||||
|
'• Gestión de posiciones U',
|
||||||
|
'• Estados de componentes',
|
||||||
|
'• Alertas de ocupación',
|
||||||
|
].map((feature) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Text(
|
||||||
|
feature,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.6),
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).animate().fadeIn(delay: 800.ms).slideX(begin: 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoRow(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.8),
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showRackDetails(RackConComponentes rack) {
|
||||||
|
// TODO: Implementar modal con detalles completos del rack
|
||||||
|
print('Mostrar detalles del rack: ${rack.nombreRack}');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import 'package:nethive_neo/models/nethive/detalle_router_firewall_model.dart';
|
|||||||
import 'package:nethive_neo/models/nethive/detalle_equipo_activo_model.dart';
|
import 'package:nethive_neo/models/nethive/detalle_equipo_activo_model.dart';
|
||||||
import 'package:nethive_neo/models/nethive/vista_conexiones_por_cables_model.dart';
|
import 'package:nethive_neo/models/nethive/vista_conexiones_por_cables_model.dart';
|
||||||
import 'package:nethive_neo/models/nethive/vista_topologia_por_negocio_model.dart';
|
import 'package:nethive_neo/models/nethive/vista_topologia_por_negocio_model.dart';
|
||||||
|
import 'package:nethive_neo/models/nethive/rack_con_componentes_model.dart';
|
||||||
|
|
||||||
class ComponentesProvider extends ChangeNotifier {
|
class ComponentesProvider extends ChangeNotifier {
|
||||||
// State managers
|
// State managers
|
||||||
@@ -65,6 +66,7 @@ class ComponentesProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
// Variables para gestión de topología
|
// Variables para gestión de topología
|
||||||
bool isLoadingTopologia = false;
|
bool isLoadingTopologia = false;
|
||||||
|
bool isLoadingRacks = false;
|
||||||
List<String> problemasTopologia = [];
|
List<String> problemasTopologia = [];
|
||||||
|
|
||||||
// Detalles específicos por tipo de componente
|
// Detalles específicos por tipo de componente
|
||||||
@@ -77,6 +79,9 @@ class ComponentesProvider extends ChangeNotifier {
|
|||||||
DetalleRouterFirewall? detalleRouterFirewall;
|
DetalleRouterFirewall? detalleRouterFirewall;
|
||||||
DetalleEquipoActivo? detalleEquipoActivo;
|
DetalleEquipoActivo? detalleEquipoActivo;
|
||||||
|
|
||||||
|
// Nueva lista para racks con componentes
|
||||||
|
List<RackConComponentes> racksConComponentes = [];
|
||||||
|
|
||||||
// Variable para controlar si el provider está activo
|
// Variable para controlar si el provider está activo
|
||||||
bool _isDisposed = false;
|
bool _isDisposed = false;
|
||||||
|
|
||||||
@@ -866,7 +871,13 @@ class ComponentesProvider extends ChangeNotifier {
|
|||||||
empresaSeleccionadaId = empresaId;
|
empresaSeleccionadaId = empresaId;
|
||||||
|
|
||||||
_limpiarDatosAnteriores();
|
_limpiarDatosAnteriores();
|
||||||
await getTopologiaPorNegocio(negocioId);
|
|
||||||
|
// Cargar datos en paralelo
|
||||||
|
await Future.wait([
|
||||||
|
getTopologiaPorNegocio(negocioId),
|
||||||
|
getRacksConComponentes(negocioId),
|
||||||
|
]);
|
||||||
|
|
||||||
_safeNotifyListeners();
|
_safeNotifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error en setNegocioSeleccionado: ${e.toString()}');
|
print('Error en setNegocioSeleccionado: ${e.toString()}');
|
||||||
@@ -895,6 +906,9 @@ class ComponentesProvider extends ChangeNotifier {
|
|||||||
detalleUps = null;
|
detalleUps = null;
|
||||||
detalleRouterFirewall = null;
|
detalleRouterFirewall = null;
|
||||||
detalleEquipoActivo = null;
|
detalleEquipoActivo = null;
|
||||||
|
|
||||||
|
racksConComponentes.clear();
|
||||||
|
isLoadingRacks = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MÉTODOS DE UTILIDAD PARA TOPOLOGÍA
|
// MÉTODOS DE UTILIDAD PARA TOPOLOGÍA
|
||||||
@@ -961,4 +975,88 @@ class ComponentesProvider extends ChangeNotifier {
|
|||||||
.where((c) => c.origenId == componenteId || c.destinoId == componenteId)
|
.where((c) => c.origenId == componenteId || c.destinoId == componenteId)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MÉTODOS PARA CARGAR RACKS CON COMPONENTES
|
||||||
|
Future<void> getRacksConComponentes(String negocioId) async {
|
||||||
|
try {
|
||||||
|
isLoadingRacks = true;
|
||||||
|
_safeNotifyListeners();
|
||||||
|
|
||||||
|
print(
|
||||||
|
'Llamando a función RPC fn_racks_con_componentes con negocio_id: $negocioId');
|
||||||
|
|
||||||
|
final response =
|
||||||
|
await supabaseLU.rpc('fn_racks_con_componentes', params: {
|
||||||
|
'p_negocio_id': negocioId,
|
||||||
|
});
|
||||||
|
|
||||||
|
print('Respuesta RPC racks: $response');
|
||||||
|
|
||||||
|
if (response != null && response is List) {
|
||||||
|
racksConComponentes =
|
||||||
|
(response).map((rack) => RackConComponentes.fromMap(rack)).toList();
|
||||||
|
|
||||||
|
print('Racks cargados: ${racksConComponentes.length}');
|
||||||
|
for (var rack in racksConComponentes) {
|
||||||
|
print(
|
||||||
|
'- ${rack.nombreRack}: ${rack.cantidadComponentes} componentes');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
racksConComponentes = [];
|
||||||
|
print('No se encontraron racks o respuesta vacía');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error en getRacksConComponentes: ${e.toString()}');
|
||||||
|
racksConComponentes = [];
|
||||||
|
} finally {
|
||||||
|
isLoadingRacks = false;
|
||||||
|
_safeNotifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MÉTODOS DE UTILIDAD PARA RACKS
|
||||||
|
RackConComponentes? getRackById(String rackId) {
|
||||||
|
try {
|
||||||
|
return racksConComponentes.firstWhere((rack) => rack.rackId == rackId);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int get totalRacks => racksConComponentes.length;
|
||||||
|
|
||||||
|
int get totalComponentesEnRacks => racksConComponentes.fold(
|
||||||
|
0, (sum, rack) => sum + rack.cantidadComponentes);
|
||||||
|
|
||||||
|
int get racksConComponentesActivos =>
|
||||||
|
racksConComponentes.where((rack) => rack.componentesActivos > 0).length;
|
||||||
|
|
||||||
|
double get porcentajeOcupacionPromedio {
|
||||||
|
if (racksConComponentes.isEmpty) return 0.0;
|
||||||
|
|
||||||
|
final totalOcupacion = racksConComponentes.fold(
|
||||||
|
0.0, (sum, rack) => sum + rack.porcentajeOcupacion);
|
||||||
|
|
||||||
|
return totalOcupacion / racksConComponentes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RackConComponentes> get racksOrdenadosPorOcupacion {
|
||||||
|
final racks = [...racksConComponentes];
|
||||||
|
racks
|
||||||
|
.sort((a, b) => b.porcentajeOcupacion.compareTo(a.porcentajeOcupacion));
|
||||||
|
return racks;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RackConComponentes> get racksConProblemas {
|
||||||
|
return racksConComponentes.where((rack) {
|
||||||
|
// Rack con problemas si tiene componentes inactivos o sin posición U
|
||||||
|
final componentesInactivos =
|
||||||
|
rack.componentes.where((c) => !c.activo).length;
|
||||||
|
|
||||||
|
final componentesSinPosicion =
|
||||||
|
rack.componentes.where((c) => c.posicionU == null).length;
|
||||||
|
|
||||||
|
return componentesInactivos > 0 || componentesSinPosicion > 0;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user