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>
249 lines
8.6 KiB
Swift
249 lines
8.6 KiB
Swift
import SwiftUI
|
||
|
||
/// Die eigene, lokale App-Übersicht eines Servers: mehrere Dashboards, Karten
|
||
/// untereinander, frei zusammenstellbar. Nur auf diesem Gerät gespeichert.
|
||
///
|
||
/// Anzeige und Bearbeitung sind getrennt: Beim Bearbeiten zeigt eine kompakte,
|
||
/// sortierbare Liste die Karten (so funktionieren Ziehen und Löschen sauber),
|
||
/// beim Ansehen die vollen Karten.
|
||
struct LocalOverview: View {
|
||
let profileID: UUID
|
||
|
||
@State private var config = AppOverviewConfig()
|
||
@State private var auswahl: UUID?
|
||
@State private var bearbeiten = false
|
||
@State private var addShown = false
|
||
@State private var produkte: [Product] = []
|
||
@State private var stand = 0
|
||
@State private var renameText = ""
|
||
@State private var renameShown = false
|
||
|
||
private var aktives: AppDashboard? {
|
||
config.dashboards.first { $0.id == auswahl } ?? config.dashboards.first
|
||
}
|
||
|
||
var body: some View {
|
||
Group {
|
||
if bearbeiten {
|
||
editierListe
|
||
} else {
|
||
anzeige
|
||
}
|
||
}
|
||
.toolbar { ToolbarItemGroup(placement: .bottomBar) { werkzeugleiste } }
|
||
.sheet(isPresented: $addShown) {
|
||
CardPickerView { neu in fuegeHinzu(neu) }
|
||
}
|
||
.alert("Dashboard umbenennen", isPresented: $renameShown) {
|
||
TextField("Name", text: $renameText)
|
||
Button("Sichern") { speichereName() }
|
||
Button("Abbrechen", role: .cancel) {}
|
||
}
|
||
.task(id: profileID) {
|
||
config = AppDashboardStore.config(for: profileID)
|
||
if auswahl == nil || !config.dashboards.contains(where: { $0.id == auswahl }) {
|
||
auswahl = config.dashboards.first?.id
|
||
}
|
||
produkte = (try? await APIClient.shared.searchProducts("")) ?? []
|
||
}
|
||
}
|
||
|
||
// MARK: - Anzeige
|
||
|
||
private var anzeige: some View {
|
||
List {
|
||
if config.dashboards.count > 1 {
|
||
Section { dashboardPicker }
|
||
}
|
||
if let aktives {
|
||
ForEach(aktives.cards) { karte in
|
||
Section {
|
||
DashboardCardView(type: karte.type, productId: karte.productId,
|
||
stand: stand) { stand += 1 }
|
||
} header: {
|
||
Text(titel(karte))
|
||
}
|
||
}
|
||
if aktives.cards.isEmpty {
|
||
Text("Noch keine Karten. Über „Bearbeiten“ lassen sich welche hinzufügen.")
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
}
|
||
.refreshable { stand += 1 }
|
||
}
|
||
|
||
// MARK: - Bearbeiten
|
||
|
||
private var editierListe: some View {
|
||
List {
|
||
if config.dashboards.count > 1 {
|
||
Section { dashboardPicker }
|
||
}
|
||
Section {
|
||
ForEach(aktives?.cards ?? []) { karte in
|
||
HStack {
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(CardCatalog.titel(karte.type))
|
||
if CardCatalogList.kind(for: karte.type)?.needsProduct == true {
|
||
artikelWahl(fuer: karte)
|
||
}
|
||
}
|
||
Spacer()
|
||
}
|
||
}
|
||
.onDelete(perform: loesche)
|
||
.onMove(perform: verschiebe)
|
||
} header: {
|
||
Text("Karten – ziehen zum Sortieren")
|
||
}
|
||
}
|
||
.environment(\.editMode, .constant(.active))
|
||
}
|
||
|
||
private var dashboardPicker: some View {
|
||
Picker("Dashboard", selection: Binding(
|
||
get: { aktives?.id ?? UUID() }, set: { auswahl = $0 }
|
||
)) {
|
||
ForEach(config.dashboards) { Text($0.name).tag($0.id) }
|
||
}
|
||
.pickerStyle(.menu)
|
||
}
|
||
|
||
@ViewBuilder private var werkzeugleiste: some View {
|
||
if bearbeiten {
|
||
Button { addShown = true } label: { Label("Karte", systemImage: "plus") }
|
||
Spacer()
|
||
Menu {
|
||
Button("Dashboard hinzufügen", systemImage: "plus") { neuesDashboard() }
|
||
if aktives != nil {
|
||
Button("Umbenennen", systemImage: "pencil") {
|
||
renameText = aktives?.name ?? ""; renameShown = true
|
||
}
|
||
if config.dashboards.count > 1 {
|
||
Button("Dashboard löschen", systemImage: "trash", role: .destructive) {
|
||
loescheDashboard()
|
||
}
|
||
}
|
||
}
|
||
} label: {
|
||
Label("Dashboards", systemImage: "rectangle.stack")
|
||
}
|
||
Spacer()
|
||
Button("Fertig") { bearbeiten = false }
|
||
} else {
|
||
Spacer()
|
||
Button { bearbeiten = true } label: {
|
||
Label("Bearbeiten", systemImage: "slider.horizontal.3")
|
||
}
|
||
}
|
||
}
|
||
|
||
private func titel(_ karte: AppCard) -> String {
|
||
if let pid = karte.productId, let p = produkte.first(where: { $0.id == pid }) {
|
||
return "\(CardCatalog.titel(karte.type)): \(p.name)"
|
||
}
|
||
return CardCatalog.titel(karte.type)
|
||
}
|
||
|
||
private func artikelWahl(fuer karte: AppCard) -> some View {
|
||
Menu {
|
||
Button("– kein Artikel –") { setzeProdukt(karte, nil) }
|
||
ForEach(produkte) { p in
|
||
Button(p.name) { setzeProdukt(karte, p.id) }
|
||
}
|
||
} label: {
|
||
Text(produkte.first { $0.id == karte.productId }?.name ?? "Artikel wählen")
|
||
.font(.caption).foregroundStyle(Color.accentColor)
|
||
}
|
||
}
|
||
|
||
// MARK: - Datenänderungen (alle sofort gespeichert)
|
||
|
||
private func mutiere(_ block: (inout AppDashboard) -> Void) {
|
||
guard let id = aktives?.id,
|
||
let index = config.dashboards.firstIndex(where: { $0.id == id }) else { return }
|
||
block(&config.dashboards[index])
|
||
AppDashboardStore.save(config, for: profileID)
|
||
}
|
||
|
||
private func fuegeHinzu(_ karte: AppCard) { mutiere { $0.cards.append(karte) } }
|
||
|
||
private func setzeProdukt(_ karte: AppCard, _ pid: Int?) {
|
||
mutiere { dash in
|
||
if let i = dash.cards.firstIndex(where: { $0.id == karte.id }) {
|
||
dash.cards[i].productId = pid
|
||
}
|
||
}
|
||
stand += 1
|
||
}
|
||
|
||
private func loesche(at offsets: IndexSet) { mutiere { $0.cards.remove(atOffsets: offsets) } }
|
||
private func verschiebe(from: IndexSet, to: Int) { mutiere { $0.cards.move(fromOffsets: from, toOffset: to) } }
|
||
|
||
private func neuesDashboard() {
|
||
let neu = AppDashboard(name: "Neues Dashboard")
|
||
config.dashboards.append(neu)
|
||
AppDashboardStore.save(config, for: profileID)
|
||
auswahl = neu.id
|
||
}
|
||
|
||
private func loescheDashboard() {
|
||
guard let id = aktives?.id else { return }
|
||
config.dashboards.removeAll { $0.id == id }
|
||
AppDashboardStore.save(config, for: profileID)
|
||
auswahl = config.dashboards.first?.id
|
||
}
|
||
|
||
private func speichereName() {
|
||
let name = renameText.trimmingCharacters(in: .whitespaces)
|
||
guard !name.isEmpty else { return }
|
||
mutiere { $0.name = name }
|
||
}
|
||
}
|
||
|
||
/// Auswahl einer neuen Karte. Diagramme sind eigens gekennzeichnet.
|
||
struct CardPickerView: View {
|
||
let onPick: (AppCard) -> Void
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
List {
|
||
Section("Karten") {
|
||
ForEach(CardCatalogList.all.filter { !$0.isChart }) { kind in
|
||
zeile(kind)
|
||
}
|
||
}
|
||
Section("Diagramme") {
|
||
ForEach(CardCatalogList.all.filter { $0.isChart }) { kind in
|
||
zeile(kind)
|
||
}
|
||
}
|
||
}
|
||
.navigationTitle("Karte hinzufügen")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } }
|
||
}
|
||
}
|
||
}
|
||
|
||
private func zeile(_ kind: CardKind) -> some View {
|
||
Button {
|
||
onPick(AppCard(type: kind.type))
|
||
dismiss()
|
||
} label: {
|
||
HStack {
|
||
Text(kind.title)
|
||
if kind.needsProduct {
|
||
Text("· Artikel").font(.caption).foregroundStyle(.secondary)
|
||
}
|
||
Spacer()
|
||
Image(systemName: "plus.circle").foregroundStyle(Color.accentColor)
|
||
}
|
||
}
|
||
.foregroundStyle(.primary)
|
||
}
|
||
}
|