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/ExplanatoryNote/index.tsx b/app/src/components/ExplanatoryNote/index.tsx new file mode 100644 index 0000000000..8fdcfa2bcf --- /dev/null +++ b/app/src/components/ExplanatoryNote/index.tsx @@ -0,0 +1,34 @@ +import { InformationLineIcon } from '@ifrc-go/icons'; + +import InfoModal from '#components/domain/InfoModal'; + +import styles from './styles.module.css'; + +interface Props { + content: React.ReactNode; + heading: string; + ariaLabel: string; + title: string; +} + +function ExplanatoryNote(props: Props) { + const { + content, + heading, + ariaLabel, + title, + } = props; + + return ( + } + ariaLabel={ariaLabel} + title={title} + /> + ); +} + +export default ExplanatoryNote; diff --git a/app/src/components/ExplanatoryNote/styles.module.css b/app/src/components/ExplanatoryNote/styles.module.css new file mode 100644 index 0000000000..a898de748a --- /dev/null +++ b/app/src/components/ExplanatoryNote/styles.module.css @@ -0,0 +1,4 @@ +.icon { + /* FIXME: use variables */ + font-size: 1rem; +} 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 3a7553d32a..0cdd7eb833 100644 --- a/app/src/components/domain/EapOperationActivityListInput/index.tsx +++ b/app/src/components/domain/EapOperationActivityListInput/index.tsx @@ -6,7 +6,7 @@ import { AddLineIcon } from '@ifrc-go/icons'; import { Button, Container, - InfoPopup, + Description, ListView, } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; @@ -18,19 +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; @@ -38,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, @@ -64,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( @@ -123,9 +128,16 @@ function EapOperationActivityListInput(props: Props heading={( {title} - {description && ( - + {description} + + )} /> )} @@ -154,12 +166,13 @@ function EapOperationActivityListInput(props: Props > {value?.map((activity, i) => ( diff --git a/app/src/components/domain/InfoModal/index.tsx b/app/src/components/domain/InfoModal/index.tsx index d0c4502be8..ddcf1f5383 100644 --- a/app/src/components/domain/InfoModal/index.tsx +++ b/app/src/components/domain/InfoModal/index.tsx @@ -1,24 +1,44 @@ import { Button, type ButtonProps, + IconButton, Modal, } from '@ifrc-go/ui'; import { useBooleanState } from '@ifrc-go/ui/hooks'; +type IconButtonProps = { + icon: React.ReactNode; + ariaLabel: string; + label?: never; + title: string; +} + +type LabelButtonProps = { + icon?: never; + ariaLabel?: string; + label?: string; +} + +type ButtonTypeProps = IconButtonProps | LabelButtonProps; + // FIXME: make the props consistent with other similar components // e.g. DropdownMenu -interface Props extends ButtonProps { +interface BaseProps extends ButtonProps { heading?: string; modalContent: React.ReactNode; - label?: string; } +type Props = BaseProps & ButtonTypeProps; + // FIXME: this component should be in `/components` function InfoModal(props: Props) { const { heading, label, modalContent, + ariaLabel, + title, + icon, ...otherButtonProps } = props; @@ -32,14 +52,25 @@ function InfoModal(props: Props) { return ( <> - + {icon ? ( + + {icon} + + ) : ( + + )} {showInfoModal && ( { + 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/constants.ts b/app/src/utils/constants.ts index 5f3c04dd69..92c3646975 100644 --- a/app/src/utils/constants.ts +++ b/app/src/utils/constants.ts @@ -242,4 +242,3 @@ export const EAP_STATUS_NS_ADDRESSING_COMMENTS = 30 satisfies EapStatus; export const EAP_STATUS_TECHNICALLY_VALIDATED = 40 satisfies EapStatus; export const EAP_STATUS_PENDING_PFA = 50 satisfies EapStatus; export const EAP_STATUS_APPROVED = 60 satisfies EapStatus; -export const EAP_STATUS_ACTIVATED = 70 satisfies EapStatus; 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/EapStatus/index.tsx b/app/src/views/AccountMyFormsEap/EapStatus/index.tsx index b1367f130e..6ecb4835f5 100644 --- a/app/src/views/AccountMyFormsEap/EapStatus/index.tsx +++ b/app/src/views/AccountMyFormsEap/EapStatus/index.tsx @@ -30,7 +30,6 @@ import { type components } from '#generated/types'; import useGlobalEnums from '#hooks/domain/useGlobalEnums'; import useAlert from '#hooks/useAlert'; import { - EAP_STATUS_ACTIVATED, EAP_STATUS_APPROVED, EAP_STATUS_NS_ADDRESSING_COMMENTS, EAP_STATUS_PENDING_PFA, @@ -67,8 +66,7 @@ const validStatusTransition: Record = { EAP_STATUS_PENDING_PFA, ], [EAP_STATUS_PENDING_PFA]: [EAP_STATUS_APPROVED], - [EAP_STATUS_APPROVED]: [EAP_STATUS_ACTIVATED], - [EAP_STATUS_ACTIVATED]: [], + [EAP_STATUS_APPROVED]: [], }; export interface Props { diff --git a/app/src/views/AccountMyFormsEap/EapTableActions/i18n.json b/app/src/views/AccountMyFormsEap/EapTableActions/i18n.json index b7706a26f5..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 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 fbd8c69ba3..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, @@ -19,6 +25,9 @@ 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, @@ -27,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'; @@ -53,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); @@ -82,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; @@ -119,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) && ( @@ -151,7 +262,7 @@ function EapTableActions(props: Props) { )} {type === 'development' && ( <> - {eap.eap_type === EAP_TYPE_SIMPLIFIED && isCreated && ( + {environment === 'development' && eap.eap_type === EAP_TYPE_SIMPLIFIED && isCreated && ( )} - {eap.eap_type === EAP_TYPE_FULL && isCreated && ( + {environment === 'development' && eap.eap_type === EAP_TYPE_FULL && isCreated && ( )} - {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 && ( @@ -196,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) && ( @@ -205,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) && ( @@ -320,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/AccountMyFormsLayout/i18n.json b/app/src/views/AccountMyFormsLayout/i18n.json index c23cf7ce5b..992e8f5685 100644 --- a/app/src/views/AccountMyFormsLayout/i18n.json +++ b/app/src/views/AccountMyFormsLayout/i18n.json @@ -3,8 +3,8 @@ "strings": { "fieldReportTabTitle": "Field Report", "perTabTitle": "PER", - "drefTabTitle": "DREF", + "drefTabTitle": "DREF Applications", "threeWTabTitle": "3W", "eapApplications": "EAP Applications" } -} \ No newline at end of file +} 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/i18n.json b/app/src/views/EapFullForm/EapActivationProcess/i18n.json index f328b0177b..86a443d576 100644 --- a/app/src/views/EapFullForm/EapActivationProcess/i18n.json +++ b/app/src/views/EapFullForm/EapActivationProcess/i18n.json @@ -10,10 +10,10 @@ "activationProcessExplanatoryLabel": "Explanatory Note", "activationProcessRequiredPointsLabel": "Required Points", "activationImplementationExplanatoryNote": "As a crucial component of the EAP, once the trigger has been reached, everyone involved should be knowledgeable about what will be done, where, when and by whom. The described implementation process shows that each step of the activation has been thought through and considered and that implementation in the lead time available is possible. The set of tasks described in this section should cover all activities from the moment the trigger is reached (Day 1) to the completion of post-impact surveys (Day X).", - "activationImplementationRequiredPoint1": "Include a matrix/flowchart for a quick overview of the early action implementation process.", - "activationImplementationRequiredPoint2": "Describe the step-by-step process from Day 1 to Day X for the implementation of the selected early actions. Indicate the day when the Stop Mechanism would occur. Include all critical and support tasks that are necessary for each of the steps. Each task should indicate the position of the person responsible (including when cash-based actions are planned liaison with the financial service provider).", - "activationImplementationRequiredPoint3": "For each action, include at which level it will take place (HQ, branch, community).", - "activationImplementationRequiredPoint4": "Each NS should have a detailed version of this process, including communication flows, for each task and the name of the person responsible with their contact information. This document should be regularly updated.", + "activationRequiredPoint1": "Include a matrix/flowchart for a quick overview of the early action implementation process.", + "activationRequiredPoint2": "Describe the step-by-step process from Day 1 to Day X for the implementation of the selected early actions. Indicate the day when the Stop Mechanism would occur. Include all critical and support tasks that are necessary for each of the steps. Each task should indicate the position of the person responsible (including when cash-based actions are planned liaison with the financial service provider).", + "activationRequiredPoint3": "For each action, include at which level it will take place (HQ, branch, community).", + "activationRequiredPoint4": "Each NS should have a detailed version of this process, including communication flows, for each task and the name of the person responsible with their contact information. This document should be regularly updated.", "activationProcessUploadLabel": "Upload", "activationTriggerTitle": "Trigger activation system", "activationTriggerDescription1": "Describe the automatic system used to monitor the forecasts, generate the intervention map and send the alert message when the trigger is reached.", @@ -31,9 +31,9 @@ "activationSelectionDescription3": "If the EAP is intending to use Social Protection systems or other government beneficiary databases, indicate how the potential number of targeted households be selected", "activationSelectionExplanatoryNote": "FbF aims to protect the most vulnerable from the impact of extreme weather events. Based on the analysis on vulnerability and exposure (in section 3) and on the described mechanism for identifying intervention areas/communities (in section 4- Intervention area), it needs to be clear, how vulnerability criteria and impact forecasts will be applied to determine who will be targeted.", "activationStopMechanismTitle": "Stop Mechanism", - "activationStopMechanismDescription1": "Indicate on which day of activation the stop mechanism is foreseen, and who is responsible to give the signal to stop.", - "activationStopMechanismDescription2": "Describe when the stop mechanism begins and whether in-kind/cash distribution would be stopped or not. For cash actions cancelled, how would this be coordinated with the financial service provider? For in-kind distribution, what would happen with the perishable items?", - "activationStopMechanismDescription3": "Explain how it would be communicated to communities and stakeholders that the activities are being stopped.", + "activationStopDescription1": "Indicate on which day of activation the stop mechanism is foreseen, and who is responsible to give the signal to stop.", + "activationStopDescription2": "Describe when the stop mechanism begins and whether in-kind/cash distribution would be stopped or not. For cash actions cancelled, how would this be coordinated with the financial service provider? For in-kind distribution, what would happen with the perishable items?", + "activationStopDescription3": "Explain how it would be communicated to communities and stakeholders that the activities are being stopped.", "activationStopMechanismExplanatoryNote": "For forecast triggers with a lead time of more than three days, the EAP should include the description of a stop mechanism. This means that if a later forecast – prior to the start of activities (related to the early action(s)) shows that the event is no longer likely to occur, the activation of the EAP will be stopped to avoid generating further use of resources. For example, if the 6-day forecast on Day 1 indicates high risk of heavy rainfall and thereby triggers the activation and the new 6-day-forecast released on Day 3 shows that the risk has significantly lowered, the trigger level is no longer reached. If the start of distributions was planned for Day 4, activation should be stopped. Items that have been purchased based on the trigger being reached and are not distributed due to the stop mechanism should be stored in the warehouse for a future activation. For forecast triggers with a lead time of less than 3 days, the EAP should include the description of what the National Society would do if the forecast changes in strength or location within the last three days before the event.", "activationAttachFilesTitle": "Attach Relevant Files", "activationAttachFilesDescription": "Attach any additional maps, documentation, files, images, etc.", diff --git a/app/src/views/EapFullForm/EapActivationProcess/index.tsx b/app/src/views/EapFullForm/EapActivationProcess/index.tsx index 84c03075ed..66677083ea 100644 --- a/app/src/views/EapFullForm/EapActivationProcess/index.tsx +++ b/app/src/views/EapFullForm/EapActivationProcess/index.tsx @@ -4,13 +4,11 @@ import { Button, Container, Description, - InfoPopup, InputSection, Label, ListView, NumberInput, TextArea, - TextOutput, } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; import { randomString } from '@togglecorp/fujs'; @@ -24,6 +22,7 @@ import { import GoMultiFileInput from '#components/domain/GoMultiFileInput'; import MultiImageWithCaptionInput from '#components/domain/MultiImageWithCaptionInput'; +import ExplanatoryNote from '#components/ExplanatoryNote'; import NonFieldError from '#components/NonFieldError'; import TabPage from '#components/TabPage'; @@ -130,7 +129,16 @@ function EapActivationProcess(props: Props) { heading={( {strings.activationProcessHeading} - + + {strings.activationProcessTooltip} + + )} + /> )} variant="form" @@ -141,34 +149,45 @@ function EapActivationProcess(props: Props) { > - - -
  • - {strings.activationImplementationRequiredPoint1} -
  • -
  • - {strings.activationImplementationRequiredPoint2} -
  • -
  • - {strings.activationImplementationRequiredPoint3} -
  • -
  • - {strings.activationImplementationRequiredPoint4} -
  • - - )} - /> -
    + headerActions={( + + + + + {strings.activationImplementationExplanatoryNote} + + + + + +
      +
    • + {strings.activationRequiredPoint1} +
    • +
    • + {strings.activationRequiredPoint2} +
    • +
    • + {strings.activationRequiredPoint3} +
    • +
    • + {strings.activationRequiredPoint4} +
    • +
    +
    +
    + + )} + /> )} description={(
      @@ -205,25 +224,42 @@ function EapActivationProcess(props: Props) { - - -
    • {strings.activationTriggerRequiredPoint1}
    • -
    • {strings.activationTriggerRequiredPoint2}
    • -
    • {strings.activationTriggerRequiredPoint3}
    • -
    - )} - /> - + headerActions={( + + + + + {strings.activationTriggerExplanatoryNote} + + + + + +
      +
    • + {strings.activationTriggerRequiredPoint1} +
    • +
    • + {strings.activationTriggerRequiredPoint2} +
    • +
    • + {strings.activationTriggerRequiredPoint3} +
    • +
    +
    +
    + + )} + /> )} description={(
      @@ -274,25 +310,42 @@ function EapActivationProcess(props: Props) { - - -
    • {strings.activationSelectionDescription1}
    • -
    • {strings.activationSelectionDescription2}
    • -
    • {strings.activationSelectionDescription3}
    • -
    - )} - /> - + headerActions={( + + + + + {strings.activationSelectionExplanatoryNote} + + + + + +
      +
    • + {strings.activationSelectionDescription1} +
    • +
    • + {strings.activationSelectionDescription2} +
    • +
    • + {strings.activationSelectionDescription3} +
    • +
    +
    +
    + + )} + /> )} description={(
      @@ -316,31 +369,42 @@ function EapActivationProcess(props: Props) { - - -
    • {strings.activationStopMechanismDescription1}
    • -
    • {strings.activationStopMechanismDescription2}
    • -
    • {strings.activationStopMechanismDescription3}
    • -
    - )} - /> - + headerActions={( + + + + + {strings.activationStopMechanismExplanatoryNote} + + + + + +
      +
    • {strings.activationStopDescription1}
    • +
    • {strings.activationStopDescription2}
    • +
    • {strings.activationStopDescription3}
    • +
    +
    +
    + + )} + /> )} description={(
      -
    • {strings.activationStopMechanismDescription1}
    • -
    • {strings.activationStopMechanismDescription2}
    • -
    • {strings.activationStopMechanismDescription3}
    • +
    • {strings.activationStopDescription1}
    • +
    • {strings.activationStopDescription2}
    • +
    • {strings.activationStopDescription3}
    )} > @@ -361,7 +425,7 @@ function EapActivationProcess(props: Props) { > + + + {strings.financeExplanatoryNote} + + + )} /> )} description={( @@ -162,15 +174,14 @@ function FinanceLogistics(props: Props) { )} withAsteriskOnTitle > - +