import '../../core/exceptions.dart'; import '../api/events_api.dart'; import '../local/database.dart'; import '../models/calendar_event.dart'; /// Calendar events repository with read-through caching (Phase 1+2) and /// offline-write queueing (Phase 3). Range-scoped reads: `getEvents(from, to)` /// mirrors that window into the cache (events outside the window are left /// alone) so successive disjoint range fetches don't clobber each other. /// /// CalendarEvent has no `updatedAt` field, so write replay uses last- /// writer-wins on update/delete (no server-side baseline check). class EventsRepository { final EventsApi _api; final FabledDatabase _db; const EventsRepository(this._api, this._db); Future> getEvents(DateTime from, DateTime to) async { try { final events = await _api.getEvents(from, to); await _db.replaceEventsInRange(from, to, events); return events; } on NetworkException { final cached = await _db.getEventsInRange(from, to); if (cached.isEmpty) rethrow; return cached; } } Future createEvent(Map payload) async { try { final event = await _api.createEvent(payload); await _db.upsertEvent(event); return event; } on NetworkException { final tempId = await _db.nextTempId(); final optimistic = _eventFromPayload(tempId, payload); await _db.upsertEvent(optimistic); await _db.enqueuePending( domain: kSyncDomainEvents, verb: kWriteVerbCreate, tempId: tempId, payload: payload, ); return optimistic; } } Future updateEvent( int id, Map fields) async { try { final updated = await _api.updateEvent(id, fields); await _db.upsertEvent(updated); return updated; } on NetworkException { final cached = (await _db.getEventsInRange( DateTime.fromMicrosecondsSinceEpoch(0), DateTime.now().add(const Duration(days: 365 * 100)), )) .where((e) => e.id == id) .firstOrNull; if (cached == null) rethrow; final optimistic = _applyEventFields(cached, fields); await _db.upsertEvent(optimistic); if (id < 0) { final queued = await _db.findQueuedCreate(kSyncDomainEvents, id); if (queued != null) { await _db.updatePendingPayload(queued.id, fields); return optimistic; } } await _db.enqueuePending( domain: kSyncDomainEvents, verb: kWriteVerbUpdate, targetId: id, payload: fields, ); return optimistic; } } Future deleteEvent(int id) async { try { await _api.deleteEvent(id); await _db.deleteEvent(id); } on NetworkException { if (id < 0) { final queued = await _db.findQueuedCreate(kSyncDomainEvents, id); if (queued != null) await _db.deletePending(queued.id); await _db.deleteEvent(id); return; } await _db.deleteEvent(id); await _db.enqueuePending( domain: kSyncDomainEvents, verb: kWriteVerbDelete, targetId: id, ); } } } CalendarEvent _eventFromPayload(int id, Map p) { return CalendarEvent( id: id, title: p['title'] as String? ?? '', startDt: _parseIso(p['start_dt'])!, endDt: _parseIso(p['end_dt']), allDay: p['all_day'] as bool? ?? false, description: p['description'] as String? ?? '', location: p['location'] as String? ?? '', color: p['color'] as String? ?? '', recurrence: p['recurrence'] as String?, projectId: p['project_id'] as int?, reminderMinutes: p['reminder_minutes'] as int?, ); } CalendarEvent _applyEventFields(CalendarEvent e, Map f) { return CalendarEvent( id: e.id, title: f['title'] as String? ?? e.title, startDt: _parseIso(f['start_dt']) ?? e.startDt, endDt: f.containsKey('end_dt') ? _parseIso(f['end_dt']) : e.endDt, allDay: f['all_day'] as bool? ?? e.allDay, description: f['description'] as String? ?? e.description, location: f['location'] as String? ?? e.location, color: f['color'] as String? ?? e.color, recurrence: f.containsKey('recurrence') ? f['recurrence'] as String? : e.recurrence, projectId: f.containsKey('project_id') ? f['project_id'] as int? : e.projectId, reminderMinutes: f.containsKey('reminder_minutes') ? f['reminder_minutes'] as int? : e.reminderMinutes, ); } DateTime? _parseIso(dynamic raw) { if (raw is String && raw.isNotEmpty) return DateTime.tryParse(raw)?.toLocal(); return null; }