Web: Mindestbestand je Lagerort + Einkaufsliste je Ort

- Neuer LocationMinStock-Editor (Zeilen: Lagerort + Menge, separat speichern).
- Produktformular: Abschnitt „Mindestbestand je Lagerort" (Menge in Artikel-
  einheit), zusaetzlich zum Gesamt-Mindestbestand.
- Gruppen: je Gruppe ein Editor „Mindestbestand je Lagerort".
- Einkaufsliste: Abschnitt „Bedarf: <Lagerort>" mit Produkten und Gruppen, die
  am Ort unter dem dort hinterlegten Mindestbestand liegen.
- API-Methoden set*LocationMinStock und shoppingByLocation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 07:12:59 +02:00
parent 67bc2dd87d
commit dff7677e50
5 changed files with 199 additions and 3 deletions

View File

@@ -0,0 +1,85 @@
import { useState } from "react";
import Icon from "./Icon";
/**
* Editor für Mindestbestände je Lagerort (Bedarfe). Eine Zeile je Ort mit Menge;
* „Bedarfe speichern" ersetzt über onSave die komplette Liste. Menge 0/leer =
* Ort fällt weg.
*/
export default function LocationMinStock({ locations, initial = [], unitLabel = "", onSave, onError }) {
const [rows, setRows] = useState(() =>
initial.map((e) => ({ location_id: String(e.location_id), min_stock: String(e.min_stock) })),
);
const [busy, setBusy] = useState(false);
const [ok, setOk] = useState(false);
const used = new Set(rows.map((r) => r.location_id));
const frei = locations.filter((l) => !used.has(String(l.id)));
function addRow() {
if (!frei.length) return;
setRows([...rows, { location_id: String(frei[0].id), min_stock: "" }]);
setOk(false);
}
function setRow(i, patch) {
setRows(rows.map((r, j) => (j === i ? { ...r, ...patch } : r)));
setOk(false);
}
function removeRow(i) {
setRows(rows.filter((_, j) => j !== i));
setOk(false);
}
async function save() {
setBusy(true);
try {
const list = rows
.filter((r) => r.location_id !== "" && Number(r.min_stock) > 0)
.map((r) => ({ location_id: Number(r.location_id), min_stock: Number(r.min_stock) }));
await onSave(list);
setOk(true);
} catch (err) {
onError?.(err.message);
} finally {
setBusy(false);
}
}
if (!locations.length) {
return <p className="muted small mt-0">Erst Lagerorte anlegen, dann Bedarfe je Ort möglich.</p>;
}
return (
<div className="stack">
{rows.length === 0 && (
<p className="muted small mt-0">Kein Bedarf je Lagerort festgelegt.</p>
)}
{rows.map((r, i) => (
<div className="field-inline" key={i} style={{ marginBottom: 0 }}>
<label className="grow" style={{ flex: "1 1 auto", minWidth: 0, margin: 0 }}>
<select value={r.location_id} onChange={(e) => setRow(i, { location_id: e.target.value })}>
{locations.map((l) => <option key={l.id} value={l.id}>{l.name}</option>)}
</select>
</label>
<label style={{ margin: 0, width: 130 }}>
<input type="number" min="0" step="0.01" placeholder="Menge" value={r.min_stock}
onChange={(e) => setRow(i, { min_stock: e.target.value })} />
</label>
{unitLabel && <span className="muted small" style={{ alignSelf: "center" }}>{unitLabel}</span>}
<button type="button" className="btn-icon danger" onClick={() => removeRow(i)} title="Entfernen">
<Icon name="trash" size={16} />
</button>
</div>
))}
<div className="field-inline" style={{ marginBottom: 0 }}>
<button type="button" className="btn" onClick={addRow} disabled={!frei.length}>
<Icon name="plus" size={16} />Lagerort
</button>
<button type="button" className="btn primary" onClick={save} disabled={busy}>
<Icon name="check" size={16} />{busy ? "Speichern…" : "Bedarfe speichern"}
</button>
{ok && <span className="muted small" style={{ alignSelf: "center" }}>Gespeichert.</span>}
</div>
</div>
);
}