saves before gran cambio he implementacion de las otras paginas
This commit is contained in:
@@ -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