Files
Vorrania/ios/Sources/HistoryView.swift
Scarriffle 0ea188374a iOS: getrennte Lebensmittel/Gegenstaende-Listen + anklickbarer Verlauf
Listen-Tab: zwei halbe Kacheln "Lebensmittel" und "Gegenstaende" (ProductListView
mit fixedType, clientseitig ueber isObject gefiltert wie im Web); Neuanlage
uebernimmt den Typ. Einzelstuecke bleiben unveraendert (reine Gegenstands-Welt).

Verlauf und "Letzte Bewegungen" auf der Startseite: Tippen auf eine Bewegung
oeffnet ueber den neuen Lader ProductByIdView das jeweilige Produkt. Der
artikel-eigene Verlauf bleibt ohne Link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 07:29:49 +02:00

132 lines
4.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
// Im Gesamtverlauf führt jede Zeile zum Produkt; im
// Artikel-eigenen Verlauf ist man schon dort (kein Link).
if productId == nil {
NavigationLink {
ProductByIdView(productId: movement.productId)
} label: {
MovementRow(movement: movement, showProduct: true)
}
} else {
MovementRow(movement: movement, showProduct: false)
}
}
}
}
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)
}
}