Last active
February 10, 2026 20:16
-
-
Save tatd3v/3eeadfcfbf4d1e2f375c38ea860643f9 to your computer and use it in GitHub Desktop.
4step-add-client
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:convert'; | |
| import 'package:admin2/global/global_constantes.dart'; | |
| import 'package:admin2/services/auth_service.dart'; | |
| import 'package:admin2/toolbox/auto_filled.dart'; | |
| import 'package:admin2/toolbox/ubicacion_widget.dart'; | |
| import 'package:admin2/toolbox/my_custom_controller.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/services.dart'; | |
| import 'package:http/http.dart' as http; | |
| import 'package:intl/intl.dart'; | |
| import 'package:intl_phone_field/intl_phone_field.dart'; | |
| import 'package:intl_phone_field/country_picker_dialog.dart'; | |
| class ClientAddDialog extends StatefulWidget { | |
| const ClientAddDialog({super.key}); | |
| @override | |
| State<ClientAddDialog> createState() => _ClientAddDialogState(); | |
| } | |
| class _ClientAddDialogState extends State<ClientAddDialog> { | |
| final _formKey = GlobalKey<FormState>(); | |
| final PageController _pageController = PageController(); | |
| int _currentStep = 0; | |
| bool _isSaving = false; | |
| // Step 1: Información Personal | |
| String _tipoDocumento = 'DNI / Cédula'; | |
| final TextEditingController _numeroIDCtrl = TextEditingController(); | |
| final TextEditingController _numeroIDInternoCtrl = TextEditingController(); | |
| final TextEditingController _nombreCtrl = TextEditingController(); | |
| final TextEditingController _apellidoCtrl = TextEditingController(); | |
| String _sexo = 'Masculino'; | |
| DateTime? _fechaNacimiento; | |
| String _estadoCivil = 'Soltero/a'; | |
| final TextEditingController _ocupacionCtrl = TextEditingController(); | |
| // Step 2: Contacto | |
| final TextEditingController _direccionCtrl = TextEditingController(); | |
| final TextEditingController _emailCtrl = TextEditingController(); | |
| String _preferenciaComunicacion = 'Email'; | |
| String _completePhoneNumber = ''; // Stores full phone with country code | |
| // Location fields | |
| final TextEditingController _idprovinciaController = TextEditingController(text: '0'); | |
| final TextEditingController _iddistritoController = TextEditingController(text: '0'); | |
| final TextEditingController _idcorregimientoController = TextEditingController(text: '0'); | |
| final MyCustomController _nacionalidadController = MyCustomController(); | |
| // Step 3: Información Profesional | |
| final TextEditingController _empresaCtrl = TextEditingController(); | |
| final TextEditingController _puestoCtrl = TextEditingController(); | |
| DateTime? _fechaIngreso; | |
| final TextEditingController _antiguedadCtrl = TextEditingController(); | |
| final TextEditingController _salarioCtrl = TextEditingController(); | |
| final TextEditingController _centroCostoCtrl = TextEditingController(); | |
| // Step 4: Perfil de Cliente | |
| String _perfilRiesgo = 'Bajo'; | |
| int _rankingCliente = 4; | |
| DateTime _fechaRegistro = DateTime.now(); | |
| final List<String> _stepTitles = ['Información', 'Contacto', 'Profesional', 'Perfil']; | |
| @override | |
| void dispose() { | |
| _pageController.dispose(); | |
| _nombreCtrl.dispose(); | |
| _apellidoCtrl.dispose(); | |
| _ocupacionCtrl.dispose(); | |
| _numeroIDCtrl.dispose(); | |
| _numeroIDInternoCtrl.dispose(); | |
| _emailCtrl.dispose(); | |
| _direccionCtrl.dispose(); | |
| _empresaCtrl.dispose(); | |
| _puestoCtrl.dispose(); | |
| _antiguedadCtrl.dispose(); | |
| _salarioCtrl.dispose(); | |
| _centroCostoCtrl.dispose(); | |
| // Dispose location controllers | |
| _idprovinciaController.dispose(); | |
| _iddistritoController.dispose(); | |
| _idcorregimientoController.dispose(); | |
| _nacionalidadController.dispose(); | |
| super.dispose(); | |
| } | |
| void _nextStep() { | |
| if (_currentStep < 3) { | |
| _pageController.nextPage( | |
| duration: const Duration(milliseconds: 300), | |
| curve: Curves.easeInOut, | |
| ); | |
| } | |
| } | |
| void _prevStep() { | |
| if (_currentStep > 0) { | |
| _pageController.previousPage( | |
| duration: const Duration(milliseconds: 300), | |
| curve: Curves.easeInOut, | |
| ); | |
| } else { | |
| Navigator.of(context).pop(); | |
| } | |
| } | |
| Future<void> _selectDate(BuildContext context, DateTime? current, ValueChanged<DateTime> onSelected) async { | |
| final picked = await showDatePicker( | |
| context: context, | |
| initialDate: current ?? DateTime.now(), | |
| firstDate: DateTime(1900), | |
| lastDate: DateTime(2100), | |
| ); | |
| if (picked != null) onSelected(picked); | |
| } | |
| InputDecoration _inputDecoration(String label) { | |
| return InputDecoration( | |
| labelText: label, | |
| filled: true, | |
| fillColor: Theme.of(context).cardColor, | |
| border: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: BorderSide(color: Theme.of(context).dividerColor), | |
| ), | |
| enabledBorder: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: BorderSide(color: Theme.of(context).dividerColor), | |
| ), | |
| focusedBorder: OutlineInputBorder( | |
| borderRadius: BorderRadius.circular(12), | |
| borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2), | |
| ), | |
| contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), | |
| ); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final theme = Theme.of(context); | |
| final screenWidth = MediaQuery.of(context).size.width; | |
| final dialogWidth = screenWidth > 600 ? 450.0 : screenWidth * 0.9; | |
| return Dialog( | |
| backgroundColor: theme.scaffoldBackgroundColor, | |
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), | |
| insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), | |
| child: SizedBox( | |
| width: dialogWidth, | |
| height: MediaQuery.of(context).size.height * 0.75, | |
| child: Column( | |
| children: [ | |
| // Header | |
| Container( | |
| padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), | |
| decoration: BoxDecoration( | |
| border: Border(bottom: BorderSide(color: theme.dividerColor)), | |
| ), | |
| child: Row( | |
| children: [ | |
| IconButton( | |
| icon: const Icon(Icons.arrow_back_ios, size: 18), | |
| onPressed: _prevStep, | |
| ), | |
| Expanded( | |
| child: Text( | |
| 'Nuevo Cliente', | |
| textAlign: TextAlign.center, | |
| style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold), | |
| ), | |
| ), | |
| const SizedBox(width: 48), | |
| ], | |
| ), | |
| ), | |
| // Stepper Indicator | |
| Padding( | |
| padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), | |
| child: Row( | |
| children: List.generate(4, (index) { | |
| final isActive = index <= _currentStep; | |
| final isCurrent = index == _currentStep; | |
| return Expanded( | |
| child: Column( | |
| children: [ | |
| Container( | |
| height: 3, | |
| margin: const EdgeInsets.symmetric(horizontal: 2), | |
| decoration: BoxDecoration( | |
| color: isActive ? theme.colorScheme.primary : theme.dividerColor, | |
| borderRadius: BorderRadius.circular(2), | |
| ), | |
| ), | |
| const SizedBox(height: 6), | |
| Text( | |
| _stepTitles[index], | |
| style: TextStyle( | |
| fontSize: 10, | |
| fontWeight: isCurrent ? FontWeight.bold : FontWeight.w500, | |
| color: isCurrent ? theme.colorScheme.primary : theme.hintColor, | |
| letterSpacing: 0.5, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| }), | |
| ), | |
| ), | |
| // Form Content | |
| Expanded( | |
| child: Form( | |
| key: _formKey, | |
| child: PageView( | |
| controller: _pageController, | |
| physics: const NeverScrollableScrollPhysics(), | |
| onPageChanged: (index) => setState(() => _currentStep = index), | |
| children: [ | |
| _buildStep1(), | |
| _buildStep2(), | |
| _buildStep3(), | |
| _buildStep4(), | |
| ], | |
| ), | |
| ), | |
| ), | |
| // Footer Navigation | |
| Container( | |
| padding: const EdgeInsets.all(12), | |
| decoration: BoxDecoration( | |
| border: Border(top: BorderSide(color: theme.dividerColor)), | |
| ), | |
| child: Row( | |
| children: [ | |
| if (_currentStep > 0) | |
| Expanded( | |
| child: OutlinedButton( | |
| onPressed: _prevStep, | |
| style: OutlinedButton.styleFrom( | |
| padding: const EdgeInsets.symmetric(vertical: 12), | |
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), | |
| ), | |
| child: const Text('Anterior', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13)), | |
| ), | |
| ), | |
| if (_currentStep > 0) const SizedBox(width: 12), | |
| Expanded( | |
| flex: _currentStep == 0 ? 1 : 2, | |
| child: ElevatedButton( | |
| onPressed: _isSaving ? null : (_currentStep == 3 ? _guardarCliente : _nextStep), | |
| style: ElevatedButton.styleFrom( | |
| padding: const EdgeInsets.symmetric(vertical: 12), | |
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), | |
| ), | |
| child: _isSaving | |
| ? const SizedBox( | |
| width: 16, | |
| height: 16, | |
| child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), | |
| ) | |
| : Row( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| Text( | |
| _currentStep == 3 ? 'Finalizar' : 'Siguiente', | |
| style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13), | |
| ), | |
| if (_currentStep < 3) ...[ | |
| const SizedBox(width: 6), | |
| const Icon(Icons.arrow_forward, size: 16), | |
| ], | |
| ], | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| // ─── Step 1: Información Personal ─── | |
| Widget _buildStep1() { | |
| return ListView( | |
| padding: const EdgeInsets.all(16), | |
| children: [ | |
| Text( | |
| 'Información Personal', | |
| style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), | |
| ), | |
| const SizedBox(height: 4), | |
| Text( | |
| 'Paso 1 de 4: Datos de identificación básica.', | |
| style: TextStyle(color: Theme.of(context).hintColor, fontSize: 13), | |
| ), | |
| const SizedBox(height: 24), | |
| // Tipo de Identificación | |
| DropdownButtonFormField<String>( | |
| value: _tipoDocumento, | |
| decoration: _inputDecoration('Tipo de Identificación'), | |
| items: ['DNI / Cédula', 'Pasaporte', 'Licencia de Conducir'] | |
| .map((e) => DropdownMenuItem(value: e, child: Text(e))) | |
| .toList(), | |
| onChanged: (v) => setState(() => _tipoDocumento = v!), | |
| ), | |
| const SizedBox(height: 16), | |
| // Número de Identificación | |
| TextFormField( | |
| controller: _numeroIDCtrl, | |
| decoration: _inputDecoration('Número de Identificación'), | |
| keyboardType: TextInputType.number, | |
| inputFormatters: [FilteringTextInputFormatter.digitsOnly], | |
| ), | |
| const SizedBox(height: 16), | |
| // ID Interno | |
| TextFormField( | |
| controller: _numeroIDInternoCtrl, | |
| decoration: _inputDecoration('Número ID Interno'), | |
| ), | |
| const Divider(height: 32), | |
| // Nombres | |
| TextFormField( | |
| controller: _nombreCtrl, | |
| decoration: _inputDecoration('Nombre(s)'), | |
| validator: (v) => v == null || v.isEmpty ? 'Requerido' : null, | |
| ), | |
| const SizedBox(height: 16), | |
| // Apellidos | |
| TextFormField( | |
| controller: _apellidoCtrl, | |
| decoration: _inputDecoration('Apellido(s)'), | |
| validator: (v) => v == null || v.isEmpty ? 'Requerido' : null, | |
| ), | |
| const SizedBox(height: 16), | |
| // Sexo & Fecha Nacimiento | |
| Row( | |
| children: [ | |
| Expanded( | |
| child: DropdownButtonFormField<String>( | |
| value: _sexo, | |
| decoration: _inputDecoration('Sexo'), | |
| items: ['Masculino', 'Femenino', 'Otro'] | |
| .map((e) => DropdownMenuItem(value: e, child: Text(e))) | |
| .toList(), | |
| onChanged: (v) => setState(() => _sexo = v!), | |
| ), | |
| ), | |
| const SizedBox(width: 12), | |
| Expanded( | |
| child: TextFormField( | |
| readOnly: true, | |
| decoration: _inputDecoration('Fecha Nacimiento'), | |
| controller: TextEditingController( | |
| text: _fechaNacimiento != null ? DateFormat('yyyy-MM-dd').format(_fechaNacimiento!) : '', | |
| ), | |
| onTap: () => _selectDate(context, _fechaNacimiento, (d) => setState(() => _fechaNacimiento = d)), | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 16), | |
| // Estado Civil | |
| DropdownButtonFormField<String>( | |
| value: _estadoCivil, | |
| decoration: _inputDecoration('Estado Civil'), | |
| items: ['Soltero/a', 'Casado/a', 'Divorciado/a', 'Viudo/a', 'Unión Libre'] | |
| .map((e) => DropdownMenuItem(value: e, child: Text(e))) | |
| .toList(), | |
| onChanged: (v) => setState(() => _estadoCivil = v!), | |
| ), | |
| const SizedBox(height: 16), | |
| // Ocupación | |
| TextFormField( | |
| controller: _ocupacionCtrl, | |
| decoration: _inputDecoration('Ocupación'), | |
| ), | |
| const SizedBox(height: 24), | |
| ], | |
| ); | |
| } | |
| // ─── Step 2: Contacto ─── | |
| Widget _buildStep2() { | |
| return ListView( | |
| padding: const EdgeInsets.all(16), | |
| children: [ | |
| Text( | |
| 'Información de Contacto', | |
| style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), | |
| ), | |
| const SizedBox(height: 4), | |
| Text( | |
| 'Paso 2 de 4: Datos de contacto del cliente.', | |
| style: TextStyle(color: Theme.of(context).hintColor, fontSize: 13), | |
| ), | |
| const SizedBox(height: 24), | |
| // Dirección | |
| TextFormField( | |
| controller: _direccionCtrl, | |
| decoration: _inputDecoration('Dirección'), | |
| ), | |
| const SizedBox(height: 16), | |
| // Teléfono con selector de código de área (todos los países) | |
| Container( | |
| child: IntlPhoneField( | |
| decoration: _inputDecoration('Teléfono'), | |
| initialCountryCode: 'PA', // Panama default | |
| disableLengthCheck: true, | |
| showCountryFlag: false, | |
| dropdownTextStyle: TextStyle(fontSize: 12), | |
| pickerDialogStyle: PickerDialogStyle( | |
| width: 350, | |
| countryNameStyle: TextStyle(fontSize: 14), | |
| countryCodeStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), | |
| padding: EdgeInsets.all(8), | |
| listTilePadding: EdgeInsets.symmetric(horizontal: 8, vertical: 0), | |
| searchFieldInputDecoration: InputDecoration( | |
| suffixIcon: const Icon(Icons.search), | |
| labelText: 'Buscar país', | |
| ), | |
| ), | |
| onChanged: (phone) { | |
| _completePhoneNumber = phone.completeNumber; | |
| }, | |
| onSaved: (phone) { | |
| if (phone != null) { | |
| _completePhoneNumber = phone.completeNumber; | |
| } | |
| }, | |
| ), | |
| ), | |
| const SizedBox(height: 16), | |
| TextFormField( | |
| controller: _emailCtrl, | |
| decoration: _inputDecoration('Correo Electrónico'), | |
| keyboardType: TextInputType.emailAddress, | |
| ), | |
| const SizedBox(height: 16), | |
| // Ubicación (Provincia, Distrito, Corregimiento) | |
| Text( | |
| 'Ubicación', | |
| style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Theme.of(context).hintColor), | |
| ), | |
| const SizedBox(height: 12), | |
| ubicacionwidget( | |
| provController: _idprovinciaController, | |
| distController: _iddistritoController, | |
| corrController: _idcorregimientoController, | |
| idempleado: 0, | |
| ), | |
| const SizedBox(height: 16), | |
| // Nacionalidad | |
| Text( | |
| 'Nacionalidad/País de Residencia:', | |
| style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Theme.of(context).hintColor), | |
| ), | |
| const SizedBox(height: 8), | |
| autofield2( | |
| etiqueta: 'Nacionalidad', | |
| urllink: Constants.urlsselectorpais, | |
| dataController: _nacionalidadController, | |
| idcampo: 'idpais', | |
| nombrecampo: 'nombre', | |
| usadatosgenericos: false, | |
| ), | |
| const SizedBox(height: 16), | |
| // Preferencia de Comunicación | |
| Text( | |
| 'Preferencia de Comunicación', | |
| style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Theme.of(context).hintColor), | |
| ), | |
| const SizedBox(height: 12), | |
| Row( | |
| children: [ | |
| _commPrefButton('Email', Icons.mail), | |
| const SizedBox(width: 8), | |
| _commPrefButton('Llamada Telefónica', Icons.call), | |
| const SizedBox(width: 8), | |
| _commPrefButton('WhatsApp', Icons.chat), | |
| ], | |
| ), | |
| const SizedBox(height: 24), | |
| ], | |
| ); | |
| } | |
| Widget _commPrefButton(String label, IconData icon) { | |
| final isSelected = _preferenciaComunicacion == label; | |
| final theme = Theme.of(context); | |
| return Expanded( | |
| child: InkWell( | |
| onTap: () => setState(() => _preferenciaComunicacion = label), | |
| borderRadius: BorderRadius.circular(12), | |
| child: Container( | |
| padding: const EdgeInsets.symmetric(vertical: 16), | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(12), | |
| border: Border.all( | |
| color: isSelected ? theme.colorScheme.primary : theme.dividerColor, | |
| width: isSelected ? 2 : 1, | |
| ), | |
| color: isSelected ? theme.colorScheme.primary.withOpacity(0.1) : theme.cardColor, | |
| ), | |
| child: Column( | |
| children: [ | |
| Icon(icon, size: 24, color: isSelected ? theme.colorScheme.primary : theme.hintColor), | |
| const SizedBox(height: 4), | |
| Text( | |
| label.split(' ').first, | |
| style: TextStyle( | |
| fontSize: 10, | |
| fontWeight: FontWeight.bold, | |
| color: isSelected ? theme.colorScheme.primary : theme.hintColor, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| // ─── Step 3: Información Profesional ─── | |
| Widget _buildStep3() { | |
| return ListView( | |
| padding: const EdgeInsets.all(16), | |
| children: [ | |
| Text( | |
| 'Información Profesional', | |
| style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), | |
| ), | |
| const SizedBox(height: 4), | |
| Text( | |
| 'Paso 3 de 4: Detalles laborales del cliente.', | |
| style: TextStyle(color: Theme.of(context).hintColor, fontSize: 13), | |
| ), | |
| const SizedBox(height: 24), | |
| // Empresa | |
| TextFormField( | |
| controller: _empresaCtrl, | |
| decoration: _inputDecoration('Empresa'), | |
| ), | |
| const SizedBox(height: 16), | |
| // Puesto | |
| TextFormField( | |
| controller: _puestoCtrl, | |
| decoration: _inputDecoration('Puesto / Cargo'), | |
| ), | |
| const SizedBox(height: 16), | |
| // Fecha Ingreso & Antigüedad | |
| Row( | |
| children: [ | |
| Expanded( | |
| child: TextFormField( | |
| readOnly: true, | |
| decoration: _inputDecoration('Fecha de Ingreso'), | |
| controller: TextEditingController( | |
| text: _fechaIngreso != null ? DateFormat('yyyy-MM-dd').format(_fechaIngreso!) : '', | |
| ), | |
| onTap: () => _selectDate(context, _fechaIngreso, (d) => setState(() => _fechaIngreso = d)), | |
| ), | |
| ), | |
| const SizedBox(width: 12), | |
| Expanded( | |
| child: TextFormField( | |
| controller: _antiguedadCtrl, | |
| decoration: _inputDecoration('Antigüedad (Años)'), | |
| keyboardType: TextInputType.number, | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 16), | |
| // Salario | |
| TextFormField( | |
| controller: _salarioCtrl, | |
| decoration: _inputDecoration('Salario Mensual').copyWith(prefixText: '\$ '), | |
| keyboardType: TextInputType.number, | |
| ), | |
| const SizedBox(height: 16), | |
| // Centro de Costo | |
| TextFormField( | |
| controller: _centroCostoCtrl, | |
| decoration: _inputDecoration('Centro de Costo (ID)'), | |
| ), | |
| const SizedBox(height: 24), | |
| ], | |
| ); | |
| } | |
| // ─── Step 4: Perfil de Cliente ─── | |
| Widget _buildStep4() { | |
| return ListView( | |
| padding: const EdgeInsets.all(16), | |
| children: [ | |
| Text( | |
| 'Perfil y Clasificación', | |
| style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), | |
| ), | |
| const SizedBox(height: 4), | |
| Text( | |
| 'Paso 4 de 4: Define el nivel de riesgo y categoría comercial.', | |
| style: TextStyle(color: Theme.of(context).hintColor, fontSize: 13), | |
| ), | |
| const SizedBox(height: 24), | |
| // Perfil de Riesgo | |
| DropdownButtonFormField<String>( | |
| value: _perfilRiesgo, | |
| decoration: _inputDecoration('Perfil de Riesgo'), | |
| items: ['Bajo', 'Medio', 'Alto', 'Muy Alto / PEP'] | |
| .map((e) => DropdownMenuItem(value: e, child: Text(e))) | |
| .toList(), | |
| onChanged: (v) => setState(() => _perfilRiesgo = v!), | |
| ), | |
| const SizedBox(height: 16), | |
| // Ranking del Cliente | |
| DropdownButtonFormField<int>( | |
| value: _rankingCliente, | |
| decoration: _inputDecoration('Ranking del Cliente'), | |
| items: const [ | |
| DropdownMenuItem(value: 5, child: Text('Platinum (Prioridad A+)')), | |
| DropdownMenuItem(value: 4, child: Text('Gold (Prioridad A)')), | |
| DropdownMenuItem(value: 3, child: Text('Silver (Prioridad B)')), | |
| DropdownMenuItem(value: 2, child: Text('Bronze (Prioridad C)')), | |
| DropdownMenuItem(value: 1, child: Text('Basic')), | |
| ], | |
| onChanged: (v) => setState(() => _rankingCliente = v!), | |
| ), | |
| const SizedBox(height: 16), | |
| // Fecha de Registro | |
| TextFormField( | |
| readOnly: true, | |
| decoration: _inputDecoration('Fecha de Registro'), | |
| controller: TextEditingController(text: DateFormat('yyyy-MM-dd').format(_fechaRegistro)), | |
| onTap: () => _selectDate(context, _fechaRegistro, (d) => setState(() => _fechaRegistro = d)), | |
| ), | |
| const SizedBox(height: 8), | |
| Text( | |
| 'Esta fecha se utilizará para el inicio del ciclo de facturación.', | |
| style: TextStyle(fontSize: 11, color: Theme.of(context).hintColor), | |
| ), | |
| const SizedBox(height: 24), | |
| ], | |
| ); | |
| } | |
| Future<void> _guardarCliente() async { | |
| if (!_formKey.currentState!.validate()) return; | |
| setState(() => _isSaving = true); | |
| try { | |
| final body = { | |
| 'TipoID': _tipoDocumento, | |
| 'NumeroID': _numeroIDCtrl.text, | |
| 'NumeroIDInterno': _numeroIDInternoCtrl.text, | |
| 'Nombre': _nombreCtrl.text, | |
| 'Apellido': _apellidoCtrl.text, | |
| 'Direccion': _direccionCtrl.text, | |
| 'Telefono': _completePhoneNumber, | |
| 'Email': _emailCtrl.text, | |
| 'FechaRegistro': _fechaRegistro.toIso8601String(), | |
| 'Sexo': _sexo, | |
| 'FechaNacimiento': (_fechaNacimiento ?? DateTime.now()).toIso8601String(), | |
| 'idprovincia': _idprovinciaController.text.isEmpty ? 0 : int.tryParse(_idprovinciaController.text) ?? 0, | |
| 'iddistrito': _iddistritoController.text.isEmpty ? 0 : int.tryParse(_iddistritoController.text) ?? 0, | |
| 'idcorregimiento': _idcorregimientoController.text.isEmpty ? 0 : int.tryParse(_idcorregimientoController.text) ?? 0, | |
| 'idpais': _nacionalidadController.valorLockup?.toString().isNotEmpty == true | |
| ? int.tryParse(_nacionalidadController.valorLockup.toString()) ?? 1 | |
| : 1, // Default to Panama if empty | |
| 'Ocupacion': _ocupacionCtrl.text, | |
| 'EstadoCivil': _estadoCivil, | |
| 'PreferenciaComunicacion': _preferenciaComunicacion, | |
| 'PerfilRiesgo': _perfilRiesgo, | |
| 'RankingCliente': _rankingCliente, | |
| 'Salario': double.tryParse(_salarioCtrl.text) ?? 0, | |
| 'Antiguedad': int.tryParse(_antiguedadCtrl.text) ?? 0, | |
| 'Empresa': _empresaCtrl.text, | |
| 'Puesto': _puestoCtrl.text, | |
| 'FechaIngreso': (_fechaIngreso ?? DateTime.now()).toIso8601String(), | |
| 'idCentroCosto': _centroCostoCtrl.text, | |
| 'CodigoPostal': '', | |
| 'ObjetivosFinancieros': '', | |
| 'CampoDato1': '', | |
| 'CampoDato2': '', | |
| 'CampoDato3': '', | |
| 'CampoDato4': '', | |
| 'CampoDato5': '', | |
| 'CampoDato6': '', | |
| 'CampoDato7': '', | |
| 'CampoDato9': '', | |
| }; | |
| final String apiUrl = '${Constants.serverapp}/clientes/'; | |
| final token = await AuthService.getToken(); | |
| final response = await http.post( | |
| Uri.parse(apiUrl), | |
| headers: { | |
| 'Content-Type': 'application/json; charset=UTF-8', | |
| 'Authorization': 'Bearer $token', | |
| }, | |
| body: jsonEncode(body), | |
| ); | |
| if (response.statusCode == 200 || response.statusCode == 201) { | |
| if (mounted) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar(content: Text('Cliente registrado correctamente'), backgroundColor: Colors.green), | |
| ); | |
| Navigator.of(context).pop(true); | |
| } | |
| } else { | |
| final msg = jsonDecode(response.body)['message'] ?? 'Error desconocido'; | |
| if (mounted) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| SnackBar(content: Text('Error: $msg'), backgroundColor: Colors.red), | |
| ); | |
| } | |
| } | |
| } catch (e) { | |
| if (mounted) { | |
| ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: $e'))); | |
| } | |
| } finally { | |
| if (mounted) setState(() => _isSaving = false); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment