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:
Trey t
2025-12-16 17:06:48 -06:00
parent 6dfc4ee57c
commit fbe45da9ff
8 changed files with 52 additions and 40 deletions

View File

@@ -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()
)
}

View File

@@ -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<TaskColumn>,
@SerialName("days_threshold") val daysThreshold: Int,
@SerialName("residence_id") val residenceId: String,
val summary: TotalSummary? = null
@SerialName("residence_id") val residenceId: String
)
/**

View File

@@ -200,12 +200,11 @@ data class WithSummaryResponse<T>(
/**
* 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<ResidenceResponse>,
val summary: TotalSummary = TotalSummary()
val residences: List<ResidenceResponse>
)
/**

View File

@@ -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)
)

View File

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