'use client'; import { useState } from 'react'; import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core'; import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { GripVertical, LayoutGrid } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { Switch } from '@/components/ui/switch'; import { cn } from '@/lib/utils'; import { useDashboardWidgets } from '@/hooks/use-dashboard-widgets'; import type { DashboardWidget, WidgetGroup } from './widget-registry'; // The dashboard renders widgets in three independent visual regions at // xl (1280+): charts (main column), rails (right aside), feed (full- // width). Below xl, all three regions stack into one visual column - // from the rep's eye it reads as a single ordered list, so the modal // flattens its sortable in that tier. At xl it splits into three // region-scoped sortables to match the actual side-by-side layout. const GROUP_LABELS: Record = { chart: 'Charts', rail: 'Side rail', feed: 'Activity', }; const GROUP_ORDER: readonly WidgetGroup[] = ['chart', 'rail', 'feed']; /** * Combined visibility + reorder picker for the dashboard header. * * The dashboard renders widgets in three independent visual regions - * Charts (main column), Side rail (right aside), Activity (full-width * feed). A drag across regions can't change the visual outcome, so the * modal exposes one sortable list per region instead of a single flat * list that silently fails on cross-region moves. Toggling a widget off * moves it to the "Hidden" section; toggling on appends it to the * bottom of its native region. * * Both visibility toggles and order changes commit optimistically via * `useDashboardWidgets` so the dashboard reflows in the background and * the rep can keep editing. */ export function CustomizeWidgetsMenu() { const [open, setOpen] = useState(false); const { allWidgets, visibleWidgets, visibility, isXlLayout, setVisible, setAll, setOrder, resetToDefaults, isSaving, } = useDashboardWidgets(); const visibleCount = Object.values(visibility).filter(Boolean).length; const allVisible = visibleCount === allWidgets.length; const allHidden = visibleCount === 0; const matchesDefaults = allWidgets.every((w) => (visibility[w.id] ?? false) === w.defaultVisible); // Hidden = everything not currently rendered. Sorted by registry order // so it reads predictably (newly-added widgets appear at the bottom // until the rep explicitly enables them). const hidden = allWidgets.filter((w) => !visibility[w.id]); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ); // Visible widgets split per region. Empty regions render nothing so // we don't show an "On dashboard / Side rail (0)" tease. const visibleByGroup: Record = { chart: visibleWidgets.filter((w) => w.group === 'chart'), rail: visibleWidgets.filter((w) => w.group === 'rail'), feed: visibleWidgets.filter((w) => w.group === 'feed'), }; // A drag inside group X only moves widgets within that group. Rebuild // the flat order by walking `visibleWidgets` in its current sequence // and replacing each group-X slot with the next id from the reordered // group list. This preserves the relative position of every other // widget - only the dragged group's internal order changes. function reorderGroup(group: WidgetGroup, oldIndex: number, newIndex: number) { const groupIds = visibleByGroup[group].map((w) => w.id); if ( oldIndex < 0 || newIndex < 0 || oldIndex >= groupIds.length || newIndex >= groupIds.length ) { return; } const reordered = arrayMove(groupIds, oldIndex, newIndex); let cursor = 0; const nextOrder = visibleWidgets.map((w) => w.group === group ? (reordered[cursor++] ?? w.id) : w.id, ); setOrder(nextOrder); } function makeDragEndHandler(group: WidgetGroup) { return (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; const ids = visibleByGroup[group].map((w) => w.id); const oldIndex = ids.indexOf(String(active.id)); const newIndex = ids.indexOf(String(over.id)); if (oldIndex === -1 || newIndex === -1) return; reorderGroup(group, oldIndex, newIndex); }; } // Flat reorder used by the stacked layout (< xl). One SortableContext // over every visible widget; drops persist via setOrder, which the // hook routes to the mobile order field. function onFlatDragEnd(event: DragEndEvent) { const { active, over } = event; if (!over || active.id === over.id) return; const ids = visibleWidgets.map((w) => w.id); const oldIndex = ids.indexOf(String(active.id)); const newIndex = ids.indexOf(String(over.id)); if (oldIndex === -1 || newIndex === -1) return; setOrder(arrayMove(ids, oldIndex, newIndex)); } return ( Customize dashboard {isXlLayout ? 'Editing the desktop layout - drag a widget to reorder it within its region.' : 'Editing the stacked layout for this device - drag a widget to reorder. Your desktop arrangement is saved separately.'}{' '} Toggle the switch to show or hide. Hidden widgets leave no empty space - the layout reflows to fill the available width. {/* Toggle + reorder list. Capped at ~60vh with internal scroll so the modal doesn't push the action footer off-screen. The layout matches what the rep is actually seeing: at xl the dashboard renders charts | rails | feed as three independent slots, so the picker exposes three region-scoped sortables. Below xl everything stacks into one column visually, so the picker collapses to a single flat sortable that reorders across the whole list. */}
{isXlLayout ? ( GROUP_ORDER.map((group) => { const widgets = visibleByGroup[group]; if (widgets.length === 0) return null; return (
w.id)} strategy={verticalListSortingStrategy} >
    {widgets.map((w, idx) => ( setVisible(w.id, checked)} /> ))}
); }) ) : visibleWidgets.length > 0 ? (
w.id)} strategy={verticalListSortingStrategy} >
    {visibleWidgets.map((w, idx) => ( setVisible(w.id, checked)} /> ))}
) : null} {hidden.length > 0 ? (
    {hidden.map((w) => ( setVisible(w.id, checked)} /> ))}
) : null}
{visibleCount} of {allWidgets.length} visible
); } function Section({ title, children }: { title: string; children: React.ReactNode }) { return (
{title}
{children}
); } function SortableVisibleRow({ widget, position, disabled, onToggle, }: { widget: DashboardWidget; position: number; disabled: boolean; onToggle: (checked: boolean) => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: widget.id, }); return (
  • {position}
    {widget.label}

    {widget.description}

  • ); } function HiddenRow({ widget, disabled, onToggle, }: { widget: DashboardWidget; disabled: boolean; onToggle: (checked: boolean) => void; }) { return (
  • {widget.label}

    {widget.description}

  • ); }