import SwiftUI import PhotosUI struct FinishWalkSheet: View { let points: [GPSPoint] let durationSeconds: Int let distanceMeters: Double let initialPhotos: [Data] let onDiscard: () -> Void let onSaved: () -> Void @Environment(\.dismiss) private var dismiss @State private var name: String @State private var selectedDogIds: Set = [] @State private var dogs: [Dog] = [] @State private var isLoadingDogs = false @State private var photoSelection: [PhotosPickerItem] = [] @State private var photoData: [Data] = [] @State private var loadingPhotos = false @State private var saveState: SaveState = .idle @State private var errorMessage: String? private enum SaveState: Equatable { case idle case savingRoute case uploadingPhotos(done: Int, total: Int) } private let shortDistanceThreshold: Double = 50 // meters init( points: [GPSPoint], durationSeconds: Int, distanceMeters: Double, initialPhotos: [Data] = [], onDiscard: @escaping () -> Void, onSaved: @escaping () -> Void ) { self.points = points self.durationSeconds = durationSeconds self.distanceMeters = distanceMeters self.initialPhotos = initialPhotos self.onDiscard = onDiscard self.onSaved = onSaved let formatter = DateFormatter() formatter.locale = Locale(identifier: "de_DE") formatter.dateStyle = .medium _name = State(initialValue: "Gassi am \(formatter.string(from: .now))") _photoData = State(initialValue: initialPhotos) } var body: some View { NavigationStack { Form { if distanceMeters < shortDistanceThreshold { shortDistanceWarning } Section("Stats") { LabeledContent("Distanz", value: String(format: "%.2f km", distanceMeters / 1000)) LabeledContent("Dauer", value: durationLabel) LabeledContent("Punkte", value: "\(points.count)") } Section("Name") { TextField("Name der Tour", text: $name) } Section("Hunde") { if isLoadingDogs && dogs.isEmpty { HStack { ProgressView(); Text("Lade Hunde…") } } else if dogs.isEmpty { Text("Keine Hunde gefunden. Wird gespeichert ohne Hund-Zuordnung.") .font(.footnote) .foregroundStyle(.secondary) } else { ForEach(dogs) { dog in dogRow(dog) } } } Section { PhotosPicker( selection: $photoSelection, maxSelectionCount: 10, matching: .images ) { Label( photoData.isEmpty ? "Fotos hinzufügen" : "Fotos ändern", systemImage: "photo.badge.plus" ) } if loadingPhotos { HStack { ProgressView(); Text("Lade Fotos…").font(.caption).foregroundStyle(.secondary) } } if !photoData.isEmpty { photoStrip } } header: { Text(photoData.isEmpty ? "Fotos" : "Fotos (\(photoData.count))") } if let errorMessage { Section { Text(errorMessage) .font(.footnote) .foregroundStyle(.red) } } } .navigationTitle("Tour speichern") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Verwerfen", role: .destructive) { onDiscard() dismiss() } .disabled(saveState != .idle) } ToolbarItem(placement: .confirmationAction) { saveToolbarItem } } .task { await loadDogs() } .onChange(of: photoSelection) { _, newItems in Task { await loadPhotos(from: newItems) } } .interactiveDismissDisabled(saveState != .idle) } } @ViewBuilder private var saveToolbarItem: some View { switch saveState { case .idle: Button("Speichern") { Task { await save() } } .disabled(canSave == false) case .savingRoute: ProgressView() case .uploadingPhotos(let done, let total): Text("\(done)/\(total)") .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) } } private var shortDistanceWarning: some View { Section { HStack(spacing: 10) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.orange) Text("Sehr kurze Tour (\(Int(distanceMeters)) m). Du kannst trotzdem speichern.") .font(.footnote) } } } private var photoStrip: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(Array(photoData.enumerated()), id: \.offset) { _, data in if let img = UIImage(data: data) { Image(uiImage: img) .resizable() .scaledToFill() .frame(width: 80, height: 80) .clipShape(RoundedRectangle(cornerRadius: 8)) } } } } } private func dogRow(_ dog: Dog) -> some View { let selected = selectedDogIds.contains(dog.id) return HStack { Image(systemName: selected ? "checkmark.circle.fill" : "circle") .foregroundStyle(selected ? Color.accentColor : .secondary) VStack(alignment: .leading) { Text(dog.name) if let rasse = dog.rasse, !rasse.isEmpty { Text(rasse).font(.caption).foregroundStyle(.secondary) } } Spacer() } .contentShape(Rectangle()) .onTapGesture { if selected { selectedDogIds.remove(dog.id) } else { selectedDogIds.insert(dog.id) } } } private var durationLabel: String { let mins = durationSeconds / 60 let secs = durationSeconds % 60 if mins >= 60 { return "\(mins / 60) h \(mins % 60) min" } return "\(mins) min \(secs) s" } private var canSave: Bool { !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && points.count >= 2 && saveState == .idle } private func loadDogs() async { isLoadingDogs = true defer { isLoadingDogs = false } do { let fetched: [Dog] = try await APIClient.shared.get("/api/dogs") self.dogs = fetched if fetched.count == 1 { selectedDogIds = [fetched[0].id] } } catch { print("FinishWalkSheet loadDogs failed: \(error)") } } private func loadPhotos(from items: [PhotosPickerItem]) async { loadingPhotos = true defer { loadingPhotos = false } var loaded: [Data] = [] for item in items { if let data = try? await item.loadTransferable(type: Data.self) { loaded.append(data) } } photoData = loaded } private func save() async { errorMessage = nil saveState = .savingRoute let body = RouteCreateBody( name: name.trimmingCharacters(in: .whitespacesAndNewlines), gpsTrack: points, distanzKm: distanceMeters / 1000, dauerMin: max(1, durationSeconds / 60), dogIds: Array(selectedDogIds), isPublic: false ) let route: RouteDetail do { route = try await APIClient.shared.post("/api/routes", body: body) } catch { errorMessage = error.localizedDescription saveState = .idle return } if !photoData.isEmpty { for (index, raw) in photoData.enumerated() { saveState = .uploadingPhotos(done: index, total: photoData.count) let resized = ImageResize.resizedJPEG(from: raw) do { try await APIClient.shared.uploadFile( "/api/routes/\(route.id)/photo", filename: "photo_\(index + 1).jpg", data: resized ) } catch { errorMessage = "Tour ist gespeichert, aber Foto \(index + 1) konnte nicht hochgeladen werden: \(error.localizedDescription)" saveState = .idle onSaved() return } } saveState = .uploadingPhotos(done: photoData.count, total: photoData.count) } onSaved() dismiss() } }