feat: Добавлена админ-панель и улучшена навигация.
This commit is contained in:
parent
e8b71a0c37
commit
49e4e2f3f1
14
web-app/src/App/AdminRoute.jsx
Normal file
14
web-app/src/App/AdminRoute.jsx
Normal 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;
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
20
web-app/src/Components/Layouts/useMainLayout.js
Normal file
20
web-app/src/Components/Layouts/useMainLayout.js
Normal 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;
|
||||||
30
web-app/src/Components/Layouts/useMainLayoutUI.js
Normal file
30
web-app/src/Components/Layouts/useMainLayoutUI.js
Normal 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;
|
||||||
@ -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 };
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user