488 lines
20 KiB
Dart
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|