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 Box from '@mui/material/Box'; | ||||
|  | ||||
| import Products from './private/products/Products'; | ||||
| 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 UserManagement from './private/users/UserManagement'; | ||||
|  | ||||
| import LoginPage from './private/LoginPage'; | ||||
| import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; | ||||
| @@ -49,11 +46,8 @@ function App() { | ||||
|                   {zone === 'restricted' && <Clients />} | ||||
|  | ||||
|                   {zone === 'public' && currentView === 'Dashboard' && <Dashboard />} | ||||
|                   {zone === 'public' && currentView === 'Products' && <Products />} | ||||
|                   {zone === 'public' && currentView === 'Clients' && <Clients />} | ||||
|                   {zone === 'public' && currentView === 'Providers' && <Providers />} | ||||
|                   {zone === 'public' && currentView === 'Categories' && <Categories />} | ||||
|                   {zone === 'public' && currentView === 'Admin' && <Admin />} | ||||
|  | ||||
|                  {zone === 'public' && currentView === 'UserManagement' && <UserManagement />} | ||||
|                 </PrivateRoute> | ||||
|               } | ||||
|             /> | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| // src/components/MenuDrawerPrivate.jsx | ||||
| import React, { useMemo, useState, useEffect } from 'react'; | ||||
| import { | ||||
|   Drawer, List, ListItemButton, ListItemIcon, ListItemText, | ||||
|   Collapse, IconButton, Tooltip, Box, useMediaQuery, InputBase | ||||
|     Drawer, List, ListItemButton, ListItemIcon, ListItemText, | ||||
|     Collapse, IconButton, Tooltip, Box, useMediaQuery, InputBase | ||||
| } from '@mui/material'; | ||||
| import { useTheme } from '@mui/material/styles'; | ||||
|  | ||||
| @@ -21,279 +21,283 @@ const MINI_WIDTH = 72; | ||||
|  | ||||
| // ---- Hierarchy (from your diagram). Leaves are "CRUD" pages (clickables). ---- | ||||
| const menuData = [ | ||||
|   { | ||||
|     title: 'Business Intelligence', | ||||
|     icon: <InsightsIcon />, | ||||
|     children: [ | ||||
|       { title: 'Sales Report' }, | ||||
|       { title: 'Customer Insights' }, | ||||
|       { title: 'Customer Insights 2' }, | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     title: 'Products Management', | ||||
|     icon: <Inventory2Icon />, | ||||
|     children: [ | ||||
|       { | ||||
|         title: 'Catalog Management', | ||||
|     { | ||||
|         title: 'Business Intelligence', | ||||
|         icon: <InsightsIcon />, | ||||
|         children: [ | ||||
|           { | ||||
|             title: 'Category Dictionary', | ||||
|             children: [ | ||||
|               { title: 'Categories' }, | ||||
|               { title: 'Products' }, | ||||
|               { title: 'All Assets Library' }, | ||||
|               { title: 'Media Management' }, | ||||
|               { title: 'Product Collections' }, | ||||
|             ] | ||||
|           } | ||||
|             { title: 'Sales Report' }, | ||||
|             { title: 'Customer Insights' }, | ||||
|             { title: 'Customer Insights 2' }, | ||||
|         ] | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     title: 'Customers', | ||||
|     icon: <PeopleAltIcon />, | ||||
|     children: [ | ||||
|       { title: 'CRM' }, | ||||
|       { title: 'Customer List' }, | ||||
|       { | ||||
|         title: 'Projects', | ||||
|     }, | ||||
|     { | ||||
|         title: 'Products Management', | ||||
|         icon: <Inventory2Icon />, | ||||
|         children: [ | ||||
|           { title: 'Customer Collections' }, | ||||
|           { title: 'Sales' }, | ||||
|           { title: 'Quotes' }, | ||||
|           { title: 'Orders' }, | ||||
|             { | ||||
|                 title: 'Catalog Management', | ||||
|                 children: [ | ||||
|                     { | ||||
|                         title: 'Category Dictionary', | ||||
|                         children: [ | ||||
|                             { title: 'Categories' }, | ||||
|                             { title: 'Products' }, | ||||
|                             { title: 'All Assets Library' }, | ||||
|                             { title: 'Media Management' }, | ||||
|                             { title: 'Product Collections' }, | ||||
|                         ] | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|         ] | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     title: 'Providers (Brands and Clients)', | ||||
|     icon: <BusinessIcon />, | ||||
|     children: [ | ||||
|       { title: 'Brand Partners' }, | ||||
|       { title: 'Companies' }, | ||||
|       { title: 'Suppliers' }, | ||||
|       { title: 'Materials Providers' }, | ||||
|     ] | ||||
|   }, | ||||
|   { | ||||
|     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' }, | ||||
|     ] | ||||
|   }, | ||||
|     }, | ||||
|     { | ||||
|         title: 'Customers', | ||||
|         icon: <PeopleAltIcon />, | ||||
|         children: [ | ||||
|             { title: 'CRM' }, | ||||
|             { title: 'Customer List' }, | ||||
|             { | ||||
|                 title: 'Projects', | ||||
|                 children: [ | ||||
|                     { title: 'Customer Collections' }, | ||||
|                     { title: 'Sales' }, | ||||
|                     { title: 'Quotes' }, | ||||
|                     { title: 'Orders' }, | ||||
|                 ] | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         title: 'Providers (Brands and Clients)', | ||||
|         icon: <BusinessIcon />, | ||||
|         children: [ | ||||
|             { title: 'Brand Partners' }, | ||||
|             { title: 'Companies' }, | ||||
|             { title: 'Suppliers' }, | ||||
|             { title: 'Materials Providers' }, | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|         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({ | ||||
|   open,                // optional: for mobile temporary drawer | ||||
|   onClose,             // optional: for mobile temporary drawer | ||||
|   onSelect,            // (title) => void | ||||
|   onExpandedChange,    // (boolean) => void   // optional: tell header if expanded/collapsed | ||||
|     open,                // optional: for mobile temporary drawer | ||||
|     onClose,             // optional: for mobile temporary drawer | ||||
|     onSelect,            // (title) => void | ||||
|     onExpandedChange,    // (boolean) => void   // optional: tell header if expanded/collapsed | ||||
| }) { | ||||
|   const theme = useTheme(); | ||||
|   const isMobile = useMediaQuery('(max-width:900px)'); | ||||
|     const theme = useTheme(); | ||||
|     const isMobile = useMediaQuery('(max-width:900px)'); | ||||
|  | ||||
|   const [collapsed, setCollapsed] = useState(false); | ||||
|   // open states per branch key | ||||
|   const [openMap, setOpenMap] = useState({}); | ||||
|     const [collapsed, setCollapsed] = useState(false); | ||||
|     // open states per branch key | ||||
|     const [openMap, setOpenMap] = useState({}); | ||||
|  | ||||
|   // keep collapsed in sync only if "open" prop provided (mobile) | ||||
|   useEffect(() => { | ||||
|     if (!isMobile) return; | ||||
|     if (typeof open === 'boolean') { | ||||
|       // temporary drawer: collapsed UI is not shown on mobile, treat as expanded | ||||
|       setCollapsed(false); | ||||
|     } | ||||
|   }, [open, isMobile]); | ||||
|     // keep collapsed in sync only if "open" prop provided (mobile) | ||||
|     useEffect(() => { | ||||
|         if (!isMobile) return; | ||||
|         if (typeof open === 'boolean') { | ||||
|             // temporary drawer: collapsed UI is not shown on mobile, treat as expanded | ||||
|             setCollapsed(false); | ||||
|         } | ||||
|     }, [open, isMobile]); | ||||
|  | ||||
|   // inform parent of expanded/collapsed (desktop) | ||||
|   useEffect(() => { | ||||
|     onExpandedChange?.(isMobile ? true : !collapsed); | ||||
|   }, [collapsed, isMobile, onExpandedChange]); | ||||
|     // inform parent of expanded/collapsed (desktop) | ||||
|     useEffect(() => { | ||||
|         onExpandedChange?.(isMobile ? true : !collapsed); | ||||
|     }, [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) => { | ||||
|     // if rail collapsed, expand rail first to reveal submenu | ||||
|     if (!isMobile && collapsed) { | ||||
|       setCollapsed(false); | ||||
|       setOpenMap((m) => ({ ...m, [key]: true })); | ||||
|       return; | ||||
|     } | ||||
|     setOpenMap((m) => ({ ...m, [key]: !m[key] })); | ||||
|   }; | ||||
|     const handleToggleNode = (key) => { | ||||
|         // if rail collapsed, expand rail first to reveal submenu | ||||
|         if (!isMobile && collapsed) { | ||||
|             setCollapsed(false); | ||||
|             setOpenMap((m) => ({ ...m, [key]: true })); | ||||
|             return; | ||||
|         } | ||||
|         setOpenMap((m) => ({ ...m, [key]: !m[key] })); | ||||
|     }; | ||||
|  | ||||
|   const renderNode = (node, keyPrefix = '') => { | ||||
|     const key = `${keyPrefix}${node.title}`; | ||||
|     const hasChildren = !!node.children?.length; | ||||
|     const renderNode = (node, keyPrefix = '') => { | ||||
|         const key = `${keyPrefix}${node.title}`; | ||||
|         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 ( | ||||
|       <Box key={key}> | ||||
|         <Tooltip title={collapsed ? node.title : ''} placement="right" disableHoverListener={!collapsed}> | ||||
|           <ListItemButton | ||||
|             onClick={() => { | ||||
|               if (hasChildren) { | ||||
|                 handleToggleNode(key); | ||||
|               } else { | ||||
|                 onSelect?.(node.title); | ||||
|                 if (isMobile) onClose?.(); | ||||
|               } | ||||
|             }} | ||||
|         <Drawer | ||||
|             anchor="left" | ||||
|             variant={isMobile ? 'temporary' : 'permanent'} | ||||
|             open={isMobile ? open : true} | ||||
|             onClose={isMobile ? onClose : undefined} | ||||
|             ModalProps={{ keepMounted: true }} | ||||
|             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 ( | ||||
|     <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', | ||||
|                 width: paperWidth, | ||||
|                 flexShrink: 0, | ||||
|                 '& .MuiDrawer-paper': { | ||||
|                     width: paperWidth, | ||||
|                     boxSizing: 'border-box', | ||||
|                     backgroundColor: '#FFFFFF', | ||||
|                     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> | ||||
|                     transition: theme.transitions.create('width', { | ||||
|                         duration: theme.transitions.duration.standard, | ||||
|                         easing: theme.transitions.easing.sharp, | ||||
|                     }), | ||||
|                     borderRight: '1px solid rgba(0,0,0,0.08)', | ||||
|                 }, | ||||
|             }} | ||||
|         > | ||||
|  | ||||
|       <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> | ||||
|   ); | ||||
|             <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', | ||||
|                                 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