Centralize kanban state in TaskViewModel to eliminate duplicate code

Move tasksResponse state and updateTaskInKanban logic from individual views
into TaskViewModel. Both AllTasksView and ResidenceDetailView now delegate
to the shared ViewModel, reducing code duplication and ensuring consistent
task state management across the app.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-02 22:07:52 -06:00
parent 00e303c3be
commit b79fda8aee
3 changed files with 168 additions and 169 deletions

View File

@@ -6,10 +6,11 @@ struct ResidenceDetailView: View {
@StateObject private var viewModel = ResidenceViewModel()
@StateObject private var taskViewModel = TaskViewModel()
@State private var tasksResponse: TaskColumnsResponse?
@State private var isLoadingTasks = false
@State private var tasksError: String?
// Use TaskViewModel's state instead of local state
private var tasksResponse: TaskColumnsResponse? { taskViewModel.tasksResponse }
private var isLoadingTasks: Bool { taskViewModel.isLoadingTasks }
private var tasksError: String? { taskViewModel.tasksError }
@State private var contractors: [ContractorSummary] = []
@State private var isLoadingContractors = false
@@ -377,89 +378,11 @@ private extension ResidenceDetailView {
}
func loadResidenceTasks(forceRefresh: Bool = false) {
guard TokenStorage.shared.getToken() != nil else { return }
isLoadingTasks = true
tasksError = nil
Task {
do {
let result = try await APILayer.shared.getTasksByResidence(
residenceId: Int32(Int(residenceId)),
forceRefresh: forceRefresh
)
await MainActor.run {
if let successResult = result as? ApiResultSuccess<TaskColumnsResponse> {
self.tasksResponse = successResult.data
self.isLoadingTasks = false
} else if let errorResult = result as? ApiResultError {
self.tasksError = errorResult.message
self.isLoadingTasks = false
} else {
self.tasksError = "Failed to load tasks"
self.isLoadingTasks = false
}
}
} catch {
await MainActor.run {
self.tasksError = error.localizedDescription
self.isLoadingTasks = false
}
}
}
taskViewModel.loadTasks(residenceId: residenceId, forceRefresh: forceRefresh)
}
/// Updates a task in the kanban board by moving it to the correct column based on kanban_column
func updateTaskInKanban(_ updatedTask: TaskResponse) {
print("DEBUG: updateTaskInKanban called")
guard let currentResponse = tasksResponse else {
print("DEBUG: tasksResponse is nil, returning")
return
}
let targetColumn = updatedTask.kanbanColumn ?? "completed_tasks"
print("DEBUG: targetColumn = \(targetColumn)")
// Build new columns array
var newColumns: [TaskColumn] = []
for column in currentResponse.columns {
print("DEBUG: Processing column: \(column.name)")
// Remove task from this column if it exists
var filteredTasks = column.tasks.filter { $0.id != updatedTask.id }
let removed = column.tasks.count - filteredTasks.count
if removed > 0 {
print("DEBUG: Removed \(removed) task(s) from \(column.name)")
}
// Add task to target column
if column.name == targetColumn {
filteredTasks.append(updatedTask)
print("DEBUG: Added task to \(column.name)")
}
// Create new column with updated tasks and count
let newColumn = TaskColumn(
name: column.name,
displayName: column.displayName,
buttonTypes: column.buttonTypes,
icons: column.icons,
color: column.color,
tasks: filteredTasks,
count: Int32(filteredTasks.count)
)
newColumns.append(newColumn)
}
// Update the response
print("DEBUG: Updating tasksResponse with new columns")
tasksResponse = TaskColumnsResponse(
columns: newColumns,
daysThreshold: currentResponse.daysThreshold,
residenceId: currentResponse.residenceId
)
print("DEBUG: tasksResponse updated")
taskViewModel.updateTaskInKanban(updatedTask)
}
func deleteResidence() {

View File

@@ -5,9 +5,6 @@ struct AllTasksView: View {
@StateObject private var taskViewModel = TaskViewModel()
@StateObject private var residenceViewModel = ResidenceViewModel()
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
@State private var tasksResponse: TaskColumnsResponse?
@State private var isLoadingTasks = false
@State private var tasksError: String?
@State private var showAddTask = false
@State private var showEditTask = false
@State private var showingUpgradePrompt = false
@@ -20,20 +17,13 @@ struct AllTasksView: View {
@State private var selectedTaskForCancel: TaskResponse?
@State private var showCancelConfirmation = false
// Count total tasks across all columns
private var totalTaskCount: Int {
guard let response = tasksResponse else { return 0 }
return response.columns.reduce(0) { $0 + $1.tasks.count }
}
private var hasNoTasks: Bool {
guard let response = tasksResponse else { return true }
return response.columns.allSatisfy { $0.tasks.isEmpty }
}
private var hasTasks: Bool {
!hasNoTasks
}
// Use ViewModel's computed properties
private var totalTaskCount: Int { taskViewModel.totalTaskCount }
private var hasNoTasks: Bool { taskViewModel.hasNoTasks }
private var hasTasks: Bool { taskViewModel.hasTasks }
private var tasksResponse: TaskColumnsResponse? { taskViewModel.tasksResponse }
private var isLoadingTasks: Bool { taskViewModel.isLoadingTasks }
private var tasksError: String? { taskViewModel.tasksError }
var body: some View {
mainContent
@@ -273,76 +263,12 @@ struct AllTasksView: View {
}
}
private func updateTaskInKanban(_ updatedTask: TaskResponse) {
guard let currentResponse = tasksResponse else { return }
let targetColumn = updatedTask.kanbanColumn ?? "completed_tasks"
var newColumns: [TaskColumn] = []
for column in currentResponse.columns {
// Remove task from this column if it exists
var filteredTasks = column.tasks.filter { $0.id != updatedTask.id }
// Add task to target column
if column.name == targetColumn {
filteredTasks.append(updatedTask)
}
// Create new column with updated tasks and count
let newColumn = TaskColumn(
name: column.name,
displayName: column.displayName,
buttonTypes: column.buttonTypes,
icons: column.icons,
color: column.color,
tasks: filteredTasks,
count: Int32(filteredTasks.count)
)
newColumns.append(newColumn)
}
// Update the response
tasksResponse = TaskColumnsResponse(
columns: newColumns,
daysThreshold: currentResponse.daysThreshold,
residenceId: currentResponse.residenceId
)
private func loadAllTasks(forceRefresh: Bool = false) {
taskViewModel.loadTasks(forceRefresh: forceRefresh)
}
private func loadAllTasks(forceRefresh: Bool = false) {
guard TokenStorage.shared.getToken() != nil else { return }
isLoadingTasks = true
tasksError = nil
Task {
do {
let result = try await APILayer.shared.getTasks(forceRefresh: forceRefresh)
await MainActor.run {
if let success = result as? ApiResultSuccess<TaskColumnsResponse>,
let data = success.data {
self.tasksResponse = data
self.isLoadingTasks = false
self.tasksError = nil
// Update widget data
WidgetDataManager.shared.saveTasks(from: data)
} else if let error = result as? ApiResultError {
self.tasksError = error.message
self.isLoadingTasks = false
} else {
self.tasksError = "Failed to load tasks"
self.isLoadingTasks = false
}
}
} catch {
await MainActor.run {
self.tasksError = error.localizedDescription
self.isLoadingTasks = false
}
}
}
private func updateTaskInKanban(_ updatedTask: TaskResponse) {
taskViewModel.updateTaskInKanban(updatedTask)
}
}

View File

@@ -11,6 +11,14 @@ class TaskViewModel: ObservableObject {
@Published var isLoadingCompletions: Bool = false
@Published var completionsError: String?
// MARK: - Kanban Board State (shared across views)
@Published var tasksResponse: TaskColumnsResponse?
@Published var isLoadingTasks: Bool = false
@Published var tasksError: String?
/// The residence ID this kanban is filtered to (nil = all tasks)
private(set) var currentResidenceId: Int32?
// MARK: - Computed Properties (Backward Compatibility)
var isLoading: Bool { actionState.isLoading }
@@ -219,4 +227,146 @@ class TaskViewModel: ObservableObject {
isLoadingCompletions = false
sharedViewModel.resetTaskCompletionsState()
}
// MARK: - Kanban Board Methods
/// Count total tasks across all columns
var totalTaskCount: Int {
guard let response = tasksResponse else { return 0 }
return response.columns.reduce(0) { $0 + $1.tasks.count }
}
/// Check if there are no tasks
var hasNoTasks: Bool {
guard let response = tasksResponse else { return true }
return response.columns.allSatisfy { $0.tasks.isEmpty }
}
/// Check if there are tasks
var hasTasks: Bool {
!hasNoTasks
}
/// Load tasks - either all tasks or filtered by residence
/// - Parameters:
/// - residenceId: Optional residence ID to filter by. If nil, loads all tasks.
/// - forceRefresh: Whether to bypass cache
func loadTasks(residenceId: Int32? = nil, forceRefresh: Bool = false) {
guard TokenStorage.shared.getToken() != nil else { return }
currentResidenceId = residenceId
isLoadingTasks = true
tasksError = nil
Task {
do {
let result: Any
if let resId = residenceId {
result = try await APILayer.shared.getTasksByResidence(
residenceId: resId,
forceRefresh: forceRefresh
)
} else {
result = try await APILayer.shared.getTasks(forceRefresh: forceRefresh)
}
await MainActor.run {
if let success = result as? ApiResultSuccess<TaskColumnsResponse>,
let data = success.data {
self.tasksResponse = data
self.isLoadingTasks = false
self.tasksError = nil
// Update widget data if loading all tasks
if residenceId == nil {
WidgetDataManager.shared.saveTasks(from: data)
}
} else if let error = result as? ApiResultError {
self.tasksError = error.message
self.isLoadingTasks = false
} else {
self.tasksError = "Failed to load tasks"
self.isLoadingTasks = false
}
}
} catch {
await MainActor.run {
self.tasksError = error.localizedDescription
self.isLoadingTasks = false
}
}
}
}
/// Updates a task in the kanban board by moving it to the correct column based on kanban_column
func updateTaskInKanban(_ updatedTask: TaskResponse) {
guard let currentResponse = tasksResponse else { return }
let targetColumn = updatedTask.kanbanColumn ?? "completed_tasks"
// Build new columns array
var newColumns: [TaskColumn] = []
for column in currentResponse.columns {
// Remove task from this column if it exists
var filteredTasks = column.tasks.filter { $0.id != updatedTask.id }
// Add task to target column
if column.name == targetColumn {
filteredTasks.append(updatedTask)
}
// Create new column with updated tasks and count
let newColumn = TaskColumn(
name: column.name,
displayName: column.displayName,
buttonTypes: column.buttonTypes,
icons: column.icons,
color: column.color,
tasks: filteredTasks,
count: Int32(filteredTasks.count)
)
newColumns.append(newColumn)
}
// Update the response
tasksResponse = TaskColumnsResponse(
columns: newColumns,
daysThreshold: currentResponse.daysThreshold,
residenceId: currentResponse.residenceId
)
}
/// Removes a task from the kanban board (after deletion)
func removeTaskFromKanban(taskId: Int32) {
guard let currentResponse = tasksResponse else { return }
var newColumns: [TaskColumn] = []
for column in currentResponse.columns {
let filteredTasks = column.tasks.filter { $0.id != taskId }
let newColumn = TaskColumn(
name: column.name,
displayName: column.displayName,
buttonTypes: column.buttonTypes,
icons: column.icons,
color: column.color,
tasks: filteredTasks,
count: Int32(filteredTasks.count)
)
newColumns.append(newColumn)
}
tasksResponse = TaskColumnsResponse(
columns: newColumns,
daysThreshold: currentResponse.daysThreshold,
residenceId: currentResponse.residenceId
)
}
/// Reloads the kanban board with current settings
func reloadTasks() {
loadTasks(residenceId: currentResidenceId, forceRefresh: true)
}
}