Files
Vorrania/ios/Sources/LocalOverview.swift
Scarriffle 6ddf6024f7 iOS: 'Bearbeiten'-Leiste sichtbar machen (nicht mehr in versteckter .bottomBar)
Der Start-Tab liegt in einer TabView; deren Tab-Balken verdeckte die
.bottomBar-Werkzeugleiste, deshalb war 'Bearbeiten' nicht auffindbar.
Jetzt eine eigene Leiste per safeAreaInset direkt ueber dem Tab-Balken.

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

325 lines
12 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
}
}
// Eigene Leiste statt `.bottomBar`: der Start-Tab liegt in einer TabView,
// deren Tab-Balken die `.bottomBar` verdecken würde. `safeAreaInset` legt
// die Leiste sichtbar direkt über den Tab-Balken.
.safeAreaInset(edge: .bottom) { 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)
}
private var werkzeugleiste: some View {
VStack(spacing: 0) {
Divider()
HStack {
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 }.fontWeight(.semibold)
} else {
Spacer()
Button { bearbeiten = true } label: {
Label("Bearbeiten", systemImage: "slider.horizontal.3")
}
}
}
.padding(.horizontal)
.padding(.vertical, 10)
}
.background(.bar)
}
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)
}
}