Files
pulse-mobile/PulseHealth/Views/MainTabView.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

107 lines
3.5 KiB
Swift

import SwiftUI
struct MainTabView: View {
@EnvironmentObject var authManager: AuthManager
@AppStorage("colorScheme") private var colorSchemeRaw: String = "dark"
@State private var selectedTab = 0
var preferredColorScheme: ColorScheme? {
colorSchemeRaw == "light" ? .light : .dark
}
let tabs: [(icon: String, label: String, color: Color)] = [
("house.fill", "Главная", Theme.teal),
("chart.bar.fill", "Трекер", Theme.indigo),
("heart.fill", "Здоровье", Theme.pink),
("building.columns.fill", "Накопления", Theme.purple),
("gearshape.fill", "Ещё", Theme.blue),
]
var body: some View {
VStack(spacing: 0) {
// Content
Group {
switch selectedTab {
case 0: DashboardView()
case 1: TrackerView()
case 2: HealthView()
case 3: SavingsView()
case 4: SettingsView()
default: DashboardView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
// Custom Tab Bar
HStack(spacing: 0) {
ForEach(0..<tabs.count, id: \.self) { index in
TabBarButton(
icon: tabs[index].icon,
label: tabs[index].label,
color: tabs[index].color,
isSelected: selectedTab == index
) {
withAnimation(.easeInOut(duration: 0.2)) {
selectedTab = index
}
UIImpactFeedbackGenerator(style: .light).impactOccurred()
}
}
}
.padding(.horizontal, 8)
.padding(.top, 8)
.padding(.bottom, 4)
.background(
Color(hex: "06060f")
.overlay(alignment: .top) {
Rectangle()
.fill(Color.white.opacity(0.06))
.frame(height: 0.5)
}
)
}
.preferredColorScheme(preferredColorScheme)
}
}
// MARK: - Tab Bar Button
struct TabBarButton: View {
let icon: String
let label: String
let color: Color
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
VStack(spacing: 4) {
ZStack {
if isSelected {
// Glow effect
Circle()
.fill(color.opacity(0.3))
.frame(width: 40, height: 40)
.blur(radius: 10)
Circle()
.fill(color.opacity(0.15))
.frame(width: 36, height: 36)
}
Image(systemName: icon)
.font(.system(size: 18, weight: isSelected ? .semibold : .regular))
.foregroundColor(isSelected ? color : Color(hex: "555566"))
}
.frame(height: 32)
Text(label)
.font(.system(size: 10, weight: isSelected ? .medium : .regular))
.foregroundColor(isSelected ? color : Color(hex: "555566"))
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.plain)
}
}