import SwiftUI import MapKit import CoreLocation struct GPXImportSheet: View { let track: GPXTrack let onDismiss: () -> Void @Environment(\.dismiss) private var dismiss @State private var name: String @State private var dogs: [Dog] = [] @State private var selectedDogIds: Set = [] @State private var isPublic: Bool = false @State private var isSaving = false @State private var errorMessage: String? @State private var saved: Bool = false init(track: GPXTrack, onDismiss: @escaping () -> Void) { self.track = track self.onDismiss = onDismiss _name = State(initialValue: track.name?.isEmpty == false ? (track.name ?? "Importierte Tour") : "Importierte Tour") } var body: some View { NavigationStack { ScrollView { VStack(alignment: .leading, spacing: 16) { mapView summaryCard importCard viewOnlyCard if let errorMessage { Text(errorMessage) .font(.footnote) .foregroundStyle(.red) } } .padding() } .navigationTitle("GPX importieren") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Schließen") { dismiss() onDismiss() } } } .task { dogs = (try? await APIClient.shared.get("/api/dogs")) ?? [] } } } // MARK: - Map private var mapView: some View { Map { MapPolyline(coordinates: track.coordinates) .stroke(Color.accentColor, lineWidth: 4) if let first = track.coordinates.first { Annotation("Start", coordinate: first) { Image(systemName: "flag.circle.fill") .font(.title2) .foregroundStyle(.white, .green) .background(.white, in: Circle()) } } if let last = track.coordinates.last, track.coordinates.count > 1 { Annotation("Ziel", coordinate: last) { Image(systemName: "flag.checkered.circle.fill") .font(.title2) .foregroundStyle(.white, .red) .background(.white, in: Circle()) } } } .mapStyle(.standard(elevation: .realistic)) .frame(height: 220) .clipShape(RoundedRectangle(cornerRadius: 16)) .allowsHitTesting(false) } // MARK: - Summary private var summaryCard: some View { HStack(spacing: 16) { stat("Distanz", value: distanceLabel, icon: "ruler") Divider() stat("Punkte", value: "\(track.points.count)", icon: "point.3.connected.trianglepath.dotted") if let dur = durationLabel { Divider() stat("Dauer", value: dur, icon: "clock") } } .padding(14) .background(.background.secondary, in: RoundedRectangle(cornerRadius: 12)) } private func stat(_ title: String, value: String, icon: String) -> some View { VStack(spacing: 4) { Image(systemName: icon).foregroundStyle(Color.accentColor) Text(value).font(.headline.monospacedDigit()) Text(title).font(.caption2).foregroundStyle(.secondary) } .frame(maxWidth: .infinity) } // MARK: - Import (save as route) private var importCard: some View { VStack(alignment: .leading, spacing: 12) { Label("Als Tour übernehmen", systemImage: "square.and.arrow.down.fill") .font(.headline) TextField("Name", text: $name) .textFieldStyle(.roundedBorder) if !dogs.isEmpty { Text("Hunde").font(.caption).foregroundStyle(.secondary) FlowDogs(dogs: dogs, selected: $selectedDogIds) } Toggle(isOn: $isPublic) { Label("Öffentlich teilen", systemImage: "globe") } .font(.subheadline) Button { Task { await saveAsRoute() } } label: { HStack { if isSaving { ProgressView() } Image(systemName: saved ? "checkmark.circle.fill" : "square.and.arrow.down") Text(saved ? "Gespeichert" : "Speichern") .bold() } .frame(maxWidth: .infinity, minHeight: 44) } .background((saved ? Color.green : Color.accentColor).opacity(isSaving ? 0.6 : 1), in: Capsule()) .foregroundStyle(.white) .disabled(isSaving || saved || name.trimmingCharacters(in: .whitespaces).isEmpty) } .padding(14) .background(.background.secondary, in: RoundedRectangle(cornerRadius: 12)) } // MARK: - View only private var viewOnlyCard: some View { VStack(alignment: .leading, spacing: 12) { Label("Nur ansehen", systemImage: "eye") .font(.headline) Text("Den Track in Apple Maps öffnen und dort z. B. eine Route zum Startpunkt planen.") .font(.caption) .foregroundStyle(.secondary) Button { openStartInMaps() } label: { HStack { Image(systemName: "arrow.triangle.turn.up.right.diamond.fill") Text("Startpunkt in Apple Maps").bold() } .frame(maxWidth: .infinity, minHeight: 44) } .background(.thinMaterial, in: Capsule()) .foregroundStyle(.primary) } .padding(14) .background(.background.secondary, in: RoundedRectangle(cornerRadius: 12)) } // MARK: - Actions private func saveAsRoute() async { isSaving = true errorMessage = nil defer { isSaving = false } let dist = track.distanceMeters / 1000.0 let durMin: Int = { if let s = track.durationSeconds { return Int(s / 60.0) } return 0 }() let body = RouteCreateBody( name: name.trimmingCharacters(in: .whitespacesAndNewlines), gpsTrack: track.points.map { GPSPoint(lat: $0.lat, lon: $0.lon, alt: $0.ele) }, distanzKm: dist, dauerMin: durMin, dogIds: Array(selectedDogIds), isPublic: isPublic ) do { let _: RouteDetail = try await APIClient.shared.post("/api/routes", body: body) saved = true } catch { errorMessage = error.localizedDescription } } private func openStartInMaps() { guard let start = track.coordinates.first else { return } let item = MKMapItem(placemark: MKPlacemark(coordinate: start)) item.name = name item.openInMaps(launchOptions: nil) } // MARK: - Labels private var distanceLabel: String { let km = track.distanceMeters / 1000.0 return km < 1 ? "\(Int(track.distanceMeters)) m" : String(format: "%.1f km", km) } private var durationLabel: String? { guard let s = track.durationSeconds else { return nil } let h = Int(s) / 3600 let m = (Int(s) % 3600) / 60 if h > 0 { return "\(h):\(String(format: "%02d", m)) h" } return "\(m) min" } } private struct FlowDogs: View { let dogs: [Dog] @Binding var selected: Set var body: some View { let cols = [GridItem(.adaptive(minimum: 110))] LazyVGrid(columns: cols, alignment: .leading, spacing: 8) { ForEach(dogs) { d in let on = selected.contains(d.id) Button { if on { selected.remove(d.id) } else { selected.insert(d.id) } } label: { HStack(spacing: 6) { Image(systemName: on ? "checkmark.circle.fill" : "circle") Text(d.name).lineLimit(1) } .font(.caption.bold()) .padding(.horizontal, 10) .padding(.vertical, 6) .background(on ? Color.accentColor.opacity(0.2) : Color.secondary.opacity(0.12), in: Capsule()) .foregroundStyle(on ? Color.accentColor : .primary) } .buttonStyle(.plain) } } } }