import SwiftUI import PhotosUI import CoreLocation struct AddDiaryEntrySheet: View { let dogId: Int let onSaved: () async -> Void @Environment(\.dismiss) private var dismiss @State private var titel = "" @State private var text = "" @State private var date = Date() @State private var isMilestone = false @State private var locationName = "" @State private var tagsInput = "" @State private var photoSelection: [PhotosPickerItem] = [] @State private var photoData: [Data] = [] @State private var location = OneShotLocation() @State private var attachLocation = true @State private var isReverseGeocoding = false @State private var saveState: SaveState = .idle @State private var errorMessage: String? private enum SaveState: Equatable { case idle case savingEntry case uploadingMedia(done: Int, total: Int) } var body: some View { NavigationStack { Form { Section("Titel") { TextField("z. B. Erster Strandbesuch", text: $titel) } Section("Text") { TextField("Was hat dein Hund heute erlebt?", text: $text, axis: .vertical) .lineLimit(4...10) } Section("Datum") { DatePicker("Datum", selection: $date, displayedComponents: .date) .environment(\.locale, Locale(identifier: "de_DE")) } Section { Toggle("Meilenstein", isOn: $isMilestone) TextField("Tags (komma-getrennt)", text: $tagsInput) .textInputAutocapitalization(.never) .autocorrectionDisabled() } Section { Toggle(isOn: $attachLocation) { Label("Standort verwenden", systemImage: "location.fill") } if attachLocation { if let coord = location.coordinate { HStack { Image(systemName: "mappin.and.ellipse") .foregroundStyle(Color.accentColor) VStack(alignment: .leading, spacing: 2) { if !locationName.isEmpty { Text(locationName).font(.subheadline) } else if isReverseGeocoding { Text("Suche Ortsbezeichnung…") .font(.caption) .foregroundStyle(.secondary) } else { Text("Ort wird mitgespeichert") .font(.subheadline) } Text(String(format: "%.5f, %.5f", coord.latitude, coord.longitude)) .font(.caption2.monospacedDigit()) .foregroundStyle(.tertiary) } } } else if location.error != nil { Label("Standort nicht verfügbar — wird ohne gespeichert", systemImage: "location.slash") .font(.caption) .foregroundStyle(.secondary) } else { HStack { ProgressView() Text("Hole Standort…").font(.caption).foregroundStyle(.secondary) } } TextField("Ort überschreiben (optional)", text: $locationName) } } header: { Text("Standort") } footer: { Text("Der Standort wird zusammen mit dem Eintrag gespeichert — banyaro.app rendert daraus deine POI-Karte.") .font(.caption2) } Section { PhotosPicker( selection: $photoSelection, maxSelectionCount: 6, matching: .images ) { Label(photoData.isEmpty ? "Fotos hinzufügen" : "Fotos ändern", systemImage: "photo.badge.plus") } if !photoData.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(Array(photoData.enumerated()), id: \.offset) { _, d in if let img = UIImage(data: d) { Image(uiImage: img) .resizable() .scaledToFill() .frame(width: 80, height: 80) .clipShape(RoundedRectangle(cornerRadius: 8)) } } } } } } header: { Text(photoData.isEmpty ? "Fotos" : "Fotos (\(photoData.count))") } if let errorMessage { Section { Text(errorMessage).font(.footnote).foregroundStyle(.red) } } } .navigationTitle("Neuer Eintrag") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Abbrechen") { dismiss() } .disabled(saveState != .idle) } ToolbarItem(placement: .confirmationAction) { saveToolbarItem } } .onChange(of: photoSelection) { _, items in Task { await loadPhotos(from: items) } } .task { location.request() } .onChange(of: location.coordinate?.latitude) { _, _ in guard let coord = location.coordinate, locationName.isEmpty else { return } Task { await reverseGeocode(coord) } } .interactiveDismissDisabled(saveState != .idle) } } private func reverseGeocode(_ coord: CLLocationCoordinate2D) async { isReverseGeocoding = true defer { isReverseGeocoding = false } let geocoder = CLGeocoder() let loc = CLLocation(latitude: coord.latitude, longitude: coord.longitude) do { let placemarks = try await geocoder.reverseGeocodeLocation(loc, preferredLocale: Locale(identifier: "de_DE")) guard let p = placemarks.first else { return } // Sample: "Café Kaiser, Berlin" — name first, then locality fallback if let name = p.name, !name.isEmpty { locationName = name return } var parts: [String] = [] if let street = p.thoroughfare { parts.append(street) } if let city = p.locality { parts.append(city) } if !parts.isEmpty { locationName = parts.joined(separator: ", ") } } catch { print("Reverse geocode failed: \(error)") } } @ViewBuilder private var saveToolbarItem: some View { switch saveState { case .idle: Button("Sichern") { Task { await save() } } .disabled(titel.trimmingCharacters(in: .whitespaces).isEmpty && text.trimmingCharacters(in: .whitespaces).isEmpty) case .savingEntry: ProgressView() case .uploadingMedia(let done, let total): Text("\(done)/\(total)").font(.caption.monospacedDigit()) } } private func loadPhotos(from items: [PhotosPickerItem]) async { var loaded: [Data] = [] for item in items { if let d = try? await item.loadTransferable(type: Data.self) { loaded.append(d) } } photoData = loaded } private func save() async { errorMessage = nil saveState = .savingEntry let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" let tags = tagsInput .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } let coord = attachLocation ? location.coordinate : nil let body = DiaryCreateBody( datum: formatter.string(from: date), typ: isMilestone ? "meilenstein" : "eintrag", titel: titel.trimmingCharacters(in: .whitespaces).isEmpty ? nil : titel.trimmingCharacters(in: .whitespaces), text: text.trimmingCharacters(in: .whitespaces).isEmpty ? nil : text.trimmingCharacters(in: .whitespacesAndNewlines), tags: tags.isEmpty ? nil : tags, gpsLat: coord?.latitude, gpsLon: coord?.longitude, locationName: locationName.trimmingCharacters(in: .whitespaces).isEmpty ? nil : locationName, isMilestone: isMilestone ) let entry: DiaryEntry do { entry = try await APIClient.shared.post("/api/dogs/\(dogId)/diary", body: body) } catch { errorMessage = error.localizedDescription saveState = .idle return } if !photoData.isEmpty { for (i, raw) in photoData.enumerated() { saveState = .uploadingMedia(done: i, total: photoData.count) let resized = ImageResize.resizedJPEG(from: raw) do { _ = try await APIClient.shared.uploadFile( "/api/dogs/\(dogId)/diary/\(entry.id)/media", filename: "media_\(i + 1).jpg", data: resized ) } catch { errorMessage = "Eintrag gespeichert, Foto \(i + 1) fehlgeschlagen: \(error.localizedDescription)" await onSaved() saveState = .idle return } } } await onSaved() dismiss() } }