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:
@@ -171,8 +171,12 @@ struct CheckInFormView: View {
|
||||
|
||||
/// Einheiten der passenden Art plus das Gebinde des Artikels.
|
||||
private var unitOptions: [UnitOption] {
|
||||
// Bei hinterlegter Zweiteinheit auch die Einheiten der Gegenart – das
|
||||
// Backend rechnet sie ueber die Bruecke am Artikel um.
|
||||
var arten: Set<String> = [product.kind]
|
||||
if product.zweitFaktor != nil, let k = product.zweitKind { arten.insert(k) }
|
||||
var options = units
|
||||
.filter { $0.kind == product.kind }
|
||||
.filter { arten.contains($0.kind) }
|
||||
.map { UnitOption(value: $0.name, label: $0.name) }
|
||||
if let size = product.packageSize, size > 0 {
|
||||
// Angezeigt wird "Glas"/"Dose", geschickt wird "package".
|
||||
|
||||
@@ -136,8 +136,12 @@ struct CheckOutFormView: View {
|
||||
@State private var error: String?
|
||||
|
||||
private var unitOptions: [UnitOption] {
|
||||
// Bei hinterlegter Zweiteinheit auch die Einheiten der Gegenart – das
|
||||
// Backend rechnet sie ueber die Bruecke am Artikel um.
|
||||
var arten: Set<String> = [product.kind]
|
||||
if product.zweitFaktor != nil, let k = product.zweitKind { arten.insert(k) }
|
||||
var options = units
|
||||
.filter { $0.kind == product.kind }
|
||||
.filter { arten.contains($0.kind) }
|
||||
.map { UnitOption(value: $0.name, label: $0.name) }
|
||||
if let size = product.packageSize, size > 0 {
|
||||
// Angezeigt wird "Glas"/"Dose", geschickt wird "package".
|
||||
|
||||
@@ -50,6 +50,14 @@ struct Product: Codable, Identifiable, Hashable {
|
||||
let baseUnit: String
|
||||
let packageSize: Double?
|
||||
let packageLabel: String?
|
||||
/// Zweiteinheit: „secondaryCount Basiseinheiten ≙ secondaryAmount
|
||||
/// secondaryBase". Bruecke zwischen den sonst strikt getrennten Arten.
|
||||
/// Optionals, damit die App auch gegen einen aelteren Server laeuft.
|
||||
let secondaryBase: String?
|
||||
let secondaryCount: Double?
|
||||
let secondaryAmount: Double?
|
||||
/// Vom Server ausgerechnet: Zweit-Basiseinheiten je EINER Basiseinheit.
|
||||
let secondaryFactor: Double?
|
||||
let groupId: Int?
|
||||
let minStock: Double?
|
||||
let stock: Double
|
||||
@@ -98,6 +106,10 @@ struct Product: Codable, Identifiable, Hashable {
|
||||
case baseUnit = "base_unit"
|
||||
case packageSize = "package_size"
|
||||
case packageLabel = "package_label"
|
||||
case secondaryBase = "secondary_base"
|
||||
case secondaryCount = "secondary_count"
|
||||
case secondaryAmount = "secondary_amount"
|
||||
case secondaryFactor = "secondary_factor"
|
||||
case groupId = "group_id"
|
||||
case minStock = "min_stock"
|
||||
case expiredCount = "expired_count"
|
||||
@@ -134,6 +146,38 @@ struct Product: Codable, Identifiable, Hashable {
|
||||
|
||||
var stockInArticleUnits: Double { stock / articleUnitFactor }
|
||||
|
||||
/// Zweit-Basiseinheiten je EINER Basiseinheit – nil ohne Bruecke.
|
||||
var zweitFaktor: Double? {
|
||||
guard let basis = secondaryBase, !basis.isEmpty, basis != baseUnit else { return nil }
|
||||
if let f = secondaryFactor, f > 0 { return f }
|
||||
// Rueckfall, falls das abgeleitete Feld fehlt (aelterer Server).
|
||||
guard let n = secondaryCount, let m = secondaryAmount, n > 0, m > 0 else { return nil }
|
||||
return m / n
|
||||
}
|
||||
|
||||
/// Art der Zweiteinheit ("count"/"weight"/"volume") – fuer die Einheitenwahl.
|
||||
var zweitKind: String? {
|
||||
switch secondaryBase {
|
||||
case "piece": return "count"
|
||||
case "gram": return "weight"
|
||||
case "milliliter": return "volume"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Kuerzel der Zweit-Basiseinheit fuer die Anzeige.
|
||||
var zweitKurz: String {
|
||||
switch secondaryBase {
|
||||
case "gram": return "g"
|
||||
case "milliliter": return "ml"
|
||||
case "piece": return "Stück"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
|
||||
/// Bestand in der Zweiteinheit, z.B. 500 g bei 6 Stueck.
|
||||
var stockInZweit: Double? { zweitFaktor.map { stock * $0 } }
|
||||
|
||||
/// Wie es um den Mindestbestand steht. Beides in Basiseinheiten verglichen.
|
||||
enum StockLevel { case none, ok, close, below }
|
||||
|
||||
@@ -585,6 +629,12 @@ struct ProductUpdateRequest: Encodable {
|
||||
var individual: Bool? = nil
|
||||
var bulk: Bool? = nil
|
||||
var sendMode = false
|
||||
// Zweiteinheit ebenfalls nur auf Wunsch – ein PATCH aus einer anderen Maske
|
||||
// soll die Bruecke nicht unbeabsichtigt loeschen.
|
||||
var secondaryBase: String? = nil
|
||||
var secondaryCount: Double? = nil
|
||||
var secondaryAmount: Double? = nil
|
||||
var sendSecondary = false
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case name, brand, individual, bulk
|
||||
@@ -600,6 +650,9 @@ struct ProductUpdateRequest: Encodable {
|
||||
case minStockInPackages = "min_stock_in_packages"
|
||||
case minStockUnitId = "min_stock_unit_id"
|
||||
case baseUnit = "base_unit"
|
||||
case secondaryBase = "secondary_base"
|
||||
case secondaryCount = "secondary_count"
|
||||
case secondaryAmount = "secondary_amount"
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
@@ -625,6 +678,11 @@ struct ProductUpdateRequest: Encodable {
|
||||
try container.encode(bulk, forKey: .bulk)
|
||||
if let baseUnit { try container.encode(baseUnit, forKey: .baseUnit) }
|
||||
}
|
||||
if sendSecondary {
|
||||
try container.encode(secondaryBase, forKey: .secondaryBase)
|
||||
try container.encode(secondaryCount, forKey: .secondaryCount)
|
||||
try container.encode(secondaryAmount, forKey: .secondaryAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)] ?? "" }
|
||||
|
||||
Reference in New Issue
Block a user