Remove summary from list responses, calculate client-side
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -285,20 +285,17 @@ object DataManager {
|
|||||||
|
|
||||||
fun setMyResidences(response: MyResidencesResponse) {
|
fun setMyResidences(response: MyResidencesResponse) {
|
||||||
_myResidences.value = response
|
_myResidences.value = response
|
||||||
// Also update totalSummary from myResidences response
|
|
||||||
_totalSummary.value = response.summary
|
|
||||||
myResidencesCacheTime = currentTimeMs()
|
myResidencesCacheTime = currentTimeMs()
|
||||||
summaryCacheTime = currentTimeMs()
|
summaryCacheTime = currentTimeMs()
|
||||||
updateLastSyncTime()
|
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()
|
persistToDisk()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setTotalSummary(summary: TotalSummary) {
|
fun setTotalSummary(summary: TotalSummary) {
|
||||||
_totalSummary.value = summary
|
_totalSummary.value = summary
|
||||||
// Also update the summary in myResidences if it exists
|
|
||||||
_myResidences.value?.let { current ->
|
|
||||||
_myResidences.value = current.copy(summary = summary)
|
|
||||||
}
|
|
||||||
summaryCacheTime = currentTimeMs()
|
summaryCacheTime = currentTimeMs()
|
||||||
persistToDisk()
|
persistToDisk()
|
||||||
}
|
}
|
||||||
@@ -356,10 +353,6 @@ object DataManager {
|
|||||||
fun refreshSummaryFromKanban() {
|
fun refreshSummaryFromKanban() {
|
||||||
val calculatedSummary = calculateSummaryFromKanban()
|
val calculatedSummary = calculateSummaryFromKanban()
|
||||||
_totalSummary.value = calculatedSummary
|
_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) {
|
fun setResidenceSummary(residenceId: Int, summary: ResidenceSummaryResponse) {
|
||||||
@@ -431,8 +424,7 @@ object DataManager {
|
|||||||
return TaskColumnsResponse(
|
return TaskColumnsResponse(
|
||||||
columns = filteredColumns,
|
columns = filteredColumns,
|
||||||
daysThreshold = allTasksData.daysThreshold,
|
daysThreshold = allTasksData.daysThreshold,
|
||||||
residenceId = residenceId.toString(),
|
residenceId = residenceId.toString()
|
||||||
summary = null // Summary is global; residence-specific not available client-side
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -178,13 +178,13 @@ data class TaskColumn(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Kanban board response matching Go API KanbanBoardResponse
|
* Kanban board response matching Go API KanbanBoardResponse
|
||||||
|
* NOTE: Summary statistics are calculated client-side from kanban data
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class TaskColumnsResponse(
|
data class TaskColumnsResponse(
|
||||||
val columns: List<TaskColumn>,
|
val columns: List<TaskColumn>,
|
||||||
@SerialName("days_threshold") val daysThreshold: Int,
|
@SerialName("days_threshold") val daysThreshold: Int,
|
||||||
@SerialName("residence_id") val residenceId: String,
|
@SerialName("residence_id") val residenceId: String
|
||||||
val summary: TotalSummary? = null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -200,12 +200,11 @@ data class WithSummaryResponse<T>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* My residences response - list of user's residences
|
* 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
|
@Serializable
|
||||||
data class MyResidencesResponse(
|
data class MyResidencesResponse(
|
||||||
val residences: List<ResidenceResponse>,
|
val residences: List<ResidenceResponse>
|
||||||
val summary: TotalSummary = TotalSummary()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import com.example.casera.ui.components.HandleErrors
|
import com.example.casera.ui.components.HandleErrors
|
||||||
import com.example.casera.ui.theme.AppRadius
|
import com.example.casera.ui.theme.AppRadius
|
||||||
import com.example.casera.viewmodel.ResidenceViewModel
|
import com.example.casera.viewmodel.ResidenceViewModel
|
||||||
|
import com.example.casera.viewmodel.TaskViewModel
|
||||||
import com.example.casera.network.ApiResult
|
import com.example.casera.network.ApiResult
|
||||||
|
import com.example.casera.data.DataManager
|
||||||
import casera.composeapp.generated.resources.*
|
import casera.composeapp.generated.resources.*
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
|
||||||
@@ -28,12 +30,16 @@ fun HomeScreen(
|
|||||||
onNavigateToResidences: () -> Unit,
|
onNavigateToResidences: () -> Unit,
|
||||||
onNavigateToTasks: () -> Unit,
|
onNavigateToTasks: () -> Unit,
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
|
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() },
|
||||||
|
taskViewModel: TaskViewModel = viewModel { TaskViewModel() }
|
||||||
) {
|
) {
|
||||||
val summaryState by viewModel.myResidencesState.collectAsState()
|
val summaryState by viewModel.myResidencesState.collectAsState()
|
||||||
|
val totalSummary by DataManager.totalSummary.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadMyResidences()
|
viewModel.loadMyResidences()
|
||||||
|
// Also load tasks to ensure summary can be calculated from kanban data
|
||||||
|
taskViewModel.loadTasks()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle errors for loading summary
|
// Handle errors for loading summary
|
||||||
@@ -159,7 +165,7 @@ fun HomeScreen(
|
|||||||
color = MaterialTheme.colorScheme.outlineVariant
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
)
|
)
|
||||||
StatItem(
|
StatItem(
|
||||||
value = "${summary.summary.totalTasks}",
|
value = "${totalSummary?.totalTasks ?: 0}",
|
||||||
label = stringResource(Res.string.home_total_tasks),
|
label = stringResource(Res.string.home_total_tasks),
|
||||||
color = Color(0xFF8B5CF6)
|
color = Color(0xFF8B5CF6)
|
||||||
)
|
)
|
||||||
@@ -170,7 +176,7 @@ fun HomeScreen(
|
|||||||
color = MaterialTheme.colorScheme.outlineVariant
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
)
|
)
|
||||||
StatItem(
|
StatItem(
|
||||||
value = "${summary.summary.totalPending}",
|
value = "${totalSummary?.totalPending ?: 0}",
|
||||||
label = stringResource(Res.string.home_pending),
|
label = stringResource(Res.string.home_pending),
|
||||||
color = Color(0xFFF59E0B)
|
color = Color(0xFFF59E0B)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.common.StatItem
|
||||||
import com.example.casera.ui.components.residence.TaskStatChip
|
import com.example.casera.ui.components.residence.TaskStatChip
|
||||||
import com.example.casera.viewmodel.ResidenceViewModel
|
import com.example.casera.viewmodel.ResidenceViewModel
|
||||||
|
import com.example.casera.viewmodel.TaskViewModel
|
||||||
import com.example.casera.network.ApiResult
|
import com.example.casera.network.ApiResult
|
||||||
import com.example.casera.utils.SubscriptionHelper
|
import com.example.casera.utils.SubscriptionHelper
|
||||||
import com.example.casera.ui.subscription.UpgradePromptDialog
|
import com.example.casera.ui.subscription.UpgradePromptDialog
|
||||||
import com.example.casera.cache.SubscriptionCache
|
import com.example.casera.cache.SubscriptionCache
|
||||||
import com.example.casera.analytics.PostHogAnalytics
|
import com.example.casera.analytics.PostHogAnalytics
|
||||||
import com.example.casera.analytics.AnalyticsEvents
|
import com.example.casera.analytics.AnalyticsEvents
|
||||||
|
import com.example.casera.data.DataManager
|
||||||
import casera.composeapp.generated.resources.*
|
import casera.composeapp.generated.resources.*
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
|
||||||
@@ -46,9 +48,11 @@ fun ResidencesScreen(
|
|||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
onNavigateToProfile: () -> Unit = {},
|
onNavigateToProfile: () -> Unit = {},
|
||||||
shouldRefresh: Boolean = false,
|
shouldRefresh: Boolean = false,
|
||||||
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() }
|
viewModel: ResidenceViewModel = viewModel { ResidenceViewModel() },
|
||||||
|
taskViewModel: TaskViewModel = viewModel { TaskViewModel() }
|
||||||
) {
|
) {
|
||||||
val myResidencesState by viewModel.myResidencesState.collectAsState()
|
val myResidencesState by viewModel.myResidencesState.collectAsState()
|
||||||
|
val totalSummary by DataManager.totalSummary.collectAsState()
|
||||||
var showJoinDialog by remember { mutableStateOf(false) }
|
var showJoinDialog by remember { mutableStateOf(false) }
|
||||||
var isRefreshing by remember { mutableStateOf(false) }
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
var showUpgradePrompt by remember { mutableStateOf(false) }
|
var showUpgradePrompt by remember { mutableStateOf(false) }
|
||||||
@@ -65,16 +69,19 @@ fun ResidencesScreen(
|
|||||||
return Pair(check.allowed, check.triggerKey)
|
return Pair(check.allowed, check.triggerKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track screen view
|
// Track screen view and load data
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
PostHogAnalytics.screen(AnalyticsEvents.RESIDENCE_SCREEN_SHOWN)
|
PostHogAnalytics.screen(AnalyticsEvents.RESIDENCE_SCREEN_SHOWN)
|
||||||
viewModel.loadMyResidences()
|
viewModel.loadMyResidences()
|
||||||
|
// Also load tasks to ensure summary can be calculated from kanban data
|
||||||
|
taskViewModel.loadTasks()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh when shouldRefresh flag changes
|
// Refresh when shouldRefresh flag changes
|
||||||
LaunchedEffect(shouldRefresh) {
|
LaunchedEffect(shouldRefresh) {
|
||||||
if (shouldRefresh) {
|
if (shouldRefresh) {
|
||||||
viewModel.loadMyResidences(forceRefresh = true)
|
viewModel.loadMyResidences(forceRefresh = true)
|
||||||
|
taskViewModel.loadTasks(forceRefresh = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +334,8 @@ fun ResidencesScreen(
|
|||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
viewModel.loadMyResidences()
|
viewModel.loadMyResidences(forceRefresh = true)
|
||||||
|
taskViewModel.loadTasks(forceRefresh = true)
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -383,12 +391,12 @@ fun ResidencesScreen(
|
|||||||
) {
|
) {
|
||||||
StatItem(
|
StatItem(
|
||||||
icon = Icons.Default.Home,
|
icon = Icons.Default.Home,
|
||||||
value = "${response.summary.totalResidences}",
|
value = "${totalSummary?.totalResidences ?: response.residences.size}",
|
||||||
label = stringResource(Res.string.home_properties)
|
label = stringResource(Res.string.home_properties)
|
||||||
)
|
)
|
||||||
StatItem(
|
StatItem(
|
||||||
icon = Icons.Default.Assignment,
|
icon = Icons.Default.Assignment,
|
||||||
value = "${response.summary.totalTasks}",
|
value = "${totalSummary?.totalTasks ?: 0}",
|
||||||
label = stringResource(Res.string.home_total_tasks)
|
label = stringResource(Res.string.home_total_tasks)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -403,18 +411,18 @@ fun ResidencesScreen(
|
|||||||
) {
|
) {
|
||||||
StatItem(
|
StatItem(
|
||||||
icon = Icons.Default.Warning,
|
icon = Icons.Default.Warning,
|
||||||
value = "${response.summary.totalOverdue}",
|
value = "${totalSummary?.totalOverdue ?: 0}",
|
||||||
label = stringResource(Res.string.home_overdue),
|
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(
|
StatItem(
|
||||||
icon = Icons.Default.CalendarToday,
|
icon = Icons.Default.CalendarToday,
|
||||||
value = "${response.summary.tasksDueNextWeek}",
|
value = "${totalSummary?.tasksDueNextWeek ?: 0}",
|
||||||
label = stringResource(Res.string.home_due_this_week)
|
label = stringResource(Res.string.home_due_this_week)
|
||||||
)
|
)
|
||||||
StatItem(
|
StatItem(
|
||||||
icon = Icons.Default.Event,
|
icon = Icons.Default.Event,
|
||||||
value = "${response.summary.tasksDueNextMonth}",
|
value = "${totalSummary?.tasksDueNextMonth ?: 0}",
|
||||||
label = stringResource(Res.string.home_next_30_days)
|
label = stringResource(Res.string.home_next_30_days)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,10 +93,7 @@ final class WidgetActionProcessor {
|
|||||||
let data = success.data {
|
let data = success.data {
|
||||||
// Update widget with fresh data
|
// Update widget with fresh data
|
||||||
WidgetDataManager.shared.saveTasks(from: data)
|
WidgetDataManager.shared.saveTasks(from: data)
|
||||||
// Update summary from response (no extra API call needed)
|
// Summary is calculated client-side by DataManager.setAllTasks() -> refreshSummaryFromKanban()
|
||||||
if let summary = data.summary {
|
|
||||||
DataManager.shared.setTotalSummary(summary: summary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
print("WidgetActionProcessor: Error refreshing tasks: \(error)")
|
print("WidgetActionProcessor: Error refreshing tasks: \(error)")
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import ComposeApp
|
|||||||
|
|
||||||
struct ResidencesListView: View {
|
struct ResidencesListView: View {
|
||||||
@StateObject private var viewModel = ResidenceViewModel()
|
@StateObject private var viewModel = ResidenceViewModel()
|
||||||
|
@StateObject private var taskViewModel = TaskViewModel()
|
||||||
@State private var showingAddResidence = false
|
@State private var showingAddResidence = false
|
||||||
@State private var showingJoinResidence = false
|
@State private var showingJoinResidence = false
|
||||||
@State private var showingUpgradePrompt = false
|
@State private var showingUpgradePrompt = false
|
||||||
@State private var showingSettings = false
|
@State private var showingSettings = false
|
||||||
@StateObject private var authManager = AuthenticationManager.shared
|
@StateObject private var authManager = AuthenticationManager.shared
|
||||||
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -22,7 +24,7 @@ struct ResidencesListView: View {
|
|||||||
errorMessage: viewModel.errorMessage,
|
errorMessage: viewModel.errorMessage,
|
||||||
content: { residences in
|
content: { residences in
|
||||||
ResidencesContent(
|
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
|
residences: residences
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -114,19 +116,30 @@ struct ResidencesListView: View {
|
|||||||
PostHogAnalytics.shared.screen(AnalyticsEvents.residenceScreenShown)
|
PostHogAnalytics.shared.screen(AnalyticsEvents.residenceScreenShown)
|
||||||
if authManager.isAuthenticated {
|
if authManager.isAuthenticated {
|
||||||
viewModel.loadMyResidences()
|
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) {
|
.fullScreenCover(isPresented: $authManager.isAuthenticated.negated) {
|
||||||
LoginView(onLoginSuccess: {
|
LoginView(onLoginSuccess: {
|
||||||
authManager.isAuthenticated = true
|
authManager.isAuthenticated = true
|
||||||
viewModel.loadMyResidences()
|
viewModel.loadMyResidences()
|
||||||
|
taskViewModel.loadTasks()
|
||||||
})
|
})
|
||||||
.interactiveDismissDisabled()
|
.interactiveDismissDisabled()
|
||||||
}
|
}
|
||||||
.onChange(of: authManager.isAuthenticated) { isAuth in
|
.onChange(of: authManager.isAuthenticated) { isAuth in
|
||||||
if isAuth {
|
if isAuth {
|
||||||
// User just logged in or registered - load their residences
|
// User just logged in or registered - load their residences and tasks
|
||||||
viewModel.loadMyResidences()
|
viewModel.loadMyResidences()
|
||||||
|
taskViewModel.loadTasks()
|
||||||
} else {
|
} else {
|
||||||
// User logged out - clear data
|
// User logged out - clear data
|
||||||
viewModel.myResidences = nil
|
viewModel.myResidences = nil
|
||||||
|
|||||||
@@ -79,16 +79,13 @@ struct iOSApp: App {
|
|||||||
if WidgetDataManager.shared.areTasksDirty() {
|
if WidgetDataManager.shared.areTasksDirty() {
|
||||||
WidgetDataManager.shared.clearDirtyFlag()
|
WidgetDataManager.shared.clearDirtyFlag()
|
||||||
Task {
|
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)
|
let result = try? await APILayer.shared.getTasks(forceRefresh: true)
|
||||||
if let success = result as? ApiResultSuccess<TaskColumnsResponse>,
|
if let success = result as? ApiResultSuccess<TaskColumnsResponse>,
|
||||||
let data = success.data {
|
let data = success.data {
|
||||||
// Update widget cache
|
// Update widget cache
|
||||||
WidgetDataManager.shared.saveTasks(from: data)
|
WidgetDataManager.shared.saveTasks(from: data)
|
||||||
// Update summary from response (no extra API call needed)
|
// Summary is calculated by DataManager.setAllTasks() -> refreshSummaryFromKanban()
|
||||||
if let summary = data.summary {
|
|
||||||
DataManager.shared.setTotalSummary(summary: summary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user