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:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<KassenzettelZeile>) -> 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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)" } ?? ""))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user