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

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,19 @@
import LinkRouting from "@/core/components/LinkRouting";
import {Typography} from "@mui/material";
import {useTranslations} from "next-intl";
export default function BreadcrumbItem(props) {
const t = useTranslations();
const isLast = props.index === props.RouterArray.length - 1;
const url = `/${props.RouterArray.slice(0, props.index + 1).join("/")}`;
return isLast ? (
<Typography variant="body2" color="primary">
{t("sidebar." + props.label)}
</Typography>
) : (
<LinkRouting underline="hover" color="inherit" passHref href={url}>
<Typography variant="body2">{t("sidebar." + props.label)}</Typography>
</LinkRouting>
);
}

View File

@@ -0,0 +1,49 @@
import {useRouter} from "next/router";
import {Box, Breadcrumbs} from "@mui/material";
import {useTheme} from "@mui/material/styles";
import {NavigateBefore, NavigateNext} from "@mui/icons-material";
import BreadcrumbItem from "./BreadcrumbItem";
const BreadCrumbs = (props) => {
const {isVisible} = props;
const theme = useTheme();
const router = useRouter();
if (!isVisible) {
return null;
}
const {pathname} = router;
const RouterArray = pathname.split("/").filter((segment) => segment !== "");
if (RouterArray.length === 1) {
return null;
}
return (
<Box p={3} component="span">
<Breadcrumbs
maxItems={2}
separator={
theme.direction === "ltr" ? (
<NavigateNext fontSize="small"/>
) : (
<NavigateBefore fontSize="small"/>
)
}
aria-label="breadcrumb"
>
{RouterArray.map((segment, index) => (
<BreadcrumbItem
RouterArray={RouterArray}
label={segment}
key={segment}
index={index}
/>
))}
</Breadcrumbs>
</Box>
);
};
export default BreadCrumbs;

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

@@ -0,0 +1,18 @@
import {Avatar, Stack, Typography} from "@mui/material";
import useUser from "@/lib/app/hooks/useUser";
export default function ProfileData() {
const {user} = useUser();
return (
<Stack alignItems="center" spacing={2} sx={{p: 3}}>
<Avatar
sx={{width: "80px", height: "80px"}}
alt="User Image"
src={user.avatar}
/>
<Typography sx={{fontSize: 15, fontWeight: 600}} textAlign="center">
{user.username}
</Typography>
</Stack>
);
}

View File

@@ -0,0 +1,62 @@
import {Avatar, IconButton, Menu, Tooltip} from "@mui/material";
import {useState} from "react";
import ProfileData from "./ProfileData";
import ProfileOptions from "./ProfileOptions";
import {useTranslations} from "next-intl";
import useUser from "@/lib/app/hooks/useUser";
function ProfileMenu() {
const t = useTranslations();
const [anchorElUser, setAnchorElUser] = useState(null);
const {user} = useUser();
const handleOpenUserMenu = (event) => {
setAnchorElUser(event.currentTarget);
};
const handleCloseUserMenu = () => {
setAnchorElUser(null);
};
return (
<>
<Tooltip title={t("header.open_profile")} arrow>
<IconButton onClick={handleOpenUserMenu} sx={{p: 0}}>
<Avatar
sx={{
width: 24,
height: 24,
backgroundColor: "#fff",
color: "primary.main",
}}
alt="User Image"
src={user.avatar}
/>
</IconButton>
</Tooltip>
<Menu
MenuListProps={{sx: {py: 0}}}
sx={{
mt: 6,
}}
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
keepMounted
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
<ProfileData/>
<ProfileOptions handleCloseUserMenu={handleCloseUserMenu}/>
</Menu>
</>
);
}
export default ProfileMenu;

View File

@@ -0,0 +1,49 @@
import {Box, Button, MenuItem, Typography} from "@mui/material";
import MeetingRoomIcon from "@mui/icons-material/MeetingRoom";
import useUser from "@/lib/app/hooks/useUser";
import {useTranslations} from "next-intl";
export default function ProfileOptionLogOut({handleCloseUserMenu}) {
const t = useTranslations();
const {clearToken} = useUser();
const handleClickLogOut = () => {
handleCloseUserMenu();
clearToken();
};
return (
<>
<MenuItem
component={Button}
to={{
pathname: "/dashboard/logout",
}}
sx={{
display: "flex",
justifyContent: "center",
borderTop: 1,
px: 3,
py: 1.5,
borderColor: "#e1e1e1",
textTransform: "unset",
}}
onClick={handleClickLogOut}
>
<Box sx={{display: "flex", alignItems: "center", flex: 1}}>
<Box
sx={{
display: "flex",
alignItems: "center",
color: "primary.main",
pr: 2,
}}
>
<MeetingRoomIcon/>
</Box>
<Typography sx={{flex: 1}} textAlign="start">
{t("header.logout")}
</Typography>
</Box>
</MenuItem>
</>
);
}

View File

@@ -0,0 +1,49 @@
import {Box, MenuItem, Typography} from "@mui/material";
import {NextLinkComposed} from "@/core/components/LinkRouting";
import {useTranslations} from "next-intl";
import headerProfileItems from "@/core/data/headerProfileItems";
import ProfileOptionLogOut from "./ProfileOptionLogOut";
export default function ProfileOptions({handleCloseUserMenu}) {
const t = useTranslations();
return (
<>
{headerProfileItems.map((profile_item) => (
<MenuItem
component={NextLinkComposed}
to={{
pathname: profile_item.route,
}}
sx={{
display: "flex",
justifyContent: "center",
borderTop: 1,
px: 3,
py: 1.5,
borderColor: "#e1e1e1",
}}
key={profile_item.key}
onClick={handleCloseUserMenu}
>
<Box sx={{display: "flex", alignItems: "center", flex: 1}}>
<Box
sx={{
display: "flex",
alignItems: "center",
color: "primary.main",
pr: 2,
}}
>
{profile_item.icon}
</Box>
<Typography sx={{flex: 1}} textAlign="start">
{t(profile_item.name)}
</Typography>
</Box>
</MenuItem>
))}
<ProfileOptionLogOut handleCloseUserMenu={handleCloseUserMenu}/>
</>
);
}

View File

@@ -0,0 +1,72 @@
import MenuIcon from "@mui/icons-material/Menu";
import {AppBar, Box, Container, CssBaseline, IconButton, Stack, Toolbar, useTheme} from "@mui/material";
import ProfileMenu from "./ProfileMenu";
function Header({drawerWidth, handleDrawerToggle}) {
const theme = useTheme();
return (
<>
<CssBaseline/>
<AppBar
position="fixed"
sx={{
width: {md: `calc(100% - ${drawerWidth}px)`},
ml: {md: `${drawerWidth}px`},
}}
>
<Container maxWidth="xxl">
<Toolbar
disableGutters
sx={{
display: "flex",
}}
>
<Stack direction="row" justifyContent="flex-start" sx={{flex: 1}}>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerToggle}
edge="start"
sx={{display: {md: "none"}, m: 0}}
>
<MenuIcon/>
</IconButton>
</Stack>
<Stack
direction="row"
justifyContent="center"
sx={{
flex: 1,
position: "relative",
...theme.mixins.toolbar,
}}
>
<Box
sx={{
position: "relative",
my: 1,
width: 56,
height: 56,
"@media (min-width:600px)": {maxWidth: 64, maxHeight: 64},
"@media (min-width:0px)": {
"@media (orientation: landscape)": {
width: 48,
height: 48,
},
},
}}
>
</Box>
</Stack>
<Stack direction="row" justifyContent="flex-end" sx={{flex: 1}}>
<ProfileMenu/>
</Stack>
</Toolbar>
</Container>
</AppBar>
</>
);
}
export default Header;

View File

@@ -0,0 +1,27 @@
import {Divider, Stack, Toolbar, Typography} from "@mui/material";
import {useTranslations} from "next-intl";
import SidebarList from "./SidebarList";
import useUser from "@/lib/app/hooks/useUser";
const SidebarDrawer = ({handleDrawerToggle}) => {
const {user} = useUser()
const t = useTranslations();
return (
<>
<Toolbar>
<Stack>
<Typography variant="h6" sx={{color: "primary.main"}}>
{t("app_short_name")}
</Typography>
<Typography variant="caption">
{user.full_name} | {user.position}
</Typography>
</Stack>
</Toolbar>
<Divider/>
<SidebarList handleDrawerToggle={handleDrawerToggle}/>
</>
);
};
export default SidebarDrawer;

View File

@@ -0,0 +1,71 @@
import {Divider, List} from "@mui/material";
import {Fragment, useEffect, useReducer} from "react";
import SidebarListItem from "./SidebarListItem";
import sidebarMenu from "@/core/data/sidebarMenu";
import {useRouter} from "next/router";
import useUser from "@/lib/app/hooks/useUser";
function reducer(state, action) {
switch (action.type) {
case "COLLAPSE_MENU":
return state.map((itemsArr) =>
itemsArr.map((item) =>
action.key == item.key
? {...item, showSubItem: !item.showSubItem}
: item
)
);
case "SELECTED":
return state.map((itemsArr) =>
itemsArr.map((item) => {
if (item.type === "page") {
if (action.route === item.route)
return {...item, selected: true}
else
return {...item, selected: false}
}
return {...item}
// item.subItem.map((subitem) => {
// if (action.route === subitem.route)
// return {...item, showSubItem: true, subItem: {...subitem, selected: true}}
// else
// return {...item, showSubItem: false, subItem: {...subitem, selected: false}}
// })
}
)
);
default:
throw new Error();
}
}
export default function SidebarList({handleDrawerToggle}) {
const [itemMenu, dispatch] = useReducer(reducer, sidebarMenu);
const {user} = useUser();
const router = useRouter();
useEffect(() => {
dispatch({type: "SELECTED", route: router.pathname});
}, [router.pathname]);
return (
<List>
{itemMenu.map((itemArr, index) => (
<Fragment key={index}>
{itemArr.map((item) =>
<Fragment key={item.key}>
{(user.permissions.includes(item.permission) || item.permission === "all") &&
<SidebarListItem
item={item}
dispatch={dispatch}
handleDrawerToggle={handleDrawerToggle}
/>}
</Fragment>
)}
<Divider/>
</Fragment>
))}
</List>
);
}

View File

@@ -0,0 +1,84 @@
import {NextLinkComposed} from "@/core/components/LinkRouting";
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";
import {Badge, IconButton, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography,} from "@mui/material";
import {useTranslations} from "next-intl";
import {Fragment} from "react";
import SidebarListSubItem from "./SidebarListSubItem";
import useNotification from "@/lib/app/hooks/useNotification";
const SidebarListItem = ({item, dispatch, handleDrawerToggle}) => {
const t = useTranslations();
const {notification_count} = useNotification()
return (
<>
<ListItem disablePadding secondaryAction={
<IconButton>
<Badge
badgeContent={notification_count ? notification_count[item.name] : 0}
color="error"
variant="standard"
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
/>
</IconButton>
}>
<ListItemButton
selected={item.selected}
{...(item.type == "page" && {
component: NextLinkComposed,
to: {
pathname: item.route,
},
})}
onClick={() => {
if (item.type == "menu") {
dispatch({type: "COLLAPSE_MENU", key: item.key});
}
handleDrawerToggle();
}}
sx={{
minHeight: 48,
}}
>
<ListItemIcon
sx={{
minWidth: 0,
justifyContent: "center",
color: "primary.main",
pr: 2,
}}
>
{item.icon}
</ListItemIcon>
<ListItemText
primary={t(item.key)}
secondary={
item.secondary !== undefined ? (
<Typography variant="caption" color="textSecondary">
{t(item.secondary)}
</Typography>
) : null
}
/>
{item.type == "menu" &&
(item.showSubItem ? <ExpandLess/> : <ExpandMore/>)}
</ListItemButton>
</ListItem>
{item.subItem && (
<SidebarListSubItem
item={item}
handleDrawerToggle={handleDrawerToggle}
/>
)}
</>
);
};
export default SidebarListItem;

View File

@@ -0,0 +1,71 @@
import {NextLinkComposed} from "@/core/components/LinkRouting";
import {
Badge,
Collapse,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Typography,
} from "@mui/material";
import {useTranslations} from "next-intl";
import useNotification from "@/lib/app/hooks/useNotification";
const SidebarListSubItem = ({item, handleDrawerToggle}) => {
const t = useTranslations();
const {notification_count} = useNotification()
return (
<Collapse in={item.showSubItem} timeout="auto" mountOnEnter={true} unmountOnExit={true}>
<List component="div" disablePadding sx={{bgcolor: "#f6f6f6", pr: 1}}>
{item.subItem.map((subitem, index) => (
<ListItem key={subitem.key} disablePadding secondaryAction={
<IconButton>
<Badge
badgeContent={notification_count ? notification_count[subitem.name] : 0}
color="error"
variant="standard"
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
/>
</IconButton>
}>
<ListItemButton
selected={subitem.selected}
component={NextLinkComposed}
to={{
pathname: subitem.route,
}}
sx={{
minHeight: 48,
}}
>
<ListItemIcon
sx={{
minWidth: 0,
justifyContent: "center",
pr: 2,
}}
>
{subitem.icon}
</ListItemIcon>
<ListItemText primary={t(subitem.key)} secondary={
subitem.secondary !== undefined ? (
<Typography variant="caption" color="textSecondary">
{t(subitem.secondary)}
</Typography>
) : null
}/>
</ListItemButton>
</ListItem>
))}
</List>
</Collapse>
);
};
export default SidebarListSubItem;

View File

@@ -0,0 +1,46 @@
import {Box, Drawer} from "@mui/material";
import SidebarDrawer from "./SidebarDrawer";
const Sidebar = (props) => {
return (
<Box
component="nav"
sx={{width: {md: props.drawerWidth}, flexShrink: {sm: 0}}}
aria-label="mailbox folders"
>
<Drawer
container={props.container}
variant="temporary"
open={props.mobileOpen}
onClose={props.handleDrawerToggle}
ModalProps={{
keepMounted: true,
}}
sx={{
display: {xs: "block", md: "none"},
"& .MuiDrawer-paper": {
boxSizing: "border-box",
width: props.drawerWidth,
},
}}
>
<SidebarDrawer handleDrawerToggle={props.handleDrawerToggle}/>
</Drawer>
<Drawer
variant="permanent"
sx={{
display: {xs: "none", md: "block"},
"& .MuiDrawer-paper": {
boxSizing: "border-box",
width: props.drawerWidth,
},
}}
open
>
<SidebarDrawer handleDrawerToggle={props.handleDrawerToggle}/>
</Drawer>
</Box>
);
};
export default Sidebar;

View File

@@ -0,0 +1,45 @@
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 "src/components/layouts/Dashboard/Breadcrumbs";
import {useState} from "react";
const drawerWidth = 240;
const Dashboard = (props) => {
const {window} = props;
const [mobileOpen, setMobileOpen] = useState(false);
const container =
window !== undefined ? () => window().document.body : undefined;
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
return (
<FullPageLayout direction="row">
<Header
handleDrawerToggle={handleDrawerToggle}
drawerWidth={drawerWidth}
/>
<Sidebar
container={container}
mobileOpen={mobileOpen}
handleDrawerToggle={handleDrawerToggle}
drawerWidth={drawerWidth}
/>
<FullPageLayout
component="main"
sx={{flexGrow: 1, width: {sm: `calc(100% - ${drawerWidth}px)`}}}
>
<Toolbar/>
<FullPageLayout sx={{mt: 3}}>
<BreadCrumbs isVisible={true}/>
{props.children}
</FullPageLayout>
</FullPageLayout>
</FullPageLayout>
)
}
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