Files
pulse-mobile/PulseHealth/Views/Dashboard/DashboardView.swift
Cosmo c015824b36 feat: полноценное Pulse приложение с TabBar
- Auth: переключено на Pulse API (api.digital-home.site) вместо health
- TabBar: Главная, Задачи, Привычки, Здоровье, Финансы
- Models: TaskModels, HabitModels, FinanceModels, обновлённые AuthModels
- Services: APIService (Pulse API), HealthAPIService (health отдельно)
- Dashboard: обзор дня с задачами, привычками, readiness, балансом
- Tasks: список, фильтр, создание, выполнение, удаление
- Habits: список с прогресс-баром, отметка выполнения, стрики
- Health: бывший DashboardView, HealthKit sync через health API key
- Finance: баланс, список транзакций, добавление расхода/дохода
- Health данные через x-api-key вместо JWT токена health сервиса
2026-03-25 11:49:52 +00:00

182 lines
7.3 KiB
Swift

import SwiftUI
struct DashboardView: View {
@EnvironmentObject var authManager: AuthManager
@State private var tasks: [PulseTask] = []
@State private var habits: [Habit] = []
@State private var readiness: ReadinessResponse?
@State private var summary: FinanceSummary?
@State private var isLoading = true
var greeting: String {
let h = Calendar.current.component(.hour, from: Date())
switch h {
case 5..<12: return "Доброе утро"
case 12..<17: return "Добрый день"
case 17..<22: return "Добрый вечер"
default: return "Доброй ночи"
}
}
var pendingTasks: [PulseTask] { tasks.filter { !$0.done } }
var completedHabitsToday: Int { habits.filter { $0.completedToday == true }.count }
var body: some View {
ZStack {
Color(hex: "0a0a1a").ignoresSafeArea()
ScrollView {
VStack(spacing: 20) {
// Header
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(greeting + ", " + authManager.userName + "!")
.font(.title2.bold()).foregroundColor(.white)
Text(Date(), style: .date)
.font(.subheadline).foregroundColor(Color(hex: "8888aa"))
}
Spacer()
// 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)
.padding(.top)
if isLoading {
ProgressView().tint(Color(hex: "00d4aa")).padding(.top, 40)
} else {
// Readiness Score mini card
if let r = readiness {
ReadinessMiniCard(readiness: r)
}
// Stats row
HStack(spacing: 12) {
StatCard(icon: "checkmark.circle.fill", value: "\(pendingTasks.count)", label: "Задач", color: "00d4aa")
StatCard(icon: "flame.fill", value: "\(completedHabitsToday)/\(habits.count)", label: "Привычек", color: "ffa502")
if let s = summary, let balance = s.balance {
StatCard(icon: "rublesign.circle.fill", value: "\(Int(balance))", label: "Баланс", color: "7c3aed")
}
}
.padding(.horizontal)
// Today's tasks
if !pendingTasks.isEmpty {
VStack(alignment: .leading, spacing: 12) {
Text("Задачи на сегодня").font(.headline).foregroundColor(.white).padding(.horizontal)
ForEach(pendingTasks.prefix(3)) { task in
TaskRowView(task: task) {
await completeTask(task)
}
}
}
}
// Habits progress
if !habits.isEmpty {
VStack(alignment: .leading, spacing: 12) {
Text("Привычки сегодня").font(.headline).foregroundColor(.white).padding(.horizontal)
ForEach(habits.prefix(4)) { habit in
HabitRowView(habit: habit) {
await logHabit(habit)
}
}
}
}
}
Spacer(minLength: 20)
}
}
.refreshable { await loadData() }
}
.task { await loadData() }
}
func loadData() async {
isLoading = true
async let t = APIService.shared.getTodayTasks(token: authManager.token)
async let h = APIService.shared.getHabits(token: authManager.token)
async let r = HealthAPIService.shared.getReadiness(apiKey: authManager.healthApiKey)
async let s = APIService.shared.getFinanceSummary(token: authManager.token)
tasks = (try? await t) ?? []
habits = (try? await h) ?? []
readiness = try? await r
summary = try? await s
isLoading = false
}
func completeTask(_ task: PulseTask) async {
try? await APIService.shared.completeTask(token: authManager.token, id: task.id)
await loadData()
}
func logHabit(_ habit: Habit) async {
try? await APIService.shared.logHabit(token: authManager.token, id: habit.id)
await loadData()
}
}
// MARK: - StatCard
struct StatCard: View {
let icon: String
let value: String
let label: String
let color: String
var body: some View {
VStack(spacing: 6) {
Image(systemName: icon).foregroundColor(Color(hex: color)).font(.title3)
Text(value).font(.headline.bold()).foregroundColor(.white)
Text(label).font(.caption).foregroundColor(Color(hex: "8888aa"))
}
.frame(maxWidth: .infinity)
.padding(12)
.background(RoundedRectangle(cornerRadius: 16).fill(Color.white.opacity(0.05)))
}
}
// MARK: - ReadinessMiniCard
struct ReadinessMiniCard: View {
let readiness: ReadinessResponse
var statusColor: Color {
readiness.score >= 80 ? Color(hex: "00d4aa") :
readiness.score >= 60 ? Color(hex: "ffa502") :
Color(hex: "ff4757")
}
var body: some View {
HStack(spacing: 16) {
ZStack {
Circle().stroke(Color.white.opacity(0.1), lineWidth: 6).frame(width: 60, height: 60)
Circle().trim(from: 0, to: CGFloat(readiness.score) / 100)
.stroke(statusColor, style: StrokeStyle(lineWidth: 6, lineCap: .round))
.frame(width: 60, height: 60).rotationEffect(.degrees(-90))
Text("\(readiness.score)").font(.headline.bold()).foregroundColor(statusColor)
}
VStack(alignment: .leading, spacing: 4) {
Text("Готовность").font(.subheadline).foregroundColor(Color(hex: "8888aa"))
Text(readiness.recommendation).font(.callout).foregroundColor(.white).lineLimit(2)
}
Spacer()
}
.padding(16)
.background(RoundedRectangle(cornerRadius: 16).fill(Color.white.opacity(0.05)))
.padding(.horizontal)
}
}