/** * grid-layout-migration — einmalige Umrechnung gespeicherter Dashboard- * Anordnungen in die feineren Raster-Einheiten (quick-260916-bwo). * * Warum: Das Raster wurde von 12 Spalten / 40 px Zeilenhoehe auf 24 Spalten / * 20 px verdoppelt (`dashboard-grid.tsx`). Eine in ALTEN Einheiten gespeicherte * Anordnung wuerde im neuen Raster halb so gross und an der halben Position * erscheinen. Deshalb werden `x, y, w, h` (und, falls vorhanden, `minW, minH, * maxW, maxH`) jedes Elements in jedem Breakpoint GENAU EINMAL mit 2 * multipliziert. * * Marker: Damit die Verdopplung nur einmal geschieht, traegt das gespeicherte * JSON den Schluessel `__gridVersion: 2`. Der Marker lebt NUR im persistierten * JSON (Spalte `DashboardLayout.layouts`, Json, kein Schema noetig) — nie im * Zustand des Stores, der mit `Object.keys` ueber die Breakpoints iteriert und * `.filter` auf jedem Wert aufruft (ein Zahlwert wuerde dort abstuerzen). * `migrateGridLayouts` entfernt den Marker beim Laden, `withGridVersion` * haengt ihn beim Speichern wieder an. Fehlt der Marker beim Speichern, wird * beim naechsten Laden ERNEUT verdoppelt — deshalb muss JEDER Speichervorgang * `withGridVersion` benutzen (T-BWO-02, Store-Tests pinnen das). * * Idempotenz: `migrateGridLayouts(withGridVersion(migrateGridLayouts(alt).layouts))` * liefert dasselbe Ergebnis wie `migrateGridLayouts(alt)` mit `migrated: false`. * * Ort: Frontend, weil die Raster-Einheiten Frontend-Konstanten sind, die API * das JSON nur durchreicht (`@IsObject()`) und so kein Schreiben auf einem * GET und keine Aenderung am API-Dienst noetig ist. Reine Funktionen ohne * React- oder Store-Abhaengigkeit. */ export const GRID_VERSION = 2; export const GRID_VERSION_KEY = '__gridVersion'; export const GRID_SCALE_FACTOR = 2; export interface GridLayoutItem { i: string; x: number; y: number; w: number; h: number; [key: string]: unknown; } export type GridLayouts = Record; const SCALED_FIELDS = ['x', 'y', 'w', 'h', 'minW', 'minH', 'maxW', 'maxH'] as const; function isPlainObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } /** * Rechnet eine rohe (aus der API geladene) Anordnung in die aktuellen * Raster-Einheiten um. Liefert die Anordnung OHNE Marker und die Angabe, ob * etwas verdoppelt wurde (dann muss der Aufrufer sofort mit Marker speichern). */ export function migrateGridLayouts(raw: unknown): { layouts: GridLayouts; migrated: boolean } { if (!isPlainObject(raw)) { return { layouts: {}, migrated: false }; } const markerValue = raw[GRID_VERSION_KEY]; // Nur eine Zahl ist ein Marker; alles andere (fehlend, Zeichenkette) zaehlt als alt. const version = typeof markerValue === 'number' ? markerValue : 1; const needsScaling = version < GRID_VERSION; const layouts: GridLayouts = {}; let migrated = false; for (const key of Object.keys(raw)) { if (key === GRID_VERSION_KEY) continue; const value = raw[key]; if (!Array.isArray(value)) continue; layouts[key] = value.map((item) => { const copy = { ...(item as GridLayoutItem) }; if (needsScaling) { for (const field of SCALED_FIELDS) { const n = copy[field]; if (typeof n === 'number') { copy[field] = n * GRID_SCALE_FACTOR; } } migrated = true; } return copy; }); } return { layouts, migrated }; } /** * Haengt den Marker fuer das Speichern an, ohne die Eingabe zu veraendern. */ export function withGridVersion(layouts: GridLayouts): Record { return { ...layouts, [GRID_VERSION_KEY]: GRID_VERSION }; }