Compare commits

..

2 Commits

Author SHA1 Message Date
Scarriffle
27f84003f3 iOS: Mindestbestand nur fuer Verbrauchsgegenstand, in echter Einheit
Mindestbestand (global + je Lagerort) nur noch bei Charge-Artikeln: das Feld
verschwindet bei Menge je Lagerort und Einzelstuecken, Alt-Werte werden beim
Speichern geleert. Angezeigt/gespeichert wird in der Anzeigeeinheit
(Milliliter/Gramm/Stueck) statt in Packungen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 09:02:10 +02:00
Scarriffle
3ff7d2a35a Web: Mindestbestand je Lagerort im Formular in echter Einheit
Der je-Lagerort-Editor (foodLike) zeigt und speichert jetzt in der Anzeigeeinheit
(Gramm/Milliliter/Stueck) statt in Packungen - konsistent zur Mindestbestaende-
Seite. Umrechnung Artikeleinheit<->Anzeigeeinheit an der Formular-Grenze.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 09:02:10 +02:00
2 changed files with 50 additions and 26 deletions

View File

@@ -61,7 +61,8 @@ struct ProductDetailView: View {
if current.isObject { if current.isObject {
if !current.isIndividual && !current.isBulk, shopId != current.shopId { return true } if !current.isIndividual && !current.isBulk, shopId != current.shopId { return true }
if !current.isBulk, productUrl != (current.productUrl ?? "") { return true } if !current.isBulk, productUrl != (current.productUrl ?? "") { return true }
if minStock != (current.minStockDisplay.map { formatAmount($0) } ?? "") { return true } // Mindestbestand gibt es nur beim Verbrauchsgegenstand.
if current.isBulk, minStock != (current.minStockDisplay.map { formatAmount($0) } ?? "") { return true }
if fieldValues != Self.stringValues(current.fieldValues) { return true } if fieldValues != Self.stringValues(current.fieldValues) { return true }
} }
return false return false
@@ -172,13 +173,16 @@ struct ProductDetailView: View {
} }
} }
if current.isObject { // Mindestbestand nur bei Verbrauchsgegenständen (Charge) in der
// Anzeigeeinheit (z.B. Milliliter), nicht in Packungen. Menge je
// Lagerort und Einzelstücke haben keinen Mindestbestand.
if current.isBulk {
Section("Mindestbestand") { Section("Mindestbestand") {
QuantityField(label: "Gesamt (\(current.isBulk ? current.articleUnitLabel : "Stück"))", QuantityField(label: "Gesamt (\(current.unitName))", text: $minStock)
text: $minStock)
} }
} }
if current.foodLike {
Section { Section {
NavigationLink { NavigationLink {
ProductLocationMinView(product: current) { await reload() } ProductLocationMinView(product: current) { await reload() }
@@ -190,6 +194,7 @@ struct ProductDetailView: View {
} footer: { } footer: {
Text("Bedarf je Lagerort (z.B. Ferienhaus, Zuhause), zusätzlich zum Gesamt-Mindestbestand.") Text("Bedarf je Lagerort (z.B. Ferienhaus, Zuhause), zusätzlich zum Gesamt-Mindestbestand.")
} }
}
Section("Erkennung") { Section("Erkennung") {
LabeledContent("Barcode", value: current.barcode ?? "") LabeledContent("Barcode", value: current.barcode ?? "")
@@ -326,8 +331,9 @@ struct ProductDetailView: View {
busy = true busy = true
defer { busy = false } defer { busy = false }
do { do {
// Mindestbestand in der Anzeigeeinheit erfasst Basiseinheiten.
let minBase = Double(minStock.replacingOccurrences(of: ",", with: ".")) let minBase = Double(minStock.replacingOccurrences(of: ",", with: "."))
.map { $0 * current.articleUnitFactor } .map { $0 * current.unitFactor }
if current.foodLike { if current.foodLike {
// Lebensmittel ODER Verbrauchsgegenstand: als Charge mit Einheit/ // Lebensmittel ODER Verbrauchsgegenstand: als Charge mit Einheit/
// Packung/MHD/Gruppe. Verbrauchsgegenstände (isObject) tragen // Packung/MHD/Gruppe. Verbrauchsgegenstände (isObject) tragen
@@ -346,15 +352,16 @@ struct ProductDetailView: View {
for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" } for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" }
req.fieldValues = fv req.fieldValues = fv
req.minStock = minBase req.minStock = minBase
req.minStockInPackages = (current.packageSize ?? 0) > 0 req.minStockInPackages = false
req.minStockUnitId = nil req.minStockUnitId = nil
req.sendMinStock = true req.sendMinStock = true
} }
current = try await APIClient.shared.updateProduct(id: current.id, req) current = try await APIClient.shared.updateProduct(id: current.id, req)
if current.isObject { fieldValues = Self.stringValues(current.fieldValues) } if current.isObject { fieldValues = Self.stringValues(current.fieldValues) }
} else { } else {
// Gegenstand: Menge je Lagerort oder Einzelstücke. Menge- // Gegenstand: Menge je Lagerort oder Einzelstücke. Kein
// Gegenstände dürfen jetzt ebenfalls in eine Gruppe. // Mindestbestand (Alt-Werte werden geleert); Menge-Gegenstände
// dürfen weiter in eine Gruppe.
var fv: [String: String?] = [:] var fv: [String: String?] = [:]
for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" } for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" }
current = try await APIClient.shared.updateProduct( current = try await APIClient.shared.updateProduct(
@@ -368,8 +375,8 @@ struct ProductDetailView: View {
shopId: current.isIndividual ? nil : shopId, shopId: current.isIndividual ? nil : shopId,
productUrl: productUrl.isEmpty ? nil : productUrl, productUrl: productUrl.isEmpty ? nil : productUrl,
fieldValues: fv, fieldValues: fv,
minStock: minBase, minStock: nil,
minStockInPackages: (current.packageSize ?? 0) > 0, minStockInPackages: false,
minStockUnitId: nil, minStockUnitId: nil,
sendMinStock: true sendMinStock: true
) )
@@ -550,10 +557,16 @@ struct ProductLocationMinView: View {
@State private var busy = false @State private var busy = false
@State private var error: String? @State private var error: String?
// Speicherung in Artikeleinheiten, Anzeige in der Anzeigeeinheit (Gramm/ml/Stück).
private var artToDisp: Double {
let disp = product.unitFactor == 0 ? 1 : product.unitFactor
return product.articleUnitFactor / disp
}
var body: some View { var body: some View {
Form { Form {
Section { Section {
Text("Bedarf je Lagerort (z.B. Ferienhaus, Zuhause). Menge in \(product.articleUnitLabel).") Text("Bedarf je Lagerort (z.B. Ferienhaus, Zuhause). Menge in \(product.unitName).")
.font(.caption).foregroundStyle(.secondary) .font(.caption).foregroundStyle(.secondary)
} }
ForEach($rows) { $row in ForEach($rows) { $row in
@@ -597,7 +610,7 @@ struct ProductLocationMinView: View {
private func load() async { private func load() async {
locations = (try? await APIClient.shared.locations()) ?? [] locations = (try? await APIClient.shared.locations()) ?? []
rows = (product.locationMinStocks ?? []).map { rows = (product.locationMinStocks ?? []).map {
MinRow(locationId: $0.locationId, amount: formatAmount($0.minStock)) MinRow(locationId: $0.locationId, amount: formatAmount($0.minStock * artToDisp))
} }
} }
@@ -611,7 +624,8 @@ struct ProductLocationMinView: View {
let wert = Double(row.amount.replacingOccurrences(of: ",", with: ".")) ?? 0 let wert = Double(row.amount.replacingOccurrences(of: ",", with: ".")) ?? 0
if wert > 0 { if wert > 0 {
gesehen.insert(loc) gesehen.insert(loc)
list.append(LocationMinStockIn(locationId: loc, minStock: wert)) // Eingabe in Anzeigeeinheit Artikeleinheiten (so gespeichert).
list.append(LocationMinStockIn(locationId: loc, minStock: wert / artToDisp))
} }
} }
do { do {

View File

@@ -34,6 +34,13 @@ const CANONICAL_NAME = { piece: "Stück", gram: "Gramm", milliliter: "Milliliter
const KIND_LABEL = { count: "Anzahl", weight: "Gewicht", volume: "Volumen" }; const KIND_LABEL = { count: "Anzahl", weight: "Gewicht", volume: "Volumen" };
const KIND_ORDER = ["count", "weight", "volume"]; const KIND_ORDER = ["count", "weight", "volume"];
// Faktor Artikeleinheit → Anzeigeeinheit: die je-Lagerort-Mindestbestände sind in
// Artikeleinheiten (Packungen) gespeichert, angezeigt wird aber die echte Einheit
// (Gramm/Milliliter/Stück), damit „Packung" nicht nichtssagend dasteht.
const artToDisp = (p) => (
(p.package_size > 0 ? p.package_size : (p.unit_factor || 1)) / (p.unit_factor || 1)
);
// Foto vor dem Hochladen verkleinern: kleiner in der DB, schneller im Upload und // Foto vor dem Hochladen verkleinern: kleiner in der DB, schneller im Upload und
// zuverlässig unter dem Server-Limit. Bei Problemen fällt es aufs Original zurück. // zuverlässig unter dem Server-Limit. Bei Problemen fällt es aufs Original zurück.
function verkleinereBild(datei, maxDim = 1600, quality = 0.85) { function verkleinereBild(datei, maxDim = 1600, quality = 0.85) {
@@ -812,16 +819,19 @@ export default function ProductForm() {
</div> </div>
<p className="muted small mt-0"> <p className="muted small mt-0">
Zusätzlich zum Gesamt-Mindestbestand: eigener Bedarf je Lagerort (z.B. Zusätzlich zum Gesamt-Mindestbestand: eigener Bedarf je Lagerort (z.B.
Ferienhaus, Zuhause). Menge in {product.package_label || product.unit_name || "Stück"}. Ferienhaus, Zuhause). Menge in {product.unit_name || "Stück"}.
Wird separat gespeichert. Wird separat gespeichert.
</p> </p>
<LocationMinStock <LocationMinStock
locations={locations} locations={locations}
initial={product.location_min_stocks || []} initial={(product.location_min_stocks || []).map(
unitLabel={product.package_label || product.unit_name || "Stück"} (e) => ({ ...e, min_stock: e.min_stock * artToDisp(product) }))}
unitLabel={product.unit_name || "Stück"}
onError={(m) => toast(m, "warn")} onError={(m) => toast(m, "warn")}
onSave={async (list) => { onSave={async (list) => {
const updated = await api.setProductLocationMinStock(product.id, list); const f = artToDisp(product);
const artList = list.map((e) => ({ ...e, min_stock: e.min_stock / f }));
const updated = await api.setProductLocationMinStock(product.id, artList);
setProduct(updated); setProduct(updated);
toast("Bedarfe je Lagerort gespeichert."); toast("Bedarfe je Lagerort gespeichert.");
}} }}