Files
Vorrania/ios/Sources/RootView.swift
Scarriffle 2d7cfa7fcb iOS: gescannter Einzelstück-Link öffnet wirklich das Stück
Universal Link oeffnete die App, sprang aber nicht aufs Stueck. Jetzt robuster:
- Kaltstart wird abgefangen (AppDelegate application(_:continue:) + pending-URL),
  zusaetzlich zu den SwiftUI-Hooks.
- Beim Link wird auf das Server-Profil gewechselt, dessen Adresse zum Host passt
  (sonst wuerde die UID am falschen Server gesucht).
- Schlaegt das Aufloesen fehl (nicht angemeldet, UID unbekannt), erscheint jetzt
  eine Meldung statt stiller Nichtreaktion.

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

224 lines
8.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
struct RootView: View {
@EnvironmentObject private var session: Session
@EnvironmentObject private var router: Router
@EnvironmentObject private var display: DisplaySettings
/// Über einen gescannten Universal Link geöffnetes Einzelstück.
@State private var linkedItem: Item?
@State private var linkError: String?
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; eine
// getippte Benachrichtigung öffnet Bald ablaufend".
.fullScreenCover(item: $router.route) { route in
NavigationStack {
switch route {
case .checkin: CheckInView()
case .checkout: CheckOutView()
case .lookup: LookupScanView()
case .assign: AssignScanView()
case .expiring:
ExpiringView()
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Fertig") { router.route = nil }
}
}
}
}
}
.task(id: router.openItemUid) {
guard let uid = router.openItemUid else { return }
router.openItemUid = nil
guard session.isLoggedIn else {
linkError = "Bitte zuerst am passenden Server anmelden, dann erneut scannen."
return
}
do {
linkedItem = try await APIClient.shared.itemByUid(uid: uid)
} catch {
linkError = "Einzelstück \(uid) nicht gefunden: \(error.localizedDescription)"
}
}
.sheet(item: $linkedItem) { it in
NavigationStack { ItemEditView(item: it) }
}
.alert("Einzelstück öffnen", isPresented: Binding(
get: { linkError != nil }, set: { if !$0 { linkError = nil } }
)) {
Button("OK", role: .cancel) { linkError = nil }
} message: { Text(linkError ?? "") }
}
}
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
@EnvironmentObject private var session: Session
@State private var showNew = false
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")
}
Button {
router.route = .lookup
} label: {
ActionTile(title: "Nachschlagen",
subtitle: "Barcode/QR scannen und nur ansehen",
systemImage: "doc.text.magnifyingglass")
}
// Ohne Barcode: gerade bei Gegenstaenden (Kleidung, Werkzeug) gibt
// es keinen Code zum Scannen. Deshalb hier ein offensichtlicher Weg.
if session.isAdmin {
Button {
showNew = true
} label: {
ActionTile(title: "Artikel anlegen",
subtitle: "Ohne Barcode Name, Kategorie, eigene Felder",
systemImage: "plus")
}
Button {
router.route = .assign
} label: {
ActionTile(title: "Ort zuordnen",
subtitle: "Einzelstück-QR scannen, dann Lagerort-QR",
systemImage: "mappin.and.ellipse")
}
}
Spacer()
}
.padding()
.navigationTitle("Scannen")
.sheet(isPresented: $showNew) {
NavigationStack {
ProductFormView(prefillBarcode: nil, groupId: nil) { _ in
showNew = false
}
}
}
}
}
}
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)
}
}