import Foundation import ComposeApp /// Backs the First-Task onboarding screen. Owns the network calls for /// personalised suggestions and the full template catalog, plus the bulk /// task-create submission. No hardcoded suggestion rules or fallback /// catalog — when the API fails the screen shows error+retry+skip. /// /// Mirrors the Android `OnboardingViewModel.suggestionsState` / /// `templatesGroupedState` / `createTasksState` flows in a purely Swift /// shape: calling `APILayer.shared.*` directly is more idiomatic here than /// observing Kotlin StateFlows, and matches the pattern used by /// `TaskViewModel.swift`. @MainActor final class OnboardingTasksViewModel: ObservableObject { // MARK: - Suggestions (For You tab) @Published private(set) var suggestions: [TaskSuggestionResponse] = [] @Published private(set) var profileCompleteness: Double = 0 @Published private(set) var isLoadingSuggestions = false @Published private(set) var suggestionsError: String? /// True once `loadSuggestions` has returned any terminal state. /// Used by the view to distinguish "haven't tried yet" from "tried and /// returned empty". @Published private(set) var suggestionsAttempted = false // MARK: - Grouped catalog (Browse All tab) @Published private(set) var grouped: TaskTemplatesGroupedResponse? @Published private(set) var isLoadingGrouped = false @Published private(set) var groupedError: String? // MARK: - Submission @Published private(set) var isSubmitting = false @Published private(set) var submitError: String? // MARK: - Private Properties private let dataManager: DataManagerObservable // MARK: - Initialization init(dataManager: DataManagerObservable = .shared) { self.dataManager = dataManager } // MARK: - Loads func loadSuggestions(residenceId: Int32) async { if isLoadingSuggestions { return } isLoadingSuggestions = true suggestionsError = nil do { let result = try await APILayer.shared.getTaskSuggestions(residenceId: residenceId) if let success = result as? ApiResultSuccess, let data = success.data { suggestions = data.suggestions profileCompleteness = data.profileCompleteness AnalyticsManager.shared.track(.onboardingSuggestionsLoaded( count: data.suggestions.count, profileCompleteness: data.profileCompleteness )) } else if let error = ApiResultBridge.error(from: result) { suggestionsError = ErrorMessageParser.parse(error.message) } else { suggestionsError = "Could not load suggestions." } } catch { suggestionsError = ErrorMessageParser.parse(error.localizedDescription) } isLoadingSuggestions = false suggestionsAttempted = true } func loadGrouped(forceRefresh: Bool = false) async { if isLoadingGrouped { return } isLoadingGrouped = true groupedError = nil do { let result = try await APILayer.shared.getTaskTemplatesGrouped(forceRefresh: forceRefresh) if let success = result as? ApiResultSuccess, let data = success.data { grouped = data } else if let error = ApiResultBridge.error(from: result) { groupedError = ErrorMessageParser.parse(error.message) } else { groupedError = "Could not load templates." } } catch { groupedError = ErrorMessageParser.parse(error.localizedDescription) } isLoadingGrouped = false } // MARK: - Submit /// Posts the picked tasks in a single transaction via the bulk endpoint. /// Returns true on any successful server response (including empty /// selections, which short-circuit without a network call). False is /// terminal — the caller should show the stored `submitError`. func submit(residenceId: Int32, requests: [TaskCreateRequest]) async -> Bool { if requests.isEmpty { AnalyticsManager.shared.track(.onboardingTaskStepSkipped(reason: "user_skip")) return true } isSubmitting = true submitError = nil let request = BulkCreateTasksRequest(residenceId: residenceId, tasks: requests) defer { isSubmitting = false } do { let result = try await APILayer.shared.bulkCreateTasks(request: request) if result is ApiResultSuccess { AnalyticsManager.shared.track(.onboardingTasksCreated(count: requests.count)) return true } else if let error = ApiResultBridge.error(from: result) { submitError = ErrorMessageParser.parse(error.message) return false } else { submitError = "Could not create tasks." return false } } catch { submitError = ErrorMessageParser.parse(error.localizedDescription) return false } } }