import SwiftUI import UserNotifications struct StammGassisList: View { @State private var items: [GassiZeit] = [] @State private var isLoading = false @State private var errorMessage: String? @State private var showAdd = false private let weekdayLabels: [String: String] = [ "mo": "Mo", "di": "Di", "mi": "Mi", "do": "Do", "fr": "Fr", "sa": "Sa", "so": "So" ] private let weekdayOrder = ["mo", "di", "mi", "do", "fr", "sa", "so"] var body: some View { content .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { showAdd = true } label: { Image(systemName: "plus") } } } .sheet(isPresented: $showAdd) { AddGassiZeitSheet { await load() } } .task { _ = try? await UNUserNotificationCenter.current() .requestAuthorization(options: [.alert, .sound, .badge]) await load() } .refreshable { await load() } } @ViewBuilder private var content: some View { if isLoading && items.isEmpty { ProgressView() } else if let errorMessage, items.isEmpty { ContentUnavailableView("Konnte nicht laden", systemImage: "wifi.slash", description: Text(errorMessage)) } else if items.isEmpty { ContentUnavailableView( "Noch keine Stamm-Gassi-Zeiten", systemImage: "alarm", description: Text("Trag deine regelmäßigen Gassi-Runden ein — du bekommst lokale Erinnerungen, und in der banyaro.app sehen andere, wann ihr euch verabreden könnt.") ) } else { List { Section { ForEach(items) { z in row(z) } } footer: { Text("Deine Zeiten landen auch im Stamm-Gassi-Pool der Community (sichtbar in banyaro.app → Gassi). Erinnerungen kommen lokal vom iPhone — auch ohne Internet.") .font(.caption2) .foregroundStyle(.tertiary) } } } } private func row(_ z: GassiZeit) -> some View { HStack(spacing: 12) { Image(systemName: "alarm.fill") .foregroundStyle(z.aktiv == 0 ? Color.secondary : Color.accentColor) .frame(width: 28) VStack(alignment: .leading, spacing: 4) { HStack(spacing: 8) { Text(z.uhrzeit).font(.headline.monospacedDigit()) Spacer() if let dog = z.dogName { Text(dog).font(.caption).foregroundStyle(.secondary) } } HStack(spacing: 6) { ForEach(weekdayOrder, id: \.self) { wd in let active = z.wochentage.contains(wd) Text(weekdayLabels[wd] ?? wd) .font(.caption2.bold()) .foregroundStyle(active ? .white : .secondary) .frame(width: 22, height: 22) .background(active ? Color.accentColor : Color.secondary.opacity(0.15), in: Circle()) } } if let n = z.notiz, !n.isEmpty { Text(n).font(.caption).foregroundStyle(.secondary) } } } .padding(.vertical, 4) } private func load() async { isLoading = true errorMessage = nil defer { isLoading = false } do { items = try await APIClient.shared.get("/api/gassi-zeiten?nur_eigene=true") // Re-sync local notifications to reflect server state. for z in items where z.isMine ?? true { await GassiZeitenScheduler.reschedule(z) } } catch { errorMessage = error.localizedDescription } } } private struct AddGassiZeitSheet: View { @Environment(\.dismiss) private var dismiss let onSaved: () async -> Void @State private var time = Calendar.current.date(bySettingHour: 8, minute: 0, second: 0, of: .now) ?? .now @State private var selectedDays: Set = ["mo", "di", "mi", "do", "fr"] @State private var notiz = "" @State private var dogs: [Dog] = [] @State private var dogId: Int? @State private var isSaving = false @State private var errorMessage: String? private let weekdayOrder = ["mo", "di", "mi", "do", "fr", "sa", "so"] private let weekdayLabels: [String: String] = [ "mo": "Mo", "di": "Di", "mi": "Mi", "do": "Do", "fr": "Fr", "sa": "Sa", "so": "So" ] var body: some View { NavigationStack { Form { Section("Uhrzeit") { DatePicker("Uhrzeit", selection: $time, displayedComponents: .hourAndMinute) .environment(\.locale, Locale(identifier: "de_DE")) } Section("Wochentage") { HStack(spacing: 8) { ForEach(weekdayOrder, id: \.self) { wd in let active = selectedDays.contains(wd) Button { if active { selectedDays.remove(wd) } else { selectedDays.insert(wd) } } label: { Text(weekdayLabels[wd] ?? wd) .font(.caption.bold()) .foregroundStyle(active ? .white : .primary) .frame(width: 34, height: 34) .background(active ? Color.accentColor : Color.secondary.opacity(0.15), in: Circle()) } .buttonStyle(.plain) } } } Section("Hund (optional)") { Picker("Hund", selection: $dogId) { Text("Ohne Hund").tag(Int?.none) ForEach(dogs) { d in Text(d.name).tag(Int?.some(d.id)) } } } Section("Notiz (optional)") { TextField("Notiz", text: $notiz, axis: .vertical) .lineLimit(2...4) } if let errorMessage { Section { Text(errorMessage).font(.footnote).foregroundStyle(.red) } } } .navigationTitle("Neue Gassi-Zeit") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Abbrechen") { dismiss() }.disabled(isSaving) } ToolbarItem(placement: .confirmationAction) { if isSaving { ProgressView() } else { Button("Sichern") { Task { await save() } }.disabled(selectedDays.isEmpty) } } } .task { dogs = (try? await APIClient.shared.get("/api/dogs")) ?? [] } } } private func save() async { isSaving = true errorMessage = nil defer { isSaving = false } let f = DateFormatter() f.dateFormat = "HH:mm" let body = GassiZeitCreateBody( dogId: dogId, wochentage: weekdayOrder.filter { selectedDays.contains($0) }, uhrzeit: f.string(from: time), ortName: nil, lat: nil, lon: nil, radiusM: 500, notiz: notiz.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : notiz ) do { let created: GassiZeit = try await APIClient.shared.post("/api/gassi-zeiten", body: body) await GassiZeitenScheduler.reschedule(created) await onSaved() dismiss() } catch { errorMessage = error.localizedDescription } } }