feat: major app overhaul — API fixes, glassmorphism UI, health dashboard, notifications
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>
This commit is contained in:
169
PulseHealth/Services/NotificationService.swift
Normal file
169
PulseHealth/Services/NotificationService.swift
Normal file
@@ -0,0 +1,169 @@
|
||||
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]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user