a4dfb38feb
- Dish.isPriority: mark dishes as priority; auto-assign gives them 2.5x weight - Tag.dayRestriction: restrict tags to weekdays or weekend only - Auto-assign CTA banner shown when there are empty slots (was hidden in context menu) - Tap on filled slot opens picker to replace dish directly (no need to remove first) - Export callout visible as soon as ≥1 slot is filled (not only when week is complete) - WeekPlanShareView shows localized "TBD" / "Eating out" for empty/eating-out slots - Second Sunday 17:00 push notification to plan next week - DishDrawer sorts: priority first, then by historical usage, then alphabetically - Search in SlotDishPickerSheet already shipped in 1.0.5 (verified ✓) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
75 lines
3.2 KiB
Swift
75 lines
3.2 KiB
Swift
import Foundation
|
|
import UserNotifications
|
|
|
|
@MainActor
|
|
final class NotificationService {
|
|
static let shared = NotificationService()
|
|
|
|
private init() {}
|
|
|
|
func requestPermissionIfNeeded() async {
|
|
let center = UNUserNotificationCenter.current()
|
|
let settings = await center.notificationSettings()
|
|
guard settings.authorizationStatus == .notDetermined else { return }
|
|
_ = try? await center.requestAuthorization(options: [.alert, .sound, .badge])
|
|
}
|
|
|
|
func schedulePlanningReminderIfNeeded(nextWeekPlan: WeekPlan?, language: AppLanguage) {
|
|
let center = UNUserNotificationCenter.current()
|
|
let identifier = "mealmood.next-week-planning"
|
|
|
|
let isComplete = nextWeekPlan?.slots.allSatisfy { $0.dishId != nil || $0.isEatingOut } ?? false
|
|
if isComplete {
|
|
center.removePendingNotificationRequests(withIdentifiers: [identifier])
|
|
} else {
|
|
let nextWeekStart = Date().startOfWeek().addingDays(7)
|
|
let reminderDay = nextWeekStart.addingDays(-1)
|
|
let calendar = Calendar.current
|
|
var components = calendar.dateComponents([.year, .month, .day], from: reminderDay)
|
|
components.hour = 19
|
|
components.minute = 0
|
|
|
|
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
|
|
let content = UNMutableNotificationContent()
|
|
content.title = localizedString("notification_planning_title", language: language)
|
|
content.body = localizedString("notification_planning_body", language: language)
|
|
content.sound = .default
|
|
|
|
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
|
center.removePendingNotificationRequests(withIdentifiers: [identifier])
|
|
center.add(request)
|
|
}
|
|
|
|
scheduleSundayReminderIfNeeded(nextWeekPlan: nextWeekPlan, language: language)
|
|
}
|
|
|
|
private func scheduleSundayReminderIfNeeded(nextWeekPlan: WeekPlan?, language: AppLanguage) {
|
|
let center = UNUserNotificationCenter.current()
|
|
let identifier = "mealmood.sunday-planning"
|
|
|
|
// Cancel if next week is already planned
|
|
let hasAnySlot = nextWeekPlan?.slots.contains { $0.dishId != nil || $0.isEatingOut } ?? false
|
|
if hasAnySlot {
|
|
center.removePendingNotificationRequests(withIdentifiers: [identifier])
|
|
return
|
|
}
|
|
|
|
// Schedule for next Sunday at 17:00
|
|
var components = DateComponents()
|
|
components.weekday = 1 // Sunday
|
|
components.hour = 17
|
|
components.minute = 0
|
|
|
|
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
|
let content = UNMutableNotificationContent()
|
|
content.title = localizedString("notification_sunday_title", language: language)
|
|
content.body = localizedString("notification_sunday_body", language: language)
|
|
content.sound = .default
|
|
|
|
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
|
center.removePendingNotificationRequests(withIdentifiers: [identifier])
|
|
center.add(request)
|
|
}
|
|
}
|
|
|