import SwiftUI struct DashboardView: View { @EnvironmentObject var authManager: AuthManager @State private var todayTasks: [PulseTask] = [] @State private var todayHabits: [Habit] = [] @State private var habitsStats: HabitsOverallStats? @State private var isLoading = true @State private var showAddSheet = false @State private var addMode: AddMode = .task @State private var errorMessage: String? @State private var showError = false // Undo state @State private var recentlyLoggedHabitId: Int? @State private var recentlyLoggedHabitLogDate: String? @State private var recentlyCompletedTaskId: Int? @State private var undoTimer: Timer? enum AddMode { case task, habit } 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 completedHabitsToday: Int { todayHabits.filter { $0.completedToday == true }.count } var totalHabitsToday: Int { todayHabits.count } var dayProgress: Double { guard totalHabitsToday > 0 else { return 0 } return Double(completedHabitsToday) / Double(totalHabitsToday) } var activeTodayTasks: [PulseTask] { todayTasks.filter { !$0.completed } } var completedTodayTasksCount: Int { todayTasks.filter { $0.completed }.count } var body: some View { ZStack(alignment: .bottomTrailing) { Color(hex: "0a0a1a").ignoresSafeArea() ScrollView { VStack(spacing: 20) { // MARK: 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() } .padding(.horizontal) .padding(.top) if isLoading { ProgressView().tint(Color(hex: "0D9488")).padding(.top, 40) } else { // MARK: Day Progress VStack(alignment: .leading, spacing: 8) { HStack { Text("Прогресс дня") .font(.subheadline).foregroundColor(Color(hex: "8888aa")) Spacer() Text("\(completedHabitsToday)/\(totalHabitsToday) привычек") .font(.caption).foregroundColor(Color(hex: "0D9488")) } GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 4) .fill(Color.white.opacity(0.1)) RoundedRectangle(cornerRadius: 4) .fill(LinearGradient(colors: [Color(hex: "0D9488"), Color(hex: "14b8a6")], startPoint: .leading, endPoint: .trailing)) .frame(width: geo.size.width * dayProgress) .animation(.easeInOut(duration: 0.5), value: dayProgress) } } .frame(height: 8) } .padding(.horizontal) // MARK: Stat Cards LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { DashStatCard(icon: "checkmark.circle.fill", value: "\(completedHabitsToday)", label: "Выполнено сегодня", color: "0D9488") DashStatCard(icon: "flame.fill", value: "\(habitsStats?.activeHabits ?? totalHabitsToday)", label: "Активных привычек", color: "ffa502") DashStatCard(icon: "calendar", value: "\(todayTasks.count)", label: "Задач на сегодня", color: "6366f1") DashStatCard(icon: "checkmark.seal.fill", value: "\(completedTodayTasksCount)", label: "Задач выполнено", color: "10b981") } .padding(.horizontal) // MARK: Today's Habits if !todayHabits.isEmpty { VStack(alignment: .leading, spacing: 10) { HStack { Text("Привычки сегодня") .font(.headline).foregroundColor(.white) Spacer() Text("\(completedHabitsToday)/\(totalHabitsToday)") .font(.caption).foregroundColor(Color(hex: "8888aa")) } .padding(.horizontal) ForEach(todayHabits) { habit in DashHabitRow( habit: habit, isUndoVisible: recentlyLoggedHabitId == habit.id, onToggle: { await toggleHabit(habit) }, onUndo: { await undoHabitLog(habit) } ) } } } else { EmptyState(icon: "flame.fill", text: "Нет привычек на сегодня") } // MARK: Today's Tasks VStack(alignment: .leading, spacing: 10) { HStack { Text("Задачи на сегодня") .font(.headline).foregroundColor(.white) Spacer() Button(action: { addMode = .task; showAddSheet = true }) { Image(systemName: "plus.circle.fill") .foregroundColor(Color(hex: "0D9488")) } } .padding(.horizontal) if todayTasks.isEmpty { EmptyState(icon: "checkmark.circle", text: "Нет задач на сегодня") } else { ForEach(todayTasks) { task in DashTaskRow( task: task, isUndoVisible: recentlyCompletedTaskId == task.id, onToggle: { await toggleTask(task) }, onUndo: { await undoTask(task) } ) } } } } Spacer(minLength: 80) } } .refreshable { await loadData(refresh: true) } // MARK: FAB Button(action: { addMode = .task; showAddSheet = true }) { ZStack { Circle() .fill(LinearGradient(colors: [Color(hex: "0D9488"), Color(hex: "14b8a6")], startPoint: .topLeading, endPoint: .bottomTrailing)) .frame(width: 56, height: 56) .shadow(color: Color(hex: "0D9488").opacity(0.4), radius: 8, y: 4) Image(systemName: "plus").font(.title2.bold()).foregroundColor(.white) } } .padding(.bottom, 90) .padding(.trailing, 20) } .task { await loadData() } .sheet(isPresented: $showAddSheet) { if addMode == .task { AddTaskView(isPresented: $showAddSheet) { await loadData(refresh: true) } .presentationDetents([.medium, .large]) .presentationDragIndicator(.visible) .presentationBackground(Color(hex: "0a0a1a")) } else { AddHabitView(isPresented: $showAddSheet) { await loadData(refresh: true) } .presentationDetents([.large]) .presentationDragIndicator(.visible) .presentationBackground(Color(hex: "0a0a1a")) } } .alert("Ошибка", isPresented: $showError) { Button("OK", role: .cancel) {} } message: { Text(errorMessage ?? "") } } // MARK: - Data Loading func loadData(refresh: Bool = false) async { if !refresh { isLoading = true } async let tasks = APIService.shared.getTodayTasks(token: authManager.token) async let habits = APIService.shared.getHabits(token: authManager.token) async let stats = APIService.shared.getHabitsStats(token: authManager.token) todayTasks = (try? await tasks) ?? [] todayHabits = (try? await habits) ?? [] habitsStats = try? await stats isLoading = false } // MARK: - Actions func toggleHabit(_ habit: Habit) async { if habit.completedToday == true { // Already done — undo will handle it return } UIImpactFeedbackGenerator(style: .medium).impactOccurred() do { let today = todayDateString() try await APIService.shared.logHabit(token: authManager.token, id: habit.id, date: today) recentlyLoggedHabitId = habit.id recentlyLoggedHabitLogDate = today await loadData(refresh: true) scheduleUndoClear() } catch APIError.serverError(let code, _) where code == 409 { await MainActor.run { if let idx = todayHabits.firstIndex(where: { $0.id == habit.id }) { todayHabits[idx].completedToday = true } recentlyLoggedHabitId = habit.id recentlyLoggedHabitLogDate = today } scheduleUndoClear() } catch { errorMessage = error.localizedDescription; showError = true } } func undoHabitLog(_ habit: Habit) async { UIImpactFeedbackGenerator(style: .light).impactOccurred() // Get logs and find today's log to delete do { let logs = try await APIService.shared.getHabitLogs(token: authManager.token, habitId: habit.id, days: 1) let today = todayDateString() if let log = logs.first(where: { $0.dateOnly == today }) { try await APIService.shared.unlogHabit(token: authManager.token, habitId: habit.id, logId: log.id) } } catch {} recentlyLoggedHabitId = nil await loadData(refresh: true) } func toggleTask(_ task: PulseTask) async { UIImpactFeedbackGenerator(style: .light).impactOccurred() do { if task.completed { try await APIService.shared.uncompleteTask(token: authManager.token, id: task.id) } else { try await APIService.shared.completeTask(token: authManager.token, id: task.id) recentlyCompletedTaskId = task.id scheduleUndoClear() } await loadData(refresh: true) } catch { errorMessage = error.localizedDescription; showError = true } } func undoTask(_ task: PulseTask) async { UIImpactFeedbackGenerator(style: .light).impactOccurred() do { try await APIService.shared.uncompleteTask(token: authManager.token, id: task.id) } catch {} recentlyCompletedTaskId = nil await loadData(refresh: true) } func scheduleUndoClear() { undoTimer?.invalidate() undoTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in recentlyLoggedHabitId = nil recentlyCompletedTaskId = nil } } func todayDateString() -> String { let df = DateFormatter() df.dateFormat = "yyyy-MM-dd" return df.string(from: Date()) } } // MARK: - DashStatCard struct DashStatCard: View { let icon: String let value: String let label: String let color: String var body: some View { VStack(spacing: 8) { Image(systemName: icon) .foregroundColor(Color(hex: color)) .font(.title2) Text(value) .font(.title3.bold()).foregroundColor(.white) Text(label) .font(.caption).foregroundColor(Color(hex: "8888aa")) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity) .padding(16) .background(RoundedRectangle(cornerRadius: 16).fill(Color.white.opacity(0.05))) } } // MARK: - DashHabitRow struct DashHabitRow: View { let habit: Habit let isUndoVisible: Bool let onToggle: () async -> Void let onUndo: () async -> Void var accentColor: Color { Color(hex: habit.accentColorHex.replacingOccurrences(of: "#", with: "")) } var isDone: Bool { habit.completedToday == true } var body: some View { HStack(spacing: 14) { ZStack { Circle().fill(accentColor.opacity(isDone ? 0.3 : 0.1)).frame(width: 44, height: 44) Text(habit.displayIcon).font(.title3) } VStack(alignment: .leading, spacing: 3) { Text(habit.name) .font(.callout.weight(.medium)).foregroundColor(.white) HStack(spacing: 6) { Text(habit.frequencyLabel).font(.caption).foregroundColor(Color(hex: "8888aa")) if let streak = habit.currentStreak, streak > 0 { Text("🔥 \(streak)").font(.caption).foregroundColor(Color(hex: "ffa502")) } } } Spacer() if isUndoVisible { Button(action: { Task { await onUndo() } }) { Text("Отмена").font(.caption.bold()) .foregroundColor(Color(hex: "ffa502")) .padding(.horizontal, 10).padding(.vertical, 6) .background(RoundedRectangle(cornerRadius: 8).fill(Color(hex: "ffa502").opacity(0.15))) } } Button(action: { guard !isDone else { return }; Task { await onToggle() } }) { Image(systemName: isDone ? "checkmark.circle.fill" : "circle") .font(.title2) .foregroundColor(isDone ? accentColor : Color(hex: "8888aa")) } } .padding(14) .background( RoundedRectangle(cornerRadius: 16) .fill(isDone ? accentColor.opacity(0.08) : Color.white.opacity(0.04)) .overlay(RoundedRectangle(cornerRadius: 16).stroke(isDone ? accentColor.opacity(0.3) : Color.clear, lineWidth: 1)) ) .padding(.horizontal) .padding(.vertical, 2) } } // MARK: - DashTaskRow struct DashTaskRow: View { let task: PulseTask let isUndoVisible: Bool let onToggle: () async -> Void let onUndo: () async -> Void var body: some View { HStack(spacing: 12) { Button(action: { Task { await onToggle() } }) { Image(systemName: task.completed ? "checkmark.circle.fill" : "circle") .font(.title3) .foregroundColor(task.completed ? Color(hex: "0D9488") : Color(hex: "8888aa")) } VStack(alignment: .leading, spacing: 3) { Text(task.title) .foregroundColor(task.completed ? Color(hex: "8888aa") : .white) .strikethrough(task.completed) .font(.callout) HStack(spacing: 6) { if let due = task.dueDateFormatted { Text(due) .font(.caption2) .foregroundColor(task.isOverdue ? Color(hex: "ff4757") : Color(hex: "ffa502")) } if let p = task.priority, p > 1 { Circle().fill(Color(hex: task.priorityColor)).frame(width: 6, height: 6) } if task.isRecurring == true { Image(systemName: "arrow.clockwise") .font(.caption2).foregroundColor(Color(hex: "8888aa")) } } } Spacer() if isUndoVisible { Button(action: { Task { await onUndo() } }) { Text("Отмена").font(.caption.bold()) .foregroundColor(Color(hex: "ffa502")) .padding(.horizontal, 10).padding(.vertical, 6) .background(RoundedRectangle(cornerRadius: 8).fill(Color(hex: "ffa502").opacity(0.15))) } } } .padding(12) .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.05))) .padding(.horizontal) .padding(.vertical, 2) } } // MARK: - EmptyState (reusable) struct EmptyState: View { let icon: String let text: String var body: some View { VStack(spacing: 8) { Image(systemName: icon).font(.system(size: 32)).foregroundColor(Color(hex: "334155")) Text(text).font(.subheadline).foregroundColor(Color(hex: "8888aa")) } .frame(maxWidth: .infinity) .padding(.vertical, 24) } }