import { useState, useEffect, useRef, useCallback } from 'react'; import { useAuth } from '../../contexts/AuthContext'; import { useTranslation } from '../../contexts/LanguageContext'; import { useSidebar } from '../../contexts/SidebarContext'; import { useModules, TOGGLEABLE_MODULES } from '../../contexts/ModulesContext'; import type { ModuleId } from '../../contexts/ModulesContext'; import Feature1Tab from '../../components/admin/Feature1Tab'; import { SwipeTabs } from '../../components/SwipeTabs'; import '../../styles/AdminPanel.css'; type TabId = 'config' | 'dashboard' | 'feature1' | 'feature2' | 'feature3' | 'search' | 'notifications'; export default function Features() { const { user: currentUser } = useAuth(); const { t } = useTranslation(); const { toggleMobileMenu } = useSidebar(); const { moduleStates, moduleOrder, setModuleEnabled, setModulePosition, setModuleOrder, saveModulesToBackend, saveModuleOrder, hasInitialized, isLoading } = useModules(); const [activeTab, setActiveTab] = useState('config'); const hasUserMadeChanges = useRef(false); const saveRef = useRef(saveModulesToBackend); const [draggedItem, setDraggedItem] = useState(null); const [localOrder, setLocalOrder] = useState([]); const [localPositions, setLocalPositions] = useState>({}); const [hasOrderChanges, setHasOrderChanges] = useState(false); const tabsContainerRef = useRef(null); const isUserEditing = useRef(false); // Track if user is actively editing // Sync local order with context order (only when moduleOrder changes, not moduleStates) useEffect(() => { if (moduleOrder.length > 0 && !isUserEditing.current) { // Start with current order const fullOrder = [...moduleOrder]; // Insert missing modules at their default positions from TOGGLEABLE_MODULES TOGGLEABLE_MODULES.forEach((module, defaultIndex) => { if (!fullOrder.includes(module.id)) { // Insert at the default index position, or at end if index is beyond current length const insertAt = Math.min(defaultIndex, fullOrder.length); fullOrder.splice(insertAt, 0, module.id); } }); setLocalOrder(fullOrder); setHasOrderChanges(false); } }, [moduleOrder]); // Sync positions from moduleStates only on initial load or when not editing useEffect(() => { if (!isUserEditing.current) { const positions: Record = {}; TOGGLEABLE_MODULES.forEach(module => { const state = moduleStates[module.id]; positions[module.id] = state?.position || module.defaultPosition; }); setLocalPositions(positions); } }, [moduleStates]); // Scroll active tab to center of container const scrollActiveTabIntoView = useCallback((tabId: string) => { if (tabsContainerRef.current) { const container = tabsContainerRef.current; const activeButton = container.querySelector(`[data-tab-id="${tabId}"]`) as HTMLElement; if (activeButton) { const containerWidth = container.clientWidth; const buttonLeft = activeButton.offsetLeft; const buttonWidth = activeButton.offsetWidth; // Calculate scroll position to center the button const scrollLeft = buttonLeft - (containerWidth / 2) + (buttonWidth / 2); container.scrollTo({ left: Math.max(0, scrollLeft), behavior: 'smooth' }); } } }, []); // Handle tab change with scroll const handleTabChange = useCallback((tabId: TabId) => { setActiveTab(tabId); setTimeout(() => scrollActiveTabIntoView(tabId), 50); }, [scrollActiveTabIntoView]); // Keep saveRef updated with latest function useEffect(() => { saveRef.current = saveModulesToBackend; }, [saveModulesToBackend]); if (!currentUser?.is_superuser) { return null; } const handleModuleToggle = async (moduleId: ModuleId, type: 'admin' | 'user', enabled: boolean) => { hasUserMadeChanges.current = true; setModuleEnabled(moduleId, type, enabled); }; // Save changes when moduleStates change, but debounce to avoid too many requests // Only save if: 1) Backend data has been loaded, and 2) User has made changes useEffect(() => { if (!hasInitialized || !hasUserMadeChanges.current) { return; } const timeoutId = setTimeout(() => { saveRef.current().catch(console.error); }, 300); return () => clearTimeout(timeoutId); }, [moduleStates, hasInitialized]); // Save on unmount if there are pending changes (empty deps = only on unmount) useEffect(() => { return () => { if (hasUserMadeChanges.current) { saveRef.current().catch(console.error); } }; }, []); const renderModuleToggle = (moduleId: ModuleId) => { const state = moduleStates[moduleId]; const adminEnabled = state?.admin ?? true; const userEnabled = state?.user ?? true; return (

{t.featuresPage?.visibility || 'Visibilità'}

{adminEnabled ? t.admin.active : t.admin.inactive}
{t.admin.adminRole}
{t.admin.userRole}
); }; // Drag and drop handlers for ordering const handleDragStart = (e: React.DragEvent, moduleId: string) => { setDraggedItem(moduleId); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', moduleId); // Add dragging class after a small delay for visual feedback setTimeout(() => { (e.target as HTMLElement).classList.add('dragging'); }, 0); }; const handleDragEnd = (e: React.DragEvent) => { setDraggedItem(null); (e.target as HTMLElement).classList.remove('dragging'); }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }; const handleDrop = (e: React.DragEvent, targetModuleId: string, targetSection: 'top' | 'bottom') => { e.preventDefault(); e.stopPropagation(); if (!draggedItem) return; const draggedPosition = localPositions[draggedItem] || 'top'; // If dropping on same item, just change section if different if (draggedItem === targetModuleId) { if (draggedPosition !== targetSection) { isUserEditing.current = true; // Mark as user editing hasUserMadeChanges.current = true; setModulePosition(draggedItem as ModuleId, targetSection); // Update local positions immediately for UI responsiveness setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection })); setHasOrderChanges(true); } return; } // Change position if moving to different section if (draggedPosition !== targetSection) { isUserEditing.current = true; // Mark as user editing hasUserMadeChanges.current = true; setModulePosition(draggedItem as ModuleId, targetSection); // Update local positions immediately for UI responsiveness setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection })); } // Reorder within the list isUserEditing.current = true; // Mark as user editing const newOrder = [...localOrder]; const draggedIndex = newOrder.indexOf(draggedItem); const targetIndex = newOrder.indexOf(targetModuleId); if (draggedIndex === -1 || targetIndex === -1) return; newOrder.splice(draggedIndex, 1); newOrder.splice(targetIndex, 0, draggedItem); setLocalOrder(newOrder); setHasOrderChanges(true); }; const handleSectionDrop = (e: React.DragEvent, section: 'top' | 'bottom') => { e.preventDefault(); if (!draggedItem) return; const draggedPosition = localPositions[draggedItem] || 'top'; // Change position if moving to different section if (draggedPosition !== section) { isUserEditing.current = true; // Mark as user editing hasUserMadeChanges.current = true; setModulePosition(draggedItem as ModuleId, section); // Update local positions immediately for UI responsiveness setLocalPositions(prev => ({ ...prev, [draggedItem]: section })); setHasOrderChanges(true); } }; const handleApplyOrder = async () => { try { setModuleOrder(localOrder); await saveModuleOrder(localOrder); } catch (error) { console.error('Failed to save order:', error); } finally { setDraggedItem(null); // Reset drag state isUserEditing.current = false; setHasOrderChanges(false); } }; const handleCancelOrder = () => { setDraggedItem(null); // Reset drag state isUserEditing.current = false; // Done editing setLocalOrder(moduleOrder); // Reset positions from moduleStates const positions: Record = {}; TOGGLEABLE_MODULES.forEach(module => { const state = moduleStates[module.id]; positions[module.id] = state?.position || module.defaultPosition; }); setLocalPositions(positions); setHasOrderChanges(false); }; const getModuleInfo = (moduleId: string) => { const module = TOGGLEABLE_MODULES.find(m => m.id === moduleId); return module || { id: moduleId, icon: 'extension', defaultEnabled: true }; }; // Split modules by position for the config tab (using localPositions for immediate UI updates) const topOrderModules = localOrder.filter(id => { const position = localPositions[id]; return !position || position === 'top'; }); const bottomOrderModules = localOrder.filter(id => { const position = localPositions[id]; return position === 'bottom'; }); const tabIds = ['config', ...topOrderModules, ...bottomOrderModules] as TabId[]; const renderConfigTab = () => { return (

{t.featuresPage?.topSection || 'Sezione Principale'}

handleSectionDrop(e, 'top')} > {topOrderModules.map((moduleId) => { const moduleInfo = getModuleInfo(moduleId); const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId; return (
handleDragStart(e, moduleId)} onDragEnd={handleDragEnd} onDragOver={handleDragOver} onDrop={(e) => handleDrop(e, moduleId, 'top')} >
{moduleInfo.icon}
{moduleName}
drag_indicator
); })} {topOrderModules.length === 0 && (
{t.featuresPage?.noModulesTop || 'Nessun modulo in questa sezione'}
)}

{t.featuresPage?.bottomSection || 'Sezione Inferiore'}

handleSectionDrop(e, 'bottom')} > {bottomOrderModules.map((moduleId) => { const moduleInfo = getModuleInfo(moduleId); const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId; return (
handleDragStart(e, moduleId)} onDragEnd={handleDragEnd} onDragOver={handleDragOver} onDrop={(e) => handleDrop(e, moduleId, 'bottom')} >
{moduleInfo.icon}
{moduleName}
drag_indicator
); })} {bottomOrderModules.length === 0 && (
{t.featuresPage?.noModulesBottom || 'Nessun modulo in questa sezione'}
)}
{hasOrderChanges && (
)}
); }; const renderTabContent = (tabId: TabId) => { if (!hasInitialized || isLoading) { return
Loading...
; } switch (tabId) { case 'config': return renderConfigTab(); case 'dashboard': return ( <> {renderModuleToggle('dashboard')}
dashboard

{t.sidebar.dashboard}

{t.features.comingSoon}

); case 'feature1': return ( <> {renderModuleToggle('feature1')} ); case 'feature2': return ( <> {renderModuleToggle('feature2')}
download

{t.features.feature2}

{t.features.comingSoon}

); case 'feature3': return ( <> {renderModuleToggle('feature3')}
cast

{t.features.feature3}

{t.features.comingSoon}

); case 'search': return ( <> {renderModuleToggle('search')}
search

{t.sidebar.search}

{t.features.comingSoon}

); case 'notifications': return ( <> {renderModuleToggle('notifications')}
notifications

{t.sidebar.notifications}

{t.features.comingSoon}

); default: return null; } }; return (
{t.featuresPage.title}
{[...topOrderModules, ...bottomOrderModules].map((moduleId) => { const moduleInfo = getModuleInfo(moduleId); const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId; return ( ); })}
); }