From 24844b816e29c8f26a6de0d72e4ef9a56f274efb Mon Sep 17 00:00:00 2001 From: Shreeyash Shrestha Date: Thu, 23 Jan 2025 14:35:31 +0545 Subject: [PATCH 1/3] Add new proposed action section in Dref Application form --- .../Operation/ProposedActionsInput/i18n.json | 8 + .../Operation/ProposedActionsInput/index.tsx | 110 ++++++ .../ProposedActionsInput/styles.module.css | 8 + .../DrefApplicationForm/Operation/i18n.json | 12 +- .../DrefApplicationForm/Operation/index.tsx | 346 +++++++++++++++++- .../Operation/styles.module.css | 2 +- app/src/views/DrefApplicationForm/common.tsx | 19 + app/src/views/DrefApplicationForm/index.tsx | 18 + app/src/views/DrefApplicationForm/schema.ts | 105 +++++- 9 files changed, 612 insertions(+), 16 deletions(-) create mode 100644 app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/i18n.json create mode 100644 app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx create mode 100644 app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/styles.module.css diff --git a/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/i18n.json b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/i18n.json new file mode 100644 index 0000000000..f1731886cf --- /dev/null +++ b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/i18n.json @@ -0,0 +1,8 @@ +{ + "namespace": "drefAppicationForm", + "strings": { + "drefFormProposedActionActivityLabel": "Activity", + "drefFormProposedActionBudgetLabel": "Budget(CHF)", + "drefFormRemoveActivity": "Remove Activity" + } +} diff --git a/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx new file mode 100644 index 0000000000..329b71a9e7 --- /dev/null +++ b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx @@ -0,0 +1,110 @@ +import { DeleteBinLineIcon } from '@ifrc-go/icons'; +import { + IconButton, + NumberInput, + SelectInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + type ArrayError, + getErrorObject, + type SetValueArg, + useFormObject, +} from '@togglecorp/toggle-form'; + +import { type GoApiResponse } from '#utils/restRequest'; + +import { type PartialDref } from '../../schema'; + +import i18n from './i18n.json'; +import styles from './styles.module.css'; + +type ProposedActionsFormFields = NonNullable[number]; +type ActivityOptions = NonNullable>[number]; + +function activityLabelSelector(option: ActivityOptions) { + return option.label; +} + +function activityKeySelector(option: ActivityOptions) { + return option.key; +} + +const defaultProposedActionsValue: ProposedActionsFormFields = { + client_id: '-1', +}; + +interface Props { + value: ProposedActionsFormFields; + error: ArrayError | undefined; + onChange: ( + value: SetValueArg, + index: number, + ) => void; + onRemove: (index: number) => void; + index: number; + disabled?: boolean; + activityOptions?: ActivityOptions[]; +} +function ProposedActionsInput(props: Props) { + const { + error: errorFromProps, + onChange, + value, + onRemove, + index, + activityOptions, + disabled, + } = props; + + const strings = useTranslation(i18n); + + const onProposedActionChange = useFormObject(index, onChange, defaultProposedActionsValue); + + const error = (value && value.client_id && errorFromProps) + ? getErrorObject(errorFromProps?.[value.client_id]) + : undefined; + + return ( +
+
+ +
+ + + + +
+ ); +} + +export default ProposedActionsInput; diff --git a/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/styles.module.css b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/styles.module.css new file mode 100644 index 0000000000..67f522b062 --- /dev/null +++ b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/styles.module.css @@ -0,0 +1,8 @@ +.action-input-container { + display: flex; + gap: var(--go-ui-spacing-md); + + .activities { + flex-grow: 1; + } +} diff --git a/app/src/views/DrefApplicationForm/Operation/i18n.json b/app/src/views/DrefApplicationForm/Operation/i18n.json index 8eb2992131..e6a978a9fe 100644 --- a/app/src/views/DrefApplicationForm/Operation/i18n.json +++ b/app/src/views/DrefApplicationForm/Operation/i18n.json @@ -8,6 +8,7 @@ "drefFormPeopleAssistedThroughOperationDescription": "Explain the logic behind our targets. Which groups are we targeting and why are we targeting these particular groups? Explain how you will target vulnerable groups (e.g., Migrants, refugees, etc.)", "drefFormPeopleTargetedWithEarlyActions": "Numbers of persons targeted with early actions (if any)", "drefFormPlannedIntervention": "Planned Intervention", + "drefFormProposedActions": "Proposed Actions", "drefFormRequestAmount": "Requested Amount in CHF", "drefFormPmer": "How will this operation be monitored?", "drefFormPmerDescription": "Will there be IFRC monitoring visits? How will it be deployed?", @@ -56,6 +57,15 @@ "drefFormTotalTargeted": "Total targeted population is different from that in Operation Overview", "drefFormTotalTargetedPopulation": "Total targeted population is not equal to sum of other population fields", "drefFormUploadTargetingSupportingDocument": "Upload any additional support document (Optional)", - "drefFormUploadTargetingDocumentButtonLabel": "Upload document" + "drefFormUploadTargetingDocumentButtonLabel": "Upload document", + "drefFormProposedActionSelectBudgetNote": "If Surge Personnel are deployed, costs include CHF 10,000 for deployment and CHF 5,800 in indirect costs. Without deployment, only CHF 5,000 in indirect costs apply.", + "drefFormProposedActionSelectActivitiesLabel": "Select the activity", + "drefFormProposedActionEarlyActionsLabel": "Early Actions", + "drefFormProposedActionEarlyResponseLabel": "Early Response", + "drefFormProposedActionAddActivityLabel": "Add Activity", + "drefFormProposedActionSubTotal": "Sub-total", + "drefFormProposedActionSurgeDeployment": "Surge Deployment", + "drefFormProposedActionIndirectCost": "Indirect cost", + "drefFormProposedActionTotal": "Total" } } diff --git a/app/src/views/DrefApplicationForm/Operation/index.tsx b/app/src/views/DrefApplicationForm/Operation/index.tsx index 57afafdb9c..f0c7bf7821 100644 --- a/app/src/views/DrefApplicationForm/Operation/index.tsx +++ b/app/src/views/DrefApplicationForm/Operation/index.tsx @@ -20,7 +20,9 @@ import { sumSafe, } from '@ifrc-go/ui/utils'; import { + isDefined, isNotDefined, + listToGroupList, listToMap, randomString, } from '@togglecorp/fujs'; @@ -28,6 +30,9 @@ import { type EntriesAsList, type Error, getErrorObject, + isCallable, + type SetBaseValueArg, + type SetValueArg, useFormArray, } from '@togglecorp/toggle-form'; @@ -35,14 +40,19 @@ import GoSingleFileInput from '#components/domain/GoSingleFileInput'; import Link from '#components/Link'; import NonFieldError from '#components/NonFieldError'; import useGlobalEnums from '#hooks/domain/useGlobalEnums'; -import { type GoApiResponse } from '#utils/restRequest'; +import { + type GoApiResponse, + useRequest, +} from '#utils/restRequest'; import { + recalculateProposedActionValues, TYPE_ASSESSMENT, TYPE_IMMINENT, } from '../common'; import { type PartialDref } from '../schema'; import InterventionInput from './InterventionInput'; +import ProposedActionsInput from './ProposedActionsInput'; import RiskSecurityInput from './RiskSecurityInput'; import i18n from './i18n.json'; @@ -50,18 +60,33 @@ import styles from './styles.module.css'; type GlobalEnumsResponse = GoApiResponse<'/api/v2/global-enums/'>; type PlannedInterventionOption = NonNullable[number]; +type ProposedActionOption = NonNullable[number]; type Value = PartialDref; type PlannedInterventionFormFields = NonNullable[number]; +type ProposedActionsFormFields = NonNullable[number]; type RiskSecurityFormFields = NonNullable[number]; +type ActivityOptions = NonNullable>[number]; + +function activityKeySelector(option: ActivityOptions) { + return option.key; +} + +function activityLabelSelector(option: ActivityOptions) { + return option.label; +} function plannedInterventionKeySelector(option: PlannedInterventionOption) { return option.key; } +const EARLY_ACTIONS = 1 satisfies ProposedActionOption['key']; +const EARLY_RESPONSE = 2 satisfies ProposedActionOption['key']; + interface Props { value: Value; setFieldValue: (...entries: EntriesAsList) => void; + setValue: (value: SetBaseValueArg, partialUpdate?: boolean) => void; error: Error | undefined; fileIdToUrlMap: Record; setFileIdToUrlMap?: React.Dispatch>>; @@ -81,8 +106,59 @@ function Operation(props: Props) { fileIdToUrlMap, setFileIdToUrlMap, disabled, + setValue, } = props; + function useProposedActionsFormArray() { + const onProposedActionChange = useCallback( + (val: SetValueArg, index: number | undefined) => { + setValue((oldVal) => { + const newProposedValue = [...(oldVal.proposed_action ?? [])]; + if (isNotDefined(index)) { + newProposedValue.push( + isCallable(val) ? val(undefined) : val, + ); + } else { + newProposedValue[index] = isCallable(val) + ? val(newProposedValue[index]) + : val; + } + + const newValue = { + ...oldVal, + proposed_action: newProposedValue, + }; + + return { + ...newValue, + ...recalculateProposedActionValues(newValue), + }; + }, true); + }, + [], + ); + + const onProposedActionRemove = useCallback( + (index: number) => { + setValue( + (oldValue) => { + const newProposedValue = [...(oldValue.proposed_action ?? [])]; + newProposedValue.splice(index, 1); + + return { + ...oldValue, + proposed_action: newProposedValue, + }; + }, + true, + ); + }, + [], + ); + + return { onProposedActionChange, onProposedActionRemove }; + } + const error = getErrorObject(formError); const [ @@ -90,6 +166,16 @@ function Operation(props: Props) { setSelectedIntervention, ] = useState(); + const [ + selectedEarlyActionsActivity, + setSelectedEarlyActionsActivity, + ] = useState(); + + const [ + selectedEarlyResponseActivity, + setSelectedEarlyResponseActivity, + ] = useState(); + const { setValue: onInterventionChange, removeValue: onInterventionRemove, @@ -98,6 +184,11 @@ function Operation(props: Props) { setFieldValue, ); + const { + onProposedActionChange, + onProposedActionRemove, + } = useProposedActionsFormArray(); + const { setValue: onRiskSecurityChange, removeValue: onRiskSecurityRemove, @@ -106,6 +197,29 @@ function Operation(props: Props) { setFieldValue, ); + const { + pending: activityOptionPending, + response: activityOptionResponse, + } = useRequest({ + url: '/api/v2/primarysector', + }); + + const handleSurgeDeployedChange = useCallback( + (val: PartialDref['is_surge_personnel_deployed'] | undefined) => ( + setValue((oldValue) => { + const newValue = { + ...oldValue, + is_surge_personnel_deployed: val, + }; + return { + ...newValue, + ...recalculateProposedActionValues(newValue), + }; + }, true) + ), + [setValue], + ); + const handleInterventionAddButtonClick = useCallback((title: PlannedInterventionOption['key'] | undefined) => { const newInterventionItem: PlannedInterventionFormFields = { client_id: randomString(), @@ -121,6 +235,91 @@ function Operation(props: Props) { setSelectedIntervention(undefined); }, [setFieldValue, setSelectedIntervention]); + const proposedActionsByType = useMemo(() => ( + listToGroupList( + (value?.proposed_action ?? []).filter((proposed) => isDefined(proposed.proposed_type)), + (proposed) => proposed.proposed_type ?? 0, + (proposed, _, index) => ({ + ...proposed, + mainIndex: index, + }), + ) + ), [value?.proposed_action]); + + const handleEarlyActionsActivityAddButtonClick = useCallback((name: ProposedActionOption['key']) => { + const newProposedActionItem: ProposedActionsFormFields = { + client_id: randomString(), + proposed_type: name, + activity: selectedEarlyActionsActivity, + }; + + setFieldValue( + (oldValue: ProposedActionsFormFields[] | undefined) => ( + [...(oldValue ?? []), newProposedActionItem] + ), + 'proposed_action' as const, + ); + setSelectedEarlyActionsActivity(undefined); + }, [setFieldValue, selectedEarlyActionsActivity]); + + const handleEarlyResponseActivityAddButtonClick = useCallback((name: ProposedActionOption['key']) => { + const newProposedActionItem: ProposedActionsFormFields = { + client_id: randomString(), + proposed_type: name, + activity: selectedEarlyResponseActivity, + }; + + setFieldValue( + (oldValue: ProposedActionsFormFields[] | undefined) => ( + [...(oldValue ?? []), newProposedActionItem] + ), + 'proposed_action' as const, + ); + setSelectedEarlyResponseActivity(undefined); + }, [setFieldValue, selectedEarlyResponseActivity]); + + const earlyActionActivitiesSelectedOptionsMap = useMemo(() => { + const earlyActionProposedActionValue = value.proposed_action?.filter( + (action) => action.proposed_type === EARLY_ACTIONS, + ); + + return listToMap( + earlyActionProposedActionValue, + (proposedAction) => proposedAction.activity ?? '', + (proposedAction) => ({ + type: proposedAction.proposed_type, + isFilter: true, + }), + ); + }, [value.proposed_action]); + + const earlyResponseActivitiesSelectedOptionsMap = useMemo(() => { + const earlyResponseProposedActionValue = value.proposed_action?.filter( + (action) => action.proposed_type === EARLY_RESPONSE, + ); + + return listToMap( + earlyResponseProposedActionValue, + (proposedAction) => proposedAction.activity ?? '', + (proposedAction) => ({ + type: proposedAction.proposed_type, + isFilter: true, + }), + ); + }, [value.proposed_action]); + + const filteredEarlyActionActivityOptions = useMemo(() => ( + activityOptionResponse?.filter( + (response) => !earlyActionActivitiesSelectedOptionsMap?.[response.key]?.isFilter, + ) + ), [activityOptionResponse, earlyActionActivitiesSelectedOptionsMap]); + + const filteredEarlyResponseActivityOptions = useMemo(() => ( + activityOptionResponse?.filter( + (response) => !earlyResponseActivitiesSelectedOptionsMap?.[response.key]?.isFilter, + ) + ), [activityOptionResponse, earlyResponseActivitiesSelectedOptionsMap]); + const warnings = useMemo(() => { if (isNotDefined(value?.total_targeted_population)) { return []; @@ -516,7 +715,7 @@ function Operation(props: Props) { )} > -
+
@@ -637,6 +836,147 @@ function Operation(props: Props) { )} + {value?.type_of_dref === TYPE_IMMINENT && ( + + +
+ + +
+ + {proposedActionsByType[EARLY_ACTIONS]?.map((action) => ( + + ))} +
+ +
+ + +
+ + {proposedActionsByType[EARLY_RESPONSE]?.map((action) => ( + + ))} +
+ + + {strings.drefFormProposedActionSelectBudgetNote} +
+ )} + > + + + {value.is_surge_personnel_deployed && ( + + )} + + + + + )}
); } diff --git a/app/src/views/DrefApplicationForm/Operation/styles.module.css b/app/src/views/DrefApplicationForm/Operation/styles.module.css index b7738d46ef..71a2cc7d6a 100644 --- a/app/src/views/DrefApplicationForm/Operation/styles.module.css +++ b/app/src/views/DrefApplicationForm/Operation/styles.module.css @@ -7,7 +7,7 @@ font-size: var(--go-ui-height-icon-multiplier); } - .intervention-selection-container { + .selection-container { display: flex; align-items: center; gap: var(--go-ui-spacing-md); diff --git a/app/src/views/DrefApplicationForm/common.tsx b/app/src/views/DrefApplicationForm/common.tsx index 64f3b37f6d..1f9d95c7d3 100644 --- a/app/src/views/DrefApplicationForm/common.tsx +++ b/app/src/views/DrefApplicationForm/common.tsx @@ -1,3 +1,4 @@ +import { sumSafe } from '@ifrc-go/ui/utils'; import { isNotDefined } from '@togglecorp/fujs'; import { analyzeErrors, @@ -152,6 +153,24 @@ const tabToFieldsMap = { submission: timeframeAndContactsTabFields, }; +export const recalculateProposedActionValues = (val: PartialDref) => { + const subTotal = sumSafe( + val.proposed_action?.map((pa) => pa.budget), + ) ?? 0; + const surgeDeployment = val.is_surge_personnel_deployed ? 10000 : undefined; + const indirectCost = val.is_surge_personnel_deployed ? 5800 : 5000; + + const total = sumSafe( + [subTotal, indirectCost, surgeDeployment], + ); + return { + sub_total: subTotal, + indirect_cost: indirectCost, + surge_deployment: surgeDeployment, + total, + }; +}; + export function checkTabErrors(error: Error | undefined, tabKey: TabKeys) { if (isNotDefined(analyzeErrors(error))) { return false; diff --git a/app/src/views/DrefApplicationForm/index.tsx b/app/src/views/DrefApplicationForm/index.tsx index 16c7bb8688..c822de01e7 100644 --- a/app/src/views/DrefApplicationForm/index.tsx +++ b/app/src/views/DrefApplicationForm/index.tsx @@ -119,6 +119,7 @@ function getNextStep(current: TabKeys, direction: 1 | -1, typeOfDref: TypeOfDref } return undefined; } + /** @knipignore */ export function Component() { @@ -274,6 +275,7 @@ export function Component() { const { planned_interventions, + proposed_action, needs_identified, national_society_actions, risk_security, @@ -292,6 +294,11 @@ export function Component() { indicators: intervention.indicators?.map(injectClientId), }), ), + proposed_action: proposed_action?.map( + (action) => ({ + ...injectClientId(action), + }), + ), source_information: source_information?.map(injectClientId), needs_identified: needs_identified?.map(injectClientId), national_society_actions: national_society_actions?.map(injectClientId), @@ -362,6 +369,11 @@ export function Component() { const [index] = match; return value?.planned_interventions?.[index]?.client_id; } + match = matchArray(locations, ['proposed_action', NUM]); + if (isDefined(match)) { + const [index] = match; + return value?.proposed_action?.[index]?.client_id; + } match = matchArray(locations, ['source_information', NUM, 'source_link', NUM]); if (isDefined(match)) { const [index] = match; @@ -450,6 +462,11 @@ export function Component() { const [index] = match; return value?.planned_interventions?.[index]?.client_id; } + match = matchArray(locations, ['proposed_action', NUM]); + if (isDefined(match)) { + const [index] = match; + return value?.proposed_action?.[index]?.client_id; + } match = matchArray(locations, ['source_information', NUM, 'source_link', NUM]); if (isDefined(match)) { const [index] = match; @@ -722,6 +739,7 @@ export function Component() { ; type NeedIdentifiedResponse = NonNullable[number]; type NsActionResponse = NonNullable[number]; type InterventionResponse = NonNullable[number]; +type ProposedActionResponse = NonNullable[number]; type IndicatorResponse = NonNullable[number]; type RiskSecurityResponse = NonNullable[number]; type ImagesFileResponse = NonNullable[number]; @@ -61,6 +65,7 @@ type SourceInformationResponse = NonNullable, + NsActionResponse, + NsActionFormFields >, - NsActionResponse, - NsActionFormFields + InterventionResponse, + InterventionFormFields >, - InterventionResponse, - InterventionFormFields + IndicatorResponse, + IndicatorFormFields >, - IndicatorResponse, - IndicatorFormFields + ProposedActionResponse, + ProposedActionFormFields >, IndicatorResponse, IndicatorFormFields @@ -129,6 +138,7 @@ type NeedsIdentifiedFields = ReturnType[number], PartialDref>['fields']>; type SourceInformationFields = ReturnType[number], PartialDref>['fields']>; type PlannedInterventionFields = ReturnType[number], PartialDref>['fields']>; +type ProposedActionsFields = ReturnType[number], PartialDref>['fields']>; type IndicatorFields = ReturnType[number]['indicators']>[number], PartialDref>['fields']>; const schema: DrefFormSchema = { @@ -584,8 +594,13 @@ const schema: DrefFormSchema = { 'has_child_safeguarding_risk_analysis_assessment', 'budget_file', 'planned_interventions', + 'proposed_action', 'human_resource', 'is_surge_personnel_deployed', + 'sub_total', + 'surge_deployment', + 'indirect_cost', + 'total', ] as const; type OperationDrefTypeRelatedFields = Pick< DrefFormSchemaFields, @@ -594,7 +609,7 @@ const schema: DrefFormSchema = { formFields = addCondition( formFields, formValue, - ['type_of_dref'], + ['type_of_dref', 'is_surge_personnel_deployed'], operationDrefTypeRelatedFields, (val): OperationDrefTypeRelatedFields => { let conditionalFields: OperationDrefTypeRelatedFields = { @@ -619,9 +634,14 @@ const schema: DrefFormSchema = { risk_security_concern: { forceValue: nullValue }, budget_file: { forceValue: nullValue }, planned_interventions: { forceValue: [] }, + proposed_action: { forceValue: [] }, human_resource: { forceValue: nullValue }, is_surge_personnel_deployed: { forceValue: nullValue }, has_child_safeguarding_risk_analysis_assessment: { forceValue: nullValue }, + sub_total: { forceValue: nullValue }, + surge_deployment: { forceValue: nullValue }, + indirect_cost: { forceValue: nullValue }, + total: { forceValue: nullValue }, }; if (val?.type_of_dref === TYPE_LOAN) { return conditionalFields; @@ -731,7 +751,70 @@ const schema: DrefFormSchema = { people_targeted_with_early_actions: { validations: [positiveIntegerCondition], }, + proposed_action: { + keySelector: (n) => n.client_id, + member: () => ({ + fields: (): ProposedActionsFields => ({ + client_id: {}, + budget: { + validations: [ + positiveIntegerCondition, + lessThanOrEqualToCondition(MAX_INT_LIMIT), + ], + }, + activity: { + required: true, + }, + proposed_type: { + required: true, + }, + }), + }), + }, + sub_total: { + required: true, + validations: [ + (value: Maybe) => ( + // FIXME: use translations + isDefined(value) && value !== 75000 + ? 'The sub-total of the budgets should be exactly CHF 75000' + : undefined + ), + ], + }, }; + + conditionalFields = addCondition( + conditionalFields, + formValue, + ['is_surge_personnel_deployed'], + ['indirect_cost', 'surge_deployment'], + (value) => { + if (value?.is_surge_personnel_deployed) { + return { + surge_deployment: { required: true }, + indirect_cost: { + required: true, + validations: [ + positiveIntegerCondition, + lessThanOrEqualToCondition(5800), + ], + }, + }; + } + return { + surge_deployment: { forceValue: nullValue }, + indirect_cost: { + required: true, + validations: [ + positiveIntegerCondition, + lessThanOrEqualToCondition(5000), + ], + }, + }; + }, + + ); } return conditionalFields; }, From 84510b107824c49a1496a5b09f46b1a684f22ef1 Mon Sep 17 00:00:00 2001 From: samshara Date: Fri, 24 Jan 2025 10:57:09 +0545 Subject: [PATCH 2/3] fix: minor styling fix --- .../Operation/ProposedActionsInput/i18n.json | 2 +- .../Operation/ProposedActionsInput/index.tsx | 27 ++++++++++--------- .../ProposedActionsInput/styles.module.css | 21 +++++++++++++-- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/i18n.json b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/i18n.json index f1731886cf..362d3ed831 100644 --- a/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/i18n.json +++ b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/i18n.json @@ -1,5 +1,5 @@ { - "namespace": "drefAppicationForm", + "namespace": "drefApplicationForm", "strings": { "drefFormProposedActionActivityLabel": "Activity", "drefFormProposedActionBudgetLabel": "Budget(CHF)", diff --git a/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx index 329b71a9e7..7276f2110e 100644 --- a/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx +++ b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx @@ -66,11 +66,10 @@ function ProposedActionsInput(props: Props) { : undefined; return ( -
-
+
+
+
- Date: Fri, 24 Jan 2025 11:22:20 +0545 Subject: [PATCH 3/3] fix: Unwrap the custom form array for the prposed action --- .../Operation/ProposedActionsInput/index.tsx | 4 +- .../ProposedActionsInput/styles.module.css | 6 +- .../DrefApplicationForm/Operation/i18n.json | 2 +- .../DrefApplicationForm/Operation/index.tsx | 99 +++++++++---------- app/src/views/DrefApplicationForm/common.tsx | 13 ++- app/src/views/DrefApplicationForm/schema.ts | 10 +- 6 files changed, 66 insertions(+), 68 deletions(-) diff --git a/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx index 7276f2110e..8ff8127eb8 100644 --- a/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx +++ b/app/src/views/DrefApplicationForm/Operation/ProposedActionsInput/index.tsx @@ -69,8 +69,8 @@ function ProposedActionsInput(props: Props) {
, index: number | undefined) => { - setValue((oldVal) => { - const newProposedValue = [...(oldVal.proposed_action ?? [])]; - if (isNotDefined(index)) { - newProposedValue.push( - isCallable(val) ? val(undefined) : val, - ); - } else { - newProposedValue[index] = isCallable(val) - ? val(newProposedValue[index]) - : val; - } - - const newValue = { - ...oldVal, - proposed_action: newProposedValue, - }; + const onProposedActionChange = useCallback( + (val: SetValueArg, index: number | undefined) => { + setValue((oldVal) => { + const newProposedValue = [...(oldVal.proposed_action ?? [])]; + if (isNotDefined(index)) { + newProposedValue.push( + isCallable(val) ? val(undefined) : val, + ); + } else { + newProposedValue[index] = isCallable(val) + ? val(newProposedValue[index]) + : val; + } - return { - ...newValue, - ...recalculateProposedActionValues(newValue), - }; - }, true); - }, - [], - ); + const newValue = { + ...oldVal, + proposed_action: newProposedValue, + }; - const onProposedActionRemove = useCallback( - (index: number) => { - setValue( - (oldValue) => { - const newProposedValue = [...(oldValue.proposed_action ?? [])]; - newProposedValue.splice(index, 1); - - return { - ...oldValue, - proposed_action: newProposedValue, - }; - }, - true, - ); - }, - [], - ); + return { + ...newValue, + ...recalculateProposedActionValues(newValue), + }; + }, true); + }, + [setValue], + ); - return { onProposedActionChange, onProposedActionRemove }; - } + const onProposedActionRemove = useCallback( + (index: number) => { + setValue( + (oldValue) => { + const newProposedValue = [...(oldValue.proposed_action ?? [])]; + newProposedValue.splice(index, 1); + + return { + ...oldValue, + proposed_action: newProposedValue, + }; + }, + true, + ); + }, + [setValue], + ); const error = getErrorObject(formError); @@ -184,11 +180,6 @@ function Operation(props: Props) { setFieldValue, ); - const { - onProposedActionChange, - onProposedActionRemove, - } = useProposedActionsFormArray(); - const { setValue: onRiskSecurityChange, removeValue: onRiskSecurityRemove, @@ -946,11 +937,11 @@ function Operation(props: Props) { )} diff --git a/app/src/views/DrefApplicationForm/common.tsx b/app/src/views/DrefApplicationForm/common.tsx index 1f9d95c7d3..521dafcce4 100644 --- a/app/src/views/DrefApplicationForm/common.tsx +++ b/app/src/views/DrefApplicationForm/common.tsx @@ -157,16 +157,23 @@ export const recalculateProposedActionValues = (val: PartialDref) => { const subTotal = sumSafe( val.proposed_action?.map((pa) => pa.budget), ) ?? 0; - const surgeDeployment = val.is_surge_personnel_deployed ? 10000 : undefined; + + // NOTE: if Surge Personnel are deployed, + // the Surge Deployment cost will be CHF 10,000, + // and the Indirect Costs will be CHF 5,800. Conversely, + // if Surge Personnel are not deployed, + // the Surge Deployment cost will not be applicable, + // and the Indirect Costs will be CHF 5,000 + const surgeDeploymentCost = val.is_surge_personnel_deployed ? 10000 : undefined; const indirectCost = val.is_surge_personnel_deployed ? 5800 : 5000; const total = sumSafe( - [subTotal, indirectCost, surgeDeployment], + [subTotal, indirectCost, surgeDeploymentCost], ); return { sub_total: subTotal, indirect_cost: indirectCost, - surge_deployment: surgeDeployment, + surge_deployment_cost: surgeDeploymentCost, total, }; }; diff --git a/app/src/views/DrefApplicationForm/schema.ts b/app/src/views/DrefApplicationForm/schema.ts index 8a2e7dafc5..a3b279f14a 100644 --- a/app/src/views/DrefApplicationForm/schema.ts +++ b/app/src/views/DrefApplicationForm/schema.ts @@ -598,7 +598,7 @@ const schema: DrefFormSchema = { 'human_resource', 'is_surge_personnel_deployed', 'sub_total', - 'surge_deployment', + 'surge_deployment_cost', 'indirect_cost', 'total', ] as const; @@ -639,7 +639,7 @@ const schema: DrefFormSchema = { is_surge_personnel_deployed: { forceValue: nullValue }, has_child_safeguarding_risk_analysis_assessment: { forceValue: nullValue }, sub_total: { forceValue: nullValue }, - surge_deployment: { forceValue: nullValue }, + surge_deployment_cost: { forceValue: nullValue }, indirect_cost: { forceValue: nullValue }, total: { forceValue: nullValue }, }; @@ -788,11 +788,11 @@ const schema: DrefFormSchema = { conditionalFields, formValue, ['is_surge_personnel_deployed'], - ['indirect_cost', 'surge_deployment'], + ['indirect_cost', 'surge_deployment_cost'], (value) => { if (value?.is_surge_personnel_deployed) { return { - surge_deployment: { required: true }, + surge_deployment_cost: { required: true }, indirect_cost: { required: true, validations: [ @@ -803,7 +803,7 @@ const schema: DrefFormSchema = { }; } return { - surge_deployment: { forceValue: nullValue }, + surge_deployment_cost: { forceValue: nullValue }, indirect_cost: { required: true, validations: [