import '../../core/exceptions.dart'; import '../api/projects_api.dart'; import '../local/database.dart'; import '../models/project.dart'; /// Projects repository with read-through caching (Phase 1+2) and offline-write /// queueing (Phase 3). See `notes_repository.dart` for the full pattern. /// /// `getAll`'s sort/order/status query parameters are server-side filters; the /// cache stores the unfiltered list as last seen. Offline fallback returns /// the full cache regardless of the requested filters — close enough for a /// "view what you had" experience while disconnected. class ProjectsRepository { final ProjectsApi _api; final FabledDatabase _db; const ProjectsRepository(this._api, this._db); Future> getAll({ String? status, String sort = 'updated_at', String order = 'desc', }) async { try { final projects = await _api.getAll(status: status, sort: sort, order: order); // Only refresh the cache on the unfiltered default fetch — otherwise a // status=archived call would clobber the active-projects cache. if (status == null) { await _db.replaceAllProjects(projects); } else { for (final p in projects) { await _db.upsertProject(p); } } return projects; } on NetworkException { final cached = await _db.getAllProjects(); if (cached.isEmpty) rethrow; if (status != null) { return cached.where((p) => p.status == status).toList(); } return cached; } } Future getOne(int id) async { try { final project = await _api.getOne(id); await _db.upsertProject(project); return project; } on NetworkException { final cached = await _db.getProject(id); if (cached == null) rethrow; return cached; } } Future create({ required String title, String? description, String? goal, String? color, String status = 'active', }) async { try { final project = await _api.create( title: title, description: description, goal: goal, color: color, status: status, ); await _db.upsertProject(project); return project; } on NetworkException { final tempId = await _db.nextTempId(); final now = DateTime.now(); final optimistic = Project( id: tempId, title: title, description: description, goal: goal, status: status, color: color, createdAt: now, updatedAt: now, ); await _db.upsertProject(optimistic); await _db.enqueuePending( domain: kSyncDomainProjects, verb: kWriteVerbCreate, tempId: tempId, payload: { 'title': title, 'description': description, 'goal': goal, 'color': color, 'status': status, }, ); return optimistic; } } Future update(int id, Map fields) async { try { final updated = await _api.update(id, fields); await _db.upsertProject(updated); return updated; } on NetworkException { final cached = await _db.getProject(id); if (cached == null) rethrow; final optimistic = _applyProjectFields(cached, fields); await _db.upsertProject(optimistic); if (id < 0) { final queued = await _db.findQueuedCreate(kSyncDomainProjects, id); if (queued != null) { await _db.updatePendingPayload(queued.id, fields); return optimistic; } } await _db.enqueuePending( domain: kSyncDomainProjects, verb: kWriteVerbUpdate, targetId: id, payload: fields, baselineUpdatedAt: cached.updatedAt, ); return optimistic; } } Future delete(int id) async { try { await _api.delete(id); await _db.deleteProject(id); } on NetworkException { if (id < 0) { final queued = await _db.findQueuedCreate(kSyncDomainProjects, id); if (queued != null) await _db.deletePending(queued.id); await _db.deleteProject(id); return; } await _db.deleteProject(id); await _db.enqueuePending( domain: kSyncDomainProjects, verb: kWriteVerbDelete, targetId: id, ); } } } Project _applyProjectFields(Project p, Map f) { return Project( id: p.id, title: f['title'] as String? ?? p.title, description: f.containsKey('description') ? f['description'] as String? : p.description, goal: f.containsKey('goal') ? f['goal'] as String? : p.goal, status: f['status'] as String? ?? p.status, color: f.containsKey('color') ? f['color'] as String? : p.color, autoSummary: p.autoSummary, createdAt: p.createdAt, updatedAt: p.updatedAt, ); }