From 5f7498b755c900f76e49dc9f61b5fcafa2775530 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 25 Apr 2026 10:38:41 -0500 Subject: [PATCH] fix: DataManager.updateTask seeds _allTasks when cache is empty (gitea#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the silent no-op when _allTasks is null on first launch (the onboarding bulkCreateTasks path). The function now upserts: builds an empty kanban shell with the standard column names if needed and places the task in its target column. Unknown column names append a new column at the end so the task is always reachable. Also drops the second branch that conditionally wrote to _tasksByResidence — that cache is being deleted in Phase 3 and updateTask should not maintain it any more. The Phase 1 unit tests now pass; the Phase 2 force-refresh in the next commit replaces the placeholder column metadata (display names, colors, icons) with authoritative server values. --- .../com/tt/honeyDue/data/DataManager.kt | 77 +++++++++++-------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt index 988028b..fe42461 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt @@ -504,45 +504,60 @@ object DataManager : IDataManager { * Also refreshes the summary from the updated kanban data. */ fun updateTask(task: TaskResponse) { - // Update in allTasks - _allTasks.value?.let { current -> - val targetColumn = task.kanbanColumn ?: "upcoming_tasks" - val newColumns = current.columns.map { column -> - // Remove task from this column if present - val filteredTasks = column.tasks.filter { it.id != task.id } - // Add task if this is the target column - val updatedTasks = if (column.name == targetColumn) { - filteredTasks + task - } else { - filteredTasks - } - column.copy(tasks = updatedTasks, count = updatedTasks.size) - } - _allTasks.value = current.copy(columns = newColumns) - } + val targetColumn = task.kanbanColumn ?: "upcoming_tasks" - // Update in tasksByResidence if this task's residence is cached - task.residenceId?.let { residenceId -> - _tasksByResidence.value[residenceId]?.let { current -> - val targetColumn = task.kanbanColumn ?: "upcoming_tasks" - val newColumns = current.columns.map { column -> - val filteredTasks = column.tasks.filter { it.id != task.id } - val updatedTasks = if (column.name == targetColumn) { - filteredTasks + task - } else { - filteredTasks - } - column.copy(tasks = updatedTasks, count = updatedTasks.size) - } - _tasksByResidence.value = _tasksByResidence.value + (residenceId to current.copy(columns = newColumns)) - } + // Upsert into _allTasks. Crucially, when _allTasks is null (fresh + // launch, kanban never fetched — the gitea#2 bug scenario), seed + // an empty kanban shell so the new task isn't silently dropped. + // The Phase 2 force-refresh after bulkCreateTasks/createTask will + // replace this shell with authoritative server data shortly. + val current = _allTasks.value ?: emptyKanbanShell() + val columnsWithTarget = if (current.columns.any { it.name == targetColumn }) { + current.columns + } else { + // Server returned a kanban_column the client doesn't know about + // yet — append it so the task is still reachable. + current.columns + emptyColumn(targetColumn) } + val newColumns = columnsWithTarget.map { column -> + val filteredTasks = column.tasks.filter { it.id != task.id } + val updatedTasks = if (column.name == targetColumn) filteredTasks + task else filteredTasks + column.copy(tasks = updatedTasks, count = updatedTasks.size) + } + _allTasks.value = current.copy(columns = newColumns) // Refresh summary from updated kanban data (API no longer returns summaries for CRUD) refreshSummaryFromKanban() persistToDisk() } + /// Default kanban skeleton used when `_allTasks` was never populated. + /// Display metadata is intentionally placeholder — the Phase 2 force-refresh + /// in `APILayer.bulkCreateTasks` / `createTask` replaces these shortly with + /// authoritative server values. The `name` field is the contract — every + /// observer keys off it. + private fun emptyKanbanShell(): TaskColumnsResponse = TaskColumnsResponse( + columns = listOf( + emptyColumn("overdue_tasks"), + emptyColumn("due_soon_tasks"), + emptyColumn("in_progress_tasks"), + emptyColumn("upcoming_tasks"), + emptyColumn("completed_tasks") + ), + daysThreshold = 30, + residenceId = "" + ) + + private fun emptyColumn(name: String): TaskColumn = TaskColumn( + name = name, + displayName = "", + buttonTypes = emptyList(), + icons = emptyMap(), + color = "", + tasks = emptyList(), + count = 0 + ) + fun removeTask(taskId: Int) { // Remove from allTasks _allTasks.value?.let { current ->