Mindestbestand auch fuer Gegenstaende (Web-Formular + iOS)
Gegenstaende (Verbrauchsmaterial wie Sonnencreme) koennen jetzt einen Mindestbestand haben - im Web-Produktformular ein Gesamt-Feld (Stueck) plus Mindestbestand je Lagerort, auf iOS ein Gesamt-Feld in der Detailansicht. Umrechnung wie bei Lebensmitteln (fuer Stueck-Artikel = Stueckzahl); iOS sendet min_stock nur bei Gegenstaenden, damit Lebensmittel-Modi unberuehrt bleiben. Backend/Einkaufsliste unterstuetzen das bereits. Namen (Lebensmittel/Gegenstaende) bleiben. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -494,6 +494,12 @@ struct ProductUpdateRequest: Encodable {
|
|||||||
var shopId: Int? = nil
|
var shopId: Int? = nil
|
||||||
var productUrl: String? = nil
|
var productUrl: String? = nil
|
||||||
var fieldValues: [String: String?]? = nil
|
var fieldValues: [String: String?]? = nil
|
||||||
|
// Mindestbestand nur senden, wenn ausdrücklich gewünscht – sonst bliebe der
|
||||||
|
// erfasste Modus (z.B. „5 kg") sonst erhalten und wird nicht überschrieben.
|
||||||
|
var minStock: Double? = nil
|
||||||
|
var minStockInPackages: Bool? = nil
|
||||||
|
var minStockUnitId: Int? = nil
|
||||||
|
var sendMinStock = false
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case name, brand
|
case name, brand
|
||||||
@@ -505,6 +511,9 @@ struct ProductUpdateRequest: Encodable {
|
|||||||
case shopId = "shop_id"
|
case shopId = "shop_id"
|
||||||
case productUrl = "product_url"
|
case productUrl = "product_url"
|
||||||
case fieldValues = "field_values"
|
case fieldValues = "field_values"
|
||||||
|
case minStock = "min_stock"
|
||||||
|
case minStockInPackages = "min_stock_in_packages"
|
||||||
|
case minStockUnitId = "min_stock_unit_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
func encode(to encoder: Encoder) throws {
|
||||||
@@ -520,6 +529,11 @@ struct ProductUpdateRequest: Encodable {
|
|||||||
try container.encode(productUrl, forKey: .productUrl)
|
try container.encode(productUrl, forKey: .productUrl)
|
||||||
// Feldwerte nur senden, wenn gesetzt – sonst nichts an den Feldern ändern.
|
// Feldwerte nur senden, wenn gesetzt – sonst nichts an den Feldern ändern.
|
||||||
if let fieldValues { try container.encode(fieldValues, forKey: .fieldValues) }
|
if let fieldValues { try container.encode(fieldValues, forKey: .fieldValues) }
|
||||||
|
if sendMinStock {
|
||||||
|
try container.encode(minStock, forKey: .minStock)
|
||||||
|
try container.encode(minStockInPackages, forKey: .minStockInPackages)
|
||||||
|
try container.encode(minStockUnitId, forKey: .minStockUnitId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ struct ProductDetailView: View {
|
|||||||
// Gegenstände (Non-Food): Bezugsquelle, Link, Lagerorte und eigene Felder.
|
// Gegenstände (Non-Food): Bezugsquelle, Link, Lagerorte und eigene Felder.
|
||||||
@State private var shopId: Int?
|
@State private var shopId: Int?
|
||||||
@State private var productUrl = ""
|
@State private var productUrl = ""
|
||||||
|
// Gesamt-Mindestbestand (nur Gegenstände; Menge in Stück/Artikeleinheit).
|
||||||
|
@State private var minStock = ""
|
||||||
@State private var shops: [ShopItem] = []
|
@State private var shops: [ShopItem] = []
|
||||||
@State private var locations: [StorageLocation] = []
|
@State private var locations: [StorageLocation] = []
|
||||||
@State private var fieldDefs: [FieldDefinition] = []
|
@State private var fieldDefs: [FieldDefinition] = []
|
||||||
@@ -51,6 +53,7 @@ struct ProductDetailView: View {
|
|||||||
if current.isObject {
|
if current.isObject {
|
||||||
if !current.isIndividual, shopId != current.shopId { return true }
|
if !current.isIndividual, shopId != current.shopId { return true }
|
||||||
if productUrl != (current.productUrl ?? "") { return true }
|
if productUrl != (current.productUrl ?? "") { return true }
|
||||||
|
if minStock != (current.minStockDisplay.map { formatAmount($0) } ?? "") { return true }
|
||||||
if fieldValues != Self.stringValues(current.fieldValues) { return true }
|
if fieldValues != Self.stringValues(current.fieldValues) { return true }
|
||||||
} else {
|
} else {
|
||||||
if packageSize != (current.packageSize.map { formatAmount($0) } ?? "") { return true }
|
if packageSize != (current.packageSize.map { formatAmount($0) } ?? "") { return true }
|
||||||
@@ -158,6 +161,12 @@ struct ProductDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if current.isObject {
|
||||||
|
Section("Mindestbestand") {
|
||||||
|
QuantityField(label: "Gesamt (Stück)", text: $minStock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
ProductLocationMinView(product: current) { await reload() }
|
ProductLocationMinView(product: current) { await reload() }
|
||||||
@@ -267,6 +276,7 @@ struct ProductDetailView: View {
|
|||||||
categoryId = current.categoryId
|
categoryId = current.categoryId
|
||||||
shopId = current.shopId
|
shopId = current.shopId
|
||||||
productUrl = current.productUrl ?? ""
|
productUrl = current.productUrl ?? ""
|
||||||
|
minStock = current.minStockDisplay.map { formatAmount($0) } ?? ""
|
||||||
fieldValues = Self.stringValues(current.fieldValues)
|
fieldValues = Self.stringValues(current.fieldValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,7 +328,12 @@ struct ProductDetailView: View {
|
|||||||
// Einzelstücke tragen den Shop je Stück – nichts ans Modell.
|
// Einzelstücke tragen den Shop je Stück – nichts ans Modell.
|
||||||
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: Double(minStock.replacingOccurrences(of: ",", with: "."))
|
||||||
|
.map { $0 * current.articleUnitFactor },
|
||||||
|
minStockInPackages: (current.packageSize ?? 0) > 0,
|
||||||
|
minStockUnitId: nil,
|
||||||
|
sendMinStock: true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
fieldValues = Self.stringValues(current.fieldValues)
|
fieldValues = Self.stringValues(current.fieldValues)
|
||||||
|
|||||||
@@ -446,7 +446,14 @@ export default function ProductForm() {
|
|||||||
Object.entries(fieldValues).forEach(([k, v]) => {
|
Object.entries(fieldValues).forEach(([k, v]) => {
|
||||||
if (erlaubt.has(String(k))) fv[k] = v;
|
if (erlaubt.has(String(k))) fv[k] = v;
|
||||||
});
|
});
|
||||||
return { ...base, individual: form.individual, field_values: fv };
|
// Auch Gegenstände dürfen einen Mindestbestand haben (Verbrauchsmaterial):
|
||||||
|
// gleiche Umrechnung wie bei Lebensmitteln, für Stück-Artikel = Stückzahl.
|
||||||
|
return {
|
||||||
|
...base, individual: form.individual, field_values: fv,
|
||||||
|
min_stock: minBase,
|
||||||
|
min_stock_unit_id: sel === "package" || sel === "" ? null : Number(sel),
|
||||||
|
min_stock_in_packages: sel === "package",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@@ -803,6 +810,36 @@ export default function ProductForm() {
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<label className="grow" style={{ maxWidth: 300 }}>
|
||||||
|
Mindestbestand (Stück)
|
||||||
|
<input type="number" step="any" min="0" value={form.min_stock}
|
||||||
|
onChange={(e) => set("min_stock", e.target.value)} disabled={readOnly}
|
||||||
|
placeholder="z.B. 3" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{!isNew && product && isAdmin && (
|
||||||
|
<div style={{ marginTop: "var(--sp-2)", marginBottom: "var(--sp-4)" }}>
|
||||||
|
<div className="card-head" style={{ marginBottom: "var(--sp-1)" }}>
|
||||||
|
<Icon name="location" size={16} />
|
||||||
|
<h3 style={{ margin: 0 }}>Mindestbestand je Lagerort</h3>
|
||||||
|
</div>
|
||||||
|
<p className="muted small mt-0">
|
||||||
|
Bedarf je Lagerort (z.B. Ferienhaus, Zuhause), zusätzlich zum Gesamt-Mindestbestand. Menge in Stück.
|
||||||
|
</p>
|
||||||
|
<LocationMinStock
|
||||||
|
locations={locations}
|
||||||
|
initial={product.location_min_stocks || []}
|
||||||
|
unitLabel="Stück"
|
||||||
|
onError={(m) => toast(m, "warn")}
|
||||||
|
onSave={async (list) => {
|
||||||
|
const updated = await api.setProductLocationMinStock(product.id, list);
|
||||||
|
setProduct(updated);
|
||||||
|
toast("Bedarfe je Lagerort gespeichert.");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{form.category_id !== "" && (
|
{form.category_id !== "" && (
|
||||||
<div style={{ marginTop: "var(--sp-2)" }}>
|
<div style={{ marginTop: "var(--sp-2)" }}>
|
||||||
<div className="card-head" style={{ marginBottom: "var(--sp-2)" }}>
|
<div className="card-head" style={{ marginBottom: "var(--sp-2)" }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user