diff --git a/web/src/components/OffVergleich.jsx b/web/src/components/OffVergleich.jsx
index 695a286..f934f42 100644
--- a/web/src/components/OffVergleich.jsx
+++ b/web/src/components/OffVergleich.jsx
@@ -35,6 +35,7 @@ export default function OffVergleich({
{abweichend.length === 0 && !bildAbweichend ? (
Keine abweichenden Angaben – deine Daten sind aktuell.
) : (
+
@@ -93,6 +94,7 @@ export default function OffVergleich({
+
)}
);
diff --git a/web/src/pages/MinStock.jsx b/web/src/pages/MinStock.jsx
index 4d91c41..fc83b57 100644
--- a/web/src/pages/MinStock.jsx
+++ b/web/src/pages/MinStock.jsx
@@ -6,7 +6,7 @@ import Icon from "../components/Icon";
import DataTable from "../components/DataTable";
import CategoryPathLabel, { pathParts } from "../components/CategoryPathLabel";
import { categoryInfoMap } from "../categoryPath";
-import { locationPathById } from "../locationPath";
+import { locationOptions, locationPathById } from "../locationPath";
import { fmt } from "../units";
// Anzeigeeinheit eines Produkts: die im Formular gewählte Einheit (Gramm,
@@ -163,6 +163,55 @@ export default function MinStock() {
} catch (err) { setError(err.message); }
}
+ // Einen bestehenden Mindestbestand auf einen anderen Ort verschieben: „(Gesamt)"
+ // (ziel = "") oder ein Lagerort. Der Wert bleibt, der bisherige Geltungsbereich
+ // wird geleert – so entsteht keine Dublette. Produkt-Werte werden zwischen
+ // Anzeige-/Artikel-/Basiseinheit umgerechnet (wie in saveSoll), Gruppen nicht.
+ async function moveRow(row, ziel) {
+ const aktuell = row.scope === "loc" ? row.locId : "";
+ if (String(ziel) === String(aktuell)) return;
+ if (row.soll == null) return; // nichts zu verschieben
+ const ent = row.entity;
+ // Zielort schon mit einem Bedarf belegt? Dann würde er überschrieben.
+ if (ziel && (ent.location_min_stocks || []).some((e) => String(e.location_id) === String(ziel))) {
+ const ok = await confirm({
+ title: "Lagerort schon belegt",
+ message: `Für „${row.name}" gibt es an diesem Lagerort bereits einen Mindestbestand. Mit dem verschobenen Wert überschreiben?`,
+ confirmLabel: "Überschreiben", danger: true,
+ });
+ if (!ok) return;
+ }
+ setError(null);
+ try {
+ if (row.kind === "product") {
+ const p = ent;
+ const artikel = (row.soll * dispFactor(p)) / articleUnit(p);
+ let liste = p.location_min_stocks || [];
+ if (row.scope === "loc") liste = mergeLoc(liste, row.locId, null); // alten Ort raus
+ if (ziel) liste = mergeLoc(liste, ziel, artikel); // Zielort rein
+ // Gesamt-Wert setzen (Umzug nach Gesamt) oder leeren (Umzug weg von Gesamt).
+ if (row.scope === "global" || !ziel) {
+ await api.updateProduct(p.id, {
+ min_stock: ziel ? null : Math.round(row.soll * dispFactor(p)),
+ min_stock_in_packages: false,
+ min_stock_unit_id: p.display_unit_id ?? null,
+ });
+ }
+ if (row.scope === "loc" || ziel) await api.setProductLocationMinStock(p.id, liste);
+ } else {
+ const g = ent; // Gruppe: Gesamt und Ort teilen dieselbe Einheit – keine Umrechnung
+ let liste = g.location_min_stocks || [];
+ if (row.scope === "loc") liste = mergeLoc(liste, row.locId, null);
+ if (ziel) liste = mergeLoc(liste, ziel, row.soll);
+ if (row.scope === "global" || !ziel) {
+ await api.updateGroup(g.id, { min_stock: ziel ? null : row.soll });
+ }
+ if (row.scope === "loc" || ziel) await api.setGroupLocationMinStock(g.id, liste);
+ }
+ await load();
+ } 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))
@@ -224,8 +273,22 @@ export default function MinStock() {
render: (r) => (r.catId != null
?
: –) },
- { key: "ort", header: "Ort", width: 220, filterText: (r) => r.ort, sortValue: (r) => r.ort,
- render: (r) => (r.scope === "global" ? {r.ort} : r.ort) },
+ { key: "ort", header: "Ort", width: 240, filterText: (r) => r.ort, sortValue: (r) => r.ort,
+ render: (r) => {
+ // Editierbar, wenn es einen Wert zu verschieben gibt (leere Produktzeilen
+ // ohne Bedarf bleiben Text – dort legt man über „+ Mindestbestand" an).
+ if (!isAdmin || r.soll == null) {
+ return r.scope === "global" ? {r.ort} : r.ort;
+ }
+ return (
+
+ );
+ } },
{ key: "soll", header: "Mindestbestand", width: 190,
sortValue: (r) => r.soll ?? -1,
render: (r) => (isAdmin ? (
diff --git a/web/src/styles.css b/web/src/styles.css
index 5a9ffda..9f13fa5 100644
--- a/web/src/styles.css
+++ b/web/src/styles.css
@@ -193,7 +193,7 @@ h2 { font-size: 0.95rem; font-weight: 650; margin: 0 0 var(--sp-3); letter-spaci
halbe Seite -, rechts die Chargen-/Bestandstabelle, die den restlichen Platz
bekommt, damit Menge/MHD/Lagerort ohne Seitwaerts-Scrollen nebeneinander
passen. Spiegelbild von .wide-aside (dort steht die Tabelle links). */
-.grid-2.form-plus-table { grid-template-columns: clamp(440px, 34%, 640px) minmax(0, 1fr); align-items: start; }
+.grid-2.form-plus-table { grid-template-columns: clamp(480px, 38%, 720px) minmax(0, 1fr); align-items: start; }
@media (max-width: 1000px) {
.grid-2.form-plus-table { grid-template-columns: minmax(0, 1fr); }
}
@@ -263,8 +263,10 @@ input::placeholder { color: var(--muted); opacity: 0.7; }
/* Knoepfe neben dem Barcode-Feld: nebeneinander, mit Umbruch auf schmalen
Fenstern. Das Eingabefeld daneben fuellt die restliche Breite. */
-.knopf-spalte { display: flex; flex-flow: row wrap; gap: var(--sp-2); flex: 0 0 auto; }
-.knopf-spalte .btn { justify-content: center; white-space: nowrap; }
+.knopf-spalte { display: flex; flex-flow: row wrap; gap: var(--sp-2); flex: 0 1 auto; min-width: 0; }
+/* Kein nowrap: in einer schmalen Formularspalte darf ein langer Button-Text
+ ("Mit Open Food Facts vergleichen") umbrechen statt abgeschnitten zu werden. */
+.knopf-spalte .btn { justify-content: center; white-space: normal; min-width: 0; }
/* Gegenueberstellung mit Open Food Facts */
.off-vergleich {