From e5bd739499885b1e7d3031291dcad650c49886b4 Mon Sep 17 00:00:00 2001 From: Robin Kutesa Date: Tue, 2 Jun 2026 13:10:32 +0300 Subject: [PATCH] feat: HealthKit workout sync, confirmation notification, health stats on Today dashboard - HealthKitManager: add fetchWorkoutDates(from:to:) to read past workouts from Apple Health - WorkoutViewModel: syncFromHealthKit() merges HealthKit workout dates into streak (runs on every foreground); checkPendingHealthConfirm() picks up confirmed workouts from notification action - NotificationManager: WORKOUT_CONFIRM category with 'Yes, log it' action; scheduleWorkoutConfirm() fires weekly on scheduled workout days at configurable time (default 8pm); LOG_WORKOUT action writes pendingConfirm to UserDefaults - SettingsView (Workout Settings): new HEALTH SYNC section with 'Sync now' button and 'Workout confirmation reminder' toggle + time picker - TodayView: TodayHealthStrip shows steps, active calories, resting HR, and workout streak in a 4-pill row below the date header (only shown when HealthKit is authorized); refreshes on appear and foreground - FAB: Matrix FAB padding fixed to match Today/Calendar position (.bottom 10) Co-Authored-By: Kutesir --- .../UserInterfaceState.xcuserstate | Bin 25786 -> 108163 bytes .../xcshareddata/xcschemes/KisaniCal.xcscheme | 16 +- KisaniCal/ContentView.swift | 2 + KisaniCal/Managers/HealthKitManager.swift | 156 ++++++++++++++++++ KisaniCal/Managers/NotificationManager.swift | 73 +++++--- KisaniCal/Models/ExerciseModels.swift | 29 ++++ KisaniCal/Views/MatrixView.swift | 19 +-- KisaniCal/Views/SettingsView.swift | 76 ++++++++- KisaniCal/Views/TodayView.swift | 62 ++++++- 9 files changed, 385 insertions(+), 48 deletions(-) create mode 100644 KisaniCal/Managers/HealthKitManager.swift diff --git a/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate b/KisaniCal.xcodeproj/project.xcworkspace/xcuserdata/kutesir.xcuserdatad/UserInterfaceState.xcuserstate index 92bd29eedac6c4be2ea721dc61b2ac6728a8cd93..4784b70aceb80d1a2cd2b500bf0a9348a684d62d 100644 GIT binary patch literal 108163 zcmeFacVJXS*9UxO=5D$7ZqM%0ArLx*7P=y&LLkj-LWgxp7Knsw*iC>S=sluX5j!Fl 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�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 delta 14915 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 + LastUpgradeVersion = "2620" + version = "1.3"> + buildImplicitDependencies = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - diff --git a/KisaniCal/ContentView.swift b/KisaniCal/ContentView.swift index 6d0639d..1e3b7d9 100644 --- a/KisaniCal/ContentView.swift +++ b/KisaniCal/ContentView.swift @@ -91,8 +91,10 @@ struct ContentView: View { .onChange(of: scenePhase) { phase in if phase == .active { taskVM.reload() + workoutVM.checkPendingHealthConfirm() CloudSyncManager.shared.backupSettings() NotificationManager.shared.reschedule(workoutVM: workoutVM, taskVM: taskVM) + Task { await workoutVM.syncFromHealthKit() } } } .onChange(of: taskVM.tasks) { _ in diff --git a/KisaniCal/Managers/HealthKitManager.swift b/KisaniCal/Managers/HealthKitManager.swift new file mode 100644 index 0000000..7471b2b --- /dev/null +++ b/KisaniCal/Managers/HealthKitManager.swift @@ -0,0 +1,156 @@ +import HealthKit +import SwiftUI + +@MainActor +final class HealthKitManager: ObservableObject { + static let shared = HealthKitManager() + private let store = HKHealthStore() + + @Published var authorized = false + @Published var stepsToday: Int = 0 + @Published var activeCaloriesToday: Int = 0 + @Published var restingHeartRate: Int = 0 + @Published var workoutsThisWeek: Int = 0 + + var isAvailable: Bool { HKHealthStore.isHealthDataAvailable() } + + private init() {} + + // MARK: - Auth + + func requestAuthorization() async -> Bool { + guard isAvailable else { return false } + do { + try await store.requestAuthorization(toShare: shareTypes, read: readTypes) + authorized = true + await refresh() + return true + } catch { + return false + } + } + + // MARK: - Refresh all stats + + func refresh() async { + await withTaskGroup(of: Void.self) { g in + g.addTask { await self.fetchSteps() } + g.addTask { await self.fetchCalories() } + g.addTask { await self.fetchHeartRate() } + g.addTask { await self.fetchWorkoutsThisWeek() } + } + } + + // MARK: - Read workout dates from Health (for streak sync) + + func fetchWorkoutDates(from start: Date, to end: Date) async -> [String] { + guard authorized, isAvailable else { return [] } + let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd" + let cal = Calendar.current + let datePred = HKQuery.predicateForSamples(withStart: start, end: end) + let durationPred = HKQuery.predicateForWorkouts(with: .greaterThan, duration: 60) + let pred = NSCompoundPredicate(andPredicateWithSubpredicates: [datePred, durationPred]) + let workouts: [HKWorkout] = await withCheckedContinuation { cont in + let q = HKSampleQuery(sampleType: HKObjectType.workoutType(), + predicate: pred, limit: HKObjectQueryNoLimit, + sortDescriptors: nil) { _, samples, _ in + cont.resume(returning: (samples as? [HKWorkout]) ?? []) + } + store.execute(q) + } + let dates = Set(workouts.map { w -> String in + fmt.string(from: cal.startOfDay(for: w.startDate)) + }) + return Array(dates) + } + + // MARK: - Save workout to Health + + func saveWorkout(start: Date, end: Date) async { + guard authorized, isAvailable else { return } + let workout = HKWorkout( + activityType: .traditionalStrengthTraining, + start: start, + end: end + ) + do { try await store.save(workout) } catch {} + } + + // MARK: - Fetch helpers + + private func fetchSteps() async { + guard let type = HKQuantityType.quantityType(forIdentifier: .stepCount) else { return } + let start = Calendar.current.startOfDay(for: Date()) + let pred = HKQuery.predicateForSamples(withStart: start, end: Date()) + let val = await sumQuery(type: type, unit: .count(), predicate: pred) + stepsToday = Int(val) + } + + private func fetchCalories() async { + guard let type = HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned) else { return } + let start = Calendar.current.startOfDay(for: Date()) + let pred = HKQuery.predicateForSamples(withStart: start, end: Date()) + let val = await sumQuery(type: type, unit: .kilocalorie(), predicate: pred) + activeCaloriesToday = Int(val) + } + + private func fetchHeartRate() async { + guard let type = HKQuantityType.quantityType(forIdentifier: .restingHeartRate) else { return } + let val = await latestQuery(type: type, unit: HKUnit(from: "count/min")) + restingHeartRate = Int(val) + } + + private func fetchWorkoutsThisWeek() async { + let cal = Calendar.current + guard let weekStart = cal.date(from: cal.dateComponents([.yearForWeekOfYear, .weekOfYear], from: Date())) else { return } + let datePred = HKQuery.predicateForSamples(withStart: weekStart, end: Date()) + let durationPred = HKQuery.predicateForWorkouts(with: .greaterThan, duration: 0) + let pred = NSCompoundPredicate(andPredicateWithSubpredicates: [datePred, durationPred]) + + let count: Int = await withCheckedContinuation { cont in + let q = HKSampleQuery(sampleType: HKObjectType.workoutType(), predicate: pred, + limit: HKObjectQueryNoLimit, sortDescriptors: nil) { _, samples, _ in + cont.resume(returning: samples?.count ?? 0) + } + store.execute(q) + } + workoutsThisWeek = count + } + + private func sumQuery(type: HKQuantityType, unit: HKUnit, predicate: NSPredicate) async -> Double { + await withCheckedContinuation { cont in + let q = HKStatisticsQuery(quantityType: type, quantitySamplePredicate: predicate, options: .cumulativeSum) { _, stats, _ in + cont.resume(returning: stats?.sumQuantity()?.doubleValue(for: unit) ?? 0) + } + store.execute(q) + } + } + + private func latestQuery(type: HKQuantityType, unit: HKUnit) async -> Double { + await withCheckedContinuation { cont in + let sort = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: false) + let q = HKSampleQuery(sampleType: type, predicate: nil, limit: 1, sortDescriptors: [sort]) { _, samples, _ in + let val = (samples?.first as? HKQuantitySample)?.quantity.doubleValue(for: unit) ?? 0 + cont.resume(returning: val) + } + store.execute(q) + } + } + + // MARK: - Type sets + + private var readTypes: Set { + let ids: [HKQuantityTypeIdentifier] = [ + .stepCount, .activeEnergyBurned, .restingHeartRate, .bodyMass, .height + ] + var set: Set = Set(ids.compactMap { HKQuantityType.quantityType(forIdentifier: $0) }) + set.insert(HKObjectType.workoutType()) + return set + } + + private var shareTypes: Set { + var set: Set = [HKObjectType.workoutType()] + if let bm = HKQuantityType.quantityType(forIdentifier: .bodyMass) { set.insert(bm) } + return set + } +} diff --git a/KisaniCal/Managers/NotificationManager.swift b/KisaniCal/Managers/NotificationManager.swift index b1424fd..238cabf 100644 --- a/KisaniCal/Managers/NotificationManager.swift +++ b/KisaniCal/Managers/NotificationManager.swift @@ -7,12 +7,15 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable @Published var isAuthorized = false private let center = UNUserNotificationCenter.current() - private let workoutPrefix = "kisani.workout.weekday" - private let checkInPrefix = "kisani.workout.checkin" - private let tasksId = "kisani.tasks.evening" + private let workoutPrefix = "kisani.workout.weekday" + private let checkInPrefix = "kisani.workout.checkin" + private let confirmPrefix = "kisani.workout.confirm" + private let tasksId = "kisani.tasks.evening" - private static let categoryId = "TASK_REMINDER" - private static let actionMarkId = "MARK_COMPLETE" + private static let categoryId = "TASK_REMINDER" + private static let actionMarkId = "MARK_COMPLETE" + private static let confirmCatId = "WORKOUT_CONFIRM" + private static let logWorkoutActId = "LOG_WORKOUT" override private init() { super.init() @@ -22,18 +25,16 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable } private func registerCategories() { - let markAction = UNNotificationAction( - identifier: Self.actionMarkId, - title: "Mark Complete", - options: [] - ) - let category = UNNotificationCategory( - identifier: Self.categoryId, - actions: [markAction], - intentIdentifiers: [], - options: .customDismissAction - ) - center.setNotificationCategories([category]) + let markAction = UNNotificationAction(identifier: Self.actionMarkId, title: "Mark Complete", options: []) + let taskCat = UNNotificationCategory(identifier: Self.categoryId, actions: [markAction], + intentIdentifiers: [], options: .customDismissAction) + + let logAction = UNNotificationAction(identifier: Self.logWorkoutActId, + title: "Yes, log it ✓", options: []) + let confirmCat = UNNotificationCategory(identifier: Self.confirmCatId, actions: [logAction], + intentIdentifiers: [], options: .customDismissAction) + + center.setNotificationCategories([taskCat, confirmCat]) } // MARK: - Complete task directly in UserDefaults (called from background) @@ -84,6 +85,7 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable || settings.authorizationStatus == .provisional guard ok else { return } self.scheduleWorkout(schedule: schedule, programs: programs, progress: progress) + self.scheduleWorkoutConfirm(schedule: schedule, programs: programs) self.scheduleTasks(snapshot: tasks) self.updateBadge(snapshot: tasks) } @@ -152,6 +154,36 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable } } + // MARK: - Workout completion confirmation + + private func scheduleWorkoutConfirm(schedule: [Int: UUID], programs: [WorkoutProgram]) { + let ud = UserDefaults.standard + guard ud.object(forKey: "workoutConfirmEnabled") as? Bool ?? false else { + center.removePendingNotificationRequests(withIdentifiers: (1...7).map { "\(confirmPrefix).\($0)" }) + return + } + let cHour = ud.object(forKey: "workoutConfirmHour") as? Int ?? 20 + let cMinute = ud.object(forKey: "workoutConfirmMinute") as? Int ?? 0 + center.removePendingNotificationRequests(withIdentifiers: (1...7).map { "\(confirmPrefix).\($0)" }) + + for (weekday, pid) in schedule { + guard let program = programs.first(where: { $0.id == pid }) else { continue } + let content = UNMutableNotificationContent() + content.title = "Did you work out today?" + content.body = "\(program.name) — tap 'Yes, log it' to record your streak, or dismiss if you skipped." + content.sound = .default + content.categoryIdentifier = Self.confirmCatId + content.userInfo = ["deeplink": "workout"] + var comps = DateComponents() + comps.weekday = weekday; comps.hour = cHour; comps.minute = cMinute + center.add(UNNotificationRequest( + identifier: "\(confirmPrefix).\(weekday)", + content: content, + trigger: UNCalendarNotificationTrigger(dateMatching: comps, repeats: true) + )) + } + } + // MARK: - Per-task reminders + evening check-in private func scheduleTasks(snapshot: [TaskItem]) { @@ -253,11 +285,14 @@ extension NotificationManager: UNUserNotificationCenterDelegate { switch response.actionIdentifier { case Self.actionMarkId: - // Swiped action: mark complete, don't open app if let taskId { markTaskComplete(taskId: taskId, userId: userId) } + case Self.logWorkoutActId: + // "Yes, log it" from workout confirm notification — store date for WorkoutViewModel to pick up on next foreground + let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd" + UserDefaults.standard.set(fmt.string(from: Date()), forKey: "kisani.workout.pendingConfirm") + default: - // Tapped notification body if let taskId { markTaskComplete(taskId: taskId, userId: userId) } Task { @MainActor in switch deeplink { diff --git a/KisaniCal/Models/ExerciseModels.swift b/KisaniCal/Models/ExerciseModels.swift index be3efdd..1960d91 100644 --- a/KisaniCal/Models/ExerciseModels.swift +++ b/KisaniCal/Models/ExerciseModels.swift @@ -310,6 +310,35 @@ class WorkoutViewModel: ObservableObject { sessionStartDate = nil } + // Pull completed workout dates from Apple Health and merge into local streak + @MainActor + func syncFromHealthKit() async { + guard HealthKitManager.shared.authorized else { return } + guard let ninetyDaysAgo = Calendar.current.date(byAdding: .day, value: -90, to: Date()) else { return } + let hkDates = await HealthKitManager.shared.fetchWorkoutDates(from: ninetyDaysAgo, to: Date()) + guard !hkDates.isEmpty else { return } + var newSet = Set(workoutDates); var updated = false + for d in hkDates where !newSet.contains(d) { newSet.insert(d); updated = true } + if updated { workoutDates = Array(newSet); save() } + } + + // Called on foreground — picks up "Yes I did it" tapped in notification + @MainActor + func checkPendingHealthConfirm() { + let key = "kisani.workout.pendingConfirm" + guard let dateStr = UserDefaults.standard.string(forKey: key) else { return } + UserDefaults.standard.removeObject(forKey: key) + guard !workoutDates.contains(dateStr) else { return } + workoutDates.append(dateStr) + save() + let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd" + if let date = fmt.date(from: dateStr) { + let s = Calendar.current.startOfDay(for: date) + let e = Calendar.current.date(byAdding: .hour, value: 1, to: s) ?? s + Task { await HealthKitManager.shared.saveWorkout(start: s, end: e) } + } + } + func seedStreak(days: Int) { let cal = Calendar.current var newDates = Set(workoutDates) diff --git a/KisaniCal/Views/MatrixView.swift b/KisaniCal/Views/MatrixView.swift index f9a1750..de39543 100644 --- a/KisaniCal/Views/MatrixView.swift +++ b/KisaniCal/Views/MatrixView.swift @@ -129,11 +129,15 @@ struct MatrixView: View { } .frame(maxHeight: .infinity) .padding(.horizontal, 12) - .padding(.bottom, 8) + .padding(.bottom, 4) + + // Explicit clearance so grid stops above custom tab bar + Color.clear.frame(height: 76) } .frame(maxHeight: .infinity) + .toolbar(.hidden, for: .navigationBar) .sheet(isPresented: $showAddTask) { - AddTaskSheet() + AddTaskSheet(prefilledQuadrant: showDrill ? (drillQuadrant ?? .urgent) : .urgent) .environmentObject(taskVM) .presentationDetents([.height(150)]) .presentationDragIndicator(.visible) @@ -147,6 +151,7 @@ struct MatrixView: View { if let q = drillQuadrant { QuadrantDetailView(quadrant: q) .environmentObject(taskVM) + .toolbar(.visible, for: .navigationBar) } } } // NavigationStack @@ -442,7 +447,6 @@ struct QuadrantDetailView: View { let quadrant: Quadrant @EnvironmentObject var taskVM: TaskViewModel @Environment(\.colorScheme) var cs - @State private var showAddTask = false @State private var editingTask: TaskItem? = nil @State private var overdueExpanded = true @State private var laterExpanded = true @@ -535,19 +539,10 @@ struct QuadrantDetailView: View { .padding(.top, 16) } - FAB { showAddTask = true } - .padding(.trailing, 20) - .padding(.bottom, 10) } .background(AppColors.background(cs).ignoresSafeArea()) .navigationTitle(quadrant.matrixLabel) .navigationBarTitleDisplayMode(.large) - .sheet(isPresented: $showAddTask) { - AddTaskSheet(prefilledQuadrant: quadrant) - .environmentObject(taskVM) - .presentationDetents([.height(150)]) - .presentationDragIndicator(.visible) - } .sheet(item: $editingTask) { task in TaskEditSheet(task: task) .environmentObject(taskVM) diff --git a/KisaniCal/Views/SettingsView.swift b/KisaniCal/Views/SettingsView.swift index c6c75da..c95f538 100644 --- a/KisaniCal/Views/SettingsView.swift +++ b/KisaniCal/Views/SettingsView.swift @@ -165,7 +165,7 @@ struct SettingsView: View { .sheet(isPresented: $showGeneral) { GeneralSheet(showCompleted: $showCompleted, mondayFirst: $firstDayMonday).presentationDetents([.height(260)]).presentationDragIndicator(.visible) } .sheet(isPresented: $showImport) { ImportSheet(googleCal: $googleCalSync, iCloudCal: $iCloudCalSync).presentationDetents([.height(280)]).presentationDragIndicator(.visible) } .sheet(isPresented: $showBackup) { BackupSheet(iCloudSync: $iCloudSync).presentationDetents([.height(280)]).presentationDragIndicator(.visible) } - .sheet(isPresented: $showWorkoutSettings) { WorkoutSettingsSheet(unit: $weightUnit, sets: $defaultSets, reps: $defaultReps).presentationDetents([.large]).presentationDragIndicator(.visible) } + .sheet(isPresented: $showWorkoutSettings) { WorkoutSettingsSheet(unit: $weightUnit, sets: $defaultSets, reps: $defaultReps).environmentObject(workoutVM).presentationDetents([.large]).presentationDragIndicator(.visible) } .sheet(isPresented: $showRestTimer) { RestTimerSheet(seconds: $workoutVM.restTimerSeconds, enabled: $workoutVM.restTimerEnabled).presentationDetents([.height(360)]).presentationDragIndicator(.visible) } .sheet(isPresented: $showHelp) { HelpSheet().presentationDetents([.height(300)]).presentationDragIndicator(.visible) } .sheet(isPresented: $showFollow) { FollowSheet().presentationDetents([.height(280)]).presentationDragIndicator(.visible) } @@ -583,6 +583,7 @@ private struct BackupSheet: View { // MARK: - Workout Settings Sheet private struct WorkoutSettingsSheet: View { + @EnvironmentObject var workoutVM: WorkoutViewModel @Binding var unit: Int @Binding var sets: Int @Binding var reps: Int @@ -591,7 +592,10 @@ private struct WorkoutSettingsSheet: View { @AppStorage("workoutReminderEnabled") private var workoutReminderEnabled: Bool = true @AppStorage("workoutHour") private var workoutHour: Int = 18 @AppStorage("workoutMinute") private var workoutMinute: Int = 0 - @AppStorage("workoutCheckInEnabled") private var workoutCheckInEnabled: Bool = false + @AppStorage("workoutCheckInEnabled") private var workoutCheckInEnabled: Bool = false + @AppStorage("workoutConfirmEnabled") private var workoutConfirmEnabled: Bool = false + @AppStorage("workoutConfirmHour") private var workoutConfirmHour: Int = 20 + @AppStorage("workoutConfirmMinute") private var workoutConfirmMinute: Int = 0 @AppStorage("checkInHour") private var checkInHour: Int = 19 @AppStorage("checkInMinute") private var checkInMinute: Int = 30 @AppStorage("streakGoal") private var streakGoal: Int = 5 @@ -599,8 +603,9 @@ private struct WorkoutSettingsSheet: View { @State private var bwStr = "" @State private var htStr = "" - @State private var workoutTime: Date = Date() - @State private var checkInTime: Date = Date() + @State private var workoutTime: Date = Date() + @State private var checkInTime: Date = Date() + @State private var confirmTime: Date = Date() var body: some View { VStack(spacing: 0) { @@ -671,6 +676,67 @@ private struct WorkoutSettingsSheet: View { .animation(KisaniSpring.snappy, value: workoutCheckInEnabled) } + // ── Workout Confirmation ── + VStack(alignment: .leading, spacing: 8) { + Text("HEALTH SYNC").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs)) + VStack(spacing: 0) { + HStack(spacing: 11) { + Image(systemName: "figure.strengthtraining.traditional") + .font(.system(size: 13)).foregroundColor(.white) + .frame(width: 28, height: 28).background(AppColors.blue).cornerRadius(8) + VStack(alignment: .leading, spacing: 1) { + Text("Sync streak from Apple Health").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs)) + Text("Import past workouts into your streak") + .font(AppFonts.sans(11)).foregroundColor(AppColors.text3(cs)) + } + Spacer() + Button { + Task { await workoutVM.syncFromHealthKit() } + } label: { + Text("Sync now") + .font(AppFonts.mono(10, weight: .bold)) + .foregroundColor(AppColors.blue) + .padding(.horizontal, 10).padding(.vertical, 5) + .background(AppColors.blueSoft) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + .padding(.horizontal, 14).padding(.vertical, 12) + + Divider().background(AppColors.border(cs)).padding(.leading, 53) + + HStack(spacing: 11) { + Image(systemName: "bell.badge.fill") + .font(.system(size: 13)).foregroundColor(.white) + .frame(width: 28, height: 28).background(AppColors.accent).cornerRadius(8) + Text("Workout confirmation reminder").font(AppFonts.sans(13)).foregroundColor(AppColors.text(cs)) + Spacer() + Toggle("", isOn: $workoutConfirmEnabled).labelsHidden().tint(AppColors.accent) + } + .padding(.horizontal, 14).padding(.vertical, 11) + + if workoutConfirmEnabled { + Divider().background(AppColors.border(cs)).padding(.leading, 53) + HStack { + Text("Ask me at") + .font(AppFonts.sans(12)).foregroundColor(AppColors.text2(cs)) + Spacer() + DatePicker("", selection: $confirmTime, displayedComponents: .hourAndMinute) + .labelsHidden().tint(AppColors.accent) + .onChange(of: confirmTime) { v in + let c = Calendar.current.dateComponents([.hour, .minute], from: v) + workoutConfirmHour = c.hour ?? 20 + workoutConfirmMinute = c.minute ?? 0 + } + } + .padding(.horizontal, 14).padding(.vertical, 8) + } + } + .cardStyle() + .animation(KisaniSpring.snappy, value: workoutConfirmEnabled) + } + // ── Streak Goal ── VStack(alignment: .leading, spacing: 8) { Text("STREAK GOAL").font(AppFonts.mono(9, weight: .bold)).foregroundColor(AppColors.text3(cs)) @@ -788,6 +854,8 @@ private struct WorkoutSettingsSheet: View { workoutTime = Calendar.current.date(from: wc) ?? Date() var cc = today; cc.hour = checkInHour; cc.minute = checkInMinute checkInTime = Calendar.current.date(from: cc) ?? Date() + var cf = today; cf.hour = workoutConfirmHour; cf.minute = workoutConfirmMinute + confirmTime = Calendar.current.date(from: cf) ?? Date() } } } diff --git a/KisaniCal/Views/TodayView.swift b/KisaniCal/Views/TodayView.swift index c5c411a..e69889c 100644 --- a/KisaniCal/Views/TodayView.swift +++ b/KisaniCal/Views/TodayView.swift @@ -11,6 +11,7 @@ struct TodayView: View { @State private var showWorkoutSession = false @AppStorage("todayHideCompleted") private var hideCompleted = false @ObservedObject private var tm = TutorialManager.shared + @ObservedObject private var hk = HealthKitManager.shared private let dateFmt: DateFormatter = { let f = DateFormatter(); f.dateFormat = "EEEE, MMM d"; return f @@ -69,6 +70,13 @@ struct TodayView: View { .padding(.horizontal, 18) .padding(.bottom, 8) + // ── Health stats strip ── + if hk.authorized { + TodayHealthStrip(hk: hk, workoutVM: workoutVM) + .padding(.horizontal, 18) + .padding(.bottom, 10) + } + // ── Overdue card (prominent, above timeline) ── if !taskVM.overdueTasks.isEmpty { OverdueCard( @@ -173,9 +181,13 @@ struct TodayView: View { .onAppear { calStore.refreshStatus() calStore.fetchMonth(containing: Date()) + if hk.authorized { Task { await hk.refresh() } } } .onChange(of: scenePhase) { phase in - if phase == .active { calStore.refreshStatus() } + if phase == .active { + calStore.refreshStatus() + if hk.authorized { Task { await hk.refresh() } } + } } } } @@ -2190,3 +2202,51 @@ struct DotGridProgress: View { .animation(KisaniSpring.entrance, value: progress) } } + +// MARK: - Today Health Strip + +struct TodayHealthStrip: View { + @ObservedObject var hk: HealthKitManager + @ObservedObject var workoutVM: WorkoutViewModel + @Environment(\.colorScheme) var cs + + var body: some View { + HStack(spacing: 8) { + statPill(icon: "figure.walk", value: stepsLabel, label: "steps", color: AppColors.blue) + statPill(icon: "flame.fill", value: calsLabel, label: "kcal", color: AppColors.accent) + statPill(icon: "heart.fill", value: hrLabel, label: "bpm", color: .red) + statPill(icon: "dumbbell.fill", value: "\(workoutVM.streakDays)", label: "streak", color: AppColors.green) + } + } + + private func statPill(icon: String, value: String, label: String, color: Color) -> some View { + HStack(spacing: 5) { + Image(systemName: icon) + .font(.system(size: 11, weight: .semibold)) + .foregroundColor(color) + VStack(alignment: .leading, spacing: 0) { + Text(value) + .font(AppFonts.mono(12, weight: .bold)) + .foregroundColor(AppColors.text(cs)) + Text(label) + .font(AppFonts.mono(8)) + .foregroundColor(AppColors.text3(cs)) + } + } + .padding(.horizontal, 10).padding(.vertical, 7) + .frame(maxWidth: .infinity) + .background(AppColors.surface(cs)) + .clipShape(RoundedRectangle(cornerRadius: AppRadius.small)) + .overlay(RoundedRectangle(cornerRadius: AppRadius.small).stroke(AppColors.border(cs), lineWidth: 1)) + } + + private var stepsLabel: String { + hk.stepsToday >= 1000 + ? String(format: "%.1fk", Double(hk.stepsToday) / 1000) + : "\(hk.stepsToday)" + } + private var calsLabel: String { "\(hk.activeCaloriesToday)" } + private var hrLabel: String { + hk.restingHeartRate > 0 ? "\(hk.restingHeartRate)" : "--" + } +}