feat: add CalendarNotifier and calendarProvider
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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<DateTime, List<CalendarEvent>> 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<DateTime, List<CalendarEvent>>? 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, CalendarState>(CalendarNotifier.new);
|
||||
|
||||
class CalendarNotifier extends AsyncNotifier<CalendarState> {
|
||||
@override
|
||||
Future<CalendarState> 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(eventsApiProvider).getEvents(from, to);
|
||||
return CalendarState(
|
||||
eventsByDay: _groupByDay(events),
|
||||
selectedDay: today,
|
||||
focusedMonth: DateTime(now.year, now.month),
|
||||
loadedRange: DateTimeRange(start: from, end: to),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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<void> 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(eventsApiProvider).getEvents(from, to);
|
||||
final s = state.value!;
|
||||
final merged = Map<DateTime, List<CalendarEvent>>.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<DateTime, List<CalendarEvent>>.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<DateTime, List<CalendarEvent>>.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<DateTime, List<CalendarEvent>>.from(current.eventsByDay);
|
||||
byDay[key] = (byDay[key] ?? []).where((e) => e.id != id).toList();
|
||||
state = AsyncData(current.copyWith(eventsByDay: byDay));
|
||||
}
|
||||
}
|
||||
|
||||
Map<DateTime, List<CalendarEvent>> _groupByDay(List<CalendarEvent> events) {
|
||||
final byDay = <DateTime, List<CalendarEvent>>{};
|
||||
for (final e in events) {
|
||||
final key = dateOnly(e.startDt);
|
||||
(byDay[key] ??= []).add(e);
|
||||
}
|
||||
return byDay;
|
||||
}
|
||||
Reference in New Issue
Block a user