import SwiftUI // MARK: - SettingsView struct SettingsView: View { @EnvironmentObject var authManager: AuthManager @AppStorage("colorScheme") private var colorSchemeRaw: String = "dark" @State private var profile: UserProfile? @State private var isLoading = true @State private var isSaving = false @State private var showPasswordChange = false // Profile fields @State private var telegramChatId = "" @State private var timezone = "Europe/Moscow" @State private var username = "" // Local notifications @AppStorage("notif_morning") private var morningNotif = false @AppStorage("notif_evening") private var eveningNotif = false @AppStorage("notif_morning_hour") private var morningHour = 8 @AppStorage("notif_morning_min") private var morningMin = 0 @AppStorage("notif_evening_hour") private var eveningHour = 21 @AppStorage("notif_evening_min") private var eveningMin = 0 @AppStorage("notif_payments") private var paymentNotif = true @State private var notifAuthorized = false @State private var morningDate = Calendar.current.date(from: DateComponents(hour: 8, minute: 0))! @State private var eveningDate = Calendar.current.date(from: DateComponents(hour: 21, minute: 0))! var isDark: Bool { colorSchemeRaw != "light" } let timezones = [ "Europe/Moscow", "Europe/Kaliningrad", "Europe/Samara", "Asia/Yekaterinburg", "Asia/Omsk", "Asia/Krasnoyarsk", "Asia/Irkutsk", "Asia/Yakutsk", "Asia/Vladivostok", "Asia/Magadan", "Asia/Kamchatka", "UTC", "Europe/London", "Europe/Berlin", "Europe/Paris", "America/New_York" ] var body: some View { ZStack { Color(hex: "06060f").ignoresSafeArea() ScrollView { VStack(spacing: 0) { // Header / Avatar VStack(spacing: 12) { ZStack { Circle().fill(Color(hex: "0D9488").opacity(0.2)).frame(width: 80, height: 80) Text(String(authManager.userName.prefix(1)).uppercased()) .font(.largeTitle.bold()).foregroundColor(Color(hex: "0D9488")) } Text(authManager.userName).font(.title2.bold()).foregroundColor(.white) Text("ID: \(authManager.userId)").font(.caption).foregroundColor(Color(hex: "8888aa")) } .padding(.top, 24) .padding(.bottom, 28) if isLoading { ProgressView().tint(Color(hex: "0D9488")).padding(.top, 20) } else { VStack(spacing: 20) { // MARK: Appearance SettingsSection(title: "Внешний вид") { SettingsToggle(icon: "moon.fill", title: isDark ? "Тёмная тема" : "Светлая тема", color: "6366f1", isOn: isDark) { colorSchemeRaw = isDark ? "light" : "dark" } } // MARK: Profile SettingsSection(title: "Профиль") { VStack(spacing: 8) { Label("Имя пользователя", systemImage: "person.fill") .font(.caption).foregroundColor(Color(hex: "8888aa")) .frame(maxWidth: .infinity, alignment: .leading) TextField("Username", text: $username) .foregroundColor(.white).padding(14) .background(RoundedRectangle(cornerRadius: 12).fill(Color.white.opacity(0.07))) } .padding(.horizontal, 4) SettingsButton(icon: "lock.fill", title: "Сменить пароль", color: "7c3aed") { showPasswordChange = true } } // MARK: Notifications SettingsSection(title: "Уведомления") { if !notifAuthorized { Button(action: { Task { notifAuthorized = await NotificationService.shared.requestPermission() } }) { HStack(spacing: 14) { GlowIcon(systemName: "bell.badge.fill", color: Theme.orange, size: 36, iconSize: .subheadline) VStack(alignment: .leading, spacing: 2) { Text("Включить уведомления").font(.callout).foregroundColor(.white) Text("Нажми, чтобы разрешить").font(.caption).foregroundColor(Theme.textSecondary) } Spacer() Image(systemName: "arrow.right.circle.fill").foregroundColor(Theme.teal) } } } else { VStack(spacing: 14) { // Morning NotifRow(icon: "sunrise.fill", title: "Утреннее", color: Theme.orange, isOn: $morningNotif, date: $morningDate) Divider().background(Color.white.opacity(0.06)) // Evening NotifRow(icon: "moon.stars.fill", title: "Вечернее", color: Theme.indigo, isOn: $eveningNotif, date: $eveningDate) Divider().background(Color.white.opacity(0.06)) // Payments HStack(spacing: 14) { GlowIcon(systemName: "creditcard.fill", color: Theme.purple, size: 36, iconSize: .subheadline) VStack(alignment: .leading, spacing: 2) { Text("Платежи").font(.callout).foregroundColor(.white) Text("За 5 дн, 1 день и в день оплаты").font(.caption2).foregroundColor(Theme.textSecondary) } Spacer() Toggle("", isOn: $paymentNotif).tint(Theme.teal).labelsHidden() } } .onChange(of: morningNotif) { applyNotifSchedule() } .onChange(of: eveningNotif) { applyNotifSchedule() } .onChange(of: morningDate) { saveTimes(); applyNotifSchedule() } .onChange(of: eveningDate) { saveTimes(); applyNotifSchedule() } .onChange(of: paymentNotif) { Task { await schedulePaymentNotifs() } } } } // MARK: Timezone SettingsSection(title: "Часовой пояс") { HStack(spacing: 14) { GlowIcon(systemName: "clock.fill", color: Theme.blue, size: 36, iconSize: .subheadline) VStack(alignment: .leading, spacing: 2) { Text("Часовой пояс").font(.callout).foregroundColor(.white) Text(timezoneDisplay(timezone)).font(.caption).foregroundColor(Theme.textSecondary) } Spacer() Menu { ForEach(timezones, id: \.self) { tz in Button(action: { timezone = tz }) { HStack { Text(timezoneDisplay(tz)) if timezone == tz { Image(systemName: "checkmark") } } } } } label: { HStack(spacing: 4) { Text(timezoneShort(timezone)).font(.callout.bold()).foregroundColor(Theme.teal) Image(systemName: "chevron.up.chevron.down").font(.caption2).foregroundColor(Theme.teal) } .padding(.horizontal, 12).padding(.vertical, 8) .background(RoundedRectangle(cornerRadius: 10).fill(Theme.teal.opacity(0.15))) } } } // MARK: Save Button Button(action: { Task { await saveProfile() } }) { HStack { if isSaving { ProgressView().tint(.black).scaleEffect(0.8) } else { Text("Сохранить изменения").font(.headline).foregroundColor(.black) } } .frame(maxWidth: .infinity).padding() .background(LinearGradient(colors: [Color(hex: "0D9488"), Color(hex: "14b8a6")], startPoint: .leading, endPoint: .trailing)) .cornerRadius(14) } .disabled(isSaving) .padding(.horizontal) // MARK: Logout Button(action: { authManager.logout() }) { HStack { Image(systemName: "rectangle.portrait.and.arrow.right") Text("Выйти из аккаунта") } .font(.callout.bold()) .foregroundColor(Color(hex: "ff4757")) .frame(maxWidth: .infinity).padding() .background(RoundedRectangle(cornerRadius: 14).fill(Color(hex: "ff4757").opacity(0.1))) .overlay(RoundedRectangle(cornerRadius: 14).stroke(Color(hex: "ff4757").opacity(0.3), lineWidth: 1)) } .padding(.horizontal) Text("Pulse v1.1 • Made with ❤️").font(.caption).foregroundColor(Color(hex: "8888aa")) .padding(.bottom, 20) } } } } } .task { await loadProfile() } .sheet(isPresented: $showPasswordChange) { ChangePasswordView(isPresented: $showPasswordChange) .presentationDetents([.medium]) .presentationDragIndicator(.visible) .presentationBackground(Color(hex: "06060f")) } } func loadProfile() async { isLoading = true username = authManager.userName notifAuthorized = await NotificationService.shared.isAuthorized() morningDate = Calendar.current.date(from: DateComponents(hour: morningHour, minute: morningMin)) ?? morningDate eveningDate = Calendar.current.date(from: DateComponents(hour: eveningHour, minute: eveningMin)) ?? eveningDate if let p = try? await APIService.shared.getProfile(token: authManager.token) { profile = p telegramChatId = p.telegramChatId ?? "" timezone = p.timezone ?? "Europe/Moscow" } isLoading = false } func saveProfile() async { isSaving = true let req = UpdateProfileRequest( telegramChatId: telegramChatId.isEmpty ? nil : telegramChatId, timezone: timezone ) _ = try? await APIService.shared.updateProfile(token: authManager.token, request: req) // Update username if changed if username != authManager.userName { var req2 = URLRequest(url: URL(string: "https://api.digital-home.site/auth/me")!) req2.httpMethod = "PUT" req2.setValue("application/json", forHTTPHeaderField: "Content-Type") req2.setValue("Bearer \(authManager.token)", forHTTPHeaderField: "Authorization") req2.httpBody = try? JSONEncoder().encode(["username": username]) _ = try? await URLSession.shared.data(for: req2) await MainActor.run { authManager.userName = username; UserDefaults.standard.set(username, forKey: "userName") } } isSaving = false } func applyNotifSchedule() { let cal = Calendar.current let mh = cal.component(.hour, from: morningDate) let mm = cal.component(.minute, from: morningDate) let eh = cal.component(.hour, from: eveningDate) let em = cal.component(.minute, from: eveningDate) NotificationService.shared.updateSchedule( morning: morningNotif, morningTime: "\(mh):\(String(format: "%02d", mm))", evening: eveningNotif, eveningTime: "\(eh):\(String(format: "%02d", em))" ) } func saveTimes() { let cal = Calendar.current morningHour = cal.component(.hour, from: morningDate) morningMin = cal.component(.minute, from: morningDate) eveningHour = cal.component(.hour, from: eveningDate) eveningMin = cal.component(.minute, from: eveningDate) } func schedulePaymentNotifs() async { guard paymentNotif else { NotificationService.shared.cancelPaymentReminders() return } let stats = try? await APIService.shared.getSavingsStats(token: authManager.token) guard let details = stats?.monthlyPaymentDetails else { return } NotificationService.shared.schedulePaymentReminders(payments: details) } func timezoneDisplay(_ tz: String) -> String { guard let zone = TimeZone(identifier: tz) else { return tz } let offset = zone.secondsFromGMT() / 3600 let sign = offset >= 0 ? "+" : "" let city = tz.split(separator: "/").last?.replacingOccurrences(of: "_", with: " ") ?? tz return "\(city) (UTC\(sign)\(offset))" } func timezoneShort(_ tz: String) -> String { guard let zone = TimeZone(identifier: tz) else { return tz } let offset = zone.secondsFromGMT() / 3600 let sign = offset >= 0 ? "+" : "" return "UTC\(sign)\(offset)" } } // MARK: - SettingsSection struct SettingsSection: View { let title: String @ViewBuilder let content: () -> Content var body: some View { VStack(alignment: .leading, spacing: 12) { Text(title).font(.subheadline.bold()).foregroundColor(Color(hex: "8888aa")).padding(.horizontal) VStack(spacing: 8) { content() } .padding(16) .glassCard(cornerRadius: 16) .padding(.horizontal) } } } // MARK: - SettingsToggle struct SettingsToggle: View { let icon: String let title: String let color: String let isOn: Bool let onToggle: () -> Void var body: some View { HStack(spacing: 14) { GlowIcon(systemName: icon, color: Color(hex: color), size: 36, iconSize: .subheadline) Text(title).font(.callout).foregroundColor(.white) Spacer() Toggle("", isOn: Binding(get: { isOn }, set: { _ in onToggle() })) .tint(Theme.teal) } } } // MARK: - SettingsButton struct SettingsButton: View { let icon: String let title: String let color: String let action: () -> Void var body: some View { Button(action: action) { HStack(spacing: 14) { GlowIcon(systemName: icon, color: Color(hex: color), size: 36, iconSize: .subheadline) Text(title).font(.callout).foregroundColor(.white) Spacer() Image(systemName: "chevron.right").foregroundColor(Theme.textSecondary).font(.caption) } } } } // MARK: - NotifRow struct NotifRow: View { let icon: String let title: String let color: Color @Binding var isOn: Bool @Binding var date: Date var body: some View { VStack(spacing: 8) { HStack(spacing: 14) { GlowIcon(systemName: icon, color: color, size: 36, iconSize: .subheadline) Text(title).font(.callout).foregroundColor(.white) Spacer() Toggle("", isOn: $isOn).tint(Theme.teal).labelsHidden() } if isOn { DatePicker("", selection: $date, displayedComponents: .hourAndMinute) .datePickerStyle(.wheel) .labelsHidden() .frame(height: 100) .clipped() .environment(\.colorScheme, .dark) } } } }