import SwiftUI /// Bewegungsverlauf: wer hat wann was ein- und ausgelagert. /// /// Dieselbe Ansicht dient dem Gesamtverlauf und dem Verlauf eines einzelnen /// Artikels - der Unterschied ist nur `productId`. struct HistoryView: View { @EnvironmentObject private var display: DisplaySettings var productId: Int? @State private var movements: [Movement] = [] @State private var limit = 100 @State private var busy = true @State private var error: String? var body: some View { List { if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } } ForEach(tage, id: \.key) { tag in Section(display.formatDay(tag.eintraege[0].createdAt)) { ForEach(tag.eintraege) { movement in MovementRow(movement: movement, showProduct: productId == nil) } } } if movements.isEmpty && !busy { Text("Noch keine Bewegungen.").foregroundStyle(.secondary) } // Nachladen erst anbieten, wenn die aktuelle Menge auch voll ist. if movements.count >= limit { Button("Mehr laden") { limit += 100 Task { await load() } } } } .navigationTitle("Verlauf") .navigationBarTitleDisplayMode(.inline) .refreshable { await load() } .task { await load() } } private struct Tag { let key: String let eintraege: [Movement] } /// Nach Tagen gruppiert, neueste zuerst. Die API liefert bereits sortiert, /// deshalb reicht es, der Reihe nach zu sammeln. private var tage: [Tag] { var result: [Tag] = [] for movement in movements { let key = DisplaySettings.dayKey(movement.createdAt) if let last = result.last, last.key == key { result[result.count - 1] = Tag(key: key, eintraege: last.eintraege + [movement]) } else { result.append(Tag(key: key, eintraege: [movement])) } } return result } private func load() async { busy = true defer { busy = false } do { movements = try await APIClient.shared.movements(limit: limit, productId: productId) error = nil } catch { self.error = error.localizedDescription } } } struct MovementRow: View { @EnvironmentObject private var display: DisplaySettings let movement: Movement var showProduct = true var body: some View { HStack(spacing: 12) { Image(systemName: movement.isIncoming ? "arrow.down.circle.fill" : "arrow.up.circle.fill") .font(.title3) .foregroundStyle(movement.isIncoming ? Color.green : Color.orange) VStack(alignment: .leading, spacing: 2) { if showProduct { Text(movement.productName) } Text(menge) .font(showProduct ? .caption : .body) .foregroundStyle(showProduct ? .secondary : .primary) if let note = movement.note, !note.isEmpty { Text(note).font(.caption2).foregroundStyle(.secondary) } } Spacer() VStack(alignment: .trailing, spacing: 2) { Text(display.formatTime(movement.createdAt)) .font(.caption).foregroundStyle(.secondary) if let user = movement.username, !user.isEmpty { Text(user).font(.caption2).foregroundStyle(.tertiary) } } } } /// Vorzeichen macht die Richtung auch ohne Farbe lesbar. private var menge: String { let zeichen = movement.isIncoming ? "+" : "−" return zeichen + display.amountText(movement.quantity, packageSize: movement.packageSize, baseUnit: movement.baseUnit) } }