import SwiftUI import UserNotifications /// Benachrichtigungs-Einstellungen eines Servers: Ein/Aus, Uhrzeit, letzte /// Warnung und die Erinnerungsregeln. struct NotificationSettingsView: View { let profileID: UUID let serverName: String @Environment(\.dismiss) private var dismiss @State private var config = ServerNotificationConfig() @State private var categories: [CategoryItem] = [] @State private var editingRule: NotificationRule? @State private var addShown = false @State private var berechtigungFehlt = false private var uhrzeit: Binding { Binding( get: { Calendar.current.date(from: DateComponents( hour: config.hour, minute: config.minute)) ?? Date() }, set: { neu in let teile = Calendar.current.dateComponents([.hour, .minute], from: neu) config.hour = teile.hour ?? 9 config.minute = teile.minute ?? 0 speichern() } ) } var body: some View { Form { Section { Toggle("Benachrichtigungen aktiv", isOn: Binding( get: { config.enabled }, set: { an in Task { await setzeAktiv(an) } } )) } footer: { Text("Erinnert vor dem Ablauf von Produkten auf „\(serverName)“. Nur auf diesem iPhone, unabhängig von den anderen Servern.") } if berechtigungFehlt { Section { Label("In den iOS-Einstellungen sind Mitteilungen für Vorrania aus. Ohne sie kommen keine Erinnerungen.", systemImage: "exclamationmark.triangle") .font(.callout).foregroundStyle(.orange) Button("iOS-Einstellungen öffnen") { if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) } } } } if config.enabled { Section("Uhrzeit der Meldung") { DatePicker("Täglich um", selection: uhrzeit, displayedComponents: .hourAndMinute) } Section { Toggle("Letzte Warnung am Ablauftag", isOn: Binding( get: { config.lastWarningOnExpiry }, set: { config.lastWarningOnExpiry = $0; speichern() } )) } footer: { Text("Am Tag des Ablaufs eine deutliche Warnung – für alle Produkte, unabhängig von den Regeln unten.") } Section { ForEach(config.rules) { regel in Button { editingRule = regel } label: { regelZeile(regel) } .foregroundStyle(.primary) } .onDelete { indexSet in config.rules.remove(atOffsets: indexSet) speichern() } Button { addShown = true } label: { Label("Regel hinzufügen", systemImage: "plus") } } header: { Text("Regeln") } footer: { Text("Jede Regel erinnert „X vor Ablauf“. Eine Regel für eine Kategorie gilt auch für ihre Unterkategorien und ersetzt für diese Produkte die allgemeine Regel.") } } } .navigationTitle("Benachrichtigungen") .navigationBarTitleDisplayMode(.inline) .task { config = NotificationStore.config(for: profileID) await ladeKategorien() await pruefeBerechtigung() } .sheet(isPresented: $addShown) { RuleEditView(rule: nil, categories: categories) { neu in config.rules.append(neu) speichern() } } .sheet(item: $editingRule) { regel in RuleEditView(rule: regel, categories: categories) { geaendert in if let index = config.rules.firstIndex(where: { $0.id == geaendert.id }) { config.rules[index] = geaendert speichern() } } } } private func regelZeile(_ regel: NotificationRule) -> some View { VStack(alignment: .leading, spacing: 2) { Text("\(regel.amount) \(regel.unit.label) vorher") Text("\(kategorieName(regel.categoryId)) · \(regel.recurring ? "täglich" : "einmalig")") .font(.caption).foregroundStyle(.secondary) } } private func kategorieName(_ id: Int?) -> String { guard let id else { return "Alle Kategorien" } return categories.first { $0.id == id }?.name ?? "Kategorie \(id)" } // MARK: - Aktionen private func setzeAktiv(_ an: Bool) async { if an { let erlaubt = await NotificationScheduler.shared.requestAuthorization() berechtigungFehlt = !erlaubt guard erlaubt else { return } } config.enabled = an speichern() } private func speichern() { NotificationStore.save(config, for: profileID) Task { await NotificationScheduler.shared.rescheduleAll() } } private func ladeKategorien() async { // Kategorien vom Server dieses Profils - auch wenn es nicht der aktive ist. guard let profile = Session.shared.profiles.first(where: { $0.id == profileID }), let url = profile.url, let token = Keychain.read(account: Session.keychainAccount(for: profileID)) else { return } categories = (try? await ServerFetch.categories(baseURL: url, token: token)) ?? [] } private func pruefeBerechtigung() async { guard config.enabled else { berechtigungFehlt = false; return } let status = await UNUserNotificationCenter.current().notificationSettings().authorizationStatus berechtigungFehlt = !(status == .authorized || status == .provisional || status == .ephemeral) } } /// Anlegen und Bearbeiten einer Regel. struct RuleEditView: View { let rule: NotificationRule? let categories: [CategoryItem] let onSave: (NotificationRule) -> Void @Environment(\.dismiss) private var dismiss @State private var amount: Int @State private var unit: LeadUnit @State private var categoryId: Int? @State private var recurring: Bool init(rule: NotificationRule?, categories: [CategoryItem], onSave: @escaping (NotificationRule) -> Void) { self.rule = rule self.categories = categories self.onSave = onSave _amount = State(initialValue: rule?.amount ?? 3) _unit = State(initialValue: rule?.unit ?? .days) _categoryId = State(initialValue: rule?.categoryId) _recurring = State(initialValue: rule?.recurring ?? false) } var body: some View { NavigationStack { Form { Section("Vorlauf") { Stepper("\(amount) \(unit.label)", value: $amount, in: 1...99) Picker("Einheit", selection: $unit) { ForEach(LeadUnit.allCases, id: \.self) { Text($0.label).tag($0) } } .pickerStyle(.segmented) } Section { CategoryPicker(categories: categories, selection: $categoryId) } header: { Text("Kategorie") } footer: { Text("„Alle Kategorien“ ist die allgemeine Regel. Eine bestimmte Kategorie schließt ihre Unterkategorien ein.") } Section { Toggle("Täglich wiederholen", isOn: $recurring) } footer: { Text(recurring ? "Erinnert jeden Tag ab „\(amount) \(unit.label) vorher“ bis zum Ablauf." : "Erinnert einmal, genau \(amount) \(unit.label) vor dem Ablauf.") } } .navigationTitle(rule == nil ? "Regel hinzufügen" : "Regel bearbeiten") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } } ToolbarItem(placement: .topBarTrailing) { Button("Sichern") { onSave(NotificationRule( id: rule?.id ?? UUID(), amount: amount, unit: unit, categoryId: categoryId, recurring: recurring)) dismiss() } } } } } }