From d8c890313fe4d3c3db9e6320dcd5b4a347f8c0bc Mon Sep 17 00:00:00 2001 From: Rodolfo Ruiz Date: Thu, 28 Aug 2025 21:45:07 -0600 Subject: [PATCH] feat: add the new menu for admin and the users page --- src/App.jsx | 12 +- src/components/MenuDrawerPrivate.jsx | 518 ++++++++++++------------ src/private/users/AddOrEditUserForm.jsx | 103 +++++ src/private/users/UserManagement.jsx | 181 +++++++++ 4 files changed, 548 insertions(+), 266 deletions(-) create mode 100644 src/private/users/AddOrEditUserForm.jsx create mode 100644 src/private/users/UserManagement.jsx diff --git a/src/App.jsx b/src/App.jsx index 085ccd6..a3136bc 100644 --- a/src/App.jsx +++ b/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' && } {zone === 'public' && currentView === 'Dashboard' && } - {zone === 'public' && currentView === 'Products' && } - {zone === 'public' && currentView === 'Clients' && } - {zone === 'public' && currentView === 'Providers' && } - {zone === 'public' && currentView === 'Categories' && } - {zone === 'public' && currentView === 'Admin' && } + + {zone === 'public' && currentView === 'UserManagement' && } } /> diff --git a/src/components/MenuDrawerPrivate.jsx b/src/components/MenuDrawerPrivate.jsx index e43a992..50b5d35 100644 --- a/src/components/MenuDrawerPrivate.jsx +++ b/src/components/MenuDrawerPrivate.jsx @@ -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: , - children: [ - { title: 'Sales Report' }, - { title: 'Customer Insights' }, - { title: 'Customer Insights 2' }, - ] - }, - { - title: 'Products Management', - icon: , - children: [ - { - title: 'Catalog Management', + { + title: 'Business Intelligence', + icon: , 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: , - children: [ - { title: 'CRM' }, - { title: 'Customer List' }, - { - title: 'Projects', + }, + { + title: 'Products Management', + icon: , 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: , - children: [ - { title: 'Brand Partners' }, - { title: 'Companies' }, - { title: 'Suppliers' }, - { title: 'Materials Providers' }, - ] - }, - { - title: 'Users', - icon: , - children: [ - { title: 'Users Management' }, - { title: 'Access Control' }, - { title: 'Roles' }, - { title: 'Permissions' }, - ] - }, - { - title: 'Settings', - icon: , - children: [ - { title: 'General Settings' }, - { title: 'WebApp Configuration' }, - { title: 'Mobile App Configuration' }, - ] - }, + }, + { + title: 'Customers', + icon: , + children: [ + { title: 'CRM' }, + { title: 'Customer List' }, + { + title: 'Projects', + children: [ + { title: 'Customer Collections' }, + { title: 'Sales' }, + { title: 'Quotes' }, + { title: 'Orders' }, + ] + } + ] + }, + { + title: 'Providers (Brands and Clients)', + icon: , + children: [ + { title: 'Brand Partners' }, + { title: 'Companies' }, + { title: 'Suppliers' }, + { title: 'Materials Providers' }, + ] + }, + { + title: 'Users', + icon: , + children: [ + { title: 'Users Management' }, + { title: 'Access Control' }, + { title: 'Roles' }, + { title: 'Permissions' }, + ] + }, + { + title: 'Settings', + icon: , + 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 ( + + + { + 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 && ( + + {node.icon} + + )} + + {!collapsed && ( + <> + + {hasChildren ? (openMap[key] ? : ) : null} + + )} + + + + {hasChildren && !collapsed && ( + + + {node.children.map((child, idx) => renderNode(child, `${key}-`))} + + + )} + + ); + }; return ( - - - { - if (hasChildren) { - handleToggleNode(key); - } else { - onSelect?.(node.title); - if (isMobile) onClose?.(); - } - }} + - {node.icon && ( - - {node.icon} - - )} - - {!collapsed && ( - <> - - {hasChildren ? (openMap[key] ? : ) : null} - - )} - - - - {hasChildren && !collapsed && ( - - - {node.children.map((child, idx) => renderNode(child, `${key}-`))} - - - )} - - ); - }; - - return ( - - - - {!collapsed && ( - - Dream Views - - - - )} - - - {collapsed && ( - - Dream Views - - )} - - {/* Tree */} - - {menuData.map((node) => renderNode(node))} - + transition: theme.transitions.create('width', { + duration: theme.transitions.duration.standard, + easing: theme.transitions.easing.sharp, + }), + borderRight: '1px solid rgba(0,0,0,0.08)', + }, + }} + > - - setCollapsed((c) => !c)} sx={{ - backgroundColor: 'transparent', - color: 'transparent', - '&:hover': { - backgroundColor: '#fff4ec', - borderColor: 'transparent' - }, - borderRadius: 0, - marginLeft: 2, - width: 40, - height: 40, - }}> - {collapsed - - - - ); + + {!collapsed && ( + + Dream Views + + + + )} + + + {collapsed && ( + + Dream Views + + )} + + {/* Tree */} + + {menuData.map((node) => renderNode(node))} + + + + setCollapsed((c) => !c)} sx={{ + backgroundColor: 'transparent', + color: 'transparent', + '&:hover': { + backgroundColor: '#fff4ec', + borderColor: 'transparent' + }, + borderRadius: 0, + marginLeft: 2, + width: 40, + height: 40, + }}> + {collapsed + + + + ); } \ No newline at end of file diff --git a/src/private/users/AddOrEditUserForm.jsx b/src/private/users/AddOrEditUserForm.jsx new file mode 100644 index 0000000..4d79483 --- /dev/null +++ b/src/private/users/AddOrEditUserForm.jsx @@ -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 ( + + + + + + Admin + User + Manager + + + Active + Inactive + + + + + + + ); +} \ No newline at end of file diff --git a/src/private/users/UserManagement.jsx b/src/private/users/UserManagement.jsx new file mode 100644 index 0000000..5cfe86c --- /dev/null +++ b/src/private/users/UserManagement.jsx @@ -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) => ( + + handleEditClick(params)} + > + + + handleDeleteClick(params.row)} + > + + + + ) + } + ]; + + return ( + + User Management + + { setOpen(false); setEditingData(null); }} maxWidth="md" fullWidth> + {editingData ? 'Edit User' : 'Add User'} + + { + await loadData(); + setOpen(false); + setEditingData(null); + }} + initialData={editingData} + onCancel={() => { + setOpen(false); + setEditingData(null); + }} + /> + + + + setConfirmOpen(false)}> + Confirm Delete + + + Are you sure you want to delete {rowToDelete?.username}? + + + + + + + + + + ({ top: 4, bottom: 4 })} + /> + + + + + + + ); +} \ No newline at end of file