import 'package:flutter/material.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../core/widgets/medical_modal.dart'; import '../../../patient/presentation/views/patient_modal_view.dart'; import '../../data/models/patient_summary_model.dart'; import '../../data/services/dashboard_service.dart'; class AppointmentsModalView extends StatefulWidget { final DateTime? initialDate; final DashboardService? service; final bool isSecretary; final void Function(int patientId, String newStatus)? onStatusChanged; const AppointmentsModalView({ super.key, this.initialDate, this.service, this.isSecretary = false, this.onStatusChanged, }); @override State createState() => _AppointmentsModalViewState(); } class _AppointmentsModalViewState extends State { late final DashboardService _service; late DateTime _selectedDate; bool _isLoading = false; String? _errorMessage; List _appointments = []; String _waitingFilter = 'todos'; // 'todos', 'en_espera', 'en_consulta', 'atendido', 'no_presente' @override void initState() { super.initState(); _service = widget.service ?? DashboardService(); _selectedDate = widget.initialDate ?? DateTime.now(); _fetchAppointments(_selectedDate); } Future _handleStatusChange(PatientSummaryModel item, String newStatus) async { final success = await _service.updatePatientWaitingStatus(item.id, newStatus); if (success && mounted) { setState(() { final index = _appointments.indexWhere((p) => p.id == item.id); if (index != -1) { _appointments[index] = _appointments[index].copyWith(estadoEspera: newStatus); } }); if (widget.onStatusChanged != null) { widget.onStatusChanged!(item.id, newStatus); } } } List get _filteredAppointments { if (_waitingFilter == 'todos') return _appointments; if (_waitingFilter == 'en_espera') return _appointments.where((a) => a.isEnEspera).toList(); if (_waitingFilter == 'en_consulta') return _appointments.where((a) => a.isEnConsulta).toList(); if (_waitingFilter == 'atendido') return _appointments.where((a) => a.isAtendido).toList(); if (_waitingFilter == 'no_presente') return _appointments.where((a) => a.isNoPresente).toList(); return _appointments; } bool _isToday(DateTime date) { final now = DateTime.now(); return date.year == now.year && date.month == now.month && date.day == now.day; } String _formatDate(DateTime date) { final day = date.day.toString().padLeft(2, '0'); final month = date.month.toString().padLeft(2, '0'); final year = date.year.toString(); return '$day/$month/$year'; } String _formatDateHuman(DateTime date) { const weekdays = [ 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo' ]; const months = [ 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre' ]; final weekdayName = weekdays[date.weekday - 1]; final monthName = months[date.month - 1]; return '$weekdayName, ${date.day} de $monthName de ${date.year}'; } Future _fetchAppointments(DateTime date) async { setState(() { _isLoading = true; _errorMessage = null; }); try { final results = await _service.getAppointmentsByDate(date); if (mounted) { setState(() { _appointments = results; _isLoading = false; }); } } catch (e) { if (mounted) { setState(() { _errorMessage = 'Error al cargar las citas para la fecha seleccionada: $e'; _isLoading = false; }); } } } void _onDateSelected(DateTime newDate) { if (newDate.year == _selectedDate.year && newDate.month == _selectedDate.month && newDate.day == _selectedDate.day) { return; } setState(() { _selectedDate = newDate; }); _fetchAppointments(newDate); } Future _pickDate() async { final picked = await showDatePicker( context: context, initialDate: _selectedDate, firstDate: DateTime(2000), lastDate: DateTime(2050), helpText: 'SELECCIONAR FECHA DE CITAS', confirmText: 'FILTRAR', cancelText: 'CANCELAR', builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light( primary: AppColors.primary, onPrimary: Colors.white, onSurface: AppColors.textPrimary, ), ), child: child!, ); }, ); if (picked != null) { _onDateSelected(picked); } } void _openPatient(int patientId) { showMedicalModal( context: context, builder: (_) => PatientModalView( patientId: patientId, isSecretary: widget.isSecretary, onSaved: (_) { _fetchAppointments(_selectedDate); if (widget.onStatusChanged != null) { widget.onStatusChanged!(patientId, 'atendido'); } }, ), ); } @override Widget build(BuildContext context) { final isFilteringToday = _isToday(_selectedDate); return MedicalModalDialog( titleIcon: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColors.warning.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), ), child: const Icon( Icons.event_available_rounded, color: AppColors.warning, size: 22, ), ), title: 'Agenda de Citas Médicas', subtitle: const Text( 'Consulte y gestione los pacientes citados por fecha de consulta', style: TextStyle(fontSize: 12, color: AppColors.textMuted), ), maxWidth: 820, maxHeight: 700, bottomBar: _buildBottomBar(), child: Column( children: [ // Barra de Filtro de Fecha _buildDateFilterBar(isFilteringToday), // Divisor const Divider(height: 1, color: Color(0xFFE2E8F0)), // Contenido Principal (Lista de Citas) Expanded( child: _buildBody(isFilteringToday), ), ], ), ); } Widget _buildDateFilterBar(bool isFilteringToday) { final filterText = isFilteringToday ? 'Hoy (${_formatDate(_selectedDate)})' : _formatDate(_selectedDate); return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: const BoxDecoration( color: Color(0xFFF8FAFC), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.center, spacing: 12, runSpacing: 8, children: [ // Indicador dinámico de filtro activo (Hoy vs Fecha Específica) Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: isFilteringToday ? AppColors.primaryContainer : const Color(0xFFEFF6FF), borderRadius: BorderRadius.circular(8), border: Border.all( color: isFilteringToday ? AppColors.primary.withValues(alpha: 0.3) : const Color(0xFF93C5FD), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( isFilteringToday ? Icons.today_rounded : Icons.date_range_rounded, size: 16, color: isFilteringToday ? AppColors.primary : const Color(0xFF2563EB), ), const SizedBox(width: 8), Text.rich( TextSpan( children: [ const TextSpan( text: 'Filtro: ', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppColors.textSecondary, ), ), TextSpan( text: filterText, style: TextStyle( fontSize: 12.5, fontWeight: FontWeight.w700, color: isFilteringToday ? AppColors.primary : const Color(0xFF1D4ED8), ), ), ], ), ), ], ), ), // Botones de Navegación Rápida Row( mainAxisSize: MainAxisSize.min, children: [ // Botón Día Anterior IconButton( icon: const Icon(Icons.chevron_left_rounded, size: 20), tooltip: 'Día anterior', splashRadius: 18, onPressed: _isLoading ? null : () => _onDateSelected(_selectedDate.subtract(const Duration(days: 1))), ), // Botón acceso directo a "Hoy" InkWell( onTap: _isLoading || isFilteringToday ? null : () => _onDateSelected(DateTime.now()), borderRadius: BorderRadius.circular(6), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: isFilteringToday ? AppColors.primary : Colors.white, borderRadius: BorderRadius.circular(6), border: Border.all( color: isFilteringToday ? AppColors.primary : const Color(0xFFCBD5E1), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.refresh_rounded, size: 14, color: isFilteringToday ? Colors.white : AppColors.textPrimary, ), const SizedBox(width: 4), Text( 'Hoy', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: isFilteringToday ? Colors.white : AppColors.textPrimary, ), ), ], ), ), ), // Botón Día Siguiente IconButton( icon: const Icon(Icons.chevron_right_rounded, size: 20), tooltip: 'Día siguiente', splashRadius: 18, onPressed: _isLoading ? null : () => _onDateSelected(_selectedDate.add(const Duration(days: 1))), ), const SizedBox(width: 6), // Botón Selector de Fecha ElevatedButton.icon( onPressed: _isLoading ? null : _pickDate, icon: const Icon(Icons.calendar_month_outlined, size: 16), label: const Text( 'Cambiar Fecha', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: AppColors.textPrimary, elevation: 0, side: const BorderSide(color: Color(0xFFCBD5E1)), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), ), ), ], ), ], ), const SizedBox(height: 6), Wrap( alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.center, spacing: 12, runSpacing: 6, children: [ Text( _formatDateHuman(_selectedDate), style: const TextStyle( fontSize: 11.5, color: AppColors.textMuted, fontWeight: FontWeight.w500, ), ), // Píldoras de Filtro de Sala de Espera Wrap( spacing: 6, runSpacing: 4, children: [ _buildFilterChip('todos', 'Todos (${_appointments.length})', null), _buildFilterChip( 'en_espera', 'En Espera (${_appointments.where((a) => a.isEnEspera).length})', const Color(0xFF15803D), ), _buildFilterChip( 'en_consulta', 'En Consulta (${_appointments.where((a) => a.isEnConsulta).length})', const Color(0xFF0369A1), ), _buildFilterChip( 'atendido', 'Atendidos (${_appointments.where((a) => a.isAtendido).length})', const Color(0xFF64748B), ), ], ), ], ), ], ), ); } Widget _buildFilterChip(String value, String label, Color? highlightColor) { final isSelected = _waitingFilter == value; return InkWell( onTap: () { setState(() { _waitingFilter = value; }); }, borderRadius: BorderRadius.circular(20), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: isSelected ? (highlightColor ?? AppColors.primary).withValues(alpha: 0.15) : Colors.white, borderRadius: BorderRadius.circular(20), border: Border.all( color: isSelected ? (highlightColor ?? AppColors.primary) : const Color(0xFFE2E8F0), width: isSelected ? 1.5 : 1, ), ), child: Text( label, style: TextStyle( fontSize: 11, fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500, color: isSelected ? (highlightColor ?? AppColors.primary) : AppColors.textSecondary, ), ), ), ); } Widget _buildBody(bool isFilteringToday) { if (_isLoading) { return const Center( child: Padding( padding: EdgeInsets.all(40), child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 14), Text( 'Consultando agenda médica...', style: TextStyle(fontSize: 13, color: AppColors.textSecondary), ), ], ), ), ); } if (_errorMessage != null) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline_rounded, color: AppColors.error, size: 40), const SizedBox(height: 12), Text( _errorMessage!, textAlign: TextAlign.center, style: const TextStyle(fontSize: 13, color: AppColors.error), ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: () => _fetchAppointments(_selectedDate), icon: const Icon(Icons.refresh_rounded, size: 16), label: const Text('Reintentar'), ), ], ), ), ); } if (_appointments.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(36), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(50), ), child: const Icon( Icons.event_busy_rounded, size: 42, color: AppColors.textMuted, ), ), const SizedBox(height: 16), Text( isFilteringToday ? 'No hay citas programadas para el día de hoy' : 'No hay citas registradas para el ${_formatDate(_selectedDate)}', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), const SizedBox(height: 6), Text( isFilteringToday ? 'Puede consultar otra fecha utilizando el selector de calendario superior.' : 'Intente seleccionar otro día o presione "Hoy" para volver a la agenda actual.', textAlign: TextAlign.center, style: const TextStyle( fontSize: 12, color: AppColors.textMuted, ), ), ], ), ), ); } final list = _filteredAppointments; if (list.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(36), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.filter_list_off_rounded, size: 38, color: AppColors.textMuted), const SizedBox(height: 12), Text( 'No hay pacientes con el filtro seleccionado', style: const TextStyle(fontWeight: FontWeight.w600, color: AppColors.textSecondary), ), const SizedBox(height: 8), TextButton( onPressed: () => setState(() => _waitingFilter = 'todos'), child: const Text('Mostrar todos'), ), ], ), ), ); } return ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), itemCount: list.length, separatorBuilder: (_, index) => const SizedBox(height: 10), itemBuilder: (context, index) { final item = list[index]; return _buildAppointmentTile(item); }, ); } Widget _buildAppointmentTile(PatientSummaryModel item) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all( color: item.isEnEspera ? const Color(0xFF86EFAC) : const Color(0xFFE2E8F0), width: item.isEnEspera ? 1.5 : 1, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.02), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Material( color: Colors.transparent, borderRadius: BorderRadius.circular(12), child: InkWell( onTap: item.id > 0 ? () => _openPatient(item.id) : null, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(14), child: Row( children: [ // Icono de Cita Container( width: 42, height: 42, decoration: BoxDecoration( color: item.isEnEspera ? const Color(0xFFDCFCE7) : AppColors.teal.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), ), child: Icon( item.isEnEspera ? Icons.chair_rounded : Icons.calendar_today_rounded, color: item.isEnEspera ? const Color(0xFF15803D) : AppColors.teal, size: 20, ), ), const SizedBox(width: 14), // Datos del Paciente y Motivo Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( item.fullName, style: const TextStyle( fontSize: 13.5, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), ), if (item.cedula.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(4), ), child: Text( item.cedula, style: const TextStyle( fontSize: 10.5, fontWeight: FontWeight.w600, color: AppColors.textSecondary, ), ), ), ], ), const SizedBox(height: 4), Row( children: [ if (item.telefono.isNotEmpty) ...[ const Icon(Icons.phone_outlined, size: 13, color: AppColors.textMuted), const SizedBox(width: 4), Text( item.telefono, style: const TextStyle(fontSize: 11.5, color: AppColors.textSecondary), ), const SizedBox(width: 12), ], const Icon(Icons.medical_information_outlined, size: 13, color: AppColors.textMuted), const SizedBox(width: 4), Expanded( child: Text( item.motCons.isNotEmpty ? item.motCons.replaceAll('\n', ' ').trim() : 'Consulta médica programada', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11.5, color: AppColors.textMuted), ), ), ], ), ], ), ), const SizedBox(width: 12), // Acciones de Sala de Espera / Estado _buildAppointmentActions(item), const SizedBox(width: 8), // Botón Ver Ficha / Abrir Historial Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: AppColors.primaryContainer, borderRadius: BorderRadius.circular(8), ), child: const Icon( Icons.arrow_forward_ios_rounded, color: AppColors.primary, size: 14, ), ), ], ), ), ), ), ); } Widget _buildAppointmentActions(PatientSummaryModel item) { const double uniformHeight = 28.0; const double uniformRadius = 6.0; if (item.isEnEspera) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( height: uniformHeight, padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: const Color(0xFFDCFCE7), borderRadius: BorderRadius.circular(uniformRadius), border: Border.all(color: const Color(0xFF86EFAC)), ), alignment: Alignment.center, child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.hourglass_top_rounded, size: 12, color: Color(0xFF15803D)), SizedBox(width: 4), Text( 'En Espera', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, color: Color(0xFF15803D), ), ), ], ), ), if (!widget.isSecretary) ...[ const SizedBox(width: 6), SizedBox( height: uniformHeight, child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(horizontal: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(uniformRadius)), visualDensity: VisualDensity.compact, ), onPressed: () async { await _handleStatusChange(item, 'en_consulta'); _openPatient(item.id); }, icon: const Icon(Icons.arrow_forward_rounded, size: 12), label: const Text('Atender', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700)), ), ), ] else ...[ const SizedBox(width: 4), Tooltip( message: 'Quitar de la sala de espera', child: InkWell( onTap: () => _handleStatusChange(item, 'no_presente'), borderRadius: BorderRadius.circular(uniformRadius), child: Container( height: uniformHeight, width: uniformHeight, alignment: Alignment.center, child: const Icon(Icons.close_rounded, size: 15, color: AppColors.textMuted), ), ), ), ], ], ); } if (item.isEnConsulta) { return Row( mainAxisSize: MainAxisSize.min, children: [ InkWell( onTap: () => _openPatient(item.id), borderRadius: BorderRadius.circular(uniformRadius), child: Container( height: uniformHeight, padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( color: const Color(0xFFE0F2FE), borderRadius: BorderRadius.circular(uniformRadius), border: Border.all(color: const Color(0xFF7DD3FC)), ), alignment: Alignment.center, child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.medical_services_rounded, size: 12, color: Color(0xFF0369A1)), SizedBox(width: 4), Text( 'En Consulta', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: Color(0xFF0369A1)), ), ], ), ), ), if (!widget.isSecretary) ...[ const SizedBox(width: 6), SizedBox( height: uniformHeight, child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF0284C7), foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(horizontal: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(uniformRadius)), visualDensity: VisualDensity.compact, ), onPressed: () => _handleStatusChange(item, 'atendido'), icon: const Icon(Icons.check_rounded, size: 13), label: const Text('Finalizar', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700)), ), ), ], ], ); } if (item.isAtendido) { return Container( height: uniformHeight, padding: const EdgeInsets.symmetric(horizontal: 9), decoration: BoxDecoration( color: const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(uniformRadius), border: Border.all(color: const Color(0xFFE2E8F0)), ), alignment: Alignment.center, child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.check_circle_rounded, size: 12, color: Color(0xFF64748B)), SizedBox(width: 4), Text( 'Atendido', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w600, color: Color(0xFF64748B)), ), ], ), ); } // no_presente return Row( mainAxisSize: MainAxisSize.min, children: [ if (widget.isSecretary) SizedBox( height: uniformHeight, child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF10B981), foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric(horizontal: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(uniformRadius)), visualDensity: VisualDensity.compact, ), onPressed: () => _handleStatusChange(item, 'en_espera'), icon: const Icon(Icons.how_to_reg_rounded, size: 13), label: const Text('Llegó', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700)), ), ), ], ); } Widget _buildBottomBar() { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: const BoxDecoration( color: Color(0xFFF8FAFC), border: Border(top: BorderSide(color: Color(0xFFE2E8F0))), ), child: Row( children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(6), border: Border.all(color: const Color(0xFFE2E8F0)), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.keyboard_outlined, size: 12, color: AppColors.textMuted), SizedBox(width: 4), Text( 'ESC para salir', style: TextStyle(fontSize: 11, color: AppColors.textMuted, fontWeight: FontWeight.w500), ), ], ), ), const Spacer(), ElevatedButton( onPressed: () => Navigator.of(context).pop(), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: const Text( 'Cerrar', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600), ), ), ], ), ); } }