feat: Добавлена админ-панель и улучшена навигация.

This commit is contained in:
Андрей Дувакин 2025-06-02 21:18:56 +05:00
parent e8b71a0c37
commit 49e4e2f3f1
8 changed files with 124 additions and 42 deletions

View File

@ -0,0 +1,14 @@
import { Navigate, Outlet } from "react-router-dom";
import { useSelector } from "react-redux";
const AdminRoute = () => {
const { user } = useSelector((state) => state.auth);
if (!user || user.role.title !== "Администратор") {
return <Navigate to="/" />;
}
return <Outlet />;
};
export default AdminRoute;

View File

@ -8,6 +8,7 @@ import LensesSetsPage from "../Components/Pages/LensesSetsPage/LensesSetsPage.js
import IssuesPage from "../Components/Pages/IssuesPage/IssuesPage.jsx"; import IssuesPage from "../Components/Pages/IssuesPage/IssuesPage.jsx";
import AppointmentsPage from "../Components/Pages/AppointmentsPage/AppointmentsPage.jsx"; import AppointmentsPage from "../Components/Pages/AppointmentsPage/AppointmentsPage.jsx";
import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx"; import ProfilePage from "../Components/Pages/ProfilePage/ProfilePage.jsx";
import AdminRoute from "./AdminRoute.jsx";
const AppRouter = () => ( const AppRouter = () => (
@ -24,6 +25,13 @@ const AppRouter = () => (
<Route path={"/"} element={<HomePage/>}/> <Route path={"/"} element={<HomePage/>}/>
</Route> </Route>
</Route> </Route>
<Route element={<AdminRoute />}>
<Route element={<MainLayout />}>
<Route path="/admin" element={<AdminPage />} />
</Route>
</Route>
<Route path={"*"} element={<Navigate to={"/"}/>}/> <Route path={"*"} element={<Navigate to={"/"}/>}/>
</Routes> </Routes>
); );

View File

@ -1,4 +1,5 @@
import { Component } from "react"; import { Component } from "react";
import {Result} from "antd";
class ErrorBoundary extends Component { class ErrorBoundary extends Component {
state = { hasError: false }; state = { hasError: false };
@ -13,7 +14,7 @@ class ErrorBoundary extends Component {
render() { render() {
if (this.state.hasError) { if (this.state.hasError) {
return <div>Произошла ошибка</div>; return <Result status="500" title="500" subTitle="Something went wrong."/>;
} }
return this.props.children; return this.props.children;
} }

View File

@ -1,6 +1,5 @@
import {useState} from "react"; import {Alert, Layout, Menu} from "antd";
import {Grid, Layout, Menu} from "antd"; import {Outlet} from "react-router-dom";
import {Outlet, useLocation, useNavigate} from "react-router-dom";
import { import {
HomeOutlined, HomeOutlined,
CalendarOutlined, CalendarOutlined,
@ -9,58 +8,56 @@ import {
UserOutlined, UserOutlined,
TeamOutlined, TeamOutlined,
LogoutOutlined, LogoutOutlined,
MessageOutlined MessageOutlined, ControlOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import useAuthUtils from "../../Hooks/useAuthUtils.js"; import useMainLayout from "./useMainLayout.js";
import useMainLayoutUI from "./useMainLayoutUI.js";
import LoadingIndicator from "../Widgets/LoadingIndicator/LoadingIndicator.jsx";
const {Content, Footer, Sider} = Layout; const {Content, Footer, Sider} = Layout;
const getItem = (label, key, icon, children) => ({key, icon, children, label});
const {useBreakpoint} = Grid;
const MainLayout = () => { const MainLayout = () => {
const screens = useBreakpoint(); const mainLayoutData = useMainLayout();
const mainLayoutUI = useMainLayoutUI(mainLayoutData.user);
const [collapsed, setCollapsed] = useState(true);
const navigate = useNavigate();
const location = useLocation();
const {logoutAndRedirect} = useAuthUtils();
const menuItems = [ const menuItems = [
getItem("Главная", "/", <HomeOutlined/>), mainLayoutUI.getItem("Главная", "/", <HomeOutlined />),
getItem("Приёмы", "/appointments", <CalendarOutlined/>), mainLayoutUI.getItem("Приёмы", "/appointments", <CalendarOutlined />),
getItem("Выдачи линз", "/issues", <DatabaseOutlined/>), mainLayoutUI.getItem("Выдачи линз", "/issues", <DatabaseOutlined />),
getItem("Линзы и наборы", "/Lenses", <FolderViewOutlined/>), mainLayoutUI.getItem("Линзы и наборы", "/Lenses", <FolderViewOutlined />),
getItem("Пациенты", "/Patients", <TeamOutlined/>), mainLayoutUI.getItem("Пациенты", "/Patients", <TeamOutlined />),
getItem("Рассылки", "/mailing", <MessageOutlined/>), mainLayoutUI.getItem("Рассылки", "/mailing", <MessageOutlined />),
{type: "divider"}, { type: "divider" }
getItem("Мой профиль", "profile", <UserOutlined/>, [
getItem("Перейти в профиль", "/profile", <UserOutlined/>),
getItem("Выйти", "logout", <LogoutOutlined/>)
])
]; ];
const handleMenuClick = ({key}) => { if (mainLayoutData.user?.role.title === "Администратор") {
if (key === "logout") { menuItems.push(mainLayoutUI.getItem("Панель администратора", "/admin", <ControlOutlined />));
logoutAndRedirect(); }
return;
} menuItems.push(
navigate(key); mainLayoutUI.getItem("Мой профиль", "profile", <UserOutlined />, [
}; mainLayoutUI.getItem("Перейти в профиль", "/profile", <UserOutlined />),
mainLayoutUI.getItem("Выйти", "logout", <LogoutOutlined />)
])
);
if (mainLayoutData.isUserError) {
return <Alert message="Произошла ошибка" type="error" showIcon/>
}
return ( return (
<Layout style={{minHeight: "100vh"}}> <Layout style={{minHeight: "100vh"}}>
<Sider <Sider
collapsible={!screens.xs} collapsible={!mainLayoutUI.screens.xs}
collapsed={collapsed} collapsed={mainLayoutUI.collapsed}
onCollapse={setCollapsed} onCollapse={mainLayoutUI.setCollapsed}
style={{height: "100vh", position: "fixed", left: 0}} style={{height: "100vh", position: "fixed", left: 0}}
> >
<div style={{display: "flex", justifyContent: "center", padding: 16}}> <div style={{display: "flex", justifyContent: "center", padding: 16}}>
<img <img
src="/logo_rounded.png" src="/logo_rounded.png"
alt="Логотип" alt="Логотип"
style={{width: collapsed ? 40 : 80, transition: "width 0.2s"}} style={{width: mainLayoutUI.collapsed ? 40 : 80, transition: "width 0.2s"}}
/> />
</div> </div>
<Menu <Menu
@ -68,12 +65,12 @@ const MainLayout = () => {
selectedKeys={[location.pathname]} selectedKeys={[location.pathname]}
mode="inline" mode="inline"
items={menuItems} items={menuItems}
onClick={handleMenuClick} onClick={mainLayoutUI.handleMenuClick}
/> />
</Sider> </Sider>
<Layout <Layout
style={{marginLeft: collapsed ? 80 : 200, transition: "margin-left 0.2s"}} style={{marginLeft: mainLayoutUI.collapsed ? 80 : 200, transition: "margin-left 0.2s"}}
> >
<Content style={{ <Content style={{
margin: "0 16px", margin: "0 16px",
@ -84,9 +81,13 @@ const MainLayout = () => {
borderRadius: 8, borderRadius: 8,
marginTop: "15px" marginTop: "15px"
}}> }}>
<Outlet/> {mainLayoutData.isUserLoading ? (
<LoadingIndicator/>
) : (
<Outlet/>
)}
</Content> </Content>
<Footer style={{textAlign: "center"}}>Линза+ © {new Date().getFullYear()}</Footer> <Footer style={{textAlign: "center"}}>Visus+ © {new Date().getFullYear()}</Footer>
</Layout> </Layout>
</Layout> </Layout>
); );

View File

@ -0,0 +1,20 @@
import {useGetAuthenticatedUserDataQuery} from "../../Api/usersApi.js";
const useMainLayout = () => {
const {
data: user,
isLoading: isUserLoading,
isError: isUserError,
} = useGetAuthenticatedUserDataQuery(undefined, {
pollingInterval: 20000,
});
return {
user,
isUserLoading,
isUserError,
};
};
export default useMainLayout;

View File

@ -0,0 +1,30 @@
import {useState} from "react";
import {Grid} from "antd";
import {useLocation, useNavigate} from "react-router-dom";
import useAuthUtils from "../../Hooks/useAuthUtils.js";
const {useBreakpoint} = Grid;
const useMainLayoutUI = () => {
const screens = useBreakpoint();
const [collapsed, setCollapsed] = useState(true);
const navigate = useNavigate();
const location = useLocation();
const {logoutAndRedirect} = useAuthUtils();
const handleMenuClick = ({key}) => {
if (key === "logout") {
logoutAndRedirect();
return;
}
navigate(key);
};
const getItem = (label, key, icon, children) => ({key, icon, children, label});
return {screens, collapsed, setCollapsed, location, handleMenuClick, getItem};
};
export default useMainLayoutUI;

View File

@ -9,6 +9,7 @@ import {
setSelectedScheduledAppointment, setSelectedScheduledAppointment,
setSelectedDate, setSelectedDate,
} from "../../../../../Redux/Slices/appointmentsSlice.js"; } from "../../../../../Redux/Slices/appointmentsSlice.js";
import {useEffect, useState} from "react";
dayjs.extend(utc); dayjs.extend(utc);
dayjs.extend(timezone); dayjs.extend(timezone);
@ -21,7 +22,14 @@ const useAppointmentCalendarUI = (appointments, scheduledAppointments) => {
const selectedDate = dayjs.tz(useSelector((state) => state.appointmentsUI.selectedDate), "Europe/Moscow"); const selectedDate = dayjs.tz(useSelector((state) => state.appointmentsUI.selectedDate), "Europe/Moscow");
const screens = useBreakpoint(); const screens = useBreakpoint();
const fullScreenCalendar = !screens.xs; const [fullScreenCalendar, setFullScreenCalendar] = useState(true);
useEffect(() => {
const isMobile = window.innerWidth < 576;
setFullScreenCalendar(!isMobile);
setFullScreenCalendar(!screens.xs);
}, [screens.xs]);
const calendarContainerStyle = { padding: 20 }; const calendarContainerStyle = { padding: 20 };