// // Casera.swift // Casera // // Created by Trey Tartt on 11/5/25. // import WidgetKit import SwiftUI import AppIntents // MARK: - Date Formatting Helper /// Parses date strings in either yyyy-MM-dd or ISO8601 (RFC3339) format /// and returns a user-friendly string like "Today" or "in X days" private func formatWidgetDate(_ dateString: String) -> String { let calendar = Calendar.current var date: Date? // Try parsing as yyyy-MM-dd first let dateOnlyFormatter = DateFormatter() dateOnlyFormatter.dateFormat = "yyyy-MM-dd" date = dateOnlyFormatter.date(from: dateString) // Try parsing as ISO8601 (RFC3339) if that fails if date == nil { let isoFormatter = ISO8601DateFormatter() isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] date = isoFormatter.date(from: dateString) // Try without fractional seconds if date == nil { isoFormatter.formatOptions = [.withInternetDateTime] date = isoFormatter.date(from: dateString) } } guard let parsedDate = date else { return dateString } let today = calendar.startOfDay(for: Date()) let dueDay = calendar.startOfDay(for: parsedDate) if calendar.isDateInToday(parsedDate) { return "Today" } let components = calendar.dateComponents([.day], from: today, to: dueDay) let days = components.day ?? 0 if days > 0 { return days == 1 ? "in 1 day" : "in \(days) days" } else { let overdueDays = abs(days) return overdueDays == 1 ? "1 day ago" : "\(overdueDays) days ago" } } /// CacheManager reads tasks from the App Group shared container /// Data is written by the main app via WidgetDataManager class CacheManager { struct CustomTask: Codable, Identifiable { let id: Int let title: String let description: String? let priority: 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, category case inProgress = "in_progress" case dueDate = "due_date" case residenceName = "residence_name" case isOverdue = "is_overdue" } /// Whether this task is pending completion (tapped on widget, waiting for sync) var isPendingCompletion: Bool { WidgetActionManager.shared.isTaskPendingCompletion(taskId: id) } /// Whether this task should be shown (not pending completion) var shouldShow: Bool { !isPendingCompletion } } private static let appGroupIdentifier = "group.com.tt.casera.CaseraDev" 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] { 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 { let data = try Data(contentsOf: fileURL) let tasks = try JSONDecoder().decode([CustomTask].self, from: data) print("CacheManager: Loaded \(tasks.count) tasks from shared container") return tasks } catch { print("CacheManager: Error decoding tasks - \(error)") return [] } } static func getUpcomingTasks() -> [CustomTask] { let allTasks = getData() // Filter for actionable tasks (not completed, including in-progress and overdue) // Also exclude tasks that are pending completion via widget let upcoming = allTasks.filter { task in // Include if: not pending completion return task.shouldShow } // 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 } } } struct Provider: AppIntentTimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry( date: Date(), configuration: ConfigurationAppIntent(), upcomingTasks: [], isInteractive: true ) } func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry { let tasks = CacheManager.getUpcomingTasks() let isInteractive = WidgetActionManager.shared.shouldShowInteractiveWidget() return SimpleEntry( date: Date(), configuration: configuration, upcomingTasks: tasks, isInteractive: isInteractive ) } func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline { let tasks = CacheManager.getUpcomingTasks() let isInteractive = WidgetActionManager.shared.shouldShowInteractiveWidget() // Update every 30 minutes (more frequent for interactive widgets) let currentDate = Date() let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)! let entry = SimpleEntry( date: currentDate, configuration: configuration, upcomingTasks: tasks, isInteractive: isInteractive ) return Timeline(entries: [entry], policy: .after(nextUpdate)) } } struct SimpleEntry: TimelineEntry { let date: Date let configuration: ConfigurationAppIntent let upcomingTasks: [CacheManager.CustomTask] let isInteractive: Bool var taskCount: Int { upcomingTasks.count } var nextTask: CacheManager.CustomTask? { 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 var overdueCount: Int { calculatedStats.overdueCount } /// Tasks due within the next 7 days (exclusive of overdue) var dueNext7DaysCount: Int { calculatedStats.next7DaysCount } /// Tasks due within next 30 days (days 8-30, exclusive of next 7 days) var dueNext30DaysCount: Int { calculatedStats.next30DaysCount } } struct CaseraEntryView : View { var entry: Provider.Entry @Environment(\.widgetFamily) var family var body: some View { if entry.isInteractive { // Premium/no-limitations: Show interactive widget with task completion switch family { case .systemSmall: SmallWidgetView(entry: entry) case .systemMedium: MediumWidgetView(entry: entry) case .systemLarge: LargeWidgetView(entry: entry) default: SmallWidgetView(entry: entry) } } else { // Free tier with limitations: Show simple task count view FreeWidgetView(entry: entry) } } } // MARK: - Free Tier Widget View (Non-Interactive) struct FreeWidgetView: View { let entry: SimpleEntry @Environment(\.colorScheme) var colorScheme var body: some View { VStack(spacing: OrganicSpacing.cozy) { Spacer() // Organic task count with glow ZStack { // Soft glow behind number Circle() .fill( RadialGradient( colors: [ Color.appPrimary.opacity(0.2), Color.appPrimary.opacity(0.05), Color.clear ], center: .center, startRadius: 0, endRadius: 50 ) ) .frame(width: 100, height: 100) Text("\(entry.taskCount)") .font(.system(size: 52, weight: .bold, design: .rounded)) .foregroundStyle( LinearGradient( colors: [Color.appPrimary, Color.appPrimary.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } Text(entry.taskCount == 1 ? "task waiting" : "tasks waiting") .font(.system(size: 14, weight: .semibold, design: .rounded)) .foregroundStyle(Color.appTextSecondary) .multilineTextAlignment(.center) Spacer() // Subtle upgrade hint with organic styling Text("Upgrade for interactive widgets") .font(.system(size: 10, weight: .medium, design: .rounded)) .foregroundStyle(Color.appTextSecondary.opacity(0.6)) .padding(.horizontal, 12) .padding(.vertical, 6) .background( Capsule() .fill(Color.appPrimary.opacity(colorScheme == .dark ? 0.15 : 0.08)) ) } .padding(OrganicSpacing.cozy) } } // MARK: - Small Widget View struct SmallWidgetView: View { let entry: SimpleEntry @Environment(\.colorScheme) var colorScheme var body: some View { VStack(alignment: .leading, spacing: 0) { // Task Count with organic glow HStack(alignment: .top) { VStack(alignment: .leading, spacing: 2) { Text("\(entry.taskCount)") .font(.system(size: 34, weight: .bold, design: .rounded)) .foregroundStyle( LinearGradient( colors: [Color.appPrimary, Color.appPrimary.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) if entry.taskCount > 0 { Text(entry.taskCount == 1 ? "task" : "tasks") .font(.system(size: 11, weight: .semibold, design: .rounded)) .foregroundStyle(Color.appTextSecondary) } } Spacer() // Small decorative accent Circle() .fill( RadialGradient( colors: [Color.appPrimary.opacity(0.15), Color.clear], center: .center, startRadius: 0, endRadius: 20 ) ) .frame(width: 40, height: 40) .offset(x: 10, y: -10) } Spacer(minLength: 8) // Next Task Card if let nextTask = entry.nextTask { VStack(alignment: .leading, spacing: 6) { Text(nextTask.title) .font(.system(size: 12, weight: .semibold, design: .rounded)) .lineLimit(1) .foregroundStyle(Color.appTextPrimary) HStack(spacing: 6) { if let dueDate = nextTask.dueDate { HStack(spacing: 3) { Image(systemName: "calendar") .font(.system(size: 8, weight: .medium)) Text(formatWidgetDate(dueDate)) .font(.system(size: 9, weight: .semibold, design: .rounded)) } .foregroundStyle(nextTask.isOverdue ? Color.appError : Color.appAccent) .padding(.horizontal, 6) .padding(.vertical, 3) .background( Capsule() .fill((nextTask.isOverdue ? Color.appError : Color.appAccent).opacity(colorScheme == .dark ? 0.2 : 0.12)) ) } Spacer() // Organic complete button Button(intent: CompleteTaskIntent(taskId: nextTask.id, taskTitle: nextTask.title)) { ZStack { Circle() .fill(Color.appPrimary.opacity(0.15)) .frame(width: 28, height: 28) Image(systemName: "checkmark") .font(.system(size: 12, weight: .bold)) .foregroundStyle(Color.appPrimary) } } .buttonStyle(.plain) } } .padding(OrganicSpacing.compact) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 14, style: .continuous) .fill(Color.appPrimary.opacity(colorScheme == .dark ? 0.12 : 0.08)) ) } else { // Empty state HStack(spacing: 8) { ZStack { Circle() .fill(Color.appPrimary.opacity(0.15)) .frame(width: 24, height: 24) Image(systemName: "checkmark") .font(.system(size: 10, weight: .bold)) .foregroundStyle(Color.appPrimary) } Text("All caught up!") .font(.system(size: 11, weight: .semibold, design: .rounded)) .foregroundStyle(Color.appTextSecondary) } .frame(maxWidth: .infinity) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 14, style: .continuous) .fill(Color.appPrimary.opacity(colorScheme == .dark ? 0.1 : 0.06)) ) } } .padding(14) } } // MARK: - Medium Widget View struct MediumWidgetView: View { let entry: SimpleEntry @Environment(\.colorScheme) var colorScheme var body: some View { HStack(spacing: 0) { // Left side - Task count with organic styling VStack(alignment: .center, spacing: 4) { Spacer() ZStack { // Soft glow Circle() .fill( RadialGradient( colors: [Color.appPrimary.opacity(0.15), Color.clear], center: .center, startRadius: 0, endRadius: 35 ) ) .frame(width: 70, height: 70) Text("\(entry.taskCount)") .font(.system(size: 38, weight: .bold, design: .rounded)) .foregroundStyle( LinearGradient( colors: [Color.appPrimary, Color.appPrimary.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } Text(entry.taskCount == 1 ? "task" : "tasks") .font(.system(size: 12, weight: .semibold, design: .rounded)) .foregroundStyle(Color.appTextSecondary) Spacer() } .frame(width: 85) // Organic divider Rectangle() .fill( LinearGradient( colors: [Color.appTextSecondary.opacity(0), Color.appTextSecondary.opacity(0.2), Color.appTextSecondary.opacity(0)], startPoint: .top, endPoint: .bottom ) ) .frame(width: 1) .padding(.vertical, 12) // Right side - Task list VStack(alignment: .leading, spacing: 6) { if entry.nextTask != nil { ForEach(Array(entry.upcomingTasks.prefix(3).enumerated()), id: \.element.id) { index, task in OrganicTaskRowView(task: task, compact: true) } Spacer(minLength: 0) } else { Spacer() // Empty state VStack(spacing: 8) { ZStack { Circle() .fill(Color.appPrimary.opacity(0.15)) .frame(width: 36, height: 36) Image(systemName: "checkmark") .font(.system(size: 16, weight: .bold)) .foregroundStyle(Color.appPrimary) } Text("All caught up!") .font(.system(size: 12, weight: .semibold, design: .rounded)) .foregroundStyle(Color.appTextSecondary) } .frame(maxWidth: .infinity) Spacer() } } .frame(maxWidth: .infinity) .padding(.leading, 12) } .padding(14) } } // MARK: - Organic Task Row View struct OrganicTaskRowView: View { let task: CacheManager.CustomTask var compact: Bool = false var showResidence: Bool = false @Environment(\.colorScheme) var colorScheme var body: some View { HStack(spacing: compact ? 8 : 10) { // Organic checkbox button Button(intent: CompleteTaskIntent(taskId: task.id, taskTitle: task.title)) { ZStack { Circle() .stroke( LinearGradient( colors: [priorityColor, priorityColor.opacity(0.7)], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 2 ) .frame(width: compact ? 18 : 22, height: compact ? 18 : 22) Circle() .fill(priorityColor.opacity(colorScheme == .dark ? 0.15 : 0.1)) .frame(width: compact ? 14 : 18, height: compact ? 14 : 18) } } .buttonStyle(.plain) VStack(alignment: .leading, spacing: compact ? 2 : 3) { Text(task.title) .font(.system(size: compact ? 11 : 12, weight: .semibold, design: .rounded)) .lineLimit(compact ? 1 : 2) .foregroundStyle(Color.appTextPrimary) HStack(spacing: 8) { if showResidence, let residenceName = task.residenceName, !residenceName.isEmpty { HStack(spacing: 2) { Image("icon") .resizable() .frame(width: 7, height: 7) Text(residenceName) .font(.system(size: 9, weight: .medium, design: .rounded)) } .foregroundStyle(Color.appTextSecondary) } if let dueDate = task.dueDate { HStack(spacing: 2) { Image(systemName: "calendar") .font(.system(size: 7, weight: .medium)) Text(formatWidgetDate(dueDate)) .font(.system(size: 9, weight: .semibold, design: .rounded)) } .foregroundStyle(task.isOverdue ? Color.appError : Color.appAccent) } } } Spacer(minLength: 0) } .padding(.vertical, compact ? 4 : 6) .padding(.horizontal, compact ? 6 : 8) .background( RoundedRectangle(cornerRadius: compact ? 10 : 12, style: .continuous) .fill(priorityColor.opacity(colorScheme == .dark ? 0.12 : 0.06)) ) } private var priorityColor: Color { if task.isOverdue { return Color.appError } switch task.priority?.lowercased() { case "urgent": return Color.appError case "high": return Color.appAccent case "medium": return Color(red: 0.92, green: 0.70, blue: 0.03) // Yellow default: return Color.appPrimary } } } // MARK: - Large Widget View struct LargeWidgetView: View { let entry: SimpleEntry @Environment(\.colorScheme) var colorScheme private var maxTasksToShow: Int { 5 } var body: some View { VStack(alignment: .leading, spacing: 0) { if entry.upcomingTasks.isEmpty { // Empty state - centered with organic styling Spacer() VStack(spacing: 14) { ZStack { Circle() .fill( RadialGradient( colors: [Color.appPrimary.opacity(0.2), Color.appPrimary.opacity(0.05), Color.clear], center: .center, startRadius: 0, endRadius: 40 ) ) .frame(width: 80, height: 80) Image(systemName: "checkmark.circle.fill") .font(.system(size: 44, weight: .medium)) .foregroundStyle( LinearGradient( colors: [Color.appPrimary, Color.appPrimary.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } Text("All caught up!") .font(.system(size: 16, weight: .semibold, design: .rounded)) .foregroundStyle(Color.appTextSecondary) } .frame(maxWidth: .infinity) Spacer() // Stats even when empty OrganicStatsView(entry: entry) } else { // Tasks section with organic rows VStack(alignment: .leading, spacing: 6) { ForEach(Array(entry.upcomingTasks.prefix(maxTasksToShow).enumerated()), id: \.element.id) { index, task in OrganicTaskRowView(task: task, showResidence: true) } if entry.upcomingTasks.count > maxTasksToShow { Text("+ \(entry.upcomingTasks.count - maxTasksToShow) more") .font(.system(size: 10, weight: .semibold, design: .rounded)) .foregroundStyle(Color.appTextSecondary) .frame(maxWidth: .infinity, alignment: .center) .padding(.top, 4) } } Spacer(minLength: 10) // Stats section at bottom OrganicStatsView(entry: entry) } } .padding(14) } } // MARK: - Organic Stats View struct OrganicStatsView: View { let entry: SimpleEntry @Environment(\.colorScheme) var colorScheme var body: some View { HStack(spacing: 8) { // Overdue OrganicStatPillWidget( value: entry.overdueCount, label: "Overdue", color: entry.overdueCount > 0 ? Color.appError : Color.appTextSecondary ) // Next 7 Days OrganicStatPillWidget( value: entry.dueNext7DaysCount, label: "7 Days", color: Color.appAccent ) // Next 30 Days OrganicStatPillWidget( value: entry.dueNext30DaysCount, label: "30 Days", color: Color.appPrimary ) } } } // MARK: - Organic Stat Pill for Widget struct OrganicStatPillWidget: View { let value: Int let label: String let color: Color @Environment(\.colorScheme) var colorScheme var body: some View { VStack(spacing: 3) { Text("\(value)") .font(.system(size: 18, weight: .bold, design: .rounded)) .foregroundStyle( LinearGradient( colors: [color, color.opacity(0.8)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) Text(label) .font(.system(size: 9, weight: .semibold, design: .rounded)) .foregroundStyle(Color.appTextSecondary) .lineLimit(1) } .frame(maxWidth: .infinity) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) .fill(color.opacity(colorScheme == .dark ? 0.15 : 0.08)) ) } } struct Casera: Widget { let kind: String = "Casera" var body: some WidgetConfiguration { AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in CaseraEntryView(entry: entry) .containerBackground(for: .widget) { // Organic warm gradient background ZStack { Color.appBackgroundPrimary // Subtle accent gradient LinearGradient( colors: [ Color.appPrimary.opacity(0.06), Color.clear ], startPoint: .topLeading, endPoint: .bottomTrailing ) } } } .configurationDisplayName("Casera Tasks") .description("View and complete your upcoming tasks.") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) } } // MARK: - Previews #Preview(as: .systemSmall) { Casera() } timeline: { SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [ CacheManager.CustomTask( id: 1, title: "Fix leaky faucet", description: "Kitchen sink needs repair", priority: "high", inProgress: false, dueDate: "2024-12-15", category: "plumbing", residenceName: "Home", isOverdue: false ), CacheManager.CustomTask( id: 2, title: "Paint living room", description: nil, priority: "medium", inProgress: false, dueDate: "2024-12-20", category: "painting", residenceName: "Home", isOverdue: false ) ], isInteractive: true ) // Free tier preview SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [ CacheManager.CustomTask( id: 1, title: "Fix leaky faucet", description: nil, priority: "high", inProgress: false, dueDate: "2024-12-15", category: "plumbing", residenceName: "Home", isOverdue: false ), CacheManager.CustomTask( id: 2, title: "Paint living room", description: nil, priority: "medium", inProgress: false, dueDate: "2024-12-20", category: "painting", residenceName: "Home", isOverdue: false ), CacheManager.CustomTask( id: 3, title: "Clean gutters", description: nil, priority: "low", inProgress: false, dueDate: "2024-12-25", category: "maintenance", residenceName: "Home", isOverdue: false ) ], isInteractive: false ) SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [], isInteractive: true ) } #Preview(as: .systemMedium) { Casera() } timeline: { SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [ CacheManager.CustomTask( id: 1, title: "Fix leaky faucet", description: "Kitchen sink needs repair", priority: "high", inProgress: false, dueDate: "2024-12-15", category: "plumbing", residenceName: "Home", isOverdue: true ), CacheManager.CustomTask( id: 2, title: "Paint living room", description: nil, priority: "medium", inProgress: true, dueDate: "2024-12-20", category: "painting", residenceName: "Cabin", isOverdue: false ), CacheManager.CustomTask( id: 3, title: "Clean gutters", description: "Remove debris", priority: "low", inProgress: false, dueDate: "2024-12-25", category: "maintenance", residenceName: "Home", isOverdue: false ) ], isInteractive: true ) // Free tier preview SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [ CacheManager.CustomTask( id: 1, title: "Fix leaky faucet", description: nil, priority: "high", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ), CacheManager.CustomTask( id: 2, title: "Paint living room", description: nil, priority: "medium", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ), CacheManager.CustomTask( id: 3, title: "Clean gutters", description: nil, priority: "low", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ), CacheManager.CustomTask( id: 4, title: "Replace HVAC filter", description: nil, priority: "medium", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ), CacheManager.CustomTask( id: 5, title: "Check smoke detectors", description: nil, priority: "high", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ) ], isInteractive: false ) SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [], isInteractive: true ) } #Preview(as: .systemLarge) { Casera() } timeline: { SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [ CacheManager.CustomTask( id: 1, title: "Fix leaky faucet in kitchen", description: "Kitchen sink needs repair", priority: "high", inProgress: false, dueDate: "2024-12-15", category: "plumbing", residenceName: "Home", isOverdue: true ), CacheManager.CustomTask( id: 2, title: "Paint living room walls", description: nil, priority: "medium", inProgress: true, dueDate: "2024-12-20", category: "painting", residenceName: "Cabin", isOverdue: false ), CacheManager.CustomTask( id: 3, title: "Clean gutters", description: "Remove debris", priority: "low", inProgress: false, dueDate: "2024-12-25", category: "maintenance", residenceName: "Home", isOverdue: false ), CacheManager.CustomTask( id: 4, title: "Replace HVAC filter", description: nil, priority: "medium", inProgress: false, 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", inProgress: false, dueDate: "2024-12-30", category: "safety", residenceName: "Home", isOverdue: false ), CacheManager.CustomTask( id: 6, title: "Service water heater", description: nil, priority: "medium", inProgress: false, dueDate: "2025-01-05", category: "plumbing", residenceName: "Cabin", isOverdue: false ), CacheManager.CustomTask( id: 7, title: "Inspect roof shingles", description: nil, priority: "low", inProgress: false, dueDate: "2025-01-10", category: "exterior", residenceName: "Home", isOverdue: false ), CacheManager.CustomTask( id: 8, title: "Clean dryer vent", description: "Fire hazard prevention", priority: "urgent", inProgress: false, dueDate: "2025-01-12", category: "appliances", residenceName: "Beach House", isOverdue: false ) ], isInteractive: true ) // Free tier preview SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [ CacheManager.CustomTask( id: 1, title: "Task 1", description: nil, priority: "high", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ), CacheManager.CustomTask( id: 2, title: "Task 2", description: nil, priority: "medium", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ), CacheManager.CustomTask( id: 3, title: "Task 3", description: nil, priority: "low", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ), CacheManager.CustomTask( id: 4, title: "Task 4", description: nil, priority: "medium", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ), CacheManager.CustomTask( id: 5, title: "Task 5", description: nil, priority: "high", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ), CacheManager.CustomTask( id: 6, title: "Task 6", description: nil, priority: "low", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ), CacheManager.CustomTask( id: 7, title: "Task 7", description: nil, priority: "medium", inProgress: false, dueDate: nil, category: nil, residenceName: nil, isOverdue: false ) ], isInteractive: false ) SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [], isInteractive: true ) }