'use client' import { useCallback, useEffect, useRef, useState } from 'react' import { DndContext, closestCenter, PointerSensor, TouchSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core' import { SortableContext, rectSortingStrategy, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { GripVertical, X, Plus, RotateCcw, Check, LayoutGrid, Eye, EyeOff } from 'lucide-react' import { useLanguage } from '@/lib/i18n' import { getDefaultDashboardLayout, resetDashboardLayout, DASHBOARD_CATEGORY_ORDER, DASHBOARD_WIDGET_META, catalogByCategory, hiddenWidgetIds, isWidgetVisible, normalizeDashboardLayout, reorderWidgets, setWidgetVisibility, visibleWidgetsInZone, type DashboardLayout, type DashboardWidgetId, type DashboardWidgetPlacement, type DashboardWidgetZone, } from '@/lib/dashboard/layout' import { toast } from 'sonner' interface DashboardWidgetGridProps { renderWidget: (id: DashboardWidgetId) => React.ReactNode } function SortableWidget({ placement, editMode, onHide, children, }: { placement: DashboardWidgetPlacement editMode: boolean onHide: (id: DashboardWidgetId) => void children: React.ReactNode }) { const { t } = useLanguage() const meta = DASHBOARD_WIDGET_META[placement.id] const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: placement.id, disabled: !editMode }) const style = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 20 : undefined, } return (
{editMode && (
{!meta.required && ( )}
)} {children}
) } function ZoneColumn({ zone, placements, editMode, onHide, renderWidget, }: { zone: DashboardWidgetZone placements: DashboardWidgetPlacement[] editMode: boolean onHide: (id: DashboardWidgetId) => void renderWidget: (id: DashboardWidgetId) => React.ReactNode }) { if (placements.length === 0) return null return ( w.id)} strategy={verticalListSortingStrategy}>
{placements.map(placement => ( {renderWidget(placement.id)} ))}
) } export function DashboardWidgetGrid({ renderWidget }: DashboardWidgetGridProps) { const { t } = useLanguage() const [layout, setLayout] = useState(() => getDefaultDashboardLayout()) const [editMode, setEditMode] = useState(false) const [catalogOpen, setCatalogOpen] = useState(false) const [loaded, setLoaded] = useState(false) const saveTimer = useRef | null>(null) useEffect(() => { fetch('/api/dashboard/layout', { cache: 'no-store' }) .then(r => r.ok ? r.json() : null) .then(json => { if (json?.layout) setLayout(normalizeDashboardLayout(json.layout)) }) .catch(() => {}) .finally(() => setLoaded(true)) }, []) const persistLayout = useCallback((next: DashboardLayout, immediate = false) => { if (saveTimer.current) clearTimeout(saveTimer.current) const save = () => { fetch('/api/dashboard/layout', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ layout: next }), }).catch(() => {}) } if (immediate) save() else saveTimer.current = setTimeout(save, 600) }, []) const applyLayout = useCallback((next: DashboardLayout, immediate = false) => { const normalized = normalizeDashboardLayout(next) setLayout(normalized) persistLayout(normalized, immediate) }, [persistLayout]) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(TouchSensor, { activationConstraint: { delay: 180, tolerance: 8 } }), ) const handleDragEnd = useCallback((event: DragEndEvent) => { const { active, over } = event if (!over || active.id === over.id) return applyLayout(reorderWidgets(layout, active.id as DashboardWidgetId, over.id as DashboardWidgetId)) }, [applyLayout, layout]) const handleHide = useCallback((id: DashboardWidgetId) => { applyLayout(setWidgetVisibility(layout, id, false)) }, [applyLayout, layout]) const handleToggle = useCallback((id: DashboardWidgetId) => { const visible = isWidgetVisible(layout, id) if (visible) { applyLayout(setWidgetVisibility(layout, id, false)) } else { const maxOrder = Math.max(...layout.widgets.map(w => w.order), 0) applyLayout({ ...layout, widgets: layout.widgets.map(w => w.id === id ? { ...w, visible: true, order: maxOrder + 1 } : w, ), }) } }, [applyLayout, layout]) const handleReset = useCallback(() => { const fresh = resetDashboardLayout() setLayout(fresh) persistLayout(fresh, true) setCatalogOpen(false) toast.success(t('homeDashboard.widgetResetDone')) }, [persistLayout, t]) const fullWidgets = visibleWidgetsInZone(layout, 'full') const mainWidgets = visibleWidgetsInZone(layout, 'main') const sideWidgets = visibleWidgetsInZone(layout, 'side') const hidden = hiddenWidgetIds(layout) const catalog = catalogByCategory(layout) const hasVisibleWidgets = fullWidgets.length + mainWidgets.length + sideWidgets.length > 0 return ( <>
{loaded ? ( hasVisibleWidgets ? ( {fullWidgets.length > 0 && ( w.id)} strategy={rectSortingStrategy}>
{fullWidgets.map(placement => ( {renderWidget(placement.id)} ))}
)} {(mainWidgets.length > 0 || sideWidgets.length > 0) && (
{mainWidgets.length > 0 && (
)} {sideWidgets.length > 0 && (
)}
)}
) : (

{t('homeDashboard.layoutEmptyTitle')}

{t('homeDashboard.layoutEmptyHint')}

) ) : (
)}
{editMode ? ( <> ) : ( )}
{editMode && catalogOpen && (

{t('homeDashboard.widgetCatalogTitle')}

{DASHBOARD_CATEGORY_ORDER.map(category => { const ids = catalog[category] if (ids.length === 0) return null return (

{t(`homeDashboard.widgetCategories.${category}`)}

{ids.map(id => { const meta = DASHBOARD_WIDGET_META[id] const active = isWidgetVisible(layout, id) return ( ) })}
) })}
)} ) }