Reapply "updates on the ui changes"

This reverts commit d2f264460a.
This commit is contained in:
2026-07-14 12:20:50 +05:30
parent d2f264460a
commit 77ecb83cef
44 changed files with 7062 additions and 4670 deletions

View File

@@ -1,242 +1,79 @@
import { useState, useRef, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
AppBar,
Toolbar,
IconButton,
Box,
InputBase,
Avatar,
Typography,
Stack,
Menu,
MenuItem,
Divider,
ListItemIcon,
Popper,
Paper,
ClickAwayListener,
CircularProgress,
alpha
} from '@mui/material';
import MenuIcon from '@mui/icons-material/Menu';
import SearchIcon from '@mui/icons-material/Search';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import LogoutIcon from '@mui/icons-material/Logout';
import { Settings, LogOut } from 'lucide-react';
import { TopNav, TopNavHeading } from '@astryxdesign/core/TopNav';
import { DropdownMenu } from '@astryxdesign/core/DropdownMenu';
import Logo from '@/components/Logo';
import UserAvatar from '@/components/UserAvatar';
import { fetchClients, fetchUsers } from '@/utils/apiClient';
import { toClient } from '@/utils/mappers';
import { fetchUsers } from '@/utils/apiClient';
const RED = '#C01227';
export default function Header({ onToggle }) {
export default function Header() {
const navigate = useNavigate();
const [account, setAccount] = useState(null);
const [search, setSearch] = useState('');
// Read user from localStorage
let storedUserObj = { name: 'Admin', role: 'Operations Admin', id: 0 };
try {
const storedUser = localStorage.getItem('user');
if (storedUser) {
storedUserObj = JSON.parse(storedUser);
}
} catch(e) {}
if (storedUser) storedUserObj = JSON.parse(storedUser);
} catch (e) {}
const [activeUserName, setActiveUserName] = useState(storedUserObj.name || 'Admin');
const displayRole = storedUserObj.role === 'admin' ? 'Administrator' :
const displayRole = storedUserObj.role === 'admin' ? 'Administrator' :
storedUserObj.role === 'manager' ? 'Manager' :
storedUserObj.role === 'executive' ? 'Executive' : 'Operations Admin';
const displayInitial = activeUserName.charAt(0).toUpperCase();
const searchRef = useRef(null);
const [clients, setClients] = useState([]);
const [loadedClients, setLoadedClients] = useState(false);
const [loadingClients, setLoadingClients] = useState(false);
const [openResults, setOpenResults] = useState(false);
// Fetch users to get the real name if the login payload didn't have it
useEffect(() => {
fetchUsers().then((users) => {
if (storedUserObj.email) {
// Strictly use email to match, as stored ID might belong to the doormile_auth table instead of appusers table
const matchingUser = users.find(u => u.email === storedUserObj.email);
if (matchingUser && matchingUser.first_name) {
setActiveUserName(matchingUser.first_name);
// Also update localStorage so it's fresh
const updatedUser = { ...storedUserObj, name: matchingUser.first_name };
localStorage.setItem('user', JSON.stringify(updatedUser));
localStorage.setItem('user', JSON.stringify({ ...storedUserObj, name: matchingUser.first_name }));
}
}
}).catch(console.error);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const ensureClients = () => {
if (loadedClients || loadingClients) return;
setLoadingClients(true);
fetchClients()
.then((points) => setClients(points.map(toClient)))
.catch(() => {})
.finally(() => { setLoadedClients(true); setLoadingClients(false); });
};
const q = search.trim().toLowerCase();
const results = q
? clients.filter((c) => [c.name, c.city, c.businessType, c.phone].join(' ').toLowerCase().includes(q)).slice(0, 6)
: [];
const onSearchChange = (e) => {
setSearch(e.target.value);
ensureClients();
setOpenResults(true);
};
const goToClients = (term) => {
navigate(`/tenants?q=${encodeURIComponent(term)}`);
setSearch('');
setOpenResults(false);
};
const submitSearch = (e) => {
e.preventDefault();
const term = search.trim();
if (term) goToClients(term);
};
return (
<AppBar
position="fixed"
elevation={0}
sx={{ bgcolor: '#fff', color: 'grey.800', zIndex: (t) => t.zIndex.drawer + 1, borderBottom: 1, borderColor: 'divider' }}
>
<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>
<Box
onClick={() => navigate('/dashboard')}
sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
>
<Logo height={22} />
</Box>
<Box sx={{ flexGrow: 1 }} />
<ClickAwayListener onClickAway={() => setOpenResults(false)}>
<Box sx={{ display: { xs: 'none', sm: 'block' }, position: 'relative' }}>
<Box
ref={searchRef}
component="form"
onSubmit={submitSearch}
sx={{
display: 'flex',
alignItems: 'center',
bgcolor: 'grey.100',
borderRadius: 2,
px: 1.5,
py: 0.5,
width: { sm: 240, md: 320 },
'&:hover': { bgcolor: 'grey.200' },
'&:focus-within': { bgcolor: 'grey.200' }
}}
>
<SearchIcon sx={{ fontSize: 20, mr: 1, color: 'grey.500' }} />
<InputBase
value={search}
onChange={onSearchChange}
onFocus={() => { ensureClients(); if (search.trim()) setOpenResults(true); }}
placeholder="Search clients…"
sx={{ color: 'grey.800', fontSize: '0.875rem', flex: 1, '&::placeholder': { color: 'grey.500' } }}
inputProps={{ 'aria-label': 'search' }}
/>
</Box>
<Popper
open={openResults && !!q}
anchorEl={searchRef.current}
placement="bottom-start"
style={{ zIndex: 1400, width: searchRef.current?.offsetWidth }}
>
<Paper sx={{ mt: 1, borderRadius: 2, overflow: 'hidden', boxShadow: '0 8px 24px rgba(0,0,0,0.18)' }}>
{loadingClients && results.length === 0 ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 2.5 }}><CircularProgress size={20} /></Box>
) : results.length === 0 ? (
<Box sx={{ px: 2, py: 2 }}>
<Typography variant="body2" color="text.secondary">No clients match {search.trim()}.</Typography>
</Box>
) : (
<>
{results.map((c) => (
<MenuItem key={c.id} onClick={() => goToClients(c.name)} sx={{ py: 1, gap: 1.25 }}>
<UserAvatar name={c.name} size={30} />
<Box sx={{ minWidth: 0 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'grey.800' }} noWrap>{c.name}</Typography>
<Typography variant="caption" color="text.secondary" noWrap>
{[c.businessType, c.city].filter(Boolean).join(' · ') || c.phone}
</Typography>
</Box>
</MenuItem>
))}
<Divider />
<MenuItem onClick={() => goToClients(search.trim())} sx={{ py: 1.25, color: 'primary.main', fontWeight: 600 }}>
<SearchIcon fontSize="small" sx={{ mr: 1 }} />
See all results for {search.trim()}
</MenuItem>
</>
)}
</Paper>
</Popper>
</Box>
</ClickAwayListener>
<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: 'grey.100' } }}
>
<Avatar sx={{ width: 34, height: 34, bgcolor: RED, color: '#fff', fontWeight: 700 }}>{displayInitial}</Avatar>
<Box sx={{ display: { xs: 'none', md: 'block' }, lineHeight: 1.1 }}>
<Typography variant="subtitle2" sx={{ color: 'grey.800', fontWeight: 600 }}>
{activeUserName}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary', textTransform: 'capitalize' }}>
{displayRole}
</Typography>
</Box>
</Box>
<Menu
anchorEl={account}
open={Boolean(account)}
onClose={() => setAccount(null)}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
PaperProps={{ sx: { mt: 1, minWidth: 220 } }}
>
<Box sx={{ px: 2, py: 1.5 }}>
<Stack direction="row" spacing={1.5} alignItems="center">
<Avatar sx={{ width: 38, height: 38, bgcolor: RED, color: '#fff', fontWeight: 700 }}>{displayInitial}</Avatar>
<Box sx={{ lineHeight: 1.2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>{activeUserName}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ textTransform: 'capitalize' }}>{displayRole}</Typography>
</Box>
</Stack>
</Box>
<Divider />
<MenuItem onClick={() => { setAccount(null); navigate('/settings'); }}>
<ListItemIcon><SettingsOutlinedIcon fontSize="small" /></ListItemIcon>
Settings
</MenuItem>
<Divider />
<MenuItem onClick={() => { setAccount(null); localStorage.removeItem('auth_token'); navigate('/login'); }} sx={{ color: 'error.main' }}>
<ListItemIcon><LogoutIcon fontSize="small" color="error" /></ListItemIcon>
Logout
</MenuItem>
</Menu>
</Toolbar>
</AppBar>
<>
<TopNav
label="Main navigation"
style={{
backgroundColor: '#ffffff',
borderBottom: '1px solid rgba(5, 54, 89, 0.08)',
boxShadow: '0 1px 3px rgba(15, 23, 42, 0.04)'
}}
heading={
<TopNavHeading
logo={<Logo compact height={28} />}
heading="Doormile CRM"
subheading={displayRole}
headingHref="/dashboard"
/>
}
endContent={
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<DropdownMenu
button={{
variant: 'ghost',
size: 'lg',
icon: <UserAvatar name={activeUserName} size={22} />,
label: activeUserName
}}
items={[
{ label: 'Settings', icon: Settings, onClick: () => navigate('/settings') },
{ type: 'divider' },
{
label: 'Logout',
icon: LogOut,
onClick: () => { localStorage.removeItem('auth_token'); navigate('/login'); }
}
]}
/>
</div>
}
/>
</>
);
}

View File

@@ -1,204 +1,158 @@
import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
Drawer,
Box,
List,
ListItemButton,
ListItemIcon,
ListItemText,
Typography,
Collapse,
Tooltip,
Toolbar,
alpha
} 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 { useLocation } from 'react-router-dom';
import { SideNav, SideNavSection, SideNavItem } from '@astryxdesign/core/SideNav';
import { Text } from '@astryxdesign/core/Text';
import navItems from '@/menu/navItems';
import Logo from '@/components/Logo';
export const DRAWER_WIDTH = 264;
export const MINI_WIDTH = 78;
// ==============================|| DOORMILE - SIDE NAV ||============================== //
// Thin wrapper around Astryx's SideNav template: sections + items driven by navItems.
// Branding lives in the TopNav heading, so this stays icon+label only (Astryx guidance:
// avoid a SideNavHeading when TopNav already carries app identity).
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: active ? RED : 'grey.700',
'& .MuiListItemIcon-root': { color: active ? RED : 'grey.500' },
'&:hover': { bgcolor: alpha(RED, 0.04), color: RED, '& .MuiListItemIcon-root': { color: RED } },
'&.Mui-selected': {
bgcolor: alpha(RED, 0.08),
color: RED,
'& .MuiListItemIcon-root': { color: RED },
borderLeft: open ? `4px solid ${RED}` : 'none',
'&:hover': { bgcolor: alpha(RED, 0.12) }
}
}}
>
<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 }) {
export default function Sidebar() {
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: '#fff', height: '100%', color: 'grey.800', display: 'flex', flexDirection: 'column', borderRight: 1, borderColor: 'divider' }}>
<Toolbar sx={{ px: expanded ? 2.5 : 0, justifyContent: expanded ? 'flex-start' : 'center', minHeight: 64 }}>
<Logo 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: '#A06060', fontSize: '0.6875rem', letterSpacing: '0.08em', fontWeight: 700 }}
>
{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 ? RED : 'grey.700',
bgcolor: childActive && !opened ? alpha(RED, 0.08) : 'transparent',
'& .MuiListItemIcon-root': { color: childActive ? RED : 'grey.500' },
'&:hover': { bgcolor: alpha(RED, 0.04), color: RED, '& .MuiListItemIcon-root': { color: RED } }
}}
>
<ListItemIcon sx={{ minWidth: expanded ? 34 : 'auto', justifyContent: 'center' }}>
<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', borderColor: 'divider' }}>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
Doormile CRM 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>
);
}
const isActive = (url) => !!url && location.pathname.startsWith(url);
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 })
<>
<SideNav
className="doormile-side-nav"
collapsible={{ defaultIsCollapsed: true, buttonLabel: 'Collapse navigation' }}
style={{
// White + a right border/shadow matches the TopNav's chrome so the
// top and side nav read as one unified surface instead of two
// mismatched panels (TopNav is white with a bottom border already).
backgroundColor: '#ffffff',
borderRight: '1px solid rgba(5, 54, 89, 0.08)',
boxShadow: '1px 0 3px rgba(15, 23, 42, 0.03)',
paddingBlock: '12px',
paddingInline: '8px',
boxSizing: 'border-box',
// Astryx's collapsed-rail width reads var(--spacing-12) (48px default),
// which feels cramped for our icons. Overriding the token on this
// element only widens the collapsed rail without touching the same
// token's unrelated uses elsewhere (e.g. AppShell's mobile top bar).
'--spacing-12': '72px'
}}
>
{navItems.map((grp) => (
<SideNavSection key={grp.group} title={grp.group}>
{grp.items.map((item) => (
<SideNavItem
key={item.id}
label={item.title}
icon={item.icon}
href={item.url}
isSelected={isActive(item.url)}
/>
))}
</SideNavSection>
))}
</SideNav>
<style>{`
/* Astryx packs items 2px apart by default, which reads as one
merged block on hover instead of distinct rows. The items
wrapper has no stable class of its own, but it's always the
section's second/last child div, right after the header. */
.doormile-side-nav .astryx-side-nav-section > div:last-child {
gap: 6px !important;
}
}}
open={open}
>
{content}
</Drawer>
/* Collapsed nav items are the only ones Astryx renders with an
aria-label on the item element itself (expanded rows show the
label as visible text instead), so it doubles as a stable hook
for "collapsed only" styling without touching the expanded rail. */
.doormile-side-nav .astryx-side-nav-item[aria-label] {
width: 40px;
height: 40px;
margin-inline: auto;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.15s ease;
}
.doormile-side-nav .astryx-side-nav-item[aria-label] .astryx-icon {
width: 18px !important;
height: 18px !important;
}
.doormile-side-nav .astryx-side-nav-item[aria-label]:hover {
background-color: rgba(192, 18, 39, 0.08) !important;
}
.doormile-side-nav .astryx-side-nav-item[aria-label]:focus-visible {
outline: 2px solid rgba(192, 18, 39, 0.4);
outline-offset: 2px;
}
.doormile-side-nav .astryx-side-nav-item[aria-label][data-selected='selected'] {
background-color: rgba(192, 18, 39, 0.12) !important;
}
.doormile-side-nav .astryx-side-nav-item[aria-label][data-selected='selected'] .astryx-icon {
color: #C01227 !important;
}
/* Expanded rows: rounded hover/selected states with a left accent
bar on the active item, instead of the default flat highlight.
The accent bar is always present (via ::before) but only faded
in on selection, so clicking a link fades it in smoothly instead
of the bar snapping into place flush against the row edges.
Row sizing mirrors doormile_console's NavItem pattern (py: 1,
i.e. 8px top/bottom padding on the button itself) instead of a
fixed-height row: the highlight is painted directly on the item's
own (now-padded) background, so it naturally gets breathing room
above and below rather than filling a cramped fixed-height box
edge to edge. */
.doormile-side-nav .astryx-side-nav-item:not([aria-label]) {
position: relative;
margin-inline: 2px;
height: auto;
padding-block: 8px !important;
transition: background-color 0.15s ease, transform 0.15s ease;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])::before {
content: '';
position: absolute;
left: -2px;
top: 8px;
bottom: 8px;
width: 3px;
border-radius: 3px;
background-color: #C01227;
opacity: 0;
transition: opacity 0.15s ease;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label]):hover {
background-color: rgba(192, 18, 39, 0.05) !important;
transform: translateX(2px);
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label]):focus-visible {
outline: 2px solid rgba(192, 18, 39, 0.4);
outline-offset: 2px;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected'] {
background-color: rgba(192, 18, 39, 0.08) !important;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected']::before {
opacity: 1;
}
.doormile-side-nav .astryx-side-nav-item:not([aria-label])[data-selected='selected'] .astryx-icon {
color: #C01227 !important;
}
/* Collapse/expand toggle (the "<" / ">" chevron button). It sits
right on the sidebar's edge, so the default square ghost-button
hover clips against that border and looks broken. Give it a
clean circular hover detached from the edge instead. */
.doormile-side-nav button[aria-label*="sidebar"],
.doormile-side-nav button[aria-label*="navigation"] {
border-radius: 50% !important;
transition: background-color 0.15s ease !important;
transform: translateX(-6px) !important;
}
.doormile-side-nav button[aria-label*="sidebar"]:hover,
.doormile-side-nav button[aria-label*="navigation"]:hover {
background-color: rgba(192, 18, 39, 0.08) !important;
}
`}</style>
</>
);
}

View File

@@ -1,45 +1,36 @@
import { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { Box, Toolbar, useMediaQuery } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { AppShell } from '@astryxdesign/core/AppShell';
import Header from './Header';
import Sidebar, { DRAWER_WIDTH, MINI_WIDTH } from './Sidebar';
import Sidebar from './Sidebar';
// ==============================|| DOORMILE - APP SHELL ||============================== //
// Astryx AppShell owns the responsive chrome: sticky top nav, collapsible side nav,
// and the auto-generated mobile drawer. Pages keep their own internal padding.
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>
<AppShell
variant="section"
height="fill"
contentPadding={0}
topNav={<Header />}
sideNav={<Sidebar />}
mobileNav={{ breakpoint: 'lg' }}
>
<div className="main-content-area" style={{ minHeight: '100%', boxSizing: 'border-box' }}>
<Outlet />
</div>
<style>{`
.main-content-area {
padding: 24px;
}
@media (max-width: 480px) {
.main-content-area {
padding: 16px;
}
}
`}</style>
</AppShell>
);
}

View File

@@ -1,11 +1,10 @@
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' }}>
<div style={{ minHeight: '100vh', backgroundColor: '#f8fafc' }}>
<Outlet />
</Box>
</div>
);
}