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>
156 lines
5.1 KiB
Swift
156 lines
5.1 KiB
Swift
import SwiftUI
|
|
|
|
struct RootView: View {
|
|
@EnvironmentObject private var session: Session
|
|
@EnvironmentObject private var router: Router
|
|
@EnvironmentObject private var display: DisplaySettings
|
|
|
|
var body: some View {
|
|
Group {
|
|
if session.isLoggedIn {
|
|
HomeView()
|
|
} else {
|
|
LoginView()
|
|
}
|
|
}
|
|
// Anzeigeeinstellungen (Datumsformat) gehoeren dem Server - bei einem
|
|
// Serverwechsel muessen sie deshalb neu geladen werden, sonst zeigt
|
|
// Server B die Formate von Server A.
|
|
.task(id: session.activeProfileID) {
|
|
if session.isLoggedIn {
|
|
await display.load()
|
|
await session.refreshMe()
|
|
}
|
|
}
|
|
// Shortcut/URL öffnet den passenden Scan-Bildschirm direkt.
|
|
.fullScreenCover(item: $router.route) { route in
|
|
NavigationStack {
|
|
switch route {
|
|
case .checkin: CheckInView()
|
|
case .checkout: CheckOutView()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Route: Identifiable {
|
|
var id: String { rawValue }
|
|
}
|
|
|
|
struct HomeView: View {
|
|
@EnvironmentObject private var session: Session
|
|
|
|
var body: some View {
|
|
TabView {
|
|
DashboardView()
|
|
.tabItem { Label("Start", systemImage: "house") }
|
|
|
|
ScanTabView()
|
|
.tabItem { Label("Scannen", systemImage: "barcode.viewfinder") }
|
|
|
|
ListsTabView()
|
|
.tabItem { Label("Listen", systemImage: "list.bullet") }
|
|
|
|
// Stammdaten gehoeren Administratoren - dieselbe Regel wie in der
|
|
// Web-Oberflaeche (web/src/App.jsx).
|
|
if session.isAdmin {
|
|
AdminTabView()
|
|
.tabItem { Label("Verwaltung", systemImage: "gearshape") }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Die beiden Kacheln, die frueher die Startseite waren.
|
|
struct ScanTabView: View {
|
|
@EnvironmentObject private var router: Router
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: 16) {
|
|
Button {
|
|
router.route = .checkin
|
|
} label: {
|
|
ActionTile(title: "Einlagern", subtitle: "Barcode scannen und Bestand erfassen",
|
|
systemImage: "arrow.down.to.line")
|
|
}
|
|
Button {
|
|
router.route = .checkout
|
|
} label: {
|
|
// "arrow.up.from.line" gibt es als SF Symbol nicht - das Icon
|
|
// blieb dadurch leer und es war nur die Kachelflaeche zu sehen.
|
|
ActionTile(title: "Auslagern", subtitle: "Barcode scannen und Menge entnehmen",
|
|
systemImage: "arrow.up.to.line")
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.navigationTitle("Scannen")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ListsTabView: View {
|
|
var body: some View {
|
|
NavigationStack {
|
|
VStack(spacing: 16) {
|
|
NavigationLink {
|
|
ProductListView()
|
|
} label: {
|
|
ActionTile(title: "Produkte", subtitle: "Suchen, ansehen und bearbeiten",
|
|
systemImage: "shippingbox")
|
|
}
|
|
NavigationLink {
|
|
ShoppingListView()
|
|
} label: {
|
|
ActionTile(title: "Einkaufsliste", subtitle: "Was unter dem Mindestbestand liegt",
|
|
systemImage: "cart")
|
|
}
|
|
NavigationLink {
|
|
ExpiringView()
|
|
} label: {
|
|
ActionTile(title: "Bald ablaufend", subtitle: "Chargen mit knappem MHD",
|
|
systemImage: "clock")
|
|
}
|
|
NavigationLink {
|
|
HistoryView()
|
|
} label: {
|
|
ActionTile(title: "Verlauf", subtitle: "Wer hat wann was ein- und ausgelagert",
|
|
systemImage: "clock.arrow.circlepath")
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
.navigationTitle("Listen")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ActionTile: View {
|
|
let title: String
|
|
let subtitle: String
|
|
let systemImage: String
|
|
|
|
var body: some View {
|
|
HStack(spacing: 14) {
|
|
Image(systemName: systemImage)
|
|
.font(.title2)
|
|
.frame(width: 44, height: 44)
|
|
.background(Color.accentColor.opacity(0.15))
|
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(title).font(.headline)
|
|
Text(subtitle).font(.caption).foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.leading)
|
|
}
|
|
Spacer()
|
|
Image(systemName: "chevron.right").foregroundStyle(.tertiary)
|
|
}
|
|
.padding()
|
|
.background(Color(.secondarySystemBackground))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
.foregroundStyle(.primary)
|
|
}
|
|
}
|