Files
zentral/src/frontend/src/components/Sidebar.tsx

301 lines
12 KiB
TypeScript

import React, { useState } from 'react';
import {
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Collapse,
Box,
Typography,
Divider,
Tooltip,
IconButton,
} from '@mui/material';
import {
ExpandLess,
ExpandMore,
Dashboard as DashboardIcon,
Event as EventIcon,
People as PeopleIcon,
Place as PlaceIcon,
CalendarMonth as CalendarIcon,
Print as PrintIcon,
Extension as ModulesIcon,
Warehouse as WarehouseIcon,
Code as AutoCodeIcon,
ShoppingCart as ShoppingCartIcon,
Sell as SellIcon,
Factory as ProductionIcon,
Settings as SettingsIcon,
Storage as StorageIcon,
SwapHoriz as SwapIcon,
Assignment as AssignmentIcon,
ListAlt as ListAltIcon,
Build as BuildIcon,
Timeline as TimelineIcon,
PrecisionManufacturing as ManufacturingIcon,
Category as CategoryIcon,
Folder as FolderIcon,
AttachMoney as AttachMoneyIcon,
Receipt as ReceiptIcon,
ChevronLeft,
ChevronRight,
Email as EmailIcon,
School as SchoolIcon,
} from '@mui/icons-material';
import { useLocation } from 'react-router-dom';
import { useLanguage } from '../contexts/LanguageContext';
import { useApps } from '../contexts/AppContext';
import { useTabs } from '../contexts/TabContext';
interface MenuItem {
id: string;
label: string;
tabLabel?: string;
icon?: React.ReactNode;
path?: string;
children?: MenuItem[];
appCode?: string;
translationKey?: string;
}
interface SidebarProps {
onClose?: () => void;
isCollapsed?: boolean;
onToggleCollapse?: () => void;
}
export default function Sidebar({ onClose, isCollapsed = false, onToggleCollapse }: SidebarProps) {
const { t } = useLanguage();
const { activeApps } = useApps();
const { openTab } = useTabs();
const location = useLocation();
const [openItems, setOpenItems] = useState<Record<string, boolean>>({
core: true,
warehouse: false,
purchases: false,
sales: false,
production: false,
events: false,
hr: false,
training: false,
admin: false,
});
const handleToggle = (id: string) => {
setOpenItems((prev) => ({ ...prev, [id]: !prev[id] }));
};
const handleItemClick = (item: MenuItem) => {
if (isCollapsed && item.children && onToggleCollapse) {
onToggleCollapse();
setOpenItems((prev) => ({ ...prev, [item.id]: true }));
return;
}
if (item.path) {
openTab(item.path, item.tabLabel || item.label, true, item.translationKey);
if (onClose) onClose();
} else if (item.children) {
handleToggle(item.id);
}
};
const menuStructure: MenuItem[] = [
{
id: 'dashboard',
label: t('menu.dashboard'),
icon: <DashboardIcon />,
path: '/',
translationKey: 'menu.dashboard',
},
{
id: 'warehouse',
label: t('menu.warehouse'),
icon: <WarehouseIcon />,
appCode: 'warehouse',
translationKey: 'menu.warehouse',
children: [
{ id: 'wh-dashboard', label: t('menu.dashboard'), tabLabel: t('menu.warehouse'), icon: <DashboardIcon />, path: '/warehouse', translationKey: 'menu.warehouse' },
{ id: 'wh-articles', label: t('menu.articles'), icon: <CategoryIcon />, path: '/warehouse/articles', translationKey: 'menu.articles' },
{ id: 'wh-categories', label: t('menu.categories'), icon: <FolderIcon />, path: '/warehouse/categories', translationKey: 'menu.categories' },
{ id: 'wh-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/warehouse/locations', translationKey: 'menu.location' },
{ id: 'wh-movements', label: t('menu.movements'), icon: <SwapIcon />, path: '/warehouse/movements', translationKey: 'menu.movements' },
{ id: 'wh-stock', label: t('menu.stock'), icon: <StorageIcon />, path: '/warehouse/stock', translationKey: 'menu.stock' },
{ id: 'wh-inventory', label: t('menu.inventory'), icon: <AssignmentIcon />, path: '/warehouse/inventory', translationKey: 'menu.inventory' },
],
},
{
id: 'purchases',
label: t('menu.purchases'),
icon: <ShoppingCartIcon />,
appCode: 'purchases',
translationKey: 'menu.purchases',
children: [
{ id: 'pur-suppliers', label: t('menu.suppliers'), icon: <PeopleIcon />, path: '/purchases/suppliers', translationKey: 'menu.suppliers' },
{ id: 'pur-orders', label: t('menu.purchaseOrders'), icon: <ListAltIcon />, path: '/purchases/orders', translationKey: 'menu.purchaseOrders' },
],
},
{
id: 'sales',
label: t('menu.sales'),
icon: <SellIcon />,
appCode: 'sales',
translationKey: 'menu.sales',
children: [
{ id: 'sal-orders', label: t('menu.salesOrders'), icon: <ListAltIcon />, path: '/sales/orders', translationKey: 'menu.salesOrders' },
],
},
{
id: 'production',
label: t('menu.production'),
icon: <ProductionIcon />,
appCode: 'production',
translationKey: 'menu.production',
children: [
{ id: 'prod-dashboard', label: t('menu.dashboard'), tabLabel: t('menu.production'), icon: <DashboardIcon />, path: '/production', translationKey: 'menu.production' },
{ id: 'prod-orders', label: t('menu.productionOrders'), icon: <ListAltIcon />, path: '/production/orders', translationKey: 'menu.productionOrders' },
{ id: 'prod-bom', label: t('menu.bom'), icon: <AssignmentIcon />, path: '/production/bom', translationKey: 'menu.bom' },
{ id: 'prod-workcenters', label: t('menu.workCenters'), icon: <BuildIcon />, path: '/production/work-centers', translationKey: 'menu.workCenters' },
{ id: 'prod-cycles', label: t('menu.cycles'), icon: <TimelineIcon />, path: '/production/cycles', translationKey: 'menu.cycles' },
{ id: 'prod-mrp', label: t('menu.mrp'), icon: <ManufacturingIcon />, path: '/production/mrp', translationKey: 'menu.mrp' },
],
},
{
id: 'events',
label: t('menu.events'),
icon: <EventIcon />,
appCode: 'events',
translationKey: 'menu.events',
children: [
{ id: 'ev-list', label: t('menu.events'), icon: <EventIcon />, path: '/events/list', translationKey: 'menu.events' },
{ id: 'ev-calendar', label: t('menu.calendar'), icon: <CalendarIcon />, path: '/events/calendar', translationKey: 'menu.calendar' },
{ id: 'ev-locations', label: t('menu.location'), icon: <PlaceIcon />, path: '/events/locations', translationKey: 'menu.location' },
],
},
{
id: 'hr',
label: t('apps.hr.title'),
icon: <PeopleIcon />,
appCode: 'hr',
translationKey: 'apps.hr.title',
children: [
{ id: 'hr-dipendenti', label: t('apps.hr.dipendenti'), icon: <PeopleIcon />, path: '/hr/dipendenti', translationKey: 'apps.hr.dipendenti' },
{ id: 'hr-contratti', label: t('apps.hr.contratti'), icon: <AssignmentIcon />, path: '/hr/contratti', translationKey: 'apps.hr.contratti' },
{ id: 'hr-assenze', label: t('apps.hr.assenze'), icon: <EventIcon />, path: '/hr/assenze', translationKey: 'apps.hr.assenze' },
{ id: 'hr-pagamenti', label: t('apps.hr.pagamenti'), icon: <AttachMoneyIcon />, path: '/hr/pagamenti', translationKey: 'apps.hr.pagamenti' },
{ id: 'hr-rimborsi', label: t('apps.hr.rimborsi'), icon: <ReceiptIcon />, path: '/hr/rimborsi', translationKey: 'apps.hr.rimborsi' },
],
},
{
id: 'training',
label: t('apps.training.title'),
icon: <SchoolIcon />,
appCode: 'training',
translationKey: 'apps.training.title',
children: [
{ id: 'tr-dashboard', label: t('apps.training.dashboard'), tabLabel: t('apps.training.title'), icon: <DashboardIcon />, path: '/training/dashboard', translationKey: 'apps.training.dashboard' },
{ id: 'tr-registry', label: t('apps.training.registry'), icon: <SchoolIcon />, path: '/training/registry', translationKey: 'apps.training.registry' },
{ id: 'tr-matrix', label: t('apps.training.matrix'), icon: <AssignmentIcon />, path: '/training/matrix', translationKey: 'apps.training.matrix' },
],
},
{
id: 'admin',
label: t('menu.administration'),
icon: <SettingsIcon />,
translationKey: 'menu.administration',
children: [
{ id: 'apps', label: t('menu.apps'), icon: <ModulesIcon />, path: '/apps', translationKey: 'menu.apps' },
{ id: 'autocodes', label: t('menu.autoCodes'), icon: <AutoCodeIcon />, path: '/admin/auto-codes', translationKey: 'menu.autoCodes' },
{ id: 'customfields', label: t('menu.customFields'), icon: <AutoCodeIcon />, path: '/admin/custom-fields', translationKey: 'menu.customFields' },
{ id: 'reports', label: t('menu.reports'), icon: <PrintIcon />, path: '/report-designer', appCode: 'report-designer', translationKey: 'menu.reports' },
{ id: 'email-config', label: t('menu.emailConfig'), icon: <EmailIcon />, path: '/admin/email-config', translationKey: 'menu.emailConfig' },
],
},
];
const activeAppCodes = activeApps.map((m) => m.code);
const renderMenuItem = (item: MenuItem, level: number = 0) => {
// Filter by module
if (item.appCode && !activeAppCodes.includes(item.appCode)) {
return null;
}
const hasChildren = item.children && item.children.length > 0;
const isOpen = openItems[item.id];
const isSelected = item.path ? location.pathname === item.path : false;
return (
<React.Fragment key={item.id}>
<ListItem disablePadding sx={{ display: 'block' }}>
<Tooltip title={isCollapsed ? item.label : ''} placement="right">
<ListItemButton
onClick={() => handleItemClick(item)}
selected={isSelected}
sx={{
minHeight: 48,
justifyContent: isCollapsed ? 'center' : 'initial',
px: 2.5,
pl: isCollapsed ? 2.5 : level * 2 + 2.5,
}}
>
<ListItemIcon
sx={{
minWidth: 0,
mr: isCollapsed ? 0 : 3,
justifyContent: 'center',
color: isSelected ? 'primary.main' : 'inherit',
}}
>
{item.icon}
</ListItemIcon>
{!isCollapsed && (
<>
<ListItemText
primary={item.label}
primaryTypographyProps={{
fontWeight: isSelected ? 'bold' : 'medium',
fontSize: level === 0 ? '0.95rem' : '0.9rem',
}}
/>
{hasChildren ? (isOpen ? <ExpandLess /> : <ExpandMore />) : null}
</>
)}
</ListItemButton>
</Tooltip>
</ListItem>
{!isCollapsed && hasChildren && (
<Collapse in={isOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{item.children?.map((child) => renderMenuItem(child, level + 1))}
</List>
</Collapse>
)}
</React.Fragment>
);
};
return (
<Box sx={{ overflow: 'hidden', display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: isCollapsed ? 'center' : 'space-between', minHeight: 64 }}>
{!isCollapsed && (
<Typography variant="h6" component="div" sx={{ fontWeight: 'bold' }}>
Zentral
</Typography>
)}
{onToggleCollapse && (
<IconButton onClick={onToggleCollapse}>
{isCollapsed ? <ChevronRight /> : <ChevronLeft />}
</IconButton>
)}
</Box>
<Divider />
<List sx={{ overflowY: 'auto', flex: 1 }}>
{menuStructure.map((item) => renderMenuItem(item))}
</List>
</Box>
);
}