diff --git a/client/src/Components/Charts/StatusPageBarChart/index.jsx b/client/src/Components/Charts/StatusPageBarChart/index.jsx index b86f19c77..e8a34806d 100644 --- a/client/src/Components/Charts/StatusPageBarChart/index.jsx +++ b/client/src/Components/Charts/StatusPageBarChart/index.jsx @@ -22,7 +22,7 @@ const Bar = ({ width, height, backgroundColor, borderRadius, children }) => { return ( }; Check.propTypes = { - text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, + text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), noHighlightText: PropTypes.string, variant: PropTypes.oneOf(["info", "error", "success"]), outlined: PropTypes.bool, diff --git a/client/src/Components/Dialog/genericDialog.jsx b/client/src/Components/Dialog/genericDialog.jsx index b2cbe1299..a13eba849 100644 --- a/client/src/Components/Dialog/genericDialog.jsx +++ b/client/src/Components/Dialog/genericDialog.jsx @@ -2,7 +2,7 @@ import { useId } from "react"; import PropTypes from "prop-types"; import { Modal, Stack, Typography } from "@mui/material"; -const GenericDialog = ({ title, description, open, onClose, theme, children }) => { +const GenericDialog = ({ title, description, open, onClose, theme, children, width }) => { const titleId = useId(); const descriptionId = useId(); const ariaDescribedBy = description?.length > 0 ? descriptionId : ""; @@ -16,6 +16,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) = > {title} @@ -46,6 +48,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) = {description} @@ -64,6 +67,7 @@ GenericDialog.propTypes = { theme: PropTypes.object.isRequired, children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]) .isRequired, + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]), }; export { GenericDialog }; diff --git a/client/src/Hooks/inviteHooks.js b/client/src/Hooks/inviteHooks.js index 705906efa..fb577272a 100644 --- a/client/src/Hooks/inviteHooks.js +++ b/client/src/Hooks/inviteHooks.js @@ -14,25 +14,25 @@ const useGetInviteToken = () => { const clearToken = () => { setToken(undefined); }; - + const fetchToken = async (email, role) => { + const response = await networkService.requestInvitationToken({ email, role }); + const token = response?.data?.data?.token; + if (typeof token === "undefined") { + throw new Error(t("inviteNoTokenFound")); + } + return token; + }; const getInviteToken = async ({ email, role }) => { try { - const response = await networkService.requestInvitationToken({ - email, - role, - }); - const token = response?.data?.data?.token; - if (typeof token === "undefined") { - throw new Error(t("inviteNoTokenFound")); - } - + setIsLoading(true); + const token = await fetchToken(email, role); let inviteLink = token; if (typeof CLIENT_HOST !== "undefined") { inviteLink = `${CLIENT_HOST}/register/${token}`; } - setToken(inviteLink); + return token; } catch (error) { setError(error); } finally { @@ -40,7 +40,26 @@ const useGetInviteToken = () => { } }; - return [getInviteToken, clearToken, isLoading, error, token]; + const addTeamMember = async (formData, role) => { + try { + setIsLoading(true); + const token = await fetchToken(formData.email, role); + const toSubmit = { + ...formData, + inviteToken: token, + }; + delete toSubmit.confirm; + const responseRegister = await networkService.registerUser(toSubmit); + return responseRegister; + } catch (error) { + setError(error); + throw error; + } finally { + setIsLoading(false); + } + }; + + return [getInviteToken, clearToken, isLoading, error, token, addTeamMember]; }; export { useGetInviteToken }; diff --git a/client/src/Pages/Account/components/AddMemberMenu/index.jsx b/client/src/Pages/Account/components/AddMemberMenu/index.jsx new file mode 100644 index 000000000..ab1ba599d --- /dev/null +++ b/client/src/Pages/Account/components/AddMemberMenu/index.jsx @@ -0,0 +1,68 @@ +import { useState } from "react"; +import Button from "@mui/material/Button"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { useTheme } from "@emotion/react"; +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import { useTranslation } from "react-i18next"; +import Proptypes from "prop-types"; + +const AddMemberMenu = ({ handleInviteOpen, handleIsRegisterOpen }) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const { t } = useTranslation(); + const theme = useTheme(); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <> + + + { + handleClose(); + handleInviteOpen(); + }} + > + {t("teamPanel.inviteTeamMember")} + + { + handleClose(); + handleIsRegisterOpen(true); + }} + > + {t("teamPanel.register")} + + + + ); +}; + +AddMemberMenu.propTypes = { + handleInviteOpen: Proptypes.func.isRequired, + handleIsRegisterOpen: Proptypes.func.isRequired, +}; + +export default AddMemberMenu; diff --git a/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx b/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx new file mode 100644 index 000000000..e42c504b2 --- /dev/null +++ b/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { newOrChangedCredentials } from "../../../../../Validation/validation"; +import { useTranslation } from "react-i18next"; +const useAddTeamMember = () => { + const { t } = useTranslation(); + const [errors, setErrors] = useState({}); + + const clearErrors = () => setErrors({}); + + const validateFields = (name, value, formData) => { + const { error } = newOrChangedCredentials.validate( + { [name]: value }, + { abortEarly: false, context: { password: formData.password } } + ); + + setErrors((prev) => ({ + ...prev, + [name]: error?.details?.[0]?.message || "", + })); + }; + + const validateForm = (formData, role) => { + const { error } = newOrChangedCredentials.validate(formData, { + abortEarly: false, + context: { password: formData.password }, + }); + const formErrors = {}; + if (error) { + for (const err of error.details) { + formErrors[err.path[0]] = err.message; + } + } + if (!role[0] || role.length === 0) { + formErrors.role = t( + "teamPanel.registerTeamMember.auth.common.inputs.role.errors.empty" + ); + } + if (Object.keys(formErrors).length > 0) { + setErrors(formErrors); + return false; + } + setErrors({}); + return true; + }; + + return { errors, setErrors, clearErrors, validateFields, validateForm }; +}; +export default useAddTeamMember; diff --git a/client/src/Pages/Account/components/AddTeamMember/index.jsx b/client/src/Pages/Account/components/AddTeamMember/index.jsx new file mode 100644 index 000000000..2df8fb226 --- /dev/null +++ b/client/src/Pages/Account/components/AddTeamMember/index.jsx @@ -0,0 +1,213 @@ +import { Button, Stack } from "@mui/material"; +import { GenericDialog } from "../../../../Components/Dialog/genericDialog"; +import TextInput from "../../../../Components/Inputs/TextInput"; +import Select from "../../../../Components/Inputs/Select"; +import { useGetInviteToken } from "../../../../Hooks/inviteHooks"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { createToast } from "../../../../Utils/toastUtils"; +import { useState } from "react"; +import PasswordTooltip from "../../../Auth/components/PasswordTooltip"; +import useAddTeamMember from "./hooks/useAddTeamMember"; +import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback"; +import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; +import PropTypes from "prop-types"; + +const INITIAL_FORM_STATE = { + firstName: "", + lastName: "", + email: "", + password: "", + confirm: "", + teamId: "", +}; + +const INITIAL_ROLE_STATE = ["user"]; +const AddTeamMember = ({ handleIsRegisterOpen, isRegisterOpen, onMemberAdded }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { errors, setErrors, clearErrors, validateFields, validateForm } = + useAddTeamMember(); + const { feedback, handlePasswordFeedback } = usePasswordFeedback(); + const [getInviteToken, clearToken, isLoading, error, token, addTeamMember] = + useGetInviteToken(); + const [form, setForm] = useState(INITIAL_FORM_STATE); + const [role, setRole] = useState(INITIAL_ROLE_STATE); + const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); + const closeAddMemberModal = () => { + handleIsRegisterOpen(false); + setForm(INITIAL_FORM_STATE); + setRole(INITIAL_ROLE_STATE); + clearErrors(); + clearToken(); + }; + + const onChange = (e) => { + let { name, value } = e.target; + if (name === "email") value = value.toLowerCase(); + const updatedForm = { ...form, [name]: value }; + validateFields(name, value, updatedForm); + setForm(updatedForm); + + if (name === "password" || name === "confirm") { + handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors); + } + }; + + const onsubmitAddMember = async (event) => { + event.preventDefault(); + if (!validateForm(form, role)) return; + try { + setIsLoadingSubmit(true); + await addTeamMember(form, role); + createToast({ + body: t("teamPanel.registerToast.success"), + }); + onMemberAdded(); + closeAddMemberModal(); + } catch (error) { + const errorMsg = error.response?.data?.msg || error.message || "unknownError"; + createToast({ + type: "error", + body: t(errorMsg), + }); + } finally { + setIsLoadingSubmit(false); + } + }; + const tErr = (key) => (key ? t([`teamPanel.registerTeamMember.${key}`, key]) : ""); + return ( + <> + + + + + + + +