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-
|
* Bedarfe je Lagerort, inline editierbar. Ergänzt die einzelnen Produkt-/Gruppen-
|
||||||
* Formulare um einen Überblick an einem Ort.
|
* Formulare um einen Überblick an einem Ort.
|
||||||
*/
|
*/
|
||||||
|
const EMPTY_DRAFT = { kind: "product", targetId: "", scope: "global", locId: "", menge: "" };
|
||||||
|
|
||||||
export default function MinStock() {
|
export default function MinStock() {
|
||||||
const { isAdmin } = useAuth();
|
const { isAdmin } = useAuth();
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState([]);
|
||||||
@@ -24,6 +26,9 @@ export default function MinStock() {
|
|||||||
const [locations, setLocations] = useState([]);
|
const [locations, setLocations] = useState([]);
|
||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
const [error, setError] = useState(null);
|
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() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
@@ -109,6 +114,53 @@ export default function MinStock() {
|
|||||||
} catch (err) { setError(err.message); }
|
} 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 = [
|
const columns = [
|
||||||
{ key: "art", header: "Art", width: 96, filterText: (r) => (r.kind === "group" ? "Gruppe" : "Produkt"),
|
{ 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> },
|
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" : ""}.
|
Gesamt- und Lagerort-Bedarfe von Produkten und Gruppen an einem Ort{isAdmin ? " – Werte direkt editierbar" : ""}.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{isAdmin && (
|
||||||
|
<button className="btn primary" onClick={() => (showAdd ? resetAdd() : setShowAdd(true))}>
|
||||||
|
<Icon name={showAdd ? "close" : "plus"} size={16} />
|
||||||
|
{showAdd ? "Abbrechen" : "Mindestbestand"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</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">
|
<div className="card">
|
||||||
<DataTable id="min-stock" columns={columns} rows={rows}
|
<DataTable id="min-stock" columns={columns} rows={rows}
|
||||||
getRowKey={(r) => r.key} empty="Noch keine Mindestbestände." />
|
getRowKey={(r) => r.key} empty="Noch keine Mindestbestände." />
|
||||||
|
|||||||
Reference in New Issue
Block a user