Files
Vorrania/ios/Sources/DashboardView.swift
Scarriffle d680614526 iOS: Diagramm-Einstellungen der Web-Karten beim Spiegeln uebernehmen
Die gespiegelte Web-Uebersicht ignorierte bisher die Karten-Props der
Diagramme. Jetzt werden uebernommen:
- Ablauf-Ring: 'Ohne MHD einbeziehen' (mit_ohne_mhd)
- Kategorien-Ring: 'Detailansicht' (alle einzeln statt 'Uebrige') (detail)
- Kategorien-Ring: 'Kategorie-Tiefe' (tiefe -> /by-category?depth=N)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 22:41:36 +02:00

217 lines
8.4 KiB
Swift

import SwiftUI
/// Der Start-Tab. Je Server wird entweder die Web-Übersicht gespiegelt oder eine
/// eigene, lokale App-Übersicht gezeigt - umschaltbar über das Menü oben.
struct DashboardView: View {
@EnvironmentObject private var session: Session
@State private var mode: OverviewMode = .mirror
@State private var manageShown = false
private var profileID: UUID? { session.activeProfileID }
var body: some View {
NavigationStack {
Group {
if mode == .local, let profileID {
LocalOverview(profileID: profileID)
} else {
MirrorOverview()
}
}
.navigationTitle(session.activeProfile?.name ?? "Übersicht")
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Menu {
Picker("Übersicht", selection: Binding(
get: { mode },
set: { neu in setzeModus(neu) }
)) {
Label("Web-Übersicht spiegeln", systemImage: "arrow.triangle.2.circlepath")
.tag(OverviewMode.mirror)
Label("Eigene App-Übersicht", systemImage: "square.grid.2x2")
.tag(OverviewMode.local)
}
} label: {
Image(systemName: "rectangle.3.group")
}
}
ToolbarItem(placement: .topBarTrailing) {
ServerMenu(manageShown: $manageShown)
}
}
.sheet(isPresented: $manageShown) { ServerListView() }
}
.task(id: profileID) { ladeModus() }
}
private func ladeModus() {
guard let profileID else { mode = .mirror; return }
mode = AppDashboardStore.config(for: profileID).mode
}
private func setzeModus(_ neu: OverviewMode) {
guard let profileID else { return }
var config = AppDashboardStore.config(for: profileID)
// Beim ersten Umschalten auf eigen" gleich eine sinnvolle Übersicht anlegen.
if neu == .local && config.dashboards.isEmpty {
config.dashboards = [AppDashboard.standard()]
}
config.mode = neu
AppDashboardStore.save(config, for: profileID)
mode = neu
}
}
// MARK: - Web-Übersicht spiegeln
/// Zeigt die Dashboards der Web-Oberfläche (schreibgeschützt). Das Raster
/// (x/y/w/h) lässt sich auf dem Telefon nicht nachbilden - die Karten stehen
/// deshalb untereinander, in der Lesereihenfolge des Rasters.
struct MirrorOverview: 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 stand = 0
private var aktives: Dashboard? {
dashboards.first { $0.id == auswahl } ?? dashboards.first
}
private var karten: [DashboardCard] {
(aktives?.layout ?? []).sorted { $0.y == $1.y ? $0.x < $1.x : $0.y < $1.y }
}
var body: some View {
List {
if let fehler {
Section { Text(fehler).foregroundStyle(.red).font(.callout) }
}
if dashboards.count > 1 {
Section {
Picker("Dashboard", selection: Binding(
get: { aktives?.id ?? 0 }, set: { auswahl = $0 }
)) {
ForEach(dashboards) { Text($0.name).tag($0.id) }
}
.pickerStyle(.menu)
}
}
ForEach(karten) { karte in
Section {
DashboardCardView(type: karte.type, productId: karte.props?.productId,
stand: stand,
detail: karte.props?.detail ?? false,
tiefe: karte.props?.tiefe.flatMap { Int($0) },
mitOhneMhd: karte.props?.mitOhneMhd ?? true) { stand += 1 }
} header: {
Text(CardCatalog.titel(karte.type))
}
}
if karten.isEmpty && !busy {
Text("Dieses Dashboard hat keine Karten. Zusammenstellen lässt es sich in der Web-Oberfläche.")
.foregroundStyle(.secondary)
}
}
.refreshable { await laden() }
.task(id: session.activeProfileID) { await laden() }
}
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. Wird sowohl beim Spiegeln
/// der Web-Dashboards als auch in der eigenen App-Uebersicht verwendet - daher
/// nur ueber Art und (fuer Artikelkarten) Produkt-Id parametrisiert.
struct DashboardCardView: View {
let type: String
let productId: Int?
let stand: Int
// Aus den Web-Karten-Einstellungen (props). Standardwerte = bisheriges Verhalten,
// damit die lokale App-Übersicht unverändert bleibt.
var detail: Bool = false
var tiefe: Int? = nil
var mitOhneMhd: Bool = true
let onChange: () -> Void
var body: some View {
switch 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: type, stand: stand)
case "shopping":
ShoppingCard(stand: stand)
case "expiring", "expired", "expiry-all":
ExpiryCard(modus: type, stand: stand)
case "movements":
MovementsCard(stand: stand)
case "product-stock":
ProductCard(productId: productId, stand: stand)
case "product-timeline":
ProductTimelineChart(productId: productId, stand: stand)
case "expiry-donut", "category-donut", "category-bars",
"stock-timeline", "activity-timeline":
ChartCardView(type: type, stand: stand,
detail: detail, tiefe: tiefe, mitOhneMhd: mitOhneMhd)
default:
Text("Unbekannte Karte „\(type)“.")
.font(.caption).foregroundStyle(.secondary)
}
}
}