From e2d264da7e881711c1e2e2d9d7f922d3194a7bed Mon Sep 17 00:00:00 2001 From: Trey t Date: Sun, 14 Dec 2025 01:04:52 -0600 Subject: [PATCH] Add client-side summary calculation and lookup resolution from cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add calculateSummaryFromKanban() to compute summary stats from cached kanban data - Add refreshSummaryFromKanban() called after task CRUD operations - Fix column name matching to use API format (e.g., "overdue_tasks" not "overdue") - Fix tasksDueNextMonth to only include due_soon tasks (not upcoming) - Update TaskResponse computed properties to resolve from DataManager cache - Update iOS task cards to use computed properties for priority/frequency/category - This enables API to skip preloading lookups for better performance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../com/example/casera/data/DataManager.kt | 64 +++++++++++++++++++ .../com/example/casera/models/CustomTask.kt | 20 +++--- iosApp/iosApp/Helpers/WidgetDataManager.swift | 4 +- .../Residence/ResidenceDetailView.swift | 3 +- .../Subviews/Task/DynamicTaskCard.swift | 6 +- iosApp/iosApp/Subviews/Task/TaskCard.swift | 4 +- iosApp/iosApp/Task/CompleteTaskView.swift | 2 +- 7 files changed, 85 insertions(+), 18 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 2c290f8..b3285c6 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/data/DataManager.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/data/DataManager.kt @@ -303,6 +303,65 @@ object DataManager { persistToDisk() } + /** + * Calculate TotalSummary from cached kanban data. + * This allows the client to compute summary stats without waiting for API responses + * after CRUD operations (API now returns empty summaries for performance). + * + * Columns from API: overdue_tasks, in_progress_tasks, due_soon_tasks, upcoming_tasks, completed_tasks, cancelled_tasks + */ + fun calculateSummaryFromKanban(): TotalSummary { + val kanban = _allTasks.value ?: return _totalSummary.value ?: TotalSummary() + val residenceCount = _myResidences.value?.residences?.size ?: _residences.value.size + + var overdueCount = 0 + var inProgressCount = 0 + var dueSoonCount = 0 + var upcomingCount = 0 + var completedCount = 0 + + for (column in kanban.columns) { + when (column.name) { + "overdue_tasks" -> overdueCount = column.count + "in_progress_tasks" -> inProgressCount = column.count + "due_soon_tasks" -> dueSoonCount = column.count + "upcoming_tasks" -> upcomingCount = column.count + "completed_tasks" -> completedCount = column.count + // cancelled_tasks is not counted in totals + } + } + + // totalTasks = all non-cancelled tasks + val totalTasks = overdueCount + inProgressCount + dueSoonCount + upcomingCount + completedCount + // totalPending = not completed (i.e., still needs attention) + val totalPending = overdueCount + inProgressCount + dueSoonCount + upcomingCount + + return TotalSummary( + totalResidences = residenceCount, + totalTasks = totalTasks, + totalPending = totalPending, + totalOverdue = overdueCount, + // due_soon_tasks column = tasks due within threshold (default 30 days) + // upcoming_tasks column = tasks with dates beyond threshold OR no due date + // Both "Due This Week" and "Next 30 Days" use due_soon since it represents the 30-day window + tasksDueNextWeek = dueSoonCount, + tasksDueNextMonth = dueSoonCount + ) + } + + /** + * Update totalSummary from cached kanban data. + * Call this after task CRUD operations to keep summary in sync without API call. + */ + 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) { _residenceSummaries.value = _residenceSummaries.value + (residenceId to summary) persistToDisk() @@ -378,6 +437,7 @@ object DataManager { /** * Update a single task - moves it to the correct kanban column based on kanban_column field. * This is called after task completion, status change, etc. + * Also refreshes the summary from the updated kanban data. */ fun updateTask(task: TaskResponse) { // Update in allTasks @@ -414,6 +474,8 @@ object DataManager { } } + // Refresh summary from updated kanban data (API no longer returns summaries for CRUD) + refreshSummaryFromKanban() persistToDisk() } @@ -436,6 +498,8 @@ object DataManager { tasks.copy(columns = newColumns) } + // Refresh summary from updated kanban data (API no longer returns summaries for CRUD) + refreshSummaryFromKanban() persistToDisk() } 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 0d66e67..0506328 100644 --- a/composeApp/src/commonMain/kotlin/com/example/casera/models/CustomTask.kt +++ b/composeApp/src/commonMain/kotlin/com/example/casera/models/CustomTask.kt @@ -1,5 +1,6 @@ package com.example.casera.models +import com.example.casera.data.DataManager import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -67,14 +68,15 @@ data class TaskResponse( // Helper to get created by username val createdByUsername: String get() = createdBy?.displayName ?: "" - // Computed properties for UI compatibility (these were in Django API but not Go API) - val categoryName: String? get() = category?.name - val categoryDescription: String? get() = category?.description - val frequencyName: String? get() = frequency?.name - val frequencyDisplayName: String? get() = frequency?.displayName - val frequencyDaySpan: Int? get() = frequency?.days - val priorityName: String? get() = priority?.name - val priorityDisplayName: String? get() = priority?.displayName + // Computed properties for UI compatibility - resolves from cache if not embedded in response + // This allows the API to skip Preloading these lookups for performance + val categoryName: String? get() = category?.name ?: DataManager.getTaskCategory(categoryId)?.name + val categoryDescription: String? get() = category?.description ?: DataManager.getTaskCategory(categoryId)?.description + val frequencyName: String? get() = frequency?.name ?: DataManager.getTaskFrequency(frequencyId)?.name + val frequencyDisplayName: String? get() = frequency?.displayName ?: DataManager.getTaskFrequency(frequencyId)?.displayName + val frequencyDaySpan: Int? get() = frequency?.days ?: DataManager.getTaskFrequency(frequencyId)?.days + val priorityName: String? get() = priority?.name ?: DataManager.getTaskPriority(priorityId)?.name + val priorityDisplayName: String? get() = priority?.displayName ?: DataManager.getTaskPriority(priorityId)?.displayName // Effective due date: use nextDueDate if set (for recurring tasks), otherwise use dueDate val effectiveDueDate: String? get() = nextDueDate ?: dueDate @@ -82,7 +84,7 @@ data class TaskResponse( // Fields that don't exist in Go API - return null/default val nextScheduledDate: String? get() = nextDueDate // Now we have this from the API val showCompletedButton: Boolean get() = true // Always show complete button since status is now just in_progress boolean - val daySpan: Int? get() = frequency?.days + val daySpan: Int? get() = frequency?.days ?: DataManager.getTaskFrequency(frequencyId)?.days val notifyDays: Int? get() = null // Not in Go API } diff --git a/iosApp/iosApp/Helpers/WidgetDataManager.swift b/iosApp/iosApp/Helpers/WidgetDataManager.swift index 4aec92f..f40aef0 100644 --- a/iosApp/iosApp/Helpers/WidgetDataManager.swift +++ b/iosApp/iosApp/Helpers/WidgetDataManager.swift @@ -273,10 +273,10 @@ final class WidgetDataManager { id: Int(task.id), title: task.title, description: task.description_, - priority: task.priority?.name ?? "", + priority: task.priorityName ?? "", inProgress: task.inProgress, dueDate: task.effectiveDueDate, // Use effective date (nextDueDate if set, otherwise dueDate) - category: task.category?.name ?? "", + category: task.categoryName ?? "", residenceName: "", // No longer available in API, residence lookup needed isOverdue: column.name == "overdue_tasks" ) diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index 6bfc656..57b2459 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -483,7 +483,8 @@ private extension ResidenceDetailView { Task { do { let result = try await APILayer.shared.getContractorsByResidence( - residenceId: Int32(Int(residenceId)) + residenceId: Int32(Int(residenceId)), + forceRefresh: false ) await MainActor.run { diff --git a/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift b/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift index ce2d060..34ef905 100644 --- a/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift @@ -16,7 +16,7 @@ struct DynamicTaskCard: View { @State private var showCompletionHistory = false var body: some View { - let _ = print("📋 DynamicTaskCard - Task: \(task.title), ButtonTypes: \(buttonTypes)") +// let _ = print("📋 DynamicTaskCard - Task: \(task.title), ButtonTypes: \(buttonTypes)") VStack(alignment: .leading, spacing: 12) { HStack { @@ -32,7 +32,7 @@ struct DynamicTaskCard: View { Spacer() - PriorityBadge(priority: task.priority?.name ?? "") + PriorityBadge(priority: task.priorityName ?? "") } if !task.description_.isEmpty { @@ -43,7 +43,7 @@ struct DynamicTaskCard: View { } HStack { - Label(task.frequency?.displayName ?? "", systemImage: "repeat") + Label(task.frequencyDisplayName ?? "", systemImage: "repeat") .font(.caption) .foregroundColor(Color.appTextSecondary) diff --git a/iosApp/iosApp/Subviews/Task/TaskCard.swift b/iosApp/iosApp/Subviews/Task/TaskCard.swift index 90619bd..cc69de9 100644 --- a/iosApp/iosApp/Subviews/Task/TaskCard.swift +++ b/iosApp/iosApp/Subviews/Task/TaskCard.swift @@ -30,7 +30,7 @@ struct TaskCard: View { Spacer() - PriorityBadge(priority: task.priority?.name ?? "") + PriorityBadge(priority: task.priorityName ?? "") } // Description @@ -47,7 +47,7 @@ struct TaskCard: View { Image(systemName: "repeat") .font(.system(size: 12, weight: .medium)) .foregroundColor(Color.appTextSecondary.opacity(0.7)) - Text(task.frequency?.displayName ?? "") + Text(task.frequencyDisplayName ?? "") .font(.caption.weight(.medium)) .foregroundColor(Color.appTextSecondary) } diff --git a/iosApp/iosApp/Task/CompleteTaskView.swift b/iosApp/iosApp/Task/CompleteTaskView.swift index aaa1bf1..b54f533 100644 --- a/iosApp/iosApp/Task/CompleteTaskView.swift +++ b/iosApp/iosApp/Task/CompleteTaskView.swift @@ -33,7 +33,7 @@ struct CompleteTaskView: View { .font(.headline) HStack { - Label((task.category?.name ?? "").capitalized, systemImage: "folder") + Label((task.categoryName ?? "").capitalized, systemImage: "folder") .font(.subheadline) .foregroundStyle(.secondary)