diff --git a/.gitignore b/.gitignore index 4c47fea03..996bb61cf 100644 --- a/.gitignore +++ b/.gitignore @@ -208,4 +208,6 @@ sync/data/ schemaspy/output # local testing -*.bat \ No newline at end of file +*.bat + +oracle-api/config diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts index e31f10e26..23b0bf613 100644 --- a/frontend/cypress.config.ts +++ b/frontend/cypress.config.ts @@ -31,7 +31,9 @@ export default defineConfig({ '**/a-class-seedlot-reg-form-extraction.cy.ts', '**/a-class-seedlot-reg-form-parent-tree-part-1.cy.ts', '**/a-class-seedlot-reg-form-parent-tree-part-2.cy.ts', - '**/a-class-seedlot-reg-form-parent-tree-part-3.cy.ts' + '**/a-class-seedlot-reg-form-parent-tree-part-3.cy.ts', + '**/create-a-class-seedlot-fdi.cy.ts', + '**/a-class-seedlot-reg-form-parent-tree-calculations-part-1.cy.ts' ], chromeWebSecurity: false, retries: { diff --git a/frontend/cypress/e2e/smoke-test/a-class-seedlot-reg-form-parent-tree-calculations-part-1.cy.ts b/frontend/cypress/e2e/smoke-test/a-class-seedlot-reg-form-parent-tree-calculations-part-1.cy.ts new file mode 100644 index 000000000..6855579a0 --- /dev/null +++ b/frontend/cypress/e2e/smoke-test/a-class-seedlot-reg-form-parent-tree-calculations-part-1.cy.ts @@ -0,0 +1,216 @@ +import prefix from '../../../src/styles/classPrefix'; + +describe('A Class Seedlot Registration form, Parent Tree Calculations Part 1', () => { + let seedlotNum: string; + const speciesKey = 'fdi'; + let totalParentTrees: number = 0; + let totalConeCount: number = 0; + let totalPollenCount: number = 0; + let effectivePopulationSize: number = 0; + let firstConeValue: number = 0; + let firstPollenValue: number = 0; + + beforeEach(() => { + // Login + cy.login(); + + cy.fixture('aclass-seedlot').then((fData) => { + cy.task('getData', fData[speciesKey].species).then((sNumber) => { + seedlotNum = sNumber as string; + const url = `/seedlots/a-class-registration/${seedlotNum}/?step=5`; + cy.visit(url); + cy.url().should('contains', url); + }); + }); + }); + + it('Orchard selection', () => { + // Press next button + cy.get('.seedlot-registration-button-row') + .find('button.form-action-btn') + .contains('Back') + .click(); + + cy.get('#primary-orchard-selection') + .siblings(`button.${prefix}--list-box__menu-icon[title="Open"]`) + .click(); + + // Select primary orchard + cy.get(`.${prefix}--list-box--expanded`) + .find('ul li') + .as('orchardDropdown') + .contains('324 - BAILEY - S - PRD') + .click(); + + // Select female gametic contribution methodology + cy.get('#orchard-female-gametic') + .siblings() + .click(); + + cy.get(`.${prefix}--list-box--expanded`) + .find('ul li') + .contains('F1 - Visual Estimate') + .click(); + + // Select male gametic contribution methodology + cy.get('#orchard-male-gametic') + .siblings() + .click(); + + cy.get(`.${prefix}--list-box--expanded`) + .find('ul li') + .contains('M2 - Pollen Volume Estimate by Partial Survey') + .click(); + + // Save changes + cy.saveSeedlotRegFormProgress(); + }); + + it('Upload csv file', () => { + // Wait for the table to load + cy.get('#parentTreeNumber', { timeout: 10000 }); + + // Upload csv file + cy.get('button.upload-button') + .click({ force: true }); + + cy.get(`.${prefix}--modal-container[aria-label="Seedlot registration"]`) + .should('be.visible'); + + cy.get(`.${prefix}--file`) + .find(`input.${prefix}--file-input`) + .selectFile('cypress/fixtures/Seedlot_composition_template_FDI.csv', { force: true }); + + cy.get('button') + .contains('Import file and continue') + .click(); + + // Save changes + cy.saveSeedlotRegFormProgress(); + }); + + it('Check Parent tree contribution summary', () => { + // Wait for the table to load + cy.get('#parentTreeNumber', { timeout: 10000 }); + + cy.get(`table.${prefix}--data-table > tbody`) + .find('tr') + .then((row) => { + totalParentTrees = row.length; + cy.get('#totalnumber\\ of\\ parent\\ trees') + .should('have.value', totalParentTrees); + + // Get total cone counts + for (let i = 0; i < totalParentTrees; i += 1) { + cy.get('.parent-tree-step-table-container-col') + .find('table tbody tr') + .eq(i) + .find('td:nth-child(2) input') + .invoke('val') + .then(($value: any) => { + totalConeCount = totalConeCount + Number($value); + + if (i === (totalParentTrees - 1)) { + // Check total cone counts + cy.get('#totalnumber\\ of\\ cone\\ count') + .should('have.value', totalConeCount); + } + }); + } + + // Get total pollen counts + for (let i = 0; i < totalParentTrees; i += 1) { + cy.get('.parent-tree-step-table-container-col') + .find('table tbody tr') + .eq(i) + .find('td:nth-child(3) input') + .invoke('val') + .then(($value) => { + totalPollenCount = totalPollenCount + Number($value); + + if (i === (totalParentTrees - 1)) { + // Check total pollen counts + cy.get('#totalnumber\\ of\\ pollen\\ count') + .should('have.value', totalPollenCount); + } + }); + } + }); + + // Click 'Calculate metrics' button + cy.get('.gen-worth-cal-row') + .find('button') + .contains('Calculate metrics') + .click(); + + cy.wait(3000); + + // Store Ne value to a variable + cy.get('#effectivepopulation\\ size\\ \\(ne\\)') + .invoke('val') + .then(($input: any) => { + effectivePopulationSize = $input; + }); + + // Save changes + cy.saveSeedlotRegFormProgress(); + }); + + it('Remove a single Parent tree contribution', () => { + // Wait for the table to load + cy.get('#parentTreeNumber', { timeout: 10000 }); + + cy.get('#8021-coneCount-value-input') + .invoke('val') + .then(($input: any) => { + // Store first cone count to a variable + firstConeValue = $input; + + // Clear cone count of first row + cy.get('#8021-coneCount-value-input') + .clear() + .type('0') + .blur(); + + // Check new total cone count + cy.get('#totalnumber\\ of\\ cone\\ count') + .should('have.value', (totalConeCount - firstConeValue)); + }); + + cy.get('#8021-pollenCount-value-input') + .invoke('val') + .then(($input: any) => { + // Store first pollen count to a variable + firstPollenValue = $input; + + // Clear pollen count of first row + cy.get('#8021-pollenCount-value-input') + .clear() + .type('0') + .blur(); + + // Check new total parent trees + cy.get('#totalnumber\\ of\\ parent\\ trees') + .should('have.value', (totalParentTrees - 1)); + + // Check new total pollen count + cy.get('#totalnumber\\ of\\ pollen\\ count') + .should('have.value', (totalPollenCount - firstPollenValue)); + }); + + // Click 'Calculate metrics' button again + cy.get('.gen-worth-cal-row') + .find('button') + .contains('Calculate metrics') + .click(); + + cy.wait(3000); + + // Check Ne value after clearing first parent tree row + cy.get('#effectivepopulation\\ size\\ \\(ne\\)') + .invoke('val') + .then($value => { + expect(Number($value)).to.be.lessThan(Number(effectivePopulationSize)); + }); + }); +}); diff --git a/frontend/cypress/e2e/smoke-test/create-a-class-seedlot-fdi.cy.ts b/frontend/cypress/e2e/smoke-test/create-a-class-seedlot-fdi.cy.ts new file mode 100644 index 000000000..f55b5ce94 --- /dev/null +++ b/frontend/cypress/e2e/smoke-test/create-a-class-seedlot-fdi.cy.ts @@ -0,0 +1,66 @@ +import { TYPE_DELAY } from 'cypress/constants'; +import prefix from '../../../src/styles/classPrefix'; +import { SeedlotRegFixtureType } from '../../definitions'; + +describe('Create FDI Seedlot', () => { + let fixtureData: SeedlotRegFixtureType = {}; + beforeEach(() => { + cy.fixture('aclass-seedlot').then((jsonData) => { + fixtureData = jsonData; + }); + + cy.login(); + cy.visit('/seedlots/register-a-class'); + }); + + it('Register fdi seedlot', () => { + const regData = fixtureData.fdi; + // Enter the applicant agency number + cy.get('#agency-number-input') + .clear() + .type(regData.agencyNumber, { delay: TYPE_DELAY }); + + // Enter the applicant email address + cy.get('#applicant-email-input') + .clear() + .type(regData.email, { delay: TYPE_DELAY }); + + // Enter the seedlot species, wait for species data to load + cy.get('#seedlot-species-combobox') + .click(); + cy.contains(`.${prefix}--list-box__menu-item__option`, regData.species) + .scrollIntoView() + .click(); + + // Select the to be registered according to fixture + const regIdToClick = regData.toBeRegistered ? '#register-w-tsc-yes' : '#register-w-tsc-no'; + cy.get(regIdToClick) + .siblings(`.${prefix}--radio-button__label`) + .find(`.${prefix}--radio-button__appearance`) + .click(); + cy.get(regIdToClick) + .should('be.checked'); + + // Select the Collected within BC according to fixture + const collectedIdToClick = regData.withinBc ? '#collected-within-bc-yes' : '#collected-within-bc-no'; + cy.get(collectedIdToClick) + .siblings(`.${prefix}--radio-button__label`) + .find(`.${prefix}--radio-button__appearance`) + .click(); + cy.get(collectedIdToClick) + .should('be.checked'); + + // Click on button Create seedlot number + cy.get('.submit-button') + .click(); + + cy.url().should('contains', '/creation-success'); + + // remember seedlot number + cy.get('#created-seedlot-number').invoke('text') + .then((seedlotNumber) => { + cy.task('setData', [regData.species, seedlotNumber]); + }); + cy.log('A-Class seedlot created with species', regData.species); + }); +}); diff --git a/frontend/cypress/fixtures/Seedlot_composition_template_FDI.csv b/frontend/cypress/fixtures/Seedlot_composition_template_FDI.csv new file mode 100644 index 000000000..d05048b13 --- /dev/null +++ b/frontend/cypress/fixtures/Seedlot_composition_template_FDI.csv @@ -0,0 +1,39 @@ +Parent Tree number,Cone count,Pollen count,SMP success,Pollen contamination +9465,45,27,, +9116,47,29,, +9524,90,49,, +9532,71,36,, +9536,80,37,, +9550,92,47,, +9551,78,44,, +9559,78,48,, +9562,78,38,, +9578,77,47,, +9583,96,56,, +9586,42,25,, +9590,15,8,, +9591,85,43,, +9592,68,34,, +9593,79,40,, +9594,57,27,, +9595,87,47,, +9598,87,53,, +9601,38,22,, +8021,37,20,, +8044,27,14,, +8168,47,168,, +8174,46,23,, +8185,17,12,, +8187,37,16,, +8206,82,44,, +8847,84,48,, +8875,44,28,, +8876,72,36,, +8946,50,28,, +8948,53,32,, +8955,88,48,, +8964,84,45,, +8965,78,42,, +8968,60,31,, +9094,51,29,, +9474,27,12,, diff --git a/frontend/cypress/fixtures/aclass-seedlot.json b/frontend/cypress/fixtures/aclass-seedlot.json index 2ea23749a..ef6b97728 100644 --- a/frontend/cypress/fixtures/aclass-seedlot.json +++ b/frontend/cypress/fixtures/aclass-seedlot.json @@ -48,5 +48,15 @@ "source": "tpt", "toBeRegistered": false, "withinBc": false + }, + "fdi": { + "agencyAcronym": "WFP", + "agencyName": "WFP - WESTERN FOREST PRODUCTS INC. - 00149081", + "agencyNumber": "22", + "email": "abc@gov.bc.ca", + "species": "FDI - Interior Douglas-fir", + "source": "tpt", + "toBeRegistered": true, + "withinBc": true } } diff --git a/frontend/src/api-service/ApiConfig.ts b/frontend/src/api-service/ApiConfig.ts index df8da4096..9b301768d 100644 --- a/frontend/src/api-service/ApiConfig.ts +++ b/frontend/src/api-service/ApiConfig.ts @@ -55,7 +55,9 @@ const ApiConfig = { areaOfUseSpzList: `${oracleServerHost}/api/area-of-use/spz-list/vegetation-code`, - parentTreeByVegCode: `${oracleServerHost}/api/parent-trees/vegetation-codes/{vegCode}` + parentTreeByVegCode: `${oracleServerHost}/api/parent-trees/vegetation-codes/{vegCode}`, + + seedlotFromOracleDbBySeedlotNumber: `${oracleServerHost}/api/seedlot/{seedlotNumber}` }; export default ApiConfig; diff --git a/frontend/src/api-service/seedlotAPI.ts b/frontend/src/api-service/seedlotAPI.ts index 24217771d..3a4c9ae49 100644 --- a/frontend/src/api-service/seedlotAPI.ts +++ b/frontend/src/api-service/seedlotAPI.ts @@ -72,3 +72,8 @@ export const getAClassSeedlotFullForm = (seedlotNumber: string) => { const url = `${ApiConfig.seedlots}/${seedlotNumber}/a-class-full-form`; return api.get(url).then((res) => res.data as SeedlotAClassFullResponseType); }; + +export const getSeedlotFromOracleDbBySeedlotNumber = (seedlotNumber: string) => { + const url = ApiConfig.seedlotFromOracleDbBySeedlotNumber.replace('{seedlotNumber}', seedlotNumber); + return api.get(url); +}; diff --git a/frontend/src/components/ClientAndCodeInput/definitions.ts b/frontend/src/components/ClientAndCodeInput/definitions.ts index 4e6063f6c..9a1518e35 100644 --- a/frontend/src/components/ClientAndCodeInput/definitions.ts +++ b/frontend/src/components/ClientAndCodeInput/definitions.ts @@ -18,6 +18,7 @@ type ClientAndCodeInputProps = { readOnly?: boolean, maxInputColSize?: number, checkBoxInput?: BooleanInputType + shouldSelectDefaultValue?: boolean } export default ClientAndCodeInputProps; diff --git a/frontend/src/components/ClientAndCodeInput/index.tsx b/frontend/src/components/ClientAndCodeInput/index.tsx index c054ba2ef..67eedc36c 100644 --- a/frontend/src/components/ClientAndCodeInput/index.tsx +++ b/frontend/src/components/ClientAndCodeInput/index.tsx @@ -26,7 +26,7 @@ import './styles.scss'; const ClientAndCodeInput = ({ checkboxId, clientInput, locationCodeInput, textConfig, defaultClientNumber, defaultLocCode, setClientAndCode, readOnly, showCheckbox, maxInputColSize, - checkBoxInput + checkBoxInput, shouldSelectDefaultValue = false }: ClientAndCodeInputProps) => { const getIsDefaultVal = () => ( checkBoxInput === undefined @@ -38,9 +38,16 @@ const ClientAndCodeInput = ({ const clientInputRef = useRef(null); const locCodeInputRef = useRef(null); const [isDefault, setIsDefault] = useState( - () => getIsDefaultVal() + !shouldSelectDefaultValue ? false : () => getIsDefaultVal() ); + const [isChecked, setIsChecked] = useState(() => { + if (!shouldSelectDefaultValue) { + return false; + } + return checkBoxInput ? checkBoxInput.value : isDefault; + }); + const [showClientValidationStatus, setShowClientValidationStatus] = useState(true); const [showLocCodeValidationStatus, setShowLocCodeValidationStatus] = useState(false); @@ -60,7 +67,9 @@ const ClientAndCodeInput = ({ useEffect(() => { const areValsDefault = getIsDefaultVal(); - setIsDefault(areValsDefault); + if (shouldSelectDefaultValue) { + setIsDefault(areValsDefault); + } // Do not show validation status if isDefault is true if (areValsDefault) { @@ -223,10 +232,8 @@ const ClientAndCodeInput = ({ } : undefined ); - - if (!checkBoxInput) { - setIsDefault(checked); - } + setIsDefault(checked); + setIsChecked(checked); }; const [openAgnTooltip, setOpenAgnTooltip] = useState(false); @@ -343,7 +350,7 @@ const ClientAndCodeInput = ({ name={textConfig.useDefaultCheckbox.name} labelText={textConfig.useDefaultCheckbox.labelText} readOnly={readOnly} - checked={checkBoxInput ? checkBoxInput.value : isDefault} + checked={isChecked} onChange={(e: React.ChangeEvent) => { handleDefaultCheckBox(e.target.checked); }} @@ -363,7 +370,7 @@ const ClientAndCodeInput = ({ id={clientInput.id} autoCapitalize="on" labelText={textConfig.agencyInput.titleText} - defaultValue={forestClientQuery.data?.acronym} + defaultValue={shouldSelectDefaultValue ? forestClientQuery.data?.acronym : ''} helperText={ (readOnly || (showCheckbox && isDefault)) ? null @@ -406,7 +413,7 @@ const ClientAndCodeInput = ({ id={locationCodeInput.id} ref={locCodeInputRef} name={textConfig.locationCode.name} - defaultValue={locationCodeInput.value} + defaultValue={shouldSelectDefaultValue ? locationCodeInput.value : ''} type="number" maxCount={LOCATION_CODE_LIMIT} enableCounter diff --git a/frontend/src/components/LotApplicantAndInfoForm/index.tsx b/frontend/src/components/LotApplicantAndInfoForm/index.tsx index 6d81177da..03fb63185 100644 --- a/frontend/src/components/LotApplicantAndInfoForm/index.tsx +++ b/frontend/src/components/LotApplicantAndInfoForm/index.tsx @@ -118,6 +118,7 @@ const LotApplicantAndInfoForm = ({ } readOnly={isEdit} maxInputColSize={6} + shouldSelectDefaultValue /> diff --git a/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/constants.ts b/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/constants.ts index a3fa2d971..9eddccfae 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/constants.ts +++ b/frontend/src/components/SeedlotRegistrationSteps/CollectionStep/constants.ts @@ -49,12 +49,12 @@ export const fieldsConfig = { }, volumePerContainers: { name: 'volumePerContainers', - labelText: 'Volume per Containers (HI)', + labelText: 'Volume per containers (hl)', invalidText: 'Invalid entry. Number must be between 0 and 10,000 and up to 3 decimal places.' }, volumeOfCones: { name: 'volumeOfCones', - labelText: 'Volume of Cones (HI)', + labelText: 'Volume of cones (hl)', invalidText: 'Number has more than 3 decimals.', helperText: 'This value must be the "Volume per container" X "Number of containers".', warnText: 'The total volume of cones does not equal, please note that this value must be the "Volume per container" x "Number of containers"' diff --git a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/SingleOwnerInfo/index.tsx b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/SingleOwnerInfo/index.tsx index f75423014..2f78c7ebf 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/SingleOwnerInfo/index.tsx +++ b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/SingleOwnerInfo/index.tsx @@ -35,12 +35,15 @@ interface SingleOwnerInfoProps { setState: Function, readOnly?: boolean, isReview?: boolean + isOwnershipStep?: boolean } const SingleOwnerInfo = ({ ownerInfo, defaultAgency, defaultCode, fundingSourcesQuery, - methodsOfPaymentQuery, deleteAnOwner, checkPortionSum, setState, readOnly, isReview + methodsOfPaymentQuery, deleteAnOwner, checkPortionSum, setState, + readOnly, isReview, isOwnershipStep }: SingleOwnerInfoProps) => { + const isReadOnly = readOnly && (isOwnershipStep || !isReview); const [ownerPortionInvalidText, setOwnerPortionInvalidText] = useState( inputText.portion.invalidText ); @@ -150,7 +153,7 @@ const SingleOwnerInfo = ({ locationCode: StringInputType ) => setClientAndCode(client, locationCode) } - readOnly={readOnly && !isReview} + readOnly={isReadOnly} /> @@ -168,7 +171,7 @@ const SingleOwnerInfo = ({ }} invalid={ownerInfo.ownerPortion.isInvalid} invalidText={ownerPortionInvalidText} - readOnly={readOnly && !isReview} + readOnly={isReadOnly} /> @@ -188,7 +191,7 @@ const SingleOwnerInfo = ({ onWheel={(e: React.ChangeEvent) => e.target.blur()} invalid={ownerInfo.reservedPerc.isInvalid} invalidText={reservedInvalidText} - readOnly={readOnly && !isReview} + readOnly={isReadOnly} />
@@ -206,7 +209,7 @@ const SingleOwnerInfo = ({ onWheel={(e: React.ChangeEvent) => e.target.blur()} invalid={ownerInfo.surplusPerc.isInvalid} invalidText={surplusInvalidText} - readOnly={readOnly && !isReview} + readOnly={isReadOnly} />
@@ -233,7 +236,7 @@ const SingleOwnerInfo = ({ onChange={(e: ComboBoxEvent) => handleFundingSource(e.selectedItem)} invalid={ownerInfo.fundingSource.isInvalid} invalidText={inputText.funding.invalidText} - readOnly={readOnly && !isReview} + readOnly={isReadOnly} /> ) } @@ -258,7 +261,7 @@ const SingleOwnerInfo = ({ onChange={(e: ComboBoxEvent) => handleMethodOfPayment(e.selectedItem)} invalid={ownerInfo.methodOfPayment.isInvalid} invalidText={inputText.payment.invalidText} - readOnly={readOnly && !isReview} + readOnly={isReadOnly} /> ) @@ -276,6 +279,7 @@ const SingleOwnerInfo = ({ className="owner-mod-btn" renderIcon={TrashCan} onClick={() => deleteAnOwner(ownerInfo.id)} + disabled={isReadOnly} > Delete owner diff --git a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/index.tsx b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/index.tsx index bef78b0ec..3733db244 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/index.tsx +++ b/frontend/src/components/SeedlotRegistrationSteps/OwnershipStep/index.tsx @@ -18,6 +18,7 @@ import { EmptyMultiOptObj } from '../../../shared-constants/shared-constants'; import { THREE_HALF_HOURS, THREE_HOURS } from '../../../config/TimeUnits'; import { getMultiOptList } from '../../../utils/MultiOptionsUtils'; import getFundingSources from '../../../api-service/fundingSourcesAPI'; +import { getSeedlotFromOracleDbBySeedlotNumber } from '../../../api-service/seedlotAPI'; import TitleAccordion from '../../TitleAccordion'; import ScrollToTop from '../../ScrollToTop'; import SingleOwnerInfo from './SingleOwnerInfo'; @@ -53,7 +54,8 @@ const OwnershipStep = ({ isReview }: OwnershipStepProps) => { setStepData, defaultClientNumber: defaultAgency, defaultCode, - isFormSubmitted + isFormSubmitted, + seedlotNumber } = useContext(ClassAContext); const [accordionControls, setAccordionControls] = useState({}); @@ -107,6 +109,12 @@ const OwnershipStep = ({ isReview }: OwnershipStepProps) => { cacheTime: THREE_HALF_HOURS }); + const getSeedlotBySeedlotNumberQuery = useQuery( + ['get-seedlot-by-seedlotNumber', seedlotNumber], + () => getSeedlotFromOracleDbBySeedlotNumber(seedlotNumber ?? ''), + { enabled: !!seedlotNumber } + ); + // Set default method of payment for the first owner. useEffect(() => { if (methodsOfPaymentQuery.status === 'success') { @@ -143,6 +151,8 @@ const OwnershipStep = ({ isReview }: OwnershipStepProps) => { const getFcQuery = (clientNumber: string): ForestClientType | undefined => qc.getQueryData(['forest-clients', clientNumber]); + const originalSeedQty = getSeedlotBySeedlotNumberQuery.data?.data?.originalSeedQty ?? 0; + return (
@@ -209,8 +219,9 @@ const OwnershipStep = ({ isReview }: OwnershipStepProps) => { checkPortionSum={ (updtEntry: SingleOwnerForm, id: number) => checkPortionSum(updtEntry, id) } - readOnly={isFormSubmitted} + readOnly={isFormSubmitted || originalSeedQty > 0} isReview={isReview} + isOwnershipStep /> )) @@ -225,6 +236,7 @@ const OwnershipStep = ({ isReview }: OwnershipStepProps) => { className="owner-add-btn" renderIcon={Add} onClick={addAnOwner} + disabled={originalSeedQty > 0} > Add owner diff --git a/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/constants.tsx b/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/constants.tsx index 9ab03349a..be6230004 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/constants.tsx +++ b/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/constants.tsx @@ -219,12 +219,13 @@ export const geneticWorthDict: GeneticWorthDictType = { PW: ['dsb'], DR: ['gvo'], EP: ['gvo'], - FDI: ['gvo'], + FDI: ['gvo', 'wwd'], HW: ['gvo'], LW: ['gvo'], PY: ['gvo'], SS: ['gvo', 'iws'], SX: ['gvo', 'iws'], + YC: ['gvo'], UNKNOWN: ['gvo'] }; diff --git a/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/definitions.ts b/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/definitions.ts index 5c8ed38d9..56cf13077 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/definitions.ts +++ b/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/definitions.ts @@ -82,7 +82,21 @@ export type NotifCtrlType = { } } -type SpeciesWithGenWorth = 'CW'| 'PLI' |'FDC' | 'PW' | 'DR' | 'EP' | 'FDI' | 'HW' | 'LW' | 'PY' | 'SS' | 'SX' | 'UNKNOWN'; +type SpeciesWithGenWorth = + 'CW' + |'PLI' + |'FDC' + |'PW' + |'DR' + |'EP' + |'FDI' + |'HW' + |'LW' + |'PY' + |'SS' + |'SX' + |'YC' + |'UNKNOWN'; export type GeneticWorthDictType = { [key in SpeciesWithGenWorth]: string[] diff --git a/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/index.tsx b/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/index.tsx index e4bfa779c..186e23f79 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/index.tsx +++ b/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/index.tsx @@ -270,8 +270,7 @@ const ParentTreeStep = ({ isReviewDisplay, isReviewRead }: ParentTreeStepProps) setHeaderConfig, weightedGwInfoItems, setWeightedGwInfoItems, - setApplicableGenWorths, - isReviewDisplay ?? false + setApplicableGenWorths ), [seedlotSpecies]); const uploadCompostion = useMutation({ diff --git a/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/utils.ts b/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/utils.ts index 80679e1c8..68b2b0e43 100644 --- a/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/utils.ts +++ b/frontend/src/components/SeedlotRegistrationSteps/ParentTreeStep/utils.ts @@ -274,48 +274,35 @@ export const processParentTreeData = ( const applicableGenWorths = geneticWorthDict[speciesKey as keyof GeneticWorthDictType]; orchardParentTreeList.forEach((orchardPtNum) => { - if (!Object.prototype.hasOwnProperty.call(tableRowData, orchardPtNum)) { - const newRowData: RowItem = structuredClone(rowTemplate); + const newRowData: RowItem = !Object.prototype.hasOwnProperty.call(tableRowData, orchardPtNum) + ? structuredClone(rowTemplate) + : tableRowData[orchardPtNum]; - const parentTree = allParentTreeData[orchardPtNum]; + const parentTree = allParentTreeData[orchardPtNum]; - newRowData.parentTreeNumber.value = orchardPtNum; + newRowData.parentTreeNumber.value = orchardPtNum; - const genWorthBySpu = parentTree.geneticQualitiesBySpu; + const genWorthBySpu = parentTree.geneticQualitiesBySpu; - const validSpuIds = Object.keys(genWorthBySpu).map((key) => parseInt(key, 10)); + const validSpuIds = Object.keys(genWorthBySpu).map((key) => parseInt(key, 10)); - // If parent tree has gen worth data under the primary orchard's SPU then use them - // Else use default from the gen worth list - if (validSpuIds.includes(primarySpu)) { - const parentTreeGenWorthVals = genWorthBySpu[primarySpu]; - applicableGenWorths.forEach((gwCode) => { - const loweredGwCode = gwCode.toLowerCase() as keyof RowItem; - const matchedGwObj = parentTreeGenWorthVals - .find((gwObj) => gwObj.geneticWorthCode.toLowerCase() === loweredGwCode); + // If parent tree has gen worth data under the primary orchard's SPU then use them + // Else use default from the gen worth list + if (validSpuIds.includes(primarySpu)) { + const parentTreeGenWorthVals = genWorthBySpu[primarySpu]; + applicableGenWorths.forEach((gwCode) => { + const loweredGwCode = gwCode.toLowerCase() as keyof RowItem; + const matchedGwObj = parentTreeGenWorthVals + .find((gwObj) => gwObj.geneticWorthCode.toLowerCase() === loweredGwCode); - if (matchedGwObj) { - (newRowData[loweredGwCode] as GeneticWorthInputType) - .value = String(matchedGwObj.geneticQualityValue); - } else { - // Assign Default GW value - const foundGwDto = geneticWorthList - .find((gwDto) => gwDto.code.toLowerCase() === loweredGwCode); - - const defaultBv = foundGwDto ? foundGwDto.defaultBv.toFixed(1) : '0.0'; - if (foundGwDto) { - (newRowData[loweredGwCode] as GeneticWorthInputType) - .value = defaultBv; - (newRowData[loweredGwCode] as GeneticWorthInputType) - .isEstimated = true; - } - } - }); - } else { - applicableGenWorths.forEach((gwCode) => { - const loweredGwCode = gwCode.toLowerCase() as keyof RowItem; + if (matchedGwObj) { + (newRowData[loweredGwCode] as GeneticWorthInputType) + .value = String(matchedGwObj.geneticQualityValue); + } else { + // Assign Default GW value const foundGwDto = geneticWorthList .find((gwDto) => gwDto.code.toLowerCase() === loweredGwCode); + const defaultBv = foundGwDto ? foundGwDto.defaultBv.toFixed(1) : '0.0'; if (foundGwDto) { (newRowData[loweredGwCode] as GeneticWorthInputType) @@ -323,13 +310,26 @@ export const processParentTreeData = ( (newRowData[loweredGwCode] as GeneticWorthInputType) .isEstimated = true; } - }); - } - - tableRowData = Object.assign(tableRowData, { - [orchardPtNum]: populateStrInputId(orchardPtNum, newRowData) + } + }); + } else { + applicableGenWorths.forEach((gwCode) => { + const loweredGwCode = gwCode.toLowerCase() as keyof RowItem; + const foundGwDto = geneticWorthList + .find((gwDto) => gwDto.code.toLowerCase() === loweredGwCode); + const defaultBv = foundGwDto ? foundGwDto.defaultBv.toFixed(1) : '0.0'; + if (foundGwDto) { + (newRowData[loweredGwCode] as GeneticWorthInputType) + .value = defaultBv; + (newRowData[loweredGwCode] as GeneticWorthInputType) + .isEstimated = true; + } }); } + + tableRowData = Object.assign(tableRowData, { + [orchardPtNum]: populateStrInputId(orchardPtNum, newRowData) + }); }); modifiedState.tableRowData = tableRowData; @@ -545,8 +545,7 @@ export const configHeaderOpt = ( setHeaderConfig: Function, weightedGwInfoItems: Record, setWeightedGwInfoItems: Function, - setApplicableGenWorths: Function, - isReview: boolean + setApplicableGenWorths: Function ) => { const speciesKey = Object.keys(geneticWorthDict).includes(seedlotSpecies.code) ? seedlotSpecies.code.toUpperCase() @@ -562,10 +561,8 @@ export const configHeaderOpt = ( // Enable option in the column customization clonedHeaders[optionIndex].isAnOption = true; - // When on review mode, display all columns - if (isReview) { - clonedHeaders[optionIndex].enabled = true; - } + // Display all columns by default + clonedHeaders[optionIndex].enabled = true; // Enable weighted option in mix tab const weightedIndex = headerConfig.findIndex((header) => header.id === `w_${opt}`); @@ -736,6 +733,7 @@ export const generatePtValCalcPayload = ( pollenContaminantBreedingValue?: string ): PtValsCalcReqPayload => { const { tableRowData, mixTabData } = state; + const payload: PtValsCalcReqPayload = { orchardPtVals: [], smpMixIdAndProps: [], @@ -744,15 +742,21 @@ export const generatePtValCalcPayload = ( }; const rows = Object.values(tableRowData); const genWorthTypes = geneticWorthDict[seedlotSpecies.code as keyof GeneticWorthDictType]; + // When recalculating the value on the review section, + // if the values are null on the backend, they come to the frontend as + // a string "null" (for some reason...) rows.forEach((row) => { const newPayloadItem: OrchardParentTreeValsType = { parentTreeId: findParentTreeId(state, row.parentTreeNumber.value), parentTreeNumber: row.parentTreeNumber.value, coneCount: Number(row.coneCount.value), pollenCount: Number(row.pollenCount.value), - smpSuccessPerc: Number(row.smpSuccessPerc.value), + smpSuccessPerc: + (row.smpSuccessPerc.value && row.smpSuccessPerc.value !== 'null') + ? Number(row.smpSuccessPerc.value) + : 0, nonOrchardPollenContamPct: - row.nonOrchardPollenContam.value + (row.nonOrchardPollenContam.value && row.nonOrchardPollenContam.value !== 'null') ? Number(row.nonOrchardPollenContam.value) : 0, geneticTraits: [] @@ -923,15 +927,11 @@ export const isMissingSecondaryOrchard = (orchardStepData: OrchardForm): boolean * Check if orchards selections are valid. */ export const areOrchardsValid = (orchardStepData: OrchardForm): boolean => { - let isValid = true; const { orchards } = orchardStepData; if (!orchards.primaryOrchard.value.code) { - isValid = false; - } - if (isMissingSecondaryOrchard(orchardStepData)) { - isValid = false; + return false; } - return isValid; + return true; }; diff --git a/frontend/src/components/SeedlotTable/constants.ts b/frontend/src/components/SeedlotTable/constants.ts index 6a7afd360..03a0e1e8d 100644 --- a/frontend/src/components/SeedlotTable/constants.ts +++ b/frontend/src/components/SeedlotTable/constants.ts @@ -63,7 +63,7 @@ export const TableText = { }; export const PageSizesConfig = [ - 10, 20, 30, 40, 50 + 50, 100, 150, 200, 250 ]; export const ExclusiveAdminRows: Array = [ diff --git a/frontend/src/types/SeedlotType.ts b/frontend/src/types/SeedlotType.ts index 544245034..b3581ccf8 100644 --- a/frontend/src/types/SeedlotType.ts +++ b/frontend/src/types/SeedlotType.ts @@ -207,9 +207,9 @@ export type CollectionFormSubmitType = { collectionLocnCode: string, collectionStartDate: string, collectionEndDate: string, - noOfContainers: number, - volPerContainer: number, - clctnVolume: number, + noOfContainers: string, + volPerContainer: string, + clctnVolume: string, seedlotComment: string, coneCollectionMethodCodes: Array } diff --git a/frontend/src/views/Seedlot/ContextContainerClassA/constants.tsx b/frontend/src/views/Seedlot/ContextContainerClassA/constants.tsx index 7eb64a11c..f9a33bc13 100644 --- a/frontend/src/views/Seedlot/ContextContainerClassA/constants.tsx +++ b/frontend/src/views/Seedlot/ContextContainerClassA/constants.tsx @@ -120,9 +120,9 @@ export const emptyCollectionStep: CollectionFormSubmitType = { collectionLocnCode: '', collectionStartDate: '', collectionEndDate: '', - noOfContainers: 1, - volPerContainer: 1, - clctnVolume: 1, + noOfContainers: '', + volPerContainer: '', + clctnVolume: '', seedlotComment: '', coneCollectionMethodCodes: [] }; diff --git a/frontend/src/views/Seedlot/ContextContainerClassA/utils.ts b/frontend/src/views/Seedlot/ContextContainerClassA/utils.ts index 5a480774a..590b51f1f 100644 --- a/frontend/src/views/Seedlot/ContextContainerClassA/utils.ts +++ b/frontend/src/views/Seedlot/ContextContainerClassA/utils.ts @@ -852,9 +852,9 @@ export const convertCollection = (collectionData: CollectionForm): CollectionFor // Assume the date values are present as validation has occurred before payload is generated collectionStartDate: localDateToUtcFormat(collectionData.startDate.value)!, collectionEndDate: localDateToUtcFormat(collectionData.endDate.value)!, - noOfContainers: +collectionData.numberOfContainers.value, - volPerContainer: +collectionData.volumePerContainers.value, - clctnVolume: +collectionData.volumeOfCones.value, + noOfContainers: `${+collectionData.numberOfContainers.value}`, + volPerContainer: `${+collectionData.volumePerContainers.value}`, + clctnVolume: `${+collectionData.volumeOfCones.value}`, seedlotComment: collectionData.comments.value, coneCollectionMethodCodes: collectionData .selectedCollectionCodes.value.map((code) => parseInt(code, 10)) diff --git a/frontend/src/views/Seedlot/MySeedlots/index.tsx b/frontend/src/views/Seedlot/MySeedlots/index.tsx index f38f87631..43baaf8bb 100644 --- a/frontend/src/views/Seedlot/MySeedlots/index.tsx +++ b/frontend/src/views/Seedlot/MySeedlots/index.tsx @@ -66,7 +66,7 @@ const MySeedlots = () => { isSortable showSearch showPagination - defaultPageSize={20} + defaultPageSize={50} /> diff --git a/frontend/src/views/Seedlot/ReviewSeedlots/index.tsx b/frontend/src/views/Seedlot/ReviewSeedlots/index.tsx index 440039d18..4c6d973e9 100644 --- a/frontend/src/views/Seedlot/ReviewSeedlots/index.tsx +++ b/frontend/src/views/Seedlot/ReviewSeedlots/index.tsx @@ -57,7 +57,7 @@ const ReviewSeedlots = () => { isSortable showSearch showPagination - defaultPageSize={20} + defaultPageSize={50} />
diff --git a/frontend/src/views/Seedlot/SeedlotReview/SeedlotReviewContent.tsx b/frontend/src/views/Seedlot/SeedlotReview/SeedlotReviewContent.tsx index c2e6b6ac8..a7f45fb1f 100644 --- a/frontend/src/views/Seedlot/SeedlotReview/SeedlotReviewContent.tsx +++ b/frontend/src/views/Seedlot/SeedlotReview/SeedlotReviewContent.tsx @@ -544,22 +544,15 @@ const SeedlotReviewContent = () => { } /> - { - // Seedlots that have 'CUS' or 'UPT' as source should not be edited. - seedlotData?.seedlotSource.seedlotSourceCode === 'TPT' - ? ( - - ) - : null - } + { + + @Query("SELECT s FROM Seedlot s WHERE s.seedlotNumber = :seedlotNumber") + Seedlot findSeedlotBySeedlotNumber(@Param("seedlotNumber") String seedlotNumber); +} diff --git a/sync/src/module/data_synchronization.py b/sync/src/module/data_synchronization.py index 2f77a75b8..39eb39e04 100644 --- a/sync/src/module/data_synchronization.py +++ b/sync/src/module/data_synchronization.py @@ -310,7 +310,6 @@ def process_seedlots(oracle_config, postgres_config, track_config, track_db_conn logger.debug('Main driver SEEDLOT data loaded on in-memory dataframe') processes = [[{"interface_id":"SEEDLOT_EXTRACT","execution_id":"101","execution_order":"10","source_file":"/SQL/SPAR/POSTGRES_SEEDLOT_EXTRACT.sql","source_table":"spar.seedlot","source_db_type":"POSTGRES","target_table":"the.seedlot","target_primary_key":"seedlot_number","target_db_type":"ORACLE","run_mode":"UPSERT","ignore_columns_on_update":"extraction_st_date,extraction_end_date,seed_store_client_number,seed_store_client_locn,temporary_storage_start_date,temporary_storage_end_date,seedlot_status_code"}] - ,[{"interface_id":"SEEDLOT_OWNER_QUANTITY_EXTRACT","execution_id":"102","execution_order":"20","source_file":"/SQL/SPAR/POSTGRES_SEEDLOT_OWNER_QUANTITY_EXTRACT.sql","source_table":"spar.seedlot_owner_quantity","source_db_type":"POSTGRES","target_table":"the.seedlot_owner_quantity","target_primary_key":"seedlot_number,client_number,client_locn_code","target_db_type":"ORACLE","run_mode":"UPSERT","ignore_columns_on_update":"qty_reserved,qty_rsrvd_cmtd_pln,qty_rsrvd_cmtd_apr,qty_surplus,qty_srpls_cmtd_pln,qty_srpls_cmtd_apr"}] ,[{"interface_id":"SEEDLOT_SEED_PLAN_ZONE_EXTRACT","execution_id":"103","execution_order":"30","source_file":"/SQL/SPAR/POSTGRES_SEEDLOT_SEED_PLAN_ZONE_EXTRACT.sql","source_table":"spar.seedlot_seed_plan_zone","source_db_type":"POSTGRES","target_table":"the.seedlot_plan_zone","target_primary_key":"seedlot_number,seed_plan_zone_code","target_db_type":"ORACLE","run_mode":"UPSERT_WITH_DELETE","ignore_columns_on_update":""}] ,[{"interface_id":"SEEDLOT_GENETIC_WORTH_EXTRACT","execution_id":"104","execution_order":"40","source_file":"/SQL/SPAR/POSTGRES_SEEDLOT_GENETIC_WORTH_EXTRACT.sql","source_table":"spar.seedlot_genetic_worth","source_db_type":"POSTGRES","target_table":"the.seedlot_genetic_worth","target_primary_key":"seedlot_number,genetic_worth_code","target_db_type":"ORACLE","run_mode":"UPSERT_WITH_DELETE","ignore_columns_on_update":""}] ,[{"interface_id":"SEEDLOT_PARENT_TREE_EXTRACT","execution_id":"105","execution_order":"50","source_file":"/SQL/SPAR/POSTGRES_SEEDLOT_PARENT_TREE_EXTRACT.sql","source_table":"spar.seedlot_parent_tree","source_db_type":"POSTGRES","target_table":"the.seedlot_parent_tree","target_primary_key":"seedlot_number,parent_tree_id","target_db_type":"ORACLE","run_mode":"UPSERT_WITH_DELETE","ignore_columns_on_update":""}] @@ -319,6 +318,7 @@ def process_seedlots(oracle_config, postgres_config, track_config, track_db_conn ,[{"interface_id":"SMP_MIX_EXTRACT","execution_id":"108","execution_order":"80","source_file":"/SQL/SPAR/POSTGRES_SMP_MIX_EXTRACT.sql","source_table":"spar.smp_mix","source_db_type":"POSTGRES","target_table":"the.smp_mix","target_primary_key":"seedlot_number,parent_tree_id","target_db_type":"ORACLE","run_mode":"UPSERT_WITH_DELETE","ignore_columns_on_update":""}] ,[{"interface_id":"SMP_MIX_GEN_QLTY_EXTRACT","execution_id":"109","execution_order":"90","source_file":"/SQL/SPAR/POSTGRES_SMP_MIX_GEN_QLTY_EXTRACT.sql","source_table":"spar.smp_mix_gen_qlty","source_db_type":"POSTGRES","target_table":"the.smp_mix_gen_qlty","target_primary_key":"seedlot_number,parent_tree_id,genetic_type_code,genetic_worth_code","target_db_type":"ORACLE","run_mode":"UPSERT_WITH_DELETE","ignore_columns_on_update":""}] ] + with db_conn.database_connection(oracle_config) as target_db_conn: try: seedlotlst = [] @@ -327,6 +327,14 @@ def process_seedlots(oracle_config, postgres_config, track_config, track_db_conn seedlot_metrics['step'] = "Seedlot" seedlot_metrics['seedlot_number'] = seedlot.seedlot_number + original_seed_qty_query = "SELECT ORIGINAL_SEED_QTY FROM the.seedlot WHERE seedlot_number = :seedlot_number" + result = target_db_conn.execute(original_seed_qty_query, params={"seedlot_number": seedlot.seedlot_number}) + + if result is not None: + original_seed_qty = result.fetchone() + if original_seed_qty and not original_seed_qty[0]: + processes.append([{"interface_id":"SEEDLOT_OWNER_QUANTITY_EXTRACT","execution_id":"102","execution_order":"20","source_file":"/SQL/SPAR/POSTGRES_SEEDLOT_OWNER_QUANTITY_EXTRACT.sql","source_table":"spar.seedlot_owner_quantity","source_db_type":"POSTGRES","target_table":"the.seedlot_owner_quantity","target_primary_key":"seedlot_number,client_number,client_locn_code","target_db_type":"ORACLE","run_mode":"UPSERT","ignore_columns_on_update":"qty_reserved,qty_rsrvd_cmtd_pln,qty_rsrvd_cmtd_apr,qty_surplus,qty_srpls_cmtd_pln,qty_srpls_cmtd_apr"}]) + #delete all tables in RI order (reversing order of processes dataframe) #note - special handling for seedlot_owner_quantity delete_metrics = delete_seedlot_child_tables(seedlot_number=seedlot.seedlot_number, diff --git a/sync/src/module/database_connection.py b/sync/src/module/database_connection.py index 4d9d0c150..e105b9d8a 100644 --- a/sync/src/module/database_connection.py +++ b/sync/src/module/database_connection.py @@ -35,11 +35,12 @@ def __exit__(self, exc_type, exc_val, exc_tb): def execute(self, query, params): """ Runs a SQL statement. """ - self.conn.execute(text(query), params) + result = self.conn.execute(text(query), params or {}) + return result def select(self, query, params=None): """ Runs a SQL statement. """ - result = self.conn.execute(text(query), params) + result = self.conn.execute(text(query), params or {}) return result def health_check(self) -> bool: