Fix iOS widget to use App Group shared data and add large widget view
- Create WidgetDataManager to write task data as JSON to App Group shared container - Update widget CacheManager to read from App Group file instead of UserDefaults - Add LargeWidgetView for .systemLarge widget family showing 5-6 tasks - Add widget data save in AllTasksView after tasks load successfully - Clear widget cache on logout in AuthenticationManager - Add residenceName and isOverdue fields to widget task model 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@
|
|||||||
import WidgetKit
|
import WidgetKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
/// CacheManager reads tasks from the App Group shared container
|
||||||
|
/// Data is written by the main app via WidgetDataManager
|
||||||
class CacheManager {
|
class CacheManager {
|
||||||
struct CustomTask: Codable {
|
struct CustomTask: Codable {
|
||||||
let id: Int
|
let id: Int
|
||||||
@@ -17,38 +19,69 @@ class CacheManager {
|
|||||||
let status: String?
|
let status: String?
|
||||||
let dueDate: String?
|
let dueDate: String?
|
||||||
let category: String?
|
let category: String?
|
||||||
|
let residenceName: String?
|
||||||
|
let isOverdue: Bool
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id, title, description, priority, status, category
|
case id, title, description, priority, status, category
|
||||||
case dueDate = "due_date"
|
case dueDate = "due_date"
|
||||||
|
case residenceName = "residence_name"
|
||||||
|
case isOverdue = "is_overdue"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let userDefaults = UserDefaults.standard
|
private static let appGroupIdentifier = "group.com.tt.mycrib.MyCribDev"
|
||||||
private static let cacheKey = "cached_tasks"
|
private static let tasksFileName = "widget_tasks.json"
|
||||||
|
|
||||||
|
/// Get the shared App Group container URL
|
||||||
|
private static var sharedContainerURL: URL? {
|
||||||
|
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the URL for the tasks file
|
||||||
|
private static var tasksFileURL: URL? {
|
||||||
|
sharedContainerURL?.appendingPathComponent(tasksFileName)
|
||||||
|
}
|
||||||
|
|
||||||
static func getData() -> [CustomTask] {
|
static func getData() -> [CustomTask] {
|
||||||
|
guard let fileURL = tasksFileURL else {
|
||||||
|
print("CacheManager: Unable to access shared container")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
||||||
|
print("CacheManager: No cached tasks file found")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let string = UserDefaults.standard.string(forKey: cacheKey) ?? "[]"
|
let data = try Data(contentsOf: fileURL)
|
||||||
let jsonData = string.data(using: .utf8)!
|
let tasks = try JSONDecoder().decode([CustomTask].self, from: data)
|
||||||
let customTask = try JSONDecoder().decode([CustomTask].self, from: jsonData)
|
print("CacheManager: Loaded \(tasks.count) tasks from shared container")
|
||||||
return customTask
|
return tasks
|
||||||
} catch {
|
} catch {
|
||||||
print("Error decoding tasks: \(error)")
|
print("CacheManager: Error decoding tasks - \(error)")
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getUpcomingTasks() -> [CustomTask] {
|
static func getUpcomingTasks() -> [CustomTask] {
|
||||||
let allTasks = getData()
|
let allTasks = getData()
|
||||||
|
|
||||||
// Filter for pending/in-progress tasks, sorted by due date
|
// Filter for pending/in-progress tasks, sorted by due date
|
||||||
let upcoming = allTasks.filter { task in
|
let upcoming = allTasks.filter { task in
|
||||||
let status = task.status?.lowercased() ?? ""
|
let status = task.status?.lowercased() ?? ""
|
||||||
return status == "pending" || status == "in_progress" || status == "in progress"
|
return status == "pending" || status == "in_progress" || status == "in progress"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by due date (earliest first)
|
// Sort by due date (earliest first), with overdue at top
|
||||||
return upcoming.sorted { task1, task2 in
|
return upcoming.sorted { task1, task2 in
|
||||||
|
// Overdue tasks first
|
||||||
|
if task1.isOverdue != task2.isOverdue {
|
||||||
|
return task1.isOverdue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then by due date
|
||||||
guard let date1 = task1.dueDate, let date2 = task2.dueDate else {
|
guard let date1 = task1.dueDate, let date2 = task2.dueDate else {
|
||||||
return task1.dueDate != nil
|
return task1.dueDate != nil
|
||||||
}
|
}
|
||||||
@@ -115,6 +148,8 @@ struct MyCribEntryView : View {
|
|||||||
SmallWidgetView(entry: entry)
|
SmallWidgetView(entry: entry)
|
||||||
case .systemMedium:
|
case .systemMedium:
|
||||||
MediumWidgetView(entry: entry)
|
MediumWidgetView(entry: entry)
|
||||||
|
case .systemLarge:
|
||||||
|
LargeWidgetView(entry: entry)
|
||||||
default:
|
default:
|
||||||
SmallWidgetView(entry: entry)
|
SmallWidgetView(entry: entry)
|
||||||
}
|
}
|
||||||
@@ -363,6 +398,162 @@ struct TaskRowView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Large Widget View
|
||||||
|
struct LargeWidgetView: View {
|
||||||
|
let entry: SimpleEntry
|
||||||
|
|
||||||
|
private var maxTasksToShow: Int {
|
||||||
|
entry.upcomingTasks.count > 6 ? 5 : 6
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
// Header
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: "house.fill")
|
||||||
|
.font(.system(size: 16, weight: .semibold))
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
|
||||||
|
Text("MyCrib")
|
||||||
|
.font(.system(size: 16, weight: .bold))
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(entry.taskCount)")
|
||||||
|
.font(.system(size: 22, weight: .bold))
|
||||||
|
.foregroundStyle(.blue)
|
||||||
|
|
||||||
|
Text(entry.taskCount == 1 ? "task" : "tasks")
|
||||||
|
.font(.system(size: 10, weight: .medium))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.upcomingTasks.isEmpty {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Image(systemName: "checkmark.circle.fill")
|
||||||
|
.font(.system(size: 48))
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
Text("All caught up!")
|
||||||
|
.font(.system(size: 16, weight: .medium))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
} else {
|
||||||
|
// Show tasks
|
||||||
|
ForEach(Array(entry.upcomingTasks.prefix(maxTasksToShow).enumerated()), id: \.element.id) { index, task in
|
||||||
|
LargeTaskRowView(task: task, isOverdue: task.isOverdue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.upcomingTasks.count > 6 {
|
||||||
|
Text("+ \(entry.upcomingTasks.count - 5) more tasks")
|
||||||
|
.font(.system(size: 11, weight: .medium))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.padding(.top, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Large Task Row
|
||||||
|
struct LargeTaskRowView: View {
|
||||||
|
let task: CacheManager.CustomTask
|
||||||
|
let isOverdue: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
// Priority indicator
|
||||||
|
RoundedRectangle(cornerRadius: 2)
|
||||||
|
.fill(priorityColor)
|
||||||
|
.frame(width: 3, height: 32)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(task.title)
|
||||||
|
.font(.system(size: 12, weight: .medium))
|
||||||
|
.lineLimit(1)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
if let residenceName = task.residenceName {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
Image(systemName: "house.fill")
|
||||||
|
.font(.system(size: 7))
|
||||||
|
Text(residenceName)
|
||||||
|
.font(.system(size: 9))
|
||||||
|
}
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let dueDate = task.dueDate {
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
Image(systemName: "calendar")
|
||||||
|
.font(.system(size: 7))
|
||||||
|
Text(formatDate(dueDate))
|
||||||
|
.font(.system(size: 9, weight: isOverdue ? .semibold : .regular))
|
||||||
|
}
|
||||||
|
.foregroundStyle(isOverdue ? .red : .secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Priority badge
|
||||||
|
if let priority = task.priority {
|
||||||
|
Text(priority.prefix(1).uppercased())
|
||||||
|
.font(.system(size: 8, weight: .bold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: 16, height: 16)
|
||||||
|
.background(Circle().fill(priorityColor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.fill(Color.primary.opacity(0.05))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var priorityColor: Color {
|
||||||
|
switch task.priority?.lowercased() {
|
||||||
|
case "urgent": return .red
|
||||||
|
case "high": return .orange
|
||||||
|
case "medium": return .blue
|
||||||
|
default: return .gray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatDate(_ dateString: String) -> String {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
|
||||||
|
if let date = formatter.date(from: dateString) {
|
||||||
|
let calendar = Calendar.current
|
||||||
|
|
||||||
|
if calendar.isDateInToday(date) {
|
||||||
|
return "Today"
|
||||||
|
} else if calendar.isDateInTomorrow(date) {
|
||||||
|
return "Tomorrow"
|
||||||
|
} else if calendar.isDateInYesterday(date) {
|
||||||
|
return "Yesterday"
|
||||||
|
} else {
|
||||||
|
formatter.dateFormat = "MMM d"
|
||||||
|
return formatter.string(from: date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dateString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct MyCrib: Widget {
|
struct MyCrib: Widget {
|
||||||
let kind: String = "MyCrib"
|
let kind: String = "MyCrib"
|
||||||
|
|
||||||
@@ -389,7 +580,9 @@ struct MyCrib: Widget {
|
|||||||
priority: "high",
|
priority: "high",
|
||||||
status: "pending",
|
status: "pending",
|
||||||
dueDate: "2024-12-15",
|
dueDate: "2024-12-15",
|
||||||
category: "plumbing"
|
category: "plumbing",
|
||||||
|
residenceName: "Home",
|
||||||
|
isOverdue: false
|
||||||
),
|
),
|
||||||
CacheManager.CustomTask(
|
CacheManager.CustomTask(
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -398,7 +591,9 @@ struct MyCrib: Widget {
|
|||||||
priority: "medium",
|
priority: "medium",
|
||||||
status: "pending",
|
status: "pending",
|
||||||
dueDate: "2024-12-20",
|
dueDate: "2024-12-20",
|
||||||
category: "painting"
|
category: "painting",
|
||||||
|
residenceName: "Home",
|
||||||
|
isOverdue: false
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -424,7 +619,9 @@ struct MyCrib: Widget {
|
|||||||
priority: "high",
|
priority: "high",
|
||||||
status: "pending",
|
status: "pending",
|
||||||
dueDate: "2024-12-15",
|
dueDate: "2024-12-15",
|
||||||
category: "plumbing"
|
category: "plumbing",
|
||||||
|
residenceName: "Home",
|
||||||
|
isOverdue: true
|
||||||
),
|
),
|
||||||
CacheManager.CustomTask(
|
CacheManager.CustomTask(
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -433,7 +630,9 @@ struct MyCrib: Widget {
|
|||||||
priority: "medium",
|
priority: "medium",
|
||||||
status: "pending",
|
status: "pending",
|
||||||
dueDate: "2024-12-20",
|
dueDate: "2024-12-20",
|
||||||
category: "painting"
|
category: "painting",
|
||||||
|
residenceName: "Cabin",
|
||||||
|
isOverdue: false
|
||||||
),
|
),
|
||||||
CacheManager.CustomTask(
|
CacheManager.CustomTask(
|
||||||
id: 3,
|
id: 3,
|
||||||
@@ -442,7 +641,114 @@ struct MyCrib: Widget {
|
|||||||
priority: "low",
|
priority: "low",
|
||||||
status: "pending",
|
status: "pending",
|
||||||
dueDate: "2024-12-25",
|
dueDate: "2024-12-25",
|
||||||
category: "maintenance"
|
category: "maintenance",
|
||||||
|
residenceName: "Home",
|
||||||
|
isOverdue: false
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
SimpleEntry(
|
||||||
|
date: .now,
|
||||||
|
configuration: ConfigurationAppIntent(),
|
||||||
|
upcomingTasks: []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(as: .systemLarge) {
|
||||||
|
MyCrib()
|
||||||
|
} timeline: {
|
||||||
|
SimpleEntry(
|
||||||
|
date: .now,
|
||||||
|
configuration: ConfigurationAppIntent(),
|
||||||
|
upcomingTasks: [
|
||||||
|
CacheManager.CustomTask(
|
||||||
|
id: 1,
|
||||||
|
title: "Fix leaky faucet in kitchen",
|
||||||
|
description: "Kitchen sink needs repair",
|
||||||
|
priority: "high",
|
||||||
|
status: "pending",
|
||||||
|
dueDate: "2024-12-15",
|
||||||
|
category: "plumbing",
|
||||||
|
residenceName: "Home",
|
||||||
|
isOverdue: true
|
||||||
|
),
|
||||||
|
CacheManager.CustomTask(
|
||||||
|
id: 2,
|
||||||
|
title: "Paint living room walls",
|
||||||
|
description: nil,
|
||||||
|
priority: "medium",
|
||||||
|
status: "pending",
|
||||||
|
dueDate: "2024-12-20",
|
||||||
|
category: "painting",
|
||||||
|
residenceName: "Cabin",
|
||||||
|
isOverdue: false
|
||||||
|
),
|
||||||
|
CacheManager.CustomTask(
|
||||||
|
id: 3,
|
||||||
|
title: "Clean gutters",
|
||||||
|
description: "Remove debris",
|
||||||
|
priority: "low",
|
||||||
|
status: "pending",
|
||||||
|
dueDate: "2024-12-25",
|
||||||
|
category: "maintenance",
|
||||||
|
residenceName: "Home",
|
||||||
|
isOverdue: false
|
||||||
|
),
|
||||||
|
CacheManager.CustomTask(
|
||||||
|
id: 4,
|
||||||
|
title: "Replace HVAC filter",
|
||||||
|
description: nil,
|
||||||
|
priority: "medium",
|
||||||
|
status: "pending",
|
||||||
|
dueDate: "2024-12-28",
|
||||||
|
category: "hvac",
|
||||||
|
residenceName: "Beach House",
|
||||||
|
isOverdue: false
|
||||||
|
),
|
||||||
|
CacheManager.CustomTask(
|
||||||
|
id: 5,
|
||||||
|
title: "Check smoke detectors",
|
||||||
|
description: "Replace batteries if needed",
|
||||||
|
priority: "high",
|
||||||
|
status: "pending",
|
||||||
|
dueDate: "2024-12-30",
|
||||||
|
category: "safety",
|
||||||
|
residenceName: "Home",
|
||||||
|
isOverdue: false
|
||||||
|
),
|
||||||
|
CacheManager.CustomTask(
|
||||||
|
id: 6,
|
||||||
|
title: "Service water heater",
|
||||||
|
description: nil,
|
||||||
|
priority: "medium",
|
||||||
|
status: "pending",
|
||||||
|
dueDate: "2025-01-05",
|
||||||
|
category: "plumbing",
|
||||||
|
residenceName: "Cabin",
|
||||||
|
isOverdue: false
|
||||||
|
),
|
||||||
|
CacheManager.CustomTask(
|
||||||
|
id: 7,
|
||||||
|
title: "Inspect roof shingles",
|
||||||
|
description: nil,
|
||||||
|
priority: "low",
|
||||||
|
status: "pending",
|
||||||
|
dueDate: "2025-01-10",
|
||||||
|
category: "exterior",
|
||||||
|
residenceName: "Home",
|
||||||
|
isOverdue: false
|
||||||
|
),
|
||||||
|
CacheManager.CustomTask(
|
||||||
|
id: 8,
|
||||||
|
title: "Clean dryer vent",
|
||||||
|
description: "Fire hazard prevention",
|
||||||
|
priority: "urgent",
|
||||||
|
status: "pending",
|
||||||
|
dueDate: "2025-01-12",
|
||||||
|
category: "appliances",
|
||||||
|
residenceName: "Beach House",
|
||||||
|
isOverdue: false
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
164
iosApp/iosApp/Helpers/WidgetDataManager.swift
Normal file
164
iosApp/iosApp/Helpers/WidgetDataManager.swift
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import Foundation
|
||||||
|
import WidgetKit
|
||||||
|
import ComposeApp
|
||||||
|
|
||||||
|
/// Manages shared data between the main app and the widget extension
|
||||||
|
/// Uses App Group container to share files
|
||||||
|
final class WidgetDataManager {
|
||||||
|
static let shared = WidgetDataManager()
|
||||||
|
|
||||||
|
private let appGroupIdentifier = "group.com.tt.mycrib.MyCribDev"
|
||||||
|
private let tasksFileName = "widget_tasks.json"
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
/// Task model for widget display - simplified version of TaskDetail
|
||||||
|
struct WidgetTask: Codable {
|
||||||
|
let id: Int
|
||||||
|
let title: String
|
||||||
|
let description: String?
|
||||||
|
let priority: String?
|
||||||
|
let status: String?
|
||||||
|
let dueDate: String?
|
||||||
|
let category: String?
|
||||||
|
let residenceName: String?
|
||||||
|
let isOverdue: Bool
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id, title, description, priority, status, category
|
||||||
|
case dueDate = "due_date"
|
||||||
|
case residenceName = "residence_name"
|
||||||
|
case isOverdue = "is_overdue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the shared App Group container URL
|
||||||
|
private var sharedContainerURL: URL? {
|
||||||
|
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the URL for the tasks file
|
||||||
|
private var tasksFileURL: URL? {
|
||||||
|
sharedContainerURL?.appendingPathComponent(tasksFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save tasks to the shared container for widget access
|
||||||
|
/// Call this after loading tasks in the main app
|
||||||
|
func saveTasks(from response: TaskColumnsResponse) {
|
||||||
|
guard let fileURL = tasksFileURL else {
|
||||||
|
print("WidgetDataManager: Unable to access shared container")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract tasks from all columns and convert to WidgetTask
|
||||||
|
var allTasks: [WidgetTask] = []
|
||||||
|
|
||||||
|
for column in response.columns {
|
||||||
|
for task in column.tasks {
|
||||||
|
let widgetTask = WidgetTask(
|
||||||
|
id: Int(task.id),
|
||||||
|
title: task.title,
|
||||||
|
description: task.description_,
|
||||||
|
priority: task.priority.name,
|
||||||
|
status: task.status?.name,
|
||||||
|
dueDate: task.dueDate,
|
||||||
|
category: task.category.name,
|
||||||
|
residenceName: task.residenceName,
|
||||||
|
isOverdue: isTaskOverdue(dueDate: task.dueDate, status: task.status?.name)
|
||||||
|
)
|
||||||
|
allTasks.append(widgetTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
encoder.outputFormatting = .prettyPrinted
|
||||||
|
let data = try encoder.encode(allTasks)
|
||||||
|
try data.write(to: fileURL, options: .atomic)
|
||||||
|
print("WidgetDataManager: Saved \(allTasks.count) tasks to widget cache")
|
||||||
|
|
||||||
|
// Reload widget timeline
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
} catch {
|
||||||
|
print("WidgetDataManager: Error saving tasks - \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load tasks from the shared container
|
||||||
|
/// Used by the widget to read cached data
|
||||||
|
func loadTasks() -> [WidgetTask] {
|
||||||
|
guard let fileURL = tasksFileURL else {
|
||||||
|
print("WidgetDataManager: Unable to access shared container")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
||||||
|
print("WidgetDataManager: No cached tasks file found")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: fileURL)
|
||||||
|
let tasks = try JSONDecoder().decode([WidgetTask].self, from: data)
|
||||||
|
print("WidgetDataManager: Loaded \(tasks.count) tasks from widget cache")
|
||||||
|
return tasks
|
||||||
|
} catch {
|
||||||
|
print("WidgetDataManager: Error loading tasks - \(error)")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get upcoming/pending tasks for widget display
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by due date (earliest first), with overdue at top
|
||||||
|
return upcoming.sorted { task1, task2 in
|
||||||
|
// Overdue tasks first
|
||||||
|
if task1.isOverdue != task2.isOverdue {
|
||||||
|
return task1.isOverdue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then by due date
|
||||||
|
guard let date1 = task1.dueDate, let date2 = task2.dueDate else {
|
||||||
|
return task1.dueDate != nil
|
||||||
|
}
|
||||||
|
return date1 < date2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear cached task data (call on logout)
|
||||||
|
func clearCache() {
|
||||||
|
guard let fileURL = tasksFileURL else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: fileURL)
|
||||||
|
print("WidgetDataManager: Cleared widget cache")
|
||||||
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
|
} catch {
|
||||||
|
print("WidgetDataManager: Error clearing cache - \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a task is overdue based on due date and status
|
||||||
|
private func isTaskOverdue(dueDate: String?, status: String?) -> Bool {
|
||||||
|
guard let dueDateStr = dueDate else { return false }
|
||||||
|
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd"
|
||||||
|
|
||||||
|
guard let date = formatter.date(from: dueDateStr) else { return false }
|
||||||
|
|
||||||
|
// Task is overdue if due date is in the past and status is not completed
|
||||||
|
let statusLower = status?.lowercased() ?? ""
|
||||||
|
let isCompleted = statusLower == "completed" || statusLower == "done"
|
||||||
|
|
||||||
|
return !isCompleted && date < Calendar.current.startOfDay(for: Date())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,9 @@ class AuthenticationManager: ObservableObject {
|
|||||||
// Clear all cached data
|
// Clear all cached data
|
||||||
DataCache.shared.clearAll()
|
DataCache.shared.clearAll()
|
||||||
|
|
||||||
|
// Clear widget task data
|
||||||
|
WidgetDataManager.shared.clearCache()
|
||||||
|
|
||||||
// Update authentication state
|
// Update authentication state
|
||||||
isAuthenticated = false
|
isAuthenticated = false
|
||||||
|
|
||||||
|
|||||||
@@ -281,10 +281,14 @@ struct AllTasksView: View {
|
|||||||
do {
|
do {
|
||||||
let result = try await APILayer.shared.getTasks(forceRefresh: forceRefresh)
|
let result = try await APILayer.shared.getTasks(forceRefresh: forceRefresh)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
if let success = result as? ApiResultSuccess<TaskColumnsResponse> {
|
if let success = result as? ApiResultSuccess<TaskColumnsResponse>,
|
||||||
self.tasksResponse = success.data
|
let data = success.data {
|
||||||
|
self.tasksResponse = data
|
||||||
self.isLoadingTasks = false
|
self.isLoadingTasks = false
|
||||||
self.tasksError = nil
|
self.tasksError = nil
|
||||||
|
|
||||||
|
// Update widget data
|
||||||
|
WidgetDataManager.shared.saveTasks(from: data)
|
||||||
} else if let error = result as? ApiResultError {
|
} else if let error = result as? ApiResultError {
|
||||||
self.tasksError = error.message
|
self.tasksError = error.message
|
||||||
self.isLoadingTasks = false
|
self.isLoadingTasks = false
|
||||||
|
|||||||
Reference in New Issue
Block a user