diff --git a/assets/fonts/Poppins-Bold.ttf b/assets/fonts/Poppins-Bold.ttf new file mode 100644 index 0000000..00559ee Binary files /dev/null and b/assets/fonts/Poppins-Bold.ttf differ diff --git a/assets/fonts/Poppins-Regular.ttf b/assets/fonts/Poppins-Regular.ttf new file mode 100644 index 0000000..9f0c71b Binary files /dev/null and b/assets/fonts/Poppins-Regular.ttf differ diff --git a/lib/models/nethive/conexion_componente_model.dart b/lib/models/nethive/conexion_componente_model.dart new file mode 100644 index 0000000..31f7a57 --- /dev/null +++ b/lib/models/nethive/conexion_componente_model.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +class ConexionComponente { + final String id; + final String componenteOrigenId; + final String componenteDestinoId; + final String? descripcion; + final bool activo; + + ConexionComponente({ + required this.id, + required this.componenteOrigenId, + required this.componenteDestinoId, + this.descripcion, + required this.activo, + }); + + factory ConexionComponente.fromMap(Map map) { + return ConexionComponente( + id: map['id'], + componenteOrigenId: map['componente_origen_id'], + componenteDestinoId: map['componente_destino_id'], + descripcion: map['descripcion'], + activo: map['activo'], + ); + } + + Map toMap() { + return { + 'id': id, + 'componente_origen_id': componenteOrigenId, + 'componente_destino_id': componenteDestinoId, + 'descripcion': descripcion, + 'activo': activo, + }; + } + + factory ConexionComponente.fromJson(String source) => + ConexionComponente.fromMap(json.decode(source)); + + String toJson() => json.encode(toMap()); +} diff --git a/lib/models/nethive/distribucion_model.dart b/lib/models/nethive/distribucion_model.dart new file mode 100644 index 0000000..bf61b92 --- /dev/null +++ b/lib/models/nethive/distribucion_model.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +class Distribucion { + final String id; + final String negocioId; + final String tipo; // 'MDF' o 'IDF' + final String nombre; + final String? descripcion; + + Distribucion({ + required this.id, + required this.negocioId, + required this.tipo, + required this.nombre, + this.descripcion, + }); + + factory Distribucion.fromMap(Map map) { + return Distribucion( + id: map['id'], + negocioId: map['negocio_id'], + tipo: map['tipo'], + nombre: map['nombre'], + descripcion: map['descripcion'], + ); + } + + Map toMap() { + return { + 'id': id, + 'negocio_id': negocioId, + 'tipo': tipo, + 'nombre': nombre, + 'descripcion': descripcion, + }; + } + + factory Distribucion.fromJson(String source) => + Distribucion.fromMap(json.decode(source)); + + String toJson() => json.encode(toMap()); +} diff --git a/lib/pages/empresa_negocios/widgets/negocios_table.dart b/lib/pages/empresa_negocios/widgets/negocios_table.dart index 1677b6f..2ce35f5 100644 --- a/lib/pages/empresa_negocios/widgets/negocios_table.dart +++ b/lib/pages/empresa_negocios/widgets/negocios_table.dart @@ -4,6 +4,8 @@ import 'package:go_router/go_router.dart'; import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; import 'package:nethive_neo/pages/widgets/animated_hover_button.dart'; import 'package:nethive_neo/theme/theme.dart'; +import 'package:provider/provider.dart'; +import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; class NegociosTable extends StatelessWidget { final EmpresasNegociosProvider provider; @@ -320,10 +322,28 @@ class NegociosTable extends StatelessWidget { rendererContext.row.cells['id']?.value; final negocioNombre = rendererContext.row.cells['nombre']?.value; + final empresaId = + rendererContext.row.cells['empresa_id']?.value; - if (negocioId != null) { - // Navegar al layout principal con el negocio seleccionado - context.go('/infrastructure/$negocioId'); + if (negocioId != null && + negocioNombre != null && + empresaId != null) { + // Establecer el contexto del negocio en ComponentesProvider + final componentesProvider = + Provider.of(context, + listen: false); + componentesProvider + .setNegocioSeleccionado( + negocioId, + negocioNombre, + empresaId, + ) + .then((_) { + // Navegar al layout principal con el negocio seleccionado + if (context.mounted) { + context.go('/infrastructure/$negocioId'); + } + }); } }, borderRadius: BorderRadius.circular(12), diff --git a/lib/pages/infrastructure/infrastructure_layout.dart b/lib/pages/infrastructure/infrastructure_layout.dart index 2ff6cc6..766eaa0 100644 --- a/lib/pages/infrastructure/infrastructure_layout.dart +++ b/lib/pages/infrastructure/infrastructure_layout.dart @@ -45,17 +45,43 @@ class _InfrastructureLayoutState extends State )); // Establecer el negocio seleccionado - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + // Primero establecer en NavigationProvider context .read() .setNegocioSeleccionado(widget.negocioId); - context - .read() - .setNegocioSeleccionado(widget.negocioId); + + // Luego obtener la información completa y establecer en ComponentesProvider + await _setupComponentesProvider(); + _fadeController.forward(); }); } + Future _setupComponentesProvider() async { + try { + final navigationProvider = context.read(); + final componentesProvider = context.read(); + + // Esperar a que NavigationProvider cargue la información del negocio + await Future.delayed(const Duration(milliseconds: 100)); + + final negocio = navigationProvider.negocioSeleccionado; + final empresa = navigationProvider.empresaSeleccionada; + + if (negocio != null && empresa != null) { + // Establecer el contexto completo en ComponentesProvider + await componentesProvider.setNegocioSeleccionado( + negocio.id, + negocio.nombre, + empresa.id, + ); + } + } catch (e) { + print('Error al configurar ComponentesProvider: ${e.toString()}'); + } + } + @override void dispose() { _fadeController.dispose(); diff --git a/lib/pages/infrastructure/pages/inventario_page.dart b/lib/pages/infrastructure/pages/inventario_page.dart index c7ade2d..fcaba8c 100644 --- a/lib/pages/infrastructure/pages/inventario_page.dart +++ b/lib/pages/infrastructure/pages/inventario_page.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; import 'package:nethive_neo/pages/infrastructure/widgets/componentes_cards_view.dart'; +import 'package:nethive_neo/pages/infrastructure/widgets/edit_componente_dialog.dart'; import 'package:nethive_neo/theme/theme.dart'; class InventarioPage extends StatefulWidget { @@ -339,7 +340,7 @@ class _InventarioPageState extends State ), ), - // Tabla de componentes con PlutoGrid (como en la imagen de referencia) + // Tabla de componentes con PlutoGrid Expanded( child: componentesProvider.componentesRows.isEmpty ? _buildEmptyState() @@ -364,7 +365,13 @@ class _InventarioPageState extends State scrollbarRadiusWhileDragging: const Radius.circular(10), ), style: PlutoGridStyleConfig( - gridBorderColor: Colors.grey.withOpacity(0.3), + enableRowColorAnimation: true, + gridBorderColor: + AppTheme.of(context).primaryColor.withOpacity(0.5), + disabledIconColor: + AppTheme.of(context).alternate.withOpacity(0.3), + iconColor: + AppTheme.of(context).alternate.withOpacity(0.3), activatedBorderColor: AppTheme.of(context).primaryColor, inactivatedBorderColor: Colors.grey.withOpacity(0.3), gridBackgroundColor: @@ -389,7 +396,7 @@ class _InventarioPageState extends State bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16), ), - rowHeight: 55, + rowHeight: 70, ), columnFilter: const PlutoGridColumnFilterConfig( filters: [ @@ -401,9 +408,9 @@ class _InventarioPageState extends State PlutoColumn( title: 'ID', field: 'id', + width: 200, titleTextAlign: PlutoColumnTextAlign.center, textAlign: PlutoColumnTextAlign.center, - /* width: 100, */ type: PlutoColumnType.text(), enableEditingMode: false, backgroundColor: AppTheme.of(context).primaryColor, @@ -429,7 +436,6 @@ class _InventarioPageState extends State field: 'nombre', titleTextAlign: PlutoColumnTextAlign.center, textAlign: PlutoColumnTextAlign.left, - /* width: 200, */ type: PlutoColumnType.text(), enableEditingMode: false, backgroundColor: AppTheme.of(context).primaryColor, @@ -506,7 +512,6 @@ class _InventarioPageState extends State field: 'categoria_nombre', titleTextAlign: PlutoColumnTextAlign.center, textAlign: PlutoColumnTextAlign.center, - /* width: 140, */ type: PlutoColumnType.text(), enableEditingMode: false, backgroundColor: AppTheme.of(context).primaryColor, @@ -550,7 +555,6 @@ class _InventarioPageState extends State field: 'activo', titleTextAlign: PlutoColumnTextAlign.center, textAlign: PlutoColumnTextAlign.center, - /* width: 100, */ type: PlutoColumnType.text(), enableEditingMode: false, backgroundColor: AppTheme.of(context).primaryColor, @@ -604,7 +608,6 @@ class _InventarioPageState extends State field: 'en_uso', titleTextAlign: PlutoColumnTextAlign.center, textAlign: PlutoColumnTextAlign.center, - /* width: 100, */ type: PlutoColumnType.text(), enableEditingMode: false, backgroundColor: AppTheme.of(context).primaryColor, @@ -642,7 +645,6 @@ class _InventarioPageState extends State field: 'ubicacion', titleTextAlign: PlutoColumnTextAlign.center, textAlign: PlutoColumnTextAlign.left, - /* width: 180, */ type: PlutoColumnType.text(), enableEditingMode: false, backgroundColor: AppTheme.of(context).primaryColor, @@ -684,7 +686,6 @@ class _InventarioPageState extends State field: 'descripcion', titleTextAlign: PlutoColumnTextAlign.center, textAlign: PlutoColumnTextAlign.left, - /* width: 200, */ type: PlutoColumnType.text(), enableEditingMode: false, backgroundColor: AppTheme.of(context).primaryColor, @@ -712,7 +713,6 @@ class _InventarioPageState extends State field: 'fecha_registro', titleTextAlign: PlutoColumnTextAlign.center, textAlign: PlutoColumnTextAlign.center, - /* width: 120, */ type: PlutoColumnType.text(), enableEditingMode: false, backgroundColor: AppTheme.of(context).primaryColor, @@ -737,13 +737,19 @@ class _InventarioPageState extends State field: 'editar', titleTextAlign: PlutoColumnTextAlign.center, textAlign: PlutoColumnTextAlign.center, - /* width: 120, */ type: PlutoColumnType.text(), enableEditingMode: false, backgroundColor: AppTheme.of(context).primaryColor, enableContextMenu: false, enableDropToResize: false, renderer: (rendererContext) { + final componenteId = rendererContext + .row.cells['id']?.value + .toString() ?? + ''; + final componente = componentesProvider + .getComponenteById(componenteId); + return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -751,15 +757,8 @@ class _InventarioPageState extends State Tooltip( message: 'Ver detalles', child: InkWell( - onTap: () { - // TODO: Implementar vista de detalles - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: - Text('Ver detalles próximamente'), - ), - ); - }, + onTap: () => _showComponentDetails( + componente, componentesProvider), child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( @@ -779,14 +778,8 @@ class _InventarioPageState extends State Tooltip( message: 'Editar', child: InkWell( - onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Editar componente próximamente'), - ), - ); - }, + onTap: () => _editComponent( + componente, componentesProvider), child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( @@ -806,14 +799,8 @@ class _InventarioPageState extends State Tooltip( message: 'Eliminar', child: InkWell( - onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Eliminar componente próximamente'), - ), - ); - }, + onTap: () => _deleteComponent( + componente, componentesProvider), child: Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( @@ -839,7 +826,7 @@ class _InventarioPageState extends State event.stateManager; }, createFooter: (stateManager) { - stateManager.setPageSize(15, notify: false); + stateManager.setPageSize(10, notify: false); return PlutoPagination(stateManager); }, ), @@ -887,4 +874,259 @@ class _InventarioPageState extends State ), ); } + + // Métodos para manejar las acciones de los botones + void _showComponentDetails(dynamic componente, ComponentesProvider provider) { + if (componente == null) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppTheme.of(context).primaryBackground, + title: Row( + children: [ + Icon( + Icons.devices, + color: AppTheme.of(context).primaryColor, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + componente.nombre, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + content: Container( + width: double.maxFinite, + constraints: const BoxConstraints(maxHeight: 400), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailRow('ID', componente.id.substring(0, 8) + '...'), + _buildDetailRow( + 'Categoría', + provider.getCategoriaById(componente.categoriaId)?.nombre ?? + 'Sin categoría'), + _buildDetailRow( + 'Estado', componente.activo ? 'Activo' : 'Inactivo'), + _buildDetailRow('En Uso', componente.enUso ? 'Sí' : 'No'), + if (componente.ubicacion != null && + componente.ubicacion!.isNotEmpty) + _buildDetailRow('Ubicación', componente.ubicacion!), + if (componente.descripcion != null && + componente.descripcion!.isNotEmpty) + _buildDetailRow('Descripción', componente.descripcion!), + _buildDetailRow( + 'Fecha de Registro', + componente.fechaRegistro?.toString().split(' ')[0] ?? + 'No disponible'), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + 'Cerrar', + style: TextStyle(color: AppTheme.of(context).primaryColor), + ), + ), + ], + ), + ); + } + + Widget _buildDetailRow(String label, String value) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 100, + child: Text( + label, + style: TextStyle( + color: AppTheme.of(context).primaryColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: Text( + value, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 12, + ), + ), + ), + ], + ), + ); + } + + void _editComponent(dynamic componente, ComponentesProvider provider) { + if (componente == null) return; + + showDialog( + context: context, + builder: (context) => EditComponenteDialog( + provider: provider, + componente: componente, + ), + ); + } + + void _deleteComponent(dynamic componente, ComponentesProvider provider) { + if (componente == null) return; + + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppTheme.of(context).primaryBackground, + title: Row( + children: [ + Icon( + Icons.warning, + color: Colors.red, + ), + const SizedBox(width: 8), + Text( + 'Eliminar Componente', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + content: Text( + '¿Estás seguro de que deseas eliminar "${componente.nombre}"?\n\nEsta acción no se puede deshacer.', + style: TextStyle( + color: AppTheme.of(context).primaryText, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + 'Cancelar', + style: TextStyle(color: AppTheme.of(context).secondaryText), + ), + ), + ElevatedButton( + onPressed: () async { + Navigator.of(context).pop(); + + // Mostrar indicador de carga + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Center( + child: CircularProgressIndicator( + color: AppTheme.of(context).primaryColor, + ), + ), + ); + + try { + final success = + await provider.eliminarComponente(componente.id); + + Navigator.of(context).pop(); // Cerrar indicador de carga + + if (success) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: const [ + Icon(Icons.check_circle, color: Colors.white), + SizedBox(width: 12), + Text( + 'Componente eliminado exitosamente', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: const [ + Icon(Icons.error, color: Colors.white), + SizedBox(width: 12), + Text( + 'Error al eliminar el componente', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + } catch (e) { + Navigator.of(context).pop(); // Cerrar indicador de carga + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.warning, color: Colors.white), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Error: $e', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + ], + ), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + child: const Text('Eliminar'), + ), + ], + ), + ); + } } diff --git a/lib/pages/infrastructure/pages/topologia_page.dart b/lib/pages/infrastructure/pages/topologia_page.dart index 48ac33d..e56c8bf 100644 --- a/lib/pages/infrastructure/pages/topologia_page.dart +++ b/lib/pages/infrastructure/pages/topologia_page.dart @@ -1,7 +1,13 @@ 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'; class TopologiaPage extends StatefulWidget { const TopologiaPage({Key? key}) : super(key: key); @@ -14,8 +20,22 @@ class _TopologiaPageState extends State with TickerProviderStateMixin { late AnimationController _animationController; late Animation _fadeAnimation; - String _selectedView = 'rack'; // rack, network, floor - double _zoomLevel = 1.0; + + String _selectedView = 'network'; // network, rack, floor + bool _isLoading = false; + + // Dashboard para el FlowChart + late Dashboard dashboard; + + // Elementos para referencias + late FlowElement mdfElement; + late FlowElement idf1Element; + late FlowElement idf2Element; + late FlowElement switch1Element; + late FlowElement switch2Element; + late FlowElement switch3Element; + late FlowElement switch4Element; + late FlowElement serverElement; @override void initState() { @@ -31,7 +51,329 @@ class _TopologiaPageState extends State parent: _animationController, curve: Curves.easeInOut, )); + _animationController.forward(); + _initializeDashboard(); + + // Cargar datos después de que el widget esté construido + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadRealTopologyData(); + }); + } + + void _initializeDashboard() { + dashboard = Dashboard( + blockDefaultZoomGestures: false, + minimumZoomFactor: 0.25, + ); + } + + void _buildNetworkTopology() { + dashboard.removeAllElements(); + + // MDF Principal + mdfElement = FlowElement( + position: const Offset(400, 100), + size: const Size(160, 120), + text: 'MDF\nPrincipal', + textColor: Colors.white, + textSize: 14, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: const Color(0xFF2196F3), + borderColor: const Color(0xFF1976D2), + borderThickness: 3, + elevation: 8, + data: { + 'type': 'MDF', + 'name': 'MDF Principal', + 'status': 'active', + 'ports': '2/48', + 'description': + 'Main Distribution Frame\nSwitch Principal 48p\nPatch Panel 48p\nUPS Respaldo' + }, + handlers: [ + Handler.bottomCenter, + Handler.leftCenter, + Handler.rightCenter, + ], + ); + + // IDF 1 + idf1Element = FlowElement( + position: const Offset(200, 300), + size: const Size(140, 100), + text: 'IDF 1\nPiso 1', + textColor: Colors.white, + textSize: 12, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: const Color(0xFF4CAF50), + borderColor: const Color(0xFF388E3C), + borderThickness: 2, + elevation: 6, + data: { + 'type': 'IDF', + 'name': 'IDF Piso 1', + 'status': 'active', + 'ports': '32/48', + 'description': + 'Intermediate Distribution Frame\nSwitch 48p\nPatch Panel\nUPS' + }, + handlers: [ + Handler.topCenter, + Handler.bottomCenter, + Handler.leftCenter, + Handler.rightCenter, + ], + ); + + // IDF 2 + idf2Element = FlowElement( + position: const Offset(600, 300), + size: const Size(140, 100), + text: 'IDF 2\nPiso 2', + textColor: Colors.white, + textSize: 12, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: const Color(0xFFFF9800), + borderColor: const Color(0xFFF57C00), + borderThickness: 2, + elevation: 6, + data: { + 'type': 'IDF', + 'name': 'IDF Piso 2', + 'status': 'warning', + 'ports': '45/48', + 'description': + 'Intermediate Distribution Frame\nSwitch 48p\nPatch Panel\nUPS\n⚠️ Alta utilización' + }, + handlers: [ + Handler.topCenter, + Handler.bottomCenter, + Handler.leftCenter, + Handler.rightCenter, + ], + ); + + // Switches de Acceso + switch1Element = FlowElement( + position: const Offset(125, 500), + size: const Size(120, 80), + text: 'Switch\nAcceso A1', + textColor: Colors.white, + textSize: 10, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: const Color(0xFF9C27B0), + borderColor: const Color(0xFF7B1FA2), + borderThickness: 2, + elevation: 4, + data: { + 'type': 'AccessSwitch', + 'name': 'Switch Acceso A1', + 'status': 'active', + 'ports': '16/24', + 'description': 'Switch de Acceso\n24 puertos\nEn línea' + }, + handlers: [ + Handler.topCenter, + ], + ); + + switch2Element = FlowElement( + position: const Offset(275, 500), + size: const Size(120, 80), + text: 'Switch\nAcceso A2', + textColor: Colors.white, + textSize: 10, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: const Color(0xFF9C27B0), + borderColor: const Color(0xFF7B1FA2), + borderThickness: 2, + elevation: 4, + data: { + 'type': 'AccessSwitch', + 'name': 'Switch Acceso A2', + 'status': 'active', + 'ports': '20/24', + 'description': 'Switch de Acceso\n24 puertos\nEn línea' + }, + handlers: [ + Handler.topCenter, + ], + ); + + switch3Element = FlowElement( + position: const Offset(525, 500), + size: const Size(120, 80), + text: 'Switch\nAcceso B1', + textColor: Colors.white, + textSize: 10, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: const Color(0xFF9C27B0), + borderColor: const Color(0xFF7B1FA2), + borderThickness: 2, + elevation: 4, + data: { + 'type': 'AccessSwitch', + 'name': 'Switch Acceso B1', + 'status': 'active', + 'ports': '18/24', + 'description': 'Switch de Acceso\n24 puertos\nEn línea' + }, + handlers: [ + Handler.topCenter, + ], + ); + + switch4Element = FlowElement( + position: const Offset(675, 500), + size: const Size(120, 80), + text: 'Switch\nAcceso B2', + textColor: Colors.white, + textSize: 10, + textIsBold: false, + kind: ElementKind.rectangle, + backgroundColor: const Color(0xFF757575), + borderColor: const Color(0xFF424242), + borderThickness: 2, + elevation: 2, + data: { + 'type': 'AccessSwitch', + 'name': 'Switch Acceso B2', + 'status': 'disconnected', + 'ports': '0/24', + 'description': 'Switch de Acceso\n24 puertos\n🔴 Desconectado' + }, + handlers: [ + Handler.topCenter, + ], + ); + + // Servidor Principal + serverElement = FlowElement( + position: const Offset(400, 650), + size: const Size(150, 90), + text: 'Servidor\nPrincipal', + textColor: Colors.white, + textSize: 12, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: const Color(0xFFE91E63), + borderColor: const Color(0xFFC2185B), + borderThickness: 3, + elevation: 6, + data: { + 'type': 'Server', + 'name': 'Servidor Principal', + 'status': 'active', + 'description': + 'Servidor Principal\nWindows Server 2022\nRAM: 32GB\nStorage: 2TB SSD' + }, + handlers: [ + Handler.topCenter, + ], + ); + + // Agregar elementos al dashboard + dashboard.addElement(mdfElement); + dashboard.addElement(idf1Element); + dashboard.addElement(idf2Element); + dashboard.addElement(switch1Element); + dashboard.addElement(switch2Element); + dashboard.addElement(switch3Element); + dashboard.addElement(switch4Element); + dashboard.addElement(serverElement); + + // Crear conexiones + _createConnections(); + } + + void _createConnections() { + // MDF -> IDF1 (Fibra) + mdfElement.next = [ + ConnectionParams( + destElementId: idf1Element.id, + arrowParams: ArrowParams( + color: Colors.cyan, + thickness: 4, + ), + ), + ]; + + // MDF -> IDF2 (Fibra) + mdfElement.next = [ + ...mdfElement.next ?? [], + ConnectionParams( + destElementId: idf2Element.id, + arrowParams: ArrowParams( + color: Colors.cyan, + thickness: 4, + ), + ), + ]; + + // IDF1 -> Switch A1 (UTP) + idf1Element.next = [ + ConnectionParams( + destElementId: switch1Element.id, + arrowParams: ArrowParams( + color: Colors.yellow, + thickness: 3, + ), + ), + ]; + + // IDF1 -> Switch A2 (UTP) + idf1Element.next = [ + ...idf1Element.next ?? [], + ConnectionParams( + destElementId: switch2Element.id, + arrowParams: ArrowParams( + color: Colors.yellow, + thickness: 3, + ), + ), + ]; + + // IDF2 -> Switch B1 (UTP) + idf2Element.next = [ + ConnectionParams( + destElementId: switch3Element.id, + arrowParams: ArrowParams( + color: Colors.yellow, + thickness: 3, + ), + ), + ]; + + // IDF2 -> Switch B2 (UTP - Desconectado) + idf2Element.next = [ + ...idf2Element.next ?? [], + ConnectionParams( + destElementId: switch4Element.id, + arrowParams: ArrowParams( + color: Colors.grey, + thickness: 2, + ), + ), + ]; + + // MDF -> Servidor (Dedicado) + mdfElement.next = [ + ...mdfElement.next ?? [], + ConnectionParams( + destElementId: serverElement.id, + arrowParams: ArrowParams( + color: Colors.purple, + thickness: 5, + ), + ), + ]; } @override @@ -42,33 +384,34 @@ class _TopologiaPageState extends State @override Widget build(BuildContext context) { - final isLargeScreen = MediaQuery.of(context).size.width > 1200; final isMediumScreen = MediaQuery.of(context).size.width > 800; return FadeTransition( opacity: _fadeAnimation, child: Consumer( builder: (context, componentesProvider, child) { + if (_isLoading) { + return _buildLoadingView(); + } + return Container( padding: EdgeInsets.all(isMediumScreen ? 24 : 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header mejorado - _buildTopologyHeader(), - + // Header profesional + _buildProfessionalHeader(), const SizedBox(height: 24), - // Controles de vista y zoom + // Controles avanzados if (isMediumScreen) ...[ - _buildTopologyControls(), + _buildAdvancedControls(), const SizedBox(height: 24), ], - // Vista principal de topología + // Vista principal profesional Expanded( - child: - _buildTopologyView(componentesProvider, isMediumScreen), + child: _buildProfessionalTopologyView(isMediumScreen), ), ], ), @@ -78,7 +421,38 @@ class _TopologiaPageState extends State ); } - Widget _buildTopologyHeader() { + Widget _buildLoadingView() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + strokeWidth: 3, + color: AppTheme.of(context).primaryColor, + ).animate().scale(duration: 600.ms).then(delay: 200.ms).fadeIn(), + const SizedBox(height: 24), + Text( + 'Cargando topología de red...', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ).animate().fadeIn(delay: 400.ms), + const SizedBox(height: 8), + Text( + 'Construyendo infraestructura profesional', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 14, + ), + ).animate().fadeIn(delay: 600.ms), + ], + ), + ); + } + + Widget _buildProfessionalHeader() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -105,27 +479,33 @@ class _TopologiaPageState extends State color: Colors.white, size: 24, ), - ), + ) + .animate() + .scale(duration: 600.ms) + .then(delay: 200.ms) + .rotate(begin: 0, end: 0.1) + .then() + .rotate(begin: 0.1, end: 0), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - 'Topología de Red MDF/IDF', + 'Topología Interactiva de Red', style: TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, ), - ), + ).animate().fadeIn(delay: 300.ms).slideX(begin: -0.3, end: 0), Text( - 'Visualización interactiva de la infraestructura de telecomunicaciones', + 'Diagrama profesional con flutter_flow_chart', style: TextStyle( color: Colors.white.withOpacity(0.9), fontSize: 14, ), - ), + ).animate().fadeIn(delay: 500.ms).slideX(begin: -0.3, end: 0), ], ), ), @@ -136,20 +516,20 @@ class _TopologiaPageState extends State borderRadius: BorderRadius.circular(12), ), child: const Text( - 'V2.0', + 'FLOW', style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600, ), ), - ), + ).animate().fadeIn(delay: 700.ms).scale(), ], ), - ); + ).animate().fadeIn().slideY(begin: -0.3, end: 0); } - Widget _buildTopologyControls() { + Widget _buildAdvancedControls() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -173,20 +553,20 @@ class _TopologiaPageState extends State ), ), const SizedBox(width: 12), - _buildViewButton('rack', 'Rack', Icons.dns), + _buildViewButton('network', 'Diagrama Interactivo', Icons.hub), const SizedBox(width: 8), - _buildViewButton('network', 'Red', Icons.hub), + _buildViewButton('rack', 'Vista Rack', Icons.dns), const SizedBox(width: 8), - _buildViewButton('floor', 'Planta', Icons.map), + _buildViewButton('floor', 'Plano de Planta', Icons.map), ], ), ), - // Controles de zoom + // Controles de la topología Row( children: [ Text( - 'Zoom:', + 'Controles:', style: TextStyle( color: AppTheme.of(context).primaryText, fontWeight: FontWeight.w600, @@ -194,34 +574,26 @@ class _TopologiaPageState extends State ), const SizedBox(width: 12), IconButton( - onPressed: () => setState( - () => _zoomLevel = (_zoomLevel - 0.2).clamp(0.5, 2.0)), - icon: const Icon(Icons.zoom_out), - tooltip: 'Alejar', - ), - Text( - '${(_zoomLevel * 100).round()}%', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), + onPressed: () { + setState(() { + _buildNetworkTopology(); + }); + }, + icon: const Icon(Icons.refresh), + tooltip: 'Actualizar topología', ), IconButton( - onPressed: () => setState( - () => _zoomLevel = (_zoomLevel + 0.2).clamp(0.5, 2.0)), - icon: const Icon(Icons.zoom_in), - tooltip: 'Acercar', - ), - IconButton( - onPressed: () => setState(() => _zoomLevel = 1.0), + onPressed: () { + dashboard.setZoomFactor(1.0); + }, icon: const Icon(Icons.center_focus_strong), - tooltip: 'Restablecer zoom', + tooltip: 'Centrar vista', ), ], ), ], ), - ); + ).animate().fadeIn(delay: 200.ms).slideY(begin: -0.2, end: 0); } Widget _buildViewButton(String value, String label, IconData icon) { @@ -267,20 +639,19 @@ class _TopologiaPageState extends State ); } - Widget _buildTopologyView( - ComponentesProvider componentesProvider, bool isMediumScreen) { + Widget _buildProfessionalTopologyView(bool isMediumScreen) { return Container( decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, + color: const Color(0xFF0D1117), // Fondo oscuro profesional tipo GitHub borderRadius: BorderRadius.circular(16), border: Border.all( color: AppTheme.of(context).primaryColor.withOpacity(0.2), ), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 8), ), ], ), @@ -288,30 +659,28 @@ class _TopologiaPageState extends State borderRadius: BorderRadius.circular(16), child: Stack( children: [ - // Fondo con patrón de cuadrícula - _buildGridBackground(), + // Vista según selección + if (_selectedView == 'network') + _buildInteractiveFlowChart() + else if (_selectedView == 'rack') + _buildRackView(isMediumScreen) + else if (_selectedView == 'floor') + _buildFloorPlanView(isMediumScreen), - // Contenido principal según la vista seleccionada - Transform.scale( - scale: _zoomLevel, - child: _buildViewContent(componentesProvider, isMediumScreen), - ), - - // Leyenda flotante - if (isMediumScreen) + // Leyenda profesional + if (isMediumScreen && _selectedView == 'network') Positioned( top: 16, right: 16, - child: _buildLegend(), + child: _buildProfessionalLegend(), ), - // Controles móviles - if (!isMediumScreen) + // Panel de información + if (_selectedView == 'network') Positioned( - bottom: 16, + top: 16, left: 16, - right: 16, - child: _buildMobileControls(), + child: _buildInfoPanel(), ), ], ), @@ -319,801 +688,59 @@ class _TopologiaPageState extends State ); } - Widget _buildGridBackground() { - return CustomPaint( - painter: GridPainter( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - ), - child: Container(), - ); - } - - Widget _buildViewContent( - ComponentesProvider componentesProvider, bool isMediumScreen) { - switch (_selectedView) { - case 'rack': - return _buildRackView(componentesProvider, isMediumScreen); - case 'network': - return _buildNetworkView(componentesProvider, isMediumScreen); - case 'floor': - return _buildFloorView(componentesProvider, isMediumScreen); - default: - return _buildRackView(componentesProvider, isMediumScreen); - } - } - - Widget _buildRackView( - ComponentesProvider componentesProvider, bool isMediumScreen) { - final racks = componentesProvider.componentes - .where((c) => - componentesProvider - .getCategoriaById(c.categoriaId) - ?.nombre - ?.toLowerCase() - .contains('rack') ?? - false) - .toList(); - - if (racks.isEmpty) { - return _buildEmptyRackView(); - } - - return SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: isMediumScreen - ? _buildDesktopRackLayout(racks, componentesProvider) - : _buildMobileRackLayout(racks, componentesProvider), - ); - } - - Widget _buildEmptyRackView() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: Icon( - Icons.dns, - size: 48, - color: AppTheme.of(context).primaryColor, - ), - ), - const SizedBox(height: 16), - Text( - 'No hay racks configurados', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 8), - Text( - 'Agregue racks desde el inventario para visualizar la topología', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - ), - ), - ], - ), - ); - } - - Widget _buildDesktopRackLayout( - List racks, ComponentesProvider componentesProvider) { - return Wrap( - spacing: 32, - runSpacing: 32, - children: racks - .map((rack) => _buildRackWidget(rack, componentesProvider, true)) - .toList(), - ); - } - - Widget _buildMobileRackLayout( - List racks, ComponentesProvider componentesProvider) { - return Column( - children: racks - .map((rack) => Container( - margin: const EdgeInsets.only(bottom: 24), - child: _buildRackWidget(rack, componentesProvider, false), - )) - .toList(), - ); - } - - Widget _buildRackWidget( - dynamic rack, ComponentesProvider componentesProvider, bool isDesktop) { - // Obtener componentes que están en este rack - final rackComponents = componentesProvider.componentes - .where((c) => - c.ubicacion?.toLowerCase().contains(rack.nombre.toLowerCase()) ?? - false) - .toList(); - - return Container( - width: isDesktop ? 280 : double.infinity, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Colors.grey[800]!, - Colors.grey[900]!, - ], - ), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.grey[600]!, width: 2), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.3), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header del rack - Row( - children: [ - Icon( - Icons.dns, - color: Colors.blue[300], - size: 20, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - rack.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: rack.activo ? Colors.green : Colors.red, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - rack.activo ? 'ACTIVO' : 'INACTIVO', - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - - // Unidades del rack (simulación visual) - Container( - height: 200, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(4), - border: Border.all(color: Colors.grey[700]!), - ), - child: _buildRackUnits(rackComponents, componentesProvider), - ), - - const SizedBox(height: 12), - - // Información del rack - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${rackComponents.length} componentes', - style: TextStyle( - color: Colors.grey[300], - fontSize: 12, - ), - ), - if (rack.ubicacion != null) - Text( - rack.ubicacion!, - style: TextStyle( - color: Colors.grey[400], - fontSize: 10, - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildRackUnits( - List components, ComponentesProvider componentesProvider) { - return ListView.builder( - padding: const EdgeInsets.all(4), - itemCount: - (components.length + 1).clamp(1, 10), // Máximo 10 unidades visuales - itemBuilder: (context, index) { - if (index < components.length) { - final component = components[index]; - final categoria = - componentesProvider.getCategoriaById(component.categoriaId); - return Container( - height: 16, - margin: const EdgeInsets.only(bottom: 2), - decoration: BoxDecoration( - color: _getComponentColor(categoria?.nombre), - borderRadius: BorderRadius.circular(2), - border: Border.all(color: Colors.grey[600]!, width: 0.5), - ), - child: Row( - children: [ - const SizedBox(width: 4), - Icon( - _getComponentIcon(categoria?.nombre), - size: 10, - color: Colors.white, - ), - const SizedBox(width: 4), - Expanded( - child: Text( - component.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 8, - fontWeight: FontWeight.w500, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - } else { - // Unidad vacía - return Container( - height: 16, - margin: const EdgeInsets.only(bottom: 2), - decoration: BoxDecoration( - color: Colors.grey[800], - borderRadius: BorderRadius.circular(2), - border: Border.all(color: Colors.grey[700]!, width: 0.5), - ), - ); - } + Widget _buildInteractiveFlowChart() { + return FlowChart( + dashboard: dashboard, + onElementPressed: (context, position, element) { + _showElementDetails(element); }, - ); + onElementLongPressed: (context, position, element) { + _showElementContextMenu(context, position, element); + }, + onNewConnection: (source, target) { + _handleNewConnection(source, target); + }, + onDashboardTapped: (context, position) { + // Limpiar selecciones + }, + ).animate().fadeIn(duration: 800.ms).scale(begin: const Offset(0.95, 0.95)); } - Widget _buildNetworkView( - ComponentesProvider componentesProvider, bool isMediumScreen) { - return Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: _buildNetworkDiagram(componentesProvider, isMediumScreen), - ), - ); - } - - Widget _buildNetworkDiagram( - ComponentesProvider componentesProvider, bool isMediumScreen) { - final switches = componentesProvider.componentes - .where((c) => - componentesProvider - .getCategoriaById(c.categoriaId) - ?.nombre - ?.toLowerCase() - .contains('switch') ?? - false) - .toList(); - - final routers = componentesProvider.componentes - .where((c) => - componentesProvider - .getCategoriaById(c.categoriaId) - ?.nombre - ?.toLowerCase() - .contains('router') ?? - false) - .toList(); - - return Column( - children: [ - // Capa de routers/firewall - if (routers.isNotEmpty) ...[ - Text( - 'Capa de Enrutamiento', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Wrap( - spacing: 24, - runSpacing: 16, - children: routers - .map((router) => _buildNetworkNode( - router, - Icons.router, - Colors.red[400]!, - componentesProvider, - )) - .toList(), - ), - const SizedBox(height: 32), - - // Líneas de conexión - Container( - height: 2, - width: isMediumScreen ? 200 : 150, - color: AppTheme.of(context).primaryColor.withOpacity(0.5), - ), - const SizedBox(height: 32), - ], - - // Capa de switches - if (switches.isNotEmpty) ...[ - Text( - 'Capa de Conmutación', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Wrap( - spacing: 24, - runSpacing: 16, - children: switches - .map((switch_) => _buildNetworkNode( - switch_, - Icons.hub, - Colors.blue[400]!, - componentesProvider, - )) - .toList(), - ), - ], - - if (switches.isEmpty && routers.isEmpty) _buildEmptyNetworkView(), - ], - ); - } - - Widget _buildNetworkNode(dynamic component, IconData icon, Color color, - ComponentesProvider componentesProvider) { - return GestureDetector( - onTap: () => _showComponentDetails(component, componentesProvider), - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.5), width: 2), - boxShadow: [ - BoxShadow( - color: color.withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), - child: Icon( - icon, - color: Colors.white, - size: 24, - ), - ), - const SizedBox(height: 8), - Text( - component.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - if (component.ubicacion != null) - Text( - component.ubicacion!, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 10, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: component.activo ? Colors.green : Colors.red, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - component.activo ? 'ON' : 'OFF', - style: const TextStyle( - color: Colors.white, - fontSize: 8, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildFloorView( - ComponentesProvider componentesProvider, bool isMediumScreen) { - return Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: _buildFloorPlan(componentesProvider, isMediumScreen), - ), - ); - } - - Widget _buildFloorPlan( - ComponentesProvider componentesProvider, bool isMediumScreen) { - return Container( - width: isMediumScreen ? 600 : double.infinity, - height: isMediumScreen ? 400 : 300, - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.grey[400]!), - ), - child: Stack( - children: [ - // Representación simple de planta - _buildFloorLayout(), - - // Componentes distribuidos en la planta - ..._buildFloorComponents(componentesProvider), - ], - ), - ); - } - - Widget _buildFloorLayout() { - return CustomPaint( - painter: FloorPlanPainter(), - child: Container(), - ); - } - - List _buildFloorComponents(ComponentesProvider componentesProvider) { - final components = componentesProvider.componentes - .take(6) - .toList(); // Límite para visualización - final positions = [ - const Offset(0.2, 0.3), - const Offset(0.8, 0.3), - const Offset(0.2, 0.7), - const Offset(0.8, 0.7), - const Offset(0.5, 0.2), - const Offset(0.5, 0.8), - ]; - - return components.asMap().entries.map((entry) { - final index = entry.key; - final component = entry.value; - final position = positions[index % positions.length]; - - return Positioned( - left: position.dx * 580 + 10, // Ajuste por padding - top: position.dy * 380 + 10, - child: _buildFloorComponent(component, componentesProvider), - ); - }).toList(); - } - - Widget _buildFloorComponent( - dynamic component, ComponentesProvider componentesProvider) { - final categoria = - componentesProvider.getCategoriaById(component.categoriaId); - - return GestureDetector( - onTap: () => _showComponentDetails(component, componentesProvider), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: _getComponentColor(categoria?.nombre), - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - _getComponentIcon(categoria?.nombre), - color: Colors.white, - size: 16, - ), - const SizedBox(height: 4), - Text( - component.nombre.length > 8 - ? '${component.nombre.substring(0, 8)}...' - : component.nombre, - style: const TextStyle( - color: Colors.white, - fontSize: 8, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } - - Widget _buildEmptyNetworkView() { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: Icon( - Icons.hub, - size: 48, - color: AppTheme.of(context).primaryColor, - ), - ), - const SizedBox(height: 16), - Text( - 'No hay equipos de red configurados', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 8), - Text( - 'Agregue switches y routers desde el inventario', - textAlign: TextAlign.center, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 14, - ), - ), - ], - ); - } - - Widget _buildLegend() { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Leyenda', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - _buildLegendItem(Icons.router, 'Router/Firewall', Colors.red[400]!), - _buildLegendItem(Icons.hub, 'Switch', Colors.blue[400]!), - _buildLegendItem(Icons.dns, 'Rack', Colors.grey[600]!), - _buildLegendItem(Icons.cable, 'Cable', Colors.orange[400]!), - _buildLegendItem( - Icons.electrical_services, 'UPS', Colors.yellow[700]!), - ], - ), - ); - } - - Widget _buildLegendItem(IconData icon, String label, Color color) { - return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 12, color: color), - const SizedBox(width: 6), - Text( - label, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 10, - ), - ), - ], - ), - ); - } - - Widget _buildMobileControls() { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Selector de vista móvil - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _buildMobileViewButton('rack', 'Rack', Icons.dns), - _buildMobileViewButton('network', 'Red', Icons.hub), - _buildMobileViewButton('floor', 'Planta', Icons.map), - ], - ), - const SizedBox(height: 12), - - // Controles de zoom móvil - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - onPressed: () => setState( - () => _zoomLevel = (_zoomLevel - 0.2).clamp(0.5, 2.0)), - icon: const Icon(Icons.zoom_out), - iconSize: 20, - ), - Text( - '${(_zoomLevel * 100).round()}%', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - ), - IconButton( - onPressed: () => setState( - () => _zoomLevel = (_zoomLevel + 0.2).clamp(0.5, 2.0)), - icon: const Icon(Icons.zoom_in), - iconSize: 20, - ), - ], - ), - ], - ), - ); - } - - Widget _buildMobileViewButton(String value, String label, IconData icon) { - final isSelected = _selectedView == value; - return GestureDetector( - onTap: () => setState(() => _selectedView = value), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - decoration: BoxDecoration( - color: isSelected - ? AppTheme.of(context).primaryColor - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - icon, - size: 16, - color: - isSelected ? Colors.white : AppTheme.of(context).primaryColor, - ), - const SizedBox(height: 4), - Text( - label, - style: TextStyle( - color: isSelected - ? Colors.white - : AppTheme.of(context).primaryColor, - fontSize: 10, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ); - } - - void _showComponentDetails( - dynamic component, ComponentesProvider componentesProvider) { - final categoria = - componentesProvider.getCategoriaById(component.categoriaId); + void _showElementDetails(FlowElement element) { + final data = element.data as Map; showDialog( context: context, builder: (context) => AlertDialog( title: Row( children: [ - Icon( - _getComponentIcon(categoria?.nombre), - color: AppTheme.of(context).primaryColor, - ), + Icon(_getIconForType(data['type']), + color: _getColorForType(data['type'])), const SizedBox(width: 8), - Expanded( - child: Text( - component.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), + Expanded(child: Text(data['name'])), ], ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildDetailRow('Categoría', categoria?.nombre ?? 'N/A'), - _buildDetailRow('Estado', component.activo ? 'Activo' : 'Inactivo'), - _buildDetailRow('En uso', component.enUso ? 'Sí' : 'No'), - if (component.ubicacion != null) - _buildDetailRow('Ubicación', component.ubicacion!), - if (component.descripcion != null) - _buildDetailRow('Descripción', component.descripcion!), - ], + content: Container( + width: double.maxFinite, + constraints: const BoxConstraints(maxHeight: 400), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Tipo: ${data['type']}'), + const SizedBox(height: 8), + Text('Estado: ${_getStatusText(data['status'])}'), + if (data['ports'] != null) ...[ + const SizedBox(height: 8), + Text('Puertos: ${data['ports']}'), + ], + const SizedBox(height: 12), + const Text('Descripción:', + style: TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Text(data['description']), + ], + ), ), actions: [ TextButton( @@ -1125,28 +752,105 @@ class _TopologiaPageState extends State ); } - Widget _buildDetailRow(String label, String value) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( + void _showElementContextMenu( + BuildContext context, Offset position, FlowElement element) { + // Implementar menú contextual + } + + void _handleNewConnection(FlowElement source, FlowElement target) { + // Mostrar diálogo para configurar nueva conexión + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Nueva Conexión'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Conectar desde: ${(source.data as Map)['name']}'), + Text('Hacia: ${(target.data as Map)['name']}'), + const SizedBox(height: 16), + const Text('Seleccione el tipo de conexión:'), + // Aquí podrías agregar controles para seleccionar tipo de cable + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancelar'), + ), + ElevatedButton( + onPressed: () { + // Crear la conexión manualmente + final newConnection = ConnectionParams( + destElementId: target.id, + arrowParams: ArrowParams( + color: Colors.green, + thickness: 3, + ), + ); + + source.next = [...source.next ?? [], newConnection]; + Navigator.of(context).pop(); + setState(() {}); + }, + child: const Text('Conectar'), + ), + ], + ), + ); + } + + Widget _buildProfessionalLegend() { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.8), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.white.withOpacity(0.2)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: 80, - child: Text( - '$label:', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontWeight: FontWeight.w600, - ), + const Text( + 'Leyenda', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, ), ), - Expanded( - child: Text( - value, - style: TextStyle( - color: AppTheme.of(context).primaryText, - ), + const SizedBox(height: 8), + _buildLegendItem(const Color(0xFF2196F3), 'MDF', Icons.router), + _buildLegendItem(const Color(0xFF4CAF50), 'IDF Activo', Icons.hub), + _buildLegendItem( + const Color(0xFFFF9800), 'IDF Advertencia', Icons.hub), + _buildLegendItem( + const Color(0xFF9C27B0), 'Switch Acceso', Icons.network_check), + _buildLegendItem(const Color(0xFFE91E63), 'Servidor', Icons.dns), + const SizedBox(height: 6), + _buildLegendItem(Colors.cyan, 'Fibra Óptica', Icons.cable), + _buildLegendItem(Colors.yellow, 'Cable UTP', Icons.cable), + _buildLegendItem(Colors.purple, 'Conexión Dedicada', Icons.cable), + _buildLegendItem(Colors.grey, 'Desconectado', Icons.cable), + ], + ), + ).animate().fadeIn(delay: 1000.ms).slideX(begin: 0.3); + } + + Widget _buildLegendItem(Color color, String label, IconData icon) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 12), + const SizedBox(width: 6), + Text( + label, + style: const TextStyle( + color: Colors.white70, + fontSize: 9, ), ), ], @@ -1154,120 +858,651 @@ class _TopologiaPageState extends State ); } - Color _getComponentColor(String? categoryName) { - if (categoryName == null) return Colors.grey[600]!; - - final name = categoryName.toLowerCase(); - if (name.contains('switch')) return Colors.blue[600]!; - if (name.contains('router') || name.contains('firewall')) - return Colors.red[600]!; - if (name.contains('cable')) return Colors.orange[600]!; - if (name.contains('rack')) return Colors.grey[700]!; - if (name.contains('ups')) return Colors.yellow[700]!; - if (name.contains('patch') || name.contains('panel')) - return Colors.purple[600]!; - return Colors.teal[600]!; + Widget _buildInfoPanel() { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.8), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.white.withOpacity(0.2)), + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Información', + style: TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text( + '• Arrastra los nodos para reposicionar', + style: TextStyle(color: Colors.white70, fontSize: 10), + ), + Text( + '• Haz clic en un nodo para ver detalles', + style: TextStyle(color: Colors.white70, fontSize: 10), + ), + Text( + '• Arrastra desde los puntos de conexión', + style: TextStyle(color: Colors.white70, fontSize: 10), + ), + Text( + '• Usa zoom con scroll del mouse', + style: TextStyle(color: Colors.white70, fontSize: 10), + ), + ], + ), + ).animate().fadeIn(delay: 1200.ms).slideX(begin: -0.3); } - IconData _getComponentIcon(String? categoryName) { - if (categoryName == null) return Icons.device_unknown; - - final name = categoryName.toLowerCase(); - if (name.contains('switch')) return Icons.hub; - if (name.contains('router') || name.contains('firewall')) - return Icons.router; - if (name.contains('cable')) return Icons.cable; - if (name.contains('rack')) return Icons.dns; - if (name.contains('ups')) return Icons.electrical_services; - if (name.contains('patch') || name.contains('panel')) - return Icons.view_module; - return Icons.memory; + Widget _buildRackView(bool isMediumScreen) { + return Container( + padding: const EdgeInsets.all(24), + child: const Center( + child: Text( + 'Vista de Racks - En desarrollo', + style: TextStyle(color: Colors.white, fontSize: 18), + ), + ), + ); } -} -// Painter personalizado para la cuadrícula de fondo -class GridPainter extends CustomPainter { - final Color color; + Widget _buildFloorPlanView(bool isMediumScreen) { + return Container( + padding: const EdgeInsets.all(24), + child: const Center( + child: Text( + 'Plano de Planta - En desarrollo', + style: TextStyle(color: Colors.white, fontSize: 18), + ), + ), + ); + } - GridPainter({required this.color}); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..strokeWidth = 0.5; - - const spacing = 20.0; - - // Líneas verticales - for (double x = 0; x < size.width; x += spacing) { - canvas.drawLine(Offset(x, 0), Offset(x, size.height), paint); - } - - // Líneas horizontales - for (double y = 0; y < size.height; y += spacing) { - canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + IconData _getIconForType(String type) { + switch (type) { + case 'MDF': + return Icons.router; + case 'IDF': + return Icons.hub; + case 'AccessSwitch': + return Icons.network_check; + case 'Server': + return Icons.dns; + default: + return Icons.device_unknown; } } - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} - -// Painter para el plano de planta -class FloorPlanPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = Colors.grey[400]! - ..strokeWidth = 2 - ..style = PaintingStyle.stroke; - - // Dibujar habitaciones/áreas - final rect1 = - Rect.fromLTWH(20, 20, size.width * 0.4 - 20, size.height * 0.6); - final rect2 = Rect.fromLTWH( - size.width * 0.6, 20, size.width * 0.4 - 20, size.height * 0.6); - final rect3 = Rect.fromLTWH( - 20, size.height * 0.7, size.width - 40, size.height * 0.3 - 20); - - canvas.drawRect(rect1, paint); - canvas.drawRect(rect2, paint); - canvas.drawRect(rect3, paint); - - // Etiquetas de áreas - final textPainter = TextPainter( - textDirection: TextDirection.ltr, - ); - - textPainter.text = const TextSpan( - text: 'MDF', - style: TextStyle( - color: Colors.grey, fontSize: 12, fontWeight: FontWeight.bold), - ); - textPainter.layout(); - textPainter.paint(canvas, - Offset(rect1.center.dx - textPainter.width / 2, rect1.center.dy)); - - textPainter.text = const TextSpan( - text: 'IDF', - style: TextStyle( - color: Colors.grey, fontSize: 12, fontWeight: FontWeight.bold), - ); - textPainter.layout(); - textPainter.paint(canvas, - Offset(rect2.center.dx - textPainter.width / 2, rect2.center.dy)); - - textPainter.text = const TextSpan( - text: 'Área de Trabajo', - style: TextStyle( - color: Colors.grey, fontSize: 12, fontWeight: FontWeight.bold), - ); - textPainter.layout(); - textPainter.paint(canvas, - Offset(rect3.center.dx - textPainter.width / 2, rect3.center.dy)); + Color _getColorForType(String type) { + switch (type) { + case 'MDF': + return const Color(0xFF2196F3); + case 'IDF': + return const Color(0xFF4CAF50); + case 'AccessSwitch': + return const Color(0xFF9C27B0); + case 'Server': + return const Color(0xFFE91E63); + default: + return Colors.grey; + } } - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + String _getStatusText(String status) { + switch (status) { + case 'active': + return '🟢 Activo'; + case 'warning': + return '🟡 Advertencia'; + case 'error': + return '🔴 Error'; + case 'disconnected': + return '⚫ Desconectado'; + default: + return '❓ Desconocido'; + } + } + + Future _loadRealTopologyData() async { + setState(() { + _isLoading = true; + }); + + try { + final componentesProvider = + Provider.of(context, listen: false); + + // Verificar que hay un negocio seleccionado en el provider + if (componentesProvider.negocioSeleccionadoId == null) { + // Si no hay negocio seleccionado, mostrar mensaje + setState(() { + _isLoading = false; + }); + _showNoBusinessSelectedDialog(); + return; + } + + // Construir la topología con datos reales del negocio seleccionado + await _buildRealNetworkTopology(); + } catch (e) { + print('Error al cargar datos de topología: ${e.toString()}'); + _showErrorDialog('Error al cargar la topología: ${e.toString()}'); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + Future _buildRealNetworkTopology() 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'); + final switchesAcceso = componentesProvider + .getComponentesPorTipo('switch') + .where((s) => !mdfComponents.contains(s) && !idfComponents.contains(s)) + .toList(); + final routers = componentesProvider.getComponentesPorTipo('router'); + final servidores = componentesProvider.getComponentesPorTipo('servidor'); + + print('Componentes encontrados:'); + print('- MDF: ${mdfComponents.length}'); + print('- IDF: ${idfComponents.length}'); + print('- Switches de acceso: ${switchesAcceso.length}'); + print('- Routers: ${routers.length}'); + print('- Servidores: ${servidores.length}'); + + double currentX = 100; + double currentY = 100; + const double espacioX = 200; + const double espacioY = 150; + + Map elementosMap = {}; + + // Crear elementos MDF + if (mdfComponents.isNotEmpty) { + final mdfElement = _createMDFElement( + mdfComponents, Offset(currentX + espacioX * 2, currentY)); + dashboard.addElement(mdfElement); + elementosMap[mdfComponents.first.id] = mdfElement; + currentY += espacioY; + } + + // Crear elementos IDF + double idfX = currentX; + for (var idfComp in idfComponents) { + final idfElement = + _createIDFElement(idfComp, Offset(idfX, currentY + espacioY)); + dashboard.addElement(idfElement); + elementosMap[idfComp.id] = idfElement; + idfX += espacioX; + } + + // Crear switches de acceso + double switchX = currentX; + currentY += espacioY * 2; + for (var switchComp in switchesAcceso) { + final switchElement = + _createSwitchElement(switchComp, Offset(switchX, currentY)); + dashboard.addElement(switchElement); + elementosMap[switchComp.id] = switchElement; + switchX += espacioX * 0.8; + } + + // Crear servidores + if (servidores.isNotEmpty) { + currentY += espacioY; + double serverX = currentX + espacioX; + for (var servidor in servidores) { + final serverElement = + _createServerElement(servidor, Offset(serverX, currentY)); + dashboard.addElement(serverElement); + elementosMap[servidor.id] = serverElement; + serverX += espacioX * 0.8; + } + } + + // Crear routers/firewalls + if (routers.isNotEmpty) { + double routerX = currentX; + for (var router in routers) { + final routerElement = _createRouterElement( + router, Offset(routerX, currentY - espacioY * 3)); + dashboard.addElement(routerElement); + elementosMap[router.id] = routerElement; + routerX += espacioX; + } + } + + // Crear conexiones basadas en la base de datos + await _createRealConnections(elementosMap, componentesProvider); + + // Si no hay elementos reales, mostrar mensaje + if (elementosMap.isEmpty) { + _showNoComponentsMessage(); + } + + setState(() {}); + } + + FlowElement _createMDFElement( + 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}', + textColor: Colors.white, + textSize: 14, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: const Color(0xFF2196F3), + borderColor: const Color(0xFF1976D2), + borderThickness: 3, + elevation: 8, + data: { + 'type': 'MDF', + 'componenteId': mainComponent.id, + 'name': mainComponent.nombre, + 'status': mainComponent.activo ? 'active' : 'disconnected', + 'description': mainComponent.descripcion ?? 'Main Distribution Frame', + 'ubicacion': mainComponent.ubicacion ?? 'Sin ubicación', + 'categoria': categoria?.nombre ?? 'Sin categoría', + 'componentes': mdfComponents.length, + }, + handlers: [ + Handler.bottomCenter, + Handler.leftCenter, + Handler.rightCenter, + ], + ); + } + + FlowElement _createIDFElement(Componente idfComponent, Offset position) { + final componentesProvider = + Provider.of(context, listen: false); + final categoria = + componentesProvider.getCategoriaById(idfComponent.categoriaId); + + return FlowElement( + position: position, + size: const Size(160, 120), + text: 'IDF\n${idfComponent.nombre}', + textColor: Colors.white, + textSize: 12, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: idfComponent.enUso + ? const Color(0xFF4CAF50) + : const Color(0xFFFF9800), + borderColor: idfComponent.enUso + ? const Color(0xFF388E3C) + : const Color(0xFFF57C00), + borderThickness: 2, + elevation: 6, + data: { + 'type': 'IDF', + 'componenteId': idfComponent.id, + 'name': idfComponent.nombre, + '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', + 'enUso': idfComponent.enUso, + }, + handlers: [ + Handler.topCenter, + Handler.bottomCenter, + Handler.leftCenter, + Handler.rightCenter, + ], + ); + } + + FlowElement _createSwitchElement( + Componente switchComponent, Offset position) { + final componentesProvider = + Provider.of(context, listen: false); + final categoria = + componentesProvider.getCategoriaById(switchComponent.categoriaId); + + return FlowElement( + position: position, + size: const Size(140, 100), + text: 'Switch\n${switchComponent.nombre}', + textColor: Colors.white, + textSize: 10, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: switchComponent.activo + ? const Color(0xFF9C27B0) + : const Color(0xFF757575), + borderColor: switchComponent.activo + ? const Color(0xFF7B1FA2) + : const Color(0xFF424242), + borderThickness: 2, + elevation: switchComponent.activo ? 4 : 2, + data: { + 'type': 'AccessSwitch', + 'componenteId': switchComponent.id, + 'name': switchComponent.nombre, + 'status': switchComponent.activo ? 'active' : 'disconnected', + 'description': switchComponent.descripcion ?? 'Switch de Acceso', + 'ubicacion': switchComponent.ubicacion ?? 'Sin ubicación', + 'categoria': categoria?.nombre ?? 'Sin categoría', + }, + handlers: [ + Handler.topCenter, + ], + ); + } + + FlowElement _createServerElement( + Componente serverComponent, Offset position) { + final componentesProvider = + Provider.of(context, listen: false); + final categoria = + componentesProvider.getCategoriaById(serverComponent.categoriaId); + + return FlowElement( + position: position, + size: const Size(160, 100), + text: 'Servidor\n${serverComponent.nombre}', + textColor: Colors.white, + textSize: 12, + textIsBold: true, + kind: ElementKind.rectangle, + backgroundColor: const Color(0xFFE91E63), + borderColor: const Color(0xFFC2185B), + borderThickness: 3, + elevation: 6, + data: { + 'type': 'Server', + 'componenteId': serverComponent.id, + 'name': serverComponent.nombre, + 'status': serverComponent.activo ? 'active' : 'disconnected', + 'description': serverComponent.descripcion ?? 'Servidor', + 'ubicacion': serverComponent.ubicacion ?? 'Sin ubicación', + 'categoria': categoria?.nombre ?? 'Sin categoría', + }, + 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, + ComponentesProvider componentesProvider) async { + // Obtener conexiones reales de la base de datos + final conexiones = componentesProvider.conexiones; + + print('Creando ${conexiones.length} conexiones...'); + + for (var conexion in conexiones) { + final origenElement = elementosMap[conexion.componenteOrigenId]; + final destinoElement = elementosMap[conexion.componenteDestinoId]; + + 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); + + print('Conectando: ${origenElement.text} -> ${destinoElement.text}'); + + // Agregar la conexión al elemento origen + origenElement.next = [ + ...origenElement.next ?? [], + ConnectionParams( + destElementId: destinoElement.id, + arrowParams: ArrowParams( + color: colorConexion, + thickness: grosorConexion, + ), + ), + ]; + } + } + + // 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); + } + } + + 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 + } + + 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; + } + + 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, + ), + ), + ]; + } + } + } + + void _showNoComponentsMessage() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Sin componentes'), + 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.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Entendido'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + // Ir al módulo de inventario (esto depende de tu estructura de navegación) + // context.go('/infrastructure/inventory'); + }, + child: const Text('Ir a Inventario'), + ), + ], + ), + ); + } + + void _showNoBusinessSelectedDialog() { + 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.', + ), + 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), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cerrar'), + ), + ], + ), + ); + } } diff --git a/lib/pages/infrastructure/widgets/componentes_cards_view.dart b/lib/pages/infrastructure/widgets/componentes_cards_view.dart index c8371b1..b4774d8 100644 --- a/lib/pages/infrastructure/widgets/componentes_cards_view.dart +++ b/lib/pages/infrastructure/widgets/componentes_cards_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; +import 'package:nethive_neo/pages/infrastructure/widgets/edit_componente_dialog.dart'; import 'package:nethive_neo/theme/theme.dart'; class ComponentesCardsView extends StatefulWidget { @@ -443,10 +444,7 @@ class _ComponentesCardsViewState extends State icon: Icons.edit, color: AppTheme.of(context).primaryColor, onTap: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Editar próximamente')), - ); + _showEditComponenteDialog(componente); }, ), const SizedBox(width: 4), @@ -729,4 +727,15 @@ class _ComponentesCardsViewState extends State ), ); } + + void _showEditComponenteDialog(dynamic componente) { + final provider = Provider.of(context, listen: false); + showDialog( + context: context, + builder: (context) => EditComponenteDialog( + provider: provider, + componente: componente, + ), + ); + } } diff --git a/lib/pages/infrastructure/widgets/edit_componente_dialog.dart b/lib/pages/infrastructure/widgets/edit_componente_dialog.dart new file mode 100644 index 0000000..955ed8e --- /dev/null +++ b/lib/pages/infrastructure/widgets/edit_componente_dialog.dart @@ -0,0 +1,1499 @@ +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; + final Componente componente; + + const EditComponenteDialog({ + Key? key, + required this.provider, + required this.componente, + }) : super(key: key); + + @override + State createState() => _EditComponenteDialogState(); +} + +class _EditComponenteDialogState extends State + with TickerProviderStateMixin { + final _formKey = GlobalKey(); + late TextEditingController _nombreController; + late TextEditingController _descripcionController; + late TextEditingController _ubicacionController; + + bool _isLoading = false; + late AnimationController _scaleController; + late AnimationController _slideController; + late AnimationController _fadeController; + late Animation _scaleAnimation; + late Animation _slideAnimation; + late Animation _fadeAnimation; + bool _isAnimationInitialized = false; + + // Variables del formulario + late int _categoriaSeleccionada; + late bool _activo; + late bool _enUso; + bool _actualizarImagen = false; + + @override + void initState() { + super.initState(); + _initializeControllers(); + _initializeAnimations(); + // Escuchar cambios del provider + widget.provider.addListener(_onProviderChanged); + } + + void _initializeControllers() { + _nombreController = TextEditingController(text: widget.componente.nombre); + _descripcionController = + TextEditingController(text: widget.componente.descripcion ?? ''); + _ubicacionController = + TextEditingController(text: widget.componente.ubicacion ?? ''); + + _categoriaSeleccionada = widget.componente.categoriaId; + _activo = widget.componente.activo; + _enUso = widget.componente.enUso; + } + + void _onProviderChanged() { + if (mounted) { + setState(() { + // Forzar rebuild cuando cambie el provider + }); + } + } + + void _initializeAnimations() { + _scaleController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + _slideController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + _fadeController = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: this, + ); + + _scaleAnimation = CurvedAnimation( + parent: _scaleController, + curve: Curves.elasticOut, + ); + _slideAnimation = Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _slideController, + curve: Curves.easeOutBack, + )); + _fadeAnimation = CurvedAnimation( + parent: _fadeController, + curve: Curves.easeInOut, + ); + + // Pequeño delay para asegurar que el widget esté completamente montado + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _isAnimationInitialized = true; + }); + _startAnimations(); + } + }); + } + + void _startAnimations() { + _fadeController.forward(); + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) _scaleController.forward(); + }); + Future.delayed(const Duration(milliseconds: 200), () { + if (mounted) _slideController.forward(); + }); + } + + @override + void dispose() { + widget.provider.removeListener(_onProviderChanged); + _scaleController.dispose(); + _slideController.dispose(); + _fadeController.dispose(); + _nombreController.dispose(); + _descripcionController.dispose(); + _ubicacionController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!_isAnimationInitialized) { + return const SizedBox.shrink(); + } + + // Detectar el tamaño de pantalla con mejor precisión + final screenSize = MediaQuery.of(context).size; + final isDesktop = screenSize.width > 1024; + final isTablet = screenSize.width > 768 && screenSize.width <= 1024; + final isMobile = screenSize.width <= 768; + + // Ajustar dimensiones según el tipo de pantalla para mejor responsividad + double maxWidth; + double maxHeight; + EdgeInsets insetPadding; + + if (isMobile) { + // Configuración específica para smartphones + maxWidth = screenSize.width * 0.95; // 95% del ancho de pantalla + maxHeight = screenSize.height * 0.9; // 90% del alto de pantalla + insetPadding = const EdgeInsets.all(10); + } else if (isTablet) { + // Configuración para tablets + maxWidth = 750.0; + maxHeight = 700.0; + insetPadding = const EdgeInsets.all(20); + } else { + // Configuración para desktop + maxWidth = 1000.0; + maxHeight = 750.0; + insetPadding = const EdgeInsets.all(40); + } + + return AnimatedBuilder( + animation: + Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]), + builder: (context, child) { + return FadeTransition( + opacity: _fadeAnimation, + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: insetPadding, + child: Transform.scale( + scale: _scaleAnimation.value, + child: Container( + width: maxWidth, + height: maxHeight, + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: maxHeight, + minHeight: isMobile ? 400 : (isDesktop ? 650 : 500), + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(isMobile ? 20 : 30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + blurRadius: 40, + offset: const Offset(0, 20), + spreadRadius: 8, + ), + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 60, + offset: const Offset(0, 10), + spreadRadius: 2, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(isMobile ? 20 : 30), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryBackground, + AppTheme.of(context).secondaryBackground, + AppTheme.of(context).tertiaryBackground, + ], + stops: const [0.0, 0.6, 1.0], + ), + ), + child: isDesktop + ? _buildDesktopLayout() + : _buildMobileLayout(isMobile), + ), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildDesktopLayout() { + final categoria = widget.provider.getCategoriaById(_categoriaSeleccionada); + + return Row( + children: [ + // Header lateral compacto para desktop + Container( + width: 300, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + AppTheme.of(context).tertiaryColor, + ], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.5), + blurRadius: 25, + offset: const Offset(5, 0), + ), + ], + ), + child: SlideTransition( + position: _slideAnimation, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 25), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Imagen del componente + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + Colors.white.withOpacity(0.4), + Colors.white.withOpacity(0.1), + Colors.transparent, + ], + ), + border: Border.all( + color: Colors.white.withOpacity(0.6), + width: 3, + ), + boxShadow: [ + BoxShadow( + color: Colors.white.withOpacity(0.4), + blurRadius: 20, + spreadRadius: 5, + ), + ], + ), + child: ClipOval( + child: widget.componente.imagenUrl != null && + widget.componente.imagenUrl!.isNotEmpty + ? Image.network( + "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/componentes/${widget.componente.imagenUrl}", + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + padding: const EdgeInsets.all(20), + child: const Icon( + Icons.devices, + color: Colors.white, + size: 40, + ), + ); + }, + ) + : Container( + padding: const EdgeInsets.all(20), + child: const Icon( + Icons.devices, + color: Colors.white, + size: 40, + ), + ), + ), + ), + const SizedBox(height: 20), + + // Título compacto + Text( + 'Editar Componente', + style: TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + + Container( + padding: + const EdgeInsets.symmetric(horizontal: 15, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Colors.white.withOpacity(0.3), + ), + ), + child: Text( + '📝 ${widget.componente.nombre}', + style: TextStyle( + color: Colors.white.withOpacity(0.95), + fontSize: 14, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + + const SizedBox(height: 16), + + // Info adicional + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Text( + 'Categoría: ${categoria?.nombre ?? 'N/A'}', + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Text( + 'ID: ${widget.componente.id.substring(0, 8)}...', + style: TextStyle( + color: Colors.white.withOpacity(0.7), + fontSize: 10, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ], + ), + ), + ), + ), + + // Contenido principal del formulario + Expanded( + child: Padding( + padding: const EdgeInsets.all(25), + child: Form( + key: _formKey, + child: Column( + children: [ + // Formulario en columnas para aprovechar el espacio + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + // Primera fila - Nombre y Categoría + Row( + children: [ + Expanded( + flex: 2, + child: _buildCompactFormField( + controller: _nombreController, + label: 'Nombre del componente', + hint: 'Ej: Switch Core Principal', + icon: Icons.devices_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El nombre es requerido'; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildCategoriaDropdown(), + ), + ], + ), + + const SizedBox(height: 16), + + // Segunda fila - Ubicación + _buildCompactFormField( + controller: _ubicacionController, + label: 'Ubicación', + hint: 'Ej: MDF Principal - Rack 1', + icon: Icons.location_on_rounded, + validator: (value) { + // La ubicación es opcional + return null; + }, + ), + + const SizedBox(height: 16), + + // Tercera fila - Descripción + _buildCompactFormField( + controller: _descripcionController, + label: 'Descripción', + hint: 'Descripción detallada del componente', + icon: Icons.description_rounded, + maxLines: 3, + validator: (value) { + // La descripción es opcional + return null; + }, + ), + + const SizedBox(height: 20), + + // Switches de estado + Row( + children: [ + Expanded( + child: _buildStatusSwitch( + 'Activo', + 'El componente está operativo', + _activo, + (value) => setState(() => _activo = value), + Icons.power_settings_new, + Colors.green, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildStatusSwitch( + 'En Uso', + 'El componente está siendo utilizado', + _enUso, + (value) => setState(() => _enUso = value), + Icons.trending_up, + Colors.orange, + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Sección de imagen + _buildImageSection(), + + const SizedBox(height: 25), + + // Botones de acción + Row( + children: [ + // Botón cancelar + Expanded( + child: Container( + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppTheme.of(context) + .secondaryText + .withOpacity(0.4), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: TextButton( + onPressed: _isLoading + ? null + : () { + widget.provider.resetFormData(); + Navigator.of(context).pop(); + }, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.close_rounded, + color: AppTheme.of(context) + .secondaryText, + size: 18, + ), + const SizedBox(width: 8), + Text( + 'Cancelar', + style: TextStyle( + color: AppTheme.of(context) + .secondaryText, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(width: 20), + + // Botón guardar cambios + Expanded( + flex: 2, + child: Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + AppTheme.of(context).tertiaryColor, + ], + ), + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.5), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: ElevatedButton( + onPressed: + _isLoading ? null : _guardarCambios, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: _isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: + AlwaysStoppedAnimation( + Colors.white), + ), + ) + : Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: const [ + Icon( + Icons.save_rounded, + color: Colors.white, + size: 20, + ), + SizedBox(width: 12), + Text( + 'Guardar Cambios', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } + + Widget _buildMobileLayout(bool isMobile) { + final categoria = widget.provider.getCategoriaById(_categoriaSeleccionada); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header espectacular con animación + SlideTransition( + position: _slideAnimation, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 25), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + AppTheme.of(context).tertiaryColor, + ], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.5), + blurRadius: 25, + offset: const Offset(0, 15), + spreadRadius: 2, + ), + ], + ), + child: Column( + children: [ + // Imagen del componente + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + Colors.white.withOpacity(0.4), + Colors.white.withOpacity(0.1), + Colors.transparent, + ], + ), + border: Border.all( + color: Colors.white.withOpacity(0.6), + width: 3, + ), + boxShadow: [ + BoxShadow( + color: Colors.white.withOpacity(0.4), + blurRadius: 20, + spreadRadius: 5, + ), + ], + ), + child: ClipOval( + child: widget.componente.imagenUrl != null && + widget.componente.imagenUrl!.isNotEmpty + ? Image.network( + "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/componentes/${widget.componente.imagenUrl}", + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + padding: const EdgeInsets.all(20), + child: const Icon( + Icons.devices, + color: Colors.white, + size: 35, + ), + ); + }, + ) + : Container( + padding: const EdgeInsets.all(20), + child: const Icon( + Icons.devices, + color: Colors.white, + size: 35, + ), + ), + ), + ), + + const SizedBox(height: 16), + + // Título + Text( + 'Editar Componente', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + ), + + const SizedBox(height: 8), + + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Colors.white.withOpacity(0.3), + ), + ), + child: Text( + '📝 ${widget.componente.nombre}', + style: TextStyle( + color: Colors.white.withOpacity(0.95), + fontSize: 14, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ), + + // Contenido del formulario + Flexible( + child: SingleChildScrollView( + padding: const EdgeInsets.all(25), + child: Form( + key: _formKey, + child: Column( + children: [ + // Campos del formulario + _buildCompactFormField( + controller: _nombreController, + label: 'Nombre del componente', + hint: 'Ej: Switch Core Principal', + icon: Icons.devices_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El nombre es requerido'; + } + return null; + }, + ), + + _buildCategoriaDropdown(), + + _buildCompactFormField( + controller: _ubicacionController, + label: 'Ubicación', + hint: 'Ej: MDF Principal - Rack 1', + icon: Icons.location_on_rounded, + ), + + _buildCompactFormField( + controller: _descripcionController, + label: 'Descripción', + hint: 'Descripción detallada del componente', + icon: Icons.description_rounded, + maxLines: 3, + ), + + const SizedBox(height: 20), + + // Switches de estado + _buildStatusSwitch( + 'Activo', + 'El componente está operativo', + _activo, + (value) => setState(() => _activo = value), + Icons.power_settings_new, + Colors.green, + ), + + const SizedBox(height: 12), + + _buildStatusSwitch( + 'En Uso', + 'El componente está siendo utilizado', + _enUso, + (value) => setState(() => _enUso = value), + Icons.trending_up, + Colors.orange, + ), + + const SizedBox(height: 20), + + // Sección de imagen + _buildImageSection(), + + const SizedBox(height: 25), + + // Botones de acción + Row( + children: [ + // Botón cancelar + Expanded( + child: Container( + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppTheme.of(context) + .secondaryText + .withOpacity(0.4), + width: 2, + ), + ), + child: TextButton( + onPressed: _isLoading + ? null + : () { + widget.provider.resetFormData(); + Navigator.of(context).pop(); + }, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.close_rounded, + color: AppTheme.of(context).secondaryText, + size: 18, + ), + const SizedBox(width: 8), + Text( + 'Cancelar', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(width: 16), + + // Botón guardar + Expanded( + flex: 2, + child: Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + AppTheme.of(context).tertiaryColor, + ], + ), + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.5), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: ElevatedButton( + onPressed: _isLoading ? null : _guardarCambios, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: _isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon( + Icons.save_rounded, + color: Colors.white, + size: 20, + ), + SizedBox(width: 10), + Text( + 'Guardar', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ); + } + + Widget _buildCompactFormField({ + required TextEditingController controller, + required String label, + required String hint, + required IconData icon, + int maxLines = 1, + TextInputType? keyboardType, + String? Function(String?)? validator, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + child: TextFormField( + controller: controller, + maxLines: maxLines, + keyboardType: keyboardType, + validator: validator, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + decoration: InputDecoration( + labelText: label, + hintText: hint, + prefixIcon: Container( + margin: const EdgeInsets.all(8), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Icon( + icon, + color: Colors.white, + size: 18, + ), + ), + labelStyle: TextStyle( + color: AppTheme.of(context).primaryColor, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + hintStyle: TextStyle( + color: AppTheme.of(context).secondaryText.withOpacity(0.7), + fontSize: 12, + ), + filled: true, + fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.7), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 2, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide( + color: AppTheme.of(context).primaryColor, + width: 2, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: const BorderSide( + color: Colors.red, + width: 2, + ), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + ), + ), + ); + } + + Widget _buildCategoriaDropdown() { + return Container( + margin: const EdgeInsets.only(bottom: 16), + child: DropdownButtonFormField( + value: _categoriaSeleccionada, + onChanged: (value) { + if (value != null) { + setState(() { + _categoriaSeleccionada = value; + }); + } + }, + validator: (value) { + if (value == null) { + return 'Seleccione una categoría'; + } + return null; + }, + decoration: InputDecoration( + labelText: 'Categoría', + prefixIcon: Container( + margin: const EdgeInsets.all(8), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: const Icon( + Icons.category_rounded, + color: Colors.white, + size: 18, + ), + ), + labelStyle: TextStyle( + color: AppTheme.of(context).primaryColor, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + filled: true, + fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.7), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 2, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide( + color: AppTheme.of(context).primaryColor, + width: 2, + ), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + ), + items: widget.provider.categorias.map((categoria) { + return DropdownMenuItem( + value: categoria.id, + child: Container( + child: Text( + categoria.nombre, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + ), + ), + ), + ); + }).toList(), + ), + ); + } + + Widget _buildStatusSwitch( + String title, + String subtitle, + bool value, + ValueChanged onChanged, + IconData icon, + Color color, + ) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + color.withOpacity(0.1), + color.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: color.withOpacity(0.3), + width: 2, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: color, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + subtitle, + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 12, + ), + ), + ], + ), + ), + Switch( + value: value, + onChanged: onChanged, + activeColor: color, + activeTrackColor: color.withOpacity(0.3), + inactiveThumbColor: Colors.grey, + inactiveTrackColor: Colors.grey.withOpacity(0.3), + ), + ], + ), + ); + } + + Widget _buildImageSection() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor.withOpacity(0.1), + AppTheme.of(context).tertiaryColor.withOpacity(0.1), + AppTheme.of(context).secondaryColor.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.4), + width: 2, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header de la sección + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.image_rounded, + color: Colors.white, + size: 18, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Imagen del Componente', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: AppTheme.of(context).primaryText, + ), + ), + Text( + 'Actualizar imagen (opcional)', + style: TextStyle( + fontSize: 12, + color: AppTheme.of(context).secondaryText, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Botón para seleccionar imagen + GestureDetector( + onTap: () async { + await widget.provider.selectImagen(); + setState(() { + _actualizarImagen = widget.provider.imagenToUpload != null; + }); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 2, + ), + color: AppTheme.of(context).secondaryBackground, + ), + child: Column( + children: [ + // Preview de imagen + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + AppTheme.of(context).primaryColor.withOpacity(0.4), + width: 2, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: widget.provider.imagenToUpload != null + ? widget.provider.getImageWidget( + widget.provider.imagenToUpload, + height: 80, + width: 80, + ) + : widget.componente.imagenUrl != null && + widget.componente.imagenUrl!.isNotEmpty + ? Image.network( + "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/componentes/${widget.componente.imagenUrl}", + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + padding: const EdgeInsets.all(20), + child: Icon( + Icons.devices, + color: + AppTheme.of(context).primaryColor, + size: 24, + ), + ); + }, + ) + : Container( + padding: const EdgeInsets.all(20), + child: Icon( + Icons.devices, + color: AppTheme.of(context).primaryColor, + size: 24, + ), + ), + ), + ), + + const SizedBox(height: 12), + + // Texto explicativo + Text( + widget.provider.imagenFileName ?? + 'Toca para seleccionar imagen', + style: TextStyle( + color: widget.provider.imagenFileName != null + ? AppTheme.of(context).primaryColor + : AppTheme.of(context).secondaryText, + fontSize: 14, + fontWeight: widget.provider.imagenFileName != null + ? FontWeight.w600 + : FontWeight.normal, + ), + textAlign: TextAlign.center, + ), + + if (widget.provider.imagenFileName == null) + Text( + 'PNG, JPG (Max 2MB)', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ], + ), + ); + } + + Future _guardarCambios() async { + if (!_formKey.currentState!.validate()) return; + + setState(() { + _isLoading = true; + }); + + try { + final success = await widget.provider.actualizarComponente( + componenteId: widget.componente.id, + negocioId: widget.componente.negocioId, + categoriaId: _categoriaSeleccionada, + nombre: _nombreController.text.trim(), + descripcion: _descripcionController.text.trim().isEmpty + ? null + : _descripcionController.text.trim(), + enUso: _enUso, + activo: _activo, + ubicacion: _ubicacionController.text.trim().isEmpty + ? null + : _ubicacionController.text.trim(), + actualizarImagen: _actualizarImagen, + ); + + if (mounted) { + if (success) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: const [ + Icon(Icons.check_circle, color: Colors.white), + SizedBox(width: 12), + Text( + 'Componente actualizado exitosamente', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: const [ + Icon(Icons.error, color: Colors.white), + SizedBox(width: 12), + Text( + 'Error al actualizar el componente', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.warning, color: Colors.white), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Error: $e', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + ], + ), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } +} diff --git a/lib/providers/nethive/componentes_provider.dart b/lib/providers/nethive/componentes_provider.dart index b1ff469..0921508 100644 --- a/lib/providers/nethive/componentes_provider.dart +++ b/lib/providers/nethive/componentes_provider.dart @@ -9,6 +9,8 @@ import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:nethive_neo/helpers/globals.dart'; import 'package:nethive_neo/models/nethive/categoria_componente_model.dart'; import 'package:nethive_neo/models/nethive/componente_model.dart'; +import 'package:nethive_neo/models/nethive/distribucion_model.dart'; +import 'package:nethive_neo/models/nethive/conexion_componente_model.dart'; import 'package:nethive_neo/models/nethive/detalle_cable_model.dart'; import 'package:nethive_neo/models/nethive/detalle_switch_model.dart'; import 'package:nethive_neo/models/nethive/detalle_patch_panel_model.dart'; @@ -33,13 +35,23 @@ class ComponentesProvider extends ChangeNotifier { List componentesRows = []; List categoriasRows = []; + // Nuevas listas para topología + List distribuciones = []; + List conexiones = []; + // Variables para formularios String? imagenFileName; Uint8List? imagenToUpload; String? negocioSeleccionadoId; + String? negocioSeleccionadoNombre; + String? empresaSeleccionadaId; int? categoriaSeleccionadaId; bool showDetallesEspecificos = false; + // Variables para gestión de topología + bool isLoadingTopologia = false; + List problemasTopologia = []; + // Detalles específicos por tipo de componente DetalleCable? detalleCable; DetalleSwitch? detalleSwitch; @@ -267,6 +279,53 @@ class ComponentesProvider extends ChangeNotifier { } } + Future actualizarComponente({ + required String componenteId, + required String negocioId, + required int categoriaId, + required String nombre, + String? descripcion, + required bool enUso, + required bool activo, + String? ubicacion, + bool actualizarImagen = false, + }) async { + try { + Map updateData = { + 'categoria_id': categoriaId, + 'nombre': nombre, + 'descripcion': descripcion, + 'en_uso': enUso, + 'activo': activo, + 'ubicacion': ubicacion, + }; + + // Solo actualizar imagen si se seleccionó una nueva + if (actualizarImagen) { + final imagenUrl = await uploadImagen(); + if (imagenUrl != null) { + updateData['imagen_url'] = imagenUrl; + } + } + + final res = await supabaseLU + .from('componente') + .update(updateData) + .eq('id', componenteId) + .select(); + + if (res.isNotEmpty) { + await getComponentesPorNegocio(negocioId); + resetFormData(); + return true; + } + return false; + } catch (e) { + print('Error en actualizarComponente: ${e.toString()}'); + return false; + } + } + Future eliminarComponente(String componenteId) async { try { // Eliminar todos los detalles específicos primero @@ -452,12 +511,6 @@ class ComponentesProvider extends ChangeNotifier { } // Métodos de utilidad - void setNegocioSeleccionado(String negocioId) { - negocioSeleccionadoId = negocioId; - getComponentesPorNegocio(negocioId); - notifyListeners(); - } - CategoriaComponente? getCategoriaById(int id) { try { return categorias.firstWhere((c) => c.id == id); @@ -550,4 +603,559 @@ 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); + + notifyListeners(); + } 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; + notifyListeners(); + + 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; + notifyListeners(); + } + } + + // 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; + } + } + + // Obtener switches principales (core/distribución) + List getSwitchesPrincipales() { + return componentes.where((c) { + final categoria = getCategoriaById(c.categoriaId); + final isSwitch = + categoria?.nombre?.toLowerCase().contains('switch') ?? false; + final isCore = c.ubicacion?.toLowerCase().contains('mdf') ?? false; + return isSwitch && isCore; + }).toList(); + } + + // Obtener routers/firewalls + List getRoutersFirewalls() { + return componentes.where((c) { + final categoria = getCategoriaById(c.categoriaId); + final nombre = categoria?.nombre?.toLowerCase() ?? ''; + return nombre.contains('router') || nombre.contains('firewall'); + }).toList(); + } + + // Obtener servidores + List getServidores() { + return componentes.where((c) { + final categoria = getCategoriaById(c.categoriaId); + final nombre = categoria?.nombre?.toLowerCase() ?? ''; + return nombre.contains('servidor') || nombre.contains('server'); + }).toList(); + } + + // Obtener cables por tipo + List getCablesPorTipo(String tipoCable) { + return componentes.where((c) { + final categoria = getCategoriaById(c.categoriaId); + final nombre = categoria?.nombre?.toLowerCase() ?? ''; + return nombre.contains('cable') && + (c.descripcion?.toLowerCase().contains(tipoCable.toLowerCase()) ?? + false); + }).toList(); + } + + // Obtener estadísticas de conectividad + Map getEstadisticasConectividad() { + int componentesActivos = componentes.where((c) => c.activo).length; + int componentesEnUso = componentes.where((c) => c.enUso).length; + int conexionesActivas = conexiones.where((c) => c.activo).length; + int totalConexiones = conexiones.length; + + return { + 'componentesActivos': componentesActivos, + 'componentesEnUso': componentesEnUso, + 'conexionesActivas': conexionesActivas, + 'totalConexiones': totalConexiones, + 'porcentajeUso': componentesActivos > 0 + ? ((componentesEnUso / componentesActivos) * 100).round() + : 0, + }; + } + + // Cargar toda la información de topología + Future cargarTopologia(String negocioId) async { + await Future.wait([ + getComponentesPorNegocio(negocioId), + getDistribucionesPorNegocio(negocioId), + getConexionesPorNegocio(negocioId), + ]); + } + + // 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; + } } diff --git a/pubspec.lock b/pubspec.lock index 05c2717..5b8c4dc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -345,6 +345,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.5" + flame: + dependency: transitive + description: + name: flame + sha256: c02390443e40a2a9034ac867d0789ab32f215eba4779bff4655a7143eb3d75d0 + url: "https://pub.dev" + source: hosted + version: "1.22.0" flip_card: dependency: "direct main" description: @@ -358,6 +366,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" flutter_credit_card: dependency: "direct main" description: @@ -366,6 +382,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + flutter_flow_chart: + dependency: "direct main" + description: + name: flutter_flow_chart + sha256: "8b05ad760fa2f2b525a6982f30513a997d86f290592fb1d2b6b4900bb7d05f8f" + url: "https://pub.dev" + source: hosted + version: "3.2.3" + flutter_graph_view: + dependency: "direct main" + description: + name: flutter_graph_view + sha256: a4da9e5781e2292a1720fe49a4c045c956d567743ba95587a8e50a3221b97d15 + url: "https://pub.dev" + source: hosted + version: "1.1.6" flutter_lints: dependency: "direct dev" description: @@ -451,6 +483,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_side_menu: dependency: "direct main" description: @@ -845,6 +885,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + ordered_set: + dependency: transitive + description: + name: ordered_set + sha256: fc861bb51fc863cd3e0718e21768af9586e0d5022b91a0fd4437636456cdb7d0 + url: "https://pub.dev" + source: hosted + version: "6.1.1" path: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1fb4737..a2ec888 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,9 @@ dependencies: responsive_builder: ^0.7.1 tab_container: ^3.5.3 group_button: ^5.3.4 + flutter_graph_view: ^1.1.6 + flutter_animate: ^4.2.0 + flutter_flow_chart: 3.2.3 dev_dependencies: flutter_test: @@ -88,3 +91,11 @@ flutter: assets: - assets/images/ - assets/fonts/ + - assets/referencia/ + + fonts: + - family: Poppins + fonts: + - asset: assets/fonts/Poppins-Regular.ttf + - asset: assets/fonts/Poppins-Bold.ttf + weight: 700