feat(dashboard): echtes freies Grid-Layout mit Resize statt Sortable+Größenstufen
CI / backend-tests (push) Failing after 2m4s
CI / frontend-build (push) Successful in 20s

@dnd-kit (nur Reihenfolge tauschen) durch react-grid-layout ersetzt - Kacheln lassen sich jetzt frei auf einem 12-Spalten-Raster positionieren (nicht nur umsortieren) UND per Ecke ziehen dynamisch in der Größe ändern (nicht nur zwischen 4 festen Stufen wechseln). Layout-Modell in layout.ts auf x/y/w/h pro Kachel umgestellt (vorher Reihenfolge-Array + Größen-Enum). TileFrame vereinfacht (kein Drag-Griff/Größen-Button mehr nötig, das übernimmt die Grid-Lib direkt an der Kachel).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
2026-09-07 13:41:23 +02:00
co-authored by Claude Sonnet 5
parent 85df756c3f
commit 74ed250ad7
6 changed files with 228 additions and 231 deletions
+35 -39
View File
@@ -8,7 +8,7 @@ export type DashboardTileId =
| "qualifikation"
| "aktivitaet";
export const STANDARD_REIHENFOLGE: DashboardTileId[] = [
export const ALLE_TILE_IDS: DashboardTileId[] = [
"aufgaben",
"einsatzbereitschaft",
"prueftermine",
@@ -19,65 +19,61 @@ export const STANDARD_REIHENFOLGE: DashboardTileId[] = [
"aktivitaet",
];
export type DashboardTileGroesse = "schmal" | "normal" | "breit" | "voll";
export const GRID_SPALTEN = 12;
export const GRID_ZEILENHOEHE = 56;
export const GROESSEN_REIHENFOLGE: DashboardTileGroesse[] = ["schmal", "normal", "breit", "voll"];
export interface RasterPosition {
x: number;
y: number;
w: number;
h: number;
}
export const GROESSE_LABEL: Record<DashboardTileGroesse, string> = {
schmal: "Schmal",
normal: "Normal",
breit: "Breit",
voll: "Volle Breite",
// Freie Grid-Positionen als Startzustand - grob nach Wichtigkeit sortiert,
// Aufgaben-Kachel volle Breite oben.
export const STANDARD_RASTER: Record<DashboardTileId, RasterPosition> = {
aufgaben: { x: 0, y: 0, w: 12, h: 3 },
einsatzbereitschaft: { x: 0, y: 3, w: 4, h: 5 },
prueftermine: { x: 4, y: 3, w: 4, h: 5 },
ablaufdaten: { x: 8, y: 3, w: 4, h: 5 },
kennzahlen: { x: 0, y: 8, w: 4, h: 5 },
maengel: { x: 4, y: 8, w: 4, h: 5 },
qualifikation: { x: 8, y: 8, w: 4, h: 4 },
aktivitaet: { x: 0, y: 13, w: 8, h: 5 },
};
export interface DashboardLayout {
reihenfolge: DashboardTileId[];
raster: Record<DashboardTileId, RasterPosition>;
ausgeblendet: DashboardTileId[];
groessen: Partial<Record<DashboardTileId, DashboardTileGroesse>>;
}
const SPEICHER_KEY = "mabea-dashboard-layout";
const SPEICHER_KEY = "mabea-dashboard-layout-v2";
function istGueltigeId(wert: unknown): wert is DashboardTileId {
return typeof wert === "string" && (STANDARD_REIHENFOLGE as string[]).includes(wert);
return typeof wert === "string" && (ALLE_TILE_IDS as string[]).includes(wert);
}
function istGueltigeGroesse(wert: unknown): wert is DashboardTileGroesse {
return typeof wert === "string" && (GROESSEN_REIHENFOLGE as string[]).includes(wert);
function istGueltigePosition(wert: unknown): wert is RasterPosition {
if (!wert || typeof wert !== "object") return false;
const p = wert as Record<string, unknown>;
return typeof p.x === "number" && typeof p.y === "number" && typeof p.w === "number" && typeof p.h === "number";
}
export function layoutLaden(): DashboardLayout {
try {
const roh = localStorage.getItem(SPEICHER_KEY);
if (!roh) return { reihenfolge: STANDARD_REIHENFOLGE, ausgeblendet: [], groessen: {} };
if (!roh) return { raster: { ...STANDARD_RASTER }, ausgeblendet: [] };
const geparst = JSON.parse(roh);
const gespeicherteReihenfolge = Array.isArray(geparst.reihenfolge)
? geparst.reihenfolge.filter(istGueltigeId)
: [];
const fehlende = STANDARD_REIHENFOLGE.filter((id) => !gespeicherteReihenfolge.includes(id));
const ausgeblendet = Array.isArray(geparst.ausgeblendet) ? geparst.ausgeblendet.filter(istGueltigeId) : [];
const groessen: Partial<Record<DashboardTileId, DashboardTileGroesse>> = {};
if (geparst.groessen && typeof geparst.groessen === "object") {
for (const [id, groesse] of Object.entries(geparst.groessen)) {
if (istGueltigeId(id) && istGueltigeGroesse(groesse)) groessen[id] = groesse;
const raster: Record<DashboardTileId, RasterPosition> = { ...STANDARD_RASTER };
if (geparst.raster && typeof geparst.raster === "object") {
for (const [id, position] of Object.entries(geparst.raster)) {
if (istGueltigeId(id) && istGueltigePosition(position)) raster[id] = position;
}
}
return { reihenfolge: [...gespeicherteReihenfolge, ...fehlende], ausgeblendet, groessen };
const ausgeblendet = Array.isArray(geparst.ausgeblendet) ? geparst.ausgeblendet.filter(istGueltigeId) : [];
return { raster, ausgeblendet };
} catch {
return { reihenfolge: STANDARD_REIHENFOLGE, ausgeblendet: [], groessen: {} };
}
}
export function flexBasisFuerGroesse(groesse: DashboardTileGroesse): string {
switch (groesse) {
case "schmal":
return "14rem";
case "breit":
return "28rem";
case "voll":
return "100%";
default:
return "18rem";
return { raster: { ...STANDARD_RASTER }, ausgeblendet: [] };
}
}