Merge branch 'feature/edit_profile' into 'develop'

add mui lab and @resolver/yup for react hook form package and complete change...

See merge request witel-front-end/rms!18
This commit is contained in:
AmirHossein Mahmoodi
2024-09-16 06:44:03 +00:00
11 changed files with 546 additions and 139 deletions

View File

@@ -14,7 +14,9 @@
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@hookform/resolvers": "^3.9.0",
"@mui/icons-material": "^5.15.6",
"@mui/lab": "^5.0.0-alpha.173",
"@mui/material": "^5.15.6",
"@mui/material-nextjs": "^5.15.6",
"@mui/x-date-pickers": "^6.19.5",

View File

@@ -0,0 +1,155 @@
"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 useRequest from "@/lib/hooks/useRequest";
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";
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) => {
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, signal: controller.signal });
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 item xs={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 item xs={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 item xs={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

@@ -1,50 +1,35 @@
"use client";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
useMediaQuery,
} from "@mui/material";
import { useTheme } from "@mui/material/styles";
import CloseIcon from "@mui/icons-material/Close";
import { Dialog, DialogActions, DialogContent } from "@mui/material";
import Form from "./Form";
import SaveIcon from "@mui/icons-material/Save";
import { LoadingButton } from "@mui/lab";
import { useState } from "react";
const ChangePass = ({ open, setOpen }) => {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
const [loading, setLoading] = useState(false);
const handleCloseForm = () => {
setOpen(false);
};
return (
<Dialog fullScreen={fullScreen} maxWidth="sm" fullWidth={true} open={open}
onClose={() => {
setOpen(false);
}}>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", mx: 1 }}>
<DialogTitle sx={{ py: 2, px: 1 }}>
تغییر رمز عبور
</DialogTitle>
<IconButton
aria-label="بستن تغییر رمز عبور"
onClick={() => {
setOpen(false);
}}
sx={{ color: (theme) => theme.palette.grey[500] }}
>
<CloseIcon />
</IconButton>
</Box>
<Dialog maxWidth="xs" fullWidth={true} open={open}>
<DialogContent dividers>
تست
<Form handleCloseForm={handleCloseForm} setLoading={setLoading} />
</DialogContent>
<DialogActions>
<Button autoFocus variant="outlined" onClick={() => {
setOpen(false);
}}>
<LoadingButton loading={loading} variant="outlined" color="error" size="large"
onClick={handleCloseForm}>
بستن
</LoadingButton>
<LoadingButton type="submit" loading={loading} form="ChangePassword"
size="large" autoFocus
variant="contained"
loadingPosition="end"
endIcon={<SaveIcon />}>
ثبت
</Button>
</LoadingButton>
</DialogActions>
</Dialog>
);

View File

@@ -0,0 +1,50 @@
"use client";
import { Box, Divider, IconButton, Tooltip } from "@mui/material";
import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew";
import ModeEditIcon from "@mui/icons-material/ModeEdit";
import VpnKeyIcon from "@mui/icons-material/VpnKey";
import { useState } from "react";
import Update from "@/core/components/Profile/Update";
import ChangePass from "@/core/components/Profile/ChangePass";
import { useAuth } from "@/lib/contexts/auth";
const ProfileActions = () => {
const { getUser } = useAuth();
const [openUpdate, setOpenUpdate] = useState(false);
const [openChangePass, setOpenChangePass] = useState(false);
const openUpdateProfile = () => {
getUser();
setOpenUpdate(true);
};
return (
<>
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Tooltip title="ویرایش پروفایل" arrow>
<IconButton aria-label="ویرایش پروفایل" color="success" onClick={openUpdateProfile}>
<ModeEditIcon />
</IconButton>
</Tooltip>
<Divider orientation="vertical" variant="middle" flexItem sx={{ mx: 1 }} />
<Tooltip title="خروج" arrow>
<IconButton aria-label="خروج" color="error">
<PowerSettingsNewIcon />
</IconButton>
</Tooltip>
<Divider orientation="vertical" variant="middle" flexItem sx={{ mx: 1 }} />
<Tooltip title="تغییر رمز عبور" arrow>
<IconButton aria-label="تغییر رمز عبور" color="primary" onClick={() => {
setOpenChangePass(true);
}}>
<VpnKeyIcon />
</IconButton>
</Tooltip>
</Box>
<Update open={openUpdate} setOpen={setOpenUpdate} />
<ChangePass open={openChangePass} setOpen={setOpenChangePass} />
</>
);
};
export default ProfileActions;

View File

@@ -0,0 +1,34 @@
"use client";
import { Avatar, Box, Chip, Divider, Stack, Typography } from "@mui/material";
const ProfileInfo = () => {
return (
<>
<Box sx={{ display: "flex", alignItems: "center", my: 0.5 }}>
<Avatar
alt="User Image"
src=""
sx={{ width: 56, height: 56 }}
/>
<Stack sx={{ ml: 1 }}>
<Box sx={{ display: "flex", alignItems: "end", gap: 0.7 }}>
<Typography variant="h6">حسن محمد زاده عبدالله</Typography>
</Box>
<Typography variant="caption">.: اداره کل ستاد :.</Typography>
</Stack>
</Box>
<Box sx={{ display: "flex", alignItems: "center", my: 0.5 }}>
<Typography variant="button" color="#757575">آخرین ورود</Typography>
<Divider sx={{ mx: 2, flexGrow: 1 }} />
<Typography variant="subtitle2" color="#757575">1 ساعت پیش</Typography>
</Box>
<Box sx={{ my: 0.5 }}>
<Divider>
<Chip size="small" label="userName" color="success" variant="outlined" />
</Divider>
</Box>
</>
);
};
export default ProfileInfo;

View File

@@ -0,0 +1,248 @@
"use client";
import {
Autocomplete,
Avatar,
Backdrop,
Box,
Button,
Chip,
Divider,
FormControl,
FormHelperText,
Grid,
InputLabel,
OutlinedInput,
TextField,
} from "@mui/material";
import StyledForm from "@/core/components/StyledForm";
import useRequest from "@/lib/hooks/useRequest";
import { Controller, useForm } from "react-hook-form";
import { useAuth } from "@/lib/contexts/auth";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import { styled } from "@mui/material/styles";
import { useEffect, useState } from "react";
import { yupResolver } from "@hookform/resolvers/yup";
import { object, string } from "yup";
import { UPDATE_USER_ROUTE } from "@/core/utils/routes";
const degreeItems = ["دیپلم", "کارشناسی", "کارشناسی ارشد", "دکترا"];
const VisuallyHiddenInput = styled("input")({
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: "nowrap",
width: 1,
});
const validationSchema = object({
first_name: string().required("اجباری"),
last_name: string().required("اجباری"),
degree: string().required("اجباری"),
major: string().required("اجباری"),
mobile: string().required("اجباری"),
});
const Form = ({ handleCloseForm, setLoading }) => {
const request = useRequest();
const { user, getUser } = useAuth();
const [uploadBackDrop, setUploadBackDrop] = useState(false);
const defaultValues = {
first_name: user.data.first_name,
last_name: user.data.last_name,
major: user.data.major,
degree: user.data.degree,
mobile: user.data.mobile,
avatar: null,
};
const {
control,
watch,
register,
setValue,
handleSubmit,
formState: { isSubmitting, errors },
} = useForm({ defaultValues, resolver: yupResolver(validationSchema) });
useEffect(() => {
setLoading(isSubmitting);
}, [isSubmitting]);
const onSubmit = async (data) => {
const formData = new FormData();
formData.append("first_name", data.first_name);
formData.append("last_name", data.last_name);
formData.append("major", data.major);
formData.append("degree", data.degree);
formData.append("mobile", data.mobile);
if (data.avatar) {
formData.append("avatar", data.avatar);
}
try {
await request(UPDATE_USER_ROUTE, "post", { data: formData });
getUser();
handleCloseForm();
} catch (error) {
}
};
return (
<>
<Divider sx={{ mb: 3 }}>
<Chip color="success" variant="outlined" label="ویرایش پروفایل" />
</Divider>
<StyledForm onSubmit={handleSubmit(onSubmit)} id={"UpdateProfile"}>
<Box sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
mb: 4,
position: "relative",
borderRadius: "50%",
}}>
<Button
component="label"
sx={{
borderRadius: "50%",
padding: "unset",
"&:hover": {
backgroundColor: "unset",
},
}}
>
<Box
sx={{ borderRadius: "50%" }}
onMouseEnter={() => setUploadBackDrop(true)}
onMouseLeave={() => setUploadBackDrop(false)}
>
<Avatar
src={`${process.env.NEXT_PUBLIC_API_URL}/${user.data.avatar}`}
sx={{ width: 120, height: 120, border: "1px solid #e1e1e1" }}
/>
<Backdrop
sx={{
color: "#fff", zIndex: (theme) => theme.zIndex.drawer + 1,
position: "absolute", width: 120, height: 120,
marginLeft: "auto", marginRight: "auto",
borderRadius: "50%",
}}
open={uploadBackDrop}
>
<CloudUploadIcon sx={{ fontSize: "2rem" }} color="inherit" />
</Backdrop>
</Box>
<VisuallyHiddenInput type="file" accept="image/*"
onChange={(event) => setValue("avatar", event.target.files[0])} />
</Button>
</Box>
<Grid container columns={{ xs: 1, sm: 2 }} spacing={2}>
<Grid item xs={1}>
<FormControl error={!!errors.first_name} size="small" fullWidth variant="outlined">
<InputLabel htmlFor="first_name">نام</InputLabel>
<OutlinedInput
id="first_name"
label="نام"
autoComplete="off"
{...register("first_name")}
size="small"
fullWidth
type="text"
/>
<FormHelperText
id="first_name">{errors.first_name ? errors.first_name.message : null}</FormHelperText>
</FormControl>
</Grid>
<Grid item xs={1}>
<FormControl error={!!errors.last_name} size="small" fullWidth variant="outlined">
<InputLabel htmlFor="last_name">نام خانوادگی</InputLabel>
<OutlinedInput
id="last_name"
label="نام خانوادگی"
autoComplete="off"
{...register("last_name")}
size="small"
fullWidth
type="text"
/>
<FormHelperText
id="last_name">{errors.last_name ? errors.last_name.message : null}</FormHelperText>
</FormControl>
</Grid>
<Grid item xs={1}>
<FormControl error={!!errors.major} size="small" fullWidth variant="outlined">
<InputLabel htmlFor="major">رشته تحصیلی</InputLabel>
<OutlinedInput
id="major"
label="رشته تحصیلی"
autoComplete="off"
{...register("major")}
size="small"
fullWidth
type="text"
/>
<FormHelperText
id="major">{errors.major ? errors.major.message : null}</FormHelperText>
</FormControl>
</Grid>
<Grid item xs={1}>
<Controller
name="degree"
control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl error={!!error} size="small" fullWidth variant="outlined">
<Autocomplete
size="small"
disablePortal
value={value || null}
onChange={(event, newValue) => {
onChange(newValue);
}}
options={degreeItems}
renderInput={(params) => (
<TextField
{...params}
label="مدرک تحصیلی"
placeholder="مدرک تحصیلی را انتخاب کنید"
autoComplete="off"
error={!!error}
InputLabelProps={{ shrink: true }}
/>
)}
/>
<FormHelperText>
{error ? error.message : null}
</FormHelperText>
</FormControl>
)}
/>
</Grid>
<Grid item xs={2}>
<FormControl error={!!errors.mobile} size="small" fullWidth variant="outlined">
<InputLabel htmlFor="mobile">موبایل</InputLabel>
<OutlinedInput
id="mobile"
label="موبایل"
autoComplete="off"
{...register("mobile")}
size="small"
fullWidth
type="text"
/>
<FormHelperText
id="mobile">{errors.mobile ? errors.mobile.message : null}</FormHelperText>
</FormControl>
</Grid>
</Grid>
</StyledForm>
</>
);
};
export default Form;

View File

@@ -1,50 +1,36 @@
"use client";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
useMediaQuery,
} from "@mui/material";
import { Button, Dialog, DialogActions, DialogContent, useMediaQuery } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import CloseIcon from "@mui/icons-material/Close";
import Form from "./Form";
import SaveIcon from "@mui/icons-material/Save";
import { LoadingButton } from "@mui/lab";
import { useState } from "react";
const Update = ({ open, setOpen }) => {
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
const [loading, setLoading] = useState(false);
const handleCloseForm = () => {
setOpen(false);
};
return (
<Dialog fullScreen={fullScreen} maxWidth="sm" fullWidth={true} open={open}
onClose={() => {
setOpen(false);
}}>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", mx: 1 }}>
<DialogTitle sx={{ py: 2, px: 1 }}>
ویرایش پروفایل
</DialogTitle>
<IconButton
aria-label="بستن ویرایش پروفایل"
onClick={() => {
setOpen(false);
}}
sx={{ color: (theme) => theme.palette.grey[500] }}
>
<CloseIcon />
</IconButton>
</Box>
<Dialog fullScreen={fullScreen} maxWidth="sm" fullWidth={true} open={open}>
<DialogContent dividers>
form will come here
<Form handleCloseForm={handleCloseForm} setLoading={setLoading} />
</DialogContent>
<DialogActions>
<Button autoFocus variant="outlined" onClick={() => {
setOpen(false);
}}>
ثبت
<Button autoFocus variant="outlined" color="error" size="large" onClick={handleCloseForm}>
بستن
</Button>
<LoadingButton type="submit" loading={loading} form="UpdateProfile" size="large" autoFocus
variant="contained"
loadingPosition="end"
endIcon={<SaveIcon />}>
ثبت
</LoadingButton>
</DialogActions>
</Dialog>
);

View File

@@ -1,75 +1,20 @@
"use client";
import { Avatar, Box, Chip, Divider, IconButton, Stack, Tooltip, Typography } from "@mui/material";
import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew";
import ModeEditIcon from "@mui/icons-material/ModeEdit";
import VpnKeyIcon from "@mui/icons-material/VpnKey";
import { useState } from "react";
import Update from "@/core/components/Profile/Update";
import ChangePass from "@/core/components/Profile/ChangePass";
import { Stack } from "@mui/material";
import ProfileInfo from "./ProfileInfo";
import ProfileActions from "./ProfileActions";
const Profile = () => {
const [openUpdate, setOpenUpdate] = useState(false);
const [openChangePass, setOpenChangePass] = useState(false);
return (
<>
<Stack sx={{
px: 2,
background: "#f7f7f7",
borderTop: "1px dashed #e1e1e1",
borderBottom: "1px dashed #e1e1e1",
}}>
<Box sx={{ display: "flex", alignItems: "center", my: 0.5 }}>
<Avatar
alt="User Image"
src=""
sx={{ width: 56, height: 56 }}
/>
<Stack sx={{ ml: 1 }}>
<Box sx={{ display: "flex", alignItems: "end", gap: 0.7 }}>
<Typography variant="h6">حسن محمد زاده عبدالله</Typography>
</Box>
<Typography variant="caption">.: اداره کل ستاد :.</Typography>
</Stack>
</Box>
<Box sx={{ display: "flex", alignItems: "center", my: 0.5 }}>
<Typography variant="button" color="#757575">آخرین ورود</Typography>
<Divider sx={{ mx: 2, flexGrow: 1 }} />
<Typography variant="subtitle2" color="#757575">1 ساعت پیش</Typography>
</Box>
<Box sx={{ my: 0.5 }}>
<Divider>
<Chip size="small" label="userName" color="success" variant="outlined" />
</Divider>
</Box>
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Tooltip title="ویرایش پروفایل" arrow>
<IconButton aria-label="ویرایش پروفایل" color="success" onClick={() => {
setOpenUpdate(true);
}}>
<ModeEditIcon />
</IconButton>
</Tooltip>
<Divider orientation="vertical" variant="middle" flexItem sx={{ mx: 1 }} />
<Tooltip title="خروج" arrow>
<IconButton aria-label="خروج" color="error">
<PowerSettingsNewIcon />
</IconButton>
</Tooltip>
<Divider orientation="vertical" variant="middle" flexItem sx={{ mx: 1 }} />
<Tooltip title="تغییر رمز عبور" arrow>
<IconButton aria-label="تغییر رمز عبور" color="primary" onClick={() => {
setOpenChangePass(true);
}}>
<VpnKeyIcon />
</IconButton>
</Tooltip>
</Box>
</Stack>
<Update open={openUpdate} setOpen={setOpenUpdate} />
<ChangePass open={openChangePass} setOpen={setOpenChangePass} />
</>
<Stack sx={{
px: 2,
background: "#f7f7f7",
borderTop: "1px dashed #e1e1e1",
borderBottom: "1px dashed #e1e1e1",
}}>
<ProfileInfo />
<ProfileActions />
</Stack>
);
};
export default Profile;

View File

@@ -1,6 +1,8 @@
const api = process.env.NEXT_PUBLIC_API_URL;
export const GET_USER_ROUTE = api + "/webapi/user/get-permission";
export const GET_USER_ROUTE = api + "/api/v3/profile/info";
export const UPDATE_USER_ROUTE = api + "/api/v3/profile/edit";
export const CHANGE_USER_PASSWORD = api + "/api/v3/profile/change_password";
export const GET_LOGIN_ROUTE = api + "/login_dev";
export const GET_INQUIRY_PRIVACY_FENCING_ROUTE = api + "/api/v3/harim/divarkeshi";
export const GET_PERMISSIONS_ROUTE = api + "/webapi/user/get-permission";