add prettier for formatting to project

This commit is contained in:
2025-07-28 12:18:27 +03:30
parent 1a6cb32528
commit 3a36561ab0
180 changed files with 15437 additions and 14748 deletions

47
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,47 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="LINE_SEPARATOR" value="&#13;&#10;" />
<HTMLCodeStyleSettings>
<option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</HTMLCodeStyleSettings>
<JSCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</JSCodeStyleSettings>
<TypeScriptCodeStyleSettings version="0">
<option name="FORCE_SEMICOLON_STYLE" value="true" />
<option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
<option name="FORCE_QUOTE_STYlE" value="true" />
<option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
<option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
<option name="SPACES_WITHIN_IMPORTS" value="true" />
</TypeScriptCodeStyleSettings>
<VueCodeStyleSettings>
<option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
<option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
</VueCodeStyleSettings>
<codeStyleSettings language="HTML">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JavaScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="TypeScript">
<option name="SOFT_MARGINS" value="120" />
</codeStyleSettings>
<codeStyleSettings language="Vue">
<option name="SOFT_MARGINS" value="120" />
<indentOptions>
<option name="INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

12
.prettierrc Normal file
View File

@@ -0,0 +1,12 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "always",
"endOfLine": "crlf"
}

View File

@@ -1,17 +1,17 @@
version: '3.8' version: "3.8"
services: services:
nextjs: nextjs:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: nextjs-app container_name: nextjs-app
environment: environment:
- NEXT_PUBLIC_VERSION=${NEXT_PUBLIC_VERSION} - NEXT_PUBLIC_VERSION=${NEXT_PUBLIC_VERSION}
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
- NEXT_PUBLIC_SERVER_SOCKET_URL=${NEXT_PUBLIC_SERVER_SOCKET_URL} - NEXT_PUBLIC_SERVER_SOCKET_URL=${NEXT_PUBLIC_SERVER_SOCKET_URL}
ports: ports:
- "3000:3000" - "3000:3000"
env_file: env_file:
- .env - .env
restart: unless-stopped restart: unless-stopped

View File

@@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const compat = new FlatCompat({ const compat = new FlatCompat({
baseDirectory: __dirname, baseDirectory: __dirname,
}); });
const eslintConfig = [...compat.extends("next/core-web-vitals")]; const eslintConfig = [...compat.extends("next/core-web-vitals")];

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}
} }
}
} }

View File

@@ -1,46 +1,46 @@
{ {
"name": "app", "name": "app",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,html,css,scss}\"" "format": "npx prettier . --write"
}, },
"dependencies": { "dependencies": {
"@emotion/cache": "^11.14.0", "@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
"@mui/icons-material": "^7.0.2", "@mui/icons-material": "^7.0.2",
"@mui/material": "^7.0.2", "@mui/material": "^7.0.2",
"@mui/material-nextjs": "^7.0.2", "@mui/material-nextjs": "^7.0.2",
"@mui/x-date-pickers": "^8.2.0", "@mui/x-date-pickers": "^8.2.0",
"@mui/x-tree-view": "^8.2.0", "@mui/x-tree-view": "^8.2.0",
"axios": "^1.9.0", "axios": "^1.9.0",
"date-fns-jalali": "4.1.0-0", "date-fns-jalali": "4.1.0-0",
"jalali-moment": "^3.3.11", "jalali-moment": "^3.3.11",
"lz-string": "^1.5.0", "lz-string": "^1.5.0",
"material-react-table": "^3.2.1", "material-react-table": "^3.2.1",
"moment": "^2.30.1", "moment": "^2.30.1",
"next": "15.3.1", "next": "15.3.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.56.1", "react-hook-form": "^7.56.1",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"stylis": "^4.3.6", "stylis": "^4.3.6",
"stylis-plugin-rtl": "^2.1.1", "stylis-plugin-rtl": "^2.1.1",
"swr": "^2.3.3", "swr": "^2.3.3",
"yup": "^1.6.1" "yup": "^1.6.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.1", "eslint-config-next": "15.3.1",
"prettier": "3.5.3", "prettier": "3.5.3",
"sass": "^1.87.0" "sass": "^1.87.0"
} }
} }

12232
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,6 @@
import PageLoading from "@/core/components/PageLoading"; import PageLoading from "@/core/components/PageLoading";
const Loading = () => { const Loading = () => {
return <PageLoading />; return <PageLoading />;
}; };
export default Loading; export default Loading;

View File

@@ -1,11 +1,11 @@
import DashboardPage from "@/components/dashboard/dashboard"; import DashboardPage from "@/components/dashboard/dashboard";
export const metadata = { export const metadata = {
title: "پیشخوان", title: "پیشخوان",
}; };
const Page = () => { const Page = () => {
return <DashboardPage />; return <DashboardPage />;
}; };
export default Page; export default Page;

View File

@@ -2,15 +2,15 @@ import UsersPage from "@/components/dashboard/Users";
import WithPermission from "@/core/middlewares/withPermission"; import WithPermission from "@/core/middlewares/withPermission";
export const metadata = { export const metadata = {
title: "کاربران", title: "کاربران",
}; };
const Page = () => { const Page = () => {
return ( return (
<WithPermission permission_name={["manage_users"]}> <WithPermission permission_name={["manage_users"]}>
<UsersPage /> <UsersPage />
</WithPermission> </WithPermission>
); );
}; };
export default Page; export default Page;

View File

@@ -8,22 +8,20 @@ import { Stack } from "@mui/material";
import { SWRConfig } from "swr"; import { SWRConfig } from "swr";
const Layout = ({ children }) => { const Layout = ({ children }) => {
const { data: permissions } = usePermissions(); const { data: permissions } = usePermissions();
const isCallWidgetEnabled = permissions?.some( const isCallWidgetEnabled = permissions?.some((permission) => permission === "manage_calls");
(permission) => permission === "manage_calls",
);
return ( return (
<SocketProvider> <SocketProvider>
<SWRConfig value={{ provider: () => new Map() }}> <SWRConfig value={{ provider: () => new Map() }}>
<Stack sx={{ width: "100%", height: "100%" }}> <Stack sx={{ width: "100%", height: "100%" }}>
<HeaderWithSidebar>{children}</HeaderWithSidebar> <HeaderWithSidebar>{children}</HeaderWithSidebar>
</Stack> </Stack>
</SWRConfig> </SWRConfig>
<WithWidgetMiddleware enable={isCallWidgetEnabled}> <WithWidgetMiddleware enable={isCallWidgetEnabled}>
<CallWidget /> <CallWidget />
</WithWidgetMiddleware> </WithWidgetMiddleware>
</SocketProvider> </SocketProvider>
); );
}; };
export default Layout; export default Layout;

View File

@@ -1,7 +1,7 @@
import WithAuthMiddleware from "@/core/middlewares/withAuth"; import WithAuthMiddleware from "@/core/middlewares/withAuth";
const Layout = ({ children }) => { const Layout = ({ children }) => {
return <WithAuthMiddleware>{children}</WithAuthMiddleware>; return <WithAuthMiddleware>{children}</WithAuthMiddleware>;
}; };
export default Layout; export default Layout;

View File

@@ -3,11 +3,11 @@ import WithoutAuthMiddleware from "@/core/middlewares/withoutAuth";
import { Suspense } from "react"; import { Suspense } from "react";
const Layout = ({ children }) => { const Layout = ({ children }) => {
return ( return (
<Suspense> <Suspense>
<WithoutAuthMiddleware>{children}</WithoutAuthMiddleware> <WithoutAuthMiddleware>{children}</WithoutAuthMiddleware>
</Suspense> </Suspense>
); );
}; };
export default Layout; export default Layout;

View File

@@ -1,10 +1,10 @@
import LoginPage from "@/components/Login"; import LoginPage from "@/components/Login";
export const metadata = { export const metadata = {
title: "ورود", title: "ورود",
}; };
const Page = () => { const Page = () => {
return <LoginPage />; return <LoginPage />;
}; };
export default Page; export default Page;

View File

@@ -1,3 +1,3 @@
export default function Default() { export default function Default() {
return null; return null;
} }

View File

@@ -5,28 +5,25 @@ import { TableSettingProvider } from "@/lib/contexts/tableSetting";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter"; import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter";
export const metadata = { export const metadata = {
title: { title: {
template: "%s | سامانه CRM", template: "%s | سامانه CRM",
default: "سامانه CRM", default: "سامانه CRM",
}, },
}; };
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
return ( return (
<html lang="fa" dir={"rtl"}> <html lang="fa" dir={"rtl"}>
<head> <head>
<link rel="icon" href={favicon.src} type="image/png" sizes="any" /> <link rel="icon" href={favicon.src} type="image/png" sizes="any" />
</head> </head>
<body style={{ height: "100vh", width: "100vw" }}> <body style={{ height: "100vh", width: "100vw" }}>
<AppRouterCacheProvider <AppRouterCacheProvider CacheProvider={Rtl} options={{ enableCssLayer: true }}>
CacheProvider={Rtl} <AuthProvider>
options={{ enableCssLayer: true }} <TableSettingProvider>{children}</TableSettingProvider>
> </AuthProvider>
<AuthProvider> </AppRouterCacheProvider>
<TableSettingProvider>{children}</TableSettingProvider> </body>
</AuthProvider> </html>
</AppRouterCacheProvider> );
</body>
</html>
);
} }

View File

@@ -3,19 +3,14 @@ import { Box, Stack, Typography } from "@mui/material";
import SvgNotFound from "@/core/components/svgs/SvgNotFound"; import SvgNotFound from "@/core/components/svgs/SvgNotFound";
export default function NotFound() { export default function NotFound() {
return ( return (
<Stack <Stack sx={{ height: "100%" }} justifyContent={"center"} alignItems={"center"} spacing={2}>
sx={{ height: "100%" }} <Box>
justifyContent={"center"} <SvgNotFound width={200} height={200} />
alignItems={"center"} </Box>
spacing={2} <Typography variant={"body1"} sx={{ color: "primary.main" }}>
> صفحه موردنظر یافت نشد ...
<Box> </Typography>
<SvgNotFound width={200} height={200} /> </Stack>
</Box> );
<Typography variant={"body1"} sx={{ color: "primary.main" }}>
صفحه موردنظر یافت نشد ...
</Typography>
</Stack>
);
} }

View File

@@ -1,8 +1,8 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
function Page() { function Page() {
redirect("/dashboard", "replace"); redirect("/dashboard", "replace");
return null; return null;
} }
export default Page; export default Page;

View File

@@ -7,34 +7,34 @@ import { CssBaseline, GlobalStyles, ThemeProvider } from "@mui/material";
import { ToastContainer } from "react-toastify"; import { ToastContainer } from "react-toastify";
const Template = ({ children }) => { const Template = ({ children }) => {
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<GlobalStyles <GlobalStyles
styles={{ styles={{
"*": { "*": {
scrollbarWidth: "thin", scrollbarWidth: "thin",
scrollbarColor: `${theme.palette.primary.dark} transparent`, scrollbarColor: `${theme.palette.primary.dark} transparent`,
}, },
"*&::-webkit-scrollbar": { "*&::-webkit-scrollbar": {
width: "4px", width: "4px",
}, },
"*&::-webkit-scrollbar-track": { "*&::-webkit-scrollbar-track": {
boxShadow: "inset 0 0 5px #fff", boxShadow: "inset 0 0 5px #fff",
borderRadius: "4px", borderRadius: "4px",
}, },
"*&::-webkit-scrollbar-thumb": { "*&::-webkit-scrollbar-thumb": {
background: `${theme.palette.primary.dark}`, background: `${theme.palette.primary.dark}`,
borderRadius: "4px", borderRadius: "4px",
}, },
}} }}
/> />
<CssBaseline /> <CssBaseline />
{children} {children}
<ToastContainer rtl containerId="filtering" closeButton={false} /> <ToastContainer rtl containerId="filtering" closeButton={false} />
<ToastContainer rtl containerId="request_data" /> <ToastContainer rtl containerId="request_data" />
<ToastContainer rtl containerId="socket_connection" position="top-left" /> <ToastContainer rtl containerId="socket_connection" position="top-left" />
</ThemeProvider> </ThemeProvider>
); );
}; };
export default Template; export default Template;

View File

@@ -1,212 +1,172 @@
@font-face { @font-face {
font-family: IRANSansFaNum; font-family: IRANSansFaNum;
font-style: normal; font-style: normal;
font-weight: 900; font-weight: 900;
src: url("../fonts/eot/IRANSansWeb(FaNum)_Black.eot"); src: url("../fonts/eot/IRANSansWeb(FaNum)_Black.eot");
src: src:
url("../fonts/eot/IRANSansWeb(FaNum)_Black.eot?#iefix") url("../fonts/eot/IRANSansWeb(FaNum)_Black.eot?#iefix") format("embedded-opentype"),
format("embedded-opentype"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum)_Black.woff2") format("woff2"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum)_Black.woff2") /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb(FaNum)_Black.woff") format("woff"),
format("woff2"), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb(FaNum)_Black.ttf") format("truetype");
/* FF39+,Chrome36+, Opera24+*/
url("../fonts/woff/IRANSansWeb(FaNum)_Black.woff") format("woff"),
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
url("../fonts/ttf/IRANSansWeb(FaNum)_Black.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: IRANSansFaNum; font-family: IRANSansFaNum;
font-style: normal; font-style: normal;
font-weight: bold; font-weight: bold;
src: url("../fonts/eot/IRANSansWeb(FaNum)_Bold.eot"); src: url("../fonts/eot/IRANSansWeb(FaNum)_Bold.eot");
src: src:
url("../fonts/eot/IRANSansWeb(FaNum)_Bold.eot?#iefix") url("../fonts/eot/IRANSansWeb(FaNum)_Bold.eot?#iefix") format("embedded-opentype"),
format("embedded-opentype"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum)_Bold.woff2") format("woff2"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum)_Bold.woff2") /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb(FaNum)_Bold.woff") format("woff"),
format("woff2"), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb(FaNum)_Bold.ttf") format("truetype");
/* FF39+,Chrome36+, Opera24+*/
url("../fonts/woff/IRANSansWeb(FaNum)_Bold.woff") format("woff"),
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
url("../fonts/ttf/IRANSansWeb(FaNum)_Bold.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: IRANSansFaNum; font-family: IRANSansFaNum;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: url("../fonts/eot/IRANSansWeb(FaNum)_Medium.eot"); src: url("../fonts/eot/IRANSansWeb(FaNum)_Medium.eot");
src: src:
url("../fonts/eot/IRANSansWeb(FaNum)_Medium.eot?#iefix") url("../fonts/eot/IRANSansWeb(FaNum)_Medium.eot?#iefix") format("embedded-opentype"),
format("embedded-opentype"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum)_Medium.woff2") format("woff2"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum)_Medium.woff2") /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb(FaNum)_Medium.woff") format("woff"),
format("woff2"), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb(FaNum)_Medium.ttf") format("truetype");
/* FF39+,Chrome36+, Opera24+*/
url("../fonts/woff/IRANSansWeb(FaNum)_Medium.woff") format("woff"),
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
url("../fonts/ttf/IRANSansWeb(FaNum)_Medium.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: IRANSansFaNum; font-family: IRANSansFaNum;
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: url("../fonts/eot/IRANSansWeb(FaNum)_Light.eot"); src: url("../fonts/eot/IRANSansWeb(FaNum)_Light.eot");
src: src:
url("../fonts/eot/IRANSansWeb(FaNum)_Light.eot?#iefix") url("../fonts/eot/IRANSansWeb(FaNum)_Light.eot?#iefix") format("embedded-opentype"),
format("embedded-opentype"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum)_Light.woff2") format("woff2"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum)_Light.woff2") /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb(FaNum)_Light.woff") format("woff"),
format("woff2"), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb(FaNum)_Light.ttf") format("truetype");
/* FF39+,Chrome36+, Opera24+*/
url("../fonts/woff/IRANSansWeb(FaNum)_Light.woff") format("woff"),
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
url("../fonts/ttf/IRANSansWeb(FaNum)_Light.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: IRANSansFaNum; font-family: IRANSansFaNum;
font-style: normal; font-style: normal;
font-weight: 200; font-weight: 200;
src: url("../fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot"); src: url("../fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot");
src: src:
url("../fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot?#iefix") url("../fonts/eot/IRANSansWeb(FaNum)_UltraLight.eot?#iefix") format("embedded-opentype"),
format("embedded-opentype"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum)_UltraLight.woff2") format("woff2"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum)_UltraLight.woff2") /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb(FaNum)_UltraLight.woff") format("woff"),
format("woff2"), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb(FaNum)_UltraLight.ttf") format("truetype");
/* FF39+,Chrome36+, Opera24+*/
url("../fonts/woff/IRANSansWeb(FaNum)_UltraLight.woff") format("woff"),
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
url("../fonts/ttf/IRANSansWeb(FaNum)_UltraLight.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: IRANSansFaNum; font-family: IRANSansFaNum;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
src: url("../fonts/eot/IRANSansWeb(FaNum).eot"); src: url("../fonts/eot/IRANSansWeb(FaNum).eot");
src: src:
url("../fonts/eot/IRANSansWeb(FaNum).eot?#iefix") url("../fonts/eot/IRANSansWeb(FaNum).eot?#iefix") format("embedded-opentype"),
format("embedded-opentype"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum).woff2") format("woff2"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb(FaNum).woff2") format("woff2"), /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb(FaNum).woff") format("woff"),
/* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb(FaNum).woff") /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb(FaNum).ttf") format("truetype");
format("woff"),
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
url("../fonts/ttf/IRANSansWeb(FaNum).ttf") format("truetype");
} }
@font-face { @font-face {
font-family: IRANSans; font-family: IRANSans;
font-style: normal; font-style: normal;
font-weight: 900; font-weight: 900;
src: url("../fonts/eot/IRANSansWeb_Black.eot"); src: url("../fonts/eot/IRANSansWeb_Black.eot");
src: src:
url("../fonts/eot/IRANSansWeb_Black.eot?#iefix") format("embedded-opentype"), url("../fonts/eot/IRANSansWeb_Black.eot?#iefix") format("embedded-opentype"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb_Black.woff2") format("woff2"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb_Black.woff2") format("woff2"),
/* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb_Black.woff") /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb_Black.woff") format("woff"),
format("woff"), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb_Black.ttf") format("truetype");
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
url("../fonts/ttf/IRANSansWeb_Black.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: IRANSans; font-family: IRANSans;
font-style: normal; font-style: normal;
font-weight: bold; font-weight: bold;
src: url("../fonts/eot/IRANSansWeb_Bold.eot"); src: url("../fonts/eot/IRANSansWeb_Bold.eot");
src: src:
url("../fonts/eot/IRANSansWeb_Bold.eot?#iefix") format("embedded-opentype"), url("../fonts/eot/IRANSansWeb_Bold.eot?#iefix") format("embedded-opentype"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb_Bold.woff2") format("woff2"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb_Bold.woff2") format("woff2"),
/* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb_Bold.woff") /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb_Bold.woff") format("woff"),
format("woff"), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb_Bold.ttf") format("truetype");
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb_Bold.ttf")
format("truetype");
} }
@font-face { @font-face {
font-family: IRANSans; font-family: IRANSans;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: url("../fonts/eot/IRANSansWeb_Medium.eot"); src: url("../fonts/eot/IRANSansWeb_Medium.eot");
src: src:
url("../fonts/eot/IRANSansWeb_Medium.eot?#iefix") url("../fonts/eot/IRANSansWeb_Medium.eot?#iefix") format("embedded-opentype"),
format("embedded-opentype"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb_Medium.woff2") format("woff2"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb_Medium.woff2") format("woff2"), /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb_Medium.woff") format("woff"),
/* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb_Medium.woff") /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb_Medium.ttf") format("truetype");
format("woff"),
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
url("../fonts/ttf/IRANSansWeb_Medium.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: IRANSans; font-family: IRANSans;
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: url("../fonts/eot/IRANSansWeb_Light.eot"); src: url("../fonts/eot/IRANSansWeb_Light.eot");
src: src:
url("../fonts/eot/IRANSansWeb_Light.eot?#iefix") format("embedded-opentype"), url("../fonts/eot/IRANSansWeb_Light.eot?#iefix") format("embedded-opentype"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb_Light.woff2") format("woff2"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb_Light.woff2") format("woff2"),
/* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb_Light.woff") /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb_Light.woff") format("woff"),
format("woff"), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb_Light.ttf") format("truetype");
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
url("../fonts/ttf/IRANSansWeb_Light.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: IRANSans; font-family: IRANSans;
font-style: normal; font-style: normal;
font-weight: 200; font-weight: 200;
src: url("../fonts/eot/IRANSansWeb_UltraLight.eot"); src: url("../fonts/eot/IRANSansWeb_UltraLight.eot");
src: src:
url("../fonts/eot/IRANSansWeb_UltraLight.eot?#iefix") url("../fonts/eot/IRANSansWeb_UltraLight.eot?#iefix") format("embedded-opentype"),
format("embedded-opentype"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb_UltraLight.woff2") format("woff2"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb_UltraLight.woff2") /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb_UltraLight.woff") format("woff"),
format("woff2"), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb_UltraLight.ttf") format("truetype");
/* FF39+,Chrome36+, Opera24+*/
url("../fonts/woff/IRANSansWeb_UltraLight.woff") format("woff"),
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/
url("../fonts/ttf/IRANSansWeb_UltraLight.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: IRANSans; font-family: IRANSans;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
src: url("../fonts/eot/IRANSansWeb.eot"); src: url("../fonts/eot/IRANSansWeb.eot");
src: src:
url("../fonts/eot/IRANSansWeb.eot?#iefix") format("embedded-opentype"), url("../fonts/eot/IRANSansWeb.eot?#iefix") format("embedded-opentype"),
/* IE6-8 */ url("../fonts/woff2/IRANSansWeb.woff2") format("woff2"), /* IE6-8 */ url("../fonts/woff2/IRANSansWeb.woff2") format("woff2"),
/* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb.woff") /* FF39+,Chrome36+, Opera24+*/ url("../fonts/woff/IRANSansWeb.woff") format("woff"),
format("woff"), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb.ttf") format("truetype");
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/IRANSansWeb.ttf")
format("truetype");
} }
@font-face { @font-face {
font-family: Parastoo; font-family: Parastoo;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
src: url("../fonts/eot/Parastoo.eot"); src: url("../fonts/eot/Parastoo.eot");
src: src:
url("../fonts/eot/Parastoo.eot?#iefix") format("embedded-opentype"), url("../fonts/eot/Parastoo.eot?#iefix") format("embedded-opentype"),
/* IE6-8 */ url("../fonts/woff/Parastoo.woff") format("woff"), /* IE6-8 */ url("../fonts/woff/Parastoo.woff") format("woff"),
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/Parastoo.ttf") /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/Parastoo.ttf") format("truetype");
format("truetype");
} }
@font-face { @font-face {
font-family: Parastoo; font-family: Parastoo;
font-style: normal; font-style: normal;
font-weight: bold; font-weight: bold;
src: url("../fonts/eot/Parastoo-Bold.eot"); src: url("../fonts/eot/Parastoo-Bold.eot");
src: src:
url("../fonts/eot/Parastoo-Bold.eot?#iefix") format("embedded-opentype"), url("../fonts/eot/Parastoo-Bold.eot?#iefix") format("embedded-opentype"),
/* IE6-8 */ url("../fonts/woff/Parastoo-Bold.woff") format("woff"), /* IE6-8 */ url("../fonts/woff/Parastoo-Bold.woff") format("woff"),
/* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/Parastoo-Bold.ttf") /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url("../fonts/ttf/Parastoo-Bold.ttf") format("truetype");
format("truetype");
} }
@font-face { @font-face {
font-family: Bnazanin; font-family: Bnazanin;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
src: url("../fonts/ttf/BNazanin.ttf") format("truetype"); src: url("../fonts/ttf/BNazanin.ttf") format("truetype");
} }

View File

@@ -1,6 +1,6 @@
.filter-toast { .filter-toast {
box-shadow: box-shadow:
rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(50, 50, 93, 0.25) 0px 13px 27px -5px,
rgba(0, 0, 0, 0.3) 0px 8px 16px -8px; rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;
background-color: #d0dfe8; background-color: #d0dfe8;
} }

View File

@@ -5,169 +5,135 @@ import { GET_USER_LOGIN_ROUTE } from "@/core/utils/routes";
import { useAuth } from "@/lib/contexts/auth"; import { useAuth } from "@/lib/contexts/auth";
import useRequest from "@/lib/hooks/useRequest"; import useRequest from "@/lib/hooks/useRequest";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { import { Lock, Person, PhoneCallback, Visibility, VisibilityOff } from "@mui/icons-material";
Lock, import { Button, Container, IconButton, InputAdornment, Stack, Typography } from "@mui/material";
Person,
PhoneCallback,
Visibility,
VisibilityOff,
} from "@mui/icons-material";
import {
Button,
Container,
IconButton,
InputAdornment,
Stack,
Typography,
} from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { object, string } from "yup"; import { object, string } from "yup";
const LoginForm = () => { const LoginForm = () => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const requestServer = useRequest(); const requestServer = useRequest();
const { getUser } = useAuth(); const { getUser } = useAuth();
const defaultValues = { const defaultValues = {
username: "", username: "",
password: "", password: "",
}; };
const validationSchema = object({ const validationSchema = object({
username: string().required("لطفا نام کاربری را وارد کنید!"), username: string().required("لطفا نام کاربری را وارد کنید!"),
password: string().required("لطفا رمز عبور را وارد کنید!"), password: string().required("لطفا رمز عبور را وارد کنید!"),
}); });
const { const {
control, control,
handleSubmit, handleSubmit,
formState: { isSubmitting }, formState: { isSubmitting },
} = useForm({ } = useForm({
defaultValues, defaultValues,
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
mode: "all", mode: "all",
}); });
const handleClickShowPassword = () => setShowPassword((show) => !show); const handleClickShowPassword = () => setShowPassword((show) => !show);
const onSubmit = async (data) => { const onSubmit = async (data) => {
try { try {
const formData = new FormData(); const formData = new FormData();
formData.append("username", data.username); formData.append("username", data.username);
formData.append("password", data.password); formData.append("password", data.password);
await requestServer(GET_USER_LOGIN_ROUTE, "post", { await requestServer(GET_USER_LOGIN_ROUTE, "post", {
data: formData, data: formData,
}); });
getUser(); getUser();
} catch (error) {} } catch (error) {}
}; };
return ( return (
<Container maxWidth="xs" sx={{ flex: 1 }}> <Container maxWidth="xs" sx={{ flex: 1 }}>
<StyledForm <StyledForm onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", height: "100%" }}>
onSubmit={handleSubmit(onSubmit)} <Stack sx={{ width: "100%", height: "100%" }} justifyContent="center" alignItems="center" spacing={6}>
style={{ width: "100%", height: "100%" }} <Stack sx={{ width: "100%", mb: 2 }} justifyContent="center" alignItems="center">
> <Typography variant="h4" color="primary" fontWeight={600} textAlign="center">
<Stack ورود به سامانه CRM
sx={{ width: "100%", height: "100%" }} </Typography>
justifyContent="center" </Stack>
alignItems="center" <Stack sx={{ width: "100%", p: 2 }} spacing={4}>
spacing={6} <Stack spacing={2}>
> <Controller
<Stack control={control}
sx={{ width: "100%", mb: 2 }} render={({ field, fieldState: { error } }) => {
justifyContent="center" return (
alignItems="center" <LtrTextField
> {...field}
<Typography label="نام کاربری"
variant="h4" variant="outlined"
color="primary" slotProps={{
fontWeight={600} input: {
textAlign="center" endAdornment: (
> <InputAdornment position="end">
ورود به سامانه CRM <Person />
</Typography> </InputAdornment>
</Stack> ),
<Stack sx={{ width: "100%", p: 2 }} spacing={4}> },
<Stack spacing={2}> }}
<Controller fullWidth
control={control} error={!!error}
render={({ field, fieldState: { error } }) => { helperText={!!error && error.message}
return ( />
<LtrTextField );
{...field} }}
label="نام کاربری" name={"username"}
variant="outlined" />
slotProps={{ <Controller
input: { control={control}
endAdornment: ( render={({ field, fieldState: { error } }) => {
<InputAdornment position="end"> return (
<Person /> <LtrTextField
</InputAdornment> {...field}
), label="رمزعبور"
}, variant="outlined"
}} type={showPassword ? "text" : "password"}
fullWidth slotProps={{
error={!!error} input: {
helperText={!!error && error.message} startAdornment: (
/> <InputAdornment position="start">
); <IconButton onClick={handleClickShowPassword}>
}} {showPassword ? <VisibilityOff /> : <Visibility />}
name={"username"} </IconButton>
/> </InputAdornment>
<Controller ),
control={control} endAdornment: (
render={({ field, fieldState: { error } }) => { <InputAdornment position="end">
return ( <Lock />
<LtrTextField </InputAdornment>
{...field} ),
label="رمزعبور" },
variant="outlined" }}
type={showPassword ? "text" : "password"} fullWidth
slotProps={{ error={!!error}
input: { helperText={!!error && error.message}
startAdornment: ( />
<InputAdornment position="start"> );
<IconButton onClick={handleClickShowPassword}> }}
{showPassword ? ( name={"password"}
<VisibilityOff /> />
) : ( </Stack>
<Visibility /> <Stack>
)} <Button
</IconButton> variant="contained"
</InputAdornment> color="primary"
), size="large"
endAdornment: ( type={"submit"}
<InputAdornment position="end"> disabled={isSubmitting}
<Lock /> >
</InputAdornment> {isSubmitting ? "درحال ورود..." : "ورود"}
), </Button>
}, </Stack>
}} </Stack>
fullWidth </Stack>
error={!!error} </StyledForm>
helperText={!!error && error.message} </Container>
/> );
);
}}
name={"password"}
/>
</Stack>
<Stack>
<Button
variant="contained"
color="primary"
size="large"
type={"submit"}
disabled={isSubmitting}
>
{isSubmitting ? "درحال ورود..." : "ورود"}
</Button>
</Stack>
</Stack>
</Stack>
</StyledForm>
</Container>
);
}; };
export default LoginForm; export default LoginForm;

View File

@@ -4,22 +4,22 @@ import Link from "next/link";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
const LoginLinkRouting = () => { const LoginLinkRouting = () => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const redirect = searchParams.get("redirect"); const redirect = searchParams.get("redirect");
return ( return (
<Stack direction="row" alignItems="center" justifyContent="center"> <Stack direction="row" alignItems="center" justifyContent="center">
<Button <Button
component={Link} component={Link}
data-testid="link_routing" data-testid="link_routing"
sx={{ margin: 2 }} sx={{ margin: 2 }}
color="secondary" color="secondary"
href={redirect ? decodeURIComponent(redirect) : "/"} href={redirect ? decodeURIComponent(redirect) : "/"}
> >
{`بازگشت به ${redirect ? "صفحه قبلی" : "صفحه اصلی"}`} {`بازگشت به ${redirect ? "صفحه قبلی" : "صفحه اصلی"}`}
</Button> </Button>
</Stack> </Stack>
); );
}; };
export default LoginLinkRouting; export default LoginLinkRouting;

View File

@@ -5,15 +5,15 @@ import LoginLinkRouting from "./LoginLinkRouting";
import { Suspense } from "react"; import { Suspense } from "react";
const LoginPage = () => { const LoginPage = () => {
return ( return (
<Suspense> <Suspense>
<Fade in={true}> <Fade in={true}>
<Stack sx={{ width: "100%", height: "100vh" }}> <Stack sx={{ width: "100%", height: "100vh" }}>
<LoginForm /> <LoginForm />
<LoginLinkRouting /> <LoginLinkRouting />
</Stack> </Stack>
</Fade> </Fade>
</Suspense> </Suspense>
); );
}; };
export default LoginPage; export default LoginPage;

View File

@@ -13,205 +13,200 @@ import OnlineCell from "./RowActions/Online";
import TelephoneId from "./RowActions/TelephoneId"; import TelephoneId from "./RowActions/TelephoneId";
const DataTable = () => { const DataTable = () => {
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
accessorKey: "id", accessorKey: "id",
header: "کد یکتا", header: "کد یکتا",
id: "id", id: "id",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
sortDescFirst: false, sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
accessorKey: "full_name", accessorKey: "full_name",
header: "نام و نام خانوادگی", header: "نام و نام خانوادگی",
id: "full_name", id: "full_name",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
sortDescFirst: false, sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
}, },
{ {
header: "شماره تماس داخلی", header: "شماره تماس داخلی",
id: "telephone_id", id: "telephone_id",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
sortDescFirst: false, sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
Cell: ({ row }) => <TelephoneId user_id={row.original.id} />, Cell: ({ row }) => <TelephoneId user_id={row.original.id} />,
}, },
{ {
accessorKey: "is_online", accessorKey: "is_online",
header: "وضعیت برخط", header: "وضعیت برخط",
id: "is_online", id: "is_online",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "text", datatype: "text",
filterMode: "equals", filterMode: "equals",
sortDescFirst: false, sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"], columnFilterModeOptions: ["equals", "contains"],
grow: false, grow: false,
size: 100, size: 100,
Cell: ({ row }) => <OnlineCell user_id={row.original.id} />, Cell: ({ row }) => <OnlineCell user_id={row.original.id} />,
}, },
{ {
header: "استان", header: "استان",
id: "province_id", id: "province_id",
enableColumnFilter: true, enableColumnFilter: true,
datatype: "numeric", datatype: "numeric",
filterMode: "equals", filterMode: "equals",
grow: false, grow: false,
size: 130, size: 130,
ColumnSelectComponent: (props) => { ColumnSelectComponent: (props) => {
const { provinces, errorProvinces, loadingProvinces } = const { provinces, errorProvinces, loadingProvinces } = useProvinces();
useProvinces(); const getColumnSelectOptions = useMemo(() => {
const getColumnSelectOptions = useMemo(() => { if (loadingProvinces) {
if (loadingProvinces) { return [{ value: "loading", label: "در حال بارگذاری..." }];
return [{ value: "loading", label: "در حال بارگذاری..." }]; }
} if (errorProvinces) {
if (errorProvinces) { return [{ value: "error", label: "خطا در بارگذاری" }];
return [{ value: "error", label: "خطا در بارگذاری" }]; }
} return [
return [ { value: "", label: "کل کشور" },
{ value: "", label: "کل کشور" }, ...provinces.map((province) => ({
...provinces.map((province) => ({ value: province.id,
value: province.id, label: province.name,
label: province.name, })),
})), ];
]; }, [provinces, errorProvinces, loadingProvinces]);
}, [provinces, errorProvinces, loadingProvinces]); return (
return ( <CustomSelectByDependency
<CustomSelectByDependency {...props}
{...props} value={loadingProvinces ? "loading" : props.filterParameters.value}
value={ columnSelectOption={getColumnSelectOptions}
loadingProvinces ? "loading" : props.filterParameters.value />
} );
columnSelectOption={getColumnSelectOptions} },
/> Cell: ({ renderedCellValue, row }) => <>{row.original.province_fa}</>,
); },
}, {
Cell: ({ renderedCellValue, row }) => <>{row.original.province_fa}</>, accessorKey: "username",
}, header: "نام کاربری",
{ id: "username",
accessorKey: "username", enableColumnFilter: true,
header: "نام کاربری", datatype: "text",
id: "username", filterMode: "equals",
enableColumnFilter: true, sortDescFirst: false,
datatype: "text", columnFilterModeOptions: ["equals", "contains"],
filterMode: "equals", grow: false,
sortDescFirst: false, size: 100,
columnFilterModeOptions: ["equals", "contains"], },
grow: false, {
size: 100, accessorKey: "position",
}, header: "سمت",
{ id: "position",
accessorKey: "position", enableColumnFilter: false,
header: "سمت", grow: false,
id: "position", size: 100,
enableColumnFilter: false, },
grow: false, {
size: 100, accessorKey: "phone_number",
}, header: "شماره همراه",
{ id: "phone_number",
accessorKey: "phone_number", enableColumnFilter: false,
header: "شماره همراه", grow: false,
id: "phone_number", size: 100,
enableColumnFilter: false, },
grow: false, {
size: 100, header: "نقش",
}, id: "role__id",
{ enableColumnFilter: false,
header: "نقش", grow: false,
id: "role__id", size: 100,
enableColumnFilter: false, enableColumnFilter: true,
grow: false, datatype: "numeric",
size: 100, filterMode: "equals",
enableColumnFilter: true, grow: false,
datatype: "numeric", size: 130,
filterMode: "equals", ColumnSelectComponent: (props) => {
grow: false, const { roles, errorRoles, loadingRoles } = useRoles();
size: 130, const getColumnSelectOptions = useMemo(() => {
ColumnSelectComponent: (props) => { if (loadingRoles) {
const { roles, errorRoles, loadingRoles } = useRoles(); return [{ value: "loading", label: "در حال بارگذاری..." }];
const getColumnSelectOptions = useMemo(() => { }
if (loadingRoles) { if (errorRoles) {
return [{ value: "loading", label: "در حال بارگذاری..." }]; return [{ value: "error", label: "خطا در بارگذاری" }];
} }
if (errorRoles) { return [
return [{ value: "error", label: "خطا در بارگذاری" }]; { value: "", label: "همه نقش ها" },
} ...roles.map((role) => ({
return [ value: role.id,
{ value: "", label: "همه نقش ها" }, label: role.name_fa,
...roles.map((role) => ({ })),
value: role.id, ];
label: role.name_fa, }, [roles, errorRoles, loadingRoles]);
})), return (
]; <CustomSelectByDependency
}, [roles, errorRoles, loadingRoles]); {...props}
return ( value={loadingRoles ? "loading" : props.filterParameters.value}
<CustomSelectByDependency columnSelectOption={getColumnSelectOptions}
{...props} />
value={loadingRoles ? "loading" : props.filterParameters.value} );
columnSelectOption={getColumnSelectOptions} },
/> Cell: ({ row }) => <>{row.original.roles[0].name_fa}</>,
); },
}, {
Cell: ({ row }) => <>{row.original.roles[0].name_fa}</>, accessorKey: "national_id",
}, header: "کد ملی",
{ id: "national_id",
accessorKey: "national_id", enableColumnFilter: true,
header: "کد ملی", datatype: "text",
id: "national_id", filterMode: "equals",
enableColumnFilter: true, sortDescFirst: false,
datatype: "text", columnFilterModeOptions: ["equals", "contains"],
filterMode: "equals", grow: false,
sortDescFirst: false, size: 100,
columnFilterModeOptions: ["equals", "contains"], },
grow: false, {
size: 100, accessorKey: "gender",
}, header: "جنسیت",
{ id: "gender",
accessorKey: "gender", enableColumnFilter: false,
header: "جنسیت", grow: false,
id: "gender", size: 100,
enableColumnFilter: false, Cell: ({ renderedCellValue }) => <>{renderedCellValue == "male" ? "آقا" : "خانم"}</>,
grow: false, },
size: 100, ],
Cell: ({ renderedCellValue }) => ( []
<>{renderedCellValue == "male" ? "آقا" : "خانم"}</> );
),
},
],
[],
);
return ( return (
<> <>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}> <Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth <DataTableWithAuth
need_filter={true} need_filter={true}
columns={columns} columns={columns}
sorting={[{ id: "id", desc: true }]} sorting={[{ id: "id", desc: true }]}
table_url={GET_USER_LIST} table_url={GET_USER_LIST}
page_name={"usersPage"} page_name={"usersPage"}
table_name={"usersList"} table_name={"usersList"}
TableToolbar={Toolbar} TableToolbar={Toolbar}
enableRowActions enableRowActions
positionActionsColumn={"last"} positionActionsColumn={"last"}
RowActions={RowActions} RowActions={RowActions}
/> />
</Box> </Box>
</> </>
); );
}; };
export default DataTable; export default DataTable;

View File

@@ -1,56 +1,39 @@
import { DELETE_USER } from "@/core/utils/routes"; import { DELETE_USER } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest"; import useRequest from "@/lib/hooks/useRequest";
import { import { Button, DialogActions, DialogContent, Stack, Typography } from "@mui/material";
Button,
DialogActions,
DialogContent,
Stack,
Typography,
} from "@mui/material";
import { useState } from "react"; import { useState } from "react";
const DeleteContent = ({ rowId, mutate, setOpenDeleteDialog }) => { const DeleteContent = ({ rowId, mutate, setOpenDeleteDialog }) => {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const requestServer = useRequest({ notificationSuccess: true }); const requestServer = useRequest({ notificationSuccess: true });
const handleClick = async () => { const handleClick = async () => {
setSubmitting(true); setSubmitting(true);
try { try {
await requestServer(`${DELETE_USER}/${rowId}`, "delete"); await requestServer(`${DELETE_USER}/${rowId}`, "delete");
mutate(); mutate();
setOpenDeleteDialog(false); setOpenDeleteDialog(false);
} catch (error) { } catch (error) {
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
}; };
return ( return (
<> <>
<DialogContent> <DialogContent>
<Stack alignItems="center" spacing={2}> <Stack alignItems="center" spacing={2}>
<Typography mt={2}>آیا از حذف کاربر اطمینان دارید؟</Typography> <Typography mt={2}>آیا از حذف کاربر اطمینان دارید؟</Typography>
</Stack> </Stack>
</DialogContent> </DialogContent>
<DialogActions <DialogActions sx={{ justifyContent: "center", paddingBottom: 2, width: "100%" }}>
sx={{ justifyContent: "center", paddingBottom: 2, width: "100%" }} <Button onClick={() => setOpenDeleteDialog(false)} variant="outlined" color="secondary">
> خیر !
<Button </Button>
onClick={() => setOpenDeleteDialog(false)} <Button onClick={handleClick} variant="contained" color="error" disabled={submitting}>
variant="outlined" {submitting ? "درحال حذف..." : "بله اطمینان دارم"}
color="secondary" </Button>
> </DialogActions>
خیر ! </>
</Button> );
<Button
onClick={handleClick}
variant="contained"
color="error"
disabled={submitting}
>
{submitting ? "درحال حذف..." : "بله اطمینان دارم"}
</Button>
</DialogActions>
</>
);
}; };
export default DeleteContent; export default DeleteContent;

View File

@@ -3,35 +3,30 @@ import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
import DeleteContent from "./DeleteContent"; import DeleteContent from "./DeleteContent";
const Delete = ({ rowId, mutate }) => { const Delete = ({ rowId, mutate }) => {
const [openDeleteDialog, setOpenDeleteDialog] = useState(false); const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
return ( return (
<> <>
<Tooltip title="حذف"> <Tooltip title="حذف">
<IconButton color="error" onClick={() => setOpenDeleteDialog(true)}> <IconButton color="error" onClick={() => setOpenDeleteDialog(true)}>
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Dialog <Dialog
open={openDeleteDialog} open={openDeleteDialog}
onClose={() => setOpenDeleteDialog(false)} onClose={() => setOpenDeleteDialog(false)}
PaperProps={{ PaperProps={{
sx: { sx: {
boxShadow: boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
"rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px", },
}, }}
}} dir="rtl"
dir="rtl" maxWidth={"md"}
maxWidth={"md"} >
> <DialogTitle>حذف</DialogTitle>
<DialogTitle>حذف</DialogTitle> <DeleteContent rowId={rowId} mutate={mutate} setOpenDeleteDialog={setOpenDeleteDialog} />
<DeleteContent </Dialog>
rowId={rowId} </>
mutate={mutate} );
setOpenDeleteDialog={setOpenDeleteDialog}
/>
</Dialog>
</>
);
}; };
export default Delete; export default Delete;

View File

@@ -7,363 +7,357 @@ import useRequest from "@/lib/hooks/useRequest";
import useRoles from "@/lib/hooks/useRoles"; import useRoles from "@/lib/hooks/useRoles";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { import {
Button, Button,
Chip, Chip,
DialogActions, DialogActions,
DialogContent, DialogContent,
Divider, Divider,
FormControl, FormControl,
FormHelperText, FormHelperText,
Grid, Grid,
InputLabel, InputLabel,
MenuItem, MenuItem,
OutlinedInput, OutlinedInput,
Select, Select,
Stack, Stack,
TextField, TextField,
} from "@mui/material"; } from "@mui/material";
import { useMemo } from "react"; import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { object, string } from "yup"; import { object, string } from "yup";
const validationSchema = object().shape({ const validationSchema = object().shape({
full_name: string().required("نام و نام خانوادگی خود را وارد کنید"), full_name: string().required("نام و نام خانوادگی خود را وارد کنید"),
username: string().required("نام کاربری خود را وارد کنید"), username: string().required("نام کاربری خود را وارد کنید"),
phone_number: string() phone_number: string()
.matches(/^09\d{9}$/, "شماره همراه باید با 09 شروع شود و 11 رقم باشد.") .matches(/^09\d{9}$/, "شماره همراه باید با 09 شروع شود و 11 رقم باشد.")
.required("شماره همراه خود را وارد کنید"), .required("شماره همراه خود را وارد کنید"),
gender: string().required("جنسیت خود را وارد کنید"), gender: string().required("جنسیت خود را وارد کنید"),
national_id: string() national_id: string()
.test( .test("max", "کد ملی باید شامل 10 رقم باشد", (value) => value.toString().length === 10)
"max", .test("validation", "کد ملی صحیح نمی باشد", (value) => validateNationalCode(value))
"کد ملی باید شامل 10 رقم باشد", .required("کد ملی خود را وارد کنید"),
(value) => value.toString().length === 10, position: string().required("سمت خود را وارد کنید"),
) province_id: string().required("استان خود را وارد کنید"),
.test("validation", "کد ملی صحیح نمی باشد", (value) => role_id: string().required("نقش خود را وارد کنید"),
validateNationalCode(value),
)
.required("کد ملی خود را وارد کنید"),
position: string().required("سمت خود را وارد کنید"),
province_id: string().required("استان خود را وارد کنید"),
role_id: string().required("نقش خود را وارد کنید"),
}); });
const UpdateUserForm = ({ values, setOpen, mutate }) => { const UpdateUserForm = ({ values, setOpen, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true }); const requestServer = useRequest({ notificationSuccess: true });
const { provinces, errorProvinces, loadingProvinces } = useProvinces(); const { provinces, errorProvinces, loadingProvinces } = useProvinces();
const { roles, errorRoles, loadingRoles } = useRoles(); const { roles, errorRoles, loadingRoles } = useRoles();
const defaultValues = { const defaultValues = {
full_name: values.full_name, full_name: values.full_name,
username: values.username, username: values.username,
position: values.position, position: values.position,
gender: values.gender, gender: values.gender,
national_id: values.national_id, national_id: values.national_id,
phone_number: values.phone_number, phone_number: values.phone_number,
province_id: values.province_id, province_id: values.province_id,
role_id: values.roles[0].id, role_id: values.roles[0].id,
}; };
const provinceSelectOptions = useMemo(() => { const provinceSelectOptions = useMemo(() => {
if (loadingProvinces) { if (loadingProvinces) {
return [{ value: "loading", label: "در حال بارگذاری..." }]; return [{ value: "loading", label: "در حال بارگذاری..." }];
} }
if (errorProvinces) { if (errorProvinces) {
return [{ value: "error", label: "خطا در بارگذاری" }]; return [{ value: "error", label: "خطا در بارگذاری" }];
} }
return [ return [
{ value: "", label: "انتخاب استان" }, { value: "", label: "انتخاب استان" },
...provinces.map((province) => ({ ...provinces.map((province) => ({
value: province.id, value: province.id,
label: province.name, label: province.name,
})), })),
]; ];
}, [provinces, errorProvinces, loadingProvinces]); }, [provinces, errorProvinces, loadingProvinces]);
const roleSelectOptions = useMemo(() => { const roleSelectOptions = useMemo(() => {
if (loadingRoles) { if (loadingRoles) {
return [{ value: "loading", label: "در حال بارگذاری..." }]; return [{ value: "loading", label: "در حال بارگذاری..." }];
} }
if (errorRoles) { if (errorRoles) {
return [{ value: "error", label: "خطا در بارگذاری" }]; return [{ value: "error", label: "خطا در بارگذاری" }];
} }
return [ return [
{ value: "", label: "انتخاب نقش" }, { value: "", label: "انتخاب نقش" },
...roles.map((role) => ({ ...roles.map((role) => ({
value: role.id, value: role.id,
label: role.name_fa, label: role.name_fa,
})), })),
]; ];
}, [roles, errorRoles, loadingRoles]); }, [roles, errorRoles, loadingRoles]);
const { const {
control, control,
handleSubmit, handleSubmit,
formState: { isSubmitting, errors }, formState: { isSubmitting, errors },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) }); } = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => { const onSubmit = async (data) => {
const formData = new FormData(); const formData = new FormData();
Object.keys(data).forEach((key) => { Object.keys(data).forEach((key) => {
formData.append(key, data[key]); formData.append(key, data[key]);
}); });
try { try {
await requestServer(`${UPDATE_USER}/${values.id}`, "post", { await requestServer(`${UPDATE_USER}/${values.id}`, "post", {
data: formData, data: formData,
}); });
setOpen(false); setOpen(false);
mutate(); mutate();
} catch (error) {} } catch (error) {}
}; };
return ( return (
<> <>
<DialogContent dividers> <DialogContent dividers>
<Divider sx={{ mb: 3 }}> <Divider sx={{ mb: 3 }}>
<Chip color="primary" variant="outlined" label="کاربر جدید" /> <Chip color="primary" variant="outlined" label="کاربر جدید" />
</Divider> </Divider>
<StyledForm <StyledForm
id="UpdateUserForm" id="UpdateUserForm"
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
style={{ width: "100%", height: "100%" }} style={{ width: "100%", height: "100%" }}
> >
<Stack spacing={2}> <Stack spacing={2}>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => {
return ( return (
<LtrTextField <LtrTextField
{...field} {...field}
label="نام کاربری" label="نام کاربری"
variant="outlined" variant="outlined"
fullWidth fullWidth
error={!!error} error={!!error}
helperText={!!error && error.message} helperText={!!error && error.message}
/> />
); );
}} }}
name={"username"} name={"username"}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => {
return ( return (
<TextField <TextField
{...field} {...field}
label="نام و نام خانوادگی" label="نام و نام خانوادگی"
variant="outlined" variant="outlined"
fullWidth fullWidth
error={!!error} error={!!error}
helperText={!!error && error.message} helperText={!!error && error.message}
/> />
); );
}} }}
name={"full_name"} name={"full_name"}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => {
return ( return (
<TextField <TextField
{...field} {...field}
label="سمت" label="سمت"
variant="outlined" variant="outlined"
fullWidth fullWidth
error={!!error} error={!!error}
helperText={!!error && error.message} helperText={!!error && error.message}
/> />
); );
}} }}
name={"position"} name={"position"}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => {
return ( return (
<LtrTextField <LtrTextField
{...field} {...field}
label="شماره همراه" label="شماره همراه"
variant="outlined" variant="outlined"
fullWidth fullWidth
type="tel" type="tel"
onChange={(e) => { onChange={(e) => {
const inputValue = event.target.value; const inputValue = event.target.value;
if (isNaN(Number(inputValue))) { if (isNaN(Number(inputValue))) {
return; return;
} }
if (inputValue.length > 11) { if (inputValue.length > 11) {
return; return;
} }
field.onChange(inputValue); field.onChange(inputValue);
}} }}
error={!!error} error={!!error}
helperText={!!error && error.message} helperText={!!error && error.message}
/> />
); );
}} }}
name={"phone_number"} name={"phone_number"}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => {
return ( return (
<FormControl fullWidth error={!!error}> <FormControl fullWidth error={!!error}>
<InputLabel id={`label-phone-number`} shrink> <InputLabel id={`label-phone-number`} shrink>
جنسیت جنسیت
</InputLabel> </InputLabel>
<Select <Select
labelId={`label-phone-number`} labelId={`label-phone-number`}
id={"phone-number"} id={"phone-number"}
name={`gender`} name={`gender`}
value={field.value} value={field.value}
input={<OutlinedInput label={"جنسیت"} />} input={<OutlinedInput label={"جنسیت"} />}
onChange={(e) => field.onChange(e.target.value)} onChange={(e) => field.onChange(e.target.value)}
displayEmpty displayEmpty
> >
<MenuItem value={"male"}>مرد</MenuItem> <MenuItem value={"male"}>مرد</MenuItem>
<MenuItem value={"female"}>زن</MenuItem> <MenuItem value={"female"}>زن</MenuItem>
</Select> </Select>
<FormHelperText error={!!error} sx={{ mt: 1 }}> <FormHelperText error={!!error} sx={{ mt: 1 }}>
{!!error && error.message} {!!error && error.message}
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
); );
}} }}
name={"gender"} name={"gender"}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => {
return ( return (
<LtrTextField <LtrTextField
{...field} {...field}
label="شماره ملی" label="شماره ملی"
variant="outlined" variant="outlined"
fullWidth fullWidth
type="tel" type="tel"
onChange={(e) => { onChange={(e) => {
const inputValue = event.target.value; const inputValue = event.target.value;
if (isNaN(Number(inputValue))) { if (isNaN(Number(inputValue))) {
return; return;
} }
if (inputValue.length > 10) { if (inputValue.length > 10) {
return; return;
} }
field.onChange(inputValue); field.onChange(inputValue);
}} }}
error={!!error} error={!!error}
helperText={!!error && error.message} helperText={!!error && error.message}
/> />
); );
}} }}
name={"national_id"} name={"national_id"}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => {
return ( return (
<FormControl fullWidth error={!!error}> <FormControl fullWidth error={!!error}>
<InputLabel id={`label-province`} shrink> <InputLabel id={`label-province`} shrink>
استان استان
</InputLabel> </InputLabel>
<Select <Select
labelId={`label-province`} labelId={`label-province`}
id={"province"} id={"province"}
name={`province_id`} name={`province_id`}
value={loadingProvinces ? "loading" : field.value} value={loadingProvinces ? "loading" : field.value}
input={<OutlinedInput label={"استان"} />} input={<OutlinedInput label={"استان"} />}
onChange={(e) => field.onChange(e.target.value)} onChange={(e) => field.onChange(e.target.value)}
displayEmpty displayEmpty
> >
{provinceSelectOptions.map((option) => ( {provinceSelectOptions.map((option) => (
<MenuItem key={option.value} value={option.value}> <MenuItem key={option.value} value={option.value}>
{option.label} {option.label}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
<FormHelperText error={!!error} sx={{ mt: 1 }}> <FormHelperText error={!!error} sx={{ mt: 1 }}>
{!!error && error.message} {!!error && error.message}
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
); );
}} }}
name={"province_id"} name={"province_id"}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => {
return ( return (
<FormControl fullWidth error={!!error}> <FormControl fullWidth error={!!error}>
<InputLabel id={`label-role`} shrink> <InputLabel id={`label-role`} shrink>
نقش نقش
</InputLabel> </InputLabel>
<Select <Select
labelId={`label-role`} labelId={`label-role`}
id={"role"} id={"role"}
name={`role_id`} name={`role_id`}
value={loadingRoles ? "loading" : field.value} value={loadingRoles ? "loading" : field.value}
input={<OutlinedInput label={"نقش"} />} input={<OutlinedInput label={"نقش"} />}
onChange={(e) => field.onChange(e.target.value)} onChange={(e) => field.onChange(e.target.value)}
displayEmpty displayEmpty
> >
{roleSelectOptions.map((option) => ( {roleSelectOptions.map((option) => (
<MenuItem key={option.value} value={option.value}> <MenuItem key={option.value} value={option.value}>
{option.label} {option.label}
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
<FormHelperText error={!!error} sx={{ mt: 1 }}> <FormHelperText error={!!error} sx={{ mt: 1 }}>
{!!error && error.message} {!!error && error.message}
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
); );
}} }}
name={"role_id"} name={"role_id"}
/> />
</Grid> </Grid>
</Grid> </Grid>
</Stack> </Stack>
</StyledForm> </StyledForm>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
disabled={isSubmitting} disabled={isSubmitting}
variant="outlined" variant="outlined"
color="secondary" color="secondary"
size="large" size="large"
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
> >
بستن بستن
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
form="UpdateUserForm" form="UpdateUserForm"
size="large" size="large"
autoFocus autoFocus
variant="contained" variant="contained"
> >
{isSubmitting ? "در حال ویرایش کاربر..." : "ویرایش کاربر"} {isSubmitting ? "در حال ویرایش کاربر..." : "ویرایش کاربر"}
</Button> </Button>
</DialogActions> </DialogActions>
</> </>
); );
}; };
export default UpdateUserForm; export default UpdateUserForm;

View File

@@ -5,28 +5,23 @@ import { useState } from "react";
import UpdateUserForm from "./Form"; import UpdateUserForm from "./Form";
const UpdateUser = ({ values, mutate }) => { const UpdateUser = ({ values, mutate }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleOpen = () => { const handleOpen = () => {
setOpen(true); setOpen(true);
}; };
return ( return (
<> <>
<Tooltip title="ویرایش"> <Tooltip title="ویرایش">
<IconButton onClick={handleOpen}> <IconButton onClick={handleOpen}>
<Edit /> <Edit />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Dialog maxWidth="sm" fullWidth={true} open={open}> <Dialog maxWidth="sm" fullWidth={true} open={open}>
<UpdateUserForm <UpdateUserForm open={open} setOpen={setOpen} mutate={mutate} values={values} />
open={open} </Dialog>
setOpen={setOpen} </>
mutate={mutate} );
values={values}
/>
</Dialog>
</>
);
}; };
export default UpdateUser; export default UpdateUser;

View File

@@ -3,15 +3,11 @@ import { Power, PowerOff } from "@mui/icons-material";
import { Stack } from "@mui/material"; import { Stack } from "@mui/material";
const OnlineCell = ({ user_id }) => { const OnlineCell = ({ user_id }) => {
const { clientsOnline } = useSocket(); const { clientsOnline } = useSocket();
return ( return (
<Stack alignItems={"center"}> <Stack alignItems={"center"}>
{clientsOnline.some((co) => co.user_id == user_id) ? ( {clientsOnline.some((co) => co.user_id == user_id) ? <Power color="success" /> : <PowerOff color="error" />}
<Power color="success" /> </Stack>
) : ( );
<PowerOff color="error" />
)}
</Stack>
);
}; };
export default OnlineCell; export default OnlineCell;

View File

@@ -2,10 +2,8 @@ import { useSocket } from "@/lib/contexts/socket";
import { Stack } from "@mui/material"; import { Stack } from "@mui/material";
const TelephoneId = ({ user_id }) => { const TelephoneId = ({ user_id }) => {
const { clientsOnline } = useSocket(); const { clientsOnline } = useSocket();
const telephone_id = clientsOnline.find( const telephone_id = clientsOnline.find((co) => co.user_id == user_id)?.telephone_id;
(co) => co.user_id == user_id, return <Stack alignItems={"center"}>{telephone_id}</Stack>;
)?.telephone_id;
return <Stack alignItems={"center"}>{telephone_id}</Stack>;
}; };
export default TelephoneId; export default TelephoneId;

View File

@@ -2,11 +2,11 @@ import { Box } from "@mui/material";
import Delete from "./Delete"; import Delete from "./Delete";
import UpdateUser from "./Edit"; import UpdateUser from "./Edit";
const RowActions = ({ row, mutate }) => { const RowActions = ({ row, mutate }) => {
return ( return (
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}> <Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
<Delete rowId={row.original.id} mutate={mutate} /> <Delete rowId={row.original.id} mutate={mutate} />
<UpdateUser values={row.original} mutate={mutate} /> <UpdateUser values={row.original} mutate={mutate} />
</Box> </Box>
); );
}; };
export default RowActions; export default RowActions;

View File

@@ -4,22 +4,22 @@ import validateNationalCode from "@/core/utils/nationalCodeValidation";
import useProvinces from "@/lib/hooks/useProvince"; import useProvinces from "@/lib/hooks/useProvince";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { import {
Button, Button,
Chip, Chip,
DialogActions, DialogActions,
DialogContent, DialogContent,
Divider, Divider,
FormControl, FormControl,
FormHelperText, FormHelperText,
Grid, Grid,
IconButton, IconButton,
InputAdornment, InputAdornment,
InputLabel, InputLabel,
MenuItem, MenuItem,
OutlinedInput, OutlinedInput,
Select, Select,
Stack, Stack,
TextField, TextField,
} from "@mui/material"; } from "@mui/material";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { object, string } from "yup"; import { object, string } from "yup";
@@ -30,387 +30,375 @@ import { CREATE_USER } from "@/core/utils/routes";
import { Visibility, VisibilityOff } from "@mui/icons-material"; import { Visibility, VisibilityOff } from "@mui/icons-material";
const defaultValues = { const defaultValues = {
full_name: "", full_name: "",
username: "", username: "",
position: "", position: "",
gender: "male", gender: "male",
national_id: "", national_id: "",
phone_number: "", phone_number: "",
province_id: "", province_id: "",
password: "", password: "",
role_id: "", role_id: "",
telephone_id: "",
}; };
const validationSchema = object().shape({ const validationSchema = object().shape({
full_name: string().required("نام و نام خانوادگی خود را وارد کنید"), full_name: string().required("نام و نام خانوادگی خود را وارد کنید"),
username: string().required("نام کاربری خود را وارد کنید"), username: string().required("نام کاربری خود را وارد کنید"),
phone_number: string() phone_number: string()
.matches(/^09\d{9}$/, "شماره همراه باید با 09 شروع شود و 11 رقم باشد.") .matches(/^09\d{9}$/, "شماره همراه باید با 09 شروع شود و 11 رقم باشد.")
.required("شماره همراه خود را وارد کنید"), .required("شماره همراه خود را وارد کنید"),
gender: string().required("جنسیت خود را وارد کنید"), gender: string().required("جنسیت خود را وارد کنید"),
national_id: string() national_id: string()
.test( .test("max", "کد ملی باید شامل 10 رقم باشد", (value) => value.toString().length === 10)
"max", .test("validation", "کد ملی صحیح نمی باشد", (value) => validateNationalCode(value))
"کد ملی باید شامل 10 رقم باشد", .required("کد ملی خود را وارد کنید"),
(value) => value.toString().length === 10, password: string()
) .required("رمز عبور خود را وارد کنید")
.test("validation", "کد ملی صحیح نمی باشد", (value) => .matches(/^(?=.*[a-zA-Z])(?=.*\d).{8,}$/, "رمز عبور باید حداقل 8 کاراکتر و متشکل از عدد، حرف یا سمبل باشد"),
validateNationalCode(value), position: string().required("سمت خود را وارد کنید"),
) province_id: string().required("استان خود را وارد کنید"),
.required("کد ملی خود را وارد کنید"), role_id: string().required("نقش خود را وارد کنید"),
password: string()
.required("رمز عبور خود را وارد کنید")
.matches(
/^(?=.*[a-zA-Z])(?=.*\d).{8,}$/,
"رمز عبور باید حداقل 8 کاراکتر و متشکل از عدد، حرف یا سمبل باشد",
),
position: string().required("سمت خود را وارد کنید"),
province_id: string().required("استان خود را وارد کنید"),
role_id: string().required("نقش خود را وارد کنید"),
}); });
const CreateUserForm = ({ setOpen, mutate }) => { const CreateUserForm = ({ setOpen, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true }); const requestServer = useRequest({ notificationSuccess: true });
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const { provinces, errorProvinces, loadingProvinces } = useProvinces(); const { provinces, errorProvinces, loadingProvinces } = useProvinces();
const { roles, errorRoles, loadingRoles } = useRoles(); const { roles, errorRoles, loadingRoles } = useRoles();
const handleClickShowPassword = () => setShowPassword((show) => !show); const handleClickShowPassword = () => setShowPassword((show) => !show);
const provinceSelectOptions = useMemo(() => { const provinceSelectOptions = useMemo(() => {
if (loadingProvinces) { if (loadingProvinces) {
return [{ value: "loading", label: "در حال بارگذاری..." }]; return [{ value: "loading", label: "در حال بارگذاری..." }];
} }
if (errorProvinces) { if (errorProvinces) {
return [{ value: "error", label: "خطا در بارگذاری" }]; return [{ value: "error", label: "خطا در بارگذاری" }];
} }
return [ return [
{ value: "", label: "انتخاب استان" }, { value: "", label: "انتخاب استان" },
...provinces.map((province) => ({ ...provinces.map((province) => ({
value: province.id, value: province.id,
label: province.name, label: province.name,
})), })),
]; ];
}, [provinces, errorProvinces, loadingProvinces]); }, [provinces, errorProvinces, loadingProvinces]);
const roleSelectOptions = useMemo(() => { const roleSelectOptions = useMemo(() => {
if (loadingRoles) { if (loadingRoles) {
return [{ value: "loading", label: "در حال بارگذاری..." }]; return [{ value: "loading", label: "در حال بارگذاری..." }];
} }
if (errorRoles) { if (errorRoles) {
return [{ value: "error", label: "خطا در بارگذاری" }]; return [{ value: "error", label: "خطا در بارگذاری" }];
} }
return [ return [
{ value: "", label: "انتخاب نقش" }, { value: "", label: "انتخاب نقش" },
...roles.map((role) => ({ ...roles.map((role) => ({
value: role.id, value: role.id,
label: role.name_fa, label: role.name_fa,
})), })),
]; ];
}, [roles, errorRoles, loadingRoles]); }, [roles, errorRoles, loadingRoles]);
const { const {
control, control,
handleSubmit, handleSubmit,
formState: { isSubmitting, errors }, formState: { isSubmitting, errors },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) }); } = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => { const onSubmit = async (data) => {
const formData = new FormData(); const formData = new FormData();
Object.keys(data).forEach((key) => { Object.keys(data).forEach((key) => {
formData.append(key, data[key]); formData.append(key, data[key]);
}); });
try { try {
await requestServer(CREATE_USER, "post", { await requestServer(CREATE_USER, "post", {
data: formData, data: formData,
}); });
setOpen(false); setOpen(false);
mutate(); mutate();
} catch (error) {} } catch (error) {}
}; };
return ( return (
<> <>
<DialogContent dividers> <DialogContent dividers>
<Divider sx={{ mb: 3 }}> <Divider sx={{ mb: 3 }}>
<Chip color="primary" variant="outlined" label="کاربر جدید" /> <Chip color="primary" variant="outlined" label="کاربر جدید" />
</Divider> </Divider>
<StyledForm <StyledForm
id="createUserForm" id="createUserForm"
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
style={{ width: "100%", height: "100%" }} style={{ width: "100%", height: "100%" }}
> >
<Stack spacing={2}> <Stack spacing={2}>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => {
return ( return (
<LtrTextField <LtrTextField
{...field} {...field}
label="نام کاربری" label="نام کاربری"
variant="outlined" variant="outlined"
fullWidth fullWidth
error={!!error} error={!!error}
helperText={!!error && error.message} helperText={!!error && error.message}
/> />
); );
}} }}
name={"username"} name={"username"}
/> />
</Grid> </Grid>
<Grid size={{ xs: 12, md: 6 }}> <Grid size={{ xs: 12, md: 6 }}>
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => {
return ( return (
<LtrTextField <LtrTextField
{...field} {...field}
label="رمز عبور" label="رمز عبور"
variant="outlined" variant="outlined"
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
slotProps={{ slotProps={{
input: { input: {
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
<IconButton onClick={handleClickShowPassword}> <IconButton onClick={handleClickShowPassword}>
{showPassword ? ( {showPassword ? <VisibilityOff /> : <Visibility />}
<VisibilityOff /> </IconButton>
) : ( </InputAdornment>
<Visibility /> ),
)} },
</IconButton> }}
</InputAdornment> fullWidth
), error={!!error}
}, helperText={!!error && error.message}
}} />
fullWidth );
error={!!error} }}
helperText={!!error && error.message} name={"password"}
/> />
); </Grid>
}} <Grid size={{ xs: 12, md: 6 }}>
name={"password"} <Controller
/> control={control}
</Grid> render={({ field, fieldState: { error } }) => {
<Grid size={{ xs: 12, md: 6 }}> return (
<Controller <TextField
control={control} {...field}
render={({ field, fieldState: { error } }) => { label="نام و نام خانوادگی"
return ( variant="outlined"
<TextField fullWidth
{...field} error={!!error}
label="نام و نام خانوادگی" helperText={!!error && error.message}
variant="outlined" />
fullWidth );
error={!!error} }}
helperText={!!error && error.message} name={"full_name"}
/> />
); </Grid>
}} <Grid size={{ xs: 12, md: 6 }}>
name={"full_name"} <Controller
/> control={control}
</Grid> render={({ field, fieldState: { error } }) => {
<Grid size={{ xs: 12, md: 6 }}> return (
<Controller <TextField
control={control} {...field}
render={({ field, fieldState: { error } }) => { label="سمت"
return ( variant="outlined"
<TextField fullWidth
{...field} error={!!error}
label="سمت" helperText={!!error && error.message}
variant="outlined" />
fullWidth );
error={!!error} }}
helperText={!!error && error.message} name={"position"}
/> />
); </Grid>
}} <Grid size={{ xs: 12, md: 6 }}>
name={"position"} <Controller
/> control={control}
</Grid> render={({ field, fieldState: { error } }) => {
<Grid size={{ xs: 12, md: 6 }}> return (
<Controller <LtrTextField
control={control} {...field}
render={({ field, fieldState: { error } }) => { label="شماره همراه"
return ( variant="outlined"
<LtrTextField fullWidth
{...field} type="tel"
label="شماره همراه" onChange={(e) => {
variant="outlined" const inputValue = event.target.value;
fullWidth if (isNaN(Number(inputValue))) {
type="tel" return;
onChange={(e) => { }
const inputValue = event.target.value; if (inputValue.length > 11) {
if (isNaN(Number(inputValue))) { return;
return; }
} field.onChange(inputValue);
if (inputValue.length > 11) { }}
return; error={!!error}
} helperText={!!error && error.message}
field.onChange(inputValue); />
}} );
error={!!error} }}
helperText={!!error && error.message} name={"phone_number"}
/> />
); </Grid>
}} <Grid size={{ xs: 12, md: 6 }}>
name={"phone_number"} <Controller
/> control={control}
</Grid> render={({ field, fieldState: { error } }) => {
<Grid size={{ xs: 12, md: 6 }}> return (
<Controller <FormControl fullWidth error={!!error}>
control={control} <InputLabel id={`label-phone-number`} shrink>
render={({ field, fieldState: { error } }) => { جنسیت
return ( </InputLabel>
<FormControl fullWidth error={!!error}> <Select
<InputLabel id={`label-phone-number`} shrink> labelId={`label-phone-number`}
جنسیت id={"phone-number"}
</InputLabel> name={`gender`}
<Select value={field.value}
labelId={`label-phone-number`} input={<OutlinedInput label={"جنسیت"} />}
id={"phone-number"} onChange={(e) => field.onChange(e.target.value)}
name={`gender`} displayEmpty
value={field.value} >
input={<OutlinedInput label={"جنسیت"} />} <MenuItem value={"male"}>مرد</MenuItem>
onChange={(e) => field.onChange(e.target.value)} <MenuItem value={"female"}>زن</MenuItem>
displayEmpty </Select>
> <FormHelperText error={!!error} sx={{ mt: 1 }}>
<MenuItem value={"male"}>مرد</MenuItem> {!!error && error.message}
<MenuItem value={"female"}>زن</MenuItem> </FormHelperText>
</Select> </FormControl>
<FormHelperText error={!!error} sx={{ mt: 1 }}> );
{!!error && error.message} }}
</FormHelperText> name={"gender"}
</FormControl> />
); </Grid>
}} <Grid size={{ xs: 12, md: 6 }}>
name={"gender"} <Controller
/> control={control}
</Grid> render={({ field, fieldState: { error } }) => {
<Grid size={{ xs: 12, md: 6 }}> return (
<Controller <LtrTextField
control={control} {...field}
render={({ field, fieldState: { error } }) => { label="شماره ملی"
return ( variant="outlined"
<LtrTextField fullWidth
{...field} type="tel"
label="شماره ملی" onChange={(e) => {
variant="outlined" const inputValue = event.target.value;
fullWidth if (isNaN(Number(inputValue))) {
type="tel" return;
onChange={(e) => { }
const inputValue = event.target.value; if (inputValue.length > 10) {
if (isNaN(Number(inputValue))) { return;
return; }
} field.onChange(inputValue);
if (inputValue.length > 10) { }}
return; error={!!error}
} helperText={!!error && error.message}
field.onChange(inputValue); />
}} );
error={!!error} }}
helperText={!!error && error.message} name={"national_id"}
/> />
); </Grid>
}} <Grid size={{ xs: 12, md: 6 }}>
name={"national_id"} <Controller
/> control={control}
</Grid> render={({ field, fieldState: { error } }) => {
<Grid size={{ xs: 12, md: 6 }}> return (
<Controller <FormControl fullWidth error={!!error}>
control={control} <InputLabel id={`label-province`} shrink>
render={({ field, fieldState: { error } }) => { استان
return ( </InputLabel>
<FormControl fullWidth error={!!error}> <Select
<InputLabel id={`label-province`} shrink> labelId={`label-province`}
استان id={"province"}
</InputLabel> name={`province_id`}
<Select value={field.value}
labelId={`label-province`} input={<OutlinedInput label={"استان"} />}
id={"province"} onChange={(e) => field.onChange(e.target.value)}
name={`province_id`} displayEmpty
value={field.value} >
input={<OutlinedInput label={"استان"} />} {provinceSelectOptions.map((option) => (
onChange={(e) => field.onChange(e.target.value)} <MenuItem key={option.value} value={option.value}>
displayEmpty {option.label}
> </MenuItem>
{provinceSelectOptions.map((option) => ( ))}
<MenuItem key={option.value} value={option.value}> </Select>
{option.label} <FormHelperText error={!!error} sx={{ mt: 1 }}>
</MenuItem> {!!error && error.message}
))} </FormHelperText>
</Select> </FormControl>
<FormHelperText error={!!error} sx={{ mt: 1 }}> );
{!!error && error.message} }}
</FormHelperText> name={"province_id"}
</FormControl> />
); </Grid>
}} <Grid size={{ xs: 12, md: 6 }}>
name={"province_id"} <Controller
/> control={control}
</Grid> render={({ field, fieldState: { error } }) => {
<Grid size={{ xs: 12, md: 6 }}> return (
<Controller <FormControl fullWidth error={!!error}>
control={control} <InputLabel id={`label-role`} shrink>
render={({ field, fieldState: { error } }) => { نقش
return ( </InputLabel>
<FormControl fullWidth error={!!error}> <Select
<InputLabel id={`label-role`} shrink> labelId={`label-role`}
نقش id={"role"}
</InputLabel> name={`role_id`}
<Select value={field.value}
labelId={`label-role`} input={<OutlinedInput label={"نقش"} />}
id={"role"} onChange={(e) => field.onChange(e.target.value)}
name={`role_id`} displayEmpty
value={field.value} >
input={<OutlinedInput label={"نقش"} />} {roleSelectOptions.map((option) => (
onChange={(e) => field.onChange(e.target.value)} <MenuItem key={option.value} value={option.value}>
displayEmpty {option.label}
> </MenuItem>
{roleSelectOptions.map((option) => ( ))}
<MenuItem key={option.value} value={option.value}> </Select>
{option.label} <FormHelperText error={!!error} sx={{ mt: 1 }}>
</MenuItem> {!!error && error.message}
))} </FormHelperText>
</Select> </FormControl>
<FormHelperText error={!!error} sx={{ mt: 1 }}> );
{!!error && error.message} }}
</FormHelperText> name={"role_id"}
</FormControl> />
); </Grid>
}} </Grid>
name={"role_id"} </Stack>
/> </StyledForm>
</Grid> </DialogContent>
</Grid> <DialogActions>
</Stack> <Button
</StyledForm> disabled={isSubmitting}
</DialogContent> variant="outlined"
<DialogActions> color="secondary"
<Button size="large"
disabled={isSubmitting} onClick={() => setOpen(false)}
variant="outlined" >
color="secondary" بستن
size="large" </Button>
onClick={() => setOpen(false)} <Button
> type="submit"
بستن disabled={isSubmitting}
</Button> form="createUserForm"
<Button size="large"
type="submit" autoFocus
disabled={isSubmitting} variant="contained"
form="createUserForm" >
size="large" {isSubmitting ? "در حال ثبت کاربر جدید..." : "ثبت کاربر جدید"}
autoFocus </Button>
variant="contained" </DialogActions>
> </>
{isSubmitting ? "در حال ثبت کاربر جدید..." : "ثبت کاربر جدید"} );
</Button>
</DialogActions>
</>
);
}; };
export default CreateUserForm; export default CreateUserForm;

View File

@@ -1,49 +1,39 @@
"use client"; "use client";
import { AddCircle } from "@mui/icons-material"; import { AddCircle } from "@mui/icons-material";
import { import { Button, Dialog, IconButton, useMediaQuery, useTheme } from "@mui/material";
Button,
Dialog,
IconButton,
useMediaQuery,
useTheme,
} from "@mui/material";
import CreateUserForm from "./Form"; import CreateUserForm from "./Form";
import { useState } from "react"; import { useState } from "react";
const CreateUser = ({ mutate }) => { const CreateUser = ({ mutate }) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("sm")); const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleOpen = () => { const handleOpen = () => {
setOpen(true); setOpen(true);
}; };
return ( return (
<> <>
{isMobile ? ( {isMobile ? (
<IconButton <IconButton aria-label="کاربر جدید" color="primary" onClick={handleOpen}>
aria-label="کاربر جدید" <AddCircle sx={{ fontSize: "25px" }} />
color="primary" </IconButton>
onClick={handleOpen} ) : (
> <Button
<AddCircle sx={{ fontSize: "25px" }} /> size={"small"}
</IconButton> variant="contained"
) : ( color="primary"
<Button startIcon={<AddCircle />}
size={"small"} onClick={handleOpen}
variant="contained" >
color="primary" کاربر جدید
startIcon={<AddCircle />} </Button>
onClick={handleOpen} )}
> <Dialog maxWidth="sm" fullWidth={true} open={open}>
کاربر جدید <CreateUserForm open={open} setOpen={setOpen} mutate={mutate} />
</Button> </Dialog>
)} </>
<Dialog maxWidth="sm" fullWidth={true} open={open}> );
<CreateUserForm open={open} setOpen={setOpen} mutate={mutate} />
</Dialog>
</>
);
}; };
export default CreateUser; export default CreateUser;

View File

@@ -1,10 +1,10 @@
import CreateUser from "./Create"; import CreateUser from "./Create";
const Toolbar = ({ mutate }) => { const Toolbar = ({ mutate }) => {
return ( return (
<> <>
<CreateUser mutate={mutate} /> <CreateUser mutate={mutate} />
</> </>
); );
}; };
export default Toolbar; export default Toolbar;

View File

@@ -3,12 +3,12 @@ import { Stack } from "@mui/material";
import DataTable from "./DataTable"; import DataTable from "./DataTable";
const UsersPage = () => { const UsersPage = () => {
return ( return (
<Stack spacing={1}> <Stack spacing={1}>
<PageTitle title={"کاربران"} /> <PageTitle title={"کاربران"} />
<DataTable /> <DataTable />
</Stack> </Stack>
); );
}; };
export default UsersPage; export default UsersPage;

View File

@@ -3,10 +3,6 @@ import { Stack } from "@mui/material";
const OnlineUsersReport = () => { const OnlineUsersReport = () => {
const { clientsOnline } = useSocket(); const { clientsOnline } = useSocket();
return ( return <Stack>clientsOnline: {clientsOnline.length}</Stack>;
<Stack> };
clientsOnline: {clientsOnline.length} export default OnlineUsersReport;
</Stack>
)
}
export default OnlineUsersReport;

View File

@@ -1,4 +1,4 @@
"use client" "use client";
import { Stack } from "@mui/material"; import { Stack } from "@mui/material";
import OnlineUsersReport from "./OnlineUsersReport"; import OnlineUsersReport from "./OnlineUsersReport";
@@ -7,6 +7,6 @@ const DashboardPage = () => {
<Stack> <Stack>
<OnlineUsersReport /> <OnlineUsersReport />
</Stack> </Stack>
) );
} };
export default DashboardPage; export default DashboardPage;

View File

@@ -1,60 +1,45 @@
import { ExpandLess, ExpandMore, Link } from "@mui/icons-material"; import { ExpandLess, ExpandMore, Link } from "@mui/icons-material";
import { import { Button, Divider, ListItemIcon, ListItemText, Menu, MenuItem, Stack } from "@mui/material";
Button,
Divider,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
} from "@mui/material";
import NextLink from "next/link"; import NextLink from "next/link";
import { useState } from "react"; import { useState } from "react";
const HeaderMenu = ({ menu }) => { const HeaderMenu = ({ menu }) => {
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const handleClick = (event) => { const handleClick = (event) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
}; };
const handleClose = () => { const handleClose = () => {
setAnchorEl(null); setAnchorEl(null);
}; };
return ( return (
<Stack sx={{ mr: 0.5 }}> <Stack sx={{ mr: 0.5 }}>
<Button <Button
color="inherit" color="inherit"
aria-haspopup="true" aria-haspopup="true"
aria-expanded={open ? "true" : undefined} aria-expanded={open ? "true" : undefined}
onClick={handleClick} onClick={handleClick}
endIcon={open ? <ExpandLess /> : <ExpandMore />} endIcon={open ? <ExpandLess /> : <ExpandMore />}
>
{menu.title}
</Button>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
{menu.subMenu.flatMap((subMenu, index) => [
...subMenu.map((sub) => (
<MenuItem
component={NextLink}
href={sub.href}
key={sub.title}
onClick={handleClose}
> >
<ListItemIcon> {menu.title}
<Link fontSize="small" /> </Button>
</ListItemIcon> <Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<ListItemText>{sub.title}</ListItemText> {menu.subMenu.flatMap((subMenu, index) => [
</MenuItem> ...subMenu.map((sub) => (
)), <MenuItem component={NextLink} href={sub.href} key={sub.title} onClick={handleClose}>
index < menu.subMenu.length - 1 && ( <ListItemIcon>
<Divider key={`divider-${index}`} /> <Link fontSize="small" />
), </ListItemIcon>
])} <ListItemText>{sub.title}</ListItemText>
</Menu> </MenuItem>
</Stack> )),
); index < menu.subMenu.length - 1 && <Divider key={`divider-${index}`} />,
])}
</Menu>
</Stack>
);
}; };
export default HeaderMenu; export default HeaderMenu;

View File

@@ -4,16 +4,16 @@ import { Chip } from "@mui/material";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
const SidebarBadge = ({ badge, chipProps = {} }) => { const SidebarBadge = ({ badge, chipProps = {} }) => {
const { data } = useSidebarBadge(); const { data } = useSidebarBadge();
const [value, setValue] = useState(); const [value, setValue] = useState();
useEffect(() => { useEffect(() => {
setValue(getValueByPath(data, badge)); setValue(getValueByPath(data, badge));
}, [data, badge]); }, [data, badge]);
if (!data) return null; if (!data) return null;
if (!value) return null; if (!value) return null;
return <Chip label={value.toLocaleString()} size="small" {...chipProps} />; return <Chip label={value.toLocaleString()} size="small" {...chipProps} />;
}; };
export default SidebarBadge; export default SidebarBadge;

View File

@@ -1,92 +1,67 @@
import { ExpandLess, ExpandMore } from "@mui/icons-material"; import { ExpandLess, ExpandMore } from "@mui/icons-material";
import { import { Collapse, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Stack } from "@mui/material";
Collapse,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Stack,
} from "@mui/material";
import Link from "next/link"; import Link from "next/link";
import SidebarBadge from "./SidebarBadge"; import SidebarBadge from "./SidebarBadge";
import SidebarSubitems from "./SidebarSubitems"; import SidebarSubitems from "./SidebarSubitems";
const SidebarListItems = ({ menuItem, dispatch }) => { const SidebarListItems = ({ menuItem, dispatch }) => {
return ( return (
<> <>
<ListItem disablePadding divider> <ListItem disablePadding divider>
<ListItemButton <ListItemButton
sx={{ p: 0.5 }} sx={{ p: 0.5 }}
disableGutters disableGutters
selected={menuItem.selected} selected={menuItem.selected}
component={menuItem.type === "page" ? Link : null} component={menuItem.type === "page" ? Link : null}
href={menuItem.type === "page" ? menuItem.route : null} href={menuItem.type === "page" ? menuItem.route : null}
onClick={() => { onClick={() => {
if (menuItem.type !== "page") { if (menuItem.type !== "page") {
dispatch({ type: "COLLAPSE_MENU", id: menuItem.id }); dispatch({ type: "COLLAPSE_MENU", id: menuItem.id });
} }
}} }}
> >
<ListItemIcon <ListItemIcon
sx={{ sx={{
minWidth: 0, minWidth: 0,
justifyContent: "center", justifyContent: "center",
color: "primary.main", color: "primary.main",
width: 40, width: 40,
height: 24, height: 24,
}} }}
> >
{menuItem.icon} {menuItem.icon}
</ListItemIcon> </ListItemIcon>
<ListItemText primary={menuItem.label} /> <ListItemText primary={menuItem.label} />
{menuItem.badges && ( {menuItem.badges && (
<Stack direction={"row"} sx={{ px: 0.5 }} spacing={0.5}> <Stack direction={"row"} sx={{ px: 0.5 }} spacing={0.5}>
{menuItem.badges.map((badge, i) => ( {menuItem.badges.map((badge, i) => (
<SidebarBadge <SidebarBadge
chipProps={{ color: badge.color || "primary" }} chipProps={{ color: badge.color || "primary" }}
key={badge.key} key={badge.key}
badge={badge.key} badge={badge.key}
/> />
))} ))}
</Stack> </Stack>
)} )}
{menuItem.hasSubitems ? ( {menuItem.hasSubitems ? menuItem.showSubitems ? <ExpandLess /> : <ExpandMore /> : null}
menuItem.showSubitems ? ( </ListItemButton>
<ExpandLess /> </ListItem>
) : ( <Collapse in={menuItem.showSubitems} timeout="auto" mountOnEnter={true} unmountOnExit={true}>
<ExpandMore /> {menuItem.hasSubitems ? (
) <List
) : null} disablePadding
</ListItemButton> dense
</ListItem> sx={{
<Collapse background: "#00000008",
in={menuItem.showSubitems} }}
timeout="auto" >
mountOnEnter={true} {menuItem.Subitems.map((subitem, index) => {
unmountOnExit={true} return <SidebarSubitems key={index} dispatch={dispatch} subitem={subitem} />;
> })}
{menuItem.hasSubitems ? ( </List>
<List ) : null}
disablePadding </Collapse>
dense </>
sx={{ );
background: "#00000008",
}}
>
{menuItem.Subitems.map((subitem, index) => {
return (
<SidebarSubitems
key={index}
dispatch={dispatch}
subitem={subitem}
/>
);
})}
</List>
) : null}
</Collapse>
</>
);
}; };
export default SidebarListItems; export default SidebarListItems;

View File

@@ -9,120 +9,106 @@ import SidebarListItems from "./SidebarListItems";
import Profile from "@/core/components/Profile"; import Profile from "@/core/components/Profile";
function selectPage(item, route) { function selectPage(item, route) {
if (item.type === "page") { if (item.type === "page") {
return { return {
...item, ...item,
selected: item.route === route, selected: item.route === route,
showSubitems: item.route === route, showSubitems: item.route === route,
}; };
} else if (item.Subitems && Array.isArray(item.Subitems)) { } else if (item.Subitems && Array.isArray(item.Subitems)) {
const updatedSubitems = item.Subitems.map((subitem) => const updatedSubitems = item.Subitems.map((subitem) => selectPage(subitem, route));
selectPage(subitem, route), return {
); ...item,
return { Subitems: updatedSubitems,
...item, showSubitems: updatedSubitems.some((subitem) => subitem.showSubitems || subitem.route === route),
Subitems: updatedSubitems, };
showSubitems: updatedSubitems.some( }
(subitem) => subitem.showSubitems || subitem.route === route, return item;
),
};
}
return item;
} }
function toggleSubitems(items, actionId) { function toggleSubitems(items, actionId) {
return items.map((item) => return items.map((item) =>
actionId === item.id actionId === item.id ? { ...item, showSubitems: !item.showSubitems } : { ...item, showSubitems: false }
? { ...item, showSubitems: !item.showSubitems } );
: { ...item, showSubitems: false },
);
} }
function reducer(state, action) { function reducer(state, action) {
switch (action.type) { switch (action.type) {
case "UPDATE_MENU": case "UPDATE_MENU":
const _permissions = action.permissions || []; const _permissions = action.permissions || [];
return filterMenuItems(state, ["all", ..._permissions]); return filterMenuItems(state, ["all", ..._permissions]);
case "COLLAPSE_MENU": case "COLLAPSE_MENU":
return state.map((item) => ({ return state.map((item) => ({
...item, ...item,
showSubitems: item.id === action.id ? !item.showSubitems : false, showSubitems: item.id === action.id ? !item.showSubitems : false,
})); }));
case "COLLAPSE_SUB_ITEMS": case "COLLAPSE_SUB_ITEMS":
return state.map((item) => return state.map((item) =>
item.hasSubitems item.hasSubitems ? { ...item, Subitems: toggleSubitems(item.Subitems, action.id) } : item
? { ...item, Subitems: toggleSubitems(item.Subitems, action.id) } );
: item,
);
case "COLLAPSE_SUB_SECOND_ITEMS": case "COLLAPSE_SUB_SECOND_ITEMS":
return state.map((item) => return state.map((item) =>
item.hasSubitems item.hasSubitems
? { ? {
...item, ...item,
Subitems: item.Subitems.map((subitem) => Subitems: item.Subitems.map((subitem) =>
subitem.hasSubitems subitem.hasSubitems
? { ? {
...subitem, ...subitem,
Subitems: toggleSubitems(subitem.Subitems, action.id), Subitems: toggleSubitems(subitem.Subitems, action.id),
} }
: subitem, : subitem
), ),
} }
: item, : item
); );
case "SELECTED": case "SELECTED":
return state.map((item) => selectPage(item, action.route)); return state.map((item) => selectPage(item, action.route));
default: default:
throw new Error(); throw new Error();
} }
} }
const SidebarMenu = () => { const SidebarMenu = () => {
const { data: userPermissions } = usePermissions(); const { data: userPermissions } = usePermissions();
const [menuItems, dispatch] = useReducer(reducer, pageMenu); const [menuItems, dispatch] = useReducer(reducer, pageMenu);
const pathname = usePathname(); const pathname = usePathname();
const [selectedKey, setSelectedKey] = useState(null); const [selectedKey, setSelectedKey] = useState(null);
useEffect(() => { useEffect(() => {
dispatch({ type: "SELECTED", route: pathname }); dispatch({ type: "SELECTED", route: pathname });
setSelectedKey(pathname); setSelectedKey(pathname);
}, [userPermissions, pathname]); }, [userPermissions, pathname]);
useEffect(() => { useEffect(() => {
if (!userPermissions) return; if (!userPermissions) return;
dispatch({ type: "UPDATE_MENU", permissions: userPermissions }); dispatch({ type: "UPDATE_MENU", permissions: userPermissions });
}, [userPermissions]); }, [userPermissions]);
useEffect(() => { useEffect(() => {
selectedKey && selectedKey &&
document.querySelector(`[data-route="${selectedKey}"]`)?.scrollIntoView({ document.querySelector(`[data-route="${selectedKey}"]`)?.scrollIntoView({
behavior: "smooth", behavior: "smooth",
block: "center", block: "center",
}); });
}, [selectedKey]); }, [selectedKey]);
return ( return (
<> <>
<Profile /> <Profile />
{userPermissions && ( {userPermissions && (
<List sx={{ overflow: "auto", flex: 1 }} disablePadding dense> <List sx={{ overflow: "auto", flex: 1 }} disablePadding dense>
{menuItems.map((menuItem) => { {menuItems.map((menuItem) => {
return ( return <SidebarListItems dispatch={dispatch} key={menuItem.id} menuItem={menuItem} />;
<SidebarListItems })}
dispatch={dispatch} </List>
key={menuItem.id} )}
menuItem={menuItem} </>
/> );
);
})}
</List>
)}
</>
);
}; };
export default SidebarMenu; export default SidebarMenu;

View File

@@ -1,96 +1,76 @@
import { ExpandLess, ExpandMore } from "@mui/icons-material"; import { ExpandLess, ExpandMore } from "@mui/icons-material";
import { import { Box, Collapse, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Stack } from "@mui/material";
Box,
Collapse,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Stack,
} from "@mui/material";
import Link from "next/link"; import Link from "next/link";
import SidebarBadge from "./SidebarBadge"; import SidebarBadge from "./SidebarBadge";
const SidebarSubitems = ({ subitem, dispatch }) => { const SidebarSubitems = ({ subitem, dispatch }) => {
return ( return (
<> <>
<ListItem disablePadding divider> <ListItem disablePadding divider>
<ListItemButton <ListItemButton
disableGutters disableGutters
sx={{ p: 0.5, pl: 1 }} sx={{ p: 0.5, pl: 1 }}
selected={subitem.selected} selected={subitem.selected}
component={subitem.type === "page" ? Link : null} component={subitem.type === "page" ? Link : null}
href={subitem.type === "page" ? subitem.route : null} href={subitem.type === "page" ? subitem.route : null}
onClick={() => { onClick={() => {
if (subitem.type !== "page") { if (subitem.type !== "page") {
dispatch({ type: "COLLAPSE_SUB_ITEMS", id: subitem.id }); dispatch({ type: "COLLAPSE_SUB_ITEMS", id: subitem.id });
} }
}} }}
> >
<ListItemIcon <ListItemIcon
sx={{ sx={{
minWidth: 0, minWidth: 0,
justifyContent: "center", justifyContent: "center",
width: 40, width: 40,
height: 24, height: 24,
}} }}
> >
{subitem.icon} {subitem.icon}
</ListItemIcon> </ListItemIcon>
<ListItemText primary={subitem.label} /> <ListItemText primary={subitem.label} />
{subitem.badges && ( {subitem.badges && (
<Stack direction={"row"} sx={{ px: 0.5 }} spacing={0.5}> <Stack direction={"row"} sx={{ px: 0.5 }} spacing={0.5}>
{subitem.badges.map((badge, i) => ( {subitem.badges.map((badge, i) => (
<SidebarBadge <SidebarBadge
chipProps={{ chipProps={{
color: badge.color || "default", color: badge.color || "default",
variant: "outlined", variant: "outlined",
}} }}
key={badge.key} key={badge.key}
badge={badge.key} badge={badge.key}
/> />
))} ))}
</Stack> </Stack>
)} )}
{subitem.hasSubitems ? ( {subitem.hasSubitems ? (
subitem.showSubitems ? ( subitem.showSubitems ? (
<ExpandLess /> <ExpandLess />
) : ( ) : (
<ExpandMore /> <ExpandMore />
) )
) : ( ) : (
<Box sx={{ width: 29 }} /> <Box sx={{ width: 29 }} />
)} )}
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<Collapse <Collapse in={subitem.showSubitems} timeout="auto" mountOnEnter={true} unmountOnExit={true}>
in={subitem.showSubitems} {subitem.hasSubitems ? (
timeout="auto" <List
mountOnEnter={true} disablePadding
unmountOnExit={true} dense
> sx={{
{subitem.hasSubitems ? ( background: "#00000008",
<List }}
disablePadding >
dense {subitem.Subitems.map((subSubitem, index) => {
sx={{ return <SidebarSubitems key={index} dispatch={dispatch} subitem={subSubitem} />;
background: "#00000008", })}
}} </List>
> ) : null}
{subitem.Subitems.map((subSubitem, index) => { </Collapse>
return ( </>
<SidebarSubitems );
key={index}
dispatch={dispatch}
subitem={subSubitem}
/>
);
})}
</List>
) : null}
</Collapse>
</>
);
}; };
export default SidebarSubitems; export default SidebarSubitems;

View File

@@ -1,17 +1,7 @@
"use client"; "use client";
import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import { import { Box, Drawer, IconButton, Stack, styled, Toolbar, Typography, useMediaQuery, useTheme } from "@mui/material";
Box,
Drawer,
IconButton,
Stack,
styled,
Toolbar,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import MuiAppBar from "@mui/material/AppBar"; import MuiAppBar from "@mui/material/AppBar";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import HeaderMenu from "./HeaderMenu"; import HeaderMenu from "./HeaderMenu";
@@ -21,170 +11,157 @@ import SidebarMenu from "./Sidebar/SidebarMenu";
const drawerWidth = 300; const drawerWidth = 300;
const Main = styled(Stack, { shouldForwardProp: (prop) => prop !== "open" })( const Main = styled(Stack, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({
({ theme, open }) => ({
flexGrow: 1, flexGrow: 1,
height: "100%", height: "100%",
transition: theme.transitions.create("margin", { transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen, duration: theme.transitions.duration.leavingScreen,
}), }),
marginLeft: `-${drawerWidth}px`, marginLeft: `-${drawerWidth}px`,
...(open && { ...(open && {
transition: theme.transitions.create("margin", { transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut, easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen, duration: theme.transitions.duration.enteringScreen,
}), }),
marginLeft: 0, marginLeft: 0,
}), }),
}), }));
);
const AppBar = styled(MuiAppBar, { const AppBar = styled(MuiAppBar, {
shouldForwardProp: (prop) => prop !== "open", shouldForwardProp: (prop) => prop !== "open",
})(({ theme, open }) => ({ })(({ theme, open }) => ({
transition: theme.transitions.create(["margin", "width"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
...(open && {
width: `calc(100% - ${drawerWidth}px)`,
marginLeft: `${drawerWidth}px`,
transition: theme.transitions.create(["margin", "width"], { transition: theme.transitions.create(["margin", "width"], {
easing: theme.transitions.easing.easeOut, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen, duration: theme.transitions.duration.leavingScreen,
}),
...(open && {
width: `calc(100% - ${drawerWidth}px)`,
marginLeft: `${drawerWidth}px`,
transition: theme.transitions.create(["margin", "width"], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
}), }),
}),
})); }));
const DrawerHeader = styled("div")(({ theme }) => ({ const DrawerHeader = styled("div")(({ theme }) => ({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
padding: theme.spacing(0, 1), padding: theme.spacing(0, 1),
...theme.mixins.toolbar, ...theme.mixins.toolbar,
minHeight: "40px !important", minHeight: "40px !important",
justifyContent: "flex-end", justifyContent: "flex-end",
})); }));
const HeaderWithSidebar = ({ children }) => { const HeaderWithSidebar = ({ children }) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); // Detect mobile/tablet screen const isMobile = useMediaQuery(theme.breakpoints.down("md")); // Detect mobile/tablet screen
const [open, setOpen] = useState(!isMobile); // Initial state based on screen size const [open, setOpen] = useState(!isMobile); // Initial state based on screen size
useEffect(() => { useEffect(() => {
setOpen(!isMobile); // Toggle state when screen size changes setOpen(!isMobile); // Toggle state when screen size changes
}, [isMobile]); }, [isMobile]);
const now = moment().locale("fa").format("YYYY/MM/DD"); const now = moment().locale("fa").format("YYYY/MM/DD");
const handleDrawerOpen = () => { const handleDrawerOpen = () => {
setOpen(true); setOpen(true);
}; };
const handleDrawerClose = () => { const handleDrawerClose = () => {
setOpen(false); setOpen(false);
}; };
return ( return (
<Box
sx={{
display: "flex",
height: `100%`,
width: "100%",
}}
>
<AppBar
sx={{ position: "fixed", top: "unset" }}
elevation={0}
open={open}
>
<Toolbar variant="dense" sx={{ minHeight: 40 }}>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
sx={{ mr: 2, ...(open && { display: "none" }) }}
>
<MenuIcon />
</IconButton>
<Box
sx={{
flexGrow: 1,
display: "flex",
overflowX: "auto",
whiteSpace: "nowrap",
scrollbarWidth: "none",
"&::-webkit-scrollbar": {
display: "none",
},
}}
>
{headerMenu.map((menu) => (
<HeaderMenu key={menu.title} menu={menu} />
))}
</Box>
</Toolbar>
</AppBar>
<Drawer
sx={{
height: "100%",
width: drawerWidth,
flexShrink: 0,
"& .MuiDrawer-paper": {
overflow: "hidden",
width: drawerWidth,
boxSizing: "border-box",
borderTop: 1,
borderTopColor: "divider",
position: "inherit",
},
}}
variant="persistent"
anchor="left"
open={open}
>
<DrawerHeader
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography variant="subtitle2" color="#757575" sx={{ px: 1 }}>
تاریخ امروز: {now}
</Typography>
<IconButton onClick={handleDrawerClose}>
<ChevronRightIcon />
</IconButton>
</DrawerHeader>
<SidebarMenu />
<Typography
textAlign={"center"}
variant="caption"
sx={{ py: 0.2, fontFamily: "sans-serif" }}
>
v{process.env.NEXT_PUBLIC_VERSION}
</Typography>
</Drawer>
<Main
open={open}
sx={{ height: "100%", width: "100%", overflow: "hidden" }}
>
<DrawerHeader />
<Box <Box
sx={{ sx={{
height: "100%", display: "flex",
width: "100%", height: `100%`,
overflowY: "auto", width: "100%",
p: 1, }}
overflowX: "hidden",
}}
> >
{children} <AppBar sx={{ position: "fixed", top: "unset" }} elevation={0} open={open}>
<Toolbar variant="dense" sx={{ minHeight: 40 }}>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
sx={{ mr: 2, ...(open && { display: "none" }) }}
>
<MenuIcon />
</IconButton>
<Box
sx={{
flexGrow: 1,
display: "flex",
overflowX: "auto",
whiteSpace: "nowrap",
scrollbarWidth: "none",
"&::-webkit-scrollbar": {
display: "none",
},
}}
>
{headerMenu.map((menu) => (
<HeaderMenu key={menu.title} menu={menu} />
))}
</Box>
</Toolbar>
</AppBar>
<Drawer
sx={{
height: "100%",
width: drawerWidth,
flexShrink: 0,
"& .MuiDrawer-paper": {
overflow: "hidden",
width: drawerWidth,
boxSizing: "border-box",
borderTop: 1,
borderTopColor: "divider",
position: "inherit",
},
}}
variant="persistent"
anchor="left"
open={open}
>
<DrawerHeader
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography variant="subtitle2" color="#757575" sx={{ px: 1 }}>
تاریخ امروز: {now}
</Typography>
<IconButton onClick={handleDrawerClose}>
<ChevronRightIcon />
</IconButton>
</DrawerHeader>
<SidebarMenu />
<Typography textAlign={"center"} variant="caption" sx={{ py: 0.2, fontFamily: "sans-serif" }}>
v{process.env.NEXT_PUBLIC_VERSION}
</Typography>
</Drawer>
<Main open={open} sx={{ height: "100%", width: "100%", overflow: "hidden" }}>
<DrawerHeader />
<Box
sx={{
height: "100%",
width: "100%",
overflowY: "auto",
p: 1,
overflowX: "hidden",
}}
>
{children}
</Box>
</Main>
</Box> </Box>
</Main> );
</Box>
);
}; };
export default HeaderWithSidebar; export default HeaderWithSidebar;

View File

@@ -1,29 +1,25 @@
import { Stack, Typography } from "@mui/material"; import { Stack, Typography } from "@mui/material";
const ActionHeader = ({ tab }) => { const ActionHeader = ({ tab }) => {
return ( return (
<Stack <Stack
sx={{ sx={{
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
py: 2, py: 2,
backgroundColor: "primary.main", backgroundColor: "primary.main",
overflow: "hidden", overflow: "hidden",
}} }}
> >
<Typography variant="subtitle1" sx={{ marginRight: 1, color: "#fff" }}> <Typography variant="subtitle1" sx={{ marginRight: 1, color: "#fff" }}>
.... عملیات های مربوط به تماس: .... عملیات های مربوط به تماس:
</Typography> </Typography>
<Typography <Typography data-testid="phone_number" variant="subtitle1" sx={{ color: "#fff" }}>
data-testid="phone_number" {tab.phone_number} ....
variant="subtitle1" </Typography>
sx={{ color: "#fff" }} </Stack>
> );
{tab.phone_number} ....
</Typography>
</Stack>
);
}; };
export default ActionHeader; export default ActionHeader;

View File

@@ -2,26 +2,24 @@ import { Button, Grid } from "@mui/material";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
const CallActionCategoriesButton = ({ category, control }) => { const CallActionCategoriesButton = ({ category, control }) => {
return ( return (
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<Button <Button
sx={{ py: 1.5 }} sx={{ py: 1.5 }}
fullWidth fullWidth
variant={ variant={field.value === category.category_id ? "contained" : "outlined"}
field.value === category.category_id ? "contained" : "outlined" color={"primary"}
} onClick={() => {
color={"primary"} field.onChange(category.category_id);
onClick={() => { }}
field.onChange(category.category_id); >
}} {category.category_name}
> </Button>
{category.category_name} )}
</Button> name={"category_id"}
)} />
name={"category_id"} );
/>
);
}; };
export default CallActionCategoriesButton; export default CallActionCategoriesButton;

View File

@@ -3,42 +3,42 @@ import { useEffect, useRef } from "react";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
const CallActionDescription = ({ control }) => { const CallActionDescription = ({ control }) => {
const inputRef = useRef(null); const inputRef = useRef(null);
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
if (inputRef.current) { if (inputRef.current) {
inputRef.current.focus(); inputRef.current.focus();
} }
}, 100); }, 100);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); }, []);
return ( return (
<> <>
<Divider sx={{ margin: 2 }}> <Divider sx={{ margin: 2 }}>
<Chip label={"توضیحات تماس (اختیاری)"} variant="outlined" /> <Chip label={"توضیحات تماس (اختیاری)"} variant="outlined" />
</Divider> </Divider>
<Box sx={{ m: 2 }}> <Box sx={{ m: 2 }}>
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<TextField <TextField
{...field} {...field}
inputRef={inputRef} inputRef={inputRef}
variant="outlined" variant="outlined"
placeholder={"لطفا توضیحات خود را وارد نمایید"} placeholder={"لطفا توضیحات خود را وارد نمایید"}
type={"text"} type={"text"}
fullWidth fullWidth
multiline multiline
rows={8} rows={8}
/> />
)} )}
name={"description"} name={"description"}
/> />
</Box> </Box>
</> </>
); );
}; };
export default CallActionDescription; export default CallActionDescription;

View File

@@ -3,36 +3,36 @@ import { useCallback } from "react";
import { Controller, useWatch } from "react-hook-form"; import { Controller, useWatch } from "react-hook-form";
const CallActionSubcategoriesButton = ({ sub_category, control, onSubmit }) => { const CallActionSubcategoriesButton = ({ sub_category, control, onSubmit }) => {
const category_id = useWatch({ control, name: "category_id" }); const category_id = useWatch({ control, name: "category_id" });
const handleClick = useCallback( const handleClick = useCallback(
(onChange) => { (onChange) => {
onChange(sub_category.subcategory_id); onChange(sub_category.subcategory_id);
setTimeout(() => { setTimeout(() => {
onSubmit(); onSubmit();
}, 0); }, 0);
}, },
[sub_category.subcategory_id], [sub_category.subcategory_id]
); );
return ( return (
<Controller <Controller
control={control} control={control}
render={({ field, fieldState: { error } }) => { render={({ field, fieldState: { error } }) => {
return sub_category.category_id === category_id ? ( return sub_category.category_id === category_id ? (
<Button <Button
sx={{ py: 1.5 }} sx={{ py: 1.5 }}
fullWidth fullWidth
variant="outlined" variant="outlined"
color={"primary"} color={"primary"}
onClick={() => handleClick(field.onChange)} onClick={() => handleClick(field.onChange)}
> >
{sub_category.subcategory_name} {sub_category.subcategory_name}
</Button> </Button>
) : null; ) : null;
}} }}
name={"subcategory_id"} name={"subcategory_id"}
/> />
); );
}; };
export default CallActionSubcategoriesButton; export default CallActionSubcategoriesButton;

View File

@@ -3,22 +3,18 @@ import CallActionCategoriesButton from "./CallActionCategoriesButton";
import { Chip, Divider, Grid, Stack } from "@mui/material"; import { Chip, Divider, Grid, Stack } from "@mui/material";
const CallActionsCategories = ({ control }) => { const CallActionsCategories = ({ control }) => {
const { categoryLists } = useCategory(); const { categoryLists } = useCategory();
return ( return (
<> <>
<Divider sx={{ margin: 2 }}> <Divider sx={{ margin: 2 }}>
<Chip label={"موضوع ها"} variant="outlined" /> <Chip label={"موضوع ها"} variant="outlined" />
</Divider> </Divider>
<Stack sx={{ m: 1, p: 1 }} direction={"row"} spacing={3}> <Stack sx={{ m: 1, p: 1 }} direction={"row"} spacing={3}>
{categoryLists.map((category) => ( {categoryLists.map((category) => (
<CallActionCategoriesButton <CallActionCategoriesButton key={category.category_id} category={category} control={control} />
key={category.category_id} ))}
category={category} </Stack>
control={control} </>
/> );
))}
</Stack>
</>
);
}; };
export default CallActionsCategories; export default CallActionsCategories;

View File

@@ -3,25 +3,25 @@ import CallActionSubcategoriesButton from "./CallActionSubcategoriesButton";
import { Chip, Divider, Grid, Stack } from "@mui/material"; import { Chip, Divider, Grid, Stack } from "@mui/material";
const CallActionsSubcategories = ({ control, onSubmit }) => { const CallActionsSubcategories = ({ control, onSubmit }) => {
const { subCategoryLists } = useCategory(); const { subCategoryLists } = useCategory();
return ( return (
<> <>
<Divider sx={{ margin: 2 }}> <Divider sx={{ margin: 2 }}>
<Chip label={"زیر موضوع ها"} variant="outlined" /> <Chip label={"زیر موضوع ها"} variant="outlined" />
</Divider> </Divider>
<Stack sx={{ m: 1, p: 1 }} direction={"row"} spacing={3}> <Stack sx={{ m: 1, p: 1 }} direction={"row"} spacing={3}>
{subCategoryLists.map((sub_category, index) => { {subCategoryLists.map((sub_category, index) => {
return ( return (
<CallActionSubcategoriesButton <CallActionSubcategoriesButton
key={index} key={index}
sub_category={sub_category} sub_category={sub_category}
control={control} control={control}
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
); );
})} })}
</Stack> </Stack>
</> </>
); );
}; };
export default CallActionsSubcategories; export default CallActionsSubcategories;

View File

@@ -12,76 +12,70 @@ import CallActionsCategories from "./CallActionsCategories";
import CallActionsSubcategories from "./CallActionsSubcategories"; import CallActionsSubcategories from "./CallActionsSubcategories";
const defaultValues = { const defaultValues = {
description: "", description: "",
category_id: "", category_id: "",
subcategory_id: "", subcategory_id: "",
}; };
const validationSchema = object({ const validationSchema = object({
category_id: string().required("لطفا نام کاربری را وارد کنید!"), category_id: string().required("لطفا نام کاربری را وارد کنید!"),
subcategory_id: string().required("لطفا رمز عبور را وارد کنید!"), subcategory_id: string().required("لطفا رمز عبور را وارد کنید!"),
}); });
function CallActions({ tab }) { function CallActions({ tab }) {
const requestServer = useRequest(); const requestServer = useRequest();
const { closeCall } = useCall(); const { closeCall } = useCall();
const { const {
control, control,
handleSubmit, handleSubmit,
formState: { isSubmitting }, formState: { isSubmitting },
} = useForm({ } = useForm({
defaultValues, defaultValues,
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
mode: "all", mode: "all",
}); });
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
try { try {
const formData = new FormData(); const formData = new FormData();
data.description != "" && data.description != "" && formData.append("description", data.description);
formData.append("description", data.description); formData.append("category_id", data.category_id);
formData.append("category_id", data.category_id); formData.append("subcategory_id", data.subcategory_id);
formData.append("subcategory_id", data.subcategory_id); await requestServer(`${CALL_ACTION}/${tab.id}`, "post", {
await requestServer(`${CALL_ACTION}/${tab.id}`, "post", { data: formData,
data: formData, });
}); closeCall(tab.id);
closeCall(tab.id); } catch (error) {}
} catch (error) {} });
});
return ( return (
<Stack <Stack
sx={{
height: "100%",
}}
>
<ActionHeader tab={tab} />
<Stack sx={{ height: "100%", overflowY: "auto", position: "relative" }}>
<CallActionDescription control={control} />
<CallActionsCategories control={control} />
<CallActionsSubcategories control={control} onSubmit={onSubmit} />
{isSubmitting && (
<Box
sx={{ sx={{
position: "absolute", height: "100%",
width: "100%",
height: "100%",
opacity: 0.9,
zIndex: 9999,
}} }}
> >
<LoadingHardPage <ActionHeader tab={tab} />
width={80} <Stack sx={{ height: "100%", overflowY: "auto", position: "relative" }}>
height={80} <CallActionDescription control={control} />
loading={true} <CallActionsCategories control={control} />
sx={{ position: "absolute" }} <CallActionsSubcategories control={control} onSubmit={onSubmit} />
/> {isSubmitting && (
</Box> <Box
)} sx={{
</Stack> position: "absolute",
</Stack> width: "100%",
); height: "100%",
opacity: 0.9,
zIndex: 9999,
}}
>
<LoadingHardPage width={80} height={80} loading={true} sx={{ position: "absolute" }} />
</Box>
)}
</Stack>
</Stack>
);
} }
export default CallActions; export default CallActions;

View File

@@ -1,39 +1,39 @@
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
const ErrorOrEmpty = ({ isErrorOrEmpty }) => { const ErrorOrEmpty = ({ isErrorOrEmpty }) => {
return ( return (
<> <>
{isErrorOrEmpty === "error" ? ( {isErrorOrEmpty === "error" ? (
<Typography <Typography
variant="subtitle1" variant="subtitle1"
sx={{ sx={{
color: "#837e7e", color: "#837e7e",
fontWeight: "500", fontWeight: "500",
letterSpacing: 1, letterSpacing: 1,
textAlign: "center", textAlign: "center",
mt: "50%", mt: "50%",
}} }}
> >
خطا در دریافت تاریخچه تماس دریافتی خطا در دریافت تاریخچه تماس دریافتی
</Typography> </Typography>
) : isErrorOrEmpty === "empty" ? ( ) : isErrorOrEmpty === "empty" ? (
<Typography <Typography
variant="subtitle1" variant="subtitle1"
sx={{ sx={{
color: "#837e7e", color: "#837e7e",
fontWeight: "500", fontWeight: "500",
letterSpacing: 1, letterSpacing: 1,
textAlign: "center", textAlign: "center",
mt: "50%", mt: "50%",
}} }}
> >
تاریخچه ای برای تماس دریافتی یافت نشد تاریخچه ای برای تماس دریافتی یافت نشد
</Typography> </Typography>
) : ( ) : (
<></> <></>
)} )}
</> </>
); );
}; };
export default ErrorOrEmpty; export default ErrorOrEmpty;

View File

@@ -1,25 +1,25 @@
import { Stack, Typography } from "@mui/material"; import { Stack, Typography } from "@mui/material";
const HistoryHeader = ({ tab }) => { const HistoryHeader = ({ tab }) => {
return ( return (
<Stack <Stack
sx={{ sx={{
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
py: 2, py: 2,
backgroundColor: "primary.main", backgroundColor: "primary.main",
overflow: "hidden", overflow: "hidden",
}} }}
> >
<Typography variant="subtitle1" sx={{ marginRight: 1, color: "#fff" }}> <Typography variant="subtitle1" sx={{ marginRight: 1, color: "#fff" }}>
.... تاریخچه تماس های: .... تاریخچه تماس های:
</Typography> </Typography>
<Typography variant="subtitle1" sx={{ color: "#fff" }}> <Typography variant="subtitle1" sx={{ color: "#fff" }}>
{tab.phone_number} .... {tab.phone_number} ....
</Typography> </Typography>
</Stack> </Stack>
); );
}; };
export default HistoryHeader; export default HistoryHeader;

View File

@@ -3,28 +3,26 @@ import { Avatar, Box, Stack, Typography } from "@mui/material";
import moment from "jalali-moment"; import moment from "jalali-moment";
const PreviousOperatorData = ({ historyItem }) => { const PreviousOperatorData = ({ historyItem }) => {
return ( return (
<Stack direction={"row"} spacing={2}> <Stack direction={"row"} spacing={2}>
<Box> <Box>
<Avatar sx={{ backgroundColor: "primary.light" }}> <Avatar sx={{ backgroundColor: "primary.light" }}>
<HeadsetMicIcon /> <HeadsetMicIcon />
</Avatar> </Avatar>
</Box> </Box>
<Stack> <Stack>
<Stack direction={"row"} sx={{ alignItems: "end" }}> <Stack direction={"row"} sx={{ alignItems: "end" }}>
<Typography sx={{ mr: 1 }}> <Typography sx={{ mr: 1 }}>{historyItem.operator_full_name}</Typography>
{historyItem.operator_full_name} <Typography variant="caption" sx={{ color: "secondary.main" }}>
</Typography> (کارشناس)
<Typography variant="caption" sx={{ color: "secondary.main" }}> </Typography>
(کارشناس) </Stack>
</Typography> <Typography variant="caption" sx={{ color: "secondary.main" }}>
{moment(historyItem.created_at).locale("fa").fromNow()}
</Typography>
</Stack>
</Stack> </Stack>
<Typography variant="caption" sx={{ color: "secondary.main" }}> );
{moment(historyItem.created_at).locale("fa").fromNow()}
</Typography>
</Stack>
</Stack>
);
}; };
export default PreviousOperatorData; export default PreviousOperatorData;

View File

@@ -1,60 +1,46 @@
import { Chip, Divider, Stack } from "@mui/material"; import { Chip, Divider, Stack } from "@mui/material";
const Topics = ({ historyItem }) => { const Topics = ({ historyItem }) => {
return ( return (
<Stack spacing={1}> <Stack spacing={1}>
<Stack <Stack
sx={{ sx={{
width: "100%", width: "100%",
alignItems: "center", alignItems: "center",
}} }}
spacing={1} spacing={1}
direction={"row"} direction={"row"}
> >
<Chip label={"موضوع"} size="small" variant="outlined" color="primary" /> <Chip label={"موضوع"} size="small" variant="outlined" color="primary" />
<Divider sx={{ flex: 1 }} /> <Divider sx={{ flex: 1 }} />
{historyItem.category_name && ( {historyItem.category_name && <Chip label={historyItem.category_name} />}
<Chip label={historyItem.category_name} /> </Stack>
)} <Stack
</Stack> sx={{
<Stack width: "100%",
sx={{ alignItems: "center",
width: "100%", }}
alignItems: "center", spacing={1}
}} direction={"row"}
spacing={1} >
direction={"row"} <Chip label={"زیر موضوع"} size="small" variant="outlined" color="primary" />
> <Divider sx={{ flex: 1 }} />
<Chip {historyItem.subcategory_name && <Chip label={historyItem.subcategory_name} />}
label={"زیر موضوع"} </Stack>
size="small" <Stack
variant="outlined" sx={{
color="primary" width: "100%",
/> alignItems: "center",
<Divider sx={{ flex: 1 }} /> }}
{historyItem.subcategory_name && ( spacing={1}
<Chip label={historyItem.subcategory_name} /> direction={"row"}
)} >
</Stack> <Chip label={"توضیحات تماس"} size="small" variant="outlined" color="primary" />
<Stack <Divider sx={{ flex: 1 }} />
sx={{ {historyItem.description && <Chip label={historyItem.description} />}
width: "100%", </Stack>
alignItems: "center", </Stack>
}} );
spacing={1}
direction={"row"}
>
<Chip
label={"توضیحات تماس"}
size="small"
variant="outlined"
color="primary"
/>
<Divider sx={{ flex: 1 }} />
{historyItem.description && <Chip label={historyItem.description} />}
</Stack>
</Stack>
);
}; };
export default Topics; export default Topics;

View File

@@ -3,12 +3,12 @@ import PreviousOperatorData from "./PreviousOperatorData";
import Topics from "./Topics."; import Topics from "./Topics.";
const ListItemOfCalls = ({ historyItem }) => { const ListItemOfCalls = ({ historyItem }) => {
return ( return (
<Stack sx={{ p: 2 }} spacing={2}> <Stack sx={{ p: 2 }} spacing={2}>
<PreviousOperatorData historyItem={historyItem} /> <PreviousOperatorData historyItem={historyItem} />
<Topics historyItem={historyItem} /> <Topics historyItem={historyItem} />
</Stack> </Stack>
); );
}; };
export default ListItemOfCalls; export default ListItemOfCalls;

View File

@@ -1,115 +1,108 @@
import { import { Box, List, ListItem, ListItemAvatar, ListItemText, Skeleton } from "@mui/material";
Box,
List,
ListItem,
ListItemAvatar,
ListItemText,
Skeleton,
} from "@mui/material";
const LoadingHistory = () => { const LoadingHistory = () => {
return ( return (
<> <>
<List <List
data-testid="loading-list" data-testid="loading-list"
sx={{
width: "100%",
bgcolor: "rgba(222,234,215,0.11)",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
px: 2,
}}
>
<ListItemAvatar data-testid="loading-listItemAvatar">
<Skeleton
data-testid="operator-avatar-skeleton"
animation="wave"
variant="circular"
width={60}
height={60}
/>
</ListItemAvatar>
<ListItemText
data-testid="loading-listItemText"
primary={
<Box sx={{ display: "flex", alignItems: "end" }}>
<Skeleton
data-testid="operator-name-skeleton"
animation="wave"
height={15}
width="50%"
style={{ marginRight: 10, marginBottom: 5 }}
/>
</Box>
}
secondary={
<Skeleton
data-testid="operator-answer-date-skeleton"
animation="wave"
height={10}
width="30%"
style={{ marginRight: 10 }}
/>
}
/>
</Box>
<Box timeout="auto" sx={{ py: 2 }}>
<List component="div" disablePadding>
<ListItem data-testid="loading-listItem-cat" sx={{ px: 4 }}>
<Box
sx={{ sx={{
width: "100%", width: "100%",
display: "flex", bgcolor: "rgba(222,234,215,0.11)",
justifyContent: "space-between",
}} }}
> >
<Skeleton <Box
data-testid="operator-category-header-skeleton" sx={{
animation="wave" display: "flex",
height={15} alignItems: "center",
width="15%" px: 2,
/> }}
<Skeleton >
data-testid="operator-category-value-skeleton" <ListItemAvatar data-testid="loading-listItemAvatar">
animation="wave" <Skeleton
height={15} data-testid="operator-avatar-skeleton"
width="30%" animation="wave"
/> variant="circular"
</Box> width={60}
</ListItem> height={60}
</List> />
<List component="div" disablePadding> </ListItemAvatar>
<ListItem data-testid="loading-listItem-subCat" sx={{ px: 4 }}> <ListItemText
<Box data-testid="loading-listItemText"
sx={{ primary={
width: "100%", <Box sx={{ display: "flex", alignItems: "end" }}>
display: "flex", <Skeleton
justifyContent: "space-between", data-testid="operator-name-skeleton"
}} animation="wave"
> height={15}
<Skeleton width="50%"
data-testid="operator-subCategory-header-skeleton" style={{ marginRight: 10, marginBottom: 5 }}
animation="wave" />
height={15} </Box>
width="25%" }
/> secondary={
<Skeleton <Skeleton
data-testid="operator-subCategory-value-skeleton" data-testid="operator-answer-date-skeleton"
animation="wave" animation="wave"
height={15} height={10}
width="50%" width="30%"
/> style={{ marginRight: 10 }}
</Box> />
</ListItem> }
</List> />
</Box> </Box>
</List> <Box timeout="auto" sx={{ py: 2 }}>
</> <List component="div" disablePadding>
); <ListItem data-testid="loading-listItem-cat" sx={{ px: 4 }}>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
}}
>
<Skeleton
data-testid="operator-category-header-skeleton"
animation="wave"
height={15}
width="15%"
/>
<Skeleton
data-testid="operator-category-value-skeleton"
animation="wave"
height={15}
width="30%"
/>
</Box>
</ListItem>
</List>
<List component="div" disablePadding>
<ListItem data-testid="loading-listItem-subCat" sx={{ px: 4 }}>
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
}}
>
<Skeleton
data-testid="operator-subCategory-header-skeleton"
animation="wave"
height={15}
width="25%"
/>
<Skeleton
data-testid="operator-subCategory-value-skeleton"
animation="wave"
height={15}
width="50%"
/>
</Box>
</ListItem>
</List>
</Box>
</List>
</>
);
}; };
export default LoadingHistory; export default LoadingHistory;

View File

@@ -9,53 +9,52 @@ import { Headphones } from "@mui/icons-material";
const max_size = 10; const max_size = 10;
const CallHistory = ({ tab }) => { const CallHistory = ({ tab }) => {
const { const { firstItemOfHistory, historyList, isLoadingHistoryList, errorHistoryList } = useCallerHistory(
firstItemOfHistory, tab.id,
historyList, tab.phone_number,
isLoadingHistoryList, max_size
errorHistoryList, );
} = useCallerHistory(tab.id, tab.phone_number, max_size); const [showRestOfHistory, setShowRestOfHistory] = useState(false);
const [showRestOfHistory, setShowRestOfHistory] = useState(false);
return ( return (
<Stack <Stack
sx={{ sx={{
height: "100%", height: "100%",
}} }}
> >
<HistoryHeader tab={tab} /> <HistoryHeader tab={tab} />
<Stack sx={{ height: "100%", overflowY: "auto" }}> <Stack sx={{ height: "100%", overflowY: "auto" }}>
{isLoadingHistoryList ? ( {isLoadingHistoryList ? (
<LoadingHistory /> <LoadingHistory />
) : errorHistoryList ? ( ) : errorHistoryList ? (
<ErrorOrEmpty isErrorOrEmpty="error" /> <ErrorOrEmpty isErrorOrEmpty="error" />
) : !firstItemOfHistory ? ( ) : !firstItemOfHistory ? (
<ErrorOrEmpty isErrorOrEmpty="empty" /> <ErrorOrEmpty isErrorOrEmpty="empty" />
) : ( ) : (
<Box> <Box>
<ListItemOfCalls key={"first"} historyItem={firstItemOfHistory} /> <ListItemOfCalls key={"first"} historyItem={firstItemOfHistory} />
{historyList && ( {historyList && (
<Divider> <Divider>
<Chip <Chip
icon={<Headphones />} icon={<Headphones />}
onClick={() => setShowRestOfHistory(true)} onClick={() => setShowRestOfHistory(true)}
label={"نمایش موارد بیشتر"} label={"نمایش موارد بیشتر"}
disabled={showRestOfHistory} disabled={showRestOfHistory}
/> />
</Divider> </Divider>
)} )}
{showRestOfHistory && {showRestOfHistory &&
historyList.map((historyItem, index) => ( historyList.map((historyItem, index) => (
<> <>
<ListItemOfCalls key={index} historyItem={historyItem} /> <ListItemOfCalls key={index} historyItem={historyItem} />
<Divider /> <Divider />
</> </>
))} ))}
</Box> </Box>
)} )}
</Stack> </Stack>
</Stack> </Stack>
); );
}; };
export default CallHistory; export default CallHistory;

View File

@@ -3,27 +3,27 @@ import CallActions from "./CallActions";
import CallHistory from "./CallHistory"; import CallHistory from "./CallHistory";
const CallTabPanel = ({ tab }) => { const CallTabPanel = ({ tab }) => {
return ( return (
<Stack <Stack
role="tabpanel" role="tabpanel"
id={`call-tabpanel-${tab.id}`} id={`call-tabpanel-${tab.id}`}
aria-labelledby={`call-tab-${tab.id}`} aria-labelledby={`call-tab-${tab.id}`}
direction={"row"} direction={"row"}
sx={{ sx={{
height: "100%", height: "100%",
display: !tab.active && "none", display: !tab.active && "none",
overflow: "hidden", overflow: "hidden",
}} }}
> >
<Box sx={{ flex: 2 }}> <Box sx={{ flex: 2 }}>
<CallActions tab={tab} /> <CallActions tab={tab} />
</Box> </Box>
<Divider variant="fullWidth" orientation="vertical" /> <Divider variant="fullWidth" orientation="vertical" />
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
<CallHistory tab={tab} /> <CallHistory tab={tab} />
</Box> </Box>
</Stack> </Stack>
); );
}; };
export default CallTabPanel; export default CallTabPanel;

View File

@@ -3,19 +3,14 @@ import { Stack, Typography } from "@mui/material";
import CallTabTime from "./CallTabTime"; import CallTabTime from "./CallTabTime";
const CallTabDetails = ({ tab }) => { const CallTabDetails = ({ tab }) => {
return ( return (
<Stack <Stack direction="row" justifyContent="center" alignItems="center" spacing={3}>
direction="row" <Call />
justifyContent="center" <Stack alignItems={"start"}>
alignItems="center" <Typography>{tab.phone_number}</Typography>
spacing={3} <CallTabTime realTimeDate={tab.date} />
> </Stack>
<Call /> </Stack>
<Stack alignItems={"start"}> );
<Typography>{tab.phone_number}</Typography>
<CallTabTime realTimeDate={tab.date} />
</Stack>
</Stack>
);
}; };
export default CallTabDetails; export default CallTabDetails;

View File

@@ -4,32 +4,31 @@ import { IconButton, Stack, Zoom } from "@mui/material";
import CallTabDetails from "./CallTabDetails"; import CallTabDetails from "./CallTabDetails";
const CallTabLabel = ({ tab, index }) => { const CallTabLabel = ({ tab, index }) => {
const { closeCall, activeCall } = useCall(); const { closeCall, activeCall } = useCall();
const handleCloseClick = (id) => { const handleCloseClick = (id) => {
closeCall(id); closeCall(id);
}; };
return ( return (
<Stack <Stack
sx={{ sx={{
width: "100%", width: "100%",
px: 1, px: 1,
borderRight: borderRight: activeCall === index ? 0 : activeCall - 1 === index ? 0 : 1,
activeCall === index ? 0 : activeCall - 1 === index ? 0 : 1, borderRightColor: "divider",
borderRightColor: "divider", }}
}} direction={"row"}
direction={"row"} alignItems={"center"}
alignItems={"center"} justifyContent={"space-between"}
justifyContent={"space-between"} >
> <CallTabDetails tab={tab} />
<CallTabDetails tab={tab} /> <Zoom in={tab.active}>
<Zoom in={tab.active}> <IconButton size="small" onClick={() => handleCloseClick(tab.id)}>
<IconButton size="small" onClick={() => handleCloseClick(tab.id)}> <Close fontSize="small" />
<Close fontSize="small" /> </IconButton>
</IconButton> </Zoom>
</Zoom> </Stack>
</Stack> );
);
}; };
export default CallTabLabel; export default CallTabLabel;

View File

@@ -3,26 +3,26 @@ import moment from "jalali-moment";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
const CallTabTime = ({ realTimeDate }) => { const CallTabTime = ({ realTimeDate }) => {
const [date, setDate] = useState("..."); const [date, setDate] = useState("...");
const changeTabTime = (tabTime) => { const changeTabTime = (tabTime) => {
moment.relativeTimeThreshold("ss", 1); moment.relativeTimeThreshold("ss", 1);
return moment(tabTime).locale("fa").fromNow(); return moment(tabTime).locale("fa").fromNow();
};
useEffect(() => {
const timer = setInterval(() => {
setDate(changeTabTime(realTimeDate));
}, 1000);
return () => {
clearInterval(timer);
}; };
}, [realTimeDate]);
return ( useEffect(() => {
<Typography variant="caption" color="gray"> const timer = setInterval(() => {
{date} setDate(changeTabTime(realTimeDate));
</Typography> }, 1000);
);
return () => {
clearInterval(timer);
};
}, [realTimeDate]);
return (
<Typography variant="caption" color="gray">
{date}
</Typography>
);
}; };
export default CallTabTime; export default CallTabTime;

View File

@@ -3,38 +3,38 @@ import { Box, Tab, Tabs } from "@mui/material";
import CallTabLabel from "./CallTabLabel"; import CallTabLabel from "./CallTabLabel";
const CallTabsList = () => { const CallTabsList = () => {
const { calls, changeActiveTabCall, activeCall } = useCall(); const { calls, changeActiveTabCall, activeCall } = useCall();
const handleChange = (event, newValue) => { const handleChange = (event, newValue) => {
changeActiveTabCall(newValue); changeActiveTabCall(newValue);
}; };
return ( return (
<Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs <Tabs
value={activeCall} value={activeCall}
onChange={handleChange} onChange={handleChange}
variant="scrollable" variant="scrollable"
scrollButtons="auto" scrollButtons="auto"
sx={{ background: "#e0e0e0" }} sx={{ background: "#e0e0e0" }}
> >
{calls.map((call, index) => ( {calls.map((call, index) => (
<Tab <Tab
component={"div"} component={"div"}
sx={{ sx={{
width: 350, width: 350,
p: 0, p: 0,
py: 0.5, py: 0.5,
background: call.active ? "#fff" : null, background: call.active ? "#fff" : null,
borderTopRightRadius: call.active ? "16px" : 0, borderTopRightRadius: call.active ? "16px" : 0,
borderTopLeftRadius: call.active ? "16px" : 0, borderTopLeftRadius: call.active ? "16px" : 0,
}} }}
label={<CallTabLabel index={index} tab={call} />} label={<CallTabLabel index={index} tab={call} />}
key={call.id} key={call.id}
/> />
))} ))}
</Tabs> </Tabs>
</Box> </Box>
); );
}; };
export default CallTabsList; export default CallTabsList;

View File

@@ -3,15 +3,15 @@ import CallTabPanel from "./CallTabPanel";
import CallTabsList from "./CallTabsList"; import CallTabsList from "./CallTabsList";
const CallTabs = () => { const CallTabs = () => {
const { calls } = useCall(); const { calls } = useCall();
return ( return (
<> <>
<CallTabsList /> <CallTabsList />
{calls.map((call) => ( {calls.map((call) => (
<CallTabPanel key={call.id} tab={call} /> <CallTabPanel key={call.id} tab={call} />
))} ))}
</> </>
); );
}; };
export default CallTabs; export default CallTabs;

View File

@@ -7,42 +7,38 @@ import { useEffect } from "react";
import CallTabs from "./CallTabs"; import CallTabs from "./CallTabs";
const CallWidgetDialog = () => { const CallWidgetDialog = () => {
const { openCallDialog, setOpenCallDialog, calls, newCall } = useCall(); const { openCallDialog, setOpenCallDialog, calls, newCall } = useCall();
const { socket } = useSocket(); const { socket } = useSocket();
useEffect(() => { useEffect(() => {
const call = (_data, act) => { const call = (_data, act) => {
const data = JSON.parse(_data); const data = JSON.parse(_data);
newCall({ newCall({
id: data.call_id, id: data.call_id,
phone_number: data.phone_number, phone_number: data.phone_number,
}); });
act(); act();
}; };
socket.on("call", call); socket.on("call", call);
return () => { return () => {
socket.off("call", call); socket.off("call", call);
}; };
}); });
useEffect(() => { useEffect(() => {
if (calls.length === 0) { if (calls.length === 0) {
setOpenCallDialog(false); setOpenCallDialog(false);
return; return;
} }
setOpenCallDialog(true); setOpenCallDialog(true);
}, [calls.length]); }, [calls.length]);
return ( return (
<Dialog <Dialog fullScreen open={openCallDialog} slots={{ transition: DialogTransition }}>
fullScreen <CallTabs />
open={openCallDialog} </Dialog>
slots={{ transition: DialogTransition }} );
>
<CallTabs />
</Dialog>
);
}; };
export default CallWidgetDialog; export default CallWidgetDialog;

View File

@@ -4,12 +4,12 @@ import { CategoriesProvider } from "@/lib/contexts/category";
import CallWidgetDialog from "./CallWidgetDialog"; import CallWidgetDialog from "./CallWidgetDialog";
const CallWidget = () => { const CallWidget = () => {
return ( return (
<CategoriesProvider> <CategoriesProvider>
<CallProvider> <CallProvider>
<CallWidgetDialog /> <CallWidgetDialog />
</CallProvider> </CallProvider>
</CategoriesProvider> </CategoriesProvider>
); );
}; };
export default CallWidget; export default CallWidget;

View File

@@ -5,56 +5,49 @@ import { LocalizationProvider, MobileDatePicker } from "@mui/x-date-pickers";
import { IconButton, InputAdornment } from "@mui/material"; import { IconButton, InputAdornment } from "@mui/material";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
const CustomDatePicker = ({ const CustomDatePicker = ({ dateValue, setDateValue, placeholder = "انتخاب تاریخ", size = "small" }) => {
dateValue, const handleDateChange = (newValue) => {
setDateValue, setDateValue(newValue);
placeholder = "انتخاب تاریخ", };
size = "small",
}) => {
const handleDateChange = (newValue) => {
setDateValue(newValue);
};
return ( return (
<LocalizationProvider <LocalizationProvider
dateAdapter={AdapterDateFnsJalali} dateAdapter={AdapterDateFnsJalali}
localeText={ localeText={faIR.components.MuiLocalizationProvider.defaultProps.localeText}
faIR.components.MuiLocalizationProvider.defaultProps.localeText >
} <MobileDatePicker
> value={dateValue || null}
<MobileDatePicker onChange={handleDateChange}
value={dateValue || null} slotProps={{
onChange={handleDateChange} textField: {
slotProps={{ size: size,
textField: { placeholder: placeholder,
size: size, InputProps: {
placeholder: placeholder, endAdornment: (
InputProps: { <InputAdornment position="end">
endAdornment: ( <IconButton
<InputAdornment position="end"> size={size}
<IconButton onClick={(event) => {
size={size} event.stopPropagation();
onClick={(event) => { setDateValue(null);
event.stopPropagation(); }}
setDateValue(null); sx={{
}} color: "#bfbfbf",
sx={{ "&:hover": {
color: "#bfbfbf", backgroundColor: "rgba(189, 189, 189, 0.1)",
"&:hover": { color: "#363434",
backgroundColor: "rgba(189, 189, 189, 0.1)", },
color: "#363434", }}
}, >
}} <ClearIcon />
> </IconButton>
<ClearIcon /> </InputAdornment>
</IconButton> ),
</InputAdornment> },
), },
}, }}
}, ></MobileDatePicker>
}} </LocalizationProvider>
></MobileDatePicker> );
</LocalizationProvider>
);
}; };
export default CustomDatePicker; export default CustomDatePicker;

View File

@@ -11,189 +11,169 @@ import { FA_DATATABLE_LOCALIZATION } from "./localization/fa/datatable";
import DataTable_Paper from "./table/Paper"; import DataTable_Paper from "./table/Paper";
const rowSelectionReducer = (state, action) => { const rowSelectionReducer = (state, action) => {
switch (action.type) { switch (action.type) {
case "TOGGLE_ROW": case "TOGGLE_ROW":
return { [action.payload]: true }; return { [action.payload]: true };
case "RESET": case "RESET":
return {}; return {};
default: default:
return state; return state;
} }
}; };
const DataTable_Main = (props) => { const DataTable_Main = (props) => {
const request = useRequest(); const request = useRequest();
const [rowSelection, dispatchRowSelection] = useReducer( const [rowSelection, dispatchRowSelection] = useReducer(rowSelectionReducer, {});
rowSelectionReducer, const { filterData, sortData, setSortData, hideData } = useDataTable();
{}, const {
); need_filter,
const { filterData, sortData, setSortData, hideData } = useDataTable(); table_url,
const { user_id,
need_filter, page_name,
table_url, table_name,
user_id, columns,
page_name, initialStateProps,
table_name, TableToolbar,
columns, table_title,
initialStateProps, RowActions,
TableToolbar, specific_data,
table_title, specialFilter,
RowActions, } = props;
specific_data, const flatColumns = flattenArrayOfObjects(columns, "columns");
specialFilter, const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
} = props; const [totalRowCount, setTotalRowCount] = useState(0);
const flatColumns = flattenArrayOfObjects(columns, "columns"); const flattenHideData = flattenObjectOfObjects(hideData);
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 }); const onSortingChange = (event) => {
const [totalRowCount, setTotalRowCount] = useState(0); setSortData(event);
const flattenHideData = flattenObjectOfObjects(hideData); };
const onSortingChange = (event) => {
setSortData(event);
};
const fetchUrl = useCallback(() => { const fetchUrl = useCallback(() => {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set("start", `${pagination.pageIndex * pagination.pageSize}`); params.set("start", `${pagination.pageIndex * pagination.pageSize}`);
params.set("size", pagination.pageSize); params.set("size", pagination.pageSize);
if (specialFilter) { if (specialFilter) {
const filteredSpecialFilterData = Object.values(specialFilter).filter( const filteredSpecialFilterData = Object.values(specialFilter).filter(
(filter) => !isArrayEmpty(filter.value), (filter) => !isArrayEmpty(filter.value)
); );
params.set("filters", JSON.stringify(filteredSpecialFilterData || [])); params.set("filters", JSON.stringify(filteredSpecialFilterData || []));
} else { } else {
const filteredFilterData = DataTableFilterDataStructure( const filteredFilterData = DataTableFilterDataStructure(filterData, isArrayEmpty);
filterData, params.set("filters", JSON.stringify(filteredFilterData.length === 0 ? [] : filteredFilterData));
isArrayEmpty, }
); params.set(
params.set( "sorting",
"filters", JSON.stringify(
JSON.stringify( Object.values(sortData).map(({ id, ...rest }) => ({
filteredFilterData.length === 0 ? [] : filteredFilterData, ...rest,
id: id.replace(/__/g, "."),
}))
)
);
return `${table_url}?${params}`;
}, [table_url, filterData, pagination, sortData, specialFilter]);
const fetcher = async (url) => {
try {
const response = await request(url);
setTotalRowCount(response.data?.meta?.totalRowCount);
if (specific_data) {
return specific_data(response.data);
} else {
return response.data?.data;
}
} catch (error) {
throw error;
}
};
const { data, isValidating, mutate } = useSWR(props.data ? "" : fetchUrl(), fetcher, {
revalidateIfStale: true,
revalidateOnFocus: false,
revalidateOnReconnect: true,
keepPreviousData: true,
});
useEffect(() => {
if (!isValidating) return;
dispatchRowSelection({ type: "RESET" });
}, [isValidating]);
const table = useMaterialReactTable({
localization: FA_DATATABLE_LOCALIZATION,
columns,
data: data ?? [],
muiTableBodyRowProps: ({ row }) => ({
onClick: () => dispatchRowSelection({ type: "TOGGLE_ROW", payload: row.id }),
selected: !!rowSelection[row.id],
}),
initialState: {
density: "compact",
columnVisibility: flattenHideData,
sorting: sortData,
...initialStateProps,
},
state: {
showProgressBars: isValidating || props.loading,
columnVisibility: flattenHideData,
sorting: sortData,
pagination,
rowSelection,
},
muiTableHeadProps: {
sx: {
borderTop: "1px solid #e1e1e1",
borderBottom: "2px solid #e1e1e1",
},
},
muiTableHeadCellProps: {
sx: {
color: "primary.main",
borderLeft: "1px solid #e1e1e1",
"&:first-of-type": {
borderLeft: "unset",
},
},
},
muiTableBodyCellProps: {
sx: {
borderLeft: "1px solid #e1e1e1",
"&:first-of-type": {
borderLeft: "unset",
},
},
},
manualFiltering: true,
manualPagination: true,
manualSorting: true,
onPaginationChange: setPagination,
onSortingChange: onSortingChange,
rowCount: totalRowCount,
renderTopToolbarCustomActions: ({ table }) => (
<>
{TableToolbar && (
<TableToolbar table={table} mutate={mutate} filterData={specialFilter || filterData} />
)}
</>
), ),
); renderRowActions: ({ row }) => <RowActions row={row} mutate={mutate} />,
} ...props,
params.set( });
"sorting",
JSON.stringify(
Object.values(sortData).map(({ id, ...rest }) => ({
...rest,
id: id.replace(/__/g, "."),
})),
),
);
return `${table_url}?${params}`;
}, [table_url, filterData, pagination, sortData, specialFilter]);
const fetcher = async (url) => { return (
try { <DataTable_Paper
const response = await request(url);
setTotalRowCount(response.data?.meta?.totalRowCount);
if (specific_data) {
return specific_data(response.data);
} else {
return response.data?.data;
}
} catch (error) {
throw error;
}
};
const { data, isValidating, mutate } = useSWR(
props.data ? "" : fetchUrl(),
fetcher,
{
revalidateIfStale: true,
revalidateOnFocus: false,
revalidateOnReconnect: true,
keepPreviousData: true,
},
);
useEffect(() => {
if (!isValidating) return;
dispatchRowSelection({ type: "RESET" });
}, [isValidating]);
const table = useMaterialReactTable({
localization: FA_DATATABLE_LOCALIZATION,
columns,
data: data ?? [],
muiTableBodyRowProps: ({ row }) => ({
onClick: () =>
dispatchRowSelection({ type: "TOGGLE_ROW", payload: row.id }),
selected: !!rowSelection[row.id],
}),
initialState: {
density: "compact",
columnVisibility: flattenHideData,
sorting: sortData,
...initialStateProps,
},
state: {
showProgressBars: isValidating || props.loading,
columnVisibility: flattenHideData,
sorting: sortData,
pagination,
rowSelection,
},
muiTableHeadProps: {
sx: {
borderTop: "1px solid #e1e1e1",
borderBottom: "2px solid #e1e1e1",
},
},
muiTableHeadCellProps: {
sx: {
color: "primary.main",
borderLeft: "1px solid #e1e1e1",
"&:first-of-type": {
borderLeft: "unset",
},
},
},
muiTableBodyCellProps: {
sx: {
borderLeft: "1px solid #e1e1e1",
"&:first-of-type": {
borderLeft: "unset",
},
},
},
manualFiltering: true,
manualPagination: true,
manualSorting: true,
onPaginationChange: setPagination,
onSortingChange: onSortingChange,
rowCount: totalRowCount,
renderTopToolbarCustomActions: ({ table }) => (
<>
{TableToolbar && (
<TableToolbar
table={table} table={table}
table_title={table_title}
need_filter={need_filter}
mutate={mutate} mutate={mutate}
filterData={specialFilter || filterData} table_url={table_url}
/> columns={flatColumns}
)} user_id={user_id}
</> page_name={page_name}
), table_name={table_name}
renderRowActions: ({ row }) => <RowActions row={row} mutate={mutate} />, special_data={props.data}
...props, special_filter={specialFilter}
}); setFilterData={props.setFilterData}
/>
return ( );
<DataTable_Paper
table={table}
table_title={table_title}
need_filter={need_filter}
mutate={mutate}
table_url={table_url}
columns={flatColumns}
user_id={user_id}
page_name={page_name}
table_name={table_name}
special_data={props.data}
special_filter={specialFilter}
setFilterData={props.setFilterData}
/>
);
}; };
export default DataTable_Main; export default DataTable_Main;

View File

@@ -2,200 +2,190 @@ import { parseFromValuesOrFunc } from "@/core/utils/utils";
import { TableBody, Typography } from "@mui/material"; import { TableBody, Typography } from "@mui/material";
import { useMRT_RowVirtualizer, useMRT_Rows } from "material-react-table"; import { useMRT_RowVirtualizer, useMRT_Rows } from "material-react-table";
import { memo, useMemo } from "react"; import { memo, useMemo } from "react";
import DataTable_TableBodyRow, { import DataTable_TableBodyRow, { Memo_DataTable_TableBodyRow } from "./TableBodyRow";
Memo_DataTable_TableBodyRow,
} from "./TableBodyRow";
const DataTable_TableBody = ({ columnVirtualizer, table, ...rest }) => { const DataTable_TableBody = ({ columnVirtualizer, table, ...rest }) => {
const { const {
getBottomRows, getBottomRows,
getIsSomeRowsPinned, getIsSomeRowsPinned,
getRowModel, getRowModel,
getState, getState,
getTopRows, getTopRows,
options: { options: {
enableStickyFooter, enableStickyFooter,
enableStickyHeader, enableStickyHeader,
layoutMode, layoutMode,
localization, localization,
memoMode, memoMode,
muiTableBodyProps, muiTableBodyProps,
renderDetailPanel, renderDetailPanel,
renderEmptyRowsFallback, renderEmptyRowsFallback,
rowPinningDisplayMode, rowPinningDisplayMode,
}, },
refs: { tableFooterRef, tableHeadRef, tablePaperRef }, refs: { tableFooterRef, tableHeadRef, tablePaperRef },
} = table; } = table;
const { columnFilters, globalFilter, isFullScreen, rowPinning } = getState(); const { columnFilters, globalFilter, isFullScreen, rowPinning } = getState();
const tableBodyProps = { const tableBodyProps = {
...parseFromValuesOrFunc(muiTableBodyProps, { table }), ...parseFromValuesOrFunc(muiTableBodyProps, { table }),
...rest, ...rest,
}; };
const tableHeadHeight = const tableHeadHeight = ((enableStickyHeader || isFullScreen) && tableHeadRef.current?.clientHeight) || 0;
((enableStickyHeader || isFullScreen) && const tableFooterHeight = (enableStickyFooter && tableFooterRef.current?.clientHeight) || 0;
tableHeadRef.current?.clientHeight) ||
0;
const tableFooterHeight =
(enableStickyFooter && tableFooterRef.current?.clientHeight) || 0;
const pinnedRowIds = useMemo(() => { const pinnedRowIds = useMemo(() => {
if (!rowPinning.bottom?.length && !rowPinning.top?.length) return []; if (!rowPinning.bottom?.length && !rowPinning.top?.length) return [];
return getRowModel() return getRowModel()
.rows.filter((row) => row.getIsPinned()) .rows.filter((row) => row.getIsPinned())
.map((r) => r.id); .map((r) => r.id);
}, [rowPinning, getRowModel().rows]); }, [rowPinning, getRowModel().rows]);
const rows = useMRT_Rows(table); const rows = useMRT_Rows(table);
const rowVirtualizer = useMRT_RowVirtualizer(table, rows); const rowVirtualizer = useMRT_RowVirtualizer(table, rows);
const { virtualRows } = rowVirtualizer ?? {}; const { virtualRows } = rowVirtualizer ?? {};
const commonRowProps = { const commonRowProps = {
columnVirtualizer, columnVirtualizer,
numRows: rows.length, numRows: rows.length,
table, table,
}; };
return ( return (
<> <>
{!rowPinningDisplayMode?.includes("sticky") && {!rowPinningDisplayMode?.includes("sticky") && getIsSomeRowsPinned("top") && (
getIsSomeRowsPinned("top") && ( <TableBody
<TableBody {...tableBodyProps}
{...tableBodyProps} sx={(theme) => ({
sx={(theme) => ({ display: layoutMode?.startsWith("grid") ? "grid" : undefined,
display: layoutMode?.startsWith("grid") ? "grid" : undefined, position: "sticky",
position: "sticky", top: tableHeadHeight - 1,
top: tableHeadHeight - 1, zIndex: 1,
zIndex: 1, ...parseFromValuesOrFunc(tableBodyProps?.sx, theme),
...parseFromValuesOrFunc(tableBodyProps?.sx, theme), })}
})} >
> {getTopRows().map((row, staticRowIndex) => {
{getTopRows().map((row, staticRowIndex) => { const props = {
const props = { ...commonRowProps,
...commonRowProps, row,
row, staticRowIndex,
staticRowIndex, };
}; return memoMode === "rows" ? (
return memoMode === "rows" ? ( <Memo_DataTable_TableBodyRow key={row.id} {...props} />
<Memo_DataTable_TableBodyRow key={row.id} {...props} /> ) : (
) : ( <DataTable_TableBodyRow key={row.id} {...props} />
<DataTable_TableBodyRow key={row.id} {...props} /> );
); })}
})} </TableBody>
</TableBody> )}
)} <TableBody
<TableBody {...tableBodyProps}
{...tableBodyProps} sx={(theme) => ({
sx={(theme) => ({ display: layoutMode?.startsWith("grid") ? "grid" : undefined,
display: layoutMode?.startsWith("grid") ? "grid" : undefined, height: rowVirtualizer ? `${rowVirtualizer.getTotalSize()}px` : undefined,
height: rowVirtualizer minHeight: !rows.length ? "100px" : undefined,
? `${rowVirtualizer.getTotalSize()}px` position: "relative",
: undefined, ...parseFromValuesOrFunc(tableBodyProps?.sx, theme),
minHeight: !rows.length ? "100px" : undefined, })}
position: "relative",
...parseFromValuesOrFunc(tableBodyProps?.sx, theme),
})}
>
{tableBodyProps?.children ??
(!rows.length ? (
<tr
style={{
display: layoutMode?.startsWith("grid") ? "grid" : undefined,
}}
> >
<td {tableBodyProps?.children ??
colSpan={table.getVisibleLeafColumns().length} (!rows.length ? (
style={{ <tr
display: layoutMode?.startsWith("grid") ? "grid" : undefined, style={{
}} display: layoutMode?.startsWith("grid") ? "grid" : undefined,
> }}
{renderEmptyRowsFallback?.({ table }) ?? ( >
<Typography <td
sx={{ colSpan={table.getVisibleLeafColumns().length}
color: "text.secondary", style={{
fontStyle: "italic", display: layoutMode?.startsWith("grid") ? "grid" : undefined,
maxWidth: `min(100vw, ${tablePaperRef.current?.clientWidth ?? 360}px)`, }}
py: "2rem", >
textAlign: "center", {renderEmptyRowsFallback?.({ table }) ?? (
width: "100%", <Typography
}} sx={{
> color: "text.secondary",
{globalFilter || columnFilters.length fontStyle: "italic",
? localization.noResultsFound maxWidth: `min(100vw, ${tablePaperRef.current?.clientWidth ?? 360}px)`,
: localization.noRecordsToDisplay} py: "2rem",
</Typography> textAlign: "center",
)} width: "100%",
</td> }}
</tr> >
) : ( {globalFilter || columnFilters.length
<> ? localization.noResultsFound
{(virtualRows ?? rows).map((rowOrVirtualRow, staticRowIndex) => { : localization.noRecordsToDisplay}
let row = rowOrVirtualRow; </Typography>
if (rowVirtualizer) { )}
if (renderDetailPanel) { </td>
if (rowOrVirtualRow.index % 2 === 1) { </tr>
return null; ) : (
} else { <>
staticRowIndex = rowOrVirtualRow.index / 2; {(virtualRows ?? rows).map((rowOrVirtualRow, staticRowIndex) => {
} let row = rowOrVirtualRow;
} else { if (rowVirtualizer) {
staticRowIndex = rowOrVirtualRow.index; if (renderDetailPanel) {
} if (rowOrVirtualRow.index % 2 === 1) {
row = rows[staticRowIndex]; return null;
} } else {
const props = { staticRowIndex = rowOrVirtualRow.index / 2;
...commonRowProps, }
pinnedRowIds, } else {
row, staticRowIndex = rowOrVirtualRow.index;
rowVirtualizer, }
staticRowIndex, row = rows[staticRowIndex];
virtualRow: rowVirtualizer ? rowOrVirtualRow : undefined, }
}; const props = {
const key = `${row.id}-${row.index}`; ...commonRowProps,
return memoMode === "rows" ? ( pinnedRowIds,
<Memo_DataTable_TableBodyRow key={key} {...props} /> row,
) : ( rowVirtualizer,
<DataTable_TableBodyRow key={key} {...props} /> staticRowIndex,
); virtualRow: rowVirtualizer ? rowOrVirtualRow : undefined,
})} };
</> const key = `${row.id}-${row.index}`;
))} return memoMode === "rows" ? (
</TableBody> <Memo_DataTable_TableBodyRow key={key} {...props} />
{!rowPinningDisplayMode?.includes("sticky") && ) : (
getIsSomeRowsPinned("bottom") && ( <DataTable_TableBodyRow key={key} {...props} />
<TableBody );
{...tableBodyProps} })}
sx={(theme) => ({ </>
bottom: tableFooterHeight - 1, ))}
display: layoutMode?.startsWith("grid") ? "grid" : undefined, </TableBody>
position: "sticky", {!rowPinningDisplayMode?.includes("sticky") && getIsSomeRowsPinned("bottom") && (
zIndex: 1, <TableBody
...parseFromValuesOrFunc(tableBodyProps?.sx, theme), {...tableBodyProps}
})} sx={(theme) => ({
> bottom: tableFooterHeight - 1,
{getBottomRows().map((row, staticRowIndex) => { display: layoutMode?.startsWith("grid") ? "grid" : undefined,
const props = { position: "sticky",
...commonRowProps, zIndex: 1,
row, ...parseFromValuesOrFunc(tableBodyProps?.sx, theme),
staticRowIndex, })}
}; >
return memoMode === "rows" ? ( {getBottomRows().map((row, staticRowIndex) => {
<Memo_DataTable_TableBodyRow key={row.id} {...props} /> const props = {
) : ( ...commonRowProps,
<DataTable_TableBodyRow key={row.id} {...props} /> row,
); staticRowIndex,
})} };
</TableBody> return memoMode === "rows" ? (
)} <Memo_DataTable_TableBodyRow key={row.id} {...props} />
</> ) : (
); <DataTable_TableBodyRow key={row.id} {...props} />
);
})}
</TableBody>
)}
</>
);
}; };
export default DataTable_TableBody; export default DataTable_TableBody;
export const Memo_DataTable_TableBody = memo( export const Memo_DataTable_TableBody = memo(
DataTable_TableBody, DataTable_TableBody,
(prev, next) => prev.table.options.data === next.table.options.data, (prev, next) => prev.table.options.data === next.table.options.data
); );

View File

@@ -1,7 +1,4 @@
import { import { getCommonMRTCellStyles, parseFromValuesOrFunc } from "@/core/utils/utils";
getCommonMRTCellStyles,
parseFromValuesOrFunc,
} from "@/core/utils/utils";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import { Skeleton, TableCell } from "@mui/material"; import { Skeleton, TableCell } from "@mui/material";
import { isCellEditable, openEditingCell } from "material-react-table"; import { isCellEditable, openEditingCell } from "material-react-table";
@@ -9,300 +6,250 @@ import { memo, useEffect, useMemo, useState } from "react";
import DataTable_CopyButton from "../buttons/CopyButton"; import DataTable_CopyButton from "../buttons/CopyButton";
import DataTable_TableBodyCellValue from "./TableBodyCellValue"; import DataTable_TableBodyCellValue from "./TableBodyCellValue";
const DataTable_TableBodyCell = ({ const DataTable_TableBodyCell = ({ cell, numRows, rowRef, staticColumnIndex, staticRowIndex, table, ...rest }) => {
cell, const theme = useTheme();
numRows, const {
rowRef, getState,
staticColumnIndex, options: {
staticRowIndex, columnResizeDirection,
table, columnResizeMode,
...rest createDisplayMode,
}) => { editDisplayMode,
const theme = useTheme(); enableCellActions,
const { enableClickToCopy,
getState, enableColumnOrdering,
options: { enableColumnPinning,
columnResizeDirection, enableGrouping,
columnResizeMode, layoutMode,
createDisplayMode, mrtTheme: { draggingBorderColor, baseBackgroundColor },
editDisplayMode, muiSkeletonProps,
enableCellActions, muiTableBodyCellProps,
enableClickToCopy,
enableColumnOrdering,
enableColumnPinning,
enableGrouping,
layoutMode,
mrtTheme: { draggingBorderColor, baseBackgroundColor },
muiSkeletonProps,
muiTableBodyCellProps,
},
setHoveredColumn,
} = table;
const {
actionCell,
columnSizingInfo,
creatingRow,
density,
draggingColumn,
draggingRow,
editingCell,
editingRow,
hoveredColumn,
hoveredRow,
isLoading,
showSkeletons,
} = getState();
const { column, row } = cell;
const { columnDef } = column;
const { columnDefType } = columnDef;
const args = { cell, column, row, table };
const tableCellProps = {
...parseFromValuesOrFunc(muiTableBodyCellProps, args),
...parseFromValuesOrFunc(columnDef.muiTableBodyCellProps, args),
...rest,
};
const skeletonProps = parseFromValuesOrFunc(muiSkeletonProps, {
cell,
column,
row,
table,
});
const [skeletonWidth, setSkeletonWidth] = useState(100);
useEffect(() => {
if ((!isLoading && !showSkeletons) || skeletonWidth !== 100) return;
const size = column.getSize();
setSkeletonWidth(
columnDefType === "display"
? size / 2
: Math.round(Math.random() * (size - size / 3) + size / 3),
);
}, [isLoading, showSkeletons]);
const draggingBorders = useMemo(() => {
const isDraggingColumn = draggingColumn?.id === column.id;
const isHoveredColumn = hoveredColumn?.id === column.id;
const isDraggingRow = draggingRow?.id === row.id;
const isHoveredRow = hoveredRow?.id === row.id;
const isFirstColumn = column.getIsFirstColumn();
const isLastColumn = column.getIsLastColumn();
const isLastRow = numRows && staticRowIndex === numRows - 1;
const isResizingColumn = columnSizingInfo.isResizingColumn === column.id;
const showResizeBorder =
isResizingColumn && columnResizeMode === "onChange";
const borderStyle = showResizeBorder
? `2px solid ${draggingBorderColor} !important`
: isDraggingColumn || isDraggingRow
? `1px dashed ${theme.palette.grey[500]} !important`
: isHoveredColumn || isHoveredRow || isResizingColumn
? `2px dashed ${draggingBorderColor} !important`
: undefined;
if (showResizeBorder) {
return columnResizeDirection === "ltr"
? { borderRight: borderStyle }
: { borderLeft: borderStyle };
}
return borderStyle
? {
borderBottom:
isDraggingRow || isHoveredRow || (isLastRow && !isResizingColumn)
? borderStyle
: undefined,
borderLeft:
isDraggingColumn ||
isHoveredColumn ||
((isDraggingRow || isHoveredRow) && isFirstColumn)
? borderStyle
: undefined,
borderRight:
isDraggingColumn ||
isHoveredColumn ||
((isDraggingRow || isHoveredRow) && isLastColumn)
? borderStyle
: undefined,
borderTop: isDraggingRow || isHoveredRow ? borderStyle : undefined,
}
: undefined;
}, [
columnSizingInfo.isResizingColumn,
draggingColumn,
draggingRow,
hoveredColumn,
hoveredRow,
staticRowIndex,
]);
const isColumnPinned =
enableColumnPinning &&
columnDef.columnDefType !== "group" &&
column.getIsPinned();
const isEditable = isCellEditable({ cell, table });
const isEditing =
isEditable &&
!["custom", "modal"].includes(editDisplayMode) &&
(editDisplayMode === "table" ||
editingRow?.id === row.id ||
editingCell?.id === cell.id) &&
!row.getIsGrouped();
const isCreating =
isEditable && createDisplayMode === "row" && creatingRow?.id === row.id;
const showClickToCopyButton =
(parseFromValuesOrFunc(enableClickToCopy, cell) === true ||
parseFromValuesOrFunc(columnDef.enableClickToCopy, cell) === true) &&
!["context-menu", false].includes(
// @ts-ignore
parseFromValuesOrFunc(columnDef.enableClickToCopy, cell),
);
const isRightClickable = parseFromValuesOrFunc(enableCellActions, cell);
const cellValueProps = {
cell,
table,
};
const handleDoubleClick = (event) => {
tableCellProps?.onDoubleClick?.(event);
openEditingCell({ cell, table });
};
const handleDragEnter = (e) => {
tableCellProps?.onDragEnter?.(e);
if (enableGrouping && hoveredColumn?.id === "drop-zone") {
setHoveredColumn(null);
}
if (enableColumnOrdering && draggingColumn) {
setHoveredColumn(
columnDef.enableColumnOrdering !== false ? column : null,
);
}
};
const handleDragOver = (e) => {
if (columnDef.enableColumnOrdering !== false) {
e.preventDefault();
}
};
const handleContextMenu = (e) => {
tableCellProps?.onContextMenu?.(e);
if (isRightClickable) {
e.preventDefault();
table.setActionCell(cell);
table.refs.actionCellRef.current = e.currentTarget;
}
};
return (
<TableCell
data-index={staticColumnIndex}
data-pinned={!!isColumnPinned || undefined}
{...tableCellProps}
onContextMenu={handleContextMenu}
onDoubleClick={handleDoubleClick}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
sx={(theme) => ({
"&:hover": {
outline:
actionCell?.id === cell.id ||
(editDisplayMode === "cell" && isEditable) ||
(editDisplayMode === "table" && (isCreating || isEditing))
? `1px solid ${theme.palette.grey[500]}`
: undefined,
textOverflow: "clip",
}, },
alignItems: layoutMode?.startsWith("grid") ? "center" : undefined, setHoveredColumn,
cursor: isRightClickable } = table;
? "context-menu" const {
: isEditable && editDisplayMode === "cell" actionCell,
? "pointer" columnSizingInfo,
: "inherit", creatingRow,
outline: density,
actionCell?.id === cell.id draggingColumn,
? `1px solid ${theme.palette.grey[500]}` draggingRow,
: undefined, editingCell,
outlineOffset: "-1px", editingRow,
overflow: "hidden", hoveredColumn,
p: hoveredRow,
density === "compact" isLoading,
? columnDefType === "display" showSkeletons,
? "0 0.5rem" } = getState();
: "0.5rem" const { column, row } = cell;
: density === "comfortable" const { columnDef } = column;
? columnDefType === "display" const { columnDefType } = columnDef;
? "0.5rem 0.75rem"
: "1rem"
: columnDefType === "display"
? "1rem 1.25rem"
: "1.5rem",
textOverflow: columnDefType !== "display" ? "ellipsis" : undefined, const args = { cell, column, row, table };
whiteSpace: const tableCellProps = {
row.getIsPinned() || density === "compact" ? "nowrap" : "normal", ...parseFromValuesOrFunc(muiTableBodyCellProps, args),
...getCommonMRTCellStyles({ ...parseFromValuesOrFunc(columnDef.muiTableBodyCellProps, args),
column, ...rest,
table, };
tableCellProps,
theme, const skeletonProps = parseFromValuesOrFunc(muiSkeletonProps, {
}), cell,
...draggingBorders, column,
backgroundColor: baseBackgroundColor, row,
})} table,
> });
{tableCellProps.children ?? (
<> const [skeletonWidth, setSkeletonWidth] = useState(100);
{cell.getIsPlaceholder() ? ( useEffect(() => {
(columnDef.PlaceholderCell?.({ cell, column, row, table }) ?? null) if ((!isLoading && !showSkeletons) || skeletonWidth !== 100) return;
) : showSkeletons !== false && (isLoading || showSkeletons) ? ( const size = column.getSize();
<Skeleton setSkeletonWidth(
animation="wave" columnDefType === "display" ? size / 2 : Math.round(Math.random() * (size - size / 3) + size / 3)
height={20} );
width={skeletonWidth} }, [isLoading, showSkeletons]);
{...skeletonProps}
/> const draggingBorders = useMemo(() => {
) : columnDefType === "display" && const isDraggingColumn = draggingColumn?.id === column.id;
(["mrt-row-expand", "mrt-row-numbers", "mrt-row-select"].includes( const isHoveredColumn = hoveredColumn?.id === column.id;
column.id, const isDraggingRow = draggingRow?.id === row.id;
) || const isHoveredRow = hoveredRow?.id === row.id;
!row.getIsGrouped()) ? ( const isFirstColumn = column.getIsFirstColumn();
columnDef.Cell?.({ const isLastColumn = column.getIsLastColumn();
cell, const isLastRow = numRows && staticRowIndex === numRows - 1;
column, const isResizingColumn = columnSizingInfo.isResizingColumn === column.id;
renderedCellValue: cell.renderValue(), const showResizeBorder = isResizingColumn && columnResizeMode === "onChange";
row,
rowRef, const borderStyle = showResizeBorder
staticColumnIndex, ? `2px solid ${draggingBorderColor} !important`
staticRowIndex, : isDraggingColumn || isDraggingRow
table, ? `1px dashed ${theme.palette.grey[500]} !important`
}) : isHoveredColumn || isHoveredRow || isResizingColumn
) : showClickToCopyButton && columnDef.enableClickToCopy !== false ? ( ? `2px dashed ${draggingBorderColor} !important`
<DataTable_CopyButton cell={cell} table={table}> : undefined;
<DataTable_TableBodyCellValue {...cellValueProps} />
</DataTable_CopyButton> if (showResizeBorder) {
) : ( return columnResizeDirection === "ltr" ? { borderRight: borderStyle } : { borderLeft: borderStyle };
<DataTable_TableBodyCellValue {...cellValueProps} /> }
)}
{cell.getIsGrouped() && !columnDef.GroupedCell && ( return borderStyle
<> ({row.subRows?.length})</> ? {
)} borderBottom:
</> isDraggingRow || isHoveredRow || (isLastRow && !isResizingColumn) ? borderStyle : undefined,
)} borderLeft:
</TableCell> isDraggingColumn || isHoveredColumn || ((isDraggingRow || isHoveredRow) && isFirstColumn)
); ? borderStyle
: undefined,
borderRight:
isDraggingColumn || isHoveredColumn || ((isDraggingRow || isHoveredRow) && isLastColumn)
? borderStyle
: undefined,
borderTop: isDraggingRow || isHoveredRow ? borderStyle : undefined,
}
: undefined;
}, [columnSizingInfo.isResizingColumn, draggingColumn, draggingRow, hoveredColumn, hoveredRow, staticRowIndex]);
const isColumnPinned = enableColumnPinning && columnDef.columnDefType !== "group" && column.getIsPinned();
const isEditable = isCellEditable({ cell, table });
const isEditing =
isEditable &&
!["custom", "modal"].includes(editDisplayMode) &&
(editDisplayMode === "table" || editingRow?.id === row.id || editingCell?.id === cell.id) &&
!row.getIsGrouped();
const isCreating = isEditable && createDisplayMode === "row" && creatingRow?.id === row.id;
const showClickToCopyButton =
(parseFromValuesOrFunc(enableClickToCopy, cell) === true ||
parseFromValuesOrFunc(columnDef.enableClickToCopy, cell) === true) &&
!["context-menu", false].includes(
// @ts-ignore
parseFromValuesOrFunc(columnDef.enableClickToCopy, cell)
);
const isRightClickable = parseFromValuesOrFunc(enableCellActions, cell);
const cellValueProps = {
cell,
table,
};
const handleDoubleClick = (event) => {
tableCellProps?.onDoubleClick?.(event);
openEditingCell({ cell, table });
};
const handleDragEnter = (e) => {
tableCellProps?.onDragEnter?.(e);
if (enableGrouping && hoveredColumn?.id === "drop-zone") {
setHoveredColumn(null);
}
if (enableColumnOrdering && draggingColumn) {
setHoveredColumn(columnDef.enableColumnOrdering !== false ? column : null);
}
};
const handleDragOver = (e) => {
if (columnDef.enableColumnOrdering !== false) {
e.preventDefault();
}
};
const handleContextMenu = (e) => {
tableCellProps?.onContextMenu?.(e);
if (isRightClickable) {
e.preventDefault();
table.setActionCell(cell);
table.refs.actionCellRef.current = e.currentTarget;
}
};
return (
<TableCell
data-index={staticColumnIndex}
data-pinned={!!isColumnPinned || undefined}
{...tableCellProps}
onContextMenu={handleContextMenu}
onDoubleClick={handleDoubleClick}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
sx={(theme) => ({
"&:hover": {
outline:
actionCell?.id === cell.id ||
(editDisplayMode === "cell" && isEditable) ||
(editDisplayMode === "table" && (isCreating || isEditing))
? `1px solid ${theme.palette.grey[500]}`
: undefined,
textOverflow: "clip",
},
alignItems: layoutMode?.startsWith("grid") ? "center" : undefined,
cursor: isRightClickable
? "context-menu"
: isEditable && editDisplayMode === "cell"
? "pointer"
: "inherit",
outline: actionCell?.id === cell.id ? `1px solid ${theme.palette.grey[500]}` : undefined,
outlineOffset: "-1px",
overflow: "hidden",
p:
density === "compact"
? columnDefType === "display"
? "0 0.5rem"
: "0.5rem"
: density === "comfortable"
? columnDefType === "display"
? "0.5rem 0.75rem"
: "1rem"
: columnDefType === "display"
? "1rem 1.25rem"
: "1.5rem",
textOverflow: columnDefType !== "display" ? "ellipsis" : undefined,
whiteSpace: row.getIsPinned() || density === "compact" ? "nowrap" : "normal",
...getCommonMRTCellStyles({
column,
table,
tableCellProps,
theme,
}),
...draggingBorders,
backgroundColor: baseBackgroundColor,
})}
>
{tableCellProps.children ?? (
<>
{cell.getIsPlaceholder() ? (
(columnDef.PlaceholderCell?.({ cell, column, row, table }) ?? null)
) : showSkeletons !== false && (isLoading || showSkeletons) ? (
<Skeleton animation="wave" height={20} width={skeletonWidth} {...skeletonProps} />
) : columnDefType === "display" &&
(["mrt-row-expand", "mrt-row-numbers", "mrt-row-select"].includes(column.id) ||
!row.getIsGrouped()) ? (
columnDef.Cell?.({
cell,
column,
renderedCellValue: cell.renderValue(),
row,
rowRef,
staticColumnIndex,
staticRowIndex,
table,
})
) : showClickToCopyButton && columnDef.enableClickToCopy !== false ? (
<DataTable_CopyButton cell={cell} table={table}>
<DataTable_TableBodyCellValue {...cellValueProps} />
</DataTable_CopyButton>
) : (
<DataTable_TableBodyCellValue {...cellValueProps} />
)}
{cell.getIsGrouped() && !columnDef.GroupedCell && <> ({row.subRows?.length})</>}
</>
)}
</TableCell>
);
}; };
export default DataTable_TableBodyCell; export default DataTable_TableBodyCell;
export const Memo_DataTable_TableBodyCell = memo( export const Memo_DataTable_TableBodyCell = memo(DataTable_TableBodyCell, (prev, next) => next.cell === prev.cell);
DataTable_TableBodyCell,
(prev, next) => next.cell === prev.cell,
);

View File

@@ -2,111 +2,102 @@ import { Box } from "@mui/material";
const allowedTypes = ["string", "number"]; const allowedTypes = ["string", "number"];
const DataTable_TableBodyCellValue = ({ const DataTable_TableBodyCellValue = ({ cell, rowRef, staticColumnIndex, staticRowIndex, table }) => {
cell, const {
rowRef, getState,
staticColumnIndex, options: {
staticRowIndex, enableFilterMatchHighlighting,
table, mrtTheme: { matchHighlightColor },
}) => { },
const { } = table;
getState, const { column, row } = cell;
options: { const { columnDef } = column;
enableFilterMatchHighlighting, const { globalFilter, globalFilterFn } = getState();
mrtTheme: { matchHighlightColor }, const filterValue = column.getFilterValue();
},
} = table;
const { column, row } = cell;
const { columnDef } = column;
const { globalFilter, globalFilterFn } = getState();
const filterValue = column.getFilterValue();
let renderedCellValue = let renderedCellValue =
cell.getIsAggregated() && columnDef.AggregatedCell cell.getIsAggregated() && columnDef.AggregatedCell
? columnDef.AggregatedCell({ ? columnDef.AggregatedCell({
cell, cell,
column, column,
row, row,
table, table,
}) })
: row.getIsGrouped() && !cell.getIsGrouped() : row.getIsGrouped() && !cell.getIsGrouped()
? null ? null
: cell.getIsGrouped() && columnDef.GroupedCell : cell.getIsGrouped() && columnDef.GroupedCell
? columnDef.GroupedCell({ ? columnDef.GroupedCell({
cell, cell,
column, column,
row, row,
table, table,
}) })
: undefined; : undefined;
const isGroupedValue = renderedCellValue !== undefined; const isGroupedValue = renderedCellValue !== undefined;
if (!isGroupedValue) { if (!isGroupedValue) {
renderedCellValue = cell.renderValue(); renderedCellValue = cell.renderValue();
}
if (
enableFilterMatchHighlighting &&
columnDef.enableFilterMatchHighlighting !== false &&
String(renderedCellValue) &&
allowedTypes.includes(typeof renderedCellValue) &&
((filterValue &&
allowedTypes.includes(typeof filterValue) &&
["autocomplete", "text"].includes(columnDef.filterVariant)) ||
(globalFilter &&
allowedTypes.includes(typeof globalFilter) &&
column.getCanGlobalFilter()))
) {
const chunks = highlightWords?.({
matchExactly:
(filterValue ? columnDef._filterFn : globalFilterFn) !== "fuzzy",
query: (filterValue ?? globalFilter ?? "").toString(),
text: renderedCellValue?.toString(),
});
if (chunks?.length > 1 || chunks?.[0]?.match) {
renderedCellValue = (
<span aria-label={renderedCellValue} role="note">
{chunks?.map(({ key, match, text }) => (
<Box
aria-hidden="true"
component="span"
key={key}
sx={
match
? {
backgroundColor: matchHighlightColor,
borderRadius: "2px",
color: (theme) =>
theme.palette.mode === "dark"
? theme.palette.common.white
: theme.palette.common.black,
padding: "2px 1px",
}
: undefined
}
>
{text}
</Box>
)) ?? renderedCellValue}
</span>
);
} }
}
if (columnDef.Cell && !isGroupedValue) { if (
renderedCellValue = columnDef.Cell({ enableFilterMatchHighlighting &&
cell, columnDef.enableFilterMatchHighlighting !== false &&
column, String(renderedCellValue) &&
renderedCellValue, allowedTypes.includes(typeof renderedCellValue) &&
row, ((filterValue &&
rowRef, allowedTypes.includes(typeof filterValue) &&
staticColumnIndex, ["autocomplete", "text"].includes(columnDef.filterVariant)) ||
staticRowIndex, (globalFilter && allowedTypes.includes(typeof globalFilter) && column.getCanGlobalFilter()))
table, ) {
}); const chunks = highlightWords?.({
} matchExactly: (filterValue ? columnDef._filterFn : globalFilterFn) !== "fuzzy",
query: (filterValue ?? globalFilter ?? "").toString(),
text: renderedCellValue?.toString(),
});
if (chunks?.length > 1 || chunks?.[0]?.match) {
renderedCellValue = (
<span aria-label={renderedCellValue} role="note">
{chunks?.map(({ key, match, text }) => (
<Box
aria-hidden="true"
component="span"
key={key}
sx={
match
? {
backgroundColor: matchHighlightColor,
borderRadius: "2px",
color: (theme) =>
theme.palette.mode === "dark"
? theme.palette.common.white
: theme.palette.common.black,
padding: "2px 1px",
}
: undefined
}
>
{text}
</Box>
)) ?? renderedCellValue}
</span>
);
}
}
return renderedCellValue; if (columnDef.Cell && !isGroupedValue) {
renderedCellValue = columnDef.Cell({
cell,
column,
renderedCellValue,
row,
rowRef,
staticColumnIndex,
staticRowIndex,
table,
});
}
return renderedCellValue;
}; };
export default DataTable_TableBodyCellValue; export default DataTable_TableBodyCellValue;

View File

@@ -1,258 +1,216 @@
import { import { commonCellBeforeAfterStyles, getCommonPinnedCellStyles, parseFromValuesOrFunc } from "@/core/utils/utils";
commonCellBeforeAfterStyles,
getCommonPinnedCellStyles,
parseFromValuesOrFunc,
} from "@/core/utils/utils";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import { TableRow, alpha, darken, lighten } from "@mui/material"; import { TableRow, alpha, darken, lighten } from "@mui/material";
import { getIsRowSelected } from "material-react-table"; import { getIsRowSelected } from "material-react-table";
import { memo, useMemo, useRef } from "react"; import { memo, useMemo, useRef } from "react";
import DataTable_TableBodyCell, { import DataTable_TableBodyCell, { Memo_DataTable_TableBodyCell } from "./TableBodyCell";
Memo_DataTable_TableBodyCell,
} from "./TableBodyCell";
import DataTable_TableDetailPanel from "./TableDetailPanel"; import DataTable_TableDetailPanel from "./TableDetailPanel";
const DataTable_TableBodyRow = ({ const DataTable_TableBodyRow = ({
columnVirtualizer, columnVirtualizer,
numRows, numRows,
pinnedRowIds, pinnedRowIds,
row, row,
rowVirtualizer, rowVirtualizer,
staticRowIndex, staticRowIndex,
table, table,
virtualRow, virtualRow,
...rest ...rest
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { const {
getState, getState,
options: { options: {
enableRowOrdering, enableRowOrdering,
enableRowPinning, enableRowPinning,
enableStickyFooter, enableStickyFooter,
enableStickyHeader, enableStickyHeader,
layoutMode, layoutMode,
memoMode, memoMode,
mrtTheme: { mrtTheme: { baseBackgroundColor, pinnedRowBackgroundColor, selectedRowBackgroundColor },
baseBackgroundColor, muiTableBodyRowProps,
pinnedRowBackgroundColor, renderDetailPanel,
selectedRowBackgroundColor, rowPinningDisplayMode,
}, },
muiTableBodyRowProps, refs: { tableFooterRef, tableHeadRef },
renderDetailPanel, setHoveredRow,
rowPinningDisplayMode, } = table;
}, const { density, draggingColumn, draggingRow, editingCell, editingRow, hoveredRow, isFullScreen, rowPinning } =
refs: { tableFooterRef, tableHeadRef }, getState();
setHoveredRow,
} = table;
const {
density,
draggingColumn,
draggingRow,
editingCell,
editingRow,
hoveredRow,
isFullScreen,
rowPinning,
} = getState();
const visibleCells = row.getVisibleCells(); const visibleCells = row.getVisibleCells();
const { virtualColumns, virtualPaddingLeft, virtualPaddingRight } = const { virtualColumns, virtualPaddingLeft, virtualPaddingRight } = columnVirtualizer ?? {};
columnVirtualizer ?? {};
const isRowSelected = getIsRowSelected({ row, table }); const isRowSelected = getIsRowSelected({ row, table });
const isRowPinned = enableRowPinning && row.getIsPinned(); const isRowPinned = enableRowPinning && row.getIsPinned();
const isDraggingRow = draggingRow?.id === row.id; const isDraggingRow = draggingRow?.id === row.id;
const isHoveredRow = hoveredRow?.id === row.id; const isHoveredRow = hoveredRow?.id === row.id;
const tableRowProps = { const tableRowProps = {
...parseFromValuesOrFunc(muiTableBodyRowProps, { ...parseFromValuesOrFunc(muiTableBodyRowProps, {
row, row,
staticRowIndex, staticRowIndex,
table, table,
}), }),
...rest, ...rest,
}; };
const [bottomPinnedIndex, topPinnedIndex] = useMemo(() => { const [bottomPinnedIndex, topPinnedIndex] = useMemo(() => {
if ( if (!enableRowPinning || !rowPinningDisplayMode?.includes("sticky") || !pinnedRowIds || !row.getIsPinned())
!enableRowPinning || return [];
!rowPinningDisplayMode?.includes("sticky") || return [[...pinnedRowIds].reverse().indexOf(row.id), pinnedRowIds.indexOf(row.id)];
!pinnedRowIds || }, [pinnedRowIds, rowPinning]);
!row.getIsPinned()
)
return [];
return [
[...pinnedRowIds].reverse().indexOf(row.id),
pinnedRowIds.indexOf(row.id),
];
}, [pinnedRowIds, rowPinning]);
const tableHeadHeight = const tableHeadHeight = ((enableStickyHeader || isFullScreen) && tableHeadRef.current?.clientHeight) || 0;
((enableStickyHeader || isFullScreen) && const tableFooterHeight = (enableStickyFooter && tableFooterRef.current?.clientHeight) || 0;
tableHeadRef.current?.clientHeight) ||
0;
const tableFooterHeight =
(enableStickyFooter && tableFooterRef.current?.clientHeight) || 0;
const sx = parseFromValuesOrFunc(tableRowProps?.sx, theme); const sx = parseFromValuesOrFunc(tableRowProps?.sx, theme);
const defaultRowHeight = const defaultRowHeight = density === "compact" ? 37 : density === "comfortable" ? 53 : 69;
density === "compact" ? 37 : density === "comfortable" ? 53 : 69;
const customRowHeight = const customRowHeight =
// @ts-ignore // @ts-ignore
parseInt(tableRowProps?.style?.height ?? sx?.height, 10) || undefined; parseInt(tableRowProps?.style?.height ?? sx?.height, 10) || undefined;
const rowHeight = customRowHeight || defaultRowHeight; const rowHeight = customRowHeight || defaultRowHeight;
const handleDragEnter = (_e) => { const handleDragEnter = (_e) => {
if (enableRowOrdering && draggingRow) { if (enableRowOrdering && draggingRow) {
setHoveredRow(row); setHoveredRow(row);
} }
}; };
const handleDragOver = (e) => { const handleDragOver = (e) => {
e.preventDefault(); e.preventDefault();
}; };
const rowRef = useRef(null); const rowRef = useRef(null);
const cellHighlightColor = isRowSelected const cellHighlightColor = isRowSelected
? selectedRowBackgroundColor ? selectedRowBackgroundColor
: isRowPinned : isRowPinned
? pinnedRowBackgroundColor ? pinnedRowBackgroundColor
: undefined; : undefined;
const cellHighlightColorHover = const cellHighlightColorHover =
tableRowProps?.hover !== false tableRowProps?.hover !== false
? isRowSelected ? isRowSelected
? cellHighlightColor ? cellHighlightColor
: theme.palette.mode === "dark" : theme.palette.mode === "dark"
? `${lighten(baseBackgroundColor, 0.2)}` ? `${lighten(baseBackgroundColor, 0.2)}`
: `${darken(baseBackgroundColor, 0.2)}` : `${darken(baseBackgroundColor, 0.2)}`
: undefined; : undefined;
return ( return (
<> <>
<TableRow <TableRow
data-index={renderDetailPanel ? staticRowIndex * 2 : staticRowIndex} data-index={renderDetailPanel ? staticRowIndex * 2 : staticRowIndex}
data-pinned={!!isRowPinned || undefined} data-pinned={!!isRowPinned || undefined}
data-selected={isRowSelected || undefined} data-selected={isRowSelected || undefined}
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragOver={handleDragOver} onDragOver={handleDragOver}
ref={(node) => { ref={(node) => {
if (node) { if (node) {
rowRef.current = node; rowRef.current = node;
rowVirtualizer?.measureElement(node); rowVirtualizer?.measureElement(node);
} }
}} }}
selected={isRowSelected} selected={isRowSelected}
{...tableRowProps} {...tableRowProps}
style={{ style={{
transform: virtualRow transform: virtualRow ? `translateY(${virtualRow.start}px)` : undefined,
? `translateY(${virtualRow.start}px)` ...tableRowProps?.style,
: undefined, }}
...tableRowProps?.style, sx={(theme) => ({
}} "&:hover td:after": cellHighlightColorHover
sx={(theme) => ({ ? {
"&:hover td:after": cellHighlightColorHover backgroundColor: alpha(cellHighlightColorHover, 0.3),
? { ...commonCellBeforeAfterStyles,
backgroundColor: alpha(cellHighlightColorHover, 0.3), }
...commonCellBeforeAfterStyles, : undefined,
} bottom:
: undefined, !virtualRow && bottomPinnedIndex !== undefined && isRowPinned
bottom: ? `${bottomPinnedIndex * rowHeight + (enableStickyFooter ? tableFooterHeight - 1 : 0)}px`
!virtualRow && bottomPinnedIndex !== undefined && isRowPinned : undefined,
? `${bottomPinnedIndex * rowHeight + (enableStickyFooter ? tableFooterHeight - 1 : 0)}px` boxSizing: "border-box",
: undefined, display: layoutMode?.startsWith("grid") ? "flex" : undefined,
boxSizing: "border-box", opacity: isRowPinned ? 0.97 : isDraggingRow || isHoveredRow ? 0.5 : 1,
display: layoutMode?.startsWith("grid") ? "flex" : undefined, position: virtualRow
opacity: isRowPinned ? 0.97 : isDraggingRow || isHoveredRow ? 0.5 : 1, ? "absolute"
position: virtualRow : rowPinningDisplayMode?.includes("sticky") && isRowPinned
? "absolute" ? "sticky"
: rowPinningDisplayMode?.includes("sticky") && isRowPinned : "relative",
? "sticky" td: {
: "relative", ...getCommonPinnedCellStyles({ table, theme }),
td: { },
...getCommonPinnedCellStyles({ table, theme }), "td:after": cellHighlightColor
}, ? {
"td:after": cellHighlightColor backgroundColor: cellHighlightColor,
? { ...commonCellBeforeAfterStyles,
backgroundColor: cellHighlightColor, }
...commonCellBeforeAfterStyles, : undefined,
} top: virtualRow
: undefined, ? 0
top: virtualRow : topPinnedIndex !== undefined && isRowPinned
? 0 ? `${
: topPinnedIndex !== undefined && isRowPinned topPinnedIndex * rowHeight +
? `${ (enableStickyHeader || isFullScreen ? tableHeadHeight - 1 : 0)
topPinnedIndex * rowHeight + }px`
(enableStickyHeader || isFullScreen ? tableHeadHeight - 1 : 0) : undefined,
}px` transition: virtualRow ? "none" : "all 150ms ease-in-out",
: undefined, width: "100%",
transition: virtualRow ? "none" : "all 150ms ease-in-out", zIndex: rowPinningDisplayMode?.includes("sticky") && isRowPinned ? 2 : 0,
width: "100%", ...sx,
zIndex: })}
rowPinningDisplayMode?.includes("sticky") && isRowPinned ? 2 : 0, >
...sx, {virtualPaddingLeft ? <td style={{ display: "flex", width: virtualPaddingLeft }} /> : null}
})} {(virtualColumns ?? visibleCells).map((cellOrVirtualCell, staticColumnIndex) => {
> let cell = cellOrVirtualCell;
{virtualPaddingLeft ? ( if (columnVirtualizer) {
<td style={{ display: "flex", width: virtualPaddingLeft }} /> staticColumnIndex = cellOrVirtualCell.index;
) : null} cell = visibleCells[staticColumnIndex];
{(virtualColumns ?? visibleCells).map( }
(cellOrVirtualCell, staticColumnIndex) => { const props = {
let cell = cellOrVirtualCell; cell,
if (columnVirtualizer) { numRows,
staticColumnIndex = cellOrVirtualCell.index; rowRef,
cell = visibleCells[staticColumnIndex]; staticColumnIndex,
} staticRowIndex,
const props = { table,
cell, };
numRows, return cell ? (
rowRef, memoMode === "cells" &&
staticColumnIndex, cell.column.columnDef.columnDefType === "data" &&
staticRowIndex, !draggingColumn &&
table, !draggingRow &&
}; editingCell?.id !== cell.id &&
return cell ? ( editingRow?.id !== row.id ? (
memoMode === "cells" && <Memo_DataTable_TableBodyCell key={cell.id} {...props} />
cell.column.columnDef.columnDefType === "data" && ) : (
!draggingColumn && <DataTable_TableBodyCell key={cell.id} {...props} />
!draggingRow && )
editingCell?.id !== cell.id && ) : null;
editingRow?.id !== row.id ? ( })}
<Memo_DataTable_TableBodyCell key={cell.id} {...props} /> {virtualPaddingRight ? <td style={{ display: "flex", width: virtualPaddingRight }} /> : null}
) : ( </TableRow>
<DataTable_TableBodyCell key={cell.id} {...props} /> {renderDetailPanel && !row.getIsGrouped() && (
) <DataTable_TableDetailPanel
) : null; parentRowRef={rowRef}
}, row={row}
)} rowVirtualizer={rowVirtualizer}
{virtualPaddingRight ? ( staticRowIndex={staticRowIndex}
<td style={{ display: "flex", width: virtualPaddingRight }} /> table={table}
) : null} virtualRow={virtualRow}
</TableRow> />
{renderDetailPanel && !row.getIsGrouped() && ( )}
<DataTable_TableDetailPanel </>
parentRowRef={rowRef} );
row={row}
rowVirtualizer={rowVirtualizer}
staticRowIndex={staticRowIndex}
table={table}
virtualRow={virtualRow}
/>
)}
</>
);
}; };
export default DataTable_TableBodyRow; export default DataTable_TableBodyRow;
export const Memo_DataTable_TableBodyRow = memo( export const Memo_DataTable_TableBodyRow = memo(
DataTable_TableBodyRow, DataTable_TableBodyRow,
(prev, next) => (prev, next) => prev.row === next.row && prev.staticRowIndex === next.staticRowIndex
prev.row === next.row && prev.staticRowIndex === next.staticRowIndex,
); );

View File

@@ -2,90 +2,86 @@ import { parseFromValuesOrFunc } from "@/core/utils/utils";
import { Collapse, TableCell, TableRow } from "@mui/material"; import { Collapse, TableCell, TableRow } from "@mui/material";
const DataTable_TableDetailPanel = ({ const DataTable_TableDetailPanel = ({
parentRowRef, parentRowRef,
row,
rowVirtualizer,
staticRowIndex,
table,
virtualRow,
...rest
}) => {
const {
getState,
getVisibleLeafColumns,
options: {
layoutMode,
mrtTheme: { baseBackgroundColor },
muiDetailPanelProps,
muiTableBodyRowProps,
renderDetailPanel,
},
} = table;
const { isLoading } = getState();
const tableRowProps = parseFromValuesOrFunc(muiTableBodyRowProps, {
isDetailPanel: true,
row, row,
rowVirtualizer,
staticRowIndex, staticRowIndex,
table, table,
}); virtualRow,
...rest
}) => {
const {
getState,
getVisibleLeafColumns,
options: {
layoutMode,
mrtTheme: { baseBackgroundColor },
muiDetailPanelProps,
muiTableBodyRowProps,
renderDetailPanel,
},
} = table;
const { isLoading } = getState();
const tableCellProps = { const tableRowProps = parseFromValuesOrFunc(muiTableBodyRowProps, {
...parseFromValuesOrFunc(muiDetailPanelProps, { isDetailPanel: true,
row, row,
table, staticRowIndex,
}), table,
...rest, });
};
const DetailPanel = !isLoading && renderDetailPanel?.({ row, table }); const tableCellProps = {
...parseFromValuesOrFunc(muiDetailPanelProps, {
row,
table,
}),
...rest,
};
return ( const DetailPanel = !isLoading && renderDetailPanel?.({ row, table });
<TableRow
className="Mui-TableBodyCell-DetailPanel" return (
data-index={renderDetailPanel ? staticRowIndex * 2 + 1 : staticRowIndex} <TableRow
ref={(node) => { className="Mui-TableBodyCell-DetailPanel"
if (node) { data-index={renderDetailPanel ? staticRowIndex * 2 + 1 : staticRowIndex}
rowVirtualizer?.measureElement?.(node); ref={(node) => {
} if (node) {
}} rowVirtualizer?.measureElement?.(node);
{...tableRowProps} }
sx={(theme) => ({ }}
display: layoutMode?.startsWith("grid") ? "flex" : undefined, {...tableRowProps}
position: virtualRow ? "absolute" : undefined, sx={(theme) => ({
top: virtualRow display: layoutMode?.startsWith("grid") ? "flex" : undefined,
? `${parentRowRef.current?.getBoundingClientRect()?.height}px` position: virtualRow ? "absolute" : undefined,
: undefined, top: virtualRow ? `${parentRowRef.current?.getBoundingClientRect()?.height}px` : undefined,
transform: virtualRow transform: virtualRow ? `translateY(${virtualRow?.start}px)` : undefined,
? `translateY(${virtualRow?.start}px)` width: "100%",
: undefined, ...parseFromValuesOrFunc(tableRowProps?.sx, theme),
width: "100%", })}
...parseFromValuesOrFunc(tableRowProps?.sx, theme), >
})} <TableCell
> className="Mui-TableBodyCell-DetailPanel"
<TableCell colSpan={getVisibleLeafColumns().length}
className="Mui-TableBodyCell-DetailPanel" {...tableCellProps}
colSpan={getVisibleLeafColumns().length} sx={(theme) => ({
{...tableCellProps} backgroundColor: virtualRow ? baseBackgroundColor : undefined,
sx={(theme) => ({ borderBottom: !row.getIsExpanded() ? "none" : undefined,
backgroundColor: virtualRow ? baseBackgroundColor : undefined, display: layoutMode?.startsWith("grid") ? "flex" : undefined,
borderBottom: !row.getIsExpanded() ? "none" : undefined, py: !!DetailPanel && row.getIsExpanded() ? "1rem" : 0,
display: layoutMode?.startsWith("grid") ? "flex" : undefined, transition: !virtualRow ? "all 150ms ease-in-out" : undefined,
py: !!DetailPanel && row.getIsExpanded() ? "1rem" : 0, width: `100%`,
transition: !virtualRow ? "all 150ms ease-in-out" : undefined, ...parseFromValuesOrFunc(tableCellProps?.sx, theme),
width: `100%`, })}
...parseFromValuesOrFunc(tableCellProps?.sx, theme), >
})} {virtualRow ? (
> row.getIsExpanded() && DetailPanel
{virtualRow ? ( ) : (
row.getIsExpanded() && DetailPanel <Collapse in={row.getIsExpanded()} mountOnEnter unmountOnExit>
) : ( {DetailPanel}
<Collapse in={row.getIsExpanded()} mountOnEnter unmountOnExit> </Collapse>
{DetailPanel} )}
</Collapse> </TableCell>
)} </TableRow>
</TableCell> );
</TableRow>
);
}; };
export default DataTable_TableDetailPanel; export default DataTable_TableDetailPanel;

View File

@@ -1,74 +1,68 @@
import { import { getCommonTooltipProps, parseFromValuesOrFunc } from "@/core/utils/utils";
getCommonTooltipProps,
parseFromValuesOrFunc,
} from "@/core/utils/utils";
import { Button, Tooltip } from "@mui/material"; import { Button, Tooltip } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
const DataTable_CopyButton = ({ cell, table, ...rest }) => { const DataTable_CopyButton = ({ cell, table, ...rest }) => {
const { const {
options: { localization, muiCopyButtonProps }, options: { localization, muiCopyButtonProps },
} = table; } = table;
const { column, row } = cell; const { column, row } = cell;
const { columnDef } = column; const { columnDef } = column;
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const handleCopy = (event, text) => { const handleCopy = (event, text) => {
event.stopPropagation(); event.stopPropagation();
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 4000); setTimeout(() => setCopied(false), 4000);
}; };
const buttonProps = { const buttonProps = {
...parseFromValuesOrFunc(muiCopyButtonProps, { ...parseFromValuesOrFunc(muiCopyButtonProps, {
cell, cell,
column, column,
row, row,
table, table,
}), }),
...parseFromValuesOrFunc(columnDef.muiCopyButtonProps, { ...parseFromValuesOrFunc(columnDef.muiCopyButtonProps, {
cell, cell,
column, column,
row, row,
table, table,
}), }),
...rest, ...rest,
}; };
return ( return (
<Tooltip <Tooltip
{...getCommonTooltipProps("top")} {...getCommonTooltipProps("top")}
title={ title={buttonProps?.title ?? (copied ? localization.copiedToClipboard : localization.clickToCopy)}
buttonProps?.title ?? >
(copied ? localization.copiedToClipboard : localization.clickToCopy) <Button
} onClick={(e) => handleCopy(e, cell.getValue())}
> size="small"
<Button type="button"
onClick={(e) => handleCopy(e, cell.getValue())} variant="text"
size="small" {...buttonProps}
type="button" sx={(theme) => ({
variant="text" backgroundColor: "transparent",
{...buttonProps} border: "none",
sx={(theme) => ({ color: "inherit",
backgroundColor: "transparent", cursor: "copy",
border: "none", fontFamily: "inherit",
color: "inherit", fontSize: "inherit",
cursor: "copy", letterSpacing: "inherit",
fontFamily: "inherit", m: "-0.25rem",
fontSize: "inherit", minWidth: "unset",
letterSpacing: "inherit", py: 0,
m: "-0.25rem", textAlign: "inherit",
minWidth: "unset", textTransform: "inherit",
py: 0, ...parseFromValuesOrFunc(buttonProps?.sx, theme),
textAlign: "inherit", })}
textTransform: "inherit", title={undefined}
...parseFromValuesOrFunc(buttonProps?.sx, theme), />
})} </Tooltip>
title={undefined} );
/>
</Tooltip>
);
}; };
export default DataTable_CopyButton; export default DataTable_CopyButton;

View File

@@ -1,52 +1,46 @@
import { import { getCommonTooltipProps, parseFromValuesOrFunc } from "@/core/utils/utils";
getCommonTooltipProps,
parseFromValuesOrFunc,
} from "@/core/utils/utils";
import { IconButton, Tooltip } from "@mui/material"; import { IconButton, Tooltip } from "@mui/material";
const DataTable_GrabHandleButton = ({ location, table, ...rest }) => { const DataTable_GrabHandleButton = ({ location, table, ...rest }) => {
const { const {
options: { options: {
icons: { DragHandleIcon }, icons: { DragHandleIcon },
localization, localization,
}, },
} = table; } = table;
return ( return (
<Tooltip <Tooltip {...getCommonTooltipProps("top")} title={rest?.title ?? localization.move}>
{...getCommonTooltipProps("top")} <IconButton
title={rest?.title ?? localization.move} aria-label={rest.title ?? localization.move}
> disableRipple
<IconButton draggable="true"
aria-label={rest.title ?? localization.move} size="small"
disableRipple {...rest}
draggable="true" onClick={(e) => {
size="small" e.stopPropagation();
{...rest} rest?.onClick?.(e);
onClick={(e) => { }}
e.stopPropagation(); sx={(theme) => ({
rest?.onClick?.(e); "&:active": {
}} cursor: "grabbing",
sx={(theme) => ({ },
"&:active": { "&:hover": {
cursor: "grabbing", backgroundColor: "transparent",
}, opacity: 1,
"&:hover": { },
backgroundColor: "transparent", cursor: "grab",
opacity: 1, m: "0 -0.1rem",
}, opacity: location === "row" ? 1 : 0.5,
cursor: "grab", p: "2px",
m: "0 -0.1rem", transition: "all 150ms ease-in-out",
opacity: location === "row" ? 1 : 0.5, ...parseFromValuesOrFunc(rest?.sx, theme),
p: "2px", })}
transition: "all 150ms ease-in-out", title={undefined}
...parseFromValuesOrFunc(rest?.sx, theme), >
})} <DragHandleIcon />
title={undefined} </IconButton>
> </Tooltip>
<DragHandleIcon /> );
</IconButton>
</Tooltip>
);
}; };
export default DataTable_GrabHandleButton; export default DataTable_GrabHandleButton;

View File

@@ -1,4 +1,4 @@
const DataTable_ToggleFiltersButton = ({ table, ...rest }) => { const DataTable_ToggleFiltersButton = ({ table, ...rest }) => {
return <></>; return <></>;
}; };
export default DataTable_ToggleFiltersButton; export default DataTable_ToggleFiltersButton;

View File

@@ -12,122 +12,121 @@ import ScrollBox from "../../ScrollBox";
import FilterBodyController from "./FilterBodyController"; import FilterBodyController from "./FilterBodyController";
function FilterBody({ columns, drawerState, setDrawerState }) { function FilterBody({ columns, drawerState, setDrawerState }) {
const { filterData, setFilterData } = useDataTable(); const { filterData, setFilterData } = useDataTable();
const [validationSchema, setValidationSchema] = useState(Yup.object({})); const [validationSchema, setValidationSchema] = useState(Yup.object({}));
console.log(filterData); console.log(filterData);
useEffect(() => { useEffect(() => {
const schema = Yup.object( const schema = Yup.object(
Object.keys(filterData).reduce((acc, key) => { Object.keys(filterData).reduce((acc, key) => {
const initialValue = filterData[key]; const initialValue = filterData[key];
if (initialValue.filterMode === "between") { if (initialValue.filterMode === "between") {
acc[key] = Yup.object().shape({ acc[key] = Yup.object().shape({
value: Yup.array() value: Yup.array()
.of(Yup.string().nullable()) .of(Yup.string().nullable())
.test({ .test({
test(value, ctx) { test(value, ctx) {
const [start, end] = value || ["", ""]; const [start, end] = value || ["", ""];
if ( if (
initialValue.datatype === "numeric" && initialValue.datatype === "numeric" &&
parseInt(end, 10) <= parseInt(start, 10) parseInt(end, 10) <= parseInt(start, 10)
) { ) {
return ctx.createError({ return ctx.createError({
message: `مقدار وارده باید بیشتر از (${start}) باشد`, message: `مقدار وارده باید بیشتر از (${start}) باشد`,
});
} else if ((start && !end) || (!start && end)) {
return ctx.createError({
message: "این بخش را تکمیل نمایید",
});
}
return true;
},
}),
}); });
} else if ((start && !end) || (!start && end)) { }
return ctx.createError({ return acc;
message: "این بخش را تکمیل نمایید", }, {})
}); );
}
return true; setValidationSchema(schema);
}, }, [filterData]);
}),
}); const {
} handleSubmit,
return acc; control,
}, {}), reset,
formState: { errors, isDirty, isValid },
} = useForm({
defaultValues: filterData,
resolver: yupResolver(validationSchema),
mode: "all",
});
const onSubmit = (data) => {
setDrawerState(false);
setFilterData(data);
};
return (
<>
<FilterHeader setDrawerState={setDrawerState} />
{Object.keys(filterData).length > 0 && (
<ScrollBox>
<form onSubmit={handleSubmit(onSubmit)}>
<Box sx={{ px: 2, py: 3 }}>
{columns.map((column) =>
column.enableColumnFilter ? (
column.dependencyId ? (
<FilterBodyControllerWithDependency
key={column.id}
column={column}
control={control}
errors={errors}
reset={reset}
/>
) : (
<FilterBodyController
key={column.id}
column={column}
control={control}
errors={errors}
reset={reset}
/>
)
) : null
)}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
pb: 2,
}}
>
<Button
type="submit"
variant="contained"
size="large"
disabled={!isDirty || !isValid}
sx={{
px: 8,
boxShadow: "rgba(0, 0, 0, 0.23) 0px 6px 6px, rgba(0, 0, 0, 0.19) 10px 0px 20px",
backgroundColor: "primary2",
":hover": {
backgroundColor: "primary2",
},
}}
>
اعمال فیلتر
</Button>
</Box>
</form>
</ScrollBox>
)}
</>
); );
setValidationSchema(schema);
}, [filterData]);
const {
handleSubmit,
control,
reset,
formState: { errors, isDirty, isValid },
} = useForm({
defaultValues: filterData,
resolver: yupResolver(validationSchema),
mode: "all",
});
const onSubmit = (data) => {
setDrawerState(false);
setFilterData(data);
};
return (
<>
<FilterHeader setDrawerState={setDrawerState} />
{Object.keys(filterData).length > 0 && (
<ScrollBox>
<form onSubmit={handleSubmit(onSubmit)}>
<Box sx={{ px: 2, py: 3 }}>
{columns.map((column) =>
column.enableColumnFilter ? (
column.dependencyId ? (
<FilterBodyControllerWithDependency
key={column.id}
column={column}
control={control}
errors={errors}
reset={reset}
/>
) : (
<FilterBodyController
key={column.id}
column={column}
control={control}
errors={errors}
reset={reset}
/>
)
) : null,
)}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
pb: 2,
}}
>
<Button
type="submit"
variant="contained"
size="large"
disabled={!isDirty || !isValid}
sx={{
px: 8,
boxShadow:
"rgba(0, 0, 0, 0.23) 0px 6px 6px, rgba(0, 0, 0, 0.19) 10px 0px 20px",
backgroundColor: "primary2",
":hover": {
backgroundColor: "primary2",
},
}}
>
اعمال فیلتر
</Button>
</Box>
</form>
</ScrollBox>
)}
</>
);
} }
export default FilterBody; export default FilterBody;

View File

@@ -2,24 +2,24 @@ import { Controller, useWatch } from "react-hook-form";
import FilterBodyField from "./FilterBodyField"; import FilterBodyField from "./FilterBodyField";
const FilterBodyController = ({ column, control, reset, errors }) => { const FilterBodyController = ({ column, control, reset, errors }) => {
return ( return (
<Controller <Controller
name={`${column.id}`} name={`${column.id}`}
control={control} control={control}
render={({ field: { value, name, onBlur, onChange } }) => { render={({ field: { value, name, onBlur, onChange } }) => {
return ( return (
<FilterBodyField <FilterBodyField
column={column} column={column}
filterParameters={value} filterParameters={value}
handleChange={onChange} handleChange={onChange}
handleBlur={onBlur} handleBlur={onBlur}
dependencyFieldValue={null} dependencyFieldValue={null}
resetForm={() => reset()} resetForm={() => reset()}
errors={errors} errors={errors}
/> />
); );
}} }}
/> />
); );
}; };
export default FilterBodyController; export default FilterBodyController;

View File

@@ -1,32 +1,27 @@
import { Controller, useWatch } from "react-hook-form"; import { Controller, useWatch } from "react-hook-form";
import FilterBodyField from "./FilterBodyField"; import FilterBodyField from "./FilterBodyField";
const FilterBodyControllerWithDependency = ({ const FilterBodyControllerWithDependency = ({ column, control, reset, errors }) => {
column, const dependencyField = useWatch({ control, name: column.dependencyId });
control,
reset,
errors,
}) => {
const dependencyField = useWatch({ control, name: column.dependencyId });
return ( return (
<Controller <Controller
name={`${column.id}`} name={`${column.id}`}
control={control} control={control}
render={({ field: { value, name, onBlur, onChange } }) => { render={({ field: { value, name, onBlur, onChange } }) => {
return ( return (
<FilterBodyField <FilterBodyField
column={column} column={column}
filterParameters={value} filterParameters={value}
handleChange={onChange} handleChange={onChange}
handleBlur={onBlur} handleBlur={onBlur}
dependencyFieldValue={dependencyField} dependencyFieldValue={dependencyField}
resetForm={() => reset()} resetForm={() => reset()}
errors={errors} errors={errors}
/> />
); );
}} }}
/> />
); );
}; };
export default FilterBodyControllerWithDependency; export default FilterBodyControllerWithDependency;

View File

@@ -8,98 +8,97 @@ import CustomTextFieldRange from "@/core/components/DataTable/filter/fieldsType/
import { useState } from "react"; import { useState } from "react";
const columnFilterModeOptionFa = { const columnFilterModeOptionFa = {
equals: "برابر", equals: "برابر",
notEquals: "نابرابر", notEquals: "نابرابر",
contains: "شامل", contains: "شامل",
lessThan: "کوچکتر", lessThan: "کوچکتر",
greaterThan: "بزرگتر", greaterThan: "بزرگتر",
fuzzy: "فازی", fuzzy: "فازی",
between: "مابین", between: "مابین",
}; };
function FilterBodyField({ function FilterBodyField({
column, column,
filterParameters, filterParameters,
handleChange, handleChange,
handleBlur, handleBlur,
dependencyFieldValue, dependencyFieldValue,
setFieldValue, setFieldValue,
resetForm, resetForm,
errors, errors,
}) { }) {
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const defaultFilterTranslation = const defaultFilterTranslation =
columnFilterModeOptionFa[filterParameters.filterMode] || columnFilterModeOptionFa[filterParameters.filterMode] || filterParameters.filterMode;
filterParameters.filterMode;
const handleOpenFilterBox = (event) => { const handleOpenFilterBox = (event) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
};
const renderField = () => {
const commonProps = {
column,
filterParameters,
defaultFilterTranslation,
handleOpenFilterBox,
dependencyFieldValue,
setFieldValue,
handleChange,
handleBlur,
errors,
}; };
switch (filterParameters.datatype) { const renderField = () => {
case "numeric": const commonProps = {
if (filterParameters.filterMode === "between") { column,
return <CustomTextFieldRange {...commonProps} />; filterParameters,
} defaultFilterTranslation,
if (filterParameters.filterMode === "equals") { handleOpenFilterBox,
return column.ColumnSelectComponent ? ( dependencyFieldValue,
<column.ColumnSelectComponent {...commonProps} /> setFieldValue,
) : ( handleChange,
<CustomSelect {...commonProps} /> handleBlur,
); errors,
} };
break;
case "date":
if (filterParameters.filterMode === "equals") {
return <CustomDatePicker {...commonProps} />;
}
if (filterParameters.filterMode === "between") {
return <CustomDatePickerRange {...commonProps} />;
}
break;
case "array": switch (filterParameters.datatype) {
if (filterParameters.filterMode === "equals") { case "numeric":
return <CustomSelectMultiple {...commonProps} />; if (filterParameters.filterMode === "between") {
return <CustomTextFieldRange {...commonProps} />;
}
if (filterParameters.filterMode === "equals") {
return column.ColumnSelectComponent ? (
<column.ColumnSelectComponent {...commonProps} />
) : (
<CustomSelect {...commonProps} />
);
}
break;
case "date":
if (filterParameters.filterMode === "equals") {
return <CustomDatePicker {...commonProps} />;
}
if (filterParameters.filterMode === "between") {
return <CustomDatePickerRange {...commonProps} />;
}
break;
case "array":
if (filterParameters.filterMode === "equals") {
return <CustomSelectMultiple {...commonProps} />;
}
break;
default:
return <CustomTextField {...commonProps} />;
} }
break; };
default: return (
return <CustomTextField {...commonProps} />; <>
} {renderField()}
}; {Array.isArray(column.columnFilterModeOptions) && (
<FilterOptionList
return ( column={column}
<> anchorEl={anchorEl}
{renderField()} setAnchorEl={setAnchorEl}
{Array.isArray(column.columnFilterModeOptions) && ( filterParameters={filterParameters}
<FilterOptionList filterType={filterParameters.filterMode}
column={column} handleChange={handleChange}
anchorEl={anchorEl} resetForm={resetForm}
setAnchorEl={setAnchorEl} filterOption={column.columnFilterModeOptions}
filterParameters={filterParameters} columnFilterModeOptionFa={columnFilterModeOptionFa}
filterType={filterParameters.filterMode} />
handleChange={handleChange} )}
resetForm={resetForm} </>
filterOption={column.columnFilterModeOptions} );
columnFilterModeOptionFa={columnFilterModeOptionFa}
/>
)}
</>
);
} }
export default FilterBodyField; export default FilterBodyField;

View File

@@ -4,36 +4,34 @@ import { IconButton, Tooltip, Badge } from "@mui/material";
import useDataTable from "@/lib/hooks/useDataTable"; import useDataTable from "@/lib/hooks/useDataTable";
function FilterButton({ drawerState, setDrawerState }) { function FilterButton({ drawerState, setDrawerState }) {
const { filterData } = useDataTable(); const { filterData } = useDataTable();
const isValueEmpty = (value) => { const isValueEmpty = (value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.length === 0 || value.every((v) => v === ""); return value.length === 0 || value.every((v) => v === "");
} }
return value === "" || value === null || value === undefined; return value === "" || value === null || value === undefined;
}; };
const filteredFilterData = Object.values(filterData).filter( const filteredFilterData = Object.values(filterData).filter((filter) => !isValueEmpty(filter.value));
(filter) => !isValueEmpty(filter.value), return (
); <Tooltip title="فیلتر">
return ( <IconButton
<Tooltip title="فیلتر"> onClick={() => {
<IconButton setDrawerState(!drawerState);
onClick={() => { }}
setDrawerState(!drawerState); aria-label="filter table"
}} >
aria-label="filter table" <Badge
> badgeContent={filteredFilterData.length}
<Badge color="primary"
badgeContent={filteredFilterData.length} anchorOrigin={{
color="primary" vertical: "top",
anchorOrigin={{ horizontal: "left",
vertical: "top", }}
horizontal: "left", >
}} <FilterAltIcon />
> </Badge>
<FilterAltIcon /> </IconButton>
</Badge> </Tooltip>
</IconButton> );
</Tooltip>
);
} }
export default FilterButton; export default FilterButton;

View File

@@ -4,35 +4,35 @@ import { Box, Drawer } from "@mui/material";
import FilterDrawer from "../../FilterDrawer"; import FilterDrawer from "../../FilterDrawer";
const FilterCustom = ({ filterData, setFilterData }) => { const FilterCustom = ({ filterData, setFilterData }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const closeDrawer = useCallback(() => setOpen(false), []); const closeDrawer = useCallback(() => setOpen(false), []);
return ( return (
<> <>
<FilterButton drawerState={open} setDrawerState={setOpen} /> <FilterButton drawerState={open} setDrawerState={setOpen} />
<Drawer <Drawer
open={open} open={open}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
sx={{ sx={{
overflowY: "hidden", overflowY: "hidden",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
height: "100%", height: "100%",
zIndex: "1300", zIndex: "1300",
}} }}
> >
<Box sx={{ width: { xs: 300, sm: 450 } }}> <Box sx={{ width: { xs: 300, sm: 450 } }}>
{open && ( {open && (
<FilterDrawer <FilterDrawer
defaultValues={filterData} defaultValues={filterData}
setFilterData={setFilterData} setFilterData={setFilterData}
closeDrawer={closeDrawer} closeDrawer={closeDrawer}
/> />
)} )}
</Box> </Box>
</Drawer> </Drawer>
</> </>
); );
}; };
export default FilterCustom; export default FilterCustom;

View File

@@ -5,31 +5,30 @@ import FilterAltIcon from "@mui/icons-material/FilterAlt";
import { Box, IconButton, Typography } from "@mui/material"; import { Box, IconButton, Typography } from "@mui/material";
function FilterHeader({ setDrawerState }) { function FilterHeader({ setDrawerState }) {
return ( return (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
px: 2, px: 2,
py: 1, py: 1,
backgroundColor: (theme) => theme.palette.primary.main, backgroundColor: (theme) => theme.palette.primary.main,
boxShadow: boxShadow: "rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px",
"rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px", maxWidth: "450px",
maxWidth: "450px", }}
}} >
> <Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ display: "flex", alignItems: "center" }}> <FilterAltIcon sx={{ color: "#fff", mr: 1 }} />
<FilterAltIcon sx={{ color: "#fff", mr: 1 }} /> <Typography variant="h6" sx={{ color: "#fff" }}>
<Typography variant="h6" sx={{ color: "#fff" }}> فیلتر
فیلتر </Typography>
</Typography> </Box>
</Box> <IconButton sx={{ color: "#fff" }} onClick={() => setDrawerState(false)}>
<IconButton sx={{ color: "#fff" }} onClick={() => setDrawerState(false)}> <CancelIcon sx={{ fontSize: "1em" }} />
<CancelIcon sx={{ fontSize: "1em" }} /> </IconButton>
</IconButton> </Box>
</Box> );
);
} }
export default FilterHeader; export default FilterHeader;

View File

@@ -4,55 +4,49 @@ import { ListItem, Menu } from "@mui/material";
import { useState } from "react"; import { useState } from "react";
function FilterOptionList({ function FilterOptionList({
filterType, filterType,
filterOption, filterOption,
filterParameters, filterParameters,
anchorEl, anchorEl,
columnFilterModeOptionFa, columnFilterModeOptionFa,
setAnchorEl, setAnchorEl,
handleChange, handleChange,
}) { }) {
const [selectedFilter, setSelectedFilter] = useState(filterType); const [selectedFilter, setSelectedFilter] = useState(filterType);
const handleChangeItem = (event, index) => { const handleChangeItem = (event, index) => {
handleChange({ handleChange({
...filterParameters, ...filterParameters,
value: filterOption[index] === "between" ? ["", ""] : "", value: filterOption[index] === "between" ? ["", ""] : "",
filterMode: filterOption[index], filterMode: filterOption[index],
}); });
setSelectedFilter(filterOption[index]); setSelectedFilter(filterOption[index]);
setAnchorEl(null); setAnchorEl(null);
}; };
const handleCloseFilterBox = () => { const handleCloseFilterBox = () => {
setAnchorEl(null); setAnchorEl(null);
}; };
return ( return (
<Menu <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleCloseFilterBox}>
id="simple-menu" {filterOption.map((option, index) => (
anchorEl={anchorEl} <ListItem
keepMounted key={index}
open={Boolean(anchorEl)} onClick={(event) => handleChangeItem(event, index)}
onClose={handleCloseFilterBox} selected={option === selectedFilter}
> sx={{
{filterOption.map((option, index) => ( cursor: "pointer",
<ListItem width: "100px",
key={index} display: "flex",
onClick={(event) => handleChangeItem(event, index)} justifyContent: "center",
selected={option === selectedFilter} }}
sx={{ >
cursor: "pointer", {columnFilterModeOptionFa[option]}
width: "100px", </ListItem>
display: "flex", ))}
justifyContent: "center", </Menu>
}} );
>
{columnFilterModeOptionFa[option]}
</ListItem>
))}
</Menu>
);
} }
export default FilterOptionList; export default FilterOptionList;

View File

@@ -2,30 +2,22 @@ import React from "react";
import MuiDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker"; import MuiDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
function CustomDatePicker({ function CustomDatePicker({ column, filterParameters, defaultFilterTranslation, handleChange }) {
column, return (
filterParameters, <MuiDatePicker
defaultFilterTranslation, name={`${column.id}.value`}
handleChange, value={filterParameters.value}
}) { setFieldValue={(name, formattedDate) => {
return ( handleChange({ ...filterParameters, value: formattedDate });
<MuiDatePicker }}
name={`${column.id}.value`} placeholder={column.header}
value={filterParameters.value} helperText={
setFieldValue={(name, formattedDate) => { <Typography variant="button" sx={{ color: (theme) => theme.palette.primary.main }}>
handleChange({ ...filterParameters, value: formattedDate }); نوع فیلتر: {defaultFilterTranslation} (تاریخ)
}} </Typography>
placeholder={column.header} }
helperText={ />
<Typography );
variant="button"
sx={{ color: (theme) => theme.palette.primary.main }}
>
نوع فیلتر: {defaultFilterTranslation} (تاریخ)
</Typography>
}
/>
);
} }
export default CustomDatePicker; export default CustomDatePicker;

View File

@@ -4,57 +4,44 @@ import React from "react";
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import MuiDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker"; import MuiDatePicker from "@/core/components/DataTable/filter/fieldsType/CustomDate/MuiDatePicker";
function CustomDatePickerRange({ function CustomDatePickerRange({ column, filterParameters, defaultFilterTranslation, handleChange, errors }) {
column, return (
filterParameters, <Box sx={{ display: "flex", gap: 1 }}>
defaultFilterTranslation, <MuiDatePicker
handleChange, label={column.header}
errors, name={`${column.id}.value[0]`}
}) { value={filterParameters.value[0]}
return ( setFieldValue={(name, formattedDate) => {
<Box sx={{ display: "flex", gap: 1 }}> handleChange({
<MuiDatePicker ...filterParameters,
label={column.header} value: [formattedDate, filterParameters.value[1]],
name={`${column.id}.value[0]`} });
value={filterParameters.value[0]} }}
setFieldValue={(name, formattedDate) => { maxDate={filterParameters.value[1]}
handleChange({ placeholder={`از تاریخ`}
...filterParameters, helperText={
value: [formattedDate, filterParameters.value[1]], <Typography variant="button" sx={{ color: (theme) => theme.palette.primary.main }}>
}); نوع فیلتر: {defaultFilterTranslation} (تاریخ)
}} </Typography>
maxDate={filterParameters.value[1]} }
placeholder={`از تاریخ`} />
helperText={ <MuiDatePicker
<Typography label={column.header}
variant="button" name={`${column.id}.value[1]`}
sx={{ color: (theme) => theme.palette.primary.main }} value={filterParameters.value[1]}
> setFieldValue={(name, formattedDate) => {
نوع فیلتر: {defaultFilterTranslation} (تاریخ) handleChange({
</Typography> ...filterParameters,
} value: [filterParameters.value[0], formattedDate],
/> });
<MuiDatePicker }}
label={column.header} minDate={filterParameters.value[0]}
name={`${column.id}.value[1]`} placeholder={`تا تاریخ`}
value={filterParameters.value[1]} helperText={errors?.[`${column.id}`]?.value ? errors?.[`${column.id}`]?.value.message : null}
setFieldValue={(name, formattedDate) => { error={Boolean(errors?.[`${column.id}`]?.value)}
handleChange({ />
...filterParameters, </Box>
value: [filterParameters.value[0], formattedDate], );
});
}}
minDate={filterParameters.value[0]}
placeholder={`تا تاریخ`}
helperText={
errors?.[`${column.id}`]?.value
? errors?.[`${column.id}`]?.value.message
: null
}
error={Boolean(errors?.[`${column.id}`]?.value)}
/>
</Box>
);
} }
export default CustomDatePickerRange; export default CustomDatePickerRange;

View File

@@ -6,81 +6,67 @@ import { Box, FormHelperText, IconButton, InputAdornment } from "@mui/material";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import moment from "jalali-moment"; import moment from "jalali-moment";
function MuiDatePicker({ function MuiDatePicker({ label, value, setFieldValue, name, minDate, maxDate, helperText, placeholder, error }) {
label, return (
value, <Box sx={{ display: "flex", flexDirection: "column", my: 1 }}>
setFieldValue, <LocalizationProvider
name, dateAdapter={AdapterDateFnsJalali}
minDate, localeText={faIR.components.MuiLocalizationProvider.defaultProps.localeText}
maxDate, >
helperText, <MobileDatePicker
placeholder, value={value ? new Date(value) : null}
error, sx={{ width: "100%" }}
}) { id={name}
return ( label={label}
<Box sx={{ display: "flex", flexDirection: "column", my: 1 }}> name={name}
<LocalizationProvider closeOnSelect={true}
dateAdapter={AdapterDateFnsJalali} aria-describedby="component-helper-text"
localeText={ onChange={(value) => {
faIR.components.MuiLocalizationProvider.defaultProps.localeText const date = new Date(value);
} const formattedDate = moment(date).locale("en").format("YYYY-MM-DD");
> setFieldValue(name, formattedDate);
<MobileDatePicker }}
value={value ? new Date(value) : null} minDate={minDate ? new Date(minDate) : null}
sx={{ width: "100%" }} maxDate={maxDate ? new Date(maxDate) : null}
id={name} slotProps={{
label={label} textField: {
name={name} size: "small",
closeOnSelect={true} error: error,
aria-describedby="component-helper-text" placeholder: placeholder,
onChange={(value) => { InputProps: {
const date = new Date(value); endAdornment: (
const formattedDate = moment(date) <InputAdornment position="end">
.locale("en") <IconButton
.format("YYYY-MM-DD"); size="small"
setFieldValue(name, formattedDate); onClick={(event) => {
}} event.stopPropagation();
minDate={minDate ? new Date(minDate) : null} setFieldValue(name, "");
maxDate={maxDate ? new Date(maxDate) : null} }}
slotProps={{ sx={{
textField: { color: "#bfbfbf",
size: "small", "&:hover": {
error: error, backgroundColor: "rgba(189, 189, 189, 0.1)",
placeholder: placeholder, color: "#363434",
InputProps: { },
endAdornment: ( }}
<InputAdornment position="end"> >
<IconButton <ClearIcon />
size="small" </IconButton>
onClick={(event) => { </InputAdornment>
event.stopPropagation(); ),
setFieldValue(name, ""); },
}} InputLabelProps: {
sx={{ shrink: true,
color: "#bfbfbf", },
"&:hover": {
backgroundColor: "rgba(189, 189, 189, 0.1)",
color: "#363434",
}, },
}} }}
> />
<ClearIcon /> {/* <FormHelperText id="component-helper-text" variant="caption" sx={{ ml: 2, color: error ? "#d32f2f" : "unset", fontWeight: 'bold' }}>
</IconButton>
</InputAdornment>
),
},
InputLabelProps: {
shrink: true,
},
},
}}
/>
{/* <FormHelperText id="component-helper-text" variant="caption" sx={{ ml: 2, color: error ? "#d32f2f" : "unset", fontWeight: 'bold' }}>
{helperText ? helperText : ""} {helperText ? helperText : ""}
</FormHelperText> */} </FormHelperText> */}
</LocalizationProvider> </LocalizationProvider>
</Box> </Box>
); );
} }
export default MuiDatePicker; export default MuiDatePicker;

View File

@@ -1,38 +1,30 @@
"use client"; "use client";
import { import { FormControl, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material";
FormControl,
InputLabel,
MenuItem,
OutlinedInput,
Select,
} from "@mui/material";
function CustomSelect({ column, filterParameters, handleChange }) { function CustomSelect({ column, filterParameters, handleChange }) {
return ( return (
<FormControl fullWidth sx={{ marginY: 1 }} size="small"> <FormControl fullWidth sx={{ marginY: 1 }} size="small">
<InputLabel id={`label${column.id}`} shrink> <InputLabel id={`label${column.id}`} shrink>
{column.header} {column.header}
</InputLabel> </InputLabel>
<Select <Select
labelId={`label${column.id}`} labelId={`label${column.id}`}
id={column.id} id={column.id}
name={`${column.id}.value`} name={`${column.id}.value`}
value={filterParameters.value} value={filterParameters.value}
input={<OutlinedInput label={column.header} />} input={<OutlinedInput label={column.header} />}
size="small" size="small"
onChange={(e) => onChange={(e) => handleChange({ ...filterParameters, value: e.target.value })}
handleChange({ ...filterParameters, value: e.target.value }) displayEmpty
} >
displayEmpty {column.columnSelectOption().map((option) => (
> <MenuItem key={option.value} value={option.value}>
{column.columnSelectOption().map((option) => ( {option.label}
<MenuItem key={option.value} value={option.value}> </MenuItem>
{option.label} ))}
</MenuItem> </Select>
))} </FormControl>
</Select> );
</FormControl>
);
} }
export default CustomSelect; export default CustomSelect;

View File

@@ -1,49 +1,39 @@
"use client"; "use client";
import { import { FormControl, FormHelperText, InputLabel, MenuItem, OutlinedInput, Select, Typography } from "@mui/material";
FormControl,
FormHelperText,
InputLabel,
MenuItem,
OutlinedInput,
Select,
Typography,
} from "@mui/material";
import { useCallback, useEffect } from "react"; import { useCallback, useEffect } from "react";
function CustomSelectByDependency({ function CustomSelectByDependency({
column, column,
filterParameters, filterParameters,
value, value,
defaultFilterTranslation, defaultFilterTranslation,
handleChange, handleChange,
columnSelectOption, columnSelectOption,
}) { }) {
return ( return (
<FormControl fullWidth sx={{ my: 1 }} size="small"> <FormControl fullWidth sx={{ my: 1 }} size="small">
<InputLabel id={`label${column.id}`} shrink> <InputLabel id={`label${column.id}`} shrink>
{column.header} {column.header}
</InputLabel> </InputLabel>
<Select <Select
labelId={`label${column.id}`} labelId={`label${column.id}`}
id={column.id} id={column.id}
name={`${column.id}.value`} name={`${column.id}.value`}
value={value} value={value}
size="small" size="small"
input={<OutlinedInput label={column.header} />} input={<OutlinedInput label={column.header} />}
onChange={(e) => onChange={(e) => handleChange({ ...filterParameters, value: e.target.value })}
handleChange({ ...filterParameters, value: e.target.value }) displayEmpty
} >
displayEmpty {columnSelectOption.map((option) => (
> <MenuItem key={option.value} value={option.value}>
{columnSelectOption.map((option) => ( {option.label}
<MenuItem key={option.value} value={option.value}> </MenuItem>
{option.label} ))}
</MenuItem> </Select>
))} </FormControl>
</Select> );
</FormControl>
);
} }
export default CustomSelectByDependency; export default CustomSelectByDependency;

View File

@@ -1,62 +1,55 @@
"use client"; "use client";
import { import {
Box, Box,
Chip, Chip,
FormControl, FormControl,
FormHelperText, FormHelperText,
InputLabel, InputLabel,
MenuItem, MenuItem,
OutlinedInput, OutlinedInput,
Select, Select,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
function CustomSelectMultiple({ function CustomSelectMultiple({ column, filterParameters, defaultFilterTranslation, handleChange }) {
column, const columnSelectOption = column.columnSelectOption;
filterParameters,
defaultFilterTranslation,
handleChange,
}) {
const columnSelectOption = column.columnSelectOption;
const getLabelForValue = (value) => { const getLabelForValue = (value) => {
const option = columnSelectOption.find((opt) => opt.value === value); const option = columnSelectOption.find((opt) => opt.value === value);
return option ? option.label : value; return option ? option.label : value;
}; };
return ( return (
<FormControl fullWidth sx={{ marginY: 1 }} size="small"> <FormControl fullWidth sx={{ marginY: 1 }} size="small">
<InputLabel htmlFor="uncontrolled-native" id={`label${column.id}`}> <InputLabel htmlFor="uncontrolled-native" id={`label${column.id}`}>
{column.header} {column.header}
</InputLabel> </InputLabel>
<Select <Select
labelId={`label${column.id}`} labelId={`label${column.id}`}
id={column.id} id={column.id}
value={filterParameters.value} value={filterParameters.value}
multiple multiple
size="small" size="small"
onChange={(e) => onChange={(e) => handleChange({ ...filterParameters, value: e.target.value })}
handleChange({ ...filterParameters, value: e.target.value }) renderValue={(selected) => (
} <Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
renderValue={(selected) => ( {selected.map((value) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}> <Chip key={value} label={getLabelForValue(value)} />
{selected.map((value) => ( ))}
<Chip key={value} label={getLabelForValue(value)} /> </Box>
))} )}
</Box> input={<OutlinedInput label={column.header} />}
)} InputLabelProps={{ shrink: true }}
input={<OutlinedInput label={column.header} />} >
InputLabelProps={{ shrink: true }} {columnSelectOption.map((option) => (
> <MenuItem key={option.value} value={option.value}>
{columnSelectOption.map((option) => ( {option.label}
<MenuItem key={option.value} value={option.value}> </MenuItem>
{option.label} ))}
</MenuItem> </Select>
))} </FormControl>
</Select> );
</FormControl>
);
} }
export default CustomSelectMultiple; export default CustomSelectMultiple;

View File

@@ -2,41 +2,35 @@ import { InputAdornment, TextField, Typography } from "@mui/material";
import FilterListIcon from "@mui/icons-material/FilterList"; import FilterListIcon from "@mui/icons-material/FilterList";
function CustomTextField({ function CustomTextField({
column, column,
defaultFilterTranslation, defaultFilterTranslation,
filterParameters, filterParameters,
handleOpenFilterBox, handleOpenFilterBox,
handleBlur, handleBlur,
handleChange, handleChange,
}) { }) {
return ( return (
<TextField <TextField
id={column.id} id={column.id}
name={column.id} name={column.id}
label={column.header} label={column.header}
value={filterParameters.value} value={filterParameters.value}
onChange={(e) => onChange={(e) => handleChange({ ...filterParameters, value: e.target.value })}
handleChange({ ...filterParameters, value: e.target.value }) onBlur={handleBlur}
} fullWidth
onBlur={handleBlur} variant="outlined"
fullWidth size="small"
variant="outlined" sx={{ my: 1 }}
size="small" InputProps={{
sx={{ my: 1 }} endAdornment: (
InputProps={{ <InputAdornment position="end" sx={{ cursor: "pointer" }} onClick={handleOpenFilterBox}>
endAdornment: ( <FilterListIcon />
<InputAdornment </InputAdornment>
position="end" ),
sx={{ cursor: "pointer" }} }}
onClick={handleOpenFilterBox} InputLabelProps={{ shrink: true }}
> />
<FilterListIcon /> );
</InputAdornment>
),
}}
InputLabelProps={{ shrink: true }}
/>
);
} }
export default CustomTextField; export default CustomTextField;

View File

@@ -2,84 +2,73 @@ import { Box, InputAdornment, TextField, Typography } from "@mui/material";
import FilterListIcon from "@mui/icons-material/FilterList"; import FilterListIcon from "@mui/icons-material/FilterList";
function CustomTextFieldRange({ function CustomTextFieldRange({
column, column,
defaultFilterTranslation, defaultFilterTranslation,
handleOpenFilterBox, handleOpenFilterBox,
handleChange, handleChange,
filterParameters, filterParameters,
handleBlur, handleBlur,
errors, errors,
}) { }) {
return ( return (
<Box sx={{ display: "flex" }}> <Box sx={{ display: "flex" }}>
<TextField <TextField
id={`${column.id}.value[0]`} id={`${column.id}.value[0]`}
name={`${column.id}.value[0]`} name={`${column.id}.value[0]`}
onChange={(e) => onChange={(e) =>
handleChange({ handleChange({
...filterParameters, ...filterParameters,
value: [e.target.value, filterParameters.value[1]], value: [e.target.value, filterParameters.value[1]],
}) })
} }
onBlur={handleBlur} onBlur={handleBlur}
label={<Typography>از {column.header}</Typography>} label={<Typography>از {column.header}</Typography>}
value={filterParameters.value[0]} value={filterParameters.value[0]}
fullWidth fullWidth
error={ error={touched?.[`${column.id}`]?.value && Boolean(errors?.[`${column.id}`]?.value)}
touched?.[`${column.id}`]?.value && helperText={
Boolean(errors?.[`${column.id}`]?.value) <Typography
} variant="caption"
helperText={ sx={{
<Typography color: (theme) => theme.palette.primary.main,
variant="caption" fontWeight: "bold",
sx={{ }}
color: (theme) => theme.palette.primary.main, >
fontWeight: "bold", نوع فیلتر: {defaultFilterTranslation}
}} </Typography>
> }
نوع فیلتر: {defaultFilterTranslation} variant="outlined"
</Typography> size="small"
} sx={{ my: 1, marginRight: 1 }}
variant="outlined" />
size="small" <TextField
sx={{ my: 1, marginRight: 1 }} id={`${column.id}.value[1]`}
/> name={`${column.id}.value[1]`}
<TextField onChange={(e) =>
id={`${column.id}.value[1]`} handleChange({
name={`${column.id}.value[1]`} ...filterParameters,
onChange={(e) => value: [filterParameters.value[0], e.target.value],
handleChange({ })
...filterParameters, }
value: [filterParameters.value[0], e.target.value], onBlur={handleBlur}
}) error={Boolean(errors?.[`${column.id}`]?.value)}
} helperText={errors?.[`${column.id}`]?.value ? errors?.[`${column.id}`]?.value.message : null}
onBlur={handleBlur} label={<Typography>تا {column.header}</Typography>}
error={Boolean(errors?.[`${column.id}`]?.value)} value={filterParameters.value[1]}
helperText={ fullWidth
errors?.[`${column.id}`]?.value variant="outlined"
? errors?.[`${column.id}`]?.value.message size="small"
: null sx={{ my: 1 }}
} InputProps={{
label={<Typography>تا {column.header}</Typography>} endAdornment: (
value={filterParameters.value[1]} <InputAdornment position="end" sx={{ cursor: "pointer" }} onClick={handleOpenFilterBox}>
fullWidth <FilterListIcon />
variant="outlined" </InputAdornment>
size="small" ),
sx={{ my: 1 }} }}
InputProps={{ />
endAdornment: ( </Box>
<InputAdornment );
position="end"
sx={{ cursor: "pointer" }}
onClick={handleOpenFilterBox}
>
<FilterListIcon />
</InputAdornment>
),
}}
/>
</Box>
);
} }
export default CustomTextFieldRange; export default CustomTextFieldRange;

View File

@@ -5,37 +5,37 @@ import FilterBody from "@/core/components/DataTable/filter/FilterBody";
import { Box, Drawer } from "@mui/material"; import { Box, Drawer } from "@mui/material";
function FilterColumn({ columns, user_id, page_name, table_name }) { function FilterColumn({ columns, user_id, page_name, table_name }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<> <>
<FilterButton drawerState={open} setDrawerState={setOpen} /> <FilterButton drawerState={open} setDrawerState={setOpen} />
<Drawer <Drawer
open={open} open={open}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
sx={{ sx={{
overflowY: "hidden", overflowY: "hidden",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
height: "100%", height: "100%",
zIndex: "1300", zIndex: "1300",
}} }}
> >
<Box sx={{ width: { xs: 300, sm: 450 } }}> <Box sx={{ width: { xs: 300, sm: 450 } }}>
{open && ( {open && (
<FilterBody <FilterBody
columns={columns} columns={columns}
drawerState={open} drawerState={open}
setDrawerState={setOpen} setDrawerState={setOpen}
user_id={user_id} user_id={user_id}
page_name={page_name} page_name={page_name}
table_name={table_name} table_name={table_name}
/> />
)} )}
</Box> </Box>
</Drawer> </Drawer>
</> </>
); );
} }
export default FilterColumn; export default FilterColumn;

View File

@@ -3,54 +3,49 @@ import { TableHead } from "@mui/material";
import DataTable_TableHeadRow from "./TableHeadRow"; import DataTable_TableHeadRow from "./TableHeadRow";
const DataTable_TableHead = ({ columnVirtualizer, table, ...rest }) => { const DataTable_TableHead = ({ columnVirtualizer, table, ...rest }) => {
const { const {
getState, getState,
options: { options: { enableStickyHeader, layoutMode, muiTableHeadProps, positionToolbarAlertBanner },
enableStickyHeader, refs: { tableHeadRef },
layoutMode, } = table;
muiTableHeadProps, const { isFullScreen, showAlertBanner } = getState();
positionToolbarAlertBanner,
},
refs: { tableHeadRef },
} = table;
const { isFullScreen, showAlertBanner } = getState();
const tableHeadProps = { const tableHeadProps = {
...parseFromValuesOrFunc(muiTableHeadProps, { table }), ...parseFromValuesOrFunc(muiTableHeadProps, { table }),
...rest, ...rest,
}; };
const stickyHeader = enableStickyHeader || isFullScreen; const stickyHeader = enableStickyHeader || isFullScreen;
return ( return (
<TableHead <TableHead
{...tableHeadProps} {...tableHeadProps}
ref={(ref) => { ref={(ref) => {
tableHeadRef.current = ref; tableHeadRef.current = ref;
if (tableHeadProps?.ref) { if (tableHeadProps?.ref) {
// @ts-ignore // @ts-ignore
tableHeadProps.ref.current = ref; tableHeadProps.ref.current = ref;
} }
}} }}
sx={(theme) => ({ sx={(theme) => ({
display: layoutMode?.startsWith("grid") ? "grid" : undefined, display: layoutMode?.startsWith("grid") ? "grid" : undefined,
opacity: 0.97, opacity: 0.97,
position: stickyHeader ? "sticky" : "relative", position: stickyHeader ? "sticky" : "relative",
top: stickyHeader && layoutMode?.startsWith("grid") ? 0 : undefined, top: stickyHeader && layoutMode?.startsWith("grid") ? 0 : undefined,
zIndex: stickyHeader ? 2 : undefined, zIndex: stickyHeader ? 2 : undefined,
...parseFromValuesOrFunc(tableHeadProps?.sx, theme), ...parseFromValuesOrFunc(tableHeadProps?.sx, theme),
})} })}
> >
{table.getHeaderGroups().map((headerGroup, index) => ( {table.getHeaderGroups().map((headerGroup, index) => (
<DataTable_TableHeadRow <DataTable_TableHeadRow
columnVirtualizer={columnVirtualizer} columnVirtualizer={columnVirtualizer}
headerGroup={headerGroup} headerGroup={headerGroup}
key={headerGroup.id} key={headerGroup.id}
table={table} table={table}
index={index} index={index}
/> />
))} ))}
</TableHead> </TableHead>
); );
}; };
export default DataTable_TableHead; export default DataTable_TableHead;

View File

@@ -1,290 +1,244 @@
import { import { getCommonMRTCellStyles, parseFromValuesOrFunc } from "@/core/utils/utils";
getCommonMRTCellStyles,
parseFromValuesOrFunc,
} from "@/core/utils/utils";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import { Box, TableCell } from "@mui/material"; import { Box, TableCell } from "@mui/material";
import { MRT_TableHeadCellSortLabel } from "material-react-table"; import { MRT_TableHeadCellSortLabel } from "material-react-table";
import { useMemo } from "react"; import { useMemo } from "react";
const DataTable_TableHeadCell = ({ const DataTable_TableHeadCell = ({ columnVirtualizer, header, staticColumnIndex, backgroundColor, table, ...rest }) => {
columnVirtualizer, const theme = useTheme();
header, const {
staticColumnIndex, getState,
backgroundColor, options: {
table, columnResizeDirection,
...rest columnResizeMode,
}) => { enableColumnActions,
const theme = useTheme(); enableColumnDragging,
const { enableColumnOrdering,
getState, enableColumnPinning,
options: { enableGrouping,
columnResizeDirection, enableMultiSort,
columnResizeMode, layoutMode,
enableColumnActions, mrtTheme: { draggingBorderColor },
enableColumnDragging, muiTableHeadCellProps,
enableColumnOrdering,
enableColumnPinning,
enableGrouping,
enableMultiSort,
layoutMode,
mrtTheme: { draggingBorderColor },
muiTableHeadCellProps,
},
refs: { tableHeadCellRefs },
setHoveredColumn,
} = table;
const {
columnSizingInfo,
density,
draggingColumn,
grouping,
hoveredColumn,
showColumnFilters,
} = getState();
const { column } = header;
const { columnDef } = column;
const { columnDefType } = columnDef;
const tableCellProps = {
...parseFromValuesOrFunc(muiTableHeadCellProps, { column, table }),
...parseFromValuesOrFunc(columnDef.muiTableHeadCellProps, {
column,
table,
}),
...rest,
};
const isColumnPinned =
enableColumnPinning &&
columnDef.columnDefType !== "group" &&
column.getIsPinned();
const showColumnActions =
(enableColumnActions || columnDef.enableColumnActions) &&
columnDef.enableColumnActions !== false;
const showDragHandle =
enableColumnDragging !== false &&
columnDef.enableColumnDragging !== false &&
(enableColumnDragging ||
(enableColumnOrdering && columnDef.enableColumnOrdering !== false) ||
(enableGrouping &&
columnDef.enableGrouping !== false &&
!grouping.includes(column.id)));
const headerPL = useMemo(() => {
let pl = 0;
if (column.getCanSort()) pl += 1;
if (showColumnActions) pl += 1.75;
if (showDragHandle) pl += 1.5;
return pl;
}, [showColumnActions, showDragHandle]);
const draggingBorders = useMemo(() => {
const showResizeBorder =
columnSizingInfo.isResizingColumn === column.id &&
columnResizeMode === "onChange" &&
!header.subHeaders.length;
const borderStyle = showResizeBorder
? `2px solid ${draggingBorderColor} !important`
: draggingColumn?.id === column.id
? `1px dashed ${theme.palette.grey[500]}`
: hoveredColumn?.id === column.id
? `2px dashed ${draggingBorderColor}`
: undefined;
if (showResizeBorder) {
return columnResizeDirection === "ltr"
? { borderRight: borderStyle }
: { borderLeft: borderStyle };
}
const draggingBorders = borderStyle
? {
borderLeft: borderStyle,
borderRight: borderStyle,
borderTop: borderStyle,
}
: undefined;
return draggingBorders;
}, [draggingColumn, hoveredColumn, columnSizingInfo.isResizingColumn]);
const handleDragEnter = (_e) => {
if (enableGrouping && hoveredColumn?.id === "drop-zone") {
setHoveredColumn(null);
}
if (enableColumnOrdering && draggingColumn && columnDefType !== "group") {
setHoveredColumn(
columnDef.enableColumnOrdering !== false ? column : null,
);
}
};
const handleDragOver = (e) => {
if (columnDef.enableColumnOrdering !== false) {
e.preventDefault();
}
};
const HeaderElement =
parseFromValuesOrFunc(columnDef.Header, {
column,
header,
table,
}) ?? columnDef.header;
const columnRelativeDepth = header.depth - column.depth;
if (columnRelativeDepth > 1) {
return null;
}
let rowSpan = 1;
if (header.isPlaceholder) {
const leafs = header.getLeafHeaders();
rowSpan = leafs[leafs.length - 1].depth - header.depth;
}
return (
<TableCell
align={
columnDefType === "group"
? "center"
: theme.direction === "rtl"
? "right"
: "left"
}
colSpan={header.colSpan}
rowSpan={rowSpan}
data-index={staticColumnIndex}
data-pinned={!!isColumnPinned || undefined}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
ref={(node) => {
if (node) {
tableHeadCellRefs.current[column.id] = node;
if (columnDefType !== "group") {
columnVirtualizer?.measureElement?.(node);
}
}
}}
{...tableCellProps}
sx={(theme) => ({
"& :hover": {
".MuiButtonBase-root": {
opacity: 1,
},
}, },
flexDirection: layoutMode?.startsWith("grid") ? "column" : undefined, refs: { tableHeadCellRefs },
fontWeight: "bold", setHoveredColumn,
overflow: "visible", } = table;
p: const { columnSizingInfo, density, draggingColumn, grouping, hoveredColumn, showColumnFilters } = getState();
density === "compact" const { column } = header;
? "0.5rem" const { columnDef } = column;
: density === "comfortable" const { columnDefType } = columnDef;
? columnDefType === "display"
? "0.75rem" const tableCellProps = {
: "1rem" ...parseFromValuesOrFunc(muiTableHeadCellProps, { column, table }),
: columnDefType === "display" ...parseFromValuesOrFunc(columnDef.muiTableHeadCellProps, {
? "1rem 1.25rem" column,
: "1.5rem", table,
pb:
columnDefType === "display"
? 0
: showColumnFilters || density === "compact"
? "0.4rem"
: "0.6rem",
pt:
columnDefType === "group" || density === "compact"
? "0.25rem"
: density === "comfortable"
? ".75rem"
: "1.25rem",
userSelect: enableMultiSort && column.getCanSort() ? "none" : undefined,
verticalAlign: "middle",
...getCommonMRTCellStyles({
column,
header,
table,
tableCellProps,
theme,
}), }),
...draggingBorders, ...rest,
backgroundColor: backgroundColor, };
})}
> const isColumnPinned = enableColumnPinning && columnDef.columnDefType !== "group" && column.getIsPinned();
{tableCellProps.children ?? (
<Box const showColumnActions =
className="Mui-TableHeadCell-Content" (enableColumnActions || columnDef.enableColumnActions) && columnDef.enableColumnActions !== false;
sx={{
alignItems: "center", const showDragHandle =
display: "flex", enableColumnDragging !== false &&
flexDirection: columnDef.enableColumnDragging !== false &&
tableCellProps?.align === "right" ? "row-reverse" : "row", (enableColumnDragging ||
justifyContent: (enableColumnOrdering && columnDef.enableColumnOrdering !== false) ||
columnDefType === "group" || tableCellProps?.align === "center" (enableGrouping && columnDef.enableGrouping !== false && !grouping.includes(column.id)));
? "center"
: column.getCanResize() const headerPL = useMemo(() => {
? "space-between" let pl = 0;
: "flex-start", if (column.getCanSort()) pl += 1;
position: "relative", if (showColumnActions) pl += 1.75;
width: "100%", if (showDragHandle) pl += 1.5;
}} return pl;
> }, [showColumnActions, showDragHandle]);
<Box
className="Mui-TableHeadCell-Content-Labels" const draggingBorders = useMemo(() => {
onClick={column.getToggleSortingHandler()} const showResizeBorder =
sx={{ columnSizingInfo.isResizingColumn === column.id &&
alignItems: "center", columnResizeMode === "onChange" &&
cursor: !header.subHeaders.length;
column.getCanSort() && columnDefType !== "group"
? "pointer" const borderStyle = showResizeBorder
: undefined, ? `2px solid ${draggingBorderColor} !important`
display: "flex", : draggingColumn?.id === column.id
flexDirection: ? `1px dashed ${theme.palette.grey[500]}`
tableCellProps?.align === "right" ? "row-reverse" : "row", : hoveredColumn?.id === column.id
overflow: columnDefType === "data" ? "hidden" : undefined, ? `2px dashed ${draggingBorderColor}`
pl: : undefined;
tableCellProps?.align === "center"
? `${headerPL}rem` if (showResizeBorder) {
: undefined, return columnResizeDirection === "ltr" ? { borderRight: borderStyle } : { borderLeft: borderStyle };
}
const draggingBorders = borderStyle
? {
borderLeft: borderStyle,
borderRight: borderStyle,
borderTop: borderStyle,
}
: undefined;
return draggingBorders;
}, [draggingColumn, hoveredColumn, columnSizingInfo.isResizingColumn]);
const handleDragEnter = (_e) => {
if (enableGrouping && hoveredColumn?.id === "drop-zone") {
setHoveredColumn(null);
}
if (enableColumnOrdering && draggingColumn && columnDefType !== "group") {
setHoveredColumn(columnDef.enableColumnOrdering !== false ? column : null);
}
};
const handleDragOver = (e) => {
if (columnDef.enableColumnOrdering !== false) {
e.preventDefault();
}
};
const HeaderElement =
parseFromValuesOrFunc(columnDef.Header, {
column,
header,
table,
}) ?? columnDef.header;
const columnRelativeDepth = header.depth - column.depth;
if (columnRelativeDepth > 1) {
return null;
}
let rowSpan = 1;
if (header.isPlaceholder) {
const leafs = header.getLeafHeaders();
rowSpan = leafs[leafs.length - 1].depth - header.depth;
}
return (
<TableCell
align={columnDefType === "group" ? "center" : theme.direction === "rtl" ? "right" : "left"}
colSpan={header.colSpan}
rowSpan={rowSpan}
data-index={staticColumnIndex}
data-pinned={!!isColumnPinned || undefined}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
ref={(node) => {
if (node) {
tableHeadCellRefs.current[column.id] = node;
if (columnDefType !== "group") {
columnVirtualizer?.measureElement?.(node);
}
}
}} }}
> {...tableCellProps}
<Box sx={(theme) => ({
className="Mui-TableHeadCell-Content-Wrapper" "& :hover": {
sx={{ ".MuiButtonBase-root": {
"&:hover": { opacity: 1,
textOverflow: "clip", },
}, },
minWidth: `${Math.min(columnDef.header?.length ?? 0, 4)}ch`, flexDirection: layoutMode?.startsWith("grid") ? "column" : undefined,
overflow: columnDefType === "data" ? "hidden" : undefined, fontWeight: "bold",
textOverflow: "ellipsis", overflow: "visible",
textAlign: "start", p:
color: "#fff", density === "compact"
fontWeight: "400", ? "0.5rem"
whiteSpace: "nowrap", : density === "comfortable"
}} ? columnDefType === "display"
> ? "0.75rem"
{HeaderElement} : "1rem"
</Box> : columnDefType === "display"
{column.getCanSort() && columnDefType !== "group" && ( ? "1rem 1.25rem"
<MRT_TableHeadCellSortLabel : "1.5rem",
sx={{ pb: columnDefType === "display" ? 0 : showColumnFilters || density === "compact" ? "0.4rem" : "0.6rem",
width: 20, pt:
"& .MuiTableSortLabel-icon": { columnDefType === "group" || density === "compact"
color: `#fff !important`, ? "0.25rem"
}, : density === "comfortable"
}} ? ".75rem"
header={header} : "1.25rem",
table={table} userSelect: enableMultiSort && column.getCanSort() ? "none" : undefined,
/> verticalAlign: "middle",
...getCommonMRTCellStyles({
column,
header,
table,
tableCellProps,
theme,
}),
...draggingBorders,
backgroundColor: backgroundColor,
})}
>
{tableCellProps.children ?? (
<Box
className="Mui-TableHeadCell-Content"
sx={{
alignItems: "center",
display: "flex",
flexDirection: tableCellProps?.align === "right" ? "row-reverse" : "row",
justifyContent:
columnDefType === "group" || tableCellProps?.align === "center"
? "center"
: column.getCanResize()
? "space-between"
: "flex-start",
position: "relative",
width: "100%",
}}
>
<Box
className="Mui-TableHeadCell-Content-Labels"
onClick={column.getToggleSortingHandler()}
sx={{
alignItems: "center",
cursor: column.getCanSort() && columnDefType !== "group" ? "pointer" : undefined,
display: "flex",
flexDirection: tableCellProps?.align === "right" ? "row-reverse" : "row",
overflow: columnDefType === "data" ? "hidden" : undefined,
pl: tableCellProps?.align === "center" ? `${headerPL}rem` : undefined,
}}
>
<Box
className="Mui-TableHeadCell-Content-Wrapper"
sx={{
"&:hover": {
textOverflow: "clip",
},
minWidth: `${Math.min(columnDef.header?.length ?? 0, 4)}ch`,
overflow: columnDefType === "data" ? "hidden" : undefined,
textOverflow: "ellipsis",
textAlign: "start",
color: "#fff",
fontWeight: "400",
whiteSpace: "nowrap",
}}
>
{HeaderElement}
</Box>
{column.getCanSort() && columnDefType !== "group" && (
<MRT_TableHeadCellSortLabel
sx={{
width: 20,
"& .MuiTableSortLabel-icon": {
color: `#fff !important`,
},
}}
header={header}
table={table}
/>
)}
</Box>
</Box>
)} )}
</Box> </TableCell>
</Box> );
)}
</TableCell>
);
}; };
export default DataTable_TableHeadCell; export default DataTable_TableHeadCell;

View File

@@ -3,67 +3,58 @@ import { TableRow, useTheme } from "@mui/material";
import DataTable_TableHeadCell from "./TableHeadCell"; import DataTable_TableHeadCell from "./TableHeadCell";
const DataTable_TableHeadRow = ({ const DataTable_TableHeadRow = ({
columnVirtualizer, columnVirtualizer,
headerGroup, headerGroup,
table, table,
index, // Add index prop index, // Add index prop
...rest ...rest
}) => { }) => {
const { palette } = useTheme(); const { palette } = useTheme();
const { const {
options: { enableStickyHeader, layoutMode, muiTableHeadRowProps }, options: { enableStickyHeader, layoutMode, muiTableHeadRowProps },
} = table; } = table;
const { virtualColumns, virtualPaddingLeft, virtualPaddingRight } = const { virtualColumns, virtualPaddingLeft, virtualPaddingRight } = columnVirtualizer ?? {};
columnVirtualizer ?? {};
const backgroundColor = const backgroundColor = index % 2 === 0 ? palette.primary.main : palette.secondary.main;
index % 2 === 0 ? palette.primary.main : palette.secondary.main;
const tableRowProps = { const tableRowProps = {
...parseFromValuesOrFunc(muiTableHeadRowProps, { ...parseFromValuesOrFunc(muiTableHeadRowProps, {
headerGroup, headerGroup,
table, table,
}), }),
...rest, ...rest,
sx: (theme) => ({ sx: (theme) => ({
// Access theme from the sx function // Access theme from the sx function
...(parseFromValuesOrFunc(muiTableHeadRowProps?.sx, theme) || {}), ...(parseFromValuesOrFunc(muiTableHeadRowProps?.sx, theme) || {}),
display: layoutMode?.startsWith("grid") ? "flex" : undefined, display: layoutMode?.startsWith("grid") ? "flex" : undefined,
position: position: enableStickyHeader && layoutMode === "semantic" ? "sticky" : "relative",
enableStickyHeader && layoutMode === "semantic" ? "sticky" : "relative", top: 0,
top: 0, }),
}), };
};
return ( return (
<TableRow {...tableRowProps}> <TableRow {...tableRowProps}>
{virtualPaddingLeft && ( {virtualPaddingLeft && <th style={{ display: "flex", width: virtualPaddingLeft }} />}
<th style={{ display: "flex", width: virtualPaddingLeft }} /> {(virtualColumns ?? headerGroup.headers).map((headerOrVirtualHeader, staticColumnIndex) => {
)} const header = columnVirtualizer
{(virtualColumns ?? headerGroup.headers).map( ? headerGroup.headers[headerOrVirtualHeader.index]
(headerOrVirtualHeader, staticColumnIndex) => { : headerOrVirtualHeader;
const header = columnVirtualizer
? headerGroup.headers[headerOrVirtualHeader.index]
: headerOrVirtualHeader;
return header ? ( return header ? (
<DataTable_TableHeadCell <DataTable_TableHeadCell
columnVirtualizer={columnVirtualizer} columnVirtualizer={columnVirtualizer}
header={header} header={header}
backgroundColor={backgroundColor} backgroundColor={backgroundColor}
key={header.id} key={header.id}
staticColumnIndex={staticColumnIndex} staticColumnIndex={staticColumnIndex}
table={table} table={table}
/> />
) : null; ) : null;
}, })}
)} {virtualPaddingRight && <th style={{ display: "flex", width: virtualPaddingRight }} />}
{virtualPaddingRight && ( </TableRow>
<th style={{ display: "flex", width: virtualPaddingRight }} /> );
)}
</TableRow>
);
}; };
export default DataTable_TableHeadRow; export default DataTable_TableHeadRow;

View File

@@ -8,36 +8,31 @@ import HideOrShowAll from "@/core/components/DataTable/hide/HideOrShowAll";
import ScrollBox from "../../ScrollBox"; import ScrollBox from "../../ScrollBox";
function HideBody({ columns, drawerState, setDrawerState }) { function HideBody({ columns, drawerState, setDrawerState }) {
const { hideData, setHideData } = useDataTable(); const { hideData, setHideData } = useDataTable();
return ( return (
<Drawer <Drawer
open={drawerState} open={drawerState}
onClose={() => setDrawerState(false)} onClose={() => setDrawerState(false)}
sx={{ sx={{
overflowY: "hidden", overflowY: "hidden",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
height: "100%", height: "100%",
zIndex: "1300", zIndex: "1300",
}} }}
> >
<HideHeader setDrawerState={setDrawerState} /> <HideHeader setDrawerState={setDrawerState} />
<HideOrShowAll hideData={hideData} setHideData={setHideData} /> <HideOrShowAll hideData={hideData} setHideData={setHideData} />
<ScrollBox> <ScrollBox>
<Box sx={{ px: 1, py: 2, display: "flex", flexDirection: "column" }}> <Box sx={{ px: 1, py: 2, display: "flex", flexDirection: "column" }}>
{columns.map((column) => ( {columns.map((column) => (
<HideBodyField <HideBodyField key={column.id} column={column} hideData={hideData} setHideData={setHideData} />
key={column.id} ))}
column={column} </Box>
hideData={hideData} </ScrollBox>
setHideData={setHideData} </Drawer>
/> );
))}
</Box>
</ScrollBox>
</Drawer>
);
} }
export default HideBody; export default HideBody;

View File

@@ -3,78 +3,74 @@ import { SimpleTreeView } from "@mui/x-tree-view";
import { TreeItem, treeItemClasses } from "@mui/x-tree-view/TreeItem"; import { TreeItem, treeItemClasses } from "@mui/x-tree-view/TreeItem";
const CustomTreeItem = styled(TreeItem)(({ theme }) => ({ const CustomTreeItem = styled(TreeItem)(({ theme }) => ({
[`& .${treeItemClasses.content}`]: { [`& .${treeItemClasses.content}`]: {
padding: theme.spacing(0.5, 0.5), padding: theme.spacing(0.5, 0.5),
margin: theme.spacing(0.2, 0), margin: theme.spacing(0.2, 0),
gap: 0, gap: 0,
}, },
[`& .${treeItemClasses.iconContainer}`]: { [`& .${treeItemClasses.iconContainer}`]: {
"& .close": { "& .close": {
opacity: 0.3, opacity: 0.3,
},
},
[`& .${treeItemClasses.groupTransition}`]: {
marginLeft: 16,
paddingLeft: 16,
borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`,
}, },
},
[`& .${treeItemClasses.groupTransition}`]: {
marginLeft: 16,
paddingLeft: 16,
borderLeft: `1px dashed ${alpha(theme.palette.text.primary, 0.4)}`,
},
})); }));
function HideBodyField({ column, hideData, setHideData }) { function HideBodyField({ column, hideData, setHideData }) {
const handleCheckboxChange = () => { const handleCheckboxChange = () => {
setHideData((prevData) => { setHideData((prevData) => {
const updateHideData = (data, id) => { const updateHideData = (data, id) => {
if (data.hasOwnProperty(id)) { if (data.hasOwnProperty(id)) {
return { ...data, [id]: !data[id] }; return { ...data, [id]: !data[id] };
}
const updatedData = { ...data };
for (const key in data) {
if (data[key] && typeof data[key] === "object") {
updatedData[key] = updateHideData(data[key], id);
}
}
return updatedData;
};
return updateHideData(prevData, column.id);
});
};
const labelContent = (() => {
if (typeof hideData[column.id] === "boolean") {
return (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Checkbox checked={hideData[column.id]} onChange={handleCheckboxChange} name={column.id} />
<Typography variant="subtitle1">{column.header}</Typography>
</Box>
);
} else {
return (
<Box sx={{ display: "flex", alignItems: "center", p: 1 }}>
<Typography variant="subtitle1">{column.header}</Typography>
</Box>
);
} }
const updatedData = { ...data }; })();
for (const key in data) {
if (data[key] && typeof data[key] === "object") {
updatedData[key] = updateHideData(data[key], id);
}
}
return updatedData;
};
return updateHideData(prevData, column.id); return (
}); <SimpleTreeView disableSelection>
}; <CustomTreeItem itemId={column.id} label={labelContent}>
{column.columns?.map((subColumn) => (
const labelContent = (() => { <HideBodyField
if (typeof hideData[column.id] === "boolean") { key={subColumn.id}
return ( column={subColumn}
<Box sx={{ display: "flex", alignItems: "center" }}> hideData={hideData[column.id]}
<Checkbox setHideData={setHideData}
checked={hideData[column.id]} />
onChange={handleCheckboxChange} ))}
name={column.id} </CustomTreeItem>
/> </SimpleTreeView>
<Typography variant="subtitle1">{column.header}</Typography> );
</Box>
);
} else {
return (
<Box sx={{ display: "flex", alignItems: "center", p: 1 }}>
<Typography variant="subtitle1">{column.header}</Typography>
</Box>
);
}
})();
return (
<SimpleTreeView disableSelection>
<CustomTreeItem itemId={column.id} label={labelContent}>
{column.columns?.map((subColumn) => (
<HideBodyField
key={subColumn.id}
column={subColumn}
hideData={hideData[column.id]}
setHideData={setHideData}
/>
))}
</CustomTreeItem>
</SimpleTreeView>
);
} }
export default HideBodyField; export default HideBodyField;

View File

@@ -5,33 +5,31 @@ import useDataTable from "@/lib/hooks/useDataTable";
import { flattenObjectOfObjects } from "@/core/utils/flattenObjectOfObjects"; import { flattenObjectOfObjects } from "@/core/utils/flattenObjectOfObjects";
function HideButton({ drawerState, setDrawerState }) { function HideButton({ drawerState, setDrawerState }) {
const { hideData } = useDataTable(); const { hideData } = useDataTable();
const flattenHideData = flattenObjectOfObjects(hideData); const flattenHideData = flattenObjectOfObjects(hideData);
const falseCount = Object.values(flattenHideData).filter( const falseCount = Object.values(flattenHideData).filter((value) => value === false).length;
(value) => value === false,
).length;
return ( return (
<Tooltip title="نمایش/مخفی کردن ستون ها"> <Tooltip title="نمایش/مخفی کردن ستون ها">
<IconButton <IconButton
onClick={() => { onClick={() => {
setDrawerState(!drawerState); setDrawerState(!drawerState);
}} }}
aria-label="hide table column" aria-label="hide table column"
> >
<Badge <Badge
badgeContent={falseCount} badgeContent={falseCount}
color="primary" color="primary"
anchorOrigin={{ anchorOrigin={{
vertical: "top", vertical: "top",
horizontal: "left", horizontal: "left",
}} }}
> >
<ViewColumnIcon /> <ViewColumnIcon />
</Badge> </Badge>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
); );
} }
export default HideButton; export default HideButton;

Some files were not shown because too many files have changed in this diff Show More