diff --git a/frontend/cypress/e2e/smoke-test/create-a-class-seedlot.cy.ts b/frontend/cypress/e2e/smoke-test/create-a-class-seedlot.cy.ts index 6cbeb71d1..53ed03821 100644 --- a/frontend/cypress/e2e/smoke-test/create-a-class-seedlot.cy.ts +++ b/frontend/cypress/e2e/smoke-test/create-a-class-seedlot.cy.ts @@ -16,29 +16,6 @@ describe('Create A-Class Seedlot', () => { cy.login(); cy.visit('/seedlots'); cy.url().should('contains', '/seedlots'); - - cy.intercept( - { - method: 'GET', - url: '**/api/forest-clients/**' - }, - { - statusCode: 200 - } - ).as('verifyLocationCode'); - - cy.intercept( - { - method: 'POST', - url: '**/api/seedlots' - }, - { - statusCode: 201, - body: { - seedlotNumber: '654321' - } - } - ).as('submitSeedlot'); }); it('should register an A-Class Seedlot', () => { @@ -60,15 +37,15 @@ describe('Create A-Class Seedlot', () => { .clear() .type(data.applicantAgency.number, { delay: TYPE_DELAY }); // Enter an invalid email address - cy.get('#appliccant-email-input') + cy.get('#applicant-email-input') .clear() .type(data.applicantAgency.invalidEmail, { delay: TYPE_DELAY }); cy.get('#agency-number-input') .click(); - cy.get('#appliccant-email-input-error-msg') + cy.get('#applicant-email-input-error-msg') .should('be.visible'); // Enter the applicant email address - cy.get('#appliccant-email-input') + cy.get('#applicant-email-input') .clear() .type(data.applicantAgency.email, { delay: TYPE_DELAY }); // Enter the seedlot species, wait for species data to load @@ -107,14 +84,13 @@ describe('Create A-Class Seedlot', () => { cy.get('#seedlot-source-radio-btn-cus') .should('not.be.checked'); // To be registered? should be checked by default - cy.get('#registered-tree-seed-center') + cy.get('#register-w-tsc-yes') .should('be.checked'); - // To be registeredCollected from B.C. source? should be checked by default// as - cy.get('#collected-bc') + // Collected within bc? "Yes" should be checked by default + cy.get('#collected-within-bc-yes') .should('be.checked'); // Click on button Create seedlot number - cy.get('.save-button') - .find('button') + cy.get('.submit-button') .click(); cy.url().should('contains', '/creation-success'); cy.get('h1').contains('654321'); diff --git a/frontend/cypress/fixtures/vegetation-code.json b/frontend/cypress/fixtures/vegetation-code.json new file mode 100644 index 000000000..8292d9703 --- /dev/null +++ b/frontend/cypress/fixtures/vegetation-code.json @@ -0,0 +1,62 @@ +[ + { + "code": "CW", + "label": "CW - Western redcedar", + "description": "Western redcedar" + }, + { + "code": "DR", + "label": "DR - Red alder", + "description": "Red alder" + }, + { + "code": "EP", + "label": "EP - Paper birch", + "description": "Paper birch" + }, + { + "code": "FDC", + "label": "FDC - Coastal Douglas-fir", + "description": "Coastal Douglas-fir" + }, + { + "code": "FDI", + "label": "FDI - Interior Douglas-fir", + "description": "Interior Douglas-fir" + }, + { + "code": "HW", + "label": "HW - Western hemlock", + "description": "Western hemlock" + }, + { + "code": "LW", + "label": "LW - Western larch", + "description": "Western larch" + }, + { + "code": "PLI", + "label": "PLI - Lodgepole pine", + "description": "Lodgepole pine" + }, + { + "code": "PW", + "label": "PW - Western white pine", + "description": "Western white pine" + }, + { + "code": "PY", + "label": "PY - Ponderosa pine", + "description": "Ponderosa pine" + }, + { + "code": "SS", + "label": "SS - Sitka spruce", + "description": "Sitka spruce" + }, + { + "code": "SX", + "label": "SX - Spruce hybrid", + "description": "Spruce hybrid" + } +] diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts index f672e0112..1358b0919 100644 --- a/frontend/cypress/support/e2e.ts +++ b/frontend/cypress/support/e2e.ts @@ -1,4 +1,37 @@ import './commands'; -// Alternatively you can use CommonJS syntax: -// require('./commands') +beforeEach(() => { + cy.intercept( + { + method: 'GET', + url: '**/api/forest-clients/**' + }, + { + statusCode: 200 + } + ).as('verifyLocationCode'); + + cy.intercept( + { + method: 'POST', + url: '**/api/seedlots' + }, + { + statusCode: 201, + body: { + seedlotNumber: '654321' + } + } + ).as('submitSeedlot'); + + cy.intercept( + { + method: 'GET', + url: '**/api/vegetation-codes*' + }, + { + statusCode: 201, + fixture: 'vegetation-code.json' + } + ).as('vegetationCode'); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 56d69ce33..f99477403 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,6 +20,7 @@ import SeedlotCreatedFeedback from './views/Seedlot/SeedlotCreatedFeedback'; import MySeedlots from './views/Seedlot/MySeedlots'; import SeedlotRegistrationForm from './views/Seedlot/SeedlotRegistrationForm'; import FourOhFour from './views/FourOhFour'; +import EditAClassApplication from './views/Seedlot/EditAClassApplication'; import awsconfig from './aws-exports'; import AuthContext from './contexts/AuthContext'; @@ -114,6 +115,15 @@ const App: React.FC = () => { )} /> + + + + )} + /> + { const newAgency: MultiOptionsObj = { code: agency.clientNumber, label: `${agency.clientNumber} - ${clientName} - ${agency.acronym}`, - description: '' + description: clientName }; options.push(newAgency); }); diff --git a/frontend/src/api-service/seedlotAPI.ts b/frontend/src/api-service/seedlotAPI.ts index e2e31fb30..84c7582b3 100644 --- a/frontend/src/api-service/seedlotAPI.ts +++ b/frontend/src/api-service/seedlotAPI.ts @@ -1,4 +1,4 @@ -import { SeedlotRegPayloadType } from '../types/SeedlotRegistrationTypes'; +import { SeedlotPatchPayloadType, SeedlotRegPayloadType } from '../types/SeedlotRegistrationTypes'; import { SeedlotType, SeedlotsReturnType } from '../types/SeedlotType'; import ApiConfig from './ApiConfig'; import api from './api'; @@ -32,3 +32,11 @@ export const getSeedlotById = (seedlotNumber: string) => { const url = `${ApiConfig.seedlots}/${seedlotNumber}`; return api.get(url).then((res): SeedlotType => res.data); }; + +export const patchSeedlotApplicationInfo = ( + seedlotNumber: string, + payload: SeedlotPatchPayloadType +) => { + const url = `${ApiConfig.seedlots}/${seedlotNumber}/application-info`; + return api.patch(url, payload); +}; diff --git a/frontend/src/components/ApplicantAgencyFields/definitions.ts b/frontend/src/components/ApplicantAgencyFields/definitions.ts index b4d731252..c9851a9f0 100644 --- a/frontend/src/components/ApplicantAgencyFields/definitions.ts +++ b/frontend/src/components/ApplicantAgencyFields/definitions.ts @@ -1,19 +1,20 @@ import AgencyTextPropsType from '../../types/AgencyTextPropsType'; -import { FormInputType } from '../../types/FormInputType'; +import { BooleanInputType, OptionsInputType, StringInputType } from '../../types/FormInputType'; import MultiOptionsObj from '../../types/MultiOptionsObject'; interface ApplicantAgencyFieldsProps { - useDefault: FormInputType & { value: boolean }; - agency: FormInputType & { value: string }; - locationCode: FormInputType & { value: string }; + checkboxId: string; + isDefault: BooleanInputType; + agency: OptionsInputType; + locationCode: StringInputType; fieldsProps: AgencyTextPropsType; agencyOptions: Array; - defaultAgency: string; - defaultCode: string; - setAllValues: Function; - showDefaultCheckbox?: boolean; - inputsColSize?: number; + setAgencyAndCode: Function; + defaultAgency?: MultiOptionsObj; + defaultCode?: string; + showCheckbox?: boolean; readOnly?: boolean; + maxInputColSize?: number; } export default ApplicantAgencyFieldsProps; diff --git a/frontend/src/components/ApplicantAgencyFields/index.tsx b/frontend/src/components/ApplicantAgencyFields/index.tsx index 6bc4edbfa..0ea1a1353 100644 --- a/frontend/src/components/ApplicantAgencyFields/index.tsx +++ b/frontend/src/components/ApplicantAgencyFields/index.tsx @@ -1,6 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { - Row, Column, TextInput, Checkbox, ComboBox, InlineLoading + Row, Column, TextInput, Checkbox, + ComboBox, InlineLoading, FlexGrid } from '@carbon/react'; import { useMutation } from '@tanstack/react-query'; import validator from 'validator'; @@ -9,48 +10,36 @@ import { getForestClientLocation } from '../../api-service/forestClientsAPI'; import ComboBoxEvent from '../../types/ComboBoxEvent'; import MultiOptionsObj from '../../types/MultiOptionsObject'; -import { FormInputType } from '../../types/FormInputType'; -import { LOCATION_CODE_LIMIT } from '../../shared-constants/shared-constants'; -import { formatLocationCode } from '../SeedlotRegistrationSteps/CollectionStep/utils'; +import { EmptyMultiOptObj, LOCATION_CODE_LIMIT } from '../../shared-constants/shared-constants'; import { FilterObj, filterInput } from '../../utils/filterUtils'; -import getForestClientNumber from '../../utils/StringUtils'; import ApplicantAgencyFieldsProps from './definitions'; import supportTexts from './constants'; +import { formatLocationCode } from './utils'; import './styles.scss'; const ApplicantAgencyFields = ({ - useDefault, agency, locationCode, fieldsProps, agencyOptions, defaultAgency, - defaultCode, setAllValues, showDefaultCheckbox = true, inputsColSize = 6, readOnly + checkboxId, isDefault, agency, locationCode, fieldsProps, agencyOptions, + defaultAgency, defaultCode, setAgencyAndCode, readOnly, showCheckbox, maxInputColSize }: ApplicantAgencyFieldsProps) => { - const [agencyClone, setAgencyClone] = useState(agency); - const [locationCodeClone, setLocationCodeClone] = useState( - locationCode - ); - const [useDefaultClone, setUseDefaultClone] = useState( - useDefault - ); - - const [shouldUpdateValues, setShouldUpdateValues] = useState(false); - - const [forestClientNumber, setForestClientNumber] = useState(''); const [invalidLocationMessage, setInvalidLocationMessage] = useState( - locationCodeClone.isInvalid && agencyClone.value + locationCode.isInvalid && agency.value ? supportTexts.locationCode.invalidLocationForSelectedAgency : supportTexts.locationCode.invalidText ); + const [locationCodeHelperText, setLocationCodeHelperText] = useState( - supportTexts.locationCode.helperTextDisabled + supportTexts.locationCode.helperTextEnabled ); const updateAfterLocValidation = (isInvalid: boolean) => { - setLocationCodeClone({ - ...locationCodeClone, + const updatedLocationCode = { + ...locationCode, isInvalid - }); + }; setLocationCodeHelperText(supportTexts.locationCode.helperTextEnabled); - setShouldUpdateValues(true); + setAgencyAndCode(isDefault, agency, updatedLocationCode); }; const validateLocationCodeMutation = useMutation({ @@ -72,92 +61,96 @@ const ApplicantAgencyFields = ({ : supportTexts.locationCode.helperTextDisabled ); - setAgencyClone({ - ...agencyClone, - value: checked ? defaultAgency : '', - isInvalid: false - }); - setLocationCodeClone({ - ...locationCodeClone, - value: checked ? defaultCode : '', - isInvalid: false - }); - setUseDefaultClone({ - ...useDefaultClone, + const updatedAgency = { + ...agency, + value: checked ? defaultAgency : EmptyMultiOptObj + }; + + const updatedLocationCode = { + ...locationCode, + value: checked ? defaultCode : '' + }; + + const updatedIsDefault = { + ...isDefault, value: checked - }); + }; - setShouldUpdateValues(true); + setAgencyAndCode(updatedIsDefault, updatedAgency, updatedLocationCode); }; const handleAgencyInput = (value: MultiOptionsObj) => { - setForestClientNumber(value ? value.code : ''); setLocationCodeHelperText( value ? supportTexts.locationCode.helperTextEnabled : supportTexts.locationCode.helperTextDisabled ); - setAgencyClone({ - ...agencyClone, - value: value ? value.label : '' - }); - setLocationCodeClone({ - ...locationCodeClone, - value: value ? locationCodeClone.value : '' - }); - setShouldUpdateValues(true); + + const updatedAgency = { + ...agency, + value: value ?? EmptyMultiOptObj, + isInvalid: false + }; + + const updatedLocationCode = { + ...locationCode, + value: value ? locationCode.value : '' + }; + + setAgencyAndCode(isDefault, updatedAgency, updatedLocationCode); }; const handleLocationCodeChange = (value: string) => { - const locationCodeUpdated = { ...locationCodeClone }; - locationCodeUpdated.value = value.slice(0, LOCATION_CODE_LIMIT); + const updatedValue = value.slice(0, LOCATION_CODE_LIMIT); + const isInRange = validator.isInt(value, { min: 0, max: 99 }); + + let updatedIsInvalid = locationCode.isInvalid; + if (!isInRange) { setInvalidLocationMessage(supportTexts.locationCode.invalidText); - locationCodeUpdated.isInvalid = true; + updatedIsInvalid = true; } - setLocationCodeClone(locationCodeUpdated); + + const updatedLocationCode = { + ...locationCode, + value: updatedValue, + isInvalid: updatedIsInvalid + }; + + setAgencyAndCode(isDefault, agency, updatedLocationCode); }; const handleLocationCodeBlur = (value: string) => { const formattedCode = value.length ? formatLocationCode(value) : ''; - setLocationCodeClone({ - ...locationCodeClone, - value: formattedCode - }); - setShouldUpdateValues(true); + + const updatedLocationCode = { + ...locationCode, + value: formattedCode, + isInValid: true + }; + + setAgencyAndCode(isDefault, agency, updatedLocationCode); + if (formattedCode === '') return; - if (forestClientNumber) { - setLocationCodeHelperText(''); - validateLocationCodeMutation.mutate([forestClientNumber, formattedCode]); - } - }; - useEffect(() => { - if (shouldUpdateValues) { - setAllValues(agencyClone, locationCodeClone, useDefaultClone); - } else { - setAgencyClone(agency); - setLocationCodeClone(locationCode); - setUseDefaultClone(useDefault); - setForestClientNumber(agency.value ? getForestClientNumber(agencyClone.value) : ''); - } - setShouldUpdateValues(false); - }, [useDefault, agency, locationCode, shouldUpdateValues]); + setLocationCodeHelperText(''); + validateLocationCodeMutation.mutate([agency.value.code, formattedCode]); + }; return ( -
+ { - showDefaultCheckbox + showCheckbox ? ( ) => { handleDefaultCheckBox(e.target.checked); }} @@ -168,46 +161,48 @@ const ApplicantAgencyFields = ({ : null } - + handleAgencyInput(e.selectedItem)} - invalid={agencyClone.isInvalid} + invalid={agency.isInvalid} shouldFilterItem={ ({ item, inputValue }: FilterObj) => filterInput({ item, inputValue }) } size="md" /> - + ) => { handleLocationCodeChange(e.target.value); }} onWheel={(e: React.ChangeEvent) => e.target.blur()} onBlur={(e: React.ChangeEvent) => { - if (!e.target.readOnly - && locationCode.value !== e.target.value) { + if ( + !e.target.readOnly + && locationCode.value !== e.target.value + ) { handleLocationCodeBlur(e.target.value); } }} @@ -219,7 +214,7 @@ const ApplicantAgencyFields = ({ } -
+ ); }; diff --git a/frontend/src/components/ApplicantAgencyFields/styles.scss b/frontend/src/components/ApplicantAgencyFields/styles.scss index f177e0f2c..e2a213fac 100644 --- a/frontend/src/components/ApplicantAgencyFields/styles.scss +++ b/frontend/src/components/ApplicantAgencyFields/styles.scss @@ -2,6 +2,9 @@ @use '@carbon/type'; .agency-information-section { + padding: 0; + width: 100%; + .agency-information-row { margin-bottom: 1.5rem; } @@ -17,12 +20,18 @@ } button, - & + .#{vars.$bcgov-prefix}--form__helper-text { + &+.#{vars.$bcgov-prefix}--form__helper-text { display: none; } } + + .#{vars.$bcgov-prefix}--text-input:disabled { + border-block-end: 0.0625rem solid var(--#{vars.$bcgov-prefix}-border-subtle); + } + .#{vars.$bcgov-prefix}--combo-box { + li, .#{vars.$bcgov-prefix}--list-box__menu-item__option { white-space: normal; @@ -34,13 +43,9 @@ .#{vars.$bcgov-prefix}--text-input-wrapper--readonly { input[type=number] { background-color: transparent; - --bx-layout-density-padding-inline-local: 0; + padding: 0; border-block-end: 0; } - - .#{vars.$bcgov-prefix}--form__helper-text { - display: none; - } } .location-code-input { diff --git a/frontend/src/components/ApplicantAgencyFields/utils.ts b/frontend/src/components/ApplicantAgencyFields/utils.ts new file mode 100644 index 000000000..5131e68c8 --- /dev/null +++ b/frontend/src/components/ApplicantAgencyFields/utils.ts @@ -0,0 +1,4 @@ +/** + * Format single digit location code to have a leading 0. + */ +export const formatLocationCode = (value: string) => (value.length > 1 ? value : value.padStart(2, '0')); diff --git a/frontend/src/components/FormReview/index.tsx b/frontend/src/components/FormReview/index.tsx index a2889a63c..58332e5d9 100644 --- a/frontend/src/components/FormReview/index.tsx +++ b/frontend/src/components/FormReview/index.tsx @@ -57,6 +57,11 @@ const mockFormData = [ const defaultCode = '16'; const defaultAgency = '0032 - Strong Seeds Orchard - SSO'; +const defaultAgencyObj = { + code: '0032', + description: 'Strong Seeds Orchard', + label: defaultAgency +}; const orchardMock: OrchardForm = { orchards: [ @@ -88,9 +93,9 @@ const interimStorageMock = { facilityType: 'VRM' }; -const ownershipMock = initOwnershipState(defaultAgency, defaultCode); +const ownershipMock = initOwnershipState(defaultAgencyObj, defaultCode); -const collectionMock = initCollectionState(defaultAgency, defaultCode); +const collectionMock = initCollectionState(defaultAgencyObj, defaultCode); const extractionMock = { extractoryUseTSC: true, @@ -144,7 +149,7 @@ const FormReview = () => {
{ + const vegCodeQuery = useQuery({ + queryKey: ['vegetation-codes'], + queryFn: () => getVegCodes(true), + enabled: !isEdit, + staleTime: THREE_HOURS, // will not refetch for 3 hours + cacheTime: THREE_HALF_HOURS // data is cached 3.5 hours then deleted + }); + + const seedlotSourcesQuery = useQuery({ + queryKey: ['seedlot-sources'], + queryFn: () => getSeedlotSources(), + staleTime: THREE_HOURS, + cacheTime: THREE_HALF_HOURS + }); + + const setDefaultSource = (sources: SeedlotSourceType[]) => { + sources.forEach((source) => { + if (source.isDefault) { + setSeedlotFormData((prevData) => ({ + ...prevData, + sourceCode: { + ...prevData.sourceCode, + value: source.code + } + })); + } + }); + }; + + /** + * Default value is only set once upon query success, when cache data is used + * we will need to set the default again here. + */ + useEffect(() => { + if (seedlotSourcesQuery.isSuccess && !seedlotFormData.sourceCode.value) { + setDefaultSource(seedlotSourcesQuery.data); + } + }, [seedlotSourcesQuery.isFetched]); + + const renderSources = () => { + if (seedlotSourcesQuery.isFetched) { + return seedlotSourcesQuery.data.map((source: SeedlotSourceType) => ( + + )); + } + return ; + }; + + const handleBoolRadioGroup = (inputName: keyof SeedlotRegFormType, checked: boolean) => { + setSeedlotFormData((prevData) => ({ + ...prevData, + [inputName]: { + ...prevData[inputName], + value: checked + } + })); + }; + + const handleSource = (value: string) => { + setSeedlotFormData((prevData) => ({ + ...prevData, + sourceCode: { + ...prevData.sourceCode, + value + } + })); + }; + + /** + * Handle combobox changes for agency and species. + */ + const handleSpeciesChage = (event: ComboBoxEvent) => { + const { selectedItem } = event; + const isInvalid = selectedItem === null; + setSeedlotFormData((prevData) => ({ + ...prevData, + species: { + ...prevData.species, + value: selectedItem?.code ? selectedItem : EmptyMultiOptObj, + isInvalid + } + })); + }; + + return ( + <> + + +

Seedlot information

+ +
+
+ + + { + vegCodeQuery.isFetching + ? + : ( + <> + filterInput({ item, inputValue }) + } + selectedItem={seedlotFormData.species.value} + placeholder={speciesFieldConfig.placeholder} + titleText={isEdit ? 'Seedlot species' : speciesFieldConfig.titleText} + onChange={(e: ComboBoxEvent) => handleSpeciesChage(e)} + invalid={seedlotFormData.species.isInvalid} + invalidText={speciesFieldConfig.invalidText} + helperText={vegCodeQuery.isError ? '' : speciesFieldConfig.helperText} + readOnly={isEdit} + /> + { + vegCodeQuery.isError + ? + : null + } + + ) + } + + + + + handleSource(e)} + > + { + seedlotSourcesQuery.isFetching + ? + : renderSources() + } + + + + + + handleBoolRadioGroup('willBeRegistered', checkedString === 'Yes')} + > + + + + + + + + handleBoolRadioGroup('isBcSource', checkedString === 'Yes')} + > + + + + + + + ); +}; + +export default SeedlotInformation; diff --git a/frontend/src/components/LotApplicantAndInfoForm/constants.ts b/frontend/src/components/LotApplicantAndInfoForm/constants.ts new file mode 100644 index 000000000..38aa53361 --- /dev/null +++ b/frontend/src/components/LotApplicantAndInfoForm/constants.ts @@ -0,0 +1,30 @@ +import { EmptyMultiOptObj } from '../../shared-constants/shared-constants'; +import AgencyTextPropsType from '../../types/AgencyTextPropsType'; +import { OptionsInputType, StringInputType } from '../../types/FormInputType'; +import { ComboBoxPropsType } from './definitions'; + +export const agencyFieldsProp: AgencyTextPropsType = { + useDefaultCheckbox: { + name: '', + labelText: '' + }, + agencyInput: { + titleText: 'Applicant agency name', + invalidText: 'Please select an agency' + }, + locationCode: { + name: 'seedlotCreationLocationCode', + labelText: 'Applicant agency number' + } +}; + +export const speciesFieldConfig: ComboBoxPropsType = { + placeholder: 'Enter or choose an species for the seedlot', + titleText: 'Type or search for the seedlot species using the drop-down list', + invalidText: 'Please select a species', + helperText: '' +}; + +// Template data for vegLot: +export const vegLotAgency: OptionsInputType = { id: '', isInvalid: false, value: EmptyMultiOptObj }; +export const vegLotLocationCode: StringInputType = { id: '', isInvalid: false, value: '' }; diff --git a/frontend/src/components/LotApplicantAndInfoForm/definitions.ts b/frontend/src/components/LotApplicantAndInfoForm/definitions.ts new file mode 100644 index 000000000..32da46125 --- /dev/null +++ b/frontend/src/components/LotApplicantAndInfoForm/definitions.ts @@ -0,0 +1,24 @@ +import React from 'react'; + +import { SeedlotRegFormType } from '../../types/SeedlotRegistrationTypes'; + +export type ComboBoxPropsType = { + placeholder: string; + titleText: string; + invalidText: string; + helperText: string; +} + +export type FormProps = { + isSeedlot: boolean, // If it's not a seedlot then it's veglot + isEdit: boolean + isBClass?: boolean, + seedlotFormData?: SeedlotRegFormType, + setSeedlotFormData?: React.Dispatch> +} + +export type SeedlotInformationProps = { + seedlotFormData: SeedlotRegFormType, + setSeedlotFormData: React.Dispatch>, + isEdit: boolean +} diff --git a/frontend/src/components/LotApplicantAndInfoForm/index.tsx b/frontend/src/components/LotApplicantAndInfoForm/index.tsx new file mode 100644 index 000000000..1efb3d925 --- /dev/null +++ b/frontend/src/components/LotApplicantAndInfoForm/index.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { useQuery } from '@tanstack/react-query'; + +import { + Row, + Column, + TextInput, + FlexGrid +} from '@carbon/react'; +import validator from 'validator'; + +import Subtitle from '../Subtitle'; +import ApplicantAgencyFields from '../ApplicantAgencyFields'; +import getApplicantAgenciesOptions from '../../api-service/applicantAgenciesAPI'; +import { BooleanInputType, OptionsInputType, StringInputType } from '../../types/FormInputType'; + +import SeedlotInformation from './SeedlotInformation'; +import { FormProps } from './definitions'; +import { + vegLotAgency, + vegLotLocationCode, + agencyFieldsProp +} from './constants'; + +import './styles.scss'; + +/** + * This component displays a form for seedlot or veglot creation/edition. + */ +const LotApplicantAndInfoForm = ({ + isSeedlot, + isEdit, + seedlotFormData, + setSeedlotFormData +}: FormProps) => { + const applicantAgencyQuery = useQuery({ + queryKey: ['applicant-agencies'], + enabled: !isEdit, + queryFn: () => getApplicantAgenciesOptions() + }); + + const handleEmail = (value: string) => { + const isEmailInvalid = !validator.isEmail(value); + if (isSeedlot && setSeedlotFormData) { + setSeedlotFormData((prevData) => ({ + ...prevData, + email: { + ...prevData.email, + value, + isInvalid: isEmailInvalid + } + })); + } + }; + + const setAgencyAndCode = (agency: OptionsInputType, locationCode: StringInputType) => { + if (isSeedlot && setSeedlotFormData) { + setSeedlotFormData((prevData) => ({ + ...prevData, + client: agency, + locationCode + })); + } + }; + + const renderSeedlotForm = () => { + if (isSeedlot && seedlotFormData && setSeedlotFormData) { + return ( + + ); + } + return null; + }; + + return ( + + + +

Applicant agency

+ +
+
+ setAgencyAndCode(agency, locationCode) + } + readOnly={isEdit} + maxInputColSize={6} + /> + + + ) => handleEmail(e.target.value)} + defaultValue={isEdit && seedlotFormData ? seedlotFormData.email.value : ''} + /> + + + + { + isSeedlot + ? renderSeedlotForm() + : null // The false case is reserved for vegLog + } +
+ ); +}; + +export default LotApplicantAndInfoForm; diff --git a/frontend/src/components/LotApplicantAndInfoForm/styles.scss b/frontend/src/components/LotApplicantAndInfoForm/styles.scss new file mode 100644 index 000000000..8f26ecd53 --- /dev/null +++ b/frontend/src/components/LotApplicantAndInfoForm/styles.scss @@ -0,0 +1,45 @@ +@use '@carbon/type'; +@use '@bcgov-nr/nr-theme/design-tokens/variables.scss' as vars; + +.applicant-information-form { + padding: 0; + + h2 { + @include type.type-style('heading-03'); + margin-bottom: 0.5rem; + } + + .section-title { + margin-bottom: 2.5rem; + } + + .form-row { + margin-bottom: 2rem; + } + + .agency-email-row{ + margin-bottom: 3rem; + } +} + +.applicant-info-combobox { + ::placeholder { + color: var(--#{vars.$bcgov-prefix}-text-primary); + } +} + +.agency-number-wrapper-class { + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + input[type=number] { + -moz-appearance: textfield; + } + + #agency-number-input { + @include type.type-style('code-02'); + } +} diff --git a/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/constants.ts b/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/constants.ts index 1ceac5961..fefdeb4fb 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/constants.ts +++ b/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/constants.ts @@ -10,8 +10,7 @@ export const agencyFieldsProps: AgencyTextPropsType = { labelText: 'Use applicant agency as collector agency' }, agencyInput: { - name: 'collectorAgency', - labelText: 'Cone Collector agency', + titleText: 'Cone Collector agency', invalidText: 'Please choose a valid collector agency, filter with agency number, name or acronym' }, locationCode: { diff --git a/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/definitions.ts b/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/definitions.ts index 39398003e..08577cdf9 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/definitions.ts +++ b/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/definitions.ts @@ -1,31 +1,32 @@ -import { FormInputType } from '../../../types/FormInputType'; +import { + BooleanInputType, + OptionsInputType, + StringArrInputType, + StringInputType +} from '../../../types/FormInputType'; import MultiOptionsObj from '../../../types/MultiOptionsObject'; -import { FormInvalidationObj } from '../../../views/Seedlot/SeedlotRegistrationForm/definitions'; export type CollectionForm = { - useDefaultAgencyInfo: FormInputType & { - value: boolean - }, - collectorAgency: FormInputType & { value: string }, - locationCode: FormInputType & { value: string }, - startDate: FormInputType & { value: string }, - endDate: FormInputType & { value: string }, - numberOfContainers: FormInputType & { value: string }, - volumePerContainers: FormInputType & { value: string }, - volumeOfCones: FormInputType & { value: string }, - selectedCollectionCodes: FormInputType & { value: string[] }, - comments: FormInputType & { value: string } + useDefaultAgencyInfo: BooleanInputType, + collectorAgency: OptionsInputType, + locationCode: StringInputType, + startDate: StringInputType, + endDate: StringInputType, + numberOfContainers: StringInputType, + volumePerContainers: StringInputType, + volumeOfCones: StringInputType, + selectedCollectionCodes: StringArrInputType, + comments: StringInputType } export interface CollectionStepProps { state: CollectionForm, setStepData: Function, - defaultAgency: string, + defaultAgency: MultiOptionsObj, defaultCode: string, agencyOptions: Array, collectionMethods: Array, readOnly?: boolean, - invalidateObj?: FormInvalidationObj } export type FormValidation = { diff --git a/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/index.tsx b/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/index.tsx index 8006d375d..3698186ea 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/index.tsx +++ b/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { FlexGrid, Column, @@ -16,7 +16,7 @@ import validator from 'validator'; import Subtitle from '../../Subtitle'; import ApplicantAgencyFields from '../../ApplicantAgencyFields'; -import { FormInputType } from '../../../types/FormInputType'; +import { BooleanInputType, OptionsInputType, StringInputType } from '../../../types/FormInputType'; import { DATE_FORMAT, MOMENT_DATE_FORMAT, agencyFieldsProps, fieldsConfig @@ -42,29 +42,18 @@ const CollectionStep = ( ) => { const [isCalcWrong, setIsCalcWrong] = useState(false); - const setAgencyInfo = ( - valueAgency: FormInputType & { value: string }, - valueLocation: FormInputType & { value: string }, - valueUseDefault: FormInputType & { value: boolean } + const setAgencyAndCode = ( + isDefault: BooleanInputType, + agency: OptionsInputType, + locationCode: StringInputType ) => { const clonedState = structuredClone(state); - clonedState.collectorAgency = valueAgency; - clonedState.locationCode = valueLocation; - clonedState.useDefaultAgencyInfo = valueUseDefault; + clonedState.useDefaultAgencyInfo = isDefault; + clonedState.collectorAgency = agency; + clonedState.locationCode = locationCode; setStepData(clonedState); }; - useEffect(() => { - const useDefault = state.useDefaultAgencyInfo.value; - const agencyValue = useDefault ? defaultAgency : state.collectorAgency.value; - const codeValue = useDefault ? defaultCode : state.locationCode.value; - - const clonedState = structuredClone(state); - clonedState.collectorAgency.value = agencyValue; - clonedState.locationCode.value = codeValue; - setStepData(clonedState); - }, [defaultAgency, defaultCode]); - const handleDateChange = (isStartDate: boolean, value: string) => { const clonedState = structuredClone(state); const dateType: keyof CollectionForm = isStartDate ? 'startDate' : 'endDate'; @@ -141,21 +130,24 @@ const CollectionStep = ( setAgencyInfo(agencyData, locationCodeData, useDefaultData) + isDefault: BooleanInputType, + agency: OptionsInputType, + locationCode: StringInputType + ) => setAgencyAndCode(isDefault, agency, locationCode) } readOnly={readOnly} + maxInputColSize={6} /> diff --git a/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/utils.ts b/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/utils.ts index 7883d416f..d2c284d3b 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/utils.ts +++ b/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/utils.ts @@ -13,8 +13,3 @@ export const calcVolume = (numOfContainer: string, volPerContainer: string) => ( export const isNumNotInRange = (value: string): boolean => ( !((Number(value) > 0) && (Number(value) < MAX_INPUT_DECIMAL)) ); - -/** - * Format single digit location code to have a leading 0. - */ -export const formatLocationCode = (value: string) => (value.length > 1 ? value : value.padStart(2, '0')); diff --git a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/SingleOwnerInfo/index.tsx b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/SingleOwnerInfo/index.tsx index 050e70de0..6280ba141 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/SingleOwnerInfo/index.tsx +++ b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/SingleOwnerInfo/index.tsx @@ -10,7 +10,7 @@ import { import { TrashCan } from '@carbon/icons-react'; import ApplicantAgencyFields from '../../../ApplicantAgencyFields'; -import { FormInputType } from '../../../../types/FormInputType'; +import { BooleanInputType, OptionsInputType, StringInputType } from '../../../../types/FormInputType'; import MultiOptionsObj from '../../../../types/MultiOptionsObject'; import ComboBoxEvent from '../../../../types/ComboBoxEvent'; @@ -29,18 +29,17 @@ interface SingleOwnerInfoProps { ownerInfo: SingleOwnerForm, deleteAnOwner: Function, agencyOptions: Array, - defaultAgency: string, + defaultAgency: MultiOptionsObj, defaultCode: string, fundingSources: Array, methodsOfPayment: Array, - addRefs: Function, checkPortionSum: Function, setState: Function, readOnly?: boolean } const SingleOwnerInfo = ({ - addRefs, ownerInfo, agencyOptions, defaultAgency, defaultCode, fundingSources, + ownerInfo, agencyOptions, defaultAgency, defaultCode, fundingSources, methodsOfPayment, deleteAnOwner, checkPortionSum, setState, readOnly }: SingleOwnerInfoProps) => { const [ownerPortionInvalidText, setOwnerPortionInvalidText] = useState( @@ -51,15 +50,15 @@ const SingleOwnerInfo = ({ const colsClass = ownerInfo.id === DEFAULT_INDEX ? 'default-owner-col' : 'other-owners-col'; - const setAgencyInfo = ( - agencyData: FormInputType & { value: string }, - locationCodeData: FormInputType & { value: string }, - useDefaultData: FormInputType & { value: boolean } + const setAgencyAndCode = ( + isDefault: BooleanInputType, + agency: OptionsInputType, + locationCode: StringInputType ) => { const clonedState = structuredClone(ownerInfo); - clonedState.ownerAgency = agencyData; - clonedState.ownerCode = locationCodeData; - clonedState.useDefaultAgencyInfo = useDefaultData; + clonedState.ownerAgency = agency; + clonedState.ownerCode = locationCode; + clonedState.useDefaultAgencyInfo = isDefault; setState(clonedState, ownerInfo.id); }; @@ -138,29 +137,28 @@ const SingleOwnerInfo = ({ setAgencyInfo(agencyData, locationCodeData, useDefaultData) + isDefault: BooleanInputType, + agency: OptionsInputType, + locationCode: StringInputType + ) => setAgencyAndCode(isDefault, agency, locationCode) } - showDefaultCheckbox={ownerInfo.id === DEFAULT_INDEX} - inputsColSize={8} - readOnly={readOnly} + showCheckbox={ownerInfo.id === DEFAULT_INDEX} + readOnly={readOnly ?? false} /> addRefs(el, 'ownerPortion')} name="ownerPortion" label={inputText.portion.label} value={ownerInfo.ownerPortion.value} @@ -196,7 +194,6 @@ const SingleOwnerInfo = ({
addRefs(el, 'reservedPerc')} name="reservedPerc" label={inputText.reserved.label} value={ownerInfo.reservedPerc.value} @@ -226,7 +223,6 @@ const SingleOwnerInfo = ({
addRefs(el, 'surplusPerc')} name="surplusPerc" label={inputText.surplus.label} value={ownerInfo.surplusPerc.value} @@ -261,7 +257,6 @@ const SingleOwnerInfo = ({ addRefs(el, 'fundingSource')} name="fundingSource" items={fundingSources} selectedItem={ownerInfo.fundingSource.value} @@ -281,7 +276,6 @@ const SingleOwnerInfo = ({ addRefs(el, 'methodOfPayment')} name="methodOfPayment" items={methodsOfPayment} selectedItem={ownerInfo.methodOfPayment.value} diff --git a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/constants.ts b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/constants.ts index 3a698aabd..7ed01362d 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/constants.ts +++ b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/constants.ts @@ -1,6 +1,5 @@ -import { emptyMultiOptObj } from '../../../shared-constants/shared-constants'; +import { EmptyMultiOptObj } from '../../../shared-constants/shared-constants'; import AgencyTextPropsType from '../../../types/AgencyTextPropsType'; -import { FormInvalidationObj } from '../../../views/Seedlot/SeedlotRegistrationForm/definitions'; import { SingleOwnerForm } from './definitions'; export const DEFAULT_INDEX = 0; @@ -13,8 +12,7 @@ export const agencyFieldsProps: AgencyTextPropsType = { labelText: 'Use applicant agency as owner agency' }, agencyInput: { - name: 'ownerAgency', - labelText: 'Owner agency', + titleText: 'Owner agency', invalidText: 'Please choose a valid owner agency, filter with agency number, name or acronym' }, locationCode: { @@ -53,12 +51,12 @@ export const createOwnerTemplate = (newId: number): SingleOwnerForm => ({ id: newId, useDefaultAgencyInfo: { id: 'ownership-use-default-agency', - value: true, + value: newId === DEFAULT_INDEX, isInvalid: false }, ownerAgency: { id: `ownership-agency-${newId}`, - value: '', + value: EmptyMultiOptObj, isInvalid: false }, ownerCode: { @@ -83,43 +81,12 @@ export const createOwnerTemplate = (newId: number): SingleOwnerForm => ({ }, fundingSource: { id: `ownership-funding-source-${newId}`, - value: emptyMultiOptObj, + value: EmptyMultiOptObj, isInvalid: false }, methodOfPayment: { id: `ownership-method-payment-${newId}`, - value: emptyMultiOptObj, + value: EmptyMultiOptObj, isInvalid: false } }); - -export const validTemplate: FormInvalidationObj = { - owner: { - isInvalid: false, - invalidText: agencyFieldsProps.agencyInput.invalidText - }, - code: { - isInvalid: false, - invalidText: '' - }, - portion: { - isInvalid: false, - invalidText: inputText.portion.invalidText - }, - reserved: { - isInvalid: false, - invalidText: '' - }, - surplus: { - isInvalid: false, - invalidText: '' - }, - funding: { - isInvalid: false, - invalidText: inputText.funding.invalidText - }, - payment: { - isInvalid: false, - invalidText: inputText.payment.invalidText - } -}; diff --git a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/definitions.ts b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/definitions.ts index 1b1eccd37..bd1a6ae8c 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/definitions.ts +++ b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/definitions.ts @@ -1,7 +1,6 @@ import React from 'react'; import MultiOptionsObj from '../../../types/MultiOptionsObject'; -import { OwnershipInvalidObj } from '../../../views/Seedlot/SeedlotRegistrationForm/definitions'; -import { FormInputType } from '../../../types/FormInputType'; +import { BooleanInputType, OptionsInputType, StringInputType } from '../../../types/FormInputType'; export type AccordionItemHeadClick = { isOpen: boolean, @@ -13,14 +12,14 @@ export type AccordionCtrlObj = { export type SingleOwnerForm = { id: number, - useDefaultAgencyInfo: FormInputType & { value: boolean }, - ownerAgency: FormInputType & { value: string }, - ownerCode: FormInputType & { value: string }, - ownerPortion: FormInputType & { value: string }, - reservedPerc: FormInputType & { value: string }, - surplusPerc: FormInputType & { value: string }, - fundingSource: FormInputType & { value: MultiOptionsObj }, - methodOfPayment: FormInputType & { value: MultiOptionsObj } + useDefaultAgencyInfo: BooleanInputType, + ownerAgency: OptionsInputType, + ownerCode: StringInputType, + ownerPortion: StringInputType, + reservedPerc: StringInputType, + surplusPerc: StringInputType, + fundingSource: OptionsInputType, + methodOfPayment: OptionsInputType } export type SingleInvalidObj = { @@ -28,19 +27,8 @@ export type SingleInvalidObj = { invalidText: string, } -export type ValidationPropNoId = { - owner: SingleInvalidObj, - code: SingleInvalidObj, - portion: SingleInvalidObj, - reserved: SingleInvalidObj, - surplus: SingleInvalidObj, - funding: SingleInvalidObj, - payment: SingleInvalidObj -} - export type StateReturnObj = { newOwnerArr: Array, - newValidObj: OwnershipInvalidObj, newId?: number } @@ -50,12 +38,11 @@ export type NumStepperVal = { } export interface OwnershipStepProps { - defaultAgency: string + defaultAgency: MultiOptionsObj defaultCode: string, agencyOptions: Array, state: Array, setStepData: Function, - invalidState: OwnershipInvalidObj, readOnly?: boolean, fundingSources: Array, methodsOfPayment: Array diff --git a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/index.tsx b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/index.tsx index b800dbba9..425765972 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/index.tsx +++ b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef } from 'react'; import { Accordion, AccordionItem, @@ -19,14 +19,10 @@ import { import { insertOwnerForm, deleteOwnerForm, - getAgencyName, formatPortionPerc, arePortionsValid } from './utils'; -import { - DEFAULT_INDEX, - MAX_OWNERS -} from './constants'; +import { MAX_OWNERS } from './constants'; import './styles.scss'; @@ -37,7 +33,6 @@ const OwnershipStep = ( { state, setStepData, - invalidState, defaultCode, defaultAgency, agencyOptions, @@ -70,32 +65,12 @@ const OwnershipStep = ( const deleteAnOwner = (id: number) => { const { newOwnerArr - }: StateReturnObj = deleteOwnerForm(id, state, invalidState); + }: StateReturnObj = deleteOwnerForm(id, state); delete refControl.current[id]; const portionsInvalid = !arePortionsValid(newOwnerArr); setPortionsValid(newOwnerArr, portionsInvalid); }; - useEffect(() => { - const useDefault = state[DEFAULT_INDEX].useDefaultAgencyInfo.value; - const agencyValue = useDefault ? defaultAgency : state[DEFAULT_INDEX].ownerAgency.value; - const codeValue = useDefault ? defaultCode : state[DEFAULT_INDEX].ownerCode.value; - - const clonedState = structuredClone(state); - clonedState[DEFAULT_INDEX].ownerAgency.value = agencyValue; - clonedState[DEFAULT_INDEX].ownerCode.value = codeValue; - setStepData(clonedState); - }, [defaultAgency, defaultCode]); - - const addRefs = (element: HTMLInputElement, id: number, name: string) => { - if (element !== null) { - refControl.current[id] = { - ...refControl.current[id], - [name]: element - }; - } - }; - const toggleAccordion = (id: number, isOpen: boolean) => { const newAccCtrls = { ...accordionControls }; newAccCtrls[id] = isOpen; @@ -144,9 +119,9 @@ const OwnershipStep = ( } title={( )} @@ -158,9 +133,6 @@ const OwnershipStep = ( defaultCode={defaultCode} fundingSources={fundingSources} methodsOfPayment={methodsOfPayment} - addRefs={(element: HTMLInputElement, name: string) => { - addRefs(element, singleOwnerInfo.id, name); - }} deleteAnOwner={(id: number) => deleteAnOwner(id)} setState={(singleState: SingleOwnerForm, id: number) => { const arrayClone = structuredClone(state); diff --git a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/utils.ts b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/utils.ts index a97782e18..8d136e6ae 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/utils.ts +++ b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/utils.ts @@ -1,5 +1,4 @@ import MultiOptionsObj from '../../../types/MultiOptionsObject'; -import { OwnershipInvalidObj } from '../../../views/Seedlot/SeedlotRegistrationForm/definitions'; import { inputText, createOwnerTemplate } from './constants'; import { @@ -35,37 +34,19 @@ export const insertOwnerForm = ( export const deleteOwnerForm = ( id: number, - ownershiptArray: Array, - validationObj: OwnershipInvalidObj + ownershiptArray: Array ) => { if (id === 0) { return { - newOwnerArr: ownershiptArray, - newValidObj: validationObj + newOwnerArr: ownershiptArray }; } const newOwnerArray = ownershiptArray.filter((obj) => obj.id !== id); - const newValidObj = { ...validationObj }; - delete newValidObj[id]; return { - newOwnerArr: newOwnerArray, - newValidObj + newOwnerArr: newOwnerArray }; }; -// Assume the fullString is in the form of '0032 - Strong Seeds Orchard - SSO' -// Returns the middle string, e.g. 'Strong Seeds Orchard' -export const getAgencyName = (fullString: string | null): string => { - if (fullString === null || !fullString.includes('-')) { - return 'Owner agency name'; - } - const splitArr = fullString.split(' - '); - if (splitArr.length === 3) { - return splitArr[1]; - } - return ''; -}; - export const formatPortionPerc = (value: string): string => { if (value === null || value === '' || Number(value) === 0) { return '--'; diff --git a/frontend/src/shared-constants/shared-constants.ts b/frontend/src/shared-constants/shared-constants.ts index aa50f6670..6beeb444d 100644 --- a/frontend/src/shared-constants/shared-constants.ts +++ b/frontend/src/shared-constants/shared-constants.ts @@ -2,7 +2,7 @@ import MultiOptionsObj from '../types/MultiOptionsObject'; export const LOCATION_CODE_LIMIT = 2; export const SPAR_REDIRECT_PATH = 'SPAR-REDIRECT-PATH'; -export const emptyMultiOptObj: MultiOptionsObj = { +export const EmptyMultiOptObj: MultiOptionsObj = { code: '', label: '', description: '' diff --git a/frontend/src/styles/custom.scss b/frontend/src/styles/custom.scss index 99dc542b8..746e1e80a 100644 --- a/frontend/src/styles/custom.scss +++ b/frontend/src/styles/custom.scss @@ -1,4 +1,5 @@ @use 'sass:map'; +@use '@bcgov-nr/nr-theme/design-tokens/variables.scss' as vars; @use '@carbon/themes/scss/utilities'; @use '@bcgov-nr/nr-theme/design-tokens/light-theme.scss' as light; @use '@bcgov-nr/nr-theme/design-tokens/dark-theme.scss' as dark; @@ -357,3 +358,22 @@ border-bottom: none; } } + +.spar-read-only-combobox { + input { + border: none; + } + + .#{vars.$bcgov-prefix}--list-box__field .#{vars.$bcgov-prefix}--text-input { + padding: 0; + border: none; + } + + .#{vars.$bcgov-prefix}--list-box { + border: none; + } + + button { + display: none; + } +} diff --git a/frontend/src/types/AgencyTextPropsType.ts b/frontend/src/types/AgencyTextPropsType.ts index 84e9e0224..637692538 100644 --- a/frontend/src/types/AgencyTextPropsType.ts +++ b/frontend/src/types/AgencyTextPropsType.ts @@ -4,8 +4,7 @@ type AgencyTextPropsType = { labelText: string }, agencyInput: { - name: string, - labelText: string, + titleText: string, invalidText: string }, locationCode: { diff --git a/frontend/src/types/FormInputType.ts b/frontend/src/types/FormInputType.ts index e57175a5d..2a00e18cd 100644 --- a/frontend/src/types/FormInputType.ts +++ b/frontend/src/types/FormInputType.ts @@ -1,4 +1,16 @@ +import MultiOptionsObj from './MultiOptionsObject'; + export type FormInputType = { id: string; isInvalid: boolean; }; + +export type OptionsInputType = FormInputType & { value: MultiOptionsObj }; + +export type BooleanInputType = FormInputType & { value: boolean }; + +export type StringInputType = FormInputType & { value: string }; + +export type StringArrInputType = FormInputType & { value: string[] }; + +export type NumberInputType = FormInputType & { value: number }; diff --git a/frontend/src/types/SeedlotRegistrationTypes.ts b/frontend/src/types/SeedlotRegistrationTypes.ts index 7a140307c..ebb7c7b05 100644 --- a/frontend/src/types/SeedlotRegistrationTypes.ts +++ b/frontend/src/types/SeedlotRegistrationTypes.ts @@ -1,17 +1,16 @@ -import { FormInputType } from './FormInputType'; -import MultiOptionsObj from './MultiOptionsObject'; +import { BooleanInputType, OptionsInputType, StringInputType } from './FormInputType'; /** * The form data obj used in seedlot creation. */ export type SeedlotRegFormType = { - client: FormInputType & { value: MultiOptionsObj }; - locationCode: FormInputType & { value: string }; - email: FormInputType & { value: string }; - species: FormInputType & { value: MultiOptionsObj }; - sourceCode: FormInputType & { value: string }; - willBeRegistered: FormInputType & { value: boolean }; - isBcSource: FormInputType & { value: boolean }; + client: OptionsInputType; + locationCode: StringInputType; + email: StringInputType; + species: OptionsInputType; + sourceCode: StringInputType; + willBeRegistered: BooleanInputType; + isBcSource: BooleanInputType; }; /** @@ -27,3 +26,10 @@ export type SeedlotRegPayloadType = { bcSourceInd: boolean; geneticClassCode: 'A' | 'B'; } + +export type SeedlotPatchPayloadType = { + applicantEmailAddress: string, + seedlotSourceCode: string, + toBeRegistrdInd: boolean, + bcSourceInd: boolean +} diff --git a/frontend/src/types/SeedlotType.ts b/frontend/src/types/SeedlotType.ts index 58cb8bb18..9c00dbf01 100644 --- a/frontend/src/types/SeedlotType.ts +++ b/frontend/src/types/SeedlotType.ts @@ -58,6 +58,7 @@ export type SeedlotType = { vegetationCode: string, geneticClass: GeneticClass, seedlotSource: { + seedlotSourceCode: string, description: string, isDefault: boolean }, @@ -113,3 +114,8 @@ export type SeedlotsReturnType = { seedlots: SeedlotType[], totalCount: number } + +export type SeedlotCreateResponseType = { + seedlotNumber: string, + seedlotStatusCode: string +} diff --git a/frontend/src/utils/ForestClientUtils.ts b/frontend/src/utils/ForestClientUtils.ts new file mode 100644 index 000000000..44ecda83c --- /dev/null +++ b/frontend/src/utils/ForestClientUtils.ts @@ -0,0 +1,19 @@ +import { ForestClientType } from '../types/ForestClientType'; +import { OptionsInputType } from '../types/FormInputType'; +import MultiOptionsObj from '../types/MultiOptionsObject'; +import { getOptionsInputObj } from './FormInputUtils'; + +export const getForestClientLabel = (client: ForestClientType) => ( + `${client.clientNumber} - ${client.clientName} - ${client.acronym}` +); + +export const getForestClientOption = (client: ForestClientType): MultiOptionsObj => ({ + code: client.clientNumber, + description: client.clientName, + label: getForestClientLabel(client) +}); + +export const getForestClientOptionInput = ( + id: string, + client: ForestClientType +): OptionsInputType => (getOptionsInputObj(id, getForestClientOption(client))); diff --git a/frontend/src/utils/FormInputUtils.ts b/frontend/src/utils/FormInputUtils.ts new file mode 100644 index 000000000..978a26a75 --- /dev/null +++ b/frontend/src/utils/FormInputUtils.ts @@ -0,0 +1,40 @@ +import MultiOptionsObj from '../types/MultiOptionsObject'; +import { + BooleanInputType, + NumberInputType, + OptionsInputType, + StringArrInputType, + StringInputType +} from '../types/FormInputType'; + +const isInvalid: boolean = false; + +export const getOptionsInputObj = (id: string, value: MultiOptionsObj): OptionsInputType => ({ + id, + isInvalid, + value +}); + +export const getBooleanInputObj = (id: string, value: boolean): BooleanInputType => ({ + id, + isInvalid, + value +}); + +export const getStringInputObj = (id: string, value: string): StringInputType => ({ + id, + isInvalid, + value +}); + +export const getStringArrInputObj = (id: string, value: string[]): StringArrInputType => ({ + id, + isInvalid, + value +}); + +export const getNumberInputObj = (id: string, value: number): NumberInputType => ({ + id, + isInvalid, + value +}); diff --git a/frontend/src/utils/MultiOptionsUtils.ts b/frontend/src/utils/MultiOptionsUtils.ts index 5fc3f6b05..dceecb53a 100644 --- a/frontend/src/utils/MultiOptionsUtils.ts +++ b/frontend/src/utils/MultiOptionsUtils.ts @@ -1,10 +1,7 @@ +import { EmptyMultiOptObj } from '../shared-constants/shared-constants'; import MultiOptionsObj from '../types/MultiOptionsObject'; -const multiOptionsItem: MultiOptionsObj = { - label: '', - code: '', - description: '' -}; +const multiOptionsItem: MultiOptionsObj = EmptyMultiOptObj; const capFirstChar = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); const uncapFirstChar = (str: string) => str.charAt(0).toLowerCase() + str.slice(1); diff --git a/frontend/src/utils/SeedlotUtils.ts b/frontend/src/utils/SeedlotUtils.ts index bd443a442..c579d5f9b 100644 --- a/frontend/src/utils/SeedlotUtils.ts +++ b/frontend/src/utils/SeedlotUtils.ts @@ -4,25 +4,37 @@ import { SeedlotApplicantType, SeedlotDisplayType, SeedlotType } from '../types/ import { MONTH_DAY_YEAR } from '../config/DateFormat'; import { ForestClientType } from '../types/ForestClientType'; import MultiOptionsObj from '../types/MultiOptionsObject'; +import { EmptyMultiOptObj } from '../shared-constants/shared-constants'; /** * Generate a species label in the form of `{code} - {description}`. */ -const getSpeciesNameByCode = (code: string, vegCodeData: MultiOptionsObj[]): string => { +export const getSpeciesLabelByCode = (code: string, vegCodeData: MultiOptionsObj[]): string => { const filtered = vegCodeData.filter((veg) => veg.code === code); if (filtered.length) { - return filtered[0].description; + return filtered[0].label; } return code; }; +/** + * Finds a species MultiOptionsObj by code. + */ +export const getSpeciesOptionByCode = ( + code: string, + vegCodeData: MultiOptionsObj[] +): MultiOptionsObj => { + const filtered = vegCodeData.filter((veg) => veg.code === code); + return filtered[0] ?? EmptyMultiOptObj; +}; + /** * Covert the raw seedlot data into an object to be displayed on seedlot detail page. */ export const covertRawToDisplayObj = (seedlot: SeedlotType, vegCodeData: MultiOptionsObj[]) => ({ seedlotNumber: seedlot.id, seedlotClass: `${seedlot.geneticClass.geneticClassCode}-class`, - seedlotSpecies: getSpeciesNameByCode(seedlot.vegetationCode, vegCodeData), + seedlotSpecies: getSpeciesLabelByCode(seedlot.vegetationCode, vegCodeData), seedlotStatus: seedlot.seedlotStatus.description, createdAt: luxon.fromISO(seedlot.auditInformation.entryTimestamp).toFormat(MONTH_DAY_YEAR), lastUpdatedAt: luxon.fromISO(seedlot.auditInformation.updateTimestamp) @@ -69,7 +81,7 @@ export const convertToApplicantInfoObj = ( agency: `${forestClient.clientNumber} - ${forestClient.clientName} - ${forestClient.acronym}`, locationCode: seedlot.applicantLocationCode, email: seedlot.applicantEmailAddress, - species: getSpeciesNameByCode(seedlot.vegetationCode, vegCodeData), + species: getSpeciesLabelByCode(seedlot.vegetationCode, vegCodeData), source: seedlot.seedlotSource.description, willRegister: seedlot.intendedForCrownLand, isBcSource: seedlot.sourceInBc diff --git a/frontend/src/views/Seedlot/CreateAClass/CreationForm/constants.ts b/frontend/src/views/Seedlot/CreateAClass/CreationForm/constants.ts deleted file mode 100644 index eee0393b2..000000000 --- a/frontend/src/views/Seedlot/CreateAClass/CreationForm/constants.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { SeedlotRegFormType } from '../../../../types/SeedlotRegistrationTypes'; -import ComboBoxPropsType from './definitions'; - -export const pageTexts = { - locCodeInput: { - helperTextDisabled: 'Please select an Applicant Agency before setting the agency number', - helperTextEnabled: '2-digit code that identifies the address of operated office or division', - invalidLocationValue: 'Please enter a valid value between 0 and 99', - invalidLocationForSelectedAgency: 'This agency number is not valid for the selected agency, please enter a valid one or change the agency', - cannotVerify: 'Cannot verify the location code at the moment' - } -}; - -export const applicantAgencyFieldConfig: ComboBoxPropsType = { - placeholder: 'Select an agency...', - titleText: 'Applicant agency name', - invalidText: 'Please select an agency', - helperText: 'You can enter your agency number, name or acronym' -}; - -export const speciesFieldConfig: ComboBoxPropsType = { - placeholder: 'Enter or choose an species for the seedlot', - titleText: 'Seedlot species', - invalidText: 'Please select a species', - helperText: '' -}; - -export const InitialSeedlotFormData: SeedlotRegFormType = { - client: { - id: 'applicant-info-combobox', - isInvalid: false, - value: { - code: '', - label: '', - description: '' - } - }, - locationCode: { - id: 'agency-number-input', - isInvalid: false, - value: '' - }, - email: { - id: 'appliccant-email-input', - isInvalid: false, - value: '' - }, - species: { - id: 'seedlot-species-combobox', - isInvalid: false, - value: { - code: '', - label: '', - description: '' - } - }, - sourceCode: { - id: '', - isInvalid: false, - value: '' - }, - willBeRegistered: { - id: '', - isInvalid: false, - value: true - }, - isBcSource: { - id: '', - isInvalid: false, - value: true - } -}; diff --git a/frontend/src/views/Seedlot/CreateAClass/CreationForm/definitions.ts b/frontend/src/views/Seedlot/CreateAClass/CreationForm/definitions.ts deleted file mode 100644 index d3d223c32..000000000 --- a/frontend/src/views/Seedlot/CreateAClass/CreationForm/definitions.ts +++ /dev/null @@ -1,8 +0,0 @@ -type ComboBoxPropsType = { - placeholder: string; - titleText: string; - invalidText: string; - helperText: string; -} - -export default ComboBoxPropsType; diff --git a/frontend/src/views/Seedlot/CreateAClass/CreationForm/index.tsx b/frontend/src/views/Seedlot/CreateAClass/CreationForm/index.tsx deleted file mode 100644 index 8708b1065..000000000 --- a/frontend/src/views/Seedlot/CreateAClass/CreationForm/index.tsx +++ /dev/null @@ -1,503 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useQuery, UseQueryResult, useMutation } from '@tanstack/react-query'; - -import { - Row, - Column, - TextInput, - RadioButtonGroup, - RadioButton, - Checkbox, - CheckboxGroup, - Button, - ComboBox, - TextInputSkeleton, - InlineLoading, - RadioButtonSkeleton, - ActionableNotification -} from '@carbon/react'; -import { DocumentAdd } from '@carbon/icons-react'; -import validator from 'validator'; -import { toast } from 'react-toastify'; -import { AxiosError } from 'axios'; - -import Subtitle from '../../../../components/Subtitle'; -import InputErrorText from '../../../../components/InputErrorText'; - -import { ErrToastOption } from '../../../../config/ToastifyConfig'; -import { FilterObj, filterInput } from '../../../../utils/filterUtils'; -import focusById from '../../../../utils/FocusUtils'; -import { THREE_HALF_HOURS, THREE_HOURS } from '../../../../config/TimeUnits'; - -import { SeedlotRegFormType, SeedlotRegPayloadType } from '../../../../types/SeedlotRegistrationTypes'; -import SeedlotSourceType from '../../../../types/SeedlotSourceType'; -import ComboBoxEvent from '../../../../types/ComboBoxEvent'; - -import getVegCodes from '../../../../api-service/vegetationCodeAPI'; -import getApplicantAgenciesOptions from '../../../../api-service/applicantAgenciesAPI'; -import { getForestClientLocation } from '../../../../api-service/forestClientsAPI'; -import getSeedlotSources from '../../../../api-service/SeedlotSourcesAPI'; -import { postSeedlot } from '../../../../api-service/seedlotAPI'; -import { LOCATION_CODE_LIMIT } from '../../../../shared-constants/shared-constants'; -import ErrorToast from '../../../../components/Toast/ErrorToast'; - -import ComboBoxPropsType from './definitions'; -import { - applicantAgencyFieldConfig, - speciesFieldConfig, - pageTexts, - InitialSeedlotFormData -} from './constants'; -import { convertToPayload } from './utils'; - -import './styles.scss'; - -const CreationForm = () => { - const navigate = useNavigate(); - - const [formData, setFormData] = useState(InitialSeedlotFormData); - const [invalidLocationMessage, setInvalidLocationMessage] = useState(''); - const [locationCodeHelper, setLocationCodeHelper] = useState( - pageTexts.locCodeInput.helperTextDisabled - ); - - const setInputValidation = (inputName: keyof SeedlotRegFormType, isInvalid: boolean) => ( - setFormData((prevData) => ({ - ...prevData, - [inputName]: { - ...prevData[inputName], - isInvalid - } - })) - ); - - const updateAfterLocValidation = (isInvalid: boolean) => { - setInputValidation('locationCode', isInvalid); - setLocationCodeHelper(pageTexts.locCodeInput.helperTextEnabled); - }; - - const validateLocationCodeMutation = useMutation({ - mutationFn: (queryParams: string[]) => getForestClientLocation( - queryParams[0], // Client Number - queryParams[1] // Location Code - ), - onError: (err: AxiosError) => { - const errMsg = err.code === 'ERR_BAD_REQUEST' - ? pageTexts.locCodeInput.invalidLocationForSelectedAgency - : pageTexts.locCodeInput.cannotVerify; - setInvalidLocationMessage(errMsg); - updateAfterLocValidation(true); - }, - onSuccess: () => updateAfterLocValidation(false) - }); - - const applicantAgencyQuery = useQuery({ - queryKey: ['applicant-agencies'], - queryFn: () => getApplicantAgenciesOptions() - }); - - const vegCodeQuery = useQuery({ - queryKey: ['vegetation-codes'], - queryFn: () => getVegCodes(true), - staleTime: THREE_HOURS, // will not refetch for 3 hours - cacheTime: THREE_HALF_HOURS // data is cached 3.5 hours then deleted - }); - - const setDefaultSource = (sources: SeedlotSourceType[]) => { - sources.forEach((source) => { - if (source.isDefault) { - setFormData((prevData) => ({ - ...prevData, - sourceCode: { - ...prevData.sourceCode, - value: source.code - } - })); - } - }); - }; - - const seedlotSourcesQuery = useQuery({ - queryKey: ['seedlot-sources'], - queryFn: () => getSeedlotSources(), - onSuccess: (sources) => setDefaultSource(sources), - staleTime: THREE_HOURS, - cacheTime: THREE_HALF_HOURS - }); - - /** - * Default value is only set once upon query success, when cache data is used - * we will need to set the default again here. - */ - useEffect(() => { - if (seedlotSourcesQuery.isSuccess && !formData.sourceCode.value) { - setDefaultSource(seedlotSourcesQuery.data); - } - }, [seedlotSourcesQuery.isFetched]); - - const handleLocationCodeBlur = (clientNumber: string, locationCode: string) => { - const isInRange = validator.isInt(locationCode, { min: 0, max: 99 }); - // Padding 0 in front of single digit code - const formattedCode = (isInRange && locationCode.length === 1) - ? locationCode.padStart(2, '0') - : locationCode; - - if (isInRange) { - setFormData((prevResBody) => ({ - ...prevResBody, - locationCode: { - ...prevResBody.locationCode, - value: formattedCode, - isInvalid: false - } - })); - } - - if (!isInRange) { - setInvalidLocationMessage(pageTexts.locCodeInput.invalidLocationValue); - return; - } - - if (clientNumber && locationCode) { - validateLocationCodeMutation.mutate([clientNumber, formattedCode]); - } - }; - - /** - * Handle changes for location code. - */ - const handleLocationCode = ( - value: string - ) => { - setFormData((prevResBody) => ({ - ...prevResBody, - locationCode: { - ...prevResBody.locationCode, - value: value.slice(0, LOCATION_CODE_LIMIT) - } - })); - }; - - /** - * Handle combobox changes for agency and species. - */ - const handleComboBox = (event: ComboBoxEvent, isApplicantAgency: boolean) => { - const { selectedItem } = event; - const inputName: keyof SeedlotRegFormType = isApplicantAgency ? 'client' : 'species'; - const isInvalid = selectedItem === null; - setFormData((prevData) => ({ - ...prevData, - [inputName]: { - ...prevData[inputName], - value: selectedItem?.code ? selectedItem : { - code: '', - label: '', - description: '' - }, - isInvalid - } - })); - - if (isApplicantAgency && selectedItem?.code && formData.locationCode.value) { - validateLocationCodeMutation.mutate([selectedItem.code, formData.locationCode.value]); - } - }; - - const handleSource = (value: string) => { - setFormData((prevData) => ({ - ...prevData, - sourceCode: { - ...prevData.sourceCode, - value - } - })); - }; - - const handleEmail = (value: string) => { - const isEmailInvalid = !validator.isEmail(value); - setFormData((prevData) => ({ - ...prevData, - email: { - ...prevData.email, - value, - isInvalid: isEmailInvalid - } - })); - }; - - const handleCheckBox = (inputName: keyof SeedlotRegFormType, checked: boolean) => { - setFormData((prevData) => ({ - ...prevData, - [inputName]: { - ...prevData[inputName], - value: checked - } - })); - }; - - const seedlotMutation = useMutation({ - mutationFn: (payload: SeedlotRegPayloadType) => postSeedlot(payload), - onError: (err: AxiosError) => { - toast.error( - , - ErrToastOption - ); - }, - onSuccess: (res) => navigate({ - pathname: '/seedlots/creation-success', - search: `?seedlotNumber=${res.data.seedlotNumber}&seedlotClass=A` - }) - }); - - const renderSources = () => { - if (seedlotSourcesQuery.isSuccess) { - return seedlotSourcesQuery.data.map((source: SeedlotSourceType) => ( - - )); - } - return ; - }; - - const displayCombobox = ( - query: UseQueryResult, - propsValues: ComboBoxPropsType, - isApplicantComboBox = false - ) => ( - query.isFetching - ? ( - - - - ) - : ( - - {/* For now the default selected item will not be set, - we need the information from each user to set the - correct one */} - filterInput({ item, inputValue }) - } - placeholder={propsValues.placeholder} - titleText={propsValues.titleText} - onChange={(e: ComboBoxEvent) => handleComboBox(e, isApplicantComboBox)} - invalid={isApplicantComboBox ? formData.client.isInvalid : formData.species.isInvalid} - invalidText={propsValues.invalidText} - helperText={query.isError ? '' : propsValues.helperText} - disabled={query.isError} - /> - { - query.isError - ? - : null - } - - ) - ); - - const validateAndSubmit = (event: React.FormEvent) => { - event.preventDefault(); - - // Validate client - if (formData.client.isInvalid || !formData.client.value.code) { - setInputValidation('client', true); - focusById(formData.client.id); - return; - } - // Vaidate location code - if ( - formData.locationCode.isInvalid - || !formData.locationCode.value - || !validateLocationCodeMutation.isSuccess - ) { - setInputValidation('locationCode', true); - setInvalidLocationMessage(pageTexts.locCodeInput.invalidLocationValue); - focusById(formData.locationCode.id); - return; - } - // Validate email - if (formData.email.isInvalid || !formData.email.value) { - setInputValidation('email', true); - focusById(formData.email.id); - return; - } - // Validate species - if (formData.species.isInvalid || !formData.species.value.code) { - setInputValidation('species', true); - focusById(formData.species.id); - return; - } - // Source code, and the two booleans always have a default value so there's no need to check. - - // Submit Seedlot. - const payload = convertToPayload(formData); - seedlotMutation.mutate(payload); - }; - - return ( -
-
- { - seedlotMutation.isError - ? ( - - - false} - > - An error has occurred when trying to create your seedlot number. - Please try submiting it again later. - {' '} - {`${seedlotMutation.error.code}: ${seedlotMutation.error.message}`} - - - - ) - : null - } - - -

Applicant agency

- -
-
- - { - displayCombobox(applicantAgencyQuery, applicantAgencyFieldConfig, true) - } - - - ) => handleLocationCode(e.target.value) - } - onBlur={ - ( - e: React.ChangeEvent - ) => handleLocationCodeBlur(formData.client.value?.code, e.target.value) - } - onWheel={(e: React.ChangeEvent) => e.target.blur()} - helperText={locationCodeHelper} - /> - { - validateLocationCodeMutation.isLoading - ? - : null - } - - - - - ) => handleEmail(e.target.value)} - /> - - - - -

Seedlot information

- -
-
- - { - displayCombobox(vegCodeQuery, speciesFieldConfig) - } - - - - handleSource(e)} - > - { - seedlotSourcesQuery.isFetching - ? ( - - ) - : renderSources() - } - - - - - - - ) => handleCheckBox('willBeRegistered', e.target.checked) - } - /> - - - - - - - ) => handleCheckBox('isBcSource', e.target.checked) - } - /> - - - - - - - - -
-
- ); -}; - -export default CreationForm; diff --git a/frontend/src/views/Seedlot/CreateAClass/CreationForm/styles.scss b/frontend/src/views/Seedlot/CreateAClass/CreationForm/styles.scss deleted file mode 100644 index 9d71d911b..000000000 --- a/frontend/src/views/Seedlot/CreateAClass/CreationForm/styles.scss +++ /dev/null @@ -1,69 +0,0 @@ -@use '@carbon/type'; -@use '@bcgov-nr/nr-theme/design-tokens/variables.scss' as vars; - -.applicant-information-form { - margin-top: 2.75rem; - - .error-row { - margin-bottom: 2rem; - - button { - display: none; - } - - #create-seedlot-error-banner { - max-width: none; - border-radius: 0.25rem; - } - - .#{vars.$bcgov-prefix}--actionable-notification__content { - flex-direction: column; - } - } -} - -.applicant-information-form h2 { - @include type.type-style('heading-03'); - margin-bottom: 0.5rem; -} - -.applicant-agency-title, -.seedlot-information-title { - margin-bottom: 2.5rem; -} - -.agency-information, -.class-source-radio, -.registered-checkbox, -.seedlot-species-row { - margin-bottom: 2rem; -} - -.applicant-info-combobox { - ::placeholder { - color: var(--#{vars.$bcgov-prefix}-text-primary); - } -} - -.agency-email, -.collected-checkbox, -.save-button { - margin-bottom: 3rem; -} - -.agency-number-wrapper-class { - - input::-webkit-outer-spin-button, - input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - input[type=number] { - -moz-appearance: textfield; - } - - #agency-number-input { - @include type.type-style('code-02'); - } -} diff --git a/frontend/src/views/Seedlot/CreateAClass/constants.ts b/frontend/src/views/Seedlot/CreateAClass/constants.ts new file mode 100644 index 000000000..cefd7e8f9 --- /dev/null +++ b/frontend/src/views/Seedlot/CreateAClass/constants.ts @@ -0,0 +1,40 @@ +import { EmptyMultiOptObj } from '../../../shared-constants/shared-constants'; +import { SeedlotRegFormType } from '../../../types/SeedlotRegistrationTypes'; + +export const InitialSeedlotFormData: SeedlotRegFormType = { + client: { + id: 'applicant-info-combobox', + isInvalid: false, + value: EmptyMultiOptObj + }, + locationCode: { + id: 'agency-number-input', + isInvalid: false, + value: '' + }, + email: { + id: 'applicant-email-input', + isInvalid: false, + value: '' + }, + species: { + id: 'seedlot-species-combobox', + isInvalid: false, + value: EmptyMultiOptObj + }, + sourceCode: { + id: '', + isInvalid: false, + value: '' + }, + willBeRegistered: { + id: '', + isInvalid: false, + value: true + }, + isBcSource: { + id: '', + isInvalid: false, + value: true + } +}; diff --git a/frontend/src/views/Seedlot/CreateAClass/index.tsx b/frontend/src/views/Seedlot/CreateAClass/index.tsx index e484e0dad..058eb0f0f 100644 --- a/frontend/src/views/Seedlot/CreateAClass/index.tsx +++ b/frontend/src/views/Seedlot/CreateAClass/index.tsx @@ -1,37 +1,161 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { + ActionableNotification, + Button, FlexGrid, Row, - Stack, + Column, Breadcrumb, BreadcrumbItem } from '@carbon/react'; +import { DocumentAdd } from '@carbon/icons-react'; +import { toast } from 'react-toastify'; +import { useMutation } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; import PageTitle from '../../../components/PageTitle'; -import CreationForm from './CreationForm'; +import LotApplicantAndInfoForm from '../../../components/LotApplicantAndInfoForm'; +import { SeedlotRegFormType, SeedlotRegPayloadType } from '../../../types/SeedlotRegistrationTypes'; +import { postSeedlot } from '../../../api-service/seedlotAPI'; +import ErrorToast from '../../../components/Toast/ErrorToast'; +import { ErrToastOption } from '../../../config/ToastifyConfig'; +import focusById from '../../../utils/FocusUtils'; + +import { InitialSeedlotFormData } from './constants'; +import { convertToPayload } from './utils'; + import './styles.scss'; const CreateAClass = () => { const navigate = useNavigate(); + const [ + seedlotFormData, + setSeedlotFormData + ] = useState(InitialSeedlotFormData); + + const seedlotMutation = useMutation({ + mutationFn: (payload: SeedlotRegPayloadType) => postSeedlot(payload), + onError: (err: AxiosError) => { + toast.error( + , + ErrToastOption + ); + }, + onSuccess: (res) => navigate({ + pathname: '/seedlots/creation-success', + search: `?seedlotNumber=${res.data.seedlotNumber}&seedlotClass=A` + }) + }); + + const setInputValidation = (inputName: keyof SeedlotRegFormType, isInvalid: boolean) => ( + setSeedlotFormData((prevData) => ({ + ...prevData, + [inputName]: { + ...prevData[inputName], + isInvalid + } + })) + ); + + const validateAndCreateSeedlot = () => { + // Validate client + if (seedlotFormData.client.isInvalid || !seedlotFormData.client.value.code) { + setInputValidation('client', true); + focusById(seedlotFormData.client.id); + return; + } + // Validate location code + if ( + seedlotFormData.locationCode.isInvalid + || !seedlotFormData.locationCode.value + ) { + setInputValidation('locationCode', true); + focusById(seedlotFormData.locationCode.id); + return; + } + // Validate email + if (seedlotFormData.email.isInvalid || !seedlotFormData.email.value) { + setInputValidation('email', true); + focusById(seedlotFormData.email.id); + return; + } + // Validate species + if (seedlotFormData.species.isInvalid || !seedlotFormData.species.value.code) { + setInputValidation('species', true); + focusById(seedlotFormData.species.id); + return; + } + // Source code, and the two booleans always have a default value so there's no need to check. + + // Submit Seedlot. + const payload = convertToPayload(seedlotFormData); + seedlotMutation.mutate(payload); + }; + return ( - - - - navigate('/seedlots')}>Seedlots - - - - + + navigate('/seedlots')}>Seedlots + + + + + + { + seedlotMutation.isError + ? ( + + + false} + > + An error has occurred when trying to create your seedlot number. + Please try submiting it again later. + {' '} + {`${seedlotMutation.error.code}: ${seedlotMutation.error.message}`} + + + + ) + : null + } + + + - - - + + + + + + + ); }; diff --git a/frontend/src/views/Seedlot/CreateAClass/styles.scss b/frontend/src/views/Seedlot/CreateAClass/styles.scss index f21527af2..3608dc102 100644 --- a/frontend/src/views/Seedlot/CreateAClass/styles.scss +++ b/frontend/src/views/Seedlot/CreateAClass/styles.scss @@ -1,5 +1,32 @@ +@use '@bcgov-nr/nr-theme/design-tokens/variables.scss' as vars; + .create-a-class-seedlot-page { .create-a-class-seedlot-breadcrumb { margin-left: 0; } + + .page-title-row { + margin-bottom: 2rem; + } + + .error-row { + margin-bottom: 2rem; + + button { + display: none; + } + + #create-seedlot-error-banner { + max-width: none; + border-radius: 0.25rem; + } + + .#{vars.$bcgov-prefix}--actionable-notification__content { + flex-direction: column; + } + } + + .submit-button { + width: 100%; + } } diff --git a/frontend/src/views/Seedlot/CreateAClass/CreationForm/utils.ts b/frontend/src/views/Seedlot/CreateAClass/utils.ts similarity index 92% rename from frontend/src/views/Seedlot/CreateAClass/CreationForm/utils.ts rename to frontend/src/views/Seedlot/CreateAClass/utils.ts index c45ae23c7..343c55fcc 100644 --- a/frontend/src/views/Seedlot/CreateAClass/CreationForm/utils.ts +++ b/frontend/src/views/Seedlot/CreateAClass/utils.ts @@ -1,4 +1,4 @@ -import { SeedlotRegFormType, SeedlotRegPayloadType } from '../../../../types/SeedlotRegistrationTypes'; +import { SeedlotRegFormType, SeedlotRegPayloadType } from '../../../types/SeedlotRegistrationTypes'; export const convertToPayload = (formData: SeedlotRegFormType): SeedlotRegPayloadType => ({ applicantClientNumber: formData.client.value.code, diff --git a/frontend/src/views/Seedlot/EditAClassApplication/index.tsx b/frontend/src/views/Seedlot/EditAClassApplication/index.tsx new file mode 100644 index 000000000..409db0169 --- /dev/null +++ b/frontend/src/views/Seedlot/EditAClassApplication/index.tsx @@ -0,0 +1,221 @@ +import React, { useEffect, useState } from 'react'; +import { + ActionableNotification, + Breadcrumb, + BreadcrumbItem, + FlexGrid, + Column, + Row, + Loading, + Button +} from '@carbon/react'; +import { Save } from '@carbon/icons-react'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { toast } from 'react-toastify'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { getSeedlotById, patchSeedlotApplicationInfo } from '../../../api-service/seedlotAPI'; +import { THREE_HALF_HOURS, THREE_HOURS } from '../../../config/TimeUnits'; +import getVegCodes from '../../../api-service/vegetationCodeAPI'; +import LotApplicantAndInfoForm from '../../../components/LotApplicantAndInfoForm'; +import { SeedlotType } from '../../../types/SeedlotType'; +import { SeedlotPatchPayloadType, SeedlotRegFormType } from '../../../types/SeedlotRegistrationTypes'; +import { getForestClientByNumber } from '../../../api-service/forestClientsAPI'; +import MultiOptionsObj from '../../../types/MultiOptionsObject'; +import PageTitle from '../../../components/PageTitle'; +import focusById from '../../../utils/FocusUtils'; +import ErrorToast from '../../../components/Toast/ErrorToast'; +import { ErrToastOption } from '../../../config/ToastifyConfig'; +import { ForestClientType } from '../../../types/ForestClientType'; +import { getForestClientOptionInput } from '../../../utils/ForestClientUtils'; +import { getBooleanInputObj, getOptionsInputObj, getStringInputObj } from '../../../utils/FormInputUtils'; +import { getSpeciesOptionByCode } from '../../../utils/SeedlotUtils'; +import { InitialSeedlotFormData } from '../CreateAClass/constants'; + +import './styles.scss'; + +const EditAClassApplication = () => { + const navigate = useNavigate(); + const { seedlotNumber } = useParams(); + + const [ + seedlotEditData, + setSeedlotEditData + ] = useState(InitialSeedlotFormData); + + const vegCodeQuery = useQuery({ + queryKey: ['vegetation-codes'], + queryFn: () => getVegCodes(true), + staleTime: THREE_HOURS, + cacheTime: THREE_HALF_HOURS + }); + + const seedlotQuery = useQuery({ + queryKey: ['seedlots', seedlotNumber], + queryFn: () => getSeedlotById(seedlotNumber ?? ''), + enabled: vegCodeQuery.isFetched + }); + + useEffect(() => { + if ( + seedlotQuery.status === 'error' + && (seedlotQuery.error as AxiosError).response?.status === 404 + ) { + navigate('/404'); + } + }, [seedlotQuery.status]); + + const forestClientQuery = useQuery({ + queryKey: ['forest-clients', seedlotQuery.data?.applicantClientNumber], + queryFn: () => getForestClientByNumber(seedlotQuery.data?.applicantClientNumber), + enabled: seedlotQuery.isFetched, + staleTime: THREE_HOURS, + cacheTime: THREE_HALF_HOURS + }); + + const convertToSeedlotForm = ( + seedlot: SeedlotType, + vegCodes: MultiOptionsObj[], + client: ForestClientType + ) => { + setSeedlotEditData({ + client: getForestClientOptionInput('edit-client-read-only', client), + locationCode: getStringInputObj('edit-seedlot-location-code', seedlot.applicantLocationCode), + email: getStringInputObj('edit-seedlot-email', seedlot.applicantEmailAddress), + species: getOptionsInputObj('edit-seedlot-species', getSpeciesOptionByCode(seedlot.vegetationCode, vegCodes)), + sourceCode: getStringInputObj('edit-seedlot-source-code', seedlot.seedlotSource.seedlotSourceCode), + willBeRegistered: getBooleanInputObj('edit-seedlot-will-be-registered', seedlot.intendedForCrownLand), + isBcSource: getBooleanInputObj('edit-seedlot-is-bc-source', seedlot.sourceInBc) + }); + }; + + useEffect(() => { + if ( + forestClientQuery.isFetched + && forestClientQuery.data + && seedlotQuery.data + && vegCodeQuery.data + ) { + convertToSeedlotForm(seedlotQuery.data, vegCodeQuery.data, forestClientQuery.data); + } + }, [forestClientQuery.isFetched]); + + const seedlotPatchMutation = useMutation({ + mutationFn: ( + payload: SeedlotPatchPayloadType + ) => patchSeedlotApplicationInfo(seedlotNumber ?? '', payload), + onSuccess: () => navigate(`/seedlots/details/${seedlotNumber}`), + onError: (err: AxiosError) => { + toast.error( + , + ErrToastOption + ); + } + }); + + const setInputValidation = (inputName: keyof SeedlotRegFormType, isInvalid: boolean) => ( + setSeedlotEditData((prevData) => ({ + ...prevData, + [inputName]: { + ...prevData[inputName], + isInvalid + } + })) + ); + + const validateAndSave = () => { + if (seedlotEditData.email.isInvalid || !seedlotEditData.email.value) { + setInputValidation('email', true); + focusById(seedlotEditData.email.id); + return; + } + + if (seedlotEditData.sourceCode.isInvalid || !seedlotEditData.sourceCode.value) { + setInputValidation('sourceCode', true); + focusById(seedlotEditData.sourceCode.id); + return; + } + + seedlotPatchMutation.mutate({ + applicantEmailAddress: seedlotEditData.email.value, + seedlotSourceCode: seedlotEditData.sourceCode.value, + toBeRegistrdInd: seedlotEditData.willBeRegistered.value, + bcSourceInd: seedlotEditData.isBcSource.value + }); + }; + + return ( + + + + navigate('/seedlots')}>Seedlots + navigate('/seedlots/my-seedlots')}>My seedlots + navigate(`/seedlots/details/${seedlotNumber}`)}>{`Seedlot ${seedlotNumber}`} + + + + + + { + seedlotPatchMutation.isError + ? ( + + + false} + > + An unexpected error occurred while saving your edits. + Please try again, and if the issue persists, contact support. + {' '} + {`${(seedlotPatchMutation.error as AxiosError).code}: ${(seedlotPatchMutation.error as AxiosError).message}`} + + + + ) + : null + } + + + { + forestClientQuery.isFetched && seedlotEditData + ? ( + + ) + : + } + + + + + + + + + ); +}; + +export default EditAClassApplication; diff --git a/frontend/src/views/Seedlot/EditAClassApplication/styles.scss b/frontend/src/views/Seedlot/EditAClassApplication/styles.scss new file mode 100644 index 000000000..7c4230c44 --- /dev/null +++ b/frontend/src/views/Seedlot/EditAClassApplication/styles.scss @@ -0,0 +1,33 @@ +@use '@bcgov-nr/nr-theme/design-tokens/variables.scss' as vars; + +.edit-a-class-seedlot-page{ + .breadcrumb-row{ + padding: 0 1rem; + } + + .title-row{ + margin-top: 0.5rem; + margin-bottom: 3rem; + } + + .submit-button{ + width: 100%; + } + + .error-row { + margin-bottom: 2rem; + + button { + display: none; + } + + #edit-seedlot-error-banner { + max-width: none; + border-radius: 0.25rem; + } + + .#{vars.$bcgov-prefix}--actionable-notification__content { + flex-direction: column; + } + } +} diff --git a/frontend/src/views/Seedlot/SeedlotDetails/ApplicantInformation/index.tsx b/frontend/src/views/Seedlot/SeedlotDetails/ApplicantInformation/index.tsx index 6098945b4..abcdc814d 100644 --- a/frontend/src/views/Seedlot/SeedlotDetails/ApplicantInformation/index.tsx +++ b/frontend/src/views/Seedlot/SeedlotDetails/ApplicantInformation/index.tsx @@ -5,24 +5,27 @@ import { Column, TextInput, TextInputSkeleton } from '@carbon/react'; import { Edit } from '@carbon/icons-react'; +import { useNavigate } from 'react-router-dom'; import { SeedlotApplicantType } from '../../../../types/SeedlotType'; import './styles.scss'; interface ApplicantSeedlotInformationProps { + seedlotNumber?: string; applicant?: SeedlotApplicantType; isFetching: boolean; } const ApplicantInformation = ( - { applicant, isFetching }: ApplicantSeedlotInformationProps + { seedlotNumber, applicant, isFetching }: ApplicantSeedlotInformationProps ) => { const triggerMailTo = () => { if (applicant?.email) { window.location.href = `mailto: ${applicant.email}`; } }; + const navigate = useNavigate(); return ( @@ -157,6 +160,7 @@ const ApplicantInformation = ( size="md" className="btn-edit" renderIcon={Edit} + onClick={() => navigate(`/seedlots/edit-a-class-application/${seedlotNumber}`)} > Edit applicant and seedlot diff --git a/frontend/src/views/Seedlot/SeedlotDetails/index.tsx b/frontend/src/views/Seedlot/SeedlotDetails/index.tsx index db1365e3d..8742b71e7 100644 --- a/frontend/src/views/Seedlot/SeedlotDetails/index.tsx +++ b/frontend/src/views/Seedlot/SeedlotDetails/index.tsx @@ -41,8 +41,8 @@ const SeedlotDetails = () => { const manageOptions = [ { text: 'Edit seedlot applicant', - onClickFunction: () => null, - disabled: true + onClickFunction: () => navigate(`/seedlots/edit-a-class-application/${seedlotNumber}`), + disabled: false }, { text: 'Print seedlot', @@ -79,6 +79,7 @@ const SeedlotDetails = () => { queryKey: ['seedlots', seedlotNumber], queryFn: () => getSeedlotById(seedlotNumber ?? ''), enabled: vegCodeQuery.isFetched, + refetchOnMount: true, onError: (err: AxiosError) => { if (err.response?.status === 404) { navigate('/404'); @@ -87,10 +88,10 @@ const SeedlotDetails = () => { }); useEffect(() => { - if (seedlotQuery.isFetched) { + if (seedlotQuery.isFetched || seedlotQuery.isFetchedAfterMount) { covertToDisplayObj(seedlotQuery.data); } - }, [seedlotQuery.isFetched]); + }, [seedlotQuery.isFetched, seedlotQuery.isFetchedAfterMount]); const forestClientQuery = useQuery({ queryKey: ['forest-clients', seedlotQuery.data?.applicantClientNumber], @@ -112,10 +113,10 @@ const SeedlotDetails = () => { }; useEffect(() => { - if (forestClientQuery.isFetched) { + if (forestClientQuery.isFetched && seedlotQuery.isFetchedAfterMount) { covertToClientObj(); } - }, [forestClientQuery.isFetched]); + }, [forestClientQuery.isFetched, seedlotQuery.isFetchedAfterMount]); return ( @@ -168,6 +169,7 @@ const SeedlotDetails = () => { isFetching={seedlotQuery.isFetching} /> diff --git a/frontend/src/views/Seedlot/SeedlotRegistrationForm/definitions.ts b/frontend/src/views/Seedlot/SeedlotRegistrationForm/definitions.ts index 2f81174be..be2a1247c 100644 --- a/frontend/src/views/Seedlot/SeedlotRegistrationForm/definitions.ts +++ b/frontend/src/views/Seedlot/SeedlotRegistrationForm/definitions.ts @@ -21,28 +21,6 @@ export type AllStepData = { extractionStorageStep: ExtractionStorage } -type SingleInvalidObj = { - isInvalid: boolean, - invalidText: string, - optInvalidText?: string -} - -export type FormInvalidationObj = { - [key: string]: SingleInvalidObj; -} - -export type OwnershipInvalidObj = { - [id: number]: FormInvalidationObj; -} - -export type AllStepInvalidationObj = { - collectionStep: FormInvalidationObj, - interimStep: FormInvalidationObj, - ownershipStep: OwnershipInvalidObj, - orchardStep: FormInvalidationObj, - extractionStorageStep: FormInvalidationObj -} - type ProgressStepStatus = { isComplete: boolean; isCurrent: boolean; diff --git a/frontend/src/views/Seedlot/SeedlotRegistrationForm/index.tsx b/frontend/src/views/Seedlot/SeedlotRegistrationForm/index.tsx index 19235574d..dedb0df68 100644 --- a/frontend/src/views/Seedlot/SeedlotRegistrationForm/index.tsx +++ b/frontend/src/views/Seedlot/SeedlotRegistrationForm/index.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { @@ -42,20 +42,15 @@ import { getMultiOptList, getCheckboxOptions } from '../../../utils/MultiOptions import ExtractionStorage from '../../../types/SeedlotTypes/ExtractionStorage'; import MultiOptionsObj from '../../../types/MultiOptionsObject'; -import { emptyMultiOptObj } from '../../../shared-constants/shared-constants'; +import { EmptyMultiOptObj } from '../../../shared-constants/shared-constants'; -import { - AllStepData, AllStepInvalidationObj, - ProgressIndicatorConfig -} from './definitions'; +import { AllStepData, ProgressIndicatorConfig } from './definitions'; import { initCollectionState, initInterimState, initOrchardState, initOwnershipState, initExtractionStorageState, - initInvalidationObj, - initOwnerShipInvalidState, initParentTreeState, generateDefaultRows, validateCollectionStep, @@ -83,9 +78,9 @@ const SeedlotRegistrationForm = () => { // Initialize all step's state here const [allStepData, setAllStepData] = useState({ - collectionStep: initCollectionState('', ''), + collectionStep: initCollectionState(EmptyMultiOptObj, ''), interimStep: initInterimState('', ''), - ownershipStep: [initOwnershipState('', '')], + ownershipStep: [initOwnershipState(EmptyMultiOptObj, '')], orchardStep: initOrchardState(), parentTreeStep: initParentTreeState(), extractionStorageStep: initExtractionStorageState(defaultExtStorAgency, defaultExtStorCode) @@ -120,6 +115,34 @@ const SeedlotRegistrationForm = () => { refetchOnWindowFocus: false }); + const setDefaultAgencyAndCode = (agency: MultiOptionsObj, locationCode: string) => { + setAllStepData((prevData) => ({ + ...prevData, + collectionStep: { + ...prevData.collectionStep, + collectorAgency: { + ...prevData.collectionStep.collectorAgency, + value: agency + }, + locationCode: { + ...prevData.collectionStep.locationCode, + value: locationCode + } + }, + ownershipStep: prevData.ownershipStep.map((singleOwner) => ({ + ...singleOwner, + ownerAgency: { + ...singleOwner.ownerAgency, + value: agency + }, + ownerCode: { + ...singleOwner.ownerCode, + value: locationCode + } + })) + })); + }; + const forestClientQuery = useQuery({ queryKey: ['forest-clients', seedlotQuery.data?.applicantClientNumber], queryFn: () => getForestClientByNumber(seedlotQuery.data?.applicantClientNumber), @@ -128,6 +151,20 @@ const SeedlotRegistrationForm = () => { cacheTime: THREE_HALF_HOURS }); + const getDefaultAgencyObj = (): MultiOptionsObj => ({ + code: forestClientQuery.data?.clientNumber ?? '', + description: forestClientQuery.data?.clientName ?? '', + label: `${forestClientQuery.data?.clientNumber} - ${forestClientQuery.data?.clientName} - ${forestClientQuery.data?.acronym}` + }); + + const getDefaultLocationCode = (): string => (seedlotQuery.data?.applicantLocationCode ?? ''); + + useEffect(() => { + if (forestClientQuery.isFetched) { + setDefaultAgencyAndCode(getDefaultAgencyObj(), getDefaultLocationCode()); + } + }, [forestClientQuery.isFetched]); + const gameticMethodologyQuery = useQuery({ queryKey: ['gametic-methodologies'], queryFn: getGameticMethodology @@ -138,18 +175,11 @@ const SeedlotRegistrationForm = () => { queryFn: () => getApplicantAgenciesOptions() }); - const [allInvalidationObj] = useState({ - collectionStep: initInvalidationObj(), - interimStep: initInvalidationObj(), - ownershipStep: initOwnerShipInvalidState(), - orchardStep: initInvalidationObj(), - extractionStorageStep: initInvalidationObj() - }); - const setStepData = (stepName: keyof AllStepData, stepData: any) => { - const newData = { ...allStepData }; - newData[stepName] = stepData; - setAllStepData(newData); + setAllStepData((prevData) => ({ + ...prevData, + [stepName]: stepData + })); }; const methodsOfPaymentQuery = useQuery({ @@ -157,7 +187,7 @@ const SeedlotRegistrationForm = () => { queryFn: getMethodsOfPayment, onSuccess: (dataArr: MultiOptionsObj[]) => { const defaultMethodArr = dataArr.filter((data: MultiOptionsObj) => data.isDefault); - const defaultMethod = defaultMethodArr.length === 0 ? emptyMultiOptObj : defaultMethodArr[0]; + const defaultMethod = defaultMethodArr.length === 0 ? EmptyMultiOptObj : defaultMethodArr[0]; if (!allStepData.ownershipStep[0].methodOfPayment.value.code) { const tempOwnershipData = structuredClone(allStepData.ownershipStep); tempOwnershipData[0].methodOfPayment.value = defaultMethod; @@ -224,11 +254,10 @@ const SeedlotRegistrationForm = () => { }; const renderStep = () => { - // Will need to convert this into a multiOption obj - const defaultAgency = `${forestClientQuery.data?.clientNumber} - ${forestClientQuery.data?.clientName} - ${forestClientQuery.data?.acronym}`; + const defaultAgencyObj = getDefaultAgencyObj(); + const defaultCode = getDefaultLocationCode(); - const defaultCode = seedlotQuery.data?.applicantLocationCode ?? ''; - const agencyOptions = applicantAgencyQuery.data ? applicantAgencyQuery.data : []; + const agencyOptions = applicantAgencyQuery.data ?? []; const seedlotSpecies = getSpeciesOptionByCode( seedlotQuery.data?.vegetationCode, @@ -242,7 +271,7 @@ const SeedlotRegistrationForm = () => { setStepData('collectionStep', data)} - defaultAgency={defaultAgency} + defaultAgency={defaultAgencyObj} defaultCode={defaultCode} agencyOptions={agencyOptions} collectionMethods={getCheckboxOptions(coneCollectionMethodsQuery.data)} @@ -253,8 +282,7 @@ const SeedlotRegistrationForm = () => { return ( { return ( setStepData('interimStep', data)} diff --git a/frontend/src/views/Seedlot/SeedlotRegistrationForm/utils.ts b/frontend/src/views/Seedlot/SeedlotRegistrationForm/utils.ts index f05e18fcf..6895608b1 100644 --- a/frontend/src/views/Seedlot/SeedlotRegistrationForm/utils.ts +++ b/frontend/src/views/Seedlot/SeedlotRegistrationForm/utils.ts @@ -1,20 +1,18 @@ import { CollectionForm } from '../../../components/SeedlotRegistrationSteps/CollectionStep/definitions'; import { OrchardForm } from '../../../components/SeedlotRegistrationSteps/OrchardStep/definitions'; -import { - createOwnerTemplate, - validTemplate as ownerInvalidTemplate -} from '../../../components/SeedlotRegistrationSteps/OwnershipStep/constants'; +import { createOwnerTemplate } from '../../../components/SeedlotRegistrationSteps/OwnershipStep/constants'; import { SingleOwnerForm } from '../../../components/SeedlotRegistrationSteps/OwnershipStep/definitions'; import { notificationCtrlObj } from '../../../components/SeedlotRegistrationSteps/ParentTreeStep/constants'; import { RowDataDictType } from '../../../components/SeedlotRegistrationSteps/ParentTreeStep/definitions'; import { getMixRowTemplate } from '../../../components/SeedlotRegistrationSteps/ParentTreeStep/utils'; +import { EmptyMultiOptObj } from '../../../shared-constants/shared-constants'; import MultiOptionsObj from '../../../types/MultiOptionsObject'; import { - FormInvalidationObj, OwnershipInvalidObj, ParentTreeStepDataObj + ParentTreeStepDataObj } from './definitions'; export const initCollectionState = ( - defaultAgency: string, + defaultAgency: MultiOptionsObj, defaultCode: string ) => ({ useDefaultAgencyInfo: { @@ -70,7 +68,7 @@ export const initCollectionState = ( }); export const initOwnershipState = ( - defaultAgency: string, + defaultAgency: MultiOptionsObj, defaultCode: string ) => { const initialOwnerState = createOwnerTemplate(0); @@ -149,19 +147,6 @@ export const initExtractionStorageState = ( seedStorageEndDate: '' } ); - -export const initInvalidationObj = () => { - const returnObj: FormInvalidationObj = {}; - return returnObj; -}; - -export const initOwnerShipInvalidState = (): OwnershipInvalidObj => { - const initialOwnerInvalidState = { ...ownerInvalidTemplate }; - return { - 0: initialOwnerInvalidState - }; -}; - /** * Validate Collection Step. * Return true if it's Invalid, false otherwise. @@ -232,7 +217,7 @@ export const verifyCollectionStepCompleteness = (collectionData: CollectionForm) */ export const verifyOwnershipStepCompleteness = (ownershipData: Array): boolean => { for (let i = 0; i < ownershipData.length; i += 1) { - if (!ownershipData[i].ownerAgency.value.length + if (!ownershipData[i].ownerAgency.value.code.length || !ownershipData[i].ownerCode.value.length || !ownershipData[i].ownerPortion.value.length || !ownershipData[i].reservedPerc.value.length @@ -250,18 +235,12 @@ export const getSpeciesOptionByCode = ( vegCode?: string, options?: MultiOptionsObj[] ): MultiOptionsObj => { - const template = { - code: '', - label: '', - description: '' - }; - if (!vegCode || !options) { - return template; + return EmptyMultiOptObj; } const filtered = options.filter((opt) => opt.code === vegCode); return filtered.length > 0 ? filtered[0] - : template; + : EmptyMultiOptObj; };