CFE-4 Merging from develop

This commit is contained in:
2023-10-10 10:01:18 +03:30
54 changed files with 884 additions and 211 deletions

2
.gitignore vendored
View File

@@ -27,7 +27,7 @@ node_modules
cypress/videos
cypress/screenshots
.next
.env.local
.env*
.env
.idea
package-lock.json

View File

@@ -7,6 +7,7 @@ NEXT_PUBLIC_PRIMARY_MAIN = "#1b4332"
NEXT_PUBLIC_SECONDARY_MAIN = "#800e13"
NEXT_PUBLIC_BASE_URL = "https://crm.witel.ir"
NEXT_PUBLIC_SERVER_SOCKET_URL = "wss://crmws.witel.ir"
NEXT_PUBLIC_POWERED_BY_URL = "https://witel.ir"
NODE_ENV = "development"

View File

@@ -2,8 +2,6 @@ import '@testing-library/jest-dom'
import {server} from "./mocks/server";
import mockRouter from 'next-router-mock'
require('dotenv').config({path: './.env.local'});
jest.mock('next/router', () => jest.requireActual('next-router-mock'))
beforeAll(() => {

View File

@@ -1,12 +1,19 @@
import {GET_PERMISSIONS_LIST, GET_USER_ROUTE} from "@/core/data/apiRoutes";
import {GET_PERMISSIONS_LIST, GET_USER_ROUTE,SET_USER_PASSWORD} from "@/core/data/apiRoutes";
import {rest} from "msw";
export const handler = [
rest.get(GET_USER_ROUTE, (req, res, ctx) => {
return res(ctx.json({
id: 10
data:{
id: 10
}
}))
}),
rest.post(SET_USER_PASSWORD, (req, res, ctx) => {
return res(
ctx.status(200),
);
}),
rest.get(GET_PERMISSIONS_LIST, (req, res, ctx) => {
return res(ctx.json(
{

View File

@@ -46,6 +46,7 @@
"react-dom": "^18.2.0",
"react-toastify": "^9.1.3",
"sass": "^1.62.0",
"socket.io-client": "^4.7.2",
"stylis": "^4.1.3",
"stylis-plugin-rtl": "^2.1.1",
"swr": "^2.1.5",

View File

@@ -15,6 +15,8 @@
"between": "میان",
"online_message": "شما به اینترنت وصل هستید",
"offline_message": "اتصال شما به اینترنت قطع شده است",
"socket_is_connect_message": "شما به سوکت وصل هستید",
"socket_is_not_connect_message": "اتصال شما به سوکت قطع شده است",
"header": {
"open_profile": "پروفایل",
"edit_profile": " پروفایل",
@@ -81,7 +83,9 @@
"label_current_password": "رمز عبور فعلی",
"label_new_password": "رمز عبور جدید",
"label_confirm_password": "تکرار رمز عبور جدید",
"error_message_required": "اجباری !",
"error_message_current_password_required": "وارد کردن رمز عبور فعلی الزامیست!",
"error_message_new_password_required": "وارد کردن رمز عبور جدید الزامیست!",
"error_message_confirm_password_required": "وارد کردن تکرار رمز عبور جدید الزامیست!",
"error_message_password_length": "رمز عبور باید حداقل 8 کاراکتر باشد.",
"error_message_password_match": "پسورد ها یکسان هستند",
"error_message_password_not_match": "پسورد و تکرار رمز عبور باید یکسان باشد"

View File

@@ -0,0 +1,305 @@
import { render, screen, waitFor, fireEvent , act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MockAppWithProviders from "../../../../../../mocks/AppWithProvider";
import ChangePasswordForm from "@/components/dashboard/change-password/change-password-form";
import {SET_USER_PASSWORD} from "@/core/data/apiRoutes";
import {server} from "../../../../../../mocks/server";
import {rest} from "msw";
describe('Change Password Form Component From Change Password Page', () => {
describe('Rendering', () => {
it('Should see change password heading',() => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const changePasswordElement = screen.getByRole('heading', { level: 4 });
expect(changePasswordElement).toHaveTextContent('تغییر رمز عبور');
});
it('Should see the label for current password field', () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const currentPasswordLabel = screen.queryByLabelText('رمز عبور فعلی');
expect(currentPasswordLabel).toBeInTheDocument();
});
it('Should see the label for new password field', () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const newPasswordLabel = screen.queryByLabelText('رمز عبور جدید');
expect(newPasswordLabel).toBeInTheDocument();
});
it('Should see the label for confirm password field', () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const confirmPasswordLabel = screen.queryByLabelText('تکرار رمز عبور جدید');
expect(confirmPasswordLabel).toBeInTheDocument();
});
it('Should see the submit button with the submit label and should be disable', () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const submitButton = screen.getByRole('button', { name: 'ثبت' });
expect(submitButton).toBeInTheDocument();
expect(submitButton).toBeDisabled();
});
it('Should have empty input fields and not see any validation error for current password, new password, and confirm password', () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
expect(currentPasswordInput).toHaveValue('');
expect(newPasswordInput).toHaveValue('');
expect(confirmPasswordInput).toHaveValue('');
expect(screen.queryByText("وارد کردن رمز عبور فعلی الزامیست!")).not.toBeInTheDocument();
expect(screen.queryByText("وارد کردن رمز عبور جدید الزامیست!")).not.toBeInTheDocument();
expect(screen.queryByText("وارد کردن تکرار رمز عبور جدید الزامیست!")).not.toBeInTheDocument();
});
});
describe("Behavior", ()=>{
it('Should fill the current password field and not see any error while its not empty', async () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
// Simulate typing something in the current password input
fireEvent.change(currentPasswordInput, { target: { value: 'witel@fani0' } });
await waitFor(() => {
expect(screen.queryByText("وارد کردن رمز عبور فعلی الزامیست!")).not.toBeInTheDocument();
expect(currentPasswordInput).toHaveValue('witel@fani0');
});
});
it('Should fill the new password field and not see any error while its not empty', async () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
// Simulate typing something in the new password input
fireEvent.change(newPasswordInput, { target: { value: 'witel@fani1' } });
await waitFor(() => {
expect(screen.queryByText("وارد کردن رمز عبور جدید الزامیست!")).not.toBeInTheDocument();
expect(newPasswordInput).toHaveValue('witel@fani1');
});
});
it('Should fill the confirm new password field and not see any error while its not empty', async () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
// Simulate typing something in the confirmation new password input
fireEvent.change(confirmPasswordInput, { target: { value: 'witel@fani1' } });
await waitFor(() => {
expect(screen.queryByText("وارد کردن تکرار رمز عبور جدید الزامیست!")).not.toBeInTheDocument();
expect(confirmPasswordInput).toHaveValue('witel@fani1');
});
});
});
describe("Validation", ()=>{
it('Should see error while current password is empty on blur event', async () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
// Simulate blur event on the current password input
const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
// Clear the input field
fireEvent.change(currentPasswordInput, { target: { value: '' } });
fireEvent.blur(currentPasswordInput);
await waitFor(()=>{
expect(screen.queryByText("وارد کردن رمز عبور فعلی الزامیست!")).toBeInTheDocument()
});
});
it('Should see error while new password is empty on blur event', async () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
// Simulate blur event on the new password input
const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
// Clear the input field
fireEvent.change(newPasswordInput, { target: { value: '' } });
fireEvent.blur(newPasswordInput);
await waitFor(()=>{
expect(screen.queryByText("وارد کردن رمز عبور جدید الزامیست!")).toBeInTheDocument()
});
});
it('Should see error while confirm new password is empty on blur event', async () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
// Simulate blur event on the new password input
const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
// Clear the input field
fireEvent.change(confirmPasswordInput, { target: { value: '' } });
fireEvent.blur(confirmPasswordInput);
await waitFor(()=>{
expect(screen.queryByText("وارد کردن تکرار رمز عبور جدید الزامیست!")).toBeInTheDocument()
});
});
it('Should see error when new password is less than 8 characters', async () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
const submitButton = screen.getByRole('button', { name: 'ثبت' });
// Simulate entering a short new password
fireEvent.change(newPasswordInput, { target: { value: 'test' } });
fireEvent.blur(newPasswordInput);
await waitFor(() => {
expect(screen.queryByText("رمز عبور باید حداقل 8 کاراکتر باشد.")).toBeInTheDocument();
});
// Simulate clicking the submit button
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.queryByText("رمز عبور باید حداقل 8 کاراکتر باشد.")).toBeInTheDocument();
});
});
it('Should not see error when new password are 8 or more characters', async () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
const submitButton = screen.getByRole('button', { name: 'ثبت' });
// Simulate entering a valid new password
fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
fireEvent.blur(newPasswordInput);
await waitFor(() => {
expect(screen.queryByText("رمز عبور باید حداقل 8 کاراکتر باشد.")).not.toBeInTheDocument();
});
// Simulate clicking the submit button
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.queryByText("رمز عبور باید حداقل 8 کاراکتر باشد.")).not.toBeInTheDocument();
});
});
it('Should see error when new password and confirm password do not match on blur event and submit click', async () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
const submitButton = screen.getByRole('button', { name: 'ثبت' });
// Simulate entering a valid new password
fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
fireEvent.blur(newPasswordInput);
// Simulate entering a different value in the confirm password field
fireEvent.change(confirmPasswordInput, { target: { value: 'MismatchedPassword' } });
fireEvent.blur(confirmPasswordInput);
await waitFor(() => {
expect(screen.queryByText("پسورد و تکرار رمز عبور باید یکسان باشد")).toBeInTheDocument();
});
// Simulate clicking the submit button
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.queryByText("پسورد و تکرار رمز عبور باید یکسان باشد")).toBeInTheDocument();
});
});
it('Should not see error when new password and confirm password match on blur event and submit click', async () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
const submitButton = screen.getByRole('button', { name: 'ثبت' });
// Simulate entering a valid new password
fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
fireEvent.blur(newPasswordInput);
// Simulate entering the same value in the confirmation password field
fireEvent.change(confirmPasswordInput, { target: { value: 'Witel@fani1' } });
fireEvent.blur(confirmPasswordInput);
await waitFor(() => {
expect(screen.queryByText("پسورد و تکرار رمز عبور باید یکسان باشد")).not.toBeInTheDocument();
});
// Simulate clicking the submit button
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.queryByText("پسورد و تکرار رمز عبور باید یکسان باشد")).not.toBeInTheDocument();
});
});
});
describe("Form Submission", () => {
it('Should enable the submit button when the inputs are valid', async () => {
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
const submitButton = screen.getByRole('button', { name: 'ثبت' });
// Simulate entering valid values in the form fields
const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
fireEvent.change(currentPasswordInput, { target: { value: 'Witel@fani0' } });
fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'Witel@fani1' } });
// Check if the submit button is enabled
await waitFor(() => {
expect(submitButton).not.toBeDisabled();
});
});
it('Should request to api and resetform in success response', async () => {
// Render the component
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
// Fill in the form fields
const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
fireEvent.change(currentPasswordInput, { target: { value: 'Witel@fani0' } });
fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'Witel@fani1' } });
// Submit the form
const submitButton = screen.getByRole('button', { name: 'ثبت' });
await userEvent.click(submitButton);
await waitFor(() => {
expect(currentPasswordInput).toHaveValue('');
expect(newPasswordInput).toHaveValue('');
expect(confirmPasswordInput).toHaveValue('');
});
});
it('Should request to api and not resetform in error response', async () => {
// Render the component
render(<MockAppWithProviders><ChangePasswordForm /></MockAppWithProviders>);
server.use([rest.get(SET_USER_PASSWORD, (req, res, ctx) => {
return res(ctx.status(422))
})])
// Fill in the form fields wrong
const currentPasswordInput = screen.queryByLabelText('رمز عبور فعلی');
const newPasswordInput = screen.queryByLabelText('رمز عبور جدید');
const confirmPasswordInput = screen.queryByLabelText('تکرار رمز عبور جدید');
fireEvent.change(currentPasswordInput, { target: { value: 'incorrect' } });
fireEvent.change(newPasswordInput, { target: { value: 'Witel@fani1' } });
fireEvent.change(confirmPasswordInput, { target: { value: 'Witel@fani1' } });
// Submit the form
const submitButton = screen.getByRole('button', { name: 'ثبت' });
await userEvent.click(submitButton);
await waitFor(() => {
expect(currentPasswordInput.value).toBe('incorrect');
expect(newPasswordInput.value).toBe('Witel@fani1');
expect(confirmPasswordInput.value).toBe('Witel@fani1');
});
});
});
})

View File

@@ -0,0 +1,137 @@
import {Formik, Form} from "formik";
import * as Yup from "yup";
import {useTranslations} from "next-intl";
import {Button, Paper, Stack, Typography} from "@mui/material";
import PasswordField from "@/core/components/PasswordField";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/app/hooks/useRequest";
import {SET_USER_PASSWORD} from "@/core/data/apiRoutes";
const ChangePasswordForm = ({onSubmit}) => {
const t = useTranslations();
const requestServer = useRequest({auth: true})
const handleSubmit = (values, {setSubmitting, resetForm}) => {
requestServer(SET_USER_PASSWORD, 'post', {
data: {
current_password: values.current_password,
new_password: values.new_password,
new_password_confirmation: values.new_password_confirmation,
},
}).then((response) => {
resetForm();
}).catch(() => {
}).finally(() => {
setSubmitting(false);
});
};
const initialValues = {
current_password: "",
new_password: "",
new_password_confirmation: "",
};
const validationSchema = Yup.object().shape({
current_password: Yup.string().required(
t("ChangePassword.error_message_current_password_required")
),
new_password: Yup.string()
.min(8, t("ChangePassword.error_message_password_length"))
.required(t("ChangePassword.error_message_new_password_required")),
new_password_confirmation: Yup.string()
.required(t("ChangePassword.error_message_confirm_password_required"))
.test(
t("ChangePassword.error_message_password_match"),
t("ChangePassword.error_message_password_not_match"), // Use the correct error message here
function (value) {
return this.parent.new_password === value;
}
),
});
return (
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validationSchema}
>
{(props) => (
<StyledForm
onSubmit={(e) => {
e.preventDefault();
props.handleSubmit();
}}
>
<Paper elevation={0}>
<Stack spacing={3} sx={{p: 5}} component="div">
<Typography margin={2} variant="h4" textAlign="center">
{t("ChangePassword.typography_change_password")}
</Typography>
<Stack spacing={1} component="div">
<PasswordField
name="current_password"
label={t("ChangePassword.label_current_password")}
error={
props.touched.current_password &&
props.errors.current_password
? true
: false
}
helperText={
props.touched.current_password
? props.errors.current_password
: null
}
/>
</Stack>
<Stack spacing={1} component="div">
<PasswordField
name="new_password"
label={t("ChangePassword.label_new_password")}
error={
props.touched.new_password && props.errors.new_password
? true
: false
}
helperText={
props.touched.new_password ? props.errors.new_password : null
}
/>
</Stack>
<Stack spacing={1} component="div">
<PasswordField
name="new_password_confirmation"
label={t("ChangePassword.label_confirm_password")}
error={
props.touched.new_password_confirmation &&
props.errors.new_password_confirmation
? true
: false
}
helperText={
props.touched.new_password_confirmation
? props.errors.new_password_confirmation
: null
}
/>
</Stack>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
size="large"
disabled={props.isSubmitting || !(props.values.current_password && props.values.new_password && props.values.new_password_confirmation)}
>
{props.isSubmitting
? t("SubmitButton.button_while_submit")
: t("SubmitButton.button_submit")}
</Button>
</Stack>
</Paper>
</StyledForm>
)}
</Formik>
);
};
export default ChangePasswordForm;

View File

@@ -1,149 +1,18 @@
import CenterLayout from "@/layouts/CenterLayout";
import DashboardLayouts from "@/layouts/dashboardLayouts";
import {Button, Container, Paper, Stack, Typography,} from "@mui/material";
import {Formik} from "formik";
import * as Yup from "yup";
import {useTranslations} from "next-intl";
import PasswordField from "@/core/components/PasswordField";
import StyledForm from "@/core/components/StyledForm";
import {SET_USER_PASSWORD} from "@/core/data/apiRoutes";
import SvgChangePassword from "@/core/components/svgs/SvgChangePassword";
import useRequest from "@/lib/app/hooks/useRequest";
import DashboardLayout from "@/layouts/DashboardLayout";
import {Container} from "@mui/material";
import ChangePasswordForm from "@/components/dashboard/change-password/change-password-form";
const DashboardChangePasswordComponent = () => {
const t = useTranslations();
const requestServer = useRequest({auth: true})
const handleSubmit = (values, {setSubmitting, resetForm}) => {
requestServer(SET_USER_PASSWORD, 'post', {
data: {
current_password: values.current_password,
new_password: values.new_password,
new_password_confirmation: values.new_password_confirmation,
},
}).then((response) => {
resetForm();
}).catch(() => {
}).finally(() => {
setSubmitting(false);
});
};
const initialValues = {
current_password: "",
new_password: "",
new_password_confirmation: "",
};
const validationSchema = Yup.object().shape({
current_password: Yup.string().required(
t("ChangePassword.error_message_required")
),
new_password: Yup.string()
.min(8, t("ChangePassword.error_message_password_length"))
.required(t("ChangePassword.error_message_required")),
new_password_confirmation: Yup.string()
.required(t("ChangePassword.error_message_required"))
.test(
t("ChangePassword.error_message_password_match"),
t("ChangePassword.error_message_password_not_match"),
function (value) {
return this.parent.new_password === value;
}
),
});
return (
<DashboardLayouts>
<DashboardLayout>
<CenterLayout>
<Container maxWidth="sm">
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validationSchema}
>
{(props) => (
<StyledForm
onSubmit={(e) => {
e.preventDefault();
props.handleSubmit();
}}
>
<Paper elevation={0}>
<Stack spacing={3} sx={{p: 5}} component="div">
<SvgChangePassword width={300} height={200}/>
<Typography margin={2} variant="h4" textAlign="center">
{t("ChangePassword.typography_change_password")}
</Typography>
<Stack spacing={1} component="div">
<PasswordField
name="current_password"
label={t("ChangePassword.label_current_password")}
error={
props.touched.current_password &&
props.errors.current_password
? true
: false
}
helperText={
props.touched.current_password
? props.errors.current_password
: null
}
/>
</Stack>
<Stack spacing={1} component="div">
<PasswordField
name="new_password"
label={t("ChangePassword.label_new_password")}
error={
props.touched.new_password &&
props.errors.new_password
? true
: false
}
helperText={
props.touched.new_password
? props.errors.new_password
: null
}
/>
</Stack>
<Stack spacing={1} component="div">
<PasswordField
name="new_password_confirmation"
label={t("ChangePassword.label_confirm_password")}
error={
props.touched.new_password_confirmation &&
props.errors.new_password_confirmation
? true
: false
}
helperText={
props.touched.new_password_confirmation
? props.errors.new_password_confirmation
: null
}
/>
</Stack>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
size="large"
disabled={props.isSubmitting}
>
{props.isSubmitting
? t("SubmitButton.button_while_submit")
: t("SubmitButton.button_submit")}
</Button>
</Stack>
</Paper>
</StyledForm>
)}
</Formik>
<ChangePasswordForm />
</Container>
</CenterLayout>
</DashboardLayouts>
</DashboardLayout>
);
};
export default DashboardChangePasswordComponent;

View File

@@ -1,6 +1,6 @@
import StyledForm from "@/core/components/StyledForm";
import CenterLayout from "@/layouts/CenterLayout";
import DashboardLayouts from "@/layouts/dashboardLayouts";
import DashboardLayout from "@/layouts/DashboardLayout";
import useUser from "@/lib/app/hooks/useUser";
import {Box, Container, Grid, Paper, Stack, TextField, Typography,} from "@mui/material";
import * as Yup from "yup";
@@ -54,7 +54,7 @@ const DashboardEditProfile = () => {
const validationSchema = Yup.object().shape({});
return (
<DashboardLayouts>
<DashboardLayout>
<CenterLayout>
<Container maxWidth="sm">
<Formik
@@ -216,7 +216,7 @@ const DashboardEditProfile = () => {
</Formik>
</Container>
</CenterLayout>
</DashboardLayouts>
</DashboardLayout>
);
};

View File

@@ -1,7 +1,7 @@
import DashboardLayouts from "@/layouts/dashboardLayouts";
import DashboardLayout from "@/layouts/DashboardLayout";
const DashboardFirstComponent = () => {
return <DashboardLayouts></DashboardLayouts>;
return <DashboardLayout></DashboardLayout>;
};
export default DashboardFirstComponent;

View File

@@ -0,0 +1,15 @@
import PermPhoneMsgIcon from '@mui/icons-material/PermPhoneMsg';
import StyledFab from "@/core/components/StyledFab";
import useCallDialog from "@/lib/callWidget/hooks/useCallDialog";
const CallWidgetButton = () => {
const {setOpenCallDialog} = useCallDialog()
return (
<StyledFab color="primary" aria-label="open-dialog"
onClick={() => setOpenCallDialog(true)}>
<PermPhoneMsgIcon/>
</StyledFab>
)
}
export default CallWidgetButton

View File

@@ -0,0 +1,7 @@
const CallActions = ({tab}) => {
return (
<>CallActions - {tab.id}</>
)
}
export default CallActions

View File

@@ -0,0 +1,7 @@
const CallHistory = ({tab}) => {
return (
<>CallHistory - {tab.id}</>
)
}
export default CallHistory

View File

@@ -0,0 +1,19 @@
import {Grid} from "@mui/material";
import CallActions from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallActions";
import CallHistory from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel/CallHistory";
const CallTabPanel = ({tab}) => {
return (
<Grid container columns={{xs: 3}} sx={{display: !tab.active && 'none'}} role="tabpanel"
id={`answer-tabpanel-${tab.id}`} aria-labelledby={`answer-tab-${tab.id}`}>
<Grid item xs={2}>
<CallActions tab={tab}/>
</Grid>
<Grid item xs={1}>
<CallHistory tab={tab}/>
</Grid>
</Grid>
)
}
export default CallTabPanel

View File

@@ -0,0 +1,23 @@
import {IconButton, Stack, Typography, Zoom} from "@mui/material";
import CloseIcon from '@mui/icons-material/Close';
import useAnswers from "@/lib/callWidget/hooks/useAnswers";
const CallTabLabel = ({tab}) => {
const {closeAnswerTab} = useAnswers()
const handleCloseClick = (id) => {
closeAnswerTab(id)
};
return (
<Stack sx={{width: '100%'}} direction={'row'} alignItems={'center'} justifyContent={'space-between'}>
<Typography>{tab.phone_number} - {tab.id}</Typography>
<Zoom in={tab.active}>
<IconButton onClick={() => handleCloseClick(tab.id)}>
<CloseIcon/>
</IconButton>
</Zoom>
</Stack>
)
}
export default CallTabLabel

View File

@@ -0,0 +1,27 @@
import {Box, Tab, Tabs} from "@mui/material";
import CallTabLabel
from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList/CallTabLabel";
import useAnswers from "@/lib/callWidget/hooks/useAnswers";
const CallTabsList = () => {
const {answersList, changeActiveTabAnswers, activeTab} = useAnswers()
const handleChange = (event, newValue) => {
changeActiveTabAnswers(newValue)
};
return (
<Box sx={{borderBottom: 1, borderColor: 'divider'}}>
<Tabs
value={activeTab}
onChange={handleChange}
variant={'fullWidth'}
>
{answersList.map((answer) => (
<Tab component={'div'} sx={{py: 0.5, pr: 0,}} label={<CallTabLabel tab={answer}/>} key={answer.id}/>
))}
</Tabs>
</Box>
)
}
export default CallTabsList

View File

@@ -0,0 +1,17 @@
import CallTabsList from "src/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabsList";
import CallTabPanel from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs/CallTabPanel";
import useAnswers from "@/lib/callWidget/hooks/useAnswers";
const CallTabs = () => {
const {answersList} = useAnswers()
return (
<>
<CallTabsList/>
{answersList.map((answer) => (
<CallTabPanel key={answer.id} tab={answer}/>
))}
</>
)
}
export default CallTabs

View File

@@ -0,0 +1,48 @@
import {Dialog} from "@mui/material";
import {DialogTransition} from "@/core/components/DialogTransition";
import {useEffect} from "react";
import useCallDialog from "@/lib/callWidget/hooks/useCallDialog";
import CallTabs from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog/CallTabs";
import useAnswers from "@/lib/callWidget/hooks/useAnswers";
import useSocket from "@/lib/app/hooks/useSocket";
const CallWidgetDialog = () => {
const {openCallDialog, setOpenCallDialog} = useCallDialog()
const {answersList, newAnswerTab} = useAnswers()
const socket = useSocket()
useEffect(() => {
const answerCall = (data, act) => {
newAnswerTab({
id: data.id,
phone_number: data.phone_number
})
act()
}
socket.on('answer', answerCall)
return () => {
socket.off('answer', answerCall)
}
}, []);
useEffect(() => {
if (answersList.length === 0) {
setOpenCallDialog(false)
return
}
setOpenCallDialog(true)
}, [answersList.length]);
return (
<Dialog
fullScreen
open={openCallDialog}
TransitionComponent={DialogTransition}
>
<CallTabs/>
</Dialog>
)
}
export default CallWidgetDialog

View File

@@ -0,0 +1,28 @@
import CallWidgetButton from "@/components/layouts/Dashboard/CallWidget/CallWidgetButton";
import CallWidgetDialog from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog";
import {AnswersProvider} from "@/lib/callWidget/contexts/answers";
import useSocket from "@/lib/app/hooks/useSocket";
import {useEffect} from "react";
const CallWidget = () => {
const socket = useSocket()
useEffect(() => {
if (socket.connected) return
socket.connect()
return () => {
socket.disconnect()
}
}, []);
return (
<AnswersProvider>
<CallWidgetButton/>
<CallWidgetDialog/>
</AnswersProvider>
)
}
export default CallWidget

View File

@@ -14,7 +14,7 @@ const SidebarDrawer = ({handleDrawerToggle}) => {
{t("app_short_name")}
</Typography>
<Typography variant="caption">
{user.name} | {user.position}
{user.full_name} | {user.position}
</Typography>
</Stack>
</Toolbar>

View File

@@ -18,7 +18,6 @@ function reducer(state, action) {
case "SELECTED":
return state.map((itemsArr) =>
itemsArr.map((item) => {
console.log(item)
if (item.type === "page") {
if (action.route === item.route)
return {...item, selected: true}

View File

@@ -1,13 +1,13 @@
import {useState} from "react";
import FullPageLayout from "../FullPageLayout";
import Header from "./header";
import Sidebar from "./sidebar";
import Header from "src/components/layouts/Dashboard/Header";
import Sidebar from "src/components/layouts/Dashboard/Sidebar";
import FullPageLayout from "@/layouts/FullPageLayout";
import {Toolbar} from "@mui/material";
import BreadCrumbs from "./breadcrumbs";
import BreadCrumbs from "src/components/layouts/Dashboard/Breadcrumbs";
import {useState} from "react";
const drawerWidth = 240;
const DashboardLayouts = (props) => {
const Dashboard = (props) => {
const {window} = props;
const [mobileOpen, setMobileOpen] = useState(false);
const container =
@@ -39,7 +39,7 @@ const DashboardLayouts = (props) => {
</FullPageLayout>
</FullPageLayout>
</FullPageLayout>
);
};
)
}
export default DashboardLayouts;
export default Dashboard

View File

@@ -10,7 +10,7 @@ import useRequest from "@/lib/app/hooks/useRequest";
import useUser from "@/lib/app/hooks/useUser";
import {useTranslations} from "next-intl";
const LoginExpertComponent = () =>{
const LoginExpertComponent = () => {
const t = useTranslations();
const requestServer = useRequest()
const {setToken} = useUser(); // pass token to set token
@@ -24,7 +24,7 @@ const LoginExpertComponent = () =>{
notification: {show: false}
}
}).then((response) => {
setToken(response.data.token)
setToken(response.data.data.token)
}).catch(() => {
props.setSubmitting(false)
})
@@ -38,7 +38,7 @@ const LoginExpertComponent = () =>{
password: Yup.string().required(t("LoginPage.password_error_message_required")),
});
return(
return (
<Container maxWidth="sm">
<Paper elevation={0}>
<Formik

View File

@@ -0,0 +1,6 @@
import {forwardRef} from "react";
import {Slide} from "@mui/material";
export const DialogTransition = forwardRef(function DialogTransition(props, ref) {
return <Slide direction="up" ref={ref} {...props}/>;
});

View File

@@ -6,13 +6,13 @@ import WifiOffIcon from '@mui/icons-material/WifiOff';
import {useTranslations} from "next-intl";
const NetworkComponent = () => {
const toastId = useRef(null);
const networkToastId = useRef(null);
const network = useNetwork()
const t = useTranslations()
useEffect(() => {
if (network.online) {
toast.update(toastId.current, {
toast.update(networkToastId.current, {
type: toast.TYPE.SUCCESS,
render: t('online_message'),
autoClose: 2000,
@@ -22,8 +22,7 @@ const NetworkComponent = () => {
});
return
}
toast.dismiss()
toastId.current = toast.warn(t('offline_message'), {
networkToastId.current = toast.warn(t('offline_message'), {
autoClose: false, closeButton: false, closeOnClick: false, icon: <WifiOffIcon/>
})
}, [network.online]);

View File

@@ -0,0 +1,10 @@
import {Fab, styled} from "@mui/material";
const StyledFab = styled(Fab)`
position: absolute;
bottom: 16px;
/* @noflip */
left: 16px;
`;
export default StyledFab;

View File

@@ -36,4 +36,4 @@ const ErrorNotification = (t, status, message) => {
);
};
export default ErrorNotification;
export default ErrorNotification;

View File

@@ -9,4 +9,4 @@ const PendingNotification = (t) => {
});
};
export default PendingNotification;
export default PendingNotification;

View File

@@ -39,4 +39,4 @@ const SuccessNotification = (t, status) => {
);
};
export default SuccessNotification;
export default SuccessNotification;

View File

@@ -35,4 +35,4 @@ const UploadFileNotification = (t) => {
);
};
export default UploadFileNotification;
export default UploadFileNotification;

View File

@@ -37,4 +37,4 @@ const WarningNotification = (t, status) => {
);
};
export default WarningNotification;
export default WarningNotification;

View File

@@ -1,15 +1,15 @@
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
//login
export const GET_USER_TOKEN = BASE_URL + "/dashboard/login";
export const GET_USER_TOKEN = BASE_URL + "/api/auth/login";
//end login
//change password
export const SET_USER_PASSWORD = BASE_URL + "/dashboard/profile/change_password";
export const SET_USER_PASSWORD = BASE_URL + "/api/profile/change_password";
//end change password
//user data
export const GET_USER_ROUTE = BASE_URL + "/dashboard/profile/info";
export const GET_USER_ROUTE = BASE_URL + "/api/profile/info";
//user data
// role management

View File

@@ -48,7 +48,7 @@ const isServerError = status => status >= 500 && status <= 599;
const isClientError = status => status >= 400 && status <= 499;
const errorLogic = (response, t, notification) => {
if (notification) ErrorNotification(t, response.status)
if (notification) ErrorNotification(t, response.status, response.data.message)
}
const errorValidation = (response, t, notification) => {
if (notification) {

View File

@@ -0,0 +1,14 @@
import CallWidget from "src/components/layouts/Dashboard/CallWidget";
import Dashboard from "src/components/layouts/Dashboard";
const DashboardLayout = (props) => {
return (
<>
<Dashboard {...props}/>
<CallWidget/>
</>
);
};
export default DashboardLayout;

View File

@@ -22,26 +22,23 @@ const MuiLayout = ({children, isBot}) => {
<ThemeProvider theme={theme}>
<GlobalStyles
styles={{
"*:not(.MuiTableContainer-root)::-webkit-scrollbar": {
display: "none",
},
"*::-webkit-scrollbar": {
height: "8px",
},
"*:not(.MuiTableContainer-root)::-webkit-scrollbar": {
display: "none",
},
"*::-webkit-scrollbar-thumb": {
background: `${theme.palette.primary.light}80`,
borderRadius: "3px",
},
"*:not(.MuiTableContainer-root)": {
scrollbarWidth: "thin",
scrollbarColor: "transparent transparent",
},
"*": {
scrollbarWidth: "thin",
scrollbarColor: `${theme.palette.primary.light}80 transparent`,
},
"*:not(.MuiTableContainer-root)": {
scrollbarWidth: "none",
},
"*::-moz-scrollbar-thumb": {
backgroundColor: `${theme.palette.primary.light}80`,
},

View File

@@ -0,0 +1,67 @@
import {createContext, useEffect, useMemo, useRef, useState} from "react";
import {io} from "socket.io-client";
import useUser from "@/lib/app/hooks/useUser";
import {useTranslations} from "next-intl";
import {toast} from "react-toastify";
import PowerIcon from "@mui/icons-material/Power";
import PowerOffIcon from "@mui/icons-material/PowerOff";
export const SocketContext = createContext()
export const SocketProvider = ({children}) => {
const {user, isAuth} = useUser()
const [connectionError, setConnectionError] = useState(false)
const socketToastId = useRef(null);
const t = useTranslations()
const socket = useMemo(() => io(process.env.NEXT_PUBLIC_SERVER_SOCKET_URL, {
autoConnect: false,
auth: {
token: ""
}
}), [])
useEffect(() => {
if (isAuth) {
socket.auth.token = user.telephone_id
return
}
socket.auth.token = ''
}, [isAuth]);
useEffect(() => {
const connectErrorHandler = () => {
setConnectionError(true)
}
const connectHandler = () => {
setConnectionError(false)
}
socket.on("connect_error", connectErrorHandler);
socket.on("connect", connectHandler);
return () => {
socket.off("connect_error", connectErrorHandler);
socket.off("connect", connectHandler);
}
}, []);
useEffect(() => {
if (!connectionError) {
toast.update(socketToastId.current, {
type: toast.TYPE.SUCCESS,
render: t('socket_is_connect_message'),
autoClose: 2000,
closeButton: true,
closeOnClick: true,
icon: <PowerIcon/>
});
return
}
socketToastId.current = toast.warn(t('socket_is_not_connect_message'), {
draggable: false,
autoClose: false, closeButton: false, closeOnClick: false, icon: <PowerOffIcon/>
})
}, [connectionError]);
return <SocketContext.Provider value={{socket}}>{children}</SocketContext.Provider>
}

View File

@@ -75,7 +75,7 @@ export const UserProvider = ({children}) => {
headers: {authorization: `Bearer ${state.token}`},
})
.then(({data}) => {
if (typeof callback === "function") callback(data);
if (typeof callback === "function") callback(data.data);
})
.catch(error => {
if (error.response) {

View File

@@ -1,29 +1,5 @@
import {useEffect, useState} from "react";
function getNetworkConnection() {
return (
navigator.connection ||
navigator.mozConnection ||
navigator.webkitConnection ||
null
);
}
function getNetworkConnectionInfo() {
const connection = getNetworkConnection();
if (!connection) {
return {};
}
return {
rtt: connection.rtt,
type: connection.type,
saveData: connection.saveData,
downLink: connection.downLink,
downLinkMax: connection.downLinkMax,
effectiveType: connection.effectiveType,
};
}
function useNetwork() {
const [state, setState] = useState(() => {
return {
@@ -51,7 +27,7 @@ function useNetwork() {
window.removeEventListener("offline", handleOffline);
};
}, []);
return state;
}

View File

@@ -0,0 +1,10 @@
import {useContext} from "react";
import {SocketContext} from "@/lib/app/contexts/socket";
const useSocket = () => {
const {socket} = useContext(SocketContext)
return socket
}
export default useSocket

View File

@@ -0,0 +1,31 @@
import {createContext, useReducer, useState} from "react";
const reducer = (state, action) => {
switch (action.type) {
case "CHANGE_ACTIVE_TAB":
return state.map((answer, index) => action.newValue === index ? {...answer, active: true} : {
...answer,
active: false
});
case "CHANGE_LIST":
return [...action.newList]
default:
return state;
}
}
export const AnswersContext = createContext()
export const AnswersProvider = ({children}) => {
const [openCallDialog, setOpenCallDialog] = useState(false)
const [activeTab, setActiveTab] = useState(0)
const [state, dispatch] = useReducer(reducer, []);
return <AnswersContext.Provider
value={{
openCallDialog,
setOpenCallDialog,
state,
dispatch,
activeTab,
setActiveTab
}}>{children}</AnswersContext.Provider>
}

View File

@@ -0,0 +1,38 @@
import {useContext} from "react";
import {AnswersContext} from "@/lib/callWidget/contexts/answers";
const useAnswers = () => {
const {state, dispatch, activeTab, setActiveTab} = useContext(AnswersContext)
const changeActiveTabAnswers = (newValue) => {
dispatch({type: 'CHANGE_ACTIVE_TAB', newValue})
setActiveTab(newValue)
}
const newAnswerTab = (data) => {
const newAnswer = {
id: data.id,
phone_number: data.phone_number,
active: true
}
const newList = [...state, newAnswer]
dispatch({type: 'CHANGE_LIST', newList})
changeActiveTabAnswers(newList.length - 1)
}
const closeAnswerTab = (id) => {
const index = state.findIndex(answer => answer.id === id)
const newIndex = index === 0 ? 0 : index - 1
const newList = [...state]
newList.splice(index, 1);
if (newList.length !== 0) {
newList[newIndex].active = true
setActiveTab(newIndex)
}
dispatch({type: 'CHANGE_LIST', newList})
}
return {answersList: state, activeTab, setActiveTab, changeActiveTabAnswers, closeAnswerTab, newAnswerTab}
}
export default useAnswers

View File

@@ -0,0 +1,10 @@
import {useContext} from "react";
import {AnswersContext} from "@/lib/callWidget/contexts/answers";
const useCallDialog = () => {
const {openCallDialog, setOpenCallDialog} = useContext(AnswersContext)
return {openCallDialog, setOpenCallDialog}
}
export default useCallDialog

View File

@@ -7,6 +7,7 @@ import {UserProvider} from "@/lib/app/contexts/user";
import "moment/locale/fa";
import {NextIntlProvider} from "next-intl";
import TitlePage from "@/core/components/TitlePage";
import {SocketProvider} from "@/lib/app/contexts/socket";
const App = ({Component, pageProps}) => {
@@ -18,7 +19,9 @@ const App = ({Component, pageProps}) => {
{pageProps.messages ? <TitlePage text={pageProps.title}/> : ''}
<LoadingProvider>
<AppLayout isBot={pageProps.isBot}>
<Component {...pageProps} />
<SocketProvider>
<Component {...pageProps} />
</SocketProvider>
</AppLayout>
</LoadingProvider>
</MuiLayout>