Created
February 9, 2026 16:38
-
-
Save tatd3v/ab10e61d7e1700b154593bdd81511582 to your computer and use it in GitHub Desktop.
1Step-add-client-form
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:flutter/material.dart'; | |
| import 'package:flutter/services.dart'; | |
| import 'package:http/http.dart' as http; | |
| import 'package:intl/intl.dart'; | |
| class ClientAddDialog extends StatefulWidget { | |
| const ClientAddDialog({super.key}); | |
| @override | |
| State<ClientAddDialog> createState() => _ClientAddDialogState(); | |
| } | |
| class _ClientAddDialogState extends State<ClientAddDialog> { | |
| final _formKey = GlobalKey<FormState>(); | |
| bool _isSaving = false; | |
| // Información Personal | |
| final TextEditingController _nombreCtrl = TextEditingController(); | |
| final TextEditingController _apellidoCtrl = TextEditingController(); | |
| String _sexo = 'Masculino'; | |
| DateTime? _fechaNacimiento; | |
| String _estadoCivil = 'Soltero/a'; | |
| final TextEditingController _ocupacionCtrl = TextEditingController(); | |
| // Identificación | |
| String _tipoDocumento = 'DNI / Cédula'; | |
| final TextEditingController _numeroIDCtrl = TextEditingController(); | |
| final TextEditingController _numeroIDInternoCtrl = TextEditingController(); | |
| // Contacto | |
| final TextEditingController _emailCtrl = TextEditingController(); | |
| final TextEditingController _telefonoCtrl = TextEditingController(); | |
| final TextEditingController _direccionCtrl = TextEditingController(); | |
| final TextEditingController _ciudadCtrl = TextEditingController(); | |
| String _pais = 'Panamá'; | |
| String _preferenciaComunicacion = 'Email'; | |
| // 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(); | |
| // Perfil de Cliente | |
| String _perfilRiesgo = 'Bajo'; | |
| int _rankingCliente = 4; | |
| DateTime _fechaRegistro = DateTime.now(); | |
| @override | |
| void dispose() { | |
| _nombreCtrl.dispose(); | |
| _apellidoCtrl.dispose(); | |
| _ocupacionCtrl.dispose(); | |
| _numeroIDCtrl.dispose(); | |
| _numeroIDInternoCtrl.dispose(); | |
| _emailCtrl.dispose(); | |
| _telefonoCtrl.dispose(); | |
| _direccionCtrl.dispose(); | |
| _ciudadCtrl.dispose(); | |
| _empresaCtrl.dispose(); | |
| _puestoCtrl.dispose(); | |
| _antiguedadCtrl.dispose(); | |
| _salarioCtrl.dispose(); | |
| _centroCostoCtrl.dispose(); | |
| super.dispose(); | |
| } | |
| InputDecoration _inputDecoration(String label, {IconData? prefixIcon, bool readOnly = false}) { | |
| return InputDecoration( | |
| labelText: label, | |
| isDense: true, | |
| prefixIcon: prefixIcon != null ? Icon(prefixIcon, size: 20) : null, | |
| border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), | |
| filled: true, | |
| fillColor: readOnly | |
| ? Theme.of(context).colorScheme.surfaceContainerHighest | |
| : Theme.of(context).inputDecorationTheme.fillColor, | |
| contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), | |
| ); | |
| } | |
| Widget _sectionHeader(String title) { | |
| return Padding( | |
| padding: const EdgeInsets.only(left: 4, bottom: 4, top: 8), | |
| child: Text( | |
| title.toUpperCase(), | |
| style: TextStyle( | |
| fontSize: 12, | |
| fontWeight: FontWeight.w600, | |
| color: Theme.of(context).colorScheme.primary, | |
| letterSpacing: 1.2, | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _sectionCard(List<Widget> children) { | |
| return Card( | |
| elevation: 1, | |
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), | |
| child: Padding( | |
| padding: const EdgeInsets.all(16), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: children, | |
| ), | |
| ), | |
| ); | |
| } | |
| 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); | |
| } | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| final screenWidth = MediaQuery.of(context).size.width; | |
| final dialogWidth = screenWidth > 900 ? 850.0 : screenWidth * 0.9; | |
| return Dialog( | |
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), | |
| child: SizedBox( | |
| width: dialogWidth, | |
| height: MediaQuery.of(context).size.height * 0.85, | |
| child: Column( | |
| children: [ | |
| // Header | |
| Container( | |
| padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), | |
| decoration: BoxDecoration( | |
| border: Border(bottom: BorderSide(color: Theme.of(context).dividerColor)), | |
| ), | |
| child: Row( | |
| children: [ | |
| IconButton( | |
| icon: const Icon(Icons.arrow_back), | |
| onPressed: () => Navigator.of(context).pop(), | |
| tooltip: 'Volver', | |
| ), | |
| const Expanded( | |
| child: Text( | |
| 'Nuevo Cliente', | |
| textAlign: TextAlign.center, | |
| style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), | |
| ), | |
| ), | |
| TextButton( | |
| onPressed: _isSaving ? null : _guardarCliente, | |
| child: const Text('Guardar', style: TextStyle(fontWeight: FontWeight.w600)), | |
| ), | |
| ], | |
| ), | |
| ), | |
| // Form Body | |
| Expanded( | |
| child: Form( | |
| key: _formKey, | |
| child: ListView( | |
| padding: const EdgeInsets.all(16), | |
| children: [ | |
| // ─── Información Personal ─── | |
| _sectionHeader('Información Personal'), | |
| _sectionCard([ | |
| Row( | |
| children: [ | |
| Expanded( | |
| child: TextFormField( | |
| controller: _nombreCtrl, | |
| decoration: _inputDecoration('Nombre'), | |
| validator: (v) => v == null || v.isEmpty ? 'Requerido' : null, | |
| ), | |
| ), | |
| const SizedBox(width: 12), | |
| Expanded( | |
| child: TextFormField( | |
| controller: _apellidoCtrl, | |
| decoration: _inputDecoration('Apellido'), | |
| validator: (v) => v == null || v.isEmpty ? 'Requerido' : null, | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 12), | |
| 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('Nacimiento'), | |
| controller: TextEditingController( | |
| text: _fechaNacimiento != null | |
| ? DateFormat('yyyy-MM-dd').format(_fechaNacimiento!) | |
| : '', | |
| ), | |
| onTap: () => _selectDate(context, _fechaNacimiento, (d) { | |
| setState(() => _fechaNacimiento = d); | |
| }), | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 12), | |
| DropdownButtonFormField<String>( | |
| value: _estadoCivil, | |
| decoration: _inputDecoration('Estado Civil'), | |
| items: ['Soltero/a', 'Casado/a', 'Divorciado/a', 'Viudo/a'] | |
| .map((e) => DropdownMenuItem(value: e, child: Text(e))) | |
| .toList(), | |
| onChanged: (v) => setState(() => _estadoCivil = v!), | |
| ), | |
| const SizedBox(height: 12), | |
| TextFormField( | |
| controller: _ocupacionCtrl, | |
| decoration: _inputDecoration('Ocupación'), | |
| ), | |
| ]), | |
| const SizedBox(height: 16), | |
| // ─── Identificación ─── | |
| _sectionHeader('Identificación'), | |
| _sectionCard([ | |
| DropdownButtonFormField<String>( | |
| value: _tipoDocumento, | |
| decoration: _inputDecoration('Tipo de Documento', prefixIcon: Icons.badge), | |
| 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: 12), | |
| TextFormField( | |
| controller: _numeroIDCtrl, | |
| decoration: _inputDecoration('Número de Documento'), | |
| keyboardType: TextInputType.number, | |
| inputFormatters: [FilteringTextInputFormatter.digitsOnly], | |
| ), | |
| const SizedBox(height: 12), | |
| TextFormField( | |
| controller: _numeroIDInternoCtrl, | |
| decoration: _inputDecoration('ID Interno (Sistema)', prefixIcon: Icons.key), | |
| ), | |
| ]), | |
| const SizedBox(height: 16), | |
| // ─── Contacto ─── | |
| _sectionHeader('Contacto'), | |
| _sectionCard([ | |
| TextFormField( | |
| controller: _emailCtrl, | |
| decoration: _inputDecoration('Correo Electrónico', prefixIcon: Icons.mail), | |
| keyboardType: TextInputType.emailAddress, | |
| ), | |
| const SizedBox(height: 12), | |
| TextFormField( | |
| controller: _telefonoCtrl, | |
| decoration: _inputDecoration('Teléfono +507', prefixIcon: Icons.call), | |
| keyboardType: TextInputType.phone, | |
| ), | |
| const SizedBox(height: 12), | |
| TextFormField( | |
| controller: _direccionCtrl, | |
| decoration: _inputDecoration('Dirección'), | |
| ), | |
| const SizedBox(height: 12), | |
| Row( | |
| children: [ | |
| Expanded( | |
| child: TextFormField( | |
| controller: _ciudadCtrl, | |
| decoration: _inputDecoration('Ciudad'), | |
| ), | |
| ), | |
| const SizedBox(width: 12), | |
| Expanded( | |
| child: DropdownButtonFormField<String>( | |
| value: _pais, | |
| decoration: _inputDecoration('País'), | |
| items: ['Panamá', 'España', 'México', 'Argentina', 'Colombia', 'USA'] | |
| .map((e) => DropdownMenuItem(value: e, child: Text(e))) | |
| .toList(), | |
| onChanged: (v) => setState(() => _pais = v!), | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 12), | |
| DropdownButtonFormField<String>( | |
| value: _preferenciaComunicacion, | |
| decoration: _inputDecoration('Preferencia Comunicación'), | |
| items: ['Email', 'Llamada Telefónica', 'WhatsApp'] | |
| .map((e) => DropdownMenuItem(value: e, child: Text(e))) | |
| .toList(), | |
| onChanged: (v) => setState(() => _preferenciaComunicacion = v!), | |
| ), | |
| ]), | |
| const SizedBox(height: 16), | |
| // ─── Información Profesional ─── | |
| _sectionHeader('Información Profesional'), | |
| _sectionCard([ | |
| TextFormField( | |
| controller: _empresaCtrl, | |
| decoration: _inputDecoration('Empresa', prefixIcon: Icons.domain), | |
| ), | |
| const SizedBox(height: 12), | |
| TextFormField( | |
| controller: _puestoCtrl, | |
| decoration: _inputDecoration('Puesto / Cargo'), | |
| ), | |
| const SizedBox(height: 12), | |
| Row( | |
| children: [ | |
| Expanded( | |
| child: TextFormField( | |
| readOnly: true, | |
| decoration: _inputDecoration('Fecha 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: 12), | |
| Row( | |
| children: [ | |
| Expanded( | |
| child: TextFormField( | |
| controller: _salarioCtrl, | |
| decoration: _inputDecoration('Salario Anual').copyWith( | |
| prefixText: '\$ ', | |
| ), | |
| keyboardType: TextInputType.number, | |
| ), | |
| ), | |
| const SizedBox(width: 12), | |
| Expanded( | |
| child: TextFormField( | |
| controller: _centroCostoCtrl, | |
| decoration: _inputDecoration('ID Centro Costo'), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ]), | |
| const SizedBox(height: 16), | |
| // ─── Perfil de Cliente ─── | |
| _sectionHeader('Perfil de Cliente'), | |
| _sectionCard([ | |
| const Text('Perfil de Riesgo', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w500)), | |
| const SizedBox(height: 8), | |
| Row( | |
| children: [ | |
| _riskButton('Bajo', Colors.green), | |
| const SizedBox(width: 8), | |
| _riskButton('Medio', Colors.orange), | |
| const SizedBox(width: 8), | |
| _riskButton('Alto', Colors.red), | |
| ], | |
| ), | |
| const SizedBox(height: 12), | |
| DropdownButtonFormField<int>( | |
| value: _rankingCliente, | |
| decoration: _inputDecoration('Ranking Cliente', prefixIcon: Icons.star), | |
| items: [ | |
| DropdownMenuItem(value: 5, child: Text('Platinum Tier')), | |
| DropdownMenuItem(value: 4, child: Text('Gold Tier')), | |
| DropdownMenuItem(value: 3, child: Text('Silver Tier')), | |
| DropdownMenuItem(value: 2, child: Text('Bronze Tier')), | |
| DropdownMenuItem(value: 1, child: Text('Basic Tier')), | |
| ], | |
| onChanged: (v) => setState(() => _rankingCliente = v!), | |
| ), | |
| const SizedBox(height: 12), | |
| 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: 24), | |
| // ─── Guardar Button ─── | |
| SizedBox( | |
| height: 48, | |
| child: ElevatedButton.icon( | |
| icon: _isSaving | |
| ? const SizedBox( | |
| width: 20, height: 20, | |
| child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) | |
| : const Icon(Icons.save), | |
| label: Text(_isSaving ? 'Guardando...' : 'Guardar Cliente'), | |
| style: ElevatedButton.styleFrom( | |
| shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), | |
| ), | |
| onPressed: _isSaving ? null : _guardarCliente, | |
| ), | |
| ), | |
| const SizedBox(height: 16), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _riskButton(String label, Color color) { | |
| final isSelected = _perfilRiesgo == label; | |
| return Expanded( | |
| child: InkWell( | |
| onTap: () => setState(() => _perfilRiesgo = label), | |
| borderRadius: BorderRadius.circular(8), | |
| child: Container( | |
| height: 48, | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(8), | |
| border: Border.all( | |
| color: isSelected ? color : Theme.of(context).dividerColor, | |
| width: isSelected ? 2 : 1, | |
| ), | |
| color: isSelected ? color.withOpacity(0.1) : null, | |
| ), | |
| child: Center( | |
| child: Text( | |
| label, | |
| style: TextStyle( | |
| fontWeight: FontWeight.w600, | |
| color: isSelected ? color : null, | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| 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': _telefonoCtrl.text.replaceAll('+', ''), | |
| 'Email': _emailCtrl.text, | |
| 'FechaRegistro': _fechaRegistro.toIso8601String(), | |
| 'Sexo': _sexo, | |
| 'FechaNacimiento': (_fechaNacimiento ?? DateTime.now()).toIso8601String(), | |
| 'Ciudad': _ciudadCtrl.text, | |
| 'Pais': _pais, | |
| '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, | |
| // Fields required by backend but not in the form | |
| '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