import 'package:intl/intl.dart'; import 'package:prosapp_web_app/models/profesional.dart'; import 'package:prosapp_web_app/models/schedules_entity.dart'; import 'package:prosapp_web_app/models/service.dart'; import 'package:prosapp_web_app/providers/auth_provider.dart'; import 'package:prosapp_web_app/providers/professional_form_provider.dart'; import 'package:prosapp_web_app/providers/professional_provider.dart'; import 'package:prosapp_web_app/providers/services_provider.dart'; import 'package:prosapp_web_app/utils/time_of_day_utils.dart'; import 'package:provider/provider.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:flutter/material.dart'; const _kPrimary = Color(0xFF1565C0); const _kAvailable = Color(0xFF16A34A); const _kOccupied = Color(0xFFDC2626); extension _Th on BuildContext { ThemeData get _t => Theme.of(this); Color get bg => _t.scaffoldBackgroundColor; Color get card => _t.cardColor; Color get onSurface => _t.colorScheme.onSurface; Color get muted => _t.colorScheme.onSurface.withOpacity(0.55); Color get subtle => _t.colorScheme.onSurface.withOpacity(0.35); bool get isDark => _t.brightness == Brightness.dark; Color get shadow => isDark ? Colors.transparent : Colors.black.withOpacity(0.07); Color get shadowSm => isDark ? Colors.transparent : Colors.black.withOpacity(0.04); } class ProfessionalCalendarView extends StatefulWidget { const ProfessionalCalendarView({super.key}); @override State createState() => _ProfessionalCalendarViewState(); } class _ProfessionalCalendarViewState extends State { DateTime _selected = DateTime.now(); List? _services; bool _loading = true; @override void initState() { super.initState(); _load(); } Future _load() async { setState(() => _loading = true); final auth = Provider.of(context, listen: false); final fp = Provider.of(context, listen: false); final sp = Provider.of(context, listen: false); final pro = await Provider.of(context, listen: false) .getProfessional(auth.user!.id); fp.setProfesional(pro); final services = await sp.getServicesForProfessional(pro.id); if (mounted) setState(() { _services = services; _loading = false; }); } void _onDaySelected(DateTime day, DateTime _) => setState(() => _selected = day); @override Widget build(BuildContext context) { return Container( color: context.bg, child: Consumer( builder: (context, fp, _) { if (fp.profesional == null || _loading) { return const Center(child: CircularProgressIndicator(color: _kPrimary)); } final pro = fp.profesional!; final schedule = _scheduleFor(_selected.weekday, pro); final slots = _buildSlots(schedule); final occupied = slots.where((t) => _isOccupied(t, _services, _selected)).length; return ListView( physics: const ClampingScrollPhysics(), padding: const EdgeInsets.only(bottom: 32), children: [ _calendarCard(context), _dayHeader(context, schedule, slots.length, occupied), if (slots.isEmpty) _emptyState(context) else ..._slotCards(context, slots), ], ); }, ), ); } Widget _calendarCard(BuildContext context) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Container( margin: const EdgeInsets.fromLTRB(16, 20, 16, 0), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: context.shadow, blurRadius: 16, offset: const Offset(0, 4))], ), child: ClipRRect( borderRadius: BorderRadius.circular(16), child: TableCalendar( locale: 'es_CO', firstDay: DateTime.now().subtract(const Duration(days: 365)), lastDay: DateTime.utc(2030, 12, 31), focusedDay: _selected, availableGestures: AvailableGestures.all, onDaySelected: _onDaySelected, selectedDayPredicate: (d) => isSameDay(d, _selected), calendarStyle: CalendarStyle( todayDecoration: BoxDecoration( border: Border.all(color: _kPrimary, width: 2), shape: BoxShape.circle), todayTextStyle: const TextStyle(color: _kPrimary, fontWeight: FontWeight.w700), selectedDecoration: const BoxDecoration(color: _kPrimary, shape: BoxShape.circle), selectedTextStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700), weekendTextStyle: TextStyle(color: Colors.red.shade400), defaultTextStyle: TextStyle(color: context.onSurface), outsideDaysVisible: false, ), headerStyle: HeaderStyle( formatButtonVisible: false, titleCentered: true, titleTextStyle: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: context.onSurface), leftChevronIcon: const Icon(Icons.chevron_left, color: _kPrimary), rightChevronIcon: const Icon(Icons.chevron_right, color: _kPrimary), ), daysOfWeekStyle: DaysOfWeekStyle( weekdayStyle: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: context.muted), weekendStyle: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFFEF4444)), ), ), ), ), ), ); } Widget _dayHeader(BuildContext context, ScheduleEntity? schedule, int total, int occupied) { final dayName = DateFormat('EEEE', 'es').format(_selected); final dateStr = DateFormat('d MMMM yyyy', 'es').format(_selected); final available = total - occupied; final hasSchedule = schedule != null && schedule.enabled; return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Container( margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 12, offset: const Offset(0, 2))], ), child: Row(children: [ Container( width: 48, height: 52, decoration: BoxDecoration(color: _kPrimary, borderRadius: BorderRadius.circular(12)), child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Text(DateFormat('d').format(_selected), style: const TextStyle(color: Colors.white, fontSize: 20, fontWeight: FontWeight.w800, height: 1)), Text(DateFormat('MMM', 'es').format(_selected).toUpperCase(), style: const TextStyle(color: Colors.white70, fontSize: 10, fontWeight: FontWeight.w600)), ]), ), const SizedBox(width: 14), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(_capitalize(dayName), style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700, color: context.onSurface)), Text(dateStr, style: TextStyle(fontSize: 12, color: context.subtle)), ])), if (hasSchedule && total > 0) ...[ _StatPill(label: '$occupied', sublabel: 'ocupadas', color: _kOccupied), const SizedBox(width: 8), _StatPill(label: '$available', sublabel: 'libres', color: _kAvailable), ], ]), ), ), ); } List _slotCards(BuildContext context, List slots) { return slots.map((time) { final occ = _isOccupied(time, _services, _selected); final matchService = occ ? _serviceFor(time, _services, _selected) : null; final color = occ ? _kOccupied : _kAvailable; return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Container( margin: const EdgeInsets.fromLTRB(16, 8, 16, 0), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(14), boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 8, offset: const Offset(0, 2))], ), child: ClipRRect( borderRadius: BorderRadius.circular(14), child: IntrinsicHeight( child: Row(children: [ Container(width: 4, color: color), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Row(children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(8)), child: Text(ScheduleEntity.getFormatTime(time) ?? '', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w800, color: color)), ), const SizedBox(width: 14), Expanded(child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text(occ ? 'Ocupado' : 'Disponible', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: color)), if (matchService != null && matchService.description.isNotEmpty) Text(matchService.description, style: TextStyle(fontSize: 12, color: context.muted), maxLines: 1, overflow: TextOverflow.ellipsis) else if (!occ) Text('Horario libre para nuevas citas', style: TextStyle(fontSize: 11, color: context.subtle)), ], )), Container( width: 32, height: 32, decoration: BoxDecoration( color: color.withOpacity(0.1), shape: BoxShape.circle), child: Icon( occ ? Icons.event_busy_outlined : Icons.event_available_outlined, size: 17, color: color), ), ]), ), ), ]), ), ), ), ), ); }).toList(); } Widget _emptyState(BuildContext context) { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Container( margin: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 24), decoration: BoxDecoration( color: context.card, borderRadius: BorderRadius.circular(16), boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 12, offset: const Offset(0, 2))], ), child: Column(children: [ Container( width: 64, height: 64, decoration: BoxDecoration( color: context.subtle.withOpacity(0.1), shape: BoxShape.circle), child: Icon(Icons.event_busy_outlined, size: 32, color: context.subtle), ), const SizedBox(height: 16), Text('Sin horario este día', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: context.muted)), const SizedBox(height: 6), Text('No tienes horario de atención configurado\npara este día de la semana.', textAlign: TextAlign.center, style: TextStyle(fontSize: 13, color: context.subtle, height: 1.5)), ]), ), ), ); } ScheduleEntity? _scheduleFor(int weekday, Profesional pro) => switch (weekday) { 1 => pro.schedules.monday, 2 => pro.schedules.tuesday, 3 => pro.schedules.wednesday, 4 => pro.schedules.thursday, 5 => pro.schedules.friday, 6 => pro.schedules.saturday, _ => pro.schedules.sunday, }; List _buildSlots(ScheduleEntity? s) { if (s == null || !s.enabled || s.range1Hour1 == null || s.range2Hour2 == null) return []; if (s.continuousDay) return TimeOfDayUtils.genRanges(s.range1Hour1!, s.range2Hour2!); if (s.range1Hour2 == null || s.range2Hour1 == null) return []; return [ ...TimeOfDayUtils.genRanges(s.range1Hour1!, s.range1Hour2!), ...TimeOfDayUtils.genRanges(s.range2Hour1!, s.range2Hour2!), ]; } bool _isOccupied(TimeOfDay time, List? services, DateTime day) { if (services == null) return false; final dayStr = day.toIso8601String().split('T').first; return services.any((s) => s.day == dayStr && s.range1Hour1 == time); } Service? _serviceFor(TimeOfDay time, List? services, DateTime day) { if (services == null) return null; final dayStr = day.toIso8601String().split('T').first; try { return services.firstWhere((s) => s.day == dayStr && s.range1Hour1 == time); } catch (_) { return null; } } String _capitalize(String s) => s.isEmpty ? s : s[0].toUpperCase() + s.substring(1); } class _StatPill extends StatelessWidget { final String label; final String sublabel; final Color color; const _StatPill({required this.label, required this.sublabel, required this.color}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: color.withOpacity(0.08), borderRadius: BorderRadius.circular(8), border: Border.all(color: color.withOpacity(0.25)), ), child: Column(mainAxisSize: MainAxisSize.min, children: [ Text(label, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: color, height: 1)), Text(sublabel, style: TextStyle(fontSize: 9, color: color.withOpacity(0.8), fontWeight: FontWeight.w600)), ]), ); } }