diff --git a/BanYaroGo/Views/VerloreneHundeView.swift b/BanYaroGo/Views/VerloreneHundeView.swift index 9769bf3..bd18127 100644 --- a/BanYaroGo/Views/VerloreneHundeView.swift +++ b/BanYaroGo/Views/VerloreneHundeView.swift @@ -1,16 +1,34 @@ import SwiftUI import MapKit +import PhotosUI struct VerloreneHundeView: View { @State private var location = OneShotLocation() @State private var lostDogs: [LostDog] = [] @State private var isLoading = false @State private var errorMessage: String? + @State private var showReport = false var body: some View { content .navigationTitle("Verlorene Hunde") .navigationBarTitleDisplayMode(.inline) + .toolbar { + if location.coordinate != nil { + ToolbarItem(placement: .topBarTrailing) { + Button { + showReport = true + } label: { + Label("Melden", systemImage: "exclamationmark.bubble") + } + } + } + } + .sheet(isPresented: $showReport) { + if let coord = location.coordinate { + ReportLostDogSheet(coord: coord) { await load() } + } + } .task { location.request() await load() @@ -119,6 +137,161 @@ private struct LostDogRow: View { } } +private struct ReportLostDogSheet: View { + @Environment(\.dismiss) private var dismiss + let coord: CLLocationCoordinate2D + let onSaved: () async -> Void + + @State private var name = "" + @State private var rasse = "" + @State private var beschreibung = "" + @State private var dogs: [Dog] = [] + @State private var ownDogId: Int? + @State private var photoSelection: [PhotosPickerItem] = [] + @State private var photoData: Data? + + @State private var saveState: SaveState = .idle + @State private var errorMessage: String? + + private enum SaveState: Equatable { + case idle + case savingEntry + case uploadingPhoto + } + + var body: some View { + NavigationStack { + Form { + Section("Hund") { + TextField("Name (z. B. Bello)", text: $name) + TextField("Rasse (optional)", text: $rasse) + if !dogs.isEmpty { + Picker("Eigener Hund", selection: $ownDogId) { + Text("Nicht meiner").tag(Int?.none) + ForEach(dogs) { dog in + Text(dog.name).tag(Int?.some(dog.id)) + } + } + } + } + + Section("Beschreibung") { + TextField("Auffälligkeiten, Halsband, Verhalten — alles was beim Wiederfinden hilft.", + text: $beschreibung, axis: .vertical) + .lineLimit(3...8) + } + + Section { + PhotosPicker(selection: $photoSelection, maxSelectionCount: 1, matching: .images) { + Label(photoData == nil ? "Foto hinzufügen" : "Foto ändern", + systemImage: "photo.badge.plus") + } + if let photoData, let img = UIImage(data: photoData) { + Image(uiImage: img) + .resizable() + .scaledToFill() + .frame(maxHeight: 200) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } header: { + Text("Foto") + } footer: { + Text("Ein aktuelles Foto erhöht die Chance, dass jemand den Hund erkennt.") + .font(.caption2) + } + + Section("Letzter bekannter Standort") { + Text(String(format: "%.5f, %.5f", coord.latitude, coord.longitude)) + .font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + } + + if let errorMessage { + Section { + Text(errorMessage).font(.footnote).foregroundStyle(.red) + } + } + } + .navigationTitle("Vermisst melden") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Abbrechen") { dismiss() } + .disabled(saveState != .idle) + } + ToolbarItem(placement: .confirmationAction) { + saveToolbarItem + } + } + .onChange(of: photoSelection) { _, items in + Task { await loadPhoto(from: items.first) } + } + .task { dogs = (try? await APIClient.shared.get("/api/dogs")) ?? [] } + .interactiveDismissDisabled(saveState != .idle) + } + } + + @ViewBuilder + private var saveToolbarItem: some View { + switch saveState { + case .idle: + Button("Melden") { Task { await save() } } + .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty || + beschreibung.trimmingCharacters(in: .whitespaces).count < 3) + case .savingEntry, .uploadingPhoto: + ProgressView() + } + } + + private func loadPhoto(from item: PhotosPickerItem?) async { + guard let item else { return } + photoData = try? await item.loadTransferable(type: Data.self) + } + + private func save() async { + errorMessage = nil + saveState = .savingEntry + + let body = LostDogCreateBody( + name: name.trimmingCharacters(in: .whitespaces), + rasse: rasse.trimmingCharacters(in: .whitespaces).isEmpty ? nil : rasse, + beschreibung: beschreibung.trimmingCharacters(in: .whitespacesAndNewlines), + lat: coord.latitude, + lon: coord.longitude, + dogId: ownDogId + ) + + let created: LostDog + do { + created = try await APIClient.shared.post("/api/lost", body: body) + } catch { + errorMessage = error.localizedDescription + saveState = .idle + return + } + + if let photoData { + saveState = .uploadingPhoto + let resized = ImageResize.resizedJPEG(from: photoData) + do { + _ = try await APIClient.shared.uploadFile( + "/api/lost/\(created.id)/foto", + filename: "lost_\(created.id).jpg", + data: resized + ) + } catch { + errorMessage = "Meldung ist gespeichert, aber Foto-Upload fehlgeschlagen: \(error.localizedDescription)" + await onSaved() + saveState = .idle + return + } + } + + await onSaved() + dismiss() + } +} + private struct LostDogDetailView: View { let dog: LostDog