Files
energymedia_content_manager/lib/pages/infrastructure/pages/topologia_page.dart
2025-08-02 22:08:11 -07:00

1293 lines
41 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.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/models/nethive/topologia_completa_model.dart';
import 'package:nethive_neo/pages/infrastructure/pages/widgets/topologia_page_widgets/rack_view_widget.dart';
import 'package:nethive_neo/pages/infrastructure/pages/widgets/topologia_page_widgets/floor_plan_view_widget.dart';
class TopologiaPage extends StatefulWidget {
const TopologiaPage({Key? key}) : super(key: key);
@override
State<TopologiaPage> createState() => _TopologiaPageState();
}
class _TopologiaPageState extends State<TopologiaPage>
with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
String _selectedView = 'network'; // network, rack, floor
bool _isLoading = false;
// Dashboard para el FlowChart
late Dashboard dashboard;
// Mapas para elementos y conexiones
Map<String, FlowElement> elementosMap = {};
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
_animationController.forward();
_initializeDashboard();
// Cargar datos después de que el widget esté construido
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadTopologyData();
});
}
void _initializeDashboard() {
dashboard = Dashboard(
blockDefaultZoomGestures: false,
minimumZoomFactor: 0.25,
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isMediumScreen = MediaQuery.of(context).size.width > 800;
return FadeTransition(
opacity: _fadeAnimation,
child: Consumer<ComponentesProvider>(
builder: (context, componentesProvider, child) {
if (componentesProvider.isLoadingTopologia || _isLoading) {
return _buildLoadingView();
}
// Verificar si hay un negocio seleccionado
if (componentesProvider.negocioSeleccionadoId == null) {
return _buildNoBusinessSelectedView();
}
// Verificar si hay componentes
if (componentesProvider.componentesTopologia.isEmpty) {
return _buildNoComponentsView();
}
return Container(
padding: EdgeInsets.all(isMediumScreen ? 24 : 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header profesional
_buildProfessionalHeader(componentesProvider),
const SizedBox(height: 24),
// Controles avanzados
if (isMediumScreen) ...[
_buildAdvancedControls(componentesProvider),
const SizedBox(height: 24),
],
// Vista principal profesional
Expanded(
child: _buildProfessionalTopologyView(
isMediumScreen, componentesProvider),
),
],
),
);
},
),
);
}
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 desde la base de datos',
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 14,
),
).animate().fadeIn(delay: 600.ms),
],
),
);
}
Widget _buildNoBusinessSelectedView() {
return Center(
child: Container(
padding: const EdgeInsets.all(40),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.business,
size: 80,
color: Colors.white,
),
const SizedBox(height: 20),
Text(
'Selecciona un Negocio',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'Debe seleccionar un negocio desde la gestión\nde empresas para visualizar su topología',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 16,
),
),
],
),
),
);
}
Widget _buildNoComponentsView() {
return Center(
child: Container(
padding: const EdgeInsets.all(40),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.device_hub_outlined,
size: 80,
color: AppTheme.of(context).primaryColor,
),
const SizedBox(height: 20),
Text(
'Sin Componentes',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Text(
'Este negocio no tiene componentes\nregistrados en la infraestructura',
textAlign: TextAlign.center,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 16,
),
),
],
),
),
);
}
Widget _buildProfessionalHeader(ComponentesProvider provider) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 5),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.account_tree,
color: Colors.white,
size: 24,
),
).animate().scale(duration: 600.ms),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Topología de Red - ${provider.negocioSeleccionadoNombre ?? "Negocio"}',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
).animate().fadeIn(delay: 300.ms),
Text(
'${provider.componentesTopologia.length} componentes • ${provider.conexionesDatos.length} conexiones de datos',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
).animate().fadeIn(delay: 500.ms),
],
),
),
if (provider.problemasTopologia.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.warning, color: Colors.white, size: 16),
const SizedBox(width: 8),
Text(
'${provider.problemasTopologia.length} alertas',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
).animate().fadeIn(delay: 700.ms),
],
),
).animate().fadeIn().slideY(begin: -0.3, end: 0);
}
Widget _buildAdvancedControls(ComponentesProvider provider) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
),
),
child: Row(
children: [
// Selector de vista
Expanded(
child: Row(
children: [
Text(
'Vista:',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 12),
_buildViewButton('network', 'Diagrama Interactivo', Icons.hub),
const SizedBox(width: 8),
_buildViewButton('rack', 'Vista Rack', Icons.dns),
const SizedBox(width: 8),
_buildViewButton('floor', 'Plano de Planta', Icons.map),
],
),
),
// Información de componentes
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: AppTheme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Resumen:',
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Row(
children: [
_buildStatChip('MDF', provider.getComponentesMDF().length,
Colors.blue),
const SizedBox(width: 8),
_buildStatChip('IDF', provider.getComponentesIDF().length,
Colors.green),
const SizedBox(width: 8),
_buildStatChip('Switch',
provider.getComponentesSwitch().length, Colors.purple),
],
),
],
),
),
const SizedBox(width: 16),
// Controles de la topología
Row(
children: [
IconButton(
onPressed: () {
_refreshTopology(provider);
},
icon: const Icon(Icons.refresh),
tooltip: 'Actualizar topología',
style: IconButton.styleFrom(
backgroundColor:
AppTheme.of(context).primaryColor.withOpacity(0.1),
),
),
const SizedBox(width: 8),
IconButton(
onPressed: () {
dashboard.setZoomFactor(1.0);
},
icon: const Icon(Icons.center_focus_strong),
tooltip: 'Centrar vista',
style: IconButton.styleFrom(
backgroundColor:
AppTheme.of(context).primaryColor.withOpacity(0.1),
),
),
],
),
],
),
).animate().fadeIn(delay: 200.ms).slideY(begin: -0.2, end: 0);
}
Widget _buildStatChip(String label, int count, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'$label: $count',
style: TextStyle(
color: color,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
);
}
Widget _buildViewButton(String value, String label, IconData icon) {
final isSelected = _selectedView == value;
return GestureDetector(
onTap: () => setState(() => _selectedView = value),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: isSelected
? AppTheme.of(context).primaryColor
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? AppTheme.of(context).primaryColor
: AppTheme.of(context).primaryColor.withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 16,
color:
isSelected ? Colors.white : AppTheme.of(context).primaryColor,
),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
color: isSelected
? Colors.white
: AppTheme.of(context).primaryColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
Widget _buildProfessionalTopologyView(
bool isMediumScreen, ComponentesProvider provider) {
return Container(
decoration: BoxDecoration(
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.1),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Stack(
children: [
// Vista según selección
if (_selectedView == 'network')
_buildInteractiveFlowChart(provider)
else if (_selectedView == 'rack')
_buildRackView(isMediumScreen, provider)
else if (_selectedView == 'floor')
_buildFloorPlanView(isMediumScreen, provider),
// Leyenda profesional
if (isMediumScreen && _selectedView == 'network')
Positioned(
top: 16,
right: 16,
child: _buildProfessionalLegend(),
),
// Panel de información
if (_selectedView == 'network')
Positioned(
top: 16,
left: 16,
child: _buildInfoPanel(provider),
),
// Panel de problemas si existen
if (provider.problemasTopologia.isNotEmpty)
Positioned(
bottom: 16,
left: 16,
child: _buildProblemasPanel(provider),
),
],
),
),
);
}
Widget _buildInteractiveFlowChart(ComponentesProvider provider) {
return FutureBuilder(
future: _buildNetworkTopologyFromData(provider),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
);
}
return FlowChart(
dashboard: dashboard,
onElementPressed: (context, position, element) {
_showElementDetails(element, provider);
},
onElementLongPressed: (context, position, element) {
_showElementContextMenu(context, position, element, provider);
},
onNewConnection: (source, target) {
_handleNewConnection(source, target, provider);
},
onDashboardTapped: (context, position) {
// Limpiar selecciones
},
)
.animate()
.fadeIn(duration: 800.ms)
.scale(begin: const Offset(0.95, 0.95));
},
);
}
Future<void> _buildNetworkTopologyFromData(
ComponentesProvider provider) async {
dashboard.removeAllElements();
elementosMap.clear();
// Obtener componentes organizados por tipo
final mdfComponents = provider.getComponentesMDF();
final idfComponents = provider.getComponentesIDF();
final switchComponents = provider.getComponentesSwitch();
final routerComponents = provider.getComponentesRouter();
final serverComponents = provider.getComponentesServidor();
print('Construyendo topología con datos reales:');
print('- MDF: ${mdfComponents.length}');
print('- IDF: ${idfComponents.length}');
print('- Switches: ${switchComponents.length}');
print('- Routers: ${routerComponents.length}');
print('- Servidores: ${serverComponents.length}');
double currentX = 100;
double currentY = 100;
const double espacioX = 220;
const double espacioY = 180;
// 1. Crear elementos MDF
if (mdfComponents.isNotEmpty) {
double mdfX = currentX + espacioX * 2;
for (var mdfComp in mdfComponents) {
final mdfElement = _createMDFElement(mdfComp, Offset(mdfX, currentY));
dashboard.addElement(mdfElement);
elementosMap[mdfComp.id] = mdfElement;
mdfX += espacioX * 0.8;
}
currentY += espacioY;
}
// 2. Crear elementos IDF
if (idfComponents.isNotEmpty) {
double idfX = currentX;
for (var idfComp in idfComponents) {
final idfElement = _createIDFElement(idfComp, Offset(idfX, currentY));
dashboard.addElement(idfElement);
elementosMap[idfComp.id] = idfElement;
idfX += espacioX;
}
currentY += espacioY;
}
// 3. Crear routers
if (routerComponents.isNotEmpty) {
double routerX = currentX;
for (var router in routerComponents) {
final routerElement =
_createRouterElement(router, Offset(routerX, currentY));
dashboard.addElement(routerElement);
elementosMap[router.id] = routerElement;
routerX += espacioX * 0.9;
}
currentY += espacioY;
}
// 4. Crear switches
if (switchComponents.isNotEmpty) {
double switchX = currentX;
for (var switchComp in switchComponents) {
final switchElement =
_createSwitchElement(switchComp, Offset(switchX, currentY));
dashboard.addElement(switchElement);
elementosMap[switchComp.id] = switchElement;
switchX += espacioX * 0.8;
}
currentY += espacioY;
}
// 5. Crear servidores
if (serverComponents.isNotEmpty) {
double serverX = currentX + espacioX;
for (var servidor in serverComponents) {
final serverElement =
_createServerElement(servidor, Offset(serverX, currentY));
dashboard.addElement(serverElement);
elementosMap[servidor.id] = serverElement;
serverX += espacioX * 0.8;
}
}
// 6. Crear conexiones basadas en datos reales
_createConnections(provider);
print('Elementos creados: ${elementosMap.length}');
print(
'Conexiones de datos disponibles: ${provider.conexionesDatos.length}');
}
FlowElement _createMDFElement(
ComponenteTopologia component, Offset position) {
return FlowElement(
position: position,
size: const Size(180, 140),
text: 'MDF\n${component.nombre}',
textColor: Colors.white,
textSize: 14,
textIsBold: true,
kind: ElementKind.rectangle,
backgroundColor:
component.activo ? const Color(0xFF2196F3) : const Color(0xFF757575),
borderColor:
component.activo ? const Color(0xFF1976D2) : const Color(0xFF424242),
borderThickness: 3,
elevation: component.activo ? 8 : 4,
data: _buildElementData(component, 'MDF'),
handlers: [
Handler.bottomCenter,
Handler.leftCenter,
Handler.rightCenter,
],
);
}
FlowElement _createIDFElement(
ComponenteTopologia component, Offset position) {
return FlowElement(
position: position,
size: const Size(160, 120),
text: 'IDF\n${component.nombre}',
textColor: Colors.white,
textSize: 12,
textIsBold: true,
kind: ElementKind.rectangle,
backgroundColor: component.activo
? (component.enUso
? const Color(0xFF4CAF50)
: const Color(0xFFFF9800))
: const Color(0xFF757575),
borderColor: component.activo
? (component.enUso
? const Color(0xFF388E3C)
: const Color(0xFFF57C00))
: const Color(0xFF424242),
borderThickness: 2,
elevation: component.activo ? 6 : 2,
data: _buildElementData(component, 'IDF'),
handlers: [
Handler.topCenter,
Handler.bottomCenter,
Handler.leftCenter,
Handler.rightCenter,
],
);
}
FlowElement _createSwitchElement(
ComponenteTopologia component, Offset position) {
return FlowElement(
position: position,
size: const Size(140, 100),
text: 'Switch\n${component.nombre}',
textColor: Colors.white,
textSize: 10,
textIsBold: true,
kind: ElementKind.rectangle,
backgroundColor:
component.activo ? const Color(0xFF9C27B0) : const Color(0xFF757575),
borderColor:
component.activo ? const Color(0xFF7B1FA2) : const Color(0xFF424242),
borderThickness: 2,
elevation: component.activo ? 4 : 2,
data: _buildElementData(component, 'Switch'),
handlers: [
Handler.topCenter,
Handler.bottomCenter,
],
);
}
FlowElement _createRouterElement(
ComponenteTopologia component, Offset position) {
return FlowElement(
position: position,
size: const Size(160, 100),
text: 'Router\n${component.nombre}',
textColor: Colors.white,
textSize: 11,
textIsBold: true,
kind: ElementKind.rectangle,
backgroundColor:
component.activo ? const Color(0xFFFF5722) : const Color(0xFF757575),
borderColor:
component.activo ? const Color(0xFFE64A19) : const Color(0xFF424242),
borderThickness: 3,
elevation: component.activo ? 6 : 2,
data: _buildElementData(component, 'Router'),
handlers: [
Handler.topCenter,
Handler.bottomCenter,
Handler.leftCenter,
Handler.rightCenter,
],
);
}
FlowElement _createServerElement(
ComponenteTopologia component, Offset position) {
return FlowElement(
position: position,
size: const Size(150, 100),
text: 'Servidor\n${component.nombre}',
textColor: Colors.white,
textSize: 11,
textIsBold: true,
kind: ElementKind.rectangle,
backgroundColor:
component.activo ? const Color(0xFFE91E63) : const Color(0xFF757575),
borderColor:
component.activo ? const Color(0xFFC2185B) : const Color(0xFF424242),
borderThickness: 3,
elevation: component.activo ? 6 : 2,
data: _buildElementData(component, 'Server'),
handlers: [
Handler.topCenter,
Handler.leftCenter,
Handler.rightCenter,
],
);
}
Map<String, dynamic> _buildElementData(
ComponenteTopologia component, String displayType) {
return {
'type': displayType,
'componenteId': component.id,
'name': component.nombre,
'categoria': component.categoria,
'status': component.activo
? (component.enUso ? 'active' : 'warning')
: 'disconnected',
'description': component.descripcion ?? 'Sin descripción',
'ubicacion': component.ubicacion ?? 'Sin ubicación',
'distribucion': component.nombreDistribucion ?? 'Sin distribución',
'tipoDistribucion': component.tipoDistribucion,
'enUso': component.enUso,
'fechaRegistro': component.fechaRegistro.toString().split(' ')[0],
};
}
void _createConnections(ComponentesProvider provider) {
// Crear conexiones basadas en los datos reales
for (var conexion in provider.conexionesDatos) {
if (!conexion.activo) continue;
final sourceElement = elementosMap[conexion.componenteOrigenId];
final targetElement = elementosMap[conexion.componenteDestinoId];
if (sourceElement != null && targetElement != null) {
// Determinar color y grosor basado en el tipo de conexión
Color connectionColor = _getConnectionColor(conexion, provider);
double thickness = _getConnectionThickness(conexion, provider);
final connectionParams = ConnectionParams(
destElementId: targetElement.id,
arrowParams: ArrowParams(
color: connectionColor,
thickness: thickness,
),
);
sourceElement.next = [...sourceElement.next ?? [], connectionParams];
}
}
// También crear conexiones de energía si es necesario
for (var conexionEnergia in provider.conexionesEnergia) {
if (!conexionEnergia.activo) continue;
final sourceElement = elementosMap[conexionEnergia.origenId];
final targetElement = elementosMap[conexionEnergia.destinoId];
if (sourceElement != null && targetElement != null) {
final connectionParams = ConnectionParams(
destElementId: targetElement.id,
arrowParams: ArrowParams(
color: Colors.red.withOpacity(0.7),
thickness: 2,
),
);
sourceElement.next = [...sourceElement.next ?? [], connectionParams];
}
}
}
Color _getConnectionColor(
ConexionDatos conexion, ComponentesProvider provider) {
// Determinar color basado en el tipo de cable o componentes conectados
if (conexion.nombreCable != null) {
final cableName = conexion.nombreCable!.toLowerCase();
if (cableName.contains('fibra')) return Colors.cyan;
if (cableName.contains('utp')) return Colors.yellow;
if (cableName.contains('coaxial')) return Colors.orange;
}
// Color por defecto basado en los componentes
final sourceComponent =
provider.getComponenteTopologiaById(conexion.componenteOrigenId);
final targetComponent =
provider.getComponenteTopologiaById(conexion.componenteDestinoId);
if (sourceComponent?.esMDF == true || targetComponent?.esMDF == true) {
return Colors.cyan; // Conexiones principales
}
if (sourceComponent?.esIDF == true || targetComponent?.esIDF == true) {
return Colors.yellow; // Conexiones intermedias
}
return Colors.green; // Conexiones generales
}
double _getConnectionThickness(
ConexionDatos conexion, ComponentesProvider provider) {
final sourceComponent =
provider.getComponenteTopologiaById(conexion.componenteOrigenId);
final targetComponent =
provider.getComponenteTopologiaById(conexion.componenteDestinoId);
if (sourceComponent?.esMDF == true || targetComponent?.esMDF == true) {
return 4; // Conexiones principales más gruesas
}
if (sourceComponent?.esIDF == true || targetComponent?.esIDF == true) {
return 3; // Conexiones intermedias
}
return 2; // Conexiones estándar
}
void _showElementDetails(FlowElement element, ComponentesProvider provider) {
final data = element.data as Map<String, dynamic>;
final componenteId = data['componenteId'] as String;
final component = provider.getComponenteTopologiaById(componenteId);
if (component == null) return;
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppTheme.of(context).primaryBackground,
title: Row(
children: [
Icon(_getIconForType(data['type']),
color: _getColorForType(data['type'])),
const SizedBox(width: 8),
Expanded(child: Text(data['name'])),
],
),
content: Container(
width: double.maxFinite,
constraints: const BoxConstraints(maxHeight: 500),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow('Tipo', data['type']),
_buildDetailRow('Categoría', component.categoria),
_buildDetailRow('Estado', _getStatusText(data['status'])),
_buildDetailRow('En Uso', component.enUso ? '' : 'No'),
_buildDetailRow(
'Ubicación', component.ubicacion ?? 'Sin especificar'),
if (component.tipoDistribucion != null)
_buildDetailRow('Distribución',
'${component.tipoDistribucion} - ${component.nombreDistribucion}'),
_buildDetailRow('Fecha Registro',
component.fechaRegistro.toString().split(' ')[0]),
const SizedBox(height: 16),
const Text('Descripción:',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(component.descripcion ?? 'Sin descripción'),
const SizedBox(height: 16),
_buildConnectionsInfo(component, provider),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cerrar'),
),
],
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
'$label:',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
Expanded(child: Text(value)),
],
),
);
}
Widget _buildConnectionsInfo(
ComponenteTopologia component, ComponentesProvider provider) {
final conexiones = provider.getConexionesPorComponente(component.id);
final conexionesEnergia =
provider.getConexionesEnergiaPorComponente(component.id);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Conexiones:',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
if (conexiones.isEmpty && conexionesEnergia.isEmpty)
const Text('Sin conexiones registradas')
else ...[
if (conexiones.isNotEmpty) ...[
const Text('Datos:', style: TextStyle(fontWeight: FontWeight.w500)),
...conexiones.map((c) => Padding(
padding: const EdgeInsets.only(left: 16, top: 4),
child: Text('${c.nombreOrigen}${c.nombreDestino}'),
)),
],
if (conexionesEnergia.isNotEmpty) ...[
const SizedBox(height: 8),
const Text('Energía:',
style: TextStyle(fontWeight: FontWeight.w500)),
...conexionesEnergia.map((c) => Padding(
padding: const EdgeInsets.only(left: 16, top: 4),
child: Text('${c.nombreOrigen}${c.nombreDestino}'),
)),
],
],
],
);
}
void _showElementContextMenu(BuildContext context, Offset position,
FlowElement element, ComponentesProvider provider) {
// TODO: Implementar menú contextual con opciones reales
}
void _handleNewConnection(
FlowElement source, FlowElement target, ComponentesProvider provider) {
// TODO: Implementar creación de nueva conexión en la base de datos
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('Funcionalidad próximamente...'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cerrar'),
),
],
),
);
}
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: [
const Text(
'Leyenda',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
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', Icons.network_check),
_buildLegendItem(const Color(0xFFFF5722), 'Router', Icons.router),
_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.green, 'Conexión General', Icons.cable),
_buildLegendItem(Colors.red, 'Alimentación', Icons.power),
_buildLegendItem(Colors.grey, 'Inactivo', Icons.clear),
],
),
).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,
),
),
],
),
);
}
Widget _buildInfoPanel(ComponentesProvider provider) {
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: [
const Text(
'Información',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
const Text(
'• Arrastra los nodos para reposicionar',
style: TextStyle(color: Colors.white70, fontSize: 10),
),
const Text(
'• Haz clic en un nodo para ver detalles',
style: TextStyle(color: Colors.white70, fontSize: 10),
),
const Text(
'• Usa zoom con scroll del mouse',
style: TextStyle(color: Colors.white70, fontSize: 10),
),
const SizedBox(height: 8),
Text(
'Datos desde: ${provider.negocioSeleccionadoNombre}',
style: const TextStyle(color: Colors.cyan, fontSize: 9),
),
],
),
).animate().fadeIn(delay: 1200.ms).slideX(begin: -0.3);
}
Widget _buildProblemasPanel(ComponentesProvider provider) {
return Container(
constraints: const BoxConstraints(maxWidth: 300),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.9),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.orange),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.warning, color: Colors.white, size: 16),
SizedBox(width: 8),
Text(
'Alertas de Topología',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
...provider.problemasTopologia.take(3).map((problema) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(
'$problema',
style: const TextStyle(color: Colors.white, fontSize: 10),
),
)),
if (provider.problemasTopologia.length > 3)
Text(
'... y ${provider.problemasTopologia.length - 3} más',
style: const TextStyle(color: Colors.white70, fontSize: 9),
),
],
),
).animate().fadeIn(delay: 1500.ms).slideY(begin: 0.3);
}
Widget _buildRackView(bool isMediumScreen, ComponentesProvider provider) {
return RackViewWidget(
isMediumScreen: isMediumScreen,
provider: provider,
);
}
Widget _buildFloorPlanView(
bool isMediumScreen, ComponentesProvider provider) {
return FloorPlanViewWidget(
isMediumScreen: isMediumScreen,
provider: provider,
);
}
IconData _getIconForType(String type) {
switch (type) {
case 'MDF':
return Icons.router;
case 'IDF':
return Icons.hub;
case 'Switch':
return Icons.network_check;
case 'Server':
return Icons.dns;
case 'Router':
return Icons.router;
default:
return Icons.device_unknown;
}
}
Color _getColorForType(String type) {
switch (type) {
case 'MDF':
return const Color(0xFF2196F3);
case 'IDF':
return const Color(0xFF4CAF50);
case 'Switch':
return const Color(0xFF9C27B0);
case 'Server':
return const Color(0xFFE91E63);
case 'Router':
return const Color(0xFFFF5722);
default:
return Colors.grey;
}
}
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<void> _loadTopologyData() async {
final provider = Provider.of<ComponentesProvider>(context, listen: false);
if (provider.negocioSeleccionadoId != null) {
setState(() {
_isLoading = true;
});
await provider.getTopologiaPorNegocio(provider.negocioSeleccionadoId!);
setState(() {
_isLoading = false;
});
}
}
Future<void> _refreshTopology(ComponentesProvider provider) async {
if (provider.negocioSeleccionadoId != null) {
setState(() {
_isLoading = true;
});
await provider.getTopologiaPorNegocio(provider.negocioSeleccionadoId!);
setState(() {
_isLoading = false;
});
}
}
}