From b5ff8571321bee75cfd62a30646748eb4e16193d Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 16 Dec 2024 16:01:29 -0800 Subject: [PATCH 1/5] feat: Add Legacy Report View * Add new brancher to control legacy vs non-legacy view * Split HistoryCard to its own component for re-usability and add tests * Update legacy view with legacy assessment card * Sent legacy_id to the front end --- .../lcfs/web/api/compliance_report/schema.py | 2 +- frontend/public/config/config.js | 1 + frontend/src/App.jsx | 4 +- frontend/src/assets/locales/en/reports.json | 3 +- frontend/src/constants/config.js | 1 + frontend/src/constants/routes/routes.js | 2 +- .../ComplianceReports/ComplianceReports.jsx | 18 +- .../EditViewComplianceReport.jsx | 18 +- .../ViewComplianceReportBrancher.jsx | 40 +++ .../ViewLegacyComplianceReport.jsx | 143 +++++++++ .../EditViewComplianceReports.test.jsx | 288 ++++++++++++------ .../__tests__/HistoryCard.test.jsx | 171 +++++++++++ .../ViewComplianceReportBrancher.test.jsx | 146 +++++++++ .../ViewLegacyComplianceReport.test.jsx | 149 +++++++++ .../components/AssessmentCard.jsx | 109 +------ .../components/HistoryCard.jsx | 114 +++++++ .../components/LegacyAssessmentCard.jsx | 128 ++++++++ .../ComplianceReports/components/_schema.jsx | 12 +- 18 files changed, 1103 insertions(+), 246 deletions(-) create mode 100644 frontend/src/views/ComplianceReports/ViewComplianceReportBrancher.jsx create mode 100644 frontend/src/views/ComplianceReports/ViewLegacyComplianceReport.jsx create mode 100644 frontend/src/views/ComplianceReports/__tests__/HistoryCard.test.jsx create mode 100644 frontend/src/views/ComplianceReports/__tests__/ViewComplianceReportBrancher.test.jsx create mode 100644 frontend/src/views/ComplianceReports/__tests__/ViewLegacyComplianceReport.test.jsx create mode 100644 frontend/src/views/ComplianceReports/components/HistoryCard.jsx create mode 100644 frontend/src/views/ComplianceReports/components/LegacyAssessmentCard.jsx diff --git a/backend/lcfs/web/api/compliance_report/schema.py b/backend/lcfs/web/api/compliance_report/schema.py index 34696dee2..e839bee11 100644 --- a/backend/lcfs/web/api/compliance_report/schema.py +++ b/backend/lcfs/web/api/compliance_report/schema.py @@ -40,7 +40,6 @@ class CompliancePeriodSchema(BaseSchema): display_order: Optional[int] = None - class SummarySchema(BaseSchema): summary_id: int is_locked: bool @@ -154,6 +153,7 @@ class ComplianceReportBaseSchema(BaseSchema): update_date: Optional[datetime] = None history: Optional[List[ComplianceReportHistorySchema]] = None has_supplemental: bool + legacy_id: Optional[int] = None class ChainedComplianceReportSchema(BaseSchema): diff --git a/frontend/public/config/config.js b/frontend/public/config/config.js index a53256ce3..c3885613c 100644 --- a/frontend/public/config/config.js +++ b/frontend/public/config/config.js @@ -1,5 +1,6 @@ export const config = { api_base: 'http://localhost:8000/api', + tfrs_base: 'http://localhost:3001', keycloak: { REALM: 'standard', CLIENT_ID: 'low-carbon-fuel-standard-5147', diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 2e6c33946..907ce057a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -35,6 +35,8 @@ import { AddEditFuelExports } from './views/FuelExports/AddEditFuelExports' import { AddEditAllocationAgreements } from './views/AllocationAgreements/AddEditAllocationAgreements' import { logout } from '@/utils/keycloak.js' import { CompareReports } from '@/views/CompareReports/CompareReports' +import { ViewLegacyComplianceReport } from '@/views/ComplianceReports/ViewLegacyComplianceReport.jsx' +import { ViewComplianceReportBrancher } from '@/views/ComplianceReports/ViewComplianceReportBrancher.jsx' const router = createBrowserRouter([ { @@ -196,7 +198,7 @@ const router = createBrowserRouter([ }, { path: ROUTES.REPORTS_VIEW, - element: , + element: , handle: { title: '' } }, { diff --git a/frontend/src/assets/locales/en/reports.json b/frontend/src/assets/locales/en/reports.json index 597eab678..91ced911b 100644 --- a/frontend/src/assets/locales/en/reports.json +++ b/frontend/src/assets/locales/en/reports.json @@ -158,5 +158,6 @@ "totalValue": "Total Value" }, "summaryLoadingMsg": "Loading compliance report summary...", - "noSigningAuthorityTooltip": "Signing authority role required." + "noSigningAuthorityTooltip": "Signing authority role required.", + "viewLegacyBtn": "View Full Historical Report in TFRS" } diff --git a/frontend/src/constants/config.js b/frontend/src/constants/config.js index 8dfec5e6c..3fc0c851f 100644 --- a/frontend/src/constants/config.js +++ b/frontend/src/constants/config.js @@ -39,6 +39,7 @@ export const FEATURE_FLAGS = { export const CONFIG = { API_BASE: getApiBaseUrl(), + TFRS_BASE: window.lcfs_config.tfrs_base, KEYCLOAK: { REALM: window.lcfs_config.keycloak.REALM ?? 'standard', CLIENT_ID: diff --git a/frontend/src/constants/routes/routes.js b/frontend/src/constants/routes/routes.js index 78c3978a8..040be0f9b 100644 --- a/frontend/src/constants/routes/routes.js +++ b/frontend/src/constants/routes/routes.js @@ -36,13 +36,13 @@ export const ORGANIZATIONS_EDITUSER = `${ORGANIZATIONS_VIEWUSER}/edit-user` export const REPORTS = '/compliance-reporting' export const REPORTS_VIEW = `${REPORTS}/:compliancePeriod/:complianceReportId` -export const REPORTS_COMPARE = `/compare-reporting` export const REPORTS_ADD_SUPPLY_OF_FUEL = `${REPORTS_VIEW}/supply-of-fuel` export const REPORTS_ADD_FINAL_SUPPLY_EQUIPMENTS = `${REPORTS_VIEW}/final-supply-equipments` export const REPORTS_ADD_ALLOCATION_AGREEMENTS = `${REPORTS_VIEW}/allocation-agreements` export const REPORTS_ADD_NOTIONAL_TRANSFERS = `${REPORTS_VIEW}/notional-transfers` export const REPORTS_ADD_OTHER_USE_FUELS = `${REPORTS_VIEW}/fuels-other-use` export const REPORTS_ADD_FUEL_EXPORTS = `${REPORTS_VIEW}/fuel-exports` +export const REPORTS_COMPARE = '/compare-reporting' export const NOTIFICATIONS = '/notifications' export const NOTIFICATIONS_SETTINGS = `${NOTIFICATIONS}/configure` diff --git a/frontend/src/views/ComplianceReports/ComplianceReports.jsx b/frontend/src/views/ComplianceReports/ComplianceReports.jsx index 973b6f46b..504d42990 100644 --- a/frontend/src/views/ComplianceReports/ComplianceReports.jsx +++ b/frontend/src/views/ComplianceReports/ComplianceReports.jsx @@ -4,20 +4,20 @@ import BCBox from '@/components/BCBox' import BCAlert from '@/components/BCAlert' import BCDataGridServer from '@/components/BCDataGrid/BCDataGridServer' // react components -import { useEffect, useMemo, useRef, useState, useCallback } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate } from 'react-router-dom' // Services import { Role } from '@/components/Role' // constants import { roles } from '@/constants/roles' -import { ROUTES, apiRoutes } from '@/constants/routes' +import { apiRoutes, ROUTES } from '@/constants/routes' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses' // hooks import { useCurrentUser } from '@/hooks/useCurrentUser' import { useCreateComplianceReport } from '@/hooks/useComplianceReports' // internal components -import { reportsColDefs, defaultSortModel } from './components/_schema' +import { defaultSortModel, reportsColDefs } from './components/_schema' import { NewComplianceReportButton } from './components/NewComplianceReportButton' import BCTypography from '@/components/BCTypography' @@ -44,15 +44,13 @@ export const ComplianceReports = () => { (params) => params.data.complianceReportId.toString(), [] ) - // eslint-disable-next-line react-hooks/exhaustive-deps const handleRowClicked = useCallback( ({ data }) => { - navigate( - ROUTES.REPORTS_VIEW.replace( - ':compliancePeriod', - data.compliancePeriod.description - ).replace(':complianceReportId', data.complianceReportId) - ) + const mappedRoute = ROUTES.REPORTS_VIEW.replace( + ':compliancePeriod', + data.compliancePeriod.description + ).replace(':complianceReportId', data.complianceReportId) + navigate(mappedRoute) }, [navigate] ) diff --git a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx index 30b2925f0..dc6b075b1 100644 --- a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx +++ b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx @@ -18,10 +18,7 @@ import { useTranslation } from 'react-i18next' import { useCurrentUser } from '@/hooks/useCurrentUser' import { useOrganization } from '@/hooks/useOrganization' import { Introduction } from './components/Introduction' -import { - useGetComplianceReport, - useUpdateComplianceReport -} from '@/hooks/useComplianceReports' +import { useUpdateComplianceReport } from '@/hooks/useComplianceReports' import ComplianceReportSummary from './components/ComplianceReportSummary' import ReportDetails from './components/ReportDetails' import { buttonClusterConfigFn } from './buttonConfigs' @@ -35,7 +32,7 @@ const iconStyle = { height: '2rem', color: colors.white.main } -export const EditViewComplianceReport = () => { +export const EditViewComplianceReport = ({ reportData, isError, error }) => { const { t } = useTranslation(['common', 'report']) const location = useLocation() const [modalData, setModalData] = useState(null) @@ -83,15 +80,6 @@ export const EditViewComplianceReport = () => { hasRoles } = useCurrentUser() const isGovernmentUser = currentUser?.isGovernmentUser - const { - data: reportData, - isLoading: isReportLoading, - isError, - error - } = useGetComplianceReport( - currentUser?.organization?.organizationId, - complianceReportId - ) const currentStatus = reportData?.report.currentStatus?.status const { data: orgData, isLoading } = useOrganization( @@ -158,7 +146,7 @@ export const EditViewComplianceReport = () => { } }, [location.state, isError, error]) - if (isLoading || isReportLoading || isCurrentUserLoading) { + if (isLoading || isCurrentUserLoading) { return } diff --git a/frontend/src/views/ComplianceReports/ViewComplianceReportBrancher.jsx b/frontend/src/views/ComplianceReports/ViewComplianceReportBrancher.jsx new file mode 100644 index 000000000..de4b97f75 --- /dev/null +++ b/frontend/src/views/ComplianceReports/ViewComplianceReportBrancher.jsx @@ -0,0 +1,40 @@ +import { useGetComplianceReport } from '@/hooks/useComplianceReports.js' +import { useCurrentUser } from '@/hooks/useCurrentUser.js' +import Loading from '@/components/Loading.jsx' +import { ViewLegacyComplianceReport } from '@/views/ComplianceReports/ViewLegacyComplianceReport.jsx' +import { useParams } from 'react-router-dom' +import { EditViewComplianceReport } from '@/views/ComplianceReports/EditViewComplianceReport.jsx' + +export const ViewComplianceReportBrancher = () => { + const { complianceReportId } = useParams() + const { data: currentUser, isLoading: isCurrentUserLoading } = + useCurrentUser() + + const { + data: reportData, + isLoading: isReportLoading, + isError, + error + } = useGetComplianceReport( + currentUser?.organization?.organizationId, + complianceReportId + ) + + if (isReportLoading || isCurrentUserLoading) { + return + } + + return reportData.report.legacyId ? ( + + ) : ( + + ) +} diff --git a/frontend/src/views/ComplianceReports/ViewLegacyComplianceReport.jsx b/frontend/src/views/ComplianceReports/ViewLegacyComplianceReport.jsx new file mode 100644 index 000000000..163591800 --- /dev/null +++ b/frontend/src/views/ComplianceReports/ViewLegacyComplianceReport.jsx @@ -0,0 +1,143 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { FloatingAlert } from '@/components/BCAlert' +import BCBox from '@/components/BCBox' +import BCModal from '@/components/BCModal' +import Loading from '@/components/Loading' +import { Fab, Stack, Tooltip } from '@mui/material' +import BCTypography from '@/components/BCTypography' +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp' +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import colors from '@/themes/base/colors.js' +import { useTranslation } from 'react-i18next' +import { useCurrentUser } from '@/hooks/useCurrentUser' +import { useOrganization } from '@/hooks/useOrganization' +import { LegacyAssessmentCard } from '@/views/ComplianceReports/components/LegacyAssessmentCard.jsx' + +const iconStyle = { + width: '2rem', + height: '2rem', + color: colors.white.main +} +export const ViewLegacyComplianceReport = ({ reportData, error, isError }) => { + const { t } = useTranslation(['common', 'report']) + const [modalData, setModalData] = useState(null) + const alertRef = useRef() + + const [isScrollingUp, setIsScrollingUp] = useState(false) + const [lastScrollTop, setLastScrollTop] = useState(0) + + const scrollToTopOrBottom = () => { + if (isScrollingUp) { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }) + } else { + window.scrollTo({ + top: document.documentElement.scrollHeight, + behavior: 'smooth' + }) + } + } + + const handleScroll = useCallback(() => { + const scrollTop = window.pageYOffset || document.documentElement.scrollTop + setIsScrollingUp(scrollTop < lastScrollTop || scrollTop === 0) + setLastScrollTop(scrollTop) + }, [lastScrollTop]) + + useEffect(() => { + window.addEventListener('scroll', handleScroll) + return () => window.removeEventListener('scroll', handleScroll) + }, [handleScroll]) + + const { data: currentUser, isLoading: isCurrentUserLoading } = + useCurrentUser() + const isGovernmentUser = currentUser?.isGovernmentUser + + const currentStatus = reportData.report.currentStatus?.status + const { data: orgData, isLoading } = useOrganization( + reportData.report.organizationId + ) + + if (isLoading || isCurrentUserLoading) { + return + } + + if (isError) { + return ( + <> + + {t('report:errorRetrieving')} + + ) + } + + return ( + <> + + + setModalData(null)} + data={modalData} + /> + + + {reportData?.report.compliancePeriod.description}  + {t('report:complianceReport')} - + {reportData?.report.nickname} + + + Status: {currentStatus} + + + + + + + + {isScrollingUp ? ( + + ) : ( + + )} + + + + + ) +} diff --git a/frontend/src/views/ComplianceReports/__tests__/EditViewComplianceReports.test.jsx b/frontend/src/views/ComplianceReports/__tests__/EditViewComplianceReports.test.jsx index cb50dbee6..a3e38b0df 100644 --- a/frontend/src/views/ComplianceReports/__tests__/EditViewComplianceReports.test.jsx +++ b/frontend/src/views/ComplianceReports/__tests__/EditViewComplianceReports.test.jsx @@ -2,9 +2,9 @@ import React from 'react' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { EditViewComplianceReport } from '../EditViewComplianceReport' -import * as useComplianceReportsHook from '@/hooks/useComplianceReports' import * as useCurrentUserHook from '@/hooks/useCurrentUser' import * as useOrganizationHook from '@/hooks/useOrganization' +import * as useComplianceReportsHook from '@/hooks/useComplianceReports' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses' import { wrapper } from '@/tests/utils/wrapper' @@ -89,17 +89,17 @@ describe('EditViewComplianceReport', () => { isLoading: false, hasRoles: mockHasRoles }, - complianceReport: { - data: { - report: { - organizationId: '123', - currentStatus: { status: COMPLIANCE_REPORT_STATUSES.DRAFT } - }, - chain: [] + reportData: { + report: { + organizationId: '123', + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.DRAFT }, + history: [], + nickname: 'Test Report' }, - isLoading: false, - isError: false + chain: [] }, + isError: false, + error: null, organization: { data: { name: 'Test Org', @@ -117,10 +117,6 @@ describe('EditViewComplianceReport', () => { } }, isLoading: false - }, - createSupplementalReport: { - mutate: vi.fn(), - isLoading: false } } @@ -131,9 +127,6 @@ describe('EditViewComplianceReport', () => { vi.mocked(useCurrentUserHook.useCurrentUser).mockReturnValue( mocks.currentUser ) - vi.mocked(useComplianceReportsHook.useGetComplianceReport).mockReturnValue( - mocks.complianceReport - ) vi.mocked(useOrganizationHook.useOrganization).mockReturnValue( mocks.organization ) @@ -142,23 +135,43 @@ describe('EditViewComplianceReport', () => { ).mockReturnValue({ mutate: vi.fn() }) vi.mocked( useComplianceReportsHook.useCreateSupplementalReport - ).mockReturnValue(mocks.createSupplementalReport) + ).mockReturnValue({ + mutate: vi.fn(), + isLoading: false + }) + + return mocks } beforeEach(() => { vi.resetAllMocks() - setupMocks() }) it('renders the component', async () => { - render(, { wrapper }) + const mocks = setupMocks() + render( + , + { wrapper } + ) await waitFor(() => { expect(screen.getByText(/2023.*complianceReport/i)).toBeInTheDocument() }) }) it('renders report components', async () => { - render(, { wrapper }) + const mocks = setupMocks() + render( + , + { wrapper } + ) await waitFor(() => { expect(screen.getByText('Report Details')).toBeInTheDocument() expect(screen.getByText('Compliance Report Summary')).toBeInTheDocument() @@ -167,44 +180,61 @@ describe('EditViewComplianceReport', () => { }) it('displays an alert message when location state has a message', async () => { - setupMocks({ + const mocks = setupMocks({ useLocation: { state: { message: 'Test alert', severity: 'success' } } }) - render(, { wrapper }) + render( + , + { wrapper } + ) await waitFor(() => { expect(screen.getByText('Test alert')).toBeInTheDocument() }) }) it('displays an error message when there is an error fetching the report', async () => { - setupMocks({ - complianceReport: { - isError: true, - error: { message: 'Error fetching report' } - } + const mocks = setupMocks({ + isError: true, + error: { message: 'Error fetching report' } }) - render(, { wrapper }) + render( + , + { wrapper } + ) await waitFor(() => { expect(screen.getByText('Error fetching report')).toBeInTheDocument() }) }) it('displays the correct buttons for Submitted status with Analyst role', async () => { - setupMocks({ - complianceReport: { - data: { - report: { - currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } - }, - chain: [] - } + const mocks = setupMocks({ + reportData: { + report: { + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } + }, + chain: [] }, currentUser: { data: { isGovernmentUser: true }, hasRoles: (role) => role === 'Analyst' } }) - render(, { wrapper }) + render( + , + { wrapper } + ) await waitFor(() => { expect( screen.getByText('report:actionBtns.recommendReportAnalystBtn') @@ -213,23 +243,28 @@ describe('EditViewComplianceReport', () => { }) it('displays the correct buttons for Recommended by Analyst status with Compliance Manager role', async () => { - setupMocks({ - complianceReport: { - data: { - report: { - currentStatus: { - status: COMPLIANCE_REPORT_STATUSES.RECOMMENDED_BY_ANALYST - } - }, - chain: [] - } + const mocks = setupMocks({ + reportData: { + report: { + currentStatus: { + status: COMPLIANCE_REPORT_STATUSES.RECOMMENDED_BY_ANALYST + } + }, + chain: [] }, currentUser: { data: { isGovernmentUser: true }, hasRoles: (role) => role === 'Compliance Manager' } }) - render(, { wrapper }) + render( + , + { wrapper } + ) await waitFor(() => { expect( screen.getByText('report:actionBtns.recommendReportManagerBtn') @@ -241,23 +276,28 @@ describe('EditViewComplianceReport', () => { }) it('displays the correct buttons for Recommended by Manager status with Director role', async () => { - setupMocks({ - complianceReport: { - data: { - report: { - currentStatus: { - status: COMPLIANCE_REPORT_STATUSES.RECOMMENDED_BY_MANAGER - } - }, - chain: [] - } + const mocks = setupMocks({ + reportData: { + report: { + currentStatus: { + status: COMPLIANCE_REPORT_STATUSES.RECOMMENDED_BY_MANAGER + } + }, + chain: [] }, currentUser: { data: { isGovernmentUser: true }, hasRoles: (role) => role === 'Director' } }) - render(, { wrapper }) + render( + , + { wrapper } + ) await waitFor(() => { expect( screen.getByText('report:actionBtns.assessReportBtn') @@ -269,21 +309,26 @@ describe('EditViewComplianceReport', () => { }) it('displays the correct buttons for Assessed status with Analyst role', async () => { - setupMocks({ - complianceReport: { - data: { - report: { - currentStatus: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED } - }, - chain: [] - } + const mocks = setupMocks({ + reportData: { + report: { + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED } + }, + chain: [] }, currentUser: { data: { isGovernmentUser: true }, hasRoles: (role) => role === 'Analyst' } }) - render(, { wrapper }) + render( + , + { wrapper } + ) await waitFor(() => { expect( screen.getByText('report:actionBtns.reAssessReportBtn') @@ -292,18 +337,23 @@ describe('EditViewComplianceReport', () => { }) it('does not display action buttons for non-government users on submitted reports', async () => { - setupMocks({ - complianceReport: { - data: { - report: { - currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } - }, - chain: [] - } + const mocks = setupMocks({ + reportData: { + report: { + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } + }, + chain: [] }, currentUser: { data: { isGovernmentUser: false }, hasRoles: () => false } }) - render(, { wrapper }) + render( + , + { wrapper } + ) await waitFor(() => { expect( screen.queryByText('report:actionBtns.recommendReportAnalystBtn') @@ -321,20 +371,34 @@ describe('EditViewComplianceReport', () => { }) it('displays internal comments section for government users', async () => { - setupMocks({ + const mocks = setupMocks({ currentUser: { data: { isGovernmentUser: true }, hasRoles: () => true } }) - render(, { wrapper }) + render( + , + { wrapper } + ) await waitFor(() => { expect(screen.getByText('report:internalComments')).toBeInTheDocument() }) }) it('does not display internal comments section for non-government users', async () => { - setupMocks({ + const mocks = setupMocks({ currentUser: { data: { isGovernmentUser: false }, hasRoles: () => false } }) - render(, { wrapper }) + render( + , + { wrapper } + ) await waitFor(() => { expect( screen.queryByText('report:internalComments') @@ -343,17 +407,22 @@ describe('EditViewComplianceReport', () => { }) it('displays ActivityListCard for Draft status', async () => { - setupMocks({ - complianceReport: { - data: { - report: { - currentStatus: { status: COMPLIANCE_REPORT_STATUSES.DRAFT } - }, - chain: [] - } + const mocks = setupMocks({ + reportData: { + report: { + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.DRAFT } + }, + chain: [] } }) - render(, { wrapper }) + render( + , + { wrapper } + ) await waitFor(() => { expect(screen.getByText('Activity Links List')).toBeInTheDocument() }) @@ -373,8 +442,8 @@ describe('EditViewComplianceReport', () => { } ] - vi.mocked(useComplianceReportsHook.useGetComplianceReport).mockReturnValue({ - data: { + const mocks = setupMocks({ + reportData: { report: { currentStatus: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED }, history: historyMock @@ -389,12 +458,17 @@ describe('EditViewComplianceReport', () => { currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } } ] - }, - isLoading: false, - isError: false + } }) - render(, { wrapper }) + render( + , + { wrapper } + ) await waitFor(() => { expect(screen.getByText('report:assessment')).toBeInTheDocument() expect(screen.getByText('report:reportHistory')).toBeInTheDocument() @@ -405,7 +479,15 @@ describe('EditViewComplianceReport', () => { }) it('displays scroll-to-top button when scrolling down', async () => { - render(, { wrapper }) + const mocks = setupMocks() + render( + , + { wrapper } + ) await waitFor(() => { fireEvent.scroll(window, { target: { pageYOffset: 100 } }) expect(screen.getByLabelText('scroll to bottom')).toBeInTheDocument() @@ -413,7 +495,15 @@ describe('EditViewComplianceReport', () => { }) it('displays scroll-to-bottom button when at the top of the page', async () => { - render(, { wrapper }) + const mocks = setupMocks() + render( + , + { wrapper } + ) await waitFor(() => { fireEvent.scroll(window, { target: { pageYOffset: 0 } }) expect(screen.getByLabelText('scroll to top')).toBeInTheDocument() diff --git a/frontend/src/views/ComplianceReports/__tests__/HistoryCard.test.jsx b/frontend/src/views/ComplianceReports/__tests__/HistoryCard.test.jsx new file mode 100644 index 000000000..defa965c7 --- /dev/null +++ b/frontend/src/views/ComplianceReports/__tests__/HistoryCard.test.jsx @@ -0,0 +1,171 @@ +import React from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses' +import { HistoryCard } from '@/views/ComplianceReports/components/HistoryCard.jsx' +import { wrapper } from '@/tests/utils/wrapper.jsx' + +import * as useCurrentUserHook from '@/hooks/useCurrentUser' + +// Mock useCurrentUser +vi.mock('@/hooks/useCurrentUser', () => ({ + useCurrentUser: vi.fn(() => ({ + data: { isGovernmentUser: false }, + isLoading: false + })) +})) + +// Mock translation +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key, opts) => { + if (opts && opts.createDate && opts.firstName && opts.lastName) { + return `${key}: ${opts.firstName} ${opts.lastName} - ${opts.createDate}` + } + return key + } + }) +})) + +// Mock timezoneFormatter +vi.mock('@/utils/formatters', () => ({ + timezoneFormatter: vi.fn(({ value }) => `formatted-${value}`) +})) + +describe('HistoryCard', () => { + const defaultReport = { + version: 0, + compliancePeriod: { description: '2024' }, + nickname: 'My Nickname', + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.DRAFT }, + history: [] + } + + const renderComponent = (overrides = {}) => { + return render(, { + wrapper + }) + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders without history', async () => { + renderComponent() + // Only the accordion header should be present + await waitFor(() => { + expect(screen.queryByRole('listitem')).not.toBeInTheDocument() + }) + }) + + it('displays compliancePeriod.description and current status if version=0', async () => { + renderComponent({ + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } + }) + await waitFor(() => { + expect( + screen.getByText(/2024 Compliance Report: SUBMITTED/i) + ).toBeInTheDocument() + }) + }) + + it('displays nickname and current status if version > 0', async () => { + renderComponent({ + version: 1, + nickname: 'My Cool Nickname', + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } + }) + await waitFor(() => { + expect( + screen.getByText(/My Cool Nickname: SUBMITTED/i) + ).toBeInTheDocument() + }) + }) + + it('sorts history in descending order by createDate and filters out DRAFT', async () => { + const history = [ + { + status: { status: COMPLIANCE_REPORT_STATUSES.DRAFT }, + createDate: '2024-10-02', + userProfile: { firstName: 'Draft', lastName: 'User' } + }, + { + status: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED }, + createDate: '2024-10-01T10:00:00Z', + userProfile: { firstName: 'John', lastName: 'Doe' } + }, + { + status: { status: COMPLIANCE_REPORT_STATUSES.RECOMMENDED_BY_ANALYST }, + createDate: '2024-10-03T15:00:00Z', + userProfile: { firstName: 'Jane', lastName: 'Smith' } + } + ] + renderComponent({ + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED }, + history + }) + + // SUBMITTED first because 2024-10-03 is later than 2024-10-01 + await waitFor(() => { + const items = screen.getAllByTestId('list-item') + expect(items.length).toBe(2) // DRAFT is filtered out + // The first item should be RECOMMENDED_BY_ANALYST (2024-10-03) + expect(items[0].textContent).toContain( + 'report:complianceReportHistory.Recommended by analyst: Jane Smith' + ) + // The second item should be SUBMITTED (2024-10-01) + expect(items[1].textContent).toContain( + 'report:complianceReportHistory.Submitted: John Doe' + ) + }) + }) + + it('replaces ASSESSED with AssessedBy if user is not government', async () => { + const history = [ + { + status: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED }, + createDate: '2024-10-01T10:00:00Z', + userProfile: { firstName: 'John', lastName: 'Doe' } + } + ] + renderComponent({ + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED }, + history + }) + + await waitFor(() => { + const item = screen.getByTestId('list-item') + // Should have replaced ASSESSED with AssessedBy + expect(item.textContent).toContain( + 'report:complianceReportHistory.AssessedBy: John Doe - formatted-2024-10-01T10:00:00Z' + ) + }) + }) + + it('does not replace ASSESSED with AssessedBy if user is government', async () => { + useCurrentUserHook.useCurrentUser.mockReturnValueOnce({ + data: { isGovernmentUser: true }, + isLoading: false + }) + const history = [ + { + status: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED }, + createDate: '2024-10-01T10:00:00Z', + userProfile: { firstName: 'John', lastName: 'Doe' } + } + ] + renderComponent({ + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED }, + history + }) + + await waitFor(() => { + const item = screen.getByTestId('list-item') + // Should NOT have replaced ASSESSED with AssessedBy + expect(item.textContent).toContain( + 'report:complianceReportHistory.Assessed: John Doe - formatted-2024-10-01T10:00:00Z' + ) + }) + }) +}) diff --git a/frontend/src/views/ComplianceReports/__tests__/ViewComplianceReportBrancher.test.jsx b/frontend/src/views/ComplianceReports/__tests__/ViewComplianceReportBrancher.test.jsx new file mode 100644 index 000000000..0b3127534 --- /dev/null +++ b/frontend/src/views/ComplianceReports/__tests__/ViewComplianceReportBrancher.test.jsx @@ -0,0 +1,146 @@ +import React from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { ViewComplianceReportBrancher } from '../ViewComplianceReportBrancher' +import * as useComplianceReportsHook from '@/hooks/useComplianceReports' +import * as useCurrentUserHook from '@/hooks/useCurrentUser' +import { wrapper } from '@/tests/utils/wrapper' + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom') + return { + ...actual, + useParams: vi.fn() + } +}) + +vi.mock('@/hooks/useComplianceReports') +vi.mock('@/hooks/useCurrentUser') + +vi.mock('@/components/Loading', () => ({ + default: () =>
Loading...
+})) + +vi.mock('@/views/ComplianceReports/ViewLegacyComplianceReport', () => ({ + ViewLegacyComplianceReport: () =>
Legacy Report View
+})) + +vi.mock('@/views/ComplianceReports/EditViewComplianceReport', () => ({ + EditViewComplianceReport: () =>
Edit Compliance Report
+})) + +// Import useParams after mocking so it's already a mock +import { useParams } from 'react-router-dom' + +describe('ViewComplianceReportBrancher', () => { + const setupMocks = ({ + currentUser = { organization: { organizationId: '123' } }, + isCurrentUserLoading = false, + reportData = { + report: { + legacyId: null, + currentStatus: { status: 'DRAFT' } + } + }, + isReportLoading = false, + isError = false, + error = null, + complianceReportId = '123' + } = {}) => { + // Set the return value for useParams + useParams.mockReturnValue({ complianceReportId }) + + // Mock useCurrentUser + useCurrentUserHook.useCurrentUser.mockReturnValue({ + data: currentUser, + isLoading: isCurrentUserLoading + }) + + // Mock useGetComplianceReport + useComplianceReportsHook.useGetComplianceReport.mockReturnValue({ + data: reportData, + isLoading: isReportLoading, + isError, + error + }) + } + + beforeEach(() => { + vi.resetAllMocks() + }) + + it('renders loading when user is loading', async () => { + setupMocks({ isCurrentUserLoading: true }) + render(, { wrapper }) + + await waitFor(() => { + expect(screen.getByText('Loading...')).toBeInTheDocument() + }) + }) + + it('renders loading when report is loading', async () => { + setupMocks({ isReportLoading: true }) + render(, { wrapper }) + + await waitFor(() => { + expect(screen.getByText('Loading...')).toBeInTheDocument() + }) + }) + + it('renders ViewLegacyComplianceReport when legacyId is present', async () => { + setupMocks({ + reportData: { + report: { + legacyId: 999, + currentStatus: { status: 'DRAFT' } + } + } + }) + + render(, { wrapper }) + + await waitFor(() => { + expect(screen.getByText('Legacy Report View')).toBeInTheDocument() + expect( + screen.queryByText('Edit Compliance Report') + ).not.toBeInTheDocument() + }) + }) + + it('renders EditViewComplianceReport when legacyId is null/undefined', async () => { + setupMocks({ + reportData: { + report: { + currentStatus: { status: 'DRAFT' } + // No legacyId means it should render the EditViewComplianceReport + } + } + }) + + render(, { wrapper }) + + await waitFor(() => { + expect(screen.getByText('Edit Compliance Report')).toBeInTheDocument() + expect(screen.queryByText('Legacy Report View')).not.toBeInTheDocument() + }) + }) + + it('passes error and isError props to the rendered component', async () => { + const testError = { message: 'Test error' } + setupMocks({ + isError: true, + error: testError, + reportData: { + report: { + currentStatus: { status: 'DRAFT' } + } + } + }) + + render(, { wrapper }) + + await waitFor(() => { + expect(screen.getByText('Edit Compliance Report')).toBeInTheDocument() + }) + }) +}) diff --git a/frontend/src/views/ComplianceReports/__tests__/ViewLegacyComplianceReport.test.jsx b/frontend/src/views/ComplianceReports/__tests__/ViewLegacyComplianceReport.test.jsx new file mode 100644 index 000000000..4394e044d --- /dev/null +++ b/frontend/src/views/ComplianceReports/__tests__/ViewLegacyComplianceReport.test.jsx @@ -0,0 +1,149 @@ +import React from 'react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses' +import { CONFIG } from '@/constants/config' +import { LegacyAssessmentCard } from '@/views/ComplianceReports/components/LegacyAssessmentCard.jsx' +import { wrapper } from '@/tests/utils/wrapper.jsx' + +// Mock useTranslation +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key) => key + }) +})) + +// Mock HistoryCard +vi.mock('@/views/ComplianceReports/components/HistoryCard.jsx', () => ({ + HistoryCard: ({ report }) =>
HistoryCard - Version {report.version}
+})) + +// Mock window.open +global.open = vi.fn() + +describe('LegacyAssessmentCard', () => { + const setup = (overrides = {}) => { + const defaultProps = { + orgData: { + name: 'Test Org', + orgAddress: { + addressLine1: '123 Test St', + city: 'Test City', + state: 'TS', + postalCode: '12345' + }, + orgAttorneyAddress: { + addressLine1: '456 Law St', + city: 'Law City', + state: 'LS', + postalCode: '67890' + } + }, + hasSupplemental: false, + isGovernmentUser: false, + currentStatus: COMPLIANCE_REPORT_STATUSES.DRAFT, + legacyReportId: '999', + chain: [] + } + + const props = { ...defaultProps, ...overrides } + + return render(, { wrapper }) + } + + beforeEach(() => { + vi.resetAllMocks() + }) + + it('renders the organization details and addresses', () => { + setup() + expect(screen.getByText('report:orgDetails')).toBeInTheDocument() + expect(screen.getByText('Test Org')).toBeInTheDocument() + expect(screen.getByText(/report:serviceAddrLabel/)).toBeInTheDocument() + expect(screen.getByText(/report:bcAddrLabel/)).toBeInTheDocument() + }) + + it('displays contact instructions for non-government users', () => { + setup({ isGovernmentUser: false }) + expect( + screen.getByText('report:contactForAddrChange', { exact: false }) + ).toBeInTheDocument() + }) + + it('does not display contact instructions for government users', () => { + setup({ isGovernmentUser: true }) + expect( + screen.queryByText('report:contactForAddrChange', { exact: false }) + ).not.toBeInTheDocument() + }) + + it('displays the supplemental warning text', () => { + setup() + expect(screen.getByText('report:supplementalWarning')).toBeInTheDocument() + }) + + it('displays "View Legacy" button which opens the legacy report link on click', async () => { + setup({ legacyReportId: '999' }) + const viewBtn = screen.getByText('report:viewLegacyBtn') + expect(viewBtn).toBeInTheDocument() + fireEvent.click(viewBtn) + await waitFor(() => { + expect(global.open).toHaveBeenCalledWith( + `${CONFIG.TFRS_BASE}/compliance_reporting/edit/999/intro`, + '_blank' + ) + }) + }) + + it('displays history when chain is not empty', () => { + setup({ + chain: [ + { + version: 0, + report: { + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.SUBMITTED } + } + }, + { + version: 1, + report: { + currentStatus: { status: COMPLIANCE_REPORT_STATUSES.ASSESSED } + } + } + ] + }) + expect(screen.getByText('report:reportHistory')).toBeInTheDocument() + expect(screen.getByText('HistoryCard - Version 0')).toBeInTheDocument() + expect(screen.getByText('HistoryCard - Version 1')).toBeInTheDocument() + }) + + it('shows "report:assessment" title when currentStatus is ASSESSED', () => { + setup({ currentStatus: COMPLIANCE_REPORT_STATUSES.ASSESSED }) + expect(screen.getByText('report:assessment')).toBeInTheDocument() + }) + + it('shows "report:assessment" title for government users even if not assessed', () => { + setup({ + isGovernmentUser: true, + currentStatus: COMPLIANCE_REPORT_STATUSES.DRAFT + }) + expect(screen.getByText('report:assessment')).toBeInTheDocument() + }) + + it('shows "report:assessment" title if supplemental is true', () => { + setup({ + hasSupplemental: true, + currentStatus: COMPLIANCE_REPORT_STATUSES.DRAFT + }) + expect(screen.getByText('report:assessment')).toBeInTheDocument() + }) + + it('shows "report:orgDetails" title if not assessed, not government user, and no supplemental', () => { + setup({ + isGovernmentUser: false, + hasSupplemental: false, + currentStatus: COMPLIANCE_REPORT_STATUSES.DRAFT + }) + expect(screen.getByText('report:orgDetails')).toBeInTheDocument() + }) +}) diff --git a/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx b/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx index 1b69bcd17..9c55d4117 100644 --- a/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx +++ b/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx @@ -6,119 +6,14 @@ import { StyledListItem } from '@/components/StyledListItem' import { roles } from '@/constants/roles' import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses' import { useCreateSupplementalReport } from '@/hooks/useComplianceReports' -import { useCurrentUser } from '@/hooks/useCurrentUser' import { constructAddress } from '@/utils/constructAddress' -import { timezoneFormatter } from '@/utils/formatters' import AssignmentIcon from '@mui/icons-material/Assignment' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import { List, ListItemText, Stack, styled } from '@mui/material' -import MuiAccordion from '@mui/material/Accordion' -import MuiAccordionDetails from '@mui/material/AccordionDetails' -import MuiAccordionSummary, { - accordionSummaryClasses -} from '@mui/material/AccordionSummary' +import { List, ListItemText, Stack } from '@mui/material' import Box from '@mui/material/Box' -import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { FEATURE_FLAGS, isFeatureEnabled } from '@/constants/config.js' - -const Accordion = styled((props) => ( - -))(() => ({ - border: `none`, - '&::before': { - display: 'none' - } -})) - -const AccordionSummary = styled((props) => ( - } - {...props} - /> -))(() => ({ - minHeight: 'unset', - padding: 0, - flexDirection: 'row-reverse', - [`& .${accordionSummaryClasses.content}`]: { - margin: 0 - }, - [`& .${accordionSummaryClasses.expanded}`]: { - margin: 0 - } -})) - -const AccordionDetails = styled(MuiAccordionDetails)(() => ({ - paddingLeft: '1rem', - paddingTop: 0, - paddingBottom: 0 -})) - -const HistoryCard = ({ report }) => { - const { data: currentUser } = useCurrentUser() - const isGovernmentUser = currentUser?.isGovernmentUser - const { t } = useTranslation(['report']) - const filteredHistory = useMemo(() => { - if (!report.history || report.history.length === 0) { - return [] - } - // Sort the history array by date in descending order - return [...report.history] - .sort((a, b) => { - return new Date(b.createDate) - new Date(a.createDate) - }) - .map((item) => { - if ( - item.status.status === COMPLIANCE_REPORT_STATUSES.ASSESSED && - !isGovernmentUser - ) { - item.status.status = 'AssessedBy' - } - return item - }) - .filter((item) => item.status.status !== COMPLIANCE_REPORT_STATUSES.DRAFT) - }, [isGovernmentUser, report.history]) - return ( - - } - aria-controls="panel1-content" - > - - {report.version === 0 - ? `${report.compliancePeriod.description} Compliance Report` - : report.nickname} - : {report.currentStatus.status} - - - - - {filteredHistory.map((item, index) => ( - - - - - - ))} - - - - ) -} +import { HistoryCard } from '@/views/ComplianceReports/components/HistoryCard.jsx' export const AssessmentCard = ({ orgData, diff --git a/frontend/src/views/ComplianceReports/components/HistoryCard.jsx b/frontend/src/views/ComplianceReports/components/HistoryCard.jsx new file mode 100644 index 000000000..fb8a755a8 --- /dev/null +++ b/frontend/src/views/ComplianceReports/components/HistoryCard.jsx @@ -0,0 +1,114 @@ +import React, { useMemo } from 'react' +import { List, ListItemText, styled } from '@mui/material' +import MuiAccordion from '@mui/material/Accordion' +import MuiAccordionSummary, { + accordionSummaryClasses +} from '@mui/material/AccordionSummary' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import MuiAccordionDetails from '@mui/material/AccordionDetails' +import { useCurrentUser } from '@/hooks/useCurrentUser.js' +import { useTranslation } from 'react-i18next' +import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses.js' +import BCTypography from '@/components/BCTypography/index.jsx' +import { StyledListItem } from '@/components/StyledListItem.jsx' +import { timezoneFormatter } from '@/utils/formatters.js' + +const Accordion = styled((props) => ( + +))(() => ({ + border: `none`, + '&::before': { + display: 'none' + } +})) + +const AccordionSummary = styled((props) => ( + } + {...props} + /> +))(() => ({ + minHeight: 'unset', + padding: 0, + flexDirection: 'row-reverse', + [`& .${accordionSummaryClasses.content}`]: { + margin: 0 + }, + [`& .${accordionSummaryClasses.expanded}`]: { + margin: 0 + } +})) + +const AccordionDetails = styled(MuiAccordionDetails)(() => ({ + paddingLeft: '1rem', + paddingTop: 0, + paddingBottom: 0 +})) + +export const HistoryCard = ({ report }) => { + const { data: currentUser } = useCurrentUser() + const isGovernmentUser = currentUser?.isGovernmentUser + const { t } = useTranslation(['report']) + const filteredHistory = useMemo(() => { + if (!report.history || report.history.length === 0) { + return [] + } + // Sort the history array by date in descending order + return [...report.history] + .sort((a, b) => { + return new Date(b.createDate) - new Date(a.createDate) + }) + .map((item) => { + if ( + item.status.status === COMPLIANCE_REPORT_STATUSES.ASSESSED && + !isGovernmentUser + ) { + item.status.status = 'AssessedBy' + } + return item + }) + .filter((item) => item.status.status !== COMPLIANCE_REPORT_STATUSES.DRAFT) + }, [isGovernmentUser, report.history]) + return ( + + } + aria-controls="panel1-content" + > + + {report.version === 0 + ? `${report.compliancePeriod.description} Compliance Report` + : report.nickname} + : {report.currentStatus.status} + + + + + {filteredHistory.map((item, index) => ( + + + + + + ))} + + + + ) +} diff --git a/frontend/src/views/ComplianceReports/components/LegacyAssessmentCard.jsx b/frontend/src/views/ComplianceReports/components/LegacyAssessmentCard.jsx new file mode 100644 index 000000000..697a17b4f --- /dev/null +++ b/frontend/src/views/ComplianceReports/components/LegacyAssessmentCard.jsx @@ -0,0 +1,128 @@ +import BCButton from '@/components/BCButton' +import BCTypography from '@/components/BCTypography' +import BCWidgetCard from '@/components/BCWidgetCard/BCWidgetCard' +import { StyledListItem } from '@/components/StyledListItem' +import { COMPLIANCE_REPORT_STATUSES } from '@/constants/statuses' +import { constructAddress } from '@/utils/constructAddress' +import AssignmentIcon from '@mui/icons-material/Assignment' +import { List, ListItemText, Stack } from '@mui/material' +import Box from '@mui/material/Box' +import { useTranslation } from 'react-i18next' +import { CONFIG } from '@/constants/config' +import { HistoryCard } from '@/views/ComplianceReports/components/HistoryCard.jsx' + +export const LegacyAssessmentCard = ({ + orgData, + hasSupplemental, + isGovernmentUser, + currentStatus, + legacyReportId, + chain +}) => { + const { t } = useTranslation(['report']) + + const viewLegacyReport = () => { + window.open( + `${CONFIG.TFRS_BASE}/compliance_reporting/edit/${legacyReportId}/intro`, + '_blank' + ) + } + return ( + <> + + + + {orgData?.name} + + + + + {t('report:serviceAddrLabel')}: + {orgData && constructAddress(orgData.orgAddress)} + + + + + {t('report:bcAddrLabel')}:{' '} + {orgData && constructAddress(orgData.orgAttorneyAddress)} + + + + + {!isGovernmentUser && ( + + )} + {!!chain.length && ( + <> + + {t('report:reportHistory')} + + {chain.map((report) => ( + + ))} + + )} + + + {t('report:supplementalWarning')} + + + } + sx={{ mt: 2 }} + onClick={viewLegacyReport} + > + {t('report:viewLegacyBtn')} + + + + + } + /> + + {t('report:questions')} + + + + ) +} diff --git a/frontend/src/views/ComplianceReports/components/_schema.jsx b/frontend/src/views/ComplianceReports/components/_schema.jsx index e83fcdc19..e7f1546fb 100644 --- a/frontend/src/views/ComplianceReports/components/_schema.jsx +++ b/frontend/src/views/ComplianceReports/components/_schema.jsx @@ -1,6 +1,6 @@ import { BCColumnSetFilter } from '@/components/BCDataGrid/components' import { SUMMARY } from '@/constants/common' -import { ReportsStatusRenderer, LinkRenderer } from '@/utils/grid/cellRenderers' +import { ReportsStatusRenderer } from '@/utils/grid/cellRenderers' import { timezoneFormatter } from '@/utils/formatters' export const reportsColDefs = (t, bceidRole) => [ @@ -8,11 +8,6 @@ export const reportsColDefs = (t, bceidRole) => [ field: 'compliancePeriod', headerName: t('report:reportColLabels.compliancePeriod'), width: 210, - cellRenderer: LinkRenderer, - cellRendererParams: { - url: ({ data }) => - `${data.compliancePeriod?.description}/${data.complianceReportId}` - }, valueGetter: ({ data }) => data.compliancePeriod?.description || '', filterParams: { buttons: ['clear'] @@ -23,11 +18,6 @@ export const reportsColDefs = (t, bceidRole) => [ headerName: t('report:reportColLabels.organization'), flex: 2, hide: bceidRole, - cellRenderer: LinkRenderer, - cellRendererParams: { - url: ({ data }) => - `${data.compliancePeriod?.description}/${data.complianceReportId}` - }, valueGetter: ({ data }) => data.organization?.name || '' }, { From 39457c8170eec56ba9056c961ca47826d36cde22 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 18 Dec 2024 10:39:52 -0800 Subject: [PATCH 2/5] Code Review Feedback * Rename ComplianceReportViewSelector --- backend/lcfs/services/rabbitmq/report_consumer.py | 3 +++ frontend/src/App.jsx | 4 ++-- ...Brancher.jsx => ComplianceReportViewSelector.jsx} | 2 +- ...est.jsx => ComplianceReportViewSelector.test.jsx} | 12 ++++++------ 4 files changed, 12 insertions(+), 9 deletions(-) rename frontend/src/views/ComplianceReports/{ViewComplianceReportBrancher.jsx => ComplianceReportViewSelector.jsx} (95%) rename frontend/src/views/ComplianceReports/__tests__/{ViewComplianceReportBrancher.test.jsx => ComplianceReportViewSelector.test.jsx} (90%) diff --git a/backend/lcfs/services/rabbitmq/report_consumer.py b/backend/lcfs/services/rabbitmq/report_consumer.py index f03df28ee..551f3781b 100644 --- a/backend/lcfs/services/rabbitmq/report_consumer.py +++ b/backend/lcfs/services/rabbitmq/report_consumer.py @@ -156,6 +156,9 @@ async def handle_message( ) user = await UserRepository(db=session).get_user_by_id(user_id) + if not user: + logger.error(f"Cannot parse Report {legacy_id} from TFRS, no user with ID {user_id}") + if action == "Created": await self._handle_created( org_id, diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 907ce057a..27d484513 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -36,7 +36,7 @@ import { AddEditAllocationAgreements } from './views/AllocationAgreements/AddEdi import { logout } from '@/utils/keycloak.js' import { CompareReports } from '@/views/CompareReports/CompareReports' import { ViewLegacyComplianceReport } from '@/views/ComplianceReports/ViewLegacyComplianceReport.jsx' -import { ViewComplianceReportBrancher } from '@/views/ComplianceReports/ViewComplianceReportBrancher.jsx' +import { ComplianceReportViewSelector } from '@/views/ComplianceReports/ComplianceReportViewSelector.jsx' const router = createBrowserRouter([ { @@ -198,7 +198,7 @@ const router = createBrowserRouter([ }, { path: ROUTES.REPORTS_VIEW, - element: , + element: , handle: { title: '' } }, { diff --git a/frontend/src/views/ComplianceReports/ViewComplianceReportBrancher.jsx b/frontend/src/views/ComplianceReports/ComplianceReportViewSelector.jsx similarity index 95% rename from frontend/src/views/ComplianceReports/ViewComplianceReportBrancher.jsx rename to frontend/src/views/ComplianceReports/ComplianceReportViewSelector.jsx index de4b97f75..04cb92111 100644 --- a/frontend/src/views/ComplianceReports/ViewComplianceReportBrancher.jsx +++ b/frontend/src/views/ComplianceReports/ComplianceReportViewSelector.jsx @@ -5,7 +5,7 @@ import { ViewLegacyComplianceReport } from '@/views/ComplianceReports/ViewLegacy import { useParams } from 'react-router-dom' import { EditViewComplianceReport } from '@/views/ComplianceReports/EditViewComplianceReport.jsx' -export const ViewComplianceReportBrancher = () => { +export const ComplianceReportViewSelector = () => { const { complianceReportId } = useParams() const { data: currentUser, isLoading: isCurrentUserLoading } = useCurrentUser() diff --git a/frontend/src/views/ComplianceReports/__tests__/ViewComplianceReportBrancher.test.jsx b/frontend/src/views/ComplianceReports/__tests__/ComplianceReportViewSelector.test.jsx similarity index 90% rename from frontend/src/views/ComplianceReports/__tests__/ViewComplianceReportBrancher.test.jsx rename to frontend/src/views/ComplianceReports/__tests__/ComplianceReportViewSelector.test.jsx index 0b3127534..4ceada497 100644 --- a/frontend/src/views/ComplianceReports/__tests__/ViewComplianceReportBrancher.test.jsx +++ b/frontend/src/views/ComplianceReports/__tests__/ComplianceReportViewSelector.test.jsx @@ -1,7 +1,7 @@ import React from 'react' import { render, screen, waitFor } from '@testing-library/react' import { describe, it, expect, beforeEach, vi } from 'vitest' -import { ViewComplianceReportBrancher } from '../ViewComplianceReportBrancher' +import { ComplianceReportViewSelector } from '../ComplianceReportViewSelector.jsx' import * as useComplianceReportsHook from '@/hooks/useComplianceReports' import * as useCurrentUserHook from '@/hooks/useCurrentUser' import { wrapper } from '@/tests/utils/wrapper' @@ -71,7 +71,7 @@ describe('ViewComplianceReportBrancher', () => { it('renders loading when user is loading', async () => { setupMocks({ isCurrentUserLoading: true }) - render(, { wrapper }) + render(, { wrapper }) await waitFor(() => { expect(screen.getByText('Loading...')).toBeInTheDocument() @@ -80,7 +80,7 @@ describe('ViewComplianceReportBrancher', () => { it('renders loading when report is loading', async () => { setupMocks({ isReportLoading: true }) - render(, { wrapper }) + render(, { wrapper }) await waitFor(() => { expect(screen.getByText('Loading...')).toBeInTheDocument() @@ -97,7 +97,7 @@ describe('ViewComplianceReportBrancher', () => { } }) - render(, { wrapper }) + render(, { wrapper }) await waitFor(() => { expect(screen.getByText('Legacy Report View')).toBeInTheDocument() @@ -117,7 +117,7 @@ describe('ViewComplianceReportBrancher', () => { } }) - render(, { wrapper }) + render(, { wrapper }) await waitFor(() => { expect(screen.getByText('Edit Compliance Report')).toBeInTheDocument() @@ -137,7 +137,7 @@ describe('ViewComplianceReportBrancher', () => { } }) - render(, { wrapper }) + render(, { wrapper }) await waitFor(() => { expect(screen.getByText('Edit Compliance Report')).toBeInTheDocument() From 78d4523d8b946b3e00554ba679263a71564f3afb Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 18 Dec 2024 10:51:14 -0800 Subject: [PATCH 3/5] Decrease logging --- backend/lcfs/web/api/base.py | 66 +++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/backend/lcfs/web/api/base.py b/backend/lcfs/web/api/base.py index ecb1d3693..288f38923 100644 --- a/backend/lcfs/web/api/base.py +++ b/backend/lcfs/web/api/base.py @@ -121,9 +121,6 @@ def validate_pagination(pagination: PaginationRequestSchema): Args: pagination (PaginationRequestSchema): The pagination object to validate. """ - logger.info("Validating pagination") - logger.debug("Pagination details", pagination=pagination) - if not pagination.page or pagination.page < 1: pagination.page = 1 if not pagination.size or pagination.size < 1: @@ -373,24 +370,55 @@ async def lcfs_cache_key_builder( # Return the cache key return cache_key + class NotificationTypeEnum(Enum): - BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT = "BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT" - BCEID__INITIATIVE_AGREEMENT__DIRECTOR_APPROVAL = "BCEID__INITIATIVE_AGREEMENT__DIRECTOR_APPROVAL" + BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT = ( + "BCEID__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT" + ) + BCEID__INITIATIVE_AGREEMENT__DIRECTOR_APPROVAL = ( + "BCEID__INITIATIVE_AGREEMENT__DIRECTOR_APPROVAL" + ) BCEID__TRANSFER__DIRECTOR_DECISION = "BCEID__TRANSFER__DIRECTOR_DECISION" BCEID__TRANSFER__PARTNER_ACTIONS = "BCEID__TRANSFER__PARTNER_ACTIONS" - IDIR_ANALYST__COMPLIANCE_REPORT__DIRECTOR_DECISION = "IDIR_ANALYST__COMPLIANCE_REPORT__DIRECTOR_DECISION" - IDIR_ANALYST__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION = "IDIR_ANALYST__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION" - IDIR_ANALYST__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW = "IDIR_ANALYST__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW" - IDIR_ANALYST__INITIATIVE_AGREEMENT__RETURNED_TO_ANALYST = "IDIR_ANALYST__INITIATIVE_AGREEMENT__RETURNED_TO_ANALYST" - IDIR_ANALYST__TRANSFER__DIRECTOR_RECORDED = "IDIR_ANALYST__TRANSFER__DIRECTOR_RECORDED" - IDIR_ANALYST__TRANSFER__RESCINDED_ACTION = "IDIR_ANALYST__TRANSFER__RESCINDED_ACTION" - IDIR_ANALYST__TRANSFER__SUBMITTED_FOR_REVIEW = "IDIR_ANALYST__TRANSFER__SUBMITTED_FOR_REVIEW" - IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__ANALYST_RECOMMENDATION = "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__ANALYST_RECOMMENDATION" - IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT = "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT" - IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW = "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW" - IDIR_DIRECTOR__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION = "IDIR_DIRECTOR__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION" - IDIR_DIRECTOR__INITIATIVE_AGREEMENT__ANALYST_RECOMMENDATION = "IDIR_DIRECTOR__INITIATIVE_AGREEMENT__ANALYST_RECOMMENDATION" - IDIR_DIRECTOR__TRANSFER__ANALYST_RECOMMENDATION = "IDIR_DIRECTOR__TRANSFER__ANALYST_RECOMMENDATION" + IDIR_ANALYST__COMPLIANCE_REPORT__DIRECTOR_DECISION = ( + "IDIR_ANALYST__COMPLIANCE_REPORT__DIRECTOR_DECISION" + ) + IDIR_ANALYST__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION = ( + "IDIR_ANALYST__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION" + ) + IDIR_ANALYST__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW = ( + "IDIR_ANALYST__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW" + ) + IDIR_ANALYST__INITIATIVE_AGREEMENT__RETURNED_TO_ANALYST = ( + "IDIR_ANALYST__INITIATIVE_AGREEMENT__RETURNED_TO_ANALYST" + ) + IDIR_ANALYST__TRANSFER__DIRECTOR_RECORDED = ( + "IDIR_ANALYST__TRANSFER__DIRECTOR_RECORDED" + ) + IDIR_ANALYST__TRANSFER__RESCINDED_ACTION = ( + "IDIR_ANALYST__TRANSFER__RESCINDED_ACTION" + ) + IDIR_ANALYST__TRANSFER__SUBMITTED_FOR_REVIEW = ( + "IDIR_ANALYST__TRANSFER__SUBMITTED_FOR_REVIEW" + ) + IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__ANALYST_RECOMMENDATION = ( + "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__ANALYST_RECOMMENDATION" + ) + IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT = ( + "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__DIRECTOR_ASSESSMENT" + ) + IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW = ( + "IDIR_COMPLIANCE_MANAGER__COMPLIANCE_REPORT__SUBMITTED_FOR_REVIEW" + ) + IDIR_DIRECTOR__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION = ( + "IDIR_DIRECTOR__COMPLIANCE_REPORT__MANAGER_RECOMMENDATION" + ) + IDIR_DIRECTOR__INITIATIVE_AGREEMENT__ANALYST_RECOMMENDATION = ( + "IDIR_DIRECTOR__INITIATIVE_AGREEMENT__ANALYST_RECOMMENDATION" + ) + IDIR_DIRECTOR__TRANSFER__ANALYST_RECOMMENDATION = ( + "IDIR_DIRECTOR__TRANSFER__ANALYST_RECOMMENDATION" + ) def __str__(self): - return self.value \ No newline at end of file + return self.value From 94cf9244cd4795304f818071c41efb34b0b4140f Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 18 Dec 2024 10:55:54 -0800 Subject: [PATCH 4/5] Add missing await --- backend/lcfs/web/api/fuel_export/repo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/lcfs/web/api/fuel_export/repo.py b/backend/lcfs/web/api/fuel_export/repo.py index d09a546dc..510af73b6 100644 --- a/backend/lcfs/web/api/fuel_export/repo.py +++ b/backend/lcfs/web/api/fuel_export/repo.py @@ -233,7 +233,7 @@ async def update_fuel_export(self, fuel_export: FuelExport) -> FuelExport: """ Update an existing fuel supply row in the database. """ - updated_fuel_export = self.db.merge(fuel_export) + updated_fuel_export = await self.db.merge(fuel_export) await self.db.flush() await self.db.refresh( updated_fuel_export, From d10b9fefc524399737d7ae05f37e5801e306a501 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 19 Dec 2024 10:01:30 -0800 Subject: [PATCH 5/5] Code Review Feedback --- backend/lcfs/tests/fuel_export/test_fuel_exports_repo.py | 2 +- frontend/public/config/config.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/lcfs/tests/fuel_export/test_fuel_exports_repo.py b/backend/lcfs/tests/fuel_export/test_fuel_exports_repo.py index 3850fd34d..539bb834a 100644 --- a/backend/lcfs/tests/fuel_export/test_fuel_exports_repo.py +++ b/backend/lcfs/tests/fuel_export/test_fuel_exports_repo.py @@ -214,7 +214,7 @@ async def test_update_fuel_export_success(fuel_export_repo, mock_db): ) updated_fuel_export = FuelExport(fuel_export_id=1) - mock_db.merge = MagicMock(return_value=updated_fuel_export) + mock_db.merge = AsyncMock(return_value=updated_fuel_export) mock_db.flush = AsyncMock() mock_db.refresh = AsyncMock() diff --git a/frontend/public/config/config.js b/frontend/public/config/config.js index c3885613c..63704496a 100644 --- a/frontend/public/config/config.js +++ b/frontend/public/config/config.js @@ -1,6 +1,6 @@ export const config = { api_base: 'http://localhost:8000/api', - tfrs_base: 'http://localhost:3001', + tfrs_base: 'http://localhost:3000', keycloak: { REALM: 'standard', CLIENT_ID: 'low-carbon-fuel-standard-5147',