From 1648d1b917f95df591fb3b4a59494367bf16bf5d Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Thu, 30 Jul 2026 19:05:03 +0200 Subject: [PATCH] iOS: Kassenzettel in den Scannen-Tab, Markenblau statt System-Blau, weisse Kacheln, luftigere Pruefen-Liste - Kassenzettel-Kachel vom Listen- in den Scannen-Tab verschoben (passt thematisch). - App-Akzent auf Markenblau (#3F51B5/#7C8AE8, wie Web/App-Icon) via .tint; alle Color.accentColor -> Color.marke. System-Blau verschwindet. - Kachel-Buttons/Links auf .buttonStyle(.plain): weisse Schrift/Symbole statt getoentem Blau. - Kassenzettel-Pruefen-Liste: je Artikel eine eigene Section (insetGrouped) mit Artikel/Menge/Lagerort in eigenen Zeilen - klar gruppiert, nicht mehr gequetscht. Co-Authored-By: Claude Opus 4.8 --- ios/Sources/AssignScanView.swift | 2 +- ios/Sources/ChartCards.swift | 4 +- ios/Sources/CheckInView.swift | 88 ++++++++++++++++++-------------- ios/Sources/DashboardCards.swift | 6 +-- ios/Sources/ItemViews.swift | 2 +- ios/Sources/ListViews.swift | 2 +- ios/Sources/LocalOverview.swift | 4 +- ios/Sources/RootView.swift | 35 ++++++++----- ios/Sources/ScannerView.swift | 2 +- ios/Sources/ServerListView.swift | 4 +- 10 files changed, 85 insertions(+), 64 deletions(-) diff --git a/ios/Sources/AssignScanView.swift b/ios/Sources/AssignScanView.swift index 0654edb..6db3f17 100644 --- a/ios/Sources/AssignScanView.swift +++ b/ios/Sources/AssignScanView.swift @@ -32,7 +32,7 @@ struct AssignScanView: View { .font(.caption).padding(.top, 2) } .padding().frame(maxWidth: .infinity) - .background(Color.accentColor.opacity(0.15)) + .background(Color.marke.opacity(0.15)) .clipShape(RoundedRectangle(cornerRadius: 12)).padding(.horizontal) } else { VStack(spacing: 8) { diff --git a/ios/Sources/ChartCards.swift b/ios/Sources/ChartCards.swift index d2156cd..edfb9f7 100644 --- a/ios/Sources/ChartCards.swift +++ b/ios/Sources/ChartCards.swift @@ -238,9 +238,9 @@ private struct LinienDiagramm: View { Chart(punkte) { p in if let datum = DisplaySettings.parseTimestamp(p.at) { LineMark(x: .value("Zeit", datum), y: .value("Bestand", p.articleUnits)) - .foregroundStyle(Color.accentColor) + .foregroundStyle(Color.marke) AreaMark(x: .value("Zeit", datum), y: .value("Bestand", p.articleUnits)) - .foregroundStyle(Color.accentColor.opacity(0.12)) + .foregroundStyle(Color.marke.opacity(0.12)) } } .frame(height: 170) diff --git a/ios/Sources/CheckInView.swift b/ios/Sources/CheckInView.swift index d7bccf0..9301bd1 100644 --- a/ios/Sources/CheckInView.swift +++ b/ios/Sources/CheckInView.swift @@ -503,12 +503,12 @@ struct KassenzettelMarkierenView: View { ZStack(alignment: .topLeading) { Image(uiImage: bild).resizable().scaledToFit() .frame(width: geo.size.width, height: geo.size.height) - Rectangle().stroke(Color.accentColor, lineWidth: 2) - .background(Color.accentColor.opacity(0.15)) + Rectangle().stroke(Color.marke, lineWidth: 2) + .background(Color.marke.opacity(0.15)) .frame(width: rect.width, height: rect.height) .offset(x: rect.minX, y: rect.minY) .gesture(zieh(resize: false)) - Rectangle().fill(Color.accentColor) + Rectangle().fill(Color.marke) .frame(width: 26, height: 26) .offset(x: rect.maxX - 13, y: rect.maxY - 13) .gesture(zieh(resize: true)) @@ -608,7 +608,14 @@ struct KassenzettelPruefenView: View { }.padding() } else { List { - ForEach($zeilen) { $z in zeile($z) } + ForEach($zeilen) { $z in + Section { + zeile($z) + } header: { + Text(z.text) + .textCase(nil).font(.footnote).foregroundStyle(.secondary) + } + } Section { Button { Task { await einlagern() } } label: { HStack { @@ -620,6 +627,7 @@ struct KassenzettelPruefenView: View { Text("Nicht zugeordnete Zeilen werden übersprungen. Neue Artikel werden nicht angelegt.") } } + .listStyle(.insetGrouped) } } .onAppear(perform: aufbauen) @@ -637,47 +645,49 @@ struct KassenzettelPruefenView: View { } message: { Text(meldung ?? "") } } + /// Drei Zeilen (Rows) je Artikel: Auswahl, Menge, Lagerort. In einer eigenen + /// Section gruppiert – so ist klar, was zusammengehört, und nichts ist gequetscht. @ViewBuilder private func zeile(_ z: Binding) -> some View { let zeile = z.wrappedValue - VStack(alignment: .leading, spacing: 8) { - Text(zeile.text).font(.caption).foregroundStyle(.secondary).lineLimit(1) - Menu { - ForEach(zeile.kandidaten) { k in - Button("\(k.name) · \(k.score) %") { - z.wrappedValue.artikel = KassenzettelArtikel(id: k.productId, name: k.name, unit: k.checkInUnit) - } - } - Divider() - Button { sucheZiel = ZeilenRef(id: zeile.id) } label: { Label("Suchen…", systemImage: "magnifyingglass") } - if zeile.artikel != nil { - Button(role: .destructive) { z.wrappedValue.artikel = nil } label: { - Label("Nicht zuordnen", systemImage: "xmark") - } - } - } label: { - HStack { - Image(systemName: zeile.artikel == nil ? "questionmark.circle" : "checkmark.circle.fill") - .foregroundStyle(zeile.artikel == nil ? Color.secondary : Color.green) - Text(zeile.artikel?.name ?? "– Artikel wählen –") - .foregroundStyle(zeile.artikel == nil ? Color.secondary : Color.primary) - Spacer() - Image(systemName: "chevron.up.chevron.down").font(.caption2).foregroundStyle(.secondary) + // Artikel wählen + Menu { + ForEach(zeile.kandidaten) { k in + Button("\(k.name) · \(k.score) %") { + z.wrappedValue.artikel = KassenzettelArtikel(id: k.productId, name: k.name, unit: k.checkInUnit) } } - HStack(spacing: 12) { - Stepper("Menge \(Int(zeile.menge))", value: z.menge, in: 1...99).fixedSize() + Divider() + Button { sucheZiel = ZeilenRef(id: zeile.id) } label: { Label("Suchen…", systemImage: "magnifyingglass") } + if zeile.artikel != nil { + Button(role: .destructive) { z.wrappedValue.artikel = nil } label: { + Label("Nicht zuordnen", systemImage: "xmark") + } + } + } label: { + HStack { + Image(systemName: zeile.artikel == nil ? "questionmark.circle" : "checkmark.circle.fill") + .foregroundStyle(zeile.artikel == nil ? Color.secondary : Color.green) + Text(zeile.artikel?.name ?? "– Artikel wählen –") + .foregroundStyle(zeile.artikel == nil ? Color.secondary : Color.primary) Spacer() - Menu { - Button("– ohne –") { z.wrappedValue.ortId = nil } - ForEach(locations) { loc in Button(loc.name) { z.wrappedValue.ortId = loc.id } } - } label: { - Label(ortName(zeile.ortId), systemImage: "mappin.and.ellipse").font(.callout).lineLimit(1) - } - Button { scanZiel = ZeilenRef(id: zeile.id) } label: { - Image(systemName: "qrcode.viewfinder") - }.buttonStyle(.borderless) + Image(systemName: "chevron.up.chevron.down").font(.caption2).foregroundStyle(.secondary) } - }.padding(.vertical, 4) + } + // Menge + Stepper("Menge: \(Int(zeile.menge))", value: z.menge, in: 1...99) + // Lagerort + QR-Scan + HStack { + Menu { + Button("– ohne –") { z.wrappedValue.ortId = nil } + ForEach(locations) { loc in Button(loc.name) { z.wrappedValue.ortId = loc.id } } + } label: { + Label(ortName(zeile.ortId), systemImage: "mappin.and.ellipse") + } + Spacer() + Button { scanZiel = ZeilenRef(id: zeile.id) } label: { + Label("QR", systemImage: "qrcode.viewfinder") + }.buttonStyle(.borderless) + } } private func ortName(_ id: String?) -> String { diff --git a/ios/Sources/DashboardCards.swift b/ios/Sources/DashboardCards.swift index 1e0925e..4481f8d 100644 --- a/ios/Sources/DashboardCards.swift +++ b/ios/Sources/DashboardCards.swift @@ -81,7 +81,7 @@ struct QuickBookingCard: View { .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) - .tint(einlagern ? Color.accentColor : .orange) + .tint(einlagern ? Color.marke : .orange) .disabled(!bereit) } @@ -171,10 +171,10 @@ struct StatusCard: View { switch art { case "kpi-products": Kennzahl(wert: String(stats.productsInStock), label: "Artikel mit Bestand", - detail: "von \(stats.productsTotal)", tint: .accentColor) + detail: "von \(stats.productsTotal)", tint: .marke) case "kpi-units": Kennzahl(wert: formatAmount(stats.articleUnits), label: "Artikeleinheiten", - detail: "", tint: .accentColor) + detail: "", tint: .marke) default: HStack(alignment: .top, spacing: 12) { Kennzahl(wert: String(stats.expiringSoon), label: "Bald ablaufend", diff --git a/ios/Sources/ItemViews.swift b/ios/Sources/ItemViews.swift index 8485b02..e07812f 100644 --- a/ios/Sources/ItemViews.swift +++ b/ios/Sources/ItemViews.swift @@ -55,7 +55,7 @@ struct DateRow: View { Text(label).foregroundStyle(.primary) Spacer() Text(Self.fmt.string(from: date)) - .foregroundStyle(open ? Color.accentColor : .secondary) + .foregroundStyle(open ? Color.marke : .secondary) } .contentShape(Rectangle()) } diff --git a/ios/Sources/ListViews.swift b/ios/Sources/ListViews.swift index 074dcb2..965ba4d 100644 --- a/ios/Sources/ListViews.swift +++ b/ios/Sources/ListViews.swift @@ -284,7 +284,7 @@ struct MinStockView: View { if r.isGroup { Text("Gruppe").font(.caption2) .padding(.horizontal, 6).padding(.vertical, 1) - .background(Color.accentColor.opacity(0.2), in: Capsule()) + .background(Color.marke.opacity(0.2), in: Capsule()) } } Text("\(r.ort) · Soll \(r.soll)" + (r.bestand.map { " · Bestand \($0)" } ?? "")) diff --git a/ios/Sources/LocalOverview.swift b/ios/Sources/LocalOverview.swift index 5d2a5bc..76f10a0 100644 --- a/ios/Sources/LocalOverview.swift +++ b/ios/Sources/LocalOverview.swift @@ -154,7 +154,7 @@ struct LocalOverview: View { } } label: { Text(produkte.first { $0.id == karte.productId }?.name ?? "Artikel wählen") - .font(.caption).foregroundStyle(Color.accentColor) + .font(.caption).foregroundStyle(Color.marke) } } @@ -240,7 +240,7 @@ struct CardPickerView: View { Text("· Artikel").font(.caption).foregroundStyle(.secondary) } Spacer() - Image(systemName: "plus.circle").foregroundStyle(Color.accentColor) + Image(systemName: "plus.circle").foregroundStyle(Color.marke) } } .foregroundStyle(.primary) diff --git a/ios/Sources/RootView.swift b/ios/Sources/RootView.swift index cd4f1f4..c20055a 100644 --- a/ios/Sources/RootView.swift +++ b/ios/Sources/RootView.swift @@ -99,14 +99,25 @@ struct HomeView: View { .tabItem { Label("Verwaltung", systemImage: "gearshape") } } } + .tint(Color.marke) } } +extension Color { + /// Markenblau (wie App-Icon und Web-Akzent) statt des System-Blaus. + static let marke = Color(UIColor { trait in + trait.userInterfaceStyle == .dark + ? UIColor(red: 0.486, green: 0.541, blue: 0.910, alpha: 1) // #7C8AE8 + : UIColor(red: 0.247, green: 0.318, blue: 0.710, alpha: 1) // #3F51B5 + }) +} + /// Die beiden Kacheln, die frueher die Startseite waren. struct ScanTabView: View { @EnvironmentObject private var router: Router @EnvironmentObject private var session: Session @State private var showNew = false + @State private var showKassenzettel = false var body: some View { NavigationStack { @@ -132,6 +143,13 @@ struct ScanTabView: View { subtitle: "Barcode/QR scannen und nur ansehen", systemImage: "doc.text.magnifyingglass") } + Button { + showKassenzettel = true + } label: { + ActionTile(title: "Kassenzettel", + subtitle: "Bon fotografieren und Lebensmittel einlagern", + systemImage: "doc.text.viewfinder") + } // Ohne Barcode: gerade bei Gegenstaenden (Kleidung, Werkzeug) gibt // es keinen Code zum Scannen. Deshalb hier ein offensichtlicher Weg. if session.isAdmin { @@ -152,8 +170,10 @@ struct ScanTabView: View { } Spacer() } + .buttonStyle(.plain) .padding() .navigationTitle("Scannen") + .fullScreenCover(isPresented: $showKassenzettel) { KassenzettelView() } .sheet(isPresented: $showNew) { NavigationStack { ProductFormView(prefillBarcode: nil, groupId: nil) { _ in @@ -166,8 +186,6 @@ struct ScanTabView: View { } struct ListsTabView: View { - @State private var kassenzettelAn = false - var body: some View { NavigationStack { VStack(spacing: 16) { @@ -215,18 +233,11 @@ struct ListsTabView: View { ActionTile(title: "Verlauf", subtitle: "Wer hat wann was ein- und ausgelagert", systemImage: "clock.arrow.circlepath") } - Button { - kassenzettelAn = true - } label: { - ActionTile(title: "Kassenzettel", subtitle: "Bon fotografieren und Lebensmittel einlagern", - systemImage: "doc.text.viewfinder") - } - .buttonStyle(.plain) Spacer() } + .buttonStyle(.plain) .padding() .navigationTitle("Listen") - .fullScreenCover(isPresented: $kassenzettelAn) { KassenzettelView() } } } } @@ -242,7 +253,7 @@ struct CompactTile: View { Image(systemName: systemImage) .font(.title2) .frame(width: 44, height: 44) - .background(Color.accentColor.opacity(0.15)) + .background(Color.marke.opacity(0.15)) .clipShape(RoundedRectangle(cornerRadius: 10)) Text(title).font(.headline) } @@ -264,7 +275,7 @@ struct ActionTile: View { Image(systemName: systemImage) .font(.title2) .frame(width: 44, height: 44) - .background(Color.accentColor.opacity(0.15)) + .background(Color.marke.opacity(0.15)) .clipShape(RoundedRectangle(cornerRadius: 10)) VStack(alignment: .leading, spacing: 2) { Text(title).font(.headline) diff --git a/ios/Sources/ScannerView.swift b/ios/Sources/ScannerView.swift index 49d975f..cec0faf 100644 --- a/ios/Sources/ScannerView.swift +++ b/ios/Sources/ScannerView.swift @@ -67,7 +67,7 @@ struct TorchButton: View { isOn.toggle() } label: { Image(systemName: isOn ? "bolt.fill" : "bolt.slash.fill") - .foregroundStyle(locked ? Color.yellow : Color.accentColor) + .foregroundStyle(locked ? Color.yellow : Color.marke) } .simultaneousGesture( LongPressGesture(minimumDuration: 0.4).onEnded { _ in diff --git a/ios/Sources/ServerListView.swift b/ios/Sources/ServerListView.swift index e15448c..03d4357 100644 --- a/ios/Sources/ServerListView.swift +++ b/ios/Sources/ServerListView.swift @@ -68,7 +68,7 @@ struct ServerListView: View { } label: { Image(systemName: NotificationStore.config(for: profile.id).enabled ? "bell.fill" : "bell") - .foregroundStyle(Color.accentColor) + .foregroundStyle(Color.marke) } .buttonStyle(.borderless) } @@ -138,7 +138,7 @@ struct ServerListView: View { } Spacer() if profile.id == session.activeProfileID { - Image(systemName: "checkmark").foregroundStyle(Color.accentColor) + Image(systemName: "checkmark").foregroundStyle(Color.marke) } } .foregroundStyle(.primary)