From a6f0c518428f12430c06bc351e658d3b8a1cf04c Mon Sep 17 00:00:00 2001 From: patrick Date: Sat, 5 Sep 2026 23:33:36 +0200 Subject: [PATCH] fix(beladungsplaner): Klick-zum-Zuordnen statt HTML5-Drag&Drop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nutzer-Fund 2026-09-05: natives Drag&Drop funktionierte auf Desktop mit Maus nicht zuverlässig und wäre auf Touch-Geräten ohnehin gar nicht nutzbar (HTML5-DnD-API unterstützt keine Touch-Eingabe) - genau die Geräteklasse, die dieses Projekt im Feld primär anspricht. Ersetzt durch Klick-zum-Zuordnen: Material im Pool antippen (markiert es), dann Ziel-Fach antippen ordnet zu. Funktioniert identisch auf Desktop und Touch. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV --- .../src/pages/admin/BeladungsplanerBoard.tsx | 188 ++++++++++-------- 1 file changed, 104 insertions(+), 84 deletions(-) diff --git a/frontend/src/pages/admin/BeladungsplanerBoard.tsx b/frontend/src/pages/admin/BeladungsplanerBoard.tsx index 07f6619..10a4226 100644 --- a/frontend/src/pages/admin/BeladungsplanerBoard.tsx +++ b/frontend/src/pages/admin/BeladungsplanerBoard.tsx @@ -1,3 +1,5 @@ +import { useState } from "react"; + import type { Fach, Material } from "../../api/types"; interface PositionZeile { @@ -16,10 +18,15 @@ interface Props { 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. */ + * übernehmen): Beladungsplanung als Board statt Zeile-für-Zeile-Formular. Klick-zum- + * Zuordnen statt HTML5-Drag&Drop (Nutzer-Fund 2026-09-05: natives Drag&Drop + * funktioniert unzuverlässig browserübergreifend und gar nicht auf Touch-Geräten, + * die dieses Projekt aber gerade im Feld primär anspricht) - Material im Pool + * antippen (markiert es), dann Ziel-Fach antippen ordnet zu. Bereits zugeordnetes + * Material antippen verschiebt/wählt es erneut, "×" entfernt es. */ export function BeladungsplanerBoard({ zeilen, onZeilenChange, materialien, faecher }: Props) { + const [ausgewaehlt, setAusgewaehlt] = useState(null); + 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]; @@ -29,17 +36,16 @@ export function BeladungsplanerBoard({ zeilen, onZeilenChange, materialien, faec 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; + function zuordnen(fachName: string) { + if (ausgewaehlt === null) return; const zielFach = fachName === OHNE_FACH ? "" : fachName; - const vorhanden = zeilen.find((z) => z.materialId === materialId); + const vorhanden = zeilen.find((z) => z.materialId === ausgewaehlt); if (vorhanden) { - onZeilenChange(zeilen.map((z) => (z.materialId === materialId ? { ...z, fach: zielFach } : z))); + onZeilenChange(zeilen.map((z) => (z.materialId === ausgewaehlt ? { ...z, fach: zielFach } : z))); } else { - onZeilenChange([...zeilen, { materialId, fach: zielFach, sollmenge: "1" }]); + onZeilenChange([...zeilen, { materialId: ausgewaehlt, fach: zielFach, sollmenge: "1" }]); } + setAusgewaehlt(null); } function sollmengeAendern(materialId: number, sollmenge: string) { @@ -48,84 +54,98 @@ export function BeladungsplanerBoard({ zeilen, onZeilenChange, materialien, faec function entfernen(materialId: number) { onZeilenChange(zeilen.filter((z) => z.materialId !== materialId)); + if (ausgewaehlt === materialId) setAusgewaehlt(null); } 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} -
  • - ))} -
-
+
+ {ausgewaehlt !== null && ( +

+ {materialName(ausgewaehlt)} ausgewählt — jetzt Ziel-Fach antippen (oder nochmal + anklicken zum Abbrechen). +

+ )} +
+
+

Material-Pool

+ {pool.length === 0 &&

Alles zugeordnet.

} +
    + {pool.map((m) => ( +
  • setAusgewaehlt(ausgewaehlt === m.id ? null : m.id)} + className="badge badge-neutral" + style={{ + display: "block", + marginBottom: "0.4rem", + padding: "0.35rem 0.5rem", + cursor: "pointer", + outline: ausgewaehlt === m.id ? "2px solid var(--color-primary, #2563eb)" : "none", + }} + > + {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…

- )} -
    - {inhalt.map((z) => ( -
  • e.dataTransfer.setData("text/plain", String(z.materialId))} - className="card" - style={{ padding: "0.4rem", marginBottom: "0.4rem", cursor: "grab" }} - > -
    - {materialName(z.materialId)} - -
    - z.materialId !== "" && sollmengeAendern(z.materialId, e.target.value)} - /> -
  • - ))} -
-
- ); - })} + {spalten.map((spalte) => { + const inhalt = zeilen.filter((z) => (spalte === OHNE_FACH ? !z.fach : z.fach === spalte)); + return ( +
zuordnen(spalte)} + > +

{spalte === OHNE_FACH ? "Ohne Fach" : spalte}

+ {inhalt.length === 0 && ( +

+ {ausgewaehlt !== null ? "Hier antippen zum Zuordnen" : "Kein Material"} +

+ )} +
    + {inhalt.map((z) => ( +
  • e.stopPropagation()} + > +
    + z.materialId !== "" && setAusgewaehlt(z.materialId)} + > + {materialName(z.materialId)} + + +
    + z.materialId !== "" && sollmengeAendern(z.materialId, e.target.value)} + /> +
  • + ))} +
+
+ ); + })} +
); }