Merge branch 'release/v2.4.1'

This commit is contained in:
AmirHossein Mahmoodi
2025-08-16 09:57:18 +03:30
252 changed files with 20289 additions and 14790 deletions

5
.env.example Normal file
View File

@@ -0,0 +1,5 @@
NEXT_PUBLIC_VERSION = "2.4.1"
NEXT_PUBLIC_API_URL = "http://192.168.1.20:8888/server"
NEXT_PUBLIC_SERVER_SOCKET_URL = "ws://192.168.1.20:3023"
NEXT_PUBLIC_POWERED_BY_URL = "https://witel.ir"

3
.gitignore vendored
View File

@@ -31,7 +31,7 @@ yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# env files (can opt-in for committing if needed) # env files (can opt-in for committing if needed)
.env* .env
# vercel # vercel
.vercel .vercel
@@ -39,3 +39,4 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
/.idea

12
.prettierrc Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

12260
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

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

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import OperatorDialoguePage from "@/components/dashboard/monitoring/operator/OperatorDialogue";
export const metadata = {
title: "پیام های سیستمی",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<OperatorDialoguePage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import OperatorDialoguePage from "@/components/dashboard/monitoring/supervisor/OperatorDialogue";
export const metadata = {
title: "ارزیابی پیام های سیستمی",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<OperatorDialoguePage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import PeopleMessagePage from "@/components/dashboard/monitoring/operator/PeopleMessage";
export const metadata = {
title: "پیام های سیستمی",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<PeopleMessagePage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import PeopleMessagePage from "@/components/dashboard/monitoring/supervisor/PeopleMessage";
export const metadata = {
title: "پیام های سیستمی",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<PeopleMessagePage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import SystemMessagesPage from "@/components/dashboard/monitoring/operator/SystemMessage";
export const metadata = {
title: "پیام های سیستمی",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<SystemMessagesPage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import SystemMessagesPage from "@/components/dashboard/monitoring/supervisor/SystemMessages";
export const metadata = {
title: "پیام های سیستمی",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<SystemMessagesPage />
</WithPermission>
);
};
export default Page;

View File

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

View File

@@ -0,0 +1,16 @@
import ProvinceCumulativeCallsReportPage from "@/components/dashboard/provinceReports/ProvinceCumulativeCallsReport";
import WithPermission from "@/core/middlewares/withPermission";
export const metadata = {
title: "گزارشات",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<ProvinceCumulativeCallsReportPage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import ProvinceDetailedLogsReportPage from "@/components/dashboard/provinceReports/ProvinceDetailedLogsReport";
import WithPermission from "@/core/middlewares/withPermission";
export const metadata = {
title: "ریز گزارشات",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<ProvinceDetailedLogsReportPage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import ProvincePeopleMessagesReportPage from "@/components/dashboard/provinceReports/ProvincePeopleMessagesReport";
import WithPermission from "@/core/middlewares/withPermission";
export const metadata = {
title: "گزارش پیام‌ها",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<ProvincePeopleMessagesReportPage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import ProvinceOperatorPerformanceReportPage from "@/components/dashboard/provinceReports/ProvinceOperatorPerformanceReport";
import WithPermission from "@/core/middlewares/withPermission";
export const metadata = {
title: "عملکرد کارشناسان",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<ProvinceOperatorPerformanceReportPage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import CountryCallsReportPage from "@/components/dashboard/reports/CountryCallsReport";
export const metadata = {
title: "گزارشات",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<CountryCallsReportPage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import CountryKeyPressReportPage from "@/components/dashboard/reports/CountryKeyPressReport";
export const metadata = {
title: "گزارشات",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<CountryKeyPressReportPage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import CountryMessageRatingReportPage from "@/components/dashboard/reports/CountryMessageRatingReport";
export const metadata = {
title: "گزارشات",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<CountryMessageRatingReportPage />
</WithPermission>
);
};
export default Page;

View File

@@ -0,0 +1,16 @@
import WithPermission from "@/core/middlewares/withPermission";
import CountryVoiceRatingReportPage from "@/components/dashboard/reports/CountryVoiceRatingReport";
export const metadata = {
title: "گزارشات",
};
const Page = () => {
return (
<WithPermission permission_name={["all"]}>
<CountryVoiceRatingReportPage />
</WithPermission>
);
};
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"; import WithPermission from "@/core/middlewares/withPermission";
export const metadata = { export const metadata = {
title: "کاربران", title: "کاربران",
}; };
const Page = () => { const Page = () => {
return ( return (
<WithPermission permission_name={["manage_users"]}> <WithPermission permission_name={["manage_users"]}>
<UsersPage /> <UsersPage />
</WithPermission> </WithPermission>
); );
}; };
export default Page; export default Page;

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
import TeleportPage from "@/components/Teleport";
export const metadata = {
title: "درحال انتقال به استان مورد نظر",
};
const Page = () => {
return <TeleportPage />;
};
export default Page;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
'use client'
import LoadingHardPage from "@/core/components/LoadingHardPage";
import { TELEPORT_USER_ROUTE } from "@/core/utils/routes";
import { useAuth } from "@/lib/contexts/auth";
import useRequest from "@/lib/hooks/useRequest";
import { Stack, Typography } from "@mui/material";
import { useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
const TelePortPage = () => {
const { setToken } = useAuth()
const searchParams = useSearchParams();
const token = searchParams.get("token");
const user_id = searchParams.get("user_id");
const [error, setError] = useState(false)
const requestServer = useRequest({ notificationShow: false });
useEffect(() => {
const login = async () => {
try {
const response = await requestServer(TELEPORT_USER_ROUTE, "post", {
data: {
token: decodeURIComponent(token),
user_id: decodeURIComponent(user_id)
}
});
setToken(response.data.data.token);
} catch (error) { setError(true) }
}
login()
}, [])
return (
<LoadingHardPage
authState={error}
label={
error ? (
<Stack justifyContent={"center"} alignItems={"center"} spacing={2}>
<Typography variant={"body1"}>مشکلی در انتقال پیش آمده است!</Typography>
</Stack>
) : (
<Typography variant={"body1"}>درحال انتقال به مرکز مورد نظر...</Typography>
)
}
loading={true}
/>
)
}
export default TelePortPage

View File

@@ -0,0 +1,102 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { ROLE_SERVICE } 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: "name",
header: "نام انگلیسی",
id: "name",
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={ROLE_SERVICE}
page_name={"rolesPage"}
table_name={"rolesList"}
TableToolbar={Toolbar}
enableRowActions
positionActionsColumn={"last"}
RowActions={RowActions}
/>
</Box>
</>
);
};
export default DataTable;

View File

@@ -0,0 +1,39 @@
import { ROLE_SERVICE } 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(`${ROLE_SERVICE}/${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,202 @@
import LtrTextField from "@/core/components/LtrTextField";
import StyledForm from "@/core/components/StyledForm";
import { ROLE_SERVICE } 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(`${ROLE_SERVICE}/${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,202 @@
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 { ROLE_SERVICE } 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(ROLE_SERVICE, "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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,140 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_OPERATOR_DIALOGUE_OPERATOR } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import moment from "jalali-moment";
import Player from "./Player";
import CheckIcon from "@mui/icons-material/Check";
import CloseIcon from "@mui/icons-material/Close";
const DataTable = () => {
const columns = useMemo(
() => [
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.calldate ? moment(row.original.calldate).locale("fa").format("yyyy/MM/DD") : <>-</>,
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.calldate ? moment(row.original.calldate).locale("fa").format("HH:mm") : <>-</>,
},
{
accessorKey: "src",
header: "شماره تماس گیرنده",
id: "src",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "dst",
header: "کارشناس",
id: "dst",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "is_listen",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => {
const isListen = row.original.is_listen;
const Icon = isListen ? CheckIcon : CloseIcon;
const color = isListen ? "success.main" : "error.main";
return (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Icon sx={{ color }} />
</Box>
);
}
},
{
accessorKey: "disposition",
header: "وضعیت",
id: "disposition",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "duration",
header: "طول تماس ( ثانیه )",
id: "duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "player",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <Player audioUrl={row.original.recordingfile} messageId={row.original.id} />,
},
],
[]
);
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_OPERATOR_DIALOGUE_OPERATOR}
page_name={"operatorDialoguePage"}
table_name={"operatorDialogueList"}
positionActionsColumn={"last"}
/>
</Box>
);
};
export default DataTable;

View File

@@ -0,0 +1,22 @@
"use client";
import React from "react";
import { IconButton, Box } from "@mui/material";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import useWaveSurferPlayer from "@/lib/hooks/useWaveSurferPlayer";
const Player = ({ audioUrl }) => {
const { containerRef, isPlaying, playPause } = useWaveSurferPlayer(audioUrl);
return (
<Box sx={{ display: "flex", alignItems: "center", gap: 1, justifyContent: "center" }}>
<div ref={containerRef} style={{ width: 180 }} />
<IconButton aria-label="play-pause" onClick={playPause}>
{isPlaying ? <PauseIcon sx={{ color: "#d32f2f" }} /> : <PlayArrowIcon sx={{ color: "#1b4332" }} />}
</IconButton>
</Box>
);
};
export default Player;

View File

@@ -0,0 +1,153 @@
"use client";
import { Button, FormControl, FormHelperText, Grid, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material";
import LtrTextField from "@/core/components/LtrTextField";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { object, string, number } from "yup";
import { GET_SYSTEM_MESSAGES_SUPERVISOR } from "@/core/utils/routes";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/hooks/useRequest";
import usePermissionsList from "@/lib/hooks/usePermissionsList";
import { useEffect } from "react";
const RateMessage = ({ date, messageId, initialValues, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const defaultValues = {
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
};
const validationSchema = object().shape({
supervisor_id: string().required("کارشناس بررسی کننده را مشخص کنید"),
is_good: string().required("نحوه پاسخگویی را مشخص کنید"),
description: string().optional(),
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, errors, isDirty, isValid },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => {
const fixData = {
date: date,
cdr_unique_id: messageId,
supervisor_id: data.supervisor_id,
is_good: data.is_good,
...(data.description !== "" && { description: data.description }),
};
try {
await requestServer(`${GET_SYSTEM_MESSAGES_SUPERVISOR}`, "post", {
data: fixData,
});
mutate();
reset({
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
});
} catch (error) {}
};
useEffect(() => {
reset({
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
});
}, [initialValues.supervisor_id, initialValues.is_good, initialValues.description]);
return (
<StyledForm id="monitoringForm" onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", height: "100%" }}>
<Grid container spacing={2} sx={{ flexWrap: "nowrap", alignItems: "start" }}>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="کارشناس بررسی کننده"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="supervisor_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="نحوه پاسخگویی"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="is_good"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="توضیحات"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="description"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Button
type="submit"
disabled={isSubmitting || !isDirty || !isValid}
size="medium"
autoFocus
variant="contained"
>
{isSubmitting ? "درحال ثبت..." : "ثبت"}
</Button>
</Grid>
</Grid>
</StyledForm>
);
};
export default RateMessage;

View File

@@ -0,0 +1,16 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const OperatorDialoguePage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"صدای کارشناسان"} />
<DataTable />
</Stack>
);
};
export default OperatorDialoguePage;

View File

@@ -0,0 +1,167 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_OPERATOR_PEOPLE_MESSAGE_OPERATOR } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import moment from "jalali-moment";
import Player from "./Player";
import CloseIcon from '@mui/icons-material/Close';
import CheckIcon from '@mui/icons-material/Check';
import RateMessage from "./RateMessage";
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,
},
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("yyyy/MM/DD") : <>-</>,
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("HH:mm") : <>-</>,
},
{
accessorKey: "caller_phone_number",
header: "شماره تماس",
id: "caller_phone_number",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "player",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <Player audioUrl={row.original.voice} messageId={row.original.id} />,
},
{
header: "پخش صدا",
id: "is_listen",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => {
const isListen = row.original.is_listen;
const Icon = isListen ? CheckIcon : CloseIcon;
const color = isListen ? "success.main" : "error.main";
return (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Icon sx={{ color }} />
</Box>
);
}
},
{
accessorKey: "listen_at",
header: "زمان شنیده شدن",
id: "listen_at",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "review_duration",
header: "زمان بررسی (دقیقه)",
id: "review_duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "ارزیابی",
id: "rate",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ cell, row, table }) => {
const mutate = table.options.meta?.mutate;
return (
<RateMessage
mutate={mutate}
messageId={row.original.id}
initialValues={{
savaneh_id: row.original.savaneh_id,
action_id: row.original.action_id,
description: row.original.description,
}}
/>
);
},
},
],
[]
);
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_OPERATOR_PEOPLE_MESSAGE_OPERATOR}
page_name={"operatorDialoguePage"}
table_name={"operatorDialogueList"}
positionActionsColumn={"last"}
/>
</Box>
);
};
export default DataTable;

View File

@@ -0,0 +1,45 @@
"use client";
import React from "react";
import { IconButton, Box, Typography } from "@mui/material";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import useWaveSurferPlayer from "@/lib/hooks/useWaveSurferPlayer";
function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return "0:00";
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, "0")}`;
}
const Player = ({ audioUrl }) => {
const { containerRef, isPlaying, playPause, currentTime, duration } =
useWaveSurferPlayer(audioUrl);
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
justifyContent: "center",
}}
>
<div ref={containerRef} style={{ width: 180 }} />
<IconButton aria-label="play-pause" onClick={playPause}>
{isPlaying ? (
<PauseIcon sx={{ color: "#d32f2f" }} />
) : (
<PlayArrowIcon sx={{ color: "#1b4332" }} />
)}
</IconButton>
<Typography variant="body2" sx={{ minWidth: 60, textAlign: "right" }}>
{formatTime(currentTime)} / {formatTime(duration)}
</Typography>
</Box>
);
};
export default Player;

View File

@@ -0,0 +1,159 @@
"use client";
import { Button, FormControl, Grid, InputLabel, MenuItem, Select } from "@mui/material";
import LtrTextField from "@/core/components/LtrTextField";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { object, string } from "yup";
import { GET_OPERATOR_PEOPLE_MESSAGE_OPERATOR } from "@/core/utils/routes";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/hooks/useRequest";
import { useEffect } from "react";
const RateMessage = ({ messageId, initialValues, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const defaultValues = {
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
};
const validationSchema = object().shape({
savaneh_id: string().required("وضعیت کیفیت پیام را مشخص کنید"),
action_id: string().optional(),
description: string().optional(),
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, errors, isDirty, isValid },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => {
const fixData = {
savaneh_id: parseInt(data.savaneh_id),
...(data.action_id !== "" && { action_id: data.action_id }),
...(data.description !== "" && { description: data.description }),
};
try {
await requestServer(`${GET_OPERATOR_PEOPLE_MESSAGE_OPERATOR}/${messageId}`, "post", {
data: fixData,
});
mutate();
reset({
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
});
} catch (error) {}
};
useEffect(() => {
reset({
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
});
}, [initialValues.savaneh_id, initialValues.action_id, initialValues.description]);
return (
<StyledForm id="monitoringForm" onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", height: "100%" }}>
<Grid container spacing={2} sx={{ flexWrap: "nowrap", alignItems: "start" }}>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error} variant="outlined">
<InputLabel id="label-message-quality" size="small" sx={{ fontSize: "12px" }}>
پیگیری
</InputLabel>
<Select
variant="outlined"
labelId="label-message-quality"
id="savaneh_id"
name="savaneh_id"
size="small"
sx={{ width: "100px", fontSize: "12px" }}
value={field.value}
label="پیگیری"
onChange={(e) => field.onChange(e.target.value)}
>
<MenuItem value={1}>عدم نیاز به ثبت (مشکل راه)</MenuItem>
<MenuItem value={2}>عدم نیاز به ثبت (مشکل حمل و نقلی)</MenuItem>
<MenuItem value={3}>عدم نیاز به ثبت (مشکل عوارضی)</MenuItem>
<MenuItem value={4}>ثبت سوانح (مشکل راه)</MenuItem>
<MenuItem value={5}>ثبت سوانح (مشکل حمل و نقلی)</MenuItem>
</Select>
</FormControl>
);
}}
name="savaneh_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="کد واقعه"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="action_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="توضیحات"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="description"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Button
type="submit"
disabled={isSubmitting || !isDirty || !isValid}
size="medium"
autoFocus
variant="contained"
>
{isSubmitting ? "درحال ثبت..." : "ثبت"}
</Button>
</Grid>
</Grid>
</StyledForm>
);
};
export default RateMessage;

View File

@@ -0,0 +1,16 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const PeopleMessagePage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"پیام های مردمی"} />
<DataTable />
</Stack>
);
};
export default PeopleMessagePage;

View File

@@ -0,0 +1,103 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_SYSTEM_MESSAGES_OPERATOR } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import moment from "jalali-moment";
import Player from "./Player";
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: "type_name",
header: "نوع",
id: "type_name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("yyyy/MM/DD") : <>-</>,
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("HH:mm") : <>-</>,
},
{
accessorKey: "duration",
header: "طول تماس ( ثانیه )",
id: "duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "player",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <Player audioUrl={row.original.voice} messageId={row.original.id} />,
},
],
[]
);
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_SYSTEM_MESSAGES_OPERATOR}
page_name={"systemMessagesPage"}
table_name={"systemMessagesList"}
positionActionsColumn={"last"}
/>
</Box>
);
};
export default DataTable;

View File

@@ -0,0 +1,45 @@
"use client";
import React from "react";
import { IconButton, Box, Typography } from "@mui/material";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import useWaveSurferPlayer from "@/lib/hooks/useWaveSurferPlayer";
function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return "0:00";
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, "0")}`;
}
const Player = ({ audioUrl }) => {
const { containerRef, isPlaying, playPause, currentTime, duration } =
useWaveSurferPlayer(audioUrl);
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
justifyContent: "center",
}}
>
<div ref={containerRef} style={{ width: 180 }} />
<IconButton aria-label="play-pause" onClick={playPause}>
{isPlaying ? (
<PauseIcon sx={{ color: "#d32f2f" }} />
) : (
<PlayArrowIcon sx={{ color: "#1b4332" }} />
)}
</IconButton>
<Typography variant="body2" sx={{ minWidth: 60, textAlign: "right" }}>
{formatTime(currentTime)} / {formatTime(duration)}
</Typography>
</Box>
);
};
export default Player;

View File

@@ -0,0 +1,16 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const SystemMessagesPage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"پیام های سیستمی"} />
<DataTable />
</Stack>
);
};
export default SystemMessagesPage;

View File

@@ -0,0 +1,168 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_OPERATOR_DIALOGUE_SUPERVISOR } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import moment from "jalali-moment";
import Player from "./Player";
import RateMessage from "./RateMessage";
import CheckIcon from "@mui/icons-material/Check";
import CloseIcon from "@mui/icons-material/Close";
const DataTable = () => {
const columns = useMemo(
() => [
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.calldate ? moment(row.original.calldate).locale("fa").format("yyyy/MM/DD") : <>-</>,
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.calldate ? moment(row.original.calldate).locale("fa").format("HH:mm") : <>-</>,
},
{
accessorKey: "src",
header: "شماره تماس گیرنده",
id: "src",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "dst",
header: "کارشناس",
id: "dst",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "is_listen",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => {
const isListen = row.original.is_listen;
const Icon = isListen ? CheckIcon : CloseIcon;
const color = isListen ? "success.main" : "error.main";
return (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Icon sx={{ color }} />
</Box>
);
}
},
{
accessorKey: "disposition",
header: "وضعیت",
id: "disposition",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "duration",
header: "طول تماس ( ثانیه )",
id: "duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "player",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <Player audioUrl={row.original.recordingfile} messageId={row.original.id} />,
},
{
header: "ارزیابی",
id: "rate",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ cell, row, table }) => {
const mutate = table.options.meta?.mutate;
return (
<RateMessage
mutate={mutate}
date={row.original.calldate}
messageId={row.original.uniqueid}
initialValues={{
supervisor_id: row.original.supervisor_id,
is_good: row.original.is_good,
description: row.original.description,
}}
/>
);
},
},
],
[]
);
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_OPERATOR_DIALOGUE_SUPERVISOR}
page_name={"operatorDialoguePage"}
table_name={"operatorDialogueList"}
positionActionsColumn={"last"}
/>
</Box>
);
};
export default DataTable;

View File

@@ -0,0 +1,22 @@
"use client";
import React from "react";
import { IconButton, Box } from "@mui/material";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import useWaveSurferPlayer from "@/lib/hooks/useWaveSurferPlayer";
const Player = ({ audioUrl }) => {
const { containerRef, isPlaying, playPause } = useWaveSurferPlayer(audioUrl);
return (
<Box sx={{ display: "flex", alignItems: "center", gap: 1, justifyContent: "center" }}>
<div ref={containerRef} style={{ width: 180 }} />
<IconButton aria-label="play-pause" onClick={playPause}>
{isPlaying ? <PauseIcon sx={{ color: "#d32f2f" }} /> : <PlayArrowIcon sx={{ color: "#1b4332" }} />}
</IconButton>
</Box>
);
};
export default Player;

View File

@@ -0,0 +1,158 @@
"use client";
import { Button, FormControl, Grid, InputLabel, MenuItem, Select } from "@mui/material";
import LtrTextField from "@/core/components/LtrTextField";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { object, string } from "yup";
import { GET_OPERATOR_DIALOGUE_SUPERVISOR } from "@/core/utils/routes";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/hooks/useRequest";
import { useEffect } from "react";
const RateMessage = ({ date, messageId, initialValues, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const defaultValues = {
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
};
const validationSchema = object().shape({
supervisor_id: string().required("کارشناس بررسی کننده را مشخص کنید"),
is_good: string().required("نحوه پاسخگویی را مشخص کنید"),
description: string().optional(),
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, errors, isDirty, isValid },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => {
const fixData = {
date: date,
cdr_unique_id: messageId,
supervisor_id: data.supervisor_id,
is_good: parseInt(data.is_good),
...(data.description !== "" && { description: data.description }),
};
try {
await requestServer(`${GET_OPERATOR_DIALOGUE_SUPERVISOR}/rate`, "post", {
data: fixData,
});
mutate();
reset({
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
});
} catch (error) {}
};
useEffect(() => {
reset({
supervisor_id: initialValues?.supervisor_id ?? "",
is_good: initialValues?.is_good ?? "",
description: initialValues?.description ?? "",
});
}, [initialValues.supervisor_id, initialValues.is_good, initialValues.description]);
return (
<StyledForm id="monitoringForm" onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", height: "100%" }}>
<Grid container spacing={2} sx={{ flexWrap: "nowrap", alignItems: "start" }}>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="کارشناس بررسی کننده"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="supervisor_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error} variant="outlined">
<InputLabel id="label-message-quality" size="small" sx={{ fontSize: "12px" }}>
نحوه پاسخگویی
</InputLabel>
<Select
variant="outlined"
labelId="label-message-quality"
id="is_good"
name="is_good"
size="small"
sx={{ width: "100px", fontSize: "12px" }}
value={field.value}
label="نحوه پاسخگویی"
onChange={(e) => field.onChange(e.target.value)}
>
<MenuItem value={0}>نامناسب</MenuItem>
<MenuItem value={1}>مناسب</MenuItem>
</Select>
</FormControl>
);
}}
name="is_good"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="توضیحات"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="description"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Button
type="submit"
disabled={isSubmitting || !isDirty || !isValid}
size="medium"
autoFocus
variant="contained"
>
{isSubmitting ? "درحال ثبت..." : "ثبت"}
</Button>
</Grid>
</Grid>
</StyledForm>
);
};
export default RateMessage;

View File

@@ -0,0 +1,16 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const OperatorDialoguePage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"ارزیابی صدای کارشناسان"} />
<DataTable />
</Stack>
);
};
export default OperatorDialoguePage;

View File

@@ -0,0 +1,167 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_OPERATOR_PEOPLE_MESSAGE_SUPERVISOR } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import moment from "jalali-moment";
import Player from "./Player";
import CloseIcon from '@mui/icons-material/Close';
import CheckIcon from '@mui/icons-material/Check';
import RateMessage from "./RateMessage";
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,
},
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("yyyy/MM/DD") : <>-</>,
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("HH:mm") : <>-</>,
},
{
accessorKey: "caller_phone_number",
header: "شماره تماس",
id: "caller_phone_number",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "player",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <Player audioUrl={row.original.voice} messageId={row.original.id} />,
},
{
header: "پخش صدا",
id: "is_listen",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => {
const isListen = row.original.is_listen;
const Icon = isListen ? CheckIcon : CloseIcon;
const color = isListen ? "success.main" : "error.main";
return (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Icon sx={{ color }} />
</Box>
);
}
},
{
accessorKey: "listen_at",
header: "زمان شنیده شدن",
id: "listen_at",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "review_duration",
header: "زمان بررسی (دقیقه)",
id: "review_duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "ارزیابی",
id: "rate",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ cell, row, table }) => {
const mutate = table.options.meta?.mutate;
return (
<RateMessage
mutate={mutate}
messageId={row.original.id}
initialValues={{
savaneh_id: row.original.savaneh_id,
action_id: row.original.action_id,
description: row.original.description,
}}
/>
);
},
},
],
[]
);
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_OPERATOR_PEOPLE_MESSAGE_SUPERVISOR}
page_name={"operatorDialoguePage"}
table_name={"operatorDialogueList"}
positionActionsColumn={"last"}
/>
</Box>
);
};
export default DataTable;

View File

@@ -0,0 +1,45 @@
"use client";
import React from "react";
import { IconButton, Box, Typography } from "@mui/material";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import useWaveSurferPlayer from "@/lib/hooks/useWaveSurferPlayer";
function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return "0:00";
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, "0")}`;
}
const Player = ({ audioUrl }) => {
const { containerRef, isPlaying, playPause, currentTime, duration } =
useWaveSurferPlayer(audioUrl);
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
justifyContent: "center",
}}
>
<div ref={containerRef} style={{ width: 180 }} />
<IconButton aria-label="play-pause" onClick={playPause}>
{isPlaying ? (
<PauseIcon sx={{ color: "#d32f2f" }} />
) : (
<PlayArrowIcon sx={{ color: "#1b4332" }} />
)}
</IconButton>
<Typography variant="body2" sx={{ minWidth: 60, textAlign: "right" }}>
{formatTime(currentTime)} / {formatTime(duration)}
</Typography>
</Box>
);
};
export default Player;

View File

@@ -0,0 +1,160 @@
"use client";
import { Button, FormControl, FormHelperText, Grid, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material";
import LtrTextField from "@/core/components/LtrTextField";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { object, string, number } from "yup";
import { GET_SYSTEM_MESSAGES_SUPERVISOR } from "@/core/utils/routes";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/hooks/useRequest";
import usePermissionsList from "@/lib/hooks/usePermissionsList";
import { useEffect } from "react";
const RateMessage = ({ messageId, initialValues, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const defaultValues = {
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
};
const validationSchema = object().shape({
savaneh_id: string().required("وضعیت کیفیت پیام را مشخص کنید"),
action_id: string().optional(),
description: string().optional(),
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, errors, isDirty, isValid },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => {
const fixData = {
savaneh_id: parseInt(data.savaneh_id),
...(data.action_id !== "" && { action_id: data.action_id }),
...(data.description !== "" && { description: data.description }),
};
try {
await requestServer(`${GET_SYSTEM_MESSAGES_SUPERVISOR}/${messageId}`, "post", {
data: fixData,
});
mutate();
reset({
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
});
} catch (error) {}
};
useEffect(() => {
reset({
savaneh_id: initialValues?.savaneh_id ?? "",
action_id: initialValues?.action_id ?? "",
description: initialValues?.description ?? "",
});
}, [initialValues.savaneh_id, initialValues.action_id, initialValues.description]);
return (
<StyledForm id="monitoringForm" onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", height: "100%" }}>
<Grid container spacing={2} sx={{ flexWrap: "nowrap", alignItems: "start" }}>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error} variant="outlined">
<InputLabel id="label-message-quality" size="small" sx={{ fontSize: "12px" }}>
پیگیری
</InputLabel>
<Select
variant="outlined"
labelId="label-message-quality"
id="savaneh_id"
name="savaneh_id"
size="small"
sx={{ width: "100px", fontSize: "12px" }}
value={field.value}
label="پیگیری"
onChange={(e) => field.onChange(e.target.value)}
>
<MenuItem value={1}>عدم نیاز به ثبت (مشکل راه)</MenuItem>
<MenuItem value={2}>عدم نیاز به ثبت (مشکل حمل و نقلی)</MenuItem>
<MenuItem value={3}>عدم نیاز به ثبت (مشکل عوارضی)</MenuItem>
<MenuItem value={4}>ثبت سوانح (مشکل راه)</MenuItem>
<MenuItem value={5}>ثبت سوانح (مشکل حمل و نقلی)</MenuItem>
</Select>
</FormControl>
);
}}
name="savaneh_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="کد واقعه"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="action_id"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="توضیحات"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="description"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Button
type="submit"
disabled={isSubmitting || !isDirty || !isValid}
size="medium"
autoFocus
variant="contained"
>
{isSubmitting ? "درحال ثبت..." : "ثبت"}
</Button>
</Grid>
</Grid>
</StyledForm>
);
};
export default RateMessage;

View File

@@ -0,0 +1,16 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const PeopleMessagePage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"ارزیابی پیام های مردمی"} />
<DataTable />
</Stack>
);
};
export default PeopleMessagePage;

View File

@@ -0,0 +1,131 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_SYSTEM_MESSAGES_SUPERVISOR } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
import moment from "jalali-moment";
import Player from "./Player";
import RateMessage from "./RateMessage";
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: "type_name",
header: "نوع",
id: "type_name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("yyyy/MM/DD") : <>-</>,
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) =>
row.original.date ? moment(row.original.date).locale("fa").format("HH:mm") : <>-</>,
},
{
accessorKey: "duration",
header: "طول تماس ( ثانیه )",
id: "duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
header: "پخش صدا",
id: "player",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <Player audioUrl={row.original.voice} messageId={row.original.id} />,
},
{
header: "ارزیابی",
id: "rate",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ cell, row, table }) => {
const mutate = table.options.meta?.mutate;
return (
<RateMessage
mutate={mutate}
messageId={row.original.id}
initialValues={{
message_quality: row.original.message_quality,
format_quality: row.original.format_quality,
description: row.original.description,
}}
/>
);
},
},
],
[]
);
return (
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_SYSTEM_MESSAGES_SUPERVISOR}
page_name={"systemMessagesPage"}
table_name={"systemMessagesList"}
positionActionsColumn={"last"}
/>
</Box>
);
};
export default DataTable;

View File

@@ -0,0 +1,45 @@
"use client";
import React from "react";
import { IconButton, Box, Typography } from "@mui/material";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import useWaveSurferPlayer from "@/lib/hooks/useWaveSurferPlayer";
function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return "0:00";
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs.toString().padStart(2, "0")}`;
}
const Player = ({ audioUrl }) => {
const { containerRef, isPlaying, playPause, currentTime, duration } =
useWaveSurferPlayer(audioUrl);
return (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
justifyContent: "center",
}}
>
<div ref={containerRef} style={{ width: 180 }} />
<IconButton aria-label="play-pause" onClick={playPause}>
{isPlaying ? (
<PauseIcon sx={{ color: "#d32f2f" }} />
) : (
<PlayArrowIcon sx={{ color: "#1b4332" }} />
)}
</IconButton>
<Typography variant="body2" sx={{ minWidth: 60, textAlign: "right" }}>
{formatTime(currentTime)} / {formatTime(duration)}
</Typography>
</Box>
);
};
export default Player;

View File

@@ -0,0 +1,163 @@
"use client";
import { Button, FormControl, FormHelperText, Grid, InputLabel, MenuItem, OutlinedInput, Select } from "@mui/material";
import LtrTextField from "@/core/components/LtrTextField";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { object, string, number } from "yup";
import { GET_SYSTEM_MESSAGES_SUPERVISOR } from "@/core/utils/routes";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/hooks/useRequest";
import usePermissionsList from "@/lib/hooks/usePermissionsList";
import { useEffect } from "react";
const RateMessage = ({ messageId, initialValues, mutate }) => {
const requestServer = useRequest({ notificationSuccess: true });
const defaultValues = {
message_quality: initialValues?.message_quality ?? "",
format_quality: initialValues?.format_quality ?? "",
description: initialValues?.description ?? "",
};
const validationSchema = object().shape({
message_quality: string().required("وضعیت کیفیت پیام را مشخص کنید"),
format_quality: string().required("وضعیت فرمت پیام را مشخص کنید"),
description: string().optional(),
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting, errors, isDirty, isValid },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
const onSubmit = async (data) => {
const fixData = {
message_quality: parseInt(data.message_quality),
format_quality: parseInt(data.format_quality),
...(data.description !== "" && { description: data.description }),
};
try {
await requestServer(`${GET_SYSTEM_MESSAGES_SUPERVISOR}/${messageId}`, "post", {
data: fixData,
});
mutate();
reset({
message_quality: initialValues?.message_quality ?? "",
format_quality: initialValues?.format_quality ?? "",
description: initialValues?.description ?? "",
});
} catch (error) {}
};
useEffect(() => {
reset({
message_quality: initialValues?.message_quality ?? "",
format_quality: initialValues?.format_quality ?? "",
description: initialValues?.description ?? "",
});
}, [initialValues.message_quality, initialValues.format_quality, initialValues.description]);
return (
<StyledForm id="monitoringForm" onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", height: "100%" }}>
<Grid container spacing={2} sx={{ flexWrap: "nowrap", alignItems: "start" }}>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error} variant="outlined">
<InputLabel id="label-message-quality" size="small" sx={{ fontSize: "12px" }}>
کیفیت پیام
</InputLabel>
<Select
variant="outlined"
labelId="label-message-quality"
id="message_quality"
name="message_quality"
size="small"
sx={{ width: "100px", fontSize: "12px" }}
value={field.value}
label="کیفیت پیام"
onChange={(e) => field.onChange(e.target.value)}
>
<MenuItem value={0}>نامناسب</MenuItem>
<MenuItem value={1}>مناسب</MenuItem>
</Select>
</FormControl>
);
}}
name="message_quality"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<FormControl fullWidth error={!!error} variant="outlined">
<InputLabel id="label-format-quality" size="small" sx={{ fontSize: "12px" }}>
فرمت پیام
</InputLabel>
<Select
variant="outlined"
labelId="label-format-quality"
id="format_quality"
name="format_quality"
size="small"
sx={{ width: "100px", fontSize: "12px" }}
value={field.value}
label="فرمت پیام"
onChange={(e) => field.onChange(e.target.value)}
>
<MenuItem value={0}>نامناسب</MenuItem>
<MenuItem value={1}>مناسب</MenuItem>
</Select>
</FormControl>
);
}}
name="format_quality"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
variant="outlined"
label="توضیحات"
size="small"
sx={{
width: "180px",
}}
InputProps={{ sx: { fontSize: "12px" } }}
InputLabelProps={{ sx: { fontSize: "12px" } }}
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name="description"
/>
</Grid>
<Grid item sx={{ xs: 3, sm: 3 }}>
<Button
type="submit"
disabled={isSubmitting || !isDirty || !isValid}
size="medium"
autoFocus
variant="contained"
>
{isSubmitting ? "درحال ثبت..." : "ثبت"}
</Button>
</Grid>
</Grid>
</StyledForm>
);
};
export default RateMessage;

View File

@@ -0,0 +1,16 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
const SystemMessagesPage = () => {
return (
<Stack spacing={1}>
<PageTitle title={"ارزیابی پیام های سیستمی"} />
<DataTable />
</Stack>
);
};
export default SystemMessagesPage;

View File

@@ -0,0 +1,200 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_PROVINCE_CUMULATIVE_CALLS_DAILY_REPORT } from "@/core/utils/routes";
import { Box } from "@mui/material";
import moment from "jalali-moment";
import { useMemo } from "react";
export default function DataTable() {
const columns = useMemo(() => [
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => (row.original.d ? moment(row.original.d).locale("fa").format("yyyy/MM/DD") : <>-</>),
},
{
accessorKey: "total",
header: "تمامی تماس ها",
id: "total",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
header: "تماس با تهران",
id: "call_to_tehran",
enableColumnFilter: false,
enableSorting: false,
grow: false,
size: 50,
columns: [
{
accessorKey: "teh4",
header: "کلید 4",
id: "teh4",
enableColumnFilter: true,
datatype: "integer",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "teh5",
header: "کلید 5",
id: "teh5",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
],
},
{
header: "تماس با اپراتور استانی",
id: "call_to_ostan",
enableColumnFilter: false,
enableSorting: false,
grow: false,
size: 50,
columns: [
{
accessorKey: "ok",
header: "موفق",
id: "ok",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "fail",
header: "جواب داده نشده",
id: "fail",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "nok",
header: "نا موفق",
id: "nok",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "os4",
header: "کلید 4",
id: "os4",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "os5",
header: "کلید 5",
id: "os5",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "dnd",
header: "dnd",
id: "dnd",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "trans",
header: "انتقال تماس",
id: "trans",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
],
},
{
accessorKey: "b10",
header: "تماس بالای 10 ثانیه",
id: "b10",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "avg",
header: "میانگین تماس ها",
id: "avg",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <>{row.original.avg ? row.original.avg : <>-</>}</>,
},
]);
return (
<>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_PROVINCE_CUMULATIVE_CALLS_DAILY_REPORT}
page_name={"provinceCumulativeCallsReportPage"}
table_name={"provinceCumulativeCallsReportList"}
positionActionsColumn={"last"}
/>
</Box>
</>
);
}

View File

@@ -0,0 +1,14 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
export default function ProvinceCumulativeCallsReportPage() {
return (
<Stack spacing={1}>
<PageTitle title={"گزارشات تجمعی"} />
<DataTable />
</Stack>
);
}

View File

@@ -0,0 +1,103 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_PROVINCE_DETAILED_LOGS_REPORT } from "@/core/utils/routes";
import { Box } from "@mui/material";
import moment from "jalali-moment";
import { useMemo } from "react";
export default function DataTable() {
const columns = useMemo(() => [
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => {
const dateTime = row.original.calldate;
if (!dateTime) return <>-</>;
const date = dateTime.split(" ")[0];
const persianDate = moment(date).locale("fa").format("YYYY-MM-DD") || "";
return <>{persianDate}</>;
},
},
{
header: "ساعت",
id: "time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => {
const dateTime = row.original.calldate;
if (!dateTime) return <>-</>;
const time = dateTime.split(" ")[1] || "";
return <>{time}</>;
},
},
{
accessorKey: "src",
header: "شماره تماس گیرنده",
id: "src",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "dst",
header: "مقصد تماس",
id: "dst",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "disposition",
header: "وضعیت پاسخگویی",
id: "disposition",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "duration",
header: "مدت زمان تماس",
id: "duration",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
]);
return (
<>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_PROVINCE_DETAILED_LOGS_REPORT}
page_name={"provinceDetailedLogsReportPage"}
table_name={"provinceDetailedLogsReportList"}
positionActionsColumn={"last"}
/>
</Box>
</>
);
}

View File

@@ -0,0 +1,14 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
export default function ProvinceDetailedLogsReportPage() {
return (
<Stack spacing={1}>
<PageTitle title={"ریز گزارش ها"} />
<DataTable />
</Stack>
);
}

View File

@@ -0,0 +1,101 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_PROVINCE_OPERATORS_PERFORMANCE_TOTAL_REPORT } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
export default function DataTableCumulative() {
const columns = useMemo(() => [
{
accessorKey: "dst",
header: "کد یکتای کارشناس",
id: "dst",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "total",
header: "تعداد کل تماس ها",
id: "total",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "ok",
header: "تماس های موفق",
id: "ok",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "fail",
header: "تماس های جواب داده نشده",
id: "fail",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "nok",
header: "تماس های ناموفق",
id: "nok",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "b10",
header: "تماس های بالای 10 ثانیه",
id: "b10",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "dnd",
header: "dnd",
id: "dnd",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
]);
return (
<>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_PROVINCE_OPERATORS_PERFORMANCE_TOTAL_REPORT}
page_name={"provinceOperatorPerformanceCumulativeReportPage"}
table_name={"provinceOperatorPerformanceCumulativeReportList"}
positionActionsColumn={"last"}
/>
</Box>
</>
);
}

View File

@@ -0,0 +1,113 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_PROVINCE_OPERATORS_PERFORMANCE_DAILY_REPORT } from "@/core/utils/routes";
import { Box } from "@mui/material";
import moment from "jalali-moment";
import { useMemo } from "react";
export default function DataTableDaily() {
const columns = useMemo(() => [
{
header: "تاریخ",
id: "date",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => (row.original.d ? moment(row.original.d).locale("fa").format("yyyy/MM/DD") : <>-</>),
},
{
accessorKey: "dst",
header: "کد یکتای کارشناس",
id: "dst",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "total",
header: "تعداد کل تماس ها",
id: "total",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "ok",
header: "تماس های موفق",
id: "ok",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "fail",
header: "تماس های جواب داده نشده",
id: "fail",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "nok",
header: "تماس های ناموفق",
id: "nok",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "b10",
header: "تماس های بالای 10 ثانیه",
id: "b10",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
accessorKey: "dnd",
header: "dnd",
id: "dnd",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
]);
return (
<>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_PROVINCE_OPERATORS_PERFORMANCE_DAILY_REPORT}
page_name={"provinceOperatorPerformanceDailyReportPage"}
table_name={"provinceOperatorPerformanceDailyReportList"}
positionActionsColumn={"last"}
/>
</Box>
</>
);
}

View File

@@ -0,0 +1,17 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTableCumulative from "./DataTableCumulative";
import DataTableDaily from "./DataTableDaily";
export default function ProvinceOperatorPerformanceReportPage() {
return (
<Stack spacing={1}>
<PageTitle title={"گزارش تجمعی عملکرد کارشناسان"} />
<DataTableCumulative />
<PageTitle title={"گزارش روزانه عملکرد کارشناسان"} />
<DataTableDaily />
</Stack>
);
}

View File

@@ -0,0 +1,59 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_PROVINCE_PEOPLE_MESSAGES_REPORT } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
export default function DataTable() {
const columns = useMemo(() => [
{
header: "تعداد",
id: "count",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
Cell: ({ row }) => <>{row.original.nl + row.original.l}</>,
},
{
accessorKey: "l",
header: "تماس های شنیده شده",
id: "l",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "nl",
header: "تماس های شنیده نشده",
id: "nl",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
]);
return (
<>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_PROVINCE_PEOPLE_MESSAGES_REPORT}
page_name={"provincePeopleMessagesReportPage"}
table_name={"provincePeopleMessagesReportList"}
positionActionsColumn={"last"}
/>
</Box>
</>
);
}

View File

@@ -0,0 +1,14 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { Stack } from "@mui/material";
import DataTable from "./DataTable";
export default function ProvincePeopleMessagesReportPage() {
return (
<Stack spacing={1}>
<PageTitle title={"گزارش وضعیت پیام های مردمی"} />
<DataTable />
</Stack>
);
}

View File

@@ -0,0 +1,205 @@
"use client";
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_COUNTRY_CALLS_REPORT } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
const DataTable = () => {
const columns = useMemo(
() => [
{
accessorKey: "province_name",
header: "استان",
id: "province_id",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "total",
header: "تمامی تماس ها",
id: "total",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
grow: false,
size: 100,
},
{
header: "تماس با تهران",
id: "call_to_tehran",
enableColumnFilter: false,
enableSorting: false,
grow: false,
size: 50,
columns: [
{
accessorKey: "route_to_teh4",
header: "کلید 4",
id: "route_to_teh4",
enableColumnFilter: true,
datatype: "integer",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "route_to_teh5",
header: "کلید 5",
id: "route_to_teh5",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
],
},
{
header: "تماس با اپراتور استانی",
id: "call_to_ostan",
enableColumnFilter: false,
enableSorting: false,
grow: false,
size: 50,
columns: [
{
accessorKey: "answered",
header: "موفق",
id: "answered",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "missed",
header: "جواب داده نشده",
id: "missed",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "fails",
header: "نا موفق",
id: "fails",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "route_to_os4",
header: "کلید 4",
id: "route_to_os4",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "route_to_os5",
header: "کلید 5",
id: "route_to_os5",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "dnd",
header: "dnd",
id: "dnd",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "transferred",
header: "انتقال تماس",
id: "transferred",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
],
},
{
accessorKey: "answered_over10",
header: "تماس بالای 10 ثانیه",
id: "answered_over10",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "average_calls_time",
header: "میانگین تماس ها",
id: "average_calls_time",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
sortDescFirst: false,
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
],
[]
);
return (
<>
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
<DataTableWithAuth
need_filter={true}
columns={columns}
table_url={GET_COUNTRY_CALLS_REPORT}
page_name={"countryCallsReportPage"}
table_name={"countryCallsReportList"}
positionActionsColumn={"last"}
/>
</Box>
</>
);
};
export default DataTable;

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