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) + } }