2026-09-21 16:33:51 -04:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
|
|
|
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
|
|
|
|
import 'package:intl/intl.dart';
|
|
|
|
|
import 'package:mobx/mobx.dart';
|
|
|
|
|
import '../../../../core/theme/app_colors.dart';
|
|
|
|
|
import '../store/patient_store.dart';
|
|
|
|
|
|
|
|
|
|
class PatientClinicalTabs extends StatefulWidget {
|
|
|
|
|
final PatientStore store;
|
|
|
|
|
|
|
|
|
|
const PatientClinicalTabs({super.key, required this.store});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<PatientClinicalTabs> createState() => _PatientClinicalTabsState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _PatientClinicalTabsState extends State<PatientClinicalTabs>
|
|
|
|
|
with SingleTickerProviderStateMixin {
|
|
|
|
|
late final TabController _tabController;
|
|
|
|
|
late final TextEditingController _motConsController;
|
|
|
|
|
late final FocusNode _motConsFocusNode;
|
|
|
|
|
late final TextEditingController _hisEnfActController;
|
|
|
|
|
late final FocusNode _hisEnfActFocusNode;
|
|
|
|
|
ReactionDisposer? _disposerMotCons;
|
|
|
|
|
ReactionDisposer? _disposerHisEnfAct;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_tabController = TabController(length: 7, vsync: this);
|
|
|
|
|
_motConsController = TextEditingController(text: widget.store.motCons);
|
|
|
|
|
_motConsFocusNode = FocusNode();
|
|
|
|
|
_hisEnfActController = TextEditingController(text: widget.store.hisEnfAct);
|
|
|
|
|
_hisEnfActFocusNode = FocusNode();
|
|
|
|
|
|
|
|
|
|
_disposerMotCons = reaction((_) => widget.store.motCons, (String val) {
|
|
|
|
|
if (_motConsController.text != val) {
|
|
|
|
|
_motConsController.text = val;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
_disposerHisEnfAct = reaction((_) => widget.store.hisEnfAct, (String val) {
|
|
|
|
|
if (_hisEnfActController.text != val) {
|
|
|
|
|
_hisEnfActController.text = val;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_disposerMotCons?.call();
|
|
|
|
|
_disposerHisEnfAct?.call();
|
|
|
|
|
_tabController.dispose();
|
|
|
|
|
_motConsController.dispose();
|
|
|
|
|
_motConsFocusNode.dispose();
|
|
|
|
|
_hisEnfActController.dispose();
|
|
|
|
|
_hisEnfActFocusNode.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _insertDateAtCursor({
|
|
|
|
|
required TextEditingController controller,
|
|
|
|
|
required FocusNode focusNode,
|
|
|
|
|
required ValueChanged<String> onUpdateStore,
|
|
|
|
|
}) {
|
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
final dateStr = DateFormat('dd/MM/yyyy').format(now);
|
|
|
|
|
final insertionText = '$dateStr: ';
|
|
|
|
|
|
|
|
|
|
final currentText = controller.text;
|
|
|
|
|
final selection = controller.selection;
|
|
|
|
|
|
|
|
|
|
int start = selection.start;
|
|
|
|
|
int end = selection.end;
|
|
|
|
|
|
|
|
|
|
// Si el cursor no es válido o no está enfocado, insertar al final
|
|
|
|
|
if (start < 0 || end < 0) {
|
|
|
|
|
start = currentText.length;
|
|
|
|
|
end = currentText.length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (start > end) {
|
|
|
|
|
final temp = start;
|
|
|
|
|
start = end;
|
|
|
|
|
end = temp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final before = currentText.substring(0, start);
|
|
|
|
|
final after = currentText.substring(end);
|
|
|
|
|
final newText = '$before$insertionText$after';
|
|
|
|
|
final newOffset = start + insertionText.length;
|
|
|
|
|
|
|
|
|
|
controller.value = TextEditingValue(
|
|
|
|
|
text: newText,
|
|
|
|
|
selection: TextSelection.collapsed(offset: newOffset),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
onUpdateStore(newText);
|
|
|
|
|
focusNode.requestFocus();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
InputDecoration _cleanTextAreaDecoration(String hintText) {
|
|
|
|
|
return InputDecoration(
|
|
|
|
|
hintText: hintText,
|
|
|
|
|
hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13),
|
|
|
|
|
filled: true,
|
|
|
|
|
fillColor: const Color(0xFFF8FAFC),
|
|
|
|
|
contentPadding: const EdgeInsets.all(16),
|
|
|
|
|
border: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
|
|
|
|
),
|
|
|
|
|
enabledBorder: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
|
|
|
|
),
|
|
|
|
|
focusedBorder: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-02 11:09:02 -04:00
|
|
|
static InputDecoration _cleanDropdownDecoration(String label) {
|
2026-09-21 16:33:51 -04:00
|
|
|
return InputDecoration(
|
|
|
|
|
labelText: label,
|
|
|
|
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
|
|
|
|
filled: true,
|
|
|
|
|
fillColor: Colors.white,
|
|
|
|
|
isDense: true,
|
|
|
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
|
|
|
|
border: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(10),
|
|
|
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
|
|
|
|
),
|
|
|
|
|
enabledBorder: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(10),
|
|
|
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
|
|
|
|
),
|
|
|
|
|
focusedBorder: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(10),
|
|
|
|
|
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
|
|
|
|
|
),
|
|
|
|
|
labelStyle: const TextStyle(
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
color: AppColors.textPrimary,
|
|
|
|
|
),
|
|
|
|
|
floatingLabelStyle: const TextStyle(
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
color: AppColors.primary,
|
|
|
|
|
),
|
|
|
|
|
hintStyle: const TextStyle(
|
|
|
|
|
fontSize: 13.5,
|
|
|
|
|
color: Color(0xFF94A3B8),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildClinicalTextArea({
|
|
|
|
|
required String name,
|
|
|
|
|
String? initialValue,
|
|
|
|
|
TextEditingController? controller,
|
|
|
|
|
FocusNode? focusNode,
|
|
|
|
|
required ValueChanged<String> onChanged,
|
|
|
|
|
required String hintText,
|
|
|
|
|
VoidCallback? onInsertDate,
|
|
|
|
|
String? dateLabel,
|
|
|
|
|
}) {
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
child: Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: FormBuilderTextField(
|
|
|
|
|
name: name,
|
|
|
|
|
controller: controller,
|
|
|
|
|
initialValue: controller == null ? initialValue : null,
|
|
|
|
|
focusNode: focusNode,
|
|
|
|
|
maxLines: null,
|
|
|
|
|
expands: true,
|
|
|
|
|
textAlignVertical: TextAlignVertical.top,
|
|
|
|
|
style: const TextStyle(fontSize: 13.5, height: 1.5, color: AppColors.textPrimary),
|
|
|
|
|
decoration: _cleanTextAreaDecoration(hintText),
|
|
|
|
|
onChanged: (v) => onChanged(v ?? ''),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
if (onInsertDate != null) ...[
|
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
|
SizedBox(
|
|
|
|
|
width: 200,
|
|
|
|
|
child: SingleChildScrollView(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
|
|
|
|
if (dateLabel != null && dateLabel.isNotEmpty) ...[
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(12),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: AppColors.primaryContainer.withValues(alpha: 0.6),
|
|
|
|
|
borderRadius: BorderRadius.circular(10),
|
|
|
|
|
border: Border.all(color: AppColors.primary.withValues(alpha: 0.2)),
|
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
const Icon(Icons.history_rounded, size: 16, color: AppColors.primary),
|
|
|
|
|
const SizedBox(width: 8),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Text(
|
|
|
|
|
dateLabel,
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 11.5,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
color: AppColors.primary,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
],
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(12),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: const Color(0xFFF8FAFC),
|
|
|
|
|
borderRadius: BorderRadius.circular(10),
|
|
|
|
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'Entrada de Consulta',
|
|
|
|
|
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: AppColors.textSecondary),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
|
const Text(
|
|
|
|
|
'Inserte el sello de tiempo para documentar una nueva nota clínica.',
|
|
|
|
|
style: TextStyle(fontSize: 11, color: AppColors.textMuted, height: 1.3),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
ElevatedButton.icon(
|
|
|
|
|
onPressed: onInsertDate,
|
|
|
|
|
icon: const Icon(Icons.add_alarm_rounded, size: 16),
|
|
|
|
|
label: const Text('Fecha actual', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)),
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
backgroundColor: AppColors.primary,
|
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
|
|
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
|
|
|
|
elevation: 0,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildDropdownItem({
|
|
|
|
|
required String name,
|
|
|
|
|
required String label,
|
|
|
|
|
required String initialValue,
|
|
|
|
|
required List<String> items,
|
|
|
|
|
required ValueChanged<String> onChanged,
|
|
|
|
|
}) {
|
2026-10-02 11:09:02 -04:00
|
|
|
return _EditableDropdownField(
|
2026-09-21 16:33:51 -04:00
|
|
|
name: name,
|
2026-10-02 11:09:02 -04:00
|
|
|
label: label,
|
|
|
|
|
initialValue: initialValue,
|
|
|
|
|
items: items,
|
|
|
|
|
onChanged: onChanged,
|
2026-09-21 16:33:51 -04:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildAntecedentesTab(PatientStore s) {
|
|
|
|
|
return SingleChildScrollView(
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
// Patológicos
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
const Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.healing_rounded, size: 18, color: AppColors.primary),
|
|
|
|
|
SizedBox(width: 8),
|
|
|
|
|
Text(
|
|
|
|
|
'Antecedentes Personales Patológicos',
|
|
|
|
|
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.textPrimary),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_ni',
|
|
|
|
|
label: 'Infancia',
|
|
|
|
|
initialValue: s.datNi,
|
|
|
|
|
items: const ['sarampión', 'varicela', 'Tos ferina', 'Hepatitis', 'Ninguna'],
|
|
|
|
|
onChanged: (v) => s.datNi = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_ado',
|
|
|
|
|
label: 'Adolescencia',
|
|
|
|
|
initialValue: s.datAdo,
|
|
|
|
|
items: const ['Asma', 'Trastornos alimenticios', 'TBC', 'VIH', 'Ninguna'],
|
|
|
|
|
onChanged: (v) => s.datAdo = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_adu',
|
|
|
|
|
label: 'Adultez',
|
|
|
|
|
initialValue: s.datAdu,
|
|
|
|
|
items: const [
|
|
|
|
|
'TBC',
|
|
|
|
|
'VIH',
|
|
|
|
|
'Hepatopatías',
|
|
|
|
|
'Cardiopatías',
|
|
|
|
|
'Diabetes',
|
|
|
|
|
'HTA',
|
|
|
|
|
'Dislipidemias',
|
|
|
|
|
'Ninguna',
|
|
|
|
|
],
|
|
|
|
|
onChanged: (v) => s.datAdu = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_alerg',
|
|
|
|
|
label: 'Alergias',
|
|
|
|
|
initialValue: s.datAlerg,
|
|
|
|
|
items: const ['Ninguna', 'Penicilina', 'Sulfas', 'AINEs'],
|
|
|
|
|
onChanged: (v) => s.datAlerg = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_tran',
|
|
|
|
|
label: 'Transfusión sanguínea',
|
|
|
|
|
initialValue: s.datTran,
|
|
|
|
|
items: const ['1 ves', '2 veces', '3 veces', 'ninguna'],
|
|
|
|
|
onChanged: (v) => s.datTran = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_cir',
|
|
|
|
|
label: 'Intervenciones quirúrgicas',
|
|
|
|
|
initialValue: s.datCir,
|
|
|
|
|
items: const ['ninguna', 'Apendicectomía', 'Cesárea', 'Colecistectomía'],
|
|
|
|
|
onChanged: (v) => s.datCir = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_ac',
|
|
|
|
|
label: 'Accidentes',
|
|
|
|
|
initialValue: s.datAc,
|
|
|
|
|
items: const ['Si', 'ninguno'],
|
|
|
|
|
onChanged: (v) => s.datAc = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_otrosp',
|
|
|
|
|
label: 'Otros patológicos',
|
|
|
|
|
initialValue: s.datOtrosp,
|
|
|
|
|
items: const ['ninguno'],
|
|
|
|
|
onChanged: (v) => s.datOtrosp = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
|
|
|
|
|
// No patológicos
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
const Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.family_restroom_rounded, size: 18, color: AppColors.primary),
|
|
|
|
|
SizedBox(width: 8),
|
|
|
|
|
Text(
|
|
|
|
|
'Antecedentes Personales No Patológicos (Familiares)',
|
|
|
|
|
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.textPrimary),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_mad',
|
|
|
|
|
label: 'Madre',
|
|
|
|
|
initialValue: s.datMad,
|
|
|
|
|
items: const ['Viva y sana', 'Fallecida', 'Enferma'],
|
|
|
|
|
onChanged: (v) => s.datMad = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_esp',
|
|
|
|
|
label: 'Pareja',
|
|
|
|
|
initialValue: s.datEsp,
|
|
|
|
|
items: const ['Viva y sana', 'Fallecida', 'Enferma'],
|
|
|
|
|
onChanged: (v) => s.datEsp = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_padre',
|
|
|
|
|
label: 'Padre',
|
|
|
|
|
initialValue: s.datPadre,
|
|
|
|
|
items: const ['Vivo y sano', 'Fallecido', 'Enfermo'],
|
|
|
|
|
onChanged: (v) => s.datPadre = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_hijos',
|
|
|
|
|
label: 'Hijos',
|
|
|
|
|
initialValue: s.datHijos,
|
|
|
|
|
items: const ['Vivos y sanos', 'Enfermos', 'Ninguno'],
|
|
|
|
|
onChanged: (v) => s.datHijos = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_her',
|
|
|
|
|
label: 'Hermanos',
|
|
|
|
|
initialValue: s.datHer,
|
|
|
|
|
items: const ['Vivos y sanos', 'Fallecidos', 'Enfermos', 'Ninguno'],
|
|
|
|
|
onChanged: (v) => s.datHer = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: _buildDropdownItem(
|
|
|
|
|
name: 'dat_otrosnp',
|
|
|
|
|
label: 'Otros no patológicos',
|
|
|
|
|
initialValue: s.datOtrosnp,
|
|
|
|
|
items: const ['Ningunos'],
|
|
|
|
|
onChanged: (v) => s.datOtrosnp = v,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildDiagnosticosTab(PatientStore s) {
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
child: Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
const Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.biotech_rounded, size: 16, color: AppColors.primary),
|
|
|
|
|
SizedBox(width: 6),
|
|
|
|
|
Text(
|
|
|
|
|
'Métodos diagnósticos, radiológicos e invasivos',
|
|
|
|
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.textPrimary),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: FormBuilderTextField(
|
|
|
|
|
name: 'met_diagn_rad',
|
|
|
|
|
initialValue: s.metDiagnRad,
|
|
|
|
|
maxLines: null,
|
|
|
|
|
expands: true,
|
|
|
|
|
textAlignVertical: TextAlignVertical.top,
|
|
|
|
|
style: const TextStyle(fontSize: 13),
|
|
|
|
|
decoration: _cleanTextAreaDecoration('Describa radiografías, ecografías, resonancias o procedimientos invasivos...'),
|
|
|
|
|
onChanged: (v) => s.metDiagnRad = v ?? '',
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
|
const Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.star_outline_rounded, size: 16, color: AppColors.primary),
|
|
|
|
|
SizedBox(width: 6),
|
|
|
|
|
Text(
|
|
|
|
|
'Diagnósticos a destacar',
|
|
|
|
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.textPrimary),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: FormBuilderTextField(
|
|
|
|
|
name: 'diagn_desc',
|
|
|
|
|
initialValue: s.diagnDesc,
|
|
|
|
|
maxLines: null,
|
|
|
|
|
expands: true,
|
|
|
|
|
textAlignVertical: TextAlignVertical.top,
|
|
|
|
|
style: const TextStyle(fontSize: 13),
|
|
|
|
|
decoration: _cleanTextAreaDecoration('Patologías principales, hallazgos o diagnósticos relevantes...'),
|
|
|
|
|
onChanged: (v) => s.diagnDesc = v ?? '',
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 16),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
const Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.verified_outlined, size: 16, color: AppColors.primary),
|
|
|
|
|
SizedBox(width: 6),
|
|
|
|
|
Text(
|
|
|
|
|
'Diagnóstico definitivo',
|
|
|
|
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.textPrimary),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: FormBuilderTextField(
|
|
|
|
|
name: 'diagn_def',
|
|
|
|
|
initialValue: s.diagnDef,
|
|
|
|
|
maxLines: null,
|
|
|
|
|
expands: true,
|
|
|
|
|
textAlignVertical: TextAlignVertical.top,
|
|
|
|
|
style: const TextStyle(fontSize: 13),
|
|
|
|
|
decoration: _cleanTextAreaDecoration('Conclusión diagnóstica definitiva y codificación clínica...'),
|
|
|
|
|
onChanged: (v) => s.diagnDef = v ?? '',
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildProximaCitaTab(PatientStore s) {
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.all(20),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
ElevatedButton.icon(
|
|
|
|
|
onPressed: () async {
|
|
|
|
|
final now = DateTime.now();
|
|
|
|
|
final picked = await showDatePicker(
|
|
|
|
|
context: context,
|
|
|
|
|
initialDate: now.add(const Duration(days: 7)),
|
|
|
|
|
firstDate: now.subtract(const Duration(days: 30)),
|
|
|
|
|
lastDate: now.add(const Duration(days: 365 * 3)),
|
|
|
|
|
builder: (context, child) {
|
|
|
|
|
return Theme(
|
|
|
|
|
data: Theme.of(context).copyWith(
|
|
|
|
|
colorScheme: const ColorScheme.light(
|
|
|
|
|
primary: AppColors.primary,
|
|
|
|
|
onPrimary: Colors.white,
|
|
|
|
|
surface: Colors.white,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
child: child!,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
if (picked != null) {
|
|
|
|
|
s.fecProxcita = DateFormat('dd/MM/yyyy').format(picked);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
icon: const Icon(Icons.calendar_month_rounded, size: 18),
|
|
|
|
|
label: const Text('Seleccionar Fecha de Cita', style: TextStyle(fontWeight: FontWeight.w600)),
|
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
backgroundColor: AppColors.primary,
|
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
|
|
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
|
|
|
elevation: 0,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 14),
|
|
|
|
|
if (s.fecProxcita.isNotEmpty)
|
|
|
|
|
TextButton.icon(
|
|
|
|
|
onPressed: () => s.fecProxcita = '',
|
|
|
|
|
icon: const Icon(Icons.clear_rounded, size: 16, color: AppColors.error),
|
|
|
|
|
label: const Text('Quitar próxima cita', style: TextStyle(color: AppColors.error, fontWeight: FontWeight.w600)),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(18),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(14),
|
|
|
|
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: Colors.black.withValues(alpha: 0.03),
|
|
|
|
|
blurRadius: 10,
|
|
|
|
|
offset: const Offset(0, 4),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(10),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: AppColors.primaryContainer,
|
|
|
|
|
borderRadius: BorderRadius.circular(10),
|
|
|
|
|
),
|
|
|
|
|
child: const Icon(Icons.event_available_rounded, size: 22, color: AppColors.primary),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 14),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'Control de Citas del Paciente',
|
|
|
|
|
style: TextStyle(fontSize: 12, color: AppColors.textMuted, fontWeight: FontWeight.w500),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 2),
|
|
|
|
|
Text(
|
|
|
|
|
s.fecProxcita.isNotEmpty
|
|
|
|
|
? 'Próxima cita programada: ${s.fecProxcita}'
|
|
|
|
|
: 'Sin próxima cita programada',
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 15,
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
color: AppColors.textPrimary,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
if (s.diasParaProximaCita != null) ...[
|
|
|
|
|
const SizedBox(height: 14),
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: s.diasParaProximaCita! < 0
|
|
|
|
|
? AppColors.error.withValues(alpha: 0.1)
|
|
|
|
|
: (s.diasParaProximaCita! == 0
|
|
|
|
|
? AppColors.warning.withValues(alpha: 0.1)
|
|
|
|
|
: AppColors.success.withValues(alpha: 0.1)),
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
|
|
|
|
child: Text(
|
|
|
|
|
'Tiempo restante para la cita: ${s.diasParaProximaCita} días',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
color: s.diasParaProximaCita! < 0
|
|
|
|
|
? AppColors.error
|
|
|
|
|
: (s.diasParaProximaCita! == 0 ? AppColors.warning : AppColors.success),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final s = widget.store;
|
|
|
|
|
|
|
|
|
|
return Column(
|
|
|
|
|
children: [
|
|
|
|
|
// Barra de Pestañas Moderna con Estilo Limpio
|
|
|
|
|
Container(
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
color: Color(0xFFF8FAFC),
|
|
|
|
|
border: Border(
|
|
|
|
|
bottom: BorderSide(color: Color(0xFFE2E8F0)),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
child: TabBar(
|
|
|
|
|
controller: _tabController,
|
|
|
|
|
isScrollable: true,
|
|
|
|
|
labelColor: AppColors.primary,
|
|
|
|
|
unselectedLabelColor: AppColors.textSecondary,
|
|
|
|
|
labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 12.5),
|
|
|
|
|
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 12.5),
|
|
|
|
|
indicatorColor: AppColors.primary,
|
|
|
|
|
indicatorWeight: 2.5,
|
|
|
|
|
tabAlignment: TabAlignment.start,
|
|
|
|
|
tabs: const [
|
|
|
|
|
Tab(
|
|
|
|
|
height: 38,
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.notes_rounded, size: 15),
|
|
|
|
|
SizedBox(width: 6),
|
|
|
|
|
Text('Consultas'),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Tab(
|
|
|
|
|
height: 38,
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.history_edu_rounded, size: 15),
|
|
|
|
|
SizedBox(width: 6),
|
|
|
|
|
Text('Hist. enfermedad actual'),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Tab(
|
|
|
|
|
height: 38,
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.medical_information_outlined, size: 15),
|
|
|
|
|
SizedBox(width: 6),
|
|
|
|
|
Text('Antec. patológicos'),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Tab(
|
|
|
|
|
height: 38,
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.biotech_outlined, size: 15),
|
|
|
|
|
SizedBox(width: 6),
|
|
|
|
|
Text('Diagnósticos'),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Tab(
|
|
|
|
|
height: 38,
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.science_outlined, size: 15),
|
|
|
|
|
SizedBox(width: 6),
|
|
|
|
|
Text('Laboratorios'),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Tab(
|
|
|
|
|
height: 38,
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.medication_outlined, size: 15),
|
|
|
|
|
SizedBox(width: 6),
|
|
|
|
|
Text('Tratamiento'),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Tab(
|
|
|
|
|
height: 38,
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.event_outlined, size: 15),
|
|
|
|
|
SizedBox(width: 6),
|
|
|
|
|
Text('Próxima cita'),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Observer(
|
|
|
|
|
builder: (_) => TabBarView(
|
|
|
|
|
controller: _tabController,
|
|
|
|
|
children: [
|
|
|
|
|
// 1. Consultas
|
|
|
|
|
_buildClinicalTextArea(
|
|
|
|
|
name: 'mot_cons',
|
|
|
|
|
controller: _motConsController,
|
|
|
|
|
focusNode: _motConsFocusNode,
|
|
|
|
|
hintText: 'Registro de consultas clínicas, motivo y evolución...',
|
|
|
|
|
dateLabel: s.fecUltcon.isNotEmpty ? 'Última consulta: ${s.fecUltcon}' : null,
|
|
|
|
|
onInsertDate: () {
|
|
|
|
|
_insertDateAtCursor(
|
|
|
|
|
controller: _motConsController,
|
|
|
|
|
focusNode: _motConsFocusNode,
|
|
|
|
|
onUpdateStore: (v) => s.motCons = v,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
onChanged: (v) => s.motCons = v,
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// 2. Hist. enfermedad actual
|
|
|
|
|
_buildClinicalTextArea(
|
|
|
|
|
name: 'his_enf_act',
|
|
|
|
|
controller: _hisEnfActController,
|
|
|
|
|
focusNode: _hisEnfActFocusNode,
|
|
|
|
|
hintText: 'Historia y cronología de la enfermedad actual...',
|
|
|
|
|
onInsertDate: () {
|
|
|
|
|
_insertDateAtCursor(
|
|
|
|
|
controller: _hisEnfActController,
|
|
|
|
|
focusNode: _hisEnfActFocusNode,
|
|
|
|
|
onUpdateStore: (v) => s.hisEnfAct = v,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
onChanged: (v) => s.hisEnfAct = v,
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// 3. Antec. patológicos
|
|
|
|
|
_buildAntecedentesTab(s),
|
|
|
|
|
|
|
|
|
|
// 4. Diagnósticos
|
|
|
|
|
_buildDiagnosticosTab(s),
|
|
|
|
|
|
|
|
|
|
// 5. Laboratorios
|
|
|
|
|
_buildClinicalTextArea(
|
|
|
|
|
name: 'dat_lab',
|
|
|
|
|
initialValue: s.datLab,
|
|
|
|
|
hintText: 'Resultados de laboratorio, hemogramas, analíticas...',
|
|
|
|
|
onChanged: (v) => s.datLab = v,
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// 6. Tratamiento
|
|
|
|
|
_buildClinicalTextArea(
|
|
|
|
|
name: 'tratamiento',
|
|
|
|
|
initialValue: s.tratamiento,
|
|
|
|
|
hintText: 'Plan terapéutico, recetas, dosis y recomendaciones...',
|
|
|
|
|
onChanged: (v) => s.tratamiento = v,
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// 7. Próxima cita
|
|
|
|
|
_buildProximaCitaTab(s),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-02 11:09:02 -04:00
|
|
|
|
|
|
|
|
class _EditableDropdownField extends StatefulWidget {
|
|
|
|
|
final String name;
|
|
|
|
|
final String label;
|
|
|
|
|
final String initialValue;
|
|
|
|
|
final List<String> items;
|
|
|
|
|
final ValueChanged<String> onChanged;
|
|
|
|
|
|
|
|
|
|
const _EditableDropdownField({
|
|
|
|
|
required this.name,
|
|
|
|
|
required this.label,
|
|
|
|
|
required this.initialValue,
|
|
|
|
|
required this.items,
|
|
|
|
|
required this.onChanged,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<_EditableDropdownField> createState() => _EditableDropdownFieldState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _EditableDropdownFieldState extends State<_EditableDropdownField> {
|
|
|
|
|
late final TextEditingController _controller;
|
|
|
|
|
late final FocusNode _focusNode;
|
|
|
|
|
final GlobalKey _fieldKey = GlobalKey();
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_controller = TextEditingController(text: widget.initialValue);
|
|
|
|
|
_focusNode = FocusNode();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void didUpdateWidget(covariant _EditableDropdownField oldWidget) {
|
|
|
|
|
super.didUpdateWidget(oldWidget);
|
|
|
|
|
if (oldWidget.initialValue != widget.initialValue && _controller.text != widget.initialValue) {
|
|
|
|
|
_controller.value = TextEditingValue(
|
|
|
|
|
text: widget.initialValue,
|
|
|
|
|
selection: TextSelection.collapsed(offset: widget.initialValue.length),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_controller.dispose();
|
|
|
|
|
_focusNode.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _showDropdownMenu(FormFieldState<String> field) async {
|
|
|
|
|
final renderBox = _fieldKey.currentContext?.findRenderObject() as RenderBox?;
|
|
|
|
|
if (renderBox == null || !renderBox.hasSize) return;
|
|
|
|
|
|
|
|
|
|
final overlay = Overlay.of(context).context.findRenderObject() as RenderBox?;
|
|
|
|
|
if (overlay == null) return;
|
|
|
|
|
|
|
|
|
|
final targetRect = Rect.fromPoints(
|
|
|
|
|
renderBox.localToGlobal(renderBox.size.bottomLeft(Offset.zero), ancestor: overlay).translate(0, 4),
|
|
|
|
|
renderBox.localToGlobal(renderBox.size.bottomRight(Offset.zero), ancestor: overlay).translate(0, 4),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final position = RelativeRect.fromRect(
|
|
|
|
|
targetRect,
|
|
|
|
|
Offset.zero & overlay.size,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final selected = await showMenu<String>(
|
|
|
|
|
context: context,
|
|
|
|
|
position: position,
|
|
|
|
|
constraints: BoxConstraints(
|
|
|
|
|
minWidth: renderBox.size.width,
|
|
|
|
|
maxWidth: renderBox.size.width < 220 ? 220 : renderBox.size.width,
|
|
|
|
|
maxHeight: 280,
|
|
|
|
|
),
|
|
|
|
|
elevation: 4,
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(10),
|
|
|
|
|
side: const BorderSide(color: Color(0xFFE2E8F0)),
|
|
|
|
|
),
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
items: widget.items.map((item) {
|
|
|
|
|
final isSelected = _controller.text.trim().toLowerCase() == item.trim().toLowerCase();
|
|
|
|
|
return PopupMenuItem<String>(
|
|
|
|
|
value: item,
|
|
|
|
|
height: 38,
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Text(
|
|
|
|
|
item,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 13.5,
|
|
|
|
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
|
|
|
|
color: isSelected ? AppColors.primary : AppColors.textPrimary,
|
|
|
|
|
),
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
if (isSelected)
|
|
|
|
|
const Icon(Icons.check_rounded, size: 16, color: AppColors.primary),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (selected != null) {
|
|
|
|
|
_controller.text = selected;
|
|
|
|
|
_controller.selection = TextSelection.collapsed(offset: selected.length);
|
|
|
|
|
field.didChange(selected);
|
|
|
|
|
widget.onChanged(selected);
|
|
|
|
|
setState(() {});
|
|
|
|
|
_focusNode.requestFocus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return FormBuilderField<String>(
|
|
|
|
|
name: widget.name,
|
|
|
|
|
initialValue: widget.initialValue,
|
|
|
|
|
builder: (FormFieldState<String> field) {
|
|
|
|
|
return TextField(
|
|
|
|
|
key: _fieldKey,
|
|
|
|
|
controller: _controller,
|
|
|
|
|
focusNode: _focusNode,
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 13.5,
|
|
|
|
|
color: AppColors.textPrimary,
|
|
|
|
|
fontWeight: FontWeight.w500,
|
|
|
|
|
),
|
|
|
|
|
decoration: _PatientClinicalTabsState._cleanDropdownDecoration(widget.label).copyWith(
|
|
|
|
|
hintText: 'Seleccionar...',
|
|
|
|
|
suffixIconConstraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
|
|
|
|
suffixIcon: Row(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
if (_controller.text.isNotEmpty)
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.clear_rounded, size: 16, color: Color(0xFF94A3B8)),
|
|
|
|
|
splashRadius: 16,
|
|
|
|
|
tooltip: 'Limpiar',
|
|
|
|
|
padding: EdgeInsets.zero,
|
|
|
|
|
constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
_controller.clear();
|
|
|
|
|
field.didChange('');
|
|
|
|
|
widget.onChanged('');
|
|
|
|
|
setState(() {});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
InkWell(
|
|
|
|
|
borderRadius: BorderRadius.circular(20),
|
|
|
|
|
onTap: () => _showDropdownMenu(field),
|
|
|
|
|
child: const Padding(
|
|
|
|
|
padding: EdgeInsets.only(left: 4, right: 8, top: 8, bottom: 8),
|
|
|
|
|
child: Icon(
|
|
|
|
|
Icons.arrow_drop_down_rounded,
|
|
|
|
|
size: 24,
|
|
|
|
|
color: Color(0xFF64748B),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onChanged: (val) {
|
|
|
|
|
field.didChange(val);
|
|
|
|
|
widget.onChanged(val);
|
|
|
|
|
setState(() {});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|