Files
2026-09-21 16:33:51 -04:00

488 lines
20 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
import 'package:toggle_switch/toggle_switch.dart';
import '../../../../core/theme/app_colors.dart';
import '../store/patient_store.dart';
class PatientDemographicsForm extends StatelessWidget {
/// Altura estricta y compartida para todos los elementos de la ficha demográfica:
/// los 8 campos de texto, las 3 secciones de toggle (sexo, seguro, estado civil)
/// y el botón Guardar Paciente siempre tendrán exactamente la misma altura.
static const double unifiedRowHeight = 46.0;
final PatientStore store;
final GlobalKey<FormBuilderState> formKey;
final VoidCallback onSave;
const PatientDemographicsForm({
super.key,
required this.store,
required this.formKey,
required this.onSave,
});
InputDecoration _cleanDecoration({
required String label,
IconData? icon,
bool isRequired = false,
String? hintText,
bool readOnly = false,
}) {
return InputDecoration(
labelText: isRequired ? '$label *' : label,
hintText: hintText,
isDense: true,
floatingLabelBehavior: FloatingLabelBehavior.auto,
filled: true,
fillColor: readOnly ? const Color(0xFFF1F5F9) : Colors.white,
prefixIcon: icon != null
? Icon(icon, size: 16, color: isRequired ? AppColors.primary : AppColors.textMuted)
: null,
prefixIconConstraints: const BoxConstraints(minWidth: 34, minHeight: 34),
contentPadding: const EdgeInsets.symmetric(horizontal: 11, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.error),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.error, width: 1.5),
),
labelStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: isRequired ? AppColors.primary : AppColors.textSecondary,
),
hintStyle: const TextStyle(fontSize: 11.5, color: Color(0xFF94A3B8)),
);
}
Widget _buildCompactSelector({
required BuildContext context,
required String title,
required IconData titleIcon,
required Widget child,
bool isRequired = false,
}) {
return Container(
height: unifiedRowHeight,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(titleIcon, size: 14, color: isRequired ? AppColors.primary : AppColors.textMuted),
const SizedBox(width: 4),
Text(
isRequired ? '$title *' : title,
style: TextStyle(
fontSize: 11.0,
fontWeight: FontWeight.w700,
color: isRequired ? AppColors.primary : AppColors.textPrimary,
),
),
],
),
const SizedBox(width: 4),
Flexible(
child: Align(
alignment: Alignment.centerRight,
child: child,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final s = store;
final cedulaFormatter = MaskTextInputFormatter(
mask: '###-#######-#',
filter: {'#': RegExp(r'[0-9]')},
);
final telefonoFormatter = MaskTextInputFormatter(
mask: '###-###-####',
filter: {'#': RegExp(r'[0-9]')},
);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: const BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(color: Color(0xFFE2E8F0), width: 1.2),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Sub-bloque 1 (Columna 1): Fecha de reg, Nombre, Dirección, Ocupación
Expanded(
flex: 7,
child: Column(
children: [
SizedBox(
height: unifiedRowHeight,
child: FormBuilderTextField(
name: 'fec_reg',
initialValue: s.fecReg,
readOnly: true,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
decoration: _cleanDecoration(
label: 'Fecha de Registro',
icon: Icons.calendar_today_rounded,
readOnly: true,
),
),
),
const SizedBox(height: 8),
SizedBox(
height: unifiedRowHeight,
child: FormBuilderTextField(
name: 'nombre',
initialValue: s.nombre,
textCapitalization: TextCapitalization.words,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
decoration: _cleanDecoration(
label: 'Nombre',
icon: Icons.person_outline_rounded,
isRequired: true,
hintText: 'Ej. Juan Alberto',
),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(errorText: 'Requerido'),
FormBuilderValidators.minLength(2, errorText: 'Mínimo 2 letras'),
]),
onChanged: (v) => s.nombre = v ?? '',
),
),
const SizedBox(height: 8),
SizedBox(
height: unifiedRowHeight,
child: FormBuilderTextField(
name: 'direccion',
initialValue: s.direccion,
style: const TextStyle(fontSize: 12),
decoration: _cleanDecoration(
label: 'Dirección',
icon: Icons.location_on_outlined,
hintText: 'Calle, número o sector',
),
onChanged: (v) => s.direccion = v ?? '',
),
),
const SizedBox(height: 8),
SizedBox(
height: unifiedRowHeight,
child: FormBuilderTextField(
name: 'ocupacion',
initialValue: s.ocupacion,
style: const TextStyle(fontSize: 12),
decoration: _cleanDecoration(
label: 'Ocupación',
icon: Icons.work_outline_rounded,
hintText: 'Profesión u oficio',
),
onChanged: (v) => s.ocupacion = v ?? '',
),
),
],
),
),
const SizedBox(width: 12),
// Sub-bloque 2 (Columna 2): Cédula o Pasaporte, Apellido, Teléfono, Edad
Expanded(
flex: 7,
child: Column(
children: [
SizedBox(
height: unifiedRowHeight,
child: FormBuilderTextField(
name: 'cedula',
initialValue: s.cedula,
inputFormatters: [cedulaFormatter],
keyboardType: TextInputType.number,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
decoration: _cleanDecoration(
label: 'Cédula o Pasaporte',
icon: Icons.badge_outlined,
hintText: '001-0000000-0',
),
onChanged: (v) => s.cedula = v ?? '',
),
),
const SizedBox(height: 8),
SizedBox(
height: unifiedRowHeight,
child: FormBuilderTextField(
name: 'apellido',
initialValue: s.apellido,
textCapitalization: TextCapitalization.words,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
decoration: _cleanDecoration(
label: 'Apellido',
icon: Icons.badge_rounded,
isRequired: true,
hintText: 'Ej. Pérez Gómez',
),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(errorText: 'Requerido'),
FormBuilderValidators.minLength(2, errorText: 'Mínimo 2 letras'),
]),
onChanged: (v) => s.apellido = v ?? '',
),
),
const SizedBox(height: 8),
SizedBox(
height: unifiedRowHeight,
child: FormBuilderTextField(
name: 'telefono',
initialValue: s.telefono,
inputFormatters: [telefonoFormatter],
keyboardType: TextInputType.phone,
style: const TextStyle(fontSize: 12),
decoration: _cleanDecoration(
label: 'Teléfono',
icon: Icons.phone_outlined,
hintText: '809-555-0101',
),
onChanged: (v) => s.telefono = v ?? '',
),
),
const SizedBox(height: 8),
SizedBox(
height: unifiedRowHeight,
child: FormBuilderTextField(
name: 'edad',
initialValue: s.edad,
keyboardType: TextInputType.number,
style: const TextStyle(fontSize: 12),
decoration: _cleanDecoration(
label: 'Edad',
icon: Icons.cake_outlined,
hintText: 'Años',
),
onChanged: (v) => s.edad = v ?? '',
),
),
],
),
),
const SizedBox(width: 12),
// Sub-bloque 3 (Columna 3): Sexo, Asegurado, Estado Civil, Botón Guardar
Expanded(
flex: 10,
child: Column(
children: [
_buildCompactSelector(
context: context,
title: 'Sexo',
titleIcon: Icons.wc_rounded,
isRequired: true,
child: FormBuilderField<String>(
name: 'sexo',
initialValue: s.sexo,
validator: FormBuilderValidators.required(errorText: 'Requerido'),
builder: (FormFieldState<String> field) {
final currentVal = field.value ?? s.sexo;
final initialIndex = currentVal.toLowerCase().contains('fem') ? 1 : 0;
return ToggleSwitch(
key: ValueKey('sexo_$currentVal'),
customWidths: const [108.0, 108.0],
minHeight: 28.0,
cornerRadius: 6.0,
radiusStyle: true,
fontSize: 10.5,
iconSize: 13.0,
centerText: true,
initialLabelIndex: initialIndex,
totalSwitches: 2,
labels: const ['Masculino', 'Femenino'],
icons: const [Icons.male_rounded, Icons.female_rounded],
activeBgColor: const [AppColors.primary],
activeFgColor: Colors.white,
inactiveBgColor: const Color(0xFFE2E8F0),
inactiveFgColor: AppColors.textSecondary,
animate: true,
curve: Curves.easeInOut,
onToggle: (index) {
final val = (index == 1) ? 'Femenino' : 'Masculino';
field.didChange(val);
s.sexo = val;
},
);
},
),
),
const SizedBox(height: 8),
_buildCompactSelector(
context: context,
title: 'Seguro',
titleIcon: Icons.health_and_safety_outlined,
isRequired: true,
child: FormBuilderField<String>(
name: 'seguro',
initialValue: s.seguro,
validator: FormBuilderValidators.required(errorText: 'Requerido'),
builder: (FormFieldState<String> field) {
final currentVal = field.value ?? s.seguro;
final cleanVal = currentVal.trim().toLowerCase();
final isNo = cleanVal == 'no' || cleanVal.contains('sin seguro') || cleanVal.contains('no asegurado');
final initialIndex = isNo ? 1 : 0;
return ToggleSwitch(
key: ValueKey('seguro_$currentVal'),
customWidths: const [116.0, 116.0],
minHeight: 28.0,
cornerRadius: 6.0,
radiusStyle: true,
fontSize: 10.5,
iconSize: 12.0,
centerText: true,
initialLabelIndex: initialIndex,
totalSwitches: 2,
labels: const ['Con Seguro', 'Sin Seguro'],
icons: const [Icons.check_rounded, Icons.close_rounded],
activeBgColor: const [AppColors.primary],
activeFgColor: Colors.white,
inactiveBgColor: const Color(0xFFE2E8F0),
inactiveFgColor: AppColors.textSecondary,
animate: true,
curve: Curves.easeInOut,
onToggle: (index) {
final val = (index == 1) ? 'Sin Seguro' : 'Con Seguro';
field.didChange(val);
s.seguro = val;
},
);
},
),
),
const SizedBox(height: 8),
_buildCompactSelector(
context: context,
title: 'Estado Civil',
titleIcon: Icons.favorite_border_rounded,
child: FormBuilderField<String>(
name: 'est_civil',
initialValue: s.estCivil,
builder: (FormFieldState<String> field) {
const options = ['Casado', 'Soltero', 'Otros'];
final currentVal = field.value ?? s.estCivil;
int initialIndex = 1;
if (currentVal.toLowerCase().contains('casad')) {
initialIndex = 0;
} else if (currentVal.toLowerCase().contains('otro')) {
initialIndex = 2;
}
return ToggleSwitch(
key: ValueKey('est_civil_$currentVal'),
customWidths: const [86.0, 90.0, 76.0],
minHeight: 28.0,
cornerRadius: 6.0,
radiusStyle: true,
fontSize: 10.5,
iconSize: 11.5,
centerText: true,
initialLabelIndex: initialIndex,
totalSwitches: 3,
labels: options,
icons: const [Icons.people_alt_outlined, Icons.person_outline_rounded, Icons.more_horiz_rounded],
activeBgColor: const [AppColors.primary],
activeFgColor: Colors.white,
inactiveBgColor: const Color(0xFFE2E8F0),
inactiveFgColor: AppColors.textSecondary,
animate: true,
curve: Curves.easeInOut,
onToggle: (index) {
final selected = options[index ?? 1];
field.didChange(selected);
s.estCivil = selected;
},
);
},
),
),
const SizedBox(height: 8),
SizedBox(
height: unifiedRowHeight,
width: double.infinity,
child: Observer(
builder: (_) => ElevatedButton(
onPressed: s.isSaving ? null : onSave,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00A65A),
foregroundColor: Colors.white,
elevation: 1,
shadowColor: const Color(0xFF00A65A).withValues(alpha: 0.35),
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: s.isSaving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle_outline_rounded, size: 16),
const SizedBox(width: 6),
Text(
s.estadoEspera == 'en_consulta'
? 'Finalizar Consulta'
: (s.isEditing ? 'Guardar Cambios' : 'Guardar Paciente'),
style: const TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
),
),
],
),
),
),
),
],
),
),
],
),
);
}
}