saves before gran cambio he implementacion de las otras paginas

This commit is contained in:
Abraham
2025-07-17 12:28:11 -07:00
parent 3e7a26dadc
commit 14a680547c
15 changed files with 2822 additions and 0 deletions

BIN
assets/referencia/crm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

View File

@@ -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<AddEmpresaDialog> createState() => _AddEmpresaDialogState();
}
class _AddEmpresaDialogState extends State<AddEmpresaDialog>
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,
),
),
],
);
}
}

View File

@@ -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<Color>(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,
),
),
],
),
),
),
),
],
);
}
}

View File

@@ -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<double> _scaleAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
late Listenable _combinedAnimation;
bool _isInitialized = false;
EmpresaDialogAnimations({required this.vsync});
// Getters para acceder a las animaciones
Animation<double> get scaleAnimation => _scaleAnimation;
Animation<Offset> get slideAnimation => _slideAnimation;
Animation<double> 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<Offset>(
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();
}
}

View File

@@ -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<EmpresaDialogForm> createState() => _EmpresaDialogFormState();
}
class _EmpresaDialogFormState extends State<EmpresaDialogForm> {
final _formKey = GlobalKey<FormState>();
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<void> _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;
});
}
}
}
}

View File

@@ -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<Offset> 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,
),
);
}
}

View File

@@ -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,
),
),
],
],
),
),
),
),
);
}
}

View File

@@ -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),
),
),
);
}
}

View File

@@ -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<AddNegocioDialog> createState() => _AddNegocioDialogState();
}
class _AddNegocioDialogState extends State<AddNegocioDialog>
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
),
),
],
);
}
}

View File

@@ -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<Color>(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,
),
),
],
),
),
),
),
],
);
}
}

View File

@@ -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<double> _scaleAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
late Listenable _combinedAnimation;
bool _isInitialized = false;
NegocioDialogAnimations({required this.vsync});
// Getters para acceder a las animaciones
Animation<double> get scaleAnimation => _scaleAnimation;
Animation<Offset> get slideAnimation => _slideAnimation;
Animation<double> 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<Offset>(
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();
}
}

View File

@@ -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<NegocioDialogForm> createState() => _NegocioDialogFormState();
}
class _NegocioDialogFormState extends State<NegocioDialogForm> {
final _formKey = GlobalKey<FormState>();
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<void> _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;
});
}
}
}
}

View File

@@ -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<Offset> 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,
),
);
}
}

View File

@@ -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<String>(
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<String>(
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;
},
),
);
}
}

View File

@@ -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<TextInputFormatter>? 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),
),
),
);
}
}