first commit

This commit is contained in:
2026-01-19 13:08:58 +03:30
commit 850b4a3f1e
293 changed files with 51775 additions and 0 deletions

223
src/routes/RouteLayout.tsx Normal file
View File

@@ -0,0 +1,223 @@
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>
);
};

54
src/routes/paths.ts Normal file
View File

@@ -0,0 +1,54 @@
//General
export const HOME = "/";
export const ABOUT = "/about";
export const PROFILE = "/profile";
export const MENU = "/menu";
export const TRAINING = "/training";
//Management
export const PERMISSION_ACCESS = "/permission-access";
export const USERS = "/users";
export const ORGANIZATIONS = "/organizations";
export const ROLES = "/roles";
export const PRODUCT_CATEGORIES = "/product-categories";
export const PRODUCT_PRICING = "/pricing";
export const FEED_INPUT_PRODUCTS = "/feed-input-products";
//Wages
export const QUOTA_INCENTIVE_PLANS = "/incentive-plans";
export const QUOTAS = "/quota";
export const QUOTA_DISTRIBUTION = "/quota/$code";
//Inventory
export const INVENTORY = "/inventory";
export const REPORTING = "/reporting";
export const REPORTING_DETAIL = "/reporting/$type/$itemid/$product";
export const INVENTORY_ENTRIES = "/inventory/$code";
//Livestock
export const LIVESTOCK_FARMERS = "/farmers";
export const LIVESTOCK_FARMERS_INCENTIVE_PLANS = "/farmers/plans/$farmid/$name";
export const LIVESTOCK_FARMER_DETAIL = "/farmers/$farmid/$name";
export const HERDS = "/herds";
export const LIVESTOCKS = "/livestocks";
export const LIVESTOCKS_HERDS_DETAIL = "/livestocks/$herdid/$name";
//POS
export const POS_POS_LIST = "/pos";
export const POS_COMPANIES = "/pos-companies";
export const POS_COMPANY_DETAIL = "/pos-companies/$id/$name";
export const POS_COMPANY_ACCOUNTS = "/pos/$id";
//TRANSACTIONS
export const TRANSACTIONS = "/transactions";
//UNITS
export const UNION_LIST = "/unions";
export const COOPERATIVE_LIST = "/cooperatives";
export const COOPERATIVE_RANCHERS_LIST = "/cooperatives/ranchers/$id/$name";
export const UNION_COOPERATIVE_LIST = "/cooperatives/$id/$name";
export const UNITS_SETTINGS = "/unit-settings";
//TAGGING
export const TAGGING = "/tagging";

View File

@@ -0,0 +1,45 @@
import { Auth } from "../Pages/Auth";
import { Menu } from "../Pages/Menu";
import { checkIsMobile } from "../utils/checkIsMobile";
import * as R from "./paths";
import Dashboard from "../Pages/Dashboard";
import { ItemWithSubItems } from "../types/userPermissions";
import UserProfile from "../Pages/UserProfile";
import Training from "../Pages/Training";
interface Route {
path: string;
component: React.ComponentType | any;
}
export const getRoutes = (
auth: string | null,
roles: ItemWithSubItems[]
): Route[] => {
let generalRoutes: Route[] = auth
? [
{ path: R.HOME, component: Dashboard },
{ path: R.PROFILE, component: UserProfile },
{ path: R.TRAINING, component: Training },
]
: [{ path: R.HOME, component: Auth }];
if (checkIsMobile()) {
generalRoutes.push({ path: R.MENU, component: Menu });
}
let availableRoutes: Route[] = [];
const routableItems = roles?.flatMap((role) =>
role?.subItems.map((item) => ({
path: item.path,
component: item.component,
}))
);
if (roles) {
availableRoutes = [...availableRoutes, ...routableItems];
}
return [...generalRoutes, ...availableRoutes];
};

41
src/routes/routes.ts Normal file
View File

@@ -0,0 +1,41 @@
import {
createRouter,
RootRoute,
Route,
AnyRoute,
} from "@tanstack/react-router";
import { RootLayout } from "./RouteLayout";
import { getRoutes } from "./routeConfigs";
import NotFound from "../Pages/NotFound";
import { Auth } from "../Pages/Auth";
import { ItemWithSubItems } from "../types/userPermissions";
export const rootRoute = new RootRoute({
component: RootLayout,
});
export function makeRouter(auth: string | null, roles: ItemWithSubItems[]) {
const routeConfigs = getRoutes(auth, roles);
const childRoutes: AnyRoute[] = routeConfigs.map(
({ path, component }) =>
new Route({
getParentRoute: () => rootRoute,
path,
component,
}) as AnyRoute
);
const notFoundRoute = new Route({
getParentRoute: () => rootRoute,
path: "*",
component: auth ? NotFound : Auth,
});
const routeTree = rootRoute.addChildren([...childRoutes, notFoundRoute]);
return createRouter({
routeTree,
defaultPreload: "intent",
});
}