add prettier for formatting to project
This commit is contained in:
@@ -5,169 +5,135 @@ import { GET_USER_LOGIN_ROUTE } from "@/core/utils/routes";
|
||||
import { useAuth } from "@/lib/contexts/auth";
|
||||
import useRequest from "@/lib/hooks/useRequest";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import {
|
||||
Lock,
|
||||
Person,
|
||||
PhoneCallback,
|
||||
Visibility,
|
||||
VisibilityOff,
|
||||
} from "@mui/icons-material";
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Lock, Person, PhoneCallback, Visibility, VisibilityOff } from "@mui/icons-material";
|
||||
import { Button, Container, IconButton, InputAdornment, Stack, Typography } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { object, string } from "yup";
|
||||
|
||||
const LoginForm = () => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const requestServer = useRequest();
|
||||
const { getUser } = useAuth();
|
||||
const defaultValues = {
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const requestServer = useRequest();
|
||||
const { getUser } = useAuth();
|
||||
const defaultValues = {
|
||||
username: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
const validationSchema = object({
|
||||
username: string().required("لطفا نام کاربری را وارد کنید!"),
|
||||
password: string().required("لطفا رمز عبور را وارد کنید!"),
|
||||
});
|
||||
const validationSchema = object({
|
||||
username: string().required("لطفا نام کاربری را وارد کنید!"),
|
||||
password: string().required("لطفا رمز عبور را وارد کنید!"),
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = useForm({
|
||||
defaultValues,
|
||||
resolver: yupResolver(validationSchema),
|
||||
mode: "all",
|
||||
});
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = useForm({
|
||||
defaultValues,
|
||||
resolver: yupResolver(validationSchema),
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("username", data.username);
|
||||
formData.append("password", data.password);
|
||||
await requestServer(GET_USER_LOGIN_ROUTE, "post", {
|
||||
data: formData,
|
||||
});
|
||||
getUser();
|
||||
} catch (error) {}
|
||||
};
|
||||
const onSubmit = async (data) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("username", data.username);
|
||||
formData.append("password", data.password);
|
||||
await requestServer(GET_USER_LOGIN_ROUTE, "post", {
|
||||
data: formData,
|
||||
});
|
||||
getUser();
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="xs" sx={{ flex: 1 }}>
|
||||
<StyledForm
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<Stack
|
||||
sx={{ width: "100%", height: "100%" }}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
spacing={6}
|
||||
>
|
||||
<Stack
|
||||
sx={{ width: "100%", mb: 2 }}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography
|
||||
variant="h4"
|
||||
color="primary"
|
||||
fontWeight={600}
|
||||
textAlign="center"
|
||||
>
|
||||
ورود به سامانه CRM
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack sx={{ width: "100%", p: 2 }} spacing={4}>
|
||||
<Stack spacing={2}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="نام کاربری"
|
||||
variant="outlined"
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Person />
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"username"}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="رمزعبور"
|
||||
variant="outlined"
|
||||
type={showPassword ? "text" : "password"}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<IconButton onClick={handleClickShowPassword}>
|
||||
{showPassword ? (
|
||||
<VisibilityOff />
|
||||
) : (
|
||||
<Visibility />
|
||||
)}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Lock />
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"password"}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
type={"submit"}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "درحال ورود..." : "ورود"}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</StyledForm>
|
||||
</Container>
|
||||
);
|
||||
return (
|
||||
<Container maxWidth="xs" sx={{ flex: 1 }}>
|
||||
<StyledForm onSubmit={handleSubmit(onSubmit)} style={{ width: "100%", height: "100%" }}>
|
||||
<Stack sx={{ width: "100%", height: "100%" }} justifyContent="center" alignItems="center" spacing={6}>
|
||||
<Stack sx={{ width: "100%", mb: 2 }} justifyContent="center" alignItems="center">
|
||||
<Typography variant="h4" color="primary" fontWeight={600} textAlign="center">
|
||||
ورود به سامانه CRM
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack sx={{ width: "100%", p: 2 }} spacing={4}>
|
||||
<Stack spacing={2}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="نام کاربری"
|
||||
variant="outlined"
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Person />
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"username"}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="رمزعبور"
|
||||
variant="outlined"
|
||||
type={showPassword ? "text" : "password"}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<IconButton onClick={handleClickShowPassword}>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<Lock />
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"password"}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
type={"submit"}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "درحال ورود..." : "ورود"}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</StyledForm>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
export default LoginForm;
|
||||
|
||||
@@ -4,22 +4,22 @@ import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
const LoginLinkRouting = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const redirect = searchParams.get("redirect");
|
||||
const redirect = searchParams.get("redirect");
|
||||
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" justifyContent="center">
|
||||
<Button
|
||||
component={Link}
|
||||
data-testid="link_routing"
|
||||
sx={{ margin: 2 }}
|
||||
color="secondary"
|
||||
href={redirect ? decodeURIComponent(redirect) : "/"}
|
||||
>
|
||||
{`بازگشت به ${redirect ? "صفحه قبلی" : "صفحه اصلی"}`}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack direction="row" alignItems="center" justifyContent="center">
|
||||
<Button
|
||||
component={Link}
|
||||
data-testid="link_routing"
|
||||
sx={{ margin: 2 }}
|
||||
color="secondary"
|
||||
href={redirect ? decodeURIComponent(redirect) : "/"}
|
||||
>
|
||||
{`بازگشت به ${redirect ? "صفحه قبلی" : "صفحه اصلی"}`}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
export default LoginLinkRouting;
|
||||
|
||||
@@ -5,15 +5,15 @@ import LoginLinkRouting from "./LoginLinkRouting";
|
||||
import { Suspense } from "react";
|
||||
|
||||
const LoginPage = () => {
|
||||
return (
|
||||
<Suspense>
|
||||
<Fade in={true}>
|
||||
<Stack sx={{ width: "100%", height: "100vh" }}>
|
||||
<LoginForm />
|
||||
<LoginLinkRouting />
|
||||
</Stack>
|
||||
</Fade>
|
||||
</Suspense>
|
||||
);
|
||||
return (
|
||||
<Suspense>
|
||||
<Fade in={true}>
|
||||
<Stack sx={{ width: "100%", height: "100vh" }}>
|
||||
<LoginForm />
|
||||
<LoginLinkRouting />
|
||||
</Stack>
|
||||
</Fade>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
export default LoginPage;
|
||||
|
||||
@@ -13,205 +13,200 @@ import OnlineCell from "./RowActions/Online";
|
||||
import TelephoneId from "./RowActions/TelephoneId";
|
||||
|
||||
const DataTable = () => {
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "کد یکتا",
|
||||
id: "id",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
accessorKey: "full_name",
|
||||
header: "نام و نام خانوادگی",
|
||||
id: "full_name",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: "شماره تماس داخلی",
|
||||
id: "telephone_id",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
Cell: ({ row }) => <TelephoneId user_id={row.original.id} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "is_online",
|
||||
header: "وضعیت برخط",
|
||||
id: "is_online",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
Cell: ({ row }) => <OnlineCell user_id={row.original.id} />,
|
||||
},
|
||||
{
|
||||
header: "استان",
|
||||
id: "province_id",
|
||||
enableColumnFilter: true,
|
||||
datatype: "numeric",
|
||||
filterMode: "equals",
|
||||
grow: false,
|
||||
size: 130,
|
||||
ColumnSelectComponent: (props) => {
|
||||
const { provinces, errorProvinces, loadingProvinces } =
|
||||
useProvinces();
|
||||
const getColumnSelectOptions = useMemo(() => {
|
||||
if (loadingProvinces) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorProvinces) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "کل کشور" },
|
||||
...provinces.map((province) => ({
|
||||
value: province.id,
|
||||
label: province.name,
|
||||
})),
|
||||
];
|
||||
}, [provinces, errorProvinces, loadingProvinces]);
|
||||
return (
|
||||
<CustomSelectByDependency
|
||||
{...props}
|
||||
value={
|
||||
loadingProvinces ? "loading" : props.filterParameters.value
|
||||
}
|
||||
columnSelectOption={getColumnSelectOptions}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Cell: ({ renderedCellValue, row }) => <>{row.original.province_fa}</>,
|
||||
},
|
||||
{
|
||||
accessorKey: "username",
|
||||
header: "نام کاربری",
|
||||
id: "username",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
accessorKey: "position",
|
||||
header: "سمت",
|
||||
id: "position",
|
||||
enableColumnFilter: false,
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
accessorKey: "phone_number",
|
||||
header: "شماره همراه",
|
||||
id: "phone_number",
|
||||
enableColumnFilter: false,
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: "نقش",
|
||||
id: "role__id",
|
||||
enableColumnFilter: false,
|
||||
grow: false,
|
||||
size: 100,
|
||||
enableColumnFilter: true,
|
||||
datatype: "numeric",
|
||||
filterMode: "equals",
|
||||
grow: false,
|
||||
size: 130,
|
||||
ColumnSelectComponent: (props) => {
|
||||
const { roles, errorRoles, loadingRoles } = useRoles();
|
||||
const getColumnSelectOptions = useMemo(() => {
|
||||
if (loadingRoles) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorRoles) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "همه نقش ها" },
|
||||
...roles.map((role) => ({
|
||||
value: role.id,
|
||||
label: role.name_fa,
|
||||
})),
|
||||
];
|
||||
}, [roles, errorRoles, loadingRoles]);
|
||||
return (
|
||||
<CustomSelectByDependency
|
||||
{...props}
|
||||
value={loadingRoles ? "loading" : props.filterParameters.value}
|
||||
columnSelectOption={getColumnSelectOptions}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Cell: ({ row }) => <>{row.original.roles[0].name_fa}</>,
|
||||
},
|
||||
{
|
||||
accessorKey: "national_id",
|
||||
header: "کد ملی",
|
||||
id: "national_id",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
accessorKey: "gender",
|
||||
header: "جنسیت",
|
||||
id: "gender",
|
||||
enableColumnFilter: false,
|
||||
grow: false,
|
||||
size: 100,
|
||||
Cell: ({ renderedCellValue }) => (
|
||||
<>{renderedCellValue == "male" ? "آقا" : "خانم"}</>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: "کد یکتا",
|
||||
id: "id",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
accessorKey: "full_name",
|
||||
header: "نام و نام خانوادگی",
|
||||
id: "full_name",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: "شماره تماس داخلی",
|
||||
id: "telephone_id",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
Cell: ({ row }) => <TelephoneId user_id={row.original.id} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "is_online",
|
||||
header: "وضعیت برخط",
|
||||
id: "is_online",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
Cell: ({ row }) => <OnlineCell user_id={row.original.id} />,
|
||||
},
|
||||
{
|
||||
header: "استان",
|
||||
id: "province_id",
|
||||
enableColumnFilter: true,
|
||||
datatype: "numeric",
|
||||
filterMode: "equals",
|
||||
grow: false,
|
||||
size: 130,
|
||||
ColumnSelectComponent: (props) => {
|
||||
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
|
||||
const getColumnSelectOptions = useMemo(() => {
|
||||
if (loadingProvinces) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorProvinces) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "کل کشور" },
|
||||
...provinces.map((province) => ({
|
||||
value: province.id,
|
||||
label: province.name,
|
||||
})),
|
||||
];
|
||||
}, [provinces, errorProvinces, loadingProvinces]);
|
||||
return (
|
||||
<CustomSelectByDependency
|
||||
{...props}
|
||||
value={loadingProvinces ? "loading" : props.filterParameters.value}
|
||||
columnSelectOption={getColumnSelectOptions}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Cell: ({ renderedCellValue, row }) => <>{row.original.province_fa}</>,
|
||||
},
|
||||
{
|
||||
accessorKey: "username",
|
||||
header: "نام کاربری",
|
||||
id: "username",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
accessorKey: "position",
|
||||
header: "سمت",
|
||||
id: "position",
|
||||
enableColumnFilter: false,
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
accessorKey: "phone_number",
|
||||
header: "شماره همراه",
|
||||
id: "phone_number",
|
||||
enableColumnFilter: false,
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: "نقش",
|
||||
id: "role__id",
|
||||
enableColumnFilter: false,
|
||||
grow: false,
|
||||
size: 100,
|
||||
enableColumnFilter: true,
|
||||
datatype: "numeric",
|
||||
filterMode: "equals",
|
||||
grow: false,
|
||||
size: 130,
|
||||
ColumnSelectComponent: (props) => {
|
||||
const { roles, errorRoles, loadingRoles } = useRoles();
|
||||
const getColumnSelectOptions = useMemo(() => {
|
||||
if (loadingRoles) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorRoles) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "همه نقش ها" },
|
||||
...roles.map((role) => ({
|
||||
value: role.id,
|
||||
label: role.name_fa,
|
||||
})),
|
||||
];
|
||||
}, [roles, errorRoles, loadingRoles]);
|
||||
return (
|
||||
<CustomSelectByDependency
|
||||
{...props}
|
||||
value={loadingRoles ? "loading" : props.filterParameters.value}
|
||||
columnSelectOption={getColumnSelectOptions}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Cell: ({ row }) => <>{row.original.roles[0].name_fa}</>,
|
||||
},
|
||||
{
|
||||
accessorKey: "national_id",
|
||||
header: "کد ملی",
|
||||
id: "national_id",
|
||||
enableColumnFilter: true,
|
||||
datatype: "text",
|
||||
filterMode: "equals",
|
||||
sortDescFirst: false,
|
||||
columnFilterModeOptions: ["equals", "contains"],
|
||||
grow: false,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
accessorKey: "gender",
|
||||
header: "جنسیت",
|
||||
id: "gender",
|
||||
enableColumnFilter: false,
|
||||
grow: false,
|
||||
size: 100,
|
||||
Cell: ({ renderedCellValue }) => <>{renderedCellValue == "male" ? "آقا" : "خانم"}</>,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
|
||||
<DataTableWithAuth
|
||||
need_filter={true}
|
||||
columns={columns}
|
||||
sorting={[{ id: "id", desc: true }]}
|
||||
table_url={GET_USER_LIST}
|
||||
page_name={"usersPage"}
|
||||
table_name={"usersList"}
|
||||
TableToolbar={Toolbar}
|
||||
enableRowActions
|
||||
positionActionsColumn={"last"}
|
||||
RowActions={RowActions}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
|
||||
<DataTableWithAuth
|
||||
need_filter={true}
|
||||
columns={columns}
|
||||
sorting={[{ id: "id", desc: true }]}
|
||||
table_url={GET_USER_LIST}
|
||||
page_name={"usersPage"}
|
||||
table_name={"usersList"}
|
||||
TableToolbar={Toolbar}
|
||||
enableRowActions
|
||||
positionActionsColumn={"last"}
|
||||
RowActions={RowActions}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default DataTable;
|
||||
|
||||
@@ -1,56 +1,39 @@
|
||||
import { DELETE_USER } from "@/core/utils/routes";
|
||||
import useRequest from "@/lib/hooks/useRequest";
|
||||
import {
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Button, DialogActions, DialogContent, Stack, Typography } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
|
||||
const DeleteContent = ({ rowId, mutate, setOpenDeleteDialog }) => {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const requestServer = useRequest({ notificationSuccess: true });
|
||||
const handleClick = async () => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await requestServer(`${DELETE_USER}/${rowId}`, "delete");
|
||||
mutate();
|
||||
setOpenDeleteDialog(false);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
<Stack alignItems="center" spacing={2}>
|
||||
<Typography mt={2}>آیا از حذف کاربر اطمینان دارید؟</Typography>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions
|
||||
sx={{ justifyContent: "center", paddingBottom: 2, width: "100%" }}
|
||||
>
|
||||
<Button
|
||||
onClick={() => setOpenDeleteDialog(false)}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
>
|
||||
خیر !
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
variant="contained"
|
||||
color="error"
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? "درحال حذف..." : "بله اطمینان دارم"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const requestServer = useRequest({ notificationSuccess: true });
|
||||
const handleClick = async () => {
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await requestServer(`${DELETE_USER}/${rowId}`, "delete");
|
||||
mutate();
|
||||
setOpenDeleteDialog(false);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
<Stack alignItems="center" spacing={2}>
|
||||
<Typography mt={2}>آیا از حذف کاربر اطمینان دارید؟</Typography>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ justifyContent: "center", paddingBottom: 2, width: "100%" }}>
|
||||
<Button onClick={() => setOpenDeleteDialog(false)} variant="outlined" color="secondary">
|
||||
خیر !
|
||||
</Button>
|
||||
<Button onClick={handleClick} variant="contained" color="error" disabled={submitting}>
|
||||
{submitting ? "درحال حذف..." : "بله اطمینان دارم"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteContent;
|
||||
|
||||
@@ -3,35 +3,30 @@ import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import DeleteContent from "./DeleteContent";
|
||||
const Delete = ({ rowId, mutate }) => {
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="حذف">
|
||||
<IconButton color="error" onClick={() => setOpenDeleteDialog(true)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Dialog
|
||||
open={openDeleteDialog}
|
||||
onClose={() => setOpenDeleteDialog(false)}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
boxShadow:
|
||||
"rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
|
||||
},
|
||||
}}
|
||||
dir="rtl"
|
||||
maxWidth={"md"}
|
||||
>
|
||||
<DialogTitle>حذف</DialogTitle>
|
||||
<DeleteContent
|
||||
rowId={rowId}
|
||||
mutate={mutate}
|
||||
setOpenDeleteDialog={setOpenDeleteDialog}
|
||||
/>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="حذف">
|
||||
<IconButton color="error" onClick={() => setOpenDeleteDialog(true)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Dialog
|
||||
open={openDeleteDialog}
|
||||
onClose={() => setOpenDeleteDialog(false)}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
|
||||
},
|
||||
}}
|
||||
dir="rtl"
|
||||
maxWidth={"md"}
|
||||
>
|
||||
<DialogTitle>حذف</DialogTitle>
|
||||
<DeleteContent rowId={rowId} mutate={mutate} setOpenDeleteDialog={setOpenDeleteDialog} />
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Delete;
|
||||
|
||||
@@ -7,363 +7,357 @@ import useRequest from "@/lib/hooks/useRequest";
|
||||
import useRoles from "@/lib/hooks/useRoles";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Grid,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
OutlinedInput,
|
||||
Select,
|
||||
Stack,
|
||||
TextField,
|
||||
Button,
|
||||
Chip,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Grid,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
OutlinedInput,
|
||||
Select,
|
||||
Stack,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { object, string } from "yup";
|
||||
|
||||
const validationSchema = object().shape({
|
||||
full_name: string().required("نام و نام خانوادگی خود را وارد کنید"),
|
||||
username: string().required("نام کاربری خود را وارد کنید"),
|
||||
phone_number: string()
|
||||
.matches(/^09\d{9}$/, "شماره همراه باید با 09 شروع شود و 11 رقم باشد.")
|
||||
.required("شماره همراه خود را وارد کنید"),
|
||||
gender: string().required("جنسیت خود را وارد کنید"),
|
||||
national_id: string()
|
||||
.test(
|
||||
"max",
|
||||
"کد ملی باید شامل 10 رقم باشد",
|
||||
(value) => value.toString().length === 10,
|
||||
)
|
||||
.test("validation", "کد ملی صحیح نمی باشد", (value) =>
|
||||
validateNationalCode(value),
|
||||
)
|
||||
.required("کد ملی خود را وارد کنید"),
|
||||
position: string().required("سمت خود را وارد کنید"),
|
||||
province_id: string().required("استان خود را وارد کنید"),
|
||||
role_id: string().required("نقش خود را وارد کنید"),
|
||||
full_name: string().required("نام و نام خانوادگی خود را وارد کنید"),
|
||||
username: string().required("نام کاربری خود را وارد کنید"),
|
||||
phone_number: string()
|
||||
.matches(/^09\d{9}$/, "شماره همراه باید با 09 شروع شود و 11 رقم باشد.")
|
||||
.required("شماره همراه خود را وارد کنید"),
|
||||
gender: string().required("جنسیت خود را وارد کنید"),
|
||||
national_id: string()
|
||||
.test("max", "کد ملی باید شامل 10 رقم باشد", (value) => value.toString().length === 10)
|
||||
.test("validation", "کد ملی صحیح نمی باشد", (value) => validateNationalCode(value))
|
||||
.required("کد ملی خود را وارد کنید"),
|
||||
position: string().required("سمت خود را وارد کنید"),
|
||||
province_id: string().required("استان خود را وارد کنید"),
|
||||
role_id: string().required("نقش خود را وارد کنید"),
|
||||
});
|
||||
|
||||
const UpdateUserForm = ({ values, setOpen, mutate }) => {
|
||||
const requestServer = useRequest({ notificationSuccess: true });
|
||||
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
|
||||
const { roles, errorRoles, loadingRoles } = useRoles();
|
||||
const requestServer = useRequest({ notificationSuccess: true });
|
||||
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
|
||||
const { roles, errorRoles, loadingRoles } = useRoles();
|
||||
|
||||
const defaultValues = {
|
||||
full_name: values.full_name,
|
||||
username: values.username,
|
||||
position: values.position,
|
||||
gender: values.gender,
|
||||
national_id: values.national_id,
|
||||
phone_number: values.phone_number,
|
||||
province_id: values.province_id,
|
||||
role_id: values.roles[0].id,
|
||||
};
|
||||
const defaultValues = {
|
||||
full_name: values.full_name,
|
||||
username: values.username,
|
||||
position: values.position,
|
||||
gender: values.gender,
|
||||
national_id: values.national_id,
|
||||
phone_number: values.phone_number,
|
||||
province_id: values.province_id,
|
||||
role_id: values.roles[0].id,
|
||||
};
|
||||
|
||||
const provinceSelectOptions = useMemo(() => {
|
||||
if (loadingProvinces) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorProvinces) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "انتخاب استان" },
|
||||
...provinces.map((province) => ({
|
||||
value: province.id,
|
||||
label: province.name,
|
||||
})),
|
||||
];
|
||||
}, [provinces, errorProvinces, loadingProvinces]);
|
||||
const provinceSelectOptions = useMemo(() => {
|
||||
if (loadingProvinces) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorProvinces) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "انتخاب استان" },
|
||||
...provinces.map((province) => ({
|
||||
value: province.id,
|
||||
label: province.name,
|
||||
})),
|
||||
];
|
||||
}, [provinces, errorProvinces, loadingProvinces]);
|
||||
|
||||
const roleSelectOptions = useMemo(() => {
|
||||
if (loadingRoles) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorRoles) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "انتخاب نقش" },
|
||||
...roles.map((role) => ({
|
||||
value: role.id,
|
||||
label: role.name_fa,
|
||||
})),
|
||||
];
|
||||
}, [roles, errorRoles, loadingRoles]);
|
||||
const roleSelectOptions = useMemo(() => {
|
||||
if (loadingRoles) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorRoles) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "انتخاب نقش" },
|
||||
...roles.map((role) => ({
|
||||
value: role.id,
|
||||
label: role.name_fa,
|
||||
})),
|
||||
];
|
||||
}, [roles, errorRoles, loadingRoles]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const formData = new FormData();
|
||||
Object.keys(data).forEach((key) => {
|
||||
formData.append(key, data[key]);
|
||||
});
|
||||
try {
|
||||
await requestServer(`${UPDATE_USER}/${values.id}`, "post", {
|
||||
data: formData,
|
||||
});
|
||||
setOpen(false);
|
||||
mutate();
|
||||
} catch (error) {}
|
||||
};
|
||||
const onSubmit = async (data) => {
|
||||
const formData = new FormData();
|
||||
Object.keys(data).forEach((key) => {
|
||||
formData.append(key, data[key]);
|
||||
});
|
||||
try {
|
||||
await requestServer(`${UPDATE_USER}/${values.id}`, "post", {
|
||||
data: formData,
|
||||
});
|
||||
setOpen(false);
|
||||
mutate();
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContent dividers>
|
||||
<Divider sx={{ mb: 3 }}>
|
||||
<Chip color="primary" variant="outlined" label="کاربر جدید" />
|
||||
</Divider>
|
||||
<StyledForm
|
||||
id="UpdateUserForm"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="نام کاربری"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"username"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<TextField
|
||||
{...field}
|
||||
label="نام و نام خانوادگی"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"full_name"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<TextField
|
||||
{...field}
|
||||
label="سمت"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"position"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="شماره همراه"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="tel"
|
||||
onChange={(e) => {
|
||||
const inputValue = event.target.value;
|
||||
if (isNaN(Number(inputValue))) {
|
||||
return;
|
||||
}
|
||||
if (inputValue.length > 11) {
|
||||
return;
|
||||
}
|
||||
field.onChange(inputValue);
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"phone_number"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-phone-number`} shrink>
|
||||
جنسیت
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-phone-number`}
|
||||
id={"phone-number"}
|
||||
name={`gender`}
|
||||
value={field.value}
|
||||
input={<OutlinedInput label={"جنسیت"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value={"male"}>مرد</MenuItem>
|
||||
<MenuItem value={"female"}>زن</MenuItem>
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"gender"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="شماره ملی"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="tel"
|
||||
onChange={(e) => {
|
||||
const inputValue = event.target.value;
|
||||
if (isNaN(Number(inputValue))) {
|
||||
return;
|
||||
}
|
||||
if (inputValue.length > 10) {
|
||||
return;
|
||||
}
|
||||
field.onChange(inputValue);
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"national_id"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-province`} shrink>
|
||||
استان
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-province`}
|
||||
id={"province"}
|
||||
name={`province_id`}
|
||||
value={loadingProvinces ? "loading" : field.value}
|
||||
input={<OutlinedInput label={"استان"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
{provinceSelectOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"province_id"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-role`} shrink>
|
||||
نقش
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-role`}
|
||||
id={"role"}
|
||||
name={`role_id`}
|
||||
value={loadingRoles ? "loading" : field.value}
|
||||
input={<OutlinedInput label={"نقش"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
{roleSelectOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"role_id"}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</StyledForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="large"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
بستن
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
form="UpdateUserForm"
|
||||
size="large"
|
||||
autoFocus
|
||||
variant="contained"
|
||||
>
|
||||
{isSubmitting ? "در حال ویرایش کاربر..." : "ویرایش کاربر"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<DialogContent dividers>
|
||||
<Divider sx={{ mb: 3 }}>
|
||||
<Chip color="primary" variant="outlined" label="کاربر جدید" />
|
||||
</Divider>
|
||||
<StyledForm
|
||||
id="UpdateUserForm"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="نام کاربری"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"username"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<TextField
|
||||
{...field}
|
||||
label="نام و نام خانوادگی"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"full_name"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<TextField
|
||||
{...field}
|
||||
label="سمت"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"position"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="شماره همراه"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="tel"
|
||||
onChange={(e) => {
|
||||
const inputValue = event.target.value;
|
||||
if (isNaN(Number(inputValue))) {
|
||||
return;
|
||||
}
|
||||
if (inputValue.length > 11) {
|
||||
return;
|
||||
}
|
||||
field.onChange(inputValue);
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"phone_number"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-phone-number`} shrink>
|
||||
جنسیت
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-phone-number`}
|
||||
id={"phone-number"}
|
||||
name={`gender`}
|
||||
value={field.value}
|
||||
input={<OutlinedInput label={"جنسیت"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value={"male"}>مرد</MenuItem>
|
||||
<MenuItem value={"female"}>زن</MenuItem>
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"gender"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="شماره ملی"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="tel"
|
||||
onChange={(e) => {
|
||||
const inputValue = event.target.value;
|
||||
if (isNaN(Number(inputValue))) {
|
||||
return;
|
||||
}
|
||||
if (inputValue.length > 10) {
|
||||
return;
|
||||
}
|
||||
field.onChange(inputValue);
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"national_id"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-province`} shrink>
|
||||
استان
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-province`}
|
||||
id={"province"}
|
||||
name={`province_id`}
|
||||
value={loadingProvinces ? "loading" : field.value}
|
||||
input={<OutlinedInput label={"استان"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
{provinceSelectOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"province_id"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-role`} shrink>
|
||||
نقش
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-role`}
|
||||
id={"role"}
|
||||
name={`role_id`}
|
||||
value={loadingRoles ? "loading" : field.value}
|
||||
input={<OutlinedInput label={"نقش"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
{roleSelectOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"role_id"}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</StyledForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="large"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
بستن
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
form="UpdateUserForm"
|
||||
size="large"
|
||||
autoFocus
|
||||
variant="contained"
|
||||
>
|
||||
{isSubmitting ? "در حال ویرایش کاربر..." : "ویرایش کاربر"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateUserForm;
|
||||
|
||||
@@ -5,28 +5,23 @@ import { useState } from "react";
|
||||
import UpdateUserForm from "./Form";
|
||||
|
||||
const UpdateUser = ({ values, mutate }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="ویرایش">
|
||||
<IconButton onClick={handleOpen}>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Dialog maxWidth="sm" fullWidth={true} open={open}>
|
||||
<UpdateUserForm
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
mutate={mutate}
|
||||
values={values}
|
||||
/>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="ویرایش">
|
||||
<IconButton onClick={handleOpen}>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Dialog maxWidth="sm" fullWidth={true} open={open}>
|
||||
<UpdateUserForm open={open} setOpen={setOpen} mutate={mutate} values={values} />
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default UpdateUser;
|
||||
|
||||
@@ -3,15 +3,11 @@ import { Power, PowerOff } from "@mui/icons-material";
|
||||
import { Stack } from "@mui/material";
|
||||
|
||||
const OnlineCell = ({ user_id }) => {
|
||||
const { clientsOnline } = useSocket();
|
||||
return (
|
||||
<Stack alignItems={"center"}>
|
||||
{clientsOnline.some((co) => co.user_id == user_id) ? (
|
||||
<Power color="success" />
|
||||
) : (
|
||||
<PowerOff color="error" />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
const { clientsOnline } = useSocket();
|
||||
return (
|
||||
<Stack alignItems={"center"}>
|
||||
{clientsOnline.some((co) => co.user_id == user_id) ? <Power color="success" /> : <PowerOff color="error" />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
export default OnlineCell;
|
||||
|
||||
@@ -2,10 +2,8 @@ import { useSocket } from "@/lib/contexts/socket";
|
||||
import { Stack } from "@mui/material";
|
||||
|
||||
const TelephoneId = ({ user_id }) => {
|
||||
const { clientsOnline } = useSocket();
|
||||
const telephone_id = clientsOnline.find(
|
||||
(co) => co.user_id == user_id,
|
||||
)?.telephone_id;
|
||||
return <Stack alignItems={"center"}>{telephone_id}</Stack>;
|
||||
const { clientsOnline } = useSocket();
|
||||
const telephone_id = clientsOnline.find((co) => co.user_id == user_id)?.telephone_id;
|
||||
return <Stack alignItems={"center"}>{telephone_id}</Stack>;
|
||||
};
|
||||
export default TelephoneId;
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Box } from "@mui/material";
|
||||
import Delete from "./Delete";
|
||||
import UpdateUser from "./Edit";
|
||||
const RowActions = ({ row, mutate }) => {
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
|
||||
<Delete rowId={row.original.id} mutate={mutate} />
|
||||
<UpdateUser values={row.original} mutate={mutate} />
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
|
||||
<Delete rowId={row.original.id} mutate={mutate} />
|
||||
<UpdateUser values={row.original} mutate={mutate} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default RowActions;
|
||||
|
||||
@@ -4,22 +4,22 @@ import validateNationalCode from "@/core/utils/nationalCodeValidation";
|
||||
import useProvinces from "@/lib/hooks/useProvince";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import {
|
||||
Button,
|
||||
Chip,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
OutlinedInput,
|
||||
Select,
|
||||
Stack,
|
||||
TextField,
|
||||
Button,
|
||||
Chip,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
OutlinedInput,
|
||||
Select,
|
||||
Stack,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { object, string } from "yup";
|
||||
@@ -30,387 +30,375 @@ import { CREATE_USER } from "@/core/utils/routes";
|
||||
import { Visibility, VisibilityOff } from "@mui/icons-material";
|
||||
|
||||
const defaultValues = {
|
||||
full_name: "",
|
||||
username: "",
|
||||
position: "",
|
||||
gender: "male",
|
||||
national_id: "",
|
||||
phone_number: "",
|
||||
province_id: "",
|
||||
password: "",
|
||||
role_id: "",
|
||||
full_name: "",
|
||||
username: "",
|
||||
position: "",
|
||||
gender: "male",
|
||||
national_id: "",
|
||||
phone_number: "",
|
||||
province_id: "",
|
||||
password: "",
|
||||
role_id: "",
|
||||
telephone_id: "",
|
||||
};
|
||||
|
||||
const validationSchema = object().shape({
|
||||
full_name: string().required("نام و نام خانوادگی خود را وارد کنید"),
|
||||
username: string().required("نام کاربری خود را وارد کنید"),
|
||||
phone_number: string()
|
||||
.matches(/^09\d{9}$/, "شماره همراه باید با 09 شروع شود و 11 رقم باشد.")
|
||||
.required("شماره همراه خود را وارد کنید"),
|
||||
gender: string().required("جنسیت خود را وارد کنید"),
|
||||
national_id: string()
|
||||
.test(
|
||||
"max",
|
||||
"کد ملی باید شامل 10 رقم باشد",
|
||||
(value) => value.toString().length === 10,
|
||||
)
|
||||
.test("validation", "کد ملی صحیح نمی باشد", (value) =>
|
||||
validateNationalCode(value),
|
||||
)
|
||||
.required("کد ملی خود را وارد کنید"),
|
||||
password: string()
|
||||
.required("رمز عبور خود را وارد کنید")
|
||||
.matches(
|
||||
/^(?=.*[a-zA-Z])(?=.*\d).{8,}$/,
|
||||
"رمز عبور باید حداقل 8 کاراکتر و متشکل از عدد، حرف یا سمبل باشد",
|
||||
),
|
||||
position: string().required("سمت خود را وارد کنید"),
|
||||
province_id: string().required("استان خود را وارد کنید"),
|
||||
role_id: string().required("نقش خود را وارد کنید"),
|
||||
full_name: string().required("نام و نام خانوادگی خود را وارد کنید"),
|
||||
username: string().required("نام کاربری خود را وارد کنید"),
|
||||
phone_number: string()
|
||||
.matches(/^09\d{9}$/, "شماره همراه باید با 09 شروع شود و 11 رقم باشد.")
|
||||
.required("شماره همراه خود را وارد کنید"),
|
||||
gender: string().required("جنسیت خود را وارد کنید"),
|
||||
national_id: string()
|
||||
.test("max", "کد ملی باید شامل 10 رقم باشد", (value) => value.toString().length === 10)
|
||||
.test("validation", "کد ملی صحیح نمی باشد", (value) => validateNationalCode(value))
|
||||
.required("کد ملی خود را وارد کنید"),
|
||||
password: string()
|
||||
.required("رمز عبور خود را وارد کنید")
|
||||
.matches(/^(?=.*[a-zA-Z])(?=.*\d).{8,}$/, "رمز عبور باید حداقل 8 کاراکتر و متشکل از عدد، حرف یا سمبل باشد"),
|
||||
position: string().required("سمت خود را وارد کنید"),
|
||||
province_id: string().required("استان خود را وارد کنید"),
|
||||
role_id: string().required("نقش خود را وارد کنید"),
|
||||
});
|
||||
|
||||
const CreateUserForm = ({ setOpen, mutate }) => {
|
||||
const requestServer = useRequest({ notificationSuccess: true });
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
|
||||
const { roles, errorRoles, loadingRoles } = useRoles();
|
||||
const requestServer = useRequest({ notificationSuccess: true });
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
|
||||
const { roles, errorRoles, loadingRoles } = useRoles();
|
||||
|
||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||
|
||||
const provinceSelectOptions = useMemo(() => {
|
||||
if (loadingProvinces) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorProvinces) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "انتخاب استان" },
|
||||
...provinces.map((province) => ({
|
||||
value: province.id,
|
||||
label: province.name,
|
||||
})),
|
||||
];
|
||||
}, [provinces, errorProvinces, loadingProvinces]);
|
||||
const provinceSelectOptions = useMemo(() => {
|
||||
if (loadingProvinces) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorProvinces) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "انتخاب استان" },
|
||||
...provinces.map((province) => ({
|
||||
value: province.id,
|
||||
label: province.name,
|
||||
})),
|
||||
];
|
||||
}, [provinces, errorProvinces, loadingProvinces]);
|
||||
|
||||
const roleSelectOptions = useMemo(() => {
|
||||
if (loadingRoles) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorRoles) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "انتخاب نقش" },
|
||||
...roles.map((role) => ({
|
||||
value: role.id,
|
||||
label: role.name_fa,
|
||||
})),
|
||||
];
|
||||
}, [roles, errorRoles, loadingRoles]);
|
||||
const roleSelectOptions = useMemo(() => {
|
||||
if (loadingRoles) {
|
||||
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||
}
|
||||
if (errorRoles) {
|
||||
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||
}
|
||||
return [
|
||||
{ value: "", label: "انتخاب نقش" },
|
||||
...roles.map((role) => ({
|
||||
value: role.id,
|
||||
label: role.name_fa,
|
||||
})),
|
||||
];
|
||||
}, [roles, errorRoles, loadingRoles]);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const formData = new FormData();
|
||||
Object.keys(data).forEach((key) => {
|
||||
formData.append(key, data[key]);
|
||||
});
|
||||
try {
|
||||
await requestServer(CREATE_USER, "post", {
|
||||
data: formData,
|
||||
});
|
||||
setOpen(false);
|
||||
mutate();
|
||||
} catch (error) {}
|
||||
};
|
||||
const onSubmit = async (data) => {
|
||||
const formData = new FormData();
|
||||
Object.keys(data).forEach((key) => {
|
||||
formData.append(key, data[key]);
|
||||
});
|
||||
try {
|
||||
await requestServer(CREATE_USER, "post", {
|
||||
data: formData,
|
||||
});
|
||||
setOpen(false);
|
||||
mutate();
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContent dividers>
|
||||
<Divider sx={{ mb: 3 }}>
|
||||
<Chip color="primary" variant="outlined" label="کاربر جدید" />
|
||||
</Divider>
|
||||
<StyledForm
|
||||
id="createUserForm"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="نام کاربری"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"username"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="رمز عبور"
|
||||
variant="outlined"
|
||||
type={showPassword ? "text" : "password"}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<IconButton onClick={handleClickShowPassword}>
|
||||
{showPassword ? (
|
||||
<VisibilityOff />
|
||||
) : (
|
||||
<Visibility />
|
||||
)}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"password"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<TextField
|
||||
{...field}
|
||||
label="نام و نام خانوادگی"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"full_name"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<TextField
|
||||
{...field}
|
||||
label="سمت"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"position"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="شماره همراه"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="tel"
|
||||
onChange={(e) => {
|
||||
const inputValue = event.target.value;
|
||||
if (isNaN(Number(inputValue))) {
|
||||
return;
|
||||
}
|
||||
if (inputValue.length > 11) {
|
||||
return;
|
||||
}
|
||||
field.onChange(inputValue);
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"phone_number"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-phone-number`} shrink>
|
||||
جنسیت
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-phone-number`}
|
||||
id={"phone-number"}
|
||||
name={`gender`}
|
||||
value={field.value}
|
||||
input={<OutlinedInput label={"جنسیت"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value={"male"}>مرد</MenuItem>
|
||||
<MenuItem value={"female"}>زن</MenuItem>
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"gender"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="شماره ملی"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="tel"
|
||||
onChange={(e) => {
|
||||
const inputValue = event.target.value;
|
||||
if (isNaN(Number(inputValue))) {
|
||||
return;
|
||||
}
|
||||
if (inputValue.length > 10) {
|
||||
return;
|
||||
}
|
||||
field.onChange(inputValue);
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"national_id"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-province`} shrink>
|
||||
استان
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-province`}
|
||||
id={"province"}
|
||||
name={`province_id`}
|
||||
value={field.value}
|
||||
input={<OutlinedInput label={"استان"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
{provinceSelectOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"province_id"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-role`} shrink>
|
||||
نقش
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-role`}
|
||||
id={"role"}
|
||||
name={`role_id`}
|
||||
value={field.value}
|
||||
input={<OutlinedInput label={"نقش"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
{roleSelectOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"role_id"}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</StyledForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="large"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
بستن
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
form="createUserForm"
|
||||
size="large"
|
||||
autoFocus
|
||||
variant="contained"
|
||||
>
|
||||
{isSubmitting ? "در حال ثبت کاربر جدید..." : "ثبت کاربر جدید"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<DialogContent dividers>
|
||||
<Divider sx={{ mb: 3 }}>
|
||||
<Chip color="primary" variant="outlined" label="کاربر جدید" />
|
||||
</Divider>
|
||||
<StyledForm
|
||||
id="createUserForm"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="نام کاربری"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"username"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="رمز عبور"
|
||||
variant="outlined"
|
||||
type={showPassword ? "text" : "password"}
|
||||
slotProps={{
|
||||
input: {
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<IconButton onClick={handleClickShowPassword}>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"password"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<TextField
|
||||
{...field}
|
||||
label="نام و نام خانوادگی"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"full_name"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<TextField
|
||||
{...field}
|
||||
label="سمت"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"position"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="شماره همراه"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="tel"
|
||||
onChange={(e) => {
|
||||
const inputValue = event.target.value;
|
||||
if (isNaN(Number(inputValue))) {
|
||||
return;
|
||||
}
|
||||
if (inputValue.length > 11) {
|
||||
return;
|
||||
}
|
||||
field.onChange(inputValue);
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"phone_number"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-phone-number`} shrink>
|
||||
جنسیت
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-phone-number`}
|
||||
id={"phone-number"}
|
||||
name={`gender`}
|
||||
value={field.value}
|
||||
input={<OutlinedInput label={"جنسیت"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value={"male"}>مرد</MenuItem>
|
||||
<MenuItem value={"female"}>زن</MenuItem>
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"gender"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<LtrTextField
|
||||
{...field}
|
||||
label="شماره ملی"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
type="tel"
|
||||
onChange={(e) => {
|
||||
const inputValue = event.target.value;
|
||||
if (isNaN(Number(inputValue))) {
|
||||
return;
|
||||
}
|
||||
if (inputValue.length > 10) {
|
||||
return;
|
||||
}
|
||||
field.onChange(inputValue);
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={!!error && error.message}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
name={"national_id"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-province`} shrink>
|
||||
استان
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-province`}
|
||||
id={"province"}
|
||||
name={`province_id`}
|
||||
value={field.value}
|
||||
input={<OutlinedInput label={"استان"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
{provinceSelectOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"province_id"}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl fullWidth error={!!error}>
|
||||
<InputLabel id={`label-role`} shrink>
|
||||
نقش
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId={`label-role`}
|
||||
id={"role"}
|
||||
name={`role_id`}
|
||||
value={field.value}
|
||||
input={<OutlinedInput label={"نقش"} />}
|
||||
onChange={(e) => field.onChange(e.target.value)}
|
||||
displayEmpty
|
||||
>
|
||||
{roleSelectOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText error={!!error} sx={{ mt: 1 }}>
|
||||
{!!error && error.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
name={"role_id"}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</StyledForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="large"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
بستن
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
form="createUserForm"
|
||||
size="large"
|
||||
autoFocus
|
||||
variant="contained"
|
||||
>
|
||||
{isSubmitting ? "در حال ثبت کاربر جدید..." : "ثبت کاربر جدید"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUserForm;
|
||||
|
||||
@@ -1,49 +1,39 @@
|
||||
"use client";
|
||||
import { AddCircle } from "@mui/icons-material";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
IconButton,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { Button, Dialog, IconButton, useMediaQuery, useTheme } from "@mui/material";
|
||||
import CreateUserForm from "./Form";
|
||||
import { useState } from "react";
|
||||
|
||||
const CreateUser = ({ mutate }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const [open, setOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<IconButton
|
||||
aria-label="کاربر جدید"
|
||||
color="primary"
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<AddCircle sx={{ fontSize: "25px" }} />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button
|
||||
size={"small"}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<AddCircle />}
|
||||
onClick={handleOpen}
|
||||
>
|
||||
کاربر جدید
|
||||
</Button>
|
||||
)}
|
||||
<Dialog maxWidth="sm" fullWidth={true} open={open}>
|
||||
<CreateUserForm open={open} setOpen={setOpen} mutate={mutate} />
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<IconButton aria-label="کاربر جدید" color="primary" onClick={handleOpen}>
|
||||
<AddCircle sx={{ fontSize: "25px" }} />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button
|
||||
size={"small"}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<AddCircle />}
|
||||
onClick={handleOpen}
|
||||
>
|
||||
کاربر جدید
|
||||
</Button>
|
||||
)}
|
||||
<Dialog maxWidth="sm" fullWidth={true} open={open}>
|
||||
<CreateUserForm open={open} setOpen={setOpen} mutate={mutate} />
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CreateUser;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import CreateUser from "./Create";
|
||||
|
||||
const Toolbar = ({ mutate }) => {
|
||||
return (
|
||||
<>
|
||||
<CreateUser mutate={mutate} />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<CreateUser mutate={mutate} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default Toolbar;
|
||||
|
||||
@@ -3,12 +3,12 @@ import { Stack } from "@mui/material";
|
||||
import DataTable from "./DataTable";
|
||||
|
||||
const UsersPage = () => {
|
||||
return (
|
||||
<Stack spacing={1}>
|
||||
<PageTitle title={"کاربران"} />
|
||||
<DataTable />
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack spacing={1}>
|
||||
<PageTitle title={"کاربران"} />
|
||||
<DataTable />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersPage;
|
||||
|
||||
@@ -3,10 +3,6 @@ import { Stack } from "@mui/material";
|
||||
|
||||
const OnlineUsersReport = () => {
|
||||
const { clientsOnline } = useSocket();
|
||||
return (
|
||||
<Stack>
|
||||
clientsOnline: {clientsOnline.length}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
export default OnlineUsersReport;
|
||||
return <Stack>clientsOnline: {clientsOnline.length}</Stack>;
|
||||
};
|
||||
export default OnlineUsersReport;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client"
|
||||
"use client";
|
||||
import { Stack } from "@mui/material";
|
||||
import OnlineUsersReport from "./OnlineUsersReport";
|
||||
|
||||
@@ -7,6 +7,6 @@ const DashboardPage = () => {
|
||||
<Stack>
|
||||
<OnlineUsersReport />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
export default DashboardPage;
|
||||
);
|
||||
};
|
||||
export default DashboardPage;
|
||||
|
||||
@@ -1,60 +1,45 @@
|
||||
import { ExpandLess, ExpandMore, Link } from "@mui/icons-material";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import { Button, Divider, ListItemIcon, ListItemText, Menu, MenuItem, Stack } from "@mui/material";
|
||||
import NextLink from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
const HeaderMenu = ({ menu }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack sx={{ mr: 0.5 }}>
|
||||
<Button
|
||||
color="inherit"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
onClick={handleClick}
|
||||
endIcon={open ? <ExpandLess /> : <ExpandMore />}
|
||||
>
|
||||
{menu.title}
|
||||
</Button>
|
||||
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
|
||||
{menu.subMenu.flatMap((subMenu, index) => [
|
||||
...subMenu.map((sub) => (
|
||||
<MenuItem
|
||||
component={NextLink}
|
||||
href={sub.href}
|
||||
key={sub.title}
|
||||
onClick={handleClose}
|
||||
return (
|
||||
<Stack sx={{ mr: 0.5 }}>
|
||||
<Button
|
||||
color="inherit"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
onClick={handleClick}
|
||||
endIcon={open ? <ExpandLess /> : <ExpandMore />}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Link fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{sub.title}</ListItemText>
|
||||
</MenuItem>
|
||||
)),
|
||||
index < menu.subMenu.length - 1 && (
|
||||
<Divider key={`divider-${index}`} />
|
||||
),
|
||||
])}
|
||||
</Menu>
|
||||
</Stack>
|
||||
);
|
||||
{menu.title}
|
||||
</Button>
|
||||
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
|
||||
{menu.subMenu.flatMap((subMenu, index) => [
|
||||
...subMenu.map((sub) => (
|
||||
<MenuItem component={NextLink} href={sub.href} key={sub.title} onClick={handleClose}>
|
||||
<ListItemIcon>
|
||||
<Link fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{sub.title}</ListItemText>
|
||||
</MenuItem>
|
||||
)),
|
||||
index < menu.subMenu.length - 1 && <Divider key={`divider-${index}`} />,
|
||||
])}
|
||||
</Menu>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderMenu;
|
||||
|
||||
@@ -4,16 +4,16 @@ import { Chip } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const SidebarBadge = ({ badge, chipProps = {} }) => {
|
||||
const { data } = useSidebarBadge();
|
||||
const [value, setValue] = useState();
|
||||
const { data } = useSidebarBadge();
|
||||
const [value, setValue] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
setValue(getValueByPath(data, badge));
|
||||
}, [data, badge]);
|
||||
useEffect(() => {
|
||||
setValue(getValueByPath(data, badge));
|
||||
}, [data, badge]);
|
||||
|
||||
if (!data) return null;
|
||||
if (!value) return null;
|
||||
if (!data) return null;
|
||||
if (!value) return null;
|
||||
|
||||
return <Chip label={value.toLocaleString()} size="small" {...chipProps} />;
|
||||
return <Chip label={value.toLocaleString()} size="small" {...chipProps} />;
|
||||
};
|
||||
export default SidebarBadge;
|
||||
|
||||
@@ -1,92 +1,67 @@
|
||||
import { ExpandLess, ExpandMore } from "@mui/icons-material";
|
||||
import {
|
||||
Collapse,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import { Collapse, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Stack } from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import SidebarBadge from "./SidebarBadge";
|
||||
import SidebarSubitems from "./SidebarSubitems";
|
||||
|
||||
const SidebarListItems = ({ menuItem, dispatch }) => {
|
||||
return (
|
||||
<>
|
||||
<ListItem disablePadding divider>
|
||||
<ListItemButton
|
||||
sx={{ p: 0.5 }}
|
||||
disableGutters
|
||||
selected={menuItem.selected}
|
||||
component={menuItem.type === "page" ? Link : null}
|
||||
href={menuItem.type === "page" ? menuItem.route : null}
|
||||
onClick={() => {
|
||||
if (menuItem.type !== "page") {
|
||||
dispatch({ type: "COLLAPSE_MENU", id: menuItem.id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
justifyContent: "center",
|
||||
color: "primary.main",
|
||||
width: 40,
|
||||
height: 24,
|
||||
}}
|
||||
>
|
||||
{menuItem.icon}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={menuItem.label} />
|
||||
{menuItem.badges && (
|
||||
<Stack direction={"row"} sx={{ px: 0.5 }} spacing={0.5}>
|
||||
{menuItem.badges.map((badge, i) => (
|
||||
<SidebarBadge
|
||||
chipProps={{ color: badge.color || "primary" }}
|
||||
key={badge.key}
|
||||
badge={badge.key}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{menuItem.hasSubitems ? (
|
||||
menuItem.showSubitems ? (
|
||||
<ExpandLess />
|
||||
) : (
|
||||
<ExpandMore />
|
||||
)
|
||||
) : null}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Collapse
|
||||
in={menuItem.showSubitems}
|
||||
timeout="auto"
|
||||
mountOnEnter={true}
|
||||
unmountOnExit={true}
|
||||
>
|
||||
{menuItem.hasSubitems ? (
|
||||
<List
|
||||
disablePadding
|
||||
dense
|
||||
sx={{
|
||||
background: "#00000008",
|
||||
}}
|
||||
>
|
||||
{menuItem.Subitems.map((subitem, index) => {
|
||||
return (
|
||||
<SidebarSubitems
|
||||
key={index}
|
||||
dispatch={dispatch}
|
||||
subitem={subitem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
) : null}
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ListItem disablePadding divider>
|
||||
<ListItemButton
|
||||
sx={{ p: 0.5 }}
|
||||
disableGutters
|
||||
selected={menuItem.selected}
|
||||
component={menuItem.type === "page" ? Link : null}
|
||||
href={menuItem.type === "page" ? menuItem.route : null}
|
||||
onClick={() => {
|
||||
if (menuItem.type !== "page") {
|
||||
dispatch({ type: "COLLAPSE_MENU", id: menuItem.id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
justifyContent: "center",
|
||||
color: "primary.main",
|
||||
width: 40,
|
||||
height: 24,
|
||||
}}
|
||||
>
|
||||
{menuItem.icon}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={menuItem.label} />
|
||||
{menuItem.badges && (
|
||||
<Stack direction={"row"} sx={{ px: 0.5 }} spacing={0.5}>
|
||||
{menuItem.badges.map((badge, i) => (
|
||||
<SidebarBadge
|
||||
chipProps={{ color: badge.color || "primary" }}
|
||||
key={badge.key}
|
||||
badge={badge.key}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{menuItem.hasSubitems ? menuItem.showSubitems ? <ExpandLess /> : <ExpandMore /> : null}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Collapse in={menuItem.showSubitems} timeout="auto" mountOnEnter={true} unmountOnExit={true}>
|
||||
{menuItem.hasSubitems ? (
|
||||
<List
|
||||
disablePadding
|
||||
dense
|
||||
sx={{
|
||||
background: "#00000008",
|
||||
}}
|
||||
>
|
||||
{menuItem.Subitems.map((subitem, index) => {
|
||||
return <SidebarSubitems key={index} dispatch={dispatch} subitem={subitem} />;
|
||||
})}
|
||||
</List>
|
||||
) : null}
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default SidebarListItems;
|
||||
|
||||
@@ -9,120 +9,106 @@ import SidebarListItems from "./SidebarListItems";
|
||||
import Profile from "@/core/components/Profile";
|
||||
|
||||
function selectPage(item, route) {
|
||||
if (item.type === "page") {
|
||||
return {
|
||||
...item,
|
||||
selected: item.route === route,
|
||||
showSubitems: item.route === route,
|
||||
};
|
||||
} else if (item.Subitems && Array.isArray(item.Subitems)) {
|
||||
const updatedSubitems = item.Subitems.map((subitem) =>
|
||||
selectPage(subitem, route),
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
Subitems: updatedSubitems,
|
||||
showSubitems: updatedSubitems.some(
|
||||
(subitem) => subitem.showSubitems || subitem.route === route,
|
||||
),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
if (item.type === "page") {
|
||||
return {
|
||||
...item,
|
||||
selected: item.route === route,
|
||||
showSubitems: item.route === route,
|
||||
};
|
||||
} else if (item.Subitems && Array.isArray(item.Subitems)) {
|
||||
const updatedSubitems = item.Subitems.map((subitem) => selectPage(subitem, route));
|
||||
return {
|
||||
...item,
|
||||
Subitems: updatedSubitems,
|
||||
showSubitems: updatedSubitems.some((subitem) => subitem.showSubitems || subitem.route === route),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
function toggleSubitems(items, actionId) {
|
||||
return items.map((item) =>
|
||||
actionId === item.id
|
||||
? { ...item, showSubitems: !item.showSubitems }
|
||||
: { ...item, showSubitems: false },
|
||||
);
|
||||
return items.map((item) =>
|
||||
actionId === item.id ? { ...item, showSubitems: !item.showSubitems } : { ...item, showSubitems: false }
|
||||
);
|
||||
}
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "UPDATE_MENU":
|
||||
const _permissions = action.permissions || [];
|
||||
return filterMenuItems(state, ["all", ..._permissions]);
|
||||
switch (action.type) {
|
||||
case "UPDATE_MENU":
|
||||
const _permissions = action.permissions || [];
|
||||
return filterMenuItems(state, ["all", ..._permissions]);
|
||||
|
||||
case "COLLAPSE_MENU":
|
||||
return state.map((item) => ({
|
||||
...item,
|
||||
showSubitems: item.id === action.id ? !item.showSubitems : false,
|
||||
}));
|
||||
case "COLLAPSE_MENU":
|
||||
return state.map((item) => ({
|
||||
...item,
|
||||
showSubitems: item.id === action.id ? !item.showSubitems : false,
|
||||
}));
|
||||
|
||||
case "COLLAPSE_SUB_ITEMS":
|
||||
return state.map((item) =>
|
||||
item.hasSubitems
|
||||
? { ...item, Subitems: toggleSubitems(item.Subitems, action.id) }
|
||||
: item,
|
||||
);
|
||||
case "COLLAPSE_SUB_ITEMS":
|
||||
return state.map((item) =>
|
||||
item.hasSubitems ? { ...item, Subitems: toggleSubitems(item.Subitems, action.id) } : item
|
||||
);
|
||||
|
||||
case "COLLAPSE_SUB_SECOND_ITEMS":
|
||||
return state.map((item) =>
|
||||
item.hasSubitems
|
||||
? {
|
||||
...item,
|
||||
Subitems: item.Subitems.map((subitem) =>
|
||||
subitem.hasSubitems
|
||||
? {
|
||||
...subitem,
|
||||
Subitems: toggleSubitems(subitem.Subitems, action.id),
|
||||
}
|
||||
: subitem,
|
||||
),
|
||||
}
|
||||
: item,
|
||||
);
|
||||
case "COLLAPSE_SUB_SECOND_ITEMS":
|
||||
return state.map((item) =>
|
||||
item.hasSubitems
|
||||
? {
|
||||
...item,
|
||||
Subitems: item.Subitems.map((subitem) =>
|
||||
subitem.hasSubitems
|
||||
? {
|
||||
...subitem,
|
||||
Subitems: toggleSubitems(subitem.Subitems, action.id),
|
||||
}
|
||||
: subitem
|
||||
),
|
||||
}
|
||||
: item
|
||||
);
|
||||
|
||||
case "SELECTED":
|
||||
return state.map((item) => selectPage(item, action.route));
|
||||
case "SELECTED":
|
||||
return state.map((item) => selectPage(item, action.route));
|
||||
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
const SidebarMenu = () => {
|
||||
const { data: userPermissions } = usePermissions();
|
||||
const [menuItems, dispatch] = useReducer(reducer, pageMenu);
|
||||
const pathname = usePathname();
|
||||
const [selectedKey, setSelectedKey] = useState(null);
|
||||
const { data: userPermissions } = usePermissions();
|
||||
const [menuItems, dispatch] = useReducer(reducer, pageMenu);
|
||||
const pathname = usePathname();
|
||||
const [selectedKey, setSelectedKey] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: "SELECTED", route: pathname });
|
||||
setSelectedKey(pathname);
|
||||
}, [userPermissions, pathname]);
|
||||
useEffect(() => {
|
||||
dispatch({ type: "SELECTED", route: pathname });
|
||||
setSelectedKey(pathname);
|
||||
}, [userPermissions, pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userPermissions) return;
|
||||
dispatch({ type: "UPDATE_MENU", permissions: userPermissions });
|
||||
}, [userPermissions]);
|
||||
useEffect(() => {
|
||||
if (!userPermissions) return;
|
||||
dispatch({ type: "UPDATE_MENU", permissions: userPermissions });
|
||||
}, [userPermissions]);
|
||||
|
||||
useEffect(() => {
|
||||
selectedKey &&
|
||||
document.querySelector(`[data-route="${selectedKey}"]`)?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}, [selectedKey]);
|
||||
useEffect(() => {
|
||||
selectedKey &&
|
||||
document.querySelector(`[data-route="${selectedKey}"]`)?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}, [selectedKey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Profile />
|
||||
{userPermissions && (
|
||||
<List sx={{ overflow: "auto", flex: 1 }} disablePadding dense>
|
||||
{menuItems.map((menuItem) => {
|
||||
return (
|
||||
<SidebarListItems
|
||||
dispatch={dispatch}
|
||||
key={menuItem.id}
|
||||
menuItem={menuItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Profile />
|
||||
{userPermissions && (
|
||||
<List sx={{ overflow: "auto", flex: 1 }} disablePadding dense>
|
||||
{menuItems.map((menuItem) => {
|
||||
return <SidebarListItems dispatch={dispatch} key={menuItem.id} menuItem={menuItem} />;
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default SidebarMenu;
|
||||
|
||||
@@ -1,96 +1,76 @@
|
||||
import { ExpandLess, ExpandMore } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Collapse,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import { Box, Collapse, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Stack } from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import SidebarBadge from "./SidebarBadge";
|
||||
|
||||
const SidebarSubitems = ({ subitem, dispatch }) => {
|
||||
return (
|
||||
<>
|
||||
<ListItem disablePadding divider>
|
||||
<ListItemButton
|
||||
disableGutters
|
||||
sx={{ p: 0.5, pl: 1 }}
|
||||
selected={subitem.selected}
|
||||
component={subitem.type === "page" ? Link : null}
|
||||
href={subitem.type === "page" ? subitem.route : null}
|
||||
onClick={() => {
|
||||
if (subitem.type !== "page") {
|
||||
dispatch({ type: "COLLAPSE_SUB_ITEMS", id: subitem.id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
justifyContent: "center",
|
||||
width: 40,
|
||||
height: 24,
|
||||
}}
|
||||
>
|
||||
{subitem.icon}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={subitem.label} />
|
||||
{subitem.badges && (
|
||||
<Stack direction={"row"} sx={{ px: 0.5 }} spacing={0.5}>
|
||||
{subitem.badges.map((badge, i) => (
|
||||
<SidebarBadge
|
||||
chipProps={{
|
||||
color: badge.color || "default",
|
||||
variant: "outlined",
|
||||
}}
|
||||
key={badge.key}
|
||||
badge={badge.key}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{subitem.hasSubitems ? (
|
||||
subitem.showSubitems ? (
|
||||
<ExpandLess />
|
||||
) : (
|
||||
<ExpandMore />
|
||||
)
|
||||
) : (
|
||||
<Box sx={{ width: 29 }} />
|
||||
)}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Collapse
|
||||
in={subitem.showSubitems}
|
||||
timeout="auto"
|
||||
mountOnEnter={true}
|
||||
unmountOnExit={true}
|
||||
>
|
||||
{subitem.hasSubitems ? (
|
||||
<List
|
||||
disablePadding
|
||||
dense
|
||||
sx={{
|
||||
background: "#00000008",
|
||||
}}
|
||||
>
|
||||
{subitem.Subitems.map((subSubitem, index) => {
|
||||
return (
|
||||
<SidebarSubitems
|
||||
key={index}
|
||||
dispatch={dispatch}
|
||||
subitem={subSubitem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
) : null}
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ListItem disablePadding divider>
|
||||
<ListItemButton
|
||||
disableGutters
|
||||
sx={{ p: 0.5, pl: 1 }}
|
||||
selected={subitem.selected}
|
||||
component={subitem.type === "page" ? Link : null}
|
||||
href={subitem.type === "page" ? subitem.route : null}
|
||||
onClick={() => {
|
||||
if (subitem.type !== "page") {
|
||||
dispatch({ type: "COLLAPSE_SUB_ITEMS", id: subitem.id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
justifyContent: "center",
|
||||
width: 40,
|
||||
height: 24,
|
||||
}}
|
||||
>
|
||||
{subitem.icon}
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={subitem.label} />
|
||||
{subitem.badges && (
|
||||
<Stack direction={"row"} sx={{ px: 0.5 }} spacing={0.5}>
|
||||
{subitem.badges.map((badge, i) => (
|
||||
<SidebarBadge
|
||||
chipProps={{
|
||||
color: badge.color || "default",
|
||||
variant: "outlined",
|
||||
}}
|
||||
key={badge.key}
|
||||
badge={badge.key}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{subitem.hasSubitems ? (
|
||||
subitem.showSubitems ? (
|
||||
<ExpandLess />
|
||||
) : (
|
||||
<ExpandMore />
|
||||
)
|
||||
) : (
|
||||
<Box sx={{ width: 29 }} />
|
||||
)}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Collapse in={subitem.showSubitems} timeout="auto" mountOnEnter={true} unmountOnExit={true}>
|
||||
{subitem.hasSubitems ? (
|
||||
<List
|
||||
disablePadding
|
||||
dense
|
||||
sx={{
|
||||
background: "#00000008",
|
||||
}}
|
||||
>
|
||||
{subitem.Subitems.map((subSubitem, index) => {
|
||||
return <SidebarSubitems key={index} dispatch={dispatch} subitem={subSubitem} />;
|
||||
})}
|
||||
</List>
|
||||
) : null}
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default SidebarSubitems;
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
"use client";
|
||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||
import MenuIcon from "@mui/icons-material/Menu";
|
||||
import {
|
||||
Box,
|
||||
Drawer,
|
||||
IconButton,
|
||||
Stack,
|
||||
styled,
|
||||
Toolbar,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { Box, Drawer, IconButton, Stack, styled, Toolbar, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||
import MuiAppBar from "@mui/material/AppBar";
|
||||
import { useState, useEffect } from "react";
|
||||
import HeaderMenu from "./HeaderMenu";
|
||||
@@ -21,170 +11,157 @@ import SidebarMenu from "./Sidebar/SidebarMenu";
|
||||
|
||||
const drawerWidth = 300;
|
||||
|
||||
const Main = styled(Stack, { shouldForwardProp: (prop) => prop !== "open" })(
|
||||
({ theme, open }) => ({
|
||||
const Main = styled(Stack, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({
|
||||
flexGrow: 1,
|
||||
height: "100%",
|
||||
transition: theme.transitions.create("margin", {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
marginLeft: `-${drawerWidth}px`,
|
||||
...(open && {
|
||||
transition: theme.transitions.create("margin", {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
marginLeft: 0,
|
||||
transition: theme.transitions.create("margin", {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
marginLeft: 0,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}));
|
||||
|
||||
const AppBar = styled(MuiAppBar, {
|
||||
shouldForwardProp: (prop) => prop !== "open",
|
||||
shouldForwardProp: (prop) => prop !== "open",
|
||||
})(({ theme, open }) => ({
|
||||
transition: theme.transitions.create(["margin", "width"], {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
...(open && {
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
marginLeft: `${drawerWidth}px`,
|
||||
transition: theme.transitions.create(["margin", "width"], {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
...(open && {
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
marginLeft: `${drawerWidth}px`,
|
||||
transition: theme.transitions.create(["margin", "width"], {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
const DrawerHeader = styled("div")(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: theme.spacing(0, 1),
|
||||
...theme.mixins.toolbar,
|
||||
minHeight: "40px !important",
|
||||
justifyContent: "flex-end",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: theme.spacing(0, 1),
|
||||
...theme.mixins.toolbar,
|
||||
minHeight: "40px !important",
|
||||
justifyContent: "flex-end",
|
||||
}));
|
||||
|
||||
const HeaderWithSidebar = ({ children }) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("md")); // Detect mobile/tablet screen
|
||||
const [open, setOpen] = useState(!isMobile); // Initial state based on screen size
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("md")); // Detect mobile/tablet screen
|
||||
const [open, setOpen] = useState(!isMobile); // Initial state based on screen size
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(!isMobile); // Toggle state when screen size changes
|
||||
}, [isMobile]);
|
||||
useEffect(() => {
|
||||
setOpen(!isMobile); // Toggle state when screen size changes
|
||||
}, [isMobile]);
|
||||
|
||||
const now = moment().locale("fa").format("YYYY/MM/DD");
|
||||
const now = moment().locale("fa").format("YYYY/MM/DD");
|
||||
|
||||
const handleDrawerOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
const handleDrawerClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
const handleDrawerOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
const handleDrawerClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: `100%`,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<AppBar
|
||||
sx={{ position: "fixed", top: "unset" }}
|
||||
elevation={0}
|
||||
open={open}
|
||||
>
|
||||
<Toolbar variant="dense" sx={{ minHeight: 40 }}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={handleDrawerOpen}
|
||||
edge="start"
|
||||
sx={{ mr: 2, ...(open && { display: "none" }) }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
overflowX: "auto",
|
||||
whiteSpace: "nowrap",
|
||||
scrollbarWidth: "none",
|
||||
"&::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{headerMenu.map((menu) => (
|
||||
<HeaderMenu key={menu.title} menu={menu} />
|
||||
))}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
"& .MuiDrawer-paper": {
|
||||
overflow: "hidden",
|
||||
width: drawerWidth,
|
||||
boxSizing: "border-box",
|
||||
borderTop: 1,
|
||||
borderTopColor: "divider",
|
||||
position: "inherit",
|
||||
},
|
||||
}}
|
||||
variant="persistent"
|
||||
anchor="left"
|
||||
open={open}
|
||||
>
|
||||
<DrawerHeader
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" color="#757575" sx={{ px: 1 }}>
|
||||
تاریخ امروز: {now}
|
||||
</Typography>
|
||||
<IconButton onClick={handleDrawerClose}>
|
||||
<ChevronRightIcon />
|
||||
</IconButton>
|
||||
</DrawerHeader>
|
||||
<SidebarMenu />
|
||||
<Typography
|
||||
textAlign={"center"}
|
||||
variant="caption"
|
||||
sx={{ py: 0.2, fontFamily: "sans-serif" }}
|
||||
>
|
||||
v{process.env.NEXT_PUBLIC_VERSION}
|
||||
</Typography>
|
||||
</Drawer>
|
||||
<Main
|
||||
open={open}
|
||||
sx={{ height: "100%", width: "100%", overflow: "hidden" }}
|
||||
>
|
||||
<DrawerHeader />
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
overflowY: "auto",
|
||||
p: 1,
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: `100%`,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<AppBar sx={{ position: "fixed", top: "unset" }} elevation={0} open={open}>
|
||||
<Toolbar variant="dense" sx={{ minHeight: 40 }}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={handleDrawerOpen}
|
||||
edge="start"
|
||||
sx={{ mr: 2, ...(open && { display: "none" }) }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
overflowX: "auto",
|
||||
whiteSpace: "nowrap",
|
||||
scrollbarWidth: "none",
|
||||
"&::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{headerMenu.map((menu) => (
|
||||
<HeaderMenu key={menu.title} menu={menu} />
|
||||
))}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
"& .MuiDrawer-paper": {
|
||||
overflow: "hidden",
|
||||
width: drawerWidth,
|
||||
boxSizing: "border-box",
|
||||
borderTop: 1,
|
||||
borderTopColor: "divider",
|
||||
position: "inherit",
|
||||
},
|
||||
}}
|
||||
variant="persistent"
|
||||
anchor="left"
|
||||
open={open}
|
||||
>
|
||||
<DrawerHeader
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" color="#757575" sx={{ px: 1 }}>
|
||||
تاریخ امروز: {now}
|
||||
</Typography>
|
||||
<IconButton onClick={handleDrawerClose}>
|
||||
<ChevronRightIcon />
|
||||
</IconButton>
|
||||
</DrawerHeader>
|
||||
<SidebarMenu />
|
||||
<Typography textAlign={"center"} variant="caption" sx={{ py: 0.2, fontFamily: "sans-serif" }}>
|
||||
v{process.env.NEXT_PUBLIC_VERSION}
|
||||
</Typography>
|
||||
</Drawer>
|
||||
<Main open={open} sx={{ height: "100%", width: "100%", overflow: "hidden" }}>
|
||||
<DrawerHeader />
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
overflowY: "auto",
|
||||
p: 1,
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</Main>
|
||||
</Box>
|
||||
</Main>
|
||||
</Box>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderWithSidebar;
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
|
||||
const ActionHeader = ({ tab }) => {
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
py: 2,
|
||||
backgroundColor: "primary.main",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" sx={{ marginRight: 1, color: "#fff" }}>
|
||||
.... عملیات های مربوط به تماس:
|
||||
</Typography>
|
||||
<Typography
|
||||
data-testid="phone_number"
|
||||
variant="subtitle1"
|
||||
sx={{ color: "#fff" }}
|
||||
>
|
||||
{tab.phone_number} ....
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
py: 2,
|
||||
backgroundColor: "primary.main",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" sx={{ marginRight: 1, color: "#fff" }}>
|
||||
.... عملیات های مربوط به تماس:
|
||||
</Typography>
|
||||
<Typography data-testid="phone_number" variant="subtitle1" sx={{ color: "#fff" }}>
|
||||
{tab.phone_number} ....
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionHeader;
|
||||
|
||||
@@ -2,26 +2,24 @@ import { Button, Grid } from "@mui/material";
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
const CallActionCategoriesButton = ({ category, control }) => {
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<Button
|
||||
sx={{ py: 1.5 }}
|
||||
fullWidth
|
||||
variant={
|
||||
field.value === category.category_id ? "contained" : "outlined"
|
||||
}
|
||||
color={"primary"}
|
||||
onClick={() => {
|
||||
field.onChange(category.category_id);
|
||||
}}
|
||||
>
|
||||
{category.category_name}
|
||||
</Button>
|
||||
)}
|
||||
name={"category_id"}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<Button
|
||||
sx={{ py: 1.5 }}
|
||||
fullWidth
|
||||
variant={field.value === category.category_id ? "contained" : "outlined"}
|
||||
color={"primary"}
|
||||
onClick={() => {
|
||||
field.onChange(category.category_id);
|
||||
}}
|
||||
>
|
||||
{category.category_name}
|
||||
</Button>
|
||||
)}
|
||||
name={"category_id"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default CallActionCategoriesButton;
|
||||
|
||||
@@ -3,42 +3,42 @@ import { useEffect, useRef } from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
|
||||
const CallActionDescription = ({ control }) => {
|
||||
const inputRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, 100);
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ margin: 2 }}>
|
||||
<Chip label={"توضیحات تماس (اختیاری)"} variant="outlined" />
|
||||
</Divider>
|
||||
<Box sx={{ m: 2 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
inputRef={inputRef}
|
||||
variant="outlined"
|
||||
placeholder={"لطفا توضیحات خود را وارد نمایید"}
|
||||
type={"text"}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={8}
|
||||
/>
|
||||
)}
|
||||
name={"description"}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ margin: 2 }}>
|
||||
<Chip label={"توضیحات تماس (اختیاری)"} variant="outlined" />
|
||||
</Divider>
|
||||
<Box sx={{ m: 2 }}>
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
inputRef={inputRef}
|
||||
variant="outlined"
|
||||
placeholder={"لطفا توضیحات خود را وارد نمایید"}
|
||||
type={"text"}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={8}
|
||||
/>
|
||||
)}
|
||||
name={"description"}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CallActionDescription;
|
||||
|
||||
@@ -3,36 +3,36 @@ import { useCallback } from "react";
|
||||
import { Controller, useWatch } from "react-hook-form";
|
||||
|
||||
const CallActionSubcategoriesButton = ({ sub_category, control, onSubmit }) => {
|
||||
const category_id = useWatch({ control, name: "category_id" });
|
||||
const category_id = useWatch({ control, name: "category_id" });
|
||||
|
||||
const handleClick = useCallback(
|
||||
(onChange) => {
|
||||
onChange(sub_category.subcategory_id);
|
||||
setTimeout(() => {
|
||||
onSubmit();
|
||||
}, 0);
|
||||
},
|
||||
[sub_category.subcategory_id],
|
||||
);
|
||||
const handleClick = useCallback(
|
||||
(onChange) => {
|
||||
onChange(sub_category.subcategory_id);
|
||||
setTimeout(() => {
|
||||
onSubmit();
|
||||
}, 0);
|
||||
},
|
||||
[sub_category.subcategory_id]
|
||||
);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return sub_category.category_id === category_id ? (
|
||||
<Button
|
||||
sx={{ py: 1.5 }}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
color={"primary"}
|
||||
onClick={() => handleClick(field.onChange)}
|
||||
>
|
||||
{sub_category.subcategory_name}
|
||||
</Button>
|
||||
) : null;
|
||||
}}
|
||||
name={"subcategory_id"}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return sub_category.category_id === category_id ? (
|
||||
<Button
|
||||
sx={{ py: 1.5 }}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
color={"primary"}
|
||||
onClick={() => handleClick(field.onChange)}
|
||||
>
|
||||
{sub_category.subcategory_name}
|
||||
</Button>
|
||||
) : null;
|
||||
}}
|
||||
name={"subcategory_id"}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default CallActionSubcategoriesButton;
|
||||
|
||||
@@ -3,22 +3,18 @@ import CallActionCategoriesButton from "./CallActionCategoriesButton";
|
||||
import { Chip, Divider, Grid, Stack } from "@mui/material";
|
||||
|
||||
const CallActionsCategories = ({ control }) => {
|
||||
const { categoryLists } = useCategory();
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ margin: 2 }}>
|
||||
<Chip label={"موضوع ها"} variant="outlined" />
|
||||
</Divider>
|
||||
<Stack sx={{ m: 1, p: 1 }} direction={"row"} spacing={3}>
|
||||
{categoryLists.map((category) => (
|
||||
<CallActionCategoriesButton
|
||||
key={category.category_id}
|
||||
category={category}
|
||||
control={control}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
const { categoryLists } = useCategory();
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ margin: 2 }}>
|
||||
<Chip label={"موضوع ها"} variant="outlined" />
|
||||
</Divider>
|
||||
<Stack sx={{ m: 1, p: 1 }} direction={"row"} spacing={3}>
|
||||
{categoryLists.map((category) => (
|
||||
<CallActionCategoriesButton key={category.category_id} category={category} control={control} />
|
||||
))}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CallActionsCategories;
|
||||
|
||||
@@ -3,25 +3,25 @@ import CallActionSubcategoriesButton from "./CallActionSubcategoriesButton";
|
||||
import { Chip, Divider, Grid, Stack } from "@mui/material";
|
||||
|
||||
const CallActionsSubcategories = ({ control, onSubmit }) => {
|
||||
const { subCategoryLists } = useCategory();
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ margin: 2 }}>
|
||||
<Chip label={"زیر موضوع ها"} variant="outlined" />
|
||||
</Divider>
|
||||
<Stack sx={{ m: 1, p: 1 }} direction={"row"} spacing={3}>
|
||||
{subCategoryLists.map((sub_category, index) => {
|
||||
return (
|
||||
<CallActionSubcategoriesButton
|
||||
key={index}
|
||||
sub_category={sub_category}
|
||||
control={control}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
const { subCategoryLists } = useCategory();
|
||||
return (
|
||||
<>
|
||||
<Divider sx={{ margin: 2 }}>
|
||||
<Chip label={"زیر موضوع ها"} variant="outlined" />
|
||||
</Divider>
|
||||
<Stack sx={{ m: 1, p: 1 }} direction={"row"} spacing={3}>
|
||||
{subCategoryLists.map((sub_category, index) => {
|
||||
return (
|
||||
<CallActionSubcategoriesButton
|
||||
key={index}
|
||||
sub_category={sub_category}
|
||||
control={control}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CallActionsSubcategories;
|
||||
|
||||
@@ -12,76 +12,70 @@ import CallActionsCategories from "./CallActionsCategories";
|
||||
import CallActionsSubcategories from "./CallActionsSubcategories";
|
||||
|
||||
const defaultValues = {
|
||||
description: "",
|
||||
category_id: "",
|
||||
subcategory_id: "",
|
||||
description: "",
|
||||
category_id: "",
|
||||
subcategory_id: "",
|
||||
};
|
||||
|
||||
const validationSchema = object({
|
||||
category_id: string().required("لطفا نام کاربری را وارد کنید!"),
|
||||
subcategory_id: string().required("لطفا رمز عبور را وارد کنید!"),
|
||||
category_id: string().required("لطفا نام کاربری را وارد کنید!"),
|
||||
subcategory_id: string().required("لطفا رمز عبور را وارد کنید!"),
|
||||
});
|
||||
|
||||
function CallActions({ tab }) {
|
||||
const requestServer = useRequest();
|
||||
const { closeCall } = useCall();
|
||||
const requestServer = useRequest();
|
||||
const { closeCall } = useCall();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = useForm({
|
||||
defaultValues,
|
||||
resolver: yupResolver(validationSchema),
|
||||
mode: "all",
|
||||
});
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = useForm({
|
||||
defaultValues,
|
||||
resolver: yupResolver(validationSchema),
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
data.description != "" &&
|
||||
formData.append("description", data.description);
|
||||
formData.append("category_id", data.category_id);
|
||||
formData.append("subcategory_id", data.subcategory_id);
|
||||
await requestServer(`${CALL_ACTION}/${tab.id}`, "post", {
|
||||
data: formData,
|
||||
});
|
||||
closeCall(tab.id);
|
||||
} catch (error) {}
|
||||
});
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
data.description != "" && formData.append("description", data.description);
|
||||
formData.append("category_id", data.category_id);
|
||||
formData.append("subcategory_id", data.subcategory_id);
|
||||
await requestServer(`${CALL_ACTION}/${tab.id}`, "post", {
|
||||
data: formData,
|
||||
});
|
||||
closeCall(tab.id);
|
||||
} catch (error) {}
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<ActionHeader tab={tab} />
|
||||
<Stack sx={{ height: "100%", overflowY: "auto", position: "relative" }}>
|
||||
<CallActionDescription control={control} />
|
||||
<CallActionsCategories control={control} />
|
||||
<CallActionsSubcategories control={control} onSubmit={onSubmit} />
|
||||
{isSubmitting && (
|
||||
<Box
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
opacity: 0.9,
|
||||
zIndex: 9999,
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<LoadingHardPage
|
||||
width={80}
|
||||
height={80}
|
||||
loading={true}
|
||||
sx={{ position: "absolute" }}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
>
|
||||
<ActionHeader tab={tab} />
|
||||
<Stack sx={{ height: "100%", overflowY: "auto", position: "relative" }}>
|
||||
<CallActionDescription control={control} />
|
||||
<CallActionsCategories control={control} />
|
||||
<CallActionsSubcategories control={control} onSubmit={onSubmit} />
|
||||
{isSubmitting && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
opacity: 0.9,
|
||||
zIndex: 9999,
|
||||
}}
|
||||
>
|
||||
<LoadingHardPage width={80} height={80} loading={true} sx={{ position: "absolute" }} />
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default CallActions;
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
const ErrorOrEmpty = ({ isErrorOrEmpty }) => {
|
||||
return (
|
||||
<>
|
||||
{isErrorOrEmpty === "error" ? (
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
color: "#837e7e",
|
||||
fontWeight: "500",
|
||||
letterSpacing: 1,
|
||||
textAlign: "center",
|
||||
mt: "50%",
|
||||
}}
|
||||
>
|
||||
خطا در دریافت تاریخچه تماس دریافتی
|
||||
</Typography>
|
||||
) : isErrorOrEmpty === "empty" ? (
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
color: "#837e7e",
|
||||
fontWeight: "500",
|
||||
letterSpacing: 1,
|
||||
textAlign: "center",
|
||||
mt: "50%",
|
||||
}}
|
||||
>
|
||||
تاریخچه ای برای تماس دریافتی یافت نشد
|
||||
</Typography>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{isErrorOrEmpty === "error" ? (
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
color: "#837e7e",
|
||||
fontWeight: "500",
|
||||
letterSpacing: 1,
|
||||
textAlign: "center",
|
||||
mt: "50%",
|
||||
}}
|
||||
>
|
||||
خطا در دریافت تاریخچه تماس دریافتی
|
||||
</Typography>
|
||||
) : isErrorOrEmpty === "empty" ? (
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
color: "#837e7e",
|
||||
fontWeight: "500",
|
||||
letterSpacing: 1,
|
||||
textAlign: "center",
|
||||
mt: "50%",
|
||||
}}
|
||||
>
|
||||
تاریخچه ای برای تماس دریافتی یافت نشد
|
||||
</Typography>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorOrEmpty;
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
|
||||
const HistoryHeader = ({ tab }) => {
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
py: 2,
|
||||
backgroundColor: "primary.main",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" sx={{ marginRight: 1, color: "#fff" }}>
|
||||
.... تاریخچه تماس های:
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" sx={{ color: "#fff" }}>
|
||||
{tab.phone_number} ....
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
py: 2,
|
||||
backgroundColor: "primary.main",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" sx={{ marginRight: 1, color: "#fff" }}>
|
||||
.... تاریخچه تماس های:
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" sx={{ color: "#fff" }}>
|
||||
{tab.phone_number} ....
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryHeader;
|
||||
|
||||
@@ -3,28 +3,26 @@ import { Avatar, Box, Stack, Typography } from "@mui/material";
|
||||
import moment from "jalali-moment";
|
||||
|
||||
const PreviousOperatorData = ({ historyItem }) => {
|
||||
return (
|
||||
<Stack direction={"row"} spacing={2}>
|
||||
<Box>
|
||||
<Avatar sx={{ backgroundColor: "primary.light" }}>
|
||||
<HeadsetMicIcon />
|
||||
</Avatar>
|
||||
</Box>
|
||||
<Stack>
|
||||
<Stack direction={"row"} sx={{ alignItems: "end" }}>
|
||||
<Typography sx={{ mr: 1 }}>
|
||||
{historyItem.operator_full_name}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: "secondary.main" }}>
|
||||
(کارشناس)
|
||||
</Typography>
|
||||
return (
|
||||
<Stack direction={"row"} spacing={2}>
|
||||
<Box>
|
||||
<Avatar sx={{ backgroundColor: "primary.light" }}>
|
||||
<HeadsetMicIcon />
|
||||
</Avatar>
|
||||
</Box>
|
||||
<Stack>
|
||||
<Stack direction={"row"} sx={{ alignItems: "end" }}>
|
||||
<Typography sx={{ mr: 1 }}>{historyItem.operator_full_name}</Typography>
|
||||
<Typography variant="caption" sx={{ color: "secondary.main" }}>
|
||||
(کارشناس)
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography variant="caption" sx={{ color: "secondary.main" }}>
|
||||
{moment(historyItem.created_at).locale("fa").fromNow()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Typography variant="caption" sx={{ color: "secondary.main" }}>
|
||||
{moment(historyItem.created_at).locale("fa").fromNow()}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviousOperatorData;
|
||||
|
||||
@@ -1,60 +1,46 @@
|
||||
import { Chip, Divider, Stack } from "@mui/material";
|
||||
|
||||
const Topics = ({ historyItem }) => {
|
||||
return (
|
||||
<Stack spacing={1}>
|
||||
<Stack
|
||||
sx={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
}}
|
||||
spacing={1}
|
||||
direction={"row"}
|
||||
>
|
||||
<Chip label={"موضوع"} size="small" variant="outlined" color="primary" />
|
||||
<Divider sx={{ flex: 1 }} />
|
||||
{historyItem.category_name && (
|
||||
<Chip label={historyItem.category_name} />
|
||||
)}
|
||||
</Stack>
|
||||
<Stack
|
||||
sx={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
}}
|
||||
spacing={1}
|
||||
direction={"row"}
|
||||
>
|
||||
<Chip
|
||||
label={"زیر موضوع"}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
/>
|
||||
<Divider sx={{ flex: 1 }} />
|
||||
{historyItem.subcategory_name && (
|
||||
<Chip label={historyItem.subcategory_name} />
|
||||
)}
|
||||
</Stack>
|
||||
<Stack
|
||||
sx={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
}}
|
||||
spacing={1}
|
||||
direction={"row"}
|
||||
>
|
||||
<Chip
|
||||
label={"توضیحات تماس"}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
/>
|
||||
<Divider sx={{ flex: 1 }} />
|
||||
{historyItem.description && <Chip label={historyItem.description} />}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack spacing={1}>
|
||||
<Stack
|
||||
sx={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
}}
|
||||
spacing={1}
|
||||
direction={"row"}
|
||||
>
|
||||
<Chip label={"موضوع"} size="small" variant="outlined" color="primary" />
|
||||
<Divider sx={{ flex: 1 }} />
|
||||
{historyItem.category_name && <Chip label={historyItem.category_name} />}
|
||||
</Stack>
|
||||
<Stack
|
||||
sx={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
}}
|
||||
spacing={1}
|
||||
direction={"row"}
|
||||
>
|
||||
<Chip label={"زیر موضوع"} size="small" variant="outlined" color="primary" />
|
||||
<Divider sx={{ flex: 1 }} />
|
||||
{historyItem.subcategory_name && <Chip label={historyItem.subcategory_name} />}
|
||||
</Stack>
|
||||
<Stack
|
||||
sx={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
}}
|
||||
spacing={1}
|
||||
direction={"row"}
|
||||
>
|
||||
<Chip label={"توضیحات تماس"} size="small" variant="outlined" color="primary" />
|
||||
<Divider sx={{ flex: 1 }} />
|
||||
{historyItem.description && <Chip label={historyItem.description} />}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Topics;
|
||||
|
||||
@@ -3,12 +3,12 @@ import PreviousOperatorData from "./PreviousOperatorData";
|
||||
import Topics from "./Topics.";
|
||||
|
||||
const ListItemOfCalls = ({ historyItem }) => {
|
||||
return (
|
||||
<Stack sx={{ p: 2 }} spacing={2}>
|
||||
<PreviousOperatorData historyItem={historyItem} />
|
||||
<Topics historyItem={historyItem} />
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack sx={{ p: 2 }} spacing={2}>
|
||||
<PreviousOperatorData historyItem={historyItem} />
|
||||
<Topics historyItem={historyItem} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItemOfCalls;
|
||||
|
||||
@@ -1,115 +1,108 @@
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Skeleton,
|
||||
} from "@mui/material";
|
||||
import { Box, List, ListItem, ListItemAvatar, ListItemText, Skeleton } from "@mui/material";
|
||||
|
||||
const LoadingHistory = () => {
|
||||
return (
|
||||
<>
|
||||
<List
|
||||
data-testid="loading-list"
|
||||
sx={{
|
||||
width: "100%",
|
||||
bgcolor: "rgba(222,234,215,0.11)",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar data-testid="loading-listItemAvatar">
|
||||
<Skeleton
|
||||
data-testid="operator-avatar-skeleton"
|
||||
animation="wave"
|
||||
variant="circular"
|
||||
width={60}
|
||||
height={60}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
data-testid="loading-listItemText"
|
||||
primary={
|
||||
<Box sx={{ display: "flex", alignItems: "end" }}>
|
||||
<Skeleton
|
||||
data-testid="operator-name-skeleton"
|
||||
animation="wave"
|
||||
height={15}
|
||||
width="50%"
|
||||
style={{ marginRight: 10, marginBottom: 5 }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Skeleton
|
||||
data-testid="operator-answer-date-skeleton"
|
||||
animation="wave"
|
||||
height={10}
|
||||
width="30%"
|
||||
style={{ marginRight: 10 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box timeout="auto" sx={{ py: 2 }}>
|
||||
<List component="div" disablePadding>
|
||||
<ListItem data-testid="loading-listItem-cat" sx={{ px: 4 }}>
|
||||
<Box
|
||||
return (
|
||||
<>
|
||||
<List
|
||||
data-testid="loading-list"
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
bgcolor: "rgba(222,234,215,0.11)",
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
data-testid="operator-category-header-skeleton"
|
||||
animation="wave"
|
||||
height={15}
|
||||
width="15%"
|
||||
/>
|
||||
<Skeleton
|
||||
data-testid="operator-category-value-skeleton"
|
||||
animation="wave"
|
||||
height={15}
|
||||
width="30%"
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
<List component="div" disablePadding>
|
||||
<ListItem data-testid="loading-listItem-subCat" sx={{ px: 4 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
data-testid="operator-subCategory-header-skeleton"
|
||||
animation="wave"
|
||||
height={15}
|
||||
width="25%"
|
||||
/>
|
||||
<Skeleton
|
||||
data-testid="operator-subCategory-value-skeleton"
|
||||
animation="wave"
|
||||
height={15}
|
||||
width="50%"
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar data-testid="loading-listItemAvatar">
|
||||
<Skeleton
|
||||
data-testid="operator-avatar-skeleton"
|
||||
animation="wave"
|
||||
variant="circular"
|
||||
width={60}
|
||||
height={60}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
data-testid="loading-listItemText"
|
||||
primary={
|
||||
<Box sx={{ display: "flex", alignItems: "end" }}>
|
||||
<Skeleton
|
||||
data-testid="operator-name-skeleton"
|
||||
animation="wave"
|
||||
height={15}
|
||||
width="50%"
|
||||
style={{ marginRight: 10, marginBottom: 5 }}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Skeleton
|
||||
data-testid="operator-answer-date-skeleton"
|
||||
animation="wave"
|
||||
height={10}
|
||||
width="30%"
|
||||
style={{ marginRight: 10 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box timeout="auto" sx={{ py: 2 }}>
|
||||
<List component="div" disablePadding>
|
||||
<ListItem data-testid="loading-listItem-cat" sx={{ px: 4 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
data-testid="operator-category-header-skeleton"
|
||||
animation="wave"
|
||||
height={15}
|
||||
width="15%"
|
||||
/>
|
||||
<Skeleton
|
||||
data-testid="operator-category-value-skeleton"
|
||||
animation="wave"
|
||||
height={15}
|
||||
width="30%"
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
<List component="div" disablePadding>
|
||||
<ListItem data-testid="loading-listItem-subCat" sx={{ px: 4 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
data-testid="operator-subCategory-header-skeleton"
|
||||
animation="wave"
|
||||
height={15}
|
||||
width="25%"
|
||||
/>
|
||||
<Skeleton
|
||||
data-testid="operator-subCategory-value-skeleton"
|
||||
animation="wave"
|
||||
height={15}
|
||||
width="50%"
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingHistory;
|
||||
|
||||
@@ -9,53 +9,52 @@ import { Headphones } from "@mui/icons-material";
|
||||
|
||||
const max_size = 10;
|
||||
const CallHistory = ({ tab }) => {
|
||||
const {
|
||||
firstItemOfHistory,
|
||||
historyList,
|
||||
isLoadingHistoryList,
|
||||
errorHistoryList,
|
||||
} = useCallerHistory(tab.id, tab.phone_number, max_size);
|
||||
const [showRestOfHistory, setShowRestOfHistory] = useState(false);
|
||||
const { firstItemOfHistory, historyList, isLoadingHistoryList, errorHistoryList } = useCallerHistory(
|
||||
tab.id,
|
||||
tab.phone_number,
|
||||
max_size
|
||||
);
|
||||
const [showRestOfHistory, setShowRestOfHistory] = useState(false);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<HistoryHeader tab={tab} />
|
||||
<Stack sx={{ height: "100%", overflowY: "auto" }}>
|
||||
{isLoadingHistoryList ? (
|
||||
<LoadingHistory />
|
||||
) : errorHistoryList ? (
|
||||
<ErrorOrEmpty isErrorOrEmpty="error" />
|
||||
) : !firstItemOfHistory ? (
|
||||
<ErrorOrEmpty isErrorOrEmpty="empty" />
|
||||
) : (
|
||||
<Box>
|
||||
<ListItemOfCalls key={"first"} historyItem={firstItemOfHistory} />
|
||||
{historyList && (
|
||||
<Divider>
|
||||
<Chip
|
||||
icon={<Headphones />}
|
||||
onClick={() => setShowRestOfHistory(true)}
|
||||
label={"نمایش موارد بیشتر"}
|
||||
disabled={showRestOfHistory}
|
||||
/>
|
||||
</Divider>
|
||||
)}
|
||||
{showRestOfHistory &&
|
||||
historyList.map((historyItem, index) => (
|
||||
<>
|
||||
<ListItemOfCalls key={index} historyItem={historyItem} />
|
||||
<Divider />
|
||||
</>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<HistoryHeader tab={tab} />
|
||||
<Stack sx={{ height: "100%", overflowY: "auto" }}>
|
||||
{isLoadingHistoryList ? (
|
||||
<LoadingHistory />
|
||||
) : errorHistoryList ? (
|
||||
<ErrorOrEmpty isErrorOrEmpty="error" />
|
||||
) : !firstItemOfHistory ? (
|
||||
<ErrorOrEmpty isErrorOrEmpty="empty" />
|
||||
) : (
|
||||
<Box>
|
||||
<ListItemOfCalls key={"first"} historyItem={firstItemOfHistory} />
|
||||
{historyList && (
|
||||
<Divider>
|
||||
<Chip
|
||||
icon={<Headphones />}
|
||||
onClick={() => setShowRestOfHistory(true)}
|
||||
label={"نمایش موارد بیشتر"}
|
||||
disabled={showRestOfHistory}
|
||||
/>
|
||||
</Divider>
|
||||
)}
|
||||
{showRestOfHistory &&
|
||||
historyList.map((historyItem, index) => (
|
||||
<>
|
||||
<ListItemOfCalls key={index} historyItem={historyItem} />
|
||||
<Divider />
|
||||
</>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallHistory;
|
||||
|
||||
@@ -3,27 +3,27 @@ import CallActions from "./CallActions";
|
||||
import CallHistory from "./CallHistory";
|
||||
|
||||
const CallTabPanel = ({ tab }) => {
|
||||
return (
|
||||
<Stack
|
||||
role="tabpanel"
|
||||
id={`call-tabpanel-${tab.id}`}
|
||||
aria-labelledby={`call-tab-${tab.id}`}
|
||||
direction={"row"}
|
||||
sx={{
|
||||
height: "100%",
|
||||
display: !tab.active && "none",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 2 }}>
|
||||
<CallActions tab={tab} />
|
||||
</Box>
|
||||
<Divider variant="fullWidth" orientation="vertical" />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<CallHistory tab={tab} />
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
role="tabpanel"
|
||||
id={`call-tabpanel-${tab.id}`}
|
||||
aria-labelledby={`call-tab-${tab.id}`}
|
||||
direction={"row"}
|
||||
sx={{
|
||||
height: "100%",
|
||||
display: !tab.active && "none",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flex: 2 }}>
|
||||
<CallActions tab={tab} />
|
||||
</Box>
|
||||
<Divider variant="fullWidth" orientation="vertical" />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<CallHistory tab={tab} />
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallTabPanel;
|
||||
|
||||
@@ -3,19 +3,14 @@ import { Stack, Typography } from "@mui/material";
|
||||
import CallTabTime from "./CallTabTime";
|
||||
|
||||
const CallTabDetails = ({ tab }) => {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
spacing={3}
|
||||
>
|
||||
<Call />
|
||||
<Stack alignItems={"start"}>
|
||||
<Typography>{tab.phone_number}</Typography>
|
||||
<CallTabTime realTimeDate={tab.date} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack direction="row" justifyContent="center" alignItems="center" spacing={3}>
|
||||
<Call />
|
||||
<Stack alignItems={"start"}>
|
||||
<Typography>{tab.phone_number}</Typography>
|
||||
<CallTabTime realTimeDate={tab.date} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
export default CallTabDetails;
|
||||
|
||||
@@ -4,32 +4,31 @@ import { IconButton, Stack, Zoom } from "@mui/material";
|
||||
import CallTabDetails from "./CallTabDetails";
|
||||
|
||||
const CallTabLabel = ({ tab, index }) => {
|
||||
const { closeCall, activeCall } = useCall();
|
||||
const handleCloseClick = (id) => {
|
||||
closeCall(id);
|
||||
};
|
||||
const { closeCall, activeCall } = useCall();
|
||||
const handleCloseClick = (id) => {
|
||||
closeCall(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
width: "100%",
|
||||
px: 1,
|
||||
borderRight:
|
||||
activeCall === index ? 0 : activeCall - 1 === index ? 0 : 1,
|
||||
borderRightColor: "divider",
|
||||
}}
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<CallTabDetails tab={tab} />
|
||||
<Zoom in={tab.active}>
|
||||
<IconButton size="small" onClick={() => handleCloseClick(tab.id)}>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Zoom>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
width: "100%",
|
||||
px: 1,
|
||||
borderRight: activeCall === index ? 0 : activeCall - 1 === index ? 0 : 1,
|
||||
borderRightColor: "divider",
|
||||
}}
|
||||
direction={"row"}
|
||||
alignItems={"center"}
|
||||
justifyContent={"space-between"}
|
||||
>
|
||||
<CallTabDetails tab={tab} />
|
||||
<Zoom in={tab.active}>
|
||||
<IconButton size="small" onClick={() => handleCloseClick(tab.id)}>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Zoom>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallTabLabel;
|
||||
|
||||
@@ -3,26 +3,26 @@ import moment from "jalali-moment";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const CallTabTime = ({ realTimeDate }) => {
|
||||
const [date, setDate] = useState("...");
|
||||
const changeTabTime = (tabTime) => {
|
||||
moment.relativeTimeThreshold("ss", 1);
|
||||
return moment(tabTime).locale("fa").fromNow();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setDate(changeTabTime(realTimeDate));
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
const [date, setDate] = useState("...");
|
||||
const changeTabTime = (tabTime) => {
|
||||
moment.relativeTimeThreshold("ss", 1);
|
||||
return moment(tabTime).locale("fa").fromNow();
|
||||
};
|
||||
}, [realTimeDate]);
|
||||
|
||||
return (
|
||||
<Typography variant="caption" color="gray">
|
||||
{date}
|
||||
</Typography>
|
||||
);
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setDate(changeTabTime(realTimeDate));
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [realTimeDate]);
|
||||
|
||||
return (
|
||||
<Typography variant="caption" color="gray">
|
||||
{date}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
export default CallTabTime;
|
||||
|
||||
@@ -3,38 +3,38 @@ import { Box, Tab, Tabs } from "@mui/material";
|
||||
import CallTabLabel from "./CallTabLabel";
|
||||
|
||||
const CallTabsList = () => {
|
||||
const { calls, changeActiveTabCall, activeCall } = useCall();
|
||||
const handleChange = (event, newValue) => {
|
||||
changeActiveTabCall(newValue);
|
||||
};
|
||||
const { calls, changeActiveTabCall, activeCall } = useCall();
|
||||
const handleChange = (event, newValue) => {
|
||||
changeActiveTabCall(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<Tabs
|
||||
value={activeCall}
|
||||
onChange={handleChange}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
sx={{ background: "#e0e0e0" }}
|
||||
>
|
||||
{calls.map((call, index) => (
|
||||
<Tab
|
||||
component={"div"}
|
||||
sx={{
|
||||
width: 350,
|
||||
p: 0,
|
||||
py: 0.5,
|
||||
background: call.active ? "#fff" : null,
|
||||
borderTopRightRadius: call.active ? "16px" : 0,
|
||||
borderTopLeftRadius: call.active ? "16px" : 0,
|
||||
}}
|
||||
label={<CallTabLabel index={index} tab={call} />}
|
||||
key={call.id}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||
<Tabs
|
||||
value={activeCall}
|
||||
onChange={handleChange}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
sx={{ background: "#e0e0e0" }}
|
||||
>
|
||||
{calls.map((call, index) => (
|
||||
<Tab
|
||||
component={"div"}
|
||||
sx={{
|
||||
width: 350,
|
||||
p: 0,
|
||||
py: 0.5,
|
||||
background: call.active ? "#fff" : null,
|
||||
borderTopRightRadius: call.active ? "16px" : 0,
|
||||
borderTopLeftRadius: call.active ? "16px" : 0,
|
||||
}}
|
||||
label={<CallTabLabel index={index} tab={call} />}
|
||||
key={call.id}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallTabsList;
|
||||
|
||||
@@ -3,15 +3,15 @@ import CallTabPanel from "./CallTabPanel";
|
||||
import CallTabsList from "./CallTabsList";
|
||||
|
||||
const CallTabs = () => {
|
||||
const { calls } = useCall();
|
||||
const { calls } = useCall();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CallTabsList />
|
||||
{calls.map((call) => (
|
||||
<CallTabPanel key={call.id} tab={call} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<CallTabsList />
|
||||
{calls.map((call) => (
|
||||
<CallTabPanel key={call.id} tab={call} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default CallTabs;
|
||||
|
||||
@@ -7,42 +7,38 @@ import { useEffect } from "react";
|
||||
import CallTabs from "./CallTabs";
|
||||
|
||||
const CallWidgetDialog = () => {
|
||||
const { openCallDialog, setOpenCallDialog, calls, newCall } = useCall();
|
||||
const { socket } = useSocket();
|
||||
const { openCallDialog, setOpenCallDialog, calls, newCall } = useCall();
|
||||
const { socket } = useSocket();
|
||||
|
||||
useEffect(() => {
|
||||
const call = (_data, act) => {
|
||||
const data = JSON.parse(_data);
|
||||
newCall({
|
||||
id: data.call_id,
|
||||
phone_number: data.phone_number,
|
||||
});
|
||||
act();
|
||||
};
|
||||
socket.on("call", call);
|
||||
useEffect(() => {
|
||||
const call = (_data, act) => {
|
||||
const data = JSON.parse(_data);
|
||||
newCall({
|
||||
id: data.call_id,
|
||||
phone_number: data.phone_number,
|
||||
});
|
||||
act();
|
||||
};
|
||||
socket.on("call", call);
|
||||
|
||||
return () => {
|
||||
socket.off("call", call);
|
||||
};
|
||||
});
|
||||
return () => {
|
||||
socket.off("call", call);
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (calls.length === 0) {
|
||||
setOpenCallDialog(false);
|
||||
return;
|
||||
}
|
||||
setOpenCallDialog(true);
|
||||
}, [calls.length]);
|
||||
useEffect(() => {
|
||||
if (calls.length === 0) {
|
||||
setOpenCallDialog(false);
|
||||
return;
|
||||
}
|
||||
setOpenCallDialog(true);
|
||||
}, [calls.length]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
fullScreen
|
||||
open={openCallDialog}
|
||||
slots={{ transition: DialogTransition }}
|
||||
>
|
||||
<CallTabs />
|
||||
</Dialog>
|
||||
);
|
||||
return (
|
||||
<Dialog fullScreen open={openCallDialog} slots={{ transition: DialogTransition }}>
|
||||
<CallTabs />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CallWidgetDialog;
|
||||
|
||||
@@ -4,12 +4,12 @@ import { CategoriesProvider } from "@/lib/contexts/category";
|
||||
import CallWidgetDialog from "./CallWidgetDialog";
|
||||
|
||||
const CallWidget = () => {
|
||||
return (
|
||||
<CategoriesProvider>
|
||||
<CallProvider>
|
||||
<CallWidgetDialog />
|
||||
</CallProvider>
|
||||
</CategoriesProvider>
|
||||
);
|
||||
return (
|
||||
<CategoriesProvider>
|
||||
<CallProvider>
|
||||
<CallWidgetDialog />
|
||||
</CallProvider>
|
||||
</CategoriesProvider>
|
||||
);
|
||||
};
|
||||
export default CallWidget;
|
||||
|
||||
Reference in New Issue
Block a user