saves before gran cambio he implementacion de las otras paginas
This commit is contained in:
BIN
assets/referencia/crm.png
Normal file
BIN
assets/referencia/crm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 767 KiB |
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user