iOS zeigt dieselben Dashboards und kann direkt buchen
Die Uebersicht in der App war fest verdrahtet: vier Kacheln, unabhaengig davon, was am Rechner zusammengestellt wurde. Jetzt liest sie dieselben Dashboards wie die Web-Oberflaeche und stellt sie nativ dar. Das Raster laesst sich auf einem Telefon nicht sinnvoll nachbilden, deshalb stehen die Karten untereinander - in der Lesereihenfolge des Rasters, erst Zeile, dann Spalte. Zusammengestellt wird weiterhin am Rechner; gibt es mehrere Dashboards, schaltet oben ein Menue um. Neu ist die Buchungskarte: Artikel suchen, Menge, ein- oder auslagern, ohne den Scan-Ablauf zu oeffnen. Nach dem Buchen zieht die Karte den Bestand nach und laesst die uebrigen Karten neu laden - sonst stuende dort weiter die alte Zahl. Diagrammkarten bekommen bewusst keinen halbgaren Nachbau, sondern den ehrlichen Hinweis, dass es sie in der Web-Oberflaeche gibt. Geprueft mit der echten Antwort eines laufenden Servers: Sie decodiert in die Modelle der App, samt zweier Karten derselben Art mit verschiedenen Artikeln. OverviewView faellt weg, DashboardView tritt an ihre Stelle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
167
ios/Sources/DashboardView.swift
Normal file
167
ios/Sources/DashboardView.swift
Normal file
@@ -0,0 +1,167 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Zeigt die Dashboards, die auch die Web-Oberflaeche kennt.
|
||||
///
|
||||
/// Das Raster (x/y/w/h) laesst sich auf einem Telefon nicht sinnvoll
|
||||
/// nachbilden - hier stehen die Karten deshalb untereinander, in der
|
||||
/// Lesereihenfolge des Rasters (erst Zeile, dann Spalte). Zusammengestellt
|
||||
/// werden Dashboards weiterhin am Rechner; die App zeigt sie.
|
||||
struct DashboardView: View {
|
||||
@EnvironmentObject private var session: Session
|
||||
|
||||
@State private var dashboards: [Dashboard] = []
|
||||
@State private var auswahl: Int?
|
||||
@State private var busy = true
|
||||
@State private var fehler: String?
|
||||
@State private var manageShown = false
|
||||
/// Zaehler, der die Karten nach einer Buchung neu laden laesst.
|
||||
@State private var stand = 0
|
||||
|
||||
private var aktives: Dashboard? {
|
||||
dashboards.first { $0.id == auswahl } ?? dashboards.first
|
||||
}
|
||||
|
||||
/// Rasterreihenfolge: erst von oben nach unten, dann von links nach rechts.
|
||||
private var karten: [DashboardCard] {
|
||||
(aktives?.layout ?? []).sorted {
|
||||
$0.y == $1.y ? $0.x < $1.x : $0.y < $1.y
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
if let fehler {
|
||||
Section { Text(fehler).foregroundStyle(.red).font(.callout) }
|
||||
}
|
||||
|
||||
// Mehrere Dashboards: oben umschalten. Bei einem einzigen waere
|
||||
// die Auswahl nur Beiwerk.
|
||||
if dashboards.count > 1 {
|
||||
Section {
|
||||
Picker("Dashboard", selection: Binding(
|
||||
get: { aktives?.id ?? 0 },
|
||||
set: { auswahl = $0 }
|
||||
)) {
|
||||
ForEach(dashboards) { d in
|
||||
Text(d.name).tag(d.id)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(karten) { karte in
|
||||
Section {
|
||||
DashboardCardView(karte: karte, stand: stand) { stand += 1 }
|
||||
} header: {
|
||||
Text(titel(fuer: karte))
|
||||
}
|
||||
}
|
||||
|
||||
if karten.isEmpty && !busy {
|
||||
Text("Dieses Dashboard hat keine Karten. Zusammenstellen lässt es sich in der Web-Oberfläche.")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.navigationTitle(aktives?.name ?? session.activeProfile?.name ?? "Übersicht")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
ServerMenu(manageShown: $manageShown)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $manageShown) { ServerListView() }
|
||||
.refreshable { await laden() }
|
||||
.task(id: session.activeProfileID) { await laden() }
|
||||
}
|
||||
}
|
||||
|
||||
private func titel(fuer karte: DashboardCard) -> String {
|
||||
CardCatalog.titel(karte.type)
|
||||
}
|
||||
|
||||
private func laden() async {
|
||||
busy = true
|
||||
defer { busy = false }
|
||||
do {
|
||||
let antwort = try await APIClient.shared.dashboards()
|
||||
dashboards = antwort.dashboards.sorted { $0.position < $1.position }
|
||||
if auswahl == nil || !dashboards.contains(where: { $0.id == auswahl }) {
|
||||
auswahl = dashboards.first?.id
|
||||
}
|
||||
fehler = nil
|
||||
} catch {
|
||||
self.fehler = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Beschriftungen der Kartenarten - dieselben wie in web/src/dashboard/cards.jsx.
|
||||
enum CardCatalog {
|
||||
static func titel(_ type: String) -> String {
|
||||
switch type {
|
||||
case "actions": return "Schnellzugriff"
|
||||
case "checkin-quick": return "Einlagern"
|
||||
case "checkout-quick": return "Auslagern"
|
||||
case "status": return "Status"
|
||||
case "kpi-products": return "Artikel mit Bestand"
|
||||
case "kpi-units": return "Artikeleinheiten"
|
||||
case "expiring": return "Bald ablaufend"
|
||||
case "expired": return "Abgelaufen"
|
||||
case "expiry-all": return "Ablauf-Übersicht"
|
||||
case "shopping": return "Einkaufsliste"
|
||||
case "movements": return "Letzte Bewegungen"
|
||||
case "product-stock": return "Artikel im Blick"
|
||||
case "product-timeline": return "Verlauf eines Artikels"
|
||||
case "expiry-donut": return "Ablauf-Anteile"
|
||||
case "category-donut": return "Kategorien-Anteile"
|
||||
case "category-bars": return "Kategorien nach Zustand"
|
||||
case "stock-timeline": return "Bestandsverlauf"
|
||||
case "activity-timeline": return "Ein- und Auslagerungen"
|
||||
default: return "Karte"
|
||||
}
|
||||
}
|
||||
|
||||
/// Karten, die nur als Diagramm Sinn ergeben. Die App zeigt statt eines
|
||||
/// halbgaren Nachbaus einen ehrlichen Hinweis.
|
||||
static func istDiagramm(_ type: String) -> Bool {
|
||||
["expiry-donut", "category-donut", "category-bars",
|
||||
"stock-timeline", "activity-timeline"].contains(type)
|
||||
}
|
||||
}
|
||||
|
||||
/// Waehlt anhand der Kartenart den passenden Inhalt.
|
||||
struct DashboardCardView: View {
|
||||
let karte: DashboardCard
|
||||
let stand: Int
|
||||
let onChange: () -> Void
|
||||
|
||||
var body: some View {
|
||||
switch karte.type {
|
||||
case "checkin-quick":
|
||||
QuickBookingCard(richtung: .ein, onChange: onChange)
|
||||
case "checkout-quick":
|
||||
QuickBookingCard(richtung: .aus, onChange: onChange)
|
||||
case "actions":
|
||||
ActionsCard()
|
||||
case "status", "kpi-products", "kpi-units":
|
||||
StatusCard(art: karte.type, stand: stand)
|
||||
case "shopping":
|
||||
ShoppingCard(stand: stand)
|
||||
case "expiring", "expired", "expiry-all":
|
||||
ExpiryCard(modus: karte.type, stand: stand)
|
||||
case "movements":
|
||||
MovementsCard(stand: stand)
|
||||
case "product-stock", "product-timeline":
|
||||
ProductCard(productId: karte.props?.productId, stand: stand)
|
||||
default:
|
||||
if CardCatalog.istDiagramm(karte.type) {
|
||||
Label("Diagramme gibt es in der Web-Oberfläche.", systemImage: "chart.xyaxis.line")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("Unbekannte Karte „\(karte.type)“.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user