diff --git a/PulseHealth/Views/Dashboard/DashboardView.swift b/PulseHealth/Views/Dashboard/DashboardView.swift index f3f0c9b..4eea994 100644 --- a/PulseHealth/Views/Dashboard/DashboardView.swift +++ b/PulseHealth/Views/Dashboard/DashboardView.swift @@ -209,6 +209,15 @@ struct DashboardView: View { 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 } diff --git a/PulseHealth/Views/Habits/EditHabitView.swift b/PulseHealth/Views/Habits/EditHabitView.swift new file mode 100644 index 0000000..238ab2e --- /dev/null +++ b/PulseHealth/Views/Habits/EditHabitView.swift @@ -0,0 +1,229 @@ +import SwiftUI + +struct EditHabitView: View { + @Binding var isPresented: Bool + @EnvironmentObject var authManager: AuthManager + let habit: Habit + let onSaved: () async -> Void + + @State private var name: String + @State private var selectedIcon: String + @State private var selectedColor: String + @State private var frequency: HabitFrequency + @State private var selectedWeekdays: Set + @State private var intervalDays: String + @State private var isLoading = false + @State private var showArchiveConfirm = false + + let weekdayNames = ["Вс","Пн","Вт","Ср","Чт","Пт","Сб"] + let icons = ["🔥", "💪", "🏃", "📚", "💧", "🧘", "🎯", "⭐️", "🌟", "✅", + "🏋️", "🚴", "🍎", "😴", "🧠", "🎨", "🎵", "💊", "🌿", "💰", + "✍️", "🧹", "🏊", "🚶", "🎮", "📝", "🌅", "🥗", "🧃", "🫁"] + let colors = ["#0D9488", "#7c3aed", "#ff4757", "#ffa502", "#6366f1", + "#ec4899", "#14b8a6", "#f59e0b", "#10b981", "#3b82f6"] + let frequencies: [(HabitFrequency, String, String)] = [ + (.daily, "Каждый день", "calendar"), + (.weekly, "По дням недели", "calendar.badge.clock"), + (.interval, "Каждые N дней", "repeat"), + (.monthly, "Каждый месяц", "calendar.badge.plus") + ] + + init(isPresented: Binding, habit: Habit, onSaved: @escaping () async -> Void) { + self._isPresented = isPresented + self.habit = habit + self.onSaved = onSaved + self._name = State(initialValue: habit.name) + self._selectedIcon = State(initialValue: habit.icon ?? "🔥") + self._selectedColor = State(initialValue: habit.color ?? "#0D9488") + self._frequency = State(initialValue: habit.frequency) + self._selectedWeekdays = State(initialValue: Set(habit.targetDays ?? [1,2,3,4,5])) + self._intervalDays = State(initialValue: String(habit.targetCount ?? 2)) + } + + var body: some View { + ZStack { + Color(hex: "0a0a1a").ignoresSafeArea() + VStack(spacing: 0) { + RoundedRectangle(cornerRadius: 3) + .fill(Color.white.opacity(0.2)).frame(width: 40, height: 4).padding(.top, 12) + HStack { + Button("Отмена") { isPresented = false }.foregroundColor(Color(hex: "8888aa")) + Spacer() + Text("Редактировать привычку").font(.headline).foregroundColor(.white) + Spacer() + Button(action: save) { + if isLoading { ProgressView().tint(Color(hex: "0D9488")).scaleEffect(0.8) } + else { Text("Сохранить").foregroundColor(name.isEmpty ? Color(hex: "8888aa") : Color(hex: "0D9488")).fontWeight(.semibold) } + }.disabled(name.isEmpty || isLoading) + } + .padding(.horizontal, 20).padding(.vertical, 16) + Divider().background(Color.white.opacity(0.1)) + ScrollView { + VStack(spacing: 20) { + // Preview + HStack(spacing: 16) { + ZStack { + Circle() + .fill(Color(hex: String(selectedColor.dropFirst())).opacity(0.25)) + .frame(width: 56, height: 56) + Text(selectedIcon).font(.title2) + } + VStack(alignment: .leading, spacing: 4) { + Text(name.isEmpty ? "Название привычки" : name) + .font(.callout.bold()) + .foregroundColor(name.isEmpty ? Color(hex: "8888aa") : .white) + Text(frequencies.first { $0.0 == frequency }?.1 ?? "") + .font(.caption).foregroundColor(Color(hex: "8888aa")) + } + Spacer() + } + .padding(16) + .background(RoundedRectangle(cornerRadius: 16).fill(Color.white.opacity(0.05))) + + // Name + VStack(alignment: .leading, spacing: 8) { + Label("Название", systemImage: "pencil").font(.caption).foregroundColor(Color(hex: "8888aa")) + TextField("Например: Читать 30 минут", text: $name) + .foregroundColor(.white).padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07))) + } + + // Frequency + VStack(alignment: .leading, spacing: 8) { + Label("Периодичность", systemImage: "calendar").font(.caption).foregroundColor(Color(hex: "8888aa")) + VStack(spacing: 6) { + ForEach(frequencies, id: \.0) { f in + Button(action: { frequency = f.0 }) { + HStack { + Image(systemName: f.2).foregroundColor(frequency == f.0 ? Color(hex: "0D9488") : Color(hex: "8888aa")) + Text(f.1).foregroundColor(frequency == f.0 ? .white : Color(hex: "8888aa")) + Spacer() + if frequency == f.0 { Image(systemName: "checkmark").foregroundColor(Color(hex: "0D9488")) } + } + .padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(frequency == f.0 ? Color(hex: "0D9488").opacity(0.15) : Color.white.opacity(0.05))) + } + } + } + if frequency == .weekly { + HStack(spacing: 8) { + ForEach(0..<7) { i in + Button(action: { + if selectedWeekdays.contains(i) { if selectedWeekdays.count > 1 { selectedWeekdays.remove(i) } } + else { selectedWeekdays.insert(i) } + }) { + Text(weekdayNames[i]) + .font(.caption.bold()) + .foregroundColor(selectedWeekdays.contains(i) ? .black : .white) + .frame(width: 32, height: 32) + .background(Circle().fill(selectedWeekdays.contains(i) ? Color(hex: "0D9488") : Color.white.opacity(0.08))) + } + } + } + } + if frequency == .interval { + HStack { + Text("Каждые").foregroundColor(Color(hex: "8888aa")).font(.callout) + TextField("2", text: $intervalDays).keyboardType(.numberPad) + .foregroundColor(.white) + .frame(width: 50) + .padding(10) + .background(RoundedRectangle(cornerRadius: 8).fill(Color.white.opacity(0.07))) + Text("дней").foregroundColor(Color(hex: "8888aa")).font(.callout) + } + } + } + + // Icon picker + VStack(alignment: .leading, spacing: 8) { + Label("Иконка", systemImage: "face.smiling").font(.caption).foregroundColor(Color(hex: "8888aa")) + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: 8) { + ForEach(icons, id: \.self) { icon in + Button(action: { selectedIcon = icon }) { + Text(icon).font(.title3) + .frame(width: 40, height: 40) + .background(Circle().fill(selectedIcon == icon ? Color(hex: "0D9488").opacity(0.25) : Color.white.opacity(0.05))) + .overlay(Circle().stroke(selectedIcon == icon ? Color(hex: "0D9488") : Color.clear, lineWidth: 2)) + } + } + } + } + + // Color picker + VStack(alignment: .leading, spacing: 8) { + Label("Цвет", systemImage: "paintpalette").font(.caption).foregroundColor(Color(hex: "8888aa")) + HStack(spacing: 10) { + ForEach(colors, id: \.self) { color in + Button(action: { selectedColor = color }) { + Circle() + .fill(Color(hex: String(color.dropFirst()))) + .frame(width: 32, height: 32) + .overlay(Circle().stroke(.white, lineWidth: selectedColor == color ? 2 : 0)) + .scaleEffect(selectedColor == color ? 1.15 : 1.0) + .animation(.easeInOut(duration: 0.15), value: selectedColor) + } + } + } + } + + // Archive / Restore button + Button(action: { showArchiveConfirm = true }) { + HStack { + Image(systemName: habit.isArchived == true ? "arrow.uturn.backward" : "archivebox") + Text(habit.isArchived == true ? "Восстановить" : "Архивировать") + } + .foregroundColor(habit.isArchived == true ? Color(hex: "0D9488") : Color(hex: "ff4757")) + .frame(maxWidth: .infinity) + .padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.05))) + } + }.padding(20) + } + } + } + .confirmationDialog( + habit.isArchived == true ? "Восстановить привычку?" : "Архивировать привычку?", + isPresented: $showArchiveConfirm, + titleVisibility: .visible + ) { + Button(habit.isArchived == true ? "Восстановить" : "Архивировать", + role: habit.isArchived == true ? .none : .destructive) { + Task { await toggleArchive() } + } + Button("Отмена", role: .cancel) {} + } + } + + func save() { + isLoading = true + Task { + var body: [String: Any] = [ + "name": name, + "frequency": frequency.rawValue, + "icon": selectedIcon, + "color": selectedColor, + "target_count": 1 + ] + if frequency == .weekly { + body["target_days"] = Array(selectedWeekdays).sorted() + } + if frequency == .interval { + body["target_count"] = Int(intervalDays) ?? 2 + } + if let reqBody = try? JSONSerialization.data(withJSONObject: body) { + try? await APIService.shared.updateHabit(token: authManager.token, id: habit.id, body: reqBody) + } + await onSaved() + await MainActor.run { isPresented = false } + } + } + + func toggleArchive() async { + let params: [String: Any] = ["is_archived": !(habit.isArchived == true)] + if let body = try? JSONSerialization.data(withJSONObject: params) { + try? await APIService.shared.updateHabit(token: authManager.token, id: habit.id, body: body) + } + await onSaved() + await MainActor.run { isPresented = false } + } +} diff --git a/PulseHealth/Views/Savings/EditSavingsCategoryView.swift b/PulseHealth/Views/Savings/EditSavingsCategoryView.swift new file mode 100644 index 0000000..ee17f3f --- /dev/null +++ b/PulseHealth/Views/Savings/EditSavingsCategoryView.swift @@ -0,0 +1,138 @@ +import SwiftUI + +struct EditSavingsCategoryView: View { + @Binding var isPresented: Bool + @EnvironmentObject var authManager: AuthManager + let category: SavingsCategory + let onSaved: () async -> Void + + @State private var name: String + @State private var isDeposit: Bool + @State private var isRecurring: Bool + @State private var isAccount: Bool + @State private var recurringAmount: String + @State private var interestRate: String + @State private var isLoading = false + @State private var showCloseConfirm = false + + init(isPresented: Binding, category: SavingsCategory, onSaved: @escaping () async -> Void) { + self._isPresented = isPresented + self.category = category + self.onSaved = onSaved + self._name = State(initialValue: category.name) + self._isDeposit = State(initialValue: category.isDeposit == true) + self._isRecurring = State(initialValue: category.isRecurring == true) + self._isAccount = State(initialValue: category.isAccount == true) + let ra = category.recurringAmount + self._recurringAmount = State(initialValue: ra != nil ? String(format: "%.0f", ra!) : "") + let ir = category.interestRate + self._interestRate = State(initialValue: ir != nil ? String(format: "%.1f", ir!) : "") + } + + var body: some View { + ZStack { + Color(hex: "0a0a1a").ignoresSafeArea() + VStack(spacing: 0) { + RoundedRectangle(cornerRadius: 3).fill(Color.white.opacity(0.2)).frame(width: 40, height: 4).padding(.top, 12) + HStack { + Button("Отмена") { isPresented = false }.foregroundColor(Color(hex: "8888aa")) + Spacer() + Text("Редактировать категорию").font(.headline).foregroundColor(.white) + Spacer() + Button(action: save) { + if isLoading { ProgressView().tint(Color(hex: "0D9488")).scaleEffect(0.8) } + else { Text("Сохранить").foregroundColor(name.isEmpty ? Color(hex: "8888aa") : Color(hex: "0D9488")).fontWeight(.semibold) } + }.disabled(name.isEmpty || isLoading) + } + .padding(.horizontal, 20).padding(.vertical, 16) + Divider().background(Color.white.opacity(0.1)) + ScrollView { + VStack(spacing: 16) { + fieldLabel("Название") { + TextField("Например: На машину", text: $name) + .foregroundColor(.white).padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07))) + } + VStack(alignment: .leading, spacing: 8) { + Label("Тип", systemImage: "tag.fill").font(.caption).foregroundColor(Color(hex: "8888aa")) + HStack(spacing: 8) { + TypeButton(label: "💰 Накопление", selected: !isDeposit && !isRecurring && !isAccount) { isDeposit = false; isRecurring = false; isAccount = false } + TypeButton(label: "🏦 Вклад", selected: isDeposit) { isDeposit = true; isRecurring = false; isAccount = false } + } + HStack(spacing: 8) { + TypeButton(label: "🔄 Регулярные", selected: isRecurring) { isDeposit = false; isRecurring = true; isAccount = false } + TypeButton(label: "🏧 Счёт", selected: isAccount) { isDeposit = false; isRecurring = false; isAccount = true } + } + } + if isRecurring { + fieldLabel("Сумма / мес. (₽)") { + TextField("0", text: $recurringAmount).keyboardType(.decimalPad) + .foregroundColor(.white).padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07))) + } + } + if isDeposit { + fieldLabel("Ставка (%)") { + TextField("0.0", text: $interestRate).keyboardType(.decimalPad) + .foregroundColor(.white).padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07))) + } + } + + // Close / Restore + Button(action: { showCloseConfirm = true }) { + HStack { + Image(systemName: category.isClosed == true ? "arrow.uturn.backward" : "checkmark.seal") + Text(category.isClosed == true ? "Восстановить" : "Закрыть категорию") + } + .foregroundColor(category.isClosed == true ? Color(hex: "0D9488") : Color(hex: "ff4757")) + .frame(maxWidth: .infinity).padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.05))) + } + }.padding(20) + } + } + } + .confirmationDialog( + category.isClosed == true ? "Восстановить категорию?" : "Закрыть категорию?", + isPresented: $showCloseConfirm, + titleVisibility: .visible + ) { + Button(category.isClosed == true ? "Восстановить" : "Закрыть", + role: category.isClosed == true ? .none : .destructive) { + Task { await toggleClose() } + } + Button("Отмена", role: .cancel) {} + } + } + + @ViewBuilder func fieldLabel(_ label: String, @ViewBuilder content: () -> Content) -> some View { + VStack(alignment: .leading, spacing: 8) { + Label(label, systemImage: "pencil").font(.caption).foregroundColor(Color(hex: "8888aa")) + content() + } + } + + func save() { + isLoading = true + Task { + var params: [String: Any] = ["name": name, "is_deposit": isDeposit, "is_recurring": isRecurring, "is_account": isAccount] + if isRecurring, let a = Double(recurringAmount) { params["recurring_amount"] = a } + if isDeposit, let r = Double(interestRate) { params["interest_rate"] = r } + if let body = try? JSONSerialization.data(withJSONObject: params) { + try? await APIService.shared.updateSavingsCategory(token: authManager.token, id: category.id, body: body) + } + await onSaved() + await MainActor.run { isPresented = false } + } + } + + func toggleClose() async { + let params: [String: Any] = ["is_closed": !(category.isClosed == true)] + if let body = try? JSONSerialization.data(withJSONObject: params) { + try? await APIService.shared.updateSavingsCategory(token: authManager.token, id: category.id, body: body) + } + await onSaved() + await MainActor.run { isPresented = false } + } +} diff --git a/PulseHealth/Views/Savings/SavingsView.swift b/PulseHealth/Views/Savings/SavingsView.swift index fa98140..720f9c0 100644 --- a/PulseHealth/Views/Savings/SavingsView.swift +++ b/PulseHealth/Views/Savings/SavingsView.swift @@ -240,6 +240,12 @@ struct SavingsCategoriesTab: View { .presentationDragIndicator(.visible) .presentationBackground(Color(hex: "0a0a1a")) } + .sheet(item: ) { cat in + EditSavingsCategoryView(isPresented: .constant(true), category: cat) { await load(refresh: true) } + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + .presentationBackground(Color(hex: 0a0a1a)) + } } func load(refresh: Bool = false) async { diff --git a/PulseHealth/Views/Tasks/EditTaskView.swift b/PulseHealth/Views/Tasks/EditTaskView.swift new file mode 100644 index 0000000..e72c5cf --- /dev/null +++ b/PulseHealth/Views/Tasks/EditTaskView.swift @@ -0,0 +1,159 @@ +import SwiftUI + +struct EditTaskView: View { + @Binding var isPresented: Bool + @EnvironmentObject var authManager: AuthManager + let task: PulseTask + let onSaved: () async -> Void + + @State private var title: String + @State private var description: String + @State private var priority: Int + @State private var selectedIcon: String + @State private var selectedColor: String + @State private var hasDueDate: Bool + @State private var dueDate: Date + @State private var isLoading = false + + let priorities: [(Int, String, String)] = [ + (1, "Низкий", "8888aa"), + (2, "Средний", "ffa502"), + (3, "Высокий", "ff4757"), + (4, "Срочный", "ff0000") + ] + let icons = ["✅","📌","🎯","💼","🏠","🛒","📞","🎓","💊","🚗", + "📅","⚡","🔧","📬","💡","🏋️","🌿","🎵","✍️","🌏"] + let colors = ["#0D9488","#7c3aed","#ff4757","#ffa502","#6366f1", + "#ec4899","#14b8a6","#f59e0b","#10b981","#3b82f6"] + + init(isPresented: Binding, task: PulseTask, onSaved: @escaping () async -> Void) { + self._isPresented = isPresented + self.task = task + self.onSaved = onSaved + self._title = State(initialValue: task.title) + self._description = State(initialValue: task.description ?? "") + self._priority = State(initialValue: task.priority ?? 2) + self._selectedIcon = State(initialValue: task.icon ?? "✅") + self._selectedColor = State(initialValue: task.color ?? "#0D9488") + if let dueDateStr = task.dueDate, let parsed = Self.parseDate(dueDateStr) { + self._hasDueDate = State(initialValue: true) + self._dueDate = State(initialValue: parsed) + } else { + self._hasDueDate = State(initialValue: false) + self._dueDate = State(initialValue: Date()) + } + } + + static func parseDate(_ str: String) -> Date? { + let df = DateFormatter() + df.dateFormat = "yyyy-MM-dd" + return df.date(from: String(str.prefix(10))) + } + + var body: some View { + ZStack { + Color(hex: "0a0a1a").ignoresSafeArea() + VStack(spacing: 0) { + RoundedRectangle(cornerRadius: 3) + .fill(Color.white.opacity(0.2)).frame(width: 40, height: 4).padding(.top, 12) + HStack { + Button("Отмена") { isPresented = false }.foregroundColor(Color(hex: "8888aa")) + Spacer() + Text("Редактировать задачу").font(.headline).foregroundColor(.white) + Spacer() + Button(action: save) { + if isLoading { ProgressView().tint(Color(hex: "0D9488")).scaleEffect(0.8) } + else { Text("Сохранить").foregroundColor(title.isEmpty ? Color(hex: "8888aa") : Color(hex: "0D9488")).fontWeight(.semibold) } + }.disabled(title.isEmpty || isLoading) + } + .padding(.horizontal, 20).padding(.vertical, 16) + Divider().background(Color.white.opacity(0.1)) + ScrollView { + VStack(spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Label("Название", systemImage: "pencil").font(.caption).foregroundColor(Color(hex: "8888aa")) + TextField("Что нужно сделать?", text: $title, axis: .vertical) + .lineLimit(1...3).foregroundColor(.white).padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07))) + } + VStack(alignment: .leading, spacing: 8) { + Label("Описание", systemImage: "text.alignleft").font(.caption).foregroundColor(Color(hex: "8888aa")) + TextField("Детали...", text: $description, axis: .vertical) + .lineLimit(2...4).foregroundColor(.white).padding(14) + .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07))) + } + VStack(alignment: .leading, spacing: 8) { + Label("Приоритет", systemImage: "flag.fill").font(.caption).foregroundColor(Color(hex: "8888aa")) + HStack(spacing: 8) { + ForEach(priorities, id: \.0) { p in + Button(action: { priority = p.0 }) { + Text(p.1).font(.caption.bold()) + .foregroundColor(priority == p.0 ? .black : Color(hex: p.2)) + .padding(.horizontal, 12).padding(.vertical, 8) + .background(RoundedRectangle(cornerRadius: 20).fill(priority == p.0 ? Color(hex: p.2) : Color(hex: p.2).opacity(0.15))) + } + } + } + } + VStack(alignment: .leading, spacing: 8) { + HStack { + Label("Срок выполнения", systemImage: "calendar").font(.caption).foregroundColor(Color(hex: "8888aa")) + Spacer() + Toggle("", isOn: $hasDueDate).tint(Color(hex: "0D9488")).labelsHidden() + } + if hasDueDate { + DatePicker("", selection: $dueDate, in: Date()..., displayedComponents: .date) + .datePickerStyle(.compact) + .colorInvert() + .colorMultiply(Color(hex: "0D9488")) + } + } + VStack(alignment: .leading, spacing: 8) { + Label("Иконка", systemImage: "face.smiling").font(.caption).foregroundColor(Color(hex: "8888aa")) + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 5), spacing: 8) { + ForEach(icons, id: \.self) { icon in + Button(action: { selectedIcon = icon }) { + Text(icon).font(.title3) + .frame(width: 44, height: 44) + .background(Circle().fill(selectedIcon == icon ? Color(hex: "0D9488").opacity(0.25) : Color.white.opacity(0.05))) + .overlay(Circle().stroke(selectedIcon == icon ? Color(hex: "0D9488") : Color.clear, lineWidth: 2)) + } + } + } + } + VStack(alignment: .leading, spacing: 8) { + Label("Цвет", systemImage: "paintpalette").font(.caption).foregroundColor(Color(hex: "8888aa")) + HStack(spacing: 10) { + ForEach(colors, id: \.self) { c in + Button(action: { selectedColor = c }) { + Circle().fill(Color(hex: String(c.dropFirst()))).frame(width: 32, height: 32) + .overlay(Circle().stroke(.white, lineWidth: selectedColor == c ? 2 : 0)) + .scaleEffect(selectedColor == c ? 1.15 : 1.0) + } + } + } + } + }.padding(20) + } + } + } + } + + func save() { + isLoading = true + let df = DateFormatter(); df.dateFormat = "yyyy-MM-dd" + let dueDateStr = hasDueDate ? df.string(from: dueDate) : nil + Task { + let req = UpdateTaskRequest( + title: title, + description: description.isEmpty ? nil : description, + priority: priority, + dueDate: dueDateStr, + completed: nil + ) + try? await APIService.shared.updateTask(token: authManager.token, id: task.id, request: req) + await onSaved() + await MainActor.run { isPresented = false } + } + } +} diff --git a/PulseHealth/Views/Tracker/TrackerView.swift b/PulseHealth/Views/Tracker/TrackerView.swift index 6229b0a..08c81cd 100644 --- a/PulseHealth/Views/Tracker/TrackerView.swift +++ b/PulseHealth/Views/Tracker/TrackerView.swift @@ -48,6 +48,7 @@ struct HabitListView: View { @State private var showArchived = false @State private var errorMsg: String? @State private var showError = false + @State private var editingHabit: Habit? = nil var activeHabits: [Habit] { habits.filter { $0.isArchived != true } } var archivedHabits: [Habit] { habits.filter { $0.isArchived == true } } @@ -67,6 +68,7 @@ struct HabitListView: View { .listRowBackground(Color.clear) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 3, leading: 16, bottom: 3, trailing: 16)) + .onTapGesture { editingHabit = habit } } .onDelete { idx in let toDelete = idx.map { activeHabits[$0] } @@ -113,6 +115,12 @@ struct HabitListView: View { .presentationDragIndicator(.visible) .presentationBackground(Color(hex: "0a0a1a")) } + .sheet(item: ) { habit in + EditHabitView(isPresented: .constant(true), habit: habit) { await loadHabits(refresh: true) } + .presentationDetents([.large]) + .presentationDragIndicator(.visible) + .presentationBackground(Color(hex: 0a0a1a)) + } .alert("Ошибка", isPresented: $showError) { Button("OK", role: .cancel) {} } message: { Text(errorMsg ?? "") } } @@ -136,6 +144,12 @@ struct HabitListView: View { try await APIService.shared.logHabit(token: authManager.token, id: habit.id) } await loadHabits(refresh: true) + } catch APIError.serverError(let code, _) where code == 409 { + await MainActor.run { + if let idx = habits.firstIndex(where: { /bin/bash.id == habit.id }) { + habits[idx].completedToday = true + } + } } catch { errorMsg = error.localizedDescription; showError = true } } @@ -213,6 +227,7 @@ struct TaskListView: View { @State private var showAddTask = false @State private var errorMsg: String? @State private var showError = false + @State private var editingTask: PulseTask? = nil enum TaskFilter: String, CaseIterable { case active = "Активные" @@ -248,6 +263,7 @@ struct TaskListView: View { .listRowBackground(Color.clear) .listRowSeparator(.hidden) .listRowInsets(EdgeInsets(top: 2, leading: 16, bottom: 2, trailing: 16)) + .onTapGesture { editingTask = task } } .onDelete { idx in let toDelete = idx.map { filtered[$0] } @@ -282,6 +298,12 @@ struct TaskListView: View { .presentationDragIndicator(.visible) .presentationBackground(Color(hex: "0a0a1a")) } + .sheet(item: ) { task in + EditTaskView(isPresented: .constant(true), task: task) { await loadTasks(refresh: true) } + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.visible) + .presentationBackground(Color(hex: 0a0a1a)) + } .alert("Ошибка", isPresented: $showError) { Button("OK", role: .cancel) {} } message: { Text(errorMsg ?? "") } }