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 сервиса
This commit is contained in:
181
PulseHealth/Views/Dashboard/DashboardView.swift
Normal file
181
PulseHealth/Views/Dashboard/DashboardView.swift
Normal file
@@ -0,0 +1,181 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user