From 35977495b6cb1f6a779a2730ae199515aee979d2 Mon Sep 17 00:00:00 2001 From: Abraham Date: Sun, 20 Jul 2025 15:18:16 -0700 Subject: [PATCH] menu hamburgesa responsivo --- assets/referencia/contexto_proyecto.md | 265 +++++++ .../infrastructure/infrastructure_layout.dart | 158 +++- .../infrastructure/pages/inventario_page.dart | 21 +- .../widgets/componentes_cards_view.dart | 732 ++++++++++++++++++ .../widgets/infrastructure_sidemenu.dart | 29 +- .../widgets/mobile_navigation_modal.dart | 569 ++++++++++++++ 6 files changed, 1735 insertions(+), 39 deletions(-) create mode 100644 assets/referencia/contexto_proyecto.md create mode 100644 lib/pages/infrastructure/widgets/componentes_cards_view.dart create mode 100644 lib/pages/infrastructure/widgets/mobile_navigation_modal.dart diff --git a/assets/referencia/contexto_proyecto.md b/assets/referencia/contexto_proyecto.md new file mode 100644 index 0000000..703d623 --- /dev/null +++ b/assets/referencia/contexto_proyecto.md @@ -0,0 +1,265 @@ +# NETHIVE NEO - Contexto del Proyecto + +## 📋 Resumen General + +**NETHIVE NEO** es una aplicación Flutter para la gestión de infraestructura de red MDF/IDF (Main Distribution Frame / Intermediate Distribution Frame). La aplicación permite administrar empresas, negocios y componentes de red de forma integral con una interfaz moderna y responsiva. + +## 🎯 Objetivo Principal + +Crear un sistema completo de gestión de infraestructura de telecomunicaciones que permita: +- Administrar múltiples empresas y sus negocios +- Gestionar inventario de componentes de red (switches, patch panels, cables, etc.) +- Monitorear el estado y uso de la infraestructura +- Proporcionar dashboards informativos +- Ofrecer una experiencia optimizada tanto para escritorio como móvil + +## 🏗️ Arquitectura del Proyecto + +### **Backend & Base de Datos** +- **Supabase** como backend principal +- Schema `nethive` para datos específicos del dominio +- Autenticación y autorización integrada +- Realtime para actualizaciones en tiempo real + +### **Frontend - Flutter** +- **Material Design** con tema personalizado +- **Provider** para gestión de estado +- **Go Router** para navegación +- **PlutoGrid** para tablas de datos +- **Responsive Design** adaptativo + +### **Estructura de Navegación** +``` +Login → Empresas/Negocios → Infraestructura MDF/IDF + ├── Dashboard + ├── Inventario + ├── Topología + ├── Alertas + └── Configuración +``` + +## 📱 Funcionalidades Implementadas + +### **1. Gestión de Empresas y Negocios** +- **Vista de escritorio**: Sidebar con empresas + tabla PlutoGrid de negocios +- **Vista móvil**: Cards responsivas con información completa +- **Funciones**: Crear, editar, eliminar empresas y negocios +- **Navegación**: Botón prominente "Acceder a Infraestructura" en cada negocio + +### **2. Layout de Infraestructura** +- **Sidemenu responsivo** con módulos: + - Dashboard (métricas y estadísticas) + - Inventario (gestión de componentes) + - Topología (visualización de red) + - Alertas (notificaciones del sistema) + - Configuración (parámetros del sistema) +- **Header dinámico** con breadcrumb: Empresa > Negocio > Módulo +- **Navegación móvil** con drawer colapsible + +### **3. Dashboard MDF/IDF** +- **Vista de escritorio**: Cards de estadísticas, gráficos, alertas recientes +- **Vista móvil**: Layout optimizado con cards compactas y información escalonada +- **Métricas**: Componentes totales, activos, en uso, categorías +- **Actividad**: Feed de eventos recientes del sistema + +### **4. Inventario de Componentes** +- **Vista de escritorio**: Tabla PlutoGrid con filtros y paginación +- **Vista móvil**: Cards interactivas con detalles completos +- **Gestión**: CRUD completo de componentes de red +- **Categorización**: Switches, patch panels, cables, etc. +- **Estados**: Activo/Inactivo, En uso/Libre + +## 🎨 Diseño y UX + +### **Tema Visual** +- **Colores primarios**: Verdes esmeralda (#10B981) con gradientes modernos +- **Colores secundarios**: Azules de acento (#3B82F6) para contrastes +- **Fondo**: Esquema oscuro moderno (#0F172A, #1E293B, #334155) +- **Tipografía**: Google Fonts Poppins con jerarquía clara +- **Iconografía**: Material Icons con iconos personalizados +- **Animaciones**: Transiciones fluidas y feedback visual + +### **Responsividad** +- **Móvil (≤800px)**: Vista de cards, menú hamburguesa, modals deslizables +- **Tablet (801-1200px)**: Tabla compacta, sidemenu colapsible +- **Escritorio (>1200px)**: Vista completa con todas las funcionalidades + +### **Componentes Clave** +- **Cards animadas** con hover effects y entrada escalonada +- **Gradientes modernos** en headers y botones +- **Estados visuales** claros con colores semánticos +- **Modals interactivos** para detalles y formularios +- **Breadcrumbs dinámicos** para navegación contextual + +## 📊 Providers (Gestión de Estado) + +### **NavigationProvider** +- Maneja el negocio seleccionado +- Controla la navegación entre módulos del sidemenu +- Mantiene el contexto de empresa/negocio activo + +### **EmpresasNegociosProvider** +- CRUD de empresas y negocios +- Estados de PlutoGrid para tablas +- Filtros y búsquedas +- Manejo de archivos (logos e imágenes) +- Integración con Supabase Storage + +### **ComponentesProvider** +- Gestión completa del inventario MDF/IDF +- Categorías de componentes dinámicas +- Estados de componentes (activo, en uso, ubicación) +- Detalles específicos por tipo de componente +- Búsquedas y filtros avanzados + +## 🔄 Flujo de Usuario Actual + +1. **Login** → Autenticación con Supabase +2. **Selección de Empresa** → Vista de empresas con negocios asociados +3. **Acceso a Infraestructura** → Click en botón de cualquier negocio +4. **Layout Principal** → Sidemenu con módulos + header con breadcrumb +5. **Navegación entre Módulos** → Dashboard, Inventario, etc. +6. **Vista Adaptativa** → Escritorio (tablas) vs Móvil (cards) + +## 🚀 Características Técnicas + +### **Dependencias Principales** +```yaml +flutter: SDK +provider: ^6.0.5 # Gestión de estado +go_router: ^12.0.0 # Navegación +supabase_flutter: ^2.0.0 # Backend +pluto_grid: ^7.0.0 # Tablas de datos +google_fonts: ^6.0.0 # Tipografías +file_picker: ^6.0.0 # Selección de archivos +``` + +### **Estructura de Archivos** +``` +lib/ +├── main.dart # Entry point con providers +├── router/router.dart # Configuración de rutas +├── theme/theme.dart # Tema y estilos globales +├── providers/nethive/ # Providers específicos del dominio +│ ├── empresas_negocios_provider.dart +│ ├── componentes_provider.dart +│ └── navigation_provider.dart +├── pages/ +│ ├── empresa_negocios/ # Gestión empresarial +│ │ ├── empresa_negocios_page.dart +│ │ └── widgets/ # Widgets especializados +│ └── infrastructure/ # Módulos MDF/IDF +│ ├── pages/ # Páginas principales +│ └── widgets/ # Componentes reutilizables +├── models/nethive/ # Modelos de datos +└── helpers/ # Utilidades y constantes +``` + +## 🎯 Estado Actual del Desarrollo + +### **✅ Completado** +- ✅ Sistema de autenticación con Supabase +- ✅ Gestión completa de empresas y negocios (escritorio + móvil) +- ✅ Layout de infraestructura con sidemenu responsivo +- ✅ Dashboard con métricas y estadísticas (escritorio + móvil) +- ✅ Inventario con tabla PlutoGrid y vista de cards móvil +- ✅ Navegación completa entre módulos con breadcrumbs +- ✅ Tema visual moderno y completamente responsivo +- ✅ Animaciones y transiciones fluidas +- ✅ Manejo de archivos e imágenes con Supabase Storage + +### **🔄 En Desarrollo** +- 🔄 Módulo de Topología de red +- 🔄 Sistema de Alertas en tiempo real +- 🔄 Configuración avanzada del sistema +- 🔄 Formularios de creación/edición de componentes + +### **📋 Próximas Funcionalidades** +- 📋 Reportes y exportación de datos +- 📋 Gestión de usuarios y permisos +- 📋 Integración con APIs de equipos de red +- 📋 Monitoreo en tiempo real de componentes +- 📋 Mapas interactivos para ubicaciones +- 📋 Módulo de mantenimiento preventivo + +## 🎨 Filosofía de Diseño + +**NETHIVE NEO** sigue los principios de **Material Design 3** con personalización corporativa, priorizando: +- **Usabilidad** por encima de la complejidad +- **Responsividad** real (no solo adaptativa) +- **Consistencia visual** en todos los módulos +- **Feedback inmediato** en todas las interacciones +- **Accesibilidad** para diferentes tipos de usuarios +- **Performance** optimizada con animaciones 60fps + +## 🔧 Configuración de Base de Datos + +### **Esquema Principal: `nethive`** +```sql +-- Tablas principales +empresa # Gestión de empresas +negocio # Sucursales/ubicaciones +categoria_componente # Tipos de componentes +componente # Inventario principal + +-- Tablas de detalles específicos +detalle_cable +detalle_switch +detalle_patch_panel +detalle_rack +detalle_organizador +detalle_ups +detalle_router_firewall +detalle_equipo_activo +``` + +### **Storage Buckets** +``` +nethive/ +├── logos/ # Logos de empresas y negocios +├── imagenes/ # Imágenes generales +└── componentes/ # Imágenes de componentes +``` + +## 📝 Patrones de Desarrollo + +### **Estado y Navegación** +- **Provider Pattern** para gestión de estado +- **Go Router** para navegación declarativa +- **Consumer Widgets** para reactividad +- **AnimationController** para transiciones + +### **Responsividad** +- **MediaQuery** para breakpoints +- **LayoutBuilder** para adaptación dinámica +- **Flexible/Expanded** para layouts fluidos +- **Custom Widgets** para cada viewport + +### **Arquitectura de Datos** +- **Repository Pattern** implícito en providers +- **Model Classes** con factory constructors +- **JSON Serialization** manual optimizada +- **Error Handling** robusto con try-catch + +## 🚀 Guías de Extensión + +### **Añadir Nuevo Módulo** +1. Crear provider en `providers/nethive/` +2. Añadir modelos en `models/nethive/` +3. Crear páginas en `pages/infrastructure/pages/` +4. Registrar en `navigation_provider.dart` +5. Añadir ruta en router + +### **Añadir Vista Móvil** +1. Detectar viewport con `MediaQuery` +2. Crear widget específico en `widgets/` +3. Implementar cards con animaciones +4. Añadir modals para detalles +5. Testear en diferentes dispositivos + +Este contexto debe servir como referencia para mantener la coherencia del proyecto en futuras iteraciones y para que nuevos desarrolladores comprendan rápidamente la estructura y objetivos del sistema. + +--- +**Fecha de creación**: 20 de julio de 2025 +**Versión**: 1.0 +**Proyecto**: NETHIVE NEO - Sistema de Gestión de Infraestructura MDF/IDF \ No newline at end of file diff --git a/lib/pages/infrastructure/infrastructure_layout.dart b/lib/pages/infrastructure/infrastructure_layout.dart index 4657ee0..2ff6cc6 100644 --- a/lib/pages/infrastructure/infrastructure_layout.dart +++ b/lib/pages/infrastructure/infrastructure_layout.dart @@ -3,6 +3,7 @@ import 'package:provider/provider.dart'; import 'package:nethive_neo/providers/nethive/navigation_provider.dart'; import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; import 'package:nethive_neo/pages/infrastructure/widgets/infrastructure_sidemenu.dart'; +import 'package:nethive_neo/pages/infrastructure/widgets/mobile_navigation_modal.dart'; import 'package:nethive_neo/pages/infrastructure/pages/dashboard_page.dart'; import 'package:nethive_neo/pages/infrastructure/pages/inventario_page.dart'; import 'package:nethive_neo/pages/infrastructure/pages/topologia_page.dart'; @@ -212,10 +213,9 @@ class _InfrastructureLayoutState extends State borderRadius: BorderRadius.circular(12), ), child: Image.asset( - 'assets/images/logo_nh.png', - width: 24, - height: 24, - color: Colors.white, + 'assets/images/favicon.png', + width: 48, + height: 48, ), ), @@ -345,17 +345,41 @@ class _InfrastructureLayoutState extends State bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20), ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], ), child: Column( children: [ Row( children: [ - // Botón de menú - IconButton( - onPressed: () => Scaffold.of(context).openDrawer(), - icon: const Icon(Icons.menu, color: Colors.white), + // Botón de menú moderno que abre el modal + GestureDetector( + onTap: () => _showMobileNavigationModal(), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), + ), + child: const Icon( + Icons.menu, + color: Colors.white, + size: 24, + ), + ), ), + const SizedBox(width: 16), + // Logo Container( padding: const EdgeInsets.all(8), @@ -364,10 +388,9 @@ class _InfrastructureLayoutState extends State borderRadius: BorderRadius.circular(8), ), child: Image.asset( - 'assets/images/logo_nh.png', - width: 20, - height: 20, - color: Colors.white, + 'assets/images/favicon.png', + width: 24, + height: 24, ), ), @@ -381,43 +404,103 @@ class _InfrastructureLayoutState extends State 'NETHIVE', style: TextStyle( color: Colors.white, - fontSize: 16, + fontSize: 18, fontWeight: FontWeight.bold, + letterSpacing: 1.2, ), ), Text( currentMenuItem.title, style: TextStyle( color: Colors.white.withOpacity(0.9), - fontSize: 12, + fontSize: 14, + fontWeight: FontWeight.w500, ), ), ], ), ), + + // Indicador de módulo actual + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + currentMenuItem.icon, + color: Colors.white, + size: 20, + ), + ), ], ), const SizedBox(height: 16), - // Info del negocio + // Info del negocio mejorada Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, + ), ), child: Row( children: [ - const Icon(Icons.business, color: Colors.white, size: 16), - const SizedBox(width: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.location_on, + color: Colors.white, + size: 16, + ), + ), + const SizedBox(width: 12), Expanded( - child: Text( - negocio.nombre, - style: const TextStyle( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + negocio.nombre, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + negocio.tipoLocal, + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.8), + borderRadius: BorderRadius.circular(10), + ), + child: const Text( + 'Activo', + style: TextStyle( color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w600, + fontSize: 10, + fontWeight: FontWeight.bold, ), ), ), @@ -429,6 +512,31 @@ class _InfrastructureLayoutState extends State ); } + // Método para mostrar el modal de navegación móvil + void _showMobileNavigationModal() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + barrierColor: Colors.black.withOpacity(0.5), + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.85, + maxChildSize: 0.95, + minChildSize: 0.3, + builder: (context, scrollController) => Container( + decoration: BoxDecoration( + color: AppTheme.of(context).primaryBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(25), + topRight: Radius.circular(25), + ), + ), + child: const MobileNavigationModal(), + ), + ), + ); + } + Widget _buildMainContent(NavigationProvider navigationProvider) { switch (navigationProvider.selectedMenuIndex) { case 0: diff --git a/lib/pages/infrastructure/pages/inventario_page.dart b/lib/pages/infrastructure/pages/inventario_page.dart index e5616c8..c7ade2d 100644 --- a/lib/pages/infrastructure/pages/inventario_page.dart +++ b/lib/pages/infrastructure/pages/inventario_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; 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/theme/theme.dart'; class InventarioPage extends StatefulWidget { @@ -41,10 +42,19 @@ class _InventarioPageState extends State @override Widget build(BuildContext context) { + final isLargeScreen = MediaQuery.of(context).size.width > 1200; + final isMobileScreen = MediaQuery.of(context).size.width <= 800; + return FadeTransition( opacity: _fadeAnimation, child: Consumer( builder: (context, componentesProvider, child) { + // Vista móvil con tarjetas + if (isMobileScreen) { + return const ComponentesCardsView(); + } + + // Vista de escritorio con tabla return Container( padding: const EdgeInsets.all(24), child: Column( @@ -55,12 +65,13 @@ class _InventarioPageState extends State const SizedBox(height: 24), - // Estadísticas rápidas - _buildQuickStats(componentesProvider), + // Estadísticas rápidas (solo en escritorio) + if (isLargeScreen) ...[ + _buildQuickStats(componentesProvider), + const SizedBox(height: 24), + ], - const SizedBox(height: 24), - - // Tabla de componentes (como en la imagen de referencia) + // Tabla de componentes (escritorio/tablet) Expanded( child: _buildComponentsTable(componentesProvider), ), diff --git a/lib/pages/infrastructure/widgets/componentes_cards_view.dart b/lib/pages/infrastructure/widgets/componentes_cards_view.dart new file mode 100644 index 0000000..c8371b1 --- /dev/null +++ b/lib/pages/infrastructure/widgets/componentes_cards_view.dart @@ -0,0 +1,732 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class ComponentesCardsView extends StatefulWidget { + const ComponentesCardsView({Key? key}) : super(key: key); + + @override + State createState() => _ComponentesCardsViewState(); +} + +class _ComponentesCardsViewState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + )); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _fadeAnimation, + child: Consumer( + builder: (context, componentesProvider, child) { + if (componentesProvider.componentes.isEmpty) { + return _buildEmptyState(); + } + + return Container( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header con filtros + _buildMobileHeader(componentesProvider), + + const SizedBox(height: 16), + + // Lista de tarjetas + Expanded( + child: ListView.builder( + itemCount: componentesProvider.componentes.length, + itemBuilder: (context, index) { + final componente = componentesProvider.componentes[index]; + return TweenAnimationBuilder( + duration: Duration(milliseconds: 300 + (index * 100)), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Transform.translate( + offset: Offset(0, 30 * (1 - value)), + child: Opacity( + opacity: value, + child: _buildComponenteCard( + componente, componentesProvider), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ); + }, + ), + ); + } + + Widget _buildMobileHeader(ComponentesProvider componentesProvider) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.inventory_2, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Inventario MDF/IDF', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + '${componentesProvider.componentes.length} componentes', + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 12, + ), + ), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Text( + 'Móvil', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Buscador móvil + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: TextField( + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: 'Buscar componentes...', + hintStyle: TextStyle(color: Colors.white.withOpacity(0.7)), + prefixIcon: Icon( + Icons.search, + color: Colors.white.withOpacity(0.8), + size: 20, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onChanged: (value) { + componentesProvider.buscarComponentes(value); + }, + ), + ), + ], + ), + ); + } + + Widget _buildComponenteCard( + dynamic componente, ComponentesProvider componentesProvider) { + // Buscar la categoría del componente + final categoria = componentesProvider.categorias + .where((cat) => cat.id == componente.categoriaId) + .firstOrNull; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + _showComponenteDetails(componente, categoria); + }, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header de la tarjeta + Row( + children: [ + // Imagen del componente + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: + AppTheme.of(context).primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: componente.imagenUrl != null && + componente.imagenUrl!.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + componente.imagenUrl!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Icon( + Icons.devices, + color: AppTheme.of(context).primaryColor, + size: 24, + ); + }, + ), + ) + : Icon( + Icons.devices, + color: AppTheme.of(context).primaryColor, + size: 24, + ), + ), + + const SizedBox(width: 12), + + // Info principal + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + componente.nombre, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + if (categoria != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + categoria.nombre, + style: TextStyle( + color: AppTheme.of(context).primaryColor, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + + // Estados + Column( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: + (componente.activo ? Colors.green : Colors.red) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + componente.activo + ? Icons.check_circle + : Icons.cancel, + color: componente.activo + ? Colors.green + : Colors.red, + size: 10, + ), + const SizedBox(width: 2), + Text( + componente.activo ? 'Activo' : 'Inactivo', + style: TextStyle( + color: componente.activo + ? Colors.green + : Colors.red, + fontSize: 9, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: + (componente.enUso ? Colors.orange : Colors.grey) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + componente.enUso ? 'En Uso' : 'Libre', + style: TextStyle( + color: componente.enUso + ? Colors.orange + : Colors.grey, + fontSize: 9, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 12), + + // Información adicional + if (componente.descripcion != null && + componente.descripcion!.isNotEmpty) + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryBackground, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + componente.descripcion!, + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 12, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + + const SizedBox(height: 8), + + // Footer con ubicación y acciones + Row( + children: [ + if (componente.ubicacion != null && + componente.ubicacion!.isNotEmpty) ...[ + Icon( + Icons.location_on, + color: AppTheme.of(context).primaryColor, + size: 14, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + componente.ubicacion!, + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 11, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ] else + Expanded( + child: Text( + 'Sin ubicación específica', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 11, + fontStyle: FontStyle.italic, + ), + ), + ), + + // Botones de acción + Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildActionButton( + icon: Icons.visibility, + color: Colors.blue, + onTap: () => + _showComponenteDetails(componente, categoria), + ), + const SizedBox(width: 4), + _buildActionButton( + icon: Icons.edit, + color: AppTheme.of(context).primaryColor, + onTap: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Editar próximamente')), + ); + }, + ), + const SizedBox(width: 4), + _buildActionButton( + icon: Icons.delete, + color: Colors.red, + onTap: () => _confirmDelete(componente), + ), + ], + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildActionButton({ + required IconData icon, + required Color color, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(4), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + ), + child: Icon( + icon, + color: color, + size: 14, + ), + ), + ); + } + + Widget _buildEmptyState() { + 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.inventory_2, + color: AppTheme.of(context).primaryColor, + size: 48, + ), + ), + const SizedBox(height: 16), + Text( + 'No hay componentes', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'No se encontraron componentes\npara este negocio', + textAlign: TextAlign.center, + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 14, + ), + ), + ], + ), + ); + } + + void _showComponenteDetails(dynamic componente, dynamic categoria) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + height: MediaQuery.of(context).size.height * 0.8, + decoration: BoxDecoration( + color: AppTheme.of(context).primaryBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Column( + children: [ + // Handle + Container( + margin: const EdgeInsets.only(top: 8), + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.5), + borderRadius: BorderRadius.circular(2), + ), + ), + + // Header + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: componente.imagenUrl != null && + componente.imagenUrl!.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + componente.imagenUrl!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return const Icon( + Icons.devices, + color: Colors.white, + size: 30, + ); + }, + ), + ) + : const Icon( + Icons.devices, + color: Colors.white, + size: 30, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + componente.nombre, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + if (categoria != null) + Text( + categoria.nombre, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 14, + ), + ), + ], + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.close, color: Colors.white), + ), + ], + ), + ), + + // Contenido + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDetailRow( + 'ID', componente.id.substring(0, 8) + '...'), + _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'), + ], + ), + ), + ), + ], + ), + ), + ); + } + + 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 _confirmDelete(dynamic componente) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppTheme.of(context).primaryBackground, + title: const Text('Eliminar Componente'), + content: Text('¿Deseas eliminar "${componente.nombre}"?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancelar', + style: TextStyle(color: AppTheme.of(context).secondaryText), + ), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Eliminar próximamente')), + ); + }, + child: const Text('Eliminar', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } +} diff --git a/lib/pages/infrastructure/widgets/infrastructure_sidemenu.dart b/lib/pages/infrastructure/widgets/infrastructure_sidemenu.dart index e54f332..30f215a 100644 --- a/lib/pages/infrastructure/widgets/infrastructure_sidemenu.dart +++ b/lib/pages/infrastructure/widgets/infrastructure_sidemenu.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:provider/provider.dart'; import 'package:go_router/go_router.dart'; import 'package:nethive_neo/providers/nethive/navigation_provider.dart'; @@ -53,7 +54,7 @@ class _InfrastructureSidemenuState extends State child: Consumer( builder: (context, navigationProvider, child) { return Container( - width: widget.isExpanded ? 280 : 70, + width: widget.isExpanded ? 280 : 80, decoration: BoxDecoration( gradient: AppTheme.of(context).darkBackgroundGradient, border: Border( @@ -137,14 +138,24 @@ class _InfrastructureSidemenuState extends State shaderCallback: (bounds) => LinearGradient( colors: [Colors.white, Colors.white.withOpacity(0.8)], ).createShader(bounds), - child: const Text( - 'NETHIVE', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - letterSpacing: 1.2, - ), + child: Row( + children: [ + Image.asset( + 'assets/images/favicon.png', + width: 32, + height: 32, + ), + const Gap(8), + const Text( + 'NETHIVE', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + ], ), ), Text( diff --git a/lib/pages/infrastructure/widgets/mobile_navigation_modal.dart b/lib/pages/infrastructure/widgets/mobile_navigation_modal.dart new file mode 100644 index 0000000..133b2f9 --- /dev/null +++ b/lib/pages/infrastructure/widgets/mobile_navigation_modal.dart @@ -0,0 +1,569 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:go_router/go_router.dart'; +import 'package:nethive_neo/providers/nethive/navigation_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class MobileNavigationModal extends StatefulWidget { + const MobileNavigationModal({Key? key}) : super(key: key); + + @override + State createState() => _MobileNavigationModalState(); +} + +class _MobileNavigationModalState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: this, + ); + _fadeAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ); + _slideAnimation = Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutCubic, + )); + + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _fadeAnimation, + child: Container( + decoration: BoxDecoration( + color: AppTheme.of(context).primaryBackground.withOpacity(0.95), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + ), + child: Consumer( + builder: (context, navigationProvider, child) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header del modal + /* _buildModalHeader(navigationProvider), */ + + // Lista de opciones de navegación + _buildNavigationOptions(navigationProvider), + + // Información del negocio + _buildBusinessInfo(navigationProvider), + + // Botón para cerrar + _buildCloseButton(), + + // Padding adicional para evitar overflow + const SizedBox(height: 20), + ], + ), + ); + }, + ), + ), + ); + } + + Widget _buildModalHeader(NavigationProvider navigationProvider) { + return SlideTransition( + position: _slideAnimation, + child: Container( + padding: const EdgeInsets.fromLTRB(24, 40, 24, 20), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30), + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: Row( + children: [ + // Logo animado + TweenAnimationBuilder( + duration: const Duration(milliseconds: 800), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Transform.scale( + scale: 0.8 + (0.2 * value), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 2, + ), + ), + child: Image.asset( + 'assets/images/favicon.png', + width: 32, + height: 32, + ), + ), + ); + }, + ), + + const SizedBox(width: 16), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShaderMask( + shaderCallback: (bounds) => LinearGradient( + colors: [Colors.white, Colors.white.withOpacity(0.8)], + ).createShader(bounds), + child: const Text( + 'NETHIVE', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + ), + ), + Text( + 'Infraestructura MDF/IDF', + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + + // Botón de cerrar + GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.close, + color: Colors.white, + size: 24, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildNavigationOptions(NavigationProvider navigationProvider) { + return Container( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Título de sección + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + children: [ + Icon( + Icons.navigation, + color: AppTheme.of(context).primaryColor, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Módulos de Infraestructura', + style: TextStyle( + color: AppTheme.of(context).primaryColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + + // Lista de opciones + ...navigationProvider.menuItems.asMap().entries.map((entry) { + final index = entry.key; + final menuItem = entry.value; + final isSelected = + navigationProvider.selectedMenuIndex == menuItem.index; + final isSpecial = menuItem.isSpecial; + + return TweenAnimationBuilder( + duration: Duration(milliseconds: 300 + (index * 100)), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Transform.translate( + offset: Offset(50 * (1 - value), 0), + child: Opacity( + opacity: value, + child: _buildNavigationItem( + menuItem, + isSelected, + isSpecial, + navigationProvider, + ), + ), + ); + }, + ); + }).toList(), + ], + ), + ); + } + + Widget _buildNavigationItem( + NavigationMenuItem menuItem, + bool isSelected, + bool isSpecial, + NavigationProvider navigationProvider, + ) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + gradient: isSelected + ? AppTheme.of(context).primaryGradient + : isSpecial + ? LinearGradient( + colors: [ + Colors.orange.withOpacity(0.1), + Colors.deepOrange.withOpacity(0.1), + ], + ) + : LinearGradient( + colors: [ + AppTheme.of(context).secondaryBackground, + AppTheme.of(context).tertiaryBackground, + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? Colors.white.withOpacity(0.3) + : isSpecial + ? Colors.orange.withOpacity(0.3) + : AppTheme.of(context).primaryColor.withOpacity(0.2), + width: isSelected ? 2 : 1, + ), + boxShadow: [ + BoxShadow( + color: isSelected + ? AppTheme.of(context).primaryColor.withOpacity(0.3) + : Colors.black.withOpacity(0.1), + blurRadius: isSelected ? 15 : 8, + offset: Offset(0, isSelected ? 8 : 3), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _handleMenuTap(menuItem, navigationProvider), + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + // Icono del módulo + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withOpacity(0.2) + : isSpecial + ? Colors.orange.withOpacity(0.2) + : AppTheme.of(context) + .primaryColor + .withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + menuItem.icon, + color: isSelected + ? Colors.white + : isSpecial + ? Colors.orange + : AppTheme.of(context).primaryColor, + size: 24, + ), + ), + + const SizedBox(width: 16), + + // Información del módulo + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + menuItem.title, + style: TextStyle( + color: isSelected + ? Colors.white + : isSpecial + ? Colors.orange + : AppTheme.of(context).primaryText, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + _getMenuItemDescription(menuItem), + style: TextStyle( + color: isSelected + ? Colors.white.withOpacity(0.8) + : isSpecial + ? Colors.orange.withOpacity(0.8) + : AppTheme.of(context).secondaryText, + fontSize: 14, + ), + ), + ], + ), + ), + + // Indicador de selección + if (isSelected) + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check, + color: Colors.white, + size: 20, + ), + ) + else + Icon( + Icons.arrow_forward_ios, + color: isSpecial + ? Colors.orange.withOpacity(0.6) + : AppTheme.of(context).secondaryText, + size: 16, + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildBusinessInfo(NavigationProvider navigationProvider) { + final negocio = navigationProvider.negocioSeleccionado; + final empresa = navigationProvider.empresaSeleccionada; + + if (negocio == null || empresa == null) return const SizedBox.shrink(); + + return Container( + margin: const EdgeInsets.all(20), + padding: const EdgeInsets.all(20), + 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), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.business_center, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Ubicación Actual', + style: TextStyle( + color: AppTheme.of(context).primaryColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Información de la empresa + Text( + empresa.nombre, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + + // Información del negocio + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.green, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.location_on, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 6), + Text( + negocio.nombre, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildCloseButton() { + return Container( + padding: const EdgeInsets.all(20), + child: SizedBox( + width: double.infinity, + child: Container( + decoration: BoxDecoration( + gradient: AppTheme.of(context).modernGradient, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: ElevatedButton.icon( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white), + label: const Text( + 'Cerrar Menú', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ), + ), + ), + ); + } + + String _getMenuItemDescription(NavigationMenuItem menuItem) { + switch (menuItem.title) { + case 'Dashboard': + return 'Métricas y estadísticas generales'; + case 'Inventario': + return 'Gestión de componentes de red'; + case 'Topología': + return 'Visualización de infraestructura'; + case 'Alertas': + return 'Notificaciones del sistema'; + case 'Configuración': + return 'Parámetros y ajustes'; + case 'Empresas': + return 'Volver a gestión empresarial'; + default: + return 'Módulo de infraestructura'; + } + } + + void _handleMenuTap( + NavigationMenuItem menuItem, + NavigationProvider navigationProvider, + ) { + if (menuItem.isSpecial) { + // Si es "Empresas", regresar a la página de empresas + navigationProvider.clearSelection(); + context.go('/'); + } else { + // Cambiar la selección del menú + navigationProvider.setSelectedMenuIndex(menuItem.index); + } + + // Cerrar el modal después de la selección + Navigator.of(context).pop(); + } +}