Merge branch 'feature/remove_tel_number' into 'develop'

complete role page with show/store/update and delete actions and remove tel number from login page and add it to user page and add prettier to format the code as code formatter

See merge request witel-front-end/crm-app!45
This commit is contained in:
2025-07-29 12:30:26 +00:00
199 changed files with 16211 additions and 14778 deletions

2
.gitignore vendored
View File

@@ -31,7 +31,7 @@ yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
.env
# vercel
.vercel

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

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
.idea/crm-app.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="JSAnnotator" enabled="false" level="ERROR" enabled_by_default="false" />
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/crm-app.iml" filepath="$PROJECT_DIR$/.idea/crm-app.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

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:
nextjs:
build:
context: .
dockerfile: Dockerfile
container_name: nextjs-app
environment:
- NEXT_PUBLIC_VERSION=${NEXT_PUBLIC_VERSION}
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
- NEXT_PUBLIC_SERVER_SOCKET_URL=${NEXT_PUBLIC_SERVER_SOCKET_URL}
ports:
- "3000:3000"
env_file:
- .env.local
restart: unless-stopped
nextjs:
build:
context: .
dockerfile: Dockerfile
container_name: nextjs-app
environment:
- NEXT_PUBLIC_VERSION=${NEXT_PUBLIC_VERSION}
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
- NEXT_PUBLIC_SERVER_SOCKET_URL=${NEXT_PUBLIC_SERVER_SOCKET_URL}
ports:
- "3000:3000"
env_file:
- .env
restart: unless-stopped

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
import RolesPage from "@/components/dashboard/Roles";
import WithPermission from "@/core/middlewares/withPermission";
export const metadata = {
title: "نقش ها",
};
const Page = () => {
return (
<WithPermission permission_name={["manage_roles"]}>
<RolesPage />
</WithPermission>
);
};
export default Page;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
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";
export const metadata = {
title: {
template: "%s | سامانه CRM",
default: "سامانه CRM",
},
title: {
template: "%s | سامانه CRM",
default: "سامانه CRM",
},
};
export default function RootLayout({ children }) {
return (
<html lang="fa" dir={"rtl"}>
<head>
<link rel="icon" href={favicon.src} type="image/png" sizes="any" />
</head>
<body style={{ height: "100vh", width: "100vw" }}>
<AppRouterCacheProvider
CacheProvider={Rtl}
options={{ enableCssLayer: true }}
>
<AuthProvider>
<TableSettingProvider>{children}</TableSettingProvider>
</AuthProvider>
</AppRouterCacheProvider>
</body>
</html>
);
return (
<html lang="fa" dir={"rtl"}>
<head>
<link rel="icon" href={favicon.src} type="image/png" sizes="any" />
</head>
<body style={{ height: "100vh", width: "100vw" }}>
<AppRouterCacheProvider CacheProvider={Rtl} options={{ enableCssLayer: true }}>
<AuthProvider>
<TableSettingProvider>{children}</TableSettingProvider>
</AuthProvider>
</AppRouterCacheProvider>
</body>
</html>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,90 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_ROLE_LIST } from "@/core/utils/routes";
import { Box, Typography } from "@mui/material";
import { useMemo } from "react";
import Toolbar from "./Toolbar";
import moment from "jalali-moment";
import RowActions from "./RowAcctions";
const DataTable = () => {
const columns = useMemo(
() => [
{
accessorKey: "id",
header: "کد یکتا",
id: "id",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "name_fa",
header: "نام",
id: "name_fa",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "created_at",
header: "تاریخ ساخت",
id: "created_at",
enableColumnFilter: false,
datatype: "date",
filterMode: "equals",
grow: false,
size: 100,
Cell: ({ renderedCellValue }) => (
<Typography variant="body2">
{renderedCellValue ? moment(renderedCellValue).locale("fa").format("HH:mm | YYYY/MM/DD") : "-"}
</Typography>
),
},
{
accessorKey: "updated_at",
header: "تاریخ آخرین بروزرسانی",
id: "updated_at",
enableColumnFilter: false,
datatype: "date",
filterMode: "equals",
grow: false,
size: 100,
Cell: ({ renderedCellValue }) => (
<Typography variant="body2">
{renderedCellValue ? moment(renderedCellValue).locale("fa").format("HH:mm | YYYY/MM/DD") : "-"}
</Typography>
),
},
],
[]
);
return (
<>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
sorting={[{ id: "id", desc: true }]}
table_url={GET_ROLE_LIST}
page_name={"rolesPage"}
table_name={"rolesList"}
TableToolbar={Toolbar}
enableRowActions
positionActionsColumn={"last"}
RowActions={RowActions}
/>
</Box>
</>
);
};
export default DataTable;

View File

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

View File

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

View File

@@ -0,0 +1,186 @@
import LtrTextField from "@/core/components/LtrTextField";
import StyledForm from "@/core/components/StyledForm";
import { CREATE_ROLE } from "@/core/utils/routes";
import useRequest from "@/lib/hooks/useRequest";
import { yupResolver } from "@hookform/resolvers/yup";
import {
Box,
Button,
Checkbox,
Chip,
DialogActions,
DialogContent,
Divider,
FormControlLabel,
FormHelperText,
Grid,
Skeleton,
Stack,
} from "@mui/material";
import { Controller, useForm } from "react-hook-form";
import { array, object, string } from "yup";
import usePermissionsList from "@/lib/hooks/usePermissionsList";
const validationSchema = object().shape({
name: string().required("نام فارسی نقش را وارد کنید"),
name_fa: string().required("نام انگلیسی نقش را وارد کنید"),
permissions: array().min(1, "حداقل یک دسترسی را انتخاب نمایید")
});
const UpdateRoleForm = ({ values, setOpen, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const { permissions, loadingPermissions } = usePermissionsList();
const defaultValues = {
name: values.name,
name_fa: values.name_fa,
permissions: values.permissions?.map(p => p.id) || []
};
const {
control,
handleSubmit,
formState: { isSubmitting, errors },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => {
try {
await requestServer(`${CREATE_ROLE}/${values.id}`, "post", {
data: {
permissions: data.permissions,
name: data.name,
name_fa: data.name_fa,
},
});
setOpen(false);
mutate();
} catch (error) {}
};
return (
<>
<DialogContent dividers>
<Divider sx={{ mb: 3 }}>
<Chip color="primary" variant="outlined" label="کاربر جدید" />
</Divider>
<StyledForm
id="UpdateRoleForm"
onSubmit={handleSubmit(onSubmit)}
style={{ width: "100%", height: "100%" }}
>
<Stack spacing={2}>
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="نام انگلیسی نقش"
variant="outlined"
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"name"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="نام فارسی نقش"
variant="outlined"
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"name_fa"}
/>
</Grid>
<Box sx={{width: "100%"}}>
<Divider>
<Chip label="سطوح دسترسی" size="medium" />
</Divider>
</Box>
<Grid container spacing={1} sx={{width: "100%"}}>
{!loadingPermissions ? (
<Controller
control={control}
name="permissions"
render={({ field, fieldState: {error} }) => (
<>
{permissions.map((permission) => (
<Grid key={permission.id} size={{ xs: 12, md: 4 }}>
<FormControlLabel
control={
<Checkbox
value={permission.id}
checked={field.value.includes(permission.id)}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
if (e.target.checked) {
field.onChange([...field.value, value]);
} else {
field.onChange(field.value.filter((id) => id !== value));
}
}}
/>
}
label={permission.name_fa}
/>
</Grid>
))}
<FormHelperText sx={{color: "error.main"}}>{!!error && error.message}</FormHelperText>
</>
)}
/>
) : (
<Grid container spacing={2} width="100%">
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
</Grid>
)}
</Grid>
</Grid>
</Stack>
</StyledForm>
</DialogContent>
<DialogActions>
<Button
disabled={isSubmitting}
variant="outlined"
color="secondary"
size="large"
onClick={() => setOpen(false)}
>
بستن
</Button>
<Button
type="submit"
disabled={isSubmitting}
form="UpdateRoleForm"
size="large"
autoFocus
variant="contained"
>
{isSubmitting ? "در حال ویرایش کاربر..." : "ویرایش کاربر"}
</Button>
</DialogActions>
</>
);
};
export default UpdateRoleForm;

View File

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

View File

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

View File

@@ -0,0 +1,184 @@
import LtrTextField from "@/core/components/LtrTextField";
import StyledForm from "@/core/components/StyledForm";
import { yupResolver } from "@hookform/resolvers/yup";
import {
Box,
Button,
Checkbox,
Chip,
DialogActions,
DialogContent,
Divider,
FormControlLabel, FormHelperText,
Grid, Skeleton,
Stack,
} from "@mui/material";
import { Controller, useForm } from "react-hook-form";
import { array, object, string } from "yup";
import useRequest from "@/lib/hooks/useRequest";
import { CREATE_ROLE } from "@/core/utils/routes";
import usePermissionsList from "@/lib/hooks/usePermissionsList";
const defaultValues = {
name: "",
name_fa: "",
permissions: []
};
const validationSchema = object().shape({
name: string().required("نام فارسی نقش را وارد کنید"),
name_fa: string().required("نام انگلیسی نقش را وارد کنید"),
permissions: array().min(1, "حداقل یک دسترسی را انتخاب نمایید")
});
const CreateRoleForm = ({ setOpen, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const { permissions, loadingPermissions } = usePermissionsList();
const {
control,
handleSubmit,
formState: { isSubmitting, errors },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => {
try {
await requestServer(CREATE_ROLE, "post", {
data: {
permissions: data.permissions,
name: data.name,
name_fa: data.name_fa,
},
});
setOpen(false);
mutate();
} catch (error) {}
};
return (
<>
<DialogContent dividers>
<Divider sx={{ mb: 3 }}>
<Chip color="primary" variant="outlined" label="نقش جدید" />
</Divider>
<StyledForm
id="createRoleForm"
onSubmit={handleSubmit(onSubmit)}
style={{ width: "100%", height: "100%" }}
>
<Stack spacing={2}>
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="نام انگلیسی نقش"
variant="outlined"
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"name"}
/>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="نام فارسی نقش"
variant="outlined"
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"name_fa"}
/>
</Grid>
<Box sx={{width: "100%"}}>
<Divider>
<Chip label="سطوح دسترسی" size="medium" />
</Divider>
</Box>
<Grid container spacing={1} sx={{width: "100%"}}>
{!loadingPermissions ? (
<Controller
control={control}
name="permissions"
render={({ field, fieldState: {error} }) => (
<>
{permissions.map((permission) => (
<Grid key={permission.id} size={{ xs: 12, md: 4 }}>
<FormControlLabel
control={
<Checkbox
value={permission.id}
checked={field.value.includes(permission.id)}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
if (e.target.checked) {
field.onChange([...field.value, value]);
} else {
field.onChange(field.value.filter((id) => id !== value));
}
}}
/>
}
label={permission.name_fa}
/>
</Grid>
))}
<FormHelperText sx={{color: "error.main"}}>{!!error && error.message}</FormHelperText>
</>
)}
/>
) : (
<Grid container spacing={2} width="100%">
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
<Grid size={{ xs: 12, md: 4 }}><Skeleton animation="wave" /></Grid>
</Grid>
)}
</Grid>
</Grid>
</Stack>
</StyledForm>
</DialogContent>
<DialogActions>
<Button
disabled={isSubmitting}
variant="outlined"
color="secondary"
size="large"
onClick={() => setOpen(false)}
>
بستن
</Button>
<Button
type="submit"
disabled={isSubmitting}
form="createRoleForm"
size="large"
autoFocus
variant="contained"
>
{isSubmitting ? "در حال ثبت نقش جدید..." : "ثبت نقش جدید"}
</Button>
</DialogActions>
</>
);
};
export default CreateRoleForm;

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const RolesPage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"نقش ها"} />
<DataTable />
</Stack>
);
};
export default RolesPage;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,16 +4,16 @@ import { Chip } from "@mui/material";
import { useEffect, useState } from "react";
const SidebarBadge = ({ badge, chipProps = {} }) => {
const { data } = useSidebarBadge();
const [value, setValue] = useState();
const { data } = useSidebarBadge();
const [value, setValue] = useState();
useEffect(() => {
setValue(getValueByPath(data, badge));
}, [data, badge]);
useEffect(() => {
setValue(getValueByPath(data, badge));
}, [data, badge]);
if (!data) return null;
if (!value) return null;
if (!data) 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;

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,7 @@
"use client";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import MenuIcon from "@mui/icons-material/Menu";
import {
Box,
Drawer,
IconButton,
Stack,
styled,
Toolbar,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Box, Drawer, IconButton, Stack, styled, Toolbar, Typography, useMediaQuery, useTheme } from "@mui/material";
import MuiAppBar from "@mui/material/AppBar";
import { useState, useEffect } from "react";
import HeaderMenu from "./HeaderMenu";
@@ -21,170 +11,157 @@ import SidebarMenu from "./Sidebar/SidebarMenu";
const drawerWidth = 300;
const Main = styled(Stack, { shouldForwardProp: (prop) => prop !== "open" })(
({ theme, open }) => ({
const Main = styled(Stack, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({
flexGrow: 1,
height: "100%",
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
marginLeft: `-${drawerWidth}px`,
...(open && {
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginLeft: 0,
transition: theme.transitions.create("margin", {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginLeft: 0,
}),
}),
);
}));
const AppBar = styled(MuiAppBar, {
shouldForwardProp: (prop) => prop !== "open",
shouldForwardProp: (prop) => prop !== "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"], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
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"], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
}),
}),
}));
const DrawerHeader = styled("div")(({ theme }) => ({
display: "flex",
alignItems: "center",
padding: theme.spacing(0, 1),
...theme.mixins.toolbar,
minHeight: "40px !important",
justifyContent: "flex-end",
display: "flex",
alignItems: "center",
padding: theme.spacing(0, 1),
...theme.mixins.toolbar,
minHeight: "40px !important",
justifyContent: "flex-end",
}));
const HeaderWithSidebar = ({ children }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); // Detect mobile/tablet screen
const [open, setOpen] = useState(!isMobile); // Initial state based on screen size
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md")); // Detect mobile/tablet screen
const [open, setOpen] = useState(!isMobile); // Initial state based on screen size
useEffect(() => {
setOpen(!isMobile); // Toggle state when screen size changes
}, [isMobile]);
useEffect(() => {
setOpen(!isMobile); // Toggle state when screen size changes
}, [isMobile]);
const now = moment().locale("fa").format("YYYY/MM/DD");
const now = moment().locale("fa").format("YYYY/MM/DD");
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
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 />
return (
<Box
sx={{
height: "100%",
width: "100%",
overflowY: "auto",
p: 1,
overflowX: "hidden",
}}
sx={{
display: "flex",
height: `100%`,
width: "100%",
}}
>
{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>
</Main>
</Box>
);
);
};
export default HeaderWithSidebar;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,115 +1,108 @@
import {
Box,
List,
ListItem,
ListItemAvatar,
ListItemText,
Skeleton,
} from "@mui/material";
import { Box, List, ListItem, ListItemAvatar, ListItemText, Skeleton } from "@mui/material";
const LoadingHistory = () => {
return (
<>
<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
return (
<>
<List
data-testid="loading-list"
sx={{
width: "100%",
display: "flex",
justifyContent: "space-between",
width: "100%",
bgcolor: "rgba(222,234,215,0.11)",
}}
>
<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>
</>
);
>
<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={{
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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,189 +11,169 @@ import { FA_DATATABLE_LOCALIZATION } from "./localization/fa/datatable";
import DataTable_Paper from "./table/Paper";
const rowSelectionReducer = (state, action) => {
switch (action.type) {
case "TOGGLE_ROW":
return { [action.payload]: true };
case "RESET":
return {};
default:
return state;
}
switch (action.type) {
case "TOGGLE_ROW":
return { [action.payload]: true };
case "RESET":
return {};
default:
return state;
}
};
const DataTable_Main = (props) => {
const request = useRequest();
const [rowSelection, dispatchRowSelection] = useReducer(
rowSelectionReducer,
{},
);
const { filterData, sortData, setSortData, hideData } = useDataTable();
const {
need_filter,
table_url,
user_id,
page_name,
table_name,
columns,
initialStateProps,
TableToolbar,
table_title,
RowActions,
specific_data,
specialFilter,
} = props;
const flatColumns = flattenArrayOfObjects(columns, "columns");
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
const [totalRowCount, setTotalRowCount] = useState(0);
const flattenHideData = flattenObjectOfObjects(hideData);
const onSortingChange = (event) => {
setSortData(event);
};
const request = useRequest();
const [rowSelection, dispatchRowSelection] = useReducer(rowSelectionReducer, {});
const { filterData, sortData, setSortData, hideData } = useDataTable();
const {
need_filter,
table_url,
user_id,
page_name,
table_name,
columns,
initialStateProps,
TableToolbar,
table_title,
RowActions,
specific_data,
specialFilter,
} = props;
const flatColumns = flattenArrayOfObjects(columns, "columns");
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
const [totalRowCount, setTotalRowCount] = useState(0);
const flattenHideData = flattenObjectOfObjects(hideData);
const onSortingChange = (event) => {
setSortData(event);
};
const fetchUrl = useCallback(() => {
const params = new URLSearchParams();
params.set("start", `${pagination.pageIndex * pagination.pageSize}`);
params.set("size", pagination.pageSize);
if (specialFilter) {
const filteredSpecialFilterData = Object.values(specialFilter).filter(
(filter) => !isArrayEmpty(filter.value),
);
params.set("filters", JSON.stringify(filteredSpecialFilterData || []));
} else {
const filteredFilterData = DataTableFilterDataStructure(
filterData,
isArrayEmpty,
);
params.set(
"filters",
JSON.stringify(
filteredFilterData.length === 0 ? [] : filteredFilterData,
const fetchUrl = useCallback(() => {
const params = new URLSearchParams();
params.set("start", `${pagination.pageIndex * pagination.pageSize}`);
params.set("size", pagination.pageSize);
if (specialFilter) {
const filteredSpecialFilterData = Object.values(specialFilter).filter(
(filter) => !isArrayEmpty(filter.value)
);
params.set("filters", JSON.stringify(filteredSpecialFilterData || []));
} else {
const filteredFilterData = DataTableFilterDataStructure(filterData, isArrayEmpty);
params.set("filters", JSON.stringify(filteredFilterData.length === 0 ? [] : filteredFilterData));
}
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) => {
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} />
)}
</>
),
);
}
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]);
renderRowActions: ({ row }) => <RowActions row={row} mutate={mutate} />,
...props,
});
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
return (
<DataTable_Paper
table={table}
table_title={table_title}
need_filter={need_filter}
mutate={mutate}
filterData={specialFilter || filterData}
/>
)}
</>
),
renderRowActions: ({ row }) => <RowActions row={row} mutate={mutate} />,
...props,
});
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}
/>
);
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;

View File

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

View File

@@ -1,7 +1,4 @@
import {
getCommonMRTCellStyles,
parseFromValuesOrFunc,
} from "@/core/utils/utils";
import { getCommonMRTCellStyles, parseFromValuesOrFunc } from "@/core/utils/utils";
import { useTheme } from "@emotion/react";
import { Skeleton, TableCell } from "@mui/material";
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_TableBodyCellValue from "./TableBodyCellValue";
const DataTable_TableBodyCell = ({
cell,
numRows,
rowRef,
staticColumnIndex,
staticRowIndex,
table,
...rest
}) => {
const theme = useTheme();
const {
getState,
options: {
columnResizeDirection,
columnResizeMode,
createDisplayMode,
editDisplayMode,
enableCellActions,
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",
const DataTable_TableBodyCell = ({ cell, numRows, rowRef, staticColumnIndex, staticRowIndex, table, ...rest }) => {
const theme = useTheme();
const {
getState,
options: {
columnResizeDirection,
columnResizeMode,
createDisplayMode,
editDisplayMode,
enableCellActions,
enableClickToCopy,
enableColumnOrdering,
enableColumnPinning,
enableGrouping,
layoutMode,
mrtTheme: { draggingBorderColor, baseBackgroundColor },
muiSkeletonProps,
muiTableBodyCellProps,
},
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",
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;
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>
);
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,
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 const Memo_DataTable_TableBodyCell = memo(
DataTable_TableBodyCell,
(prev, next) => next.cell === prev.cell,
);
export const Memo_DataTable_TableBodyCell = memo(DataTable_TableBodyCell, (prev, next) => next.cell === prev.cell);

View File

@@ -2,111 +2,102 @@ import { Box } from "@mui/material";
const allowedTypes = ["string", "number"];
const DataTable_TableBodyCellValue = ({
cell,
rowRef,
staticColumnIndex,
staticRowIndex,
table,
}) => {
const {
getState,
options: {
enableFilterMatchHighlighting,
mrtTheme: { matchHighlightColor },
},
} = table;
const { column, row } = cell;
const { columnDef } = column;
const { globalFilter, globalFilterFn } = getState();
const filterValue = column.getFilterValue();
const DataTable_TableBodyCellValue = ({ cell, rowRef, staticColumnIndex, staticRowIndex, table }) => {
const {
getState,
options: {
enableFilterMatchHighlighting,
mrtTheme: { matchHighlightColor },
},
} = table;
const { column, row } = cell;
const { columnDef } = column;
const { globalFilter, globalFilterFn } = getState();
const filterValue = column.getFilterValue();
let renderedCellValue =
cell.getIsAggregated() && columnDef.AggregatedCell
? columnDef.AggregatedCell({
cell,
column,
row,
table,
})
: row.getIsGrouped() && !cell.getIsGrouped()
? null
: cell.getIsGrouped() && columnDef.GroupedCell
? columnDef.GroupedCell({
cell,
column,
row,
table,
})
: undefined;
let renderedCellValue =
cell.getIsAggregated() && columnDef.AggregatedCell
? columnDef.AggregatedCell({
cell,
column,
row,
table,
})
: row.getIsGrouped() && !cell.getIsGrouped()
? null
: cell.getIsGrouped() && columnDef.GroupedCell
? columnDef.GroupedCell({
cell,
column,
row,
table,
})
: undefined;
const isGroupedValue = renderedCellValue !== undefined;
const isGroupedValue = renderedCellValue !== undefined;
if (!isGroupedValue) {
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 (!isGroupedValue) {
renderedCellValue = cell.renderValue();
}
}
if (columnDef.Cell && !isGroupedValue) {
renderedCellValue = columnDef.Cell({
cell,
column,
renderedCellValue,
row,
rowRef,
staticColumnIndex,
staticRowIndex,
table,
});
}
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>
);
}
}
return renderedCellValue;
if (columnDef.Cell && !isGroupedValue) {
renderedCellValue = columnDef.Cell({
cell,
column,
renderedCellValue,
row,
rowRef,
staticColumnIndex,
staticRowIndex,
table,
});
}
return renderedCellValue;
};
export default DataTable_TableBodyCellValue;

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,122 +12,121 @@ import ScrollBox from "../../ScrollBox";
import FilterBodyController from "./FilterBodyController";
function FilterBody({ columns, drawerState, setDrawerState }) {
const { filterData, setFilterData } = useDataTable();
const [validationSchema, setValidationSchema] = useState(Yup.object({}));
const { filterData, setFilterData } = useDataTable();
const [validationSchema, setValidationSchema] = useState(Yup.object({}));
console.log(filterData);
console.log(filterData);
useEffect(() => {
const schema = Yup.object(
Object.keys(filterData).reduce((acc, key) => {
const initialValue = filterData[key];
if (initialValue.filterMode === "between") {
acc[key] = Yup.object().shape({
value: Yup.array()
.of(Yup.string().nullable())
.test({
test(value, ctx) {
const [start, end] = value || ["", ""];
if (
initialValue.datatype === "numeric" &&
parseInt(end, 10) <= parseInt(start, 10)
) {
return ctx.createError({
message: `مقدار وارده باید بیشتر از (${start}) باشد`,
useEffect(() => {
const schema = Yup.object(
Object.keys(filterData).reduce((acc, key) => {
const initialValue = filterData[key];
if (initialValue.filterMode === "between") {
acc[key] = Yup.object().shape({
value: Yup.array()
.of(Yup.string().nullable())
.test({
test(value, ctx) {
const [start, end] = value || ["", ""];
if (
initialValue.datatype === "numeric" &&
parseInt(end, 10) <= parseInt(start, 10)
) {
return ctx.createError({
message: `مقدار وارده باید بیشتر از (${start}) باشد`,
});
} else if ((start && !end) || (!start && end)) {
return ctx.createError({
message: "این بخش را تکمیل نمایید",
});
}
return true;
},
}),
});
} else if ((start && !end) || (!start && end)) {
return ctx.createError({
message: "این بخش را تکمیل نمایید",
});
}
return true;
},
}),
});
}
return acc;
}, {}),
}
return acc;
}, {})
);
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>
)}
</>
);
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;

View File

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

View File

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

View File

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

View File

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

View File

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

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