diff --git a/.env.local.example b/.env.local.example index e69de29..c93a078 100644 --- a/.env.local.example +++ b/.env.local.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_VERSION="0.2.0" +NEXT_PUBLIC_API_URL="https://rms.witel.ir" \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index bffb357..121cefd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,3 @@ { - "extends": "next/core-web-vitals" + "extends": ["next/core-web-vitals","prettier"] } diff --git a/.prettierrc b/.prettierrc index fe94791..7eeebb6 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,12 +1,12 @@ { - "printWidth": 120, - "tabWidth": 4, - "useTabs": false, - "semi": true, - "singleQuote": false, - "trailingComma": "es5", - "bracketSpacing": true, - "jsxBracketSameLine": false, - "arrowParens": "always", - "endOfLine": "crlf" + "printWidth": 120, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "arrowParens": "always", + "endOfLine": "crlf" } \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index bb49d74..72cfa4a 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -3,6 +3,9 @@ const isProd = process.env.NODE_ENV === 'production' /** @type {import('next').NextConfig} */ const nextConfig = { assetPrefix: isProd ? '/v3' : undefined, + images: { + path: isProd ? '/v3/_next/image' : undefined, + } }; export default nextConfig; diff --git a/package.json b/package.json index 7662699..7bec569 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "build": "next build", "start": "next start", "lint": "next lint", - "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,html,css,scss}\"" + "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,html,css,scss}\"", + "build:publish": "npm i && npm run build && pm2 restart ecosystem.config.js" }, "dependencies": { "@emotion/cache": "^11.11.0", @@ -17,6 +18,7 @@ "@mui/material": "^5.15.6", "@mui/material-nextjs": "^5.15.6", "@mui/x-date-pickers": "^6.19.5", + "axios": "^1.7.2", "date-fns": "^3.3.1", "date-fns-jalali": "^2.13.0-0", "dayjs": "^1.11.10", @@ -36,6 +38,7 @@ "devDependencies": { "eslint": "^8", "eslint-config-next": "14.1.0", + "eslint-config-prettier": "^9.1.0", "prettier": "^3.3.2" } } diff --git a/public/global.scss b/public/global.scss index e3d2485..d1d10e3 100644 --- a/public/global.scss +++ b/public/global.scss @@ -1,6 +1,8 @@ @import "fontiran"; +@import "react-toastify/dist/ReactToastify.css"; .filter-toast { - box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px; + box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, + rgba(0, 0, 0, 0.3) 0px 8px 16px -8px; background-color: #d0dfe8; -} \ No newline at end of file +} diff --git a/public/images/headerLogo.png b/public/images/headerLogo.png new file mode 100644 index 0000000..86dbfcd Binary files /dev/null and b/public/images/headerLogo.png differ diff --git a/public/images/rmsLogo.png b/public/images/rmsLogo.png new file mode 100644 index 0000000..8340d5a Binary files /dev/null and b/public/images/rmsLogo.png differ diff --git a/src/app/(withAuth)/dashboard/inquiry_privacy/fencing/page.js b/src/app/(withAuth)/dashboard/inquiry_privacy/fencing/page.js new file mode 100644 index 0000000..0a1b9ec --- /dev/null +++ b/src/app/(withAuth)/dashboard/inquiry_privacy/fencing/page.js @@ -0,0 +1,10 @@ +import InquiryPrivacyFencingPage from "@/components/dashboard/inquiryPrivacyFencing"; + +const Page = () => { + + return ( + + ); +}; + +export default Page; \ No newline at end of file diff --git a/src/app/(withAuth)/dashboard/page.js b/src/app/(withAuth)/dashboard/page.js index 25989ef..c43c1ae 100644 --- a/src/app/(withAuth)/dashboard/page.js +++ b/src/app/(withAuth)/dashboard/page.js @@ -1,5 +1,9 @@ const Page = () => { - return
dashboard
; + + return ( + <> + + ); }; export default Page; diff --git a/src/app/(withAuth)/dashboard/template.js b/src/app/(withAuth)/dashboard/template.js deleted file mode 100644 index 28b4ea0..0000000 --- a/src/app/(withAuth)/dashboard/template.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Box, Stack } from "@mui/material"; -import HeaderWithLogo from "@/components/layouts/dashboard/headerWithLogo"; -import HeaderWithSidebar from "@/components/layouts/dashboard/headerWithSidebar"; -import "react-toastify/dist/ReactToastify.css"; - -const Template = ({ children }) => { - return ( - - - - {children} - - - ); -}; - -export default Template; diff --git a/src/app/(withAuth)/layout.js b/src/app/(withAuth)/layout.js new file mode 100644 index 0000000..400bbc9 --- /dev/null +++ b/src/app/(withAuth)/layout.js @@ -0,0 +1,12 @@ +import WithAuthMiddleware from "@/core/middlewares/withAuth"; +import { AuthProvider } from "@/lib/contexts/auth"; + +const Layout = ({ children }) => { + return ( + + {children} + + ); +}; + +export default Layout; diff --git a/src/app/default.js b/src/app/default.js new file mode 100644 index 0000000..d157a71 --- /dev/null +++ b/src/app/default.js @@ -0,0 +1,3 @@ +export default function Default() { + return null; +} diff --git a/src/app/lfwd/page.js b/src/app/lfwd/page.js new file mode 100644 index 0000000..b3456a3 --- /dev/null +++ b/src/app/lfwd/page.js @@ -0,0 +1,57 @@ +"use client"; + +import { GET_LOGIN_ROUTE } from "@/core/utils/routes"; +import useRequest from "@/lib/hooks/useRequest"; +import { Button, Stack, Typography, Zoom } from "@mui/material"; +import Link from "next/link"; +import { useEffect, useState } from "react"; + +const Page = ({ searchParams }) => { + const { username } = searchParams; + const [login, setLogin] = useState(0); + const request = useRequest(); + + useEffect(() => { + const login = async () => { + try { + await request(`${GET_LOGIN_ROUTE}?username=${username}`); + setLogin(1); + } catch (error) { + setLogin(2); + } + }; + + login(); + }, []); + + const retry = async () => { + setLogin(0); + try { + await request(`${GET_LOGIN_ROUTE}?username=${username}`); + setLogin(1); + } catch (error) { + setLogin(2); + } + }; + + return ( + + + {login === 0 + ? "درحال دریافت مجوز برای ارتباط با سرور..." + : login === 1 + ? "ارتباط با سرور برقرار شد." + : "ارتباط با سرور برقرار نشد. مشکلی وجود دارد."} + + + + + + + + + ); +}; +export default Page; diff --git a/src/app/page.js b/src/app/page.js index e4c74e8..2997c35 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,8 +1,8 @@ -"use client"; -import HomeComponent from "@/components/home"; +import { redirect } from "next/navigation"; function Page() { - return ; + redirect("/dashboard", "replace"); + return null; } export default Page; diff --git a/src/app/template.js b/src/app/template.js index 9cf7484..3ffc656 100644 --- a/src/app/template.js +++ b/src/app/template.js @@ -2,7 +2,6 @@ import cacheProviderRtl from "@/core/utils/cacheRtl"; import { CssBaseline, ThemeProvider } from "@mui/material"; import theme from "@/core/utils/theme"; import { AppRouterCacheProvider } from "@mui/material-nextjs/v14-appRouter"; -import "react-toastify/dist/ReactToastify.css"; import "^/global.scss"; import { ToastContainer } from "react-toastify"; @@ -12,7 +11,8 @@ const Template = ({ children }) => { {children} - + + ); diff --git a/src/components/dashboard/inquiryPrivacyFencing/index.jsx b/src/components/dashboard/inquiryPrivacyFencing/index.jsx new file mode 100644 index 0000000..539885d --- /dev/null +++ b/src/components/dashboard/inquiryPrivacyFencing/index.jsx @@ -0,0 +1,4 @@ +const InquiryPrivacyFencingPage = () => { + return <> +} +export default InquiryPrivacyFencingPage \ No newline at end of file diff --git a/src/components/layouts/dashboard/headerWithLogo/HaederBottom/index.jsx b/src/components/layouts/dashboard/headerWithLogo/HaederBottom/index.jsx new file mode 100644 index 0000000..4f0b159 --- /dev/null +++ b/src/components/layouts/dashboard/headerWithLogo/HaederBottom/index.jsx @@ -0,0 +1,27 @@ +"use client"; +import { Stack, Typography } from "@mui/material"; +import Image from "next/image"; + +const HeaderBottom = () => { + return ( + theme.palette.primary2.main, + }} + spacing={4} + alignItems={"center"} + direction={"row"} + > + + + theme.palette.primary2.contrastText }}> + سامانه جامع راهداری + + theme.palette.primary2.contrastText }}>rms.rmto.ir + + + ); +}; +export default HeaderBottom; diff --git a/src/components/layouts/dashboard/headerWithLogo/HaederTop/index.jsx b/src/components/layouts/dashboard/headerWithLogo/HaederTop/index.jsx new file mode 100644 index 0000000..6899881 --- /dev/null +++ b/src/components/layouts/dashboard/headerWithLogo/HaederTop/index.jsx @@ -0,0 +1,11 @@ +import { Stack } from "@mui/material"; +import Image from "next/image"; + +const HeaderTop = () => { + return ( + + + + ); +}; +export default HeaderTop; diff --git a/src/components/layouts/dashboard/headerWithLogo/index.jsx b/src/components/layouts/dashboard/headerWithLogo/index.jsx index ed083a4..16ef4b2 100644 --- a/src/components/layouts/dashboard/headerWithLogo/index.jsx +++ b/src/components/layouts/dashboard/headerWithLogo/index.jsx @@ -1,10 +1,12 @@ import { Box, Stack } from "@mui/material"; +import HeaderTop from "./HaederTop"; +import HeaderBottom from "./HaederBottom"; const HeaderWithLogo = () => { return ( - header1 - header2 + + ); }; diff --git a/src/components/layouts/dashboard/headerWithSidebar/HeaderMenu/index.jsx b/src/components/layouts/dashboard/headerWithSidebar/HeaderMenu/index.jsx new file mode 100644 index 0000000..c705242 --- /dev/null +++ b/src/components/layouts/dashboard/headerWithSidebar/HeaderMenu/index.jsx @@ -0,0 +1,4 @@ +const HeaderMenu = () => { + return <>منو; +}; +export default HeaderMenu; diff --git a/src/components/layouts/dashboard/headerWithSidebar/index.jsx b/src/components/layouts/dashboard/headerWithSidebar/index.jsx index 1774a85..c8febb6 100644 --- a/src/components/layouts/dashboard/headerWithSidebar/index.jsx +++ b/src/components/layouts/dashboard/headerWithSidebar/index.jsx @@ -1,12 +1,13 @@ "use client"; -import { useState } from "react"; -import { Box, Drawer, IconButton, Paper, styled, Toolbar, Typography } from "@mui/material"; -import MuiAppBar from "@mui/material/AppBar"; -import MenuIcon from "@mui/icons-material/Menu"; -import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import SidebarMenu from "@/core/components/SidebarMenu"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; +import MenuIcon from "@mui/icons-material/Menu"; +import { Box, Drawer, IconButton, Paper, styled, Toolbar } from "@mui/material"; +import MuiAppBar from "@mui/material/AppBar"; +import { useState } from "react"; +import HeaderMenu from "./HeaderMenu"; -const drawerWidth = 260; +const drawerWidth = 300; const Main = styled(Paper, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({ flexGrow: 1, @@ -44,25 +45,24 @@ const AppBar = styled(MuiAppBar, { const DrawerHeader = styled("div")(({ theme }) => ({ display: "flex", alignItems: "center", - padding: theme.spacing(0, 1), // necessary for content to be below app bar + padding: theme.spacing(0, 1), ...theme.mixins.toolbar, justifyContent: "flex-end", })); const HeaderWithSidebar = ({ children }) => { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(true); const handleDrawerOpen = () => { setOpen(true); }; - const handleDrawerClose = () => { setOpen(false); }; return ( - + { > - - menu - + + + { return ( <> - + { dispatch({ type: "COLLAPSE_MENU", id: menuItem.id }); }} @@ -25,21 +24,28 @@ const SidebarListItems = ({ menuItem, dispatch }) => { color: "primary.main", width: 40, height: 24, - pr: 2, }} > {menuItem.icon} - + {menuItem.hasSubitems ? menuItem.showSubitems ? : : null} - {menuItem.hasSubitems - ? menuItem.Subitems.map((subitem, index) => { - return ; - }) - : null} + {menuItem.hasSubitems ? ( + + {menuItem.Subitems.map((subitem, index) => { + return ; + })} + + ) : null} ); diff --git a/src/core/components/SidebarMenu.jsx b/src/core/components/SidebarMenu.jsx index 55cc9b9..30a9297 100644 --- a/src/core/components/SidebarMenu.jsx +++ b/src/core/components/SidebarMenu.jsx @@ -1,221 +1,86 @@ "use client"; -import { List } from "@mui/material"; import SidebarListItems from "@/core/components/SidebarListItems"; -import { useEffect, useReducer, useState } from "react"; +import { usePermissions } from "@/lib/hooks/usePermissions"; +import { List } from "@mui/material"; import { usePathname } from "next/navigation"; -import SecurityIcon from "@mui/icons-material/Security"; -import SpaceDashboardIcon from "@mui/icons-material/SpaceDashboard"; -import AssessmentIcon from "@mui/icons-material/Assessment"; -import AirlineSeatReclineNormalIcon from "@mui/icons-material/AirlineSeatReclineNormal"; -import RepartitionIcon from "@mui/icons-material/Repartition"; +import { useEffect, useReducer, useState } from "react"; +import { filterMenuItems } from "../utils/filterMenuItems"; +import { pageMenu } from "../utils/pageMenu"; -const data = [ - { - id: "hello", - type: "menu", - route: "/hello", - icon: , - selected: false, - Subitems: [ - { - id: "amin", - route: "/s", - icon: , - firstName: "amin", - lastName: "ali", - Subitems: [ - { - id: "shahrokh", - shahrokh: "shahrokh", - type: "page", - selected: false, - route: "/dashbssoard", - icon: , - }, - { - id: "shasdasdsahrokh", - shahrokh: "shahroasdasdkh", - type: "page", - selected: false, - route: "/dashboardsd", - icon: , - }, - ], - hasSubitems: true, - showSubitems: false, - type: "menu", - selected: false, - }, - { - id: "amir", - route: "/dashboarsad", - icon: , - firstName: "amir", - lastName: "akbar", - hasSubitems: false, - showSubitems: false, - selected: false, - type: "page", - }, - ], - hasSubitems: true, - showSubitems: false, - }, - { - id: "hi", - type: "menu", - icon: , - route: "/hi", - selected: false, - Subitems: [ - { - id: "ali", - route: "/dashboardss", - firstName: "ali", - lastName: "ali", - icon: , - hasSubitems: false, - showSubitems: false, - type: "page", - selected: false, - }, - { - id: "akbar", - route: "/akbar", - firstName: "akbar", - icon: , - lastName: "akbar", - hasSubitems: false, - showSubitems: false, - type: "page", - selected: false, - }, - ], - hasSubitems: true, - showSubitems: false, - }, - { - id: "hoo", - type: "page", - route: "/dashboard", - icon: , - selected: false, - hasSubitems: false, - showSubitems: false, - }, -]; +function selectPage(item, route) { + if (item.type === "page") { + return { + ...item, + selected: item.route === route, + showSubitems: item.route === route, + }; + } else if (item.Subitems && Array.isArray(item.Subitems)) { + const updatedSubitems = item.Subitems.map(subitem => selectPage(subitem, route)); + return { + ...item, + Subitems: updatedSubitems, + showSubitems: updatedSubitems.some(subitem => subitem.showSubitems || subitem.route === route), + }; + } + return item; +} function reducer(state, action) { switch (action.type) { + case "UPDATE_MENU": + const _permissions = action.permissions || [] + const filteredPageMenu = filterMenuItems(state, ['all', ..._permissions]) + return filteredPageMenu case "COLLAPSE_MENU": return state.map((item) => action.id == item.id ? { - ...item, - showSubitems: !item.showSubitems, - } + ...item, + showSubitems: !item.showSubitems, + } : item ); case "COLLAPSE_SUB_ITEMS": return state.map((item) => { return item.hasSubitems ? { - ...item, - Subitems: item.Subitems.map((subitem) => - action.id === subitem.id - ? { - ...subitem, - showSubitems: !subitem.showSubitems, - } - : subitem - ), - } + ...item, + Subitems: item.Subitems.map((subitem) => + action.id === subitem.id + ? { + ...subitem, + showSubitems: !subitem.showSubitems, + } + : subitem + ), + } : item; }); case "COLLAPSE_SUB_SECOND_ITEMS": return state.map((item) => { return item.hasSubitems ? { - ...item, - Subitems: item.Subitems.map((subitem) => { - return subitem.hasSubitems - ? { - ...subitem, - Subitems: subitem.Subitems.map((secondSubitem) => - action.id === secondSubitem.id - ? { - ...secondSubitem, - showSubitems: !secondSubitem.showSubitems, - } - : secondSubitem - ), - } - : subitem; - }), - } + ...item, + Subitems: item.Subitems.map((subitem) => { + return subitem.hasSubitems + ? { + ...subitem, + Subitems: subitem.Subitems.map((secondSubitem) => + action.id === secondSubitem.id + ? { + ...secondSubitem, + showSubitems: !secondSubitem.showSubitems, + } + : secondSubitem + ), + } + : subitem; + }), + } : item; }); case "SELECTED": - return state.map((item) => { - return item.type === "page" - ? { - ...item, - selected: action.route === item.route, - showSubitems: item.route === action.route, - } - : item.Subitems && Array.isArray(item.Subitems) - ? { - ...item, - Subitems: item.Subitems.map((subitem) => { - return subitem.type === "page" - ? { - ...subitem, - selected: action.route === subitem.route, - showSubitems: subitem.route === action.route, - } - : subitem.Subitems && Array.isArray(subitem.Subitems) - ? { - ...subitem, - Subitems: subitem.Subitems.map((secondSubItem) => ({ - ...secondSubItem, - selected: secondSubItem.route === action.route, - })), - showSubitems: subitem.Subitems.some( - (secondSubItem) => secondSubItem.route === action.route - ), - } - : subitem; - }), - showSubitems: item.Subitems.some((subitem) => subitem.showSubitems), - } - : item; - }); - // case "SELECTED": - // return state.map((item) => { - // return item.type === "page" ? { - // ...item, - // selected: action.route === item.route - // } : item.Subitems && Array.isArray(item.Subitems) ? { - // ...item, - // Subitems: item.Subitems.map((subitem) => { - // const updatedSubitem = subitem.type === "page" ? { - // ...subitem, - // selected: action.route === subitem.route - // } : subitem.Subitems && Array.isArray(subitem.Subitems) ? { - // ...subitem, - // Subitems: subitem.Subitems.map((secondSubItem) => ({ - // ...secondSubItem, - // selected: secondSubItem.route === action.route - // })), - // showSubitems: subitem.Subitems.some((secondSubItem) => secondSubItem.route === action.route), - // } : subitem; - // - // console.log("Updated subitem:", updatedSubitem); // Log the updated subitem - // return updatedSubitem; - // }), - // showSubitems: item.Subitems.some((subitem) => subitem.showSubitems), - // } : item - // }); + return state.map(item => selectPage(item, action.route)); default: throw new Error(); @@ -223,13 +88,21 @@ function reducer(state, action) { } const SidebarMenu = () => { - const [menuItems, dispatch] = useReducer(reducer, data); + const { data: userPermissions } = usePermissions() + const [menuItems, dispatch] = useReducer(reducer, pageMenu); const pathname = usePathname(); const [selectedKey, setSelectedKey] = useState(null); + useEffect(() => { dispatch({ type: "SELECTED", route: pathname }); setSelectedKey(pathname); - }, [pathname]); + }, [userPermissions, pathname]); + + useEffect(() => { + if (!userPermissions) return + dispatch({ type: 'UPDATE_MENU', permissions: userPermissions }); + }, [userPermissions]) + useEffect(() => { selectedKey && @@ -240,11 +113,13 @@ const SidebarMenu = () => { }, [selectedKey]); return ( - - {menuItems.map((menuItem) => { - return ; - })} - + userPermissions && ( + + {menuItems.map((menuItem) => { + return ; + })} + + ) ); }; export default SidebarMenu; diff --git a/src/core/components/SidebarSubitems.jsx b/src/core/components/SidebarSubitems.jsx index 18c1016..69418ea 100644 --- a/src/core/components/SidebarSubitems.jsx +++ b/src/core/components/SidebarSubitems.jsx @@ -1,82 +1,50 @@ -import { Collapse, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from "@mui/material"; import { ExpandLess, ExpandMore } from "@mui/icons-material"; +import { Collapse, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from "@mui/material"; +import Link from "next/link"; const SidebarSubitems = ({ subitem, dispatch }) => { return ( <> - - - + { + dispatch({ type: "COLLAPSE_SUB_ITEMS", id: subitem.id }); + }} + > + { - dispatch({ type: "COLLAPSE_SUB_ITEMS", id: subitem.id }); + minWidth: 0, + justifyContent: "center", + width: 40, + height: 24, }} > - - {subitem.icon} - - - {subitem.hasSubitems ? subitem.showSubitems ? : : null} - - - - {subitem.hasSubitems - ? subitem.Subitems.map((subSubitem, index) => { - return ( - - - - { - dispatch({ type: "COLLAPSE_SUB_SECOND_ITEMS", id: subSubitem.id }); - }} - selected={subSubitem.selected} - sx={{ - "&.Mui-selected": { - backgroundColor: "rgba(0, 0, 0, 0.05)", - }, - }} - > - - {subSubitem.icon} - - - - - - - ); - }) - : null} + {subitem.icon} + + + {subitem.hasSubitems ? subitem.showSubitems ? : : null} + + + + {subitem.hasSubitems ? ( + + {subitem.Subitems.map((subSubitem, index) => { + return ; + })} + + ) : null} + ); }; diff --git a/src/core/components/Toasts/error.jsx b/src/core/components/Toasts/error.jsx new file mode 100644 index 0000000..e93138b --- /dev/null +++ b/src/core/components/Toasts/error.jsx @@ -0,0 +1,187 @@ +import { toast } from "react-toastify"; +import { Box, Typography } from "@mui/material"; +import { Dangerous } from "@mui/icons-material"; + +export const errorServerToast = (toastContainer) => + toast.error( + () => ( + + + + + {"The request failed due to an internal error."} + + + + ), + { + icon: false, + containerId: toastContainer, + autoClose: 5000, + hideProgressBar: true, + pauseOnHover: true, + closeOnClick: false, + draggable: true, + } + ); + +export const errorUnauthorizedToast = (toastContainer) => + toast.error( + () => ( + + + + + {"The user is not authorized to make the request."} + + + + ), + { + icon: false, + containerId: toastContainer, + autoClose: 3000, + hideProgressBar: true, + pauseOnHover: true, + closeOnClick: false, + draggable: true, + } + ); + +export const errorLogicToast = (message, toastContainer) => + toast.error( + () => ( + + + + + + {message || + "The request was well-formed but was unable to be followed due to semantic errors."} + + + + + ), + { + icon: false, + containerId: toastContainer, + autoClose: false, + closeOnClick: false, + draggable: false, + } + ); + +export const errorValidationToast = (message, toastContainer) => + toast.error( + () => ( + + + + + + {message || + "The request was well-formed but was unable to be followed due to semantic errors."} + + + + + ), + { + icon: false, + containerId: toastContainer, + autoClose: false, + closeOnClick: false, + draggable: false, + } + ); + +export const errorTooManyToast = (toastContainer) => + toast.error( + () => ( + + + + + + {"Too many requests have been sent within a given time span."} + + + + + ), + { + icon: false, + containerId: toastContainer, + autoClose: false, + closeOnClick: false, + draggable: false, + } + ); + +export const errorClientToast = (toastContainer) => + toast.error( + () => ( + + + + + + { + "The API request is invalid or improperly formed. Consequently, the API server could not understand the request." + } + + + + + ), + { + icon: false, + containerId: toastContainer, + autoClose: false, + closeOnClick: false, + draggable: false, + } + ); diff --git a/src/core/components/Toasts/success.jsx b/src/core/components/Toasts/success.jsx new file mode 100644 index 0000000..36023c6 --- /dev/null +++ b/src/core/components/Toasts/success.jsx @@ -0,0 +1,33 @@ +import { toast } from "react-toastify"; +import { Box, Typography } from "@mui/material"; +import { Beenhere } from "@mui/icons-material"; + +export const successToast = (toastContainer) => + toast.success( + () => ( + + + + + {"Your operation was successful"} + + + + ), + { + icon: false, + containerId: toastContainer, + autoClose: 3000, + hideProgressBar: true, + pauseOnHover: true, + closeOnClick: false, + draggable: true, + } + ); diff --git a/src/core/middlewares/withAuth.js b/src/core/middlewares/withAuth.js new file mode 100644 index 0000000..216b55b --- /dev/null +++ b/src/core/middlewares/withAuth.js @@ -0,0 +1,22 @@ +"use client"; +import { useAuth } from "@/lib/contexts/auth"; +import { usePathname, useRouter } from "next/navigation"; +import { useEffect } from "react"; + +function WithAuthMiddleware({ children }) { + const router = useRouter(); + const pathName = usePathname(); + const { isAuth, initAuthState } = useAuth(); + + useEffect(() => { + if (!initAuthState) return; + if (!isAuth) { + router.replace(`${process.env.NEXT_PUBLIC_API_URL}/login?_back=${encodeURIComponent(pathName)}`); + } + }, [isAuth, initAuthState]); + + if (!initAuthState || !isAuth) return null; + return <>{children}; +} + +export default WithAuthMiddleware; diff --git a/src/core/utils/errorResponse.js b/src/core/utils/errorResponse.js new file mode 100644 index 0000000..47317bd --- /dev/null +++ b/src/core/utils/errorResponse.js @@ -0,0 +1,60 @@ +"use client"; +import { toast } from "react-toastify"; +import { + errorClientToast, + errorLogicToast, + errorServerToast, + errorTooManyToast, + errorUnauthorizedToast, + errorValidationToast, +} from "@/core/components/Toasts/error"; + +const isServerError = (status) => status >= 500 && status <= 599; +const isClientError = (status) => status >= 400 && status <= 499; + +const errorServer = (response, notification, toastContainer) => { + if (notification) errorServerToast(toastContainer); +}; + +const errorClient = (response, notification, toastContainer) => { + switch (response.status) { + case 401: + if (notification) errorUnauthorizedToast(toastContainer); + break; + case 422: + if ("type" in response.data) { + if (Array.isArray(response.data.message)) { + response.data.message.map((item) => { + if (notification) errorLogicToast(item, toastContainer); + }); + } else { + if (notification) errorLogicToast(response.data.message, toastContainer); + } + break; + } + if (notification) { + const errorsMap = Object.keys(response.data.errors); + const errorsArray = response.data.errors; + + errorsMap.map((item, index) => { + errorValidationToast(errorsArray[item][0], toastContainer); + }); + } + break; + case 429: + if (notification) errorTooManyToast(toastContainer); + break; + default: + if (notification) errorClientToast(toastContainer); + break; + } +}; + +export const errorResponse = (response, notification, toastContainer) => { + if (notification) toast.dismiss({ container: toastContainer }); + if (isServerError(response.status)) { + errorServer(response, notification, toastContainer); + } else if (isClientError(response.status)) { + errorClient(response, notification, toastContainer); + } +}; diff --git a/src/core/utils/filterMenuItems.js b/src/core/utils/filterMenuItems.js new file mode 100644 index 0000000..7bceeb5 --- /dev/null +++ b/src/core/utils/filterMenuItems.js @@ -0,0 +1,17 @@ +export function filterMenuItems(items, _permissions = []) { + return items.reduce((acc, item) => { + if (item.permissions) { + if (item.permissions.some(permission => _permissions.includes(permission))) { + acc.push(item); + } + } else if (item.hasSubitems) { + const filteredSubItems = filterMenuItems(item.Subitems, _permissions); + if (filteredSubItems.length > 0) { + acc.push({ ...item, Subitems: filteredSubItems }); + } + } else { + acc.push(item); + } + return acc; + }, []); +} \ No newline at end of file diff --git a/src/core/utils/pageMenu.js b/src/core/utils/pageMenu.js new file mode 100644 index 0000000..0129a78 --- /dev/null +++ b/src/core/utils/pageMenu.js @@ -0,0 +1,409 @@ +import { Security } from "@mui/icons-material"; +const prefix = process.env.NODE_ENV === 'production' ? '/v3' : '' + +export const pageMenu = [ + { + id: "dashboard", + label: "پیشخوان", + type: "page", + route: prefix + "/dashboard", + icon: , + permissions: ["all"] + }, + { + id: "userManagement", + label: "مدیریت کاربران", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/user_management", + icon: , + permissions: ['full-user-management', 'limited-user-management'] + }, + { + id: "projectsManagment", + label: "پروژه های راهداری", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "projectsManagmentFinance", + label: "ثبت قرارداد و پروژه", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/rahdari_projects/finance", + icon: , + permissions: ['show-contract', 'show-contract-province'], + }, + { + id: "projectsManagmentList", + label: "لیست پروژه ها", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/rahdari_projects/projects_list", + icon: , + permissions: ["all"] + }, + { + id: "projectsManagmentProposal", + label: "پروژه های پیشنهادی", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/rahdari_projects/proposal", + icon: , + permissions: ['show-proposal', 'show-proposal-province'] + }, + { + id: "projectsManagmentLawmaker", + label: "درخواست نمایندگان", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/rahdari_projects/lawmakers", + icon: , + permissions: ['show-lawmaker', 'show-lawmaker-province'] + }, + { + id: "projectsManagmentMap", + label: "پراکندگی بر روی نقشه", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/map?type=rahdari_projects", + icon: , + permissions: ["all"] + }, + ], + }, + { + id: "roadItemManagment", + label: "فعالیت های روزانه", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "roadItemManagmentSupervisor", + label: "ارزیابی", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "roadItemManagmentSupervisorCartable", + label: "کارتابل", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_items/supervisor/cartable", + permissions: ['show-road-item-supervise-cartable', 'show-road-item-supervise-cartable-province'] + }, + ], + }, + { + id: "roadItemManagmentOparation", + label: "عملیات", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "roadItemManagmentOparationCreate", + label: "ثبت فعالیت", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_items/operator/create", + permissions: ['create-road-item'] + }, + { + id: "roadItemManagmentOparationCartable", + label: "کارتابل", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_items/operator/cartable", + permissions: ['create-road-item'] + }, + ], + }, + { + id: "roadItemManagmentMap", + label: "پراکندگی بر روی نقشه", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/map?type=road_items", + icon: , + permissions: ['show-road-item-supervise-cartable', 'show-road-item-supervise-cartable-province', 'create-road-item'] + }, + { + id: "roadItemManagmentReport", + label: "گزارش ها", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_items/report", + icon: , + permissions: ['show-road-item-supervise-cartable', 'show-road-item-supervise-cartable-province', 'create-road-item'] + }, + ], + }, + { + id: "roadPatrolManagment", + label: "گشت راهداری و ترابری", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "roadPatrolManagmentSupervisor", + label: "ارزیابی", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "roadPatrolManagmentSupervisorCartable", + label: "کارتابل", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/road_patrols/supervisor/cartable", + permissions: ['show-road-patrol-supervise-cartable', 'show-road-patrol-supervise-cartable-province'] + }, + ], + }, + { + id: "roadPatrolManagmentOparation", + label: "عملیات", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "roadPatrolManagmentOparationCreate", + label: "ثبت اقدام", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_patrols/operator/create", + permissions: ['add-road-patrol'] + }, + { + id: "roadPatrolManagmentOparationCartable", + label: "کارتابل", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_patrols/operator/cartable", + permissions: ['add-road-patrol'] + }, + ], + }, + { + id: "roadPatrolManagmentMap", + label: "پراکندگی بر روی نقشه", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/map?type=road_patrols", + icon: , + permissions: ['add-road-patrol', 'show-road-patrol-supervise-cartable', 'show-road-patrol-supervise-cartable-province'] + }, + { + id: "roadPatrolManagmentReport", + label: "گزارش ها", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_patrols/report", + icon: , + permissions: ['add-road-patrol', 'show-road-patrol-supervise-cartable', 'show-road-patrol-supervise-cartable-province'] + }, + ], + }, + { + id: "inquiryPrivacyManagment", + label: "استعلام حریم راه", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "inquiryPrivacyManagmentFencing", + label: "احداث و بهره برداری دیوارکشی و مستحدثات", + type: "page", + route: prefix + "/dashboard/inquiry_privacy/fencing", + permissions: ['all'] + }, + { + id: "inquiryPrivacyManagmentRoad", + label: "احداث راه دسترسی اختصاصی", + type: "page", + route: prefix + "/dashboard/inquiry_privacy/road", + permissions: ['all'] + }, + { + id: "inquiryPrivacyManagmentInfrastructure", + label: "احداث و بهره برداری تاسیسات زیربنایی", + type: "page", + route: prefix + "/dashboard/inquiry_privacy/infrastructure", + permissions: ['all'] + }, + ] + }, + { + id: "safetyAndPrivacyManagment", + label: "نگهداری حریم راه", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "safetyAndPrivacyManagmentOparation", + label: "عملیات", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "safetyAndPrivacyManagmentOparationCreate", + label: "ثبت فعالیت", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/safety_and_privacy/operator/first_step", + permissions: ['add-safety-and-privacy'] + }, + { + id: "safetyAndPrivacyManagmentOparationCartable", + label: "کارتابل", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/safety_and_privacy/operator/cartable", + permissions: ['show-safety-and-privacy-operator-cartable', 'show-safety-and-privacy-operator-cartable-province', 'show-safety-and-privacy-operator-cartable-edarate-shahri'] + }, + ], + }, + { + id: "safetyAndPrivacyManagmentMap", + label: "پراکندگی بر روی نقشه", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/map?type=safety_and_privacy", + icon: , + permissions: ['show-safety-and-privacy-operator-cartable', 'show-safety-and-privacy-operator-cartable-province', + 'show-safety-and-privacy-operator-cartable-edarate-shahri', 'add-safety-and-privacy'] + }, + { + id: "safetyAndPrivacyManagmentReport", + label: "گزارش ها", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/safety_and_privacy/report", + icon: , + permissions: ['show-safety-and-privacy-operator-cartable', 'show-safety-and-privacy-operator-cartable-province', + 'show-safety-and-privacy-operator-cartable-edarate-shahri', 'add-safety-and-privacy'] + }, + ], + }, + { + id: "roadObservationsManagment", + label: "واکنش سریع", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "roadObservationsManagmentSupervisor", + label: "ارزیابی", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "roadObservationsManagmentSupervisorCartable", + label: "کارتابل", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_observations/supervisor/cartable", + permissions: ['supervise-fast-react', 'supervise-fast-react-province'] + }, + ], + }, + { + id: "roadObservationsManagmentList", + label: "رسیدگی به شکایات", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_observations", + icon: , + permissions: ['show-fast-react', 'show-fast-react-province', 'show-fast-react-edarate-shahri'] + }, + { + id: "roadObservationsManagmentOparation", + label: "عملیات", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "roadObservationsManagmentOparationCartable", + label: "کارتابل", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_observations/operator/cartable", + permissions: ['show-fast-react', 'show-fast-react-province', 'show-fast-react-edarate-shahri'] + }, + ], + }, + { + id: "roadObservationsManagmentMap", + label: "پراکندگی بر روی نقشه", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/map?type=road_observations", + icon: , + permissions: ["all"] + }, + { + id: "roadObservationsManagmentReport", + label: "گزارش ها", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/road_observations/report/index", + icon: , + permissions: ["all"] + }, + ], + }, + { + id: "winterCampManagment", + label: "قرارگاه زمستانی", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "winterCampManagmentBlocking", + label: "محور های انسدادی", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/winter_camp", + icon: , + permissions: ['show-camp', 'show-camp-province'] + }, + { + id: "winterCampManagmentFirstRing", + label: "محور های حلقه اول", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/map?type=camp", + icon: , + permissions: ['show-camp', 'show-camp-province'] + }, + { + id: "winterCampManagmentReport", + label: "گزارش ها", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/map?type=campNew", + icon: , + permissions: ['show-camp', 'show-camp-province'] + }, + ], + }, + { + id: "receiptManagment", + label: "خسارات وارده بر ابنیه فنی و تاسیسات راه", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "receiptManagmentOparation", + label: "عملیات", + type: "menu", + icon: , + hasSubitems: true, + Subitems: [ + { + id: "receiptManagmentOparationCartable", + label: "کارتابل", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/receipt", + permissions: ["all"] + }, + ], + }, + { + id: "receiptManagmentReport", + label: "گزارش ها", + type: "page", + route: process.env.NEXT_PUBLIC_API_URL + "/v2/receipt/report", + icon: , + permissions: ["all"] + }, + ], + }, +]; diff --git a/src/core/utils/routes.js b/src/core/utils/routes.js new file mode 100644 index 0000000..81ac016 --- /dev/null +++ b/src/core/utils/routes.js @@ -0,0 +1,6 @@ +const api = process.env.NEXT_PUBLIC_API_URL; + +export const GET_USER_ROUTE = api + "/webapi/user/get-permission"; +export const GET_LOGIN_ROUTE = api + "/login_dev"; + +export const GET_PERMISSIONS_ROUTE = api + "/webapi/user/get-permission"; \ No newline at end of file diff --git a/src/core/utils/successRequest.js b/src/core/utils/successRequest.js new file mode 100644 index 0000000..b06d83d --- /dev/null +++ b/src/core/utils/successRequest.js @@ -0,0 +1,10 @@ +"use client"; +import { toast } from "react-toastify"; +import { successToast } from "@/core/components/Toasts/success"; + +export const successRequest = (notification, toastContainer) => { + if (notification) { + toast.dismiss({ container: toastContainer }); + successToast(toastContainer); + } +}; diff --git a/src/core/utils/theme.js b/src/core/utils/theme.js index efe56eb..f32031b 100644 --- a/src/core/utils/theme.js +++ b/src/core/utils/theme.js @@ -7,6 +7,16 @@ const theme = createTheme({ fontFamily: `IRANSansFaNum, sans-serif`, fontSize: 12, }, + palette: { + primary: { + main: "#2070af", + contrastText: "#fff", + }, + primary2: { + main: "#015688", + contrastText: "#fff", + }, + }, }); export default theme; diff --git a/src/lib/contexts/DataTable.js b/src/lib/contexts/DataTable.js new file mode 100644 index 0000000..fb03fd7 --- /dev/null +++ b/src/lib/contexts/DataTable.js @@ -0,0 +1,11 @@ +import { createContext, useState } from "react"; + +export const DataTableContext = createContext(); + +const DataTableProvider = ({ children }) => { + const [filterData, setFilterData] = useState({}); + + return {children}; +}; + +export default DataTableProvider; diff --git a/src/lib/contexts/auth.js b/src/lib/contexts/auth.js new file mode 100644 index 0000000..f2df0e6 --- /dev/null +++ b/src/lib/contexts/auth.js @@ -0,0 +1,82 @@ +"use client"; +import { createContext, useCallback, useContext, useEffect, useReducer } from "react"; +import useRequest from "../hooks/useRequest"; +import { GET_USER_ROUTE } from "@/core/utils/routes"; + +const initAuth = { + initAuthState: false, + isAuth: false, + user: {}, +}; + +const authReducer = (state, action) => { + switch (action.type) { + case "CLEAR_USER": + return { ...state, user: {} }; + case "CHANGE_USER": + return { ...state, user: action.user }; + case "CHANGE_AUTH_STATE": + return { ...state, isAuth: action.isAuth }; + case "CHANGE_INIT_AUTH": + return { ...state, initAuthState: action.initAuthState }; + default: + return state; + } +}; + +const AuthContext = createContext(); + +export const AuthProvider = ({ children }) => { + const [state, dispatch] = useReducer(authReducer, initAuth); + const request = useRequest(); + + const clearUser = useCallback(() => { + dispatch({ type: "CLEAR_USER" }); + }, []); + + const changeUser = useCallback((user) => { + dispatch({ type: "CHANGE_USER", user }); + }, []); + + const changeAuthState = useCallback((isAuth) => { + dispatch({ type: "CHANGE_AUTH_STATE", isAuth }); + }, []); + + const changeInitAuth = useCallback((initAuthState) => { + dispatch({ type: "CHANGE_INIT_AUTH", initAuthState }); + }, []); + + const getUser = useCallback(async () => { + try { + const { data } = await request(GET_USER_ROUTE, "get"); + changeUser(data); + changeAuthState(true); + changeInitAuth(true); + } catch (error) { + if (error.response && error.response.status === 401) { + clearUser(); + changeAuthState(false); + changeInitAuth(true); + } + } + }, [clearUser, changeUser, changeAuthState, changeInitAuth]); + + useEffect(() => { + getUser(); + }, []); + + return ( + + {children} + + ); +}; + +export const useAuth = () => useContext(AuthContext); diff --git a/src/lib/contexts/tableSetting.jsx b/src/lib/contexts/tableSetting.js similarity index 100% rename from src/lib/contexts/tableSetting.jsx rename to src/lib/contexts/tableSetting.js diff --git a/src/lib/hooks/useDataTable.js b/src/lib/hooks/useDataTable.js new file mode 100644 index 0000000..e396908 --- /dev/null +++ b/src/lib/hooks/useDataTable.js @@ -0,0 +1,9 @@ +import { useContext } from "react"; +import { DataTableContext } from "@/lib/contexts/DataTable"; + +const useTableSetting = () => { + const { filterData, setFilterData } = useContext(DataTableContext); + return { filterData, setFilterData }; +}; + +export default useTableSetting; diff --git a/src/lib/hooks/usePermissions.js b/src/lib/hooks/usePermissions.js new file mode 100644 index 0000000..4f54b41 --- /dev/null +++ b/src/lib/hooks/usePermissions.js @@ -0,0 +1,18 @@ +import { GET_PERMISSIONS_ROUTE } from "@/core/utils/routes"; +import useSWR from "swr"; +import useRequest from "./useRequest"; + +export const usePermissions = () => { + const request = useRequest() + + const fetcher = async (url) => { + try { + const response = await request(url) + return response.data.data + } catch (error) { + throw new Error(); + } + }; + + return useSWR(GET_PERMISSIONS_ROUTE, fetcher, { keepPreviousData: true }); +} \ No newline at end of file diff --git a/src/lib/hooks/useRequest.js b/src/lib/hooks/useRequest.js new file mode 100644 index 0000000..2e35512 --- /dev/null +++ b/src/lib/hooks/useRequest.js @@ -0,0 +1,46 @@ +import { errorResponse } from "@/core/utils/errorResponse"; +import { successRequest } from "@/core/utils/successRequest"; +import axios from "axios"; + +const defaultOptions = { + data: {}, + requestOptions: { + headers: {}, + }, + notification: { + show: true, + success: false, + failed: true, + }, +}; + +const useRequest = (initOptions) => { + const _options = Object.assign({}, defaultOptions, initOptions); + + return async (url = "", method = "get", options) => { + const mergedOptions = Object.assign({}, _options, options); + + try { + const response = await axios({ + url, + method, + data: method === "get" ? null : mergedOptions.data, + withCredentials: true, + ...mergedOptions.requestOptions, + }); + successRequest(mergedOptions.notification.show && mergedOptions.notification.success, "request_data"); + return response; + } catch (error) { + if (error.response) { + errorResponse( + error.response, + mergedOptions.notification.show && mergedOptions.notification.failed, + "request_data" + ); + } + throw error; + } + }; +}; + +export default useRequest; diff --git a/src/lib/hooks/useTableSetting.js b/src/lib/hooks/useTableSetting.js new file mode 100644 index 0000000..6588446 --- /dev/null +++ b/src/lib/hooks/useTableSetting.js @@ -0,0 +1,9 @@ +import { useContext } from "react"; +import { TableSettingContext } from "../contexts/tableSetting"; + +const useTableSetting = () => { + const { settingStore, hideAction, sortAction, filterAction, refactorAction } = useContext(TableSettingContext); + return { settingStore, hideAction, sortAction, filterAction, refactorAction }; +}; + +export default useTableSetting;