import SwiftUI struct HeimView: View { @Environment(AuthSession.self) private var auth @Environment(ActiveDogStore.self) private var activeDog @State private var dashboard: DashboardSnapshot? @State private var isLoading = false private var greeting: String { let hour = Calendar.current.component(.hour, from: .now) let name = auth.profile?.name ?? auth.userName ?? "" let salute: String = { switch hour { case 5..<11: return "Guten Morgen" case 11..<14: return "Hallo" case 14..<18: return "Hallo" case 18..<22: return "Guten Abend" default: return "Gute Nacht" } }() return name.isEmpty ? salute : "\(salute), \(name)" } var body: some View { NavigationStack { ZStack { background .ignoresSafeArea() ScrollView { VStack(spacing: 16) { Spacer(minLength: 200) // breathing room for the photo header dogPickerCard if let dashboard { dashboardCards(dashboard) } quickActions Spacer(minLength: 24) } .padding(.horizontal) } .refreshable { await load() } } .navigationBarTitleDisplayMode(.inline) .toolbar(.hidden, for: .navigationBar) .task { if activeDog.dogs.isEmpty { await activeDog.loadDogs() } await load() } .onChange(of: activeDog.activeDogId) { _, _ in Task { await load() } } } } @ViewBuilder private var background: some View { ZStack { Color.accentColor.opacity(0.08).ignoresSafeArea() if let path = dashboard?.randomPhoto?.url, let url = URL(string: "https://banyaro.app\(path)") { AsyncImage(url: url) { phase in switch phase { case .success(let img): img.resizable().scaledToFill() default: Color.clear } } .frame(maxWidth: .infinity, maxHeight: 320, alignment: .top) .clipped() .overlay( LinearGradient( colors: [.clear, .clear, Color(.systemBackground).opacity(0.95), Color(.systemBackground)], startPoint: .top, endPoint: .bottom ) ) .frame(maxHeight: .infinity, alignment: .top) } } } private var header: some View { VStack(spacing: 4) { Text(greeting) .font(.title2.bold()) .multilineTextAlignment(.center) .shadow(color: .black.opacity(0.4), radius: 3, y: 1) if let dog = activeDog.activeDog { Text("Was machst du heute mit \(dog.name)?") .font(.subheadline) .foregroundStyle(.secondary) .multilineTextAlignment(.center) } } .frame(maxWidth: .infinity) .padding(.bottom, 6) } private var dogPickerCard: some View { Group { if activeDog.dogs.isEmpty { EmptyView() } else { Menu { ForEach(activeDog.dogs) { dog in Button { activeDog.setActive(dog.id) } label: { if dog.id == activeDog.activeDogId { Label(dog.name, systemImage: "checkmark") } else { Text(dog.name) } } } } label: { HStack(spacing: 12) { dogAvatar .frame(width: 48, height: 48) .clipShape(Circle()) VStack(alignment: .leading) { Text(activeDog.activeDog?.name ?? "Hund wählen") .font(.headline) .foregroundStyle(.primary) if let rasse = activeDog.activeDog?.rasse, !rasse.isEmpty { Text(rasse) .font(.caption) .foregroundStyle(.secondary) } } Spacer() Image(systemName: "chevron.up.chevron.down") .font(.caption) .foregroundStyle(.secondary) } .padding(14) .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 16)) } } } } @ViewBuilder private var dogAvatar: some View { if let path = activeDog.activeDog?.fotoUrl, let url = URL(string: "https://banyaro.app\(path)") { AsyncImage(url: url) { phase in switch phase { case .success(let img): img.resizable().scaledToFill() default: avatarFallback } } } else { avatarFallback } } private var avatarFallback: some View { ZStack { Color.accentColor.opacity(0.2) Image(systemName: "pawprint.fill") .foregroundStyle(Color.accentColor) } } private func dashboardCards(_ snap: DashboardSnapshot) -> some View { VStack(spacing: 10) { if let last = snap.lastDiary, last.titel != nil { infoCard( icon: "book.fill", title: "Letzter Eintrag", value: last.titel ?? "—", detail: last.datum.map(DiaryUtil.format) ?? "" ) } if let appt = snap.nextAppointment, let bez = appt.bezeichnung { infoCard( icon: "calendar", title: "Nächster Termin", value: bez, detail: appt.naechstes.map(DiaryUtil.format) ?? "" ) } if let weight = snap.lastWeight, let wert = weight.wert { infoCard( icon: "scalemass.fill", title: "Letztes Gewicht", value: String(format: "%.1f %@", wert, weight.einheit ?? "kg"), detail: weight.datum.map(DiaryUtil.format) ?? "" ) } if let count = snap.diaryCount, count > 0 { infoCard( icon: "books.vertical.fill", title: "Tagebucheinträge", value: "\(count)", detail: "" ) } } } private func infoCard(icon: String, title: String, value: String, detail: String) -> some View { HStack(spacing: 12) { Image(systemName: icon) .font(.title3) .foregroundStyle(Color.accentColor) .frame(width: 28) VStack(alignment: .leading, spacing: 2) { Text(title).font(.caption).foregroundStyle(.secondary) Text(value).font(.subheadline.bold()) if !detail.isEmpty { Text(detail).font(.caption2).foregroundStyle(.tertiary) } } Spacer() } .padding(14) .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 14)) } private var quickActions: some View { VStack(spacing: 10) { NavigationLink { TagebuchView() } label: { actionRow(icon: "book.fill", title: "Tagebuch", subtitle: "Eintrag anlegen oder lesen") } NavigationLink { WetterView() } label: { actionRow(icon: "cloud.sun.fill", title: "Wetter", subtitle: "Vorhersage für heute") } NavigationLink { ErsteHilfeView() } label: { actionRow(icon: "cross.case.fill", title: "Erste Hilfe", subtitle: "Notfall-Anleitung — offline") } } } private func actionRow(icon: String, title: String, subtitle: String) -> some View { HStack(spacing: 12) { Image(systemName: icon) .font(.title3) .foregroundStyle(.white) .frame(width: 36, height: 36) .background(Color.accentColor, in: RoundedRectangle(cornerRadius: 10)) VStack(alignment: .leading, spacing: 2) { Text(title).font(.subheadline.bold()).foregroundStyle(.primary) Text(subtitle).font(.caption).foregroundStyle(.secondary) } Spacer() Image(systemName: "chevron.right") .font(.caption.bold()) .foregroundStyle(.tertiary) } .padding(14) .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 14)) } private func load() async { guard let dog = activeDog.activeDog else { return } isLoading = true defer { isLoading = false } dashboard = try? await APIClient.shared.get("/api/dogs/\(dog.id)/welcome-dashboard") } }