From 12f91f0c8ebf37459e3f28de5702a7a30cc03753 Mon Sep 17 00:00:00 2001 From: Hamed Valiollahi Bayeki Date: Wed, 11 Dec 2024 13:24:13 -0800 Subject: [PATCH] refactor: reformat organizations and user management to material card design --- frontend/src/assets/locales/en/admin.json | 2 + .../src/assets/locales/en/organization.json | 4 +- .../components/BCWidgetCard/BCWidgetCard.jsx | 39 ++- .../Admin/AdminMenu/components/ViewUser.jsx | 263 +++++++++++------- .../views/Admin/__tests__/ViewUser.test.jsx | 4 +- .../ViewOrganization/ViewOrganization.jsx | 242 ++++++++-------- .../__tests__/TransferView.test.jsx | 3 +- 7 files changed, 325 insertions(+), 232 deletions(-) diff --git a/frontend/src/assets/locales/en/admin.json b/frontend/src/assets/locales/en/admin.json index 6a197da0f..f839b3570 100644 --- a/frontend/src/assets/locales/en/admin.json +++ b/frontend/src/assets/locales/en/admin.json @@ -22,6 +22,8 @@ "usersNotFound": "No users found", "activitiesNotFound": "No user activities found", "newUserBtn": "New user", + "userDetails": "User details", + "editBtn": "Edit", "userColLabels": { "userName": "User name", "role": "Role(s)", diff --git a/frontend/src/assets/locales/en/organization.json b/frontend/src/assets/locales/en/organization.json index b6f21ca34..1d3e9227c 100644 --- a/frontend/src/assets/locales/en/organization.json +++ b/frontend/src/assets/locales/en/organization.json @@ -45,5 +45,7 @@ "editOrgTitle": "Edit organization", "toUpdateMsg": " to update address information.", "selectOrgLabel": "Select an Organization", - "complianceUnitBalance": "Compliance Unit Balance" + "complianceUnitBalance": "Compliance Unit Balance", + "editBtn": "Edit", + "orgDetails": "Organization details" } diff --git a/frontend/src/components/BCWidgetCard/BCWidgetCard.jsx b/frontend/src/components/BCWidgetCard/BCWidgetCard.jsx index f93804226..319e3191c 100644 --- a/frontend/src/components/BCWidgetCard/BCWidgetCard.jsx +++ b/frontend/src/components/BCWidgetCard/BCWidgetCard.jsx @@ -1,17 +1,28 @@ import PropTypes from 'prop-types' - -// @mui material components import { Card, CardContent, Divider } from '@mui/material' import BCBox from '@/components/BCBox' import BCTypography from '@/components/BCTypography' +import { useNavigate } from 'react-router-dom' +import EditIcon from '@mui/icons-material/Edit' +import BCButton from '@/components/BCButton' function BCWidgetCard({ color = 'nav', title = 'Title', content, style, - disableHover = false + disableHover = false, + editButtonText = null, + editButtonRoute = null }) { + const navigate = useNavigate() + + const handleButtonClick = () => { + if (editButtonRoute) { + navigate(editButtonRoute) + } + } + return ( {title} + {editButtonRoute && ( + } + sx={{ + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)' + } + }} + > + {editButtonText} + + )} { const { t } = useTranslation(['common', 'admin']) const gridRef = useRef() const alertRef = useRef() + const gridOptions = { overlayNoRowsTemplate: t('admin:activitiesNotFound'), suppressHeaderMenuButton: false, @@ -30,134 +34,187 @@ export const ViewUser = () => { const { userID, orgID } = useParams() const { data: currentUser, hasRoles } = useCurrentUser() const navigate = useNavigate() - const { data, isLoading, isLoadingError, isError, error } = hasRoles(roles.supplier) + const [editButtonRoute, setEditButtonRoute] = useState(null) + + const { data, isLoading, isLoadingError, isError, error } = hasRoles( + roles.supplier + ) ? // eslint-disable-next-line react-hooks/rules-of-hooks - useOrganizationUser( - orgID || currentUser?.organization.organizationId, - userID - ) + useOrganizationUser( + orgID || currentUser?.organization.organizationId, + userID + ) : // eslint-disable-next-line react-hooks/rules-of-hooks - useUser(parseInt(userID)) + useUser(parseInt(userID)) + + const canEdit = hasRoles(roles.administrator) || hasRoles(roles.manage_users) - const handleEditClick = () => { + useEffect(() => { + let route = null if (hasRoles(roles.supplier)) { - navigate(ROUTES.ORGANIZATION_EDITUSER.replace(':userID', userID)) - } else if (orgID) - navigate( - ROUTES.ORGANIZATIONS_EDITUSER.replace(':orgID', orgID).replace( - ':userID', - userID - ) + route = ROUTES.ORGANIZATION_EDITUSER.replace(':userID', userID) + } else if (orgID) { + route = ROUTES.ORGANIZATIONS_EDITUSER.replace(':orgID', orgID).replace( + ':userID', + userID ) - else navigate(ROUTES.ADMIN_USERS_EDIT.replace(':userID', userID)) - } + } else { + route = ROUTES.ADMIN_USERS_EDIT.replace(':userID', userID) + } + setEditButtonRoute(route) + }, [hasRoles, orgID, userID]) const apiEndpoint = apiRoutes.getUserActivities.replace(':userID', userID) const gridKey = `user-activity-grid-${userID}` const getRowId = useCallback((params) => { - return `${params.data.transactionType.toLowerCase()}-${params.data.transactionId}`; - }, []); - - const handleRowClicked = useCallback((params) => { - const { transactionType, transactionId } = params.data; - - let route; - switch (transactionType) { - case 'Transfer': - route = ROUTES.TRANSFERS_VIEW.replace(':transferId', transactionId); - break; - case 'AdminAdjustment': - route = ROUTES.ADMIN_ADJUSTMENT_VIEW.replace(':transactionId', transactionId); - break; - case 'InitiativeAgreement': - route = ROUTES.INITIATIVE_AGREEMENT_VIEW.replace(':transactionId', transactionId); - } + return `${params.data.transactionType.toLowerCase()}-${ + params.data.transactionId + }` + }, []) + + const handleRowClicked = useCallback( + (params) => { + const { transactionType, transactionId } = params.data + let route + switch (transactionType) { + case 'Transfer': + route = ROUTES.TRANSFERS_VIEW.replace(':transferId', transactionId) + break + case 'AdminAdjustment': + route = ROUTES.ADMIN_ADJUSTMENT_VIEW.replace( + ':transactionId', + transactionId + ) + break + case 'InitiativeAgreement': + route = ROUTES.INITIATIVE_AGREEMENT_VIEW.replace( + ':transactionId', + transactionId + ) + break + default: + route = null + } - navigate(route); - }, [navigate]); + if (route) { + navigate(route) + } + }, + [navigate] + ) useEffect(() => { if (isError) { - alertRef.current?.triggerAlert({ message: error.response?.data?.detail || error.message, severity: 'error' }) + alertRef.current?.triggerAlert({ + message: error.response?.data?.detail || error.message, + severity: 'error' + }) } }, [isError, error]) if (isError) { - return <> - - + return ( + <> + + + ) } if (isLoading) return return ( -
+ <> {isLoadingError ? ( ) : ( <> - - {data.firstName + ' ' + data.lastName}  - - - - - - - - - {t('Organization')}:  - {data.organization?.name || t('govOrg')} - - - {t('admin:Email')}: {data.keycloakEmail} - - - {t('admin:WorkPhone')}:  - {phoneNumberFormatter({ value: data.phone })} - - - {t('admin:MobilePhone')}:  - {phoneNumberFormatter({ value: data.mobilePhone })} - - - {t('Status')}:  - {StatusRenderer({ data, isView: true })} - - - {t('admin:Roles')}:  - {RoleSpanRenderer({ data })} - - - {t('admin:Title')}: {data.title} + + + + {/* Left Column */} + + + {t('Name')}: {data.firstName}{' '} + {data.lastName} + + + {t('admin:Title')}: {data.title} + + + {t('Organization')}:{' '} + {data.organization?.name || t('govOrg')} + + + {t('Status')}:{' '} + {StatusRenderer({ data, isView: true })} + + + {t('Roles')}:{' '} + {RoleSpanRenderer({ data })} + + + + {/* Right Column */} + + + {t('admin:Email')}:{' '} + {data.keycloakEmail} + + + {t('admin:WorkPhone')}:{' '} + {phoneNumberFormatter({ value: data.phone })} + + + {t('admin:MobilePhone')}:{' '} + {phoneNumberFormatter({ value: data.mobilePhone })} + + + + + } + /> + + + + + {t('admin:UserActivity')} - - - - {t('admin:UserActivity')} - - - + + )} -
+ ) } + +export default ViewUser diff --git a/frontend/src/views/Admin/__tests__/ViewUser.test.jsx b/frontend/src/views/Admin/__tests__/ViewUser.test.jsx index bcf5984ef..6968208fa 100644 --- a/frontend/src/views/Admin/__tests__/ViewUser.test.jsx +++ b/frontend/src/views/Admin/__tests__/ViewUser.test.jsx @@ -153,8 +153,8 @@ describe('ViewUser Component', () => { wrapper({ children, initialEntries: ['/admin/users/1'] }) } ) - - const editButton = screen.getByLabelText('edit') + + const editButton = screen.getByRole('button', { name: /admin:editBtn/i }) fireEvent.click(editButton) await waitFor(() => { diff --git a/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx b/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx index 2bf32af03..d3c8dcf40 100644 --- a/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx +++ b/frontend/src/views/Organizations/ViewOrganization/ViewOrganization.jsx @@ -1,17 +1,13 @@ -// components import BCBox from '@/components/BCBox' import BCButton from '@/components/BCButton' import BCTypography from '@/components/BCTypography' import BCAlert from '@/components/BCAlert' import BCDataGridServer from '@/components/BCDataGrid/BCDataGridServer' import Loading from '@/components/Loading' -import { IconButton, Typography } from '@mui/material' -// icons +import BCWidgetCard from '@/components/BCWidgetCard/BCWidgetCard' import colors from '@/themes/base/colors.js' import { faCirclePlus } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import EditIcon from '@mui/icons-material/Edit' -// hooks import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { ROUTES, apiRoutes } from '@/constants/routes' @@ -42,7 +38,9 @@ export const ViewOrganization = () => { isLoading: isCurrentUserLoading, hasRoles } = useCurrentUser() - const { data: orgData, isLoading } = useOrganization(orgID ?? currentUser?.organization?.organizationId) + const { data: orgData, isLoading } = useOrganization( + orgID ?? currentUser?.organization?.organizationId + ) let orgBalance = {} if (hasRoles(roles.government)) { @@ -51,20 +49,13 @@ export const ViewOrganization = () => { } const { data: orgBalaceInfo } = orgBalance - const handleEditClick = () => { - navigate( - ROUTES.ORGANIZATIONS_EDIT.replace( + const canEdit = hasRoles(roles.administrator) + const editButtonRoute = canEdit + ? ROUTES.ORGANIZATIONS_EDIT.replace( ':orgID', orgID || currentUser?.organization?.organizationId - ), - { - state: { - orgID: orgID || currentUser?.organization?.organizationId, - isEditMode: true - } - } - ) - } + ) + : null const [gridKey, setGridKey] = useState(`users-grid-${orgID}-active`) const handleGridKey = useCallback(() => { @@ -73,29 +64,34 @@ export const ViewOrganization = () => { } else { setGridKey(`users-grid-${orgID}-inactive`) } - }, []) + }, [showActive, orgID]) const gridOptions = { overlayNoRowsTemplate: 'No users found', includeHiddenColumnsInQuickFilter: true } - const handleRowClicked = useCallback((params) => - // Based on the user Type (BCeID or IDIR) navigate to specific view - hasRoles(roles.supplier) - ? navigate( - ROUTES.ORGANIZATION_VIEWUSER.replace( - ':userID', - params.data.userProfileId - ) - ) - : navigate( - ROUTES.ORGANIZATIONS_VIEWUSER.replace(':orgID', orgID).replace( - ':userID', - params.data.userProfileId + + const handleRowClicked = useCallback( + (params) => + // Based on the user Type (BCeID or IDIR) navigate to specific view + hasRoles(roles.supplier) + ? navigate( + ROUTES.ORGANIZATION_VIEWUSER.replace( + ':userID', + params.data.userProfileId + ) ) - ) + : navigate( + ROUTES.ORGANIZATIONS_VIEWUSER.replace(':orgID', orgID).replace( + ':userID', + params.data.userProfileId + ) + ), + [hasRoles, navigate, orgID] ) - const getRowId = useCallback((params) => params.data.userProfileId) + + const getRowId = useCallback((params) => params.data.userProfileId, []) + const gridRef = useRef() useEffect(() => { @@ -111,7 +107,7 @@ export const ViewOrganization = () => { } gridRef?.current?.api?.onFilterChanged() } - }, [showActive]) + }, [showActive, gridKey]) useEffect(() => { if (location.state?.message) { @@ -131,95 +127,99 @@ export const ViewOrganization = () => { {alertMessage} )} - - {orgData?.name}{' '} - - - - - - - - - - - {t('org:legalNameLabel')}: - - {orgData?.name} - - {t('org:operatingNameLabel')}: - - - {orgData?.operatingName || orgData?.name} - - - {t('org:phoneNbrLabel')}: - - - {phoneNumberFormatter({ value: orgData?.phone })} - - - {t('org:emailAddrLabel')}: - - {orgData?.email} - - - {t('org:complianceUnitBalance')}: - - - {orgBalaceInfo?.totalBalance.toLocaleString()} ( - {Math.abs(orgBalaceInfo?.reservedBalance).toLocaleString()}) - - - - - - {t('org:serviceAddrLabel')}: - - - {orgData && constructAddress(orgData?.orgAddress)} - - {t('org:bcAddrLabel')}: - - {orgData && constructAddress(orgData?.orgAttorneyAddress)} - - {t('org:regTrnLabel')}: - - {orgData?.orgStatus.status === ORGANIZATION_STATUSES.REGISTERED - ? 'Yes — A registered organization is able to transfer compliance units.' - : 'No — An organization must be registered to transfer compliance units.'} - - - - {!isCurrentUserLoading && !hasRoles(roles.government) && ( - - - Email {t('lcfsEmail')} - {t('org:toUpdateMsg')} - - - )} + + + + {/* Left Column */} + + + {t('org:legalNameLabel')}: {orgData?.name} + + + + {t('org:operatingNameLabel')}:{' '} + {orgData?.operatingName || orgData?.name} + + + + {t('org:phoneNbrLabel')}:{' '} + {phoneNumberFormatter({ value: orgData?.phone })} + + + + {t('org:emailAddrLabel')}: {orgData?.email} + + + + + {t('org:complianceUnitBalance')}:{' '} + {orgBalaceInfo?.totalBalance.toLocaleString()} ( + {Math.abs( + orgBalaceInfo?.reservedBalance + ).toLocaleString()} + ) + + + + + {/* Right Column */} + + + {t('org:serviceAddrLabel')}:{' '} + {orgData && constructAddress(orgData?.orgAddress)} + + + + {t('org:bcAddrLabel')}:{' '} + {orgData && constructAddress(orgData?.orgAttorneyAddress)} + + + + {t('org:regTrnLabel')}:{' '} + {orgData?.orgStatus.status === + ORGANIZATION_STATUSES.REGISTERED + ? 'Yes — A registered organization is able to transfer compliance units.' + : 'No — An organization must be registered to transfer compliance units.'} + + + + + {!isCurrentUserLoading && !hasRoles(roles.government) && ( + + + Email{' '} + {t('lcfsEmail')}{' '} + {t('org:toUpdateMsg')} + + + )} + + } + /> { ) } + +export default ViewOrganization diff --git a/frontend/src/views/Transfers/components/__tests__/TransferView.test.jsx b/frontend/src/views/Transfers/components/__tests__/TransferView.test.jsx index b8bee5bd0..d3a4d14ab 100644 --- a/frontend/src/views/Transfers/components/__tests__/TransferView.test.jsx +++ b/frontend/src/views/Transfers/components/__tests__/TransferView.test.jsx @@ -62,7 +62,8 @@ describe('TransferView Component', () => { sameOrganization: vi.fn(() => true), data: { isGovernmentUser: false - } + }, + hasAnyRole: vi.fn(() => false) }) })