Files
pulse-mobile/PulseHealth/Views/Profile/ProfileView.swift
Daniil Klimov 28fca1de89 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>
2026-04-05 23:15:36 +03:00

82 lines
4.0 KiB
Swift

import SwiftUI
// ProfileView is now replaced by SettingsView in MainTabView.
// This file is kept for legacy support (ChangePasswordView is here).
struct ProfileView: View {
@EnvironmentObject var authManager: AuthManager
@State private var showChangePassword = false
var body: some View {
SettingsView()
}
}
struct ChangePasswordView: View {
@Binding var isPresented: Bool
@EnvironmentObject var authManager: AuthManager
@State private var oldPassword = ""
@State private var newPassword = ""
@State private var confirm = ""
@State private var isLoading = false
@State private var errorMessage = ""
@State private var success = false
var body: some View {
ZStack {
Color(hex: "06060f").ignoresSafeArea()
VStack(spacing: 16) {
RoundedRectangle(cornerRadius: 3).fill(Color.white.opacity(0.2)).frame(width: 40, height: 4).padding(.top, 12)
Text("Смена пароля").font(.title3.bold()).foregroundColor(.white).padding(.top, 4)
if success {
VStack(spacing: 12) {
Text("").font(.system(size: 50))
Text("Пароль изменён!").font(.title2.bold()).foregroundColor(.white)
}.padding(.top, 40)
Button("Закрыть") { isPresented = false }.foregroundColor(Color(hex: "0D9488"))
} else {
VStack(spacing: 12) {
SecureField("Текущий пароль", text: $oldPassword)
.padding().background(Color.white.opacity(0.08)).cornerRadius(12).foregroundColor(.white)
SecureField("Новый пароль", text: $newPassword)
.padding().background(Color.white.opacity(0.08)).cornerRadius(12).foregroundColor(.white)
SecureField("Подтвердите пароль", text: $confirm)
.padding().background(Color.white.opacity(0.08)).cornerRadius(12).foregroundColor(.white)
if !errorMessage.isEmpty {
Text(errorMessage).foregroundColor(.red).font(.caption)
}
Button(action: change) {
if isLoading { ProgressView().tint(.black) }
else { Text("Сменить").font(.headline).foregroundColor(.black) }
}
.frame(maxWidth: .infinity).padding()
.background(Color(hex: "0D9488")).cornerRadius(12)
.disabled(oldPassword.isEmpty || newPassword.isEmpty || isLoading)
}
Button("Отмена") { isPresented = false }
.foregroundColor(Color(hex: "8888aa")).padding(.top, 4)
}
Spacer()
}.padding()
}
}
func change() {
guard newPassword == confirm else { errorMessage = "Пароли не совпадают"; return }
guard newPassword.count >= 8 else { errorMessage = "Минимум 8 символов"; return }
isLoading = true
Task {
var req = URLRequest(url: URL(string: "https://api.digital-home.site/auth/password")!)
req.httpMethod = "PUT"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue("Bearer \(authManager.token)", forHTTPHeaderField: "Authorization")
req.httpBody = try? JSONEncoder().encode(["old_password": oldPassword, "new_password": newPassword])
let (_, response) = (try? await URLSession.shared.data(for: req)) ?? (Data(), nil)
await MainActor.run {
if (response as? HTTPURLResponse)?.statusCode == 200 { success = true }
else { errorMessage = "Неверный текущий пароль" }
isLoading = false
}
}
}
}