Files
Vorrania/ios/Sources/HistoryView.swift
Scarriffle ff158a1e5f iOS: mehrere Server, TabBar, Uebersicht, Verlauf und Stammdaten
Die App konnte fuenf Dinge, die Web-Oberflaeche vierzehn. Alles darueber hinaus
- nachsehen was gestern passiert ist, einen Lagerort anlegen - zwang zurueck an
den Browser.

Mehrere Server: Statt einer Adresse und einem Token kennt die App jetzt
beliebig viele Profile, jedes mit eigenem Token im Keychain. Ein Wechsel ist
damit ein Tipp und meldet an keinem der Server ab. Jedes Profil hat einen frei
waehlbaren Namen ("Zuhause"), der oben in der App steht; ohne Angabe faellt er
auf den Rechnernamen zurueck. Eine bestehende Anmeldung wird beim ersten Start
in ein Profil ueberfuehrt - ohne das waere man nach dem Update abgemeldet.

Statt einer Startseite mit Kacheln gibt es vier Tabs: Start, Scannen, Listen
und - nur fuer Administratoren, wie im Web - Verwaltung. Neu darin sind eine
Uebersicht mit den Kennzahlen des Servers, die per Tipp in die passende Liste
fuehren, der Bewegungsverlauf (auch je Artikel, weil /movements danach filtern
kann) und die Stammdaten: Lagerorte, Einheiten, Gebinde, Kategorien und
Gruppen. Die fuenf teilen sich eine Ansicht und unterscheiden sich nur darin,
wie geladen und geschrieben wird.

Zwei Dinge, die beim Pruefen gegen einen echten Server auffielen: Der Server
schickt Zeitstempel in UTC, haengt hinter SQLite aber keine Zeitzone an. Naiv
gelesen haette die App jede Uhrzeit um die eigene Zeitverschiebung daneben
angezeigt, deshalb wird die nackte Form ausdruecklich als UTC gelesen. Und die
Anzeigeeinstellungen gehoeren dem Server, werden beim Wechsel also neu geladen.

Bewusst nicht dabei: Benutzer, Einstellungen, Branding und Import/Export. Die
bleiben vorerst im Web.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 18:24:07 +02:00

122 lines
4.1 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
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)
}
}