import { create } from 'zustand'; import * as api from '@/lib/dashboard-api'; import type { WidgetType } from '@/components/dashboard/widget-registry'; import { WIDGET_CONSTRAINTS } from '@/components/dashboard/widget-registry'; import { migrateGridLayouts, withGridVersion } from '@/lib/grid-layout-migration'; export interface WidgetInstance { id: string; widgetType: string; config: Record; } interface DashboardState { layouts: Record>; widgets: WidgetInstance[]; isEditMode: boolean; isDirty: boolean; isLoading: boolean; error: string | null; setEditMode: (mode: boolean) => void; updateLayouts: (layouts: Record) => void; addWidget: (widgetType: WidgetType) => Promise; removeWidget: (id: string) => Promise; loadDashboard: () => Promise; saveLayout: () => Promise; } /** * Dashboard Zustand store. NO persist middleware — layout comes from DB (D-05). * Save only on exiting edit mode (D-01), not on every drag. * * quick-260916-bwo: Gespeicherte Anordnungen in alten Raster-Einheiten werden * beim Laden einmalig umgerechnet (`migrateGridLayouts`) und sofort mit dem * Marker `__gridVersion` zurueckgeschrieben. Der Marker lebt NUR im * gespeicherten JSON, nie im Zustand (dieser Store iteriert mit Object.keys * ueber die Breakpoints). Fehlt der Marker beim Speichern, wird beim naechsten * Laden erneut verdoppelt — deshalb `withGridVersion` an BEIDEN Speicherstellen * (Sofort-Speichern nach der Umrechnung und `saveLayout`), T-BWO-02. */ export const useDashboardStore = create()((set, get) => ({ layouts: { lg: [], md: [], sm: [], xs: [], xxs: [] }, widgets: [], isEditMode: false, isDirty: false, isLoading: false, error: null, setEditMode: (mode: boolean) => { const prev = get().isEditMode; set({ isEditMode: mode }); // On exit edit mode, auto-save if dirty (D-01) if (prev && !mode && get().isDirty) { get().saveLayout(); } }, updateLayouts: (layouts) => { set({ layouts: layouts as DashboardState['layouts'], isDirty: true, }); }, addWidget: async (widgetType: WidgetType) => { try { const newWidget = await api.addWidget(widgetType); const constraints = WIDGET_CONSTRAINTS[widgetType]; set((state) => { // Add default layout entry for all breakpoints const newLayouts = { ...state.layouts }; for (const bp of Object.keys(newLayouts)) { const arr = [...(newLayouts[bp] || [])]; // Place at next available y position const maxY = arr.reduce((max, item) => Math.max(max, item.y + item.h), 0); arr.push({ i: newWidget.id, x: 0, y: maxY, w: constraints.defaultW, h: constraints.defaultH, }); newLayouts[bp] = arr; } return { widgets: [...state.widgets, newWidget], layouts: newLayouts, isDirty: true, }; }); } catch (err) { console.error('Failed to add widget:', err); } }, removeWidget: async (id: string) => { try { await api.removeWidget(id); set((state) => { const newLayouts = { ...state.layouts }; for (const bp of Object.keys(newLayouts)) { newLayouts[bp] = newLayouts[bp].filter((item) => item.i !== id); } return { widgets: state.widgets.filter((w) => w.id !== id), layouts: newLayouts, isDirty: true, }; }); } catch (err) { console.error('Failed to remove widget:', err); } }, loadDashboard: async () => { set({ isLoading: true, error: null }); try { const [rawLayouts, widgets] = await Promise.all([ api.fetchLayout(), api.fetchWidgets(), ]); const { layouts: migratedLayouts, migrated } = migrateGridLayouts(rawLayouts); set({ layouts: migratedLayouts, widgets, isLoading: false, }); // Umgerechnete Anordnung SOFORT mit Marker persistieren — nach dem set, // damit die Oberflaeche unabhaengig vom Speichern rendert; eigener // try/catch, damit ein Speicherfehler NICHT als Ladefehler erscheint. if (migrated) { try { await api.saveLayout(withGridVersion(migratedLayouts)); } catch (err) { console.error('Failed to persist migrated layout:', err); } } } catch { set({ error: 'Failed to load dashboard', isLoading: false, }); } }, saveLayout: async () => { try { await api.saveLayout(withGridVersion(get().layouts)); set({ isDirty: false }); } catch (err) { console.error('Failed to save layout:', err); } }, }));