Web: zentrale Mindestbestaende per "+ hinzufuegen" anlegen
Neuer Dialog auf der Mindestbestaende-Seite: Ziel (Produkt/Gegenstand oder Gruppe) -> Geltung (Gesamt oder Lagerort) -> Menge in der Einheit des Ziels. Speichert ueber die vorhandenen Endpunkte (updateProduct/updateGroup bzw. set*LocationMinStock). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@ const articleLabel = (p) => p.package_label || p.unit_name || "Stück";
|
||||
* Bedarfe je Lagerort, inline editierbar. Ergänzt die einzelnen Produkt-/Gruppen-
|
||||
* Formulare um einen Überblick an einem Ort.
|
||||
*/
|
||||
const EMPTY_DRAFT = { kind: "product", targetId: "", scope: "global", locId: "", menge: "" };
|
||||
|
||||
export default function MinStock() {
|
||||
const { isAdmin } = useAuth();
|
||||
const [products, setProducts] = useState([]);
|
||||
@@ -24,6 +26,9 @@ export default function MinStock() {
|
||||
const [locations, setLocations] = useState([]);
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [error, setError] = useState(null);
|
||||
// „+ hinzufügen"-Dialog: Ziel (Produkt/Gruppe) → Geltung (Gesamt/Lagerort) → Menge.
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [draft, setDraft] = useState(EMPTY_DRAFT);
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
@@ -109,6 +114,53 @@ export default function MinStock() {
|
||||
} catch (err) { setError(err.message); }
|
||||
}
|
||||
|
||||
// Ausgewähltes Ziel des Dialogs + dessen Einheit (Menge wird darin erfasst).
|
||||
const draftTarget = draft.kind === "product"
|
||||
? products.find((p) => String(p.id) === String(draft.targetId))
|
||||
: groups.find((g) => String(g.id) === String(draft.targetId));
|
||||
const draftUnit = draft.kind === "product"
|
||||
? (draftTarget ? articleLabel(draftTarget) : "")
|
||||
: (draftTarget ? (draftTarget.min_stock_unit_name || "Stück") : "");
|
||||
|
||||
function resetAdd() {
|
||||
setShowAdd(false);
|
||||
setDraft(EMPTY_DRAFT);
|
||||
}
|
||||
|
||||
async function saveNew() {
|
||||
setError(null);
|
||||
const roh = String(draft.menge).trim().replace(",", ".");
|
||||
const num = roh === "" ? null : Number(roh);
|
||||
if (!draft.targetId) { setError("Bitte ein Ziel wählen."); return; }
|
||||
if (num == null || Number.isNaN(num) || num <= 0) { setError("Bitte eine Menge größer 0 angeben."); return; }
|
||||
if (draft.scope === "loc" && !draft.locId) { setError("Bitte einen Lagerort wählen."); return; }
|
||||
try {
|
||||
if (draft.kind === "product") {
|
||||
const p = draftTarget;
|
||||
if (draft.scope === "global") {
|
||||
// Menge in der Artikeleinheit erfasst → in Basiseinheiten speichern,
|
||||
// wie beim Inline-Editieren ohne bestehenden Wert.
|
||||
await api.updateProduct(p.id, {
|
||||
min_stock: Math.round(num * articleUnit(p)),
|
||||
min_stock_in_packages: (p.package_size || 0) > 0,
|
||||
min_stock_unit_id: null,
|
||||
});
|
||||
} else {
|
||||
await api.setProductLocationMinStock(p.id, mergeLoc(p.location_min_stocks, draft.locId, num));
|
||||
}
|
||||
} else {
|
||||
const g = draftTarget;
|
||||
if (draft.scope === "global") {
|
||||
await api.updateGroup(g.id, { min_stock: num });
|
||||
} else {
|
||||
await api.setGroupLocationMinStock(g.id, mergeLoc(g.location_min_stocks, draft.locId, num));
|
||||
}
|
||||
}
|
||||
resetAdd();
|
||||
await load();
|
||||
} catch (err) { setError(err.message); }
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ key: "art", header: "Art", width: 96, filterText: (r) => (r.kind === "group" ? "Gruppe" : "Produkt"),
|
||||
render: (r) => <span className="badge nowrap">{r.kind === "group" ? "Gruppe" : "Produkt"}</span> },
|
||||
@@ -146,8 +198,81 @@ export default function MinStock() {
|
||||
Gesamt- und Lagerort-Bedarfe von Produkten und Gruppen an einem Ort{isAdmin ? " – Werte direkt editierbar" : ""}.
|
||||
</div>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<button className="btn primary" onClick={() => (showAdd ? resetAdd() : setShowAdd(true))}>
|
||||
<Icon name={showAdd ? "close" : "plus"} size={16} />
|
||||
{showAdd ? "Abbrechen" : "Mindestbestand"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
|
||||
|
||||
{isAdmin && showAdd && (
|
||||
<div className="card" style={{ marginBottom: "var(--sp-3)" }}>
|
||||
<div className="card-head"><Icon name="plus" /><h2>Mindestbestand hinzufügen</h2></div>
|
||||
<div className="row">
|
||||
<label className="seg-label">
|
||||
Ziel
|
||||
<div className="segmented">
|
||||
<button type="button" className={draft.kind === "product" ? "active" : ""}
|
||||
onClick={() => setDraft({ ...draft, kind: "product", targetId: "" })}>Gegenstand / Lebensmittel</button>
|
||||
<button type="button" className={draft.kind === "group" ? "active" : ""}
|
||||
onClick={() => setDraft({ ...draft, kind: "group", targetId: "" })}>Gruppe</button>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="row">
|
||||
<label className="grow">
|
||||
{draft.kind === "product" ? "Produkt / Gegenstand" : "Gruppe"}
|
||||
<select value={draft.targetId} onChange={(e) => setDraft({ ...draft, targetId: e.target.value })}>
|
||||
<option value="">– wählen –</option>
|
||||
{(draft.kind === "product"
|
||||
? [...products].sort((a, b) => a.name.localeCompare(b.name, "de"))
|
||||
: [...groups].sort((a, b) => a.name.localeCompare(b.name, "de"))
|
||||
).map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.name}{draft.kind === "product" && t.brand ? ` · ${t.brand}` : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="row">
|
||||
<label className="seg-label">
|
||||
Geltung
|
||||
<div className="segmented">
|
||||
<button type="button" className={draft.scope === "global" ? "active" : ""}
|
||||
onClick={() => setDraft({ ...draft, scope: "global", locId: "" })}>Gesamt</button>
|
||||
<button type="button" className={draft.scope === "loc" ? "active" : ""}
|
||||
onClick={() => setDraft({ ...draft, scope: "loc" })}>Lagerort</button>
|
||||
</div>
|
||||
</label>
|
||||
{draft.scope === "loc" && (
|
||||
<label className="grow" style={{ maxWidth: 360 }}>
|
||||
Lagerort
|
||||
<select value={draft.locId} onChange={(e) => setDraft({ ...draft, locId: e.target.value })}>
|
||||
<option value="">– wählen –</option>
|
||||
{locations.map((l) => (
|
||||
<option key={l.id} value={l.id}>{locationPathById(l.id, locations)}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="row">
|
||||
<label className="grow" style={{ maxWidth: 300 }}>
|
||||
Menge{draftUnit ? ` (in ${draftUnit})` : ""}
|
||||
<input type="number" step="any" min="0" value={draft.menge}
|
||||
onChange={(e) => setDraft({ ...draft, menge: e.target.value })} placeholder="z.B. 2" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="field-inline" style={{ marginTop: "var(--sp-2)" }}>
|
||||
<button className="btn primary" onClick={saveNew}>Speichern</button>
|
||||
<button className="btn" onClick={resetAdd}>Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card">
|
||||
<DataTable id="min-stock" columns={columns} rows={rows}
|
||||
getRowKey={(r) => r.key} empty="Noch keine Mindestbestände." />
|
||||
|
||||
Reference in New Issue
Block a user