Skip to content

Instantly share code, notes, and snippets.

@tatd3v
Created February 9, 2026 16:38
Show Gist options
  • Select an option

  • Save tatd3v/ab10e61d7e1700b154593bdd81511582 to your computer and use it in GitHub Desktop.

Select an option

Save tatd3v/ab10e61d7e1700b154593bdd81511582 to your computer and use it in GitHub Desktop.
1Step-add-client-form
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