diff --git a/src/components/accessories/admin/users/Users.tsx b/src/components/accessories/admin/users/Users.tsx index a9d074085..44ac4d8b2 100644 --- a/src/components/accessories/admin/users/Users.tsx +++ b/src/components/accessories/admin/users/Users.tsx @@ -9,10 +9,17 @@ import UsersTable from "./usersTable"; import { PATHS } from "../../../../consts"; +import { UserDTO } from "../../../../generated"; + export const Users = () => { const navigate = useNavigate(); const { t } = useTranslation(); + const handleEditUser = (row: UserDTO) => + navigate(PATHS.admin_users_edit.replace(":id", row.userName!), { + state: row, + }); + const [tab, setTab] = useState<"users" | "groups">("users"); return ( <> @@ -38,6 +45,7 @@ export const Users = () => { {t("user.addUser")} } + onEdit={handleEditUser} /> ) : ( diff --git a/src/components/accessories/admin/users/editUser/EditUser.tsx b/src/components/accessories/admin/users/editUser/EditUser.tsx new file mode 100644 index 000000000..09a431d3d --- /dev/null +++ b/src/components/accessories/admin/users/editUser/EditUser.tsx @@ -0,0 +1,64 @@ +import { CircularProgress } from "@mui/material"; +import { useAppDispatch, useAppSelector } from "libraries/hooks/redux"; +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router"; +import { Navigate } from "react-router-dom"; + +import { PATHS } from "../../../../../consts"; +import { UserDTO } from "../../../../../generated"; +import { getUserGroups } from "../../../../../state/usergroups"; +import { getUsers, updateUser, updateUserReset } from "../../../../../state/users"; +import { EditUserForm } from "./EditUserForm"; + +export const EditUser = () => { + const dispatch = useAppDispatch(); + const { id } = useParams(); + const [user, setUser] = useState(); + const [userNotFound, setUserNotFound] = useState(false); + const { isLoading, hasSucceeded, hasFailed, error } = useAppSelector( + (state) => state.users.update + ); + const users = useAppSelector((state) => state.users.userList); + const groups = useAppSelector((state) => state.usergroups.groupList) + + useEffect(() => { + dispatch(getUsers({})) + dispatch(getUserGroups()) + return () => { + dispatch(updateUserReset()); + }; + }, [dispatch]); + + useEffect(() => { + if (users.hasSucceeded) { + const user = users.data?.find(({ userName }) => userName === id); + if (!!user) setUser(user); + else setUserNotFound(true); + } + }, [users.hasSucceeded, users.data, id]); + + const handleUpdate = (user: UserDTO) => { + dispatch(updateUser(user)); + }; + + if(userNotFound) return ; + + if (users.isLoading || groups.isLoading || !users || !user || !groups.data) { + return ; + } + if(!user) { + console.log("user not found") + } + + return ( + + ); +}; diff --git a/src/components/accessories/admin/users/editUser/EditUserForm.tsx b/src/components/accessories/admin/users/editUser/EditUserForm.tsx new file mode 100644 index 000000000..0eab6b457 --- /dev/null +++ b/src/components/accessories/admin/users/editUser/EditUserForm.tsx @@ -0,0 +1,178 @@ +import { Autocomplete } from "@mui/lab"; +import { + FormControl, + FormHelperText, + TextField as MuiTextField, +} from "@mui/material"; +import { useFormik } from "formik"; +import React, { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; + +import { UserDTO, UserGroupDTO } from "../../../../../generated"; + +import checkIcon from "../../../../../assets/check-icon.png"; +import Button from "../../../button/Button"; +import ConfirmationDialog from "../../../confirmationDialog/ConfirmationDialog"; +import InfoBox from "../../../infoBox/InfoBox"; +import TextField from "../../../textField/TextField"; + +import { PATHS } from "../../../../../consts"; +import "./styles.scss"; +import { userSchema } from "./validation"; + +interface IProps { + initialValues: UserDTO; + isLoading: boolean; + hasSucceeded: boolean; + hasFailed: boolean; + error: any; + groups: UserGroupDTO[]; + onSubmit: (userValue: UserDTO) => void; +} + +export const EditUserForm = ({ + initialValues, + isLoading, + hasSucceeded, + hasFailed, + error, + onSubmit, + groups, +}: IProps) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const { + handleSubmit, + handleBlur, + getFieldProps, + isValid, + dirty, + resetForm, + errors, + touched, + values, + setFieldTouched, + setFieldValue, + } = useFormik({ + initialValues, + validationSchema: userSchema(t), + onSubmit, + }); + + return ( + + + + + + setFieldTouched("userGroupName")} + onChange={(_ev: any, value: UserGroupDTO | null) => { + setFieldValue("userGroupName", value); + }} + renderInput={(params) => ( + + )} + getOptionLabel={(option: UserGroupDTO) => + option.code.toString() + + (option.desc ? ` - ${option.desc}` : "") + } + isOptionEqualToValue={(option, value) => option.code === value.code} + /> + {touched.userGroupName && errors.userGroupName && ( + + { + (errors.userGroupName?.code || + errors.userGroupName) as ReactNode + } + + )} + + + + + + + + + + + + + {hasFailed && ( + + + + )} + + + + + {t("common.save")} + + + + { + resetForm(); + }} + > + {t("common.reset")} + + + + + { + navigate(PATHS.admin_users); + }} + handleSecondaryButtonClick={() => ({})} + /> + + ); +}; diff --git a/src/components/accessories/admin/users/editUser/index.ts b/src/components/accessories/admin/users/editUser/index.ts new file mode 100644 index 000000000..541868560 --- /dev/null +++ b/src/components/accessories/admin/users/editUser/index.ts @@ -0,0 +1 @@ +export { EditUser } from "./EditUser"; diff --git a/src/components/accessories/admin/users/editUser/styles.scss b/src/components/accessories/admin/users/editUser/styles.scss new file mode 100644 index 000000000..5ef3e0363 --- /dev/null +++ b/src/components/accessories/admin/users/editUser/styles.scss @@ -0,0 +1,84 @@ +@import "../../../../../styles/variables"; +@import "../../../../../../node_modules/susy/sass/susy"; + +.editUserForm { + display: inline-block; + flex-direction: column; + align-items: center; + width: 100%; + + .formInsertMode { + margin: 0px 0px 20px; + } + + .row { + justify-content: space-between; + } + + .editUserForm__item { + margin: 7px 0px; + padding: 0px 15px; + width: 50%; + @include susy-media($narrow) { + padding: 0px 10px; + } + @include susy-media($tablet_land) { + padding: 0px 10px; + } + @include susy-media($medium-up) { + width: 25%; + } + @include susy-media($tablet_port) { + width: 50%; + } + @include susy-media($smartphone) { + width: 100%; + } + .textField, + .selectField { + width: 100%; + } + + &.fullWidth { + width: 100%; + } + + &.halfWidth { + width: 50%; + @include susy-media($smartphone) { + width: 100%; + } + } + &.thirdWidth { + width: 33%; + @include susy-media($smartphone) { + width: 100%; + } + } + } + + .editUserForm__buttonSet { + display: flex; + margin-top: 25px; + padding: 0px 15px; + flex-direction: row-reverse; + @include susy-media($smartphone_small) { + display: block; + } + + .submit_button, + .reset_button { + .MuiButton-label { + font-size: smaller; + letter-spacing: 1px; + font-weight: 600; + } + button { + @include susy-media($smartphone_small) { + width: 100%; + margin-top: 10px; + } + } + } + } +} diff --git a/src/components/accessories/admin/users/editUser/validation.ts b/src/components/accessories/admin/users/editUser/validation.ts new file mode 100644 index 000000000..019e38aca --- /dev/null +++ b/src/components/accessories/admin/users/editUser/validation.ts @@ -0,0 +1,23 @@ +import { TFunction } from "react-i18next"; +import { object, string } from "yup"; +import { UserDTO, UserGroupDTO } from "../../../../../generated"; + +// min 5 characters, 1 upper case letter, 1 lower case letter, 1 numeric digit. +export const passwordRules = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{5,}$/; + +export const userSchema = (t: TFunction<"translation">) => + object().shape>({ + userGroupName: object({ + code: string().required(t("user.validateUserNeedsGroup")), + desc: string(), + }) + .nullable() + .required(t("user.validateUserNeedsGroup")), + passwd: string() + .required(t("user.validatePasswordNeeded")) + .min(5, t("user.validatePasswordTooShort")) + .matches(passwordRules, { + message: t("user.validatePasswordTooWeak"), + }), + desc: string(), + }); diff --git a/src/components/accessories/admin/users/index.ts b/src/components/accessories/admin/users/index.ts index 10a0ec7f0..70a5ccbc9 100644 --- a/src/components/accessories/admin/users/index.ts +++ b/src/components/accessories/admin/users/index.ts @@ -1,2 +1,3 @@ -export { Users } from "./Users"; +export { EditUser } from "./editUser"; export { NewUser } from "./newUser"; +export { Users } from "./Users"; diff --git a/src/components/accessories/admin/users/usersTable/UsersTable.tsx b/src/components/accessories/admin/users/usersTable/UsersTable.tsx index f76cb76cb..2aa37b877 100644 --- a/src/components/accessories/admin/users/usersTable/UsersTable.tsx +++ b/src/components/accessories/admin/users/usersTable/UsersTable.tsx @@ -3,6 +3,7 @@ import React, { ReactNode, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useAppDispatch, useAppSelector } from "libraries/hooks/redux"; +import { usePermission } from "libraries/permissionUtils/usePermission"; import { UserDTO } from "../../../../../generated"; import { getUsers } from "../../../../../state/users"; import InfoBox from "../../../infoBox/InfoBox"; @@ -13,11 +14,13 @@ import classes from "./UsersTable.module.scss"; interface IOwnProps { headerActions: ReactNode; + onEdit: (row: UserDTO) => void; } -export const UsersTable = ({ headerActions }: IOwnProps) => { +export const UsersTable = ({ headerActions, onEdit }: IOwnProps) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); + const canUpdate = usePermission("users.update"); useEffect(() => { dispatch(getUsers({})); @@ -47,6 +50,7 @@ export const UsersTable = ({ headerActions }: IOwnProps) => { userGroupName: item.userGroupName?.desc ?? item.userGroupName?.code ?? "", desc: item.desc ?? "", + passwd: item.passwd ?? "" }; }); }; @@ -78,6 +82,7 @@ export const UsersTable = ({ headerActions }: IOwnProps) => { rawData={data} rowKey="userName" headerActions={headerActions} + onEdit={canUpdate ? onEdit: undefined} /> ); case "SUCCESS_EMPTY": diff --git a/src/consts.ts b/src/consts.ts index 91fe44272..1974a2ef9 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -25,6 +25,7 @@ export const PATHS = { admin_vaccines_types_edit: "/admin/types/vaccines/:code/edit", admin_users: "/admin/users", admin_users_new: "/admin/users/new", + admin_users_edit: "/admin/users/:id/edit", admin_vaccines: "/admin/vaccines", admin_vaccines_new: "/admin/vaccines/new", admin_vaccines_edit: "/admin/vaccines/:code/edit", diff --git a/src/routes/Admin/AdminRoutes.tsx b/src/routes/Admin/AdminRoutes.tsx index 2e092a382..c3ac713e8 100644 --- a/src/routes/Admin/AdminRoutes.tsx +++ b/src/routes/Admin/AdminRoutes.tsx @@ -17,7 +17,7 @@ import { NewSupplier, Suppliers, } from "../../components/accessories/admin/suppliers"; -import { NewUser, Users } from "../../components/accessories/admin/users"; +import { EditUser, NewUser, Users } from "../../components/accessories/admin/users"; import { EditVaccine, NewVaccine, @@ -201,6 +201,11 @@ export const AdminRoutes = () => { element: ( } /> ), + },{ + path: getPath(PATHS.admin_users_edit), + element: ( + } /> + ), }, { path: getPath(PATHS.admin_types),