From 0bbea98fb22324adf556f2147a0896bf3e023382 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 18 Dec 2024 14:44:45 -0800 Subject: [PATCH 01/18] fix: Round non-currency numbers in Summary Table * Update number formatter to take max decimals * Set max decimals to 0 on Summary Page --- frontend/src/utils/formatters.js | 9 +++++++-- .../views/ComplianceReports/components/SummaryTable.jsx | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/utils/formatters.js b/frontend/src/utils/formatters.js index 78ed0e0e6..ebcdaeef6 100644 --- a/frontend/src/utils/formatters.js +++ b/frontend/src/utils/formatters.js @@ -7,9 +7,14 @@ import { ROLES_BADGE_SIZE } from '@/constants/common' * @param {Object|number|string|null} params - The input parameter which can be an object with a `value` property, a number, or a string. * @param {number|string} [params.value] - The value to be formatted, if params is an object. * @param {boolean} [useParentheses=false] - Whether to use parentheses for negative numbers. + * @param maxDecimals the max number of decimals to return * @returns {string} - The formatted number as a string, or the original value if it cannot be parsed as a number. */ -export const numberFormatter = (params, useParentheses = false) => { +export const numberFormatter = ( + params, + useParentheses = false, + maxDecimals = 10 +) => { if (params == null || (typeof params === 'object' && params.value == null)) return '' @@ -21,7 +26,7 @@ export const numberFormatter = (params, useParentheses = false) => { const absValue = Math.abs(parsedValue) const formattedValue = absValue.toLocaleString(undefined, { minimumFractionDigits: 0, - maximumFractionDigits: 10 + maximumFractionDigits: maxDecimals }) if (parsedValue < 0) { diff --git a/frontend/src/views/ComplianceReports/components/SummaryTable.jsx b/frontend/src/views/ComplianceReports/components/SummaryTable.jsx index 0077e5bde..952b918ee 100644 --- a/frontend/src/views/ComplianceReports/components/SummaryTable.jsx +++ b/frontend/src/views/ComplianceReports/components/SummaryTable.jsx @@ -194,7 +194,8 @@ const SummaryTable = ({ {row.format && colIndex !== 0 ? rowFormatters[row.format]( row[column.id], - useParenthesis + useParenthesis, + 0 ) : row[column.id]} From 871f88075368e06e6a442549ee3dfd4cfaa8b351 Mon Sep 17 00:00:00 2001 From: Arturo Reyes Lopez Date: Thu, 19 Dec 2024 05:50:39 -0700 Subject: [PATCH 02/18] Optimize query for fuel supply --- backend/lcfs/web/api/fuel_supply/repo.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/lcfs/web/api/fuel_supply/repo.py b/backend/lcfs/web/api/fuel_supply/repo.py index 77a8c3852..dca195539 100644 --- a/backend/lcfs/web/api/fuel_supply/repo.py +++ b/backend/lcfs/web/api/fuel_supply/repo.py @@ -443,20 +443,23 @@ async def get_effective_fuel_supplies( query = ( select(FuelSupply) .options( - # Use joinedload for scalar relationships - joinedload(FuelSupply.fuel_code).options( - joinedload(FuelCode.fuel_code_status), - joinedload(FuelCode.fuel_code_prefix), + # Use selectinload for collections + selectinload(FuelSupply.fuel_code).options( + selectinload(FuelCode.fuel_code_status), + selectinload(FuelCode.fuel_code_prefix), ), - joinedload(FuelSupply.fuel_category).options( - joinedload(FuelCategory.target_carbon_intensities), - joinedload(FuelCategory.energy_effectiveness_ratio), + # Use selectinload for one-to-many relationships + selectinload(FuelSupply.fuel_category).options( + selectinload(FuelCategory.target_carbon_intensities), + selectinload(FuelCategory.energy_effectiveness_ratio), ), + # Use joinedload for many-to-one relationships joinedload(FuelSupply.fuel_type).options( joinedload(FuelType.energy_density), joinedload(FuelType.additional_carbon_intensity), joinedload(FuelType.energy_effectiveness_ratio), ), + # Use joinedload for single relationships joinedload(FuelSupply.provision_of_the_act), selectinload(FuelSupply.end_use_type), ) @@ -467,6 +470,7 @@ async def get_effective_fuel_supplies( FuelSupply.version == valid_fuel_supplies_subq.c.max_version, user_type_priority == valid_fuel_supplies_subq.c.max_role_priority, ), + isouter=False # Explicit inner join ) .order_by(FuelSupply.create_date.asc()) ) From e3b77f642addc7392f4eccc037a1585032c910b1 Mon Sep 17 00:00:00 2001 From: Hamed Valiollahi Bayeki Date: Thu, 19 Dec 2024 08:44:24 -0800 Subject: [PATCH 03/18] fix: correct pagination to reflect filtered user count --- backend/lcfs/web/api/user/services.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/lcfs/web/api/user/services.py b/backend/lcfs/web/api/user/services.py index bd539f6bb..e20a3c7ff 100644 --- a/backend/lcfs/web/api/user/services.py +++ b/backend/lcfs/web/api/user/services.py @@ -125,16 +125,16 @@ async def get_all_users(self, pagination: PaginationRequestSchema) -> UsersSchem Get all users """ users, total_count = await self.repo.get_users_paginated(pagination=pagination) - if len(users) == 0: - raise DataNotFoundException("No users found") return UsersSchema( pagination=PaginationResponseSchema( total=total_count, page=pagination.page, size=pagination.size, - total_pages=math.ceil(total_count / pagination.size), + total_pages=( + math.ceil(total_count / pagination.size) if total_count > 0 else 0 + ), ), - users=users, + users=users if users else [], ) @service_handler From b5ff8571321bee75cfd62a30646748eb4e16193d Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 16 Dec 2024 16:01:29 -0800 Subject: [PATCH 04/18] 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 05/18] 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 06/18] 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 07/18] 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 08/18] 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', From da7a5b469fbaf02f11acd22b54e408d86a8140f5 Mon Sep 17 00:00:00 2001 From: Arturo Reyes Lopez Date: Thu, 19 Dec 2024 13:00:40 -0700 Subject: [PATCH 09/18] Removing pre-select in Fuel Category when having multiple elements. --- frontend/src/assets/locales/en/fuelSupply.json | 2 +- .../src/views/FuelSupplies/AddEditFuelSupplies.jsx | 10 ++++++---- frontend/src/views/FuelSupplies/_schema.jsx | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/assets/locales/en/fuelSupply.json b/frontend/src/assets/locales/en/fuelSupply.json index 93c75760b..92ed0968f 100644 --- a/frontend/src/assets/locales/en/fuelSupply.json +++ b/frontend/src/assets/locales/en/fuelSupply.json @@ -15,7 +15,7 @@ "fuelSupplyId": "Fuel supply ID", "fuelType": "Fuel type", "fuelTypeOther": "Fuel type other", - "fuelCategory": "Fuel category", + "fuelCategoryId": "Fuel category", "endUse": "End use", "provisionOfTheActId": "Determining carbon intensity", "fuelCode": "Fuel code", diff --git a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx index 9d3ae3134..3873ed68d 100644 --- a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx +++ b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx @@ -161,10 +161,12 @@ export const AddEditFuelSupplies = () => { (item) => item.fuelCategory ) - params.node.setDataValue( - 'fuelCategory', - fuelCategoryOptions[0] ?? null - ) + // Set to null if multiple options, otherwise use first item + const categoryValue = fuelCategoryOptions.length === 1 + ? fuelCategoryOptions[0] + : null + + params.node.setDataValue('fuelCategory', categoryValue) } } }, diff --git a/frontend/src/views/FuelSupplies/_schema.jsx b/frontend/src/views/FuelSupplies/_schema.jsx index 5939d074b..969a3e9f9 100644 --- a/frontend/src/views/FuelSupplies/_schema.jsx +++ b/frontend/src/views/FuelSupplies/_schema.jsx @@ -145,9 +145,9 @@ export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ minWidth: 250 }, { - field: 'fuelCategory', + field: 'fuelCategoryId', headerComponent: RequiredHeader, - headerName: i18n.t('fuelSupply:fuelSupplyColLabels.fuelCategory'), + headerName: i18n.t('fuelSupply:fuelSupplyColLabels.fuelCategoryId'), cellEditor: AutocompleteCellEditor, cellRenderer: (params) => params.value || From 45c8c733c8dc708448d184ac18d302762fbdd5c0 Mon Sep 17 00:00:00 2001 From: Hamed Valiollahi Bayeki Date: Thu, 19 Dec 2024 12:38:38 -0800 Subject: [PATCH 10/18] fix: remove unnecessary condition for get_users_paginated --- backend/lcfs/web/api/user/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/lcfs/web/api/user/services.py b/backend/lcfs/web/api/user/services.py index e20a3c7ff..f6d21fa08 100644 --- a/backend/lcfs/web/api/user/services.py +++ b/backend/lcfs/web/api/user/services.py @@ -134,7 +134,7 @@ async def get_all_users(self, pagination: PaginationRequestSchema) -> UsersSchem math.ceil(total_count / pagination.size) if total_count > 0 else 0 ), ), - users=users if users else [], + users=users, ) @service_handler From fefcaccf802b8443e5c8fc6084b49e66c4d101d6 Mon Sep 17 00:00:00 2001 From: prv-proton Date: Thu, 19 Dec 2024 13:05:46 -0800 Subject: [PATCH 11/18] Update fuel code explanation for IDIR users --- frontend/src/assets/locales/en/fuelCode.json | 1 + .../FuelCodes/AddFuelCode/AddEditFuelCode.jsx | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/frontend/src/assets/locales/en/fuelCode.json b/frontend/src/assets/locales/en/fuelCode.json index 84f150236..e1948199c 100644 --- a/frontend/src/assets/locales/en/fuelCode.json +++ b/frontend/src/assets/locales/en/fuelCode.json @@ -6,6 +6,7 @@ "fuelCodeDownloadFailMsg": "Failed to download fuel code information.", "fuelCodeLoadFailMsg": "Failed to load fuel code information.", "newFuelCodeTitle": "Add new fuel code(s)", + "fuelCodeEntryGuide": "Draft fuel codes are saved automatically when all mandatory fields have been entered and validated. Click “Approve code” to make it available in compliance reporting.", "editFuelCodeTitle": "Edit draft fuel code", "viewFuelCodeTitle": "View fuel code", "approvedFuelCodeTitle": "Approved draft fuel code", diff --git a/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx b/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx index ca05f1413..1e54f01bc 100644 --- a/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx +++ b/frontend/src/views/FuelCodes/AddFuelCode/AddEditFuelCode.jsx @@ -103,12 +103,14 @@ const AddEditFuelCodeBase = () => { if (existingFuelCode) { const transformedData = { ...existingFuelCode, - feedstockFuelTransportMode: existingFuelCode.feedstockFuelTransportModes.map( - (mode) => mode.feedstockFuelTransportMode.transportMode - ), - finishedFuelTransportMode: existingFuelCode.finishedFuelTransportModes.map( - (mode) => mode.finishedFuelTransportMode.transportMode - ) + feedstockFuelTransportMode: + existingFuelCode.feedstockFuelTransportModes.map( + (mode) => mode.feedstockFuelTransportMode.transportMode + ), + finishedFuelTransportMode: + existingFuelCode.finishedFuelTransportModes.map( + (mode) => mode.finishedFuelTransportMode.transportMode + ) } setRowData([transformedData]) } else { @@ -452,6 +454,12 @@ const AddEditFuelCodeBase = () => { {existingFuelCode?.fuelCodeStatus.status === FUEL_CODE_STATUSES.APPROVED && t('fuelCode:viewFuelCodeTitle')} + + {(!existingFuelCode || + existingFuelCode?.fuelCodeStatus.status === + FUEL_CODE_STATUSES.DRAFT) && + t('fuelCode:fuelCodeEntryGuide')} + Date: Thu, 12 Dec 2024 16:12:23 -0700 Subject: [PATCH 12/18] Supporting documents for Compliance Report with status submitted or assessed. For only Analyst view. --- backend/lcfs/web/api/document/views.py | 2 +- .../EditViewComplianceReport.jsx | 2 +- .../components/ReportDetails.jsx | 36 ++++++++++++++++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/backend/lcfs/web/api/document/views.py b/backend/lcfs/web/api/document/views.py index 456419da6..163aaacc1 100644 --- a/backend/lcfs/web/api/document/views.py +++ b/backend/lcfs/web/api/document/views.py @@ -40,7 +40,7 @@ async def get_all_documents( response_model=FileResponseSchema, status_code=status.HTTP_201_CREATED, ) -@view_handler([RoleEnum.SUPPLIER]) +@view_handler([RoleEnum.SUPPLIER, RoleEnum.ANALYST]) async def upload_file( request: Request, parent_id: int, diff --git a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx index dc6b075b1..cb26fb85c 100644 --- a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx +++ b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx @@ -209,7 +209,7 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => { {!location.state?.newReport && ( <> - + { +const ReportDetails = ({ currentStatus = 'Draft', isGovernmentUser}) => { const { t } = useTranslation() const { compliancePeriod, complianceReportId } = useParams() const navigate = useNavigate() @@ -214,6 +215,39 @@ const ReportDetails = ({ currentStatus = 'Draft' }) => { {t('report:collapseAll')} + {/* Supporting Documents */} + {isGovernmentUser && (currentStatus === COMPLIANCE_REPORT_STATUSES.SUBMITTED || + currentStatus === COMPLIANCE_REPORT_STATUSES.ASSESSED) && ( + + } + aria-controls="panel1-content" + > + + {t('report:supportingDocs')} + + { + e.stopPropagation(); + setFileDialogOpen(true); + }} + > + + + + + setFileDialogOpen(false)} + /> + + + )} {activityList.map((activity, index) => { const { data, error, isLoading } = activity.useFetch(complianceReportId) return ( From adbb4fdd3da93af566e75f7444b903e9fc074784 Mon Sep 17 00:00:00 2001 From: Arturo Reyes Lopez Date: Mon, 16 Dec 2024 12:59:34 -0700 Subject: [PATCH 13/18] Adding Supporting Documents to compliance reports submitted/assessed status. --- .../EditViewComplianceReport.jsx | 6 +- .../components/ReportDetails.jsx | 86 ++++++++----------- 2 files changed, 39 insertions(+), 53 deletions(-) diff --git a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx index cb26fb85c..3b85bf216 100644 --- a/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx +++ b/frontend/src/views/ComplianceReports/EditViewComplianceReport.jsx @@ -7,6 +7,7 @@ import BCModal from '@/components/BCModal' import BCButton from '@/components/BCButton' import Loading from '@/components/Loading' import { Role } from '@/components/Role' +import { roles } from '@/constants/roles' import { Fab, Stack, Tooltip } from '@mui/material' import BCTypography from '@/components/BCTypography' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -80,7 +81,8 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => { hasRoles } = useCurrentUser() const isGovernmentUser = currentUser?.isGovernmentUser - + const isAnalystRole = currentUser?.roles?.some(role => role.name === roles.analyst) || false; + const currentStatus = reportData?.report.currentStatus?.status const { data: orgData, isLoading } = useOrganization( reportData?.report.organizationId @@ -209,7 +211,7 @@ export const EditViewComplianceReport = ({ reportData, isError, error }) => { {!location.state?.newReport && ( <> - + { +const ReportDetails = ({ currentStatus = 'Draft', isAnalystRole }) => { const { t } = useTranslation() const { compliancePeriod, complianceReportId } = useParams() const navigate = useNavigate() const [isFileDialogOpen, setFileDialogOpen] = useState(false) + const showSupportingDocs = useMemo(() => { + return isAnalystRole && ( + currentStatus === COMPLIANCE_REPORT_STATUSES.SUBMITTED || + currentStatus === COMPLIANCE_REPORT_STATUSES.ASSESSED + ) || currentStatus === COMPLIANCE_REPORT_STATUSES.DRAFT; + }, [isAnalystRole, currentStatus]); const isArrayEmpty = useCallback((data) => { if (Array.isArray(data)) { @@ -65,11 +71,22 @@ const ReportDetails = ({ currentStatus = 'Draft', isGovernmentUser}) => { }, useFetch: useComplianceReportDocuments, component: (data) => ( - - ) + <> + + { + setFileDialogOpen(false) + }} + /> + + ), + condition: showSupportingDocs }, { name: t('report:activityLists.supplyOfFuel'), @@ -172,9 +189,13 @@ const ReportDetails = ({ currentStatus = 'Draft', isGovernmentUser}) => { ] ) - const [expanded, setExpanded] = useState(() => - activityList.map((_, index) => `panel${index}`) - ) + const [expanded, setExpanded] = useState(activityList.map((activity, index) => { + if (activity.name === t('report:supportingDocs')) { + return isArrayEmpty(activity.useFetch(complianceReportId).data) ? '' : `panel${index}` + } + return `panel${index}` + }).filter(Boolean)) // Initialize with panels that should be open by default + const [allExpanded, setAllExpanded] = useState(true) const handleChange = (panel) => (event, isExpanded) => { @@ -215,47 +236,13 @@ const ReportDetails = ({ currentStatus = 'Draft', isGovernmentUser}) => { {t('report:collapseAll')} - {/* Supporting Documents */} - {isGovernmentUser && (currentStatus === COMPLIANCE_REPORT_STATUSES.SUBMITTED || - currentStatus === COMPLIANCE_REPORT_STATUSES.ASSESSED) && ( - - } - aria-controls="panel1-content" - > - - {t('report:supportingDocs')} - - { - e.stopPropagation(); - setFileDialogOpen(true); - }} - > - - - - - setFileDialogOpen(false)} - /> - - - )} {activityList.map((activity, index) => { const { data, error, isLoading } = activity.useFetch(complianceReportId) return ( - data && - !isArrayEmpty(data) && ( + ((activity.name === t('report:supportingDocs') ? showSupportingDocs : (data && !isArrayEmpty(data)))) && ( { component="div" > {activity.name}   - {currentStatus === 'Draft' && ( - <> - { /> - - )} From acd349fe0fdb54228eea9c6fd4e668f103e835a5 Mon Sep 17 00:00:00 2001 From: Arturo Reyes Lopez Date: Mon, 16 Dec 2024 13:28:42 -0700 Subject: [PATCH 14/18] Always show supportind documents and edit based on conditions --- .../ComplianceReports/components/ReportDetails.jsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/views/ComplianceReports/components/ReportDetails.jsx b/frontend/src/views/ComplianceReports/components/ReportDetails.jsx index aa29b8eaf..f14545f29 100644 --- a/frontend/src/views/ComplianceReports/components/ReportDetails.jsx +++ b/frontend/src/views/ComplianceReports/components/ReportDetails.jsx @@ -40,7 +40,7 @@ const ReportDetails = ({ currentStatus = 'Draft', isAnalystRole }) => { const navigate = useNavigate() const [isFileDialogOpen, setFileDialogOpen] = useState(false) - const showSupportingDocs = useMemo(() => { + const editSupportingDocs = useMemo(() => { return isAnalystRole && ( currentStatus === COMPLIANCE_REPORT_STATUSES.SUBMITTED || currentStatus === COMPLIANCE_REPORT_STATUSES.ASSESSED @@ -86,7 +86,7 @@ const ReportDetails = ({ currentStatus = 'Draft', isAnalystRole }) => { /> ), - condition: showSupportingDocs + condition: true }, { name: t('report:activityLists.supplyOfFuel'), @@ -239,7 +239,7 @@ const ReportDetails = ({ currentStatus = 'Draft', isAnalystRole }) => { {activityList.map((activity, index) => { const { data, error, isLoading } = activity.useFetch(complianceReportId) return ( - ((activity.name === t('report:supportingDocs') ? showSupportingDocs : (data && !isArrayEmpty(data)))) && ( + (data && !isArrayEmpty(data) || activity.name === t('report:supportingDocs')) && ( { component="div" > {activity.name}   - + { /> + + )} From 95defd25e4f90b66408f2f6ba34137a40c2adc30 Mon Sep 17 00:00:00 2001 From: Arturo Reyes Lopez Date: Tue, 17 Dec 2024 15:34:00 -0700 Subject: [PATCH 15/18] Add validation when uploading documents --- backend/lcfs/web/api/document/views.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/backend/lcfs/web/api/document/views.py b/backend/lcfs/web/api/document/views.py index 163aaacc1..e0ef5deca 100644 --- a/backend/lcfs/web/api/document/views.py +++ b/backend/lcfs/web/api/document/views.py @@ -1,3 +1,6 @@ +from http.client import HTTPException +from lcfs.db.models.compliance.ComplianceReportStatus import ComplianceReportStatusEnum +from lcfs.web.api.compliance_report.services import ComplianceReportServices import structlog from typing import List @@ -47,7 +50,20 @@ async def upload_file( parent_type: str, file: UploadFile = File(...), document_service: DocumentService = Depends(), + compliance_report_service: ComplianceReportServices = Depends(), ) -> FileResponseSchema: + # Fetch the compliance report + compliance_report = await compliance_report_service.get_compliance_report_by_id(parent_id) + if not compliance_report: + raise HTTPException(status_code=404, detail="Compliance report not found") + + # Check if the compliance report is submitted + if compliance_report.current_status.status == ComplianceReportStatusEnum.Submitted: + raise HTTPException( + status_code=400, + detail="Cannot upload files when the compliance report is submitted", + ) + document = await document_service.upload_file(file, parent_id, parent_type) return FileResponseSchema.model_validate(document) From faae96e2628fe0a170700abab8e4b0da8354cc21 Mon Sep 17 00:00:00 2001 From: Arturo Reyes Lopez Date: Wed, 18 Dec 2024 13:27:50 -0700 Subject: [PATCH 16/18] move validation to service --- backend/lcfs/services/s3/client.py | 27 +++++++++++++++++++++++++- backend/lcfs/web/api/document/views.py | 17 +++------------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/backend/lcfs/services/s3/client.py b/backend/lcfs/services/s3/client.py index 260f8b0ea..5d25ff689 100644 --- a/backend/lcfs/services/s3/client.py +++ b/backend/lcfs/services/s3/client.py @@ -1,5 +1,9 @@ +from fastapi import HTTPException import os import uuid +from lcfs.db.models.compliance.ComplianceReportStatus import ComplianceReportStatusEnum +from lcfs.db.models.user.Role import RoleEnum +from lcfs.web.api.compliance_report.services import ComplianceReportServices from fastapi import Depends from pydantic.v1 import ValidationError from sqlalchemy import select @@ -26,13 +30,34 @@ def __init__( db: AsyncSession = Depends(get_async_db_session), clamav_service: ClamAVService = Depends(), s3_client=Depends(get_s3_client), + compliance_report_service: ComplianceReportServices = Depends(), ): self.db = db self.clamav_service = clamav_service self.s3_client = s3_client + self.compliance_report_service = compliance_report_service @repo_handler - async def upload_file(self, file, parent_id: str, parent_type="compliance_report"): + async def upload_file( + self, file, parent_id: str, parent_type="compliance_report", user=None + ): + compliance_report = ( + await self.compliance_report_service.get_compliance_report_by_id(parent_id) + ) + if not compliance_report: + raise HTTPException(status_code=404, detail="Compliance report not found") + + # Check if the user is a supplier and the compliance report status is different from Draft + if ( + RoleEnum.SUPPLIER in user.role_names + and compliance_report.current_status.status + != ComplianceReportStatusEnum.Draft.value + ): + raise HTTPException( + status_code=400, + detail="Suppliers can only upload files when the compliance report status is Draft", + ) + file_id = uuid.uuid4() file_key = f"{settings.s3_docs_path}/{parent_type}/{parent_id}/{file_id}" diff --git a/backend/lcfs/web/api/document/views.py b/backend/lcfs/web/api/document/views.py index e0ef5deca..9f1cc026a 100644 --- a/backend/lcfs/web/api/document/views.py +++ b/backend/lcfs/web/api/document/views.py @@ -50,21 +50,10 @@ async def upload_file( parent_type: str, file: UploadFile = File(...), document_service: DocumentService = Depends(), - compliance_report_service: ComplianceReportServices = Depends(), ) -> FileResponseSchema: - # Fetch the compliance report - compliance_report = await compliance_report_service.get_compliance_report_by_id(parent_id) - if not compliance_report: - raise HTTPException(status_code=404, detail="Compliance report not found") - - # Check if the compliance report is submitted - if compliance_report.current_status.status == ComplianceReportStatusEnum.Submitted: - raise HTTPException( - status_code=400, - detail="Cannot upload files when the compliance report is submitted", - ) - - document = await document_service.upload_file(file, parent_id, parent_type) + document = await document_service.upload_file( + file, parent_id, parent_type, request.user + ) return FileResponseSchema.model_validate(document) From aa830be2e9aadc47fcbd28ef5c5687fe3e533f1e Mon Sep 17 00:00:00 2001 From: Kevin Hashimoto Date: Fri, 20 Dec 2024 10:44:42 -0800 Subject: [PATCH 17/18] fix: adjust line 1 and 2 calculations --- .../versions/2024-12-20-05-17_59873cafbcd8.py | 40 +++++++++++++++++++ .../db/seeders/common/seed_fuel_data.json | 6 +-- .../compliance_report/test_summary_service.py | 22 ++++++---- .../tests/fuel_code/test_fuel_code_repo.py | 35 ++++++++++------ .../test_notional_transfer_services.py | 19 +++++---- .../other_uses/test_other_uses_services.py | 31 +++++++++----- backend/lcfs/utils/constants.py | 12 ++++++ .../api/compliance_report/summary_service.py | 23 +++++++++-- .../web/api/notional_transfer/services.py | 17 ++++---- backend/lcfs/web/api/other_uses/services.py | 14 ++++--- 10 files changed, 163 insertions(+), 56 deletions(-) create mode 100644 backend/lcfs/db/migrations/versions/2024-12-20-05-17_59873cafbcd8.py diff --git a/backend/lcfs/db/migrations/versions/2024-12-20-05-17_59873cafbcd8.py b/backend/lcfs/db/migrations/versions/2024-12-20-05-17_59873cafbcd8.py new file mode 100644 index 000000000..79914e3b0 --- /dev/null +++ b/backend/lcfs/db/migrations/versions/2024-12-20-05-17_59873cafbcd8.py @@ -0,0 +1,40 @@ +"""update other diesel + +Revision ID: 59873cafbcd8 +Revises: 851e09cf8661 +Create Date: 2024-12-20 05:17:40.638826 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "59873cafbcd8" +down_revision = "851e09cf8661" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.execute( + """ + UPDATE fuel_type + SET fossil_derived = false, other_uses_fossil_derived = false + WHERE fuel_type = 'Other diesel' + """ + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.execute( + """ + UPDATE fuel_type + SET fossil_derived = true, other_uses_fossil_derived = true + WHERE fuel_type = 'Other diesel' + """ + ) + # ### end Alembic commands ### diff --git a/backend/lcfs/db/seeders/common/seed_fuel_data.json b/backend/lcfs/db/seeders/common/seed_fuel_data.json index c80c5f972..3584662f2 100644 --- a/backend/lcfs/db/seeders/common/seed_fuel_data.json +++ b/backend/lcfs/db/seeders/common/seed_fuel_data.json @@ -185,8 +185,8 @@ { "fuel_type_id": 20, "fuel_type": "Other diesel", - "fossil_derived": true, - "other_uses_fossil_derived": true, + "fossil_derived": false, + "other_uses_fossil_derived": false, "provision_1_id": 1, "default_carbon_intensity": 100.21, "units": "L", @@ -1095,4 +1095,4 @@ "display_order": 4 } ] -} +} \ No newline at end of file diff --git a/backend/lcfs/tests/compliance_report/test_summary_service.py b/backend/lcfs/tests/compliance_report/test_summary_service.py index 30cf918ef..dce14fef2 100644 --- a/backend/lcfs/tests/compliance_report/test_summary_service.py +++ b/backend/lcfs/tests/compliance_report/test_summary_service.py @@ -59,7 +59,8 @@ async def test_calculate_low_carbon_fuel_target_summary( # Assertions assert isinstance(summary, list) - assert all(isinstance(item, ComplianceReportSummaryRowSchema) for item in summary) + assert all(isinstance(item, ComplianceReportSummaryRowSchema) + for item in summary) assert len(summary) == 11 # Ensure all 11 lines are present # Check specific line values @@ -510,7 +511,8 @@ async def test_calculate_renewable_fuel_target_summary_no_renewables( assert len(result) == 11 assert isinstance(result[0], ComplianceReportSummaryRowSchema) - assert result[10].gasoline == 15.0 # Penalty should be applied due to no renewables + # Penalty should be applied due to no renewables + assert result[10].gasoline == 15.0 assert result[10].diesel == 36.0 assert result[10].jet_fuel == 45.0 assert result[10].total_value == 96.0 @@ -548,14 +550,12 @@ async def test_calculate_renewable_fuel_target_summary_high_renewables( assert len(result) == 11 assert isinstance(result[0], ComplianceReportSummaryRowSchema) - assert result[10].gasoline == 0 # No penalty since renewables exceed requirements + # No penalty since renewables exceed requirements + assert result[10].gasoline == 0 assert result[10].diesel == 0 assert result[10].jet_fuel == 0 -import pytest - - @pytest.mark.anyio async def test_calculate_renewable_fuel_target_summary_copy_lines_6_and_8( compliance_report_summary_service, @@ -710,6 +710,13 @@ async def test_can_sign_flag_logic( "jet_fuel": 25, } ) + mock_repo.aggregate_other_uses = AsyncMock( + return_value={ + "gasoline": 50, + "diesel": 25, + "jet_fuel": 10, + } + ) mock_repo.get_assessed_compliance_report_by_period = AsyncMock( return_value=MagicMock( summary=MagicMock( @@ -810,7 +817,8 @@ async def test_calculate_fuel_quantities_renewable( ): # Create a mock repository mock_repo.aggregate_fuel_supplies.return_value = {"gasoline": 200.0} - mock_repo.aggregate_other_uses.return_value = {"diesel": 75.0, "jet-fuel": 25.0} + mock_repo.aggregate_other_uses.return_value = { + "diesel": 75.0, "jet-fuel": 25.0} # Define test inputs compliance_report_id = 2 diff --git a/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py b/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py index 99bf2341d..2fdfc689b 100644 --- a/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py +++ b/backend/lcfs/tests/fuel_code/test_fuel_code_repo.py @@ -158,7 +158,7 @@ async def test_get_fuel_categories(fuel_code_repo, mock_db): @pytest.mark.anyio -async def test_get_fuel_category_by_name(fuel_code_repo, mock_db): +async def test_get_fuel_category_by(fuel_code_repo, mock_db): mock_fc = FuelCategory( fuel_category_id=2, category="Fossil", default_carbon_intensity=0 ) @@ -327,7 +327,8 @@ async def test_get_fuel_codes_paginated(fuel_code_repo, mock_db): unique=MagicMock( return_value=MagicMock( scalars=MagicMock( - return_value=MagicMock(all=MagicMock(return_value=[fc])) + return_value=MagicMock( + all=MagicMock(return_value=[fc])) ) ) ) @@ -342,7 +343,8 @@ async def test_get_fuel_codes_paginated(fuel_code_repo, mock_db): @pytest.mark.anyio async def test_get_fuel_code_statuses(fuel_code_repo, mock_db): - fcs = FuelCodeStatus(fuel_code_status_id=1, status=FuelCodeStatusEnum.Approved) + fcs = FuelCodeStatus(fuel_code_status_id=1, + status=FuelCodeStatusEnum.Approved) mock_result = MagicMock() mock_result.scalars.return_value.all.return_value = [fcs] mock_db.execute.return_value = mock_result @@ -371,7 +373,8 @@ async def test_get_fuel_code(fuel_code_repo, mock_db, valid_fuel_code): @pytest.mark.anyio async def test_get_fuel_code_status_enum(fuel_code_repo, mock_db): - fcs = FuelCodeStatus(fuel_code_status_id=2, status=FuelCodeStatusEnum.Deleted) + fcs = FuelCodeStatus(fuel_code_status_id=2, + status=FuelCodeStatusEnum.Deleted) mock_db.scalar.return_value = fcs result = await fuel_code_repo.get_fuel_code_status(FuelCodeStatusEnum.Deleted) assert result == fcs @@ -403,7 +406,8 @@ async def test_delete_fuel_code(fuel_code_repo, mock_db): @pytest.mark.anyio async def test_get_distinct_company_names(fuel_code_repo, mock_db): mock_result = MagicMock() - mock_result.scalars.return_value.all.return_value = ["CompanyA", "CompanyB"] + mock_result.scalars.return_value.all.return_value = [ + "CompanyA", "CompanyB"] mock_db.execute.return_value = mock_result result = await fuel_code_repo.get_distinct_company_names("Com") @@ -413,7 +417,8 @@ async def test_get_distinct_company_names(fuel_code_repo, mock_db): @pytest.mark.anyio async def test_get_contact_names_by_company(fuel_code_repo, mock_db): mock_result = MagicMock() - mock_result.scalars.return_value.all.return_value = ["John Doe", "Jane Doe"] + mock_result.scalars.return_value.all.return_value = [ + "John Doe", "Jane Doe"] mock_db.execute.return_value = mock_result result = await fuel_code_repo.get_contact_names_by_company("CompanyA", "J") @@ -446,7 +451,8 @@ async def test_get_distinct_fuel_codes_by_code(fuel_code_repo, mock_db): async def test_get_fuel_code_by_code_prefix(fuel_code_repo, mock_db): fc = FuelCode(fuel_code_id=10, fuel_suffix="200.0") mock_result = MagicMock() - mock_result.unique.return_value.scalars.return_value.all.return_value = [fc] + mock_result.unique.return_value.scalars.return_value.all.return_value = [ + fc] mock_db.execute.return_value = mock_result # Mock the next available suffix @@ -612,7 +618,8 @@ async def test_get_standardized_fuel_data(fuel_code_repo, mock_db): MagicMock( scalars=MagicMock( return_value=MagicMock( - first=MagicMock(return_value=EnergyEffectivenessRatio(ratio=2.0)) + first=MagicMock( + return_value=EnergyEffectivenessRatio(ratio=2.0)) ) ) ), @@ -669,7 +676,8 @@ async def test_get_standardized_fuel_data_unrecognized(fuel_code_repo, mock_db): mock_db.get_one.return_value = mock_fuel_type # Mock the repo method to get the fuel category - fuel_code_repo.get_fuel_category_by = AsyncMock(return_value=mock_fuel_category) + fuel_code_repo.get_fuel_category_by = AsyncMock( + return_value=mock_fuel_category) # Setup side effects for subsequent queries: # Energy Density @@ -684,7 +692,8 @@ async def test_get_standardized_fuel_data_unrecognized(fuel_code_repo, mock_db): eer_result = MagicMock( scalars=MagicMock( return_value=MagicMock( - first=MagicMock(return_value=EnergyEffectivenessRatio(ratio=2.0)) + first=MagicMock( + return_value=EnergyEffectivenessRatio(ratio=2.0)) ) ) ) @@ -693,7 +702,8 @@ async def test_get_standardized_fuel_data_unrecognized(fuel_code_repo, mock_db): scalars=MagicMock( return_value=MagicMock( all=MagicMock( - return_value=[TargetCarbonIntensity(target_carbon_intensity=50.0)] + return_value=[TargetCarbonIntensity( + target_carbon_intensity=50.0)] ) ) ) @@ -729,7 +739,8 @@ async def test_get_standardized_fuel_data_unrecognized(fuel_code_repo, mock_db): assert result.uci == 5.0 # Ensure get_fuel_category_by was called once with the correct parameter - fuel_code_repo.get_fuel_category_by.assert_awaited_once_with(fuel_category_id=2) + fuel_code_repo.get_fuel_category_by.assert_awaited_once_with( + fuel_category_id=2) @pytest.mark.anyio diff --git a/backend/lcfs/tests/notional_transfer/test_notional_transfer_services.py b/backend/lcfs/tests/notional_transfer/test_notional_transfer_services.py index 073530a3b..cf8345153 100644 --- a/backend/lcfs/tests/notional_transfer/test_notional_transfer_services.py +++ b/backend/lcfs/tests/notional_transfer/test_notional_transfer_services.py @@ -21,7 +21,8 @@ def notional_transfer_service(): mock_repo = MagicMock(spec=NotionalTransferRepository) mock_fuel_repo = MagicMock() - service = NotionalTransferServices(repo=mock_repo, fuel_repo=mock_fuel_repo) + service = NotionalTransferServices( + repo=mock_repo, fuel_repo=mock_fuel_repo) return service, mock_repo, mock_fuel_repo @@ -47,12 +48,13 @@ async def test_get_table_options(notional_transfer_service): async def test_create_notional_transfer(notional_transfer_service): service, mock_repo, mock_fuel_repo = notional_transfer_service notional_transfer_data = create_mock_schema({}) - mock_fuel_repo.get_fuel_category_by_name = AsyncMock( + mock_fuel_repo.get_fuel_category_by = AsyncMock( return_value=MagicMock(fuel_category_id=1) ) mock_created_transfer = create_mock_entity({}) - mock_repo.create_notional_transfer = AsyncMock(return_value=mock_created_transfer) + mock_repo.create_notional_transfer = AsyncMock( + return_value=mock_created_transfer) response = await service.create_notional_transfer( notional_transfer_data, UserTypeEnum.SUPPLIER @@ -93,7 +95,7 @@ async def test_update_notional_transfer(notional_transfer_service): mock_repo.get_latest_notional_transfer_by_group_uuid = AsyncMock( return_value=mock_existing_transfer ) - mock_fuel_repo.get_fuel_category_by_name = AsyncMock( + mock_fuel_repo.get_fuel_category_by = AsyncMock( return_value=MagicMock(category="Gasoline") ) @@ -112,7 +114,8 @@ async def test_update_notional_transfer(notional_transfer_service): action_type=ActionTypeEnum.UPDATE, ) # Set the return value for update_notional_transfer - mock_repo.update_notional_transfer = AsyncMock(return_value=mock_updated_transfer) + mock_repo.update_notional_transfer = AsyncMock( + return_value=mock_updated_transfer) # Execute the update function and capture the response response = await service.update_notional_transfer( @@ -131,7 +134,8 @@ async def test_update_notional_transfer_not_found(notional_transfer_service): service, mock_repo, _ = notional_transfer_service notional_transfer_data = create_mock_schema({}) - mock_repo.get_notional_transfer_version_by_user = AsyncMock(return_value=None) + mock_repo.get_notional_transfer_version_by_user = AsyncMock( + return_value=None) with pytest.raises(ServiceException): await service.update_notional_transfer( @@ -171,7 +175,8 @@ async def test_delete_notional_transfer(notional_transfer_service): mock_repo.get_latest_notional_transfer_by_group_uuid = AsyncMock( return_value=mock_existing_transfer ) - mock_repo.create_notional_transfer = AsyncMock(return_value=mock_existing_transfer) + mock_repo.create_notional_transfer = AsyncMock( + return_value=mock_existing_transfer) # Call the delete service response = await service.delete_notional_transfer( diff --git a/backend/lcfs/tests/other_uses/test_other_uses_services.py b/backend/lcfs/tests/other_uses/test_other_uses_services.py index 14b3bb518..86794426a 100644 --- a/backend/lcfs/tests/other_uses/test_other_uses_services.py +++ b/backend/lcfs/tests/other_uses/test_other_uses_services.py @@ -66,11 +66,16 @@ async def test_create_other_use(other_uses_service): mock_fuel_code.fuel_code = "FuelCode123" # Mock fuel repository methods - mock_fuel_repo.get_fuel_category_by_name = AsyncMock(return_value=mock_fuel_category) - mock_fuel_repo.get_fuel_type_by_name = AsyncMock(return_value=mock_fuel_type) - mock_fuel_repo.get_expected_use_type_by_name = AsyncMock(return_value=mock_expected_use) - mock_fuel_repo.get_provision_of_the_act_by_name = AsyncMock(return_value=mock_provision_of_the_act) - mock_fuel_repo.get_fuel_code_by_name = AsyncMock(return_value=mock_fuel_code) + mock_fuel_repo.get_fuel_category_by = AsyncMock( + return_value=mock_fuel_category) + mock_fuel_repo.get_fuel_type_by_name = AsyncMock( + return_value=mock_fuel_type) + mock_fuel_repo.get_expected_use_type_by_name = AsyncMock( + return_value=mock_expected_use) + mock_fuel_repo.get_provision_of_the_act_by_name = AsyncMock( + return_value=mock_provision_of_the_act) + mock_fuel_repo.get_fuel_code_by_name = AsyncMock( + return_value=mock_fuel_code) # Create a mock for the created other use mock_created_use = create_mock_entity({}) @@ -115,7 +120,8 @@ async def test_update_other_use(other_uses_service): mock_existing_use = create_mock_entity({}) # Configure repository methods to return these mocked objects - mock_repo.get_other_use_version_by_user = AsyncMock(return_value=mock_existing_use) + mock_repo.get_other_use_version_by_user = AsyncMock( + return_value=mock_existing_use) # Mock related entities with proper string attributes mock_fuel_type = MagicMock() @@ -134,13 +140,17 @@ async def test_update_other_use(other_uses_service): mock_fuel_code.fuel_code = "NewFuelCode" # Mock fuel repository methods - mock_fuel_repo.get_fuel_type_by_name = AsyncMock(return_value=mock_fuel_type) - mock_fuel_repo.get_fuel_category_by_name = AsyncMock(return_value=mock_fuel_category) - mock_fuel_repo.get_expected_use_type_by_name = AsyncMock(return_value=mock_expected_use) + mock_fuel_repo.get_fuel_type_by_name = AsyncMock( + return_value=mock_fuel_type) + mock_fuel_repo.get_fuel_category_by = AsyncMock( + return_value=mock_fuel_category) + mock_fuel_repo.get_expected_use_type_by_name = AsyncMock( + return_value=mock_expected_use) mock_fuel_repo.get_provision_of_the_act_by_name = AsyncMock( return_value=mock_provision_of_the_act ) - mock_fuel_repo.get_fuel_code_by_name = AsyncMock(return_value=mock_fuel_code) + mock_fuel_repo.get_fuel_code_by_name = AsyncMock( + return_value=mock_fuel_code) # Mock the updated use returned after the update mock_updated_use = MagicMock() @@ -181,7 +191,6 @@ async def test_update_other_use(other_uses_service): mock_repo.update_other_use.assert_awaited_once() - @pytest.mark.anyio async def test_update_other_use_not_found(other_uses_service): service, mock_repo, _ = other_uses_service diff --git a/backend/lcfs/utils/constants.py b/backend/lcfs/utils/constants.py index 23d6752df..793fe2a4e 100644 --- a/backend/lcfs/utils/constants.py +++ b/backend/lcfs/utils/constants.py @@ -83,3 +83,15 @@ class FILE_MEDIA_TYPE(Enum): } default_ci = {"Gasoline": 93.67, "Diesel": 100.21, "Jet fuel": 88.83} + + +RENEWABLE_FUEL_TYPES = [ + "Renewable gasoline", + "Ethanol", + "Renewable naphtha", + "Biodiesel", + "HDRD", + "Other diesel", + "Alternative jet fuel", + "Other" +] diff --git a/backend/lcfs/web/api/compliance_report/summary_service.py b/backend/lcfs/web/api/compliance_report/summary_service.py index f241da143..c4a737c72 100644 --- a/backend/lcfs/web/api/compliance_report/summary_service.py +++ b/backend/lcfs/web/api/compliance_report/summary_service.py @@ -32,6 +32,8 @@ from lcfs.web.exception.exceptions import DataNotFoundException from lcfs.web.utils.calculations import calculate_compliance_units +from lcfs.utils.constants import RENEWABLE_FUEL_TYPES + logger = logging.getLogger(__name__) @@ -241,6 +243,12 @@ async def update_compliance_report_summary( return summary_data + def filter_renewable_fuel_supplies(self, fuel_supplies: List[FuelSupply]) -> List[FuelSupply]: + return [ + supply for supply in fuel_supplies + if supply.fuel_type and supply.fuel_type.fuel_type in RENEWABLE_FUEL_TYPES + ] + @service_handler async def calculate_compliance_report_summary( self, report_id: int @@ -326,14 +334,19 @@ async def calculate_compliance_report_summary( ) # Fetch fuel quantities + # line 1 fossil_quantities = await self.calculate_fuel_quantities( compliance_report.compliance_report_id, effective_fuel_supplies, fossil_derived=True ) + # line 2 + filtered_renewable_fuel_supplies = self.filter_renewable_fuel_supplies( + effective_fuel_supplies) + renewable_quantities = await self.calculate_fuel_quantities( compliance_report.compliance_report_id, - effective_fuel_supplies, + filtered_renewable_fuel_supplies, fossil_derived=False ) @@ -730,10 +743,14 @@ async def calculate_fuel_quantities( fuel_quantities = self.repo.aggregate_fuel_supplies( effective_fuel_supplies, fossil_derived ) - fuel_quantities.update( - await self.repo.aggregate_other_uses(compliance_report_id, fossil_derived) + + other_uses = await self.repo.aggregate_other_uses( + compliance_report_id, fossil_derived ) + for key, value in other_uses.items(): + fuel_quantities[key] = fuel_quantities.get(key, 0) + value + return dict(fuel_quantities) @service_handler diff --git a/backend/lcfs/web/api/notional_transfer/services.py b/backend/lcfs/web/api/notional_transfer/services.py index 4fdca07f4..207707bf6 100644 --- a/backend/lcfs/web/api/notional_transfer/services.py +++ b/backend/lcfs/web/api/notional_transfer/services.py @@ -50,12 +50,13 @@ async def convert_to_model( """ Converts data from NotionalTransferCreateSchema to NotionalTransfer data model to store into the database. """ - fuel_category = await self.fuel_repo.get_fuel_category_by_name( - notional_transfer_data.fuel_category + fuel_category = await self.fuel_repo.get_fuel_category_by( + category=notional_transfer_data.fuel_category ) return NotionalTransfer( **notional_transfer_data.model_dump( - exclude=NOTIONAL_TRANSFER_EXCLUDE_FIELDS.union({"fuel_category"}) + exclude=NOTIONAL_TRANSFER_EXCLUDE_FIELDS.union( + {"fuel_category"}) ), fuel_category_id=fuel_category.fuel_category_id, ) @@ -155,7 +156,8 @@ async def update_notional_transfer( ): # Update existing record if compliance report ID matches for field, value in notional_transfer_data.model_dump( - exclude=NOTIONAL_TRANSFER_EXCLUDE_FIELDS.union({"fuel_category"}) + exclude=NOTIONAL_TRANSFER_EXCLUDE_FIELDS.union( + {"fuel_category"}) ).items(): setattr(existing_transfer, field, value) @@ -164,8 +166,8 @@ async def update_notional_transfer( != notional_transfer_data.fuel_category ): existing_transfer.fuel_category = ( - await self.fuel_repo.get_fuel_category_by_name( - notional_transfer_data.fuel_category + await self.fuel_repo.get_fuel_category_by( + category=notional_transfer_data.fuel_category ) ) @@ -228,7 +230,8 @@ async def delete_notional_transfer( # Copy fields from the latest version for the deletion record for field in existing_transfer.__table__.columns.keys(): if field not in NOTIONAL_TRANSFER_EXCLUDE_FIELDS: - setattr(deleted_entity, field, getattr(existing_transfer, field)) + setattr(deleted_entity, field, getattr( + existing_transfer, field)) await self.repo.create_notional_transfer(deleted_entity) return DeleteNotionalTransferResponseSchema(message="Marked as deleted.") diff --git a/backend/lcfs/web/api/other_uses/services.py b/backend/lcfs/web/api/other_uses/services.py index 6fcc42f85..db5b22f81 100644 --- a/backend/lcfs/web/api/other_uses/services.py +++ b/backend/lcfs/web/api/other_uses/services.py @@ -52,8 +52,8 @@ async def schema_to_model(self, other_use: OtherUsesCreateSchema) -> OtherUses: """ Converts data from OtherUsesCreateSchema to OtherUses data model to store into the database. """ - fuel_category = await self.fuel_repo.get_fuel_category_by_name( - other_use.fuel_category + fuel_category = await self.fuel_repo.get_fuel_category_by( + category=other_use.fuel_category ) fuel_type = await self.fuel_repo.get_fuel_type_by_name(other_use.fuel_type) expected_use = await self.fuel_repo.get_expected_use_type_by_name( @@ -147,7 +147,8 @@ async def get_other_uses(self, compliance_report_id: int) -> OtherUsesListSchema """ other_uses = await self.repo.get_other_uses(compliance_report_id) return OtherUsesAllSchema( - other_uses=[OtherUsesSchema.model_validate(ou) for ou in other_uses] + other_uses=[OtherUsesSchema.model_validate( + ou) for ou in other_uses] ) @service_handler @@ -201,8 +202,8 @@ async def update_other_use( if other_use.fuel_category.category != other_use_data.fuel_category: other_use.fuel_category = ( - await self.fuel_repo.get_fuel_category_by_name( - other_use_data.fuel_category + await self.fuel_repo.get_fuel_category_by( + category=other_use_data.fuel_category ) ) @@ -290,7 +291,8 @@ async def delete_other_use( # Copy fields from the latest version for the deletion record for field in existing_fuel_supply.__table__.columns.keys(): if field not in OTHER_USE_EXCLUDE_FIELDS: - setattr(deleted_entity, field, getattr(existing_fuel_supply, field)) + setattr(deleted_entity, field, getattr( + existing_fuel_supply, field)) await self.repo.create_other_use(deleted_entity) return DeleteOtherUsesResponseSchema(success=True, message="Marked as deleted.") From eace15cc2d54d00fcdb475e604bbcf90b10b6506 Mon Sep 17 00:00:00 2001 From: Arturo Reyes Lopez Date: Fri, 20 Dec 2024 15:01:36 -0700 Subject: [PATCH 18/18] Code review changes --- .../src/views/FuelSupplies/AddEditFuelSupplies.jsx | 6 ++++-- .../src/views/FuelSupplies/FuelSupplySummary.jsx | 2 +- frontend/src/views/FuelSupplies/_schema.jsx | 14 +++----------- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx index 3873ed68d..2c3c90356 100644 --- a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx +++ b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx @@ -7,7 +7,7 @@ import { useGetFuelSupplies, useSaveFuelSupply } from '@/hooks/useFuelSupply' -import { isArrayEmpty } from '@/utils/formatters' +import { isArrayEmpty, cleanEmptyStringValues } from '@/utils/formatters' import BCTypography from '@/components/BCTypography' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -201,7 +201,9 @@ export const AddEditFuelSupplies = () => { severity: 'pending' }) - let updatedData = params.node.data + // clean up any null or empty string values + let updatedData = cleanEmptyStringValues(params.node.data) + if (updatedData.fuelType === 'Other') { updatedData.ciOfFuel = DEFAULT_CI_FUEL[updatedData.fuelCategory] } diff --git a/frontend/src/views/FuelSupplies/FuelSupplySummary.jsx b/frontend/src/views/FuelSupplies/FuelSupplySummary.jsx index 6944b0383..b9808f1ad 100644 --- a/frontend/src/views/FuelSupplies/FuelSupplySummary.jsx +++ b/frontend/src/views/FuelSupplies/FuelSupplySummary.jsx @@ -66,7 +66,7 @@ export const FuelSupplySummary = ({ data, status }) => { valueGetter: (params) => params.data.fuelType?.fuelType }, { - headerName: t('fuelSupply:fuelSupplyColLabels.fuelCategory'), + headerName: t('fuelSupply:fuelSupplyColLabels.fuelCategoryId'), field: 'fuelCategory', valueGetter: (params) => params.data.fuelCategory?.category }, diff --git a/frontend/src/views/FuelSupplies/_schema.jsx b/frontend/src/views/FuelSupplies/_schema.jsx index 969a3e9f9..ba1b52df9 100644 --- a/frontend/src/views/FuelSupplies/_schema.jsx +++ b/frontend/src/views/FuelSupplies/_schema.jsx @@ -98,6 +98,7 @@ export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ params.data.fuelTypeId = fuelType?.fuelTypeId params.data.fuelTypeOther = null params.data.fuelCategory = null + params.data.fuelCategoryId = null params.data.endUseId = null params.data.endUseType = null params.data.eer = null @@ -145,7 +146,7 @@ export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ minWidth: 250 }, { - field: 'fuelCategoryId', + field: 'fuelCategory', headerComponent: RequiredHeader, headerName: i18n.t('fuelSupply:fuelSupplyColLabels.fuelCategoryId'), cellEditor: AutocompleteCellEditor, @@ -182,16 +183,7 @@ export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ }, suppressKeyboardEvent, minWidth: 135, - valueGetter: (params) => { - const options = optionsData?.fuelTypes - ?.find((obj) => params.data.fuelType === obj.fuelType) - ?.fuelCategories.map((item) => item.fuelCategory) - if (options?.length === 1) { - return options[0] - } else { - return params.data.fuelCategory - } - }, + valueGetter: (params) => params.data.fuelCategory, editable: (params) => optionsData?.fuelTypes ?.find((obj) => params.data.fuelType === obj.fuelType)