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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-30 19:05:03 +02:00
parent 648b8294b8
commit 1648d1b917
10 changed files with 85 additions and 64 deletions

View File

@@ -32,7 +32,7 @@ struct AssignScanView: View {
.font(.caption).padding(.top, 2) .font(.caption).padding(.top, 2)
} }
.padding().frame(maxWidth: .infinity) .padding().frame(maxWidth: .infinity)
.background(Color.accentColor.opacity(0.15)) .background(Color.marke.opacity(0.15))
.clipShape(RoundedRectangle(cornerRadius: 12)).padding(.horizontal) .clipShape(RoundedRectangle(cornerRadius: 12)).padding(.horizontal)
} else { } else {
VStack(spacing: 8) { VStack(spacing: 8) {

View File

@@ -238,9 +238,9 @@ private struct LinienDiagramm: View {
Chart(punkte) { p in Chart(punkte) { p in
if let datum = DisplaySettings.parseTimestamp(p.at) { if let datum = DisplaySettings.parseTimestamp(p.at) {
LineMark(x: .value("Zeit", datum), y: .value("Bestand", p.articleUnits)) 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)) 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) .frame(height: 170)

View File

@@ -503,12 +503,12 @@ struct KassenzettelMarkierenView: View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
Image(uiImage: bild).resizable().scaledToFit() Image(uiImage: bild).resizable().scaledToFit()
.frame(width: geo.size.width, height: geo.size.height) .frame(width: geo.size.width, height: geo.size.height)
Rectangle().stroke(Color.accentColor, lineWidth: 2) Rectangle().stroke(Color.marke, lineWidth: 2)
.background(Color.accentColor.opacity(0.15)) .background(Color.marke.opacity(0.15))
.frame(width: rect.width, height: rect.height) .frame(width: rect.width, height: rect.height)
.offset(x: rect.minX, y: rect.minY) .offset(x: rect.minX, y: rect.minY)
.gesture(zieh(resize: false)) .gesture(zieh(resize: false))
Rectangle().fill(Color.accentColor) Rectangle().fill(Color.marke)
.frame(width: 26, height: 26) .frame(width: 26, height: 26)
.offset(x: rect.maxX - 13, y: rect.maxY - 13) .offset(x: rect.maxX - 13, y: rect.maxY - 13)
.gesture(zieh(resize: true)) .gesture(zieh(resize: true))
@@ -608,7 +608,14 @@ struct KassenzettelPruefenView: View {
}.padding() }.padding()
} else { } else {
List { 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 { Section {
Button { Task { await einlagern() } } label: { Button { Task { await einlagern() } } label: {
HStack { HStack {
@@ -620,6 +627,7 @@ struct KassenzettelPruefenView: View {
Text("Nicht zugeordnete Zeilen werden übersprungen. Neue Artikel werden nicht angelegt.") Text("Nicht zugeordnete Zeilen werden übersprungen. Neue Artikel werden nicht angelegt.")
} }
} }
.listStyle(.insetGrouped)
} }
} }
.onAppear(perform: aufbauen) .onAppear(perform: aufbauen)
@@ -637,47 +645,49 @@ struct KassenzettelPruefenView: View {
} message: { Text(meldung ?? "") } } 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<KassenzettelZeile>) -> some View { @ViewBuilder private func zeile(_ z: Binding<KassenzettelZeile>) -> some View {
let zeile = z.wrappedValue let zeile = z.wrappedValue
VStack(alignment: .leading, spacing: 8) { // Artikel wählen
Text(zeile.text).font(.caption).foregroundStyle(.secondary).lineLimit(1) Menu {
Menu { ForEach(zeile.kandidaten) { k in
ForEach(zeile.kandidaten) { k in Button("\(k.name) · \(k.score) %") {
Button("\(k.name) · \(k.score) %") { z.wrappedValue.artikel = KassenzettelArtikel(id: k.productId, name: k.name, unit: k.checkInUnit)
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)
} }
} }
HStack(spacing: 12) { Divider()
Stepper("Menge \(Int(zeile.menge))", value: z.menge, in: 1...99).fixedSize() 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() Spacer()
Menu { Image(systemName: "chevron.up.chevron.down").font(.caption2).foregroundStyle(.secondary)
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)
} }
}.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 { private func ortName(_ id: String?) -> String {

View File

@@ -81,7 +81,7 @@ struct QuickBookingCard: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(einlagern ? Color.accentColor : .orange) .tint(einlagern ? Color.marke : .orange)
.disabled(!bereit) .disabled(!bereit)
} }
@@ -171,10 +171,10 @@ struct StatusCard: View {
switch art { switch art {
case "kpi-products": case "kpi-products":
Kennzahl(wert: String(stats.productsInStock), label: "Artikel mit Bestand", Kennzahl(wert: String(stats.productsInStock), label: "Artikel mit Bestand",
detail: "von \(stats.productsTotal)", tint: .accentColor) detail: "von \(stats.productsTotal)", tint: .marke)
case "kpi-units": case "kpi-units":
Kennzahl(wert: formatAmount(stats.articleUnits), label: "Artikeleinheiten", Kennzahl(wert: formatAmount(stats.articleUnits), label: "Artikeleinheiten",
detail: "", tint: .accentColor) detail: "", tint: .marke)
default: default:
HStack(alignment: .top, spacing: 12) { HStack(alignment: .top, spacing: 12) {
Kennzahl(wert: String(stats.expiringSoon), label: "Bald ablaufend", Kennzahl(wert: String(stats.expiringSoon), label: "Bald ablaufend",

View File

@@ -55,7 +55,7 @@ struct DateRow: View {
Text(label).foregroundStyle(.primary) Text(label).foregroundStyle(.primary)
Spacer() Spacer()
Text(Self.fmt.string(from: date)) Text(Self.fmt.string(from: date))
.foregroundStyle(open ? Color.accentColor : .secondary) .foregroundStyle(open ? Color.marke : .secondary)
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
} }

View File

@@ -284,7 +284,7 @@ struct MinStockView: View {
if r.isGroup { if r.isGroup {
Text("Gruppe").font(.caption2) Text("Gruppe").font(.caption2)
.padding(.horizontal, 6).padding(.vertical, 1) .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)" } ?? "")) Text("\(r.ort) · Soll \(r.soll)" + (r.bestand.map { " · Bestand \($0)" } ?? ""))

View File

@@ -154,7 +154,7 @@ struct LocalOverview: View {
} }
} label: { } label: {
Text(produkte.first { $0.id == karte.productId }?.name ?? "Artikel wählen") 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) Text("· Artikel").font(.caption).foregroundStyle(.secondary)
} }
Spacer() Spacer()
Image(systemName: "plus.circle").foregroundStyle(Color.accentColor) Image(systemName: "plus.circle").foregroundStyle(Color.marke)
} }
} }
.foregroundStyle(.primary) .foregroundStyle(.primary)

View File

@@ -99,14 +99,25 @@ struct HomeView: View {
.tabItem { Label("Verwaltung", systemImage: "gearshape") } .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. /// Die beiden Kacheln, die frueher die Startseite waren.
struct ScanTabView: View { struct ScanTabView: View {
@EnvironmentObject private var router: Router @EnvironmentObject private var router: Router
@EnvironmentObject private var session: Session @EnvironmentObject private var session: Session
@State private var showNew = false @State private var showNew = false
@State private var showKassenzettel = false
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@@ -132,6 +143,13 @@ struct ScanTabView: View {
subtitle: "Barcode/QR scannen und nur ansehen", subtitle: "Barcode/QR scannen und nur ansehen",
systemImage: "doc.text.magnifyingglass") 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 // Ohne Barcode: gerade bei Gegenstaenden (Kleidung, Werkzeug) gibt
// es keinen Code zum Scannen. Deshalb hier ein offensichtlicher Weg. // es keinen Code zum Scannen. Deshalb hier ein offensichtlicher Weg.
if session.isAdmin { if session.isAdmin {
@@ -152,8 +170,10 @@ struct ScanTabView: View {
} }
Spacer() Spacer()
} }
.buttonStyle(.plain)
.padding() .padding()
.navigationTitle("Scannen") .navigationTitle("Scannen")
.fullScreenCover(isPresented: $showKassenzettel) { KassenzettelView() }
.sheet(isPresented: $showNew) { .sheet(isPresented: $showNew) {
NavigationStack { NavigationStack {
ProductFormView(prefillBarcode: nil, groupId: nil) { _ in ProductFormView(prefillBarcode: nil, groupId: nil) { _ in
@@ -166,8 +186,6 @@ struct ScanTabView: View {
} }
struct ListsTabView: View { struct ListsTabView: View {
@State private var kassenzettelAn = false
var body: some View { var body: some View {
NavigationStack { NavigationStack {
VStack(spacing: 16) { VStack(spacing: 16) {
@@ -215,18 +233,11 @@ struct ListsTabView: View {
ActionTile(title: "Verlauf", subtitle: "Wer hat wann was ein- und ausgelagert", ActionTile(title: "Verlauf", subtitle: "Wer hat wann was ein- und ausgelagert",
systemImage: "clock.arrow.circlepath") systemImage: "clock.arrow.circlepath")
} }
Button {
kassenzettelAn = true
} label: {
ActionTile(title: "Kassenzettel", subtitle: "Bon fotografieren und Lebensmittel einlagern",
systemImage: "doc.text.viewfinder")
}
.buttonStyle(.plain)
Spacer() Spacer()
} }
.buttonStyle(.plain)
.padding() .padding()
.navigationTitle("Listen") .navigationTitle("Listen")
.fullScreenCover(isPresented: $kassenzettelAn) { KassenzettelView() }
} }
} }
} }
@@ -242,7 +253,7 @@ struct CompactTile: View {
Image(systemName: systemImage) Image(systemName: systemImage)
.font(.title2) .font(.title2)
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.background(Color.accentColor.opacity(0.15)) .background(Color.marke.opacity(0.15))
.clipShape(RoundedRectangle(cornerRadius: 10)) .clipShape(RoundedRectangle(cornerRadius: 10))
Text(title).font(.headline) Text(title).font(.headline)
} }
@@ -264,7 +275,7 @@ struct ActionTile: View {
Image(systemName: systemImage) Image(systemName: systemImage)
.font(.title2) .font(.title2)
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.background(Color.accentColor.opacity(0.15)) .background(Color.marke.opacity(0.15))
.clipShape(RoundedRectangle(cornerRadius: 10)) .clipShape(RoundedRectangle(cornerRadius: 10))
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(title).font(.headline) Text(title).font(.headline)

View File

@@ -67,7 +67,7 @@ struct TorchButton: View {
isOn.toggle() isOn.toggle()
} label: { } label: {
Image(systemName: isOn ? "bolt.fill" : "bolt.slash.fill") Image(systemName: isOn ? "bolt.fill" : "bolt.slash.fill")
.foregroundStyle(locked ? Color.yellow : Color.accentColor) .foregroundStyle(locked ? Color.yellow : Color.marke)
} }
.simultaneousGesture( .simultaneousGesture(
LongPressGesture(minimumDuration: 0.4).onEnded { _ in LongPressGesture(minimumDuration: 0.4).onEnded { _ in

View File

@@ -68,7 +68,7 @@ struct ServerListView: View {
} label: { } label: {
Image(systemName: NotificationStore.config(for: profile.id).enabled Image(systemName: NotificationStore.config(for: profile.id).enabled
? "bell.fill" : "bell") ? "bell.fill" : "bell")
.foregroundStyle(Color.accentColor) .foregroundStyle(Color.marke)
} }
.buttonStyle(.borderless) .buttonStyle(.borderless)
} }
@@ -138,7 +138,7 @@ struct ServerListView: View {
} }
Spacer() Spacer()
if profile.id == session.activeProfileID { if profile.id == session.activeProfileID {
Image(systemName: "checkmark").foregroundStyle(Color.accentColor) Image(systemName: "checkmark").foregroundStyle(Color.marke)
} }
} }
.foregroundStyle(.primary) .foregroundStyle(.primary)