197 lines
6.6 KiB
TypeScript
197 lines
6.6 KiB
TypeScript
import type { ReactNode } from 'react'
|
||
import { useMemo, useState } from 'react'
|
||
import Badge from '@mui/material/Badge'
|
||
import Box from '@mui/material/Box'
|
||
import Divider from '@mui/material/Divider'
|
||
import Drawer from '@mui/material/Drawer'
|
||
import IconButton from '@mui/material/IconButton'
|
||
import List from '@mui/material/List'
|
||
import ListItemButton from '@mui/material/ListItemButton'
|
||
import ListItemIcon from '@mui/material/ListItemIcon'
|
||
import ListItemText from '@mui/material/ListItemText'
|
||
import Stack from '@mui/material/Stack'
|
||
import { useTheme } from '@mui/material/styles'
|
||
import Typography from '@mui/material/Typography'
|
||
import useMediaQuery from '@mui/material/useMediaQuery'
|
||
import { useQuery } from '@tanstack/react-query'
|
||
import { useUnit } from 'effector-react'
|
||
import { Bell, Image, LayoutGrid, ListOrdered, MessageSquare, Store, Users } from 'lucide-react'
|
||
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'
|
||
import { fetchAdminOrdersSummary } from '@/entities/order/api/admin-order-api'
|
||
import { AdminCategoriesPage } from '@/pages/admin-categories'
|
||
import { AdminGalleryPage } from '@/pages/admin-gallery'
|
||
import { AdminOrdersPage } from '@/pages/admin-orders'
|
||
import { AdminProductsPage } from '@/pages/admin-products'
|
||
import { AdminReviewsPage } from '@/pages/admin-reviews'
|
||
import { AdminUsersPage } from '@/pages/admin-users'
|
||
import { $user } from '@/shared/model/auth'
|
||
import { AdminNotificationsPage } from './AdminNotificationsPage'
|
||
|
||
type NavItem = {
|
||
to: string
|
||
label: string
|
||
icon: ReactNode
|
||
}
|
||
|
||
export function AdminLayoutPage() {
|
||
const navigate = useNavigate()
|
||
const location = useLocation()
|
||
const theme = useTheme()
|
||
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
||
const [mobileOpen, setMobileOpen] = useState(false)
|
||
const user = useUnit($user)
|
||
const isAdmin = Boolean(user?.isAdmin)
|
||
|
||
const ordersSummaryQuery = useQuery({
|
||
queryKey: ['admin', 'orders', 'summary'],
|
||
queryFn: fetchAdminOrdersSummary,
|
||
enabled: isAdmin,
|
||
refetchInterval: 45_000,
|
||
refetchOnWindowFocus: true,
|
||
})
|
||
|
||
const newOrdersAttention = ordersSummaryQuery.data?.attentionCount ?? 0
|
||
|
||
const navItems: NavItem[] = useMemo(
|
||
() => [
|
||
{ to: '/admin', label: 'Товары', icon: <Store /> },
|
||
{ to: '/admin/categories', label: 'Категории', icon: <LayoutGrid /> },
|
||
{ to: '/admin/gallery', label: 'Галерея', icon: <Image /> },
|
||
{ to: '/admin/orders', label: 'Заказы', icon: <ListOrdered /> },
|
||
{ to: '/admin/reviews', label: 'Отзывы', icon: <MessageSquare /> },
|
||
{ to: '/admin/users', label: 'Пользователи', icon: <Users /> },
|
||
{ to: '/admin/notifications', label: 'Оповещения', icon: <Bell /> },
|
||
],
|
||
[],
|
||
)
|
||
|
||
if (!isAdmin) {
|
||
return <Navigate to="/auth" replace />
|
||
}
|
||
|
||
const activeTo =
|
||
navItems.find((x) => location.pathname === x.to)?.to ??
|
||
navItems.find((x) => location.pathname.startsWith(`${x.to}/`))?.to ??
|
||
null
|
||
|
||
const nav = (
|
||
<Box sx={{ width: 300, maxWidth: '88vw', py: 1 }}>
|
||
<Box sx={{ px: 2, py: 2, mx: 1, borderRadius: 2, bgcolor: 'warning.50' }}>
|
||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||
Админка
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary">
|
||
Управление магазином
|
||
</Typography>
|
||
</Box>
|
||
<Divider sx={{ my: 1 }} />
|
||
<List disablePadding>
|
||
{navItems.map((i) => (
|
||
<ListItemButton
|
||
key={i.to}
|
||
selected={activeTo === i.to}
|
||
sx={{
|
||
mx: 1,
|
||
mb: 0.5,
|
||
borderRadius: 2,
|
||
'&.Mui-selected': {
|
||
bgcolor: 'warning.100',
|
||
},
|
||
'&.Mui-selected:hover': {
|
||
bgcolor: 'warning.200',
|
||
},
|
||
}}
|
||
onClick={() => {
|
||
navigate(i.to)
|
||
setMobileOpen(false)
|
||
}}
|
||
>
|
||
<ListItemIcon>
|
||
{i.to === '/admin/orders' ? (
|
||
<Badge color="error" badgeContent={newOrdersAttention} max={99} invisible={newOrdersAttention === 0}>
|
||
{i.icon}
|
||
</Badge>
|
||
) : (
|
||
i.icon
|
||
)}
|
||
</ListItemIcon>
|
||
<ListItemText primary={i.label} />
|
||
</ListItemButton>
|
||
))}
|
||
</List>
|
||
</Box>
|
||
)
|
||
|
||
return (
|
||
<Stack direction={{ xs: 'column', md: 'row' }} spacing={3} sx={{ alignItems: 'flex-start' }}>
|
||
{isMobile ? (
|
||
<>
|
||
<Stack direction="row" spacing={1} sx={{ width: '100%', alignItems: 'center' }}>
|
||
<IconButton
|
||
onClick={() => setMobileOpen(true)}
|
||
aria-label="Открыть панель админки"
|
||
sx={{
|
||
borderRadius: 2,
|
||
border: 1,
|
||
borderColor: 'warning.300',
|
||
bgcolor: 'warning.50',
|
||
}}
|
||
>
|
||
<LayoutGrid />
|
||
</IconButton>
|
||
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||
Админка
|
||
</Typography>
|
||
</Stack>
|
||
<Drawer
|
||
open={mobileOpen}
|
||
onClose={() => setMobileOpen(false)}
|
||
anchor="right"
|
||
ModalProps={{ keepMounted: true }}
|
||
slotProps={{
|
||
paper: {
|
||
sx: {
|
||
borderTopLeftRadius: 16,
|
||
borderBottomLeftRadius: 16,
|
||
borderLeft: 1,
|
||
borderColor: 'divider',
|
||
},
|
||
},
|
||
}}
|
||
>
|
||
{nav}
|
||
</Drawer>
|
||
</>
|
||
) : (
|
||
<Box
|
||
sx={{
|
||
position: 'sticky',
|
||
top: 88,
|
||
alignSelf: 'flex-start',
|
||
border: 1,
|
||
borderColor: 'divider',
|
||
borderRadius: 2,
|
||
bgcolor: 'background.paper',
|
||
overflow: 'hidden',
|
||
}}
|
||
>
|
||
{nav}
|
||
</Box>
|
||
)}
|
||
|
||
<Box sx={{ flexGrow: 1, minWidth: 0, width: '100%' }}>
|
||
<Routes>
|
||
<Route index element={<AdminProductsPage />} />
|
||
<Route path="categories" element={<AdminCategoriesPage />} />
|
||
<Route path="gallery" element={<AdminGalleryPage />} />
|
||
<Route path="orders" element={<AdminOrdersPage />} />
|
||
<Route path="reviews" element={<AdminReviewsPage />} />
|
||
<Route path="users" element={<AdminUsersPage />} />
|
||
<Route path="notifications" element={<AdminNotificationsPage />} />
|
||
<Route path="*" element={<Navigate to="/admin" replace />} />
|
||
</Routes>
|
||
</Box>
|
||
</Stack>
|
||
)
|
||
}
|