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.isIndividual && !current.isBulk, shopId != current.shopId { 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 }
}
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") {
QuantityField(label: "Gesamt (\(current.isBulk ? current.articleUnitLabel : "Stück"))",
text: $minStock)
QuantityField(label: "Gesamt (\(current.unitName))", text: $minStock)
}
}
if current.foodLike {
Section {
NavigationLink {
ProductLocationMinView(product: current) { await reload() }
@@ -190,6 +194,7 @@ struct ProductDetailView: View {
} footer: {
Text("Bedarf je Lagerort (z.B. Ferienhaus, Zuhause), zusätzlich zum Gesamt-Mindestbestand.")
}
}
Section("Erkennung") {
LabeledContent("Barcode", value: current.barcode ?? "")
@@ -326,8 +331,9 @@ struct ProductDetailView: View {
busy = true
defer { busy = false }
do {
// Mindestbestand in der Anzeigeeinheit erfasst Basiseinheiten.
let minBase = Double(minStock.replacingOccurrences(of: ",", with: "."))
.map { $0 * current.articleUnitFactor }
.map { $0 * current.unitFactor }
if current.foodLike {
// Lebensmittel ODER Verbrauchsgegenstand: als Charge mit Einheit/
// 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)] ?? "" }
req.fieldValues = fv
req.minStock = minBase
req.minStockInPackages = (current.packageSize ?? 0) > 0
req.minStockInPackages = false
req.minStockUnitId = nil
req.sendMinStock = true
}
current = try await APIClient.shared.updateProduct(id: current.id, req)
if current.isObject { fieldValues = Self.stringValues(current.fieldValues) }
} else {
// Gegenstand: Menge je Lagerort oder Einzelstücke. Menge-
// Gegenstände dürfen jetzt ebenfalls in eine Gruppe.
// Gegenstand: Menge je Lagerort oder Einzelstücke. Kein
// Mindestbestand (Alt-Werte werden geleert); Menge-Gegenstände
// dürfen weiter in eine Gruppe.
var fv: [String: String?] = [:]
for def in fieldDefs { fv[String(def.id)] = fieldValues[String(def.id)] ?? "" }
current = try await APIClient.shared.updateProduct(
@@ -368,8 +375,8 @@ struct ProductDetailView: View {
shopId: current.isIndividual ? nil : shopId,
productUrl: productUrl.isEmpty ? nil : productUrl,
fieldValues: fv,
minStock: minBase,
minStockInPackages: (current.packageSize ?? 0) > 0,
minStock: nil,
minStockInPackages: false,
minStockUnitId: nil,
sendMinStock: true
)
@@ -550,10 +557,16 @@ struct ProductLocationMinView: View {
@State private var busy = false
@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 {
Form {
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)
}
ForEach($rows) { $row in
@@ -597,7 +610,7 @@ struct ProductLocationMinView: View {
private func load() async {
locations = (try? await APIClient.shared.locations()) ?? []
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
if wert > 0 {
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 {

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_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
// zuverlässig unter dem Server-Limit. Bei Problemen fällt es aufs Original zurück.
function verkleinereBild(datei, maxDim = 1600, quality = 0.85) {
@@ -812,16 +819,19 @@ export default function ProductForm() {
</div>
<p className="muted small mt-0">
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.
</p>
<LocationMinStock
locations={locations}
initial={product.location_min_stocks || []}
unitLabel={product.package_label || product.unit_name || "Stück"}
initial={(product.location_min_stocks || []).map(
(e) => ({ ...e, min_stock: e.min_stock * artToDisp(product) }))}
unitLabel={product.unit_name || "Stück"}
onError={(m) => toast(m, "warn")}
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);
toast("Bedarfe je Lagerort gespeichert.");
}}