feat: add the new menu for admin and the users page
This commit is contained in:
		
							
								
								
									
										12
									
								
								src/App.jsx
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/App.jsx
									
									
									
									
									
								
							| @@ -4,12 +4,9 @@ import AppHeader from './components/AppHeader'; | |||||||
| import Footer from './components/Footer'; | import Footer from './components/Footer'; | ||||||
| import Box from '@mui/material/Box'; | import Box from '@mui/material/Box'; | ||||||
|  |  | ||||||
| import Products from './private/products/Products'; |  | ||||||
| import Clients from './private/clients/Clients'; | import Clients from './private/clients/Clients'; | ||||||
| import Providers from './private/providers/Providers'; |  | ||||||
| import Categories from './private/categories/Categories'; |  | ||||||
| import Admin from './private/mongo/Admin'; |  | ||||||
| import Dashboard from './private/dashboard/Dashboard'; | import Dashboard from './private/dashboard/Dashboard'; | ||||||
|  | import UserManagement from './private/users/UserManagement'; | ||||||
|  |  | ||||||
| import LoginPage from './private/LoginPage'; | import LoginPage from './private/LoginPage'; | ||||||
| import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; | import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; | ||||||
| @@ -49,11 +46,8 @@ function App() { | |||||||
|                   {zone === 'restricted' && <Clients />} |                   {zone === 'restricted' && <Clients />} | ||||||
|  |  | ||||||
|                   {zone === 'public' && currentView === 'Dashboard' && <Dashboard />} |                   {zone === 'public' && currentView === 'Dashboard' && <Dashboard />} | ||||||
|                   {zone === 'public' && currentView === 'Products' && <Products />} |  | ||||||
|                   {zone === 'public' && currentView === 'Clients' && <Clients />} |                  {zone === 'public' && currentView === 'UserManagement' && <UserManagement />} | ||||||
|                   {zone === 'public' && currentView === 'Providers' && <Providers />} |  | ||||||
|                   {zone === 'public' && currentView === 'Categories' && <Categories />} |  | ||||||
|                   {zone === 'public' && currentView === 'Admin' && <Admin />} |  | ||||||
|                 </PrivateRoute> |                 </PrivateRoute> | ||||||
|               } |               } | ||||||
|             /> |             /> | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| // src/components/MenuDrawerPrivate.jsx | // src/components/MenuDrawerPrivate.jsx | ||||||
| import React, { useMemo, useState, useEffect } from 'react'; | import React, { useMemo, useState, useEffect } from 'react'; | ||||||
| import { | import { | ||||||
|   Drawer, List, ListItemButton, ListItemIcon, ListItemText, |     Drawer, List, ListItemButton, ListItemIcon, ListItemText, | ||||||
|   Collapse, IconButton, Tooltip, Box, useMediaQuery, InputBase |     Collapse, IconButton, Tooltip, Box, useMediaQuery, InputBase | ||||||
| } from '@mui/material'; | } from '@mui/material'; | ||||||
| import { useTheme } from '@mui/material/styles'; | import { useTheme } from '@mui/material/styles'; | ||||||
|  |  | ||||||
| @@ -21,279 +21,283 @@ const MINI_WIDTH = 72; | |||||||
|  |  | ||||||
| // ---- Hierarchy (from your diagram). Leaves are "CRUD" pages (clickables). ---- | // ---- Hierarchy (from your diagram). Leaves are "CRUD" pages (clickables). ---- | ||||||
| const menuData = [ | const menuData = [ | ||||||
|   { |     { | ||||||
|     title: 'Business Intelligence', |         title: 'Business Intelligence', | ||||||
|     icon: <InsightsIcon />, |         icon: <InsightsIcon />, | ||||||
|     children: [ |  | ||||||
|       { title: 'Sales Report' }, |  | ||||||
|       { title: 'Customer Insights' }, |  | ||||||
|       { title: 'Customer Insights 2' }, |  | ||||||
|     ] |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     title: 'Products Management', |  | ||||||
|     icon: <Inventory2Icon />, |  | ||||||
|     children: [ |  | ||||||
|       { |  | ||||||
|         title: 'Catalog Management', |  | ||||||
|         children: [ |         children: [ | ||||||
|           { |             { title: 'Sales Report' }, | ||||||
|             title: 'Category Dictionary', |             { title: 'Customer Insights' }, | ||||||
|             children: [ |             { title: 'Customer Insights 2' }, | ||||||
|               { title: 'Categories' }, |  | ||||||
|               { title: 'Products' }, |  | ||||||
|               { title: 'All Assets Library' }, |  | ||||||
|               { title: 'Media Management' }, |  | ||||||
|               { title: 'Product Collections' }, |  | ||||||
|             ] |  | ||||||
|           } |  | ||||||
|         ] |         ] | ||||||
|       } |     }, | ||||||
|     ] |     { | ||||||
|   }, |         title: 'Products Management', | ||||||
|   { |         icon: <Inventory2Icon />, | ||||||
|     title: 'Customers', |  | ||||||
|     icon: <PeopleAltIcon />, |  | ||||||
|     children: [ |  | ||||||
|       { title: 'CRM' }, |  | ||||||
|       { title: 'Customer List' }, |  | ||||||
|       { |  | ||||||
|         title: 'Projects', |  | ||||||
|         children: [ |         children: [ | ||||||
|           { title: 'Customer Collections' }, |             { | ||||||
|           { title: 'Sales' }, |                 title: 'Catalog Management', | ||||||
|           { title: 'Quotes' }, |                 children: [ | ||||||
|           { title: 'Orders' }, |                     { | ||||||
|  |                         title: 'Category Dictionary', | ||||||
|  |                         children: [ | ||||||
|  |                             { title: 'Categories' }, | ||||||
|  |                             { title: 'Products' }, | ||||||
|  |                             { title: 'All Assets Library' }, | ||||||
|  |                             { title: 'Media Management' }, | ||||||
|  |                             { title: 'Product Collections' }, | ||||||
|  |                         ] | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             } | ||||||
|         ] |         ] | ||||||
|       } |     }, | ||||||
|     ] |     { | ||||||
|   }, |         title: 'Customers', | ||||||
|   { |         icon: <PeopleAltIcon />, | ||||||
|     title: 'Providers (Brands and Clients)', |         children: [ | ||||||
|     icon: <BusinessIcon />, |             { title: 'CRM' }, | ||||||
|     children: [ |             { title: 'Customer List' }, | ||||||
|       { title: 'Brand Partners' }, |             { | ||||||
|       { title: 'Companies' }, |                 title: 'Projects', | ||||||
|       { title: 'Suppliers' }, |                 children: [ | ||||||
|       { title: 'Materials Providers' }, |                     { title: 'Customer Collections' }, | ||||||
|     ] |                     { title: 'Sales' }, | ||||||
|   }, |                     { title: 'Quotes' }, | ||||||
|   { |                     { title: 'Orders' }, | ||||||
|     title: 'Users', |                 ] | ||||||
|     icon: <AdminPanelSettingsIcon />, |             } | ||||||
|     children: [ |         ] | ||||||
|       { title: 'Users Management' }, |     }, | ||||||
|       { title: 'Access Control' }, |     { | ||||||
|       { title: 'Roles' }, |         title: 'Providers (Brands and Clients)', | ||||||
|       { title: 'Permissions' }, |         icon: <BusinessIcon />, | ||||||
|     ] |         children: [ | ||||||
|   }, |             { title: 'Brand Partners' }, | ||||||
|   { |             { title: 'Companies' }, | ||||||
|     title: 'Settings', |             { title: 'Suppliers' }, | ||||||
|     icon: <SettingsIcon />, |             { title: 'Materials Providers' }, | ||||||
|     children: [ |         ] | ||||||
|       { title: 'General Settings' }, |     }, | ||||||
|       { title: 'WebApp Configuration' }, |     { | ||||||
|       { title: 'Mobile App Configuration' }, |         title: 'Users', | ||||||
|     ] |         icon: <AdminPanelSettingsIcon />, | ||||||
|   }, |         children: [ | ||||||
|  |             { title: 'Users Management' }, | ||||||
|  |             { title: 'Access Control' }, | ||||||
|  |             { title: 'Roles' }, | ||||||
|  |             { title: 'Permissions' }, | ||||||
|  |         ] | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         title: 'Settings', | ||||||
|  |         icon: <SettingsIcon />, | ||||||
|  |         children: [ | ||||||
|  |             { title: 'General Settings' }, | ||||||
|  |             { title: 'WebApp Configuration' }, | ||||||
|  |             { title: 'Mobile App Configuration' }, | ||||||
|  |         ] | ||||||
|  |     }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| export default function MenuDrawerPrivate({ | export default function MenuDrawerPrivate({ | ||||||
|   open,                // optional: for mobile temporary drawer |     open,                // optional: for mobile temporary drawer | ||||||
|   onClose,             // optional: for mobile temporary drawer |     onClose,             // optional: for mobile temporary drawer | ||||||
|   onSelect,            // (title) => void |     onSelect,            // (title) => void | ||||||
|   onExpandedChange,    // (boolean) => void   // optional: tell header if expanded/collapsed |     onExpandedChange,    // (boolean) => void   // optional: tell header if expanded/collapsed | ||||||
| }) { | }) { | ||||||
|   const theme = useTheme(); |     const theme = useTheme(); | ||||||
|   const isMobile = useMediaQuery('(max-width:900px)'); |     const isMobile = useMediaQuery('(max-width:900px)'); | ||||||
|  |  | ||||||
|   const [collapsed, setCollapsed] = useState(false); |     const [collapsed, setCollapsed] = useState(false); | ||||||
|   // open states per branch key |     // open states per branch key | ||||||
|   const [openMap, setOpenMap] = useState({}); |     const [openMap, setOpenMap] = useState({}); | ||||||
|  |  | ||||||
|   // keep collapsed in sync only if "open" prop provided (mobile) |     // keep collapsed in sync only if "open" prop provided (mobile) | ||||||
|   useEffect(() => { |     useEffect(() => { | ||||||
|     if (!isMobile) return; |         if (!isMobile) return; | ||||||
|     if (typeof open === 'boolean') { |         if (typeof open === 'boolean') { | ||||||
|       // temporary drawer: collapsed UI is not shown on mobile, treat as expanded |             // temporary drawer: collapsed UI is not shown on mobile, treat as expanded | ||||||
|       setCollapsed(false); |             setCollapsed(false); | ||||||
|     } |         } | ||||||
|   }, [open, isMobile]); |     }, [open, isMobile]); | ||||||
|  |  | ||||||
|   // inform parent of expanded/collapsed (desktop) |     // inform parent of expanded/collapsed (desktop) | ||||||
|   useEffect(() => { |     useEffect(() => { | ||||||
|     onExpandedChange?.(isMobile ? true : !collapsed); |         onExpandedChange?.(isMobile ? true : !collapsed); | ||||||
|   }, [collapsed, isMobile, onExpandedChange]); |     }, [collapsed, isMobile, onExpandedChange]); | ||||||
|  |  | ||||||
|   const paperWidth = isMobile ? OPEN_WIDTH : (collapsed ? MINI_WIDTH : OPEN_WIDTH); |     const paperWidth = isMobile ? OPEN_WIDTH : (collapsed ? MINI_WIDTH : OPEN_WIDTH); | ||||||
|  |  | ||||||
|   const toggleCollapse = () => setCollapsed(c => !c); |     const toggleCollapse = () => setCollapsed(c => !c); | ||||||
|  |  | ||||||
|   const handleToggleNode = (key) => { |     const handleToggleNode = (key) => { | ||||||
|     // if rail collapsed, expand rail first to reveal submenu |         // if rail collapsed, expand rail first to reveal submenu | ||||||
|     if (!isMobile && collapsed) { |         if (!isMobile && collapsed) { | ||||||
|       setCollapsed(false); |             setCollapsed(false); | ||||||
|       setOpenMap((m) => ({ ...m, [key]: true })); |             setOpenMap((m) => ({ ...m, [key]: true })); | ||||||
|       return; |             return; | ||||||
|     } |         } | ||||||
|     setOpenMap((m) => ({ ...m, [key]: !m[key] })); |         setOpenMap((m) => ({ ...m, [key]: !m[key] })); | ||||||
|   }; |     }; | ||||||
|  |  | ||||||
|   const renderNode = (node, keyPrefix = '') => { |     const renderNode = (node, keyPrefix = '') => { | ||||||
|     const key = `${keyPrefix}${node.title}`; |         const key = `${keyPrefix}${node.title}`; | ||||||
|     const hasChildren = !!node.children?.length; |         const hasChildren = !!node.children?.length; | ||||||
|  |  | ||||||
|  |         return ( | ||||||
|  |             <Box key={key}> | ||||||
|  |                 <Tooltip title={collapsed ? node.title : ''} placement="right" disableHoverListener={!collapsed}> | ||||||
|  |                     <ListItemButton | ||||||
|  |                         onClick={() => { | ||||||
|  |                             if (hasChildren) { | ||||||
|  |                                 handleToggleNode(key); | ||||||
|  |                             } else { | ||||||
|  |                                 if (node.title === 'Users Management') { | ||||||
|  |                                     onSelect?.('UserManagement'); | ||||||
|  |                                 } else { | ||||||
|  |                                     onSelect?.(node.title); | ||||||
|  |                                 } | ||||||
|  |                                 if (isMobile) onClose?.(); | ||||||
|  |                             } | ||||||
|  |                         }} | ||||||
|  |                         sx={{ | ||||||
|  |                             px: collapsed ? 0 : 2, | ||||||
|  |                             minHeight: 48, | ||||||
|  |                             justifyContent: collapsed ? 'center' : 'flex-start', | ||||||
|  |                         }} | ||||||
|  |                     > | ||||||
|  |                         {node.icon && ( | ||||||
|  |                             <ListItemIcon | ||||||
|  |                                 sx={{ | ||||||
|  |                                     color: '#40120EFF', | ||||||
|  |                                     minWidth: collapsed ? 'auto' : 40, | ||||||
|  |                                     mr: collapsed ? 0 : 1.5, | ||||||
|  |                                     justifyContent: 'center', | ||||||
|  |                                 }} | ||||||
|  |                             > | ||||||
|  |                                 {node.icon} | ||||||
|  |                             </ListItemIcon> | ||||||
|  |                         )} | ||||||
|  |  | ||||||
|  |                         {!collapsed && ( | ||||||
|  |                             <> | ||||||
|  |                                 <ListItemText | ||||||
|  |                                     primary={node.title} | ||||||
|  |                                     slotProps={{ | ||||||
|  |                                         primary: { sx: { color: '#40120EFF', fontWeight: hasChildren ? 600 : 400, whiteSpace: 'nowrap' } } | ||||||
|  |                                     }} | ||||||
|  |                                 /> | ||||||
|  |                                 {hasChildren ? (openMap[key] ? <ExpandLess /> : <ExpandMore />) : null} | ||||||
|  |                             </> | ||||||
|  |                         )} | ||||||
|  |                     </ListItemButton> | ||||||
|  |                 </Tooltip> | ||||||
|  |  | ||||||
|  |                 {hasChildren && !collapsed && ( | ||||||
|  |                     <Collapse in={!!openMap[key]} timeout="auto" unmountOnExit> | ||||||
|  |                         <List component="div" disablePadding sx={{ pl: 7 }}> | ||||||
|  |                             {node.children.map((child, idx) => renderNode(child, `${key}-`))} | ||||||
|  |                         </List> | ||||||
|  |                     </Collapse> | ||||||
|  |                 )} | ||||||
|  |             </Box> | ||||||
|  |         ); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|       <Box key={key}> |         <Drawer | ||||||
|         <Tooltip title={collapsed ? node.title : ''} placement="right" disableHoverListener={!collapsed}> |             anchor="left" | ||||||
|           <ListItemButton |             variant={isMobile ? 'temporary' : 'permanent'} | ||||||
|             onClick={() => { |             open={isMobile ? open : true} | ||||||
|               if (hasChildren) { |             onClose={isMobile ? onClose : undefined} | ||||||
|                 handleToggleNode(key); |             ModalProps={{ keepMounted: true }} | ||||||
|               } else { |  | ||||||
|                 onSelect?.(node.title); |  | ||||||
|                 if (isMobile) onClose?.(); |  | ||||||
|               } |  | ||||||
|             }} |  | ||||||
|             sx={{ |             sx={{ | ||||||
|               px: collapsed ? 0 : 2, |                 width: paperWidth, | ||||||
|               minHeight: 48, |                 flexShrink: 0, | ||||||
|               justifyContent: collapsed ? 'center' : 'flex-start', |                 '& .MuiDrawer-paper': { | ||||||
|             }} |                     width: paperWidth, | ||||||
|           > |                     boxSizing: 'border-box', | ||||||
|             {node.icon && ( |                     backgroundColor: '#FFFFFF', | ||||||
|               <ListItemIcon |  | ||||||
|                 sx={{ |  | ||||||
|                   color: '#40120EFF', |  | ||||||
|                   minWidth: collapsed ? 'auto' : 40, |  | ||||||
|                   mr: collapsed ? 0 : 1.5, |  | ||||||
|                   justifyContent: 'center', |  | ||||||
|                 }} |  | ||||||
|               > |  | ||||||
|                 {node.icon} |  | ||||||
|               </ListItemIcon> |  | ||||||
|             )} |  | ||||||
|  |  | ||||||
|             {!collapsed && ( |  | ||||||
|               <> |  | ||||||
|                 <ListItemText |  | ||||||
|                   primary={node.title} |  | ||||||
|                   slotProps={{ |  | ||||||
|                     primary: { sx: { color: '#40120EFF', fontWeight: hasChildren ? 600 : 400, whiteSpace: 'nowrap' } } |  | ||||||
|                   }} |  | ||||||
|                 /> |  | ||||||
|                 {hasChildren ? (openMap[key] ? <ExpandLess /> : <ExpandMore />) : null} |  | ||||||
|               </> |  | ||||||
|             )} |  | ||||||
|           </ListItemButton> |  | ||||||
|         </Tooltip> |  | ||||||
|  |  | ||||||
|         {hasChildren && !collapsed && ( |  | ||||||
|           <Collapse in={!!openMap[key]} timeout="auto" unmountOnExit> |  | ||||||
|             <List component="div" disablePadding sx={{ pl: 7 }}> |  | ||||||
|               {node.children.map((child, idx) => renderNode(child, `${key}-`))} |  | ||||||
|             </List> |  | ||||||
|           </Collapse> |  | ||||||
|         )} |  | ||||||
|       </Box> |  | ||||||
|     ); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |  | ||||||
|     <Drawer |  | ||||||
|       anchor="left" |  | ||||||
|       variant={isMobile ? 'temporary' : 'permanent'} |  | ||||||
|       open={isMobile ? open : true} |  | ||||||
|       onClose={isMobile ? onClose : undefined} |  | ||||||
|       ModalProps={{ keepMounted: true }} |  | ||||||
|       sx={{ |  | ||||||
|         width: paperWidth, |  | ||||||
|         flexShrink: 0, |  | ||||||
|         '& .MuiDrawer-paper': { |  | ||||||
|           width: paperWidth, |  | ||||||
|           boxSizing: 'border-box', |  | ||||||
|           backgroundColor: '#FFFFFF', |  | ||||||
|           color: '#40120EFF', |  | ||||||
|           transition: theme.transitions.create('width', { |  | ||||||
|             duration: theme.transitions.duration.standard, |  | ||||||
|             easing: theme.transitions.easing.sharp, |  | ||||||
|           }), |  | ||||||
|           borderRight: '1px solid rgba(0,0,0,0.08)', |  | ||||||
|         }, |  | ||||||
|       }} |  | ||||||
|     > |  | ||||||
|      |  | ||||||
|       <Box |  | ||||||
|             sx={{ |  | ||||||
|               display: 'flex', |  | ||||||
|               alignItems: 'center', |  | ||||||
|               gap: 1, |  | ||||||
|               px: collapsed ? 1 : 2, |  | ||||||
|               py: 1.5, |  | ||||||
|               justifyContent: collapsed ? 'center' : 'space-between', |  | ||||||
|             }} |  | ||||||
|           > |  | ||||||
|             {!collapsed && ( |  | ||||||
|               <Box textAlign="center" p={3} alignItems="center" minHeight={72}> |  | ||||||
|                 <img |  | ||||||
|                   src="Logo.png" |  | ||||||
|                   alt="Dream Views" |  | ||||||
|                 /> |  | ||||||
|      |  | ||||||
|                 <InputBase |  | ||||||
|                   placeholder="Filter options..." |  | ||||||
|                   sx={{ |  | ||||||
|                     pl: 1.5, |  | ||||||
|                     pr: 1.5, |  | ||||||
|                     py: 0.75, |  | ||||||
|                     borderRadius: 2, |  | ||||||
|                     border: '1px solid #40120EFF', |  | ||||||
|                     color: '#40120EFF', |                     color: '#40120EFF', | ||||||
|                     width: '100%', |                     transition: theme.transitions.create('width', { | ||||||
|                   }} |                         duration: theme.transitions.duration.standard, | ||||||
|                 /> |                         easing: theme.transitions.easing.sharp, | ||||||
|               </Box> |                     }), | ||||||
|             )} |                     borderRight: '1px solid rgba(0,0,0,0.08)', | ||||||
|           </Box> |                 }, | ||||||
|      |             }} | ||||||
|           {collapsed && ( |         > | ||||||
|             <Box textAlign="center" p={3} minHeight={112} justifyContent="center" display="flex" |  | ||||||
|               alignItems="start"> |  | ||||||
|               <img |  | ||||||
|                 style={{ marginTop: 5 }} |  | ||||||
|                 src="MiniLogo.png" |  | ||||||
|                 alt="Dream Views" |  | ||||||
|               /> |  | ||||||
|             </Box> |  | ||||||
|           )} |  | ||||||
|      |  | ||||||
|       {/* Tree */} |  | ||||||
|       <List sx={{ width: '100%', py: 0 }}> |  | ||||||
|         {menuData.map((node) => renderNode(node))} |  | ||||||
|       </List> |  | ||||||
|  |  | ||||||
|       <Tooltip title={collapsed ? 'Expand' : 'Collapse'} placement="right"> |             <Box | ||||||
|         <IconButton onClick={() => setCollapsed((c) => !c)} sx={{ |                 sx={{ | ||||||
|           backgroundColor: 'transparent', |                     display: 'flex', | ||||||
|           color: 'transparent', |                     alignItems: 'center', | ||||||
|           '&:hover': { |                     gap: 1, | ||||||
|             backgroundColor: '#fff4ec', |                     px: collapsed ? 1 : 2, | ||||||
|             borderColor: 'transparent' |                     py: 1.5, | ||||||
|           }, |                     justifyContent: collapsed ? 'center' : 'space-between', | ||||||
|           borderRadius: 0, |                 }} | ||||||
|           marginLeft: 2, |             > | ||||||
|           width: 40, |                 {!collapsed && ( | ||||||
|           height: 40, |                     <Box textAlign="center" p={3} alignItems="center" minHeight={72}> | ||||||
|         }}> |                         <img | ||||||
|           <img |                             src="Logo.png" | ||||||
|             src={collapsed ? '/Expand.png' : '/Contract.png'} |                             alt="Dream Views" | ||||||
|             alt={collapsed ? 'Expand' : 'Contract'} |                         /> | ||||||
|             width={24} |  | ||||||
|             height={24} |                         <InputBase | ||||||
|           /> |                             placeholder="Filter options..." | ||||||
|         </IconButton> |                             sx={{ | ||||||
|       </Tooltip> |                                 pl: 1.5, | ||||||
|     </Drawer> |                                 pr: 1.5, | ||||||
|   ); |                                 py: 0.75, | ||||||
|  |                                 borderRadius: 2, | ||||||
|  |                                 border: '1px solid #40120EFF', | ||||||
|  |                                 color: '#40120EFF', | ||||||
|  |                                 width: '100%', | ||||||
|  |                             }} | ||||||
|  |                         /> | ||||||
|  |                     </Box> | ||||||
|  |                 )} | ||||||
|  |             </Box> | ||||||
|  |  | ||||||
|  |             {collapsed && ( | ||||||
|  |                 <Box textAlign="center" p={3} minHeight={112} justifyContent="center" display="flex" | ||||||
|  |                     alignItems="start"> | ||||||
|  |                     <img | ||||||
|  |                         style={{ marginTop: 5 }} | ||||||
|  |                         src="MiniLogo.png" | ||||||
|  |                         alt="Dream Views" | ||||||
|  |                     /> | ||||||
|  |                 </Box> | ||||||
|  |             )} | ||||||
|  |  | ||||||
|  |             {/* Tree */} | ||||||
|  |             <List sx={{ width: '100%', py: 0 }}> | ||||||
|  |                 {menuData.map((node) => renderNode(node))} | ||||||
|  |             </List> | ||||||
|  |  | ||||||
|  |             <Tooltip title={collapsed ? 'Expand' : 'Collapse'} placement="right"> | ||||||
|  |                 <IconButton onClick={() => setCollapsed((c) => !c)} sx={{ | ||||||
|  |                     backgroundColor: 'transparent', | ||||||
|  |                     color: 'transparent', | ||||||
|  |                     '&:hover': { | ||||||
|  |                         backgroundColor: '#fff4ec', | ||||||
|  |                         borderColor: 'transparent' | ||||||
|  |                     }, | ||||||
|  |                     borderRadius: 0, | ||||||
|  |                     marginLeft: 2, | ||||||
|  |                     width: 40, | ||||||
|  |                     height: 40, | ||||||
|  |                 }}> | ||||||
|  |                     <img | ||||||
|  |                         src={collapsed ? '/Expand.png' : '/Contract.png'} | ||||||
|  |                         alt={collapsed ? 'Expand' : 'Contract'} | ||||||
|  |                         width={24} | ||||||
|  |                         height={24} | ||||||
|  |                     /> | ||||||
|  |                 </IconButton> | ||||||
|  |             </Tooltip> | ||||||
|  |         </Drawer> | ||||||
|  |     ); | ||||||
| } | } | ||||||
							
								
								
									
										103
									
								
								src/private/users/AddOrEditUserForm.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/private/users/AddOrEditUserForm.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | import { useState, useEffect } from 'react'; | ||||||
|  | import { Box, Button, TextField, MenuItem } from '@mui/material'; | ||||||
|  | import { createExternalData, updateExternalData } from '../../api/mongo/actions'; | ||||||
|  |  | ||||||
|  | export default function AddOrEditUserForm({ onAdd, initialData, onCancel }) { | ||||||
|  |     const [formData, setFormData] = useState({ | ||||||
|  |         username: '', | ||||||
|  |         fullName: '', | ||||||
|  |         email: '', | ||||||
|  |         role: 'User', | ||||||
|  |         status: 'Active' | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     useEffect(() => { | ||||||
|  |         if (initialData) { | ||||||
|  |             setFormData({ ...initialData }); | ||||||
|  |         } else { | ||||||
|  |             setFormData({ | ||||||
|  |                 username: '', | ||||||
|  |                 fullName: '', | ||||||
|  |                 email: '', | ||||||
|  |                 role: 'User', | ||||||
|  |                 status: 'Active' | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     }, [initialData]); | ||||||
|  |  | ||||||
|  |     const handleChange = (e) => { | ||||||
|  |         const { name, value } = e.target; | ||||||
|  |         setFormData(prev => ({ ...prev, [name]: value })); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleSubmit = async () => { | ||||||
|  |         try { | ||||||
|  |             if (initialData) { | ||||||
|  |                 await updateExternalData(formData); | ||||||
|  |             } else { | ||||||
|  |                 await createExternalData(formData); | ||||||
|  |             } | ||||||
|  |             if (onAdd) onAdd(); | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error('Error submitting form:', error); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <Box sx={{ py: 2 }}> | ||||||
|  |             <TextField | ||||||
|  |                 fullWidth | ||||||
|  |                 label="Username" | ||||||
|  |                 name="username" | ||||||
|  |                 value={formData.username} | ||||||
|  |                 onChange={handleChange} | ||||||
|  |                 margin="normal" | ||||||
|  |             /> | ||||||
|  |             <TextField | ||||||
|  |                 fullWidth | ||||||
|  |                 label="Full Name" | ||||||
|  |                 name="fullName" | ||||||
|  |                 value={formData.fullName} | ||||||
|  |                 onChange={handleChange} | ||||||
|  |                 margin="normal" | ||||||
|  |             /> | ||||||
|  |             <TextField | ||||||
|  |                 fullWidth | ||||||
|  |                 label="Email" | ||||||
|  |                 name="email" | ||||||
|  |                 value={formData.email} | ||||||
|  |                 onChange={handleChange} | ||||||
|  |                 margin="normal" | ||||||
|  |             /> | ||||||
|  |             <TextField | ||||||
|  |                 fullWidth | ||||||
|  |                 select | ||||||
|  |                 label="Role" | ||||||
|  |                 name="role" | ||||||
|  |                 value={formData.role} | ||||||
|  |                 onChange={handleChange} | ||||||
|  |                 margin="normal" | ||||||
|  |             > | ||||||
|  |                 <MenuItem value="Admin">Admin</MenuItem> | ||||||
|  |                 <MenuItem value="User">User</MenuItem> | ||||||
|  |                 <MenuItem value="Manager">Manager</MenuItem> | ||||||
|  |             </TextField> | ||||||
|  |             <TextField | ||||||
|  |                 fullWidth | ||||||
|  |                 select | ||||||
|  |                 label="Status" | ||||||
|  |                 name="status" | ||||||
|  |                 value={formData.status} | ||||||
|  |                 onChange={handleChange} | ||||||
|  |                 margin="normal" | ||||||
|  |             > | ||||||
|  |                 <MenuItem value="Active">Active</MenuItem> | ||||||
|  |                 <MenuItem value="Inactive">Inactive</MenuItem> | ||||||
|  |             </TextField> | ||||||
|  |             <Box display="flex" justifyContent="flex-end" mt={3} gap={1}> | ||||||
|  |                 <Button onClick={onCancel} className="button-transparent">Cancel</Button> | ||||||
|  |                 <Button variant="contained" onClick={handleSubmit} className="button-gold">Save</Button> | ||||||
|  |             </Box> | ||||||
|  |         </Box> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										181
									
								
								src/private/users/UserManagement.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								src/private/users/UserManagement.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | |||||||
|  | import SectionContainer from '../../components/SectionContainer'; | ||||||
|  | import { useEffect, useState, useRef } from 'react'; | ||||||
|  | import { DataGrid } from '@mui/x-data-grid'; | ||||||
|  | import { Typography, Button, Dialog, DialogTitle, DialogContent, IconButton, Box } from '@mui/material'; | ||||||
|  | import EditRoundedIcon from '@mui/icons-material/EditRounded'; | ||||||
|  | import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded'; | ||||||
|  | import AddOrEditUserForm from './AddOrEditUserForm'; | ||||||
|  | import { getExternalData, deleteExternalData } from '../../api/mongo/actions'; | ||||||
|  | import useApiToast from '../../hooks/useApiToast'; | ||||||
|  |  | ||||||
|  | const columnsBase = [ | ||||||
|  |     { field: 'username', headerName: 'Username', flex: 1 }, | ||||||
|  |     { field: 'fullName', headerName: 'Full Name', flex: 2 }, | ||||||
|  |     { field: 'email', headerName: 'Email', flex: 2 }, | ||||||
|  |     { field: 'role', headerName: 'Role', flex: 1 }, | ||||||
|  |     { field: 'status', headerName: 'Status', width: 120 }, | ||||||
|  |     { | ||||||
|  |         field: 'createdAt', | ||||||
|  |         headerName: 'Created At', | ||||||
|  |         width: 160, | ||||||
|  |         valueFormatter: (params) => { | ||||||
|  |             const date = params?.value; | ||||||
|  |             return date ? new Date(date).toLocaleString() : '—'; | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     { field: 'createdBy', headerName: 'Created By', flex: 1 }, | ||||||
|  |     { | ||||||
|  |         field: 'updatedAt', | ||||||
|  |         headerName: 'Updated At', | ||||||
|  |         width: 160, | ||||||
|  |         valueFormatter: (params) => { | ||||||
|  |             const date = params?.value; | ||||||
|  |             return date ? new Date(date).toLocaleString() : '—'; | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     { field: 'updatedBy', headerName: 'Updated By', flex: 1 }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export default function UserManagement() { | ||||||
|  |     const [rows, setRows] = useState([]); | ||||||
|  |     const [open, setOpen] = useState(false); | ||||||
|  |     const [editingData, setEditingData] = useState(null); | ||||||
|  |     const [confirmOpen, setConfirmOpen] = useState(false); | ||||||
|  |     const [rowToDelete, setRowToDelete] = useState(null); | ||||||
|  |     const { handleError } = useApiToast(); | ||||||
|  |  | ||||||
|  |     const hasLoaded = useRef(false); | ||||||
|  |  | ||||||
|  |     useEffect(() => { | ||||||
|  |         if (!hasLoaded.current) { | ||||||
|  |             loadData(); | ||||||
|  |             hasLoaded.current = true; | ||||||
|  |         } | ||||||
|  |     }, []); | ||||||
|  |  | ||||||
|  |     const handleEditClick = (params) => { | ||||||
|  |         setEditingData(params.row); | ||||||
|  |         setOpen(true); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleDeleteClick = (row) => { | ||||||
|  |         setRowToDelete(row); | ||||||
|  |         setConfirmOpen(true); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const handleConfirmDelete = async () => { | ||||||
|  |         try { | ||||||
|  |             await deleteExternalData(rowToDelete._Id); | ||||||
|  |             await loadData(); | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error('Delete failed:', error); | ||||||
|  |         } finally { | ||||||
|  |             setConfirmOpen(false); | ||||||
|  |             setRowToDelete(null); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const loadData = async () => { | ||||||
|  |         try { | ||||||
|  |             const data = await getExternalData(); | ||||||
|  |             const safeData = Array.isArray(data) ? data : []; | ||||||
|  |             setRows(safeData); | ||||||
|  |         } catch (error) { | ||||||
|  |             console.error('Error loading data:', error); | ||||||
|  |             handleError(error, 'Failed to load data'); | ||||||
|  |             setRows([]); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const columns = [ | ||||||
|  |         ...columnsBase, | ||||||
|  |         { | ||||||
|  |             field: 'actions', | ||||||
|  |             headerName: '', | ||||||
|  |             width: 130, | ||||||
|  |             renderCell: (params) => ( | ||||||
|  |                 <Box display="flex" alignItems="center" justifyContent="flex-end" height="100%" gap={2}> | ||||||
|  |                     <IconButton | ||||||
|  |                         size="small" | ||||||
|  |                         sx={{ | ||||||
|  |                             backgroundColor: '#DFCCBC', | ||||||
|  |                             color: '#26201A', | ||||||
|  |                             '&:hover': { backgroundColor: '#C2B2A4' }, | ||||||
|  |                             borderRadius: 2, | ||||||
|  |                             p: 1, | ||||||
|  |                         }} | ||||||
|  |                         onClick={() => handleEditClick(params)} | ||||||
|  |                     > | ||||||
|  |                         <EditRoundedIcon fontSize="small" /> | ||||||
|  |                     </IconButton> | ||||||
|  |                     <IconButton | ||||||
|  |                         size="small" | ||||||
|  |                         sx={{ | ||||||
|  |                             backgroundColor: '#FBE9E7', | ||||||
|  |                             color: '#C62828', | ||||||
|  |                             '&:hover': { backgroundColor: '#EF9A9A' }, | ||||||
|  |                             borderRadius: 2, | ||||||
|  |                             p: 1, | ||||||
|  |                         }} | ||||||
|  |                         onClick={() => handleDeleteClick(params.row)} | ||||||
|  |                     > | ||||||
|  |                         <DeleteRoundedIcon fontSize="small" /> | ||||||
|  |                     </IconButton> | ||||||
|  |                 </Box> | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     ]; | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <SectionContainer sx={{ width: '100%' }}> | ||||||
|  |             <Typography variant="h4" gutterBottom color='#26201AFF'>User Management</Typography> | ||||||
|  |  | ||||||
|  |             <Dialog open={open} onClose={() => { setOpen(false); setEditingData(null); }} maxWidth="md" fullWidth> | ||||||
|  |                 <DialogTitle>{editingData ? 'Edit User' : 'Add User'}</DialogTitle> | ||||||
|  |                 <DialogContent> | ||||||
|  |                     <AddOrEditUserForm | ||||||
|  |                         onAdd={async () => { | ||||||
|  |                             await loadData(); | ||||||
|  |                             setOpen(false); | ||||||
|  |                             setEditingData(null); | ||||||
|  |                         }} | ||||||
|  |                         initialData={editingData} | ||||||
|  |                         onCancel={() => { | ||||||
|  |                             setOpen(false); | ||||||
|  |                             setEditingData(null); | ||||||
|  |                         }} | ||||||
|  |                     /> | ||||||
|  |                 </DialogContent> | ||||||
|  |             </Dialog> | ||||||
|  |  | ||||||
|  |             <Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)}> | ||||||
|  |                 <DialogTitle>Confirm Delete</DialogTitle> | ||||||
|  |                 <DialogContent> | ||||||
|  |                     <Typography> | ||||||
|  |                         Are you sure you want to delete <strong>{rowToDelete?.username}</strong>? | ||||||
|  |                     </Typography> | ||||||
|  |                     <Box mt={2} display="flex" justifyContent="flex-end" gap={1}> | ||||||
|  |                         <Button onClick={() => setConfirmOpen(false)} className='button-transparent'>Cancel</Button> | ||||||
|  |                         <Button variant="contained" onClick={handleConfirmDelete} className="button-gold">Delete</Button> | ||||||
|  |                     </Box> | ||||||
|  |                 </DialogContent> | ||||||
|  |             </Dialog> | ||||||
|  |  | ||||||
|  |             <Box mt={2}> | ||||||
|  |                 <DataGrid | ||||||
|  |                     rows={rows} | ||||||
|  |                     columns={columns} | ||||||
|  |                     pageSize={5} | ||||||
|  |                     rowsPerPageOptions={[5]} | ||||||
|  |                     getRowSpacing={() => ({ top: 4, bottom: 4 })} | ||||||
|  |                 /> | ||||||
|  |  | ||||||
|  |                 <Box display="flex" justifyContent="flex-end" mt={2}> | ||||||
|  |                     <Button variant="contained" onClick={() => setOpen(true)} className="button-gold"> | ||||||
|  |                         Add User | ||||||
|  |                     </Button> | ||||||
|  |                 </Box> | ||||||
|  |             </Box> | ||||||
|  |         </SectionContainer> | ||||||
|  |     ); | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Rodolfo Ruiz
					Rodolfo Ruiz