From 158175f6934785d6329e4abfd7a6027320e373a5 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sun, 16 Aug 2026 09:21:42 +0200 Subject: [PATCH] App: Einheiten-Felder als zusammenhaengender Block Dieselbe Umstellung wie im Web. Der Abschnitt "Artikel" traegt nur noch Name und Marke; alles zur Einheit steht unter "Einheit und Menge" und liest sich als Satz ("1 Packung sind 3 Stueck"). Das Gebinde heisst jetzt auch so statt "Bezeichnung" und steht vor der Packungsgroesse, weil die Zeile darunter es beim Namen nennt. Die Umrechnung heisst nach dem, was sie liefert (Gewicht / Volumen / Stueckzahl), beginnt mit einer einzigen Auswahl und zeigt den Satz erst danach - mit passendem Verb und Ein-/Mehrzahl. Die linke Zahl wird beim Waehlen der Zielart einmalig mit der Packungsgroesse vorbelegt. Das Anlege-Formular kannte die Umrechnung bisher gar nicht; wer in der App einen Artikel anlegte, musste ihn erst speichern und wieder oeffnen. Es hat jetzt denselben Abschnitt und uebernimmt den Vorschlag aus dem Barcode-Lookup ("3 x 80 g"). Reine Oberflaeche - 226 Backend-Tests unveraendert gruen. Co-Authored-By: Claude Opus 5 --- ios/Sources/Models.swift | 14 +++++ ios/Sources/ProductDetailView.swift | 75 ++++++++++++++++++++----- ios/Sources/ProductViews.swift | 87 ++++++++++++++++++++++++++++- 3 files changed, 160 insertions(+), 16 deletions(-) diff --git a/ios/Sources/Models.swift b/ios/Sources/Models.swift index bcc2596..f75dbe9 100644 --- a/ios/Sources/Models.swift +++ b/ios/Sources/Models.swift @@ -277,6 +277,10 @@ struct LookupResult: Codable { let baseUnit: String? let packageSize: Double? let quantityText: String? + /// Aus „3 x 80 g" vorgeschlagene Umrechnung – nur ein Vorschlag. + let secondaryBase: String? + let secondaryCount: Double? + let secondaryAmount: Double? enum CodingKeys: String, CodingKey { case barcode, name, brand @@ -284,6 +288,9 @@ struct LookupResult: Codable { case baseUnit = "base_unit" case packageSize = "package_size" case quantityText = "quantity_text" + case secondaryBase = "secondary_base" + case secondaryCount = "secondary_count" + case secondaryAmount = "secondary_amount" } } } @@ -410,6 +417,10 @@ struct NewProductRequest: Codable { var bulk: Bool? = nil /// Selbst definierte Feldwerte der Kategorie: {feld_id (als Text): Wert}. var fieldValues: [String: String?]? = nil + /// Umrechnung in die andere Art („3 Stück ≙ 250 g"), optional. + var secondaryBase: String? = nil + var secondaryCount: Double? = nil + var secondaryAmount: Double? = nil enum CodingKeys: String, CodingKey { case barcode, name, brand, individual, bulk @@ -421,6 +432,9 @@ struct NewProductRequest: Codable { case groupId = "group_id" case categoryId = "category_id" case fieldValues = "field_values" + case secondaryBase = "secondary_base" + case secondaryCount = "secondary_count" + case secondaryAmount = "secondary_amount" } } diff --git a/ios/Sources/ProductDetailView.swift b/ios/Sources/ProductDetailView.swift index eb3d8cb..7d887c5 100644 --- a/ios/Sources/ProductDetailView.swift +++ b/ios/Sources/ProductDetailView.swift @@ -122,38 +122,44 @@ struct ProductDetailView: View { Section("Artikel") { LabeledField(label: "Name", text: $name) LabeledField(label: "Marke", text: $brand) - if current.foodLike { - QuantityField(label: "Packungsgröße", text: $packageSize, - suffix: display.baseUnitLabel(current.baseUnit)) - Picker("Bezeichnung", selection: $packageLabel) { + } + + // Alles zur Einheit steht zusammen und liest sich als Satz: das + // Gebinde zuerst, weil die Zeile darunter es beim Namen nennt. + if current.foodLike { + Section("Einheit und Menge") { + Picker("Gebinde", selection: $packageLabel) { ForEach(labels, id: \.self) { Text($0.isEmpty ? "Packung (Standard)" : $0).tag($0) } } + QuantityField(label: "1 \(gebindeName) sind", text: $packageSize, + suffix: display.baseUnitLabel(current.baseUnit)) Picker("MHD-Angabe", selection: $datePrecision) { Text("Tagesdatum").tag("day") Text("nur Monat/Jahr").tag("month") } } - } - // Zweiteinheit: dieselbe Ware in der ANDEREN Art lesbar machen. - // Damit zaehlt der Artikel auch in Gruppen der anderen Art und - // laesst sich darin ein- und auslagern. Das Gebinde bleibt frei. - if current.foodLike { + // Umrechnung in die ANDERE Art: „3 Stück wiegen 250 g". Damit + // zaehlt der Artikel auch in Gruppen, die in dieser Einheit + // rechnen. Das Gebinde bleibt davon unberuehrt. Section { - QuantityField(label: "Menge", text: $zweitAnzahl, - suffix: display.baseUnitLabel(current.baseUnit)) - Picker("entspricht", selection: $zweitBasis) { + Picker("Umrechnen in", selection: Binding( + get: { zweitBasis }, + set: { waehleZweitart($0) } + )) { Text("– keine –").tag("") ForEach(zweitBasisOptionen, id: \.self) { basis in Text(display.baseUnitLabel(basis)).tag(basis) } } if !zweitBasis.isEmpty { - QuantityField(label: "das sind", text: $zweitMenge, + QuantityField(label: "Menge", text: $zweitAnzahl, + suffix: display.baseUnitLabel(current.baseUnit)) + QuantityField(label: zweitVerb, text: $zweitMenge, suffix: display.baseUnitLabel(zweitBasis)) } } header: { - Text("Zweiteinheit (optional)") + Text("\(zweitTitel) (optional)") } footer: { Text(zweitHinweis) } @@ -385,6 +391,44 @@ struct ProductDetailView: View { ["piece", "gram", "milliliter"].filter { $0 != current.baseUnit } } + /// Die Umrechnung heisst nach dem, was sie liefert – „Zweiteinheit" musste + /// man erst uebersetzen. Ohne gewaehlte Zielart ein neutraler Titel. + private var zweitTitel: String { + switch zweitBasis { + case "gram": return "Gewicht" + case "milliliter": return "Volumen" + case "piece": return "Stückzahl" + default: return "Umrechnung" + } + } + + /// Verb passend zur Zielart und zur Anzahl: „1 Stück wiegt" / „3 wiegen". + private var zweitVerb: String { + let eins = Double(zweitAnzahl.replacingOccurrences(of: ",", with: ".")) == 1 + switch zweitBasis { + case "gram": return eins ? "wiegt" : "wiegen" + case "milliliter": return eins ? "fasst" : "fassen" + case "piece": return eins ? "ist" : "sind" + default: return "sind" + } + } + + /// Das Gebinde beim Namen nennen: „1 Packung sind …". Ohne Auswahl der + /// Standard, sonst stuende dort „1 sind 3 Stück". + private var gebindeName: String { + packageLabel.trimmingCharacters(in: .whitespaces).isEmpty ? "Packung" : packageLabel + } + + /// Zielart waehlen und die linke Zahl EINMALIG mit der Packungsgroesse + /// vorbelegen – im Normalfall ist sie genau das. Bewusst keine dauerhafte + /// Kopplung: wer die Packung spaeter aendert, hat dieselbe Ware. + private func waehleZweitart(_ basis: String) { + zweitBasis = basis + if !basis.isEmpty, zweitAnzahl.isEmpty { + zweitAnzahl = packageSize.isEmpty ? "1" : packageSize + } + } + /// Kontrolle unter der Eingabe: „1 Stück ≈ 83,33 g". private var zweitHinweis: String { let n = Double(zweitAnzahl.replacingOccurrences(of: ",", with: ".")) ?? 0 @@ -394,7 +438,8 @@ struct ProductDetailView: View { + "Gruppen der anderen Art und lässt sich darin ein- und auslagern." } return "1 \(display.baseUnitLabel(current.baseUnit)) ≈ \(formatAmount(m / n)) " - + "\(display.baseUnitLabel(zweitBasis)). Bestände bleiben beim Ändern " + + "\(display.baseUnitLabel(zweitBasis)). Damit zählt der Artikel auch in " + + "Gruppen, die in dieser Einheit rechnen. Bestände bleiben beim Ändern " + "unverändert – nur ihre Umrechnung verschiebt sich." } diff --git a/ios/Sources/ProductViews.swift b/ios/Sources/ProductViews.swift index 1349b17..552d2c6 100644 --- a/ios/Sources/ProductViews.swift +++ b/ios/Sources/ProductViews.swift @@ -105,6 +105,11 @@ struct ProductFormView: View { // Beschriftungen kommen sonst aus DisplaySettings.baseUnitLabel. private let labels = ["", "Glas", "Tüte", "Flasche", "Dose", "Tube", "Becher", "Beutel", "Karton"] + // Umrechnung in die andere Art („3 Stück ≙ 250 g") – siehe ProductDetailView. + @State private var zweitBasis = "" + @State private var zweitAnzahl = "" + @State private var zweitMenge = "" + /// Nutzt die Lebensmittel-Pfade (Einheit, MHD, Charge): Lebensmittel ODER /// Verbrauchsgegenstand. private var foodLike: Bool { modus == "food" || objMode == "bulk" } @@ -195,6 +200,31 @@ struct ProductFormView: View { Text("nur Monat/Jahr").tag("month") } } + + // Dieselbe Ware in der ANDEREN Art lesbar machen. Damit zaehlt + // der Artikel auch in Gruppen, die in dieser Einheit rechnen. + Section { + Picker("Umrechnen in", selection: Binding( + get: { zweitBasis }, + set: { waehleZweitart($0) } + )) { + Text("– keine –").tag("") + ForEach(zweitBasisOptionen, id: \.self) { basis in + Text(DisplaySettings.baseUnitLabel(basis)).tag(basis) + } + } + if !zweitBasis.isEmpty { + QuantityField(label: "Menge", text: $zweitAnzahl, + suffix: DisplaySettings.baseUnitLabel(baseUnit)) + QuantityField(label: zweitVerb, text: $zweitMenge, + suffix: DisplaySettings.baseUnitLabel(zweitBasis)) + } + } header: { + Text("\(zweitTitel) (optional)") + } footer: { + Text("Zweite Lesart desselben Artikels – z.B. „3 Stück wiegen 250 g“. " + + "Lässt sich auch später am Artikel nachtragen.") + } } // Eigene Felder der Gegenstands-Kategorie (z.B. Größe, Farbe beim // T-Shirt) – schon beim Anlegen ausfüllbar, nicht erst danach. @@ -299,6 +329,48 @@ struct ProductFormView: View { return fv } + /// Basiseinheiten der ANDEREN Arten – eine Bruecke auf die eigene Art + /// haette nichts umzurechnen. + private var zweitBasisOptionen: [String] { + ["piece", "gram", "milliliter"].filter { $0 != baseUnit } + } + + private var zweitTitel: String { + switch zweitBasis { + case "gram": return "Gewicht" + case "milliliter": return "Volumen" + case "piece": return "Stückzahl" + default: return "Umrechnung" + } + } + + private var zweitVerb: String { + let eins = Double(zweitAnzahl.replacingOccurrences(of: ",", with: ".")) == 1 + switch zweitBasis { + case "gram": return eins ? "wiegt" : "wiegen" + case "milliliter": return eins ? "fasst" : "fassen" + case "piece": return eins ? "ist" : "sind" + default: return "sind" + } + } + + /// Zielart waehlen und die linke Zahl EINMALIG mit der Packungsgroesse + /// vorbelegen – im Normalfall ist sie genau das. + private func waehleZweitart(_ basis: String) { + zweitBasis = basis + if !basis.isEmpty, zweitAnzahl.isEmpty { + zweitAnzahl = packageSize.isEmpty ? "1" : packageSize + } + } + + /// Nur eine vollstaendige Angabe ist eine Bruecke – eine halbe waere keine. + private var zweitVollstaendig: Bool { + guard !zweitBasis.isEmpty, + let n = Double(zweitAnzahl.replacingOccurrences(of: ",", with: ".")), + let m = Double(zweitMenge.replacingOccurrences(of: ",", with: ".")) else { return false } + return n > 0 && m > 0 + } + private func prefill() { if let initialType { modus = initialType } barcode = suggestion?.barcode ?? prefillBarcode ?? "" @@ -312,6 +384,14 @@ struct ProductFormView: View { brand = suggestion.brand ?? "" baseUnit = suggestion.baseUnit ?? "piece" if let size = suggestion.packageSize { packageSize = String(size) } + // „3 x 80 g" schlaegt die Umrechnung gleich mit vor. + if let basis = suggestion.secondaryBase, + let anzahl = suggestion.secondaryCount, + let menge = suggestion.secondaryAmount { + zweitBasis = basis + zweitAnzahl = formatAmount(anzahl) + zweitMenge = formatAmount(menge) + } } } @@ -334,7 +414,12 @@ struct ProductFormView: View { categoryId: selectedCategoryId, individual: modus == "object" ? (objMode == "individual") : nil, bulk: modus == "object" ? (objMode == "bulk") : nil, - fieldValues: eigeneFeldwerte() + fieldValues: eigeneFeldwerte(), + secondaryBase: zweitVollstaendig ? zweitBasis : nil, + secondaryCount: zweitVollstaendig + ? Double(zweitAnzahl.replacingOccurrences(of: ",", with: ".")) : nil, + secondaryAmount: zweitVollstaendig + ? Double(zweitMenge.replacingOccurrences(of: ",", with: ".")) : nil ) ) // Vorgemerktes Foto hochladen – ein Fehler darf das Anlegen nicht