import SwiftUI struct TagebuchView: View { @Environment(ActiveDogStore.self) private var activeDog @State private var entries: [DiaryEntry] = [] @State private var isLoading = false @State private var errorMessage: String? @State private var showAdd = false var body: some View { content .navigationTitle("Tagebuch") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { showAdd = true } label: { Image(systemName: "plus") } .disabled(activeDog.activeDog == nil) } } .sheet(isPresented: $showAdd) { if let dog = activeDog.activeDog { AddDiaryEntrySheet(dogId: dog.id) { await load() } } } .task { if activeDog.dogs.isEmpty { await activeDog.loadDogs() } await load() } .refreshable { await load() } } @ViewBuilder private var content: some View { if activeDog.activeDog == nil { ContentUnavailableView( "Noch kein Hund", systemImage: "pawprint", description: Text("Lege deinen ersten Hund in der PWA an, dann kannst du Tagebucheinträge anlegen.") ) } else if isLoading && entries.isEmpty { ProgressView() } else if let errorMessage, entries.isEmpty { ContentUnavailableView("Konnte nicht laden", systemImage: "wifi.slash", description: Text(errorMessage)) } else if entries.isEmpty { ContentUnavailableView( "Noch keine Einträge", systemImage: "book", description: Text("Tippe oben rechts auf +, um deinen ersten Eintrag anzulegen.") ) } else if let dog = activeDog.activeDog { List(entries) { entry in NavigationLink { DiaryDetailView(dogId: dog.id, entry: entry) { await load() } } label: { DiaryRow(entry: entry) } } .listStyle(.plain) } } private func load() async { guard let dog = activeDog.activeDog else { return } isLoading = true errorMessage = nil defer { isLoading = false } do { entries = try await APIClient.shared.get("/api/dogs/\(dog.id)/diary?limit=50") } catch let decodingError as DecodingError { errorMessage = Self.describe(decodingError) print("Tagebuch decode error: \(decodingError)") } catch { errorMessage = error.localizedDescription } } private static func describe(_ error: DecodingError) -> String { switch error { case .typeMismatch(let type, let ctx): return "Feldtyp falsch (\(type)) bei „\(ctx.codingPath.map(\.stringValue).joined(separator: "."))" case .valueNotFound(let type, let ctx): return "Feld fehlt (\(type)) bei „\(ctx.codingPath.map(\.stringValue).joined(separator: "."))" case .keyNotFound(let key, let ctx): return "Key fehlt: \(key.stringValue) bei „\(ctx.codingPath.map(\.stringValue).joined(separator: "."))" case .dataCorrupted(let ctx): return "Datenfehler bei „\(ctx.codingPath.map(\.stringValue).joined(separator: ".")): \(ctx.debugDescription)" @unknown default: return String(describing: error) } } } private struct DiaryRow: View { let entry: DiaryEntry var body: some View { VStack(alignment: .leading, spacing: 8) { HStack(alignment: .firstTextBaseline) { if entry.isMilestoneFlag || entry.typ == "meilenstein" { Image(systemName: "star.fill").foregroundStyle(.orange) } if let titel = entry.titel, !titel.isEmpty { Text(titel).font(.headline) } else { Text("Eintrag").font(.headline).foregroundStyle(.secondary) } Spacer() if let datum = entry.datum { Text(DiaryUtil.format(datum)) .font(.caption.monospacedDigit()) .foregroundStyle(.secondary) } } if let text = entry.text, !text.isEmpty { Text(text) .font(.subheadline) .lineLimit(3) .foregroundStyle(.secondary) } if let loc = entry.locationName, !loc.isEmpty { Label(loc, systemImage: "mappin.and.ellipse") .font(.caption) .foregroundStyle(.tertiary) } if let media = entry.mediaItems, !media.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 6) { ForEach(media) { m in AsyncImage(url: URL(string: "https://banyaro.app\(m.url)")) { phase in switch phase { case .success(let img): img.resizable().scaledToFill() default: Rectangle().fill(.gray.opacity(0.15)) } } .frame(width: 80, height: 80) .clipShape(RoundedRectangle(cornerRadius: 8)) } } } } } .padding(.vertical, 6) } } enum DiaryUtil { static func format(_ str: String) -> String { let parser = DateFormatter() parser.locale = Locale(identifier: "en_US_POSIX") parser.dateFormat = "yyyy-MM-dd" if let d = parser.date(from: String(str.prefix(10))) { let out = DateFormatter() out.locale = Locale(identifier: "de_DE") out.dateStyle = .medium return out.string(from: d) } return str } }