fix: DataManager.updateTask seeds _allTasks when cache is empty (gitea#2)

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.
This commit is contained in:
Trey t
2026-04-25 10:38:41 -05:00
parent c9d5c048b7
commit 5d0c3597fa
@@ -480,45 +480,60 @@ object DataManager {
* Also refreshes the summary from the updated kanban data. * Also refreshes the summary from the updated kanban data.
*/ */
fun updateTask(task: TaskResponse) { fun updateTask(task: TaskResponse) {
// Update in allTasks val targetColumn = task.kanbanColumn ?: "upcoming_tasks"
_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)
}
// Update in tasksByResidence if this task's residence is cached // Upsert into _allTasks. Crucially, when _allTasks is null (fresh
task.residenceId?.let { residenceId -> // launch, kanban never fetched — the gitea#2 bug scenario), seed
_tasksByResidence.value[residenceId]?.let { current -> // an empty kanban shell so the new task isn't silently dropped.
val targetColumn = task.kanbanColumn ?: "upcoming_tasks" // The Phase 2 force-refresh after bulkCreateTasks/createTask will
val newColumns = current.columns.map { column -> // replace this shell with authoritative server data shortly.
val filteredTasks = column.tasks.filter { it.id != task.id } val current = _allTasks.value ?: emptyKanbanShell()
val updatedTasks = if (column.name == targetColumn) { val columnsWithTarget = if (current.columns.any { it.name == targetColumn }) {
filteredTasks + task current.columns
} else { } else {
filteredTasks // Server returned a kanban_column the client doesn't know about
} // yet — append it so the task is still reachable.
column.copy(tasks = updatedTasks, count = updatedTasks.size) current.columns + emptyColumn(targetColumn)
}
_tasksByResidence.value = _tasksByResidence.value + (residenceId to current.copy(columns = newColumns))
}
} }
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) // Refresh summary from updated kanban data (API no longer returns summaries for CRUD)
refreshSummaryFromKanban() refreshSummaryFromKanban()
persistToDisk() 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) { fun removeTask(taskId: Int) {
// Remove from allTasks // Remove from allTasks
_allTasks.value?.let { current -> _allTasks.value?.let { current ->