From 39d0e622d37799b1d8ad036f1cfa541d90a9c6e0 Mon Sep 17 00:00:00 2001 From: Arturo Reyes Lopez Date: Fri, 13 Dec 2024 10:07:48 -0700 Subject: [PATCH 1/2] Autopopulate fuelCategory and fuelCode when selecting fuelType --- backend/lcfs/web/api/other_uses/schema.py | 7 ++++ frontend/src/components/BCDataGrid/columns.js | 1 + .../src/views/OtherUses/AddEditOtherUses.jsx | 41 ++++++++++++++----- frontend/src/views/OtherUses/_schema.jsx | 17 +++++--- 4 files changed, 49 insertions(+), 17 deletions(-) diff --git a/backend/lcfs/web/api/other_uses/schema.py b/backend/lcfs/web/api/other_uses/schema.py index 51327f772..db3e591be 100644 --- a/backend/lcfs/web/api/other_uses/schema.py +++ b/backend/lcfs/web/api/other_uses/schema.py @@ -40,12 +40,19 @@ class ExpectedUseTypeSchema(BaseSchema): description: Optional[str] = None +class FuelCategorySchema(BaseSchema): + fuel_category_id: int + category: str + description: Optional[str] = None + + class FuelTypeSchema(BaseSchema): fuel_type_id: int fuel_type: str fossil_derived: Optional[bool] = None provision_1_id: Optional[int] = None provision_2_id: Optional[int] = None + fuel_categories: List[FuelCategorySchema] default_carbon_intensity: Optional[float] = None fuel_codes: Optional[List[FuelCodeSchema]] = [] provision_of_the_act: Optional[List[ProvisionOfTheActSchema]] = [] diff --git a/frontend/src/components/BCDataGrid/columns.js b/frontend/src/components/BCDataGrid/columns.js index 0e491f34e..4e26c12cd 100644 --- a/frontend/src/components/BCDataGrid/columns.js +++ b/frontend/src/components/BCDataGrid/columns.js @@ -24,6 +24,7 @@ export const actions = (props) => ({ cellRendererParams: props, pinned: 'left', maxWidth: 110, + minWidth: 90, editable: false, suppressKeyboardEvent, filter: false, diff --git a/frontend/src/views/OtherUses/AddEditOtherUses.jsx b/frontend/src/views/OtherUses/AddEditOtherUses.jsx index bbd553ca3..af806ce00 100644 --- a/frontend/src/views/OtherUses/AddEditOtherUses.jsx +++ b/frontend/src/views/OtherUses/AddEditOtherUses.jsx @@ -151,20 +151,39 @@ export const AddEditOtherUses = () => { const ciOfFuel = findCiOfFuel(params.data, optionsData) params.node.setDataValue('ciOfFuel', ciOfFuel) - // Auto-populate the "Unit" field based on the selected fuel type - if (params.colDef.field === 'fuelType') { - const fuelType = optionsData?.fuelTypes?.find( - (obj) => params.data.fuelType === obj.fuelType - ); - if (fuelType && fuelType.units) { - params.node.setDataValue('units', fuelType.units); - } else { - params.node.setDataValue('units', ''); + // Auto-populate fields based on the selected fuel type + if (params.colDef.field === 'fuelType') { + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ); + if (fuelType) { + // Auto-populate the "units" field + if (fuelType.units) { + params.node.setDataValue('units', fuelType.units); + } else { + params.node.setDataValue('units', ''); + } + + // Auto-populate the "fuelCategory" field + const fuelCategoryOptions = fuelType.fuelCategories.map( + (item) => item.category + ); + params.node.setDataValue('fuelCategory', fuelCategoryOptions[0] ?? null); + + // Auto-populate the "fuelCode" field + const fuelCodeOptions = fuelType.fuelCodes.map( + (code) => code.fuelCode + ); + params.node.setDataValue('fuelCode', fuelCodeOptions[0] ?? null); + params.node.setDataValue( + 'fuelCodeId', + fuelType.fuelCodes[0]?.fuelCodeId ?? null + ); + } } } - } }, - [optionsData] + [optionsData, findCiOfFuel] ) const onCellEditingStopped = useCallback( diff --git a/frontend/src/views/OtherUses/_schema.jsx b/frontend/src/views/OtherUses/_schema.jsx index 9af82de0b..555e86f25 100644 --- a/frontend/src/views/OtherUses/_schema.jsx +++ b/frontend/src/views/OtherUses/_schema.jsx @@ -49,12 +49,17 @@ export const otherUsesColDefs = (optionsData, errors) => [ headerName: i18n.t('otherUses:otherUsesColLabels.fuelCategory'), headerComponent: RequiredHeader, cellEditor: AutocompleteCellEditor, - cellEditorParams: { - options: optionsData.fuelCategories.map((obj) => obj.category), - multiple: false, - disableCloseOnSelect: false, - freeSolo: false, - openOnFocus: true + cellEditorParams: (params) => { + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ); + return { + options: fuelType ? fuelType.fuelCategories.map((item) => item.category) : [], + multiple: false, + disableCloseOnSelect: false, + freeSolo: false, + openOnFocus: true + }; }, suppressKeyboardEvent, cellRenderer: (params) => From ff27e832b63b57d41e638a2bbc262028fa92ff14 Mon Sep 17 00:00:00 2001 From: Hamed Valiollahi Bayeki Date: Fri, 13 Dec 2024 12:48:02 -0800 Subject: [PATCH 2/2] feat: enforce mandatory fuel code selection for schedules validation --- backend/lcfs/web/api/fuel_export/repo.py | 1 + backend/lcfs/web/application.py | 2 + .../locales/en/allocationAgreement.json | 1 + .../src/assets/locales/en/fuelExport.json | 3 +- .../src/assets/locales/en/fuelSupply.json | 1 + frontend/src/assets/locales/en/otherUses.json | 1 + .../AddEditAllocationAgreements.jsx | 24 ++- .../views/AllocationAgreements/_schema.jsx | 78 +++++++++- .../views/FuelExports/AddEditFuelExports.jsx | 34 ++++- frontend/src/views/FuelExports/_schema.jsx | 99 ++++++++++-- .../FuelSupplies/AddEditFuelSupplies.jsx | 37 +++-- frontend/src/views/FuelSupplies/_schema.jsx | 81 +++++++--- .../src/views/OtherUses/AddEditOtherUses.jsx | 61 +++++--- frontend/src/views/OtherUses/_schema.jsx | 142 +++++++++++------- 14 files changed, 437 insertions(+), 128 deletions(-) diff --git a/backend/lcfs/web/api/fuel_export/repo.py b/backend/lcfs/web/api/fuel_export/repo.py index 36aeb4ce1..d09a546dc 100644 --- a/backend/lcfs/web/api/fuel_export/repo.py +++ b/backend/lcfs/web/api/fuel_export/repo.py @@ -260,6 +260,7 @@ async def create_fuel_export(self, fuel_export: FuelExport) -> FuelExport: "fuel_type", "provision_of_the_act", "end_use_type", + "fuel_code", ], ) return fuel_export diff --git a/backend/lcfs/web/application.py b/backend/lcfs/web/application.py index e7117a105..8aef6126c 100644 --- a/backend/lcfs/web/application.py +++ b/backend/lcfs/web/application.py @@ -1,4 +1,6 @@ import logging +import os +import debugpy import uuid import structlog diff --git a/frontend/src/assets/locales/en/allocationAgreement.json b/frontend/src/assets/locales/en/allocationAgreement.json index 1da1af078..f120b7ebe 100644 --- a/frontend/src/assets/locales/en/allocationAgreement.json +++ b/frontend/src/assets/locales/en/allocationAgreement.json @@ -3,6 +3,7 @@ "noAllocationAgreementsFound": "No allocation agreements found", "addAllocationAgreementRowsTitle": "Allocation agreements (e.g., allocating responsibility for fuel)", "allocationAgreementSubtitle": "Enter allocation agreement details below", + "fuelCodeFieldRequiredError": "Error updating row: Fuel code field required", "allocationAgreementColLabels": { "transaction": "Responsibility", "transactionPartner": "Legal name of transaction partner", diff --git a/frontend/src/assets/locales/en/fuelExport.json b/frontend/src/assets/locales/en/fuelExport.json index 002fba7c1..d5cf55dc6 100644 --- a/frontend/src/assets/locales/en/fuelExport.json +++ b/frontend/src/assets/locales/en/fuelExport.json @@ -32,5 +32,6 @@ }, "validateMsg": { "isRequired": "{{field}} is required" - } + }, + "fuelCodeFieldRequiredError": "Error updating row: Fuel code field required" } diff --git a/frontend/src/assets/locales/en/fuelSupply.json b/frontend/src/assets/locales/en/fuelSupply.json index 3e6036080..93c75760b 100644 --- a/frontend/src/assets/locales/en/fuelSupply.json +++ b/frontend/src/assets/locales/en/fuelSupply.json @@ -9,6 +9,7 @@ "LoadFailMsg": "Failed to load supply of fuel rows", "addRow": "Add row", "rows": "rows", + "fuelCodeFieldRequiredError": "Error updating row: Fuel code field required", "fuelSupplyColLabels": { "complianceReportId": "Compliance Report ID", "fuelSupplyId": "Fuel supply ID", diff --git a/frontend/src/assets/locales/en/otherUses.json b/frontend/src/assets/locales/en/otherUses.json index c67e328ab..70b32650d 100644 --- a/frontend/src/assets/locales/en/otherUses.json +++ b/frontend/src/assets/locales/en/otherUses.json @@ -21,6 +21,7 @@ "approveConfirmText": "Are you sure you want to approve this other use entry?", "addRow": "Add row", "rows": "rows", + "fuelCodeFieldRequiredError": "Error updating row: Fuel code field required", "otherUsesColLabels": { "complianceReportId": "Compliance report ID", "fuelType": "Fuel type", diff --git a/frontend/src/views/AllocationAgreements/AddEditAllocationAgreements.jsx b/frontend/src/views/AllocationAgreements/AddEditAllocationAgreements.jsx index f09d3ee0e..e5a2e34c9 100644 --- a/frontend/src/views/AllocationAgreements/AddEditAllocationAgreements.jsx +++ b/frontend/src/views/AllocationAgreements/AddEditAllocationAgreements.jsx @@ -3,7 +3,6 @@ import BCTypography from '@/components/BCTypography' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate, useParams } from 'react-router-dom' -import { BCAlert2 } from '@/components/BCAlert' import BCBox from '@/components/BCBox' import { BCGridEditor } from '@/components/BCDataGrid/BCGridEditor' import { @@ -169,6 +168,29 @@ export const AddEditAllocationAgreements = () => { updatedData.ciOfFuel = DEFAULT_CI_FUEL[updatedData.fuelCategory] } + const isFuelCodeScenario = + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + if (isFuelCodeScenario && !updatedData.fuelCode) { + // Fuel code is required but not provided + setErrors((prevErrors) => ({ + ...prevErrors, + [params.node.data.id]: ['fuelCode'] + })) + + alertRef.current?.triggerAlert({ + message: t('allocationAgreement:fuelCodeFieldRequiredError'), + severity: 'error' + }) + + updatedData = { + ...updatedData, + validationStatus: 'error' + } + + params.node.updateData(updatedData) + return // Stop execution, do not proceed to save + } + try { setErrors({}) await saveRow(updatedData) diff --git a/frontend/src/views/AllocationAgreements/_schema.jsx b/frontend/src/views/AllocationAgreements/_schema.jsx index 6e503e3dd..d90a4e212 100644 --- a/frontend/src/views/AllocationAgreements/_schema.jsx +++ b/frontend/src/views/AllocationAgreements/_schema.jsx @@ -196,6 +196,7 @@ export const allocationAgreementColDefs = (optionsData, errors) => [ params.data.units = fuelType?.units params.data.unrecognized = fuelType?.unrecognized params.data.provisionOfTheAct = null + params.data.fuelCode = undefined } return true }, @@ -302,16 +303,85 @@ export const allocationAgreementColDefs = (optionsData, errors) => [ }), cellStyle: (params) => { const style = StandardCellErrors(params, errors) - const conditionalStyle = + const isFuelCodeScenario = params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE - ? { backgroundColor: '#fff', borderColor: 'unset' } - : { backgroundColor: '#f2f2f2' } + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + const fuelCodes = fuelType?.fuelCodes || [] + const fuelCodeRequiredAndMissing = + isFuelCodeScenario && !params.data.fuelCode + + let conditionalStyle = {} + + // If required and missing, show red border and white background + if (fuelCodeRequiredAndMissing) { + style.borderColor = 'red' + style.backgroundColor = '#fff' + } else { + // Apply conditional styling if not missing + conditionalStyle = + isFuelCodeScenario && fuelCodes.length > 0 + ? { + backgroundColor: '#fff', + borderColor: style.borderColor || 'unset' + } + : { backgroundColor: '#f2f2f2' } + } + return { ...style, ...conditionalStyle } }, suppressKeyboardEvent, minWidth: 150, editable: (params) => - params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE, + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE && + optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + )?.fuelCodes?.length > 0, + valueGetter: (params) => { + const fuelTypeObj = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + if (!fuelTypeObj) return params.data.fuelCode + + const isFuelCodeScenario = + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + const fuelCodes = + fuelTypeObj.fuelCodes?.map((item) => item.fuelCode) || [] + + if (isFuelCodeScenario && !params.data.fuelCode) { + // Autopopulate if only one fuel code is available + if (fuelCodes.length === 1) { + const singleFuelCode = fuelTypeObj.fuelCodes[0] + params.data.fuelCode = singleFuelCode.fuelCode + params.data.fuelCodeId = singleFuelCode.fuelCodeId + } + } + + return params.data.fuelCode + }, + valueSetter: (params) => { + if (params.newValue) { + params.data.fuelCode = params.newValue + + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + if (params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE) { + const matchingFuelCode = fuelType?.fuelCodes?.find( + (fuelCode) => params.data.fuelCode === fuelCode.fuelCode + ) + if (matchingFuelCode) { + params.data.fuelCodeId = matchingFuelCode.fuelCodeId + } + } + } else { + // If user clears the value + params.data.fuelCode = undefined + params.data.fuelCodeId = undefined + } + return true + }, tooltipValueGetter: (p) => 'Select the approved fuel code' }, { diff --git a/frontend/src/views/FuelExports/AddEditFuelExports.jsx b/frontend/src/views/FuelExports/AddEditFuelExports.jsx index ebaa03498..b9a40d86d 100644 --- a/frontend/src/views/FuelExports/AddEditFuelExports.jsx +++ b/frontend/src/views/FuelExports/AddEditFuelExports.jsx @@ -13,7 +13,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { v4 as uuid } from 'uuid' -import { defaultColDef, fuelExportColDefs } from './_schema' +import { + defaultColDef, + fuelExportColDefs, + PROVISION_APPROVED_FUEL_CODE +} from './_schema' export const AddEditFuelExports = () => { const [rowData, setRowData] = useState([]) @@ -143,7 +147,33 @@ export const AddEditFuelExports = () => { acc[key] = value return acc }, {}) + updatedData.compliancePeriod = compliancePeriod + + // Local validation before saving + const isFuelCodeScenario = + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + + if (isFuelCodeScenario && !updatedData.fuelCode) { + // Fuel code is required but not provided + setErrors((prevErrors) => ({ + ...prevErrors, + [params.node.data.id]: ['fuelCode'] + })) + + alertRef.current?.triggerAlert({ + message: t('fuelExport:fuelCodeFieldRequiredError'), + severity: 'error' + }) + + updatedData = { + ...updatedData, + validationStatus: 'error' + } + params.node.updateData(updatedData) + return // Don't proceed with save + } + try { setErrors({}) await saveRow(updatedData) @@ -189,7 +219,7 @@ export const AddEditFuelExports = () => { params.node.updateData(updatedData) }, - [saveRow, t] + [saveRow, t, compliancePeriod] ) const onAction = async (action, params) => { diff --git a/frontend/src/views/FuelExports/_schema.jsx b/frontend/src/views/FuelExports/_schema.jsx index f113671d3..0caae593b 100644 --- a/frontend/src/views/FuelExports/_schema.jsx +++ b/frontend/src/views/FuelExports/_schema.jsx @@ -17,6 +17,8 @@ import { fuelTypeOtherConditionalStyle } from '@/utils/fuelTypeOther' +export const PROVISION_APPROVED_FUEL_CODE = 'Fuel code - section 19 (b) (i)' + const cellErrorStyle = (params, errors) => { let style = {} if ( @@ -318,29 +320,94 @@ export const fuelExportColDefs = (optionsData, errors) => [ field: 'fuelCode', headerName: i18n.t('fuelExport:fuelExportColLabels.fuelCode'), cellEditor: 'agSelectCellEditor', - cellEditorParams: (params) => ({ - values: optionsData?.fuelTypes - ?.find((obj) => params.data.fuelType === obj.fuelType) - ?.fuelCodes.map((item) => item.fuelCode) - }), + suppressKeyboardEvent, + minWidth: 135, + cellEditorParams: (params) => { + const fuelTypeObj = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + return { + values: fuelTypeObj?.fuelCodes?.map((item) => item.fuelCode) || [] + } + }, cellStyle: (params) => { const style = cellErrorStyle(params, errors) + const fuelTypeObj = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + const fuelCodes = + fuelTypeObj?.fuelCodes.map((item) => item.fuelCode) || [] + const isFuelCodeScenario = + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + + // Check if fuel code is required (scenario) but missing + const fuelCodeRequiredAndMissing = + isFuelCodeScenario && !params.data.fuelCode + + if (fuelCodeRequiredAndMissing) { + // If required and missing, force a red border + style.borderColor = 'red' + } + const conditionalStyle = - optionsData?.fuelTypes - ?.find((obj) => params.data.fuelType === obj.fuelType) - ?.fuelCodes.map((item) => item.fuelCode).length > 0 && - /Fuel code/i.test(params.data.provisionOfTheAct) + fuelCodes.length > 0 && + isFuelCodeScenario && + !fuelCodeRequiredAndMissing ? { backgroundColor: '#fff', borderColor: 'unset' } : { backgroundColor: '#f2f2f2' } return { ...style, ...conditionalStyle } }, - suppressKeyboardEvent, - minWidth: 135, - editable: (params) => - optionsData?.fuelTypes - ?.find((obj) => params.data.fuelType === obj.fuelType) - ?.fuelCodes.map((item) => item.fuelCode).length > 0 && - /Fuel code/i.test(params.data.provisionOfTheAct) + editable: (params) => { + const fuelTypeObj = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + const fuelCodes = fuelTypeObj?.fuelCodes || [] + return ( + fuelCodes.length > 0 && + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + ) + }, + valueGetter: (params) => { + const fuelTypeObj = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + if (!fuelTypeObj) return params.data.fuelCode + + const isFuelCodeScenario = + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + const fuelCodes = + fuelTypeObj.fuelCodes?.map((item) => item.fuelCode) || [] + + if (isFuelCodeScenario && !params.data.fuelCode) { + // Autopopulate if only one fuel code is available + if (fuelCodes.length === 1) { + const singleFuelCode = fuelTypeObj.fuelCodes[0] + params.data.fuelCode = singleFuelCode.fuelCode + params.data.fuelCodeId = singleFuelCode.fuelCodeId + } + } + + return params.data.fuelCode + }, + valueSetter: (params) => { + const newCode = params.newValue + const fuelTypeObj = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + const selectedFuelCodeObj = fuelTypeObj?.fuelCodes.find( + (item) => item.fuelCode === newCode + ) + + if (selectedFuelCodeObj) { + params.data.fuelCode = selectedFuelCodeObj.fuelCode + params.data.fuelCodeId = selectedFuelCodeObj.fuelCodeId + } else { + params.data.fuelCode = undefined + params.data.fuelCodeId = undefined + } + + return true + } }, { field: 'quantity', diff --git a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx index 7d7c4bd29..c78718694 100644 --- a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx +++ b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx @@ -14,7 +14,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { v4 as uuid } from 'uuid' -import { defaultColDef, fuelSupplyColDefs } from './_schema' +import { + defaultColDef, + fuelSupplyColDefs, + PROVISION_APPROVED_FUEL_CODE +} from './_schema' export const AddEditFuelSupplies = () => { const [rowData, setRowData] = useState([]) @@ -131,15 +135,6 @@ export const AddEditFuelSupplies = () => { 'fuelCategory', fuelCategoryOptions[0] ?? null ) - - const fuelCodeOptions = selectedFuelType.fuelCodes.map( - (code) => code.fuelCode - ) - params.node.setDataValue('fuelCode', fuelCodeOptions[0] ?? null) - params.node.setDataValue( - 'fuelCodeId', - selectedFuelType.fuelCodes[0]?.fuelCodeId ?? null - ) } } }, @@ -164,6 +159,28 @@ export const AddEditFuelSupplies = () => { if (updatedData.fuelType === 'Other') { updatedData.ciOfFuel = DEFAULT_CI_FUEL[updatedData.fuelCategory] } + + const isFuelCodeScenario = + params.node.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + if (isFuelCodeScenario && !params.node.data.fuelCode) { + // Set error on the row + setErrors({ + [params.node.data.id]: ['fuelCode'] + }) + + alertRef.current?.triggerAlert({ + message: t('fuelSupply:fuelCodeFieldRequiredError'), + severity: 'error' + }) + + // Update node data to reflect error state + params.node.updateData({ + ...params.node.data, + validationStatus: 'error' + }) + return // Stop saving further + } + try { setErrors({}) await saveRow(updatedData) diff --git a/frontend/src/views/FuelSupplies/_schema.jsx b/frontend/src/views/FuelSupplies/_schema.jsx index 912dc994d..0dc792d73 100644 --- a/frontend/src/views/FuelSupplies/_schema.jsx +++ b/frontend/src/views/FuelSupplies/_schema.jsx @@ -19,6 +19,8 @@ import { } from '@/utils/grid/errorRenderers' import { apiRoutes } from '@/constants/routes' +export const PROVISION_APPROVED_FUEL_CODE = 'Fuel code - section 19 (b) (i)' + export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ validation, actions({ @@ -292,21 +294,35 @@ export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ field: 'fuelCode', headerName: i18n.t('fuelSupply:fuelSupplyColLabels.fuelCode'), cellEditor: 'agSelectCellEditor', - cellEditorParams: (params) => ({ - values: optionsData?.fuelTypes - ?.find((obj) => params.data.fuelType === obj.fuelType) - ?.fuelCodes.map((item) => item.fuelCode) - }), + cellEditorParams: (params) => { + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + return { + values: fuelType?.fuelCodes.map((item) => item.fuelCode) || [] + } + }, cellStyle: (params) => { const style = StandardCellWarningAndErrors(params, errors, warnings) - const conditionalStyle = - optionsData?.fuelTypes - ?.find((obj) => params.data.fuelType === obj.fuelType) - ?.fuelCodes.map((item) => item.fuelCode).length > 0 && - /Fuel code/i.test(params.data.provisionOfTheAct) - ? { backgroundColor: '#fff' } - : { backgroundColor: '#f2f2f2', borderColor: 'unset' } - return { ...style, ...conditionalStyle } + const isFuelCodeScenario = + params.data?.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + const fuelCodes = fuelType?.fuelCodes || [] + const fuelCodeRequiredAndMissing = + isFuelCodeScenario && !params.data.fuelCode + + if (fuelCodeRequiredAndMissing) { + // Highlight the cell if fuel code is required but not selected + return { ...style, backgroundColor: '#fff', borderColor: 'red' } + } else if (isFuelCodeScenario && fuelCodes.length > 0) { + // Allow selection when scenario matches and codes are present + return { ...style, backgroundColor: '#fff', borderColor: 'unset' } + } else { + // Otherwise disabled styling + return { ...style, backgroundColor: '#f2f2f2', borderColor: 'unset' } + } }, suppressKeyboardEvent, minWidth: 135, @@ -314,29 +330,50 @@ export const fuelSupplyColDefs = (optionsData, errors, warnings) => [ const fuelType = optionsData?.fuelTypes?.find( (obj) => params.data.fuelType === obj.fuelType ) - if (fuelType) { - return ( - fuelType.fuelCodes.map((item) => item.fuelCode).length > 0 && - /Fuel code/i.test(params.data.provisionOfTheAct) - ) + return ( + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE && + fuelType?.fuelCodes?.length > 0 + ) + }, + valueGetter: (params) => { + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + if (!fuelType) return params.data.fuelCode + + const isFuelCodeScenario = + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + const fuelCodes = fuelType?.fuelCodes?.map((item) => item.fuelCode) || [] + + if (isFuelCodeScenario && !params.data.fuelCode) { + // If only one code is available, auto-populate + if (fuelCodes.length === 1) { + const singleFuelCode = fuelType.fuelCodes[0] + params.data.fuelCode = singleFuelCode.fuelCode + params.data.fuelCodeId = singleFuelCode.fuelCodeId + } } - return false + + return params.data.fuelCode }, valueSetter: (params) => { if (params.newValue) { params.data.fuelCode = params.newValue - const fuelType = optionsData?.fuelTypes?.find( (obj) => params.data.fuelType === obj.fuelType ) - if (/Fuel code/i.test(params.data.provisionOfTheAct)) { - const matchingFuelCode = fuelType.fuelCodes?.find( + if (params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE) { + const matchingFuelCode = fuelType?.fuelCodes?.find( (fuelCode) => params.data.fuelCode === fuelCode.fuelCode ) if (matchingFuelCode) { params.data.fuelCodeId = matchingFuelCode.fuelCodeId } } + } else { + // If user clears the value + params.data.fuelCode = undefined + params.data.fuelCodeId = undefined } return true } diff --git a/frontend/src/views/OtherUses/AddEditOtherUses.jsx b/frontend/src/views/OtherUses/AddEditOtherUses.jsx index 58586cb9e..e77edef9a 100644 --- a/frontend/src/views/OtherUses/AddEditOtherUses.jsx +++ b/frontend/src/views/OtherUses/AddEditOtherUses.jsx @@ -1,5 +1,4 @@ -import { BCAlert2 } from '@/components/BCAlert' -import BCButton from '@/components/BCButton' + import { BCGridEditor } from '@/components/BCDataGrid/BCGridEditor' import Loading from '@/components/Loading' import { @@ -8,16 +7,17 @@ import { useSaveOtherUses } from '@/hooks/useOtherUses' import { cleanEmptyStringValues } from '@/utils/formatters' -import { faFloppyDisk } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { Stack } from '@mui/material' import BCTypography from '@/components/BCTypography' import Grid2 from '@mui/material/Unstable_Grid2/Grid2' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation, useNavigate, useParams } from 'react-router-dom' import { v4 as uuid } from 'uuid' -import { defaultColDef, otherUsesColDefs, PROVISION_APPROVED_FUEL_CODE} from './_schema' +import { + defaultColDef, + otherUsesColDefs, + PROVISION_APPROVED_FUEL_CODE +} from './_schema' import * as ROUTES from '@/constants/routes/routes.js' export const AddEditOtherUses = () => { @@ -55,31 +55,31 @@ export const AddEditOtherUses = () => { rows.map((row) => ({ ...row, id: row.id || uuid(), - isValid: true, - })); + isValid: true + })) - setRowData(ensureRowIds(otherUses)); + setRowData(ensureRowIds(otherUses)) } - }, [otherUses]); + }, [otherUses]) const findCiOfFuel = useCallback((data, optionsData) => { - let ciOfFuel = 0; + let ciOfFuel = 0 if (data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE) { const fuelType = optionsData?.fuelTypes?.find( (obj) => data.fuelType === obj.fuelType - ); + ) const fuelCode = fuelType?.fuelCodes?.find( (item) => item.fuelCode === data.fuelCode - ); - ciOfFuel = fuelCode?.carbonIntensity || 0; + ) + ciOfFuel = fuelCode?.carbonIntensity || 0 } else { const fuelType = optionsData?.fuelTypes?.find( (obj) => data.fuelType === obj.fuelType - ); - ciOfFuel = fuelType?.defaultCarbonIntensity || 0; + ) + ciOfFuel = fuelType?.defaultCarbonIntensity || 0 } - return ciOfFuel; - }, []); + return ciOfFuel + }, []) const onGridReady = (params) => { const ensureRowIds = (rows) => { @@ -169,6 +169,29 @@ export const AddEditOtherUses = () => { // clean up any null or empty string values let updatedData = cleanEmptyStringValues(params.data) + const isFuelCodeScenario = + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + if (isFuelCodeScenario && !updatedData.fuelCode) { + // Fuel code is required but not provided + setErrors((prevErrors) => ({ + ...prevErrors, + [params.node.data.id]: ['fuelCode'] + })) + + alertRef.current?.triggerAlert({ + message: t('otherUses:fuelCodeFieldRequiredError'), + severity: 'error' + }) + + updatedData = { + ...updatedData, + validationStatus: 'error' + } + + params.node.updateData(updatedData) + return // Stop execution, do not proceed to save + } + try { setErrors({}) await saveRow(updatedData) diff --git a/frontend/src/views/OtherUses/_schema.jsx b/frontend/src/views/OtherUses/_schema.jsx index a392bfc4a..a9088f6a6 100644 --- a/frontend/src/views/OtherUses/_schema.jsx +++ b/frontend/src/views/OtherUses/_schema.jsx @@ -23,7 +23,7 @@ export const otherUsesColDefs = (optionsData, errors) => [ hide: true }, { - field:'otherUsesId', + field: 'otherUsesId', hide: true }, { @@ -42,7 +42,15 @@ export const otherUsesColDefs = (optionsData, errors) => [ suppressKeyboardEvent, cellRenderer: (params) => params.value || Select, - cellStyle: (params) => StandardCellErrors(params, errors) + cellStyle: (params) => StandardCellErrors(params, errors), + valueSetter: (params) => { + if (params.newValue) { + // TODO: Evaluate if additional fields need to be reset when fuel type changes + params.data.fuelType = params.newValue + params.data.fuelCode = undefined + } + return true + } }, { field: 'fuelCategory', @@ -65,9 +73,7 @@ export const otherUsesColDefs = (optionsData, errors) => [ { field: 'provisionOfTheAct', headerComponent: RequiredHeader, - headerName: i18n.t( - 'otherUses:otherUsesColLabels.provisionOfTheAct' - ), + headerName: i18n.t('otherUses:otherUsesColLabels.provisionOfTheAct'), cellEditor: 'agSelectCellEditor', cellEditorParams: (params) => { const fuelType = optionsData?.fuelTypes?.find( @@ -89,11 +95,11 @@ export const otherUsesColDefs = (optionsData, errors) => [ suppressKeyboardEvent, valueSetter: (params) => { if (params.newValue !== params.oldValue) { - params.data.provisionOfTheAct = params.newValue; - params.data.fuelCode = ''; // Reset fuelCode when provisionOfTheAct changes - return true; + params.data.provisionOfTheAct = params.newValue + params.data.fuelCode = '' // Reset fuelCode when provisionOfTheAct changes + return true } - return false; + return false }, minWidth: 300, editable: true, @@ -105,61 +111,91 @@ export const otherUsesColDefs = (optionsData, errors) => [ headerName: i18n.t('otherUses:otherUsesColLabels.fuelCode'), cellEditor: AutocompleteCellEditor, cellEditorParams: (params) => { - const fuelType = optionsData?.fuelTypes?.find((obj) => params.data.fuelType === obj.fuelType); + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) - return { - options: fuelType?.fuelCodes?.map((item) => item.fuelCode) || [], // Safely access fuelCodes - multiple: false, - disableCloseOnSelect: false, - freeSolo: false, - openOnFocus: true - }; + return { + options: fuelType?.fuelCodes?.map((item) => item.fuelCode) || [], // Safely access fuelCodes + multiple: false, + disableCloseOnSelect: false, + freeSolo: false, + openOnFocus: true + } }, cellRenderer: (params) => { - const fuelType = optionsData?.fuelTypes?.find((obj) => params.data.fuelType === obj.fuelType); + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) if ( params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE && fuelType?.fuelCodes?.length > 0 ) { - return params.value || Select; + return ( + params.value || Select + ) } - return null; + return null }, cellStyle: (params) => { - const style = StandardCellErrors(params, errors); - const conditionalStyle = - params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE && - optionsData?.fuelTypes - ?.find((obj) => params.data.fuelType === obj.fuelType) - ?.fuelCodes?.length > 0 - ? { backgroundColor: '#fff', borderColor: 'unset' } - : { backgroundColor: '#f2f2f2' }; - return { ...style, ...conditionalStyle }; + const style = StandardCellErrors(params, errors) + const isFuelCodeScenario = + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + const fuelCodes = fuelType?.fuelCodes || [] + const fuelCodeRequiredAndMissing = + isFuelCodeScenario && !params.data.fuelCode + + // If required and missing, show red border + if (fuelCodeRequiredAndMissing) { + style.borderColor = 'red' + } + + const conditionalStyle = + isFuelCodeScenario && + fuelCodes.length > 0 && + !fuelCodeRequiredAndMissing + ? { + backgroundColor: '#fff', + borderColor: style.borderColor || 'unset' + } + : { backgroundColor: '#f2f2f2' } + + return { ...style, ...conditionalStyle } }, suppressKeyboardEvent, minWidth: 150, editable: (params) => { - const fuelType = optionsData?.fuelTypes?.find((obj) => params.data.fuelType === obj.fuelType); + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) return ( params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE && fuelType?.fuelCodes?.length > 0 - ); + ) }, - valueSetter: (params) => { - if (params.newValue) { - params.data.fuelCode = params.newValue; + valueGetter: (params) => { + const isFuelCodeScenario = + params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE + const fuelType = optionsData?.fuelTypes?.find( + (obj) => params.data.fuelType === obj.fuelType + ) + const fuelCodes = fuelType?.fuelCodes || [] - const fuelType = optionsData?.fuelTypes?.find((obj) => params.data.fuelType === obj.fuelType); - if (params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE) { - const matchingFuelCode = fuelType?.fuelCodes?.find( - (fuelCode) => params.data.fuelCode === fuelCode.fuelCode - ); - if (matchingFuelCode) { - params.data.fuelCodeId = matchingFuelCode.fuelCodeId; - } - } - } - return true; + if ( + isFuelCodeScenario && + !params.data.fuelCode && + fuelCodes.length === 1 + ) { + // Autopopulate if only one fuel code is available + const singleFuelCode = fuelCodes[0] + params.data.fuelCode = singleFuelCode.fuelCode + params.data.fuelCodeId = singleFuelCode.fuelCodeId + } + + return params.data.fuelCode }, tooltipValueGetter: (p) => 'Select the approved fuel code' }, @@ -205,31 +241,31 @@ export const otherUsesColDefs = (optionsData, errors) => [ valueGetter: (params) => { const fuelType = optionsData?.fuelTypes?.find( (obj) => params.data.fuelType === obj.fuelType - ); + ) if (params.data.provisionOfTheAct === PROVISION_APPROVED_FUEL_CODE) { return ( fuelType?.fuelCodes?.find( (item) => item.fuelCode === params.data.fuelCode )?.carbonIntensity || 0 - ); + ) } if (fuelType) { if (params.data.fuelType === 'Other' && params.data.fuelCategory) { - const categories = fuelType.fuelCategories; + const categories = fuelType.fuelCategories const defaultCI = categories?.find( (cat) => cat.category === params.data.fuelCategory - )?.defaultAndPrescribedCi; + )?.defaultAndPrescribedCi - return defaultCI || 0; + return defaultCI || 0 } - return fuelType.defaultCarbonIntensity || 0; + return fuelType.defaultCarbonIntensity || 0 } - return 0; + return 0 }, - minWidth: 150, + minWidth: 150 }, { field: 'expectedUse',