import SwiftUI struct CheckInView: View { @Environment(\.dismiss) private var dismiss @State private var paused = false @State private var torchOn = false @State private var torchLocked = false @State private var status: String? @State private var error: String? @State private var product: Product? @State private var scannedItem: Item? @State private var suggestion: LookupResult.Suggestion? @State private var suggestedGroupId: Int? @State private var suggestedGroupName: String? @State private var suggestedCategoryId: Int? @State private var suggestedCategoryName: String? @State private var unknownCode: String? @State private var manualCodeShown = false @State private var manualCode = "" @State private var units: [Unit] = [] @State private var locations: [StorageLocation] = [] var body: some View { // Kamera bewusst nur als Feld oben statt bildschirmfuellend - der // Sucher ueber die ganze Flaeche wirkte erschlagend. ScrollView { VStack(spacing: 14) { ScannerView(onCode: { code in Task { await resolve(code) } }, isPaused: $paused, torchOn: $torchOn, torchLocked: $torchLocked) .aspectRatio(4.0 / 3.0, contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 16)) .padding(.horizontal) Text("Barcode vor die Kamera halten") .font(.caption).foregroundStyle(.secondary) if let status { banner(status, color: .green) } if let error { banner(error, color: .red) } if let suggestion { suggestionCard(suggestion) } else if let unknownCode { unknownCard(unknownCode) } VStack(spacing: 10) { Button { paused = true manualCodeShown = true } label: { Label("EAN manuell", systemImage: "keyboard") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) NavigationLink { ProductFormView(prefillBarcode: nil, groupId: nil) { created in product = created } } label: { Label("Artikel anlegen", systemImage: "plus") .frame(maxWidth: .infinity) } .buttonStyle(.bordered) } .padding(.horizontal) } .padding(.vertical) } .navigationTitle("Einlagern") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } } ToolbarItem(placement: .topBarTrailing) { TorchButton(isOn: $torchOn, locked: $torchLocked) } } .task { units = (try? await APIClient.shared.units()) ?? [] locations = (try? await APIClient.shared.locations()) ?? [] } .alert("EAN eingeben", isPresented: $manualCodeShown) { TextField("z.B. 8076809572569", text: $manualCode) .keyboardType(.numberPad) Button("Suchen") { let code = manualCode manualCode = "" Task { await resolve(code) } } Button("Abbrechen", role: .cancel) { paused = false } } .sheet(item: $product) { item in NavigationStack { CheckInFormView(product: item, units: units, locations: locations) { message in status = message product = nil paused = false } } } .sheet(item: $scannedItem, onDismiss: { paused = false }) { it in NavigationStack { ItemEditView(item: it) } } } // MARK: - Bausteine private func banner(_ text: String, color: Color) -> some View { Text(text) .font(.callout) .padding(10) .frame(maxWidth: .infinity, alignment: .leading) .background(color.opacity(0.9)) .foregroundStyle(.white) .clipShape(RoundedRectangle(cornerRadius: 10)) .padding(.horizontal) } /// Sagt vor dem Anlegen, welche Gruppe der Artikel bekommt und warum. @ViewBuilder private func groupNote() -> some View { if let kategorie = suggestedCategoryName { noteRow(symbol: "square.grid.2x2", title: "Wird der Kategorie „\(kategorie)“ zugeordnet", detail: "Aus der Produkt-Datenbank vorgeschlagen – im Formular änderbar.") } if let name = suggestedGroupName { noteRow(symbol: "tag", title: "Wird der Gruppe „\(name)“ zugeordnet", detail: "Dieser EAN-Code ist dort hinterlegt.") } } private func noteRow(symbol: String, title: String, detail: String) -> some View { HStack(alignment: .top, spacing: 6) { Image(systemName: symbol) VStack(alignment: .leading, spacing: 1) { Text(title).font(.caption).bold() Text(detail).font(.caption2).foregroundStyle(.secondary) } } .padding(8) .frame(maxWidth: .infinity, alignment: .leading) .background(Color.accentColor.opacity(0.15)) .clipShape(RoundedRectangle(cornerRadius: 8)) } private func suggestionCard(_ item: LookupResult.Suggestion) -> some View { VStack(alignment: .leading, spacing: 8) { Text(item.name).font(.headline) Text([item.brand, item.quantityText].compactMap { $0 }.joined(separator: " · ")) .font(.caption).foregroundStyle(.secondary) Text("In einer Produkt-Datenbank gefunden, noch nicht im Katalog.") .font(.caption2).foregroundStyle(.secondary) groupNote() NavigationLink { ProductFormView(prefillBarcode: item.barcode, groupId: suggestedGroupId, categoryId: suggestedCategoryId, suggestion: item) { created in suggestion = nil product = created } } label: { Label("Anlegen & einlagern", systemImage: "plus.circle.fill") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) } .padding() .background(.thinMaterial) .clipShape(RoundedRectangle(cornerRadius: 12)) .padding(.horizontal) } private func unknownCard(_ code: String) -> some View { VStack(alignment: .leading, spacing: 8) { Text("Unbekannter Code \(code)").font(.headline) Text("Weder im Katalog noch in Open Food / Products Facts.") .font(.caption).foregroundStyle(.secondary) groupNote() NavigationLink { ProductFormView(prefillBarcode: code, groupId: suggestedGroupId, categoryId: suggestedCategoryId) { created in unknownCode = nil product = created } } label: { Label("Artikel anlegen", systemImage: "plus") .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) } .padding() .background(.thinMaterial) .clipShape(RoundedRectangle(cornerRadius: 12)) .padding(.horizontal) } // MARK: - Logik private func resolve(_ code: String) async { paused = true error = nil status = nil suggestion = nil unknownCode = nil do { // Einzelstück-QR (…/i/) → direkt das Stück öffnen statt Barcode-Suche. if let r = code.range(of: "/i/") { let uid = String(code[r.upperBound...]) .trimmingCharacters(in: CharacterSet(charactersIn: "/ ")) .uppercased() if !uid.isEmpty, let item = try? await APIClient.shared.itemByUid(uid: uid) { scannedItem = item return } } let result = try await APIClient.shared.lookup(barcode: code) suggestedGroupId = result.groupId suggestedGroupName = result.groupName suggestedCategoryId = result.categoryId suggestedCategoryName = result.categoryName if let existing = result.existingProduct { product = existing } else if let hint = result.suggestion { suggestion = hint } else { unknownCode = code } } catch { self.error = error.localizedDescription paused = false } } }