diff --git a/lib/pages/empresa_negocios/widgets/negocios_cards_view.dart b/lib/pages/empresa_negocios/widgets/negocios_cards_view.dart index 32f23ee..c57852a 100644 --- a/lib/pages/empresa_negocios/widgets/negocios_cards_view.dart +++ b/lib/pages/empresa_negocios/widgets/negocios_cards_view.dart @@ -619,20 +619,89 @@ class NegociosCardsView extends StatelessWidget { ), TextButton( onPressed: () async { + // Cerrar el diálogo antes de la operación asíncrona Navigator.pop(context); - final success = await provider.eliminarNegocio(negocio.id); + + // Mostrar indicador de carga if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - success - ? 'Sucursal eliminada correctamente' - : 'Error al eliminar la sucursal', + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Center( + child: CircularProgressIndicator( + color: AppTheme.of(context).primaryColor, ), - backgroundColor: success ? Colors.green : Colors.red, ), ); } + + try { + final success = await provider.eliminarNegocio(negocio.id); + + // Cerrar indicador de carga + if (context.mounted) { + Navigator.pop(context); + } + + // Mostrar resultado solo si el contexto sigue válido + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon( + success ? Icons.check_circle : Icons.error, + color: Colors.white, + ), + const SizedBox(width: 12), + Text( + success + ? 'Sucursal eliminada correctamente' + : 'Error al eliminar la sucursal', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), + backgroundColor: success ? Colors.green : Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + } catch (e) { + // Cerrar indicador de carga en caso de error + if (context.mounted) { + Navigator.pop(context); + } + + // Mostrar error solo si el contexto sigue válido + if (context.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), + ), + ), + ); + } + } }, child: const Text( 'Eliminar', diff --git a/lib/pages/empresa_negocios/widgets/negocios_table.dart b/lib/pages/empresa_negocios/widgets/negocios_table.dart index 2ce35f5..df94af8 100644 --- a/lib/pages/empresa_negocios/widgets/negocios_table.dart +++ b/lib/pages/empresa_negocios/widgets/negocios_table.dart @@ -514,20 +514,90 @@ class NegociosTable extends StatelessWidget { ), TextButton( onPressed: () async { + // Cerrar el diálogo antes de la operación asíncrona Navigator.of(context).pop(); - final success = await provider.eliminarNegocio(negocioId); + + // Mostrar indicador de carga if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - success - ? 'Sucursal eliminada correctamente' - : 'Error al eliminar la sucursal', + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Center( + child: CircularProgressIndicator( + color: AppTheme.of(context).primaryColor, ), - backgroundColor: success ? Colors.green : Colors.red, ), ); } + + try { + final success = await provider.eliminarNegocio(negocioId); + + // Cerrar indicador de carga + if (context.mounted) { + Navigator.of(context).pop(); + } + + // Mostrar resultado solo si el contexto sigue válido + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + Icon( + success ? Icons.check_circle : Icons.error, + color: Colors.white, + ), + const SizedBox(width: 12), + Text( + success + ? 'Sucursal eliminada correctamente' + : 'Error al eliminar la sucursal', + style: + const TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), + backgroundColor: success ? Colors.green : Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + } catch (e) { + // Cerrar indicador de carga en caso de error + if (context.mounted) { + Navigator.of(context).pop(); + } + + // Mostrar error solo si el contexto sigue válido + if (context.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), + ), + ), + ); + } + } }, child: const Text( 'Eliminar', diff --git a/lib/pages/infrastructure/pages/inventario_page.dart b/lib/pages/infrastructure/pages/inventario_page.dart index fcaba8c..775076f 100644 --- a/lib/pages/infrastructure/pages/inventario_page.dart +++ b/lib/pages/infrastructure/pages/inventario_page.dart @@ -4,6 +4,7 @@ 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/pages/infrastructure/widgets/add_componente_dialog.dart'; import 'package:nethive_neo/theme/theme.dart'; class InventarioPage extends StatefulWidget { @@ -18,6 +19,10 @@ class _InventarioPageState extends State late AnimationController _animationController; late Animation _fadeAnimation; + // GlobalKey para manejar el overlay de manera segura + final GlobalKey _overlayKey = GlobalKey(); + OverlayEntry? _loadingOverlay; + @override void initState() { super.initState(); @@ -37,10 +42,69 @@ class _InventarioPageState extends State @override void dispose() { + // Limpiar el overlay si existe antes de dispose + _removeLoadingOverlay(); _animationController.dispose(); super.dispose(); } + // Método para mostrar overlay de loading de manera segura + void _showLoadingOverlay(String message) { + _removeLoadingOverlay(); // Remover cualquier overlay existente + + if (mounted) { + _loadingOverlay = OverlayEntry( + builder: (context) => Material( + color: Colors.black.withOpacity(0.7), + child: Center( + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text( + message, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ), + ); + + Overlay.of(context).insert(_loadingOverlay!); + } + } + + // Método para remover overlay de manera segura + void _removeLoadingOverlay() { + if (_loadingOverlay != null) { + try { + _loadingOverlay!.remove(); + } catch (e) { + // Ignorar errores si el overlay ya fue removido + } + _loadingOverlay = null; + } + } + @override Widget build(BuildContext context) { final isLargeScreen = MediaQuery.of(context).size.width > 1200; @@ -135,7 +199,7 @@ class _InventarioPageState extends State ], ), ), - // Botón para añadir componente + // Botón para añadir componente - ACTUALIZADO Container( decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), @@ -143,10 +207,24 @@ class _InventarioPageState extends State ), child: TextButton.icon( onPressed: () { - // TODO: Abrir dialog para añadir componente - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Función de añadir componente próximamente'), + // Verificar que tengamos un negocio seleccionado + if (componentesProvider.negocioSeleccionadoId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Debe seleccionar un negocio antes de añadir componentes'), + backgroundColor: Colors.orange, + ), + ); + return; + } + + // Abrir el diálogo para añadir componente + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AddComponenteDialog( + provider: componentesProvider, ), ); }, @@ -879,64 +957,667 @@ class _InventarioPageState extends State void _showComponentDetails(dynamic componente, ComponentesProvider provider) { if (componente == null) return; + // Detectar el tamaño de pantalla + final screenSize = MediaQuery.of(context).size; + final isDesktop = screenSize.width > 1024; + final isMobile = screenSize.width <= 768; + + // Obtener la URL de la imagen del componente + final imagenUrl = provider.componentesRows + .where((row) => row.cells['id']?.value == componente.id) + .firstOrNull + ?.cells['imagen_url'] + ?.value + ?.toString(); + + final categoria = provider.getCategoriaById(componente.categoriaId); + 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, + barrierDismissible: true, + builder: (context) => Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), + child: Container( + width: isDesktop ? 900 : (isMobile ? screenSize.width * 0.95 : 700), + height: isDesktop ? 650 : (isMobile ? screenSize.height * 0.8 : 600), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 30, + offset: const Offset(0, 15), + spreadRadius: 5, + ), + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + blurRadius: 40, + offset: const Offset(0, 10), + spreadRadius: 2, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + 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 + ? _buildDesktopDetailLayout( + componente, provider, categoria, imagenUrl) + : _buildMobileDetailLayout( + componente, provider, categoria, imagenUrl), ), - ], + ), ), - content: Container( - width: double.maxFinite, - constraints: const BoxConstraints(maxHeight: 400), - child: SingleChildScrollView( + ), + ); + } + + Widget _buildDesktopDetailLayout( + dynamic componente, + ComponentesProvider provider, + dynamic categoria, + String? imagenUrl, + ) { + return Row( + children: [ + // Panel izquierdo con imagen espectacular + Container( + width: 350, + 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.4), + blurRadius: 20, + offset: const Offset(5, 0), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(30), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, 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'), + // Imagen principal del componente - MÁS GRANDE + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: RadialGradient( + colors: [ + Colors.white.withOpacity(0.3), + Colors.white.withOpacity(0.1), + Colors.transparent, + ], + ), + border: Border.all( + color: Colors.white.withOpacity(0.4), + width: 3, + ), + boxShadow: [ + BoxShadow( + color: Colors.white.withOpacity(0.3), + blurRadius: 30, + spreadRadius: 10, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(17), + child: imagenUrl != null && imagenUrl.isNotEmpty + ? Image.network( + imagenUrl, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(17), + ), + child: Center( + child: CircularProgressIndicator( + color: Colors.white, + value: loadingProgress.expectedTotalBytes != + null + ? loadingProgress + .cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(17), + ), + child: const Icon( + Icons.devices, + color: Colors.white, + size: 80, + ), + ); + }, + ) + : Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(17), + ), + child: const Icon( + Icons.devices, + color: Colors.white, + size: 80, + ), + ), + ), + ), + + const SizedBox(height: 24), + + // Título del componente + Text( + componente.nombre, + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + const SizedBox(height: 12), + + // Categoría con estilo + if (categoria != null) + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.3), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.category, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 8), + Text( + categoria.nombre, + style: TextStyle( + color: Colors.white.withOpacity(0.95), + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + + const SizedBox(height: 20), + + // Estados con iconos + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildStatusIndicator( + componente.activo ? 'Activo' : 'Inactivo', + componente.activo ? Icons.check_circle : Icons.cancel, + componente.activo ? Colors.green : Colors.red, + ), + _buildStatusIndicator( + componente.enUso ? 'En Uso' : 'Libre', + componente.enUso + ? Icons.trending_up + : Icons.trending_flat, + componente.enUso ? Colors.orange : Colors.grey, + ), + ], + ), + + const SizedBox(height: 20), + + // ID con estilo + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'ID: ${componente.id.substring(0, 8)}...', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 12, + fontFamily: 'monospace', + ), + ), + ), ], ), ), ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text( - 'Cerrar', - style: TextStyle(color: AppTheme.of(context).primaryColor), + + // Panel derecho con detalles + Expanded( + child: Padding( + padding: const EdgeInsets.all(30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header del panel de detalles + Row( + children: [ + Icon( + Icons.info_outline, + color: AppTheme.of(context).primaryColor, + size: 24, + ), + const SizedBox(width: 12), + Text( + 'Detalles del Componente', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: Icon( + Icons.close, + color: AppTheme.of(context).secondaryText, + ), + style: IconButton.styleFrom( + backgroundColor: + AppTheme.of(context).secondaryBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + + const SizedBox(height: 24), + + // Información detallada + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + if (componente.ubicacion != null && + componente.ubicacion!.isNotEmpty) + _buildEnhancedDetailCard( + 'Ubicación', + componente.ubicacion!, + Icons.location_on, + Colors.blue, + ), + if (componente.descripcion != null && + componente.descripcion!.isNotEmpty) + _buildEnhancedDetailCard( + 'Descripción', + componente.descripcion!, + Icons.description, + Colors.purple, + ), + _buildEnhancedDetailCard( + 'Fecha de Registro', + componente.fechaRegistro?.toString().split(' ')[0] ?? + 'No disponible', + Icons.calendar_today, + Colors.green, + ), + _buildEnhancedDetailCard( + 'Estado Operativo', + componente.activo + ? 'Componente activo y operativo' + : 'Componente inactivo', + componente.activo + ? Icons.power_settings_new + : Icons.power_off, + componente.activo ? Colors.green : Colors.red, + ), + _buildEnhancedDetailCard( + 'Estado de Uso', + componente.enUso + ? 'Componente en uso actual' + : 'Componente disponible para uso', + componente.enUso ? Icons.work : Icons.work_off, + componente.enUso ? Colors.orange : Colors.grey, + ), + ], + ), + ), + ), + + // Botones de acción + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () { + Navigator.of(context).pop(); + _editComponent(componente, provider); + }, + icon: const Icon(Icons.edit, size: 18), + label: const Text('Editar'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.of(context).primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: OutlinedButton.icon( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, size: 18), + label: const Text('Cerrar'), + style: OutlinedButton.styleFrom( + foregroundColor: AppTheme.of(context).secondaryText, + side: BorderSide( + color: AppTheme.of(context) + .secondaryText + .withOpacity(0.5), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildMobileDetailLayout( + dynamic componente, + ComponentesProvider provider, + dynamic categoria, + String? imagenUrl, + ) { + return Column( + children: [ + // Header con imagen para móvil + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + AppTheme.of(context).tertiaryColor, + ], + ), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Detalles', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white), + ), + ], + ), + const SizedBox(height: 16), + // Imagen del componente en móvil + Container( + width: 120, + height: 120, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Colors.white.withOpacity(0.3), width: 2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(13), + child: imagenUrl != null && imagenUrl.isNotEmpty + ? Image.network( + imagenUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.white.withOpacity(0.1), + child: const Icon( + Icons.devices, + color: Colors.white, + size: 50, + ), + ); + }, + ) + : Container( + color: Colors.white.withOpacity(0.1), + child: const Icon( + Icons.devices, + color: Colors.white, + size: 50, + ), + ), + ), + ), + const SizedBox(height: 12), + Text( + componente.nombre, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + if (categoria != null) ...[ + const SizedBox(height: 8), + Text( + categoria.nombre, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 14, + ), + ), + ], + ], + ), + ), + + // Contenido de detalles para móvil + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: _buildStatusIndicator( + componente.activo ? 'Activo' : 'Inactivo', + componente.activo ? Icons.check_circle : Icons.cancel, + componente.activo ? Colors.green : Colors.red, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatusIndicator( + componente.enUso ? 'En Uso' : 'Libre', + componente.enUso + ? Icons.trending_up + : Icons.trending_flat, + componente.enUso ? Colors.orange : Colors.grey, + ), + ), + ], + ), + const SizedBox(height: 16), + + if (componente.ubicacion != null && + componente.ubicacion!.isNotEmpty) + _buildEnhancedDetailCard( + 'Ubicación', + componente.ubicacion!, + Icons.location_on, + Colors.blue, + ), + + if (componente.descripcion != null && + componente.descripcion!.isNotEmpty) + _buildEnhancedDetailCard( + 'Descripción', + componente.descripcion!, + Icons.description, + Colors.purple, + ), + + _buildEnhancedDetailCard( + 'Fecha de Registro', + componente.fechaRegistro?.toString().split(' ')[0] ?? + 'No disponible', + Icons.calendar_today, + Colors.green, + ), + + _buildEnhancedDetailCard( + 'ID del Componente', + componente.id.substring(0, 8) + '...', + Icons.fingerprint, + Colors.indigo, + ), + + const SizedBox(height: 20), + + // Botones de acción para móvil + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () { + Navigator.of(context).pop(); + _editComponent(componente, provider); + }, + icon: const Icon(Icons.edit, size: 18), + label: const Text('Editar'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.of(context).primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: OutlinedButton.icon( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, size: 18), + label: const Text('Cerrar'), + style: OutlinedButton.styleFrom( + foregroundColor: AppTheme.of(context).secondaryText, + side: BorderSide( + color: AppTheme.of(context) + .secondaryText + .withOpacity(0.5), + ), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildStatusIndicator(String text, IconData icon, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 16), + const SizedBox(width: 6), + Text( + text, + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.bold, ), ), ], @@ -944,38 +1625,67 @@ class _InventarioPageState extends State ); } - Widget _buildDetailRow(String label, String value) { + Widget _buildEnhancedDetailCard( + String title, String value, IconData icon, Color color) { return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.2), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).secondaryBackground, + AppTheme.of(context).tertiaryBackground, + ], ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: color.withOpacity(0.3), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(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, - ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), ), + child: Icon(icon, color: color, size: 20), ), + const SizedBox(width: 16), Expanded( - child: Text( - value, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 12, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: color, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 13, + height: 1.4, + ), + ), + ], ), ), ], @@ -1034,27 +1744,29 @@ class _InventarioPageState extends State ), ElevatedButton( onPressed: () async { + // Cerrar el diálogo de confirmación Navigator.of(context).pop(); - // Mostrar indicador de carga - showDialog( - context: context, - barrierDismissible: false, - builder: (context) => Center( - child: CircularProgressIndicator( - color: AppTheme.of(context).primaryColor, - ), - ), - ); + // Capturar el ScaffoldMessenger antes de la operación asíncrona + final scaffoldMessenger = ScaffoldMessenger.of(context); try { + // Mostrar loading de manera segura + _showLoadingOverlay('Eliminando componente...'); + + // Realizar la eliminación final success = await provider.eliminarComponente(componente.id); - Navigator.of(context).pop(); // Cerrar indicador de carga + // Remover loading de manera segura + _removeLoadingOverlay(); + // Verificar que el widget sigue montado antes de mostrar mensajes + if (!mounted) return; + + // Mostrar resultado usando el ScaffoldMessenger capturado if (success) { - ScaffoldMessenger.of(context).showSnackBar( + scaffoldMessenger.showSnackBar( SnackBar( content: Row( children: const [ @@ -1071,10 +1783,11 @@ class _InventarioPageState extends State shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), + duration: const Duration(seconds: 3), ), ); } else { - ScaffoldMessenger.of(context).showSnackBar( + scaffoldMessenger.showSnackBar( SnackBar( content: Row( children: const [ @@ -1091,12 +1804,19 @@ class _InventarioPageState extends State shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), + duration: const Duration(seconds: 4), ), ); } } catch (e) { - Navigator.of(context).pop(); // Cerrar indicador de carga - ScaffoldMessenger.of(context).showSnackBar( + // Asegurar que el overlay se remueva en caso de error + _removeLoadingOverlay(); + + // Verificar que el widget sigue montado antes de mostrar error + if (!mounted) return; + + // Mostrar error usando el ScaffoldMessenger capturado + scaffoldMessenger.showSnackBar( SnackBar( content: Row( children: [ @@ -1115,6 +1835,7 @@ class _InventarioPageState extends State shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), + duration: const Duration(seconds: 4), ), ); } diff --git a/lib/pages/infrastructure/widgets/add_componente_dialog.dart b/lib/pages/infrastructure/widgets/add_componente_dialog.dart new file mode 100644 index 0000000..6f7a47a --- /dev/null +++ b/lib/pages/infrastructure/widgets/add_componente_dialog.dart @@ -0,0 +1,1255 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class AddComponenteDialog extends StatefulWidget { + final ComponentesProvider provider; + + const AddComponenteDialog({ + Key? key, + required this.provider, + }) : super(key: key); + + @override + State createState() => _AddComponenteDialogState(); +} + +class _AddComponenteDialogState extends State + with TickerProviderStateMixin { + final _formKey = GlobalKey(); + late TextEditingController _nombreController; + late TextEditingController _descripcionController; + late TextEditingController _ubicacionController; + + bool _enUso = false; + bool _activo = true; + int? _categoriaSeleccionada; + bool _isLoading = false; + + // Animaciones + late AnimationController _slideController; + late AnimationController _fadeController; + late Animation _slideAnimation; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _nombreController = TextEditingController(); + _descripcionController = TextEditingController(); + _ubicacionController = TextEditingController(); + + // Configurar animaciones + _slideController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + _fadeController = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: this, + ); + + _slideAnimation = Tween( + begin: const Offset(-1.0, 0.0), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _slideController, + curve: Curves.easeOutCubic, + )); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _fadeController, + curve: Curves.easeInOut, + )); + + // Iniciar animaciones + _slideController.forward(); + _fadeController.forward(); + } + + @override + void dispose() { + _nombreController.dispose(); + _descripcionController.dispose(); + _ubicacionController.dispose(); + _slideController.dispose(); + _fadeController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final isDesktop = screenSize.width > 1024; + final isMobile = screenSize.width <= 768; + + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), + child: Container( + width: isDesktop ? 900 : (isMobile ? screenSize.width * 0.95 : 700), + height: isDesktop ? 650 : (isMobile ? screenSize.height * 0.9 : 600), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 30, + offset: const Offset(0, 15), + spreadRadius: 5, + ), + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + blurRadius: 40, + offset: const Offset(0, 10), + spreadRadius: 2, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + 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: FadeTransition( + opacity: _fadeAnimation, + child: isDesktop + ? _buildDesktopLayout() + : _buildMobileLayout(isMobile), + ), + ), + ), + ), + ); + } + + Widget _buildDesktopLayout() { + return Row( + children: [ + // Panel lateral izquierdo con diseño espectacular + 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: [ + // Icono principal del componente + Container( + width: 140, + height: 140, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: RadialGradient( + colors: [ + Colors.white.withOpacity(0.3), + Colors.white.withOpacity(0.1), + Colors.transparent, + ], + ), + border: Border.all( + color: Colors.white.withOpacity(0.4), + width: 3, + ), + boxShadow: [ + BoxShadow( + color: Colors.white.withOpacity(0.3), + blurRadius: 30, + spreadRadius: 10, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(17), + child: widget.provider.imagenToUpload != null + ? Image.memory( + widget.provider.imagenToUpload!, + fit: BoxFit.cover, + ) + : Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(17), + ), + child: const Icon( + Icons.add_circle_outline, + color: Colors.white, + size: 60, + ), + ), + ), + ), + + const SizedBox(height: 24), + + // Botón para seleccionar imagen + Container( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () async { + await widget.provider.selectImagen(); + setState(() {}); + }, + icon: const Icon(Icons.image, color: Colors.white), + label: Text( + widget.provider.imagenToUpload != null + ? 'Cambiar Imagen' + : 'Seleccionar Imagen', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white.withOpacity(0.2), + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Colors.white.withOpacity(0.3), + ), + ), + ), + ), + ), + + const SizedBox(height: 20), + + // Título + const Text( + 'Nuevo Componente', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 12), + + // Subtítulo + Text( + 'Registra un nuevo componente\npara tu infraestructura', + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 14, + height: 1.4, + ), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 30), + + // Estados predeterminados + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildStatusIndicator( + _activo ? 'Activo' : 'Inactivo', + _activo ? Icons.check_circle : Icons.cancel, + _activo ? Colors.green : Colors.red, + ), + _buildStatusIndicator( + _enUso ? 'En Uso' : 'Libre', + _enUso ? Icons.trending_up : Icons.trending_flat, + _enUso ? Colors.orange : Colors.grey, + ), + ], + ), + + const SizedBox(height: 20), + + // Info del negocio + if (widget.provider.negocioSeleccionadoNombre != null) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Negocio: ${widget.provider.negocioSeleccionadoNombre}', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ), + + // Panel principal con formulario + Expanded( + child: Padding( + padding: const EdgeInsets.all(30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header del formulario + Row( + children: [ + Icon( + Icons.add_box_rounded, + color: AppTheme.of(context).primaryColor, + size: 28, + ), + const SizedBox(width: 12), + Text( + 'Información del Componente', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: Icon( + Icons.close, + color: AppTheme.of(context).secondaryText, + ), + style: IconButton.styleFrom( + backgroundColor: + AppTheme.of(context).secondaryBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + + const SizedBox(height: 30), + + // Formulario + Expanded( + child: Form( + key: _formKey, + 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 Principal MDF', + icon: Icons.devices_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El nombre es obligatorio'; + } + 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: _buildSwitchCard( + title: 'Componente Activo', + subtitle: 'El componente está operativo', + value: _activo, + onChanged: (value) { + setState(() { + _activo = value; + }); + }, + icon: Icons.power_settings_new, + activeColor: Colors.green, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildSwitchCard( + title: 'En Uso', + subtitle: + 'El componente está siendo utilizado', + value: _enUso, + onChanged: (value) { + setState(() { + _enUso = value; + }); + }, + icon: Icons.work, + activeColor: Colors.orange, + ), + ), + ], + ), + ], + ), + ), + ), + ), + + // Botones de acción + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _isLoading + ? null + : () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, size: 18), + label: const Text('Cancelar'), + style: OutlinedButton.styleFrom( + foregroundColor: AppTheme.of(context).secondaryText, + side: BorderSide( + color: AppTheme.of(context) + .secondaryText + .withOpacity(0.5), + ), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: Container( + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.4), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: ElevatedButton.icon( + onPressed: _isLoading ? null : _guardarComponente, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + icon: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ) + : const Icon(Icons.save_rounded, + color: Colors.white, size: 20), + label: Text( + _isLoading ? 'Guardando...' : 'Crear Componente', + style: const TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildMobileLayout(bool isMobile) { + return Column( + children: [ + // Header móvil + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + AppTheme.of(context).tertiaryColor, + ], + ), + ), + child: SafeArea( + bottom: false, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Nuevo Componente', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, color: Colors.white), + ), + ], + ), + const SizedBox(height: 16), + // Imagen/selector móvil + GestureDetector( + onTap: () async { + await widget.provider.selectImagen(); + setState(() {}); + }, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Colors.white.withOpacity(0.3), width: 2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(13), + child: widget.provider.imagenToUpload != null + ? Image.memory( + widget.provider.imagenToUpload!, + fit: BoxFit.cover, + ) + : Container( + color: Colors.white.withOpacity(0.1), + child: const Icon( + Icons.add_photo_alternate, + color: Colors.white, + size: 40, + ), + ), + ), + ), + ), + const SizedBox(height: 8), + Text( + widget.provider.imagenToUpload != null + ? 'Toca para cambiar imagen' + : 'Toca para añadir imagen', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 12, + ), + ), + ], + ), + ), + ), + + // Contenido del formulario para móvil + Expanded( + child: Form( + key: _formKey, + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // Estados en móvil + Row( + children: [ + Expanded( + child: _buildStatusIndicator( + _activo ? 'Activo' : 'Inactivo', + _activo ? Icons.check_circle : Icons.cancel, + _activo ? Colors.green : Colors.red, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatusIndicator( + _enUso ? 'En Uso' : 'Libre', + _enUso ? Icons.trending_up : Icons.trending_flat, + _enUso ? Colors.orange : Colors.grey, + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Campos del formulario + _buildCompactFormField( + controller: _nombreController, + label: 'Nombre del Componente', + hint: 'Ej: Switch Principal MDF', + icon: Icons.devices_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El nombre es obligatorio'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + _buildCategoriaDropdown(), + + const SizedBox(height: 16), + + _buildCompactFormField( + controller: _ubicacionController, + label: 'Ubicación', + hint: 'Ej: MDF Principal - Rack 1', + icon: Icons.location_on_rounded, + ), + + const SizedBox(height: 16), + + _buildCompactFormField( + controller: _descripcionController, + label: 'Descripción', + hint: 'Descripción detallada del componente', + icon: Icons.description_rounded, + maxLines: 3, + ), + + const SizedBox(height: 20), + + // Switches en móvil + _buildSwitchCard( + title: 'Componente Activo', + subtitle: 'El componente está operativo', + value: _activo, + onChanged: (value) { + setState(() { + _activo = value; + }); + }, + icon: Icons.power_settings_new, + activeColor: Colors.green, + ), + + const SizedBox(height: 12), + + _buildSwitchCard( + title: 'En Uso', + subtitle: 'El componente está siendo utilizado', + value: _enUso, + onChanged: (value) { + setState(() { + _enUso = value; + }); + }, + icon: Icons.work, + activeColor: Colors.orange, + ), + + const SizedBox(height: 30), + + // Botones para móvil + Column( + children: [ + Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.4), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: ElevatedButton.icon( + onPressed: _isLoading ? null : _guardarComponente, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + icon: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ) + : const Icon(Icons.save_rounded, + color: Colors.white, size: 20), + label: Text( + _isLoading ? 'Guardando...' : 'Crear Componente', + style: const TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _isLoading + ? null + : () => Navigator.of(context).pop(), + icon: const Icon(Icons.close, size: 18), + label: const Text('Cancelar'), + style: OutlinedButton.styleFrom( + foregroundColor: AppTheme.of(context).secondaryText, + side: BorderSide( + color: AppTheme.of(context) + .secondaryText + .withOpacity(0.5), + ), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ); + } + + Widget _buildCompactFormField({ + required TextEditingController controller, + required String label, + required String hint, + required IconData icon, + String? Function(String?)? validator, + int? maxLines, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 4), + child: TextFormField( + controller: controller, + validator: validator, + maxLines: maxLines ?? 1, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + ), + decoration: InputDecoration( + labelText: label, + labelStyle: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 14, + ), + hintText: hint, + hintStyle: TextStyle( + color: AppTheme.of(context).secondaryText.withOpacity(0.7), + fontSize: 13, + ), + 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, 2), + ), + ], + ), + child: Icon( + icon, + color: Colors.white, + size: 20, + ), + ), + filled: true, + fillColor: AppTheme.of(context).secondaryBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + width: 1, + ), + ), + 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: 1), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + ), + ), + ); + } + + Widget _buildCategoriaDropdown() { + return Container( + margin: const EdgeInsets.only(bottom: 4), + child: Theme( + data: Theme.of(context).copyWith( + canvasColor: AppTheme.of(context).secondaryBackground, + shadowColor: AppTheme.of(context).primaryColor.withOpacity(0.3), + ), + child: DropdownButtonFormField( + value: _categoriaSeleccionada, + onChanged: (value) { + if (value != null) { + setState(() { + _categoriaSeleccionada = value; + }); + } + }, + validator: (value) { + if (value == null) { + return 'Seleccione una categoría'; + } + return null; + }, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + ), + icon: Icon( + Icons.arrow_drop_down, + color: AppTheme.of(context).primaryColor, + ), + iconSize: 24, + isExpanded: true, // Esto soluciona el overflow + menuMaxHeight: 300, + decoration: InputDecoration( + labelText: 'Categoría', + labelStyle: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 14, + ), + 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, 2), + ), + ], + ), + child: const Icon( + Icons.category_rounded, + color: Colors.white, + size: 20, + ), + ), + filled: true, + fillColor: AppTheme.of(context).secondaryBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide( + color: AppTheme.of(context).primaryColor, + width: 2, + ), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + ), + dropdownColor: AppTheme.of(context).secondaryBackground, + items: widget.provider.categorias.map((categoria) { + return DropdownMenuItem( + value: categoria.id, + child: Container( + constraints: const BoxConstraints(minHeight: 48), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: AppTheme.of(context).primaryColor, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + categoria.nombre, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ); + } + + Widget _buildSwitchCard({ + required String title, + required String subtitle, + required bool value, + required Function(bool) onChanged, + required IconData icon, + required Color activeColor, + }) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: value + ? activeColor.withOpacity(0.3) + : AppTheme.of(context).primaryColor.withOpacity(0.1), + ), + boxShadow: [ + BoxShadow( + color: value + ? activeColor.withOpacity(0.1) + : Colors.black.withOpacity(0.02), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: (value ? activeColor : Colors.grey).withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: value ? activeColor : Colors.grey, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + Text( + subtitle, + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 12, + ), + ), + ], + ), + ), + Switch( + value: value, + onChanged: onChanged, + activeColor: activeColor, + activeTrackColor: activeColor.withOpacity(0.3), + inactiveThumbColor: Colors.grey, + inactiveTrackColor: Colors.grey.withOpacity(0.3), + ), + ], + ), + ); + } + + Widget _buildStatusIndicator(String text, IconData icon, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withOpacity(0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: color, size: 16), + const SizedBox(width: 6), + Text( + text, + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } + + Future _guardarComponente() async { + if (!_formKey.currentState!.validate()) { + return; + } + + if (_categoriaSeleccionada == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Debe seleccionar una categoría'), + backgroundColor: Colors.red, + ), + ); + return; + } + + if (widget.provider.negocioSeleccionadoId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error: No se ha seleccionado un negocio'), + backgroundColor: Colors.red, + ), + ); + return; + } + + setState(() { + _isLoading = true; + }); + + try { + final success = await widget.provider.crearComponente( + negocioId: widget.provider.negocioSeleccionadoId!, + categoriaId: _categoriaSeleccionada!, + nombre: _nombreController.text.trim(), + descripcion: _descripcionController.text.trim().isNotEmpty + ? _descripcionController.text.trim() + : null, + enUso: _enUso, + activo: _activo, + ubicacion: _ubicacionController.text.trim().isNotEmpty + ? _ubicacionController.text.trim() + : null, + ); + + if (success) { + if (mounted) { + 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 creado exitosamente', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: const [ + Icon(Icons.error, color: Colors.white), + SizedBox(width: 12), + Text( + 'Error al crear 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 0921508..87c0585 100644 --- a/lib/providers/nethive/componentes_provider.dart +++ b/lib/providers/nethive/componentes_provider.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -62,10 +61,26 @@ class ComponentesProvider extends ChangeNotifier { DetalleRouterFirewall? detalleRouterFirewall; DetalleEquipoActivo? detalleEquipoActivo; + // Variable para controlar si el provider está activo + bool _isDisposed = false; + ComponentesProvider() { getCategorias(); } + @override + void dispose() { + _isDisposed = true; + super.dispose(); + } + + // Método seguro para notificar listeners + void _safeNotifyListeners() { + if (!_isDisposed) { + notifyListeners(); + } + } + // Métodos para categorías Future getCategorias([String? busqueda]) async { try { @@ -82,7 +97,7 @@ class ComponentesProvider extends ChangeNotifier { .toList(); _buildCategoriasRows(); - notifyListeners(); + _safeNotifyListeners(); } catch (e) { print('Error en getCategorias: ${e.toString()}'); } @@ -122,7 +137,7 @@ class ComponentesProvider extends ChangeNotifier { .toList(); _buildComponentesRows(); - notifyListeners(); + _safeNotifyListeners(); } catch (e) { print('Error en getComponentesPorNegocio: ${e.toString()}'); } @@ -177,7 +192,7 @@ class ComponentesProvider extends ChangeNotifier { imagenToUpload = picker.files.single.bytes; } - notifyListeners(); + _safeNotifyListeners(); } Future uploadImagen() async { @@ -328,15 +343,44 @@ class ComponentesProvider extends ChangeNotifier { Future eliminarComponente(String componenteId) async { try { + // Primero obtener la información del componente para obtener la URL de la imagen + final componenteData = await supabaseLU + .from('componente') + .select('imagen_url') + .eq('id', componenteId) + .maybeSingle(); + + // Guardar la URL de la imagen para eliminarla después + String? imagenUrl; + if (componenteData != null && componenteData['imagen_url'] != null) { + imagenUrl = componenteData['imagen_url'] as String; + } + // Eliminar todos los detalles específicos primero await _eliminarDetallesComponente(componenteId); - // Luego eliminar el componente + // Eliminar el componente de la base de datos await supabaseLU.from('componente').delete().eq('id', componenteId); - if (negocioSeleccionadoId != null) { + // Actualizar la lista ANTES de eliminar la imagen + if (!_isDisposed && negocioSeleccionadoId != null) { await getComponentesPorNegocio(negocioSeleccionadoId!); } + + // AHORA eliminar la imagen del storage (después de que la UI se haya actualizado) + if (imagenUrl != null) { + try { + await supabaseLU.storage + .from('nethive') + .remove(["componentes/$imagenUrl"]); + print('Imagen eliminada del storage: $imagenUrl'); + } catch (storageError) { + print( + 'Error al eliminar imagen del storage: ${storageError.toString()}'); + // No retornamos false aquí porque el componente ya fue eliminado exitosamente + } + } + return true; } catch (e) { print('Error en eliminarComponente: ${e.toString()}'); @@ -344,42 +388,6 @@ class ComponentesProvider extends ChangeNotifier { } } - Future _eliminarDetallesComponente(String componenteId) async { - // Eliminar de todas las tablas de detalles - await supabaseLU - .from('detalle_cable') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_switch') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_patch_panel') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_rack') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_organizador') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_ups') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_router_firewall') - .delete() - .eq('componente_id', componenteId); - await supabaseLU - .from('detalle_equipo_activo') - .delete() - .eq('componente_id', componenteId); - } - // Métodos para obtener detalles específicos Future getDetallesComponente( String componenteId, int categoriaId) async { @@ -408,7 +416,7 @@ class ComponentesProvider extends ChangeNotifier { } showDetallesEspecificos = true; - notifyListeners(); + _safeNotifyListeners(); } catch (e) { print('Error en getDetallesComponente: ${e.toString()}'); } @@ -535,7 +543,7 @@ class ComponentesProvider extends ChangeNotifier { detalleRouterFirewall = null; detalleEquipoActivo = null; - notifyListeners(); + _safeNotifyListeners(); } void buscarComponentes(String busqueda) { @@ -687,7 +695,7 @@ class ComponentesProvider extends ChangeNotifier { // Cargar toda la información de topología para este negocio await cargarTopologiaCompleta(negocioId); - notifyListeners(); + _safeNotifyListeners(); } catch (e) { print('Error en setNegocioSeleccionado: ${e.toString()}'); } @@ -714,7 +722,7 @@ class ComponentesProvider extends ChangeNotifier { // Cargar toda la información de topología de forma optimizada Future cargarTopologiaCompleta(String negocioId) async { isLoadingTopologia = true; - notifyListeners(); + _safeNotifyListeners(); try { // Cargar datos en paralelo para mejor performance @@ -733,7 +741,7 @@ class ComponentesProvider extends ChangeNotifier { ]; } finally { isLoadingTopologia = false; - notifyListeners(); + _safeNotifyListeners(); } } @@ -952,73 +960,6 @@ class ComponentesProvider extends ChangeNotifier { } } - // 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 = []; @@ -1158,4 +1099,63 @@ class ComponentesProvider extends ChangeNotifier { return sugerencias; } + + // Obtener estadísticas de conectividad + Map getEstadisticasConectividad() { + final totalComponentes = componentes.length; + final componentesActivos = componentes.where((c) => c.activo).length; + final componentesEnUso = componentes.where((c) => c.enUso).length; + final conexionesActivas = conexiones.where((c) => c.activo).length; + + return { + 'totalComponentes': totalComponentes.toDouble(), + 'componentesActivos': componentesActivos.toDouble(), + 'componentesEnUso': componentesEnUso.toDouble(), + 'conexionesActivas': conexionesActivas.toDouble(), + 'porcentajeActivos': totalComponentes > 0 + ? (componentesActivos / totalComponentes) * 100 + : 0, + 'porcentajeUso': componentesActivos > 0 + ? (componentesEnUso / componentesActivos) * 100 + : 0, + 'densidadConexiones': + componentesActivos > 0 ? (conexionesActivas / componentesActivos) : 0, + }; + } + + Future _eliminarDetallesComponente(String componenteId) async { + // Eliminar de todas las tablas de detalles + await supabaseLU + .from('detalle_cable') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_switch') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_patch_panel') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_rack') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_organizador') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_ups') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_router_firewall') + .delete() + .eq('componente_id', componenteId); + await supabaseLU + .from('detalle_equipo_activo') + .delete() + .eq('componente_id', componenteId); + } } diff --git a/lib/providers/nethive/empresas_negocios_provider.dart b/lib/providers/nethive/empresas_negocios_provider.dart index 18a23f5..b569cd2 100644 --- a/lib/providers/nethive/empresas_negocios_provider.dart +++ b/lib/providers/nethive/empresas_negocios_provider.dart @@ -35,10 +35,28 @@ class EmpresasNegociosProvider extends ChangeNotifier { String? empresaSeleccionadaId; Empresa? empresaSeleccionada; + // Variable para controlar si el provider está activo + bool _isDisposed = false; + EmpresasNegociosProvider() { getEmpresas(); } + @override + void dispose() { + _isDisposed = true; + busquedaEmpresaController.dispose(); + busquedaNegocioController.dispose(); + super.dispose(); + } + + // Método seguro para notificar listeners + void _safeNotifyListeners() { + if (!_isDisposed) { + notifyListeners(); + } + } + // Métodos para empresas Future getEmpresas([String? busqueda]) async { try { @@ -56,7 +74,7 @@ class EmpresasNegociosProvider extends ChangeNotifier { .toList(); _buildEmpresasRows(); - notifyListeners(); + _safeNotifyListeners(); } catch (e) { print('Error en getEmpresas: ${e.toString()}'); } @@ -105,7 +123,7 @@ class EmpresasNegociosProvider extends ChangeNotifier { .toList(); _buildNegociosRows(); - notifyListeners(); + _safeNotifyListeners(); } catch (e) { print('Error en getNegociosPorEmpresa: ${e.toString()}'); } @@ -164,7 +182,7 @@ class EmpresasNegociosProvider extends ChangeNotifier { logoToUpload = picker.files.single.bytes; // Notificar inmediatamente después de seleccionar - notifyListeners(); + _safeNotifyListeners(); } } @@ -186,7 +204,7 @@ class EmpresasNegociosProvider extends ChangeNotifier { imagenToUpload = picker.files.single.bytes; // Notificar inmediatamente después de seleccionar - notifyListeners(); + _safeNotifyListeners(); } } @@ -297,7 +315,10 @@ class EmpresasNegociosProvider extends ChangeNotifier { // Luego eliminar la empresa await supabaseLU.from('empresa').delete().eq('id', empresaId); - await getEmpresas(); + // Solo actualizar si el provider sigue activo + if (!_isDisposed) { + await getEmpresas(); + } return true; } catch (e) { print('Error en eliminarEmpresa: ${e.toString()}'); @@ -309,7 +330,8 @@ class EmpresasNegociosProvider extends ChangeNotifier { try { await supabaseLU.from('negocio').delete().eq('id', negocioId); - if (empresaSeleccionadaId != null) { + // Solo actualizar si el provider sigue activo y hay una empresa seleccionada + if (!_isDisposed && empresaSeleccionadaId != null) { await getNegociosPorEmpresa(empresaSeleccionadaId!); } return true; @@ -324,7 +346,7 @@ class EmpresasNegociosProvider extends ChangeNotifier { empresaSeleccionadaId = empresaId; empresaSeleccionada = empresas.firstWhere((e) => e.id == empresaId); getNegociosPorEmpresa(empresaId); - notifyListeners(); + _safeNotifyListeners(); } void resetFormData() { @@ -332,7 +354,7 @@ class EmpresasNegociosProvider extends ChangeNotifier { imagenFileName = null; logoToUpload = null; imagenToUpload = null; - notifyListeners(); + _safeNotifyListeners(); } void buscarEmpresas(String busqueda) {