diff --git a/lib/models/nethive/componente_model.dart b/lib/models/nethive/componente_model.dart index 8847e23..3c97f03 100644 --- a/lib/models/nethive/componente_model.dart +++ b/lib/models/nethive/componente_model.dart @@ -23,6 +23,7 @@ class Componente { this.ubicacion, this.imagenUrl, required this.fechaRegistro, + String? distribucionId, }); factory Componente.fromMap(Map map) { diff --git a/lib/models/nethive/vista_conexiones_por_cables_model.dart b/lib/models/nethive/vista_conexiones_por_cables_model.dart new file mode 100644 index 0000000..fbc4ef0 --- /dev/null +++ b/lib/models/nethive/vista_conexiones_por_cables_model.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; + +class VistaConexionesPorCables { + final String conexionId; + final String? descripcion; + final bool activo; + final String origenId; + final String componenteOrigen; + final String destinoId; + final String componenteDestino; + final String? cableId; + final String? cableUsado; + final String? tipoCable; + final String? color; + final double? tamano; + final String? tipoConector; + + VistaConexionesPorCables({ + required this.conexionId, + this.descripcion, + required this.activo, + required this.origenId, + required this.componenteOrigen, + required this.destinoId, + required this.componenteDestino, + this.cableId, + this.cableUsado, + this.tipoCable, + this.color, + this.tamano, + this.tipoConector, + }); + + factory VistaConexionesPorCables.fromMap(Map map) { + return VistaConexionesPorCables( + conexionId: map['conexion_id']?.toString() ?? '', + descripcion: map['descripcion']?.toString(), + activo: map['activo'] == true, + origenId: map['origen_id']?.toString() ?? '', + componenteOrigen: map['componente_origen']?.toString() ?? '', + destinoId: map['destino_id']?.toString() ?? '', + componenteDestino: map['componente_destino']?.toString() ?? '', + cableId: map['cable_id']?.toString(), + cableUsado: map['cable_usado']?.toString(), + tipoCable: map['tipo_cable']?.toString(), + color: map['color']?.toString(), + tamano: map['tamaño']?.toDouble(), + tipoConector: map['tipo_conector']?.toString(), + ); + } + + Map toMap() { + return { + 'conexion_id': conexionId, + 'descripcion': descripcion, + 'activo': activo, + 'origen_id': origenId, + 'componente_origen': componenteOrigen, + 'destino_id': destinoId, + 'componente_destino': componenteDestino, + 'cable_id': cableId, + 'cable_usado': cableUsado, + 'tipo_cable': tipoCable, + 'color': color, + 'tamaño': tamano, + 'tipo_conector': tipoConector, + }; + } + + factory VistaConexionesPorCables.fromJson(String source) => + VistaConexionesPorCables.fromMap(json.decode(source)); + + String toJson() => json.encode(toMap()); + + // Método para obtener el color del cable para visualización + String getColorForVisualization() { + if (color == null || color!.isEmpty) { + // Color por defecto basado en tipo de cable + switch (tipoCable?.toLowerCase()) { + case 'fibra': + case 'fibra optica': + return '#00BCD4'; // Cyan + case 'utp': + case 'cat6': + case 'cat5e': + return '#FFEB3B'; // Yellow + case 'coaxial': + return '#FF9800'; // Orange + default: + return '#2196F3'; // Blue por defecto + } + } + + // Convertir nombre de color a código hex + switch (color!.toLowerCase()) { + case 'azul': + case 'blue': + return '#2196F3'; + case 'rojo': + case 'red': + return '#F44336'; + case 'verde': + case 'green': + return '#4CAF50'; + case 'amarillo': + case 'yellow': + return '#FFEB3B'; + case 'naranja': + case 'orange': + return '#FF9800'; + case 'morado': + case 'purple': + return '#9C27B0'; + case 'cyan': + return '#00BCD4'; + case 'gris': + case 'gray': + return '#757575'; + default: + return '#2196F3'; + } + } + + // Método para determinar el grosor de línea basado en el tipo de cable + double getThicknessForVisualization() { + switch (tipoCable?.toLowerCase()) { + case 'fibra': + case 'fibra optica': + return 5.0; // Más grueso para backbone + case 'utp': + case 'cat6': + return 4.0; + case 'cat5e': + return 3.0; + case 'coaxial': + return 3.5; + default: + return 3.0; + } + } +} diff --git a/lib/models/nethive/vista_topologia_por_negocio_model.dart b/lib/models/nethive/vista_topologia_por_negocio_model.dart new file mode 100644 index 0000000..e650130 --- /dev/null +++ b/lib/models/nethive/vista_topologia_por_negocio_model.dart @@ -0,0 +1,183 @@ +import 'dart:convert'; + +class VistaTopologiaPorNegocio { + final String negocioId; + final String nombreNegocio; + final String? distribucionId; + final String? tipoDistribucion; + final String? distribucionNombre; + final String componenteId; + final String componenteNombre; + final String? descripcion; + final String categoriaComponente; + final bool enUso; + final bool activo; + final String? ubicacion; + final String? imagenUrl; + final DateTime fechaRegistro; + + VistaTopologiaPorNegocio({ + required this.negocioId, + required this.nombreNegocio, + this.distribucionId, + this.tipoDistribucion, + this.distribucionNombre, + required this.componenteId, + required this.componenteNombre, + this.descripcion, + required this.categoriaComponente, + required this.enUso, + required this.activo, + this.ubicacion, + this.imagenUrl, + required this.fechaRegistro, + }); + + factory VistaTopologiaPorNegocio.fromMap(Map map) { + return VistaTopologiaPorNegocio( + negocioId: map['negocio_id']?.toString() ?? '', + nombreNegocio: map['nombre_negocio']?.toString() ?? '', + distribucionId: map['distribucion_id']?.toString(), + tipoDistribucion: map['tipo_distribucion']?.toString(), + distribucionNombre: map['distribucion_nombre']?.toString(), + componenteId: map['componente_id']?.toString() ?? '', + componenteNombre: map['componente_nombre']?.toString() ?? '', + descripcion: map['descripcion']?.toString(), + categoriaComponente: map['categoria_componente']?.toString() ?? '', + enUso: map['en_uso'] == true, + activo: map['activo'] == true, + ubicacion: map['ubicacion']?.toString(), + imagenUrl: map['imagen_url']?.toString(), + fechaRegistro: + DateTime.tryParse(map['fecha_registro']?.toString() ?? '') ?? + DateTime.now(), + ); + } + + Map toMap() { + return { + 'negocio_id': negocioId, + 'nombre_negocio': nombreNegocio, + 'distribucion_id': distribucionId, + 'tipo_distribucion': tipoDistribucion, + 'distribucion_nombre': distribucionNombre, + 'componente_id': componenteId, + 'componente_nombre': componenteNombre, + 'descripcion': descripcion, + 'categoria_componente': categoriaComponente, + 'en_uso': enUso, + 'activo': activo, + 'ubicacion': ubicacion, + 'imagen_url': imagenUrl, + 'fecha_registro': fechaRegistro.toIso8601String(), + }; + } + + factory VistaTopologiaPorNegocio.fromJson(String source) => + VistaTopologiaPorNegocio.fromMap(json.decode(source)); + + String toJson() => json.encode(toMap()); + + // Método para obtener el tipo de componente principal basado en IDs + String get tipoComponentePrincipal { + final categoria = categoriaComponente.toLowerCase(); + + // Clasificación basada en los nombres de categorías exactos + if (categoria == 'cable') return 'cable'; + if (categoria == 'switch') return 'switch'; + if (categoria == 'patch panel') return 'patch_panel'; + if (categoria == 'rack') return 'rack'; + if (categoria == 'ups') return 'ups'; + if (categoria == 'mdf') return 'mdf'; + if (categoria == 'idf') return 'idf'; + + // Clasificación por contenido para compatibilidad + if (categoria.contains('switch')) return 'switch'; + if (categoria.contains('router') || categoria.contains('firewall')) + return 'router'; + if (categoria.contains('servidor') || categoria.contains('server')) + return 'servidor'; + if (categoria.contains('cable')) return 'cable'; + if (categoria.contains('patch') || categoria.contains('panel')) + return 'patch_panel'; + if (categoria.contains('rack')) return 'rack'; + if (categoria.contains('ups')) return 'ups'; + if (categoria.contains('organizador')) return 'organizador'; + + return 'otro'; + } + + // Método mejorado para determinar si es MDF + bool get esMDF { + final categoria = categoriaComponente.toLowerCase(); + return categoria == 'mdf' || + tipoDistribucion?.toUpperCase() == 'MDF' || + ubicacion?.toLowerCase().contains('mdf') == true; + } + + // Método mejorado para determinar si es IDF + bool get esIDF { + final categoria = categoriaComponente.toLowerCase(); + return categoria == 'idf' || + tipoDistribucion?.toUpperCase() == 'IDF' || + ubicacion?.toLowerCase().contains('idf') == true; + } + + // Método para obtener el nivel de prioridad del componente (para ordenamiento en topología) + int get prioridadTopologia { + if (esMDF) return 1; // Máxima prioridad para MDF + if (esIDF) return 2; // Segunda prioridad para IDF + + switch (tipoComponentePrincipal) { + case 'router': + return 3; + case 'switch': + return 4; + case 'servidor': + return 5; + case 'patch_panel': + return 6; + case 'rack': + return 7; + case 'ups': + return 8; + case 'cable': + return 9; + case 'organizador': + return 10; + default: + return 11; + } + } + + // Método para determinar el color del componente en el diagrama + String getColorForDiagram() { + if (esMDF) return '#2196F3'; // Azul para MDF + if (esIDF) { + return enUso + ? '#4CAF50' + : '#FF9800'; // Verde si está en uso, naranja si no + } + + switch (tipoComponentePrincipal) { + case 'router': + return '#FF5722'; // Naranja rojizo + case 'switch': + return '#9C27B0'; // Morado + case 'servidor': + return '#E91E63'; // Rosa + case 'patch_panel': + return '#607D8B'; // Azul gris + case 'rack': + return '#795548'; // Marrón + case 'ups': + return '#FFC107'; // Ámbar + case 'cable': + return '#4CAF50'; // Verde + case 'organizador': + return '#9E9E9E'; // Gris + default: + return activo ? '#2196F3' : '#757575'; + } + } +} diff --git a/lib/pages/infrastructure/pages/topologia_page.dart b/lib/pages/infrastructure/pages/topologia_page.dart index e56c8bf..d2a12e5 100644 --- a/lib/pages/infrastructure/pages/topologia_page.dart +++ b/lib/pages/infrastructure/pages/topologia_page.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:go_router/go_router.dart'; import 'package:flutter_flow_chart/flutter_flow_chart.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:nethive_neo/theme/theme.dart'; import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; -import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; -import 'package:nethive_neo/models/nethive/componente_model.dart'; -import 'package:nethive_neo/models/nethive/conexion_componente_model.dart'; +import 'package:nethive_neo/models/nethive/vista_topologia_por_negocio_model.dart'; +import 'package:nethive_neo/models/nethive/vista_conexiones_por_cables_model.dart'; class TopologiaPage extends StatefulWidget { const TopologiaPage({Key? key}) : super(key: key); @@ -306,8 +304,7 @@ class _TopologiaPageState extends State ]; // MDF -> IDF2 (Fibra) - mdfElement.next = [ - ...mdfElement.next ?? [], + mdfElement.next!.add( ConnectionParams( destElementId: idf2Element.id, arrowParams: ArrowParams( @@ -315,7 +312,7 @@ class _TopologiaPageState extends State thickness: 4, ), ), - ]; + ); // IDF1 -> Switch A1 (UTP) idf1Element.next = [ @@ -329,8 +326,7 @@ class _TopologiaPageState extends State ]; // IDF1 -> Switch A2 (UTP) - idf1Element.next = [ - ...idf1Element.next ?? [], + idf1Element.next!.add( ConnectionParams( destElementId: switch2Element.id, arrowParams: ArrowParams( @@ -338,7 +334,7 @@ class _TopologiaPageState extends State thickness: 3, ), ), - ]; + ); // IDF2 -> Switch B1 (UTP) idf2Element.next = [ @@ -352,8 +348,7 @@ class _TopologiaPageState extends State ]; // IDF2 -> Switch B2 (UTP - Desconectado) - idf2Element.next = [ - ...idf2Element.next ?? [], + idf2Element.next!.add( ConnectionParams( destElementId: switch4Element.id, arrowParams: ArrowParams( @@ -361,11 +356,10 @@ class _TopologiaPageState extends State thickness: 2, ), ), - ]; + ); // MDF -> Servidor (Dedicado) - mdfElement.next = [ - ...mdfElement.next ?? [], + mdfElement.next!.add( ConnectionParams( destElementId: serverElement.id, arrowParams: ArrowParams( @@ -373,7 +367,7 @@ class _TopologiaPageState extends State thickness: 5, ), ), - ]; + ); } @override @@ -934,6 +928,8 @@ class _TopologiaPageState extends State return Icons.network_check; case 'Server': return Icons.dns; + case 'Router': + return Icons.router; default: return Icons.device_unknown; } @@ -949,6 +945,8 @@ class _TopologiaPageState extends State return const Color(0xFF9C27B0); case 'Server': return const Color(0xFFE91E63); + case 'Router': + return const Color(0xFFFF5722); default: return Colors.grey; } @@ -988,8 +986,15 @@ class _TopologiaPageState extends State return; } + // Cargar toda la topología del negocio seleccionado usando el método optimizado + await componentesProvider.cargarTopologiaCompletaOptimizada( + componentesProvider.negocioSeleccionadoId!); + + // Mostrar estadísticas detalladas para debug + _mostrarEstadisticasComponentes(); + // Construir la topología con datos reales del negocio seleccionado - await _buildRealNetworkTopology(); + await _buildRealNetworkTopologyOptimized(); } catch (e) { print('Error al cargar datos de topología: ${e.toString()}'); _showErrorDialog('Error al cargar la topología: ${e.toString()}'); @@ -1000,23 +1005,25 @@ class _TopologiaPageState extends State } } - Future _buildRealNetworkTopology() async { + Future _buildRealNetworkTopologyOptimized() async { dashboard.removeAllElements(); final componentesProvider = Provider.of(context, listen: false); - // Obtener componentes agrupados por tipo - final mdfComponents = componentesProvider.getComponentesPorTipo('mdf'); - final idfComponents = componentesProvider.getComponentesPorTipo('idf'); + // Usar los datos optimizados + final mdfComponents = componentesProvider.getComponentesMDFOptimizados(); + final idfComponents = componentesProvider.getComponentesIDFOptimizados(); final switchesAcceso = componentesProvider - .getComponentesPorTipo('switch') - .where((s) => !mdfComponents.contains(s) && !idfComponents.contains(s)) + .getComponentesPorTipoOptimizado('switch') + .where((s) => !s.esMDF && !s.esIDF) .toList(); - final routers = componentesProvider.getComponentesPorTipo('router'); - final servidores = componentesProvider.getComponentesPorTipo('servidor'); + final routers = + componentesProvider.getComponentesPorTipoOptimizado('router'); + final servidores = + componentesProvider.getComponentesPorTipoOptimizado('servidor'); - print('Componentes encontrados:'); + print('Componentes optimizados encontrados:'); print('- MDF: ${mdfComponents.length}'); print('- IDF: ${idfComponents.length}'); print('- Switches de acceso: ${switchesAcceso.length}'); @@ -1030,63 +1037,63 @@ class _TopologiaPageState extends State Map elementosMap = {}; - // Crear elementos MDF + // Crear elementos MDF usando datos optimizados if (mdfComponents.isNotEmpty) { - final mdfElement = _createMDFElement( + final mdfElement = _createMDFElementOptimized( mdfComponents, Offset(currentX + espacioX * 2, currentY)); dashboard.addElement(mdfElement); - elementosMap[mdfComponents.first.id] = mdfElement; + elementosMap[mdfComponents.first.componenteId] = mdfElement; currentY += espacioY; } - // Crear elementos IDF + // Crear elementos IDF usando datos optimizados double idfX = currentX; for (var idfComp in idfComponents) { - final idfElement = - _createIDFElement(idfComp, Offset(idfX, currentY + espacioY)); + final idfElement = _createIDFElementOptimized( + idfComp, Offset(idfX, currentY + espacioY)); dashboard.addElement(idfElement); - elementosMap[idfComp.id] = idfElement; + elementosMap[idfComp.componenteId] = idfElement; idfX += espacioX; } - // Crear switches de acceso + // Crear switches de acceso usando datos optimizados double switchX = currentX; currentY += espacioY * 2; for (var switchComp in switchesAcceso) { final switchElement = - _createSwitchElement(switchComp, Offset(switchX, currentY)); + _createSwitchElementOptimized(switchComp, Offset(switchX, currentY)); dashboard.addElement(switchElement); - elementosMap[switchComp.id] = switchElement; + elementosMap[switchComp.componenteId] = switchElement; switchX += espacioX * 0.8; } - // Crear servidores + // Crear servidores usando datos optimizados if (servidores.isNotEmpty) { currentY += espacioY; double serverX = currentX + espacioX; for (var servidor in servidores) { final serverElement = - _createServerElement(servidor, Offset(serverX, currentY)); + _createServerElementOptimized(servidor, Offset(serverX, currentY)); dashboard.addElement(serverElement); - elementosMap[servidor.id] = serverElement; + elementosMap[servidor.componenteId] = serverElement; serverX += espacioX * 0.8; } } - // Crear routers/firewalls + // Crear routers/firewalls usando datos optimizados if (routers.isNotEmpty) { double routerX = currentX; for (var router in routers) { - final routerElement = _createRouterElement( + final routerElement = _createRouterElementOptimized( router, Offset(routerX, currentY - espacioY * 3)); dashboard.addElement(routerElement); - elementosMap[router.id] = routerElement; + elementosMap[router.componenteId] = routerElement; routerX += espacioX; } } - // Crear conexiones basadas en la base de datos - await _createRealConnections(elementosMap, componentesProvider); + // Crear conexiones basadas en la vista optimizada de cables + await _createOptimizedConnections(elementosMap, componentesProvider); // Si no hay elementos reales, mostrar mensaje if (elementosMap.isEmpty) { @@ -1096,18 +1103,14 @@ class _TopologiaPageState extends State setState(() {}); } - FlowElement _createMDFElement( - List mdfComponents, Offset position) { + FlowElement _createMDFElementOptimized( + List mdfComponents, Offset position) { final mainComponent = mdfComponents.first; - final componentesProvider = - Provider.of(context, listen: false); - final categoria = - componentesProvider.getCategoriaById(mainComponent.categoriaId); return FlowElement( position: position, size: const Size(180, 140), - text: 'MDF\n${mainComponent.nombre}', + text: 'MDF\n${mainComponent.componenteNombre}', textColor: Colors.white, textSize: 14, textIsBold: true, @@ -1118,13 +1121,14 @@ class _TopologiaPageState extends State elevation: 8, data: { 'type': 'MDF', - 'componenteId': mainComponent.id, - 'name': mainComponent.nombre, + 'componenteId': mainComponent.componenteId, + 'name': mainComponent.componenteNombre, 'status': mainComponent.activo ? 'active' : 'disconnected', 'description': mainComponent.descripcion ?? 'Main Distribution Frame', 'ubicacion': mainComponent.ubicacion ?? 'Sin ubicación', - 'categoria': categoria?.nombre ?? 'Sin categoría', + 'categoria': mainComponent.categoriaComponente, 'componentes': mdfComponents.length, + 'distribucion': mainComponent.distribucionNombre ?? 'MDF Principal', }, handlers: [ Handler.bottomCenter, @@ -1134,16 +1138,12 @@ class _TopologiaPageState extends State ); } - FlowElement _createIDFElement(Componente idfComponent, Offset position) { - final componentesProvider = - Provider.of(context, listen: false); - final categoria = - componentesProvider.getCategoriaById(idfComponent.categoriaId); - + FlowElement _createIDFElementOptimized( + VistaTopologiaPorNegocio idfComponent, Offset position) { return FlowElement( position: position, size: const Size(160, 120), - text: 'IDF\n${idfComponent.nombre}', + text: 'IDF\n${idfComponent.componenteNombre}', textColor: Colors.white, textSize: 12, textIsBold: true, @@ -1158,16 +1158,17 @@ class _TopologiaPageState extends State elevation: 6, data: { 'type': 'IDF', - 'componenteId': idfComponent.id, - 'name': idfComponent.nombre, + 'componenteId': idfComponent.componenteId, + 'name': idfComponent.componenteNombre, 'status': idfComponent.activo ? (idfComponent.enUso ? 'active' : 'warning') : 'disconnected', 'description': idfComponent.descripcion ?? 'Intermediate Distribution Frame', 'ubicacion': idfComponent.ubicacion ?? 'Sin ubicación', - 'categoria': categoria?.nombre ?? 'Sin categoría', + 'categoria': idfComponent.categoriaComponente, 'enUso': idfComponent.enUso, + 'distribucion': idfComponent.distribucionNombre ?? 'IDF', }, handlers: [ Handler.topCenter, @@ -1178,17 +1179,12 @@ class _TopologiaPageState extends State ); } - FlowElement _createSwitchElement( - Componente switchComponent, Offset position) { - final componentesProvider = - Provider.of(context, listen: false); - final categoria = - componentesProvider.getCategoriaById(switchComponent.categoriaId); - + FlowElement _createSwitchElementOptimized( + VistaTopologiaPorNegocio switchComponent, Offset position) { return FlowElement( position: position, size: const Size(140, 100), - text: 'Switch\n${switchComponent.nombre}', + text: 'Switch\n${switchComponent.componenteNombre}', textColor: Colors.white, textSize: 10, textIsBold: true, @@ -1203,12 +1199,13 @@ class _TopologiaPageState extends State elevation: switchComponent.activo ? 4 : 2, data: { 'type': 'AccessSwitch', - 'componenteId': switchComponent.id, - 'name': switchComponent.nombre, + 'componenteId': switchComponent.componenteId, + 'name': switchComponent.componenteNombre, 'status': switchComponent.activo ? 'active' : 'disconnected', 'description': switchComponent.descripcion ?? 'Switch de Acceso', 'ubicacion': switchComponent.ubicacion ?? 'Sin ubicación', - 'categoria': categoria?.nombre ?? 'Sin categoría', + 'categoria': switchComponent.categoriaComponente, + 'enUso': switchComponent.enUso, }, handlers: [ Handler.topCenter, @@ -1216,239 +1213,158 @@ class _TopologiaPageState extends State ); } - FlowElement _createServerElement( - Componente serverComponent, Offset position) { - final componentesProvider = - Provider.of(context, listen: false); - final categoria = - componentesProvider.getCategoriaById(serverComponent.categoriaId); - + FlowElement _createServerElementOptimized( + VistaTopologiaPorNegocio serverComponent, Offset position) { return FlowElement( position: position, - size: const Size(160, 100), - text: 'Servidor\n${serverComponent.nombre}', + size: const Size(150, 100), + text: 'Servidor\n${serverComponent.componenteNombre}', textColor: Colors.white, - textSize: 12, + textSize: 11, textIsBold: true, kind: ElementKind.rectangle, - backgroundColor: const Color(0xFFE91E63), - borderColor: const Color(0xFFC2185B), + backgroundColor: serverComponent.activo + ? const Color(0xFFE91E63) + : const Color(0xFF757575), + borderColor: serverComponent.activo + ? const Color(0xFFC2185B) + : const Color(0xFF424242), borderThickness: 3, - elevation: 6, + elevation: serverComponent.activo ? 6 : 2, data: { 'type': 'Server', - 'componenteId': serverComponent.id, - 'name': serverComponent.nombre, + 'componenteId': serverComponent.componenteId, + 'name': serverComponent.componenteNombre, 'status': serverComponent.activo ? 'active' : 'disconnected', - 'description': serverComponent.descripcion ?? 'Servidor', + 'description': serverComponent.descripcion ?? 'Servidor de red', 'ubicacion': serverComponent.ubicacion ?? 'Sin ubicación', - 'categoria': categoria?.nombre ?? 'Sin categoría', + 'categoria': serverComponent.categoriaComponente, + 'enUso': serverComponent.enUso, }, handlers: [ Handler.topCenter, - ], - ); - } - - FlowElement _createRouterElement( - Componente routerComponent, Offset position) { - final componentesProvider = - Provider.of(context, listen: false); - final categoria = - componentesProvider.getCategoriaById(routerComponent.categoriaId); - - return FlowElement( - position: position, - size: const Size(160, 100), - text: 'Router\n${routerComponent.nombre}', - textColor: Colors.white, - textSize: 12, - textIsBold: true, - kind: ElementKind.rectangle, - backgroundColor: const Color(0xFFFF5722), - borderColor: const Color(0xFFD84315), - borderThickness: 3, - elevation: 6, - data: { - 'type': 'Router', - 'componenteId': routerComponent.id, - 'name': routerComponent.nombre, - 'status': routerComponent.activo ? 'active' : 'disconnected', - 'description': routerComponent.descripcion ?? 'Router/Firewall', - 'ubicacion': routerComponent.ubicacion ?? 'Sin ubicación', - 'categoria': categoria?.nombre ?? 'Sin categoría', - }, - handlers: [ - Handler.bottomCenter, - Handler.topCenter, Handler.leftCenter, Handler.rightCenter, ], ); } - Future _createRealConnections(Map elementosMap, + FlowElement _createRouterElementOptimized( + VistaTopologiaPorNegocio routerComponent, Offset position) { + return FlowElement( + position: position, + size: const Size(160, 100), + text: 'Router\n${routerComponent.componenteNombre}', + textColor: Colors.white, + textSize: 11, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: routerComponent.activo + ? const Color(0xFFFF5722) + : const Color(0xFF757575), + borderColor: routerComponent.activo + ? const Color(0xFFE64A19) + : const Color(0xFF424242), + borderThickness: 3, + elevation: routerComponent.activo ? 6 : 2, + data: { + 'type': 'Router', + 'componenteId': routerComponent.componenteId, + 'name': routerComponent.componenteNombre, + 'status': routerComponent.activo ? 'active' : 'disconnected', + 'description': routerComponent.descripcion ?? 'Router/Firewall', + 'ubicacion': routerComponent.ubicacion ?? 'Sin ubicación', + 'categoria': routerComponent.categoriaComponente, + 'enUso': routerComponent.enUso, + }, + handlers: [ + Handler.topCenter, + Handler.bottomCenter, + Handler.leftCenter, + Handler.rightCenter, + ], + ); + } + + Future _createOptimizedConnections( + Map elementosMap, ComponentesProvider componentesProvider) async { - // Obtener conexiones reales de la base de datos - final conexiones = componentesProvider.conexiones; + try { + print('Creando conexiones optimizadas...'); + print( + 'Conexiones con cables encontradas: ${componentesProvider.conexionesConCables.length}'); - print('Creando ${conexiones.length} conexiones...'); + for (var conexionCable in componentesProvider.conexionesConCables) { + if (!conexionCable.activo) continue; - for (var conexion in conexiones) { - final origenElement = elementosMap[conexion.componenteOrigenId]; - final destinoElement = elementosMap[conexion.componenteDestinoId]; + final elementoOrigen = elementosMap[conexionCable.origenId]; + final elementoDestino = elementosMap[conexionCable.destinoId]; - if (origenElement != null && destinoElement != null) { - // Determinar el color y grosor de la conexión basado en los tipos de componentes - final colorConexion = - _getConnectionColor(conexion, componentesProvider); - final grosorConexion = - _getConnectionThickness(conexion, componentesProvider); + if (elementoOrigen != null && elementoDestino != null) { + // Usar la información real del cable para determinar color y grosor + final colorConexion = _getColorFromCableData(conexionCable); + final grosorConexion = _getThicknessFromCableData(conexionCable); - print('Conectando: ${origenElement.text} -> ${destinoElement.text}'); + print( + 'Creando conexión: ${conexionCable.componenteOrigen} -> ${conexionCable.componenteDestino}'); + if (conexionCable.tipoCable != null) { + print( + ' Cable: ${conexionCable.tipoCable} (${conexionCable.color ?? 'sin color'})'); + } - // Agregar la conexión al elemento origen - origenElement.next = [ - ...origenElement.next ?? [], - ConnectionParams( - destElementId: destinoElement.id, - arrowParams: ArrowParams( - color: colorConexion, - thickness: grosorConexion, + // Crear la conexión en el FlowChart + elementoOrigen.next ??= []; + + elementoOrigen.next!.add( + ConnectionParams( + destElementId: elementoDestino.id, + arrowParams: ArrowParams( + color: colorConexion, + thickness: grosorConexion, + ), ), - ), - ]; + ); + } else { + print( + 'Elementos no encontrados para conexión: ${conexionCable.origenId} -> ${conexionCable.destinoId}'); + } } - } - // Si no hay conexiones en la BD, crear conexiones automáticas basadas en la ubicación - if (conexiones.isEmpty) { - print('No hay conexiones en BD, creando automáticas...'); - _createAutomaticConnections(elementosMap, componentesProvider); + setState(() {}); + } catch (e) { + print('Error en _createOptimizedConnections: ${e.toString()}'); } } - Color _getConnectionColor( - ConexionComponente conexion, ComponentesProvider componentesProvider) { - // Obtener los componentes origen y destino - final origenComp = - componentesProvider.getComponenteById(conexion.componenteOrigenId); - final destinoComp = - componentesProvider.getComponenteById(conexion.componenteDestinoId); - - if (origenComp == null || destinoComp == null) return Colors.grey; - - // Determinar el tipo de conexión basado en las ubicaciones - final origenUbicacion = origenComp.ubicacion?.toUpperCase() ?? ''; - final destinoUbicacion = destinoComp.ubicacion?.toUpperCase() ?? ''; - - // MDF a IDF = Fibra (cyan) - if (origenUbicacion.contains('MDF') && destinoUbicacion.contains('IDF')) { - return Colors.cyan; - } - - // IDF a Switch = UTP (yellow) - if (origenUbicacion.contains('IDF')) { - return Colors.yellow; - } - - // Servidor = Dedicado (purple) - final origenCategoria = - componentesProvider.getCategoriaById(origenComp.categoriaId); - final destinoCategoria = - componentesProvider.getCategoriaById(destinoComp.categoriaId); - - if ((origenCategoria?.nombre?.toLowerCase().contains('servidor') ?? - false) || - (destinoCategoria?.nombre?.toLowerCase().contains('servidor') ?? - false)) { - return Colors.purple; - } - - return Colors.green; // Por defecto + Color _getColorFromCableData(VistaConexionesPorCables conexionCable) { + // Usar el método del modelo para obtener el color + final colorHex = conexionCable.getColorForVisualization(); + return _hexToColor(colorHex); } - double _getConnectionThickness( - ConexionComponente conexion, ComponentesProvider componentesProvider) { - final origenComp = - componentesProvider.getComponenteById(conexion.componenteOrigenId); - final destinoComp = - componentesProvider.getComponenteById(conexion.componenteDestinoId); - - if (origenComp == null || destinoComp == null) return 2.0; - - final origenUbicacion = origenComp.ubicacion?.toUpperCase() ?? ''; - final destinoUbicacion = destinoComp.ubicacion?.toUpperCase() ?? ''; - - // Conexiones principales más gruesas - if (origenUbicacion.contains('MDF') && destinoUbicacion.contains('IDF')) { - return 4.0; - } - - return conexion.activo ? 3.0 : 2.0; + double _getThicknessFromCableData(VistaConexionesPorCables conexionCable) { + // Usar el método del modelo para obtener el grosor + return conexionCable.getThicknessForVisualization(); } - void _createAutomaticConnections(Map elementosMap, - ComponentesProvider componentesProvider) { - // Crear conexiones automáticas cuando no hay datos en la BD - final mdfElements = elementosMap.values - .where((e) => (e.data as Map)['type'] == 'MDF') - .toList(); - final idfElements = elementosMap.values - .where((e) => (e.data as Map)['type'] == 'IDF') - .toList(); - final switchElements = elementosMap.values - .where((e) => (e.data as Map)['type'] == 'AccessSwitch') - .toList(); - - print('Creando conexiones automáticas...'); - print( - 'MDF: ${mdfElements.length}, IDF: ${idfElements.length}, Switches: ${switchElements.length}'); - - // Conectar MDF a IDFs - for (var mdf in mdfElements) { - for (var idf in idfElements) { - mdf.next = [ - ...mdf.next ?? [], - ConnectionParams( - destElementId: idf.id, - arrowParams: ArrowParams( - color: Colors.cyan, - thickness: 4, - ), - ), - ]; - } - } - - // Conectar IDFs a Switches - for (int i = 0; i < idfElements.length && i < switchElements.length; i++) { - final idf = idfElements[i]; - final switchesParaEsteIdf = switchElements.skip(i * 2).take(2); - - for (var switch_ in switchesParaEsteIdf) { - idf.next = [ - ...idf.next ?? [], - ConnectionParams( - destElementId: switch_.id, - arrowParams: ArrowParams( - color: Colors.yellow, - thickness: 3, - ), - ), - ]; - } - } + Color _hexToColor(String hex) { + hex = hex.replaceAll('#', ''); + return Color(int.parse('FF$hex', radix: 16)); } - void _showNoComponentsMessage() { + void _showNoBusinessSelectedDialog() { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Sin componentes'), + title: const Row( + children: [ + Icon(Icons.business, color: Colors.orange), + SizedBox(width: 8), + Text('Negocio no seleccionado'), + ], + ), content: const Text( - 'No se encontraron componentes de red para este negocio.\n\n' - 'Para ver una topología completa, agregue componentes en el módulo de Inventario con ubicaciones como "MDF", "IDF", etc.', + 'Para visualizar la topología de red, primero debe seleccionar un negocio desde la página de empresas.', ), actions: [ TextButton( @@ -1458,44 +1374,28 @@ class _TopologiaPageState extends State ElevatedButton( onPressed: () { Navigator.of(context).pop(); - // Ir al módulo de inventario (esto depende de tu estructura de navegación) - // context.go('/infrastructure/inventory'); + // Navegar a la página de empresas + // router.pushNamed('/infrastructure/empresas'); }, - child: const Text('Ir a Inventario'), + child: const Text('Ir a Empresas'), ), ], ), ); } - void _showNoBusinessSelectedDialog() { + void _showErrorDialog(String mensaje) { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Negocio no seleccionado'), - content: const Text( - 'No se ha seleccionado ningún negocio. Por favor, regrese a la página de negocios y seleccione "Acceder a Infraestructura" en la tabla.', + title: const Row( + children: [ + Icon(Icons.error, color: Colors.red), + SizedBox(width: 8), + Text('Error'), + ], ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - // Regresar a la página de empresas/negocios - context.go('/empresa-negocios'); - }, - child: const Text('Ir a Negocios'), - ), - ], - ), - ); - } - - void _showErrorDialog(String message) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Error'), - content: Text(message), + content: Text(mensaje), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), @@ -1505,4 +1405,95 @@ class _TopologiaPageState extends State ), ); } + + void _showNoComponentsMessage() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Row( + children: [ + Icon(Icons.info, color: Colors.blue), + SizedBox(width: 8), + Text('Sin componentes'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'No se encontraron componentes de infraestructura para este negocio.', + ), + const SizedBox(height: 16), + const Text( + 'Para visualizar la topología, necesita:', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + const Text('• Registrar componentes (switches, routers, etc.)'), + const Text('• Configurar distribuciones (MDF/IDF)'), + const Text('• Crear conexiones entre componentes'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Entendido'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + // Navegar a la página de componentes + // router.pushNamed('/infrastructure/componentes'); + }, + child: const Text('Gestionar Componentes'), + ), + ], + ), + ); + } + + // Método para mostrar estadísticas detalladas de componentes + void _mostrarEstadisticasComponentes() { + final provider = Provider.of(context, listen: false); + print('\n=== ESTADÍSTICAS DETALLADAS DE COMPONENTES ==='); + print('Total de componentes cargados: ${provider.topologiaOptimizada.length}'); + + if (provider.topologiaOptimizada.isEmpty) { + print('❌ No hay componentes cargados'); + return; + } + + // Agrupar por categoría + final componentesPorCategoria = >{}; + for (final componente in provider.topologiaOptimizada) { + final categoria = componente.categoriaComponente; + componentesPorCategoria.putIfAbsent(categoria, () => []).add(componente); + } + + print('\n📊 Componentes por categoría:'); + componentesPorCategoria.forEach((categoria, lista) { + print(' - $categoria: ${lista.length} componentes'); + for (final comp in lista) { + print(' • ${comp.componenteNombre} (${comp.tipoComponentePrincipal}) - Activo: ${comp.activo}'); + } + }); + + // Clasificación por tipo principal + final mdfComponents = provider.getComponentesMDFOptimizados(); + final idfComponents = provider.getComponentesIDFOptimizados(); + final switchComponents = provider.getComponentesPorTipoOptimizado('switch'); + final routerComponents = provider.getComponentesPorTipoOptimizado('router'); + final servidorComponents = provider.getComponentesPorTipoOptimizado('servidor'); + + print('\n🔧 Clasificación por tipo:'); + print(' - MDF: ${mdfComponents.length}'); + print(' - IDF: ${idfComponents.length}'); + print(' - Switches: ${switchComponents.length}'); + print(' - Routers: ${routerComponents.length}'); + print(' - Servidores: ${servidorComponents.length}'); + + print('\n🔗 Conexiones disponibles: ${provider.conexionesConCables.length}'); + print('===============================================\n'); + } } diff --git a/lib/pages/infrastructure/widgets/edit_componente_dialog.dart b/lib/pages/infrastructure/widgets/edit_componente_dialog.dart index 955ed8e..7b362ba 100644 --- a/lib/pages/infrastructure/widgets/edit_componente_dialog.dart +++ b/lib/pages/infrastructure/widgets/edit_componente_dialog.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; import 'package:nethive_neo/helpers/globals.dart'; -import 'package:provider/provider.dart'; import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; import 'package:nethive_neo/theme/theme.dart'; import 'package:nethive_neo/models/nethive/componente_model.dart'; -import 'package:nethive_neo/models/nethive/categoria_componente_model.dart'; class EditComponenteDialog extends StatefulWidget { final ComponentesProvider provider; diff --git a/lib/providers/nethive/componentes_provider.dart b/lib/providers/nethive/componentes_provider.dart index 87c0585..08edaef 100644 --- a/lib/providers/nethive/componentes_provider.dart +++ b/lib/providers/nethive/componentes_provider.dart @@ -18,6 +18,8 @@ import 'package:nethive_neo/models/nethive/detalle_organizador_model.dart'; import 'package:nethive_neo/models/nethive/detalle_ups_model.dart'; 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/vista_conexiones_por_cables_model.dart'; +import 'package:nethive_neo/models/nethive/vista_topologia_por_negocio_model.dart'; class ComponentesProvider extends ChangeNotifier { // State managers @@ -34,9 +36,11 @@ class ComponentesProvider extends ChangeNotifier { List componentesRows = []; List categoriasRows = []; - // Nuevas listas para topología + // Listas para topología optimizada List distribuciones = []; List conexiones = []; + List conexionesConCables = []; + List topologiaOptimizada = []; // Variables para formularios String? imagenFileName; @@ -71,6 +75,8 @@ class ComponentesProvider extends ChangeNotifier { @override void dispose() { _isDisposed = true; + busquedaComponenteController.dispose(); + busquedaCategoriaController.dispose(); super.dispose(); } @@ -81,7 +87,7 @@ class ComponentesProvider extends ChangeNotifier { } } - // Métodos para categorías + // MÉTODOS PARA CATEGORÍAS Future getCategorias([String? busqueda]) async { try { var query = supabaseLU.from('categoria_componente').select(); @@ -116,7 +122,54 @@ class ComponentesProvider extends ChangeNotifier { } } - // Métodos para componentes + Future crearCategoria(String nombre) async { + try { + final res = await supabaseLU.from('categoria_componente').insert({ + 'nombre': nombre, + }).select(); + + if (res.isNotEmpty) { + await getCategorias(); + return true; + } + return false; + } catch (e) { + print('Error en crearCategoria: ${e.toString()}'); + return false; + } + } + + Future actualizarCategoria(int id, String nombre) async { + try { + final res = await supabaseLU + .from('categoria_componente') + .update({'nombre': nombre}) + .eq('id', id) + .select(); + + if (res.isNotEmpty) { + await getCategorias(); + return true; + } + return false; + } catch (e) { + print('Error en actualizarCategoria: ${e.toString()}'); + return false; + } + } + + Future eliminarCategoria(int id) async { + try { + await supabaseLU.from('categoria_componente').delete().eq('id', id); + await getCategorias(); + return true; + } catch (e) { + print('Error en eliminarCategoria: ${e.toString()}'); + return false; + } + } + + // MÉTODOS PARA COMPONENTES Future getComponentesPorNegocio(String negocioId, [String? busqueda]) async { try { @@ -173,92 +226,6 @@ class ComponentesProvider extends ChangeNotifier { } } - // Métodos para subir imágenes - Future selectImagen() async { - imagenFileName = null; - imagenToUpload = null; - - FilePickerResult? picker = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['jpg', 'png', 'jpeg'], - ); - - if (picker != null) { - var now = DateTime.now(); - var formatter = DateFormat('yyyyMMddHHmmss'); - var timestamp = formatter.format(now); - - imagenFileName = 'componente-$timestamp-${picker.files.single.name}'; - imagenToUpload = picker.files.single.bytes; - } - - _safeNotifyListeners(); - } - - Future uploadImagen() async { - if (imagenToUpload != null && imagenFileName != null) { - await supabaseLU.storage.from('nethive/componentes').uploadBinary( - imagenFileName!, - imagenToUpload!, - fileOptions: const FileOptions( - cacheControl: '3600', - upsert: false, - ), - ); - return imagenFileName; - } - return null; - } - - // CRUD Categorías - Future crearCategoria(String nombre) async { - try { - final res = await supabaseLU.from('categoria_componente').insert({ - 'nombre': nombre, - }).select(); - - if (res.isNotEmpty) { - await getCategorias(); - return true; - } - return false; - } catch (e) { - print('Error en crearCategoria: ${e.toString()}'); - return false; - } - } - - Future actualizarCategoria(int id, String nombre) async { - try { - final res = await supabaseLU - .from('categoria_componente') - .update({'nombre': nombre}) - .eq('id', id) - .select(); - - if (res.isNotEmpty) { - await getCategorias(); - return true; - } - return false; - } catch (e) { - print('Error en actualizarCategoria: ${e.toString()}'); - return false; - } - } - - Future eliminarCategoria(int id) async { - try { - await supabaseLU.from('categoria_componente').delete().eq('id', id); - await getCategorias(); - return true; - } catch (e) { - print('Error en eliminarCategoria: ${e.toString()}'); - return false; - } - } - - // CRUD Componentes Future crearComponente({ required String negocioId, required int categoriaId, @@ -315,7 +282,6 @@ class ComponentesProvider extends ChangeNotifier { 'ubicacion': ubicacion, }; - // Solo actualizar imagen si se seleccionó una nueva if (actualizarImagen) { final imagenUrl = await uploadImagen(); if (imagenUrl != null) { @@ -343,41 +309,32 @@ class ComponentesProvider extends ChangeNotifier { Future eliminarComponente(String componenteId) async { try { - // Primero obtener la información del componente para obtener la URL de la imagen final componenteData = await supabaseLU .from('componente') .select('imagen_url') .eq('id', componenteId) .maybeSingle(); - // Guardar la URL de la imagen para eliminarla después String? imagenUrl; if (componenteData != null && componenteData['imagen_url'] != null) { imagenUrl = componenteData['imagen_url'] as String; } - // Eliminar todos los detalles específicos primero await _eliminarDetallesComponente(componenteId); - - // Eliminar el componente de la base de datos await supabaseLU.from('componente').delete().eq('id', componenteId); - // Actualizar la lista ANTES de eliminar la imagen if (!_isDisposed && negocioSeleccionadoId != null) { await getComponentesPorNegocio(negocioSeleccionadoId!); } - // AHORA eliminar la imagen del storage (después de que la UI se haya actualizado) if (imagenUrl != null) { try { await supabaseLU.storage .from('nethive') .remove(["componentes/$imagenUrl"]); - print('Imagen eliminada del storage: $imagenUrl'); } catch (storageError) { print( 'Error al eliminar imagen del storage: ${storageError.toString()}'); - // No retornamos false aquí porque el componente ya fue eliminado exitosamente } } @@ -388,152 +345,410 @@ class ComponentesProvider extends ChangeNotifier { } } - // Métodos para obtener detalles específicos - Future getDetallesComponente( - String componenteId, int categoriaId) async { + Future _eliminarDetallesComponente(String componenteId) async { try { - final categoriaNombre = - getCategoriaById(categoriaId)?.nombre?.toLowerCase() ?? ''; - - if (categoriaNombre.contains('cable')) { - await _getDetalleCable(componenteId); - } else if (categoriaNombre.contains('switch')) { - await _getDetalleSwitch(componenteId); - } else if (categoriaNombre.contains('patch') || - categoriaNombre.contains('panel')) { - await _getDetallePatchPanel(componenteId); - } else if (categoriaNombre.contains('rack')) { - await _getDetalleRack(componenteId); - } else if (categoriaNombre.contains('organizador')) { - await _getDetalleOrganizador(componenteId); - } else if (categoriaNombre.contains('ups')) { - await _getDetalleUps(componenteId); - } else if (categoriaNombre.contains('router') || - categoriaNombre.contains('firewall')) { - await _getDetalleRouterFirewall(componenteId); - } else { - await _getDetalleEquipoActivo(componenteId); - } - - showDetallesEspecificos = true; - _safeNotifyListeners(); + await Future.wait([ + supabaseLU + .from('detalle_cable') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_switch') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_patch_panel') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_rack') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_organizador') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_ups') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_router_firewall') + .delete() + .eq('componente_id', componenteId), + supabaseLU + .from('detalle_equipo_activo') + .delete() + .eq('componente_id', componenteId), + supabaseLU.from('conexion_componente').delete().or( + 'componente_origen_id.eq.$componenteId,componente_destino_id.eq.$componenteId'), + ]); } catch (e) { - print('Error en getDetallesComponente: ${e.toString()}'); + print('Error al eliminar detalles del componente: ${e.toString()}'); } } - Future _getDetalleCable(String componenteId) async { - final res = await supabaseLU - .from('detalle_cable') - .select() - .eq('componente_id', componenteId) - .maybeSingle(); - - if (res != null) { - detalleCable = DetalleCable.fromMap(res); - } - } - - Future _getDetalleSwitch(String componenteId) async { - final res = await supabaseLU - .from('detalle_switch') - .select() - .eq('componente_id', componenteId) - .maybeSingle(); - - if (res != null) { - detalleSwitch = DetalleSwitch.fromMap(res); - } - } - - Future _getDetallePatchPanel(String componenteId) async { - final res = await supabaseLU - .from('detalle_patch_panel') - .select() - .eq('componente_id', componenteId) - .maybeSingle(); - - if (res != null) { - detallePatchPanel = DetallePatchPanel.fromMap(res); - } - } - - Future _getDetalleRack(String componenteId) async { - final res = await supabaseLU - .from('detalle_rack') - .select() - .eq('componente_id', componenteId) - .maybeSingle(); - - if (res != null) { - detalleRack = DetalleRack.fromMap(res); - } - } - - Future _getDetalleOrganizador(String componenteId) async { - final res = await supabaseLU - .from('detalle_organizador') - .select() - .eq('componente_id', componenteId) - .maybeSingle(); - - if (res != null) { - detalleOrganizador = DetalleOrganizador.fromMap(res); - } - } - - Future _getDetalleUps(String componenteId) async { - final res = await supabaseLU - .from('detalle_ups') - .select() - .eq('componente_id', componenteId) - .maybeSingle(); - - if (res != null) { - detalleUps = DetalleUps.fromMap(res); - } - } - - Future _getDetalleRouterFirewall(String componenteId) async { - final res = await supabaseLU - .from('detalle_router_firewall') - .select() - .eq('componente_id', componenteId) - .maybeSingle(); - - if (res != null) { - detalleRouterFirewall = DetalleRouterFirewall.fromMap(res); - } - } - - Future _getDetalleEquipoActivo(String componenteId) async { - final res = await supabaseLU - .from('detalle_equipo_activo') - .select() - .eq('componente_id', componenteId) - .maybeSingle(); - - if (res != null) { - detalleEquipoActivo = DetalleEquipoActivo.fromMap(res); - } - } - - // Métodos de utilidad - CategoriaComponente? getCategoriaById(int id) { - try { - return categorias.firstWhere((c) => c.id == id); - } catch (e) { - return null; - } - } - - void resetFormData() { + // MÉTODOS PARA IMÁGENES + Future selectImagen() async { imagenFileName = null; imagenToUpload = null; - categoriaSeleccionadaId = null; + + FilePickerResult? picker = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['jpg', 'png', 'jpeg'], + ); + + if (picker != null) { + var now = DateTime.now(); + var formatter = DateFormat('yyyyMMddHHmmss'); + var timestamp = formatter.format(now); + + imagenFileName = 'componente-$timestamp-${picker.files.single.name}'; + imagenToUpload = picker.files.single.bytes; + } + + _safeNotifyListeners(); + } + + Future uploadImagen() async { + if (imagenToUpload != null && imagenFileName != null) { + await supabaseLU.storage.from('nethive/componentes').uploadBinary( + imagenFileName!, + imagenToUpload!, + fileOptions: const FileOptions( + cacheControl: '3600', + upsert: false, + ), + ); + return imagenFileName; + } + return null; + } + + // MÉTODOS PARA DISTRIBUCIONES + Future getDistribucionesPorNegocio(String negocioId) async { + try { + final res = await supabaseLU + .from('distribucion') + .select() + .eq('negocio_id', negocioId) + .order('tipo', ascending: false); + + distribuciones = (res as List) + .map((distribucion) => Distribucion.fromMap(distribucion)) + .toList(); + + print('Distribuciones cargadas: ${distribuciones.length}'); + } catch (e) { + print('Error en getDistribucionesPorNegocio: ${e.toString()}'); + distribuciones = []; + } + } + + // MÉTODOS PARA CONEXIONES + Future getConexionesPorNegocio(String negocioId) async { + try { + List res; + + try { + res = await supabaseLU + .from('vista_conexiones_por_negocio') + .select() + .eq('negocio_id', negocioId); + } catch (e) { + final componentesDelNegocio = await supabaseLU + .from('componente') + .select('id') + .eq('negocio_id', negocioId); + + if (componentesDelNegocio.isEmpty) { + conexiones = []; + return; + } + + final componenteIds = + componentesDelNegocio.map((comp) => comp['id'] as String).toList(); + + res = await supabaseLU + .from('conexion_componente') + .select() + .or('componente_origen_id.in.(${componenteIds.join(',')}),componente_destino_id.in.(${componenteIds.join(',')})') + .eq('activo', true); + } + + conexiones = (res as List) + .map((conexion) => ConexionComponente.fromMap(conexion)) + .toList(); + + print('Conexiones cargadas: ${conexiones.length}'); + } catch (e) { + print('Error en getConexionesPorNegocio: ${e.toString()}'); + conexiones = []; + } + } + + // MÉTODOS PARA TOPOLOGÍA OPTIMIZADA + Future getConexionesConCablesPorNegocio(String negocioId) async { + try { + final res = await supabaseLU + .from('vista_conexiones_con_cables') + .select() + .eq('activo', true); + + conexionesConCables = (res as List) + .map((conexion) => VistaConexionesPorCables.fromMap(conexion)) + .toList(); + + print('Conexiones con cables cargadas: ${conexionesConCables.length}'); + } catch (e) { + print('Error en getConexionesConCablesPorNegocio: ${e.toString()}'); + await _getConexionesConCablesManual(negocioId); + } + } + + Future _getConexionesConCablesManual(String negocioId) async { + try { + final componentesDelNegocio = await supabaseLU + .from('componente') + .select('id') + .eq('negocio_id', negocioId); + + if (componentesDelNegocio.isEmpty) { + conexionesConCables = []; + return; + } + + final componenteIds = + componentesDelNegocio.map((comp) => comp['id'] as String).toList(); + + final conexionesRes = await supabaseLU + .from('conexion_componente') + .select('''' + *, + componente_origen:componente!componente_origen_id(id, nombre), + componente_destino:componente!componente_destino_id(id, nombre), + cable:componente!cable_id(id, nombre), + detalle_cable!cable_id(*) + ''') + .or('componente_origen_id.in.(${componenteIds.join(',')}),componente_destino_id.in.(${componenteIds.join(',')})') + .eq('activo', true); + + conexionesConCables = (conexionesRes as List).map((conexion) { + final origenData = + conexion['componente_origen'] as Map?; + final destinoData = + conexion['componente_destino'] as Map?; + final cableData = conexion['cable'] as Map?; + final detalleCableData = + conexion['detalle_cable'] as Map?; + + return VistaConexionesPorCables( + conexionId: conexion['id'], + descripcion: conexion['descripcion'], + activo: conexion['activo'] ?? false, + origenId: origenData?['id'] ?? '', + componenteOrigen: origenData?['nombre'] ?? '', + destinoId: destinoData?['id'] ?? '', + componenteDestino: destinoData?['nombre'] ?? '', + cableId: cableData?['id'], + cableUsado: cableData?['nombre'], + tipoCable: detalleCableData?['tipo_cable'], + color: detalleCableData?['color'], + tamano: detalleCableData?['tamaño']?.toDouble(), + tipoConector: detalleCableData?['tipo_conector'], + ); + }).toList(); + + print('Conexiones con cables (manual): ${conexionesConCables.length}'); + } catch (e) { + print('Error en _getConexionesConCablesManual: ${e.toString()}'); + conexionesConCables = []; + } + } + + Future getTopologiaOptimizadaPorNegocio(String negocioId) async { + try { + final res = await supabaseLU + .from('vista_topologia_por_negocio') + .select() + .eq('negocio_id', negocioId) + .eq('activo', true) + .order('fecha_registro', ascending: false); + + topologiaOptimizada = (res as List) + .map((item) => VistaTopologiaPorNegocio.fromMap(item)) + .toList(); + + print( + 'Topología optimizada cargada: ${topologiaOptimizada.length} componentes'); + } catch (e) { + print('Error en getTopologiaOptimizadaPorNegocio: ${e.toString()}'); + await _getTopologiaOptimizadaManual(negocioId); + } + } + + Future _getTopologiaOptimizadaManual(String negocioId) async { + try { + final res = await supabaseLU + .from('componente') + .select(''' + *, + categoria_componente!inner(id, nombre), + distribucion(id, tipo, nombre), + negocio!inner(id, nombre) + ''') + .eq('negocio_id', negocioId) + .eq('activo', true) + .order('fecha_registro', ascending: false); + + topologiaOptimizada = (res as List).map((item) { + // Manejar datos de negocio + final negocioData = item['negocio']; + final String negocioIdRes = negocioData is Map + ? negocioData['id']?.toString() ?? negocioId + : negocioId; + final String nombreNegocio = negocioData is Map + ? negocioData['nombre']?.toString() ?? 'Sin nombre' + : 'Sin nombre'; + + // Manejar datos de categoría + final categoriaData = item['categoria_componente']; + final String nombreCategoria = categoriaData is Map + ? categoriaData['nombre']?.toString() ?? 'Sin categoría' + : 'Sin categoría'; + + // Manejar datos de distribución (puede ser null) + final distribucionData = item['distribucion']; + String? distribucionId; + String? tipoDistribucion; + String? distribucionNombre; + + if (distribucionData is Map) { + distribucionId = distribucionData['id']?.toString(); + tipoDistribucion = distribucionData['tipo']?.toString(); + distribucionNombre = distribucionData['nombre']?.toString(); + } + + return VistaTopologiaPorNegocio( + negocioId: negocioIdRes, + nombreNegocio: nombreNegocio, + distribucionId: distribucionId, + tipoDistribucion: tipoDistribucion, + distribucionNombre: distribucionNombre, + componenteId: item['id']?.toString() ?? '', + componenteNombre: item['nombre']?.toString() ?? '', + descripcion: item['descripcion']?.toString(), + categoriaComponente: nombreCategoria, + enUso: item['en_uso'] == true, + activo: item['activo'] == true, + ubicacion: item['ubicacion']?.toString(), + imagenUrl: item['imagen_url']?.toString(), + fechaRegistro: + DateTime.tryParse(item['fecha_registro']?.toString() ?? '') ?? + DateTime.now(), + ); + }).toList(); + + print( + 'Topología optimizada (manual): ${topologiaOptimizada.length} componentes'); + + // Debug: Mostrar las categorías encontradas + final categoriasEncontradas = topologiaOptimizada + .map((c) => c.categoriaComponente) + .toSet() + .toList(); + print('Categorías encontradas: $categoriasEncontradas'); + } catch (e) { + print('Error en _getTopologiaOptimizadaManual: ${e.toString()}'); + topologiaOptimizada = []; + } + } + + Future cargarTopologiaCompletaOptimizada(String negocioId) async { + isLoadingTopologia = true; + _safeNotifyListeners(); + + try { + await Future.wait([ + getTopologiaOptimizadaPorNegocio(negocioId), + getConexionesConCablesPorNegocio(negocioId), + getDistribucionesPorNegocio(negocioId), + ]); + + _sincronizarConListasPrincipales(); + problemasTopologia = validarTopologiaOptimizada(); + print('Topología completa cargada exitosamente'); + } catch (e) { + print('Error en cargarTopologiaCompletaOptimizada: ${e.toString()}'); + problemasTopologia = [ + 'Error al cargar datos de topología optimizada: ${e.toString()}' + ]; + await cargarTopologiaCompleta(negocioId); + } finally { + isLoadingTopologia = false; + _safeNotifyListeners(); + } + } + + void _sincronizarConListasPrincipales() { + if (topologiaOptimizada.isNotEmpty) { + componentes = topologiaOptimizada.map((topo) { + return Componente( + id: topo.componenteId, + negocioId: topo.negocioId, + categoriaId: _getCategoriaIdPorNombre(topo.categoriaComponente), + nombre: topo.componenteNombre, + descripcion: topo.descripcion, + ubicacion: topo.ubicacion, + imagenUrl: topo.imagenUrl, + enUso: topo.enUso, + activo: topo.activo, + fechaRegistro: topo.fechaRegistro, + distribucionId: topo.distribucionId, + ); + }).toList(); + + _buildComponentesRows(); + } + + if (conexionesConCables.isNotEmpty) { + conexiones = conexionesConCables.map((conexionCable) { + return ConexionComponente( + id: conexionCable.conexionId, + componenteOrigenId: conexionCable.origenId, + componenteDestinoId: conexionCable.destinoId, + descripcion: conexionCable.descripcion, + activo: conexionCable.activo, + ); + }).toList(); + } + } + + // MÉTODOS DE GESTIÓN DE TOPOLOGÍA + Future setNegocioSeleccionado( + String negocioId, String negocioNombre, String empresaId) async { + try { + negocioSeleccionadoId = negocioId; + negocioSeleccionadoNombre = negocioNombre; + empresaSeleccionadaId = empresaId; + + _limpiarDatosAnteriores(); + await cargarTopologiaCompleta(negocioId); + _safeNotifyListeners(); + } catch (e) { + print('Error en setNegocioSeleccionado: ${e.toString()}'); + } + } + + void _limpiarDatosAnteriores() { + componentes.clear(); + distribuciones.clear(); + conexiones.clear(); + conexionesConCables.clear(); + topologiaOptimizada.clear(); + componentesRows.clear(); showDetallesEspecificos = false; - // Limpiar detalles específicos detalleCable = null; detalleSwitch = null; detallePatchPanel = null; @@ -542,7 +757,188 @@ class ComponentesProvider extends ChangeNotifier { detalleUps = null; detalleRouterFirewall = null; detalleEquipoActivo = null; + } + Future cargarTopologiaCompleta(String negocioId) async { + isLoadingTopologia = true; + _safeNotifyListeners(); + + try { + await Future.wait([ + getComponentesPorNegocio(negocioId), + getDistribucionesPorNegocio(negocioId), + getConexionesPorNegocio(negocioId), + ]); + + problemasTopologia = validarTopologia(); + print( + 'Topología cargada - Componentes: ${componentes.length}, Distribuciones: ${distribuciones.length}, Conexiones: ${conexiones.length}'); + } catch (e) { + print('Error en cargarTopologiaCompleta: ${e.toString()}'); + problemasTopologia = [ + 'Error al cargar datos de topología: ${e.toString()}' + ]; + } finally { + isLoadingTopologia = false; + _safeNotifyListeners(); + } + } + + // MÉTODOS DE UTILIDAD + CategoriaComponente? getCategoriaById(int categoriaId) { + try { + return categorias.firstWhere((c) => c.id == categoriaId); + } catch (e) { + return null; + } + } + + Componente? getComponenteById(String componenteId) { + try { + return componentes.firstWhere((c) => c.id == componenteId); + } catch (e) { + return null; + } + } + + List getComponentesPorTipo(String tipo) { + return componentes.where((c) { + if (!c.activo) return false; + + final categoria = getCategoriaById(c.categoriaId); + final nombreCategoria = categoria?.nombre.toLowerCase() ?? ''; + + switch (tipo.toLowerCase()) { + case 'mdf': + return c.ubicacion?.toLowerCase().contains('mdf') == true || + nombreCategoria.contains('mdf') || + c.descripcion?.toLowerCase().contains('mdf') == true; + case 'idf': + return c.ubicacion?.toLowerCase().contains('idf') == true || + nombreCategoria.contains('idf') || + c.descripcion?.toLowerCase().contains('idf') == true; + case 'switch': + return nombreCategoria.contains('switch'); + case 'router': + return nombreCategoria.contains('router') || + nombreCategoria.contains('firewall'); + case 'servidor': + case 'server': + return nombreCategoria.contains('servidor') || + nombreCategoria.contains('server'); + default: + return false; + } + }).toList(); + } + + List validarTopologia() { + List problemas = []; + + if (componentes.isEmpty) { + problemas.add('No se encontraron componentes para este negocio'); + return problemas; + } + + final mdfComponents = getComponentesPorTipo('mdf'); + final idfComponents = getComponentesPorTipo('idf'); + + if (mdfComponents.isEmpty) { + problemas + .add('No se encontraron componentes MDF (distribución principal)'); + } + + if (idfComponents.isEmpty) { + problemas.add( + 'No se encontraron componentes IDF (distribuciones intermedias)'); + } + + final sinUbicacion = componentes + .where((c) => + c.activo && (c.ubicacion == null || c.ubicacion!.trim().isEmpty)) + .length; + if (sinUbicacion > 0) { + problemas.add('$sinUbicacion componentes activos sin ubicación definida'); + } + + final componentesActivos = componentes.where((c) => c.activo).length; + final conexionesActivas = conexiones.where((c) => c.activo).length; + + if (componentesActivos > 1 && conexionesActivas == 0) { + problemas.add('No se encontraron conexiones entre componentes'); + } + + return problemas; + } + + List validarTopologiaOptimizada() { + List problemas = []; + + if (topologiaOptimizada.isEmpty) { + problemas.add('No se encontraron componentes para este negocio'); + return problemas; + } + + final mdfComponents = topologiaOptimizada.where((c) => c.esMDF).toList(); + final idfComponents = topologiaOptimizada.where((c) => c.esIDF).toList(); + + if (mdfComponents.isEmpty) { + problemas.add('No se encontraron componentes configurados como MDF'); + } + + if (idfComponents.isEmpty) { + problemas.add('No se encontraron componentes configurados como IDF'); + } + + final sinUbicacion = topologiaOptimizada + .where((c) => c.ubicacion == null || c.ubicacion!.trim().isEmpty) + .length; + + if (sinUbicacion > 0) { + problemas.add('$sinUbicacion componentes sin ubicación definida'); + } + + if (conexionesConCables.isEmpty && topologiaOptimizada.length > 1) { + problemas.add('No se encontraron conexiones entre componentes'); + } + + return problemas; + } + + // MÉTODOS PARA VISTAS OPTIMIZADAS + List getComponentesMDFOptimizados() { + return topologiaOptimizada.where((c) => c.esMDF).toList() + ..sort((a, b) => a.prioridadTopologia.compareTo(b.prioridadTopologia)); + } + + List getComponentesIDFOptimizados() { + return topologiaOptimizada.where((c) => c.esIDF).toList() + ..sort((a, b) => a.prioridadTopologia.compareTo(b.prioridadTopologia)); + } + + List getComponentesPorTipoOptimizado(String tipo) { + return topologiaOptimizada + .where((c) => c.tipoComponentePrincipal == tipo) + .toList() + ..sort((a, b) => a.prioridadTopologia.compareTo(b.prioridadTopologia)); + } + + int _getCategoriaIdPorNombre(String nombreCategoria) { + try { + final categoria = categorias.firstWhere( + (c) => c.nombre.toLowerCase() == nombreCategoria.toLowerCase(), + ); + return categoria.id; + } catch (e) { + return 1; + } + } + + // MÉTODOS DE UTILIDAD PARA FORMULARIOS + void resetFormData() { + imagenFileName = null; + imagenToUpload = null; + categoriaSeleccionadaId = null; _safeNotifyListeners(); } @@ -611,551 +1007,4 @@ class ComponentesProvider extends ChangeNotifier { fit: BoxFit.cover, ); } - - // Métodos para distribuciones (MDF/IDF) - Future getDistribucionesPorNegocio(String negocioId) async { - try { - final res = await supabaseLU - .from('distribucion') - .select() - .eq('negocio_id', negocioId) - .order('tipo', ascending: false); // MDF primero, luego IDF - - distribuciones = (res as List) - .map((distribucion) => Distribucion.fromMap(distribucion)) - .toList(); - - print('Distribuciones cargadas: ${distribuciones.length}'); - for (var dist in distribuciones) { - print('- ${dist.tipo}: ${dist.nombre}'); - } - } catch (e) { - print('Error en getDistribucionesPorNegocio: ${e.toString()}'); - distribuciones = []; - } - } - - // Métodos para conexiones - Future getConexionesPorNegocio(String negocioId) async { - try { - // Usar la vista optimizada si existe, sino usar query manual - List res; - - try { - // Intentar usar la vista optimizada - res = await supabaseLU - .from('vista_conexiones_por_negocio') - .select() - .eq('negocio_id', negocioId); - } catch (e) { - print('Vista no disponible, usando query manual...'); - // Fallback: obtener conexiones manualmente - final componentesDelNegocio = await supabaseLU - .from('componente') - .select('id') - .eq('negocio_id', negocioId); - - if (componentesDelNegocio.isEmpty) { - conexiones = []; - return; - } - - final componenteIds = - componentesDelNegocio.map((comp) => comp['id'] as String).toList(); - - res = await supabaseLU - .from('conexion_componente') - .select() - .or('componente_origen_id.in.(${componenteIds.join(',')}),componente_destino_id.in.(${componenteIds.join(',')})') - .eq('activo', true); - } - - conexiones = (res as List) - .map((conexion) => ConexionComponente.fromMap(conexion)) - .toList(); - - print('Conexiones cargadas: ${conexiones.length}'); - } catch (e) { - print('Error en getConexionesPorNegocio: ${e.toString()}'); - conexiones = []; - } - } - - // Método principal para establecer el contexto del negocio desde la navegación - Future setNegocioSeleccionado( - String negocioId, String negocioNombre, String empresaId) async { - try { - negocioSeleccionadoId = negocioId; - negocioSeleccionadoNombre = negocioNombre; - empresaSeleccionadaId = empresaId; - - // Limpiar datos anteriores - _limpiarDatosAnteriores(); - - // Cargar toda la información de topología para este negocio - await cargarTopologiaCompleta(negocioId); - - _safeNotifyListeners(); - } catch (e) { - print('Error en setNegocioSeleccionado: ${e.toString()}'); - } - } - - void _limpiarDatosAnteriores() { - componentes.clear(); - distribuciones.clear(); - conexiones.clear(); - componentesRows.clear(); - showDetallesEspecificos = false; - - // Limpiar detalles específicos - detalleCable = null; - detalleSwitch = null; - detallePatchPanel = null; - detalleRack = null; - detalleOrganizador = null; - detalleUps = null; - detalleRouterFirewall = null; - detalleEquipoActivo = null; - } - - // Cargar toda la información de topología de forma optimizada - Future cargarTopologiaCompleta(String negocioId) async { - isLoadingTopologia = true; - _safeNotifyListeners(); - - try { - // Cargar datos en paralelo para mejor performance - await Future.wait([ - getComponentesPorNegocio(negocioId), - getDistribucionesPorNegocio(negocioId), - getConexionesPorNegocio(negocioId), - ]); - - // Validar integridad de la topología - problemasTopologia = validarTopologia(); - } catch (e) { - print('Error en cargarTopologiaCompleta: ${e.toString()}'); - problemasTopologia = [ - 'Error al cargar datos de topología: ${e.toString()}' - ]; - } finally { - isLoadingTopologia = false; - _safeNotifyListeners(); - } - } - - // Validar y obtener estadísticas de componentes por categoría - Map> getComponentesAgrupadosPorCategoria() { - Map> grupos = {}; - - for (var componente in componentes.where((c) => c.activo)) { - final categoria = getCategoriaById(componente.categoriaId); - final nombreCategoria = categoria?.nombre ?? 'Sin categoría'; - - if (!grupos.containsKey(nombreCategoria)) { - grupos[nombreCategoria] = []; - } - grupos[nombreCategoria]!.add(componente); - } - - return grupos; - } - - // Obtener componentes por tipo específico with better logic - List getComponentesPorTipo(String tipo) { - return componentes.where((c) { - if (!c.activo) return false; - - final categoria = getCategoriaById(c.categoriaId); - final nombreCategoria = categoria?.nombre?.toLowerCase() ?? ''; - final ubicacion = c.ubicacion?.toLowerCase() ?? ''; - - switch (tipo.toLowerCase()) { - case 'mdf': - return ubicacion.contains('mdf') || - nombreCategoria.contains('mdf') || - (nombreCategoria.contains('switch') && ubicacion.contains('mdf')); - - case 'idf': - return ubicacion.contains('idf') || - nombreCategoria.contains('idf') || - (nombreCategoria.contains('switch') && ubicacion.contains('idf')); - - case 'switch': - return nombreCategoria.contains('switch'); - - case 'router': - return nombreCategoria.contains('router') || - nombreCategoria.contains('firewall'); - - case 'servidor': - return nombreCategoria.contains('servidor') || - nombreCategoria.contains('server'); - - case 'cable': - return nombreCategoria.contains('cable'); - - case 'patch': - return nombreCategoria.contains('patch') || - nombreCategoria.contains('panel'); - - case 'rack': - return nombreCategoria.contains('rack'); - - default: - return false; - } - }).toList(); - } - - // Obtener componentes por ubicación específica - List getComponentesPorUbicacionEspecifica(String ubicacion) { - return componentes.where((c) { - if (!c.activo) return false; - return c.ubicacion?.toLowerCase().contains(ubicacion.toLowerCase()) ?? - false; - }).toList(); - } - - // Crear conexión automática inteligente - Future crearConexionAutomatica(String origenId, String destinoId, - {String? descripcion}) async { - try { - final origen = getComponenteById(origenId); - final destino = getComponenteById(destinoId); - - if (origen == null || destino == null) return false; - - // Generar descripción automática si no se proporciona - if (descripcion == null) { - final origenCategoria = getCategoriaById(origen.categoriaId); - final destinoCategoria = getCategoriaById(destino.categoriaId); - descripcion = - 'Conexión automática: ${origenCategoria?.nombre ?? 'Componente'} → ${destinoCategoria?.nombre ?? 'Componente'}'; - } - - return await crearConexion( - componenteOrigenId: origenId, - componenteDestinoId: destinoId, - descripcion: descripcion, - activo: true, - ); - } catch (e) { - print('Error en crearConexionAutomatica: ${e.toString()}'); - return false; - } - } - - // Crear una nueva conexión entre componentes - Future crearConexion({ - required String componenteOrigenId, - required String componenteDestinoId, - String? descripcion, - bool activo = true, - }) async { - try { - final res = await supabaseLU.from('conexion_componente').insert({ - 'componente_origen_id': componenteOrigenId, - 'componente_destino_id': componenteDestinoId, - 'descripcion': descripcion, - 'activo': activo, - }).select(); - - if (res.isNotEmpty && negocioSeleccionadoId != null) { - await getConexionesPorNegocio(negocioSeleccionadoId!); - return true; - } - return false; - } catch (e) { - print('Error en crearConexion: ${e.toString()}'); - return false; - } - } - - // Eliminar una conexión - Future eliminarConexion(String conexionId) async { - try { - await supabaseLU - .from('conexion_componente') - .delete() - .eq('id', conexionId); - - if (negocioSeleccionadoId != null) { - await getConexionesPorNegocio(negocioSeleccionadoId!); - } - return true; - } catch (e) { - print('Error en eliminarConexion: ${e.toString()}'); - return false; - } - } - - // Crear una nueva distribución (MDF/IDF) - Future crearDistribucion({ - required String negocioId, - required String tipo, // 'MDF' o 'IDF' - required String nombre, - String? descripcion, - }) async { - try { - final res = await supabaseLU.from('distribucion').insert({ - 'negocio_id': negocioId, - 'tipo': tipo, - 'nombre': nombre, - 'descripcion': descripcion, - }).select(); - - if (res.isNotEmpty) { - await getDistribucionesPorNegocio(negocioId); - return true; - } - return false; - } catch (e) { - print('Error en crearDistribucion: ${e.toString()}'); - return false; - } - } - - // Obtener componentes por distribución - List getComponentesPorDistribucion(String distribucionNombre) { - return componentes - .where((c) => - c.ubicacion - ?.toLowerCase() - .contains(distribucionNombre.toLowerCase()) ?? - false) - .toList(); - } - - // Obtener MDF del negocio - Distribucion? getMDF() { - try { - return distribuciones.firstWhere((d) => d.tipo == 'MDF'); - } catch (e) { - return null; - } - } - - // Obtener todos los IDF del negocio - List getIDFs() { - return distribuciones.where((d) => d.tipo == 'IDF').toList(); - } - - // Obtener conexiones de un componente específico - List getConexionesDeComponente(String componenteId) { - return conexiones - .where((c) => - c.componenteOrigenId == componenteId || - c.componenteDestinoId == componenteId) - .toList(); - } - - // Obtener componente por ID - Componente? getComponenteById(String componenteId) { - try { - return componentes.firstWhere((c) => c.id == componenteId); - } catch (e) { - return null; - } - } - - // Validar integridad de topología mejorado - List validarTopologia() { - List problemas = []; - - if (componentes.isEmpty) { - problemas.add('No se encontraron componentes para este negocio'); - return problemas; - } - - // Verificar distribuciones - final mdfCount = distribuciones.where((d) => d.tipo == 'MDF').length; - final idfCount = distribuciones.where((d) => d.tipo == 'IDF').length; - - if (mdfCount == 0) { - problemas.add('No se encontró ningún MDF configurado'); - } else if (mdfCount > 1) { - problemas.add( - 'Se encontraron múltiples MDF ($mdfCount). Se recomienda solo uno por negocio'); - } - - if (idfCount == 0) { - problemas.add('No se encontraron IDFs configurados'); - } - - // Verificar componentes principales - final switchesMDF = getComponentesPorTipo('mdf') - .where((c) => - getCategoriaById(c.categoriaId) - ?.nombre - ?.toLowerCase() - .contains('switch') ?? - false) - .toList(); - - if (switchesMDF.isEmpty) { - problemas.add('No se encontró switch principal en MDF'); - } - - // Verificar componentes sin ubicación - final sinUbicacion = componentes - .where((c) => - c.activo && (c.ubicacion == null || c.ubicacion!.trim().isEmpty)) - .length; - if (sinUbicacion > 0) { - problemas.add('$sinUbicacion componentes activos sin ubicación definida'); - } - - // Verificar conexiones - final componentesActivos = componentes.where((c) => c.activo).length; - final conexionesActivas = conexiones.where((c) => c.activo).length; - - if (componentesActivos > 1 && conexionesActivas == 0) { - problemas.add('No se encontraron conexiones entre componentes'); - } - - // Verificar componentes críticos aislados - final componentesCriticos = [ - ...switchesMDF, - ...getComponentesPorTipo('router') - ]; - for (var componente in componentesCriticos) { - final conexionesComponente = getConexionesDeComponente(componente.id); - if (conexionesComponente.isEmpty) { - final categoria = getCategoriaById(componente.categoriaId); - problemas.add( - 'Componente crítico sin conexiones: ${componente.nombre} (${categoria?.nombre})'); - } - } - - return problemas; - } - - // Obtener resumen de topología para dashboard - Map getResumenTopologia() { - final stats = getEstadisticasConectividad(); - final componentesPorCategoria = getComponentesAgrupadosPorCategoria(); - - return { - 'estadisticas': stats, - 'categorias': componentesPorCategoria.map((key, value) => MapEntry(key, { - 'total': value.length, - 'activos': value.where((c) => c.activo).length, - 'enUso': value.where((c) => c.enUso).length, - })), - 'distribuciones': { - 'mdf': distribuciones.where((d) => d.tipo == 'MDF').length, - 'idf': distribuciones.where((d) => d.tipo == 'IDF').length, - }, - 'problemas': problemasTopologia.length, - 'salud': problemasTopologia.isEmpty - ? 'Excelente' - : problemasTopologia.length <= 2 - ? 'Buena' - : problemasTopologia.length <= 5 - ? 'Regular' - : 'Crítica', - }; - } - - // Obtener sugerencias de mejora - List getSugerenciasMejora() { - List sugerencias = []; - - final stats = getEstadisticasConectividad(); - final componentesPorTipo = getComponentesAgrupadosPorCategoria(); - - // Sugerencias basadas en uso - if (stats['porcentajeUso']! < 70) { - sugerencias.add( - 'Considere optimizar el uso de componentes (${stats['porcentajeUso']}% en uso)'); - } - - // Sugerencias de redundancia - final switchesPrincipales = getComponentesPorTipo('mdf'); - if (switchesPrincipales.length == 1) { - sugerencias - .add('Considere agregar redundancia en el switch principal del MDF'); - } - - // Sugerencias de documentación - final sinDescripcion = componentes - .where((c) => - c.activo && - (c.descripcion == null || c.descripcion!.trim().isEmpty)) - .length; - if (sinDescripcion > 0) { - sugerencias.add('Documente $sinDescripcion componentes sin descripción'); - } - - // Sugerencias de organización - final componentesSinCategoria = - componentesPorTipo['Sin categoría']?.length ?? 0; - if (componentesSinCategoria > 0) { - sugerencias - .add('Categorice $componentesSinCategoria componentes sin categoría'); - } - - return sugerencias; - } - - // Obtener estadísticas de conectividad - Map getEstadisticasConectividad() { - final totalComponentes = componentes.length; - final componentesActivos = componentes.where((c) => c.activo).length; - final componentesEnUso = componentes.where((c) => c.enUso).length; - final conexionesActivas = conexiones.where((c) => c.activo).length; - - return { - 'totalComponentes': totalComponentes.toDouble(), - 'componentesActivos': componentesActivos.toDouble(), - 'componentesEnUso': componentesEnUso.toDouble(), - 'conexionesActivas': conexionesActivas.toDouble(), - 'porcentajeActivos': totalComponentes > 0 - ? (componentesActivos / totalComponentes) * 100 - : 0, - 'porcentajeUso': componentesActivos > 0 - ? (componentesEnUso / componentesActivos) * 100 - : 0, - 'densidadConexiones': - componentesActivos > 0 ? (conexionesActivas / componentesActivos) : 0, - }; - } - - Future _eliminarDetallesComponente(String componenteId) async { - // Eliminar de todas las tablas de detalles - await supabaseLU - .from('detalle_cable') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_switch') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_patch_panel') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_rack') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_organizador') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_ups') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_router_firewall') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_equipo_activo') - .delete() - .eq('componente_id', componenteId); - } }