224 lines
6.9 KiB
TypeScript
224 lines
6.9 KiB
TypeScript
|
|
import { Outlet, useRouterState } from "@tanstack/react-router";
|
|||
|
|
import Drawer from "../components/Drawer/Drawer";
|
|||
|
|
import { useDrawerStore } from "../context/zustand-store/appStore";
|
|||
|
|
import { checkIsMobile } from "../utils/checkIsMobile";
|
|||
|
|
import BottomNavigation from "../screen/BottomNavigation";
|
|||
|
|
import Navbar from "../screen/NavBar";
|
|||
|
|
import Backdrop from "../components/BackDrop/Backdrop";
|
|||
|
|
import Modal from "../components/Modal/Modal";
|
|||
|
|
import {
|
|||
|
|
useUserProfileStore,
|
|||
|
|
useUserStore,
|
|||
|
|
} from "../context/zustand-store/userStore";
|
|||
|
|
import "../App.css";
|
|||
|
|
import { SideBar } from "../screen/SideBar";
|
|||
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|||
|
|
import { useEffect, useRef, useState } from "react";
|
|||
|
|
import { ItemWithSubItems } from "../types/userPermissions";
|
|||
|
|
import { getUserPermissions } from "../utils/getUserAvalableItems";
|
|||
|
|
import { getFaPermissions } from "../utils/getFaPermissions";
|
|||
|
|
import Typography from "../components/Typography/Typography";
|
|||
|
|
import { ArrowLeftIcon } from "@heroicons/react/24/outline";
|
|||
|
|
import Divider from "../components/Divider/Divider";
|
|||
|
|
import { RolesContextMenu } from "../components/Button/RolesContextMenu";
|
|||
|
|
import { useApiRequest } from "../utils/useApiRequest";
|
|||
|
|
|
|||
|
|
const sidebarVariants = {
|
|||
|
|
hidden: { opacity: 0, x: 50 },
|
|||
|
|
visible: {
|
|||
|
|
opacity: 1,
|
|||
|
|
x: 0,
|
|||
|
|
transition: {
|
|||
|
|
type: "spring",
|
|||
|
|
stiffness: 300,
|
|||
|
|
damping: 30,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
exit: {
|
|||
|
|
opacity: 0,
|
|||
|
|
x: 50,
|
|||
|
|
transition: {
|
|||
|
|
type: "tween",
|
|||
|
|
ease: "easeInOut",
|
|||
|
|
duration: 0.25,
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export const RootLayout = () => {
|
|||
|
|
const { drawerState } = useDrawerStore();
|
|||
|
|
const contentRef = useRef<HTMLDivElement>(null);
|
|||
|
|
const { auth } = useUserStore();
|
|||
|
|
const isMobile = checkIsMobile();
|
|||
|
|
const location = useRouterState({ select: (state) => state.location });
|
|||
|
|
const { profile } = useUserProfileStore();
|
|||
|
|
// const [, setIsDark] = useDarkMode();
|
|||
|
|
const [contextMenu, setContextMenu] = useState<{
|
|||
|
|
x: number;
|
|||
|
|
y: number;
|
|||
|
|
} | null>(null);
|
|||
|
|
const [selectedAccess, setSelectedAccess] = useState<string | null>(null);
|
|||
|
|
|
|||
|
|
const menuItems: ItemWithSubItems[] = getUserPermissions(
|
|||
|
|
profile?.permissions
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const currentPath = window.location.pathname;
|
|||
|
|
const matchedItem = menuItems.find((item) =>
|
|||
|
|
item.subItems.some(
|
|||
|
|
(sub) =>
|
|||
|
|
sub.path === currentPath ||
|
|||
|
|
sub.path === "/" + currentPath.split("/")[1] ||
|
|||
|
|
sub.path.includes(currentPath.split("/")[1] + "/")
|
|||
|
|
)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const matchedSubItem = menuItems
|
|||
|
|
.flatMap((item) => item.subItems)
|
|||
|
|
.find(
|
|||
|
|
(sub) =>
|
|||
|
|
sub.path === currentPath ||
|
|||
|
|
sub.path.includes(currentPath.split("/")[1] + "/")
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
const isAdmin = profile?.role?.type?.key === "ADM";
|
|||
|
|
|
|||
|
|
const { data: permissionsData } = useApiRequest({
|
|||
|
|
api: "/auth/api/v1/permission/",
|
|||
|
|
method: "get",
|
|||
|
|
params: { page: 1, page_size: 1000 },
|
|||
|
|
queryKey: ["all-permissions"],
|
|||
|
|
enabled: !!contextMenu && isAdmin,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const handleSitemapContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
|
|||
|
|
if (!isAdmin) return;
|
|||
|
|
const pageKey = matchedSubItem?.name;
|
|||
|
|
if (!pageKey) return;
|
|||
|
|
e.preventDefault();
|
|||
|
|
e.stopPropagation();
|
|||
|
|
setContextMenu({ x: e.clientX, y: e.clientY });
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (contextMenu && permissionsData?.results && matchedSubItem?.name) {
|
|||
|
|
const firstNonModify =
|
|||
|
|
permissionsData.results.find(
|
|||
|
|
(p: any) => p.page === matchedSubItem.name && p.modify_state === false
|
|||
|
|
) || null;
|
|||
|
|
setSelectedAccess(firstNonModify?.name || null);
|
|||
|
|
}
|
|||
|
|
}, [contextMenu, permissionsData, matchedSubItem?.name]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const closeOnClick = () => {
|
|||
|
|
if (contextMenu) setContextMenu(null);
|
|||
|
|
};
|
|||
|
|
if (contextMenu) {
|
|||
|
|
document.addEventListener("click", closeOnClick);
|
|||
|
|
}
|
|||
|
|
return () => {
|
|||
|
|
document.removeEventListener("click", closeOnClick);
|
|||
|
|
};
|
|||
|
|
}, [contextMenu]);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
contentRef.current?.scrollTo(0, 0);
|
|||
|
|
// if (!auth) {
|
|||
|
|
// setIsDark(false);
|
|||
|
|
// }
|
|||
|
|
}, [location.pathname]);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="h-screen flex flex-col relative bg-white dark:bg-dark-900 overflow-hidden">
|
|||
|
|
{auth && <Navbar />}
|
|||
|
|
{drawerState?.isOpen && <Drawer />}
|
|||
|
|
|
|||
|
|
<motion.div
|
|||
|
|
layout
|
|||
|
|
layoutId="root-container"
|
|||
|
|
className={`flex flex-1 overflow-hidden`}
|
|||
|
|
>
|
|||
|
|
{auth && (
|
|||
|
|
<AnimatePresence mode="popLayout">
|
|||
|
|
<motion.div
|
|||
|
|
layout
|
|||
|
|
key="sidebar"
|
|||
|
|
variants={sidebarVariants}
|
|||
|
|
initial="hidden"
|
|||
|
|
animate="visible"
|
|||
|
|
exit="exit"
|
|||
|
|
className={`hidden sm:block`}
|
|||
|
|
style={{ zIndex: 10 }}
|
|||
|
|
>
|
|||
|
|
<SideBar />
|
|||
|
|
</motion.div>
|
|||
|
|
</AnimatePresence>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<motion.div
|
|||
|
|
ref={contentRef}
|
|||
|
|
layout
|
|||
|
|
layoutId="content"
|
|||
|
|
className={`flex-1 overflow-y-auto ${auth && "pt-2 px-2"} ${
|
|||
|
|
auth && isMobile ? "select-none" : "pb-4"
|
|||
|
|
} ${drawerState.isOpen && "blur-[1px] "}`}
|
|||
|
|
key="content"
|
|||
|
|
>
|
|||
|
|
{window.location.pathname !== "/" &&
|
|||
|
|
matchedItem?.fa &&
|
|||
|
|
auth &&
|
|||
|
|
!isMobile && (
|
|||
|
|
<div
|
|||
|
|
className="select-none mb-4 grid"
|
|||
|
|
onContextMenu={handleSitemapContextMenu}
|
|||
|
|
>
|
|||
|
|
<div className="flex justify-between mb-2">
|
|||
|
|
<Typography
|
|||
|
|
variant="caption"
|
|||
|
|
color="text-[#727272] dark:text-dark-100"
|
|||
|
|
>
|
|||
|
|
{matchedItem?.fa} /{" "}
|
|||
|
|
{getFaPermissions(matchedSubItem?.name || "") || "نمایش"}
|
|||
|
|
</Typography>
|
|||
|
|
{location.pathname !== "/" && (
|
|||
|
|
<div
|
|||
|
|
onClick={() => window.history.back()}
|
|||
|
|
className="p-1 rounded-full cursor-pointer w-25"
|
|||
|
|
>
|
|||
|
|
<div className="gap-2 flex items-center justify-center">
|
|||
|
|
<Typography
|
|||
|
|
variant="caption"
|
|||
|
|
color="text-[#727272] dark:text-dark-100"
|
|||
|
|
>
|
|||
|
|
بازگشت
|
|||
|
|
</Typography>
|
|||
|
|
<ArrowLeftIcon className="h-4 w-4 text-[#727272] dark:text-dark-100 " />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<Divider />
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<Outlet />
|
|||
|
|
</motion.div>
|
|||
|
|
</motion.div>
|
|||
|
|
|
|||
|
|
<Backdrop />
|
|||
|
|
<Modal />
|
|||
|
|
{isMobile && auth && <BottomNavigation />}
|
|||
|
|
{contextMenu && isAdmin && matchedSubItem?.name && selectedAccess && (
|
|||
|
|
<RolesContextMenu
|
|||
|
|
page={matchedSubItem.name}
|
|||
|
|
access={selectedAccess}
|
|||
|
|
position={contextMenu}
|
|||
|
|
onClose={() => setContextMenu(null)}
|
|||
|
|
isPage={true}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|