import SwiftUI /// Artikel per Namenssuche auswählen (Alternative zum Scannen). struct ProductSearchView: View { var onPick: (Product) -> Void @Environment(\.dismiss) private var dismiss @State private var query = "" @State private var results: [Product] = [] @State private var busy = false var body: some View { List { ForEach(results) { product in Button { onPick(product) dismiss() } label: { VStack(alignment: .leading, spacing: 2) { Text(product.name) Text("\(formatted(product.stockInArticleUnits)) \(product.articleUnitLabel)") .font(.caption).foregroundStyle(.secondary) } } } if results.isEmpty && !busy { Text("Keine Treffer").foregroundStyle(.secondary) } } .searchable(text: $query, prompt: "Artikel suchen") .onSubmit(of: .search) { Task { await search() } } .task { await search() } .onChange(of: query) { _ in Task { await search() } } .navigationTitle("Artikel suchen") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } } } } private func formatted(_ value: Double) -> String { value == value.rounded() ? String(Int(value)) : String(format: "%.2f", value) } private func search() async { busy = true defer { busy = false } results = (try? await APIClient.shared.searchProducts(query)) ?? [] } } /// Neues Produkt anlegen – bei Bedarf mit den Daten von Open Food Facts vorbefüllt. struct ProductFormView: View { let prefillBarcode: String? let groupId: Int? var suggestion: LookupResult.Suggestion? var onCreated: (Product) -> Void @Environment(\.dismiss) private var dismiss @EnvironmentObject private var session: Session @State private var barcode = "" @State private var name = "" @State private var brand = "" @State private var baseUnit = "piece" @State private var packageSize = "" @State private var packageLabel = "" @State private var datePrecision = "day" @State private var selectedGroupId: Int? @State private var selectedCategoryId: Int? @State private var groups: [GroupItem] = [] @State private var categories: [CategoryItem] = [] @State private var units: [Unit] = [] @State private var newGroupShown = false @State private var busy = false @State private var error: String? private let baseUnits = [("piece", "Stück"), ("gram", "Gramm"), ("milliliter", "Milliliter")] // Beschriftungen kommen sonst aus DisplaySettings.baseUnitLabel. private let labels = ["", "Glas", "Tüte", "Flasche", "Dose", "Tube", "Becher", "Beutel", "Karton"] var body: some View { Form { Section("Artikel") { LabeledField(label: "Barcode", text: $barcode, keyboard: .numberPad) LabeledField(label: "Name", text: $name) LabeledField(label: "Marke", text: $brand) } Section { CategoryPicker(categories: categories, selection: $selectedCategoryId) } footer: { Text("Kategorie: nur für den Überblick in der Artikelliste.") } Section { Picker("Gruppe", selection: $selectedGroupId) { Text("– keine –").tag(Int?.none) ForEach(groups) { gruppe in Text(gruppe.name).tag(Int?.some(gruppe.id)) } } if session.isAdmin { Button { newGroupShown = true } label: { Label("Neue Gruppe anlegen", systemImage: "plus") } } } footer: { Text("Gruppe: zählt Bestände mehrerer Marken zusammen. Der EAN-Code " + "dieses Artikels erscheint danach automatisch bei der Gruppe.") } Section("Einheit") { Picker("Basiseinheit", selection: $baseUnit) { ForEach(baseUnits, id: \.0) { Text($0.1).tag($0.0) } } QuantityField(label: "Packungsgröße", text: $packageSize, suffix: DisplaySettings.baseUnitLabel(baseUnit)) Picker("Bezeichnung", selection: $packageLabel) { ForEach(labels, id: \.self) { Text($0.isEmpty ? "Packung (Standard)" : $0).tag($0) } } } Section("MHD") { Picker("Angabe", selection: $datePrecision) { Text("Tagesdatum").tag("day") Text("nur Monat/Jahr").tag("month") } } if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } } Section { Button(busy ? "Anlegen…" : "Anlegen & weiter") { Task { await create() } } .disabled(busy || name.isEmpty) } } .navigationTitle("Neuer Artikel") .navigationBarTitleDisplayMode(.inline) .onAppear(perform: prefill) .task { groups = (try? await APIClient.shared.groups()) ?? [] categories = (try? await APIClient.shared.categories()) ?? [] units = (try? await APIClient.shared.units()) ?? [] } .sheet(isPresented: $newGroupShown) { NavigationStack { NewGroupSheet(units: units) { created in groups.append(created) selectedGroupId = created.id } } } } private func prefill() { barcode = suggestion?.barcode ?? prefillBarcode ?? "" // Aus dem gescannten Code vorgeschlagene Gruppe uebernehmen. selectedGroupId = groupId if let suggestion { name = suggestion.name brand = suggestion.brand ?? "" baseUnit = suggestion.baseUnit ?? "piece" if let size = suggestion.packageSize { packageSize = String(size) } } } private func create() async { error = nil busy = true defer { busy = false } do { let product = try await APIClient.shared.createProduct( NewProductRequest( barcode: barcode.isEmpty ? nil : barcode, name: name, brand: brand.isEmpty ? nil : brand, imageUrl: suggestion?.imageUrl, baseUnit: baseUnit, packageSize: Double(packageSize.replacingOccurrences(of: ",", with: ".")), packageLabel: packageLabel.isEmpty ? nil : packageLabel, datePrecision: datePrecision, groupId: selectedGroupId, categoryId: selectedCategoryId ) ) onCreated(product) dismiss() } catch { self.error = error.localizedDescription } } }