banyaro-ios/BanYaroGo/Views/GassiZeitenView.swift
rene 0867a2171f Statistik weg, Mehr entrümpelt, Gassi-Zeiten korrekt gerahmt
- Statistik-Tab raus (für Go-Companion nicht relevant)
- Mehr-Duplikate raus: Meine Hunde, Tagebuch, Wetter, Erste Hilfe sitzen
  bereits auf Heim als Quick-Action bzw. im Dog-Picker
- Im PWA ist 'Gassi' der social walks-Bereich (walks.py) und 'Stamm-Gassi-
  Zeiten' nur ein Tab darin (Community-Pool, gassi_zeiten.py). Meine
  Implementierung als 'tägliche Erinnerungen' war fachlich falsch:
  + Mehr-Eintrag heißt jetzt 'Stamm-Gassi-Zeiten'
  + ContentUnavailableView + Footer erklären die Community-Komponente
  + Pitch-Karte unterscheidet jetzt klar: 'Gassi-Treffen' (sich verabreden)
    und 'Stamm-Gassi-Zeiten' (regelmäßige Runden + Pool)
  + 'Hunde-Orte' getrennt als eigener Pitch-Punkt
2026-05-30 13:04:35 +02:00

207 lines
8.1 KiB
Swift

import SwiftUI
import UserNotifications
struct GassiZeitenView: 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
.navigationTitle("Stamm-Gassi-Zeiten")
.navigationBarTitleDisplayMode(.inline)
.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<String> = ["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
}
}
}