import '../../core/exceptions.dart'; import '../api/milestones_api.dart'; import '../local/database.dart'; import '../models/milestone.dart'; /// Milestones repository with read-through caching (Phase 1+2) and /// offline-write queueing (Phase 3). See `notes_repository.dart` for the /// full pattern. class MilestonesRepository { final MilestonesApi _api; final FabledDatabase _db; const MilestonesRepository(this._api, this._db); Future> getAll(int projectId, {String? status}) async { try { final milestones = await _api.getAll(projectId, status: status); // Only mirror the full per-project list to cache on the unfiltered // fetch; a status filter would otherwise drop entries from cache. if (status == null) { await _db.replaceMilestonesForProject(projectId, milestones); } else { for (final m in milestones) { await _db.upsertMilestone(m); } } return milestones; } on NetworkException { final cached = await _db.getMilestonesForProject(projectId); if (cached.isEmpty) rethrow; if (status != null) { return cached.where((m) => m.status == status).toList(); } return cached; } } Future create( int projectId, { required String title, String? description, int orderIndex = 0, }) async { try { final milestone = await _api.create( projectId, title: title, description: description, orderIndex: orderIndex, ); await _db.upsertMilestone(milestone); return milestone; } on NetworkException { final tempId = await _db.nextTempId(); final now = DateTime.now(); final optimistic = Milestone( id: tempId, projectId: projectId, title: title, description: description, status: 'active', orderIndex: orderIndex, total: 0, completed: 0, pct: 0.0, createdAt: now, updatedAt: now, ); await _db.upsertMilestone(optimistic); await _db.enqueuePending( domain: kSyncDomainMilestones, verb: kWriteVerbCreate, tempId: tempId, payload: { 'project_id': projectId, 'title': title, 'description': description, 'order_index': orderIndex, }, ); return optimistic; } } Future update( int projectId, int milestoneId, Map fields, ) async { try { final updated = await _api.update(projectId, milestoneId, fields); await _db.upsertMilestone(updated); return updated; } on NetworkException { final cached = (await _db.getMilestonesForProject(projectId)) .where((m) => m.id == milestoneId) .firstOrNull; if (cached == null) rethrow; final optimistic = _applyMilestoneFields(cached, fields); await _db.upsertMilestone(optimistic); if (milestoneId < 0) { final queued = await _db.findQueuedCreate(kSyncDomainMilestones, milestoneId); if (queued != null) { // Carry project_id through coalesced payload — replay needs it. final merged = { 'project_id': projectId, ...fields, }; await _db.updatePendingPayload(queued.id, merged); return optimistic; } } await _db.enqueuePending( domain: kSyncDomainMilestones, verb: kWriteVerbUpdate, targetId: milestoneId, payload: {'project_id': projectId, ...fields}, baselineUpdatedAt: cached.updatedAt, ); return optimistic; } } Future delete(int projectId, int milestoneId) async { try { await _api.delete(projectId, milestoneId); await _db.deleteMilestone(milestoneId); } on NetworkException { if (milestoneId < 0) { final queued = await _db.findQueuedCreate(kSyncDomainMilestones, milestoneId); if (queued != null) await _db.deletePending(queued.id); await _db.deleteMilestone(milestoneId); return; } await _db.deleteMilestone(milestoneId); await _db.enqueuePending( domain: kSyncDomainMilestones, verb: kWriteVerbDelete, targetId: milestoneId, payload: {'project_id': projectId}, ); } } } Milestone _applyMilestoneFields(Milestone m, Map f) { return Milestone( id: m.id, projectId: m.projectId, title: f['title'] as String? ?? m.title, description: f.containsKey('description') ? f['description'] as String? : m.description, status: f['status'] as String? ?? m.status, orderIndex: f['order_index'] as int? ?? m.orderIndex, total: m.total, completed: m.completed, pct: m.pct, createdAt: m.createdAt, updatedAt: m.updatedAt, ); }