Gassi: Tabs Treffen + Stamm-Gassis wie in der PWA
In der PWA ist die Seite 'Gassi-Treffen' mit drei Tabs:
- Treffen (walks.py — sich verabreden)
- Challenge (Monatsfoto)
- Stamm-Gassis (gassi_zeiten.py — regelmäßige Runden)
Mein bisheriger Mehr-Eintrag hieß 'Stamm-Gassi-Zeiten' und zeigte nur die
Stamm-Gassi-Funktion isoliert — das stimmte nicht mit der PWA überein.
Neu:
- GassiView mit Segmented Picker (Treffen / Stamm-Gassis)
- GassiTreffenList: GET /api/walks?lat&lon&radius=20000, Liste mit Datum,
Uhrzeit, Ort, Teilnehmer-Zahl
- GassiTreffenDetail: Karte mit Pin, Stats, Beitreten/Verlassen
(POST/DELETE /api/walks/{id}/join), Owner-Check
- AddWalkSheet: Titel, Datum, Uhrzeit, Treffpunkt-Name, Max-Teilnehmer,
Beschreibung — POST /api/walks
- StammGassisList = bisherige GassiZeitenView umbenannt + Nav-Title raus
(wird vom GassiView vergeben)
Im Mehr-Tab heißt der Link jetzt 'Gassi-Treffen' (pawprint-Icon) statt
'Stamm-Gassi-Zeiten' (alarm-Icon).
DTOs: WalkMeeting, WalkCreateBody, WalkJoinBody.
This commit is contained in:
parent
5c4754caff
commit
b49883ca79
9 changed files with 684 additions and 5 deletions
107
BanYaroGo/Views/AddWalkSheet.swift
Normal file
107
BanYaroGo/Views/AddWalkSheet.swift
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
struct AddWalkSheet: View {
|
||||
let coord: CLLocationCoordinate2D?
|
||||
let onSaved: () async -> Void
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var titel = ""
|
||||
@State private var date = Calendar.current.date(byAdding: .day, value: 1, to: .now) ?? .now
|
||||
@State private var time = Calendar.current.date(bySettingHour: 17, minute: 0, second: 0, of: .now) ?? .now
|
||||
@State private var ortName = ""
|
||||
@State private var maxTeilnehmer = 10
|
||||
@State private var beschreibung = ""
|
||||
|
||||
@State private var isSaving = false
|
||||
@State private var errorMessage: String?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("Titel") {
|
||||
TextField("z. B. Gassi-Runde am Schlosspark", text: $titel)
|
||||
}
|
||||
Section("Datum & Uhrzeit") {
|
||||
DatePicker("Datum", selection: $date, in: Date.now..., displayedComponents: .date)
|
||||
.environment(\.locale, Locale(identifier: "de_DE"))
|
||||
DatePicker("Uhrzeit", selection: $time, displayedComponents: .hourAndMinute)
|
||||
.environment(\.locale, Locale(identifier: "de_DE"))
|
||||
}
|
||||
Section("Treffpunkt") {
|
||||
TextField("Name (z. B. Hauptplatz)", text: $ortName)
|
||||
if let coord {
|
||||
Text(String(format: "%.5f, %.5f", coord.latitude, coord.longitude))
|
||||
.font(.caption.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("Standort wird noch geholt — bitte einen Moment warten.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Section("Max. Teilnehmer") {
|
||||
Stepper("\(maxTeilnehmer)", value: $maxTeilnehmer, in: 2...100)
|
||||
}
|
||||
Section("Beschreibung (optional)") {
|
||||
TextField("Was ist geplant? Wie lange?", text: $beschreibung, axis: .vertical)
|
||||
.lineLimit(2...6)
|
||||
}
|
||||
if let errorMessage {
|
||||
Section {
|
||||
Text(errorMessage).font(.footnote).foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Neues Treffen")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Abbrechen") { dismiss() }
|
||||
.disabled(isSaving)
|
||||
}
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
if isSaving { ProgressView() }
|
||||
else {
|
||||
Button("Sichern") { Task { await save() } }
|
||||
.disabled(!canSave)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var canSave: Bool {
|
||||
coord != nil && !titel.trimmingCharacters(in: .whitespaces).isEmpty
|
||||
}
|
||||
|
||||
private func save() async {
|
||||
guard let coord else { return }
|
||||
isSaving = true
|
||||
errorMessage = nil
|
||||
defer { isSaving = false }
|
||||
|
||||
let dateFmt = DateFormatter(); dateFmt.dateFormat = "yyyy-MM-dd"
|
||||
let timeFmt = DateFormatter(); timeFmt.dateFormat = "HH:mm"
|
||||
|
||||
let body = WalkCreateBody(
|
||||
titel: titel.trimmingCharacters(in: .whitespaces),
|
||||
datum: dateFmt.string(from: date),
|
||||
uhrzeit: timeFmt.string(from: time),
|
||||
lat: coord.latitude,
|
||||
lon: coord.longitude,
|
||||
ortName: ortName.trimmingCharacters(in: .whitespaces).isEmpty ? nil : ortName,
|
||||
maxTeilnehmer: maxTeilnehmer,
|
||||
beschreibung: beschreibung.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : beschreibung
|
||||
)
|
||||
|
||||
do {
|
||||
let _: WalkMeeting = try await APIClient.shared.post("/api/walks", body: body)
|
||||
await onSaved()
|
||||
dismiss()
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue