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 <noreply@anthropic.com>
This commit is contained in:
@@ -277,6 +277,10 @@ struct LookupResult: Codable {
|
|||||||
let baseUnit: String?
|
let baseUnit: String?
|
||||||
let packageSize: Double?
|
let packageSize: Double?
|
||||||
let quantityText: String?
|
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 {
|
enum CodingKeys: String, CodingKey {
|
||||||
case barcode, name, brand
|
case barcode, name, brand
|
||||||
@@ -284,6 +288,9 @@ struct LookupResult: Codable {
|
|||||||
case baseUnit = "base_unit"
|
case baseUnit = "base_unit"
|
||||||
case packageSize = "package_size"
|
case packageSize = "package_size"
|
||||||
case quantityText = "quantity_text"
|
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
|
var bulk: Bool? = nil
|
||||||
/// Selbst definierte Feldwerte der Kategorie: {feld_id (als Text): Wert}.
|
/// Selbst definierte Feldwerte der Kategorie: {feld_id (als Text): Wert}.
|
||||||
var fieldValues: [String: String?]? = nil
|
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 {
|
enum CodingKeys: String, CodingKey {
|
||||||
case barcode, name, brand, individual, bulk
|
case barcode, name, brand, individual, bulk
|
||||||
@@ -421,6 +432,9 @@ struct NewProductRequest: Codable {
|
|||||||
case groupId = "group_id"
|
case groupId = "group_id"
|
||||||
case categoryId = "category_id"
|
case categoryId = "category_id"
|
||||||
case fieldValues = "field_values"
|
case fieldValues = "field_values"
|
||||||
|
case secondaryBase = "secondary_base"
|
||||||
|
case secondaryCount = "secondary_count"
|
||||||
|
case secondaryAmount = "secondary_amount"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,38 +122,44 @@ struct ProductDetailView: View {
|
|||||||
Section("Artikel") {
|
Section("Artikel") {
|
||||||
LabeledField(label: "Name", text: $name)
|
LabeledField(label: "Name", text: $name)
|
||||||
LabeledField(label: "Marke", text: $brand)
|
LabeledField(label: "Marke", text: $brand)
|
||||||
if current.foodLike {
|
}
|
||||||
QuantityField(label: "Packungsgröße", text: $packageSize,
|
|
||||||
suffix: display.baseUnitLabel(current.baseUnit))
|
// Alles zur Einheit steht zusammen und liest sich als Satz: das
|
||||||
Picker("Bezeichnung", selection: $packageLabel) {
|
// 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) }
|
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) {
|
Picker("MHD-Angabe", selection: $datePrecision) {
|
||||||
Text("Tagesdatum").tag("day")
|
Text("Tagesdatum").tag("day")
|
||||||
Text("nur Monat/Jahr").tag("month")
|
Text("nur Monat/Jahr").tag("month")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Zweiteinheit: dieselbe Ware in der ANDEREN Art lesbar machen.
|
// Umrechnung in die ANDERE Art: „3 Stück wiegen 250 g". Damit
|
||||||
// Damit zaehlt der Artikel auch in Gruppen der anderen Art und
|
// zaehlt der Artikel auch in Gruppen, die in dieser Einheit
|
||||||
// laesst sich darin ein- und auslagern. Das Gebinde bleibt frei.
|
// rechnen. Das Gebinde bleibt davon unberuehrt.
|
||||||
if current.foodLike {
|
|
||||||
Section {
|
Section {
|
||||||
QuantityField(label: "Menge", text: $zweitAnzahl,
|
Picker("Umrechnen in", selection: Binding(
|
||||||
suffix: display.baseUnitLabel(current.baseUnit))
|
get: { zweitBasis },
|
||||||
Picker("entspricht", selection: $zweitBasis) {
|
set: { waehleZweitart($0) }
|
||||||
|
)) {
|
||||||
Text("– keine –").tag("")
|
Text("– keine –").tag("")
|
||||||
ForEach(zweitBasisOptionen, id: \.self) { basis in
|
ForEach(zweitBasisOptionen, id: \.self) { basis in
|
||||||
Text(display.baseUnitLabel(basis)).tag(basis)
|
Text(display.baseUnitLabel(basis)).tag(basis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !zweitBasis.isEmpty {
|
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))
|
suffix: display.baseUnitLabel(zweitBasis))
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text("Zweiteinheit (optional)")
|
Text("\(zweitTitel) (optional)")
|
||||||
} footer: {
|
} footer: {
|
||||||
Text(zweitHinweis)
|
Text(zweitHinweis)
|
||||||
}
|
}
|
||||||
@@ -385,6 +391,44 @@ struct ProductDetailView: View {
|
|||||||
["piece", "gram", "milliliter"].filter { $0 != current.baseUnit }
|
["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".
|
/// Kontrolle unter der Eingabe: „1 Stück ≈ 83,33 g".
|
||||||
private var zweitHinweis: String {
|
private var zweitHinweis: String {
|
||||||
let n = Double(zweitAnzahl.replacingOccurrences(of: ",", with: ".")) ?? 0
|
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."
|
+ "Gruppen der anderen Art und lässt sich darin ein- und auslagern."
|
||||||
}
|
}
|
||||||
return "1 \(display.baseUnitLabel(current.baseUnit)) ≈ \(formatAmount(m / n)) "
|
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."
|
+ "unverändert – nur ihre Umrechnung verschiebt sich."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ struct ProductFormView: View {
|
|||||||
// Beschriftungen kommen sonst aus DisplaySettings.baseUnitLabel.
|
// Beschriftungen kommen sonst aus DisplaySettings.baseUnitLabel.
|
||||||
private let labels = ["", "Glas", "Tüte", "Flasche", "Dose", "Tube", "Becher", "Beutel", "Karton"]
|
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
|
/// Nutzt die Lebensmittel-Pfade (Einheit, MHD, Charge): Lebensmittel ODER
|
||||||
/// Verbrauchsgegenstand.
|
/// Verbrauchsgegenstand.
|
||||||
private var foodLike: Bool { modus == "food" || objMode == "bulk" }
|
private var foodLike: Bool { modus == "food" || objMode == "bulk" }
|
||||||
@@ -195,6 +200,31 @@ struct ProductFormView: View {
|
|||||||
Text("nur Monat/Jahr").tag("month")
|
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
|
// Eigene Felder der Gegenstands-Kategorie (z.B. Größe, Farbe beim
|
||||||
// T-Shirt) – schon beim Anlegen ausfüllbar, nicht erst danach.
|
// T-Shirt) – schon beim Anlegen ausfüllbar, nicht erst danach.
|
||||||
@@ -299,6 +329,48 @@ struct ProductFormView: View {
|
|||||||
return fv
|
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() {
|
private func prefill() {
|
||||||
if let initialType { modus = initialType }
|
if let initialType { modus = initialType }
|
||||||
barcode = suggestion?.barcode ?? prefillBarcode ?? ""
|
barcode = suggestion?.barcode ?? prefillBarcode ?? ""
|
||||||
@@ -312,6 +384,14 @@ struct ProductFormView: View {
|
|||||||
brand = suggestion.brand ?? ""
|
brand = suggestion.brand ?? ""
|
||||||
baseUnit = suggestion.baseUnit ?? "piece"
|
baseUnit = suggestion.baseUnit ?? "piece"
|
||||||
if let size = suggestion.packageSize { packageSize = String(size) }
|
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,
|
categoryId: selectedCategoryId,
|
||||||
individual: modus == "object" ? (objMode == "individual") : nil,
|
individual: modus == "object" ? (objMode == "individual") : nil,
|
||||||
bulk: modus == "object" ? (objMode == "bulk") : 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
|
// Vorgemerktes Foto hochladen – ein Fehler darf das Anlegen nicht
|
||||||
|
|||||||
Reference in New Issue
Block a user