import SwiftUI import MapKit struct GassiTreffenDetail: View { let meetingId: Int let fallbackTitle: String let onChange: () async -> Void @Environment(AuthSession.self) private var auth @Environment(\.dismiss) private var dismiss @State private var meeting: WalkMeeting? @State private var isLoading = false @State private var errorMessage: String? @State private var isMutating = false @State private var dogs: [Dog] = [] @State private var hasJoined: Bool? = nil var body: some View { ScrollView { VStack(alignment: .leading, spacing: 16) { if let meeting { detailContent(meeting) } else if isLoading { ProgressView().padding(.top, 60) } else if let errorMessage { ContentUnavailableView( "Fehler", systemImage: "exclamationmark.triangle", description: Text(errorMessage) ) .padding(.top, 60) } } .padding() } .navigationTitle(meeting?.titel ?? fallbackTitle) .navigationBarTitleDisplayMode(.inline) .task { await load() } } @ViewBuilder private func detailContent(_ m: WalkMeeting) -> some View { Map(initialPosition: .region(MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: m.lat, longitude: m.lon), span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02) ))) { Annotation(m.titel, coordinate: CLLocationCoordinate2D(latitude: m.lat, longitude: m.lon)) { Image(systemName: "pawprint.circle.fill") .font(.title) .foregroundStyle(.white, Color.accentColor) .background(.white, in: Circle()) } } .frame(height: 220) .clipShape(RoundedRectangle(cornerRadius: 12)) .allowsHitTesting(false) VStack(alignment: .leading, spacing: 6) { HStack { Image(systemName: "calendar") Text(formatDate(m.datum)) Text(m.uhrzeit).monospacedDigit() } .font(.headline) if let ort = m.ortName, !ort.isEmpty { Label(ort, systemImage: "mappin.and.ellipse") .font(.subheadline) .foregroundStyle(.secondary) } HStack { Label("\(m.teilnehmerCount ?? 0) / \(m.maxTeilnehmer) Teilnehmer", systemImage: "person.2.fill") if let veranstalter = m.veranstalterName { Spacer() Text("von \(veranstalter)") .font(.caption) .foregroundStyle(.secondary) } } .font(.subheadline) .padding(.top, 4) } if let beschreibung = m.beschreibung, !beschreibung.isEmpty { Text(beschreibung) .font(.body) .padding(.top, 4) } if !isOwn(m) { joinButton(m) } if let errorMessage { Text(errorMessage) .font(.footnote) .foregroundStyle(.red) } } private func joinButton(_ m: WalkMeeting) -> some View { Button { Task { await toggleJoin() } } label: { HStack { Image(systemName: (hasJoined == true) ? "person.crop.circle.badge.minus" : "person.crop.circle.badge.plus") Text((hasJoined == true) ? "Verlassen" : "Beitreten").bold() } .frame(maxWidth: .infinity, minHeight: 50) } .background((hasJoined == true) ? Color.red : Color.accentColor, in: Capsule()) .foregroundStyle(.white) .disabled(isMutating) } private func isOwn(_ m: WalkMeeting) -> Bool { m.userId == auth.profile?.id } private func load() async { isLoading = true errorMessage = nil defer { isLoading = false } do { meeting = try await APIClient.shared.get("/api/walks/\(meetingId)") if dogs.isEmpty { dogs = (try? await APIClient.shared.get("/api/dogs")) ?? [] } // We don't have a clean "have I joined" endpoint, so we treat hasJoined // as nil → button shows "Beitreten". After successful join we set true, // after successful leave we set false. } catch { errorMessage = error.localizedDescription } } private func toggleJoin() async { isMutating = true errorMessage = nil defer { isMutating = false } do { if hasJoined == true { try await APIClient.shared.delete("/api/walks/\(meetingId)/join") hasJoined = false } else { let body = WalkJoinBody(dogIds: dogs.map { $0.id }) let _: WalkMeeting = try await APIClient.shared.post("/api/walks/\(meetingId)/join", body: body) hasJoined = true } await onChange() await load() } catch { errorMessage = error.localizedDescription } } private func formatDate(_ s: String) -> String { let parser = DateFormatter() parser.locale = Locale(identifier: "en_US_POSIX") parser.dateFormat = "yyyy-MM-dd" if let d = parser.date(from: String(s.prefix(10))) { let out = DateFormatter() out.locale = Locale(identifier: "de_DE") out.dateStyle = .full return out.string(from: d) } return s } }