diff --git a/frontend/src/pages/admin/BeladungsplanerBoard.tsx b/frontend/src/pages/admin/BeladungsplanerBoard.tsx
new file mode 100644
index 0000000..07f6619
--- /dev/null
+++ b/frontend/src/pages/admin/BeladungsplanerBoard.tsx
@@ -0,0 +1,131 @@
+import type { Fach, Material } from "../../api/types";
+
+interface PositionZeile {
+ materialId: number | "";
+ fach: string;
+ sollmenge: string;
+}
+
+interface Props {
+ zeilen: PositionZeile[];
+ onZeilenChange: (zeilen: PositionZeile[]) => void;
+ materialien: Material[];
+ faecher: Fach[];
+}
+
+const OHNE_FACH = "__ohne_fach__";
+
+/** Shelf-inspiriert (Nutzer-Vorgabe 2026-09-05: gute Inhalte aus Vergleichsprojekten
+ * übernehmen): Beladungsplanung per Drag&Drop statt Zeile-für-Zeile-Formular -
+ * Material aus dem Pool links in eine Fach-Spalte rechts ziehen, weiterziehen
+ * verschiebt es ins andere Fach, "×" entfernt es wieder in den Pool. */
+export function BeladungsplanerBoard({ zeilen, onZeilenChange, materialien, faecher }: Props) {
+ const zugewieseneIds = new Set(zeilen.filter((z) => z.materialId !== "").map((z) => z.materialId));
+ const pool = materialien.filter((m) => !zugewieseneIds.has(m.id));
+ const spalten = [...faecher.map((f) => f.name), OHNE_FACH];
+
+ function materialName(id: number | ""): string {
+ if (id === "") return "";
+ return materialien.find((m) => m.id === id)?.name ?? `#${id}`;
+ }
+
+ function onDropAufSpalte(e: React.DragEvent, fachName: string) {
+ e.preventDefault();
+ const materialId = Number(e.dataTransfer.getData("text/plain"));
+ if (!materialId) return;
+ const zielFach = fachName === OHNE_FACH ? "" : fachName;
+ const vorhanden = zeilen.find((z) => z.materialId === materialId);
+ if (vorhanden) {
+ onZeilenChange(zeilen.map((z) => (z.materialId === materialId ? { ...z, fach: zielFach } : z)));
+ } else {
+ onZeilenChange([...zeilen, { materialId, fach: zielFach, sollmenge: "1" }]);
+ }
+ }
+
+ function sollmengeAendern(materialId: number, sollmenge: string) {
+ onZeilenChange(zeilen.map((z) => (z.materialId === materialId ? { ...z, sollmenge } : z)));
+ }
+
+ function entfernen(materialId: number) {
+ onZeilenChange(zeilen.filter((z) => z.materialId !== materialId));
+ }
+
+ return (
+
+
e.preventDefault()}
+ onDrop={(e) => {
+ // Zurück in den Pool ziehen entfernt die Zuordnung.
+ e.preventDefault();
+ const materialId = Number(e.dataTransfer.getData("text/plain"));
+ if (materialId) entfernen(materialId);
+ }}
+ >
+
Material-Pool
+ {pool.length === 0 &&
Alles zugeordnet.
}
+
+ {pool.map((m) => (
+ - e.dataTransfer.setData("text/plain", String(m.id))}
+ className="badge badge-neutral"
+ style={{ display: "block", marginBottom: "0.4rem", padding: "0.35rem 0.5rem", cursor: "grab" }}
+ >
+ {m.name}
+
+ ))}
+
+
+
+ {spalten.map((spalte) => {
+ const inhalt = zeilen.filter((z) => (spalte === OHNE_FACH ? !z.fach : z.fach === spalte));
+ return (
+
e.preventDefault()}
+ onDrop={(e) => onDropAufSpalte(e, spalte)}
+ >
+
{spalte === OHNE_FACH ? "Ohne Fach" : spalte}
+ {inhalt.length === 0 && (
+
Material hierher ziehen…
+ )}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend/src/pages/admin/VorlageSection.tsx b/frontend/src/pages/admin/VorlageSection.tsx
index 960d901..1e09775 100644
--- a/frontend/src/pages/admin/VorlageSection.tsx
+++ b/frontend/src/pages/admin/VorlageSection.tsx
@@ -2,6 +2,7 @@ import { useState } from "react";
import { apiRequest } from "../../api/client";
import type { Beladungsvorlage, Fach, Material, Objekttyp } from "../../api/types";
+import { BeladungsplanerBoard } from "./BeladungsplanerBoard";
import { FachAuswahl } from "./FachAuswahl";
interface PositionZeile {
@@ -41,10 +42,6 @@ export function VorlageSection({ vorlagen, objekttypen, materialien, faecher, on
);
}
- function neueVersionZeileAendern(index: number, zeile: Partial) {
- setNeueVersionZeilen((bisherige) => bisherige.map((z, i) => (i === index ? { ...z, ...zeile } : z)));
- }
-
async function positionenSpeichern(vorlageId: number) {
const positionen = neueVersionZeilen
.filter((z) => z.materialId !== "")
@@ -177,42 +174,12 @@ export function VorlageSection({ vorlagen, objekttypen, materialien, faecher, on
Objekte behalten ihre bisherige Sollmenge – nur neu angelegte Objekte nutzen die
aktualisierten Werte.
- {neueVersionZeilen.map((zeile, i) => (
-
-
- f.objekttyp_id === v.objekttyp_id)}
- wert={zeile.fach}
- onAendern={(wert) => neueVersionZeileAendern(i, { fach: wert })}
- />
- neueVersionZeileAendern(i, { sollmenge: e.target.value })}
- />
-
- ))}
-
+ f.objekttyp_id === v.objekttyp_id)}
+ />