From 3e7a26dadc95742bbd5ab6d1d5b64b4b4ce983c7 Mon Sep 17 00:00:00 2001 From: Abraham Date: Wed, 16 Jul 2025 21:54:10 -0700 Subject: [PATCH] primera pagina empresa y negocios alfa --- .../empresa_negocios_page.dart | 753 ++++-- .../widgets/add_empresa_dialog.dart | 1674 ++++++++++-- .../widgets/add_negocio_dialog.dart | 2293 ++++++++++++++--- .../widgets/empresa_selector_sidebar.dart | 783 ++++-- .../widgets/mobile_empresa_selector.dart | 472 ++++ .../widgets/negocios_cards_view.dart | 646 +++++ .../nethive/empresas_negocios_provider.dart | 10 +- lib/theme/theme.dart | 95 +- 8 files changed, 5729 insertions(+), 997 deletions(-) create mode 100644 lib/pages/empresa_negocios/widgets/mobile_empresa_selector.dart create mode 100644 lib/pages/empresa_negocios/widgets/negocios_cards_view.dart diff --git a/lib/pages/empresa_negocios/empresa_negocios_page.dart b/lib/pages/empresa_negocios/empresa_negocios_page.dart index 0bb7631..8457796 100644 --- a/lib/pages/empresa_negocios/empresa_negocios_page.dart +++ b/lib/pages/empresa_negocios/empresa_negocios_page.dart @@ -3,7 +3,10 @@ import 'package:provider/provider.dart'; import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; import 'package:nethive_neo/pages/empresa_negocios/widgets/empresa_selector_sidebar.dart'; import 'package:nethive_neo/pages/empresa_negocios/widgets/negocios_table.dart'; +import 'package:nethive_neo/pages/empresa_negocios/widgets/negocios_cards_view.dart'; +import 'package:nethive_neo/pages/empresa_negocios/widgets/mobile_empresa_selector.dart'; import 'package:nethive_neo/theme/theme.dart'; +import 'package:nethive_neo/helpers/globals.dart'; class EmpresaNegociosPage extends StatefulWidget { const EmpresaNegociosPage({Key? key}) : super(key: key); @@ -12,135 +15,380 @@ class EmpresaNegociosPage extends StatefulWidget { State createState() => _EmpresaNegociosPageState(); } -class _EmpresaNegociosPageState extends State { +class _EmpresaNegociosPageState extends State + with TickerProviderStateMixin { bool showMapView = false; + late AnimationController _animationController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + @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, + )); + _slideAnimation = Tween( + begin: const Offset(0, 0.3), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutCubic, + )); + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { + final isLargeScreen = MediaQuery.of(context).size.width > 1200; + final isMediumScreen = MediaQuery.of(context).size.width > 800; + return Scaffold( backgroundColor: AppTheme.of(context).primaryBackground, - body: Consumer( - builder: (context, provider, child) { - return Row( - children: [ - // Sidebar izquierdo con empresas - SizedBox( - width: 300, - child: EmpresaSelectorSidebar( - provider: provider, - onEmpresaSelected: (empresaId) { - provider.setEmpresaSeleccionada(empresaId); - }, - ), - ), - - // Área principal - Expanded( - child: Container( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header con título y switch - _buildHeader(provider), - - const SizedBox(height: 16), - - // Contenido principal (tabla o mapa) - Expanded( - child: showMapView - ? _buildMapView() - : _buildTableView(provider), + body: Container( + decoration: BoxDecoration( + gradient: AppTheme.of(context).darkBackgroundGradient, + ), + child: Consumer( + builder: (context, provider, child) { + if (isLargeScreen) { + // Vista de escritorio + return Row( + children: [ + // Sidebar izquierdo con empresas + SlideTransition( + position: Tween( + begin: const Offset(-1, 0), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutCubic, + )), + child: SizedBox( + width: 320, + child: EmpresaSelectorSidebar( + provider: provider, + onEmpresaSelected: (empresaId) { + provider.setEmpresaSeleccionada(empresaId); + }, ), - ], + ), ), - ), - ), - ], - ); - }, + + // Área principal + Expanded( + child: SlideTransition( + position: _slideAnimation, + child: FadeTransition( + opacity: _fadeAnimation, + child: Container( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header con título y switch + _buildEnhancedHeader(provider), + + const SizedBox(height: 24), + + // Contenido principal (tabla o mapa) + Expanded( + child: showMapView + ? _buildMapView() + : _buildTableView(provider), + ), + ], + ), + ), + ), + ), + ), + ], + ); + } else { + // Vista móvil/tablet + return Column( + children: [ + // Header móvil + _buildMobileHeader(provider), + + // Contenido principal + Expanded( + child: showMapView + ? _buildMapView() + : NegociosCardsView(provider: provider), + ), + ], + ); + } + }, + ), ), + // FAB para vista móvil + floatingActionButton: MediaQuery.of(context).size.width <= 800 + ? _buildMobileFAB(context) + : null, ); } - Widget _buildHeader(EmpresasNegociosProvider provider) { + Widget _buildEnhancedHeader(EmpresasNegociosProvider provider) { return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(24), decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(12), + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(20), boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 8), + ), BoxShadow( color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), + blurRadius: 10, + offset: const Offset(0, 4), ), ], ), child: Row( children: [ + // Icono animado + TweenAnimationBuilder( + duration: const Duration(milliseconds: 1000), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Transform.rotate( + angle: value * 0.1, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 2, + ), + ), + child: Icon( + Icons.business_center, + color: Colors.white, + size: 32, + ), + ), + ); + }, + ), + + const SizedBox(width: 20), + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - provider.empresaSeleccionada != null - ? 'Sucursales de ${provider.empresaSeleccionada!.nombre}' - : 'Selecciona una empresa para ver sus sucursales', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 24, - fontWeight: FontWeight.bold, + // Título principal con gradiente + ShaderMask( + shaderCallback: (bounds) => LinearGradient( + colors: [Colors.white, Colors.white.withOpacity(0.8)], + ).createShader(bounds), + child: Text( + provider.empresaSeleccionada != null + ? 'Sucursales de ${provider.empresaSeleccionada!.nombre}' + : 'Gestión de Empresas y Sucursales', + style: const TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), ), ), - const SizedBox(height: 8), - if (provider.empresaSeleccionada != null) + const SizedBox(height: 12), + if (provider.empresaSeleccionada != null) ...[ Row( children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '${provider.negocios.length} sucursales', - style: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), + // Badge animado + TweenAnimationBuilder( + duration: const Duration(milliseconds: 600), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.store, + color: AppTheme.of(context).primaryColor, + size: 18, + ), + const SizedBox(width: 8), + Text( + '${provider.negocios.length} sucursales', + style: TextStyle( + color: AppTheme.of(context).primaryColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + }, ), - const SizedBox(width: 12), - Text( - provider.empresaSeleccionada!.nombre, - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 16, + const SizedBox(width: 16), + Expanded( + child: Text( + provider.empresaSeleccionada!.rfc, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 16, + fontWeight: FontWeight.w500, + ), ), ), ], ), + ], ], ), ), - // Switch para cambiar vista - Column( + // Switch mejorado para cambiar vista + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withOpacity(0.2), + ), + ), + child: Column( + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.table_chart, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Vista', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.map, + color: Colors.white, + size: 20, + ), + ], + ), + const SizedBox(height: 12), + Switch( + value: showMapView, + onChanged: (value) { + setState(() { + showMapView = value; + }); + }, + activeColor: Colors.white, + activeTrackColor: Colors.white.withOpacity(0.3), + inactiveThumbColor: Colors.white.withOpacity(0.7), + inactiveTrackColor: Colors.white.withOpacity(0.1), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildMobileHeader(EmpresasNegociosProvider provider) { + return Container( + padding: const EdgeInsets.fromLTRB(20, 40, 20, 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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Text( - 'Vista de Mapa', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w500, + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.business, + color: Colors.white, + size: 24, ), ), - const SizedBox(height: 8), + const SizedBox(width: 16), + Expanded( + child: Text( + 'NETHIVE', + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + ), + ), + // Switch para modo vista Switch( value: showMapView, onChanged: (value) { @@ -148,10 +396,37 @@ class _EmpresaNegociosPageState extends State { showMapView = value; }); }, - activeColor: AppTheme.of(context).primaryColor, + activeColor: Colors.white, ), ], ), + if (provider.empresaSeleccionada != null) ...[ + const SizedBox(height: 16), + Text( + provider.empresaSeleccionada!.nombre, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${provider.negocios.length} sucursales', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ], ), ); @@ -159,74 +434,89 @@ class _EmpresaNegociosPageState extends State { Widget _buildTableView(EmpresasNegociosProvider provider) { if (provider.empresaSeleccionada == null) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.business, - size: 80, - color: AppTheme.of(context).secondaryText, - ), - const SizedBox(height: 16), - Text( - 'Selecciona una empresa para ver sus sucursales', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 18, - ), - ), - ], - ), - ); + return _buildEmptyState(); } return Container( decoration: BoxDecoration( color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + width: 1, + ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), + blurRadius: 15, + offset: const Offset(0, 5), ), ], ), child: Column( children: [ - // Header de la tabla + // Header de la tabla mejorado Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor, + gradient: AppTheme.of(context).modernGradient, borderRadius: const BorderRadius.only( - topLeft: Radius.circular(12), - topRight: Radius.circular(12), + topLeft: Radius.circular(20), + topRight: Radius.circular(20), ), ), child: Row( children: [ - Icon( - Icons.store, - color: Colors.white, - size: 24, - ), - const SizedBox(width: 12), - Text( - 'Sucursales', - style: const TextStyle( + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.store, color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, + size: 20, ), ), - const Spacer(), - Text( - 'Mostrando 1 a ${provider.negocios.length} de ${provider.negocios.length} sucursales', - style: const TextStyle( - color: Colors.white70, - fontSize: 14, + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Sucursales', + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + Text( + 'Gestión y control de ubicaciones', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 14, + ), + ), + ], + ), + ), + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${provider.negocios.length} registros', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), ), ), ], @@ -242,44 +532,185 @@ class _EmpresaNegociosPageState extends State { ); } + Widget _buildEmptyState() { + return Center( + child: TweenAnimationBuilder( + duration: const Duration(milliseconds: 1000), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: Container( + padding: const EdgeInsets.all(40), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.business_center, + size: 80, + color: Colors.white, + ), + const SizedBox(height: 20), + Text( + 'Selecciona una empresa', + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Text( + 'Elige una empresa del panel lateral\npara ver y gestionar sus sucursales', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 16, + ), + ), + ], + ), + ), + ); + }, + ), + ); + } + Widget _buildMapView() { return Container( decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.3), - borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.blue.withOpacity(0.1), + Colors.blue.withOpacity(0.3), + AppTheme.of(context).primaryColor.withOpacity(0.2), + ], + ), + borderRadius: BorderRadius.circular(20), border: Border.all( - color: Colors.blue, + color: AppTheme.of(context).primaryColor, width: 2, ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], ), child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.map, - size: 80, - color: Colors.blue[700], - ), - const SizedBox(height: 16), - Text( - 'Vista de Mapa', - style: TextStyle( - color: Colors.blue[700], - fontSize: 24, - fontWeight: FontWeight.bold, + child: TweenAnimationBuilder( + duration: const Duration(milliseconds: 1500), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Transform.scale( + scale: 0.8 + (0.2 * value), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppTheme.of(context).modernGradient, + borderRadius: BorderRadius.circular(20), + ), + child: Icon( + Icons.map, + size: 80, + color: Colors.white, + ), + ), + const SizedBox(height: 24), + Text( + 'Vista de Mapa', + style: TextStyle( + color: AppTheme.of(context).primaryColor, + fontSize: 28, + fontWeight: FontWeight.bold, + letterSpacing: 1, + ), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(15), + ), + child: Text( + 'Próximamente se implementará el mapa interactivo\ncon las ubicaciones de todas las sucursales', + style: TextStyle( + color: AppTheme.of(context).primaryColor, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + ], ), + ); + }, + ), + ), + ); + } + + Widget _buildMobileFAB(BuildContext context) { + return Consumer( + builder: (context, provider, child) { + return FloatingActionButton.extended( + onPressed: () { + _showMobileEmpresaSelector(context, provider); + }, + backgroundColor: AppTheme.of(context).primaryColor, + icon: const Icon(Icons.business, color: Colors.white), + label: Text( + provider.empresaSeleccionada != null + ? provider.empresaSeleccionada!.nombre.length > 15 + ? '${provider.empresaSeleccionada!.nombre.substring(0, 15)}...' + : provider.empresaSeleccionada!.nombre + : 'Empresas', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, ), - const SizedBox(height: 8), - Text( - 'Próximamente se implementará el mapa con las ubicaciones de las sucursales', - style: TextStyle( - color: Colors.blue[600], - fontSize: 16, - ), - textAlign: TextAlign.center, - ), - ], + ), + ); + }, + ); + } + + void _showMobileEmpresaSelector( + BuildContext context, EmpresasNegociosProvider provider) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.7, + maxChildSize: 0.95, + minChildSize: 0.3, + builder: (context, scrollController) => MobileEmpresaSelector( + provider: provider, + onEmpresaSelected: (empresaId) { + provider.setEmpresaSeleccionada(empresaId); + }, ), ), ); diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog.dart index 7413404..46538c5 100644 --- a/lib/pages/empresa_negocios/widgets/add_empresa_dialog.dart +++ b/lib/pages/empresa_negocios/widgets/add_empresa_dialog.dart @@ -14,7 +14,8 @@ class AddEmpresaDialog extends StatefulWidget { State createState() => _AddEmpresaDialogState(); } -class _AddEmpresaDialogState extends State { +class _AddEmpresaDialogState extends State + with TickerProviderStateMixin { final _formKey = GlobalKey(); final _nombreController = TextEditingController(); final _rfcController = TextEditingController(); @@ -23,9 +24,87 @@ class _AddEmpresaDialogState extends State { final _emailController = TextEditingController(); bool _isLoading = false; + late AnimationController _scaleController; + late AnimationController _slideController; + late AnimationController _fadeController; + late Animation _scaleAnimation; + late Animation _slideAnimation; + late Animation _fadeAnimation; + bool _isAnimationInitialized = false; + + @override + void initState() { + super.initState(); + _initializeAnimations(); + // Escuchar cambios del provider + widget.provider.addListener(_onProviderChanged); + } + + void _onProviderChanged() { + if (mounted) { + setState(() { + // Forzar rebuild cuando cambie el provider + }); + } + } + + void _initializeAnimations() { + _scaleController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + _slideController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + _fadeController = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: this, + ); + + _scaleAnimation = CurvedAnimation( + parent: _scaleController, + curve: Curves.elasticOut, + ); + _slideAnimation = Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _slideController, + curve: Curves.easeOutBack, + )); + _fadeAnimation = CurvedAnimation( + parent: _fadeController, + curve: Curves.easeInOut, + ); + + // Pequeño delay para asegurar que el widget esté completamente montado + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _isAnimationInitialized = true; + }); + _startAnimations(); + } + }); + } + + void _startAnimations() { + _fadeController.forward(); + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) _scaleController.forward(); + }); + Future.delayed(const Duration(milliseconds: 200), () { + if (mounted) _slideController.forward(); + }); + } @override void dispose() { + widget.provider.removeListener(_onProviderChanged); + _scaleController.dispose(); + _slideController.dispose(); + _fadeController.dispose(); _nombreController.dispose(); _rfcController.dispose(); _direccionController.dispose(); @@ -36,273 +115,1339 @@ class _AddEmpresaDialogState extends State { @override Widget build(BuildContext context) { - return AlertDialog( - backgroundColor: AppTheme.of(context).primaryBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - title: Row( - children: [ - Icon( - Icons.business, - color: AppTheme.of(context).primaryColor, - ), - const SizedBox(width: 8), - Text( - 'Añadir Nueva Empresa', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, + if (!_isAnimationInitialized) { + return const SizedBox.shrink(); + } + + // Detectar el tamaño de pantalla + final screenSize = MediaQuery.of(context).size; + final isDesktop = screenSize.width > 1024; + final isTablet = screenSize.width > 768 && screenSize.width <= 1024; + + // Ajustar dimensiones según el tipo de pantalla + final maxWidth = isDesktop ? 900.0 : (isTablet ? 750.0 : 650.0); + final maxHeight = isDesktop ? 700.0 : 750.0; + + return AnimatedBuilder( + animation: + Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]), + builder: (context, child) { + return FadeTransition( + opacity: _fadeAnimation, + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), + child: Transform.scale( + scale: _scaleAnimation.value, + child: Container( + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: maxHeight, + minHeight: isDesktop ? 600 : 400, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.4), + blurRadius: 40, + offset: const Offset(0, 20), + spreadRadius: 8, + ), + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 60, + offset: const Offset(0, 10), + spreadRadius: 2, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(30), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryBackground, + AppTheme.of(context).secondaryBackground, + AppTheme.of(context).tertiaryBackground, + ], + stops: const [0.0, 0.6, 1.0], + ), + ), + child: isDesktop + ? _buildDesktopLayout() + : _buildMobileLayout(), + ), + ), + ), ), ), - ], - ), - content: SizedBox( - width: 500, - child: Form( - key: _formKey, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Nombre - TextFormField( - controller: _nombreController, - decoration: InputDecoration( - labelText: 'Nombre de la empresa *', - hintText: 'Ej: TechCorp Solutions', - prefixIcon: Icon(Icons.business), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - const SizedBox(height: 16), + ); + }, + ); + } - // RFC - TextFormField( - controller: _rfcController, - decoration: InputDecoration( - labelText: 'RFC *', - hintText: 'Ej: ABC123456789', - prefixIcon: Icon(Icons.assignment), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El RFC es requerido'; - } - return null; - }, - ), - const SizedBox(height: 16), - - // Dirección - TextFormField( - controller: _direccionController, - maxLines: 3, - decoration: InputDecoration( - labelText: 'Dirección *', - hintText: 'Dirección completa de la empresa', - prefixIcon: Icon(Icons.location_on), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La dirección es requerida'; - } - return null; - }, - ), - const SizedBox(height: 16), - - // Teléfono - TextFormField( - controller: _telefonoController, - decoration: InputDecoration( - labelText: 'Teléfono *', - hintText: 'Ej: +52 555 123 4567', - prefixIcon: Icon(Icons.phone), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El teléfono es requerido'; - } - return null; - }, - ), - const SizedBox(height: 16), - - // Email - TextFormField( - controller: _emailController, - keyboardType: TextInputType.emailAddress, - decoration: InputDecoration( - labelText: 'Email *', - hintText: 'contacto@empresa.com', - prefixIcon: Icon(Icons.email), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El email es requerido'; - } - if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') - .hasMatch(value)) { - return 'Email inválido'; - } - return null; - }, - ), - const SizedBox(height: 24), - - // Sección de archivos - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Colors.grey.withOpacity(0.3), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Archivos (Opcional)', - style: TextStyle( - fontWeight: FontWeight.bold, - color: AppTheme.of(context).primaryText, + Widget _buildDesktopLayout() { + return Row( + children: [ + // Header lateral compacto para desktop + Container( + width: 280, + 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 compacto + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + Colors.white.withOpacity(0.4), + Colors.white.withOpacity(0.1), + Colors.transparent, + ], + ), + border: Border.all( + color: Colors.white.withOpacity(0.6), + width: 3, + ), + boxShadow: [ + BoxShadow( + color: Colors.white.withOpacity(0.4), + blurRadius: 20, + spreadRadius: 5, ), - ), - const SizedBox(height: 12), + ], + ), + child: const Icon( + Icons.business_center_rounded, + color: Colors.white, + size: 35, + ), + ), + const SizedBox(height: 20), - // Logo - Row( + // Título compacto + Text( + 'Nueva Empresa', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + + Container( + padding: + const EdgeInsets.symmetric(horizontal: 15, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Colors.white.withOpacity(0.3), + ), + ), + child: Text( + '✨ Registra una nueva empresa', + style: TextStyle( + color: Colors.white.withOpacity(0.95), + fontSize: 14, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ), + + // Contenido principal del formulario + Expanded( + child: Padding( + padding: const EdgeInsets.all(25), + child: Form( + key: _formKey, + child: Column( + children: [ + // Formulario en columnas para aprovechar el espacio + Expanded( + child: SingleChildScrollView( + child: Column( children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: widget.provider.selectLogo, - icon: Icon(Icons.image), - label: Text( - widget.provider.logoFileName ?? - 'Seleccionar Logo', - overflow: TextOverflow.ellipsis, + // Primera fila - Nombre y RFC + Row( + children: [ + Expanded( + flex: 2, + child: _buildCompactFormField( + controller: _nombreController, + label: 'Nombre de la empresa', + hint: 'Ej: TechCorp Solutions S.A.', + icon: Icons.business_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El nombre es requerido'; + } + return null; + }, + ), ), + const SizedBox(width: 16), + Expanded( + child: _buildCompactFormField( + controller: _rfcController, + label: 'RFC', + hint: 'Ej: ABC123456789', + icon: Icons.assignment_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El RFC es requerido'; + } + return null; + }, + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Segunda fila - Dirección + _buildCompactFormField( + controller: _direccionController, + label: 'Dirección', + hint: 'Dirección completa de la empresa', + icon: Icons.location_on_rounded, + maxLines: 2, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'La dirección es requerida'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + // Tercera fila - Teléfono y Email + Row( + children: [ + Expanded( + child: _buildCompactFormField( + controller: _telefonoController, + label: 'Teléfono', + hint: 'Ej: +52 555 123 4567', + icon: Icons.phone_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El teléfono es requerido'; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildCompactFormField( + controller: _emailController, + label: 'Email', + hint: 'contacto@empresa.com', + icon: Icons.email_rounded, + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El email es requerido'; + } + if (!RegExp( + r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') + .hasMatch(value)) { + return 'Email inválido'; + } + return null; + }, + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Sección de archivos compacta + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context) + .primaryColor + .withOpacity(0.1), + AppTheme.of(context) + .tertiaryColor + .withOpacity(0.1), + AppTheme.of(context) + .secondaryColor + .withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.4), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header de la sección + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: AppTheme.of(context) + .primaryGradient, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.4), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon( + Icons.cloud_upload_rounded, + color: Colors.white, + size: 18, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Archivos Opcionales', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: AppTheme.of(context) + .primaryText, + ), + ), + Text( + 'Logo e imagen de la empresa', + style: TextStyle( + fontSize: 12, + color: AppTheme.of(context) + .secondaryText, + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 16), + + // Botones de archivos en fila + Row( + children: [ + Expanded( + child: _buildCompactFileButton( + label: 'Logo de la empresa', + subtitle: 'PNG, JPG (Max 2MB)', + icon: Icons.image_rounded, + fileName: widget.provider.logoFileName, + file: widget.provider.logoToUpload, + onPressed: widget.provider.selectLogo, + gradient: LinearGradient( + colors: [ + Colors.blue.shade400, + Colors.blue.shade600 + ], + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildCompactFileButton( + label: 'Imagen principal', + subtitle: 'Imagen representativa', + icon: Icons.photo_library_rounded, + fileName: + widget.provider.imagenFileName, + file: widget.provider.imagenToUpload, + onPressed: widget.provider.selectImagen, + gradient: LinearGradient( + colors: [ + Colors.purple.shade400, + Colors.purple.shade600 + ], + ), + ), + ), + ], + ), + ], ), ), - if (widget.provider.logoToUpload != null) ...[ - const SizedBox(width: 8), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(4), + + const SizedBox(height: 25), + + // Botones de acción + Row( + children: [ + // Botón cancelar + Expanded( + child: Container( + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppTheme.of(context) + .secondaryText + .withOpacity(0.4), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: TextButton( + onPressed: _isLoading + ? null + : () { + widget.provider.resetFormData(); + Navigator.of(context).pop(); + }, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Icon( + Icons.close_rounded, + color: AppTheme.of(context) + .secondaryText, + size: 18, + ), + const SizedBox(width: 8), + Text( + 'Cancelar', + style: TextStyle( + color: AppTheme.of(context) + .secondaryText, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), ), - child: widget.provider.getImageWidget( - widget.provider.logoToUpload, - height: 40, - width: 40, + + const SizedBox(width: 20), + + // Botón crear empresa + Expanded( + flex: 2, + child: Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + AppTheme.of(context).tertiaryColor, + ], + ), + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.5), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: ElevatedButton( + onPressed: + _isLoading ? null : _crearEmpresa, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: _isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: + AlwaysStoppedAnimation( + Colors.white), + ), + ) + : Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: const [ + Icon( + Icons.add_business_rounded, + color: Colors.white, + size: 20, + ), + SizedBox(width: 12), + Text( + 'Crear Empresa', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), ), - ), - ], + ], + ), ], ), - const SizedBox(height: 8), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } - // Imagen - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: widget.provider.selectImagen, - icon: Icon(Icons.photo), - label: Text( - widget.provider.imagenFileName ?? - 'Seleccionar Imagen', - overflow: TextOverflow.ellipsis, - ), - ), - ), - if (widget.provider.imagenToUpload != null) ...[ - const SizedBox(width: 8), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(4), - ), - child: widget.provider.getImageWidget( - widget.provider.imagenToUpload, - height: 40, - width: 40, - ), - ), - ], - ], + Widget _buildMobileLayout() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header espectacular con animación (más compacto) + SlideTransition( + position: _slideAnimation, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 25), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + AppTheme.of(context).tertiaryColor, + ], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.5), + blurRadius: 25, + offset: const Offset(0, 15), + spreadRadius: 2, + ), + ], + ), + child: Column( + children: [ + // Icono central compacto + Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + Colors.white.withOpacity(0.4), + Colors.white.withOpacity(0.1), + Colors.transparent, + ], + ), + border: Border.all( + color: Colors.white.withOpacity(0.6), + width: 3, + ), + boxShadow: [ + BoxShadow( + color: Colors.white.withOpacity(0.4), + blurRadius: 20, + spreadRadius: 5, ), ], ), + child: const Icon( + Icons.business_center_rounded, + color: Colors.white, + size: 35, + ), + ), + + const SizedBox(height: 16), + + // Título compacto + Text( + 'Nueva Empresa', + style: TextStyle( + color: Colors.white, + fontSize: 26, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + ), + + const SizedBox(height: 8), + + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: Colors.white.withOpacity(0.3), + ), + ), + child: Text( + '✨ Registra una nueva empresa en tu sistema', + style: TextStyle( + color: Colors.white.withOpacity(0.95), + fontSize: 14, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + ), ), ], ), ), ), - ), - actions: [ - TextButton( - onPressed: _isLoading - ? null - : () { - widget.provider.resetFormData(); - Navigator.of(context).pop(); - }, - child: Text( - 'Cancelar', - style: TextStyle( - color: AppTheme.of(context).secondaryText, + + // Contenido del formulario mejorado + Flexible( + child: SingleChildScrollView( + padding: const EdgeInsets.all(25), + child: Form( + key: _formKey, + child: Column( + children: [ + // Campos del formulario con animaciones compactas + _buildCompactFormField( + controller: _nombreController, + label: 'Nombre de la empresa', + hint: 'Ej: TechCorp Solutions S.A. de C.V.', + icon: Icons.business_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El nombre es requerido'; + } + return null; + }, + ), + + _buildCompactFormField( + controller: _rfcController, + label: 'RFC', + hint: 'Ej: ABC123456789', + icon: Icons.assignment_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El RFC es requerido'; + } + return null; + }, + ), + + _buildCompactFormField( + controller: _direccionController, + label: 'Dirección', + hint: 'Dirección completa de la empresa', + icon: Icons.location_on_rounded, + maxLines: 3, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'La dirección es requerida'; + } + return null; + }, + ), + + _buildCompactFormField( + controller: _telefonoController, + label: 'Teléfono', + hint: 'Ej: +52 555 123 4567', + icon: Icons.phone_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El teléfono es requerido'; + } + return null; + }, + ), + + _buildCompactFormField( + controller: _emailController, + label: 'Email', + hint: 'contacto@empresa.com', + icon: Icons.email_rounded, + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El email es requerido'; + } + if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$') + .hasMatch(value)) { + return 'Email inválido'; + } + return null; + }, + ), + + const SizedBox(height: 20), + + // Sección de archivos compacta para móvil + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor.withOpacity(0.1), + AppTheme.of(context).tertiaryColor.withOpacity(0.1), + AppTheme.of(context).secondaryColor.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: + AppTheme.of(context).primaryColor.withOpacity(0.4), + width: 2, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header de la sección + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.cloud_upload_rounded, + color: Colors.white, + size: 18, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Archivos Opcionales', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: AppTheme.of(context).primaryText, + ), + ), + Text( + 'Logo e imagen de la empresa', + style: TextStyle( + fontSize: 12, + color: AppTheme.of(context).secondaryText, + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 16), + + // Botones de archivos mejorados + _buildEnhancedFileButton( + label: 'Logo de la empresa', + subtitle: 'Formato PNG, JPG (Max 2MB)', + icon: Icons.image_rounded, + fileName: widget.provider.logoFileName, + file: widget.provider.logoToUpload, + onPressed: widget.provider.selectLogo, + gradient: LinearGradient( + colors: [ + Colors.blue.shade400, + Colors.blue.shade600 + ], + ), + ), + + const SizedBox(height: 12), + + _buildEnhancedFileButton( + label: 'Imagen principal', + subtitle: 'Imagen representativa de la empresa', + icon: Icons.photo_library_rounded, + fileName: widget.provider.imagenFileName, + file: widget.provider.imagenToUpload, + onPressed: widget.provider.selectImagen, + gradient: LinearGradient( + colors: [ + Colors.purple.shade400, + Colors.purple.shade600 + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 25), + + // Botones de acción compactos + Row( + children: [ + // Botón cancelar + Expanded( + child: Container( + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppTheme.of(context) + .secondaryText + .withOpacity(0.4), + width: 2, + ), + ), + child: TextButton( + onPressed: _isLoading + ? null + : () { + widget.provider.resetFormData(); + Navigator.of(context).pop(); + }, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.close_rounded, + color: AppTheme.of(context).secondaryText, + size: 18, + ), + const SizedBox(width: 8), + Text( + 'Cancelar', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(width: 16), + + // Botón crear empresa + Expanded( + flex: 2, + child: Container( + height: 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + AppTheme.of(context).tertiaryColor, + ], + ), + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.5), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: ElevatedButton( + onPressed: _isLoading ? null : _crearEmpresa, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: _isLoading + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon( + Icons.add_business_rounded, + color: Colors.white, + size: 20, + ), + SizedBox(width: 10), + Text( + 'Crear Empresa', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ], + ), ), ), ), - ElevatedButton( - onPressed: _isLoading ? null : _crearEmpresa, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.of(context).primaryColor, - foregroundColor: Colors.white, - ), - child: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : const Text('Crear Empresa'), - ), ], ); } + Widget _buildCompactFormField({ + required TextEditingController controller, + required String label, + required String hint, + required IconData icon, + int maxLines = 1, + TextInputType? keyboardType, + String? Function(String?)? validator, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + child: TextFormField( + controller: controller, + maxLines: maxLines, + keyboardType: keyboardType, + validator: validator, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + decoration: InputDecoration( + labelText: label, + hintText: hint, + prefixIcon: Container( + margin: const EdgeInsets.all(8), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Icon( + icon, + color: Colors.white, + size: 18, + ), + ), + labelStyle: TextStyle( + color: AppTheme.of(context).primaryColor, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + hintStyle: TextStyle( + color: AppTheme.of(context).secondaryText.withOpacity(0.7), + fontSize: 12, + ), + filled: true, + fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.7), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 2, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: BorderSide( + color: AppTheme.of(context).primaryColor, + width: 2, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(15), + borderSide: const BorderSide( + color: Colors.red, + width: 2, + ), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + ), + ), + ); + } + + Widget _buildCompactFileButton({ + required String label, + required String subtitle, + required IconData icon, + required String? fileName, + required dynamic file, + required VoidCallback onPressed, + required Gradient gradient, + }) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + // Icono con gradiente + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + gradient: gradient, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Icon( + icon, + color: Colors.white, + size: 20, + ), + ), + + const SizedBox(height: 8), + + // Información del archivo + Text( + label, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: 13, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Text( + fileName ?? subtitle, + style: TextStyle( + color: fileName != null + ? AppTheme.of(context).primaryColor + : AppTheme.of(context).secondaryText, + fontSize: 11, + fontWeight: + fileName != null ? FontWeight.w600 : FontWeight.normal, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + + // Preview de imagen si existe + if (file != null) ...[ + const SizedBox(height: 8), + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: + AppTheme.of(context).primaryColor.withOpacity(0.4), + width: 2, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: widget.provider.getImageWidget( + file, + height: 40, + width: 40, + ), + ), + ), + ], + ], + ), + ), + ), + ), + ); + } + + Widget _buildEnhancedFileButton({ + required String label, + required String subtitle, + required IconData icon, + required String? fileName, + required dynamic file, + required VoidCallback onPressed, + required Gradient gradient, + }) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + blurRadius: 15, + offset: const Offset(0, 6), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // Icono con gradiente + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: gradient, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon( + icon, + color: Colors.white, + size: 24, + ), + ), + + const SizedBox(width: 16), + + // Información del archivo + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + fileName ?? subtitle, + style: TextStyle( + color: fileName != null + ? AppTheme.of(context).primaryColor + : AppTheme.of(context).secondaryText, + fontSize: 13, + fontWeight: fileName != null + ? FontWeight.w600 + : FontWeight.normal, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + + // Preview de imagen si existe + if (file != null) ...[ + const SizedBox(width: 16), + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: + AppTheme.of(context).primaryColor.withOpacity(0.4), + width: 2, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: widget.provider.getImageWidget( + file, + height: 50, + width: 50, + ), + ), + ), + ] else ...[ + const SizedBox(width: 16), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.upload_file, + color: AppTheme.of(context).primaryColor, + size: 20, + ), + ), + ], + ], + ), + ), + ), + ), + ); + } + Future _crearEmpresa() async { if (!_formKey.currentState!.validate()) return; @@ -323,16 +1468,42 @@ class _AddEmpresaDialogState extends State { if (success) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Empresa creada exitosamente'), + SnackBar( + content: Row( + children: const [ + Icon(Icons.check_circle, color: Colors.white), + SizedBox(width: 12), + Text( + 'Empresa creada exitosamente', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), ); } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Error al crear la empresa'), + SnackBar( + content: Row( + children: const [ + Icon(Icons.error, color: Colors.white), + SizedBox(width: 12), + Text( + 'Error al crear la empresa', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), ); } @@ -341,8 +1512,23 @@ class _AddEmpresaDialogState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Error: $e'), + 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), + ), ), ); } diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog.dart index b680c9f..4f07b11 100644 --- a/lib/pages/empresa_negocios/widgets/add_negocio_dialog.dart +++ b/lib/pages/empresa_negocios/widgets/add_negocio_dialog.dart @@ -17,7 +17,8 @@ class AddNegocioDialog extends StatefulWidget { State createState() => _AddNegocioDialogState(); } -class _AddNegocioDialogState extends State { +class _AddNegocioDialogState extends State + with TickerProviderStateMixin { final _formKey = GlobalKey(); final _nombreController = TextEditingController(); final _direccionController = TextEditingController(); @@ -26,6 +27,13 @@ class _AddNegocioDialogState extends State { final _tipoLocalController = TextEditingController(text: 'Sucursal'); bool _isLoading = false; + late AnimationController _scaleController; + late AnimationController _slideController; + late AnimationController _fadeController; + late Animation _scaleAnimation; + late Animation _slideAnimation; + late Animation _fadeAnimation; + bool _isAnimationInitialized = false; final List _tiposLocal = [ 'Sucursal', @@ -36,8 +44,79 @@ class _AddNegocioDialogState extends State { 'Otro' ]; + @override + void initState() { + super.initState(); + _initializeAnimations(); + // Escuchar cambios del provider + widget.provider.addListener(_onProviderChanged); + } + + void _onProviderChanged() { + if (mounted) { + setState(() { + // Forzar rebuild cuando cambie el provider + }); + } + } + + void _initializeAnimations() { + _scaleController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + _slideController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + _fadeController = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: this, + ); + + _scaleAnimation = CurvedAnimation( + parent: _scaleController, + curve: Curves.elasticOut, + ); + _slideAnimation = Tween( + begin: const Offset(0, -1), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _slideController, + curve: Curves.easeOutBack, + )); + _fadeAnimation = CurvedAnimation( + parent: _fadeController, + curve: Curves.easeInOut, + ); + + // Pequeño delay para asegurar que el widget esté completamente montado + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _isAnimationInitialized = true; + }); + _startAnimations(); + } + }); + } + + void _startAnimations() { + _fadeController.forward(); + Future.delayed(const Duration(milliseconds: 100), () { + if (mounted) _scaleController.forward(); + }); + Future.delayed(const Duration(milliseconds: 200), () { + if (mounted) _slideController.forward(); + }); + } + @override void dispose() { + widget.provider.removeListener(_onProviderChanged); + _scaleController.dispose(); + _slideController.dispose(); + _fadeController.dispose(); _nombreController.dispose(); _direccionController.dispose(); _latitudController.dispose(); @@ -48,390 +127,1835 @@ class _AddNegocioDialogState extends State { @override Widget build(BuildContext context) { - return AlertDialog( - backgroundColor: AppTheme.of(context).primaryBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - title: Row( - children: [ - Icon( - Icons.store, - color: AppTheme.of(context).primaryColor, - ), - const SizedBox(width: 8), - Text( - 'Añadir Nueva Sucursal', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.bold, + if (!_isAnimationInitialized) { + return const SizedBox.shrink(); + } + + // Detectar el tamaño de pantalla + final screenSize = MediaQuery.of(context).size; + final isDesktop = screenSize.width > 1024; + final isTablet = screenSize.width > 768 && screenSize.width <= 1024; + + // Ajustar dimensiones según el tipo de pantalla + final maxWidth = isDesktop ? 900.0 : (isTablet ? 750.0 : 650.0); + final maxHeight = isDesktop ? 700.0 : 750.0; + + // Ajustar el padding del header según la pantalla + final headerPadding = isDesktop + ? const EdgeInsets.symmetric(vertical: 20, horizontal: 30) + : const EdgeInsets.all(25); + + // Ajustar el tamaño del icono + final iconSize = isDesktop ? 35.0 : 40.0; + final titleSize = isDesktop ? 24.0 : 28.0; + final subtitleSize = isDesktop ? 14.0 : 16.0; + + return AnimatedBuilder( + animation: + Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]), + builder: (context, child) { + return FadeTransition( + opacity: _fadeAnimation, + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), + child: Transform.scale( + scale: _scaleAnimation.value, + child: Container( + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: maxHeight, + minHeight: isDesktop ? 600 : 400, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 30, + offset: const Offset(0, 15), + spreadRadius: 5, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(25), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryBackground, + AppTheme.of(context).secondaryBackground, + AppTheme.of(context).tertiaryBackground, + ], + stops: const [0.0, 0.6, 1.0], + ), + ), + child: isDesktop + ? _buildDesktopLayout( + headerPadding, iconSize, titleSize, subtitleSize) + : _buildMobileLayout( + headerPadding, iconSize, titleSize, subtitleSize), + ), + ), + ), ), ), - ], - ), - content: SizedBox( - width: 500, - child: Form( - key: _formKey, - child: SingleChildScrollView( + ); + }, + ); + } + + Widget _buildDesktopLayout(EdgeInsets headerPadding, double iconSize, + double titleSize, double subtitleSize) { + return Row( + children: [ + // Header lateral compacto para desktop + Container( + width: 280, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).tertiaryColor.withOpacity(0.4), + blurRadius: 20, + offset: const Offset(5, 0), + ), + ], + ), + child: SlideTransition( + position: _slideAnimation, + child: Padding( + padding: headerPadding, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Icono compacto + Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withOpacity(0.4), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.white.withOpacity(0.3), + blurRadius: 15, + spreadRadius: 2, + ), + ], + ), + child: Icon( + Icons.store_mall_directory, + color: Colors.white, + size: iconSize, + ), + ), + const SizedBox(height: 16), + + // Título compacto + Text( + 'Nueva Sucursal', + style: TextStyle( + color: Colors.white, + fontSize: titleSize, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + + Text( + 'Añadir una nueva ubicación a tu empresa', + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: subtitleSize, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + + // Contenido principal del formulario + Expanded( + child: Padding( + padding: const EdgeInsets.all(25), + child: Form( + key: _formKey, + child: Column( + children: [ + // Información de empresa compacta + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor.withOpacity(0.1), + AppTheme.of(context).primaryColor.withOpacity(0.1), + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + AppTheme.of(context).tertiaryColor.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.business, + color: Colors.white, + size: 18, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Empresa', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + widget.provider.empresaSeleccionada?.nombre ?? + "Empresa no seleccionada", + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Formulario en columnas para aprovechar el espacio + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + // Primera fila - Nombre y Tipo + Row( + children: [ + Expanded( + flex: 2, + child: _buildCompactFormField( + controller: _nombreController, + label: 'Nombre de la sucursal', + hint: 'Ej: Sucursal Centro', + icon: Icons.store, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El nombre es requerido'; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildCompactDropdown(), + ), + ], + ), + + const SizedBox(height: 16), + + // Segunda fila - Dirección + _buildCompactFormField( + controller: _direccionController, + label: 'Dirección', + hint: 'Dirección completa de la sucursal', + icon: Icons.location_on_outlined, + maxLines: 2, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'La dirección es requerida'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + // Tercera fila - Coordenadas + Row( + children: [ + Expanded( + child: _buildCompactFormField( + controller: _latitudController, + label: 'Latitud', + hint: 'Ej: 19.4326', + icon: Icons.location_searching, + keyboardType: + const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Requerido'; + } + final lat = double.tryParse(value); + if (lat == null) { + return 'Número inválido'; + } + if (lat < -90 || lat > 90) { + return 'Rango: -90 a 90'; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildCompactFormField( + controller: _longitudController, + label: 'Longitud', + hint: 'Ej: -99.1332', + icon: Icons.location_searching, + keyboardType: + const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Requerido'; + } + final lng = double.tryParse(value); + if (lng == null) { + return 'Número inválido'; + } + if (lng < -180 || lng > 180) { + return 'Rango: -180 a 180'; + } + return null; + }, + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Sección de archivos compacta + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context) + .primaryColor + .withOpacity(0.1), + AppTheme.of(context) + .tertiaryColor + .withOpacity(0.1), + AppTheme.of(context) + .secondaryColor + .withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.4), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header de la sección + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).tertiaryColor, + ], + ), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.4), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon( + Icons.cloud_upload_rounded, + color: Colors.white, + size: 18, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Archivos Opcionales', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: AppTheme.of(context) + .primaryText, + ), + ), + Text( + 'Logo e imagen de la sucursal', + style: TextStyle( + fontSize: 12, + color: AppTheme.of(context) + .secondaryText, + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 16), + + // Botones de archivos mejorados + _buildEnhancedFileButton( + label: 'Logo de la sucursal', + subtitle: 'Formato PNG, JPG (Max 2MB)', + icon: Icons.image_rounded, + fileName: widget.provider.logoFileName, + file: widget.provider.logoToUpload, + onPressed: widget.provider.selectLogo, + gradient: LinearGradient( + colors: [ + Colors.blue.shade400, + Colors.blue.shade600 + ], + ), + ), + + const SizedBox(height: 12), + + _buildEnhancedFileButton( + label: 'Imagen principal', + subtitle: + 'Imagen representativa de la sucursal', + icon: Icons.photo_library_rounded, + fileName: widget.provider.imagenFileName, + file: widget.provider.imagenToUpload, + onPressed: widget.provider.selectImagen, + gradient: LinearGradient( + colors: [ + Colors.purple.shade400, + Colors.purple.shade600 + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 20), + + // Botones de acción + Row( + children: [ + Expanded( + child: Container( + height: 45, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context) + .secondaryText + .withOpacity(0.3), + ), + ), + child: TextButton( + onPressed: _isLoading + ? null + : () { + widget.provider.resetFormData(); + Navigator.of(context).pop(); + }, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Cancelar', + style: TextStyle( + color: + AppTheme.of(context).secondaryText, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: Container( + height: 45, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .tertiaryColor + .withOpacity(0.4), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: ElevatedButton( + onPressed: + _isLoading ? null : _crearNegocio, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: + AlwaysStoppedAnimation( + Colors.white), + ), + ) + : Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: const [ + Icon( + Icons.add_location, + color: Colors.white, + size: 18, + ), + SizedBox(width: 8), + Text( + 'Crear Sucursal', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ], + ); + } + + Widget _buildMobileLayout(EdgeInsets headerPadding, double iconSize, + double titleSize, double subtitleSize) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header para móvil (estructura original pero más compacto) + SlideTransition( + position: _slideAnimation, + child: Container( + width: double.infinity, + padding: headerPadding, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).tertiaryColor.withOpacity(0.4), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), child: Column( - mainAxisSize: MainAxisSize.min, children: [ - // Información de la empresa Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.all(15), decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), + color: Colors.white.withOpacity(0.2), + shape: BoxShape.circle, border: Border.all( - color: AppTheme.of(context).primaryColor.withOpacity(0.3), + color: Colors.white.withOpacity(0.4), + width: 2, ), ), - child: Row( - children: [ - Icon( - Icons.business, - color: AppTheme.of(context).primaryColor, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Empresa: ${widget.provider.empresaSeleccionada?.nombre ?? ""}', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontWeight: FontWeight.w600, - ), - ), - ], + child: Icon( + Icons.store_mall_directory, + color: Colors.white, + size: iconSize, ), ), - const SizedBox(height: 20), - - // Nombre - TextFormField( - controller: _nombreController, - decoration: InputDecoration( - labelText: 'Nombre de la sucursal *', - hintText: 'Ej: Sucursal Centro, Sede Norte', - prefixIcon: Icon(Icons.store), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'El nombre es requerido'; - } - return null; - }, - ), - const SizedBox(height: 16), - - // Dirección - TextFormField( - controller: _direccionController, - maxLines: 3, - decoration: InputDecoration( - labelText: 'Dirección *', - hintText: 'Dirección completa de la sucursal', - prefixIcon: Icon(Icons.location_on), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'La dirección es requerida'; - } - return null; - }, - ), - const SizedBox(height: 16), - - // Tipo de local - DropdownButtonFormField( - value: _tipoLocalController.text, - decoration: InputDecoration( - labelText: 'Tipo de local *', - prefixIcon: Icon(Icons.category), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - items: _tiposLocal.map((String tipo) { - return DropdownMenuItem( - value: tipo, - child: Text(tipo), - ); - }).toList(), - onChanged: (String? newValue) { - if (newValue != null) { - _tipoLocalController.text = newValue; - } - }, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Selecciona un tipo de local'; - } - return null; - }, - ), - const SizedBox(height: 20), - - // Coordenadas - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Colors.grey.withOpacity(0.3), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.map, - color: AppTheme.of(context).primaryColor, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Coordenadas Geográficas *', - style: TextStyle( - fontWeight: FontWeight.bold, - color: AppTheme.of(context).primaryText, - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - // Latitud - Expanded( - child: TextFormField( - controller: _latitudController, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp(r'^-?\d*\.?\d*'), - ), - ], - decoration: InputDecoration( - labelText: 'Latitud', - hintText: 'Ej: 19.4326', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Requerido'; - } - final lat = double.tryParse(value); - if (lat == null) { - return 'Número inválido'; - } - if (lat < -90 || lat > 90) { - return 'Rango: -90 a 90'; - } - return null; - }, - ), - ), - const SizedBox(width: 16), - // Longitud - Expanded( - child: TextFormField( - controller: _longitudController, - keyboardType: - const TextInputType.numberWithOptions( - decimal: true, - signed: true, - ), - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp(r'^-?\d*\.?\d*'), - ), - ], - decoration: InputDecoration( - labelText: 'Longitud', - hintText: 'Ej: -99.1332', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Requerido'; - } - final lng = double.tryParse(value); - if (lng == null) { - return 'Número inválido'; - } - if (lng < -180 || lng > 180) { - return 'Rango: -180 a 180'; - } - return null; - }, - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - 'Tip: Puedes obtener las coordenadas desde Google Maps', - style: TextStyle( - fontSize: 12, - color: AppTheme.of(context).secondaryText, - fontStyle: FontStyle.italic, - ), - ), - ], + const SizedBox(height: 12), + Text( + 'Nueva Sucursal', + style: TextStyle( + color: Colors.white, + fontSize: titleSize, + fontWeight: FontWeight.bold, + letterSpacing: 1.2, ), ), - const SizedBox(height: 20), - - // Sección de archivos - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.of(context).secondaryBackground, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Colors.grey.withOpacity(0.3), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Archivos (Opcional)', - style: TextStyle( - fontWeight: FontWeight.bold, - color: AppTheme.of(context).primaryText, - ), - ), - const SizedBox(height: 12), - - // Logo - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: widget.provider.selectLogo, - icon: Icon(Icons.image), - label: Text( - widget.provider.logoFileName ?? - 'Seleccionar Logo', - overflow: TextOverflow.ellipsis, - ), - ), - ), - if (widget.provider.logoToUpload != null) ...[ - const SizedBox(width: 8), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(4), - ), - child: widget.provider.getImageWidget( - widget.provider.logoToUpload, - height: 40, - width: 40, - ), - ), - ], - ], - ), - const SizedBox(height: 8), - - // Imagen - Row( - children: [ - Expanded( - child: OutlinedButton.icon( - onPressed: widget.provider.selectImagen, - icon: Icon(Icons.photo), - label: Text( - widget.provider.imagenFileName ?? - 'Seleccionar Imagen', - overflow: TextOverflow.ellipsis, - ), - ), - ), - if (widget.provider.imagenToUpload != null) ...[ - const SizedBox(width: 8), - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - border: Border.all(color: Colors.grey), - borderRadius: BorderRadius.circular(4), - ), - child: widget.provider.getImageWidget( - widget.provider.imagenToUpload, - height: 40, - width: 40, - ), - ), - ], - ], - ), - ], + const SizedBox(height: 6), + Text( + 'Añadir nueva ubicación', + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: subtitleSize, + fontWeight: FontWeight.w500, ), ), ], ), ), ), - ), - actions: [ - TextButton( - onPressed: _isLoading - ? null - : () { - widget.provider.resetFormData(); - Navigator.of(context).pop(); - }, - child: Text( - 'Cancelar', - style: TextStyle( - color: AppTheme.of(context).secondaryText, + + // Contenido del formulario para móvil (estructura original) + Flexible( + child: SingleChildScrollView( + padding: const EdgeInsets.all(25), + child: Form( + key: _formKey, + child: Column( + children: [ + // Información de la empresa + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor.withOpacity(0.1), + AppTheme.of(context).primaryColor.withOpacity(0.1), + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + AppTheme.of(context).tertiaryColor.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.business, + color: Colors.white, + size: 18, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Empresa', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + widget.provider.empresaSeleccionada?.nombre ?? + "Empresa no seleccionada", + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // Campos del formulario + _buildCompactFormField( + controller: _nombreController, + label: 'Nombre de la sucursal', + hint: 'Ej: Sucursal Centro, Sede Norte', + icon: Icons.store, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El nombre es requerido'; + } + return null; + }, + ), + + _buildCompactFormField( + controller: _direccionController, + label: 'Dirección', + hint: 'Dirección completa de la sucursal', + icon: Icons.location_on_outlined, + maxLines: 3, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'La dirección es requerida'; + } + return null; + }, + ), + + // Tipo de local + Container( + margin: const EdgeInsets.only(bottom: 20), + child: DropdownButtonFormField( + value: _tipoLocalController.text, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + ), + dropdownColor: AppTheme.of(context) + .secondaryBackground, // Fondo del dropdown + decoration: InputDecoration( + labelText: 'Tipo de local', + prefixIcon: Container( + margin: const EdgeInsets.all(6), + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.category, + color: Colors.white, + size: 16, + ), + ), + labelStyle: TextStyle( + color: AppTheme.of(context).tertiaryColor, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + filled: true, + fillColor: AppTheme.of(context) + .secondaryBackground + .withOpacity(0.5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.of(context) + .tertiaryColor + .withOpacity(0.3), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.of(context).tertiaryColor, + width: 2, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 12), + ), + items: _tiposLocal.map((String tipo) { + return DropdownMenuItem( + value: tipo, + child: Container( + constraints: const BoxConstraints( + maxWidth: + 200), // Limitar ancho para evitar overflow + child: Text( + tipo, + style: TextStyle( + fontSize: 14, + color: AppTheme.of(context).primaryText, + ), + overflow: TextOverflow + .ellipsis, // Truncar texto si es muy largo + maxLines: 1, + ), + ), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + _tipoLocalController.text = newValue; + } + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Selecciona un tipo'; + } + return null; + }, + isExpanded: + true, // Expandir para usar todo el ancho disponible + icon: Icon( + Icons.arrow_drop_down, + color: AppTheme.of(context).tertiaryColor, + ), + ), + ), + + // Sección de coordenadas + Container( + padding: const EdgeInsets.all(20), + margin: const EdgeInsets.only(bottom: 20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor.withOpacity(0.1), + AppTheme.of(context).primaryColor.withOpacity(0.1), + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: + AppTheme.of(context).tertiaryColor.withOpacity(0.3), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .tertiaryColor + .withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.location_searching, + color: Colors.white, + size: 16, + ), + ), + const SizedBox(width: 12), + Text( + 'Coordenadas Geográficas', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: AppTheme.of(context).primaryText, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + // Latitud + Expanded( + child: TextFormField( + controller: _latitudController, + keyboardType: + const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'^-?\d*\.?\d*')), + ], + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + ), + decoration: InputDecoration( + labelText: 'Latitud', + hintText: 'Ej: 19.4326', + labelStyle: TextStyle( + color: AppTheme.of(context).tertiaryColor, + fontWeight: FontWeight.w600, + ), + hintStyle: TextStyle( + color: AppTheme.of(context).secondaryText, + ), + filled: true, + fillColor: Colors.white.withOpacity(0.8), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.of(context) + .tertiaryColor + .withOpacity(0.3), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.of(context).tertiaryColor, + width: 2, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 14), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Requerido'; + } + final lat = double.tryParse(value); + if (lat == null) { + return 'Número inválido'; + } + if (lat < -90 || lat > 90) { + return 'Rango: -90 a 90'; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + + // Longitud + Expanded( + child: TextFormField( + controller: _longitudController, + keyboardType: + const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'^-?\d*\.?\d*')), + ], + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + ), + decoration: InputDecoration( + labelText: 'Longitud', + hintText: 'Ej: -99.1332', + labelStyle: TextStyle( + color: AppTheme.of(context).tertiaryColor, + fontWeight: FontWeight.w600, + ), + hintStyle: TextStyle( + color: AppTheme.of(context).secondaryText, + ), + filled: true, + fillColor: Colors.white.withOpacity(0.8), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.of(context) + .tertiaryColor + .withOpacity(0.3), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.of(context).tertiaryColor, + width: 2, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 14), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Requerido'; + } + final lng = double.tryParse(value); + if (lng == null) { + return 'Número inválido'; + } + if (lng < -180 || lng > 180) { + return 'Rango: -180 a 180'; + } + return null; + }, + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: Colors.blue, + size: 16, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Puedes obtener las coordenadas desde Google Maps', + style: TextStyle( + color: Colors.blue, + fontSize: 12, + ), + ), + ), + ], + ), + ), + ], + ), + ), + + // Sección de archivos compacta para móvil + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor.withOpacity(0.1), + AppTheme.of(context).tertiaryColor.withOpacity(0.1), + AppTheme.of(context).secondaryColor.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: + AppTheme.of(context).primaryColor.withOpacity(0.4), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.2), + blurRadius: 15, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header de la sección + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).primaryColor, + AppTheme.of(context).tertiaryColor, + ], + ), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.4), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: const Icon( + Icons.cloud_upload_rounded, + color: Colors.white, + size: 18, + ), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Archivos Opcionales', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: AppTheme.of(context).primaryText, + ), + ), + Text( + 'Logo e imagen de la sucursal', + style: TextStyle( + fontSize: 12, + color: AppTheme.of(context).secondaryText, + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 16), + + // Botones de archivos mejorados + _buildEnhancedFileButton( + label: 'Logo de la sucursal', + subtitle: 'Formato PNG, JPG (Max 2MB)', + icon: Icons.image_rounded, + fileName: widget.provider.logoFileName, + file: widget.provider.logoToUpload, + onPressed: widget.provider.selectLogo, + gradient: LinearGradient( + colors: [ + Colors.blue.shade400, + Colors.blue.shade600 + ], + ), + ), + + const SizedBox(height: 12), + + _buildEnhancedFileButton( + label: 'Imagen principal', + subtitle: 'Imagen representativa de la sucursal', + icon: Icons.photo_library_rounded, + fileName: widget.provider.imagenFileName, + file: widget.provider.imagenToUpload, + onPressed: widget.provider.selectImagen, + gradient: LinearGradient( + colors: [ + Colors.purple.shade400, + Colors.purple.shade600 + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 30), + + // Botones de acción + Row( + children: [ + Expanded( + child: Container( + height: 45, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context) + .secondaryText + .withOpacity(0.3), + ), + ), + child: TextButton( + onPressed: _isLoading + ? null + : () { + widget.provider.resetFormData(); + Navigator.of(context).pop(); + }, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Cancelar', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: Container( + height: 45, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .tertiaryColor + .withOpacity(0.4), + blurRadius: 15, + offset: const Offset(0, 5), + ), + ], + ), + child: ElevatedButton( + onPressed: _isLoading ? null : _crearNegocio, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.white), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon( + Icons.add_location, + color: Colors.white, + size: 18, + ), + SizedBox(width: 8), + Text( + 'Crear Sucursal', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ], + ), ), ), ), - ElevatedButton( - onPressed: _isLoading ? null : _crearNegocio, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.of(context).primaryColor, - foregroundColor: Colors.white, - ), - child: _isLoading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : const Text('Crear Sucursal'), - ), ], ); } + Widget _buildCompactFormField({ + required TextEditingController controller, + required String label, + required String hint, + required IconData icon, + int maxLines = 1, + TextInputType? keyboardType, + String? Function(String?)? validator, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + child: TextFormField( + controller: controller, + maxLines: maxLines, + keyboardType: keyboardType, + validator: validator, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + ), + decoration: InputDecoration( + labelText: label, + hintText: hint, + prefixIcon: Container( + margin: const EdgeInsets.all(6), + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + icon, + color: Colors.white, + size: 16, + ), + ), + labelStyle: TextStyle( + color: AppTheme.of(context).tertiaryColor, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + hintStyle: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 12, + ), + filled: true, + fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.of(context).tertiaryColor.withOpacity(0.3), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.of(context).tertiaryColor, + width: 2, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Colors.red, + width: 1, + ), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + ), + ); + } + + Widget _buildCompactDropdown() { + return Container( + margin: const EdgeInsets.only(bottom: 12), + child: DropdownButtonFormField( + value: _tipoLocalController.text, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + ), + dropdownColor: + AppTheme.of(context).secondaryBackground, // Fondo del dropdown + decoration: InputDecoration( + labelText: 'Tipo de local', + prefixIcon: Container( + margin: const EdgeInsets.all(6), + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.category, + color: Colors.white, + size: 16, + ), + ), + labelStyle: TextStyle( + color: AppTheme.of(context).tertiaryColor, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + filled: true, + fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.5), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.of(context).tertiaryColor.withOpacity(0.3), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: AppTheme.of(context).tertiaryColor, + width: 2, + ), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + items: _tiposLocal.map((String tipo) { + return DropdownMenuItem( + value: tipo, + child: Container( + constraints: const BoxConstraints( + maxWidth: 200), // Limitar ancho para evitar overflow + child: Text( + tipo, + style: TextStyle( + fontSize: 14, + color: AppTheme.of(context).primaryText, + ), + overflow: + TextOverflow.ellipsis, // Truncar texto si es muy largo + maxLines: 1, + ), + ), + ); + }).toList(), + onChanged: (String? newValue) { + if (newValue != null) { + _tipoLocalController.text = newValue; + } + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Selecciona un tipo'; + } + return null; + }, + isExpanded: true, // Expandir para usar todo el ancho disponible + icon: Icon( + Icons.arrow_drop_down, + color: AppTheme.of(context).tertiaryColor, + ), + ), + ); + } + + Widget _buildCompactFileButton({ + required String label, + required IconData icon, + required String? fileName, + required dynamic file, + required VoidCallback onPressed, + }) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppTheme.of(context).tertiaryColor.withOpacity(0.3), + ), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.of(context).tertiaryColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(6), + ), + child: Icon( + icon, + color: AppTheme.of(context).tertiaryColor, + size: 16, + ), + ), + const SizedBox(height: 8), + Text( + label, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + if (fileName != null) ...[ + const SizedBox(height: 4), + Text( + fileName + .split('-') + .last, // Solo mostrar el nombre del archivo + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 10, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ], + ], + ), + ), + ), + ), + ); + } + + Widget _buildEnhancedCompactFileButton({ + required String label, + required String subtitle, + required IconData icon, + required String? fileName, + required dynamic file, + required VoidCallback onPressed, + required LinearGradient gradient, + }) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + gradient: gradient, + boxShadow: [ + BoxShadow( + color: gradient.colors.last.withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon( + icon, + color: gradient.colors.first, + size: 20, + ), + ), + const SizedBox(height: 12), + Text( + label, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Text( + subtitle, + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 12, + ), + textAlign: TextAlign.center, + ), + if (fileName != null) ...[ + const SizedBox(height: 8), + Text( + fileName + .split('-') + .last, // Solo mostrar el nombre del archivo + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 10, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ], + ], + ), + ), + ), + ), + ); + } + + Widget _buildEnhancedFileButton({ + required String label, + required String subtitle, + required IconData icon, + required String? fileName, + required dynamic file, + required VoidCallback onPressed, + required Gradient gradient, + }) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + blurRadius: 15, + offset: const Offset(0, 6), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // Icono con gradiente + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: gradient, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon( + icon, + color: Colors.white, + size: 24, + ), + ), + + const SizedBox(width: 16), + + // Información del archivo + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 4), + Text( + fileName ?? subtitle, + style: TextStyle( + color: fileName != null + ? AppTheme.of(context).primaryColor + : AppTheme.of(context).secondaryText, + fontSize: 13, + fontWeight: fileName != null + ? FontWeight.w600 + : FontWeight.normal, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + + // Preview de imagen si existe + if (file != null) ...[ + const SizedBox(width: 16), + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: + AppTheme.of(context).primaryColor.withOpacity(0.4), + width: 2, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: widget.provider.getImageWidget( + file, + height: 50, + width: 50, + ), + ), + ), + ] else ...[ + const SizedBox(width: 16), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.upload_file, + color: AppTheme.of(context).primaryColor, + size: 20, + ), + ), + ], + ], + ), + ), + ), + ), + ); + } + Future _crearNegocio() async { if (!_formKey.currentState!.validate()) return; @@ -453,16 +1977,42 @@ class _AddNegocioDialogState extends State { if (success) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Sucursal creada exitosamente'), + SnackBar( + content: Row( + children: const [ + Icon(Icons.check_circle, color: Colors.white), + SizedBox(width: 12), + Text( + 'Sucursal creada exitosamente', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), ); } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Error al crear la sucursal'), + SnackBar( + content: Row( + children: const [ + Icon(Icons.error, color: Colors.white), + SizedBox(width: 12), + Text( + 'Error al crear la sucursal', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), ); } @@ -471,8 +2021,23 @@ class _AddNegocioDialogState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Error: $e'), + 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), + ), ), ); } diff --git a/lib/pages/empresa_negocios/widgets/empresa_selector_sidebar.dart b/lib/pages/empresa_negocios/widgets/empresa_selector_sidebar.dart index 738175d..8e75192 100644 --- a/lib/pages/empresa_negocios/widgets/empresa_selector_sidebar.dart +++ b/lib/pages/empresa_negocios/widgets/empresa_selector_sidebar.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:nethive_neo/helpers/globals.dart'; import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; import 'package:nethive_neo/pages/empresa_negocios/widgets/add_empresa_dialog.dart'; import 'package:nethive_neo/pages/empresa_negocios/widgets/add_negocio_dialog.dart'; import 'package:nethive_neo/theme/theme.dart'; +import 'package:nethive_neo/helpers/globals.dart'; -class EmpresaSelectorSidebar extends StatelessWidget { +class EmpresaSelectorSidebar extends StatefulWidget { final EmpresasNegociosProvider provider; final Function(String) onEmpresaSelected; @@ -15,219 +15,363 @@ class EmpresaSelectorSidebar extends StatelessWidget { required this.onEmpresaSelected, }) : super(key: key); + @override + State createState() => _EmpresaSelectorSidebarState(); +} + +class _EmpresaSelectorSidebarState extends State + with TickerProviderStateMixin { + late AnimationController _pulseController; + late Animation _pulseAnimation; + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(reverse: true); + _pulseAnimation = Tween( + begin: 0.8, + end: 1.0, + ).animate(CurvedAnimation( + parent: _pulseController, + curve: Curves.easeInOut, + )); + } + + @override + void dispose() { + _pulseController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Container( - color: AppTheme.of(context).secondaryBackground, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppTheme.of(context).secondaryBackground, + AppTheme.of(context).primaryBackground, + ], + ), + border: Border( + right: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + width: 1, + ), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(2, 0), + ), + ], + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header + // Header mejorado con gradiente Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor, + gradient: AppTheme.of(context).primaryGradient, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 15, + offset: const Offset(0, 5), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Seleccionar Empresa', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, - ), + Row( + children: [ + // Logo animado + AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + return Transform.scale( + scale: _pulseAnimation.value, + 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.4), + width: 2, + ), + ), + child: Icon( + Icons.business_center, + color: Colors.white, + size: 24, + ), + ), + ); + }, + ), + 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: Text( + 'NETHIVE', + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + ), + ), + Text( + 'Empresas', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], ), - const SizedBox(height: 8), - Text( - 'Haz una empresas para ver sus sucursales', - style: TextStyle( - color: Colors.white70, - fontSize: 14, + const SizedBox(height: 16), + + // Contador de empresas con efecto + Container( + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.3), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.domain, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 8), + TweenAnimationBuilder( + duration: const Duration(milliseconds: 800), + tween: IntTween( + begin: 0, end: widget.provider.empresas.length), + builder: (context, value, child) { + return Text( + '$value empresas', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ); + }, + ), + ], ), ), ], ), ), - // Lista de empresas + // Lista de empresas con animaciones escalonadas Expanded( - child: ListView.builder( - padding: const EdgeInsets.all(8), - itemCount: provider.empresas.length, - itemBuilder: (context, index) { - final empresa = provider.empresas[index]; - final isSelected = provider.empresaSeleccionadaId == empresa.id; + child: widget.provider.empresas.isEmpty + ? _buildEmptyState() + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: widget.provider.empresas.length, + itemBuilder: (context, index) { + return TweenAnimationBuilder( + duration: Duration(milliseconds: 200 + (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: _buildEmpresaCard( + widget.provider.empresas[index], index), + ), + ); + }, + ); + }, + ), + ), - return Container( - margin: const EdgeInsets.only(bottom: 8), + // Botón añadir empresa mejorado + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppTheme.of(context).primaryBackground.withOpacity(0.0), + AppTheme.of(context).primaryBackground, + ], + ), + ), + child: Column( + children: [ + // Línea divisoria con gradiente + Container( + height: 1, decoration: BoxDecoration( - color: isSelected - ? AppTheme.of(context).primaryColor.withOpacity(0.1) - : AppTheme.of(context).primaryBackground, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isSelected - ? AppTheme.of(context).primaryColor - : Colors.grey.withOpacity(0.3), - width: isSelected ? 2 : 1, + gradient: LinearGradient( + colors: [ + Colors.transparent, + AppTheme.of(context).primaryColor.withOpacity(0.5), + Colors.transparent, + ], ), ), - child: InkWell( - onTap: () => onEmpresaSelected(empresa.id), - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - // Logo de la empresa - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor, - borderRadius: BorderRadius.circular(8), - ), - child: empresa.logoUrl != null - ? ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network( - "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${empresa.logoUrl}", - fit: BoxFit.cover, - errorBuilder: - (context, error, stackTrace) { - return Image.asset( - 'assets/images/placeholder_no_image.jpg', - fit: BoxFit.cover, - ); - }, - ), - ) - : Image.asset( - 'assets/images/placeholder_no_image.jpg', - fit: BoxFit.cover, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - empresa.nombre, - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - 'Tecnología', - style: TextStyle( - color: - AppTheme.of(context).secondaryText, - fontSize: 12, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 8), - Text( - 'Sucursales: ${isSelected ? provider.negocios.length : '...'}', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - ), - ), - ], + ), + const SizedBox(height: 20), + + // Botón principal + 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: () { + showDialog( + context: context, + builder: (context) => + AddEmpresaDialog(provider: widget.provider), + ); + }, + icon: const Icon(Icons.add, color: Colors.white), + label: const Text( + 'Nueva Empresa', + 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), + ), ), ), ), - ); - }, + ), + ], ), ), - // Botón añadir empresa - Padding( - padding: const EdgeInsets.all(16), - child: SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () { - showDialog( - context: context, - builder: (context) => AddEmpresaDialog(provider: provider), - ); - }, - icon: const Icon(Icons.add, color: Colors.white), - label: const Text( - 'Añadir Empresa', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - ), - ), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.of(context).primaryColor, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ), - ), - - // Información de empresa seleccionada - if (provider.empresaSeleccionada != null) ...[ + // Información de empresa seleccionada mejorada + if (widget.provider.empresaSeleccionada != null) ...[ Container( margin: const EdgeInsets.fromLTRB(16, 0, 16, 16), - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), decoration: BoxDecoration( - color: AppTheme.of(context).primaryColor.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), + 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: 2, ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Empresa seleccionada:', - style: TextStyle( - color: AppTheme.of(context).secondaryText, - fontSize: 12, - fontWeight: FontWeight.w500, - ), + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.check_circle, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Empresa activa', + style: TextStyle( + color: AppTheme.of(context).primaryColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ], ), - const SizedBox(height: 4), + const SizedBox(height: 12), + Text( - provider.empresaSeleccionada!.nombre, + widget.provider.empresaSeleccionada!.nombre, style: TextStyle( color: AppTheme.of(context).primaryText, - fontSize: 16, + fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), + + // Estadísticas Row( children: [ Icon( @@ -235,41 +379,71 @@ class EmpresaSelectorSidebar extends StatelessWidget { size: 16, color: AppTheme.of(context).primaryColor, ), - const SizedBox(width: 4), - Text( - '${provider.negocios.length} Sucursales', - style: TextStyle( - color: AppTheme.of(context).primaryText, - fontSize: 14, - fontWeight: FontWeight.w500, - ), + const SizedBox(width: 6), + TweenAnimationBuilder( + duration: const Duration(milliseconds: 600), + tween: IntTween( + begin: 0, end: widget.provider.negocios.length), + builder: (context, value, child) { + return Text( + '$value sucursales', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ); + }, ), ], ), - const SizedBox(height: 12), + const SizedBox(height: 16), + + // Botón para añadir sucursal SizedBox( width: double.infinity, - child: ElevatedButton.icon( - onPressed: () { - showDialog( - context: context, - builder: (context) => AddNegocioDialog( - provider: provider, - empresaId: provider.empresaSeleccionada!.id, + child: Container( + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 3), ), - ); - }, - icon: const Icon(Icons.add, size: 16), - label: const Text( - 'Añadir Sucursal', - style: TextStyle(fontSize: 12), + ], ), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.of(context).primaryColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 8), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), + child: ElevatedButton.icon( + onPressed: () { + showDialog( + context: context, + builder: (context) => AddNegocioDialog( + provider: widget.provider, + empresaId: + widget.provider.empresaSeleccionada!.id, + ), + ); + }, + icon: const Icon(Icons.add_location, + size: 18, color: Colors.white), + label: const Text( + 'Añadir Sucursal', + style: TextStyle( + fontSize: 14, + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), ), ), @@ -282,4 +456,231 @@ class EmpresaSelectorSidebar extends StatelessWidget { ), ); } + + Widget _buildEmpresaCard(dynamic empresa, int index) { + final isSelected = widget.provider.empresaSeleccionadaId == empresa.id; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + gradient: isSelected + ? AppTheme.of(context).primaryGradient + : LinearGradient( + colors: [ + AppTheme.of(context).secondaryBackground, + AppTheme.of(context).tertiaryBackground, + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? Colors.white.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: () => widget.onEmpresaSelected(empresa.id), + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // Logo de la empresa con efectos + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + gradient: isSelected + ? LinearGradient( + colors: [ + Colors.white.withOpacity(0.3), + Colors.white.withOpacity(0.1) + ], + ) + : AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: (isSelected + ? Colors.white + : AppTheme.of(context).primaryColor) + .withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: empresa.logoUrl != null && + empresa.logoUrl!.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${empresa.logoUrl}", + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + 'assets/images/placeholder_no_image.jpg', + fit: BoxFit.cover, + ); + }, + ), + ) + : Icon( + Icons.business, + color: isSelected ? Colors.white : Colors.white, + size: 24, + ), + ), + const SizedBox(width: 16), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + empresa.nombre, + style: TextStyle( + color: isSelected + ? Colors.white + : AppTheme.of(context).primaryText, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: (isSelected + ? Colors.white + : AppTheme.of(context).primaryColor) + .withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + 'Tecnología', + style: TextStyle( + color: isSelected + ? Colors.white + : AppTheme.of(context).primaryColor, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + + // Información adicional + Row( + children: [ + Icon( + Icons.store, + size: 16, + color: isSelected + ? Colors.white.withOpacity(0.8) + : AppTheme.of(context).secondaryText, + ), + const SizedBox(width: 6), + Text( + 'Sucursales: ${isSelected ? widget.provider.negocios.length : '...'}', + style: TextStyle( + color: isSelected + ? Colors.white.withOpacity(0.9) + : AppTheme.of(context).secondaryText, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: const EdgeInsets.all(40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedBuilder( + animation: _pulseAnimation, + builder: (context, child) { + return Transform.scale( + scale: _pulseAnimation.value, + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + child: Icon( + Icons.business, + color: Colors.white, + size: 40, + ), + ), + ); + }, + ), + const SizedBox(height: 20), + Text( + 'Sin empresas', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Añade tu primera empresa\npara comenzar', + textAlign: TextAlign.center, + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 14, + ), + ), + ], + ), + ), + ); + } } diff --git a/lib/pages/empresa_negocios/widgets/mobile_empresa_selector.dart b/lib/pages/empresa_negocios/widgets/mobile_empresa_selector.dart new file mode 100644 index 0000000..36e5984 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/mobile_empresa_selector.dart @@ -0,0 +1,472 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'package:nethive_neo/helpers/globals.dart'; + +class MobileEmpresaSelector extends StatefulWidget { + final EmpresasNegociosProvider provider; + final Function(String) onEmpresaSelected; + + const MobileEmpresaSelector({ + Key? key, + required this.provider, + required this.onEmpresaSelected, + }) : super(key: key); + + @override + State createState() => _MobileEmpresaSelectorState(); +} + +class _MobileEmpresaSelectorState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation _fadeAnimation; + final TextEditingController _searchController = TextEditingController(); + List _filteredEmpresas = []; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _fadeAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOut, + ); + + _filteredEmpresas = widget.provider.empresas; + _animationController.forward(); + + _searchController.addListener(_onSearchChanged); + } + + @override + void dispose() { + _animationController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + void _onSearchChanged() { + final query = _searchController.text.toLowerCase(); + setState(() { + _filteredEmpresas = widget.provider.empresas.where((empresa) { + return empresa.nombre.toLowerCase().contains(query) || + empresa.rfc.toLowerCase().contains(query); + }).toList(); + }); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _fadeAnimation, + child: Container( + decoration: BoxDecoration( + color: AppTheme.of(context).primaryBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(25), + topRight: Radius.circular(25), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Handle + Container( + margin: const EdgeInsets.symmetric(vertical: 10), + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryText.withOpacity(0.3), + 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(25), + topRight: Radius.circular(25), + ), + ), + child: Column( + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.business_center, + color: Colors.white, + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Seleccionar Empresa', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + Text( + 'Elige una empresa para ver sus sucursales', + style: TextStyle( + color: Colors.white.withOpacity(0.8), + fontSize: 14, + ), + ), + ], + ), + ), + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon( + Icons.close, + color: Colors.white, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Buscador + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(15), + ), + child: TextField( + controller: _searchController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: 'Buscar empresa...', + hintStyle: TextStyle( + color: Colors.white.withOpacity(0.7), + ), + prefixIcon: Icon( + Icons.search, + color: Colors.white.withOpacity(0.7), + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 15, + ), + ), + ), + ), + ], + ), + ), + + // Lista de empresas + Flexible( + child: _filteredEmpresas.isEmpty + ? _buildEmptyState() + : ListView.builder( + padding: const EdgeInsets.all(16), + shrinkWrap: true, + itemCount: _filteredEmpresas.length, + itemBuilder: (context, index) { + return TweenAnimationBuilder( + duration: Duration(milliseconds: 200 + (index * 50)), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Transform.translate( + offset: Offset(30 * (1 - value), 0), + child: Opacity( + opacity: value, + child: _buildEmpresaCard( + _filteredEmpresas[index], + index, + ), + ), + ); + }, + ); + }, + ), + ), + + // Footer con información adicional + if (widget.provider.empresaSeleccionada != null) ...[ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + border: Border( + top: BorderSide( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Empresa Actual', + style: TextStyle( + color: AppTheme.of(context).primaryColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + widget.provider.empresaSeleccionada!.nombre, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + '${widget.provider.negocios.length} sucursales', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 14, + ), + ), + ], + ), + ), + ], + ], + ), + ), + ); + } + + Widget _buildEmpresaCard(dynamic empresa, int index) { + final isSelected = widget.provider.empresaSeleccionadaId == empresa.id; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + gradient: isSelected + ? AppTheme.of(context).primaryGradient + : LinearGradient( + colors: [ + AppTheme.of(context).secondaryBackground, + AppTheme.of(context).tertiaryBackground, + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? Colors.white.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: () { + widget.onEmpresaSelected(empresa.id); + Navigator.of(context).pop(); + }, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // Logo de la empresa + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + gradient: isSelected + ? LinearGradient( + colors: [ + Colors.white.withOpacity(0.3), + Colors.white.withOpacity(0.1) + ], + ) + : AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: (isSelected + ? Colors.white + : AppTheme.of(context).primaryColor) + .withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: empresa.logoUrl != null && empresa.logoUrl!.isNotEmpty + ? ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${empresa.logoUrl}", + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.asset( + 'assets/images/placeholder_no_image.jpg', + fit: BoxFit.cover, + ); + }, + ), + ) + : Icon( + Icons.business, + color: isSelected ? Colors.white : Colors.white, + size: 30, + ), + ), + const SizedBox(width: 16), + + // Información de la empresa + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + empresa.nombre, + style: TextStyle( + color: isSelected + ? Colors.white + : AppTheme.of(context).primaryText, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + empresa.rfc, + style: TextStyle( + color: isSelected + ? Colors.white.withOpacity(0.8) + : AppTheme.of(context).secondaryText, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: (isSelected + ? Colors.white + : AppTheme.of(context).primaryColor) + .withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + 'Tecnología', + style: TextStyle( + color: isSelected + ? Colors.white + : AppTheme.of(context).primaryColor, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + + // 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: AppTheme.of(context).secondaryText, + size: 16, + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: const EdgeInsets.all(40), + 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.search_off, + color: AppTheme.of(context).primaryColor, + size: 40, + ), + ), + const SizedBox(height: 16), + Text( + 'No se encontraron empresas', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Intenta con otro término de búsqueda', + textAlign: TextAlign.center, + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 14, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/empresa_negocios/widgets/negocios_cards_view.dart b/lib/pages/empresa_negocios/widgets/negocios_cards_view.dart new file mode 100644 index 0000000..32f23ee --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/negocios_cards_view.dart @@ -0,0 +1,646 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'package:nethive_neo/helpers/globals.dart'; + +class NegociosCardsView extends StatelessWidget { + final EmpresasNegociosProvider provider; + + const NegociosCardsView({ + Key? key, + required this.provider, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (provider.empresaSeleccionada == null) { + return _buildEmptyState(context); + } + + if (provider.negocios.isEmpty) { + return _buildNoDataState(context); + } + + return Container( + padding: const EdgeInsets.all(16), + child: ListView.builder( + itemCount: provider.negocios.length, + itemBuilder: (context, index) { + final negocio = provider.negocios[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, 50 * (1 - value)), + child: Opacity( + opacity: value, + child: Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).secondaryBackground, + AppTheme.of(context).tertiaryBackground, + ], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: + AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.1), + blurRadius: 15, + offset: const Offset(0, 5), + ), + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + _showNegocioDetails(context, negocio); + }, + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header con logo y nombre + Row( + children: [ + // Logo del negocio + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + gradient: AppTheme.of(context) + .primaryGradient, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.3), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: negocio.logoUrl != null && + negocio.logoUrl!.isNotEmpty + ? ClipRRect( + borderRadius: + BorderRadius.circular(15), + child: Image.network( + "${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/logos/${negocio.logoUrl}", + fit: BoxFit.cover, + errorBuilder: (context, error, + stackTrace) { + return Image.asset( + 'assets/images/placeholder_no_image.jpg', + fit: BoxFit.cover, + ); + }, + ), + ) + : Icon( + Icons.store, + color: Colors.white, + size: 30, + ), + ), + const SizedBox(width: 16), + + // Información principal + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + negocio.nombre, + style: TextStyle( + color: AppTheme.of(context) + .primaryText, + fontSize: 20, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 4), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.2), + borderRadius: + BorderRadius.circular(15), + border: Border.all( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.4), + ), + ), + child: Text( + negocio.tipoLocal.isNotEmpty + ? negocio.tipoLocal + : 'Sucursal', + style: TextStyle( + color: AppTheme.of(context) + .primaryColor, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + + // Botón de acciones + PopupMenuButton( + icon: Icon( + Icons.more_vert, + color: + AppTheme.of(context).secondaryText, + ), + color: AppTheme.of(context) + .secondaryBackground, + onSelected: (value) { + _handleMenuAction( + context, value, negocio); + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'edit', + child: Row( + children: [ + Icon(Icons.edit, + color: AppTheme.of(context) + .primaryColor), + const SizedBox(width: 8), + Text( + 'Editar', + style: TextStyle( + color: AppTheme.of(context) + .primaryText), + ), + ], + ), + ), + PopupMenuItem( + value: 'components', + child: Row( + children: [ + Icon(Icons.inventory_2, + color: Colors.orange), + const SizedBox(width: 8), + Text( + 'Ver Componentes', + style: TextStyle( + color: AppTheme.of(context) + .primaryText), + ), + ], + ), + ), + PopupMenuItem( + value: 'delete', + child: Row( + children: [ + Icon(Icons.delete, + color: Colors.red), + const SizedBox(width: 8), + Text( + 'Eliminar', + style: TextStyle( + color: AppTheme.of(context) + .primaryText), + ), + ], + ), + ), + ], + ), + ], + ), + + const SizedBox(height: 16), + + // Información de ubicación + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.of(context) + .primaryBackground + .withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context) + .primaryColor + .withOpacity(0.2), + ), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.location_on, + color: AppTheme.of(context) + .primaryColor, + size: 18, + ), + const SizedBox(width: 8), + Text( + 'Ubicación', + style: TextStyle( + color: AppTheme.of(context) + .primaryColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + negocio.direccion.isNotEmpty + ? negocio.direccion + : 'Sin dirección', + style: TextStyle( + color: + AppTheme.of(context).primaryText, + fontSize: 14, + height: 1.4, + ), + ), + ], + ), + ), + + const SizedBox(height: 12), + + // Coordenadas y estadísticas + Row( + children: [ + Expanded( + child: _buildInfoChip( + context, + icon: Icons.gps_fixed, + label: 'Coordenadas', + value: + '${negocio.latitud.toStringAsFixed(4)}, ${negocio.longitud.toStringAsFixed(4)}', + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildInfoChip( + context, + icon: Icons.people, + label: 'Empleados', + value: negocio.tipoLocal == 'Sucursal' + ? '95' + : '120', + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Fecha de creación + Row( + children: [ + Icon( + Icons.calendar_today, + color: AppTheme.of(context).secondaryText, + size: 16, + ), + const SizedBox(width: 8), + Text( + 'Creado: ${negocio.fechaCreacion.toString().split(' ')[0]}', + style: TextStyle( + color: + AppTheme.of(context).secondaryText, + fontSize: 12, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ), + ), + ); + }, + ); + }, + ), + ); + } + + Widget _buildInfoChip( + BuildContext context, { + required IconData icon, + required String label, + required String value, + }) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).primaryColor.withOpacity(0.1), + AppTheme.of(context).tertiaryColor.withOpacity(0.1), + ], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + icon, + color: AppTheme.of(context).primaryColor, + size: 16, + ), + const SizedBox(width: 6), + Text( + label, + style: TextStyle( + color: AppTheme.of(context).primaryColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState(BuildContext context) { + return Center( + child: Container( + padding: const EdgeInsets.all(40), + margin: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.business_center, + size: 60, + color: Colors.white, + ), + const SizedBox(height: 16), + Text( + 'Selecciona una empresa', + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Toca el botón de empresas para seleccionar una y ver sus sucursales', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: 14, + ), + ), + ], + ), + ), + ); + } + + Widget _buildNoDataState(BuildContext context) { + return Center( + child: Container( + padding: const EdgeInsets.all(40), + margin: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.store_mall_directory, + size: 60, + color: AppTheme.of(context).secondaryText, + ), + const SizedBox(height: 16), + Text( + 'Sin sucursales', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Esta empresa aún no tiene sucursales registradas', + textAlign: TextAlign.center, + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 14, + ), + ), + ], + ), + ), + ); + } + + void _showNegocioDetails(BuildContext context, dynamic negocio) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.6, + maxChildSize: 0.9, + 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: Column( + children: [ + // Handle + Container( + margin: const EdgeInsets.symmetric(vertical: 10), + width: 40, + height: 4, + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryText, + borderRadius: BorderRadius.circular(2), + ), + ), + // Contenido del modal + Expanded( + child: SingleChildScrollView( + controller: scrollController, + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Detalles de la Sucursal', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 20), + // Aquí puedes agregar más detalles específicos + Text( + 'Información adicional de ${negocio.nombre}', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 16, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + void _handleMenuAction(BuildContext context, String action, dynamic negocio) { + switch (action) { + case 'edit': + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Función de edición próximamente')), + ); + break; + case 'components': + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ver componentes de ${negocio.nombre}')), + ); + break; + case 'delete': + _showDeleteDialog(context, negocio); + break; + } + } + + void _showDeleteDialog(BuildContext context, dynamic negocio) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppTheme.of(context).primaryBackground, + title: Text( + 'Confirmar eliminación', + style: TextStyle(color: AppTheme.of(context).primaryText), + ), + content: Text( + '¿Estás seguro de que deseas eliminar la sucursal "${negocio.nombre}"?', + style: TextStyle(color: AppTheme.of(context).secondaryText), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancelar', + style: TextStyle(color: AppTheme.of(context).secondaryText), + ), + ), + TextButton( + onPressed: () async { + Navigator.pop(context); + final success = await provider.eliminarNegocio(negocio.id); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + success + ? 'Sucursal eliminada correctamente' + : 'Error al eliminar la sucursal', + ), + backgroundColor: success ? Colors.green : Colors.red, + ), + ); + } + }, + child: const Text( + 'Eliminar', + style: TextStyle(color: Colors.red), + ), + ), + ], + ), + ); + } +} diff --git a/lib/providers/nethive/empresas_negocios_provider.dart b/lib/providers/nethive/empresas_negocios_provider.dart index fdf4ecd..b6d73fe 100644 --- a/lib/providers/nethive/empresas_negocios_provider.dart +++ b/lib/providers/nethive/empresas_negocios_provider.dart @@ -161,9 +161,10 @@ class EmpresasNegociosProvider extends ChangeNotifier { logoFileName = 'logo-$timestamp-${picker.files.single.name}'; logoToUpload = picker.files.single.bytes; - } - notifyListeners(); + // Notificar inmediatamente después de seleccionar + notifyListeners(); + } } Future selectImagen() async { @@ -182,9 +183,10 @@ class EmpresasNegociosProvider extends ChangeNotifier { imagenFileName = 'imagen-$timestamp-${picker.files.single.name}'; imagenToUpload = picker.files.single.bytes; - } - notifyListeners(); + // Notificar inmediatamente después de seleccionar + notifyListeners(); + } } Future uploadLogo() async { diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index ffcbe38..ad2ce51 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -135,39 +135,40 @@ abstract class AppTheme { class LightModeTheme extends AppTheme { @override - Color primaryColor = const Color(0xFF3B82F6); // Azul brillante del login + Color primaryColor = const Color(0xFF10B981); // Verde esmeralda principal @override - Color secondaryColor = const Color(0xFF10B981); // Verde esmeralda del login + Color secondaryColor = const Color(0xFF059669); // Verde más oscuro @override - Color tertiaryColor = const Color(0xFF0369A1); // Azul medio del login + Color tertiaryColor = const Color(0xFF0D9488); // Verde azulado @override - Color alternate = const Color(0xFF7C3AED); // Púrpura del login + Color alternate = const Color(0xFF3B82F6); // Azul de acento @override - Color primaryBackground = const Color(0xFFFFFFFF); + Color primaryBackground = const Color(0xFF0F172A); // Fondo muy oscuro @override - Color secondaryBackground = const Color(0xFFF8FAFC); // Gris muy claro + Color secondaryBackground = + const Color(0xFF1E293B); // Fondo secundario oscuro @override - Color tertiaryBackground = const Color(0xFFF1F5F9); // Gris claro azulado + Color tertiaryBackground = const Color(0xFF334155); // Fondo terciario @override Color transparentBackground = - const Color(0xFF1E293B).withOpacity(.1); // Azul oscuro transparente + const Color(0xFF1E293B).withOpacity(.1); // Fondo transparente @override - Color primaryText = const Color(0xFF0F172A); // Azul muy oscuro del login + Color primaryText = const Color(0xFFFFFFFF); // Texto blanco @override - Color secondaryText = const Color(0xFF1E293B); // Azul oscuro + Color secondaryText = const Color(0xFF94A3B8); // Texto gris claro @override - Color tertiaryText = const Color(0xFF64748B); // Gris azulado + Color tertiaryText = const Color(0xFF64748B); // Texto gris medio @override - Color hintText = const Color(0xFF94A3B8); // Gris claro + Color hintText = const Color(0xFF475569); // Texto de sugerencia @override - Color error = const Color(0xFFEF4444); // Rojo moderno + Color error = const Color(0xFFEF4444); // Rojo para errores @override - Color warning = const Color(0xFFF59E0B); // Amarillo moderno + Color warning = const Color(0xFFF59E0B); // Amarillo para advertencias @override - Color success = const Color(0xFF10B981); // Verde esmeralda del login + Color success = const Color(0xFF10B981); // Verde para éxito @override Color formBackground = - const Color(0xFF3B82F6).withOpacity(.05); // Azul muy claro + const Color(0xFF10B981).withOpacity(.05); // Fondo de formularios LightModeTheme({Mode? mode}) { if (mode != null) { @@ -182,40 +183,68 @@ class LightModeTheme extends AppTheme { class DarkModeTheme extends AppTheme { @override - Color primaryColor = const Color(0xFF3B82F6); // Azul brillante del login + Color primaryColor = const Color(0xFF10B981); // Verde esmeralda principal @override - Color secondaryColor = const Color(0xFF10B981); // Verde esmeralda del login + Color secondaryColor = const Color(0xFF059669); // Verde más oscuro @override - Color tertiaryColor = const Color(0xFF0369A1); // Azul medio del login + Color tertiaryColor = const Color(0xFF0D9488); // Verde azulado @override - Color alternate = const Color(0xFF7C3AED); // Púrpura del login + Color alternate = const Color(0xFF3B82F6); // Azul de acento @override - Color primaryBackground = - const Color(0xFF0F172A); // Azul muy oscuro del login + Color primaryBackground = const Color(0xFF0F172A); // Fondo muy oscuro @override - Color secondaryBackground = const Color(0xFF1E293B); // Azul oscuro del login + Color secondaryBackground = + const Color(0xFF1E293B); // Fondo secundario oscuro @override - Color tertiaryBackground = const Color(0xFF334155); // Azul gris + Color tertiaryBackground = const Color(0xFF334155); // Fondo terciario @override Color transparentBackground = - const Color(0xFF1E293B).withOpacity(.3); // Azul oscuro transparente + const Color(0xFF1E293B).withOpacity(.3); // Fondo transparente @override - Color primaryText = const Color(0xFFFFFFFF); // Blanco para contraste + Color primaryText = const Color(0xFFFFFFFF); // Texto blanco @override - Color secondaryText = const Color(0xFFF1F5F9); // Gris muy claro + Color secondaryText = const Color(0xFF94A3B8); // Texto gris claro @override - Color tertiaryText = const Color(0xFF94A3B8); // Gris azulado claro + Color tertiaryText = const Color(0xFF64748B); // Texto gris medio @override - Color hintText = const Color(0xFF64748B); // Gris medio + Color hintText = const Color(0xFF475569); // Texto de sugerencia @override - Color error = const Color(0xFFEF4444); // Rojo moderno + Color error = const Color(0xFFEF4444); // Rojo para errores @override - Color warning = const Color(0xFFF59E0B); // Amarillo moderno + Color warning = const Color(0xFFF59E0B); // Amarillo para advertencias @override - Color success = const Color(0xFF10B981); // Verde esmeralda del login + Color success = const Color(0xFF10B981); // Verde para éxito @override Color formBackground = - const Color(0xFF3B82F6).withOpacity(.1); // Azul transparente + const Color(0xFF10B981).withOpacity(.1); // Fondo de formularios + + // Nuevos gradientes modernos + LinearGradient get primaryGradient => LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + primaryColor, + secondaryColor, + ], + ); + + LinearGradient get modernGradient => LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + tertiaryColor, + primaryColor, + ], + ); + + LinearGradient get darkBackgroundGradient => LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + primaryBackground, + secondaryBackground, + ], + ); DarkModeTheme({Mode? mode}) { if (mode != null) {