Replace status_id with in_progress boolean across mobile apps

- Remove TaskStatus model and status_id foreign key references
- Add in_progress boolean field to task models and forms
- Update TaskApi to use dedicated POST endpoints for task actions:
  - POST /tasks/:id/cancel/ instead of PATCH with is_cancelled
  - POST /tasks/:id/uncancel/
  - POST /tasks/:id/archive/
  - POST /tasks/:id/unarchive/
- Fix iOS TaskViewModel to use error-first pattern for Kotlin-Swift
  generic type bridging issues
- Update iOS callback signatures to pass full TaskResponse instead
  of just taskId to avoid stale closure lookups
- Add in_progress localization strings
- Update widget preview data to use inProgress boolean

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-08 20:47:59 -06:00
parent a067228597
commit 4a04aff1e6
33 changed files with 314 additions and 376 deletions

View File

@@ -64,7 +64,6 @@ class DataManagerObservable: ObservableObject {
@Published var residenceTypes: [ResidenceType] = []
@Published var taskFrequencies: [TaskFrequency] = []
@Published var taskPriorities: [TaskPriority] = []
@Published var taskStatuses: [TaskStatus] = []
@Published var taskCategories: [TaskCategory] = []
@Published var contractorSpecialties: [ContractorSpecialty] = []
@@ -292,16 +291,6 @@ class DataManagerObservable: ObservableObject {
}
observationTasks.append(taskPrioritiesTask)
// Lookups - TaskStatuses
let taskStatusesTask = Task {
for await items in DataManager.shared.taskStatuses {
await MainActor.run {
self.taskStatuses = items
}
}
}
observationTasks.append(taskStatusesTask)
// Lookups - TaskCategories
let taskCategoriesTask = Task {
for await items in DataManager.shared.taskCategories {
@@ -459,12 +448,6 @@ class DataManagerObservable: ObservableObject {
return taskPriorities.first { $0.id == id }
}
/// Get task status by ID
func getTaskStatus(id: Int32?) -> TaskStatus? {
guard let id = id else { return nil }
return taskStatuses.first { $0.id == id }
}
/// Get task category by ID
func getTaskCategory(id: Int32?) -> TaskCategory? {
guard let id = id else { return nil }

View File

@@ -251,6 +251,7 @@ enum L10n {
// Task Card Actions
static var inProgress: String { String(localized: "tasks_in_progress") }
static var inProgressLabel: String { String(localized: "tasks_in_progress_label") }
static var complete: String { String(localized: "tasks_complete") }
static var edit: String { String(localized: "tasks_edit") }
static var cancel: String { String(localized: "tasks_cancel") }

View File

@@ -93,6 +93,10 @@ final class WidgetActionProcessor {
let data = success.data {
// Update widget with fresh data
WidgetDataManager.shared.saveTasks(from: data)
// Update summary from response (no extra API call needed)
if let summary = data.summary {
DataManager.shared.setTotalSummary(summary: summary)
}
}
} catch {
print("WidgetActionProcessor: Error refreshing tasks: \(error)")

View File

@@ -223,14 +223,15 @@ final class WidgetDataManager {
let title: String
let description: String?
let priority: String?
let status: String?
let inProgress: Bool
let dueDate: String?
let category: String?
let residenceName: String?
let isOverdue: Bool
enum CodingKeys: String, CodingKey {
case id, title, description, priority, status, category
case id, title, description, priority, category
case inProgress = "in_progress"
case dueDate = "due_date"
case residenceName = "residence_name"
case isOverdue = "is_overdue"
@@ -273,7 +274,7 @@ final class WidgetDataManager {
title: task.title,
description: task.description_,
priority: task.priority?.name ?? "",
status: task.status?.name,
inProgress: task.inProgress,
dueDate: task.dueDate,
category: task.category?.name ?? "",
residenceName: "", // No longer available in API, residence lookup needed
@@ -325,14 +326,9 @@ final class WidgetDataManager {
func getUpcomingTasks() -> [WidgetTask] {
let allTasks = loadTasks()
// Filter for pending/in-progress tasks (non-archived, non-completed)
let upcoming = allTasks.filter { task in
let status = task.status?.lowercased() ?? ""
return status == "pending" || status == "in_progress" || status == "in progress"
}
// All loaded tasks are already filtered (archived and completed columns are excluded during save)
// Sort by due date (earliest first), with overdue at top
return upcoming.sorted { task1, task2 in
return allTasks.sorted { task1, task2 in
// Overdue tasks first
if task1.isOverdue != task2.isOverdue {
return task1.isOverdue

View File

@@ -27381,6 +27381,71 @@
}
}
},
"tasks_in_progress_label" : {
"extractionState" : "manual",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "In Bearbeitung"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "In Progress"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "En Progreso"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "En Cours"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "In corso"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "進行中"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "진행 중"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "In behandeling"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"value" : "Em Andamento"
}
},
"zh" : {
"stringUnit" : {
"state" : "translated",
"value" : "进行中"
}
}
}
},
"tasks_library" : {
"extractionState" : "manual",
"localizations" : {

View File

@@ -85,6 +85,10 @@ class AppleSignInViewModel: ObservableObject {
// - Initializes lookups
// - Prefetches all data
// Share token and API URL with widget extension
WidgetDataManager.shared.saveAuthToken(response.token)
WidgetDataManager.shared.saveAPIBaseURL(ApiClient.shared.getBaseUrl())
// Track Apple Sign In
PostHogAnalytics.shared.capture(AnalyticsEvents.userSignedInApple, properties: [
"is_new_user": isNewUser

View File

@@ -361,7 +361,7 @@ struct OnboardingFirstTaskContent: View {
description: nil,
categoryId: categoryId.map { KotlinInt(int: $0) },
priorityId: nil,
statusId: nil,
inProgress: false,
frequencyId: frequencyId.map { KotlinInt(int: $0) },
assignedToId: nil,
dueDate: todayString,

View File

@@ -506,8 +506,8 @@ private struct TasksSectionContainer: View {
selectedTaskForEdit = task
showEditTask = true
},
onCancelTask: { taskId in
taskViewModel.cancelTask(id: taskId) { _ in
onCancelTask: { task in
taskViewModel.cancelTask(id: task.id) { _ in
reloadTasks()
}
},
@@ -526,12 +526,9 @@ private struct TasksSectionContainer: View {
onCompleteTask: { task in
selectedTaskForComplete = task
},
onArchiveTask: { taskId in
let allTasks = tasksResponse.columns.flatMap { $0.tasks }
if let task = allTasks.first(where: { $0.id == taskId }) {
selectedTaskForArchive = task
showArchiveConfirmation = true
}
onArchiveTask: { task in
selectedTaskForArchive = task
showArchiveConfirmation = true
},
onUnarchiveTask: { taskId in
taskViewModel.unarchiveTask(id: taskId) { _ in

View File

@@ -25,8 +25,8 @@ struct DynamicTaskCard: View {
.font(.title3)
.foregroundColor(.primary)
if let status = task.status {
StatusBadge(status: status.name)
if task.inProgress {
StatusBadge(status: "in_progress")
}
}

View File

@@ -5,11 +5,11 @@ import ComposeApp
struct DynamicTaskColumnView: View {
let column: TaskColumn
let onEditTask: (TaskResponse) -> Void
let onCancelTask: (Int32) -> Void
let onCancelTask: (TaskResponse) -> Void
let onUncancelTask: (Int32) -> Void
let onMarkInProgress: (Int32) -> Void
let onCompleteTask: (TaskResponse) -> Void
let onArchiveTask: (Int32) -> Void
let onArchiveTask: (TaskResponse) -> Void
let onUnarchiveTask: (Int32) -> Void
// Get icon from API response, with fallback
@@ -65,11 +65,11 @@ struct DynamicTaskColumnView: View {
task: task,
buttonTypes: column.buttonTypes,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task.id) },
onCancel: { onCancelTask(task) },
onUncancel: { onUncancelTask(task.id) },
onMarkInProgress: { onMarkInProgress(task.id) },
onComplete: { onCompleteTask(task) },
onArchive: { onArchiveTask(task.id) },
onArchive: { onArchiveTask(task) },
onUnarchive: { onUnarchiveTask(task.id) }
)
}

View File

@@ -23,8 +23,8 @@ struct TaskCard: View {
.foregroundColor(Color.appTextPrimary)
.lineLimit(2)
if let status = task.status {
StatusBadge(status: status.name)
if task.inProgress {
StatusBadge(status: "in_progress")
}
}
@@ -116,7 +116,7 @@ struct TaskCard: View {
// Primary Actions
if task.showCompletedButton {
VStack(spacing: AppSpacing.xs) {
if let onMarkInProgress = onMarkInProgress, task.status?.name != "in_progress" {
if let onMarkInProgress = onMarkInProgress, !task.inProgress {
Button(action: onMarkInProgress) {
HStack(spacing: AppSpacing.xs) {
Image(systemName: "play.circle.fill")
@@ -258,8 +258,7 @@ struct TaskCard: View {
category: TaskCategory(id: 1, name: "maintenance", description: "", icon: "", color: "", displayOrder: 0),
priorityId: 2,
priority: TaskPriority(id: 2, name: "medium", level: 2, color: "", displayOrder: 0),
statusId: 1,
status: TaskStatus(id: 1, name: "pending", description: "", color: "", displayOrder: 0),
inProgress: false,
frequencyId: 1,
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
dueDate: "2024-12-15",

View File

@@ -4,11 +4,11 @@ import ComposeApp
struct TasksSection: View {
let tasksResponse: TaskColumnsResponse
let onEditTask: (TaskResponse) -> Void
let onCancelTask: (Int32) -> Void
let onCancelTask: (TaskResponse) -> Void
let onUncancelTask: (Int32) -> Void
let onMarkInProgress: (Int32) -> Void
let onCompleteTask: (TaskResponse) -> Void
let onArchiveTask: (Int32) -> Void
let onArchiveTask: (TaskResponse) -> Void
let onUnarchiveTask: (Int32) -> Void
private var hasNoTasks: Bool {
@@ -35,8 +35,8 @@ struct TasksSection: View {
onEditTask: { task in
onEditTask(task)
},
onCancelTask: { taskId in
onCancelTask(taskId)
onCancelTask: { task in
onCancelTask(task)
},
onUncancelTask: { taskId in
onUncancelTask(taskId)
@@ -47,8 +47,8 @@ struct TasksSection: View {
onCompleteTask: { task in
onCompleteTask(task)
},
onArchiveTask: { taskId in
onArchiveTask(taskId)
onArchiveTask: { task in
onArchiveTask(task)
},
onUnarchiveTask: { taskId in
onUnarchiveTask(taskId)
@@ -92,8 +92,7 @@ struct TasksSection: View {
category: TaskCategory(id: 1, name: "maintenance", description: "", icon: "", color: "", displayOrder: 0),
priorityId: 2,
priority: TaskPriority(id: 2, name: "medium", level: 2, color: "", displayOrder: 0),
statusId: 1,
status: TaskStatus(id: 1, name: "pending", description: "", color: "", displayOrder: 0),
inProgress: false,
frequencyId: 1,
frequency: TaskFrequency(id: 1, name: "monthly", days: 30, displayOrder: 0),
dueDate: "2024-12-15",
@@ -132,8 +131,7 @@ struct TasksSection: View {
category: TaskCategory(id: 2, name: "plumbing", description: "", icon: "", color: "", displayOrder: 0),
priorityId: 3,
priority: TaskPriority(id: 3, name: "high", level: 3, color: "", displayOrder: 0),
statusId: 3,
status: TaskStatus(id: 3, name: "completed", description: "", color: "", displayOrder: 0),
inProgress: false,
frequencyId: 6,
frequency: TaskFrequency(id: 6, name: "once", days: nil, displayOrder: 0),
dueDate: "2024-11-01",
@@ -154,7 +152,7 @@ struct TasksSection: View {
)
],
daysThreshold: 30,
residenceId: "1"
residenceId: "1", summary: nil
),
onEditTask: { _ in },
onCancelTask: { _ in },

View File

@@ -221,12 +221,9 @@ struct AllTasksView: View {
selectedTaskForEdit = task
showEditTask = true
},
onCancelTask: { taskId in
let allTasks = tasksResponse.columns.flatMap { $0.tasks }
if let task = allTasks.first(where: { $0.id == taskId }) {
selectedTaskForCancel = task
showCancelConfirmation = true
}
onCancelTask: { task in
selectedTaskForCancel = task
showCancelConfirmation = true
},
onUncancelTask: { taskId in
taskViewModel.uncancelTask(id: taskId) { _ in
@@ -243,12 +240,9 @@ struct AllTasksView: View {
onCompleteTask: { task in
selectedTaskForComplete = task
},
onArchiveTask: { taskId in
let allTasks = tasksResponse.columns.flatMap { $0.tasks }
if let task = allTasks.first(where: { $0.id == taskId }) {
selectedTaskForArchive = task
showArchiveConfirmation = true
}
onArchiveTask: { task in
selectedTaskForArchive = task
showArchiveConfirmation = true
},
onUnarchiveTask: { taskId in
taskViewModel.unarchiveTask(id: taskId) { _ in

View File

@@ -39,8 +39,8 @@ struct CompleteTaskView: View {
Spacer()
if let status = task.status {
Text(status.displayName)
if task.inProgress {
Text(L10n.Tasks.inProgress)
.font(.caption)
.foregroundStyle(.secondary)
.padding(.horizontal, 8)

View File

@@ -29,15 +29,13 @@ struct TaskFormView: View {
(!needsResidenceSelection || selectedResidence != nil) &&
selectedCategory != nil &&
selectedFrequency != nil &&
selectedPriority != nil &&
selectedStatus != nil
selectedPriority != nil
}
// Lookups from DataManagerObservable
private var taskCategories: [TaskCategory] { dataManager.taskCategories }
private var taskFrequencies: [TaskFrequency] { dataManager.taskFrequencies }
private var taskPriorities: [TaskPriority] { dataManager.taskPriorities }
private var taskStatuses: [TaskStatus] { dataManager.taskStatuses }
private var isLoadingLookups: Bool { !dataManager.lookupsInitialized }
// Form fields
@@ -47,7 +45,7 @@ struct TaskFormView: View {
@State private var selectedCategory: TaskCategory?
@State private var selectedFrequency: TaskFrequency?
@State private var selectedPriority: TaskPriority?
@State private var selectedStatus: TaskStatus?
@State private var inProgress: Bool
@State private var dueDate: Date
@State private var intervalDays: String
@State private var estimatedCost: String
@@ -66,7 +64,7 @@ struct TaskFormView: View {
_selectedCategory = State(initialValue: task.category)
_selectedFrequency = State(initialValue: task.frequency)
_selectedPriority = State(initialValue: task.priority)
_selectedStatus = State(initialValue: task.status)
_inProgress = State(initialValue: task.inProgress)
// Parse date from string
let formatter = DateFormatter()
@@ -78,6 +76,7 @@ struct TaskFormView: View {
} else {
_title = State(initialValue: "")
_description = State(initialValue: "")
_inProgress = State(initialValue: false)
_dueDate = State(initialValue: Date())
_intervalDays = State(initialValue: "")
_estimatedCost = State(initialValue: "")
@@ -246,16 +245,11 @@ struct TaskFormView: View {
}
}
Picker(L10n.Tasks.status, selection: $selectedStatus) {
Text(L10n.Tasks.selectStatus).tag(nil as TaskStatus?)
ForEach(taskStatuses, id: \.id) { status in
Text(status.displayName).tag(status as TaskStatus?)
}
}
Toggle(L10n.Tasks.inProgressLabel, isOn: $inProgress)
} header: {
Text(L10n.Tasks.priorityAndStatus)
} footer: {
Text(L10n.Tasks.bothRequired)
Text(L10n.Tasks.required)
.font(.caption)
.foregroundColor(Color.appError)
}
@@ -409,11 +403,6 @@ struct TaskFormView: View {
selectedPriority = taskPriorities.first { $0.name == "medium" } ?? taskPriorities.first
}
if selectedStatus == nil && !taskStatuses.isEmpty {
// Default to "pending"
selectedStatus = taskStatuses.first { $0.name == "pending" } ?? taskStatuses.first
}
// Set default residence if provided
if needsResidenceSelection && selectedResidence == nil, let residences = residences, !residences.isEmpty {
selectedResidence = residences.first
@@ -452,11 +441,6 @@ struct TaskFormView: View {
isValid = false
}
if selectedStatus == nil {
viewModel.errorMessage = "Please select a status"
isValid = false
}
return isValid
}
@@ -465,8 +449,7 @@ struct TaskFormView: View {
guard let category = selectedCategory,
let frequency = selectedFrequency,
let priority = selectedPriority,
let status = selectedStatus else {
let priority = selectedPriority else {
return
}
@@ -483,7 +466,7 @@ struct TaskFormView: View {
description: description.isEmpty ? nil : description,
categoryId: KotlinInt(int: Int32(category.id)),
priorityId: KotlinInt(int: Int32(priority.id)),
statusId: KotlinInt(int: Int32(status.id)),
inProgress: inProgress,
frequencyId: KotlinInt(int: Int32(frequency.id)),
assignedToId: nil,
dueDate: dueDateString,
@@ -514,7 +497,7 @@ struct TaskFormView: View {
description: description.isEmpty ? nil : description,
categoryId: KotlinInt(int: Int32(category.id)),
priorityId: KotlinInt(int: Int32(priority.id)),
statusId: selectedStatus.map { KotlinInt(int: Int32($0.id)) },
inProgress: inProgress,
frequencyId: KotlinInt(int: Int32(frequency.id)),
assignedToId: nil,
dueDate: dueDateString,

View File

@@ -75,14 +75,13 @@ class TaskViewModel: ObservableObject {
do {
let result = try await APILayer.shared.createTask(request: request)
if result is ApiResultSuccess<TaskResponse> {
self.actionState = .success(.create)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
if let error = result as? ApiResultError {
self.actionState = .error(.create, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false)
} else {
self.actionState = .success(.create)
completion(true)
}
} catch {
self.actionState = .error(.create, error.localizedDescription)
@@ -100,14 +99,16 @@ class TaskViewModel: ObservableObject {
do {
let result = try await APILayer.shared.cancelTask(taskId: id)
if result is ApiResultSuccess<TaskResponse> {
self.actionState = .success(.cancel)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
// Check for error first, then treat non-error as success
// This handles Kotlin-Swift generic type bridging issues
if let error = result as? ApiResultError {
self.actionState = .error(.cancel, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false)
} else {
// Not an error = success (DataManager is updated by APILayer)
self.actionState = .success(.cancel)
completion(true)
}
} catch {
self.actionState = .error(.cancel, error.localizedDescription)
@@ -125,14 +126,13 @@ class TaskViewModel: ObservableObject {
do {
let result = try await APILayer.shared.uncancelTask(taskId: id)
if result is ApiResultSuccess<TaskResponse> {
self.actionState = .success(.uncancel)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
if let error = result as? ApiResultError {
self.actionState = .error(.uncancel, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false)
} else {
self.actionState = .success(.uncancel)
completion(true)
}
} catch {
self.actionState = .error(.uncancel, error.localizedDescription)
@@ -150,14 +150,13 @@ class TaskViewModel: ObservableObject {
do {
let result = try await APILayer.shared.markInProgress(taskId: id)
if result is ApiResultSuccess<TaskResponse> {
self.actionState = .success(.markInProgress)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
if let error = result as? ApiResultError {
self.actionState = .error(.markInProgress, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false)
} else {
self.actionState = .success(.markInProgress)
completion(true)
}
} catch {
self.actionState = .error(.markInProgress, error.localizedDescription)
@@ -175,14 +174,13 @@ class TaskViewModel: ObservableObject {
do {
let result = try await APILayer.shared.archiveTask(taskId: id)
if result is ApiResultSuccess<TaskResponse> {
self.actionState = .success(.archive)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
if let error = result as? ApiResultError {
self.actionState = .error(.archive, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false)
} else {
self.actionState = .success(.archive)
completion(true)
}
} catch {
self.actionState = .error(.archive, error.localizedDescription)
@@ -200,14 +198,13 @@ class TaskViewModel: ObservableObject {
do {
let result = try await APILayer.shared.unarchiveTask(taskId: id)
if result is ApiResultSuccess<TaskResponse> {
self.actionState = .success(.unarchive)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
if let error = result as? ApiResultError {
self.actionState = .error(.unarchive, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false)
} else {
self.actionState = .success(.unarchive)
completion(true)
}
} catch {
self.actionState = .error(.unarchive, error.localizedDescription)
@@ -225,14 +222,13 @@ class TaskViewModel: ObservableObject {
do {
let result = try await APILayer.shared.updateTask(id: id, request: request)
if result is ApiResultSuccess<TaskResponse> {
self.actionState = .success(.update)
// DataManager is updated by APILayer, view updates via observation
completion(true)
} else if let error = result as? ApiResultError {
if let error = result as? ApiResultError {
self.actionState = .error(.update, ErrorMessageParser.parse(error.message))
self.errorMessage = ErrorMessageParser.parse(error.message)
completion(false)
} else {
self.actionState = .success(.update)
completion(true)
}
} catch {
self.actionState = .error(.update, error.localizedDescription)
@@ -406,7 +402,7 @@ class TaskViewModel: ObservableObject {
tasksResponse = TaskColumnsResponse(
columns: newColumns,
daysThreshold: currentResponse.daysThreshold,
residenceId: currentResponse.residenceId
residenceId: currentResponse.residenceId, summary: nil
)
}
@@ -434,7 +430,7 @@ class TaskViewModel: ObservableObject {
tasksResponse = TaskColumnsResponse(
columns: newColumns,
daysThreshold: currentResponse.daysThreshold,
residenceId: currentResponse.residenceId
residenceId: currentResponse.residenceId, summary: nil
)
}

View File

@@ -55,6 +55,13 @@ struct iOSApp: App {
}
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
// Sync auth token to widget if user is logged in
// This ensures widget has credentials even if user logged in before widget support was added
if let token = TokenStorage.shared.getToken() {
WidgetDataManager.shared.saveAuthToken(token)
WidgetDataManager.shared.saveAPIBaseURL(ApiClient.shared.getBaseUrl())
}
// Check and register device token when app becomes active
PushNotificationManager.shared.checkAndRegisterDeviceIfNeeded()
@@ -67,6 +74,24 @@ struct iOSApp: App {
Task { @MainActor in
WidgetActionProcessor.shared.processPendingActions()
}
// Check if widget completed a task - refresh data globally
if WidgetDataManager.shared.areTasksDirty() {
WidgetDataManager.shared.clearDirtyFlag()
Task {
// Refresh tasks - response includes summary for dashboard stats
let result = try? await APILayer.shared.getTasks(forceRefresh: true)
if let success = result as? ApiResultSuccess<TaskColumnsResponse>,
let data = success.data {
// Update widget cache
WidgetDataManager.shared.saveTasks(from: data)
// Update summary from response (no extra API call needed)
if let summary = data.summary {
DataManager.shared.setTotalSummary(summary: summary)
}
}
}
}
} else if newPhase == .background {
// Refresh widget when app goes to background
WidgetCenter.shared.reloadAllTimelines()