Files
Vorrania/ios/Sources/LocalOverview.swift
Scarriffle 80aa857bb0 iOS: Diagramm-Einstellungen im lokalen App-Editor
Der lokale Uebersicht-Editor kann jetzt pro Diagramm-Karte einstellen,
wie im Web-Editor:
- Ablauf-Ring: 'Ohne MHD einbeziehen'
- Kategorien-Ring: 'Detailansicht' (alle einzeln) + 'Kategorie-Tiefe'

AppCard speichert die Optionen (optional -> alte Uebersichten dekodieren
weiter) und reicht sie an DashboardCardView durch.

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

314 lines
11 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
/// 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,
detail: karte.detail ?? false,
tiefe: karte.tiefe,
mitOhneMhd: karte.mitOhneMhd ?? true) { 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
VStack(alignment: .leading, spacing: 6) {
Text(CardCatalog.titel(karte.type))
if CardCatalogList.kind(for: karte.type)?.needsProduct == true {
artikelWahl(fuer: karte)
}
diagrammEinstellungen(fuer: karte)
}
}
.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.marke)
}
}
// MARK: - Diagramm-Einstellungen (nur bestimmte Ringe, wie im Web-Editor)
private struct TiefeOption: Identifiable {
let tiefe: Int?
let label: String
var id: String { label }
}
private static let tiefeOptionen: [TiefeOption] = [
.init(tiefe: nil, label: "Feinste (jede Kategorie einzeln)"),
.init(tiefe: 1, label: "Nur oberste Stufe"),
.init(tiefe: 2, label: "Bis 2. Stufe"),
.init(tiefe: 3, label: "Bis 3. Stufe"),
]
@ViewBuilder private func diagrammEinstellungen(fuer karte: AppCard) -> some View {
switch karte.type {
case "expiry-donut":
Toggle(isOn: Binding(
get: { karte.mitOhneMhd ?? true },
set: { setzeMitOhneMhd(karte, $0) }
)) {
Text("„Ohne MHD“ einbeziehen").font(.caption)
}
case "category-donut":
Toggle(isOn: Binding(
get: { karte.detail ?? false },
set: { setzeDetail(karte, $0) }
)) {
Text("Detailansicht (alle einzeln)").font(.caption)
}
Menu {
ForEach(Self.tiefeOptionen) { opt in
Button(opt.label) { setzeTiefe(karte, opt.tiefe) }
}
} label: {
HStack(spacing: 4) {
Text("Tiefe:").font(.caption).foregroundStyle(.secondary)
Text(Self.tiefeOptionen.first { $0.tiefe == karte.tiefe }?.label ?? "Feinste")
.font(.caption).foregroundStyle(Color.marke)
}
}
default:
EmptyView()
}
}
// 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) } }
/// Ändert genau eine Karte und lädt die Anzeige neu.
private func mutiereKarte(_ karte: AppCard, _ block: (inout AppCard) -> Void) {
mutiere { dash in
if let i = dash.cards.firstIndex(where: { $0.id == karte.id }) {
block(&dash.cards[i])
}
}
stand += 1
}
private func setzeProdukt(_ karte: AppCard, _ pid: Int?) {
mutiereKarte(karte) { $0.productId = pid }
}
private func setzeMitOhneMhd(_ karte: AppCard, _ wert: Bool) {
mutiereKarte(karte) { $0.mitOhneMhd = wert }
}
private func setzeDetail(_ karte: AppCard, _ wert: Bool) {
mutiereKarte(karte) { $0.detail = wert }
}
private func setzeTiefe(_ karte: AppCard, _ wert: Int?) {
mutiereKarte(karte) { $0.tiefe = wert }
}
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.marke)
}
}
.foregroundStyle(.primary)
}
}