first commit

This commit is contained in:
2026-06-05 17:28:05 +05:30
commit a162fa89e5
62 changed files with 8729 additions and 0 deletions

View File

@@ -0,0 +1,261 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
AppBar,
Toolbar,
IconButton,
Box,
InputBase,
Badge,
Avatar,
Typography,
Menu,
MenuItem,
Divider,
ListItemIcon,
ListItemText,
Tooltip,
Button,
Stack,
alpha
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone';
import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
import PersonOutlineIcon from '@mui/icons-material/PersonOutline';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import LogoutIcon from '@mui/icons-material/Logout';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined';
import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined';
import AssignmentOutlinedIcon from '@mui/icons-material/AssignmentOutlined';
import DoneAllIcon from '@mui/icons-material/DoneAll';
import Logo from '@/components/Logo';
const RED = '#C01227';
const INITIAL_NOTIFICATIONS = [
{ id: 1, icon: Inventory2OutlinedIcon, title: 'New order #ORD-10482 placed', time: '2 min ago', to: '/orders', read: false },
{ id: 2, icon: TwoWheelerOutlinedIcon, title: 'Rider Imran went online', time: '18 min ago', to: '/riders', read: false },
{ id: 3, icon: PaymentsOutlinedIcon, title: 'Invoice INV-2041 marked paid', time: '1 hr ago', to: '/invoice', read: false },
{ id: 4, icon: AssignmentOutlinedIcon, title: '3 new onboarding requests', time: '3 hrs ago', to: '/requests', read: true }
];
const MESSAGES = [
{ id: 1, name: 'Priya Nair', text: 'Can we reroute the MG Road batch?', time: '5 min ago', initials: 'PN' },
{ id: 2, name: 'Imran Khan', text: 'Reached the warehouse, loading now.', time: '22 min ago', initials: 'IK' },
{ id: 3, name: 'Acme Logistics', text: 'Please confirm the revised pricing.', time: '2 hrs ago', initials: 'AL' }
];
export default function Header({ onToggle }) {
const navigate = useNavigate();
const [account, setAccount] = useState(null);
const [notifAnchor, setNotifAnchor] = useState(null);
const [msgAnchor, setMsgAnchor] = useState(null);
const [notifications, setNotifications] = useState(INITIAL_NOTIFICATIONS);
const [search, setSearch] = useState('');
const unread = notifications.filter((n) => !n.read).length;
const openNotif = (e) => setNotifAnchor(e.currentTarget);
const closeNotif = () => setNotifAnchor(null);
const markAllRead = () => setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
const onNotifClick = (n) => {
setNotifications((prev) => prev.map((x) => (x.id === n.id ? { ...x, read: true } : x)));
closeNotif();
navigate(n.to);
};
const submitSearch = (e) => {
e.preventDefault();
const q = search.trim();
if (q) navigate(`/orders?q=${encodeURIComponent(q)}`);
};
return (
<AppBar
position="fixed"
elevation={0}
sx={{ bgcolor: RED, color: '#fff', zIndex: (t) => t.zIndex.drawer + 1, boxShadow: '0 1px 0 rgba(0,0,0,0.06)' }}
>
<Toolbar sx={{ minHeight: 64, px: { xs: 1.5, sm: 2.5 }, gap: 1 }}>
<IconButton color="inherit" edge="start" onClick={onToggle} sx={{ mr: 0.5 }}>
<MenuIcon />
</IconButton>
{/* Brand wordmark — left side */}
<Box
onClick={() => navigate('/dashboard')}
sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
>
<Logo onDark height={22} />
</Box>
<Box sx={{ flexGrow: 1 }} />
{/* Search — moved to the right */}
<Box
component="form"
onSubmit={submitSearch}
sx={{
display: { xs: 'none', sm: 'flex' },
alignItems: 'center',
bgcolor: alpha('#fff', 0.16),
borderRadius: 2,
px: 1.5,
py: 0.5,
width: { sm: 240, md: 320 },
'&:hover': { bgcolor: alpha('#fff', 0.22) },
'&:focus-within': { bgcolor: alpha('#fff', 0.26) }
}}
>
<SearchIcon sx={{ fontSize: 20, mr: 1, opacity: 0.9 }} />
<InputBase
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search orders, riders, customers…"
sx={{ color: '#fff', fontSize: '0.875rem', flex: 1, '&::placeholder': { color: '#fff' } }}
inputProps={{ style: { color: '#fff' }, 'aria-label': 'search' }}
/>
</Box>
<Tooltip title="Messages">
<IconButton color="inherit" onClick={(e) => setMsgAnchor(e.currentTarget)}>
<Badge badgeContent={MESSAGES.length} color="warning">
<ChatBubbleOutlineIcon />
</Badge>
</IconButton>
</Tooltip>
<Tooltip title="Notifications">
<IconButton color="inherit" onClick={openNotif}>
<Badge badgeContent={unread} color="warning">
<NotificationsNoneIcon />
</Badge>
</IconButton>
</Tooltip>
<Box
onClick={(e) => setAccount(e.currentTarget)}
sx={{ display: 'flex', alignItems: 'center', gap: 1, ml: 0.5, cursor: 'pointer', py: 0.5, px: 0.5, borderRadius: 2, '&:hover': { bgcolor: alpha('#fff', 0.14) } }}
>
<Avatar sx={{ width: 34, height: 34, bgcolor: '#fff', color: RED, fontWeight: 700 }}>AD</Avatar>
<Box sx={{ display: { xs: 'none', md: 'block' }, lineHeight: 1.1 }}>
<Typography variant="subtitle2" sx={{ color: '#fff', fontWeight: 600 }}>
Aman Deshmukh
</Typography>
<Typography variant="caption" sx={{ color: alpha('#fff', 0.8) }}>
Operations Admin
</Typography>
</Box>
</Box>
{/* Notifications dropdown */}
<Menu
anchorEl={notifAnchor}
open={Boolean(notifAnchor)}
onClose={closeNotif}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{ sx: { mt: 1, width: 360, maxWidth: '90vw' } }}
>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ px: 2, py: 1.25 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
Notifications
</Typography>
<Button size="small" startIcon={<DoneAllIcon fontSize="small" />} onClick={markAllRead} disabled={unread === 0}>
Mark all read
</Button>
</Stack>
<Divider />
{notifications.length === 0 && (
<MenuItem disabled>
<ListItemText primary="No notifications" />
</MenuItem>
)}
{notifications.map((n) => {
const Icon = n.icon;
return (
<MenuItem key={n.id} onClick={() => onNotifClick(n)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}>
<ListItemIcon sx={{ mt: 0.25 }}>
<Avatar sx={{ width: 34, height: 34, bgcolor: n.read ? 'grey.200' : alpha(RED, 0.12), color: RED }}>
<Icon fontSize="small" />
</Avatar>
</ListItemIcon>
<ListItemText
primary={n.title}
secondary={n.time}
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: n.read ? 500 : 700 }}
secondaryTypographyProps={{ fontSize: '0.75rem' }}
/>
{!n.read && <Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: RED, mt: 1, ml: 0.5 }} />}
</MenuItem>
);
})}
<Divider />
<MenuItem onClick={() => { closeNotif(); navigate('/requests'); }} sx={{ justifyContent: 'center', color: 'primary.main', fontWeight: 600 }}>
View all activity
</MenuItem>
</Menu>
{/* Messages dropdown */}
<Menu
anchorEl={msgAnchor}
open={Boolean(msgAnchor)}
onClose={() => setMsgAnchor(null)}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{ sx: { mt: 1, width: 340, maxWidth: '90vw' } }}
>
<Typography variant="subtitle1" sx={{ fontWeight: 700, px: 2, py: 1.25 }}>
Messages
</Typography>
<Divider />
{MESSAGES.map((m) => (
<MenuItem key={m.id} onClick={() => setMsgAnchor(null)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}>
<ListItemIcon sx={{ mt: 0.25 }}>
<Avatar sx={{ width: 34, height: 34, bgcolor: alpha(RED, 0.12), color: RED, fontWeight: 700, fontSize: '0.8rem' }}>
{m.initials}
</Avatar>
</ListItemIcon>
<ListItemText
primary={m.name}
secondary={m.text}
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: 700 }}
secondaryTypographyProps={{ fontSize: '0.8rem' }}
/>
<Typography variant="caption" color="text.secondary" sx={{ ml: 1, mt: 0.5, flexShrink: 0 }}>
{m.time}
</Typography>
</MenuItem>
))}
</Menu>
{/* Account dropdown */}
<Menu
anchorEl={account}
open={Boolean(account)}
onClose={() => setAccount(null)}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{ sx: { mt: 1, minWidth: 200 } }}
>
<MenuItem onClick={() => { setAccount(null); navigate('/profile'); }}>
<ListItemIcon><PersonOutlineIcon fontSize="small" /></ListItemIcon>
View Profile
</MenuItem>
<MenuItem onClick={() => { setAccount(null); navigate('/settings'); }}>
<ListItemIcon><SettingsOutlinedIcon fontSize="small" /></ListItemIcon>
Settings
</MenuItem>
<Divider />
<MenuItem onClick={() => { setAccount(null); navigate('/login'); }} sx={{ color: 'error.main' }}>
<ListItemIcon><LogoutIcon fontSize="small" color="error" /></ListItemIcon>
Logout
</MenuItem>
</Menu>
</Toolbar>
</AppBar>
);
}

View File

@@ -0,0 +1,200 @@
import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
Drawer,
Box,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Typography,
Collapse,
Tooltip,
Toolbar
} from '@mui/material';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
import navItems from '@/menu/navItems';
import Logo from '@/components/Logo';
export const DRAWER_WIDTH = 264;
export const MINI_WIDTH = 78;
const RED = '#C01227';
function NavLeaf({ item, open, active, depth = 0, onClick }) {
const Icon = item.icon;
const button = (
<ListItemButton
selected={active}
onClick={onClick}
sx={{
minHeight: 44,
my: 0.25,
mx: open ? 1 : 0.75,
px: open ? 1.5 : 0,
justifyContent: open ? 'flex-start' : 'center',
borderRadius: 2,
color: 'rgba(255,255,255,0.78)',
'& .MuiListItemIcon-root': { color: 'inherit' },
'&:hover': { bgcolor: 'rgba(255,255,255,0.12)', color: '#fff' },
'&.Mui-selected': {
bgcolor: 'rgba(255,255,255,0.18)',
color: '#fff',
'&:hover': { bgcolor: 'rgba(255,255,255,0.22)' }
}
}}
>
<ListItemIcon sx={{ minWidth: open ? 34 : 'auto', justifyContent: 'center' }}>
{depth > 0 && !Icon ? <FiberManualRecordIcon sx={{ fontSize: 8 }} /> : Icon ? <Icon fontSize="small" /> : null}
</ListItemIcon>
{open && (
<ListItemText
primary={item.title}
primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: active ? 700 : 500 }}
/>
)}
</ListItemButton>
);
return open ? button : <Tooltip title={item.title} placement="right">{button}</Tooltip>;
}
export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) {
const location = useLocation();
const navigate = useNavigate();
const isActive = (url) => url && location.pathname.startsWith(url);
const expanded = open || isMobile;
const initialOpen = navItems
.flatMap((g) => g.items)
.filter((i) => i.children && i.children.some((c) => isActive(c.url)))
.map((i) => i.id);
const [collapse, setCollapse] = useState(initialOpen);
const go = (url) => {
navigate(url);
if (isMobile) onMobileClose();
};
const content = (
<Box sx={{ bgcolor: RED, height: '100%', color: '#fff', display: 'flex', flexDirection: 'column' }}>
<Toolbar sx={{ px: expanded ? 2.5 : 0, justifyContent: expanded ? 'flex-start' : 'center', minHeight: 64 }}>
<Logo onDark compact={!expanded} />
</Toolbar>
<Box sx={{ overflowY: 'auto', overflowX: 'hidden', flexGrow: 1, pb: 2 }}>
{navItems.map((grp) => (
<Box key={grp.group} sx={{ mt: 1 }}>
{expanded && (
<Typography
variant="overline"
sx={{ px: 2.5, color: 'rgba(255,255,255,0.55)', fontSize: '0.6875rem', letterSpacing: '0.08em' }}
>
{grp.group}
</Typography>
)}
<List disablePadding sx={{ mt: 0.5 }}>
{grp.items.map((item) => {
if (item.children) {
const opened = collapse.includes(item.id);
const childActive = item.children.some((c) => isActive(c.url));
const Icon = item.icon;
const head = (
<ListItemButton
onClick={() =>
expanded
? setCollapse((p) => (p.includes(item.id) ? p.filter((x) => x !== item.id) : [...p, item.id]))
: go(item.children[0].url)
}
sx={{
minHeight: 44,
my: 0.25,
mx: expanded ? 1 : 0.75,
px: expanded ? 1.5 : 0,
justifyContent: expanded ? 'flex-start' : 'center',
borderRadius: 2,
color: childActive ? '#fff' : 'rgba(255,255,255,0.78)',
bgcolor: childActive && !opened ? 'rgba(255,255,255,0.12)' : 'transparent',
'&:hover': { bgcolor: 'rgba(255,255,255,0.12)', color: '#fff' }
}}
>
<ListItemIcon sx={{ minWidth: expanded ? 34 : 'auto', justifyContent: 'center', color: 'inherit' }}>
<Icon fontSize="small" />
</ListItemIcon>
{expanded && (
<>
<ListItemText primary={item.title} primaryTypographyProps={{ fontSize: '0.875rem', fontWeight: 500 }} />
{opened ? <ExpandLess fontSize="small" /> : <ExpandMore fontSize="small" />}
</>
)}
</ListItemButton>
);
return (
<Box key={item.id}>
{expanded ? head : <Tooltip title={item.title} placement="right">{head}</Tooltip>}
{expanded && (
<Collapse in={opened} timeout="auto" unmountOnExit>
<Box sx={{ pl: 1.5 }}>
{item.children.map((c) => (
<NavLeaf key={c.id} item={c} open depth={1} active={isActive(c.url)} onClick={() => go(c.url)} />
))}
</Box>
</Collapse>
)}
</Box>
);
}
return (
<NavLeaf key={item.id} item={item} open={expanded} active={isActive(item.url)} onClick={() => go(item.url)} />
);
})}
</List>
</Box>
))}
</Box>
{expanded && (
<Box sx={{ p: 2, borderTop: '1px solid rgba(255,255,255,0.12)' }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.55)' }}>
Doormile Console v1.0
</Typography>
</Box>
)}
</Box>
);
if (isMobile) {
return (
<Drawer
variant="temporary"
open={mobileOpen}
onClose={onMobileClose}
ModalProps={{ keepMounted: true }}
sx={{ '& .MuiDrawer-paper': { width: DRAWER_WIDTH, border: 'none' } }}
>
{content}
</Drawer>
);
}
return (
<Drawer
variant="permanent"
sx={{
width: open ? DRAWER_WIDTH : MINI_WIDTH,
flexShrink: 0,
whiteSpace: 'nowrap',
'& .MuiDrawer-paper': {
width: open ? DRAWER_WIDTH : MINI_WIDTH,
border: 'none',
overflowX: 'hidden',
transition: (t) => t.transitions.create('width', { duration: t.transitions.duration.standard })
}
}}
open={open}
>
{content}
</Drawer>
);
}

View File

@@ -0,0 +1,45 @@
import { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { Box, Toolbar, useMediaQuery } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import Header from './Header';
import Sidebar, { DRAWER_WIDTH, MINI_WIDTH } from './Sidebar';
export default function MainLayout() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('lg'));
const [open, setOpen] = useState(true);
const [mobileOpen, setMobileOpen] = useState(false);
const toggle = () => {
if (isMobile) setMobileOpen((p) => !p);
else setOpen((p) => !p);
};
return (
<Box sx={{ display: 'flex', bgcolor: 'background.default', minHeight: '100vh' }}>
<Header onToggle={toggle} />
<Sidebar
open={open}
isMobile={isMobile}
mobileOpen={mobileOpen}
onMobileClose={() => setMobileOpen(false)}
/>
<Box
component="main"
sx={{
flexGrow: 1,
width: { lg: `calc(100% - ${open ? DRAWER_WIDTH : MINI_WIDTH}px)` },
minHeight: '100vh',
transition: theme.transitions.create('width', { duration: theme.transitions.duration.standard })
}}
>
<Toolbar sx={{ minHeight: 64 }} />
<Box sx={{ p: { xs: 2, sm: 3 } }}>
<Outlet />
</Box>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,11 @@
import { Outlet } from 'react-router-dom';
import { Box } from '@mui/material';
// Used by auth + maintenance pages — full-bleed, no shell.
export default function MinimalLayout() {
return (
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default' }}>
<Outlet />
</Box>
);
}