App: Zweiteinheit erfassen und anzeigen

Eigener Abschnitt im Artikel mit "Menge [Stk] entspricht [Gramm] das sind
[250]" und der ausgerechneten Kontrolle in der Fusszeile. Nur Charge-Artikel,
und nur die anderen Arten stehen zur Wahl - eine Bruecke auf die eigene Art
haette nichts umzurechnen.

ProductUpdateRequest bekommt sendSecondary nach dem Vorbild von sendMinStock:
ein PATCH aus einer anderen Maske soll die Bruecke nicht unbeabsichtigt
loeschen. Die Einheitenauswahl beim Ein- und Auslagern nimmt bei hinterlegter
Bruecke die Gegenart mit auf, der Bestand steht in beiden Lesarten
("6 Stueck · 500 g").

Alle neuen Product-Felder sind Optionals, damit die App auch gegen einen
aelteren Server laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-16 00:42:52 +02:00
parent 788ef2caf2
commit 71d52917e4
4 changed files with 138 additions and 4 deletions

View File

@@ -20,6 +20,10 @@ struct ProductDetailView: View {
@State private var packageSize = ""
@State private var packageLabel = ""
@State private var datePrecision = "day"
// Zweiteinheit (3 Stück 250 g") Bruecke zur anderen Art.
@State private var zweitAnzahl = ""
@State private var zweitMenge = ""
@State private var zweitBasis = ""
@State private var groupId: Int?
@State private var categoryId: Int?
// Verwaltungsart eines Gegenstands: count | individual | bulk umstellbar.
@@ -62,6 +66,9 @@ struct ProductDetailView: View {
if packageSize != (current.packageSize.map { formatAmount($0) } ?? "") { return true }
if packageLabel != (current.packageLabel ?? "") { return true }
if datePrecision != (current.datePrecision == "month" ? "month" : "day") { return true }
if zweitBasis != (current.secondaryBase ?? "") { return true }
if zweitAnzahl != (current.secondaryCount.map { formatAmount($0) } ?? "") { return true }
if zweitMenge != (current.secondaryAmount.map { formatAmount($0) } ?? "") { return true }
}
if current.isObject {
if objMode != currentObjMode { return true }
@@ -97,8 +104,12 @@ struct ProductDetailView: View {
} else {
// Lebensmittel und Verbrauchsgegenstände: Vorrat aus den Chargen.
Section("Bestand") {
LabeledContent("Vorrat",
value: "\(formatAmount(current.stockInArticleUnits)) \(current.articleUnitLabel)")
// Mit Zweiteinheit beide Lesarten: 6 Stück · 500 g".
LabeledContent("Vorrat", value: {
let haupt = "\(formatAmount(current.stockInArticleUnits)) \(current.articleUnitLabel)"
guard let zweit = current.stockInZweit else { return haupt }
return "\(haupt) · \(formatAmount(zweit)) \(current.zweitKurz)"
}())
if current.expiredCount > 0 {
LabeledContent("Abgelaufen", value: "\(current.expiredCount)")
.foregroundStyle(.red)
@@ -124,6 +135,30 @@ struct ProductDetailView: View {
}
}
// 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 {
Section {
QuantityField(label: "Menge", text: $zweitAnzahl,
suffix: display.baseUnitLabel(current.baseUnit))
Picker("entspricht", selection: $zweitBasis) {
Text(" keine ").tag("")
ForEach(zweitBasisOptionen, id: \.self) { basis in
Text(display.baseUnitLabel(basis)).tag(basis)
}
}
if !zweitBasis.isEmpty {
QuantityField(label: "das sind", text: $zweitMenge,
suffix: display.baseUnitLabel(zweitBasis))
}
} header: {
Text("Zweiteinheit (optional)")
} footer: {
Text(zweitHinweis)
}
}
Section {
CategoryPicker(categories: categories, selection: $categoryId,
tracking: current.tracking)
@@ -344,6 +379,25 @@ struct ProductDetailView: View {
return teile.joined(separator: " · ")
}
/// Basiseinheiten der ANDEREN Arten eine Bruecke auf die eigene Art
/// haette nichts umzurechnen.
private var zweitBasisOptionen: [String] {
["piece", "gram", "milliliter"].filter { $0 != current.baseUnit }
}
/// Kontrolle unter der Eingabe: 1 Stück 83,33 g".
private var zweitHinweis: String {
let n = Double(zweitAnzahl.replacingOccurrences(of: ",", with: ".")) ?? 0
let m = Double(zweitMenge.replacingOccurrences(of: ",", with: ".")) ?? 0
guard !zweitBasis.isEmpty, n > 0, m > 0 else {
return "Zusätzliche Lesart desselben Artikels damit zählt er auch in "
+ "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 "
+ "unverändert nur ihre Umrechnung verschiebt sich."
}
/// Aktuelle Verwaltungsart des geladenen Artikels.
private var currentObjMode: String {
current.isIndividual ? "individual" : (current.isBulk ? "bulk" : "count")
@@ -355,6 +409,9 @@ struct ProductDetailView: View {
packageSize = current.packageSize.map { formatAmount($0) } ?? ""
packageLabel = current.packageLabel ?? ""
datePrecision = current.datePrecision == "month" ? "month" : "day"
zweitBasis = current.secondaryBase ?? ""
zweitAnzahl = current.secondaryCount.map { formatAmount($0) } ?? ""
zweitMenge = current.secondaryAmount.map { formatAmount($0) } ?? ""
groupId = current.groupId
categoryId = current.categoryId
shopId = current.shopId
@@ -435,6 +492,17 @@ struct ProductDetailView: View {
// Der Mindestbestand wird in ProductLocationMinView gepflegt und
// hier bewusst NICHT mitgeschickt sonst überschriebe das
// Stammdaten-Speichern die dort gesetzte Überall"-Zeile.
//
// Zweiteinheit nur vollstaendig; leere oder halbe Angabe loescht
// die Bruecke (secondary_base null raeumt im Backend mit auf).
let zn = Double(zweitAnzahl.replacingOccurrences(of: ",", with: "."))
let zm = Double(zweitMenge.replacingOccurrences(of: ",", with: "."))
req.sendSecondary = true
if !zweitBasis.isEmpty, let zn, let zm, zn > 0, zm > 0 {
req.secondaryBase = zweitBasis
req.secondaryCount = zn
req.secondaryAmount = zm
}
if current.isObject {
var fv: [String: String?] = [:]
for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" }