banyaro-ios/BanYaroGo/Views/TagebuchView.swift
rene f054b2a07f Tagebuch + Heim-Tab mit täglichem Background
Tagebuch (Diary):
- DiaryEntry + DiaryMedia + DiaryCreateBody DTOs
- TagebuchView: Liste der Einträge für aktiven Hund mit Titel, Text,
  Ortsname, Meilenstein-Stern, Foto-Strip
- AddDiaryEntrySheet: Titel/Text/Datum/Meilenstein/Ort/Tags +
  PhotosPicker, nach POST /api/dogs/{id}/diary werden Fotos einzeln
  via POST /api/dogs/{id}/diary/{entry_id}/media hochgeladen (mit
  ImageResize.resizedJPEG)

Heim-Tab als neuer 1. Tab:
- DashboardSnapshot DTO für /api/dogs/{id}/welcome-dashboard
- ActiveDogStore (@Observable + UserDefaults("activeDogId")): hält
  den aktiven Hund app-weit
- HeimView: tägliches Hintergrundfoto aus random_photo.url (rotiert
  pro Tag, vom Backend gewählt), Gradient zur Lesbarkeit, Tagezeit-
  Begrüßung mit User-Namen, Hund-Picker (Menu), Info-Karten für
  letzten Eintrag/nächsten Termin/Gewicht/Eintragszahl,
  Quick-Action-Buttons (Tagebuch, Wetter, Erste Hilfe)

Reorganisation:
- 5 Tabs: Heim, Touren, Aufnehmen, Statistik, Mehr
- Hunde-Liste wandert in Mehr → "Hund & Alltag"
- Tagebuch in Mehr → "Hund & Alltag" + erreichbar von Heim
2026-05-30 12:22:51 +02:00

140 lines
5 KiB
Swift

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 {
List(entries) { entry in
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 {
errorMessage = error.localizedDescription
}
}
}
private struct DiaryRow: View {
let entry: DiaryEntry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .firstTextBaseline) {
if entry.isMilestone == true || 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.media, !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
}
}