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 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ export default function OffVergleich({
|
||||
{abweichend.length === 0 && !bildAbweichend ? (
|
||||
<p className="muted small">Keine abweichenden Angaben – deine Daten sind aktuell.</p>
|
||||
) : (
|
||||
<div className="table-wrap">
|
||||
<table className="table off-tabelle">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -93,6 +94,7 @@ export default function OffVergleich({
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
? <CategoryPathLabel parts={catInfo.get(r.catId)?.parts} fallback="–" />
|
||||
: <span className="muted">–</span>) },
|
||||
{ key: "ort", header: "Ort", width: 220, filterText: (r) => r.ort, sortValue: (r) => r.ort,
|
||||
render: (r) => (r.scope === "global" ? <span className="muted">{r.ort}</span> : 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" ? <span className="muted">{r.ort}</span> : r.ort;
|
||||
}
|
||||
return (
|
||||
<select value={r.scope === "loc" ? r.locId : ""} style={{ marginTop: 0, minWidth: 150 }}
|
||||
title="Lagerort ändern – der Mindestbestand wird verschoben"
|
||||
onChange={(e) => moveRow(r, e.target.value)}>
|
||||
<option value="">(Gesamt)</option>
|
||||
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
|
||||
</select>
|
||||
);
|
||||
} },
|
||||
{ key: "soll", header: "Mindestbestand", width: 190,
|
||||
sortValue: (r) => r.soll ?? -1,
|
||||
render: (r) => (isAdmin ? (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user