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 createState() => _PatientClinicalTabsState(); } class _PatientClinicalTabsState extends State 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 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), ), ); } InputDecoration _cleanDropdownDecoration(String label) { 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 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 items, required ValueChanged onChanged, }) { final list = items.contains(initialValue) ? items : (initialValue.isNotEmpty ? [initialValue, ...items] : items); return FormBuilderDropdown( name: name, initialValue: initialValue.isNotEmpty ? initialValue : null, decoration: _cleanDropdownDecoration(label), hint: const Text( 'Seleccionar...', style: TextStyle(fontSize: 13.5, color: Color(0xFF94A3B8)), ), style: const TextStyle(fontSize: 13.5, color: AppColors.textPrimary, fontWeight: FontWeight.w500), onChanged: (v) { if (v != null) onChanged(v); }, items: list.map((item) { return DropdownMenuItem( value: item, child: Text( item, style: const TextStyle(fontSize: 13.5), overflow: TextOverflow.ellipsis, ), ); }).toList(), ); } 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), ], ), ), ), ], ); } }