diff --git a/client/src/Components/v2/inputs/Dialog.tsx b/client/src/Components/v2/inputs/Dialog.tsx index 717126edb..b581e7390 100644 --- a/client/src/Components/v2/inputs/Dialog.tsx +++ b/client/src/Components/v2/inputs/Dialog.tsx @@ -23,6 +23,7 @@ export const DialogInput = ({ children, maxWidth, fullWidth = false, + additionalButtons, }: { open: boolean; title?: string; @@ -37,6 +38,7 @@ export const DialogInput = ({ children?: ReactNode; maxWidth?: DialogProps["maxWidth"]; fullWidth?: boolean; + additionalButtons?: ReactNode; }) => { const { t } = useTranslation(); return ( @@ -59,6 +61,7 @@ export const DialogInput = ({ > {cancelText ?? t("common.buttons.cancel")} + {additionalButtons} {onConfirm && ( + + ); +}; diff --git a/client/src/Pages/Account/TabProfile.tsx b/client/src/Pages/Account/TabProfile.tsx new file mode 100644 index 000000000..8ea8a50a0 --- /dev/null +++ b/client/src/Pages/Account/TabProfile.tsx @@ -0,0 +1,139 @@ +import { Stack } from "@mui/material"; +import { ConfigBox } from "@/Components/v2/design-elements"; +import { TextField, Button } from "@/Components/v2/inputs"; +import { ImageUpload } from "@/Components/v2/inputs"; + +import { useTheme } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useForm, Controller } from "react-hook-form"; +import { useSelector, useDispatch } from "react-redux"; +import { useProfileForm } from "@/Hooks/useProfileForm"; +import { usePatch } from "@/Hooks/UseApi"; +import { setUser } from "@/Features/Auth/authSlice"; +import type { ProfileFormData } from "@/Validation/profile"; +import type { RootState } from "@/Types/state"; +import type { User } from "@/Types/User"; + +export const TabProfile = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const dispatch = useDispatch(); + const user = useSelector((state: RootState) => state.auth?.user); + const { resolver, defaults } = useProfileForm(); + const { patch, loading: patchLoading } = usePatch(); + + const { control, handleSubmit, setValue, watch } = useForm({ + resolver, + defaultValues: { + firstName: user?.firstName ?? defaults.firstName, + lastName: user?.lastName ?? defaults.lastName, + profileImage: defaults.profileImage, + deleteProfileImage: defaults.deleteProfileImage, + }, + }); + + const currentImage = watch("profileImage"); + const deleteImage = watch("deleteProfileImage"); + + const getCurrentImageSrc = () => { + if (deleteImage) return undefined; + if (currentImage) return URL.createObjectURL(currentImage); + if (user?.avatarImage) return `data:image/png;base64,${user.avatarImage}`; + return undefined; + }; + + const handleImageChange = ( + fileObj: { src: string; name: string; file: File } | undefined + ) => { + if (fileObj) { + setValue("profileImage", fileObj.file); + setValue("deleteProfileImage", false); + } else { + setValue("profileImage", null); + setValue("deleteProfileImage", true); + } + }; + + const onSubmit = async (data: ProfileFormData) => { + const fd = new FormData(); + fd.append("firstName", data.firstName); + fd.append("lastName", data.lastName); + + if (data.profileImage) { + fd.append("profileImage", data.profileImage); + } + + if (data.deleteProfileImage) { + fd.append("deleteProfileImage", "true"); + } + + const result = await patch("/auth/user", fd); + if (result?.success && result.data) { + dispatch(setUser(result.data)); + } + }; + + return ( + + + ( + + )} + /> + ( + + )} + /> + + } + /> + + } + /> + + + ); +}; diff --git a/client/src/Pages/Account/TabTeam.tsx b/client/src/Pages/Account/TabTeam.tsx new file mode 100644 index 000000000..4eb2f8a97 --- /dev/null +++ b/client/src/Pages/Account/TabTeam.tsx @@ -0,0 +1,56 @@ +import { Stack } from "@mui/material"; +import { useTheme } from "@mui/material"; +import { useState, useMemo } from "react"; +import { HeaderTeamControls } from "./components/HeaderTeamControls"; +import { TeamTable } from "./components/TeamTable"; +import { InviteTeamMemberDialog } from "./components/InviteTeamMemberDialog"; +import { AddTeamMemberDialog } from "./components/AddTeamMemberDialog"; +import { useGet } from "@/Hooks/UseApi"; +import type { User, UserRole } from "@/Types/User"; + +export const TabTeam = () => { + const theme = useTheme(); + const [filter, setFilter] = useState(""); + const [inviteDialogOpen, setInviteDialogOpen] = useState(false); + const [addMemberDialogOpen, setAddMemberDialogOpen] = useState(false); + + const { data: users, refetch } = useGet("/auth/users"); + + const filteredUsers = useMemo(() => { + if (!users) return []; + if (!filter) return users; + + return users.filter((u) => u.role.includes(filter)); + }, [users, filter]); + + const handleOpenInviteDialog = () => setInviteDialogOpen(true); + const handleCloseInviteDialog = () => setInviteDialogOpen(false); + + const handleOpenAddMemberDialog = () => setAddMemberDialogOpen(true); + const handleCloseAddMemberDialog = () => setAddMemberDialogOpen(false); + + const handleRefetch = () => { + refetch(); + }; + + return ( + + + + + + + ); +}; diff --git a/client/src/Pages/Account/components/AddMemberMenu/index.jsx b/client/src/Pages/Account/components/AddMemberMenu/index.jsx deleted file mode 100644 index 96bea3522..000000000 --- a/client/src/Pages/Account/components/AddMemberMenu/index.jsx +++ /dev/null @@ -1,74 +0,0 @@ -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 Icon from "@/Components/v1/Icon"; -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 deleted file mode 100644 index 1237777ff..000000000 --- a/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useState } from "react"; -import { newOrChangedCredentials } from "../../../../../Validation/validation.js"; -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 deleted file mode 100644 index e5280af16..000000000 --- a/client/src/Pages/Account/components/AddTeamMember/index.jsx +++ /dev/null @@ -1,213 +0,0 @@ -import { Button, Stack } from "@mui/material"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import { useGetInviteToken } from "../../../../Hooks/inviteHooks.js"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import { useState } from "react"; -import PasswordTooltip from "../../../Auth/components/PasswordTooltip.jsx"; -import useAddTeamMember from "./hooks/useAddTeamMember.jsx"; -import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback.jsx"; -import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -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 ( - <> - - - - - - - - field.onChange([e.target.value])} + fieldLabel={t("pages.account.team.invite.role.label")} + placeholder={t("pages.account.team.invite.role.placeholder")} + fullWidth + > + {roleOptions.map((option) => ( + + {option.label} + + ))} + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + ); +}; diff --git a/client/src/Pages/Account/components/HeaderTeamControls.tsx b/client/src/Pages/Account/components/HeaderTeamControls.tsx new file mode 100644 index 000000000..57f000170 --- /dev/null +++ b/client/src/Pages/Account/components/HeaderTeamControls.tsx @@ -0,0 +1,80 @@ +import Stack from "@mui/material/Stack"; +import MenuItem from "@mui/material/MenuItem"; +import { Button, Select } from "@/Components/v2/inputs"; +import { Icon } from "@/Components/v2/design-elements"; +import { UserPlus, Mail } from "lucide-react"; + +import { UserRoles } from "@/Types/User"; +import { useTheme } from "@mui/material/styles"; +import type { UserRole } from "@/Types/User"; +import { useTranslation } from "react-i18next"; +import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin"; + +interface HeaderTeamControlsProps { + filter: UserRole | ""; + onFilterChange: (value: UserRole | "") => void; + onInviteClick?: () => void; + onAddMemberClick?: () => void; +} + +export const HeaderTeamControls = ({ + filter, + onFilterChange, + onInviteClick, + onAddMemberClick, +}: HeaderTeamControlsProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const isAdmin = useIsAdmin(); + const isSuperAdmin = useIsSuperAdmin(); + + const handleFilterChange = (event: { target: { value: unknown } }) => { + onFilterChange(event.target.value as UserRole | ""); + }; + + return ( + + + {isAdmin && ( + + )} + {isSuperAdmin && ( + + )} + + ); +}; diff --git a/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx new file mode 100644 index 000000000..24f1f58f9 --- /dev/null +++ b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx @@ -0,0 +1,164 @@ +import { Stack } from "@mui/material"; +import MenuItem from "@mui/material/MenuItem"; +import { useTheme, FormHelperText, Typography } from "@mui/material"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useForm, Controller } from "react-hook-form"; +import { Dialog, TextField, Select, Button } from "@/Components/v2/inputs"; +import type { UserRole } from "@/Types/User"; +import { useInviteForm } from "@/Hooks/useInviteForm"; +import type { InviteFormData } from "@/Validation/invite"; +import { usePost } from "@/Hooks/UseApi"; + +const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST; + +interface InviteResponse { + token: string; +} + +interface InviteTeamMemberDialogProps { + open: boolean; + onClose: () => void; +} + +export const InviteTeamMemberDialog = ({ + open, + onClose, +}: InviteTeamMemberDialogProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { resolver, defaults } = useInviteForm(); + + const { control, handleSubmit, reset } = useForm({ + resolver, + defaultValues: defaults, + values: defaults, + }); + + const { post: generateToken, loading: generateLoading } = usePost< + InviteFormData, + InviteResponse + >(); + const { post: sendInvite, loading: sendLoading } = usePost< + InviteFormData, + InviteResponse + >(); + const [inviteLink, setInviteLink] = useState(null); + + const roleOptions: { value: UserRole; label: string }[] = [ + { value: "admin", label: t("common.auth.roles.admin") }, + { value: "user", label: t("common.auth.roles.user") }, + ]; + + const handleGenerateToken = async (data: InviteFormData) => { + const result = await generateToken("/invite", data); + if (result?.data?.token) { + const token = result.data.token; + const link = CLIENT_HOST ? `${CLIENT_HOST}/register/${token}` : token; + setInviteLink(link); + } + }; + + const handleSendInvite = async (data: InviteFormData) => { + const result = await sendInvite("/invite/send", data); + if (result?.success) { + handleClose(); + } + }; + + const handleClose = () => { + reset(); + setInviteLink(null); + onClose(); + }; + + return ( + + {t("common.buttons.generateToken")} + + } + > + + ( + + )} + /> + ( + <> + + {fieldState.error && ( + {fieldState.error.message} + )} + + )} + /> + {inviteLink && ( + <> + + {t("pages.account.team.invite.linkLabel")} + + + + )} + + + ); +}; diff --git a/client/src/Pages/Account/components/PasswordPanel.jsx b/client/src/Pages/Account/components/PasswordPanel.jsx deleted file mode 100644 index b2a7d8ee0..000000000 --- a/client/src/Pages/Account/components/PasswordPanel.jsx +++ /dev/null @@ -1,302 +0,0 @@ -import TabPanel from "@mui/lab/TabPanel"; -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography, Button } from "@mui/material"; -import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { newOrChangedCredentials } from "../../../Validation/validation.js"; -import Alert from "@/Components/v1/Alert/index.jsx"; -import { update } from "../../../Features/Auth/authSlice.js"; -import { useDispatch, useSelector } from "react-redux"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { getTouchedFieldErrors } from "../../../Validation/error.js"; -import { useTranslation } from "react-i18next"; -import { useNavigate, useLocation } from "react-router-dom"; -import { clearAuthState } from "../../../Features/Auth/authSlice.js"; -import TextLink from "@/Components/v1/TextLink/index.jsx"; - -const defaultPasswordsState = { - password: "", - newPassword: "", - confirm: "", -}; - -/** - * PasswordPanel component manages the form for editing password. - * - * @returns {JSX.Element} - */ - -const PasswordPanel = () => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const location = useLocation(); - - const SPACING_GAP = theme.spacing(12); - - //redux state - const { isLoading, authToken } = useSelector((state) => state.auth); - - const idToName = { - "edit-current-password": "password", - "edit-new-password": "newPassword", - "edit-confirm-password": "confirm", - }; - - const [localData, setLocalData] = useState(defaultPasswordsState); - const [errors, setErrors] = useState(defaultPasswordsState); - const [touchedFields, setTouchedFields] = useState({ - password: false, - newPassword: false, - confirm: false, - }); - - const handleChange = (event) => { - const { value, id } = event.target; - const name = idToName[id]; - - const updatedData = { - ...localData, - [name]: value, - }; - const updatedTouchedFields = { - ...touchedFields, - [name]: true, - }; - - const validation = newOrChangedCredentials.validate( - { ...updatedData }, - { abortEarly: false, context: { password: updatedData.newPassword } } - ); - - const updatedErrors = getTouchedFieldErrors(validation, updatedTouchedFields); - - if (!touchedFields[name]) { - setTouchedFields(updatedTouchedFields); - } - - setLocalData(updatedData); - setErrors(updatedErrors); - }; - - const handleSubmit = async (event) => { - event.preventDefault(); - - // Check for expired/invalid token before submitting - if (!authToken) { - createToast({ - body: "Your session has expired. Please log in again.", - }); - dispatch(clearAuthState()); - navigate("/login"); - return; - } - - const { error } = newOrChangedCredentials.validate(localData, { - abortEarly: false, - context: { password: localData.newPassword }, - }); - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - } else { - const action = await dispatch(update({ localData })); - if (action.payload.success) { - createToast({ - body: t( - "passwordPanel.passwordChangedSuccess", - "Your password was changed successfully." - ), - }); - setLocalData({ - password: "", - newPassword: "", - confirm: "", - }); - } else { - // TODO: Check for other errors? - createToast({ - body: t( - "passwordPanel.passwordInputIncorrect", - "Your password input was incorrect." - ), - }); - setErrors({ password: "*" + action.payload.msg + "." }); - } - } - }; - - return ( - - - - - ); -}; - -PasswordPanel.propTypes = { - // No props are being passed to this component, hence no specific PropTypes are defined. -}; - -export default PasswordPanel; diff --git a/client/src/Pages/Account/components/ProfilePanel.jsx b/client/src/Pages/Account/components/ProfilePanel.jsx deleted file mode 100644 index dfe508879..000000000 --- a/client/src/Pages/Account/components/ProfilePanel.jsx +++ /dev/null @@ -1,437 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { useState } from "react"; -import TabPanel from "@mui/lab/TabPanel"; -import { Box, Button, Divider, Stack, Typography } from "@mui/material"; -import Avatar from "@/Components/v1/Avatar/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx"; -import { newOrChangedCredentials } from "../../../Validation/validation.js"; -import { useDispatch, useSelector } from "react-redux"; -import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice.js"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { logger } from "../../../Utils/Logger.js"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import { useTranslation } from "react-i18next"; - -/** - * ProfilePanel component displays a form for editing user profile information - * and allows for actions like updating profile picture, credentials, - * and deleting account. - * - * @returns {JSX.Element} - */ - -const ProfilePanel = () => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - const SPACING_GAP = theme.spacing(12); - - //redux state - const { user, isLoading } = useSelector((state) => state.auth); - - const idToName = { - "edit-first-name": "firstName", - "edit-last-name": "lastName", - // Disabled for now, will revisit in the future - // "edit-email": "email", - }; - - // Local state for form data, errors, and file handling - const [localData, setLocalData] = useState({ - firstName: user.firstName, - lastName: user.lastName, - // email: user.email, // Disabled for now - }); - const [errors, setErrors] = useState({}); - const [file, setFile] = useState(); - - // Handles input field changes and performs validation - const handleChange = (event) => { - errors["unchanged"] && clearError("unchanged"); - const { value, id } = event.target; - const name = idToName[id]; - setLocalData((prev) => ({ - ...prev, - [name]: value, - })); - - validateField({ [name]: value }, newOrChangedCredentials, name); - }; - - // Validates input against provided schema and updates error state - const validateField = (toValidate, schema, name = "picture") => { - const { error } = schema.validate(toValidate, { abortEarly: false }); - setErrors((prev) => { - const prevErrors = { ...prev }; - if (error) prevErrors[name] = error.details[0].message; - else delete prevErrors[name]; - return prevErrors; - }); - if (error) return true; - }; - - // Clears specific error from errors state - const clearError = (err) => { - setErrors((prev) => { - const updatedErrors = { ...prev }; - if (updatedErrors[err]) delete updatedErrors[err]; - return updatedErrors; - }); - }; - - // Resets picture-related states and clears interval - const removePicture = () => { - errors["picture"] && clearError("picture"); - setFile(undefined); - setLocalData((prev) => ({ - ...prev, - file: undefined, - deleteProfileImage: true, - })); - }; - - // Opens the picture update modal - const openPictureModal = () => { - setIsOpen("picture"); - setFile(undefined); - }; - - // Closes the picture update modal and resets related states - const closePictureModal = () => { - if (errors["picture"]) clearError("picture"); - setFile(undefined); - setIsOpen(""); - }; - - // Updates profile image displayed on UI - const handleUpdatePicture = () => { - setLocalData((prev) => ({ - ...prev, - file: file?.src, - deleteProfileImage: false, - })); - setIsOpen(""); - if (errors["unchanged"]) clearError("unchanged"); - }; - - // Handles form submission to update user profile - const handleSaveProfile = async (event) => { - event.preventDefault(); - const nameChanged = - localData.firstName !== user.firstName || localData.lastName !== user.lastName; - - const avatarChanged = - localData.deleteProfileImage === true || - (localData.file && localData.file !== `data:image/png;base64,${user.avatarImage}`); - - if (!nameChanged && !avatarChanged) { - createToast({ - body: "Unable to update profile — no changes detected.", - }); - setErrors({ unchanged: "unable to update profile" }); - return; - } - - const action = await dispatch(update({ localData })); - if (action.payload.success) { - createToast({ - body: "Your profile data was changed successfully.", - }); - } else { - createToast({ - body: "There was an error updating your profile data.", - }); - } - }; - - // Removes current profile image from UI - const handleDeletePicture = () => { - setLocalData((prev) => ({ - ...prev, - deleteProfileImage: true, - })); - setFile(undefined); - errors["unchanged"] && clearError("unchanged"); - }; - - // Initiates the account deletion process - const handleDeleteAccount = async () => { - const action = await dispatch(deleteUser()); - if (action.payload.success) { - dispatch(clearAuthState()); - } else { - if (action.payload) { - // dispatch errors - createToast({ - body: action.payload.msg, - }); - } else { - // unknown errors - createToast({ - body: "Unknown error.", - }); - } - } - }; - - // Modal state and control functions - const [isOpen, setIsOpen] = useState(""); - const isModalOpen = (name) => isOpen === name; - - return ( - - - - {/* This 0.9 is a bit magic numbering, refactor */} - - {t("FirstName")} - - - - - - {t("LastName")} - - - - - - {t("auth.common.inputs.email.label")} - - {t("EmailDescriptionText")} - - - logger.warn("disabled")} - disabled={true} - flex={1} - /> - - - - {t("YourPhoto")} - - {t("PhotoDescriptionText")} - - - - - - - - - - - ); -}; - -ProfilePanel.propTypes = { - // No props are being passed to this component, hence no specific PropTypes are defined. -}; - -export default ProfilePanel; diff --git a/client/src/Pages/Account/components/TeamPanel.jsx b/client/src/Pages/Account/components/TeamPanel.jsx deleted file mode 100644 index 6c658cc60..000000000 --- a/client/src/Pages/Account/components/TeamPanel.jsx +++ /dev/null @@ -1,360 +0,0 @@ -import { useTheme } from "@emotion/react"; -import TabPanel from "@mui/lab/TabPanel"; -import { Button, ButtonGroup, Stack, Typography } from "@mui/material"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { newOrChangedCredentials } from "../../../Validation/validation.js"; -import { networkService } from "../../../main.jsx"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import AddTeamMember from "./AddTeamMember/index.jsx"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import { useGetInviteToken } from "../../../Hooks/inviteHooks.js"; -import { useNavigate } from "react-router-dom"; -import { useIsSuperAdmin } from "@/Hooks/useIsAdmin.js"; -import AddMemberMenu from "./AddMemberMenu/index.jsx"; -/** - * TeamPanel component manages the organization and team members, - * providing functionalities like renaming the organization, managing team members, - * and inviting new members. - * - * @returns {JSX.Element} - */ - -const TeamPanel = () => { - const theme = useTheme(); - const { t } = useTranslation(); - const SPACING_GAP = theme.spacing(12); - - const [toInvite, setToInvite] = useState({ - email: "", - role: ["user"], - }); - const [data, setData] = useState([]); - const [members, setMembers] = useState([]); - const [filter, setFilter] = useState("all"); - const [isDisabled, setIsDisabled] = useState(true); - const [errors, setErrors] = useState({}); - const [isSendingInvite, setIsSendingInvite] = useState(false); - const navigate = useNavigate(); - const [getInviteToken, clearToken, isLoading, error, token] = useGetInviteToken(); - const isSuperAdmin = useIsSuperAdmin(); - - const headers = [ - { - id: "name", - content: t("teamPanel.table.name"), - render: (row) => { - return ( - - - {row.firstName + " " + row.lastName} - - - {t("teamPanel.table.created")}{" "} - {new Date(row.createdAt).toLocaleDateString()} - - - ); - }, - }, - { id: "email", content: t("teamPanel.table.email"), render: (row) => row.email }, - { - id: "role", - content: t("teamPanel.table.role"), - render: (row) => row.role, - }, - ]; - const [refreshTrigger, setRefreshTrigger] = useState(false); - const refreshTeamList = () => { - setRefreshTrigger((prev) => !prev); - }; - useEffect(() => { - const fetchTeam = async () => { - try { - const response = await networkService.getAllUsers(); - setMembers(response.data.data); - } catch (error) { - createToast({ - body: error.message || "Error fetching team members.", - }); - } - }; - - fetchTeam(); - }, [refreshTrigger]); - - useEffect(() => { - const ROLE_MAP = { - superadmin: t("roles.superAdmin"), - admin: t("roles.admin"), - user: t("roles.teamMember"), - demo: t("roles.demoUser"), - }; - let team = members; - if (filter !== "all") - team = members.filter((member) => { - if (filter === "admin") { - return member.role.includes("admin") || member.role.includes("superadmin"); - } - return member.role.includes(filter); - }); - - team = team.map((member) => ({ - ...member, - id: member.id, - role: member.role.map((role) => ROLE_MAP[role]).join(","), - })); - setData(team); - }, [members, filter, t]); - - useEffect(() => { - setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === ""); - }, [errors, toInvite.email]); - const [isOpen, setIsOpen] = useState(false); - const [isRegisterOpen, setIsRegisterOpen] = useState(false); - const handleIsRegisterOpen = (open) => { - setIsRegisterOpen(open); - }; - - const handleChange = (event) => { - const { value } = event.target; - const newEmail = value?.toLowerCase() || value; - setToInvite((prev) => ({ - ...prev, - email: newEmail, - })); - - const validation = newOrChangedCredentials.validate( - { email: newEmail }, - { abortEarly: false } - ); - - setErrors((prev) => { - const updatedErrors = { ...prev }; - - if (validation.error) { - updatedErrors.email = validation.error.details[0].message; - } else { - delete updatedErrors.email; - } - return updatedErrors; - }); - }; - - const handleGetToken = async () => { - await getInviteToken({ email: toInvite.email, role: toInvite.role }); - }; - const handleInviteMember = async () => { - if (!toInvite.email) { - setErrors((prev) => ({ ...prev, email: "Email is required." })); - return; - } - setIsSendingInvite(true); - if (!toInvite.role.includes("user") || !toInvite.role.includes("admin")) - setToInvite((prev) => ({ ...prev, role: ["user"] })); - - const { error } = newOrChangedCredentials.validate( - { email: toInvite.email }, - { - abortEarly: false, - } - ); - - if (error) { - setErrors((prev) => ({ ...prev, email: error.details[0].message })); - return; - } - - try { - await networkService.sendInvitationToken({ - email: toInvite.email, - role: toInvite.role, - }); - closeInviteModal(); - createToast({ - body: "Member invited. They will receive an email with details on how to create their account.", - }); - } catch (error) { - createToast({ - body: error?.response?.data?.msg || error.message || "Unknown error.", - }); - } finally { - setIsSendingInvite(false); - } - }; - - const closeInviteModal = () => { - setIsOpen(false); - clearToken(); - setToInvite({ email: "", role: ["0"] }); - setErrors({}); - }; - - return ( - - - {t("teamPanel.teamMembers")} - - - - - - - - - - - setIsOpen(true)} - handleIsRegisterOpen={handleIsRegisterOpen} - /> - - - { - if (isSuperAdmin) { - navigate(`/account/team/${row.id}`); - } - }, - }} - /> - - - -