import '../../core/exceptions.dart'; import '../api/tasks_api.dart'; import '../local/database.dart'; import '../models/task.dart'; /// Tasks repository with read-through caching (Phase 1+2) and offline-write /// queueing (Phase 3). See `notes_repository.dart` for the full pattern. class TasksRepository { final TasksApi _api; final FabledDatabase _db; const TasksRepository(this._api, this._db); Future> getAll() async { try { final tasks = await _api.getAll(); await _db.replaceAllTasks(tasks); return tasks; } on NetworkException { final cached = await _db.getAllTasks(); if (cached.isEmpty) rethrow; return cached; } } Future getOne(int id) async { try { final task = await _api.getOne(id); await _db.upsertTask(task); return task; } on NetworkException { final cached = await _db.getTask(id); if (cached == null) rethrow; return cached; } } Future> getByProject(int projectId) async { try { final tasks = await _api.getByProject(projectId); for (final t in tasks) { await _db.upsertTask(t); } return tasks; } on NetworkException { return _db.getTasksByProject(projectId); } } Future> getSubTasks(int parentId) async { try { final tasks = await _api.getSubTasks(parentId); for (final t in tasks) { await _db.upsertTask(t); } return tasks; } on NetworkException { return _db.getSubTasks(parentId); } } Future create({ required String title, String? description, required TaskStatus status, required TaskPriority priority, DateTime? dueDate, int? projectId, int? parentId, }) async { try { final task = await _api.create( title: title, description: description, status: status, priority: priority, dueDate: dueDate, projectId: projectId, parentId: parentId, ); await _db.upsertTask(task); return task; } on NetworkException { final tempId = await _db.nextTempId(); final now = DateTime.now(); final optimistic = Task( id: tempId, title: title, description: description, status: status, priority: priority, dueDate: dueDate, projectId: projectId, parentId: parentId, createdAt: now, updatedAt: now, ); await _db.upsertTask(optimistic); await _db.enqueuePending( domain: kSyncDomainTasks, verb: kWriteVerbCreate, tempId: tempId, payload: { 'title': title, 'description': description, 'status': status.value, 'priority': priority.value, 'due_date': dueDate?.toIso8601String(), 'project_id': projectId, 'parent_id': parentId, }, ); return optimistic; } } Future update(int id, Map fields) async { try { final updated = await _api.update(id, fields); await _db.upsertTask(updated); return updated; } on NetworkException { final cached = await _db.getTask(id); if (cached == null) rethrow; final optimistic = _applyTaskFields(cached, fields); await _db.upsertTask(optimistic); if (id < 0) { final queued = await _db.findQueuedCreate(kSyncDomainTasks, id); if (queued != null) { await _db.updatePendingPayload(queued.id, fields); return optimistic; } } await _db.enqueuePending( domain: kSyncDomainTasks, verb: kWriteVerbUpdate, targetId: id, payload: fields, baselineUpdatedAt: cached.updatedAt, ); return optimistic; } } Future delete(int id) async { try { await _api.delete(id); await _db.deleteTask(id); } on NetworkException { if (id < 0) { final queued = await _db.findQueuedCreate(kSyncDomainTasks, id); if (queued != null) await _db.deletePending(queued.id); await _db.deleteTask(id); return; } await _db.deleteTask(id); await _db.enqueuePending( domain: kSyncDomainTasks, verb: kWriteVerbDelete, targetId: id, ); } } } /// Applies a partial-fields map (the same shape sent to the PUT endpoint) /// to a cached Task so the UI can show the edit immediately. Task _applyTaskFields(Task t, Map f) { return Task( id: t.id, title: f['title'] as String? ?? t.title, description: f.containsKey('body') ? f['body'] as String? : t.description, status: f.containsKey('status') ? TaskStatusExtension.fromString(f['status'] as String?) : t.status, priority: f.containsKey('priority') ? TaskPriorityExtension.fromString(f['priority'] as String?) : t.priority, dueDate: f.containsKey('due_date') ? (f['due_date'] is String ? DateTime.tryParse(f['due_date'] as String) : null) : t.dueDate, projectId: f.containsKey('project_id') ? f['project_id'] as int? : t.projectId, milestoneId: f.containsKey('milestone_id') ? f['milestone_id'] as int? : t.milestoneId, parentId: f.containsKey('parent_id') ? f['parent_id'] as int? : t.parentId, createdAt: t.createdAt, updatedAt: t.updatedAt, ); }