Files
Vorrania/ios/Sources/ProductViews.swift
Scarriffle b2a38501d2 Vorgeschlagene Kategorie wird uebernommen, nicht nur angezeigt
Beim Scannen stand auf der Karte "Kategorie ... vorgeschlagen", im Formular war
das Feld danach trotzdem leer - man musste dieselbe Kategorie von Hand
nochmal auswaehlen. Der Vorschlag war damit wertlos: Wer weiss, dass Pesto eine
Sosse ist, gewinnt nichts daraus, es dem Formular noch einmal zu erzaehlen.

Die Ursache lag nicht am Server. Der Lookup liefert category_id und
category_name; die App merkte sich aber nur den Namen fuer die Anzeige und warf
die ID weg. ProductFormView bekam sie gar nicht erst gereicht - anders als die
Gruppe, die schon immer uebernommen wurde. Die Web-Oberflaeche macht es seit
jeher richtig (setNewCategoryId in pages/CheckIn.jsx), nur die App nicht.

Jetzt wird die ID durchgereicht und wie die Gruppe vorbelegt. Aenderbar bleibt
sie: Gespeichert wird ohnehin erst mit "Anlegen & weiter". Der Hinweis auf der
Karte heisst deshalb nicht mehr "vorgeschlagen", sondern "wird zugeordnet" -
das war vorher richtig und waere es jetzt nicht mehr.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 18:48:54 +02:00

197 lines
7.5 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
/// 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?
/// Aus der Open-Food-Facts-Einordnung vorgeschlagene Kategorie.
var categoryId: 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 und Kategorie
// uebernehmen. Beides bleibt aenderbar - gespeichert wird erst mit
// "Anlegen & weiter".
selectedGroupId = groupId
selectedCategoryId = categoryId
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
}
}
}