From 48d47f1d3949fdadc19f13ca6eb774df6defddb5 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Thu, 30 Jul 2026 09:32:45 +0200 Subject: [PATCH] Web: Mindestbestand-Ort per Dropdown verschieben + OFF-Panel-Regression behoben MinStock: die ORT-Spalte ist jetzt ein Dropdown (Gesamt/Lagerort). Auswahl verschiebt den Bedarf (moveRow) statt ihn zu duplizieren - mit Rueckfrage, falls der Zielort schon belegt ist. Produktwerte werden dabei umgerechnet, Gruppen nicht. OFF-Panel: meine schmalere Formularspalte hatte den Vergleichs-Button abgeschnitten und die OFF-Tabelle aus der Karte laufen lassen. Tabelle jetzt in .table-wrap (scrollt/stapelt), Button-Text darf umbrechen, Formularspalte etwas breiter. Co-Authored-By: Claude Opus 4.8 --- web/src/components/OffVergleich.jsx | 2 + web/src/pages/MinStock.jsx | 69 +++++++++++++++++++++++++++-- web/src/styles.css | 8 ++-- 3 files changed, 73 insertions(+), 6 deletions(-) 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 {