Wire onboarding task suggestions to backend, delete hardcoded catalog
Both "For You" and "Browse All" tabs are now fully server-driven on iOS and Android. No on-device task list, no client-side scoring rules. When the API fails the screen shows error + Retry + Skip so onboarding can still complete on a flaky network. Shared (KMM) - TaskCreateRequest + TaskResponse carry templateId - New BulkCreateTasksRequest/Response, TaskApi.bulkCreateTasks, APILayer.bulkCreateTasks (updates DataManager + TotalSummary) - OnboardingViewModel: templatesGroupedState + loadTemplatesGrouped; createTasks(residenceId, requests) posts once via the bulk path - Deleted regional-template plumbing: APILayer.getRegionalTemplates, OnboardingViewModel.loadRegionalTemplates, TaskTemplateApi. getTemplatesByRegion, TaskTemplate.regionId/regionName - 5 new AnalyticsEvents constants for the onboarding funnel Android (Compose) - OnboardingFirstTaskContent rewritten against the server catalog; ~70 lines of hardcoded taskCategories gone. Loading / Error / Empty panes with Retry + Skip buttons. Category icons derived from name keywords, colours from a 5-value palette keyed by category id - Browse selection carries template.id into the bulk request so task_template_id is populated server-side iOS (SwiftUI) - New OnboardingTasksViewModel (@MainActor ObservableObject) wrapping APILayer.shared for suggestions / grouped / bulk-submit with loading + error state (mirrors the TaskViewModel.swift pattern) - OnboardingFirstTaskView rewritten: buildForYouSuggestions (130 lines) and fallbackCategories (68 lines) deleted; both tabs show the same error+skip UX as Android; ForYouSuggestion/SuggestionRelevance gone - 5 new AnalyticsEvent cases with identical PostHog event names to the Kotlin constants so cross-platform funnels join cleanly - Existing TaskCreateRequest / TaskResponse call sites in TaskCard, TasksSection, TaskFormView updated for the new templateId parameter Docs - CLAUDE.md gains an "Onboarding task suggestions (server-driven)" subsection covering the data flow, key files on both platforms, and the KotlinInt(int: template.id) wrapping requirement Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,13 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import com.tt.honeyDue.models.AuthResponse
|
||||
import com.tt.honeyDue.models.BulkCreateTasksRequest
|
||||
import com.tt.honeyDue.models.LoginRequest
|
||||
import com.tt.honeyDue.models.RegisterRequest
|
||||
import com.tt.honeyDue.models.ResidenceCreateRequest
|
||||
import com.tt.honeyDue.models.TaskCreateRequest
|
||||
import com.tt.honeyDue.models.TaskSuggestionsResponse
|
||||
import com.tt.honeyDue.models.TaskTemplate
|
||||
import com.tt.honeyDue.models.TaskTemplatesGroupedResponse
|
||||
import com.tt.honeyDue.models.VerifyEmailRequest
|
||||
import com.tt.honeyDue.network.ApiResult
|
||||
import com.tt.honeyDue.network.APILayer
|
||||
@@ -80,15 +81,17 @@ class OnboardingViewModel : ViewModel() {
|
||||
private val _joinResidenceState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||
val joinResidenceState: StateFlow<ApiResult<Unit>> = _joinResidenceState
|
||||
|
||||
// Task creation state
|
||||
// Task creation state (bulk create)
|
||||
private val _createTasksState = MutableStateFlow<ApiResult<Unit>>(ApiResult.Idle)
|
||||
val createTasksState: StateFlow<ApiResult<Unit>> = _createTasksState
|
||||
|
||||
// Regional templates state
|
||||
private val _regionalTemplates = MutableStateFlow<ApiResult<List<TaskTemplate>>>(ApiResult.Idle)
|
||||
val regionalTemplates: StateFlow<ApiResult<List<TaskTemplate>>> = _regionalTemplates
|
||||
// Grouped templates for the Browse tab on the First-Task screen
|
||||
private val _templatesGroupedState = MutableStateFlow<ApiResult<TaskTemplatesGroupedResponse>>(ApiResult.Idle)
|
||||
val templatesGroupedState: StateFlow<ApiResult<TaskTemplatesGroupedResponse>> = _templatesGroupedState
|
||||
|
||||
// ZIP code entered during location step (persisted on residence)
|
||||
// ZIP code entered during onboarding (persisted on residence). Still
|
||||
// collected so the suggestion service can factor in the user's climate
|
||||
// zone as its 15th scoring condition.
|
||||
private val _postalCode = MutableStateFlow("")
|
||||
val postalCode: StateFlow<String> = _postalCode
|
||||
|
||||
@@ -396,9 +399,15 @@ class OnboardingViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create selected tasks during onboarding
|
||||
* Create the user's selected tasks in a single atomic bulk request.
|
||||
* The server runs the inserts inside one transaction, so either every
|
||||
* entry lands or none do — no risk of a half-populated kanban board
|
||||
* if the network flakes mid-batch.
|
||||
*
|
||||
* [residenceId] overrides every entry's residence_id on the server side;
|
||||
* pass the just-created residence's ID.
|
||||
*/
|
||||
fun createTasks(taskRequests: List<TaskCreateRequest>) {
|
||||
fun createTasks(residenceId: Int, taskRequests: List<TaskCreateRequest>) {
|
||||
viewModelScope.launch {
|
||||
if (taskRequests.isEmpty()) {
|
||||
_createTasksState.value = ApiResult.Success(Unit)
|
||||
@@ -407,31 +416,28 @@ class OnboardingViewModel : ViewModel() {
|
||||
|
||||
_createTasksState.value = ApiResult.Loading
|
||||
|
||||
var successCount = 0
|
||||
for (request in taskRequests) {
|
||||
val result = APILayer.createTask(request)
|
||||
if (result is ApiResult.Success) {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
|
||||
_createTasksState.value = if (successCount > 0) {
|
||||
ApiResult.Success(Unit)
|
||||
} else {
|
||||
ApiResult.Error("Failed to create tasks")
|
||||
val request = BulkCreateTasksRequest(
|
||||
residenceId = residenceId,
|
||||
tasks = taskRequests
|
||||
)
|
||||
_createTasksState.value = when (val result = APILayer.bulkCreateTasks(request)) {
|
||||
is ApiResult.Success -> ApiResult.Success(Unit)
|
||||
is ApiResult.Error -> ApiResult.Error(result.message, result.code)
|
||||
is ApiResult.Loading -> ApiResult.Loading
|
||||
is ApiResult.Idle -> ApiResult.Idle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load regional templates by ZIP code (backend resolves ZIP → state → climate zone).
|
||||
* Also stores the ZIP code for later use when creating the residence.
|
||||
* Load the flat template catalog grouped by category. Feeds the Browse
|
||||
* tab on the First-Task screen; no caching special-case because
|
||||
* APILayer.getTaskTemplatesGrouped already reads from DataManager first.
|
||||
*/
|
||||
fun loadRegionalTemplates(zip: String) {
|
||||
_postalCode.value = zip
|
||||
fun loadTemplatesGrouped(forceRefresh: Boolean = false) {
|
||||
viewModelScope.launch {
|
||||
_regionalTemplates.value = ApiResult.Loading
|
||||
_regionalTemplates.value = APILayer.getRegionalTemplates(zip = zip)
|
||||
_templatesGroupedState.value = ApiResult.Loading
|
||||
_templatesGroupedState.value = APILayer.getTaskTemplatesGrouped(forceRefresh = forceRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +462,7 @@ class OnboardingViewModel : ViewModel() {
|
||||
_createResidenceState.value = ApiResult.Idle
|
||||
_joinResidenceState.value = ApiResult.Idle
|
||||
_createTasksState.value = ApiResult.Idle
|
||||
_regionalTemplates.value = ApiResult.Idle
|
||||
_templatesGroupedState.value = ApiResult.Idle
|
||||
_postalCode.value = ""
|
||||
_heatingType.value = null
|
||||
_coolingType.value = null
|
||||
|
||||
Reference in New Issue
Block a user