From 5856f3ec5a283cc23e23d4d698b4a98226a90039 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Wed, 24 Apr 2024 11:21:18 -0400 Subject: [PATCH] feat: add unit form --- .../api/cadt/v1/projects/projects.api.ts | 8 +- src/renderer/api/cadt/v1/units/units.api.ts | 16 +- .../{CoBenifetForm.tsx => CoBenifetsForm.tsx} | 10 +- ...EstimationForm.tsx => EstimationsForm.tsx} | 8 +- .../{IssuanceForm.tsx => IssuancesForm.tsx} | 8 +- .../forms/{LabelForm.tsx => LabelsForm.tsx} | 28 +-- .../components/blocks/forms/ProjectForm.tsx | 38 ++-- ...ationForm.tsx => ProjectLocationsForm.tsx} | 8 +- .../forms/{RatingForm.tsx => RatingsForm.tsx} | 10 +- ...rojectForm.tsx => RelatedProjectsForm.tsx} | 8 +- .../components/blocks/forms/UnitForm.tsx | 154 ++++++++++------ .../blocks/forms/UnitIssuanceForm.tsx | 82 +++++++++ src/renderer/components/blocks/forms/index.ts | 15 +- .../components/blocks/modals/ProjectModal.tsx | 24 +-- .../blocks/modals/StagedUnitSuccessModal.tsx | 29 +++ .../components/blocks/modals/UnitModal.tsx | 13 +- .../blocks/modals/UpsertProjectModal.tsx | 106 ++++++----- .../blocks/modals/UpsertUnitModal.tsx | 174 ++++++++++++++++-- .../components/blocks/modals/index.ts | 1 + .../blocks/widgets/SyncIndicator.tsx | 2 +- src/renderer/components/form/Field.tsx | 2 +- .../layout/ComponentCenteredSpinner.tsx | 7 +- src/renderer/pages/MyUnitsPage.tsx | 5 + src/renderer/schemas/Issuance.schema.ts | 2 +- src/renderer/schemas/Project.schema.ts | 34 ++-- src/renderer/schemas/Unit.schema.ts | 30 ++- src/renderer/translations/tokens/en-US.json | 4 +- src/renderer/utils/formik-utils.ts | 2 +- 28 files changed, 564 insertions(+), 264 deletions(-) rename src/renderer/components/blocks/forms/{CoBenifetForm.tsx => CoBenifetsForm.tsx} (86%) rename src/renderer/components/blocks/forms/{EstimationForm.tsx => EstimationsForm.tsx} (91%) rename src/renderer/components/blocks/forms/{IssuanceForm.tsx => IssuancesForm.tsx} (93%) rename src/renderer/components/blocks/forms/{LabelForm.tsx => LabelsForm.tsx} (82%) rename src/renderer/components/blocks/forms/{ProjectLocationForm.tsx => ProjectLocationsForm.tsx} (91%) rename src/renderer/components/blocks/forms/{RatingForm.tsx => RatingsForm.tsx} (91%) rename src/renderer/components/blocks/forms/{RelatedProjectForm.tsx => RelatedProjectsForm.tsx} (91%) create mode 100644 src/renderer/components/blocks/forms/UnitIssuanceForm.tsx create mode 100644 src/renderer/components/blocks/modals/StagedUnitSuccessModal.tsx diff --git a/src/renderer/api/cadt/v1/projects/projects.api.ts b/src/renderer/api/cadt/v1/projects/projects.api.ts index 152ef432..50c20c13 100644 --- a/src/renderer/api/cadt/v1/projects/projects.api.ts +++ b/src/renderer/api/cadt/v1/projects/projects.api.ts @@ -81,5 +81,9 @@ const projectsApi = cadtApi.injectEndpoints({ }), }); -export const { useGetProjectsQuery, useGetProjectQuery, useDeleteProjectMutation, useStageProjectMutation } = - projectsApi; +export const { + useGetProjectsQuery, + useGetProjectQuery, + useDeleteProjectMutation, + useStageProjectMutation +} = projectsApi; diff --git a/src/renderer/api/cadt/v1/units/units.api.ts b/src/renderer/api/cadt/v1/units/units.api.ts index 21357ba7..58aac5f0 100644 --- a/src/renderer/api/cadt/v1/units/units.api.ts +++ b/src/renderer/api/cadt/v1/units/units.api.ts @@ -75,14 +75,18 @@ const unitsApi = cadtApi.injectEndpoints({ }), stageUnit: builder.mutation({ - query: (unit) => ({ - url: `/v1/units`, - method: 'POST', - body: unit, - }), + query: (unit) => { + delete unit.warehouseProjectId; + + return { + url: `/v1/units`, + method: 'POST', + body: unit, + }; + }, invalidatesTags: [stagedUnitsTag], }), }), }); -export const { useGetUnitsQuery, useGetUnitQuery, useDeleteUnitMutation } = unitsApi; +export const { useGetUnitsQuery, useGetUnitQuery, useDeleteUnitMutation, useStageUnitMutation } = unitsApi; diff --git a/src/renderer/components/blocks/forms/CoBenifetForm.tsx b/src/renderer/components/blocks/forms/CoBenifetsForm.tsx similarity index 86% rename from src/renderer/components/blocks/forms/CoBenifetForm.tsx rename to src/renderer/components/blocks/forms/CoBenifetsForm.tsx index 8e532cf6..06f4ce08 100644 --- a/src/renderer/components/blocks/forms/CoBenifetForm.tsx +++ b/src/renderer/components/blocks/forms/CoBenifetsForm.tsx @@ -1,4 +1,4 @@ -import React, { useRef, forwardRef, useImperativeHandle } from 'react'; +import { useRef, forwardRef, useImperativeHandle } from 'react'; import { Formik, Form, FormikProps } from 'formik'; import { Field, Repeater } from '@/components'; import * as yup from 'yup'; @@ -14,17 +14,17 @@ const validationSchema = yup.object({ ), }); -interface CoBenefitFormProps { +interface CoBenefitsFormProps { readonly?: boolean; data?: CoBenefit[]; picklistOptions?: PickList; } -export interface CoBenefitFormRef { +export interface CoBenefitsFormRef { submitForm: () => Promise; } -const CoBenefitForm = forwardRef( +const CoBenefitsForm = forwardRef( ({ readonly = false, data, picklistOptions }, ref) => { const formikRef = useRef>(null); @@ -69,4 +69,4 @@ const CoBenefitForm = forwardRef( } ); -export { CoBenefitForm }; +export { CoBenefitsForm }; diff --git a/src/renderer/components/blocks/forms/EstimationForm.tsx b/src/renderer/components/blocks/forms/EstimationsForm.tsx similarity index 91% rename from src/renderer/components/blocks/forms/EstimationForm.tsx rename to src/renderer/components/blocks/forms/EstimationsForm.tsx index 4fe205b7..cb947195 100644 --- a/src/renderer/components/blocks/forms/EstimationForm.tsx +++ b/src/renderer/components/blocks/forms/EstimationsForm.tsx @@ -19,16 +19,16 @@ const validationSchema = yup.object({ ), }); -interface EstimationFormProps { +interface EstimationsFormProps { readonly?: boolean; data?: Estimation[]; } -export interface EstimationFormRef { +export interface EstimationsFormRef { submitForm: () => Promise; } -const EstimationForm = forwardRef( +const EstimationsForm = forwardRef( ({ readonly = false, data, }, ref) => { const formikRef = useRef>(null); @@ -91,4 +91,4 @@ const EstimationForm = forwardRef( } ); -export { EstimationForm }; +export { EstimationsForm }; diff --git a/src/renderer/components/blocks/forms/IssuanceForm.tsx b/src/renderer/components/blocks/forms/IssuancesForm.tsx similarity index 93% rename from src/renderer/components/blocks/forms/IssuanceForm.tsx rename to src/renderer/components/blocks/forms/IssuancesForm.tsx index 2ecc96ad..8c8d4857 100644 --- a/src/renderer/components/blocks/forms/IssuanceForm.tsx +++ b/src/renderer/components/blocks/forms/IssuancesForm.tsx @@ -21,20 +21,20 @@ const validationSchema = yup.object({ ), }); -interface IssuanceFormProps { +interface IssuancesFormProps { readonly?: boolean; data?: Issuance[] | undefined; showUnits?: boolean; picklistOptions?: PickList | undefined; } -export interface IssuanceFormRef { +export interface IssuancesFormRef { submitForm: () => Promise; } const defaultIssuanceData: Issuance[] = []; -const IssuanceForm = forwardRef( +const IssuancesForm = forwardRef( ({ readonly = false, data = defaultIssuanceData, showUnits = false, picklistOptions }, ref) => { const formikRef = useRef>(null); @@ -121,4 +121,4 @@ const IssuanceForm = forwardRef( }, ); -export { IssuanceForm }; +export { IssuancesForm }; diff --git a/src/renderer/components/blocks/forms/LabelForm.tsx b/src/renderer/components/blocks/forms/LabelsForm.tsx similarity index 82% rename from src/renderer/components/blocks/forms/LabelForm.tsx rename to src/renderer/components/blocks/forms/LabelsForm.tsx index 2dd4fe5f..094de555 100644 --- a/src/renderer/components/blocks/forms/LabelForm.tsx +++ b/src/renderer/components/blocks/forms/LabelsForm.tsx @@ -13,22 +13,14 @@ const validationSchema = yup.object({ labelType: yup.string().required('Label type is required'), labelLink: yup.string().url('Must be a valid URL').nullable(), validityPeriodStartDate: yup.date().nullable(), - validityPeriodEndDate: yup - .date() - .nullable() + validityPeriodEndDate: yup.date().nullable() .when('validityPeriodStartDate', (validityPeriodStartDate, schema) => - validityPeriodStartDate - ? schema.min(validityPeriodStartDate, 'Validity Period End Date must be after the start date') - : schema, + validityPeriodStartDate ? schema.min(validityPeriodStartDate, 'Validity Period End Date must be after the start date') : schema ), creditingPeriodStartDate: yup.date().nullable(), - creditingPeriodEndDate: yup - .date() - .nullable() + creditingPeriodEndDate: yup.date().nullable() .when('creditingPeriodStartDate', (creditingPeriodStartDate, schema) => - creditingPeriodStartDate - ? schema.min(creditingPeriodStartDate, 'Crediting Period End Date must be after the start date') - : schema, + creditingPeriodStartDate ? schema.min(creditingPeriodStartDate, 'Crediting Period End Date must be after the start date') : schema ), unitQuantity: yup .number() @@ -39,17 +31,17 @@ const validationSchema = yup.object({ ), }); -interface LabelFormFormProps { +interface LabelsFormProps { readonly?: boolean; data?: Label[]; picklistOptions?: PickList | null; } -export interface LabelFormRef { +export interface LabelsFormRef { submitForm: () => Promise; } -const LabelForm = forwardRef( +const LabelsForm = forwardRef( ({ readonly = false, data, picklistOptions }, ref) => { const formikRef = useRef>(null); @@ -86,7 +78,7 @@ const LabelForm = forwardRef( {(label: Label, index: number, name: string) => ( <>
- + ( readonly={readonly} initialValue={label.labelLink} /> -
-
( }, ); -export { LabelForm }; +export { LabelsForm }; diff --git a/src/renderer/components/blocks/forms/ProjectForm.tsx b/src/renderer/components/blocks/forms/ProjectForm.tsx index b91fd0c9..e902d35c 100644 --- a/src/renderer/components/blocks/forms/ProjectForm.tsx +++ b/src/renderer/components/blocks/forms/ProjectForm.tsx @@ -45,25 +45,25 @@ export interface ProjectFormRef { } const defaultProjectData: Project = { - projectName: '', - projectId: '', - projectDeveloper: '', - program: '', - projectLink: '', - sector: '', - projectType: '', - projectStatus: '', - projectStatusDate: new Date(), - coveredByNDC: '', - ndcInformation: '', - currentRegistry: '', - registryOfOrigin: '', - originProjectId: '', - unitMetric: '', - methodology: '', - validationBody: '', - validationDate: new Date(), - projectTags: '', + projectName: null, + projectId: null, + projectDeveloper: null, + program: null, + projectLink: null, + sector: null, + projectType: null, + projectStatus: null, + projectStatusDate: null, + coveredByNDC: null, + ndcInformation: null, + currentRegistry: null, + registryOfOrigin: null, + originProjectId: null, + unitMetric: null, + methodology: null, + validationBody: null, + validationDate: null, + projectTags: null, }; const getDefaultInitialValues = (data: Partial): Project => { diff --git a/src/renderer/components/blocks/forms/ProjectLocationForm.tsx b/src/renderer/components/blocks/forms/ProjectLocationsForm.tsx similarity index 91% rename from src/renderer/components/blocks/forms/ProjectLocationForm.tsx rename to src/renderer/components/blocks/forms/ProjectLocationsForm.tsx index 26408ace..90a771b8 100644 --- a/src/renderer/components/blocks/forms/ProjectLocationForm.tsx +++ b/src/renderer/components/blocks/forms/ProjectLocationsForm.tsx @@ -17,17 +17,17 @@ const validationSchema = yup.object({ ), }); -interface ProjectLocationFormProps { +interface ProjectLocationsFormProps { readonly?: boolean; data?: ProjectLocation[]; picklistOptions?: PickList; } -export interface ProjectLocationFormRef { +export interface ProjectLocationsFormRef { submitForm: () => Promise; } -const ProjectLocationForm = forwardRef( +const ProjectLocationsForm = forwardRef( ({ readonly = false, data, picklistOptions }, ref) => { const formikRef = useRef>(null); @@ -98,4 +98,4 @@ const ProjectLocationForm = forwardRef Promise; } -const RatingForm = forwardRef( +const RatingsForm = forwardRef( ({ readonly = false, data, picklistOptions }, ref) => { const formikRef = useRef>(null); @@ -109,4 +109,4 @@ const RatingForm = forwardRef( }, ); -export { RatingForm }; +export { RatingsForm }; diff --git a/src/renderer/components/blocks/forms/RelatedProjectForm.tsx b/src/renderer/components/blocks/forms/RelatedProjectsForm.tsx similarity index 91% rename from src/renderer/components/blocks/forms/RelatedProjectForm.tsx rename to src/renderer/components/blocks/forms/RelatedProjectsForm.tsx index 969a9669..c02c002b 100644 --- a/src/renderer/components/blocks/forms/RelatedProjectForm.tsx +++ b/src/renderer/components/blocks/forms/RelatedProjectsForm.tsx @@ -16,16 +16,16 @@ const validationSchema = yup.object({ ), }); -interface RelatedProjectFormProps { +interface RelatedProjectsFormProps { readonly?: boolean; data?: RelatedProject[]; } -export interface RelatedProjectFormRef { +export interface RelatedProjectsFormRef { submitForm: () => Promise; } -const RelatedProjectForm = forwardRef( +const RelatedProjectsForm = forwardRef( ({ readonly = false, data }, ref) => { const formikRef = useRef>(null); const [orgUid] = useQueryParamState('orgUid'); @@ -96,4 +96,4 @@ const RelatedProjectForm = forwardRef = ({ readonly = false, data, picklistOptions }) => { +export interface UnitFormRef { + submitForm: () => Promise; +} + +const UnitForm = forwardRef(({ readonly = false, data: unit, picklistOptions }, ref) => { + const formikRef = useRef>(null); const { data: homeOrg, isLoading: isHomeOrgLoading } = useGetHomeOrgQuery(); + const [error, setError] = useState(null); const { projects: projectOptions, isLoading: isProjectOptionsLoading } = useGetProjectOptionsList(homeOrg?.orgUid); - const [selectedWarehouseProjectId, setSelectedWarehouseProjectId] = useState(); + const [selectedWarehouseProjectId, setSelectedWarehouseProjectId] = useState( + unit?.warehouseProjectId?.toString(), + ); - const { data: projectData, isLoading: isProjectLoading, isFetching: isProjectFetching } = useGetProjectQuery( + const { + data: projectData, + isLoading: isProjectLoading, + isFetching: isProjectFetching, + } = useGetProjectQuery( { // @ts-ignore warehouseProjectId: selectedWarehouseProjectId, @@ -59,28 +75,58 @@ const UnitForm: React.FC = ({ readonly = false, data, picklistOpt ); }, [projectData, isProjectLoading]); - if (isHomeOrgLoading || isProjectOptionsLoading || isProjectLoading || isProjectFetching) { - return ; + useImperativeHandle(ref, () => ({ + submitForm: async () => { + if (formikRef.current) { + const formik = formikRef.current; + if (formik) { + const errors = await formik.validateForm(formik.values); + formik.setTouched(Object.keys(errors).reduce((acc, key) => ({ ...acc, [key]: true }), {})); + + if (selectedWarehouseProjectId) { + formik.values.warehouseProjectId = selectedWarehouseProjectId; + } else { + const error = { warehouseProjectId: 'A valid project must be selected' }; + setError(error.warehouseProjectId); + return [{...errors, ...error}, formik.values]; + } + + return [errors, formik.values]; + } + } + }, + })); + + const handleSetWarehouseProjectId = (event: React.ChangeEvent) => { + setError(null); + setSelectedWarehouseProjectId(event.target.value); + }; + + if (isHomeOrgLoading || isProjectOptionsLoading) { + return ; + } + + if (isProjectLoading || isProjectFetching) { + return ; } return ( - {}}> + {}}> {() => (
- - + {error &&

{error}

}
= ({ readonly = false, data, picklistOpt type="picklist" options={projectLocationOptions} readonly={readonly} - initialValue={data?.projectLocationId} + initialValue={unit?.projectLocationId} /> = ({ readonly = false, data, picklistOpt label="Unit Block End" type="text" readonly={readonly} - initialValue={data?.unitBlockEnd} + initialValue={unit?.unitBlockEnd} /> - - = ({ readonly = false, data, picklistOpt type="picklist" options={picklistOptions?.unitType} readonly={readonly} - initialValue={data?.unitType} + initialValue={unit?.unitType} /> + +
@@ -178,7 +226,7 @@ const UnitForm: React.FC = ({ readonly = false, data, picklistOpt label="Unit Registry Link" type="link" readonly={readonly} - initialValue={data?.unitRegistryLink} + initialValue={unit?.unitRegistryLink} />
@@ -189,7 +237,7 @@ const UnitForm: React.FC = ({ readonly = false, data, picklistOpt label="Marketplace" type="text" readonly={readonly} - initialValue={data?.marketplace} + initialValue={unit?.marketplace} /> = ({ readonly = false, data, picklistOpt label="Marketplace Identifier" type="text" readonly={readonly} - initialValue={data?.marketplaceIdentifier} + initialValue={unit?.marketplaceIdentifier} /> = ({ readonly = false, data, picklistOpt label="Marketplace Link" type="text" readonly={readonly} - initialValue={data?.marketplaceLink} + initialValue={unit?.marketplaceLink} /> = ({ readonly = false, data, picklistOpt type="picklist" options={picklistOptions?.correspondingAdjustmentStatus} readonly={readonly} - initialValue={data?.correspondingAdjustmentStatus} + initialValue={unit?.correspondingAdjustmentStatus} /> = ({ readonly = false, data, picklistOpt type="picklist" options={picklistOptions?.correspondingAdjustmentDeclaration} readonly={readonly} - initialValue={data?.correspondingAdjustmentDeclaration} + initialValue={unit?.correspondingAdjustmentDeclaration} />
- +
)} ); -}; +}); export { UnitForm }; diff --git a/src/renderer/components/blocks/forms/UnitIssuanceForm.tsx b/src/renderer/components/blocks/forms/UnitIssuanceForm.tsx new file mode 100644 index 00000000..b149d490 --- /dev/null +++ b/src/renderer/components/blocks/forms/UnitIssuanceForm.tsx @@ -0,0 +1,82 @@ +import { omit } from 'lodash'; +import { isEmpty } from 'lodash'; +import { forwardRef, useImperativeHandle, useState, useMemo, useCallback } from 'react'; +import { Issuance } from '@/schemas/Issuance.schema'; +import { useGetProjectQuery } from '@/api'; +import { SelectOption, Select, IssuancesForm, Spacer } from '@/components'; +import dayjs from 'dayjs'; + +interface UnitIssuanceFormProps { + readonly?: boolean; + data?: Issuance | null; + selectedWarehouseProjectId?: string | null; +} + +export interface UnitIssuanceFormRef { + submitForm: () => Promise; +} + +const UnitIssuanceForm = forwardRef( + ({ data: issuance, selectedWarehouseProjectId }, ref) => { + console.log(issuance, selectedWarehouseProjectId) + const [selectedIssuance, setSelectedIssuance] = useState(issuance || undefined); + const [error, setError] = useState(null); + const { + data: projectData, + isLoading: isProjectLoading, + } = useGetProjectQuery( + // @ts-ignore + { warehouseProjectId: selectedWarehouseProjectId }, + { skip: !selectedWarehouseProjectId }, + ); + + useImperativeHandle(ref, () => ({ + submitForm: async () => { + const errors: string[] = []; + if (!selectedIssuance) { + setError('A valid issuance must be selected'); + return; + } + + return [isEmpty(errors) ? null : errors, { issuance: omit(selectedIssuance, ['orgUid'])}]; + }, + })); + + const issuanceOptions = useMemo(() => { + if (isProjectLoading || !projectData?.issuances) { + return []; + } + return projectData.issuances.map((issuance): SelectOption => ({ + label: `${dayjs(issuance.startDate).format('DD/MM/YYYY')} - ${dayjs(issuance.endDate).format('DD/MM/YYYY')}`, + value: issuance.id || '', + })); + }, [projectData, isProjectLoading]); + + const handleSetIssuance = useCallback((event: React.ChangeEvent) => { + setError(null); + const selectedIssuanceId = event.target.value; + const foundIssuance = projectData?.issuances?.find(issuance => issuance.id === selectedIssuanceId); + setSelectedIssuance(foundIssuance); + }, [projectData]); + + return ( +
+