import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../data/models/calendar_event.dart'; import 'api_client_provider.dart'; // ─── CalendarState ──────────────────────────────────────────────────────────── class CalendarState { final Map> eventsByDay; final DateTime selectedDay; final DateTime focusedMonth; final DateTimeRange loadedRange; const CalendarState({ required this.eventsByDay, required this.selectedDay, required this.focusedMonth, required this.loadedRange, }); CalendarState copyWith({ Map>? eventsByDay, DateTime? selectedDay, DateTime? focusedMonth, DateTimeRange? loadedRange, }) { return CalendarState( eventsByDay: eventsByDay ?? this.eventsByDay, selectedDay: selectedDay ?? this.selectedDay, focusedMonth: focusedMonth ?? this.focusedMonth, loadedRange: loadedRange ?? this.loadedRange, ); } } // ─── CalendarNotifier ───────────────────────────────────────────────────────── final calendarProvider = AsyncNotifierProvider(CalendarNotifier.new); class CalendarNotifier extends AsyncNotifier { @override Future build() async { final now = DateTime.now(); final today = dateOnly(now); // Fetch current month ± 1 month as the initial window. final from = DateTime(now.year, now.month - 1, 1); final to = DateTime(now.year, now.month + 2, 0, 23, 59, 59); final events = await ref.watch(eventsRepositoryProvider).getEvents(from, to); return CalendarState( eventsByDay: _groupByDay(events), selectedDay: today, focusedMonth: DateTime(now.year, now.month), loadedRange: DateTimeRange(start: from, end: to), ); } /// Re-fetch events for the current range without clearing state (no flicker). Future refresh() async { final current = state.value; if (current == null) return; final events = await ref.read(eventsRepositoryProvider).getEvents( current.loadedRange.start, current.loadedRange.end, ); state = AsyncData(current.copyWith(eventsByDay: _groupByDay(events))); } /// Synchronously updates selectedDay and focusedMonth. No API call. void selectDay(DateTime day) { final current = state.value; if (current == null) return; state = AsyncData(current.copyWith( selectedDay: dateOnly(day), focusedMonth: DateTime(day.year, day.month), )); } /// Updates focusedMonth. Fetches events for [month] if not already loaded. Future loadMonth(DateTime month) async { final current = state.value; if (current == null) return; final focused = DateTime(month.year, month.month); // Always update focusedMonth so TableCalendar shows the right page. state = AsyncData(current.copyWith(focusedMonth: focused)); // Skip fetch if the first day of [month] is within the already-loaded range. final monthStart = DateTime(month.year, month.month, 1); if (!monthStart.isBefore(current.loadedRange.start) && !monthStart.isAfter(current.loadedRange.end)) { return; } try { final from = DateTime(month.year, month.month, 1); final to = DateTime(month.year, month.month + 1, 0, 23, 59, 59); final events = await ref.read(eventsRepositoryProvider).getEvents(from, to); final s = state.value!; final merged = Map>.from(s.eventsByDay); for (final e in events) { final key = dateOnly(e.startDt); (merged[key] ??= []).add(e); } final newStart = from.isBefore(s.loadedRange.start) ? from : s.loadedRange.start; final newEnd = to.isAfter(s.loadedRange.end) ? to : s.loadedRange.end; state = AsyncData(s.copyWith( eventsByDay: merged, loadedRange: DateTimeRange(start: newStart, end: newEnd), )); } catch (_) { // Failures are silent — already-loaded months remain visible. } } /// Inserts [event] into eventsByDay after a successful createEvent API call. void addEvent(CalendarEvent event) { final current = state.value; if (current == null) return; final key = dateOnly(event.startDt); final updated = Map>.from(current.eventsByDay); (updated[key] ??= []).add(event); state = AsyncData(current.copyWith(eventsByDay: updated)); } /// Replaces the old entry for [updated.id] with [updated] after a successful /// updateEvent API call. Scans all buckets to handle date changes. void updateEvent(CalendarEvent updated) { final current = state.value; if (current == null) return; final byDay = Map>.from(current.eventsByDay); for (final key in byDay.keys) { byDay[key] = byDay[key]!.where((e) => e.id != updated.id).toList(); } final newKey = dateOnly(updated.startDt); (byDay[newKey] ??= []).add(updated); state = AsyncData(current.copyWith(eventsByDay: byDay)); } /// Removes event [id] from [date]'s bucket after a successful deleteEvent /// API call. void removeEvent(int id, DateTime date) { final current = state.value; if (current == null) return; final key = dateOnly(date); final byDay = Map>.from(current.eventsByDay); byDay[key] = (byDay[key] ?? []).where((e) => e.id != id).toList(); state = AsyncData(current.copyWith(eventsByDay: byDay)); } } Map> _groupByDay(List events) { final byDay = >{}; for (final e in events) { final key = dateOnly(e.startDt); (byDay[key] ??= []).add(e); } return byDay; }