diff --git a/assets/referencia/crm.png b/assets/referencia/crm.png new file mode 100644 index 0000000..d466ea7 Binary files /dev/null and b/assets/referencia/crm.png differ diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/add_empresa_dialog.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/add_empresa_dialog.dart new file mode 100644 index 0000000..7e4d6eb --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/add_empresa_dialog.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'empresa_dialog_animations.dart'; +import 'empresa_dialog_header.dart'; +import 'empresa_dialog_form.dart'; + +class AddEmpresaDialog extends StatefulWidget { + final EmpresasNegociosProvider provider; + + const AddEmpresaDialog({ + Key? key, + required this.provider, + }) : super(key: key); + + @override + State createState() => _AddEmpresaDialogState(); +} + +class _AddEmpresaDialogState extends State + with TickerProviderStateMixin { + late EmpresaDialogAnimations _animations; + + @override + void initState() { + super.initState(); + _animations = EmpresaDialogAnimations(vsync: this); + _animations.initialize(); + + // Escuchar cambios del provider + widget.provider.addListener(_onProviderChanged); + } + + void _onProviderChanged() { + if (mounted) { + setState(() { + // Forzar rebuild cuando cambie el provider + }); + } + } + + @override + void dispose() { + widget.provider.removeListener(_onProviderChanged); + _animations.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!_animations.isInitialized) { + 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: _animations.combinedAnimation, + builder: (context, child) { + return FadeTransition( + opacity: _animations.fadeAnimation, + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), + child: Transform.scale( + scale: _animations.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(), + ), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildDesktopLayout() { + return Row( + children: [ + // Header lateral compacto para desktop + EmpresaDialogHeader( + isDesktop: true, + slideAnimation: _animations.slideAnimation, + ), + + // Contenido principal del formulario + Expanded( + child: EmpresaDialogForm( + provider: widget.provider, + isDesktop: true, + ), + ), + ], + ); + } + + Widget _buildMobileLayout() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header para móvil + EmpresaDialogHeader( + isDesktop: false, + slideAnimation: _animations.slideAnimation, + ), + + // Contenido del formulario para móvil + Flexible( + child: EmpresaDialogForm( + provider: widget.provider, + isDesktop: false, + ), + ), + ], + ); + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_action_buttons.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_action_buttons.dart new file mode 100644 index 0000000..3756248 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_action_buttons.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class EmpresaActionButtons extends StatelessWidget { + final bool isLoading; + final bool isDesktop; + final VoidCallback onCancel; + final VoidCallback onSubmit; + + const EmpresaActionButtons({ + Key? key, + required this.isLoading, + required this.isDesktop, + required this.onCancel, + required this.onSubmit, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + // Botón Cancelar + Expanded( + child: Container( + height: isDesktop ? 45 : 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppTheme.of(context).secondaryText.withOpacity(0.3), + width: 2, + ), + ), + child: TextButton( + onPressed: isLoading ? null : onCancel, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.close_rounded, + color: AppTheme.of(context).secondaryText, + size: 18, + ), + const SizedBox(width: 8), + Text( + 'Cancelar', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(width: 16), + + // Botón Crear Empresa + Expanded( + flex: 2, + child: Container( + height: isDesktop ? 45 : 50, + decoration: BoxDecoration( + gradient: AppTheme.of(context).primaryGradient, + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.4), + blurRadius: 15, + offset: const Offset(0, 5), + spreadRadius: 2, + ), + ], + ), + child: ElevatedButton( + onPressed: isLoading ? null : onSubmit, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.business_center_rounded, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 10), + Text( + 'Crear Empresa', + style: TextStyle( + color: Colors.white, + fontSize: isDesktop ? 14 : 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_animations.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_animations.dart new file mode 100644 index 0000000..c99ba64 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_animations.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +class EmpresaDialogAnimations { + final TickerProvider vsync; + + late AnimationController _scaleController; + late AnimationController _slideController; + late AnimationController _fadeController; + late Animation _scaleAnimation; + late Animation _slideAnimation; + late Animation _fadeAnimation; + late Listenable _combinedAnimation; + bool _isInitialized = false; + + EmpresaDialogAnimations({required this.vsync}); + + // Getters para acceder a las animaciones + Animation get scaleAnimation => _scaleAnimation; + Animation get slideAnimation => _slideAnimation; + Animation get fadeAnimation => _fadeAnimation; + Listenable get combinedAnimation => _combinedAnimation; + bool get isInitialized => _isInitialized; + + void initialize() { + _scaleController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: vsync, + ); + _slideController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: vsync, + ); + _fadeController = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: vsync, + ); + + _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, + ); + + _combinedAnimation = + Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]); + + // Pequeño delay para asegurar que el widget esté completamente montado + WidgetsBinding.instance.addPostFrameCallback((_) { + _isInitialized = true; + _startAnimations(); + }); + } + + void _startAnimations() { + _fadeController.forward(); + Future.delayed(const Duration(milliseconds: 100), () { + _scaleController.forward(); + }); + Future.delayed(const Duration(milliseconds: 200), () { + _slideController.forward(); + }); + } + + void dispose() { + _scaleController.dispose(); + _slideController.dispose(); + _fadeController.dispose(); + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_form.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_form.dart new file mode 100644 index 0000000..2d79fb2 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_form.dart @@ -0,0 +1,228 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'empresa_form_fields.dart'; +import 'empresa_file_section.dart'; +import 'empresa_action_buttons.dart'; + +class EmpresaDialogForm extends StatefulWidget { + final EmpresasNegociosProvider provider; + final bool isDesktop; + + const EmpresaDialogForm({ + Key? key, + required this.provider, + required this.isDesktop, + }) : super(key: key); + + @override + State createState() => _EmpresaDialogFormState(); +} + +class _EmpresaDialogFormState extends State { + final _formKey = GlobalKey(); + final _nombreController = TextEditingController(); + final _rfcController = TextEditingController(); + final _direccionController = TextEditingController(); + final _telefonoController = TextEditingController(); + final _emailController = TextEditingController(); + + bool _isLoading = false; + + @override + void dispose() { + _nombreController.dispose(); + _rfcController.dispose(); + _direccionController.dispose(); + _telefonoController.dispose(); + _emailController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(25), + child: Form( + key: _formKey, + child: widget.isDesktop ? _buildDesktopForm() : _buildMobileForm(), + ), + ); + } + + Widget _buildDesktopForm() { + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + // Campos del formulario en filas para desktop + EmpresaFormFields( + isDesktop: true, + nombreController: _nombreController, + rfcController: _rfcController, + direccionController: _direccionController, + telefonoController: _telefonoController, + emailController: _emailController, + ), + + const SizedBox(height: 20), + + // Sección de archivos + EmpresaFileSection( + provider: widget.provider, + isDesktop: true, + ), + + const SizedBox(height: 25), + + // Botones de acción + EmpresaActionButtons( + isLoading: _isLoading, + isDesktop: true, + onCancel: () => _handleCancel(), + onSubmit: () => _crearEmpresa(), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildMobileForm() { + return SingleChildScrollView( + child: Column( + children: [ + // Campos del formulario en columnas para móvil + EmpresaFormFields( + isDesktop: false, + nombreController: _nombreController, + rfcController: _rfcController, + direccionController: _direccionController, + telefonoController: _telefonoController, + emailController: _emailController, + ), + + const SizedBox(height: 20), + + // Sección de archivos + EmpresaFileSection( + provider: widget.provider, + isDesktop: false, + ), + + const SizedBox(height: 25), + + // Botones de acción + EmpresaActionButtons( + isLoading: _isLoading, + isDesktop: false, + onCancel: () => _handleCancel(), + onSubmit: () => _crearEmpresa(), + ), + ], + ), + ); + } + + void _handleCancel() { + widget.provider.resetFormData(); + Navigator.of(context).pop(); + } + + Future _crearEmpresa() async { + if (!_formKey.currentState!.validate()) return; + + setState(() { + _isLoading = true; + }); + + try { + final success = await widget.provider.crearEmpresa( + nombre: _nombreController.text.trim(), + rfc: _rfcController.text.trim(), + direccion: _direccionController.text.trim(), + telefono: _telefonoController.text.trim(), + email: _emailController.text.trim(), + ); + + if (mounted) { + if (success) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: const [ + Icon(Icons.check_circle, color: Colors.white), + SizedBox(width: 12), + Text( + '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( + 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), + ), + ), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.warning, color: Colors.white), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Error: $e', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + ], + ), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_header.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_header.dart new file mode 100644 index 0000000..9c6d807 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_dialog_header.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class EmpresaDialogHeader extends StatelessWidget { + final bool isDesktop; + final Animation slideAnimation; + + const EmpresaDialogHeader({ + Key? key, + required this.isDesktop, + required this.slideAnimation, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (isDesktop) { + return _buildDesktopHeader(context); + } else { + return _buildMobileHeader(context); + } + } + + Widget _buildDesktopHeader(BuildContext context) { + return 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: [ + _buildIcon(context), + const SizedBox(height: 20), + _buildTitle(context, fontSize: 24), + const SizedBox(height: 8), + _buildSubtitle(context, fontSize: 14), + ], + ), + ), + ), + ); + } + + Widget _buildMobileHeader(BuildContext context) { + return 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: [ + _buildIcon(context), + const SizedBox(height: 16), + _buildTitle(context, fontSize: 26), + const SizedBox(height: 8), + _buildSubtitle(context, fontSize: 14, isMobile: true), + ], + ), + ), + ); + } + + Widget _buildIcon(BuildContext context) { + return Container( + padding: EdgeInsets.all(isDesktop ? 20 : 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: Icon( + Icons.business_center_rounded, + color: Colors.white, + size: isDesktop ? 35 : 35, + ), + ); + } + + Widget _buildTitle(BuildContext context, {required double fontSize}) { + return Text( + 'Nueva Empresa', + style: TextStyle( + color: Colors.white, + fontSize: fontSize, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + textAlign: TextAlign.center, + ); + } + + Widget _buildSubtitle(BuildContext context, + {required double fontSize, bool isMobile = false}) { + return 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( + isMobile + ? '✨ Registra una nueva empresa en tu sistema' + : '✨ Registra una nueva empresa', + style: TextStyle( + color: Colors.white.withOpacity(0.95), + fontSize: fontSize, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + ), + ); + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_file_section.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_file_section.dart new file mode 100644 index 0000000..c0970e2 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_file_section.dart @@ -0,0 +1,425 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class EmpresaFileSection extends StatelessWidget { + final EmpresasNegociosProvider provider; + final bool isDesktop; + + const EmpresaFileSection({ + Key? key, + required this.provider, + required this.isDesktop, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).primaryColor.withOpacity(0.1), + AppTheme.of(context).tertiaryColor.withOpacity(0.1), + AppTheme.of(context).secondaryColor.withOpacity(0.05), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.4), + width: 2, + ), + 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 + _buildSectionHeader(context), + + const SizedBox(height: 16), + + // Botones de archivos + if (isDesktop) + _buildDesktopFileButtons(context) + else + _buildMobileFileButtons(context), + ], + ), + ); + } + + Widget _buildSectionHeader(BuildContext context) { + return 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, + ), + ), + ], + ), + ], + ); + } + + Widget _buildDesktopFileButtons(BuildContext context) { + return Row( + children: [ + Expanded( + child: _buildCompactFileButton( + context: context, + label: 'Logo de la empresa', + subtitle: 'PNG, JPG (Max 2MB)', + icon: Icons.image_rounded, + fileName: provider.logoFileName, + file: provider.logoToUpload, + onPressed: provider.selectLogo, + gradient: LinearGradient( + colors: [Colors.blue.shade400, Colors.blue.shade600], + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildCompactFileButton( + context: context, + label: 'Imagen principal', + subtitle: 'Imagen representativa', + icon: Icons.photo_library_rounded, + fileName: provider.imagenFileName, + file: provider.imagenToUpload, + onPressed: provider.selectImagen, + gradient: LinearGradient( + colors: [Colors.purple.shade400, Colors.purple.shade600], + ), + ), + ), + ], + ); + } + + Widget _buildMobileFileButtons(BuildContext context) { + return Column( + children: [ + _buildEnhancedFileButton( + context: context, + label: 'Logo de la empresa', + subtitle: 'Formato PNG, JPG (Max 2MB)', + icon: Icons.image_rounded, + fileName: provider.logoFileName, + file: provider.logoToUpload, + onPressed: provider.selectLogo, + gradient: LinearGradient( + colors: [Colors.blue.shade400, Colors.blue.shade600], + ), + ), + const SizedBox(height: 12), + _buildEnhancedFileButton( + context: context, + label: 'Imagen principal', + subtitle: 'Imagen representativa de la empresa', + icon: Icons.photo_library_rounded, + fileName: provider.imagenFileName, + file: provider.imagenToUpload, + onPressed: provider.selectImagen, + gradient: LinearGradient( + colors: [Colors.purple.shade400, Colors.purple.shade600], + ), + ), + ], + ); + } + + Widget _buildCompactFileButton({ + required BuildContext context, + 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: provider.getImageWidget( + file, + height: 40, + width: 40, + ), + ), + ), + ], + ], + ), + ), + ), + ), + ); + } + + Widget _buildEnhancedFileButton({ + required BuildContext context, + 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: 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, + ), + ), + ], + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_form_fields.dart b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_form_fields.dart new file mode 100644 index 0000000..3b0e5e3 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_empresa_dialog/empresa_form_fields.dart @@ -0,0 +1,301 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class EmpresaFormFields extends StatelessWidget { + final bool isDesktop; + final TextEditingController nombreController; + final TextEditingController rfcController; + final TextEditingController direccionController; + final TextEditingController telefonoController; + final TextEditingController emailController; + + const EmpresaFormFields({ + Key? key, + required this.isDesktop, + required this.nombreController, + required this.rfcController, + required this.direccionController, + required this.telefonoController, + required this.emailController, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (isDesktop) { + return _buildDesktopFields(context); + } else { + return _buildMobileFields(context); + } + } + + Widget _buildDesktopFields(BuildContext context) { + return Column( + children: [ + // Primera fila - Nombre y RFC + Row( + children: [ + Expanded( + flex: 2, + child: _buildFormField( + context: context, + 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: _buildFormField( + context: context, + 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 + _buildFormField( + context: context, + 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: _buildFormField( + context: context, + 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: _buildFormField( + context: context, + 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; + }, + ), + ), + ], + ), + ], + ); + } + + Widget _buildMobileFields(BuildContext context) { + return Column( + children: [ + _buildFormField( + context: context, + 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; + }, + ), + _buildFormField( + context: context, + 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; + }, + ), + _buildFormField( + context: context, + 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; + }, + ), + _buildFormField( + context: context, + 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; + }, + ), + _buildFormField( + context: context, + 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; + }, + ), + ], + ); + } + + Widget _buildFormField({ + required BuildContext context, + 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), + ), + ), + ); + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/add_negocio_dialog.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/add_negocio_dialog.dart new file mode 100644 index 0000000..8a6ec0e --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/add_negocio_dialog.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; +import 'negocio_dialog_animations.dart'; +import 'negocio_dialog_header.dart'; +import 'negocio_dialog_form.dart'; + +class AddNegocioDialog extends StatefulWidget { + final EmpresasNegociosProvider provider; + final String empresaId; + + const AddNegocioDialog({ + Key? key, + required this.provider, + required this.empresaId, + }) : super(key: key); + + @override + State createState() => _AddNegocioDialogState(); +} + +class _AddNegocioDialogState extends State + with TickerProviderStateMixin { + late NegocioDialogAnimations _animations; + + @override + void initState() { + super.initState(); + _animations = NegocioDialogAnimations(vsync: this); + _animations.initialize(); + + // Escuchar cambios del provider + widget.provider.addListener(_onProviderChanged); + } + + void _onProviderChanged() { + if (mounted) { + setState(() { + // Forzar rebuild cuando cambie el provider + }); + } + } + + @override + void dispose() { + widget.provider.removeListener(_onProviderChanged); + _animations.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!_animations.isInitialized) { + 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 ? 950.0 : (isTablet ? 800.0 : 700.0); + final maxHeight = isDesktop ? 750.0 : 800.0; + + return AnimatedBuilder( + animation: _animations.combinedAnimation, + builder: (context, child) { + return FadeTransition( + opacity: _animations.fadeAnimation, + child: Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.all(isDesktop ? 40 : 20), + child: Transform.scale( + scale: _animations.scaleAnimation.value, + child: Container( + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: maxHeight, + minHeight: isDesktop ? 650 : 450, + ), + 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(), + ), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildDesktopLayout() { + return Row( + children: [ + // Header lateral compacto para desktop + NegocioDialogHeader( + isDesktop: true, + slideAnimation: _animations.slideAnimation, + ), + + // Contenido principal del formulario + Expanded( + child: NegocioDialogForm( + provider: widget.provider, + isDesktop: true, + empresaId: widget.empresaId, // Pasar empresaId al formulario + ), + ), + ], + ); + } + + Widget _buildMobileLayout() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header para móvil + NegocioDialogHeader( + isDesktop: false, + slideAnimation: _animations.slideAnimation, + ), + + // Contenido del formulario para móvil + Flexible( + child: NegocioDialogForm( + provider: widget.provider, + isDesktop: false, + empresaId: widget.empresaId, // Pasar empresaId al formulario + ), + ), + ], + ); + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_action_buttons.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_action_buttons.dart new file mode 100644 index 0000000..5257dc1 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_action_buttons.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class NegocioActionButtons extends StatelessWidget { + final bool isLoading; + final bool isDesktop; + final VoidCallback onCancel; + final VoidCallback onSubmit; + + const NegocioActionButtons({ + Key? key, + required this.isLoading, + required this.isDesktop, + required this.onCancel, + required this.onSubmit, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + // Botón Cancelar + Expanded( + child: Container( + height: isDesktop ? 45 : 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppTheme.of(context).secondaryText.withOpacity(0.3), + width: 2, + ), + ), + child: TextButton( + onPressed: isLoading ? null : onCancel, + style: TextButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.close_rounded, + color: AppTheme.of(context).secondaryText, + size: 18, + ), + const SizedBox(width: 8), + Text( + 'Cancelar', + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ), + ), + + const SizedBox(width: 16), + + // Botón Crear Negocio + Expanded( + flex: 2, + child: Container( + height: isDesktop ? 45 : 50, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + ], + ), + borderRadius: BorderRadius.circular(15), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.4), + blurRadius: 15, + offset: const Offset(0, 5), + spreadRadius: 2, + ), + ], + ), + child: ElevatedButton( + onPressed: isLoading ? null : onSubmit, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + 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_business_rounded, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 10), + Text( + 'Crear Negocio', + style: TextStyle( + color: Colors.white, + fontSize: isDesktop ? 14 : 16, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_animations.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_animations.dart new file mode 100644 index 0000000..9877f39 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_animations.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +class NegocioDialogAnimations { + final TickerProvider vsync; + + late AnimationController _scaleController; + late AnimationController _slideController; + late AnimationController _fadeController; + late Animation _scaleAnimation; + late Animation _slideAnimation; + late Animation _fadeAnimation; + late Listenable _combinedAnimation; + bool _isInitialized = false; + + NegocioDialogAnimations({required this.vsync}); + + // Getters para acceder a las animaciones + Animation get scaleAnimation => _scaleAnimation; + Animation get slideAnimation => _slideAnimation; + Animation get fadeAnimation => _fadeAnimation; + Listenable get combinedAnimation => _combinedAnimation; + bool get isInitialized => _isInitialized; + + void initialize() { + _scaleController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: vsync, + ); + _slideController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: vsync, + ); + _fadeController = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: vsync, + ); + + _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, + ); + + _combinedAnimation = + Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]); + + // Pequeño delay para asegurar que el widget esté completamente montado + WidgetsBinding.instance.addPostFrameCallback((_) { + _isInitialized = true; + _startAnimations(); + }); + } + + void _startAnimations() { + _fadeController.forward(); + Future.delayed(const Duration(milliseconds: 100), () { + _scaleController.forward(); + }); + Future.delayed(const Duration(milliseconds: 200), () { + _slideController.forward(); + }); + } + + void dispose() { + _scaleController.dispose(); + _slideController.dispose(); + _fadeController.dispose(); + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_form.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_form.dart new file mode 100644 index 0000000..4e54849 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_form.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; +import 'negocio_form_fields.dart'; +import 'negocio_empresa_selector.dart'; +import 'negocio_action_buttons.dart'; + +class NegocioDialogForm extends StatefulWidget { + final EmpresasNegociosProvider provider; + final bool isDesktop; + final String empresaId; + + const NegocioDialogForm({ + Key? key, + required this.provider, + required this.isDesktop, + required this.empresaId, + }) : super(key: key); + + @override + State createState() => _NegocioDialogFormState(); +} + +class _NegocioDialogFormState extends State { + final _formKey = GlobalKey(); + final _nombreController = TextEditingController(); + final _direccionController = TextEditingController(); + final _latitudController = TextEditingController(); + final _longitudController = TextEditingController(); + final _tipoLocalController = TextEditingController(); + bool _isLoading = false; + + @override + void dispose() { + _nombreController.dispose(); + _direccionController.dispose(); + _latitudController.dispose(); + _longitudController.dispose(); + _tipoLocalController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(25), + child: Form( + key: _formKey, + child: widget.isDesktop ? _buildDesktopForm() : _buildMobileForm(), + ), + ); + } + + Widget _buildDesktopForm() { + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + // Selector de empresa + NegocioEmpresaSelector( + provider: widget.provider, + isDesktop: true, + ), + + const SizedBox(height: 20), + + // Campos del formulario en filas para desktop + NegocioFormFields( + isDesktop: true, + nombreController: _nombreController, + direccionController: _direccionController, + latitudController: _latitudController, + longitudController: _longitudController, + tipoLocalController: _tipoLocalController, + ), + + const SizedBox(height: 25), + + // Botones de acción + NegocioActionButtons( + isLoading: _isLoading, + isDesktop: true, + onCancel: () => _handleCancel(), + onSubmit: () => _crearNegocio(), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildMobileForm() { + return SingleChildScrollView( + child: Column( + children: [ + // Selector de empresa + NegocioEmpresaSelector( + provider: widget.provider, + isDesktop: false, + ), + + const SizedBox(height: 20), + + // Campos del formulario en columnas para móvil + NegocioFormFields( + isDesktop: false, + nombreController: _nombreController, + direccionController: _direccionController, + latitudController: _latitudController, + longitudController: _longitudController, + tipoLocalController: _tipoLocalController, + ), + + const SizedBox(height: 25), + + // Botones de acción + NegocioActionButtons( + isLoading: _isLoading, + isDesktop: false, + onCancel: () => _handleCancel(), + onSubmit: () => _crearNegocio(), + ), + ], + ), + ); + } + + void _handleCancel() { + widget.provider.resetFormData(); + Navigator.of(context).pop(); + } + + Future _crearNegocio() async { + if (!_formKey.currentState!.validate()) return; + + setState(() { + _isLoading = true; + }); + + try { + final latitud = double.parse(_latitudController.text.trim()); + final longitud = double.parse(_longitudController.text.trim()); + + final success = await widget.provider.crearNegocio( + empresaId: widget.empresaId, // Usar el empresaId pasado como parámetro + nombre: _nombreController.text.trim(), + direccion: _direccionController.text.trim(), + latitud: latitud, + longitud: longitud, + tipoLocal: _tipoLocalController.text.trim(), + ); + + if (mounted) { + if (success) { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: const [ + Icon(Icons.check_circle, color: Colors.white), + SizedBox(width: 12), + Text( + 'Negocio creado exitosamente', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: const [ + Icon(Icons.error, color: Colors.white), + SizedBox(width: 12), + Text( + 'Error al crear el negocio', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ], + ), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.warning, color: Colors.white), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Error: $e', + style: const TextStyle(fontWeight: FontWeight.w600), + ), + ), + ], + ), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_header.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_header.dart new file mode 100644 index 0000000..b5e5219 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_dialog_header.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class NegocioDialogHeader extends StatelessWidget { + final bool isDesktop; + final Animation slideAnimation; + + const NegocioDialogHeader({ + Key? key, + required this.isDesktop, + required this.slideAnimation, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (isDesktop) { + return _buildDesktopHeader(context); + } else { + return _buildMobileHeader(context); + } + } + + Widget _buildDesktopHeader(BuildContext context) { + return Container( + width: 300, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + ], + ), + 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: [ + _buildIcon(context), + const SizedBox(height: 20), + _buildTitle(context, fontSize: 24), + const SizedBox(height: 8), + _buildSubtitle(context, fontSize: 14), + ], + ), + ), + ), + ); + } + + Widget _buildMobileHeader(BuildContext context) { + return 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).tertiaryColor, + AppTheme.of(context).primaryColor, + AppTheme.of(context).secondaryColor, + ], + ), + boxShadow: [ + BoxShadow( + color: AppTheme.of(context).primaryColor.withOpacity(0.5), + blurRadius: 25, + offset: const Offset(0, 15), + spreadRadius: 2, + ), + ], + ), + child: Column( + children: [ + _buildIcon(context), + const SizedBox(height: 16), + _buildTitle(context, fontSize: 26), + const SizedBox(height: 8), + _buildSubtitle(context, fontSize: 14, isMobile: true), + ], + ), + ), + ); + } + + Widget _buildIcon(BuildContext context) { + return Container( + padding: EdgeInsets.all(isDesktop ? 20 : 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: Icon( + Icons.store_rounded, + color: Colors.white, + size: isDesktop ? 35 : 35, + ), + ); + } + + Widget _buildTitle(BuildContext context, {required double fontSize}) { + return Text( + 'Nuevo Negocio', + style: TextStyle( + color: Colors.white, + fontSize: fontSize, + fontWeight: FontWeight.bold, + letterSpacing: 1.5, + ), + textAlign: TextAlign.center, + ); + } + + Widget _buildSubtitle(BuildContext context, + {required double fontSize, bool isMobile = false}) { + return 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( + isMobile + ? '🏪 Registra un nuevo negocio o sucursal' + : '🏪 Registra un nuevo negocio', + style: TextStyle( + color: Colors.white.withOpacity(0.95), + fontSize: fontSize, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + ), + ); + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_empresa_selector.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_empresa_selector.dart new file mode 100644 index 0000000..4ee1eb0 --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_empresa_selector.dart @@ -0,0 +1,192 @@ +import 'package:flutter/material.dart'; +import 'package:nethive_neo/providers/nethive/empresas_negocios_provider.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class NegocioEmpresaSelector extends StatelessWidget { + final EmpresasNegociosProvider provider; + final bool isDesktop; + + const NegocioEmpresaSelector({ + Key? key, + required this.provider, + required this.isDesktop, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + AppTheme.of(context).formBackground, + AppTheme.of(context).secondaryBackground.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(20), + 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, 5), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.business_rounded, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + Text( + 'Seleccionar Empresa', + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 15), + _buildEmpresaDropdown(context), + ], + ), + ); + } + + Widget _buildEmpresaDropdown(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: AppTheme.of(context).secondaryBackground.withOpacity(0.7), + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: AppTheme.of(context).primaryColor.withOpacity(0.3), + width: 2, + ), + ), + child: DropdownButtonFormField( + value: provider.empresaSeleccionadaId, + decoration: InputDecoration( + hintText: 'Selecciona una empresa', + hintStyle: TextStyle( + color: AppTheme.of(context).secondaryText.withOpacity(0.7), + fontSize: 14, + ), + prefixIcon: Container( + margin: const EdgeInsets.all(8), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.business_center_rounded, + color: Colors.white, + size: 18, + ), + ), + border: InputBorder.none, + contentPadding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + ), + dropdownColor: AppTheme.of(context).secondaryBackground, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + icon: Icon( + Icons.keyboard_arrow_down_rounded, + color: AppTheme.of(context).primaryColor, + size: 28, + ), + items: provider.empresas.map((empresa) { + return DropdownMenuItem( + value: empresa.id, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: AppTheme.of(context).primaryColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.business_rounded, + color: AppTheme.of(context).primaryColor, + size: 16, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + empresa.nombre, + style: TextStyle( + color: AppTheme.of(context).primaryText, + fontWeight: FontWeight.w600, + fontSize: 14, + ), + ), + if (empresa.rfc != null) + Text( + empresa.rfc!, + style: TextStyle( + color: AppTheme.of(context).secondaryText, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }).toList(), + onChanged: (String? newValue) { + provider.setEmpresaSeleccionada(newValue!); + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Selecciona una empresa'; + } + return null; + }, + ), + ); + } +} diff --git a/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_form_fields.dart b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_form_fields.dart new file mode 100644 index 0000000..54dba4d --- /dev/null +++ b/lib/pages/empresa_negocios/widgets/add_negocio_dialog/negocio_form_fields.dart @@ -0,0 +1,350 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:nethive_neo/theme/theme.dart'; + +class NegocioFormFields extends StatelessWidget { + final bool isDesktop; + final TextEditingController nombreController; + final TextEditingController direccionController; + final TextEditingController latitudController; + final TextEditingController longitudController; + final TextEditingController tipoLocalController; + + const NegocioFormFields({ + Key? key, + required this.isDesktop, + required this.nombreController, + required this.direccionController, + required this.latitudController, + required this.longitudController, + required this.tipoLocalController, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (isDesktop) { + return _buildDesktopFields(context); + } else { + return _buildMobileFields(context); + } + } + + Widget _buildDesktopFields(BuildContext context) { + return Column( + children: [ + // Primera fila - Nombre del negocio + _buildFormField( + context: context, + controller: nombreController, + label: 'Nombre del negocio', + hint: 'Ej: Sucursal Centro, Tienda Principal', + icon: Icons.store_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El nombre es requerido'; + } + return null; + }, + ), + + const SizedBox(height: 16), + + // Segunda fila - Dirección + _buildFormField( + context: context, + controller: direccionController, + label: 'Dirección', + hint: 'Dirección completa del negocio', + 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 - Coordenadas + Row( + children: [ + Expanded( + child: _buildFormField( + context: context, + controller: latitudController, + label: 'Latitud', + hint: 'Ej: 19.4326', + icon: Icons.location_searching, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')), + ], + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'La latitud es requerida'; + } + final lat = double.tryParse(value); + if (lat == null) { + return 'Número inválido'; + } + if (lat < -90 || lat > 90) { + return 'Debe estar entre -90 y 90'; + } + return null; + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildFormField( + context: context, + controller: longitudController, + label: 'Longitud', + hint: 'Ej: -99.1332', + icon: Icons.location_searching, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')), + ], + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'La longitud es requerida'; + } + final lng = double.tryParse(value); + if (lng == null) { + return 'Número inválido'; + } + if (lng < -180 || lng > 180) { + return 'Debe estar entre -180 y 180'; + } + return null; + }, + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Cuarta fila - Tipo de local + _buildFormField( + context: context, + controller: tipoLocalController, + label: 'Tipo de local', + hint: 'Ej: Sucursal, Matriz, Almacén', + icon: Icons.business, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El tipo de local es requerido'; + } + return null; + }, + ), + ], + ); + } + + Widget _buildMobileFields(BuildContext context) { + return Column( + children: [ + _buildFormField( + context: context, + controller: nombreController, + label: 'Nombre del negocio', + hint: 'Ej: Sucursal Centro, Tienda Principal', + icon: Icons.store_rounded, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El nombre es requerido'; + } + return null; + }, + ), + _buildFormField( + context: context, + controller: direccionController, + label: 'Dirección', + hint: 'Dirección completa del negocio', + icon: Icons.location_on_rounded, + maxLines: 3, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'La dirección es requerida'; + } + return null; + }, + ), + _buildFormField( + context: context, + controller: latitudController, + label: 'Latitud', + hint: 'Ej: 19.4326', + icon: Icons.location_searching, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')), + ], + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'La latitud es requerida'; + } + final lat = double.tryParse(value); + if (lat == null) { + return 'Número inválido'; + } + if (lat < -90 || lat > 90) { + return 'Debe estar entre -90 y 90'; + } + return null; + }, + ), + _buildFormField( + context: context, + controller: longitudController, + label: 'Longitud', + hint: 'Ej: -99.1332', + icon: Icons.location_searching, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + signed: true, + ), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^-?\d*\.?\d*')), + ], + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'La longitud es requerida'; + } + final lng = double.tryParse(value); + if (lng == null) { + return 'Número inválido'; + } + if (lng < -180 || lng > 180) { + return 'Debe estar entre -180 y 180'; + } + return null; + }, + ), + _buildFormField( + context: context, + controller: tipoLocalController, + label: 'Tipo de local', + hint: 'Ej: Sucursal, Matriz, Almacén', + icon: Icons.business, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'El tipo de local es requerido'; + } + return null; + }, + ), + ], + ); + } + + Widget _buildFormField({ + required BuildContext context, + required TextEditingController controller, + required String label, + required String hint, + required IconData icon, + int maxLines = 1, + TextInputType? keyboardType, + List? inputFormatters, + String? Function(String?)? validator, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 16), + child: TextFormField( + controller: controller, + maxLines: maxLines, + keyboardType: keyboardType, + inputFormatters: inputFormatters, + 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: LinearGradient( + colors: [ + AppTheme.of(context).tertiaryColor, + AppTheme.of(context).primaryColor, + ], + ), + 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), + ), + ), + ); + } +}