Files
pulse-mobile/PulseHealth/Views/DashboardView.swift
2026-03-25 11:29:11 +00:00

247 lines
8.1 KiB
Swift

import SwiftUI
struct DashboardView: View {
@EnvironmentObject var authManager: AuthManager
@StateObject private var healthKit = HealthKitService()
@State private var readiness: ReadinessResponse?
@State private var latest: LatestHealthResponse?
@State private var heatmapData: [HeatmapEntry] = []
@State private var isLoading = true
// Toast state
@State private var showToast = false
@State private var toastMessage = ""
@State private var toastSuccess = true
var greeting: String {
let hour = Calendar.current.component(.hour, from: Date())
switch hour {
case 5..<12: return "Доброе утро"
case 12..<17: return "Добрый день"
case 17..<22: return "Добрый вечер"
default: return "Доброй ночи"
}
}
var dateString: String {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "ru_RU")
formatter.dateFormat = "d MMMM, EEEE"
return formatter.string(from: Date())
}
var body: some View {
ZStack {
// Background
Color(hex: "0a0a1a")
.ignoresSafeArea()
ScrollView(showsIndicators: false) {
VStack(spacing: 20) {
// MARK: - Header
headerView
.padding(.top, 8)
if isLoading {
loadingView
} else {
// MARK: - Readiness
if let r = readiness {
ReadinessCardView(readiness: r)
}
// MARK: - Metrics Grid
metricsGrid
// MARK: - Weekly Chart
if !heatmapData.isEmpty {
WeeklyChartCard(heatmapData: heatmapData)
}
// MARK: - Insights
InsightsCard(readiness: readiness, latest: latest)
Spacer(minLength: 30)
}
}
}
.refreshable {
await loadData()
}
}
.toast(isShowing: $showToast, message: toastMessage, isSuccess: toastSuccess)
.task { await loadData() }
}
// MARK: - Header
private var headerView: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("\(greeting), \(authManager.userName) 👋")
.font(.title2.bold())
.foregroundColor(.white)
Text(dateString)
.font(.subheadline)
.foregroundColor(Color(hex: "8888aa"))
}
Spacer()
// Sync button
Button {
Task { await syncHealthKit() }
} label: {
ZStack {
Circle()
.fill(Color(hex: "1a1a3e"))
.frame(width: 42, height: 42)
if healthKit.isSyncing {
ProgressView()
.tint(Color(hex: "00d4aa"))
.scaleEffect(0.8)
} else {
Image(systemName: "arrow.triangle.2.circlepath")
.font(.system(size: 16, weight: .medium))
.foregroundColor(Color(hex: "00d4aa"))
}
}
}
.disabled(healthKit.isSyncing)
// Logout
Button {
UIImpactFeedbackGenerator(style: .light).impactOccurred()
authManager.logout()
} label: {
ZStack {
Circle()
.fill(Color(hex: "1a1a3e"))
.frame(width: 42, height: 42)
Image(systemName: "rectangle.portrait.and.arrow.right")
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color(hex: "8888aa"))
}
}
}
}
.padding(.horizontal)
}
// MARK: - Loading
private var loadingView: some View {
VStack(spacing: 16) {
ProgressView()
.tint(Color(hex: "00d4aa"))
.scaleEffect(1.2)
Text("Загрузка данных...")
.font(.subheadline)
.foregroundColor(Color(hex: "8888aa"))
}
.padding(.top, 80)
}
// MARK: - Metrics Grid
private var metricsGrid: some View {
LazyVGrid(columns: [GridItem(.flexible(), spacing: 12), GridItem(.flexible(), spacing: 12)], spacing: 12) {
// Sleep
if let sleep = latest?.sleep {
SleepCard(sleep: sleep)
.frame(maxHeight: .infinity)
}
// Heart Rate
if let rhr = latest?.restingHeartRate {
MetricCardView(
icon: "heart.fill",
title: "Пульс покоя",
value: "\(Int(rhr.value ?? 0)) уд/мин",
subtitle: latest?.heartRate != nil ? "Avg: \(latest?.heartRate?.avg ?? 0) уд/мин" : "",
color: Color(hex: "ff4757"),
gradientColors: [Color(hex: "ff4757"), Color(hex: "ff6b81")]
)
.frame(maxHeight: .infinity)
}
// HRV
if let hrv = latest?.hrv {
MetricCardView(
icon: "waveform.path.ecg",
title: "HRV",
value: "\(Int(hrv.avg ?? 0)) мс",
subtitle: hrv.latest != nil ? "Последнее: \(Int(hrv.latest!)) мс" : "Вариабельность",
color: Color(hex: "00d4aa"),
gradientColors: [Color(hex: "00d4aa"), Color(hex: "00b894")]
)
.frame(maxHeight: .infinity)
}
// Steps
if let steps = latest?.steps {
StepsCard(steps: steps.total ?? 0)
.frame(maxHeight: .infinity)
}
}
.padding(.horizontal)
}
// MARK: - Load Data
func loadData() async {
isLoading = true
async let r = APIService.shared.getReadiness(token: authManager.token)
async let l = APIService.shared.getLatest(token: authManager.token)
async let h = APIService.shared.getHeatmap(token: authManager.token, days: 7)
readiness = try? await r
latest = try? await l
heatmapData = (try? await h) ?? []
isLoading = false
}
// MARK: - Sync HealthKit
func syncHealthKit() async {
guard healthKit.isAvailable else {
showToastMessage("HealthKit недоступен на этом устройстве", success: false)
return
}
guard !authManager.apiKey.isEmpty else {
showToastMessage("API ключ не найден. Войдите заново.", success: false)
return
}
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
do {
try await healthKit.syncToServer(apiKey: authManager.apiKey)
UINotificationFeedbackGenerator().notificationOccurred(.success)
showToastMessage("Данные синхронизированы ✓", success: true)
// Reload dashboard after sync
await loadData()
} catch {
UINotificationFeedbackGenerator().notificationOccurred(.error)
showToastMessage(error.localizedDescription, success: false)
}
}
private func showToastMessage(_ message: String, success: Bool) {
toastMessage = message
toastSuccess = success
withAnimation {
showToast = true
}
}
}