Der Start-Tab spiegelte bisher fest die Web-Dashboards - dieselbe gespeicherte Anordnung, und Diagramme gab es nur den Hinweis "im Web". Das war der Punkt, der geaergert hat. Jetzt ist der Modus je Server umschaltbar: "Web-Uebersicht spiegeln" (weiter Standard, damit nach dem Update nichts ueberrascht) oder "Eigene App-Uebersicht". Die eigene Uebersicht liegt nur auf dem Geraet (pro Server), unabhaengig vom Web und nicht synchronisiert. Beim ersten Umschalten entsteht gleich eine sinnvolle Startanordnung. Sie funktioniert wie im Web, nur fuers Telefon: mehrere Dashboards, Karten einspaltig untereinander, im Bearbeiten-Modus hinzufuegen, per Ziehen sortieren und loeschen; Artikelkarten bekommen ihren Artikel zugewiesen. Anzeigen und Bearbeiten sind getrennt - so laufen Sortieren und Loeschen sauber, statt an Sektionsgrenzen zu haken. Die Diagramme sind nativ mit Swift Charts: Ablauf- und Kategorien-Anteile (Ring ab iOS 17, sonst ein 100-%-Balken - das Ziel ist iOS 16, deshalb kein stilles Anheben), Kategorien nach Zustand als gestapelte Balken, Bestands- und Artikelverlauf als Linie, Ein-/Auslagerungen als gruppierte Balken. Dieselben Endpunkte wie das Web-Dashboard, kein Backend-Eingriff. Der "gibt es nur im Web"-Hinweis faellt damit weg - auch beim Spiegeln werden die Diagramme jetzt nativ gezeichnet. Geprueft: Build; alle vier Diagramm-Modelle gegen echte Serverantworten dekodiert (auch der Zeitstempel mit Z und Sekundenbruchteilen); refactorte DashboardCardView ohne alte Aufrufstellen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
121 lines
4.3 KiB
Swift
121 lines
4.3 KiB
Swift
import Foundation
|
|
|
|
/// Eine **eigene** Übersicht der App - unabhängig von den Web-Dashboards und nur
|
|
/// auf diesem Gerät gespeichert. Je Server wählbar, ob die Web-Dashboards
|
|
/// gespiegelt werden oder diese lokale Übersicht gilt.
|
|
|
|
/// Modus je Server.
|
|
enum OverviewMode: String, Codable {
|
|
case mirror // die Web-Dashboards spiegeln (Standard)
|
|
case local // eigene App-Übersicht
|
|
}
|
|
|
|
/// Eine Karte der lokalen Übersicht. Einspaltig - die Reihenfolge im Array
|
|
/// bestimmt die Anordnung, deshalb kein Raster.
|
|
struct AppCard: Codable, Identifiable, Equatable {
|
|
let id: UUID
|
|
var type: String
|
|
var productId: Int?
|
|
|
|
init(id: UUID = UUID(), type: String, productId: Int? = nil) {
|
|
self.id = id
|
|
self.type = type
|
|
self.productId = productId
|
|
}
|
|
}
|
|
|
|
struct AppDashboard: Codable, Identifiable, Equatable {
|
|
let id: UUID
|
|
var name: String
|
|
var cards: [AppCard]
|
|
|
|
init(id: UUID = UUID(), name: String, cards: [AppCard] = []) {
|
|
self.id = id
|
|
self.name = name
|
|
self.cards = cards
|
|
}
|
|
|
|
/// Sinnvolle Startkarten, wenn jemand die eigene Übersicht zum ersten Mal
|
|
/// anlegt - eine brauchbare Zusammenstellung statt einer leeren Seite.
|
|
static func standard() -> AppDashboard {
|
|
AppDashboard(name: "Übersicht", cards: [
|
|
AppCard(type: "status"),
|
|
AppCard(type: "checkin-quick"),
|
|
AppCard(type: "shopping"),
|
|
AppCard(type: "expiry-all"),
|
|
AppCard(type: "movements"),
|
|
])
|
|
}
|
|
}
|
|
|
|
/// Was ein Server lokal an Übersicht hat: der Modus und die eigenen Dashboards.
|
|
struct AppOverviewConfig: Codable, Equatable {
|
|
var mode: OverviewMode = .mirror
|
|
var dashboards: [AppDashboard] = []
|
|
}
|
|
|
|
/// Ablage je Profil-UUID in den UserDefaults, wie `NotificationStore`.
|
|
enum AppDashboardStore {
|
|
static let key = "app_overviews"
|
|
|
|
static func loadAll() -> [String: AppOverviewConfig] {
|
|
guard let data = UserDefaults.standard.data(forKey: key),
|
|
let dict = try? JSONDecoder().decode([String: AppOverviewConfig].self, from: data)
|
|
else { return [:] }
|
|
return dict
|
|
}
|
|
|
|
static func config(for profileID: UUID) -> AppOverviewConfig {
|
|
loadAll()[profileID.uuidString] ?? AppOverviewConfig()
|
|
}
|
|
|
|
static func save(_ config: AppOverviewConfig, for profileID: UUID) {
|
|
var alle = loadAll()
|
|
alle[profileID.uuidString] = config
|
|
guard let data = try? JSONEncoder().encode(alle) else { return }
|
|
UserDefaults.standard.set(data, forKey: key)
|
|
}
|
|
|
|
static func remove(for profileID: UUID) {
|
|
var alle = loadAll()
|
|
alle.removeValue(forKey: profileID.uuidString)
|
|
guard let data = try? JSONEncoder().encode(alle) else { return }
|
|
UserDefaults.standard.set(data, forKey: key)
|
|
}
|
|
}
|
|
|
|
/// Verfügbare Kartenarten für die lokale Übersicht - Grundlage des Hinzufügen-Menüs.
|
|
struct CardKind: Identifiable {
|
|
let type: String
|
|
let needsProduct: Bool
|
|
var id: String { type }
|
|
var title: String { CardCatalog.titel(type) }
|
|
var isChart: Bool { CardCatalog.istDiagramm(type) }
|
|
}
|
|
|
|
enum CardCatalogList {
|
|
/// Reihenfolge wie sie im Hinzufügen-Menü erscheinen soll.
|
|
static let all: [CardKind] = [
|
|
CardKind(type: "status", needsProduct: false),
|
|
CardKind(type: "kpi-products", needsProduct: false),
|
|
CardKind(type: "kpi-units", needsProduct: false),
|
|
CardKind(type: "checkin-quick", needsProduct: false),
|
|
CardKind(type: "checkout-quick", needsProduct: false),
|
|
CardKind(type: "actions", needsProduct: false),
|
|
CardKind(type: "shopping", needsProduct: false),
|
|
CardKind(type: "expiry-all", needsProduct: false),
|
|
CardKind(type: "expiring", needsProduct: false),
|
|
CardKind(type: "expired", needsProduct: false),
|
|
CardKind(type: "movements", needsProduct: false),
|
|
CardKind(type: "product-stock", needsProduct: true),
|
|
CardKind(type: "product-timeline", needsProduct: true),
|
|
CardKind(type: "expiry-donut", needsProduct: false),
|
|
CardKind(type: "category-donut", needsProduct: false),
|
|
CardKind(type: "category-bars", needsProduct: false),
|
|
CardKind(type: "stock-timeline", needsProduct: false),
|
|
CardKind(type: "activity-timeline", needsProduct: false),
|
|
]
|
|
|
|
static func kind(for type: String) -> CardKind? { all.first { $0.type == type } }
|
|
}
|