import '../../../../data/network/i_api_client.dart'; import '../../../../data/cache/cache_store.dart'; import '../../../../core/notification/models/reminder_alarm.dart'; import '../../../../core/notification/services/reminder_reconcile_service.dart'; import '../models/schedule_item_model.dart'; class CalendarService { static const _prefix = '/api/v1/schedule-items'; final IApiClient _apiClient; final CacheInvalidator _invalidator; final ReminderReconcileService? _reminderReconcileService; CalendarService({ required IApiClient apiClient, required CacheInvalidator invalidator, ReminderReconcileService? reminderReconcileService, }) : _apiClient = apiClient, _invalidator = invalidator, _reminderReconcileService = reminderReconcileService; Future> getEventsForDay(DateTime date) async { final start = DateTime(date.year, date.month, date.day); final end = DateTime(date.year, date.month, date.day, 23, 59, 59); return getEventsForRange(start, end); } Future> getEventsForRange( DateTime start, DateTime end, ) async { final startParam = Uri.encodeQueryComponent( start.toUtc().toIso8601String(), ); final endParam = Uri.encodeQueryComponent(end.toUtc().toIso8601String()); final response = await _apiClient.get>( '$_prefix?start_at=$startParam&end_at=$endParam', ); final data = response.data; if (data == null) { throw StateError('Invalid getEventsForRange response: empty payload'); } final events = data .map((item) => item as Map) .map(ScheduleItemModel.fromJson) .toList(growable: false); await _reminderReconcileService?.reconcileEvents( events.map(_toReminderSnapshot).toList(growable: false), ); return events; } Future getEventById( String id, { bool reconcileReminder = true, }) async { final response = await _apiClient.get>('$_prefix/$id'); final data = response.data; if (data == null) { throw StateError('Invalid getEventById response: empty payload'); } final event = ScheduleItemModel.fromJson(data); if (reconcileReminder) { await _reminderReconcileService?.reconcileEvent( _toReminderSnapshot(event), ); } return event; } Future addEvent(ScheduleItemModel event) async { final response = await _apiClient.post>( _prefix, data: event.toCreateJson(), ); final data = response.data; if (data == null) { throw StateError('Invalid addEvent response: empty payload'); } final created = ScheduleItemModel.fromJson(data); _invalidateEventCache(created); await _reminderReconcileService?.reconcileEvent( _toReminderSnapshot(created), ); return created; } Future updateEvent(ScheduleItemModel event) async { final response = await _apiClient.patch>( '$_prefix/${event.id}', data: event.toUpdateJson(), ); final data = response.data; if (data == null) { throw StateError('Invalid updateEvent response: empty payload'); } final updated = ScheduleItemModel.fromJson(data); _invalidateEventCache(updated); await _reminderReconcileService?.reconcileEvent( _toReminderSnapshot(updated), ); return updated; } Future archiveEvent(String id) async { final event = await getEventById(id); final updatedEvent = await updateEvent( event.copyWith(status: ScheduleStatus.archived), ); _invalidateEventCache(updatedEvent); await _reminderReconcileService?.archiveAndCancel(id); return updatedEvent; } Future deleteEvent(String id) async { final event = await getEventById(id); _invalidateEventCache(event); await _apiClient.delete('$_prefix/$id'); await _reminderReconcileService?.archiveAndCancel(id); } void _invalidateEventCache(ScheduleItemModel event) { var current = DateTime( event.startAt.year, event.startAt.month, event.startAt.day, ); final end = DateTime( event.endAt?.year ?? event.startAt.year, event.endAt?.month ?? event.startAt.month, event.endAt?.day ?? event.startAt.day, ); while (!current.isAfter(end)) { _invalidator.invalidateCalendarDay(current); current = current.add(const Duration(days: 1)); } } ReminderEventSnapshot _toReminderSnapshot(ScheduleItemModel event) { return ReminderEventSnapshot( eventId: event.id, title: event.title, startAt: event.startAt, endAt: event.endAt, timezone: event.timezone, reminderMinutes: event.metadata?.reminderMinutes, location: event.metadata?.location, notes: event.metadata?.notes, isArchived: event.status == ScheduleStatus.archived, ); } }