182 lines
7.4 KiB
Swift
182 lines
7.4 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.completed } }
|
|
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(refresh: true) }
|
|
}
|
|
.task { await loadData() }
|
|
}
|
|
|
|
func loadData(refresh: Bool = false) async {
|
|
if !refresh { 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)
|
|
}
|
|
}
|