From b13d0e5636b2b0979d553917a6910376180d76db Mon Sep 17 00:00:00 2001 From: Shreeyash Shrestha Date: Fri, 3 Apr 2026 14:59:03 +0545 Subject: [PATCH 1/2] feat(go-ui): update type props in DateInput to implement month input variant --- packages/ui/src/components/DateInput/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/DateInput/index.tsx b/packages/ui/src/components/DateInput/index.tsx index 73666698ce..46317bd2ec 100644 --- a/packages/ui/src/components/DateInput/index.tsx +++ b/packages/ui/src/components/DateInput/index.tsx @@ -14,6 +14,7 @@ export interface Props extends InheritedProps { inputClassName?: string; withDiffView?: boolean; prevValue?: RawInputProps['value']; + type?: 'date' | 'month'; } function DateInput(props: Props) { @@ -25,6 +26,7 @@ function DateInput(props: Props) { withDiffView, value, prevValue, + type = 'date', ...otherProps } = props; @@ -59,7 +61,7 @@ function DateInput(props: Props) { readOnly={readOnly} disabled={disabled} className={inputClassName} - type="date" + type={type} /> )} /> From cc154e5dd7bdb4b31e292d14532dcbaf7523ea61 Mon Sep 17 00:00:00 2001 From: Shreeyash Shrestha Date: Fri, 3 Apr 2026 15:00:29 +0545 Subject: [PATCH 2/2] fix(eap): update in whole eap module according feedback - add partners field in both eap forms - pass type props in every DateInput field - remove year option in timeFrame for readiness and prepositioning activities - make ap code form fields view only - update in translation strings - add missing validation for plannedOperations and enablingApproaches form fields - add eap apCode options for both forms - improve eap table action flow - add revise button for creating new version - add additional attachments modal in EAP table - make lead_timeframe_unit and lead_time field required - replace diff library to diff-match-patch - new DiffTextOutput components - update PrintableDescription and PrintableLabel components - add utility function for expected_submission_time field --- app/package.json | 3 +- .../components/domain/Admin2Input/i18n.json | 8 + .../components/domain/Admin2Input/index.tsx | 35 +- .../domain/EapIndicatorInput/index.tsx | 5 +- .../domain/EapIndicatorInput/schema.ts | 8 +- .../domain/EapIndicatorListInput/index.tsx | 5 +- .../EapOperationActivityInput/index.tsx | 29 +- .../EapOperationActivityInput/schema.ts | 6 +- .../EapOperationActivityListInput/index.tsx | 17 +- .../printable/DiffTextOutput/index.tsx | 91 ++++ .../styles.module.css | 2 +- .../printable/PrintableDescription/index.tsx | 79 +--- .../printable/PrintableLabel/index.tsx | 79 +--- .../PrintableLabel/styles.module.css | 16 - app/src/utils/domain/eap.ts | 15 + .../EapTableActions/i18n.json | 22 +- .../EapTableActions/index.tsx | 255 +++++++++-- app/src/views/EapFullExport/index.tsx | 33 +- .../EapActivationProcess/index.tsx | 2 +- .../EapFullForm/FinanceLogistics/i18n.json | 1 + .../EapFullForm/FinanceLogistics/index.tsx | 2 + app/src/views/EapFullForm/Meal/index.tsx | 2 +- .../NationalSocietyCapacity/index.tsx | 2 +- app/src/views/EapFullForm/Overview/i18n.json | 4 +- app/src/views/EapFullForm/Overview/index.tsx | 31 +- .../views/EapFullForm/RiskAnalysis/i18n.json | 10 +- .../views/EapFullForm/RiskAnalysis/index.tsx | 3 +- .../ApproachesInput/index.tsx | 29 +- .../SelectionActions/OperationInput/index.tsx | 30 +- .../EapFullForm/SelectionActions/i18n.json | 5 +- .../EapFullForm/SelectionActions/index.tsx | 10 +- .../views/EapFullForm/TriggerModel/i18n.json | 3 +- .../views/EapFullForm/TriggerModel/index.tsx | 23 +- app/src/views/EapFullForm/common.tsx | 6 +- app/src/views/EapFullForm/i18n.json | 6 +- app/src/views/EapFullForm/index.tsx | 48 +- app/src/views/EapFullForm/schema.ts | 39 +- app/src/views/EapRegistration/index.tsx | 20 +- app/src/views/EapRegistration/schema.ts | 3 +- app/src/views/EapSimplifiedExport/index.tsx | 32 +- .../EapSimplifiedForm/EarlyAction/index.tsx | 5 +- .../ApproachesInput/index.tsx | 29 +- .../EnablingApproaches/index.tsx | 5 + .../EapSimplifiedForm/Overview/i18n.json | 2 + .../EapSimplifiedForm/Overview/index.tsx | 17 +- .../OperationsInput/index.tsx | 30 +- .../PlannedOperations/index.tsx | 6 + app/src/views/EapSimplifiedForm/common.tsx | 6 +- app/src/views/EapSimplifiedForm/i18n.json | 2 +- app/src/views/EapSimplifiedForm/index.tsx | 33 +- app/src/views/EapSimplifiedForm/schema.ts | 37 +- app/src/views/EapSummaryExport/index.tsx | 430 +++++++++--------- go-api | 2 +- pnpm-lock.yaml | 25 +- 54 files changed, 1060 insertions(+), 588 deletions(-) create mode 100644 app/src/components/domain/Admin2Input/i18n.json create mode 100644 app/src/components/printable/DiffTextOutput/index.tsx rename app/src/components/printable/{PrintableDescription => DiffTextOutput}/styles.module.css (95%) delete mode 100644 app/src/components/printable/PrintableLabel/styles.module.css create mode 100644 app/src/utils/domain/eap.ts diff --git a/app/package.json b/app/package.json index ad1cc4220e..be30c223b5 100644 --- a/app/package.json +++ b/app/package.json @@ -51,8 +51,9 @@ "@togglecorp/toggle-request": "^1.0.0-beta.3", "@turf/bbox": "^6.5.0", "@turf/buffer": "^6.5.0", + "@types/diff-match-patch": "^1.0.36", + "diff-match-patch": "^1.0.5", "exceljs": "^4.4.0", - "diff": "^8.0.2", "file-saver": "^2.0.5", "html-to-image": "^1.11.11", "mapbox-gl": "^1.13.0", diff --git a/app/src/components/domain/Admin2Input/i18n.json b/app/src/components/domain/Admin2Input/i18n.json new file mode 100644 index 0000000000..813a463a5a --- /dev/null +++ b/app/src/components/domain/Admin2Input/i18n.json @@ -0,0 +1,8 @@ +{ + "namespace": "admin2Input", + "strings": { + "heading": "Selected Areas", + "buttonLabel": "Select Areas", + "emptyMessage": "Admin Level 2 data is not available for this country." + } +} diff --git a/app/src/components/domain/Admin2Input/index.tsx b/app/src/components/domain/Admin2Input/index.tsx index 59bcf814db..7af7bc6dd1 100644 --- a/app/src/components/domain/Admin2Input/index.tsx +++ b/app/src/components/domain/Admin2Input/index.tsx @@ -13,7 +13,10 @@ import { ListView, Modal, } from '@ifrc-go/ui'; -import { useBooleanState } from '@ifrc-go/ui/hooks'; +import { + useBooleanState, + useTranslation, +} from '@ifrc-go/ui/hooks'; import { isDefined, isNotDefined, @@ -53,6 +56,8 @@ import { import BaseMap from '../BaseMap'; +import i18n from './i18n.json'; + interface Props { name: NAME; value: number[] | null | undefined; @@ -72,6 +77,8 @@ function Admin2Input(props: Props) { readOnly, } = props; + const strings = useTranslation(i18n); + const countryDetails = useCountry({ id: countryId }); const iso3 = countryDetails?.iso3; @@ -91,6 +98,24 @@ function Admin2Input(props: Props) { }, }); + // NOTE: To check if country has admin2 value or not + const { + response: admin2TestResponse, + pending: admin2TestPending, + } = useRequest({ + skip: isNotDefined(iso3), + url: '/api/v2/admin2/', + query: { + admin1__country__iso3: iso3 ?? undefined, + // NOTE: we just need 1 value to check + limit: 1, + }, + }); + + const hasAdmin2 = !admin2TestPending + && isDefined(admin2TestResponse) + && admin2TestResponse?.results.length > 0; + const { response: admin2Details } = useRequest({ skip: isNotDefined(selectedCodesDebounced) || selectedCodesDebounced.length === 0, url: '/api/v2/admin2/', @@ -287,20 +312,20 @@ function Admin2Input(props: Props) { return ( - Select areas + {strings.buttonLabel} )} withCompactMessage empty={!value || value.length === 0} + emptyMessage={!hasAdmin2 ? strings.emptyMessage : undefined} withBorder withPadding > diff --git a/app/src/components/domain/EapIndicatorInput/index.tsx b/app/src/components/domain/EapIndicatorInput/index.tsx index 857c1a6cb9..5b812cace0 100644 --- a/app/src/components/domain/EapIndicatorInput/index.tsx +++ b/app/src/components/domain/EapIndicatorInput/index.tsx @@ -10,6 +10,7 @@ import { useTranslation } from '@ifrc-go/ui/hooks'; import { type ArrayError, getErrorObject, + type LeafError, type PartialForm, type SetValueArg, useFormObject, @@ -28,7 +29,7 @@ const defaultIndicatorValue: IndicatorFormFields = { interface Props { value: IndicatorFormFields; - error: ArrayError | undefined; + error: ArrayError | LeafError | undefined; onChange: (value: SetValueArg, index: number) => void; onRemove: (index: number) => void; index: number; @@ -52,7 +53,7 @@ function EapIndicatorInput(props: Props) { const onFieldChange = useFormObject(index, onChange, defaultIndicatorValue); const error = value && value.client_id && errorFromProps - ? getErrorObject(errorFromProps?.[value.client_id]) + ? getErrorObject(getErrorObject(errorFromProps)?.[value.client_id]) : undefined; return ( diff --git a/app/src/components/domain/EapIndicatorInput/schema.ts b/app/src/components/domain/EapIndicatorInput/schema.ts index 43bcf4fcac..d502d53249 100644 --- a/app/src/components/domain/EapIndicatorInput/schema.ts +++ b/app/src/components/domain/EapIndicatorInput/schema.ts @@ -16,20 +16,20 @@ export type IndicatorFormFields = PartialForm & { type IndicatorSchema = ObjectSchema; -const schema: IndicatorSchema = { +const schema = (isSubmit: boolean): IndicatorSchema => ({ fields: (): ReturnType => ({ client_id: {}, id: { defaultValue: undefinedValue }, title: { // FIXME: add validation for character limit - required: true, + required: isSubmit, requiredValidation: requiredStringCondition, }, target: { - required: true, + required: isSubmit, validations: [positiveNumberCondition], }, }), -}; +}); export default schema; diff --git a/app/src/components/domain/EapIndicatorListInput/index.tsx b/app/src/components/domain/EapIndicatorListInput/index.tsx index a8b4efe4e5..662d07a8eb 100644 --- a/app/src/components/domain/EapIndicatorListInput/index.tsx +++ b/app/src/components/domain/EapIndicatorListInput/index.tsx @@ -13,6 +13,7 @@ import { import { type ArrayError, getErrorObject, + type LeafError, type SetValueArg, useFormArray, } from '@togglecorp/toggle-form'; @@ -32,7 +33,7 @@ interface Props { name: NAME, value: IndicatorFormFields[] | undefined; onChange: (newValue: SetValueArg, name: NAME) => void; - error: ArrayError | undefined; + error: ArrayError | LeafError | undefined; } function EapIndicatorListInput(props: Props) { @@ -113,7 +114,7 @@ function EapIndicatorListInput(props: Props) value={activity} onChange={onReadinessChange} onRemove={onReadinessRemove} - error={getErrorObject(error?.readiness_activities)} + error={getErrorObject(error)?.indicators} disabled={disabled} readOnly={readOnly} /> diff --git a/app/src/components/domain/EapOperationActivityInput/index.tsx b/app/src/components/domain/EapOperationActivityInput/index.tsx index dc3e04656f..9b6f6da2e2 100644 --- a/app/src/components/domain/EapOperationActivityInput/index.tsx +++ b/app/src/components/domain/EapOperationActivityInput/index.tsx @@ -1,4 +1,7 @@ -import { useCallback } from 'react'; +import { + useCallback, + useMemo, +} from 'react'; import { DeleteBinTwoLineIcon } from '@ifrc-go/icons'; import { Checklist, @@ -14,12 +17,14 @@ import { type ArrayError, getErrorObject, getErrorString, + type LeafError, type SetValueArg, useFormObject, } from '@togglecorp/toggle-form'; import { type components } from '#generated/types'; import useGlobalEnums from '#hooks/domain/useGlobalEnums'; +import { TIMEFRAME_YEAR } from '#utils/constants'; import { type OperationActivityFormFields } from './schema'; import TimeSpanCheck from './TimeSpanCheck'; @@ -30,6 +35,8 @@ const defaultActivityValue: OperationActivityFormFields = { client_id: '-1', }; +export type ActivityInputType = 'readiness_activities' | 'prepositioning_activities' | 'early_action_activities'; + type TimeframeOption = components['schemas']['EapTimeframeEnum']; function timeframeKeySelector(option: TimeframeOption) { @@ -42,12 +49,14 @@ const timeValueKeySelector = ( interface Props { value: OperationActivityFormFields; - error: ArrayError | undefined; + error: ArrayError | LeafError | undefined; onChange: (value: SetValueArg, index: number) => void; onRemove: (index: number) => void; index: number; disabled?: boolean; readOnly?: boolean; + + name: ActivityInputType; } function EapOperationActivityInput(props: Props) { @@ -59,6 +68,7 @@ function EapOperationActivityInput(props: Props) { onRemove, disabled, readOnly, + name, } = props; const strings = useTranslation(i18n); @@ -73,9 +83,18 @@ function EapOperationActivityInput(props: Props) { const onFieldChange = useFormObject(index, onChange, defaultActivityValue); const error = (value && value.client_id && errorFromProps) - ? getErrorObject(errorFromProps?.[value.client_id]) + ? getErrorObject(getErrorObject(errorFromProps)?.[value.client_id]) : undefined; + const eapTimeframeOption = useMemo(() => { + if (name === 'early_action_activities') { + return eap_timeframe?.filter((item) => item.key !== TIMEFRAME_YEAR); + } + return eap_timeframe; + }, [eap_timeframe, name]); + + const eapTimeFrameReadOnly = name === 'readiness_activities' || name === 'prepositioning_activities'; + const getTimeValueOptions = useCallback( (timeframe?: number) => { switch (timeframe) { @@ -143,10 +162,10 @@ function EapOperationActivityInput(props: Props) { onChange={handleTimeframeChange} keySelector={timeframeKeySelector} labelSelector={stringValueSelector} - options={eap_timeframe} + options={eapTimeframeOption} disabled={disabled} error={error?.timeframe} - readOnly={readOnly} + readOnly={readOnly || eapTimeFrameReadOnly} /> {value?.timeframe && ( & { type OperationActivitySchema = ObjectSchema; -const schema: OperationActivitySchema = { +const schema = (isSubmit: boolean): OperationActivitySchema => ({ fields: (): ReturnType => ({ client_id: {}, id: { defaultValue: undefinedValue }, activity: { // FIXME: add validation for character limit - required: true, + required: isSubmit, requiredValidation: requiredStringCondition, }, time_value: {}, timeframe: {}, }), -}; +}); export default schema; diff --git a/app/src/components/domain/EapOperationActivityListInput/index.tsx b/app/src/components/domain/EapOperationActivityListInput/index.tsx index 493139dc95..0cdd7eb833 100644 --- a/app/src/components/domain/EapOperationActivityListInput/index.tsx +++ b/app/src/components/domain/EapOperationActivityListInput/index.tsx @@ -18,20 +18,20 @@ import { import { type ArrayError, getErrorObject, + type LeafError, type SetValueArg, useFormArray, } from '@togglecorp/toggle-form'; -import EapOperationActivityInput from '#components/domain/EapOperationActivityInput'; +import EapOperationActivityInput, { type ActivityInputType } from '#components/domain/EapOperationActivityInput'; import { type OperationActivityFormFields } from '#components/domain/EapOperationActivityInput/schema'; import ExplanatoryNote from '#components/ExplanatoryNote'; import Link from '#components/Link'; import NonFieldError from '#components/NonFieldError'; +import { TIMEFRAME_YEAR } from '#utils/constants'; import i18n from './i18n.json'; -type FormName = 'readiness_activities' | 'prepositioning_activities' | 'early_action_activities'; - interface Props { disabled?: boolean; readOnly?: boolean; @@ -39,10 +39,10 @@ interface Props { name: NAME, value: OperationActivityFormFields[] | undefined; onChange: (newValue: SetValueArg, name: NAME) => void; - error: ArrayError | undefined; + error: ArrayError | LeafError | undefined; } -function EapOperationActivityListInput(props: Props) { +function EapOperationActivityListInput(props: Props) { const { disabled, readOnly, @@ -65,8 +65,12 @@ function EapOperationActivityListInput(props: Props const handleReadinessAddButtonClick = useCallback( () => { + const timeframeValue = name === 'readiness_activities' || name === 'prepositioning_activities' + ? TIMEFRAME_YEAR + : undefined; const newActionItem: OperationActivityFormFields = { client_id: randomString(), + timeframe: timeframeValue, }; onChange( @@ -162,12 +166,13 @@ function EapOperationActivityListInput(props: Props > {value?.map((activity, i) => ( diff --git a/app/src/components/printable/DiffTextOutput/index.tsx b/app/src/components/printable/DiffTextOutput/index.tsx new file mode 100644 index 0000000000..fd5a918947 --- /dev/null +++ b/app/src/components/printable/DiffTextOutput/index.tsx @@ -0,0 +1,91 @@ +import { + Fragment, + useMemo, +} from 'react'; +import { + _cs, + isNotDefined, +} from '@togglecorp/fujs'; +import DiffMatchPatch from 'diff-match-patch'; + +import styles from './styles.module.css'; + +const ADDED = 1; +const REMOVED = -1; + +interface Props { + value?: string | null; + className?: string; + withDiff?: boolean; + prevValue?: string | null; +} + +function DiffTextOutput(props: Props) { + const { + className, + value, + prevValue, + withDiff = false, + } = props; + + const diff = useMemo(() => { + if (!withDiff) { + return undefined; + } + const diffMatch = new DiffMatchPatch(); + return diffMatch.diff_main(prevValue ?? '', value ?? ''); + }, [withDiff, value, prevValue]); + + if (isNotDefined(diff)) { + return ( +
+ {value} +
+ ); + } + + return ( +
+ {diff.map(([changeType, content], index) => { + if (changeType === ADDED) { + return ( + + {content} + + ); + } + + if (changeType === REMOVED) { + return ( + + {content} + + ); + } + + return ( + // eslint-disable-next-line react/no-array-index-key + + {content} + + ); + })} +
+ ); +} + +export default DiffTextOutput; diff --git a/app/src/components/printable/PrintableDescription/styles.module.css b/app/src/components/printable/DiffTextOutput/styles.module.css similarity index 95% rename from app/src/components/printable/PrintableDescription/styles.module.css rename to app/src/components/printable/DiffTextOutput/styles.module.css index a52f405f09..7e9b59945e 100644 --- a/app/src/components/printable/PrintableDescription/styles.module.css +++ b/app/src/components/printable/DiffTextOutput/styles.module.css @@ -1,4 +1,4 @@ -.printable-description { +.diff-text-output { text-align: justify; white-space: pre-wrap; overflow-wrap: break-word; diff --git a/app/src/components/printable/PrintableDescription/index.tsx b/app/src/components/printable/PrintableDescription/index.tsx index 230ebaefd8..25a1f86c67 100644 --- a/app/src/components/printable/PrintableDescription/index.tsx +++ b/app/src/components/printable/PrintableDescription/index.tsx @@ -1,89 +1,24 @@ -import { - Fragment, - useMemo, -} from 'react'; -import { - _cs, - isNotDefined, -} from '@togglecorp/fujs'; -import { diffSentences } from 'diff'; - -import styles from './styles.module.css'; +import DiffTextOutput from '../DiffTextOutput'; interface Props { value?: string | null; - className?: string; withDiff?: boolean; prevValue?: string | null; } function PrintableDescription(props: Props) { const { - className, value, prevValue, - withDiff = false, + withDiff, } = props; - const diff = useMemo(() => { - if (!withDiff) { - return undefined; - } - - return diffSentences(prevValue ?? '', value ?? ''); - }, [withDiff, value, prevValue]); - - if (isNotDefined(diff)) { - return ( -
- {value} -
- ); - } - return ( -
- {diff.map((part, index) => { - const { added, removed, value: partValue } = part; - - if (added) { - return ( - - {partValue} - - ); - } - - if (removed) { - return ( - - {partValue} - - ); - } - - return ( - // eslint-disable-next-line react/no-array-index-key - - {partValue} - - ); - })} -
+ ); } diff --git a/app/src/components/printable/PrintableLabel/index.tsx b/app/src/components/printable/PrintableLabel/index.tsx index 9917ac5ce7..d0f83a0a42 100644 --- a/app/src/components/printable/PrintableLabel/index.tsx +++ b/app/src/components/printable/PrintableLabel/index.tsx @@ -1,89 +1,24 @@ -import { - Fragment, - useMemo, -} from 'react'; -import { - _cs, - isNotDefined, -} from '@togglecorp/fujs'; -import { diffWordsWithSpace } from 'diff'; - -import styles from './styles.module.css'; +import DiffTextOutput from '../DiffTextOutput'; interface Props { value?: string | null; - className?: string; withDiff?: boolean; prevValue?: string | null; } function PrintableLabel(props: Props) { const { - className, value, prevValue, - withDiff = false, + withDiff, } = props; - const diff = useMemo(() => { - if (!withDiff) { - return undefined; - } - - return diffWordsWithSpace(prevValue ?? '', value ?? ''); - }, [withDiff, value, prevValue]); - - if (isNotDefined(diff)) { - return ( -
- {value} -
- ); - } - return ( -
- {diff.map((part, index) => { - const { added, removed, value: partValue } = part; - - if (added) { - return ( - - {partValue} - - ); - } - - if (removed) { - return ( - - {partValue} - - ); - } - - return ( - // eslint-disable-next-line react/no-array-index-key - - {partValue} - - ); - })} -
+ ); } diff --git a/app/src/components/printable/PrintableLabel/styles.module.css b/app/src/components/printable/PrintableLabel/styles.module.css deleted file mode 100644 index 62c8ca20aa..0000000000 --- a/app/src/components/printable/PrintableLabel/styles.module.css +++ /dev/null @@ -1,16 +0,0 @@ -.printable-label{ - overflow-wrap: break-word; - - &.with-diff-view { - .added { - background-color: color-mix(in srgb, var(--go-ui-color-green) 10%, transparent); - color: var(--go-ui-color-green); - } - - .removed { - background-color: color-mix(in srgb, var(--go-ui-color-red) 10%, transparent); - text-decoration: line-through; - color: var(--go-ui-color-red); - } - } -} diff --git a/app/src/utils/domain/eap.ts b/app/src/utils/domain/eap.ts new file mode 100644 index 0000000000..32ec7d334f --- /dev/null +++ b/app/src/utils/domain/eap.ts @@ -0,0 +1,15 @@ +import { isNotDefined } from '@togglecorp/fujs'; + +export function getFullDateFromYearMonth(val: string | undefined) { + if (isNotDefined(val)) { + return undefined; + } + return `${val}-01`; +} + +export function getYearMonthFromFullDate(val: string | undefined) { + if (isNotDefined(val)) { + return undefined; + } + return val?.slice(0, 7); +} diff --git a/app/src/views/AccountMyFormsEap/EapTableActions/i18n.json b/app/src/views/AccountMyFormsEap/EapTableActions/i18n.json index 5af45f3e42..1e4f9f11b6 100644 --- a/app/src/views/AccountMyFormsEap/EapTableActions/i18n.json +++ b/app/src/views/AccountMyFormsEap/EapTableActions/i18n.json @@ -5,15 +5,25 @@ "startSimplifiedEapLinkLabel": "Start sEAP", "editFullEapLinkLabel": "Edit Full EAP", "viewFullEapLinkLabel": "View Full EAP", - "exportWithChangesButtonLabel": "Export with track changes", + "exportWithChangesButtonLabel": "Export v{version} PDF (with track changes)", "exportButtonLabel": "Export", "exportSummaryButtonLabel": "Export Summary", - "previewExportLinkLabel": "Preview export", - "previewSummaryExportLinkLabel": "Preview summary export", - "downloadReviewChecklistLinkLabel": "Download Review checklist", - "downloadUpdatedChecklistLinkLabel": "Download updated checklist", + "previewExportLinkLabel": "Preview Export", + "previewSummaryExportLinkLabel": "Preview Summary Export", + "downloadReviewChecklistLinkLabel": "Review Checklist v{version} (with IFRC comments)", + "downloadUpdatedChecklistLinkLabel": "Review Checklist v{version} (with NS comments)", + "downloadBudgetFileLabel": "Budget v{version}", "editSimplifiedEapLinkLabel": "Edit sEAP", "viewSimplifiedEapLinkLabel": "View sEAP", - "downloadValidatedBudgetLinkLabel": "Download validated budget" + "reviseEapLabel": "Revise EAP", + "reviseEapMessage": "Revising this EAP will create a new version. You can make any necessary changes in the new version.", + "downloadValidatedBudgetLinkLabel": "Download Validated Budget", + "additionalFilesButtonLabel": "Additional Attachments", + "theoryOfChangeTableLinkLabel": "Theory of Change Table", + "fullReviseSuccessAlert": "Full EAP revised successfully", + "fullReviseFailedAlert": "Full EAP revision failed", + "simplifiedReviseSuccessAlert": "Simplified EAP revised successfully", + "simplifiedReviseFailedAlert": "Simplified EAP revision failed", + "forecastTableLinkLabel": "Forecast Table" } } diff --git a/app/src/views/AccountMyFormsEap/EapTableActions/index.tsx b/app/src/views/AccountMyFormsEap/EapTableActions/index.tsx index 0839c6aa1e..811af6c20f 100644 --- a/app/src/views/AccountMyFormsEap/EapTableActions/index.tsx +++ b/app/src/views/AccountMyFormsEap/EapTableActions/index.tsx @@ -9,9 +9,15 @@ import { } from '@ifrc-go/icons'; import { Button, + ConfirmButton, ListView, + Modal, } from '@ifrc-go/ui'; -import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + useBooleanState, + useTranslation, +} from '@ifrc-go/ui/hooks'; +import { resolveToString } from '@ifrc-go/ui/utils'; import { isDefined, isNotDefined, @@ -20,6 +26,8 @@ import { import EapExportModal from '#components/domain/EapExportModal'; import Link from '#components/Link'; import { environment } from '#config'; +import useAlert from '#hooks/useAlert'; +import useRouting from '#hooks/useRouting'; import { EAP_STATUS_NS_ADDRESSING_COMMENTS, EAP_STATUS_PENDING_PFA, @@ -28,6 +36,7 @@ import { EAP_TYPE_FULL, EAP_TYPE_SIMPLIFIED, } from '#utils/constants'; +import { useLazyRequest } from '#utils/restRequest'; import { type EapExpandedListItem } from '../utils'; import BudgetFileInput from './BudgetFileInput'; @@ -54,6 +63,16 @@ function EapTableActions(props: Props) { const [exportWithDiffView, setExportWithDiffView] = useState(false); const [summaryExport, setSummaryExport] = useState(false); const [showExportModal, setShowExportModal] = useState(false); + const [ + showAdditionalFileModal, + { + setTrue: setShowAdditionalFileModalTrue, + setFalse: setShowAdditionalFileModalFalse, + }, + ] = useBooleanState(false); + + const alert = useAlert(); + const { navigate } = useRouting(); const strings = useTranslation(i18n); @@ -83,6 +102,85 @@ function EapTableActions(props: Props) { return undefined; }, [eap]); + const { + trigger: reviseSEAP, + pending: reviseSEAPPending, + } = useLazyRequest({ + method: 'POST', + url: '/api/v2/simplified-eap/{id}/revise/', + pathVariables: isDefined(latestId) ? { id: latestId } : undefined, + body: () => ({} as never), + onSuccess: () => { + alert.show( + strings.simplifiedReviseSuccessAlert, + { variant: 'success' }, + ); + navigate( + 'simplifiedEapForm', + { params: { eapId: eap.id } }, + ); + }, + onFailure: ({ + value: { messageForNotification }, + }) => { + alert.show( + strings.simplifiedReviseFailedAlert, + { + description: messageForNotification, + variant: 'danger', + }, + ); + }, + }); + + const { + trigger: reviseFullEAP, + pending: reviseFullEAPPending, + } = useLazyRequest({ + method: 'POST', + url: '/api/v2/full-eap/{id}/revise/', + pathVariables: isDefined(latestId) ? { id: latestId } : undefined, + body: () => ({} as never), + onSuccess: () => { + alert.show( + strings.fullReviseSuccessAlert, + { variant: 'success' }, + ); + navigate( + 'fullEapForm', + { params: { eapId: eap.id } }, + ); + }, + onFailure: ({ + value: { messageForNotification }, + }) => { + alert.show( + strings.fullReviseFailedAlert, + { + description: messageForNotification, + variant: 'danger', + }, + ); + }, + }); + + const handleReviseClick = useCallback( + () => { + if (eap.eap_type === EAP_TYPE_SIMPLIFIED) { + reviseSEAP(null); + } + + if (eap.eap_type === EAP_TYPE_FULL) { + reviseFullEAP(null); + } + }, + [ + eap.eap_type, + reviseSEAP, + reviseFullEAP, + ], + ); + const latestVersion = useMemo(() => { if (eap.eap_type === EAP_TYPE_SIMPLIFIED) { return eap.simplified_eap_details.find(({ id }) => latestId === id)?.version; @@ -120,14 +218,26 @@ function EapTableActions(props: Props) { } if (eap.status !== EAP_STATUS_UNDER_DEVELOPMENT - && eap.status !== EAP_STATUS_NS_ADDRESSING_COMMENTS - ) { + && eap.status !== EAP_STATUS_NS_ADDRESSING_COMMENTS) { return false; } return true; }, [isCreated, isLatestVersion, isLocked, eap]); + const isRevised = useMemo(() => { + if (!isLatestVersion) { + return false; + } + if (!isLocked) { + return false; + } + if (eap.status !== EAP_STATUS_NS_ADDRESSING_COMMENTS) { + return false; + } + return true; + }, [eap, isLocked, isLatestVersion]); + return ( {type === 'registration' && isNotDefined(eap.eap_type) && isNotDefined(details) && ( @@ -178,16 +288,21 @@ function EapTableActions(props: Props) { {strings.previewExportLinkLabel} )} - {isCreated && ( - - )} + {details?.eapType === EAP_TYPE_FULL + && (isDefined(details?.data.theory_of_change_table_file_details) + && isDefined(details?.data.forecast_table_file_details)) + && ( + + )} {isDefined(details?.data.version) && details.data.version > 1 && ( @@ -197,7 +312,12 @@ function EapTableActions(props: Props) { before={} styleVariant="action" > - {strings.exportWithChangesButtonLabel} + {resolveToString( + strings.exportWithChangesButtonLabel, + { + version: details.data.version, + }, + )} )} {isDefined(details?.data?.review_checklist_file) && ( @@ -206,18 +326,54 @@ function EapTableActions(props: Props) { href={details.data.review_checklist_file} before={} > - {strings.downloadReviewChecklistLinkLabel} + {resolveToString( + strings.downloadReviewChecklistLinkLabel, + { + version: details.data.version, + }, + )} )} - {isDefined(details?.data?.updated_checklist_file_details?.file) && ( + {isDefined(details?.data?.updated_checklist_file_details?.file) + && isDefined(details.data.version) && ( } > - {strings.downloadUpdatedChecklistLinkLabel} + {resolveToString( + strings.downloadUpdatedChecklistLinkLabel, + { + version: details.data.version - 1, + }, + )} + + )} + {isDefined(details?.data.budget_file_details) && ( + } + > + {resolveToString( + strings.downloadBudgetFileLabel, + { + version: details.data.version, + }, + )} )} + {isRevised && ( + + {strings.reviseEapLabel} + + )} {eap.eap_type === EAP_TYPE_SIMPLIFIED && isEditable && ( )} - {type === 'pending-pfa' && eap.status >= EAP_STATUS_PENDING_PFA && eap.eap_type === EAP_TYPE_FULL && ( + {type === 'pending-pfa' && eap.status >= EAP_STATUS_PENDING_PFA && ( - } - > - {strings.previewSummaryExportLinkLabel} - + {eap.eap_type === EAP_TYPE_FULL && ( + <> + } + > + {strings.previewSummaryExportLinkLabel} + + + + )} )} {showExportModal && isDefined(eap.eap_type) && ( @@ -321,6 +489,33 @@ function EapTableActions(props: Props) { summary={summaryExport} /> )} + {showAdditionalFileModal && details?.eapType === EAP_TYPE_FULL && ( + + + {isDefined(details?.data.theory_of_change_table_file_details) && ( + } + > + {strings.theoryOfChangeTableLinkLabel} + + )} + {isDefined(details?.data.forecast_table_file_details) && ( + } + > + {strings.forecastTableLinkLabel} + + )} + + + )} ); } diff --git a/app/src/views/EapFullExport/index.tsx b/app/src/views/EapFullExport/index.tsx index b330b61a4a..7355490d71 100644 --- a/app/src/views/EapFullExport/index.tsx +++ b/app/src/views/EapFullExport/index.tsx @@ -96,6 +96,10 @@ export function Component() { : undefined, }); + const { response: apCodeOptions } = useRequest({ + url: '/api/v2/eap/options/', + }); + const { eap_sector, eap_approach } = useGlobalEnums(); const eapSectorTitleMap = listToMap( @@ -1257,6 +1261,12 @@ export function Component() { {planned_operations?.map((operation) => { const prevOperation = prevPlannedOperationsMapping?.[operation.sector]; + const apCodeSectorValue = apCodeOptions?.sector_ap_codes + ?.[operation.sector]?.join(', '); + + const prevApCodeSectorValue = apCodeOptions?.sector_ap_codes + ?.[prevOperation?.sector]?.join(', '); + const prevOperationIndicatorMap = listToMap( prevOperation?.indicators, ({ id }) => id!, @@ -1304,11 +1314,10 @@ export function Component() { withDiff={withDiff} /> @@ -1430,6 +1439,14 @@ export function Component() { {enabling_approaches?.map((approach) => { const prevApproach = prevEnableApproachesMapping?.[approach.approach]; + const apCodeApproachValue = apCodeOptions?.approach_ap_codes + ?.[approach.approach]?.join(', '); + + const prevApCodeApproachValue = isDefined(prevApproach) + ? apCodeOptions?.approach_ap_codes + ?.[prevApproach.approach]?.join(', ') + : '-'; + const prevApproachIndicatorMap = listToMap( prevApproach?.indicators, ({ id }) => id!, @@ -1470,9 +1487,9 @@ export function Component() { /> diff --git a/app/src/views/EapFullForm/EapActivationProcess/index.tsx b/app/src/views/EapFullForm/EapActivationProcess/index.tsx index 7e02ffba95..66677083ea 100644 --- a/app/src/views/EapFullForm/EapActivationProcess/index.tsx +++ b/app/src/views/EapFullForm/EapActivationProcess/index.tsx @@ -425,7 +425,7 @@ function EapActivationProcess(props: Props) { > {strings.financeDownloadDescription} )} + withAsteriskOnTitle > { + if (!val) { + return; + } + setFieldValue(getFullDateFromYearMonth(val), 'expected_submission_time'); + }, [setFieldValue]); + const { setValue: onKeyActorsChange, removeValue: onKeyActorsRemove } = useFormArray<'key_actors', KeyActorsFormFields>( 'key_actors', setFieldValue, @@ -214,11 +227,12 @@ function Overview(props: Props) { > + + +
[number]; +type EapApproachApCodeOption = NonNullable['approach_ap_codes']>; + const defaultApproachValue: EnableApproachesFormFields = { approach: 10, }; @@ -35,6 +40,7 @@ interface Props { disabled?: boolean; approachTitle?: React.ReactNode; readOnly?: boolean; + approachApCodeOption?: EapApproachApCodeOption; } function ApproachesInput(props: Props) { @@ -47,11 +53,14 @@ function ApproachesInput(props: Props) { disabled, approachTitle, readOnly, + approachApCodeOption, } = props; const strings = useTranslation(i18n); const onFieldChange = useFormObject(index, onChange, defaultApproachValue); + const apCodeValue = approachApCodeOption?.[value?.approach]?.join(', '); + const error = (value && value.approach && errorFromProps) ? getErrorObject(errorFromProps?.[value.approach]) : undefined; @@ -90,14 +99,12 @@ function ApproachesInput(props: Props) { readOnly={readOnly} error={error?.budget_per_approach} /> - diff --git a/app/src/views/EapFullForm/SelectionActions/OperationInput/index.tsx b/app/src/views/EapFullForm/SelectionActions/OperationInput/index.tsx index 340638c391..3800052ad3 100644 --- a/app/src/views/EapFullForm/SelectionActions/OperationInput/index.tsx +++ b/app/src/views/EapFullForm/SelectionActions/OperationInput/index.tsx @@ -4,8 +4,10 @@ import { ExpandableContainer, ListView, NumberInput, + TextInput, } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; +import { noOp } from '@togglecorp/fujs'; import { type ArrayError, getErrorObject, @@ -15,6 +17,7 @@ import { import EapIndicatorListInput from '#components/domain/EapIndicatorListInput'; import EapOperationActivityListInput from '#components/domain/EapOperationActivityListInput'; +import { type GoApiResponse } from '#utils/restRequest'; import { type PartialEapFullFormType } from '../../schema'; @@ -24,6 +27,8 @@ type PlannedOperationFormFields = NonNullable< PartialEapFullFormType['planned_operations'] >[number]; +type EapSectorApCodeOption = NonNullable['sector_ap_codes']>; + const defaultOperationValue: PlannedOperationFormFields = { sector: 101, }; @@ -40,6 +45,7 @@ interface Props { disabled?: boolean; operationTitle?: React.ReactNode; readOnly?: boolean; + sectorApCodeOption?: EapSectorApCodeOption; } function OperationsInput(props: Props) { @@ -52,11 +58,14 @@ function OperationsInput(props: Props) { disabled, operationTitle, readOnly, + sectorApCodeOption, } = props; const strings = useTranslation(i18n); const onFieldChange = useFormObject(index, onChange, defaultOperationValue); + const apCodeValue = sectorApCodeOption?.[value.sector]?.join(', '); + const error = value && value.sector && errorFromProps ? getErrorObject(errorFromProps?.[value.sector]) : undefined; @@ -106,15 +115,12 @@ function OperationsInput(props: Props) { error={error?.budget_per_sector} readOnly={readOnly} /> - diff --git a/app/src/views/EapFullForm/SelectionActions/i18n.json b/app/src/views/EapFullForm/SelectionActions/i18n.json index 23fdbc5ba2..ac5f65b8e9 100644 --- a/app/src/views/EapFullForm/SelectionActions/i18n.json +++ b/app/src/views/EapFullForm/SelectionActions/i18n.json @@ -1,9 +1,9 @@ { "namespace": "eapFullForm", "strings": { - "selectionActionsHeading": "Selection of actions", + "selectionActionsHeading": "Selection of Actions", "selectionActionsTooltipDescription": "It is crucial to select early actions that have the most potential to reduce the identified risks(s) and are feasible to implement given the lead time of the forecast. It is important to describe briefly: Who was involved? What data was consulted? Was research conducted? Were communities involved? For more guidance see FbF Manual, Chapter 4.2 Select Early Actions.", - "selectionProcessTitle": "Early action selection process", + "selectionProcessTitle": "Early Action Selection Process", "selectionProcessDescription1": "Add the list of early actions", "selectionProcessDescription2": "Based on the prioritized impacts/risks of section 3, provide a brief rationale (e.g. your criteria) for the selection of action(s), also indicating which actions were considered but not chosen.", "selectionProcessDescription3": "Describe the process used to select the actions: Who was consulted? Which assessments/research were carried out?", @@ -23,6 +23,7 @@ "earlyActionsEmptyMessage": "No actions yet!", "selectionActionUploadLabel": "Upload", "selectionActionSelectImagesLabel": "Select images", + "theoryOfChangeTableTitle": "Theory of Change Table", "downloadTableLabel": "Download Theory of Change table", "selectionActionUploadTableLabel": "Upload Table", "evidenceBaseTitle": "Evidence Base", diff --git a/app/src/views/EapFullForm/SelectionActions/index.tsx b/app/src/views/EapFullForm/SelectionActions/index.tsx index 8ea376b883..af71b9f9a0 100644 --- a/app/src/views/EapFullForm/SelectionActions/index.tsx +++ b/app/src/views/EapFullForm/SelectionActions/index.tsx @@ -118,6 +118,10 @@ function SelectionActions(props: Props) { }, }); + const { response: apCodeOptions } = useRequest({ + url: '/api/v2/eap/options/', + }); + const { setValue: onOperationChange, removeValue: onOperationRemove } = useFormArray<'planned_operations', PlannedOperationFormFields>( 'planned_operations', setFieldValue, @@ -441,6 +445,7 @@ function SelectionActions(props: Props) { /> )} + withAsteriskOnTitle > ))} diff --git a/app/src/views/EapFullForm/TriggerModel/i18n.json b/app/src/views/EapFullForm/TriggerModel/i18n.json index 47f3935a11..4e1b87ff1c 100644 --- a/app/src/views/EapFullForm/TriggerModel/i18n.json +++ b/app/src/views/EapFullForm/TriggerModel/i18n.json @@ -14,7 +14,8 @@ "addNewTriggerButtonLabel": "Add", "sourcesForecastDescription": "Add respective source(s) for the forecast and the link to the website, if applicable.", "forecastExplanatoryNote": "In order for the DREF to approve funding when a trigger occurs, there must be a certain probability the extreme event will take place. To safeguard this, it is crucial to select those forecasts that have a certain “skill” level (certain level of confidence). If observations are used, these can also be included in the table. Note that this information does not need to be calculated by the National Society but can be obtained by working with hydro-meteorological services, research institutions, experts etc.", - "forecastSelectionTitle": "Forecast selection", + "forecastSelectionTitle": "Forecast Selection", + "forecastTableDetails": "Forecast Table Details", "downloadForecastTableLabel": "Download Menu of Forecast", "forecastRequiredPoint1": "State clearly which forecast(s) and observations will be used and why they were chosen.", "forecastRequiredPoint2": "Include a table with all available forecasts for your hazard. The table must include: Name of forecast, Lead time, Source, False Alarm Ratio, Number of times the forecast has been issued for this hazard in the last 10 years ", diff --git a/app/src/views/EapFullForm/TriggerModel/index.tsx b/app/src/views/EapFullForm/TriggerModel/index.tsx index ecfc5b38b5..b0cbf9cb61 100644 --- a/app/src/views/EapFullForm/TriggerModel/index.tsx +++ b/app/src/views/EapFullForm/TriggerModel/index.tsx @@ -286,6 +286,7 @@ function TriggerModel(props: Props) { numPreferredColumns={2} > )} + withAsteriskOnTitle > {strings.triggerUploadTableLabel} @@ -592,7 +599,7 @@ function TriggerModel(props: Props) { > (); + const [searchParams] = useSearchParams(); const version = searchParams.get('version') ?? undefined; @@ -193,7 +197,9 @@ export function Component() { prioritized_impact_images, early_action_implementation_images, budget_file_details, + forecast_table_file_details, updated_checklist_file_details, + theory_of_change_table_file_details, } = response; return { ...prevMap, @@ -210,7 +216,9 @@ export function Component() { ...(prioritized_impact_images ?? []), ...(early_action_implementation_images ?? []), budget_file_details, + forecast_table_file_details, updated_checklist_file_details, + theory_of_change_table_file_details, ] .map((eapFile) => { if (isNotDefined(eapFile)) { @@ -518,6 +526,16 @@ export function Component() { const [index] = match; return formValue?.trigger_activation_system_images?.[index!]?.client_id; } + match = matchArray(locations, ['planned_operations', + NUM, + 'indicators', + NUM, + ]); + if (isDefined(match)) { + const [planned_intervention_index, index] = match; + return formValue?.planned_operations?.[planned_intervention_index!] + ?.indicators?.[index!]?.client_id; + } match = matchArray(locations, [ 'planned_operations', NUM, @@ -556,6 +574,16 @@ export function Component() { const [poIndex] = match; return formValue?.planned_operations?.[poIndex!]?.sector; } + match = matchArray(locations, ['enabling_approaches', + NUM, + 'indicators', + NUM, + ]); + if (isDefined(match)) { + const [planned_intervention_index, index] = match; + return formValue?.enabling_approaches?.[planned_intervention_index!] + ?.indicators?.[index!]?.client_id; + } match = matchArray(locations, [ 'enabling_approaches', NUM, @@ -608,15 +636,15 @@ export function Component() { response: fullEapResponse, error: fullEapResponseError, } = useRequest({ - skip: isNotDefined(latestFullEapId), + skip: isNotDefined(currentFullEapId), url: '/api/v2/full-eap/{id}/', - pathVariables: isDefined(latestFullEapId) - ? { id: latestFullEapId } + pathVariables: isDefined(currentFullEapId) + ? { id: currentFullEapId } : undefined, onSuccess: (response) => { loadResponseToFormValue(response); - processServerErrors(state.error, value); + processServerErrors(state?.error, value); // NOTE state was used to pass error through navigation. // and cleared here to prevent stale error from reappearing on page refresh @@ -726,6 +754,7 @@ export function Component() { const { expected_submission_time, + partners, national_society_contact_email, national_society_contact_name, national_society_contact_title, @@ -754,6 +783,7 @@ export function Component() { ifrc_head_of_delegation_name: ifrc_contact_name, ifrc_head_of_delegation_title: ifrc_contact_title, ifrc_head_of_delegation_phone_number: ifrc_contact_phone_number, + partners, }); }, [eapDetailResponse, fullEapPending, fullEapResponse, setValue]); @@ -859,7 +889,13 @@ export function Component() { return ( (key: KEY, requi export const formSchema: EapFullFormSchema = { fields: (formValue, __, context): EapFullFormSchemaFields => { - const isSubmit = context?.getIsSubmission(); + const isSubmit = context?.getIsSubmission() ?? false; let formFields: EapFullFormSchemaFields = { // ------------Overview----------------- expected_submission_time: { required: isSubmit }, @@ -603,6 +603,9 @@ export const formSchema: EapFullFormSchema = { }, }), }, + partners: { + required: isSubmit, + }, partner_contacts: { keySelector: (item) => item.client_id, member: () => ({ @@ -919,15 +922,12 @@ export const formSchema: EapFullFormSchema = { budget_per_sector: { required: isSubmit, }, - ap_code: { - required: isSubmit, - }, indicators: { keySelector: (indicator) => indicator.client_id, - member: () => indicatorSchema, + member: () => indicatorSchema(isSubmit), validation: (indicators) => { - if (isNotDefined(indicators) - || indicators.length === 0) { + if (isSubmit && (isNotDefined(indicators) + || indicators.length === 0)) { return 'This field is required'; } @@ -942,15 +942,15 @@ export const formSchema: EapFullFormSchema = { }, early_action_activities: { keySelector: (item) => item.client_id, - member: () => operationActivitySchema, + member: () => operationActivitySchema(isSubmit), }, readiness_activities: { keySelector: (item) => item.client_id, - member: () => operationActivitySchema, + member: () => operationActivitySchema(isSubmit), }, prepositioning_activities: { keySelector: (item) => item.client_id, - member: () => operationActivitySchema, + member: () => operationActivitySchema(isSubmit), }, }), }), @@ -975,15 +975,12 @@ export const formSchema: EapFullFormSchema = { budget_per_approach: { required: isSubmit, }, - ap_code: { - required: isSubmit, - }, indicators: { keySelector: (indicator) => indicator.client_id, - member: () => indicatorSchema, + member: () => indicatorSchema(isSubmit), validation: (indicators) => { - if (isNotDefined(indicators) - || indicators.length === 0) { + if (isSubmit && (isNotDefined(indicators) + || indicators.length === 0)) { return 'This field is required'; } @@ -992,15 +989,15 @@ export const formSchema: EapFullFormSchema = { }, early_action_activities: { keySelector: (item) => item.client_id, - member: () => operationActivitySchema, + member: () => operationActivitySchema(isSubmit), }, readiness_activities: { keySelector: (item) => item.client_id, - member: () => operationActivitySchema, + member: () => operationActivitySchema(isSubmit), }, prepositioning_activities: { keySelector: (item) => item.client_id, - member: () => operationActivitySchema, + member: () => operationActivitySchema(isSubmit), }, }), }), @@ -1072,7 +1069,7 @@ export const formSchema: EapFullFormSchema = { people_targeted: { required: isSubmit, validations: [ - lessThanOrEqualToCondition(10000), + greaterThanOrEqualToCondition(charLimits.people_targeted), positiveIntegerCondition, ], }, diff --git a/app/src/views/EapRegistration/index.tsx b/app/src/views/EapRegistration/index.tsx index faea07076a..c46982c13c 100644 --- a/app/src/views/EapRegistration/index.tsx +++ b/app/src/views/EapRegistration/index.tsx @@ -37,6 +37,10 @@ import Page from '#components/Page'; import useGlobalEnums from '#hooks/domain/useGlobalEnums'; import useAlert from '#hooks/useAlert'; import useRouting from '#hooks/useRouting'; +import { + getFullDateFromYearMonth, + getYearMonthFromFullDate, +} from '#utils/domain/eap'; import { type GoApiResponse, useLazyRequest, @@ -115,12 +119,19 @@ export function Component() { }, }); + const onExpectedSubmissionTimeChange = useCallback((val: string | undefined) => { + if (!val) { + return; + } + setFieldValue(getFullDateFromYearMonth(val), 'expected_submission_time'); + }, [setFieldValue]); + const handleEapTypeNotSureClick = useCallback(() => { - setFieldValue(null, 'eap_type'); + setFieldValue(undefined, 'eap_type'); }, [setFieldValue]); const handleSubmissionTimeNotSureClick = useCallback(() => { - setFieldValue(null, 'expected_submission_time'); + setFieldValue(undefined, 'expected_submission_time'); }, [setFieldValue]); const handleRegisterFormValidation = useCallback((formValues: EapRegisterFormFields) => { @@ -272,8 +283,9 @@ export function Component() { > ; +export type EapRegisterRequestBody = PurgeNull>; export const defaultFormValue: EapRegisterFormFields = { eap_type: undefined, diff --git a/app/src/views/EapSimplifiedExport/index.tsx b/app/src/views/EapSimplifiedExport/index.tsx index 38ecbe6c7e..2c0d5a8290 100644 --- a/app/src/views/EapSimplifiedExport/index.tsx +++ b/app/src/views/EapSimplifiedExport/index.tsx @@ -97,6 +97,10 @@ export function Component() { : undefined, }); + const { response: apCodeOptions } = useRequest({ + url: '/api/v2/eap/options/', + }); + const { eap_sector, eap_approach } = useGlobalEnums(); const eapSectorTitleMap = listToMap( @@ -509,6 +513,14 @@ export function Component() { {planned_operations?.map((operation) => { const prevOperation = prevPlannedOperationMap?.[operation.sector]; + const apCodeSectorValue = apCodeOptions?.sector_ap_codes + ?.[operation.sector]?.join(', '); + + const prevApCodeSectorValue = isDefined(prevOperation) + ? apCodeOptions?.sector_ap_codes + ?.[prevOperation?.sector]?.join(', ') + : '-'; + const prevOperationIndicatorMap = listToMap( prevOperation?.indicators, ({ id }) => id!, @@ -557,9 +569,9 @@ export function Component() { /> @@ -685,6 +697,14 @@ export function Component() { {enabling_approaches?.map((approach) => { const prevApproach = prevEnablingApproachesMap?.[approach.approach]; + const apCodeApproachValue = apCodeOptions?.approach_ap_codes + ?.[approach.approach]?.join(', '); + + const prevApCodeApproachValue = isDefined(prevApproach) + ? apCodeOptions?.approach_ap_codes + ?.[prevApproach.approach]?.join(', ') + : '-'; + const prevApproachIndicatorMap = listToMap( prevApproach?.indicators, ({ id }) => id!, @@ -725,9 +745,9 @@ export function Component() { /> diff --git a/app/src/views/EapSimplifiedForm/EarlyAction/index.tsx b/app/src/views/EapSimplifiedForm/EarlyAction/index.tsx index 474f92de73..870e848dec 100644 --- a/app/src/views/EapSimplifiedForm/EarlyAction/index.tsx +++ b/app/src/views/EapSimplifiedForm/EarlyAction/index.tsx @@ -466,7 +466,8 @@ function EarlyAction(props: Props) { labelSelector={stringValueSelector} options={eapTimeframeOption} error={error?.operational_timeframe_unit} - disabled + disabled={disabled} + readOnly />