Charge aufteilen: Teilmenge mit gleichem MHD an anderen Lagerort umlagern

Bisher liess sich nur die ganze Charge umlagern. Neu: POST /lots/{id}/split zweigt
eine Teilmenge (Basiseinheiten) als neue Charge mit gleichem MHD an einen anderen
Lagerort ab; der Rest bleibt. Reine Umbuchung ohne Bewegungseintrag, Gesamtbestand
unveraendert. Schutz gegen zu grosse Menge und gleichen Zielort.

Web: gemeinsamer SplitLotDialog (Menge in Artikeleinheit + Zielort, zeigt Rest),
Aufteilen-Knopf auf der Chargen-Seite und in der Chargen-Tabelle der Artikelseite.
Neues Split-Icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-30 08:43:16 +02:00
parent 8cf7cef6e1
commit 231d86895f
7 changed files with 184 additions and 0 deletions

View File

@@ -38,6 +38,14 @@ const PATHS = {
<path d="M12 22V12" />
</>
),
split: (
<>
<line x1="6" y1="3" x2="6" y2="15" />
<circle cx="18" cy="6" r="3" />
<circle cx="6" cy="18" r="3" />
<path d="M18 9a9 9 0 0 1-9 9" />
</>
),
tag: (
<>
<path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" />

View File

@@ -0,0 +1,83 @@
import { useState } from "react";
import { api } from "../api";
import Icon from "./Icon";
import { fmt } from "../units";
import { locationOptions, locationPathById } from "../locationPath";
/**
* Charge aufteilen: einen Teil der Menge (in Artikeleinheiten Packungen/Stück/g)
* mit gleichem MHD an einen anderen Lagerort verschieben. Die Restmenge bleibt in
* der ursprünglichen Charge; der Gesamtbestand ändert sich nicht.
*
* `factor` Basiseinheiten je Artikeleinheit (z.B. 415 ml pro Packung).
* `labelFor` (menge) => Einheitentext, mengenabhängig ("1 Packung"/"3 Packungen").
*/
export default function SplitLotDialog({ lot, factor, labelFor, locations, onClose, onDone, onError }) {
const total = lot.quantity / factor; // Gesamtmenge in Artikeleinheiten
const [menge, setMenge] = useState("");
const [locId, setLocId] = useState("");
const [busy, setBusy] = useState(false);
const num = Number(String(menge).trim().replace(",", "."));
const gueltig = menge !== "" && !Number.isNaN(num) && num > 0 && num < total;
const rest = gueltig ? total - num : null;
const zielGleich = gueltig && (locId || null) === (lot.location_id || null);
async function submit() {
if (!gueltig || zielGleich || busy) return;
setBusy(true);
try {
await api.splitLot(lot.id, { quantity: num * factor, location_id: locId || null });
onClose();
await onDone();
} catch (err) {
onError?.(err.message);
} finally {
setBusy(false);
}
}
return (
<div className="modal-backdrop" onClick={(e) => e.target === e.currentTarget && onClose()}>
<div className="modal" role="dialog" aria-modal="true">
<div className="modal-head"><Icon name="split" size={18} /><h2>Charge aufteilen</h2></div>
<p className="modal-body mt-0">
Aktuell {fmt(total)} {labelFor(total)}
{lot.location_id ? ` in ${locationPathById(lot.location_id, locations) || "?"}` : " ohne Lagerort"}.
Ein Teil davon wird mit gleichem MHD an einen anderen Lagerort verschoben.
</p>
<div className="modal-body" style={{ display: "grid", gap: "var(--sp-3)" }}>
<label>
Menge abteilen (in {labelFor(2)})
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
<input type="number" step="any" min="0" max={total} value={menge} autoFocus
onChange={(e) => setMenge(e.target.value)} placeholder={`max. ${fmt(total)}`} />
</label>
<label>
nach Lagerort
<select value={locId} onChange={(e) => setLocId(e.target.value)}>
<option value=""> ohne Lagerort </option>
{locationOptions(locations).map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
</select>
</label>
{rest != null && !zielGleich && (
<p className="muted small mt-0">
Danach: {fmt(num)} {labelFor(num)} am neuen Ort, {fmt(rest)} {labelFor(rest)} bleiben.
</p>
)}
{zielGleich && (
<p className="small mt-0" style={{ color: "var(--danger)" }}>
Bitte einen anderen Lagerort als den aktuellen wählen.
</p>
)}
</div>
<div className="btn-pair">
<button type="button" className="btn" onClick={onClose}>Abbrechen</button>
<button type="button" className="btn primary" disabled={!gueltig || zielGleich || busy} onClick={submit}>
<Icon name="split" size={16} />Aufteilen
</button>
</div>
</div>
</div>
);
}