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) { Theme.bg.ignoresSafeArea() ScrollView(showsIndicators: false) { 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(Theme.textSecondary) } Spacer() } .padding(.horizontal) .padding(.top) if isLoading { ProgressView().tint(Theme.teal).padding(.top, 40) } else { // MARK: Day Progress VStack(alignment: .leading, spacing: 10) { HStack { Text("Прогресс дня") .font(.subheadline.weight(.medium)).foregroundColor(Theme.textSecondary) Spacer() Text("\(completedHabitsToday)/\(totalHabitsToday)") .font(.caption.bold()).foregroundColor(Theme.teal) } GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 6) .fill(Color.white.opacity(0.08)) RoundedRectangle(cornerRadius: 6) .fill(LinearGradient(colors: [Theme.teal, Theme.tealLight], startPoint: .leading, endPoint: .trailing)) .frame(width: geo.size.width * dayProgress) .shadow(color: Theme.teal.opacity(0.5), radius: 8, y: 0) .animation(.easeInOut(duration: 0.5), value: dayProgress) } } .frame(height: 8) } .padding(16) .glassCard(cornerRadius: 16) .padding(.horizontal) // MARK: Stat Cards LazyVGrid(columns: [GridItem(.flexible(), spacing: 12), GridItem(.flexible(), spacing: 12)], spacing: 12) { GlowStatCard(icon: "checkmark.circle.fill", value: "\(completedHabitsToday)", label: "Выполнено", color: Theme.teal) GlowStatCard(icon: "flame.fill", value: "\(habitsStats?.activeHabits ?? totalHabitsToday)", label: "Активных", color: Theme.orange) GlowStatCard(icon: "calendar", value: "\(todayTasks.count)", label: "Задач", color: Theme.indigo) GlowStatCard(icon: "checkmark.seal.fill", value: "\(completedTodayTasksCount)", label: "Готово", color: Theme.green) } .padding(.horizontal) // MARK: Today's Habits if !todayHabits.isEmpty { VStack(alignment: .leading, spacing: 10) { SectionHeader(title: "Привычки", trailing: "\(completedHabitsToday)/\(totalHabitsToday)") 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(Theme.teal).font(.title3) } } .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: 100) } } .refreshable { await loadData(refresh: true) } // MARK: FAB Button(action: { addMode = .task; showAddSheet = true }) { ZStack { Circle() .fill(Theme.teal.opacity(0.3)) .frame(width: 64, height: 64) .blur(radius: 10) Circle() .fill(LinearGradient(colors: [Theme.teal, Theme.tealLight], startPoint: .topLeading, endPoint: .bottomTrailing)) .frame(width: 56, height: 56) .shadow(color: Theme.teal.opacity(0.5), radius: 12, y: 4) Image(systemName: "plus").font(.title2.bold()).foregroundColor(.white) } } .padding(.bottom, 100) .padding(.trailing, 20) } .task { await loadData() } .sheet(isPresented: $showAddSheet) { if addMode == .task { AddTaskView(isPresented: $showAddSheet) { await loadData(refresh: true) } .presentationDetents([.large]) .presentationDragIndicator(.visible) .presentationBackground(Theme.bg) } else { AddHabitView(isPresented: $showAddSheet) { await loadData(refresh: true) } .presentationDetents([.large]) .presentationDragIndicator(.visible) .presentationBackground(Theme.bg) } } .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 allHabits = APIService.shared.getHabits(token: authManager.token) async let stats = APIService.shared.getHabitsStats(token: authManager.token) todayTasks = (try? await tasks) ?? [] habitsStats = try? await stats // Filter habits for today + check completion var habits = ((try? await allHabits) ?? []).filter { !($0.isArchived ?? false) } habits = filterHabitsForToday(habits) // Check which habits are completed today let today = todayDateString() for i in habits.indices { let logs = (try? await APIService.shared.getHabitLogs(token: authManager.token, habitId: habits[i].id, days: 1)) ?? [] habits[i].completedToday = logs.contains { $0.dateOnly == today } } todayHabits = habits isLoading = false // Update widget let completed = habits.filter { $0.completedToday == true }.count let activeTasks = todayTasks.filter { !$0.completed }.count WidgetDataService.updateHabits(completed: completed, total: habits.count, tasksCount: activeTasks) } func filterHabitsForToday(_ habits: [Habit]) -> [Habit] { let weekday = Calendar.current.component(.weekday, from: Date()) - 1 // 0=Sun, 1=Mon...6=Sat return habits.filter { habit in switch habit.frequency { case .daily: return true case .weekly: guard let days = habit.targetDays, !days.isEmpty else { return true } return days.contains(weekday) case .monthly: let day = Calendar.current.component(.day, from: Date()) return habit.targetCount == day || habit.targetDays?.contains(day) == true case .interval: // Show interval habits every N days from start guard let startStr = habit.startDate ?? habit.createdAt, let startDate = parseDate(startStr) else { return true } let daysSince = Calendar.current.dateComponents([.day], from: startDate, to: Date()).day ?? 0 let interval = max(habit.targetCount ?? 1, 1) return daysSince % interval == 0 case .custom: // Custom habits: check target_days if set, otherwise show daily guard let days = habit.targetDays, !days.isEmpty else { return true } return days.contains(weekday) } } } func parseDate(_ str: String) -> Date? { let df = DateFormatter(); df.dateFormat = "yyyy-MM-dd" return df.date(from: String(str.prefix(10))) } // MARK: - Actions func toggleHabit(_ habit: Habit) async { if habit.completedToday == true { return } UIImpactFeedbackGenerator(style: .medium).impactOccurred() let today = todayDateString() do { 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() 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: - 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 { if isDone { Circle().fill(accentColor.opacity(0.2)).frame(width: 44, height: 44).blur(radius: 6) } Circle().fill(accentColor.opacity(isDone ? 0.2 : 0.08)).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(Theme.textSecondary) if let streak = habit.currentStreak, streak > 0 { HStack(spacing: 2) { Image(systemName: "flame.fill").font(.caption2) Text("\(streak)") } .font(.caption).foregroundColor(Theme.orange) } } } Spacer() if isUndoVisible { Button(action: { Task { await onUndo() } }) { Text("Отмена").font(.caption.bold()) .foregroundColor(Theme.orange) .padding(.horizontal, 10).padding(.vertical, 6) .background(RoundedRectangle(cornerRadius: 8).fill(Theme.orange.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: "555566")) } .buttonStyle(.plain) } .padding(14) .glassCard(cornerRadius: 16) .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 ? Theme.teal : Color(hex: "555566")) } VStack(alignment: .leading, spacing: 3) { Text(task.title) .foregroundColor(task.completed ? Theme.textSecondary : .white) .strikethrough(task.completed) .font(.callout) HStack(spacing: 6) { if let due = task.dueDateFormatted { Text(due) .font(.caption2) .foregroundColor(task.isOverdue ? Theme.red : Theme.orange) } 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(Theme.textSecondary) } } } Spacer() if isUndoVisible { Button(action: { Task { await onUndo() } }) { Text("Отмена").font(.caption.bold()) .foregroundColor(Theme.orange) .padding(.horizontal, 10).padding(.vertical, 6) .background(RoundedRectangle(cornerRadius: 8).fill(Theme.orange.opacity(0.15))) } } } .padding(12) .glassCard(cornerRadius: 14) .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(Theme.textSecondary) } .frame(maxWidth: .infinity) .padding(.vertical, 24) } }