Files
Vorrania/ios/Sources/AssignScanView.swift
Scarriffle 1648d1b917 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>
2026-07-30 19:05:03 +02:00

149 lines
6.3 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
/// Zuordnen per Scan: erst ein Einzelstück (/i/<UID>) scannen, dann einen
/// Lagerort (/l/<ID>) das Stück wird dem Ort zugewiesen. Danach gleich das
/// nächste Stück, ohne Tippen.
struct AssignScanView: View {
@Environment(\.dismiss) private var dismiss
@State private var paused = false
@State private var torchOn = false
@State private var torchLocked = false
@State private var pending: Item? // gescanntes/gewähltes Stück, wartet auf Ort
@State private var status: String?
@State private var error: String?
@State private var showPicker = false
var body: some View {
ScrollView {
VStack(spacing: 14) {
ScannerView(onCode: { code in Task { await handle(code) } },
isPaused: $paused, torchOn: $torchOn, torchLocked: $torchLocked)
.aspectRatio(4.0 / 3.0, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 16))
.padding(.horizontal)
if let pending {
VStack(spacing: 4) {
Text("Einzelstück \(pending.uid)").font(.headline)
Text("Jetzt den Lagerort-QR scannen")
.font(.caption).foregroundStyle(.secondary)
Button("Abbrechen") { self.pending = nil }
.font(.caption).padding(.top, 2)
}
.padding().frame(maxWidth: .infinity)
.background(Color.marke.opacity(0.15))
.clipShape(RoundedRectangle(cornerRadius: 12)).padding(.horizontal)
} else {
VStack(spacing: 8) {
Text("Schritt 1: Einzelstück-QR scannen oder ohne Code:")
.font(.caption).foregroundStyle(.secondary)
// Für Stücke ohne eigenen QR (z.B. Kleidung): aus der Liste wählen.
Button { showPicker = true } label: {
Label("Einzelstück aus Liste wählen", systemImage: "list.bullet")
}
}
}
if let status {
Text(status).font(.callout).foregroundStyle(.green)
.frame(maxWidth: .infinity, alignment: .leading).padding(.horizontal)
}
if let error {
Text(error).font(.callout).foregroundStyle(.red)
.frame(maxWidth: .infinity, alignment: .leading).padding(.horizontal)
}
}
.padding(.vertical)
}
.navigationTitle("Ort zuordnen")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } }
ToolbarItem(placement: .topBarTrailing) { TorchButton(isOn: $torchOn, locked: $torchLocked) }
}
.sheet(isPresented: $showPicker) {
ItemPickerSheet { it in pending = it; status = nil; error = nil }
}
}
private func handle(_ code: String) async {
// Lagerort-QR (/l/<Code>)? Der Code ist eine 10-stellige Zeichenkette.
if let r = code.range(of: "/l/") {
let locCode = String(code[r.upperBound...])
.trimmingCharacters(in: CharacterSet(charactersIn: "/ "))
.uppercased()
if !locCode.isEmpty {
guard let it = pending else {
error = "Erst ein Einzelstück scannen."; status = nil; return
}
do {
_ = try await APIClient.shared.updateItem(id: it.id, ItemUpdateRequest(
locationId: locCode, shopId: it.shopId,
acquiredOn: it.acquiredOn, warrantyUntil: it.warrantyUntil, note: it.note))
status = "\(it.uid) zugeordnet. Nächstes Stück scannen."
error = nil
pending = nil
} catch { self.error = error.localizedDescription }
return
}
}
// Einzelstück-QR (/i/<UID>)?
if let r = code.range(of: "/i/") {
let uid = String(code[r.upperBound...])
.trimmingCharacters(in: CharacterSet(charactersIn: "/ ")).uppercased()
if !uid.isEmpty, let it = try? await APIClient.shared.itemByUid(uid: uid) {
pending = it; status = nil; error = nil
return
}
}
error = "Unbekannter Code bitte einen Einzelstück- oder Lagerort-QR scannen."
}
}
/// Einzelstück aus einer durchsuchbaren Liste wählen für Stücke ohne eigenen QR.
struct ItemPickerSheet: View {
var onPick: (Item) -> Void
@Environment(\.dismiss) private var dismiss
@State private var items: [Item] = []
@State private var locations: [StorageLocation] = []
@State private var query = ""
@State private var busy = true
private var gefiltert: [Item] {
let q = query.trimmingCharacters(in: .whitespaces).lowercased()
guard !q.isEmpty else { return items }
return items.filter {
[$0.productName, $0.uid, $0.productBrand, $0.note]
.compactMap { $0 }.joined(separator: " ").lowercased().contains(q)
}
}
var body: some View {
NavigationStack {
List {
if busy && items.isEmpty { ProgressView() }
ForEach(gefiltert) { it in
Button { onPick(it); dismiss() } label: {
ItemRow(item: it, locations: locations)
}
.buttonStyle(.plain)
}
if !busy && gefiltert.isEmpty {
Text("Nichts gefunden.").foregroundStyle(.secondary)
}
}
.searchable(text: $query, prompt: "Produkt, UID oder Notiz")
.navigationTitle("Einzelstück wählen")
.navigationBarTitleDisplayMode(.inline)
.toolbar { ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } } }
.task {
items = (try? await APIClient.shared.allItems()) ?? []
locations = (try? await APIClient.shared.locations()) ?? []
busy = false
}
}
}
}