From b79fda8aee08a9d25b033aa464d3bc4c27f45d89 Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 2 Dec 2025 22:07:52 -0600 Subject: [PATCH] Centralize kanban state in TaskViewModel to eliminate duplicate code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Residence/ResidenceDetailView.swift | 91 +---------- iosApp/iosApp/Task/AllTasksView.swift | 96 ++--------- iosApp/iosApp/Task/TaskViewModel.swift | 150 ++++++++++++++++++ 3 files changed, 168 insertions(+), 169 deletions(-) diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index dff2752..49c7b1b 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -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 { - 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() { diff --git a/iosApp/iosApp/Task/AllTasksView.swift b/iosApp/iosApp/Task/AllTasksView.swift index 4f817c3..04cc463 100644 --- a/iosApp/iosApp/Task/AllTasksView.swift +++ b/iosApp/iosApp/Task/AllTasksView.swift @@ -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, - 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) } } diff --git a/iosApp/iosApp/Task/TaskViewModel.swift b/iosApp/iosApp/Task/TaskViewModel.swift index 9017ee4..f87523b 100644 --- a/iosApp/iosApp/Task/TaskViewModel.swift +++ b/iosApp/iosApp/Task/TaskViewModel.swift @@ -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, + 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) + } }