From fbe45da9ffeb66f414aa46712530cb4098374150 Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 16 Dec 2025 17:06:48 -0600 Subject: [PATCH] Remove summary from list responses, calculate client-side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove summary field from MyResidencesResponse and TaskColumnsResponse - Update HomeScreen and ResidencesScreen to observe DataManager.totalSummary - Load tasks when residence views appear to ensure accurate summary - Add pull-to-refresh for tasks on ResidencesScreen - Update iOS views to use client-side calculated summary Summary is now calculated via refreshSummaryFromKanban() from cached kanban data. This ensures summary is always up-to-date after CRUD operations and when residence views are shown. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../com/example/casera/data/DataManager.kt | 16 +++--------- .../com/example/casera/models/CustomTask.kt | 4 +-- .../com/example/casera/models/Residence.kt | 5 ++-- .../example/casera/ui/screens/HomeScreen.kt | 12 ++++++--- .../casera/ui/screens/ResidencesScreen.kt | 26 ++++++++++++------- .../Helpers/WidgetActionProcessor.swift | 5 +--- .../iosApp/Residence/ResidencesListView.swift | 17 ++++++++++-- iosApp/iosApp/iOSApp.swift | 7 ++--- 8 files changed, 52 insertions(+), 40 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/data/DataManager.kt b/composeApp/src/commonMain/kotlin/com/example/casera/data/DataManager.kt index be8c41c..68c15b1 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/data/DataManager.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/data/DataManager.kt @@ -285,20 +285,17 @@ object DataManager { fun setMyResidences(response: MyResidencesResponse) { _myResidences.value = response - // Also update totalSummary from myResidences response - _totalSummary.value = response.summary myResidencesCacheTime = currentTimeMs() summaryCacheTime = currentTimeMs() updateLastSyncTime() + // Calculate summary from cached kanban data (API no longer returns summary stats) + // This ensures summary is always up-to-date when residence view is shown + refreshSummaryFromKanban() persistToDisk() } fun setTotalSummary(summary: TotalSummary) { _totalSummary.value = summary - // Also update the summary in myResidences if it exists - _myResidences.value?.let { current -> - _myResidences.value = current.copy(summary = summary) - } summaryCacheTime = currentTimeMs() persistToDisk() } @@ -356,10 +353,6 @@ object DataManager { fun refreshSummaryFromKanban() { val calculatedSummary = calculateSummaryFromKanban() _totalSummary.value = calculatedSummary - // Also update myResidences summary if present - _myResidences.value?.let { current -> - _myResidences.value = current.copy(summary = calculatedSummary) - } } fun setResidenceSummary(residenceId: Int, summary: ResidenceSummaryResponse) { @@ -431,8 +424,7 @@ object DataManager { return TaskColumnsResponse( columns = filteredColumns, daysThreshold = allTasksData.daysThreshold, - residenceId = residenceId.toString(), - summary = null // Summary is global; residence-specific not available client-side + residenceId = residenceId.toString() ) } diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/models/CustomTask.kt b/composeApp/src/commonMain/kotlin/com/example/casera/models/CustomTask.kt index 0506328..4c7f190 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/models/CustomTask.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/models/CustomTask.kt @@ -178,13 +178,13 @@ data class TaskColumn( /** * Kanban board response matching Go API KanbanBoardResponse + * NOTE: Summary statistics are calculated client-side from kanban data */ @Serializable data class TaskColumnsResponse( val columns: List, @SerialName("days_threshold") val daysThreshold: Int, - @SerialName("residence_id") val residenceId: String, - val summary: TotalSummary? = null + @SerialName("residence_id") val residenceId: String ) /** diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/models/Residence.kt b/composeApp/src/commonMain/kotlin/com/example/casera/models/Residence.kt index 6b5be40..02b0777 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/models/Residence.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/models/Residence.kt @@ -200,12 +200,11 @@ data class WithSummaryResponse( /** * My residences response - list of user's residences - * Go API returns array directly, this wraps for consistency + * NOTE: Summary statistics are calculated client-side from kanban data */ @Serializable data class MyResidencesResponse( - val residences: List, - val summary: TotalSummary = TotalSummary() + val residences: List ) /** diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/HomeScreen.kt index 25dc3f3..7ad767e 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/HomeScreen.kt @@ -18,7 +18,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.example.casera.ui.components.HandleErrors import com.example.casera.ui.theme.AppRadius import com.example.casera.viewmodel.ResidenceViewModel +import com.example.casera.viewmodel.TaskViewModel import com.example.casera.network.ApiResult +import com.example.casera.data.DataManager import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -28,12 +30,16 @@ fun HomeScreen( onNavigateToResidences: () -> Unit, onNavigateToTasks: () -> Unit, onLogout: () -> Unit, - viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() } + viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }, + taskViewModel: TaskViewModel = viewModel { TaskViewModel() } ) { val summaryState by viewModel.myResidencesState.collectAsState() + val totalSummary by DataManager.totalSummary.collectAsState() LaunchedEffect(Unit) { viewModel.loadMyResidences() + // Also load tasks to ensure summary can be calculated from kanban data + taskViewModel.loadTasks() } // Handle errors for loading summary @@ -159,7 +165,7 @@ fun HomeScreen( color = MaterialTheme.colorScheme.outlineVariant ) StatItem( - value = "${summary.summary.totalTasks}", + value = "${totalSummary?.totalTasks ?: 0}", label = stringResource(Res.string.home_total_tasks), color = Color(0xFF8B5CF6) ) @@ -170,7 +176,7 @@ fun HomeScreen( color = MaterialTheme.colorScheme.outlineVariant ) StatItem( - value = "${summary.summary.totalPending}", + value = "${totalSummary?.totalPending ?: 0}", label = stringResource(Res.string.home_pending), color = Color(0xFFF59E0B) ) diff --git a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidencesScreen.kt b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidencesScreen.kt index 43e9117..451b8fd 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidencesScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/ui/screens/ResidencesScreen.kt @@ -29,12 +29,14 @@ import com.example.casera.ui.components.JoinResidenceDialog import com.example.casera.ui.components.common.StatItem import com.example.casera.ui.components.residence.TaskStatChip import com.example.casera.viewmodel.ResidenceViewModel +import com.example.casera.viewmodel.TaskViewModel import com.example.casera.network.ApiResult import com.example.casera.utils.SubscriptionHelper import com.example.casera.ui.subscription.UpgradePromptDialog import com.example.casera.cache.SubscriptionCache import com.example.casera.analytics.PostHogAnalytics import com.example.casera.analytics.AnalyticsEvents +import com.example.casera.data.DataManager import casera.composeapp.generated.resources.* import org.jetbrains.compose.resources.stringResource @@ -46,9 +48,11 @@ fun ResidencesScreen( onLogout: () -> Unit, onNavigateToProfile: () -> Unit = {}, shouldRefresh: Boolean = false, - viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() } + viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }, + taskViewModel: TaskViewModel = viewModel { TaskViewModel() } ) { val myResidencesState by viewModel.myResidencesState.collectAsState() + val totalSummary by DataManager.totalSummary.collectAsState() var showJoinDialog by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) } var showUpgradePrompt by remember { mutableStateOf(false) } @@ -65,16 +69,19 @@ fun ResidencesScreen( return Pair(check.allowed, check.triggerKey) } - // Track screen view + // Track screen view and load data LaunchedEffect(Unit) { PostHogAnalytics.screen(AnalyticsEvents.RESIDENCE_SCREEN_SHOWN) viewModel.loadMyResidences() + // Also load tasks to ensure summary can be calculated from kanban data + taskViewModel.loadTasks() } // Refresh when shouldRefresh flag changes LaunchedEffect(shouldRefresh) { if (shouldRefresh) { viewModel.loadMyResidences(forceRefresh = true) + taskViewModel.loadTasks(forceRefresh = true) } } @@ -327,7 +334,8 @@ fun ResidencesScreen( isRefreshing = isRefreshing, onRefresh = { isRefreshing = true - viewModel.loadMyResidences() + viewModel.loadMyResidences(forceRefresh = true) + taskViewModel.loadTasks(forceRefresh = true) }, modifier = Modifier .fillMaxSize() @@ -383,12 +391,12 @@ fun ResidencesScreen( ) { StatItem( icon = Icons.Default.Home, - value = "${response.summary.totalResidences}", + value = "${totalSummary?.totalResidences ?: response.residences.size}", label = stringResource(Res.string.home_properties) ) StatItem( icon = Icons.Default.Assignment, - value = "${response.summary.totalTasks}", + value = "${totalSummary?.totalTasks ?: 0}", label = stringResource(Res.string.home_total_tasks) ) } @@ -403,18 +411,18 @@ fun ResidencesScreen( ) { StatItem( icon = Icons.Default.Warning, - value = "${response.summary.totalOverdue}", + value = "${totalSummary?.totalOverdue ?: 0}", label = stringResource(Res.string.home_overdue), - valueColor = if (response.summary.totalOverdue > 0) MaterialTheme.colorScheme.error else null + valueColor = if ((totalSummary?.totalOverdue ?: 0) > 0) MaterialTheme.colorScheme.error else null ) StatItem( icon = Icons.Default.CalendarToday, - value = "${response.summary.tasksDueNextWeek}", + value = "${totalSummary?.tasksDueNextWeek ?: 0}", label = stringResource(Res.string.home_due_this_week) ) StatItem( icon = Icons.Default.Event, - value = "${response.summary.tasksDueNextMonth}", + value = "${totalSummary?.tasksDueNextMonth ?: 0}", label = stringResource(Res.string.home_next_30_days) ) } diff --git a/iosApp/iosApp/Helpers/WidgetActionProcessor.swift b/iosApp/iosApp/Helpers/WidgetActionProcessor.swift index 2e2d0f8..eac9eec 100644 --- a/iosApp/iosApp/Helpers/WidgetActionProcessor.swift +++ b/iosApp/iosApp/Helpers/WidgetActionProcessor.swift @@ -93,10 +93,7 @@ final class WidgetActionProcessor { let data = success.data { // Update widget with fresh data WidgetDataManager.shared.saveTasks(from: data) - // Update summary from response (no extra API call needed) - if let summary = data.summary { - DataManager.shared.setTotalSummary(summary: summary) - } + // Summary is calculated client-side by DataManager.setAllTasks() -> refreshSummaryFromKanban() } } catch { print("WidgetActionProcessor: Error refreshing tasks: \(error)") diff --git a/iosApp/iosApp/Residence/ResidencesListView.swift b/iosApp/iosApp/Residence/ResidencesListView.swift index ae3182e..28d1413 100644 --- a/iosApp/iosApp/Residence/ResidencesListView.swift +++ b/iosApp/iosApp/Residence/ResidencesListView.swift @@ -3,12 +3,14 @@ import ComposeApp struct ResidencesListView: View { @StateObject private var viewModel = ResidenceViewModel() + @StateObject private var taskViewModel = TaskViewModel() @State private var showingAddResidence = false @State private var showingJoinResidence = false @State private var showingUpgradePrompt = false @State private var showingSettings = false @StateObject private var authManager = AuthenticationManager.shared @StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared + @Environment(\.scenePhase) private var scenePhase var body: some View { ZStack { @@ -22,7 +24,7 @@ struct ResidencesListView: View { errorMessage: viewModel.errorMessage, content: { residences in ResidencesContent( - summary: viewModel.totalSummary ?? response.summary, + summary: viewModel.totalSummary ?? TotalSummary(totalResidences: Int32(residences.count), totalTasks: 0, totalOverdue: 0, totalDueSoon: 0, totalPending: 0, tasksDueNextWeek: 0, tasksDueNextMonth: 0), residences: residences ) }, @@ -114,19 +116,30 @@ struct ResidencesListView: View { PostHogAnalytics.shared.screen(AnalyticsEvents.residenceScreenShown) if authManager.isAuthenticated { viewModel.loadMyResidences() + // Also load tasks to populate summary stats + taskViewModel.loadTasks() + } + } + .onChange(of: scenePhase) { newPhase in + // Refresh data when app comes back from background + if newPhase == .active && authManager.isAuthenticated { + viewModel.loadMyResidences(forceRefresh: true) + taskViewModel.loadTasks(forceRefresh: true) } } .fullScreenCover(isPresented: $authManager.isAuthenticated.negated) { LoginView(onLoginSuccess: { authManager.isAuthenticated = true viewModel.loadMyResidences() + taskViewModel.loadTasks() }) .interactiveDismissDisabled() } .onChange(of: authManager.isAuthenticated) { isAuth in if isAuth { - // User just logged in or registered - load their residences + // User just logged in or registered - load their residences and tasks viewModel.loadMyResidences() + taskViewModel.loadTasks() } else { // User logged out - clear data viewModel.myResidences = nil diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift index 0101198..a42de68 100644 --- a/iosApp/iosApp/iOSApp.swift +++ b/iosApp/iosApp/iOSApp.swift @@ -79,16 +79,13 @@ struct iOSApp: App { if WidgetDataManager.shared.areTasksDirty() { WidgetDataManager.shared.clearDirtyFlag() Task { - // Refresh tasks - response includes summary for dashboard stats + // Refresh tasks - summary is calculated client-side from kanban data let result = try? await APILayer.shared.getTasks(forceRefresh: true) if let success = result as? ApiResultSuccess, let data = success.data { // Update widget cache WidgetDataManager.shared.saveTasks(from: data) - // Update summary from response (no extra API call needed) - if let summary = data.summary { - DataManager.shared.setTotalSummary(summary: summary) - } + // Summary is calculated by DataManager.setAllTasks() -> refreshSummaryFromKanban() } } }