Consolidate task metrics to single source of truth

- Add API column name constants to WidgetDataManager (overdueColumn,
  dueWithin7DaysColumn, due8To30DaysColumn, etc.)
- Update DataManagerObservable to use WidgetDataManager column constants
- Remove duplicate ResidenceTaskStats struct, use TaskMetrics everywhere
- Delete TaskStatsCalculator.swift (consolidated into WidgetDataManager)
- Rename confusing flags: isUpcoming → isDueWithin7Days, isLater → isDue8To30Days
- Add comprehensive unit tests for TaskMetrics and WidgetTask

🤖 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-23 20:26:52 -06:00
parent cacdf86938
commit 4daaa1f7d8
8 changed files with 637 additions and 240 deletions

View File

@@ -23,10 +23,7 @@ struct ResidencesListView: View {
isLoading: viewModel.isLoading,
errorMessage: viewModel.errorMessage,
content: { residences in
ResidencesContent(
residences: residences,
tasksResponse: taskViewModel.tasksResponse
)
ResidencesContent(residences: residences)
},
emptyContent: {
OrganicEmptyResidencesView()
@@ -170,67 +167,28 @@ private struct OrganicToolbarButton: View {
}
}
// MARK: - Residence Task Stats
struct ResidenceTaskStats {
let totalCount: Int
let overdueCount: Int
let dueThisWeekCount: Int
let dueNext30DaysCount: Int
}
// MARK: - Residences Content View
private struct ResidencesContent: View {
let residences: [ResidenceResponse]
let tasksResponse: TaskColumnsResponse?
@ObservedObject private var dataManager = DataManagerObservable.shared
/// Extract active tasks - skip completed_tasks and cancelled_tasks columns
private var activeTasks: [TaskResponse] {
guard let response = tasksResponse else { return [] }
var tasks: [TaskResponse] = []
for column in response.columns {
// Skip completed and cancelled columns (cancelled includes archived)
let columnName = column.name.lowercased()
if columnName == "completed_tasks" || columnName == "cancelled_tasks" {
continue
}
// Add tasks from this column
for task in column.tasks {
tasks.append(task)
}
}
return tasks
}
/// Compute total summary from task data using shared TaskStatsCalculator
/// Compute total summary from DataManagerObservable (single source of truth)
private var computedSummary: TotalSummary {
let dueDates = activeTasks.map { $0.effectiveDueDate }
let stats = TaskStatsCalculator.calculate(from: dueDates)
let metrics = dataManager.totalTaskMetrics
return TotalSummary(
totalResidences: Int32(residences.count),
totalTasks: Int32(activeTasks.count),
totalTasks: Int32(dataManager.activeTasks.count),
totalPending: 0,
totalOverdue: Int32(stats.overdueCount),
tasksDueNextWeek: Int32(stats.next7DaysCount),
tasksDueNextMonth: Int32(stats.next30DaysCount)
totalOverdue: Int32(metrics.overdueCount),
tasksDueNextWeek: Int32(metrics.upcoming7Days),
tasksDueNextMonth: Int32(metrics.upcoming30Days)
)
}
/// Get task stats for a specific residence using shared TaskStatsCalculator
private func taskStats(for residenceId: Int32) -> ResidenceTaskStats {
let residenceTasks = activeTasks.filter { $0.residenceId == residenceId }
let dueDates = residenceTasks.map { $0.effectiveDueDate }
let stats = TaskStatsCalculator.calculate(from: dueDates)
return ResidenceTaskStats(
totalCount: residenceTasks.count,
overdueCount: stats.overdueCount,
dueThisWeekCount: stats.next7DaysCount,
dueNext30DaysCount: stats.next30DaysCount
)
/// Get task metrics for a specific residence from DataManagerObservable
private func taskMetrics(for residenceId: Int32) -> WidgetDataManager.TaskMetrics {
dataManager.taskMetrics(for: residenceId)
}
var body: some View {
@@ -247,7 +205,7 @@ private struct ResidencesContent: View {
NavigationLink(destination: ResidenceDetailView(residenceId: residence.id)) {
ResidenceCard(
residence: residence,
taskStats: taskStats(for: residence.id)
taskMetrics: taskMetrics(for: residence.id)
)
.padding(.horizontal, 16)
}