Einlagern-UX: 3-Wege-Barcode, Inline-Anlage aus OFF, Mehr-Chargen mit MHD

Einlagern:
- Barcode-Erkennung dreiteilig: bekannt -> direkt Menge; unbekannt aber in OFF
  -> "Anlegen & einlagern" inline; gar nicht gefunden -> manuell anlegen.
- Mehrere Chargen pro Vorgang: je Zeile eigene Menge + eigenes MHD
  (z.B. 5 Glaeser mit unterschiedlichen Daten). Neuer Endpoint /stock/checkin/batch.

Produkte/OFF:
- OFF-Fuellmenge (quantity) wird in Basiseinheit + Packungsgroesse geparst
  (kg->g, l->ml, cl/dl); parse_quantity + Tests.
- Produktformular uebernimmt Barcode aus ?barcode= und schlaegt automatisch nach,
  fuellt Packungsgroesse; gemeinsame offUtils (guessGroup/suggestionToProduct).

Lagerorte:
- Baum jetzt rekursiv ueber beliebig viele Ebenen (Schrank->Fach->Kiste->...).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-22 10:40:54 +02:00
parent b683f2d93b
commit 4c17705290
10 changed files with 368 additions and 105 deletions

View File

@@ -42,13 +42,19 @@ export default function Locations() {
}
const nameById = Object.fromEntries(locations.map((l) => [l.id, l.name]));
const roots = locations.filter((l) => !l.parent_id || !nameById[l.parent_id]);
const childrenOf = (pid) => locations.filter((l) => l.parent_id === pid);
const ids = new Set(locations.map((l) => l.id));
const isRoot = (l) => !l.parent_id || !ids.has(l.parent_id);
// Rekursiv über beliebig viele Ebenen (Schrank -> Fach -> Kiste -> …).
const ordered = [];
for (const r of roots) {
ordered.push({ ...r, depth: 0 });
for (const c of childrenOf(r.id)) ordered.push({ ...c, depth: 1 });
const seen = new Set();
function walk(node, depth) {
if (seen.has(node.id)) return; // Schutz vor Zyklen
seen.add(node.id);
ordered.push({ ...node, depth });
for (const c of locations.filter((l) => l.parent_id === node.id)) walk(c, depth + 1);
}
for (const r of locations.filter(isRoot)) walk(r, 0);
return (
<div>
@@ -86,7 +92,8 @@ export default function Locations() {
<ul className="simple-list">
{ordered.map((l) => (
<li key={l.id}>
<span className={l.depth ? "tree-child" : ""} style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ display: "flex", alignItems: "center", gap: 8, paddingLeft: l.depth * 22 }}>
{l.depth > 0 && <span className="muted"></span>}
<Icon name="location" size={15} className="muted" />
{l.name}
{l.depth > 0 && l.parent_id && nameById[l.parent_id] && (