- Update all Kotlin API models to match Go API response structures - Fix Swift type aliases (TaskDetail→TaskResponse, Residence→ResidenceResponse, etc.) - Update TaskCompletionCreateRequest to simplified Go API format (taskId, notes, actualCost, photoUrl) - Fix optional handling for frequency, priority, category, status in task models - Replace isPrimaryOwner with ownerId comparison against current user - Update ResidenceUsersResponse to use owner.id instead of ownerId - Fix non-optional String fields to use isEmpty checks instead of optional binding - Add type aliases for backwards compatibility in Kotlin models 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
165 lines
5.7 KiB
Swift
165 lines
5.7 KiB
Swift
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: "", // No longer available in API, residence lookup needed
|
|
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())
|
|
}
|
|
}
|