diff --git a/frontend/src/components/forms/settings/NavOrderForm.jsx b/frontend/src/components/forms/settings/NavOrderForm.jsx
new file mode 100644
index 00000000..9bd56fe2
--- /dev/null
+++ b/frontend/src/components/forms/settings/NavOrderForm.jsx
@@ -0,0 +1,213 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Box,
+ Button,
+ Text,
+ Group,
+ ActionIcon,
+ Stack,
+ useMantineTheme,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { GripVertical } from 'lucide-react';
+import {
+ closestCenter,
+ DndContext,
+ KeyboardSensor,
+ MouseSensor,
+ TouchSensor,
+ useSensor,
+ useSensors,
+} from '@dnd-kit/core';
+import {
+ arrayMove,
+ SortableContext,
+ useSortable,
+ verticalListSortingStrategy,
+} from '@dnd-kit/sortable';
+import { CSS } from '@dnd-kit/utilities';
+import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
+import useAuthStore from '../../../store/auth';
+import {
+ NAV_ITEMS,
+ DEFAULT_ADMIN_ORDER,
+ DEFAULT_USER_ORDER,
+ getOrderedNavItems,
+} from '../../../config/navigation';
+import { USER_LEVELS } from '../../../constants';
+
+const DraggableNavItem = ({ item }) => {
+ const theme = useMantineTheme();
+ const { transform, transition, setNodeRef, isDragging, attributes, listeners } = useSortable({
+ id: item.id,
+ });
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition: transition,
+ opacity: isDragging ? 0.8 : 1,
+ zIndex: isDragging ? 1 : 0,
+ position: 'relative',
+ };
+
+ const IconComponent = item.icon;
+
+ return (
+
+
+
+
+
+
+ {IconComponent && }
+
+ {item.label}
+
+
+
+
+ );
+};
+
+const NavOrderForm = ({ active }) => {
+ const theme = useMantineTheme();
+ const user = useAuthStore((s) => s.user);
+ const getNavOrder = useAuthStore((s) => s.getNavOrder);
+ const setNavOrder = useAuthStore((s) => s.setNavOrder);
+
+ const isAdmin = user?.user_level >= USER_LEVELS.ADMIN;
+ const defaultOrder = isAdmin ? DEFAULT_ADMIN_ORDER : DEFAULT_USER_ORDER;
+
+ const [items, setItems] = useState([]);
+ const [isSaving, setIsSaving] = useState(false);
+
+ const sensors = useSensors(
+ useSensor(MouseSensor, {}),
+ useSensor(TouchSensor, {}),
+ useSensor(KeyboardSensor, {})
+ );
+
+ useEffect(() => {
+ if (active) {
+ const savedOrder = getNavOrder();
+ const orderedItems = getOrderedNavItems(savedOrder, isAdmin);
+ setItems(orderedItems);
+ }
+ }, [active, isAdmin, getNavOrder]);
+
+ const handleDragEnd = async ({ active, over }) => {
+ if (!over || active.id === over.id) return;
+
+ const oldIndex = items.findIndex((item) => item.id === active.id);
+ const newIndex = items.findIndex((item) => item.id === over.id);
+ const newItems = arrayMove(items, oldIndex, newIndex);
+
+ // Optimistic update
+ setItems(newItems);
+
+ // Save to backend
+ setIsSaving(true);
+ try {
+ const newOrder = newItems.map((item) => item.id);
+ await setNavOrder(newOrder);
+ notifications.show({
+ title: 'Navigation',
+ message: 'Order saved successfully',
+ color: 'green',
+ autoClose: 2000,
+ });
+ } catch (error) {
+ // Revert on failure
+ const savedOrder = getNavOrder();
+ const orderedItems = getOrderedNavItems(savedOrder, isAdmin);
+ setItems(orderedItems);
+ notifications.show({
+ title: 'Error',
+ message: 'Failed to save navigation order',
+ color: 'red',
+ });
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleReset = async () => {
+ setIsSaving(true);
+ try {
+ await setNavOrder(defaultOrder);
+ const orderedItems = getOrderedNavItems(defaultOrder, isAdmin);
+ setItems(orderedItems);
+ notifications.show({
+ title: 'Navigation',
+ message: 'Reset to default order',
+ color: 'blue',
+ autoClose: 2000,
+ });
+ } catch (error) {
+ notifications.show({
+ title: 'Error',
+ message: 'Failed to reset navigation order',
+ color: 'red',
+ });
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ if (!active) {
+ return null;
+ }
+
+ return (
+
+
+ Drag and drop to reorder the sidebar navigation items.
+
+
+
+ item.id)}
+ strategy={verticalListSortingStrategy}
+ >
+ {items.map((item) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+export default NavOrderForm;
diff --git a/frontend/src/components/forms/settings/__tests__/NavOrderForm.test.jsx b/frontend/src/components/forms/settings/__tests__/NavOrderForm.test.jsx
new file mode 100644
index 00000000..3d60e11c
--- /dev/null
+++ b/frontend/src/components/forms/settings/__tests__/NavOrderForm.test.jsx
@@ -0,0 +1,190 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import userEvent from '@testing-library/user-event';
+import NavOrderForm from '../NavOrderForm';
+import useAuthStore from '../../../../store/auth';
+import { USER_LEVELS } from '../../../../constants';
+import { DEFAULT_ADMIN_ORDER, DEFAULT_USER_ORDER } from '../../../../config/navigation';
+
+// Mock dependencies
+vi.mock('../../../../store/auth');
+vi.mock('@mantine/notifications', () => ({
+ notifications: {
+ show: vi.fn(),
+ },
+}));
+
+// Mock dnd-kit
+vi.mock('@dnd-kit/core', () => ({
+ DndContext: ({ children }) =>
{children}
,
+ closestCenter: vi.fn(),
+ KeyboardSensor: vi.fn(),
+ MouseSensor: vi.fn(),
+ TouchSensor: vi.fn(),
+ useSensor: vi.fn(),
+ useSensors: vi.fn(() => []),
+}));
+
+vi.mock('@dnd-kit/sortable', () => ({
+ SortableContext: ({ children }) => {children}
,
+ useSortable: () => ({
+ transform: null,
+ transition: null,
+ setNodeRef: vi.fn(),
+ isDragging: false,
+ attributes: {},
+ listeners: {},
+ }),
+ arrayMove: vi.fn((arr, from, to) => {
+ const result = [...arr];
+ const [removed] = result.splice(from, 1);
+ result.splice(to, 0, removed);
+ return result;
+ }),
+ verticalListSortingStrategy: vi.fn(),
+}));
+
+vi.mock('@dnd-kit/utilities', () => ({
+ CSS: {
+ Transform: {
+ toString: vi.fn(() => ''),
+ },
+ },
+}));
+
+vi.mock('@dnd-kit/modifiers', () => ({
+ restrictToVerticalAxis: vi.fn(),
+}));
+
+// Mock Mantine components
+vi.mock('@mantine/core', () => ({
+ Box: ({ children, ...props }) => {children}
,
+ Button: ({ children, onClick, disabled, ...props }) => (
+
+ ),
+ Text: ({ children }) => {children},
+ Group: ({ children }) => {children}
,
+ ActionIcon: ({ children, ...props }) => ,
+ Stack: ({ children }) => {children}
,
+ useMantineTheme: () => ({}),
+}));
+
+describe('NavOrderForm', () => {
+ const mockSetNavOrder = vi.fn();
+ const mockGetNavOrder = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockSetNavOrder.mockResolvedValue({});
+ mockGetNavOrder.mockReturnValue(null);
+ });
+
+ describe('Admin User', () => {
+ beforeEach(() => {
+ useAuthStore.mockImplementation((selector) => {
+ const state = {
+ user: { user_level: USER_LEVELS.ADMIN, custom_properties: {} },
+ getNavOrder: mockGetNavOrder,
+ setNavOrder: mockSetNavOrder,
+ };
+ return selector(state);
+ });
+ });
+
+ it('renders all nav items for admin user', () => {
+ render();
+
+ expect(screen.getByText('Channels')).toBeInTheDocument();
+ expect(screen.getByText('VODs')).toBeInTheDocument();
+ expect(screen.getByText('M3U & EPG Manager')).toBeInTheDocument();
+ expect(screen.getByText('TV Guide')).toBeInTheDocument();
+ expect(screen.getByText('DVR')).toBeInTheDocument();
+ expect(screen.getByText('Stats')).toBeInTheDocument();
+ expect(screen.getByText('Plugins')).toBeInTheDocument();
+ expect(screen.getByText('Users')).toBeInTheDocument();
+ expect(screen.getByText('Logo Manager')).toBeInTheDocument();
+ expect(screen.getByText('Settings')).toBeInTheDocument();
+ });
+
+ it('renders reset to default button', () => {
+ render();
+
+ expect(screen.getByTestId('reset-button')).toBeInTheDocument();
+ expect(screen.getByText('Reset to Default')).toBeInTheDocument();
+ });
+
+ it('does not render when not active', () => {
+ render();
+
+ expect(screen.queryByText('Channels')).not.toBeInTheDocument();
+ });
+
+ it('calls setNavOrder when reset button is clicked', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const resetButton = screen.getByTestId('reset-button');
+ await user.click(resetButton);
+
+ await waitFor(() => {
+ expect(mockSetNavOrder).toHaveBeenCalledWith(DEFAULT_ADMIN_ORDER);
+ });
+ });
+
+ it('uses saved order when available', () => {
+ const customOrder = ['settings', 'channels', 'vods', 'sources', 'guide', 'dvr', 'stats', 'plugins', 'users', 'logos'];
+ mockGetNavOrder.mockReturnValue(customOrder);
+
+ render();
+
+ // The component should render with custom order
+ expect(screen.getByText('Channels')).toBeInTheDocument();
+ expect(screen.getByText('Settings')).toBeInTheDocument();
+ });
+ });
+
+ describe('Non-Admin User', () => {
+ beforeEach(() => {
+ useAuthStore.mockImplementation((selector) => {
+ const state = {
+ user: { user_level: USER_LEVELS.USER, custom_properties: {} },
+ getNavOrder: mockGetNavOrder,
+ setNavOrder: mockSetNavOrder,
+ };
+ return selector(state);
+ });
+ });
+
+ it('renders only non-admin nav items for regular user', () => {
+ render();
+
+ // Non-admin items should be visible
+ expect(screen.getByText('Channels')).toBeInTheDocument();
+ expect(screen.getByText('TV Guide')).toBeInTheDocument();
+ expect(screen.getByText('Settings')).toBeInTheDocument();
+
+ // Admin-only items should not be visible
+ expect(screen.queryByText('VODs')).not.toBeInTheDocument();
+ expect(screen.queryByText('M3U & EPG Manager')).not.toBeInTheDocument();
+ expect(screen.queryByText('DVR')).not.toBeInTheDocument();
+ expect(screen.queryByText('Stats')).not.toBeInTheDocument();
+ expect(screen.queryByText('Plugins')).not.toBeInTheDocument();
+ expect(screen.queryByText('Users')).not.toBeInTheDocument();
+ expect(screen.queryByText('Logo Manager')).not.toBeInTheDocument();
+ });
+
+ it('calls setNavOrder with user default order when reset', async () => {
+ const user = userEvent.setup();
+ render();
+
+ const resetButton = screen.getByTestId('reset-button');
+ await user.click(resetButton);
+
+ await waitFor(() => {
+ expect(mockSetNavOrder).toHaveBeenCalledWith(DEFAULT_USER_ORDER);
+ });
+ });
+ });
+});