diff --git a/app/src/components/domain/SourceInformationInput/index.tsx b/app/src/components/domain/SourceInformationInput/index.tsx index 4a4da8b968..af5923a2b8 100644 --- a/app/src/components/domain/SourceInformationInput/index.tsx +++ b/app/src/components/domain/SourceInformationInput/index.tsx @@ -5,10 +5,7 @@ import { TextInput, } from '@ifrc-go/ui'; import { useTranslation } from '@ifrc-go/ui/hooks'; -import { - isNotDefined, - randomString, -} from '@togglecorp/fujs'; +import { randomString } from '@togglecorp/fujs'; import { type ArrayError, getErrorObject, @@ -17,17 +14,23 @@ import { } from '@togglecorp/toggle-form'; import NonFieldError from '#components/NonFieldError'; +import { formatSourceLink } from '#utils/common'; import { type PartialDref } from '#views/DrefApplicationForm/schema'; import i18n from './i18n.json'; import styles from './styles.module.css'; -type SourceInformationFormFields = NonNullable[number]; +type SourceInformationFormFields = NonNullable< + PartialDref['source_information'] +>[number]; interface Props { value: SourceInformationFormFields; error: ArrayError | undefined; - onChange: (value: SetValueArg, index: number) => void; + onChange: ( + value: SetValueArg, + index: number + ) => void; onRemove: (index: number) => void; index: number; disabled?: boolean; @@ -47,39 +50,17 @@ function SourceInformationInput(props: Props) { const strings = useTranslation(i18n); - const onFieldChange = useFormObject( - index, - onChange, - () => ({ - client_id: randomString(), - }), - ); + const onFieldChange = useFormObject(index, onChange, () => ({ + client_id: randomString(), + })); - const error = (value && value.client_id && errorFromProps) + const error = value && value.client_id && errorFromProps ? getErrorObject(errorFromProps?.[value.client_id]) : undefined; const handleSourceFieldChange = useCallback( - (newValue: string | undefined) => { - if ( - isNotDefined(newValue) - || newValue.startsWith('http://') - || newValue.startsWith('https://') - || newValue === 'h' - || newValue === 'ht' - || newValue === 'htt' - || newValue === 'http' - || newValue === 'http:' - || newValue === 'http:/' - || newValue === 'https' - || newValue === 'https:' - || newValue === 'https:/' - ) { - onFieldChange(newValue, 'source_link'); - return; - } - - onFieldChange(`https://${newValue}`, 'source_link'); + (linkValue: string | undefined) => { + onFieldChange(formatSourceLink(linkValue), 'source_link'); }, [onFieldChange], ); diff --git a/app/src/utils/common.ts b/app/src/utils/common.ts index 50a375b918..6b08f51e6f 100644 --- a/app/src/utils/common.ts +++ b/app/src/utils/common.ts @@ -72,6 +72,27 @@ export function joinStrings( return values.filter(Boolean).join(separator); } +export function formatSourceLink(value: string | undefined): string | undefined { + if ( + isNotDefined(value) + || value.startsWith('http://') + || value.startsWith('https://') + || value === 'h' + || value === 'ht' + || value === 'htt' + || value === 'http' + || value === 'http:' + || value === 'http:/' + || value === 'https' + || value === 'https:' + || value === 'https:/' + ) { + return value; + } + + return `https://${value}`; +} + export function hasChanged(prevValue: unknown, newValue: unknown) { // NOTE: we consider `null` and `undefined` as same for // this scenario diff --git a/app/src/views/AccountMyFormsEap/EapTableActions/index.tsx b/app/src/views/AccountMyFormsEap/EapTableActions/index.tsx index c832d52d3f..2933bc53b1 100644 --- a/app/src/views/AccountMyFormsEap/EapTableActions/index.tsx +++ b/app/src/views/AccountMyFormsEap/EapTableActions/index.tsx @@ -64,6 +64,21 @@ function EapTableActions(props: Props) { setShowExportModal(false); }, []); + const latestVersion = useMemo(() => { + if (eap.eap_type === EAP_TYPE_SIMPLIFIED) { + return eap.latest_simplified_eap ?? undefined; + } + + if (eap.eap_type === EAP_TYPE_FULL) { + return eap.latest_full_eap ?? undefined; + } + + return undefined; + }, [eap]); + + const isCreated = isDefined(latestVersion); + const isLocked = isDefined(details) && !!details.data.is_locked; + const isLatestVersion = useMemo(() => { if (eap.eap_type === EAP_TYPE_SIMPLIFIED) { return eap.latest_simplified_eap === details?.data.id; @@ -76,10 +91,23 @@ function EapTableActions(props: Props) { return false; }, [eap, details]); - const isEditable = details?.data.is_locked === false && ( - eap.status === EAP_STATUS_UNDER_DEVELOPMENT - || eap.status === EAP_STATUS_NS_ADDRESSING_COMMENTS - ) && isLatestVersion; + const isEditable = useMemo(() => { + if (isCreated && !isLatestVersion) { + return false; + } + + if (isLocked) { + return false; + } + + if (eap.status !== EAP_STATUS_UNDER_DEVELOPMENT + && eap.status !== EAP_STATUS_NS_ADDRESSING_COMMENTS + ) { + return false; + } + + return true; + }, [isCreated, isLatestVersion, isLocked, eap]); return ( @@ -105,7 +133,7 @@ function EapTableActions(props: Props) { )} {type === 'development' && ( <> - {eap.eap_type === EAP_TYPE_SIMPLIFIED && ( + {eap.eap_type === EAP_TYPE_SIMPLIFIED && isCreated && ( )} - {eap.eap_type === EAP_TYPE_SIMPLIFIED && ( + {eap.eap_type === EAP_TYPE_SIMPLIFIED && isCreated && ( + + + )} + > + {strings.approvalFullEapDescription} + + ); +} + +export default ApprovalModal; diff --git a/app/src/views/EapFullForm/ContactInputsSection/i18n.json b/app/src/views/EapFullForm/ContactInputsSection/i18n.json new file mode 100644 index 0000000000..3958a25989 --- /dev/null +++ b/app/src/views/EapFullForm/ContactInputsSection/i18n.json @@ -0,0 +1,9 @@ +{ + "namespace": "eapFullForm", + "strings": { + "fullContactNameLabel": "Name", + "fullContactTitleLabel": "Title", + "fullContactEmailLabel": "Email", + "fullContactPhoneLabel": "Phone number" + } +} diff --git a/app/src/views/EapFullForm/ContactInputsSection/index.tsx b/app/src/views/EapFullForm/ContactInputsSection/index.tsx new file mode 100644 index 0000000000..fa2b1536e6 --- /dev/null +++ b/app/src/views/EapFullForm/ContactInputsSection/index.tsx @@ -0,0 +1,97 @@ +import { + InputSection, + TextInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { + type EntriesAsList, + type Error, + getErrorObject, +} from '@togglecorp/toggle-form'; + +import { + type PartialEapFullFormType, + type ValidContactFieldPrefixes, +} from '../schema'; + +import i18n from './i18n.json'; + +interface Props { + title?: React.ReactNode; + description?: React.ReactNode; + namePrefix: ValidContactFieldPrefixes; + value: PartialEapFullFormType; + setFieldValue: (...entries: EntriesAsList) => void; + error: Error | undefined; + disabled?: boolean; + readOnly?: boolean; +} + +function ContactInputsSection(props: Props) { + const { + title: sectionTitle, + description, + namePrefix, + value, + setFieldValue, + error: formError, + disabled, + readOnly, + } = props; + + const strings = useTranslation(i18n); + + const error = getErrorObject(formError); + + const name = `${namePrefix}_name` satisfies keyof PartialEapFullFormType; + const title = `${namePrefix}_title` satisfies keyof PartialEapFullFormType; + const email = `${namePrefix}_email` satisfies keyof PartialEapFullFormType; + const phoneNumber = `${namePrefix}_phone_number` satisfies keyof PartialEapFullFormType; + + return ( + + + + + + + ); +} + +export default ContactInputsSection; diff --git a/app/src/views/EapFullForm/EAPSourceInformationInput/i18n.json b/app/src/views/EapFullForm/EAPSourceInformationInput/i18n.json new file mode 100644 index 0000000000..fb51cf85e1 --- /dev/null +++ b/app/src/views/EapFullForm/EAPSourceInformationInput/i18n.json @@ -0,0 +1,8 @@ +{ + "namespace": "eapFullForm", + "strings": { + "eapSourceInformationNameLabel": "Name", + "eapSourceInformationLinkLabel": "Link", + "eapSourceInformationDeleteButton": "Delete Source Information" + } +} diff --git a/app/src/views/EapFullForm/EAPSourceInformationInput/index.tsx b/app/src/views/EapFullForm/EAPSourceInformationInput/index.tsx new file mode 100644 index 0000000000..4d7619b837 --- /dev/null +++ b/app/src/views/EapFullForm/EAPSourceInformationInput/index.tsx @@ -0,0 +1,115 @@ +import { useCallback } from 'react'; +import { DeleteBinTwoLineIcon } from '@ifrc-go/icons'; +import { + Button, + InlineView, + ListView, + TextInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { randomString } from '@togglecorp/fujs'; +import { + type ArrayError, + getErrorObject, + type PartialForm, + type SetValueArg, + useFormObject, +} from '@togglecorp/toggle-form'; + +import NonFieldError from '#components/NonFieldError'; +import { type components } from '#generated/types'; +import { formatSourceLink } from '#utils/common'; + +import i18n from './i18n.json'; + +type EAPSourceInformation = components['schemas']['EAPSourceInformation'] & { + client_id: string; +}; + +export type SourceInformationFormFields = PartialForm< + EAPSourceInformation, + 'client_id' +>; + +interface Props { + value: SourceInformationFormFields; + error: ArrayError | undefined; + onChange: ( + value: SetValueArg, + index: number + ) => void; + onRemove: (index: number) => void; + index: number; + disabled?: boolean; + readOnly?: boolean; +} + +function EAPSourceInformationInput(props: Props) { + const { + error: errorFromProps, + onChange, + value, + index, + onRemove, + disabled, + readOnly, + } = props; + + const strings = useTranslation(i18n); + + const onFieldChange = useFormObject(index, onChange, () => ({ + client_id: randomString(), + })); + + const error = value && value.client_id && errorFromProps + ? getErrorObject(errorFromProps?.[value.client_id]) + : undefined; + + const handleSourceFieldChange = useCallback( + (linkValue: string | undefined) => { + onFieldChange(formatSourceLink(linkValue), 'source_link'); + }, + [onFieldChange], + ); + + return ( + <> + + + + + )} + > + + + + + + + ); +} + +export default EAPSourceInformationInput; diff --git a/app/src/views/EapFullForm/EapActivationProcess/i18n.json b/app/src/views/EapFullForm/EapActivationProcess/i18n.json new file mode 100644 index 0000000000..b25e56db9a --- /dev/null +++ b/app/src/views/EapFullForm/EapActivationProcess/i18n.json @@ -0,0 +1,44 @@ +{ + "namespace": "eapFullForm", + "strings": { + "activationProcessHeading": "EAP Activation Process", + "activationProcessTooltip": "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.", + "activationProcessTitle": "Early action implementation process", + "activationProcessDescription1": "Include a matrix/flowchart for a quick overview of the early action implementation process.", + "activationProcessDescription2": "Early 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)... implementation process", + "activationProcessDescriptionLabel": "Description", + "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.", + "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.", + "activationTriggerDescription2": "If this automatic system does not yet exist, explain how forecasts will be monitored, intervention maps generated and how the relevant actors will be informed that the trigger has been reached.", + "activationTriggerDescription3": "Indicate who gives the signal to start the activation.", + "activationTriggerExplanatoryNote": "The activation process starts with the message that the trigger has been reached (on Day 1). Ideally, there is a system in place to automatically monitor the forecasts and send an automatic message of alert to relevant actors as soon as a trigger is reached. It is expected that this will be executed by the national meteorological office and/or national DRM authority. If this automatic system does not exist, a mechanism needs to be in place to monitor the forecasts and alert relevant actors as soon as a trigger is reached to initiate the early actions.", + "activationTriggerRequiredPoint1": "Describe the automatic system used to monitor the forecasts, generate the intervention map and send the alert message when the trigger is reached.", + "activationTriggerRequiredPoint2": "If this automatic system does not yet exist, explain how forecasts will be monitored, intervention maps generated and how the relevant actors will be informed that the trigger has been reached.", + "activationTriggerRequiredPoint3": "Indicate who gives the signal to start the activation.", + "activationPeopleTargetedTitle": "People Targeted", + "activationPeopleTargetedDescription": "Specify number of people targeted", + "activationSelectionPopulationTitle": "Selection of target population", + "activationSelectionDescription1": "Provide a short summary of the target population, (the number, location etc.)", + "activationSelectionDescription2": "Describe how the target population will be selected, with a special focus on feasibility in the short period of time between forecast and event", + "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.", + "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.", + "activationSourceOfInformationTitle": "Sources of Information", + "activationSourceOfInformationAddNewLabel": "Add New Source of Information", + "activationSourceOfInformationDescription": "Add the description of the sources one at a time. If the source has a link, add in the second field." + } +} diff --git a/app/src/views/EapFullForm/EapActivationProcess/index.tsx b/app/src/views/EapFullForm/EapActivationProcess/index.tsx new file mode 100644 index 0000000000..cf0139fb79 --- /dev/null +++ b/app/src/views/EapFullForm/EapActivationProcess/index.tsx @@ -0,0 +1,349 @@ +import { useCallback } from 'react'; +import { + Button, + Heading, + InfoPopup, + InputSection, + ListView, + NumberInput, + TextArea, + TextOutput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { randomString } from '@togglecorp/fujs'; +import { + type EntriesAsList, + type Error, + getErrorObject, + getErrorString, + useFormArray, +} from '@togglecorp/toggle-form'; + +import GoMultiFileInput from '#components/domain/GoMultiFileInput'; +import MultiImageWithCaptionInput from '#components/domain/MultiImageWithCaptionInput'; +import NonFieldError from '#components/NonFieldError'; +import TabPage from '#components/TabPage'; + +import EAPSourceInformationInput, { type SourceInformationFormFields } from '../EAPSourceInformationInput'; +import { type PartialEapFullFormType } from '../schema'; + +import i18n from './i18n.json'; + +interface Props { + value: PartialEapFullFormType; + setFieldValue: (...entries: EntriesAsList) => void; + error: Error | undefined; + disabled?: boolean; + fileIdToUrlMap: Record; + setFileIdToUrlMap?: React.Dispatch< + React.SetStateAction> + >; + readOnly?: boolean; +} + +function EapActivationProcess(props: Props) { + const { + value, + setFieldValue, + error: formError, + disabled, + fileIdToUrlMap, + setFileIdToUrlMap, + readOnly, + } = props; + + const strings = useTranslation(i18n); + + const error = getErrorObject(formError); + + const { + setValue: onRiskSourceInformationChange, + removeValue: onRiskSourceInformationRemove, + } = useFormArray< + 'activation_process_source_of_information', + SourceInformationFormFields + >('activation_process_source_of_information', setFieldValue); + + const handleSourceInformationAdd = useCallback(() => { + const newSourceInformationItem: SourceInformationFormFields = { + client_id: randomString(), + }; + + setFieldValue( + (oldValue: SourceInformationFormFields[] | undefined) => [ + ...(oldValue ?? []), + newSourceInformationItem, + ], + 'activation_process_source_of_information' as const, + ); + }, [setFieldValue]); + + return ( + + + + {strings.activationProcessHeading} + + + + + +
  • {strings.activationImplementationRequiredPoint1}
  • +
  • {strings.activationImplementationRequiredPoint2}
  • +
  • {strings.activationImplementationRequiredPoint3}
  • +
  • {strings.activationImplementationRequiredPoint4}
  • + + )} + /> +
    + )} + description={( +
      +
    • {strings.activationProcessDescription1}
    • +
    • {strings.activationProcessDescription2}
    • +
    + )} + withAsteriskOnTitle + > +