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

@@ -69,6 +69,8 @@ class CacheManager {
let category: String?
let residenceName: String?
let isOverdue: Bool
let isDueWithin7Days: Bool
let isDue8To30Days: Bool
enum CodingKeys: String, CodingKey {
case id, title, description, priority, category
@@ -76,6 +78,8 @@ class CacheManager {
case dueDate = "due_date"
case residenceName = "residence_name"
case isOverdue = "is_overdue"
case isDueWithin7Days = "is_due_within_7_days"
case isDue8To30Days = "is_due_8_to_30_days"
}
/// Whether this task is pending completion (tapped on widget, waiting for sync)
@@ -203,27 +207,19 @@ struct SimpleEntry: TimelineEntry {
upcomingTasks.first
}
/// Computed task stats using shared TaskStatsCalculator
/// Uses exclusive buckets: overdue | next 7 days | next 30 days (8-30)
private var calculatedStats: TaskStats {
let dueDates = upcomingTasks.map { $0.dueDate }
return TaskStatsCalculator.calculate(from: dueDates)
}
/// Overdue tasks count - uses the isOverdue flag from kanban categorization
/// which correctly excludes in-progress tasks
/// Overdue tasks count - uses isOverdue flag from API column
var overdueCount: Int {
upcomingTasks.filter { $0.isOverdue }.count
}
/// Tasks due within the next 7 days (exclusive of overdue)
/// Tasks due within the next 7 days
var dueNext7DaysCount: Int {
calculatedStats.next7DaysCount
upcomingTasks.filter { $0.isDueWithin7Days }.count
}
/// Tasks due within next 30 days (days 8-30, exclusive of next 7 days)
/// Tasks due within 8-30 days
var dueNext30DaysCount: Int {
calculatedStats.next30DaysCount
upcomingTasks.filter { $0.isDue8To30Days }.count
}
}
@@ -785,7 +781,9 @@ struct Casera: Widget {
dueDate: "2024-12-15",
category: "plumbing",
residenceName: "Home",
isOverdue: false
isOverdue: false,
isDueWithin7Days: true,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 2,
@@ -796,7 +794,9 @@ struct Casera: Widget {
dueDate: "2024-12-20",
category: "painting",
residenceName: "Home",
isOverdue: false
isOverdue: false,
isDueWithin7Days: true,
isDue8To30Days: false
)
],
isInteractive: true
@@ -816,7 +816,9 @@ struct Casera: Widget {
dueDate: "2024-12-15",
category: "plumbing",
residenceName: "Home",
isOverdue: false
isOverdue: false,
isDueWithin7Days: true,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 2,
@@ -827,7 +829,9 @@ struct Casera: Widget {
dueDate: "2024-12-20",
category: "painting",
residenceName: "Home",
isOverdue: false
isOverdue: false,
isDueWithin7Days: true,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 3,
@@ -838,7 +842,9 @@ struct Casera: Widget {
dueDate: "2024-12-25",
category: "maintenance",
residenceName: "Home",
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: true
)
],
isInteractive: false
@@ -868,7 +874,9 @@ struct Casera: Widget {
dueDate: "2024-12-15",
category: "plumbing",
residenceName: "Home",
isOverdue: true
isOverdue: true,
isDueWithin7Days: false,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 2,
@@ -879,7 +887,9 @@ struct Casera: Widget {
dueDate: "2024-12-20",
category: "painting",
residenceName: "Cabin",
isOverdue: false
isOverdue: false,
isDueWithin7Days: true,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 3,
@@ -890,7 +900,9 @@ struct Casera: Widget {
dueDate: "2024-12-25",
category: "maintenance",
residenceName: "Home",
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: true
)
],
isInteractive: true
@@ -910,7 +922,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: true,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 2,
@@ -921,7 +935,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: true,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 3,
@@ -932,7 +948,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: true
),
CacheManager.CustomTask(
id: 4,
@@ -943,7 +961,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: true
),
CacheManager.CustomTask(
id: 5,
@@ -954,7 +974,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: true,
isDue8To30Days: false
)
],
isInteractive: false
@@ -984,7 +1006,9 @@ struct Casera: Widget {
dueDate: "2024-12-15",
category: "plumbing",
residenceName: "Home",
isOverdue: true
isOverdue: true,
isDueWithin7Days: false,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 2,
@@ -995,7 +1019,9 @@ struct Casera: Widget {
dueDate: "2024-12-20",
category: "painting",
residenceName: "Cabin",
isOverdue: false
isOverdue: false,
isDueWithin7Days: true,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 3,
@@ -1006,7 +1032,9 @@ struct Casera: Widget {
dueDate: "2024-12-25",
category: "maintenance",
residenceName: "Home",
isOverdue: false
isOverdue: false,
isDueWithin7Days: true,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 4,
@@ -1017,7 +1045,9 @@ struct Casera: Widget {
dueDate: "2024-12-28",
category: "hvac",
residenceName: "Beach House",
isOverdue: false
isOverdue: false,
isDueWithin7Days: true,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 5,
@@ -1028,7 +1058,9 @@ struct Casera: Widget {
dueDate: "2024-12-30",
category: "safety",
residenceName: "Home",
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: true
),
CacheManager.CustomTask(
id: 6,
@@ -1039,7 +1071,9 @@ struct Casera: Widget {
dueDate: "2025-01-05",
category: "plumbing",
residenceName: "Cabin",
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: true
),
CacheManager.CustomTask(
id: 7,
@@ -1050,7 +1084,9 @@ struct Casera: Widget {
dueDate: "2025-01-10",
category: "exterior",
residenceName: "Home",
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: true
),
CacheManager.CustomTask(
id: 8,
@@ -1061,7 +1097,9 @@ struct Casera: Widget {
dueDate: "2025-01-12",
category: "appliances",
residenceName: "Beach House",
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: true
)
],
isInteractive: true
@@ -1081,7 +1119,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 2,
@@ -1092,7 +1132,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 3,
@@ -1103,7 +1145,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 4,
@@ -1114,7 +1158,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 5,
@@ -1125,7 +1171,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 6,
@@ -1136,7 +1184,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: false
),
CacheManager.CustomTask(
id: 7,
@@ -1147,7 +1197,9 @@ struct Casera: Widget {
dueDate: nil,
category: nil,
residenceName: nil,
isOverdue: false
isOverdue: false,
isDueWithin7Days: false,
isDue8To30Days: false
)
],
isInteractive: false