Merge branch 'release/v2.1.5'

This commit is contained in:
AmirHossein Mahmoodi
2025-05-07 09:18:23 +03:30
14 changed files with 349 additions and 23 deletions

View File

@@ -1,4 +1,4 @@
NEXT_PUBLIC_VERSION = "2.1.0"
NEXT_PUBLIC_VERSION = "2.1.5"
NEXT_PUBLIC_API_URL = "https://crm.witel.ir/server"
NEXT_PUBLIC_SERVER_SOCKET_URL = "wss://crm.witel.ir"

View File

@@ -1,3 +1,4 @@
import favicon from "@/assets/images/favicon.png";
import { Rtl } from "@/core/utils/cacheRtl";
import { AuthProvider } from "@/lib/contexts/auth";
import { TableSettingProvider } from "@/lib/contexts/tableSetting";
@@ -13,7 +14,9 @@ export const metadata = {
export default function RootLayout({ children }) {
return (
<html lang="fa" dir={"rtl"}>
<head></head>
<head>
<link rel="icon" href={favicon.src} type="image/png" sizes="any" />
</head>
<body style={{ height: "100vh", width: "100vw" }}>
<AppRouterCacheProvider
CacheProvider={Rtl}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -5,21 +5,39 @@ 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 { Button, Container, Stack, Typography } from "@mui/material";
import {
Lock,
Person,
PhoneCallback,
Visibility,
VisibilityOff,
} from "@mui/icons-material";
import {
Button,
Container,
IconButton,
InputAdornment,
Stack,
Typography,
} from "@mui/material";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { object, string } from "yup";
const LoginForm = () => {
const [showPassword, setShowPassword] = useState(false);
const requestServer = useRequest();
const { getUser } = useAuth();
const defaultValues = {
username: "",
password: "",
telephone_id: "",
};
const validationSchema = object({
username: string().required("لطفا نام کاربری را وارد کنید!"),
password: string().required("لطفا رمز عبور را وارد کنید!"),
telephone_id: string().required("لطفا شماره تماس داخلی را وارد کنید!"),
});
const {
@@ -32,18 +50,19 @@ const LoginForm = () => {
mode: "all",
});
const handleClickShowPassword = () => setShowPassword((show) => !show);
const onSubmit = async (data) => {
try {
const formData = new FormData();
formData.append("username", data.username);
formData.append("password", data.password);
formData.append("telephone_id", data.telephone_id);
await requestServer(GET_USER_LOGIN_ROUTE, "post", {
data: formData,
});
getUser();
} catch (error) {
console.error("Login error:", error);
}
} catch (error) {}
};
return (
@@ -82,6 +101,15 @@ const LoginForm = () => {
{...field}
label="نام کاربری"
variant="outlined"
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<Person />
</InputAdornment>
),
},
}}
fullWidth
error={!!error}
helperText={!!error && error.message}
@@ -98,7 +126,27 @@ const LoginForm = () => {
{...field}
label="رمزعبور"
variant="outlined"
type="password"
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}
@@ -107,6 +155,32 @@ const LoginForm = () => {
}}
name={"password"}
/>
<Controller
control={control}
render={({ field, fieldState: { error } }) => {
return (
<LtrTextField
{...field}
label="شماره تماس داخلی"
variant="outlined"
type="tel"
slotProps={{
input: {
endAdornment: (
<InputAdornment position="end">
<PhoneCallback />
</InputAdornment>
),
},
}}
fullWidth
error={!!error}
helperText={!!error && error.message}
/>
);
}}
name={"telephone_id"}
/>
</Stack>
<Stack>
<Button

View File

@@ -1,9 +1,9 @@
import StyledForm from "@/core/components/StyledForm";
import LoadingHardPage from "@/core/components/LoadingHardPage";
import { CALL_ACTION } from "@/core/utils/routes";
import { useCall } from "@/lib/contexts/call";
import useRequest from "@/lib/hooks/useRequest";
import { yupResolver } from "@hookform/resolvers/yup";
import { Stack } from "@mui/material";
import { Box, Stack } from "@mui/material";
import { useForm } from "react-hook-form";
import { object, string } from "yup";
import ActionHeader from "./ActionHeader";
@@ -47,9 +47,7 @@ function CallActions({ tab }) {
data: formData,
});
closeCall(tab.id);
} catch (error) {
console.error("Login error:", error);
}
} catch (error) {}
});
return (
@@ -59,10 +57,28 @@ function CallActions({ tab }) {
}}
>
<ActionHeader tab={tab} />
<Stack sx={{ height: "100%", overflowY: "auto" }}>
<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>
);

View File

@@ -0,0 +1,185 @@
"use client";
import {
Chip,
Divider,
FormControl,
FormHelperText,
Grid,
IconButton,
InputAdornment,
InputLabel,
OutlinedInput,
} from "@mui/material";
import StyledForm from "@/core/components/StyledForm";
import { useForm } from "react-hook-form";
import { CHANGE_USER_PASSWORD } from "@/core/utils/routes";
import { object, string } from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { useEffect, useState } from "react";
import Visibility from "@mui/icons-material/Visibility";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import useRequest from "@/lib/hooks/useRequest";
const validationSchema = object({
current_password: string().required("اجباری"),
new_password: string().required("اجباری"),
repeat_new_password: string()
.required("اجباری")
.test(
"password-match",
"رمز عبور جدید و تکرار آن باید یکسان باشند",
function (value) {
return this.parent.new_password === value;
},
),
});
const Form = ({ handleCloseForm, setLoading }) => {
const request = useRequest();
const [showNewPassword, setShowNewPassword] = useState();
const [showRepeatNewPassword, setShowRepeatNewPassword] = useState();
const defaultValues = {
current_password: "",
new_password: "",
repeat_new_password: "",
};
const {
register,
handleSubmit,
formState: { isSubmitting, errors },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
useEffect(() => {
setLoading(isSubmitting);
}, [isSubmitting]);
const onSubmit = async (data) => {
console.log(data);
const formData = new FormData();
formData.append("current_password", data.current_password);
formData.append("new_password", data.new_password);
try {
await request(CHANGE_USER_PASSWORD, "post", { data: formData });
handleCloseForm();
} catch (error) {}
};
return (
<>
<Divider sx={{ mb: 3 }}>
<Chip color="primary" variant="outlined" label="تغییر رمز عبور" />
</Divider>
<StyledForm onSubmit={handleSubmit(onSubmit)} id={"ChangePassword"}>
<Grid container columns={1} spacing={3}>
<Grid size={1}>
<FormControl
error={!!errors.current_password}
size="small"
fullWidth
variant="outlined"
>
<InputLabel htmlFor="current_password">رمز عبور فعلی</InputLabel>
<OutlinedInput
id="current_password"
label="رمز عبور فعلی"
autoComplete="off"
{...register("current_password")}
size="small"
fullWidth
type="text"
/>
<FormHelperText id="current_password">
{errors.current_password
? errors.current_password.message
: null}
</FormHelperText>
</FormControl>
</Grid>
<Grid size={1}>
<FormControl
error={!!errors.new_password}
size="small"
fullWidth
variant="outlined"
>
<InputLabel htmlFor="new_password">رمز عبور جدید</InputLabel>
<OutlinedInput
id="new_password"
autoComplete="off"
{...register("new_password")}
label="رمز عبور جدید"
size="small"
fullWidth
type={showNewPassword ? "text" : "password"}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setShowNewPassword(!showNewPassword)}
onMouseDown={(event) => event.preventDefault()}
edge="end"
>
{showNewPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
}
/>
<FormHelperText id="new_password">
{errors.new_password ? errors.new_password.message : null}
</FormHelperText>
</FormControl>
</Grid>
<Grid size={1}>
<FormControl
error={!!errors.repeat_new_password}
size="small"
fullWidth
variant="outlined"
>
<InputLabel htmlFor="repeat_new_password">
تکرار رمز عبور جدید
</InputLabel>
<OutlinedInput
id="repeat_new_password"
autoComplete="off"
{...register("repeat_new_password")}
size="small"
fullWidth
label="تکرار رمز عبور جدید"
type={showRepeatNewPassword ? "text" : "password"}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() =>
setShowRepeatNewPassword(!showRepeatNewPassword)
}
onMouseDown={(event) => event.preventDefault()}
edge="end"
>
{showRepeatNewPassword ? (
<VisibilityOff />
) : (
<Visibility />
)}
</IconButton>
</InputAdornment>
}
/>
<FormHelperText id="repeat_new_password">
{errors.repeat_new_password
? errors.repeat_new_password.message
: null}
</FormHelperText>
</FormControl>
</Grid>
</Grid>
</StyledForm>
</>
);
};
export default Form;

View File

@@ -0,0 +1,45 @@
"use client";
import { Button, Dialog, DialogActions, DialogContent } from "@mui/material";
import SaveIcon from "@mui/icons-material/Save";
import { useState } from "react";
import Form from "./Form";
const ChangePassword = ({ open, setOpen }) => {
const [loading, setLoading] = useState(false);
const handleCloseForm = () => {
setOpen(false);
};
return (
<Dialog maxWidth="xs" fullWidth={true} open={open}>
<DialogContent dividers>
<Form handleCloseForm={handleCloseForm} setLoading={setLoading} />
</DialogContent>
<DialogActions>
<Button
disabled={loading}
variant="outlined"
color="secondary"
size="large"
onClick={handleCloseForm}
>
بستن
</Button>
<Button
type="submit"
disabled={loading}
form="ChangePassword"
size="large"
autoFocus
variant="contained"
endIcon={<SaveIcon />}
>
تغییر
</Button>
</DialogActions>
</Dialog>
);
};
export default ChangePassword;

View File

@@ -7,6 +7,7 @@ import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew";
import VpnKeyIcon from "@mui/icons-material/VpnKey";
import { Box, Divider, IconButton, Tooltip } from "@mui/material";
import { useState } from "react";
import ChangePassword from "./ChangePassword";
const ProfileActions = () => {
const { getUser, logout } = useAuth();
@@ -27,7 +28,7 @@ const ProfileActions = () => {
return (
<>
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Tooltip title="ویرایش پروفایل" arrow>
{/* <Tooltip title="ویرایش پروفایل" arrow>
<IconButton
aria-label="ویرایش پروفایل"
color="success"
@@ -41,7 +42,7 @@ const ProfileActions = () => {
variant="middle"
flexItem
sx={{ mx: 1 }}
/>
/> */}
<Tooltip title="خروج" arrow>
<IconButton onClick={handleLogout} aria-label="خروج" color="error">
<PowerSettingsNewIcon />
@@ -65,8 +66,8 @@ const ProfileActions = () => {
</IconButton>
</Tooltip>
</Box>
{/* <Update open={openUpdate} setOpen={setOpenUpdate} />
<ChangePass open={openChangePass} setOpen={setOpenChangePass} /> */}
{/* <Update open={openUpdate} setOpen={setOpenUpdate} /> */}
<ChangePassword open={openChangePass} setOpen={setOpenChangePass} />
</>
);
};

View File

@@ -1,6 +1,7 @@
const api = process.env.NEXT_PUBLIC_API_URL;
export const GET_USER_ROUTE = api + "/profile/info";
export const CHANGE_USER_PASSWORD = api + "/profile/change_password";
export const GET_USER_LOGIN_ROUTE = api + "/auth/login";
export const GET_USER_LOGOUT_ROUTE = api + "/auth/logout";
export const GET_PERMISSIONS_ROUTE = "";

View File

@@ -9,19 +9,21 @@ const theme = createTheme({
},
palette: {
primary: {
main: "#0D47A1",
contrastText: "#fff",
main: "#1b4332",
contrastText: "#FFFFFF",
},
secondary: {
main: "#90A4AE",
main: "#1B4043",
contrastText: "#FFFFFF",
},
},
components: {
MuiBackdrop: {
styleOverrides: {
root: {
backdropFilter: "blur(2px)",
backgroundColor: "rgba(0, 0, 0, 0.5)",
backdropFilter: "blur(3px)",
backgroundColor: "rgba(0, 0, 0, 0.3)",
},
},
},

View File

@@ -28,7 +28,6 @@ const useCallerHistory = (callId, phoneNumber, maxSize) => {
setHistoryData(unique);
} catch (error) {
console.error("Error fetching caller history:", error);
setHasError(true);
setHistoryData([]);
} finally {