API Integration: - Fix logHabit: send "date" instead of "completed_at" - Fix FinanceCategory: "icon" → "emoji" to match API - Fix task priorities: remove level 4, keep 1-3 matching API - Fix habit frequencies: map monthly/interval → "custom" for API - Add token refresh (401 → auto retry with new token) - Add proper error handling (remove try? in save functions, show errors in UI) - Add date field to savings transactions - Add MonthlyPaymentDetail and OverduePayment models - Fix habit completedToday: compute on client from logs (API doesn't return it) - Filter habits by day of week on client (daily/weekly/monthly/interval) Design System (glassmorphism): - New DesignSystem.swift: Theme colors, GlassCard modifier, GlowIcon, GlowStatCard - Custom tab bar with per-tab glow colors (VStack layout, not ZStack overlay) - Deep dark background #06060f across all views - Glass cards with gradient fill + stroke throughout app - App icon: glassmorphism style with teal glow Health Dashboard: - Compact ReadinessBanner with recommendation text - 8 metric tiles: sleep, HR, HRV, steps, SpO2, respiratory rate, energy, distance - Each tile with status indicator (good/ok/bad) and hint text - Heart rate card (min/avg/max) - Weekly trends card (averages) - Recovery score (weighted: 40% sleep, 35% HRV, 25% RHR) - Tips card with actionable recommendations - Sleep detail view with hypnogram (step chart of phases) - Sleep segments timeline from HealthKit (deep/rem/core/awake with exact times) - Line chart replacing bar chart for weekly data - Collect respiratory_rate and sleep phases with timestamps from HealthKit - Background sync every ~30min via BGProcessingTask Notifications: - NotificationService for local push notifications - Morning/evening reminders with native DatePicker (wheel) - Payment reminders: 5 days, 1 day, and day-of for recurring savings - Notification settings in Settings tab UI Fixes: - Fix color picker overflow: HStack → LazyVGrid 5 columns - Fix sheet headers: shorter text, proper padding - Fix task/habit toggle: separate tap zones (checkbox vs edit) - Fix deprecated onChange syntax for iOS 17+ - Savings overview: real monthly payments and detailed overdues from API - Settings: timezone as Menu picker, removed Telegram/server notifications sections - All sheets use .presentationDetents([.large]) Config: - project.yml: real DEVELOPMENT_TEAM, HealthKit + BackgroundModes capabilities - Info.plist: BGTaskScheduler + UIBackgroundModes - Assets.xcassets with AppIcon - CLAUDE.md project documentation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
170 lines
6.3 KiB
Swift
170 lines
6.3 KiB
Swift
import UserNotifications
|
||
import Foundation
|
||
|
||
class NotificationService {
|
||
static let shared = NotificationService()
|
||
|
||
// MARK: - Permission
|
||
|
||
func requestPermission() async -> Bool {
|
||
do {
|
||
let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound])
|
||
return granted
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
func isAuthorized() async -> Bool {
|
||
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
||
return settings.authorizationStatus == .authorized
|
||
}
|
||
|
||
// MARK: - Morning Reminder
|
||
|
||
func scheduleMorningReminder(hour: Int, minute: Int) {
|
||
let content = UNMutableNotificationContent()
|
||
content.title = "Доброе утро!"
|
||
content.body = "Посмотри свои привычки и задачи на сегодня"
|
||
content.sound = .default
|
||
|
||
var components = DateComponents()
|
||
components.hour = hour
|
||
components.minute = minute
|
||
|
||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||
let request = UNNotificationRequest(identifier: "morning_reminder", content: content, trigger: trigger)
|
||
|
||
UNUserNotificationCenter.current().add(request)
|
||
}
|
||
|
||
// MARK: - Evening Reminder
|
||
|
||
func scheduleEveningReminder(hour: Int, minute: Int) {
|
||
let content = UNMutableNotificationContent()
|
||
content.title = "Итоги дня"
|
||
content.body = "Проверь, все ли привычки выполнены сегодня"
|
||
content.sound = .default
|
||
|
||
var components = DateComponents()
|
||
components.hour = hour
|
||
components.minute = minute
|
||
|
||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||
let request = UNNotificationRequest(identifier: "evening_reminder", content: content, trigger: trigger)
|
||
|
||
UNUserNotificationCenter.current().add(request)
|
||
}
|
||
|
||
// MARK: - Task Deadline Reminder
|
||
|
||
func scheduleTaskReminder(taskId: Int, title: String, dueDate: Date) {
|
||
let content = UNMutableNotificationContent()
|
||
content.title = "Задача скоро"
|
||
content.body = title
|
||
content.sound = .default
|
||
|
||
// За 1 час до дедлайна
|
||
let reminderDate = dueDate.addingTimeInterval(-3600)
|
||
guard reminderDate > Date() else { return }
|
||
|
||
let components = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: reminderDate)
|
||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: false)
|
||
let request = UNNotificationRequest(identifier: "task_\(taskId)", content: content, trigger: trigger)
|
||
|
||
UNUserNotificationCenter.current().add(request)
|
||
}
|
||
|
||
// MARK: - Habit Reminder
|
||
|
||
func scheduleHabitReminder(habitId: Int, name: String, hour: Int, minute: Int) {
|
||
let content = UNMutableNotificationContent()
|
||
content.title = "Привычка"
|
||
content.body = name
|
||
content.sound = .default
|
||
|
||
var components = DateComponents()
|
||
components.hour = hour
|
||
components.minute = minute
|
||
|
||
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
|
||
let request = UNNotificationRequest(identifier: "habit_\(habitId)", content: content, trigger: trigger)
|
||
|
||
UNUserNotificationCenter.current().add(request)
|
||
}
|
||
|
||
// MARK: - Cancel
|
||
|
||
func cancelReminder(_ identifier: String) {
|
||
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [identifier])
|
||
}
|
||
|
||
func cancelAllReminders() {
|
||
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
||
}
|
||
|
||
// MARK: - Payment Reminders
|
||
|
||
func schedulePaymentReminders(payments: [MonthlyPaymentDetail]) {
|
||
cancelPaymentReminders()
|
||
|
||
let cal = Calendar.current
|
||
let now = Date()
|
||
|
||
for payment in payments {
|
||
let day = payment.day
|
||
guard day >= 1, day <= 28 else { continue }
|
||
|
||
// Build due date for this month
|
||
var components = cal.dateComponents([.year, .month], from: now)
|
||
components.day = day
|
||
components.hour = 11
|
||
components.minute = 0
|
||
guard let dueDate = cal.date(from: components) else { continue }
|
||
|
||
let offsets = [(-5, "через 5 дней"), (-1, "завтра"), (0, "сегодня")]
|
||
|
||
for (offset, label) in offsets {
|
||
guard let notifDate = cal.date(byAdding: .day, value: offset, to: dueDate),
|
||
notifDate > now else { continue }
|
||
|
||
let content = UNMutableNotificationContent()
|
||
content.title = "Платёж \(label)"
|
||
content.body = "\(payment.categoryName): \(Int(payment.amount)) ₽ — \(day) числа"
|
||
content.sound = .default
|
||
|
||
let trigger = UNCalendarNotificationTrigger(
|
||
dateMatching: cal.dateComponents([.year, .month, .day, .hour, .minute], from: notifDate),
|
||
repeats: false
|
||
)
|
||
let id = "payment_\(payment.categoryId)_\(offset)"
|
||
UNUserNotificationCenter.current().add(UNNotificationRequest(identifier: id, content: content, trigger: trigger))
|
||
}
|
||
}
|
||
}
|
||
|
||
func cancelPaymentReminders() {
|
||
let center = UNUserNotificationCenter.current()
|
||
center.getPendingNotificationRequests { requests in
|
||
let ids = requests.filter { $0.identifier.hasPrefix("payment_") }.map(\.identifier)
|
||
center.removePendingNotificationRequests(withIdentifiers: ids)
|
||
}
|
||
}
|
||
|
||
// MARK: - Update from settings
|
||
|
||
func updateSchedule(morning: Bool, morningTime: String, evening: Bool, eveningTime: String) {
|
||
cancelReminder("morning_reminder")
|
||
cancelReminder("evening_reminder")
|
||
|
||
if morning {
|
||
let parts = morningTime.split(separator: ":").compactMap { Int($0) }
|
||
if parts.count == 2 { scheduleMorningReminder(hour: parts[0], minute: parts[1]) }
|
||
}
|
||
if evening {
|
||
let parts = eveningTime.split(separator: ":").compactMap { Int($0) }
|
||
if parts.count == 2 { scheduleEveningReminder(hour: parts[0], minute: parts[1]) }
|
||
}
|
||
}
|
||
}
|