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 _kAccent = Color(0xFF42A4EF); const _kBg = Color(0xFFF4F6FA); const _kCard = Colors.white; const _kAvailable = Color(0xFF16A34A); const _kOccupied = Color(0xFFDC2626); 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: _kBg, 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(), _dayHeader(schedule, slots.length, occupied), if (slots.isEmpty) _emptyState() else ..._slotCards(slots, schedule), ], ); }, ), ); } // ── Calendar card ───────────────────────────────────────────────────────── Widget _calendarCard() { return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Container( margin: const EdgeInsets.fromLTRB(16, 20, 16, 0), decoration: BoxDecoration( color: _kCard, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.07), 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), outsideDaysVisible: false, ), headerStyle: const HeaderStyle( formatButtonVisible: false, titleCentered: true, titleTextStyle: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: Color(0xFF1E293B), ), leftChevronIcon: Icon(Icons.chevron_left, color: _kPrimary), rightChevronIcon: Icon(Icons.chevron_right, color: _kPrimary), ), daysOfWeekStyle: const DaysOfWeekStyle( weekdayStyle: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF64748B), ), weekendStyle: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFFEF4444), ), ), ), ), ), ), ); } // ── Day header + stats ──────────────────────────────────────────────────── Widget _dayHeader( 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: _kCard, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 12, offset: const Offset(0, 2), ) ], ), child: Row( children: [ // Date block 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: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: Color(0xFF1E293B), ), ), Text( dateStr, style: const TextStyle( fontSize: 12, color: Color(0xFF94A3B8), ), ), ], ), ), if (hasSchedule && total > 0) ...[ _StatPill( label: '$occupied', sublabel: 'ocupadas', color: _kOccupied, ), const SizedBox(width: 8), _StatPill( label: '$available', sublabel: 'libres', color: _kAvailable, ), ], ], ), ), ), ); } // ── Slot cards ──────────────────────────────────────────────────────────── List _slotCards(List slots, ScheduleEntity? schedule) { return slots.map((time) { final occupied = _isOccupied(time, _services, _selected); final matchService = occupied ? _serviceFor(time, _services, _selected) : null; return Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 720), child: Container( margin: const EdgeInsets.fromLTRB(16, 8, 16, 0), decoration: BoxDecoration( color: _kCard, borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 2), ) ], ), child: ClipRRect( borderRadius: BorderRadius.circular(14), child: IntrinsicHeight( child: Row( children: [ // Color accent strip Container( width: 4, color: occupied ? _kOccupied : _kAvailable, ), Expanded( child: Padding( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 12), child: Row( children: [ // Time badge Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6), decoration: BoxDecoration( color: (occupied ? _kOccupied : _kAvailable) .withOpacity(0.08), borderRadius: BorderRadius.circular(8), ), child: Text( ScheduleEntity.getFormatTime(time) ?? '', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w800, color: occupied ? _kOccupied : _kAvailable, ), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( occupied ? 'Ocupado' : 'Disponible', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w700, color: occupied ? _kOccupied : _kAvailable, ), ), if (matchService != null && matchService.description.isNotEmpty) Text( matchService.description, style: const TextStyle( fontSize: 12, color: Color(0xFF64748B), ), maxLines: 1, overflow: TextOverflow.ellipsis, ) else if (!occupied) const Text( 'Horario libre para nuevas citas', style: TextStyle( fontSize: 11, color: Color(0xFF94A3B8), ), ), ], ), ), // Status icon Container( width: 32, height: 32, decoration: BoxDecoration( color: (occupied ? _kOccupied : _kAvailable) .withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( occupied ? Icons.event_busy_outlined : Icons.event_available_outlined, size: 17, color: occupied ? _kOccupied : _kAvailable, ), ), ], ), ), ), ], ), ), ), ), ), ); }).toList(); } // ── Empty state ─────────────────────────────────────────────────────────── Widget _emptyState() { 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: _kCard, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 12, offset: const Offset(0, 2), ) ], ), child: Column( children: [ Container( width: 64, height: 64, decoration: BoxDecoration( color: const Color(0xFF94A3B8).withOpacity(0.1), shape: BoxShape.circle, ), child: const Icon(Icons.event_busy_outlined, size: 32, color: Color(0xFF94A3B8)), ), const SizedBox(height: 16), const Text( 'Sin horario este día', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: Color(0xFF475569), ), ), const SizedBox(height: 6), const Text( 'No tienes horario de atención configurado\npara este día de la semana.', textAlign: TextAlign.center, style: TextStyle( fontSize: 13, color: Color(0xFF94A3B8), height: 1.5, ), ), ], ), ), ), ); } // ── Helpers ─────────────────────────────────────────────────────────────── ScheduleEntity? _scheduleFor(int weekday, Profesional pro) { return 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); } // ── Stat pill ───────────────────────────────────────────────────────────── 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, ), ), ], ), ); } }