diff --git a/.changeset/gold-rabbits-thank.md b/.changeset/gold-rabbits-thank.md new file mode 100644 index 0000000000..035c1d9a7e --- /dev/null +++ b/.changeset/gold-rabbits-thank.md @@ -0,0 +1,5 @@ +--- +"go-web-app": patch +--- + +Add an option to download excel import template for DREF (Response) which user can fill up and import. diff --git a/app/package.json b/app/package.json index 37e49c1471..f90ea82725 100644 --- a/app/package.json +++ b/app/package.json @@ -36,7 +36,7 @@ "lint:fix": "yarn lint:js --fix && yarn lint:css --fix", "test": "vitest", "test:coverage": "vitest run --coverage", - "surge:deploy": "branch=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD); branch=$(echo $branch | tr ./ -); cp build/index.html build/200.html; surge -p build/ -d https://ifrc-go-$branch.surge.sh", + "surge:deploy": "branch=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD); branch=$(echo $branch | tr ./ -); cp ../build/index.html ../build/200.html; surge -p ../build/ -d https://ifrc-go-$branch.surge.sh", "surge:teardown": "branch=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD); branch=$(echo $branch | tr ./ -); surge teardown https://ifrc-go-$branch.surge.sh", "postinstall": "patch-package" }, diff --git a/app/src/hooks/domain/useNationalSociety.ts b/app/src/hooks/domain/useNationalSociety.ts index 39730d2538..d76b9836d1 100644 --- a/app/src/hooks/domain/useNationalSociety.ts +++ b/app/src/hooks/domain/useNationalSociety.ts @@ -62,7 +62,7 @@ function useNationalSociety( const nationalSocieties = useMemo( () => ( - countriesUnsafe?.results?.filter(isValidNationalSociety) + countriesUnsafe?.results?.filter(isValidNationalSociety) ?? [] ), [countriesUnsafe], ); diff --git a/app/src/utils/constants.ts b/app/src/utils/constants.ts index 9f89c3d556..1d906266c8 100644 --- a/app/src/utils/constants.ts +++ b/app/src/utils/constants.ts @@ -96,7 +96,8 @@ export const PROJECT_STATUS_PLANNED = 0 satisfies StatusTypeEnum; type DrefStatus = components<'read'>['schemas']['DrefDrefStatusEnumKey']; export const DREF_STATUS_COMPLETED = 1 satisfies DrefStatus; export const DREF_STATUS_IN_PROGRESS = 0 satisfies DrefStatus; -type TypeOfDrefEnum = components<'read'>['schemas']['DrefDrefDrefTypeEnumKey']; + +export type TypeOfDrefEnum = components<'read'>['schemas']['DrefDrefDrefTypeEnumKey']; export const DREF_TYPE_IMMINENT = 0 satisfies TypeOfDrefEnum; export const DREF_TYPE_ASSESSMENT = 1 satisfies TypeOfDrefEnum; export const DREF_TYPE_RESPONSE = 2 satisfies TypeOfDrefEnum; diff --git a/app/src/utils/importTemplate.ts b/app/src/utils/importTemplate.ts index e3b70dc6ee..ad36ac298b 100644 --- a/app/src/utils/importTemplate.ts +++ b/app/src/utils/importTemplate.ts @@ -1,10 +1,17 @@ -import { isNotDefined } from '@togglecorp/fujs'; +import { hasSomeDefinedValue } from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, + isObject, + listToMap, + mapToMap, +} from '@togglecorp/fujs'; type ValidationType = string | number | boolean; type TypeToLiteral = T extends string ? 'string' | 'date' : T extends number - ? 'number' + ? 'integer' | 'number' : T extends boolean ? 'boolean' : never; @@ -14,6 +21,7 @@ type ExtractValidation = T extends ValidationType interface BaseField { label: string; + description?: string; } interface InputField< @@ -25,7 +33,7 @@ interface InputField< interface SelectField< VALIDATION extends ValidationType, - OPTIONS_MAPPING extends OptionsMapping, + OPTIONS_MAPPING extends TemplateFieldOptionsMapping, > extends BaseField { type: 'select' validation: TypeToLiteral @@ -39,18 +47,19 @@ interface SelectField< interface ListField< VALUE, - OPTIONS_MAPPING extends OptionsMapping + OPTIONS_MAPPING extends TemplateFieldOptionsMapping > extends BaseField { type: 'list' // TODO: Make this more strict optionsKey: keyof OPTIONS_MAPPING; + keyFieldName?: string; children: TemplateSchema< VALUE, OPTIONS_MAPPING >; } -interface ObjectField { +interface ObjectField { type: 'object', fields: { [key in keyof VALUE]+?: TemplateSchema< @@ -60,18 +69,20 @@ interface ObjectField { }, } -interface OptionItem { +export interface TemplateOptionItem { key: T; label: string; } -interface OptionsMapping { - [key: string]: OptionItem[] | OptionItem[] | OptionItem[] +export interface TemplateFieldOptionsMapping { + [key: string]: TemplateOptionItem[] + | TemplateOptionItem[] + | TemplateOptionItem[] } export type TemplateSchema< VALUE, - OPTIONS_MAPPING extends OptionsMapping, + OPTIONS_MAPPING extends TemplateFieldOptionsMapping, > = VALUE extends (infer LIST_ITEM)[] ? ( ListField @@ -98,17 +109,33 @@ type InputTemplateField = { name: string | number | boolean; label: string; outlineLevel: number; + description?: string; } & ({ dataValidation: 'list'; optionsKey: ObjectKey; } | { - dataValidation?: never; + dataValidation?: 'number' | 'integer' | 'date'; optionsKey?: never; }) +export function getCombinedKey( + key: string | number | boolean | symbol, + parentKey: string | number | boolean | symbol | undefined, +) { + if (isNotDefined(parentKey)) { + return String(key); + } + + return `${String(parentKey)}__${String(key)}`; +} + type TemplateField = HeadingTemplateField | InputTemplateField; -export function createImportTemplate( +// TODO: add test +export function createImportTemplate< + TEMPLATE_SCHEMA, + OPTIONS_MAPPING extends TemplateFieldOptionsMapping +>( schema: TemplateSchema, optionsMap: OPTIONS_MAPPING, fieldName: string | undefined = undefined, @@ -124,7 +151,7 @@ export function createImportTemplate( fieldSchema, optionsMap, - key, + getCombinedKey(key, fieldName), outlineLevel + 1, ); @@ -145,6 +172,10 @@ export function createImportTemplate { const subHeadingField = { type: 'heading', - name: option.key, + // name: option.key, + name: getCombinedKey(option.key, fieldName), label: option.label, outlineLevel: outlineLevel + 1, } satisfies HeadingTemplateField; @@ -186,7 +219,8 @@ export function createImportTemplate( schema.children, optionsMap, - undefined, + // undefined, + getCombinedKey(option.key, fieldName), outlineLevel + 1, ); @@ -201,3 +235,210 @@ export function createImportTemplate( + schema: TemplateSchema, + optionsMap: OPTIONS_MAPPING, + formValues: Record, + fieldName: string | undefined = undefined, +): unknown { + const optionsReverseMap = mapToMap( + optionsMap, + (key) => key, + (optionList) => ( + listToMap( + // FIXME: inspect this + optionList as TemplateOptionItem[], + ({ label }) => label, + ({ key }) => key, + ) + ), + ); + + if (schema.type === 'object') { + return mapToMap( + schema.fields, + (key) => key, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (fieldSchema, key) => getValueFromImportTemplate( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fieldSchema as TemplateSchema, + optionsMap, + formValues, + getCombinedKey(key, fieldName), + ), + ); + } + + if (isNotDefined(fieldName)) { + return undefined; + } + + if (schema.type === 'input') { + const value = formValues[fieldName]; + // TODO: add validation from schema.validation + return value; + } + + if (schema.type === 'select') { + const value = formValues[fieldName]; + const valueKey = optionsReverseMap[ + schema.optionsKey as string + ]?.[value]; + // TODO: add validation from schema.validation + return valueKey; + } + + const options = optionsMap[schema.optionsKey]; + + const listValue = options.map((option) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const value = getValueFromImportTemplate( + schema.children, + optionsMap, + formValues, + getCombinedKey(option.key, fieldName), + ); + + if (isObject(value) && hasSomeDefinedValue(value)) { + return { + [schema.keyFieldName ?? 'client_id']: option.key, + ...value, + }; + } + + return undefined; + }).filter(isDefined); + + if (listValue.length === 0) { + return undefined; + } + + return listValue; +} + +type TemplateName = 'dref-application' | 'dref-operational-update' | 'dref-final-report'; + +export interface ImportTemplateDescription { + application: 'ifrc-go', + templateName: TemplateName, + meta: Record; + schema: TemplateSchema, + optionsMap: TemplateFieldOptionsMapping, + fieldNameToTabNameMap: Record, +} + +/* +function isValidTemplate(templateName: unknown): templateName is TemplateName { + const templateNameMap: Record = { + 'dref-application': true, + 'dref-operational-update': true, + 'dref-final-report': true, + }; + + return !!templateNameMap[templateName as TemplateName]; +} + +function isValidOption( + option: unknown, +): option is TemplateOptionItem { + if (!isObject(option)) { + return false; + } + + if (!('key' in option) || !('label' in option)) { + return false; + } + + if (typeof option.label !== 'string') { + return false; + } + + if ( + typeof option.key !== 'string' + && typeof option.key !== 'number' + && typeof option.key !== 'boolean' + ) { + return false; + } + + return true; +} + +function isValidOptionList( + optionList: unknown, +): optionList is (TemplateOptionItem[] + | TemplateOptionItem[] + | TemplateOptionItem[] +) { + if (!Array.isArray(optionList)) { + return false; + } + + const firstElement = optionList[0]; + if (!isValidOption(firstElement)) { + return false; + } + + const keyType = typeof firstElement.key; + + return optionList.every((option) => ( + isValidOption(option) && typeof option.key === keyType + )); +} +*/ + +/* +export function isValidTemplateDescription( + value: unknown, +): value is ImportTemplateDescription { + if (!isObject(value)) { + return false; + } + + type DescriptionKey = keyof ImportTemplateDescription; + + if (!('application' satisfies DescriptionKey in value) || value.application !== 'ifrc-go') { + return false; + } + + if ( + !('templateName' satisfies DescriptionKey in value) + || !isValidTemplate(value.templateName) + ) { + return false; + } + + if (!('meta' satisfies DescriptionKey in value) || !isObject(value.meta)) { + return false; + } + + if (!('schema' satisfies DescriptionKey in value) || !isObject(value.schema)) { + return false; + } + + if ( + !('optionsMap' satisfies DescriptionKey in value) + || !isObject(value.optionsMap) + || !(Object.values(value.optionsMap).every((optionList) => isValidOptionList(optionList))) + ) { + return false; + } + + if ( + !('fieldNameToTabNameMap' satisfies DescriptionKey in value) + || !(isObject(value.fieldNameToTabNameMap)) + || !(Object.values(value.fieldNameToTabNameMap).every( + (field) => typeof field === 'string', + )) + ) { + return false; + } + + return true; +} +*/ diff --git a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/i18n.json b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/i18n.json new file mode 100644 index 0000000000..e9fb1da36f --- /dev/null +++ b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/i18n.json @@ -0,0 +1,9 @@ +{ + "namespace": "DownloadImportTemplateModal", + "strings": { + "heading": "Download DREF import template", + "typeOfDrefInputLabel": "Select type of DREF for template", + "description": "The download template (.xlsx) can be used to fill up the DREF form separately and import back from the \"New DREF Application\" page in the GO.", + "downloadButtonLabel": "Download" + } +} diff --git a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/index.tsx b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/index.tsx new file mode 100644 index 0000000000..f8a311d997 --- /dev/null +++ b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/index.tsx @@ -0,0 +1,335 @@ +import { + useCallback, + useMemo, + useState, +} from 'react'; +import { + Button, + Modal, + RadioInput, +} from '@ifrc-go/ui'; +import { useTranslation } from '@ifrc-go/ui/hooks'; +import { stringValueSelector } from '@ifrc-go/ui/utils'; +import { + isDefined, + isNotDefined, + listToGroupList, + listToMap, + mapToList, +} from '@togglecorp/fujs'; +import xlsx from 'exceljs'; +import FileSaver from 'file-saver'; + +import useGlobalEnums from '#hooks/domain/useGlobalEnums'; +import { + DREF_TYPE_RESPONSE, + TypeOfDrefEnum, +} from '#utils/constants'; +import { + DrefSheetName, + SHEET_ACTIONS_NEEDS, + SHEET_EVENT_DETAIL, + SHEET_OPERATION, + SHEET_OPERATION_OVERVIEW, + SHEET_TIMEFRAMES_AND_CONTACTS, +} from '#utils/domain/dref'; +import { createImportTemplate } from '#utils/importTemplate'; +import { + actionsTabFields, + eventDetailTabFields, + operationTabFields, + overviewTabFields, + timeframeAndContactsTabFields, +} from '#views/DrefApplicationForm/common'; + +import { + addHeadingRow, + addInputRow, + buildCoverWorksheet, + headerRowStyle, +} from '../utils'; +import useImportTemplateSchema from './useImportTemplateSchema'; + +import i18n from './i18n.json'; + +function typeOfDrefKeySelector(option: { key: TypeOfDrefEnum }) { + return option.key; +} + +interface Props { + onComplete: () => void; +} + +function DownloadImportTemplateModal(props: Props) { + const { onComplete } = props; + + const { dref_dref_dref_type } = useGlobalEnums(); + const strings = useTranslation(i18n); + + const [generationPending, setGenerationPending] = useState(false); + const [typeOfDref, setTypeOfDref] = useState(DREF_TYPE_RESPONSE); + + const { drefFormSchema, optionsMap } = useImportTemplateSchema(); + const templateActions = createImportTemplate(drefFormSchema, optionsMap); + + const drefTypeLabelMap = useMemo( + () => ( + listToMap( + dref_dref_dref_type, + (option) => option.key, + (option) => option.value, + ) + ), + [dref_dref_dref_type], + ); + + const handleDownloadClick = useCallback(() => { + if (isNotDefined(templateActions)) { + return; + } + + async function generateTemplate() { + const workbook = new xlsx.Workbook(); + const now = new Date(); + workbook.created = now; + + const fieldNameToTabNameMap: Record = { + ...listToMap( + overviewTabFields, + (key) => key, + () => SHEET_OPERATION_OVERVIEW, + ), + ...listToMap( + eventDetailTabFields, + (key) => key, + () => SHEET_EVENT_DETAIL, + ), + ...listToMap( + actionsTabFields, + (key) => key, + () => SHEET_ACTIONS_NEEDS, + ), + ...listToMap( + operationTabFields, + (key) => key, + () => SHEET_OPERATION, + ), + ...listToMap( + timeframeAndContactsTabFields, + (key) => key, + () => SHEET_TIMEFRAMES_AND_CONTACTS, + ), + }; + + /* + const description: ImportTemplateDescription = { + application: 'ifrc-go', + templateName: 'dref-application', + meta: { + typeOfDref: 'response', + }, + fieldNameToTabNameMap, + }; + + workbook.description = JSON.stringify(description); + */ + + const typeOfDrefLabel = drefTypeLabelMap?.[typeOfDref ?? DREF_TYPE_RESPONSE] ?? ''; + + const coverWorksheet = workbook.addWorksheet('DREF Import'); + await buildCoverWorksheet(coverWorksheet, workbook, typeOfDrefLabel); + + const overviewWorksheet = workbook.addWorksheet(SHEET_OPERATION_OVERVIEW); + const eventDetailsWorksheet = workbook.addWorksheet(SHEET_EVENT_DETAIL); + const actionsNeedsWorksheet = workbook.addWorksheet(SHEET_ACTIONS_NEEDS); + const operationWorksheet = workbook.addWorksheet(SHEET_OPERATION); + const timeframeAndContactsWorksheet = workbook.addWorksheet( + SHEET_TIMEFRAMES_AND_CONTACTS, + ); + + const sheetMap: Record = { + [SHEET_OPERATION_OVERVIEW]: overviewWorksheet, + [SHEET_EVENT_DETAIL]: eventDetailsWorksheet, + [SHEET_ACTIONS_NEEDS]: actionsNeedsWorksheet, + [SHEET_OPERATION]: operationWorksheet, + [SHEET_TIMEFRAMES_AND_CONTACTS]: timeframeAndContactsWorksheet, + }; + + const optionsWorksheet = workbook.addWorksheet('options'); + optionsWorksheet.state = 'veryHidden'; + const optionKeys = Object.keys(optionsMap) as (keyof (typeof optionsMap))[]; + + optionsWorksheet.columns = optionKeys.map((key) => ( + { header: key, key } + )); + + optionKeys.forEach((key) => { + const options = optionsMap[key]; + + if (isDefined(options)) { + const column = optionsWorksheet.getColumnKey(key); + + options.forEach((option, i) => { + const cell = optionsWorksheet.getCell(i + 2, column.number); + cell.name = String(option.key); + cell.value = option.label; + }); + } + }); + + const tabGroupedTemplateActions = mapToList( + listToGroupList( + templateActions, + (templateAction) => { + const fieldName = String(templateAction.name).split('__')[0]; + const tabName = fieldNameToTabNameMap[fieldName]; + return tabName; + }, + ), + (actions, tabName) => { + const worksheet = workbook.getWorksheet(tabName); + if (isNotDefined(worksheet)) { + return undefined; + } + + return { + worksheet, + tabName, + actions, + }; + }, + ).filter(isDefined); + + const ROW_OFFSET = 2; + tabGroupedTemplateActions.forEach(({ actions, worksheet }) => { + actions.forEach((templateAction, i) => { + if (templateAction.type === 'heading') { + addHeadingRow( + worksheet, + i + ROW_OFFSET, + templateAction.outlineLevel, + String(templateAction.name), + templateAction.label, + ); + } else if (templateAction.type === 'input') { + if (templateAction.dataValidation === 'list') { + addInputRow( + worksheet, + i + ROW_OFFSET, + templateAction.outlineLevel, + String(templateAction.name), + templateAction.label, + templateAction.dataValidation, + String(templateAction.optionsKey), + optionsWorksheet, + templateAction.description, + ); + } else { + addInputRow( + worksheet, + i + ROW_OFFSET, + templateAction.outlineLevel, + String(templateAction.name), + templateAction.label, + templateAction.dataValidation, + templateAction.description, + ); + } + } + }); + }); + + Object.values(sheetMap).forEach( + (sheet) => { + const worksheet = sheet; + worksheet.properties.defaultRowHeight = 20; + worksheet.properties.showGridLines = false; + + worksheet.columns = [ + { + key: 'field', + header: 'Field', + protection: { locked: true }, + width: 50, + }, + { + key: 'value', + header: 'Value', + width: 50, + }, + ]; + + worksheet.getRow(1).eachCell( + (cell) => { + // eslint-disable-next-line no-param-reassign + cell.style = headerRowStyle; + }, + ); + }, + ); + + const templateFileName = `DREF Application ${typeOfDrefLabel} import template ${now.toLocaleString()}.xlsx`; + + await workbook.xlsx.writeBuffer().then( + (sheet) => { + FileSaver.saveAs( + new Blob([sheet], { type: 'application/vnd.ms-excel;charset=utf-8' }), + templateFileName, + ); + }, + ); + + setGenerationPending(false); + onComplete(); + } + + setGenerationPending((alreadyGenerating) => { + if (!alreadyGenerating) { + generateTemplate(); + } + + return true; + }); + }, [ + templateActions, + optionsMap, + onComplete, + drefTypeLabelMap, + typeOfDref, + ]); + + return ( + + {strings.downloadButtonLabel} + + )} + contentViewType="vertical" + spacing="comfortable" + onClose={onComplete} + > + +
+ {strings.description} +
+
+ ); +} + +export default DownloadImportTemplateModal; diff --git a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/useImportTemplateSchema.ts b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/useImportTemplateSchema.ts new file mode 100644 index 0000000000..c82c61f8a5 --- /dev/null +++ b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/useImportTemplateSchema.ts @@ -0,0 +1,740 @@ +import { useMemo } from 'react'; + +import useCountry from '#hooks/domain/useCountry'; +import useDisasterTypes from '#hooks/domain/useDisasterType'; +import useGlobalEnums from '#hooks/domain/useGlobalEnums'; +import useNationalSociety from '#hooks/domain/useNationalSociety'; +import { type TemplateSchema } from '#utils/importTemplate'; +import { type DrefRequestBody } from '#views/DrefApplicationForm/schema'; + +function useImportTemplateSchema() { + const nationalSocieties = useNationalSociety(); + const countries = useCountry(); + const disasterTypes = useDisasterTypes(); + + const { + dref_planned_intervention_title, + dref_national_society_action_title, + dref_identified_need_title, + dref_dref_onset_type, + dref_dref_disaster_category, + } = useGlobalEnums(); + + const optionsMap = useMemo(() => ({ + __boolean: [ + { + key: true, + label: 'Yes', + }, + { + key: false, + label: 'No', + }, + ], + national_society: nationalSocieties.map( + ({ id, society_name }) => ({ key: id, label: society_name }), + ), + country: countries?.map( + ({ id, name }) => ({ key: id, label: name }), + ), + disaster_type: disasterTypes?.map( + ({ id, name }) => ({ key: id, label: name }), + ) ?? [], + type_of_onset: dref_dref_onset_type?.map( + ({ key, value }) => ({ key, label: value }), + ) ?? [], + disaster_category: dref_dref_disaster_category?.map( + ({ key, value }) => ({ key, label: value }), + ) ?? [], + planned_interventions: dref_planned_intervention_title?.map( + ({ key, value }) => ({ key, label: value }), + ) ?? [], + source_information: [ + { key: 'source__0', label: 'Source #1' }, + { key: 'source__1', label: 'Source #2' }, + { key: 'source__2', label: 'Source #3' }, + ], + planned_interventions__indicators: [ + { key: 'indicator__0', label: 'Indicator #1' }, + { key: 'indicator__1', label: 'Indicator #2' }, + { key: 'indicator__2', label: 'Indicator #3' }, + ], + risk_security: [ + { key: 'risk__0', label: 'Risk #1' }, + { key: 'risk__1', label: 'Risk #2' }, + { key: 'risk__2', label: 'Risk #3' }, + ], + national_society_actions: dref_national_society_action_title?.map( + ({ key, value }) => ({ key, label: value }), + ) ?? [], + identified_needs: dref_identified_need_title?.map( + ({ key, value }) => ({ key, label: value }), + ) ?? [], + }), [ + countries, + disasterTypes, + nationalSocieties, + dref_planned_intervention_title, + dref_national_society_action_title, + dref_identified_need_title, + dref_dref_onset_type, + dref_dref_disaster_category, + ]); + + const drefFormSchema: TemplateSchema = useMemo(() => ({ + type: 'object', + fields: { + national_society: { + type: 'select', + label: 'National society', + validation: 'number', + optionsKey: 'national_society', + }, + + // We're skipping type of DREF since we'll have separate + // template for each type of dref + // type_of_dref: xxx + + disaster_type: { + type: 'select', + label: 'Type of disaster', + validation: 'number', + optionsKey: 'disaster_type', + }, + + type_of_onset: { + type: 'select', + label: 'Type of Onset', + validation: 'number', + optionsKey: 'type_of_onset', + }, + + is_man_made_event: { + type: 'select', + label: 'Is this a man made event?', + validation: 'boolean', + optionsKey: '__boolean', + }, + + disaster_category: { + type: 'select', + label: 'Disaster Category', + validation: 'number', + optionsKey: 'disaster_category', + }, + + country: { + type: 'select', + label: 'Country', + validation: 'number', + optionsKey: 'country', + }, + + title: { + type: 'input', + label: 'DREF Title', + validation: 'string', + }, + + emergency_appeal_planned: { + type: 'select', + label: 'Emergency appeal planned', + optionsKey: '__boolean', + validation: 'boolean', + }, + + // Event eventDetail + // Previous Operations + did_it_affect_same_area: { + type: 'select', + label: 'Has a similar event affected the same area(s) in the last 3 years?', + optionsKey: '__boolean', + validation: 'boolean', + }, + + did_it_affect_same_population: { + type: 'select', + label: 'Did it affect the same population groups?', + optionsKey: '__boolean', + validation: 'boolean', + description: 'Select only if you\'ve selected Yes for the above', + }, + + did_ns_respond: { + type: 'select', + label: 'Did the National Society respond?', + optionsKey: '__boolean', + validation: 'boolean', + description: 'Select only if you\'ve selected Yes for the above', + }, + + did_ns_request_fund: { + type: 'select', + label: 'Did the National Society request funding from DREF for that event(s)?', + optionsKey: '__boolean', + validation: 'boolean', + description: 'Select only if you\'ve selected Yes for the above', + }, + + ns_request_text: { + type: 'input', + label: 'If yes, please specify which operations', + validation: 'string', + description: 'Select only if you\'ve selected Yes for the above', + }, + + dref_recurrent_text: { + type: 'input', + label: 'If you have answered yes to all questions above, justify why the use of DREF for a recurrent event, or how this event should not be considered recurrent', + validation: 'string', + }, + + lessons_learned: { + type: 'input', + label: 'Lessons Learned', + validation: 'string', + description: 'Specify how the lessons learnt from these previous operations are being used to mitigate similar challenges in the current operation', + }, + + event_date: { + type: 'input', + label: 'Date of the Event', + validation: 'date', + }, + + num_affected: { + type: 'input', + validation: 'number', + label: 'Total affected population', + description: 'People Affected include all those whose lives and livelihoods have been impacted as a direct result of the shock or stress.', + }, + + people_in_need: { + type: 'input', + validation: 'number', + label: 'People in need (Optional)', + description: 'People in Need (PIN) are those members whose physical security, basic rights, dignity, living conditions or livelihoods are threatened or have been disrupted, and whose current level of access to basic services, goods and social protection is inadequate to re-establish normal living conditions without additional assistance.', + }, + + event_description: { + type: 'input', + validation: 'string', + label: 'What happened, where and when?', + }, + + event_scope: { + type: 'input', + validation: 'string', + label: 'Scope and scale of the event', + }, + + source_information: { + type: 'list', + label: 'Source Information', + optionsKey: 'source_information', + children: { + type: 'object', + fields: { + source_name: { + type: 'input', + validation: 'string', + label: 'Name', + }, + source_link: { + type: 'input', + validation: 'string', + label: 'Link', + }, + }, + }, + }, + + did_national_society: { + type: 'select', + validation: 'boolean', + optionsKey: '__boolean', + label: 'Has the National Society started any actions?', + }, + + national_society_actions: { + type: 'list', + label: 'National Society Actions', + keyFieldName: 'title', + optionsKey: 'national_society_actions', + children: { + type: 'object', + fields: { + description: { + type: 'input', + validation: 'string', + label: 'Description', + }, + }, + }, + }, + + ifrc: { + type: 'input', + validation: 'string', + label: 'IFRC', + }, + + partner_national_society: { + type: 'input', + validation: 'string', + label: 'Participating National Societies', + }, + + icrc: { + type: 'input', + validation: 'string', + label: 'ICRC', + }, + + government_requested_assistance: { + type: 'select', + validation: 'boolean', + optionsKey: '__boolean', + label: 'Government has requested international assistance', + }, + + national_authorities: { + type: 'input', + validation: 'string', + label: 'National authorities', + }, + + un_or_other_actor: { + type: 'input', + validation: 'string', + label: 'UN or other actors', + }, + + is_there_major_coordination_mechanism: { + type: 'select', + validation: 'boolean', + optionsKey: '__boolean', + label: 'Are there major coordination mechanisms in place?', + }, + + needs_identified: { + type: 'list', + label: 'Identified Needs', + optionsKey: 'identified_needs', + children: { + type: 'object', + fields: { + description: { + type: 'input', + validation: 'string', + label: 'Description', + }, + }, + }, + }, + + identified_gaps: { + type: 'input', + validation: 'string', + label: 'Any identified gaps/limitations in the assessment', + }, + + // Operation + operation_objective: { + type: 'input', + validation: 'string', + label: 'Overall objective of the operation', + }, + + response_strategy: { + type: 'input', + validation: 'string', + label: 'Operation strategy rationale', + }, + + people_assisted: { + type: 'input', + validation: 'string', + label: 'Who will be targeted through this operation?', + }, + + selection_criteria: { + type: 'input', + validation: 'string', + label: 'Explain the selection criteria for the targeted population', + }, + + women: { + type: 'input', + validation: 'number', + label: 'Women', + }, + + men: { + type: 'input', + validation: 'number', + label: 'Men', + }, + + girls: { + type: 'input', + validation: 'number', + label: 'Girls', + }, + + boys: { + type: 'input', + validation: 'number', + label: 'Boys (under 18)', + }, + + total_targeted_population: { + type: 'input', + validation: 'number', + label: 'Total Population', + }, + + disability_people_per: { + type: 'input', + validation: 'number', + label: 'Estimated Percentage People with Disability', + }, + + people_per_urban: { + type: 'input', + validation: 'number', + label: 'Estimated Percentage (Urban to Rural)', + }, + + displaced_people: { + type: 'input', + validation: 'number', + label: 'Estimated number of People on the move (if any)', + }, + + risk_security: { + type: 'list', + label: 'Please indicate about potential operational risk for this operations and mitigation actions', + optionsKey: 'risk_security', + children: { + type: 'object', + fields: { + risk: { + type: 'input', + validation: 'string', + label: 'Risk', + }, + mitigation: { + type: 'input', + validation: 'string', + label: 'Mitigation action', + }, + }, + }, + }, + + risk_security_concern: { + type: 'input', + validation: 'string', + label: 'Please indicate any security and safety concerns for this operation', + }, + + has_child_safeguarding_risk_analysis_assessment: { + type: 'select', + optionsKey: '__boolean', + validation: 'boolean', + label: 'Has the child safeguarding risk analysis assessment been completed?', + }, + + amount_requested: { + type: 'input', + validation: 'number', + label: 'Requested Amount in CHF', + }, + + planned_interventions: { + type: 'list', + label: 'Planned interventions', + optionsKey: 'planned_interventions', + keyFieldName: 'title', + children: { + type: 'object', + fields: { + budget: { + type: 'input', + validation: 'number', + label: 'Budget', + }, + person_targeted: { + type: 'input', + validation: 'number', + label: 'Person targeted', + }, + description: { + type: 'input', + validation: 'string', + label: 'List of activities', + }, + indicators: { + type: 'list', + label: 'Indicators', + optionsKey: 'planned_interventions__indicators', + children: { + type: 'object', + fields: { + title: { + type: 'input', + validation: 'string', + label: 'Title', + }, + target: { + type: 'input', + validation: 'number', + label: 'Target', + }, + }, + }, + }, + }, + }, + }, + + human_resource: { + type: 'input', + validation: 'string', + label: 'How many staff and volunteers will be involved in this operation. Briefly describe their role.', + }, + + is_surge_personnel_deployed: { + type: 'select', + validation: 'boolean', + optionsKey: '__boolean', + label: 'Will be surge personnel be deployed?', + }, + + surge_personnel_deployed: { + type: 'input', + validation: 'string', + label: 'Description', + }, + + logistic_capacity_of_ns: { + type: 'input', + validation: 'string', + label: 'If there is procurement, will be done by National Society or IFRC?', + }, + + pmer: { + type: 'input', + validation: 'string', + label: 'How will this operation be monitored?', + }, + + communication: { + type: 'input', + validation: 'string', + label: 'Please briefly explain the National Societies communication strategy for this operation.', + }, + + // Submission + ns_request_date: { + type: 'input', + validation: 'date', + label: 'Date of National Society Application', + }, + + submission_to_geneva: { + type: 'input', + validation: 'date', + label: 'Date of Submission to GVA', + }, + + date_of_approval: { + type: 'input', + validation: 'date', + label: 'Date of Approval', + }, + + operation_timeframe: { + type: 'input', + validation: 'number', + label: 'Operation timeframe', + }, + + end_date: { + type: 'input', + validation: 'date', + label: 'End date of Operation', + }, + + publishing_date: { + type: 'input', + validation: 'date', + label: 'Date of Publishing', + }, + + appeal_code: { + type: 'input', + validation: 'string', + label: 'Appeal Code', + }, + + glide_code: { + type: 'input', + validation: 'string', + label: 'GLIDE number', + }, + + ifrc_appeal_manager_name: { + type: 'input', + validation: 'string', + label: 'IFRC Appeal Manager Name', + }, + + ifrc_appeal_manager_title: { + type: 'input', + validation: 'string', + label: 'IFRC Appeal Manager Title', + }, + + ifrc_appeal_manager_email: { + type: 'input', + validation: 'string', + label: 'IFRC Appeal Manager Email', + }, + + ifrc_appeal_manager_phone_number: { + type: 'input', + validation: 'string', + label: 'IFRC Appeal Manager Phone Number', + }, + + ifrc_project_manager_name: { + type: 'input', + validation: 'string', + label: 'IFRC Project Manager Name', + }, + + ifrc_project_manager_title: { + type: 'input', + validation: 'string', + label: 'IFRC Project Manager Title', + }, + + ifrc_project_manager_email: { + type: 'input', + validation: 'string', + label: 'IFRC Project Manager Email', + }, + + ifrc_project_manager_phone_number: { + type: 'input', + validation: 'string', + label: 'IFRC Project Manager Phone Number', + }, + + national_society_contact_name: { + type: 'input', + validation: 'string', + label: 'National Society Contact Name', + }, + + national_society_contact_title: { + type: 'input', + validation: 'string', + label: 'National Society Contact Title', + }, + + national_society_contact_email: { + type: 'input', + validation: 'string', + label: 'National Society Contact Email', + }, + + national_society_contact_phone_number: { + type: 'input', + validation: 'string', + label: 'National Society Contact Phone Number', + }, + + ifrc_emergency_name: { + type: 'input', + validation: 'string', + label: 'IFRC focal point for the emergency Name', + }, + + ifrc_emergency_title: { + type: 'input', + validation: 'string', + label: 'IFRC focal point for the emergency Title', + }, + + ifrc_emergency_email: { + type: 'input', + validation: 'string', + label: 'IFRC focal point for the emergency Email', + }, + + ifrc_emergency_phone_number: { + type: 'input', + validation: 'string', + label: 'IFRC focal point for the emergency Phone number', + }, + + regional_focal_point_name: { + type: 'input', + validation: 'string', + label: 'DREF Regional Focal Point Name', + }, + + regional_focal_point_title: { + type: 'input', + validation: 'string', + label: 'DREF Regional Focal Point Title', + }, + + regional_focal_point_email: { + type: 'input', + validation: 'string', + label: 'DREF Regional Focal Point Email', + }, + + regional_focal_point_phone_number: { + type: 'input', + validation: 'string', + label: 'DREF Regional Focal Point Phone Number', + }, + + media_contact_name: { + type: 'input', + validation: 'string', + label: 'Media Contact Name', + }, + + media_contact_title: { + type: 'input', + validation: 'string', + label: 'Media Contact Title', + }, + + media_contact_email: { + type: 'input', + validation: 'string', + label: 'Media Contact Email', + }, + + media_contact_phone_number: { + type: 'input', + validation: 'string', + label: 'Media Contact Phone Number', + }, + }, + }), []); + + return { + drefFormSchema, + optionsMap, + }; +} + +export default useImportTemplateSchema; diff --git a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/index.tsx b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/index.tsx index 61815b9a23..bdfd78a9af 100644 --- a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/index.tsx +++ b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/index.tsx @@ -1,1078 +1,32 @@ -import { - useCallback, - useState, -} from 'react'; import { Button } from '@ifrc-go/ui'; -import { - isDefined, - isNotDefined, -} from '@togglecorp/fujs'; -import xlsx from 'exceljs'; -import FileSaver from 'file-saver'; +import { useBooleanState } from '@ifrc-go/ui/hooks'; -import ifrcLogoFile from '#assets/icons/ifrc-square.png'; -import useCountry from '#hooks/domain/useCountry'; -import useDisasterTypes from '#hooks/domain/useDisasterType'; -import useGlobalEnums from '#hooks/domain/useGlobalEnums'; -import useNationalSociety from '#hooks/domain/useNationalSociety'; -import { - COLOR_DARK_GREY, - COLOR_PRIMARY_BLUE, - COLOR_PRIMARY_RED, -} from '#utils/constants'; -import { - DrefSheetName, - SHEET_ACTIONS_NEEDS, - SHEET_EVENT_DETAIL, - SHEET_OPERATION, - SHEET_OPERATION_OVERVIEW, - SHEET_TIMEFRAMES_AND_CONTACTS, -} from '#utils/domain/dref'; -import { - createImportTemplate, - TemplateSchema, -} from '#utils/importTemplate'; -import { DrefRequestBody } from '#views/DrefApplicationForm/schema'; - -function hexToArgb(hexStr: string, alphaStr = 'ff') { - const hexWithoutHash = hexStr.substring(1, hexStr.length); - - return `${alphaStr}${hexWithoutHash}`; -} - -const headerRowStyle: Partial = { - font: { - name: 'Montserrat', - bold: true, - }, - fill: { - type: 'pattern', - pattern: 'lightVertical', - fgColor: { argb: hexToArgb(COLOR_PRIMARY_RED, '10') }, - }, - alignment: { - vertical: 'middle', - horizontal: 'center', - }, -}; - -const headingStyle: Partial = { - font: { - name: 'Montserrat', - color: { argb: hexToArgb(COLOR_PRIMARY_BLUE) }, - }, - alignment: { - horizontal: 'left', - vertical: 'middle', - }, -}; - -const defaultCellStyle: Partial = { - font: { - name: 'Poppins', - }, - alignment: { - horizontal: 'left', - vertical: 'top', - wrapText: true, - }, -}; - -const inputBorderStyle: Partial = { - style: 'dashed', - color: { argb: hexToArgb(COLOR_PRIMARY_BLUE) }, -}; - -const inputCellStyle: Partial = { - fill: { - type: 'pattern', - pattern: 'lightVertical', - fgColor: { argb: hexToArgb(COLOR_DARK_GREY, '10') }, - }, - border: { - top: inputBorderStyle, - left: inputBorderStyle, - right: inputBorderStyle, - bottom: inputBorderStyle, - }, - alignment: { - vertical: 'top', - wrapText: true, - }, -}; - -function addRow( - sheet: xlsx.Worksheet, - rowNum: number, - outlineLevel: number, - name: string, - label: string, - style?: Partial, -) { - const col = 1; - - const row = sheet.getRow(rowNum); - - row.getCell(col).name = name; - row.getCell(col + 1).name = name; - - row.getCell(col).value = label; - row.outlineLevel = outlineLevel; - - if (style) { - row.getCell(col).style = style; - } else { - row.getCell(col).style = defaultCellStyle; - } - - const prevStyle = row.getCell(col).style; - row.getCell(col).style = { - ...prevStyle, - alignment: { - ...prevStyle?.alignment, - indent: outlineLevel * 2, - }, - }; - - return row; -} - -function addInputRow( - sheet: xlsx.Worksheet, - rowNum: number, - outlineLevel: number, - name: string, - label: string, - optionKey?: string, - optionsWorksheet?: xlsx.Worksheet, - style?: Partial, -) { - const col = 1; - - const row = addRow( - sheet, - rowNum, - outlineLevel, - name, - label, - style, - ); - - const inputCell = row.getCell(col + 1); - inputCell.style = inputCellStyle; - - if (isDefined(optionKey) && isDefined(optionsWorksheet)) { - const optionsColumn = optionsWorksheet.getColumnKey(optionKey); - - if (optionsColumn) { - const colLetter = optionsColumn.letter; - const numOptions = optionsColumn.values.length; - - const formulae = `=${optionsWorksheet.name}!$${colLetter}$2:$${colLetter}$${numOptions}`; - - inputCell.dataValidation = { - type: 'list', - formulae: [formulae], - }; - } - } - - return row; -} +import DownloadImportTemplateModal from './DownloadImportTemplateModal'; function DownloadImportTemplateButton() { - const [generationPending, setGenerationPending] = useState(false); - - const nationalSocieties = useNationalSociety(); - const countries = useCountry(); - const disasterTypes = useDisasterTypes(); - - const { - dref_planned_intervention_title, - dref_national_society_action_title, - dref_identified_need_title, - dref_dref_onset_type, - dref_dref_disaster_category, - } = useGlobalEnums(); - - const handleClick = useCallback( - () => { - const optionsMap = { - __boolean: [ - { - key: true, - label: 'Yes', - }, - { - key: false, - label: 'No', - }, - ], - national_society: nationalSocieties.map( - ({ id, society_name }) => ({ key: id, label: society_name }), - ), - country: countries.map( - ({ id, name }) => ({ key: id, label: name }), - ), - disaster_type: disasterTypes?.map( - ({ id, name }) => ({ key: id, label: name }), - ) ?? [], - type_of_onset: dref_dref_onset_type?.map( - ({ key, value }) => ({ key, label: value }), - ) ?? [], - disaster_category: dref_dref_disaster_category?.map( - ({ key, value }) => ({ key, label: value }), - ) ?? [], - planned_interventions: dref_planned_intervention_title?.map( - ({ key, value }) => ({ key, label: value }), - ) ?? [], - source_information: [ - { key: 'source__0', label: 'Source #1' }, - { key: 'source__1', label: 'Source #2' }, - { key: 'source__2', label: 'Source #3' }, - ], - planned_interventions__indicators: [ - { key: 'indicator__0', label: 'Indicator #1' }, - { key: 'indicator__1', label: 'Indicator #2' }, - { key: 'indicator__2', label: 'Indicator #3' }, - ], - risk_security: [ - { key: 'risk__0', label: 'Risk #1' }, - { key: 'risk__1', label: 'Risk #2' }, - { key: 'risk__2', label: 'Risk #3' }, - ], - ns_actions: dref_national_society_action_title?.map( - ({ key, value }) => ({ key, label: value }), - ) ?? [], - identified_needs: dref_identified_need_title?.map( - ({ key, value }) => ({ key, label: value }), - ) ?? [], - }; - - const drefFormSchema: TemplateSchema = { - type: 'object', - fields: { - national_society: { - type: 'select', - label: 'National society', - validation: 'number', - optionsKey: 'national_society', - }, - - // We're skipping type of DREF since we'll have separate - // template for each type of dref - // type_of_dref: xxx - - disaster_type: { - type: 'select', - label: 'Type of disaster', - validation: 'number', - optionsKey: 'disaster_type', - }, - - type_of_onset: { - type: 'select', - label: 'Type of Onset', - validation: 'number', - optionsKey: 'type_of_onset', - }, - - is_man_made_event: { - type: 'select', - label: 'Is this a man made event?', - validation: 'boolean', - optionsKey: '__boolean', - }, - - disaster_category: { - type: 'select', - label: 'Disaster Category', - validation: 'number', - optionsKey: 'disaster_category', - }, - - country: { - type: 'select', - label: 'Country', - validation: 'number', - optionsKey: 'country', - }, - - title: { - type: 'input', - label: 'DREF Title', - validation: 'string', - }, - - emergency_appeal_planned: { - type: 'select', - label: 'Emergency appeal planned', - optionsKey: '__boolean', - validation: 'boolean', - }, - - // Event eventDetail - // Previous Operations - did_it_affect_same_area: { - type: 'select', - label: 'Has a similar event affected the same area(s) in the last 3 years?', - optionsKey: '__boolean', - validation: 'boolean', - }, - - did_it_affect_same_population: { - type: 'select', - label: 'Did it affect the same population groups?', - optionsKey: '__boolean', - validation: 'boolean', - }, - - did_ns_respond: { - type: 'select', - label: 'Did the National Society respond?', - optionsKey: '__boolean', - validation: 'boolean', - }, - - did_ns_request_fund: { - type: 'select', - label: 'Did the National Society request funding from DREF for that event(s)?', - optionsKey: '__boolean', - validation: 'boolean', - }, - - ns_request_text: { - type: 'input', - label: 'If yes, please specify which operations', - validation: 'string', - }, - - dref_recurrent_text: { - type: 'input', - label: 'If you have answered yes to all questions above, justify why the use of DREF for a recurrent event, or how this event should not be considered recurrent', - validation: 'string', - }, - - lessons_learned: { - type: 'input', - label: 'Lessons Learned', - validation: 'string', - }, - - event_date: { - type: 'input', - label: 'Date of the Event', - validation: 'date', - }, - - num_affected: { - type: 'input', - validation: 'number', - label: 'Total affected population', - }, - - people_in_need: { - type: 'input', - validation: 'number', - label: 'People in need(Optional)', - }, - - event_description: { - type: 'input', - validation: 'string', - label: 'What happened, where and when?', - }, - - event_scope: { - type: 'input', - validation: 'string', - label: 'Scope and scale of the event', - }, - - source_information: { - type: 'list', - label: 'Source Information', - optionsKey: 'source_information', - children: { - type: 'object', - fields: { - source_name: { - type: 'input', - validation: 'string', - label: 'Name', - }, - source_link: { - type: 'input', - validation: 'string', - label: 'Link', - }, - }, - }, - }, - - did_national_society: { - type: 'select', - validation: 'boolean', - optionsKey: '__boolean', - label: 'Has the National Society started any actions?', - }, - - ns_actions: { - type: 'select', - label: 'Select the actions that apply.', - validation: 'number', - optionsKey: 'ns_actions', - }, - - ifrc: { - type: 'input', - validation: 'string', - label: 'IFRC', - }, - - partner_national_society: { - type: 'input', - validation: 'string', - label: 'Participating National Societies', - }, - - icrc: { - type: 'input', - validation: 'string', - label: 'ICRC', - }, - - government_requested_assistance: { - type: 'select', - validation: 'boolean', - optionsKey: '__boolean', - label: 'Government has requested international assistance', - }, - - national_authorities: { - type: 'input', - validation: 'string', - label: 'National authorities', - }, - - un_or_other_actor: { - type: 'input', - validation: 'string', - label: 'UN or other actors', - }, - - is_there_major_coordination_mechanism: { - type: 'select', - validation: 'boolean', - optionsKey: '__boolean', - label: 'Are there major coordination mechanisms in place?', - }, - - dref_identified_need_title: { - type: 'select', - label: 'Select the needs that apply.', - validation: 'number', - optionsKey: 'dref_identified_need_title', - }, - - identified_gaps: { - type: 'input', - validation: 'string', - label: 'Any identified gaps/limitations in the assessment', - }, - - // Operation - operation_objective: { - type: 'input', - validation: 'string', - label: 'Overall objective of the operation', - }, - - response_strategy: { - type: 'input', - validation: 'string', - label: 'Operation strategy rationale', - }, - - people_assisted: { - type: 'input', - validation: 'string', - label: 'Who will be targeted through this operation?', - }, - - selection_criteria: { - type: 'input', - validation: 'string', - label: 'Explain the selection criteria for the targeted population', - }, - - women: { - type: 'input', - validation: 'number', - label: 'Women', - }, - - men: { - type: 'input', - validation: 'number', - label: 'Men', - }, - - girls: { - type: 'input', - validation: 'number', - label: 'Girls', - }, - - boys: { - type: 'input', - validation: 'number', - label: 'Boys (under 18)', - }, - - total_targeted_population: { - type: 'input', - validation: 'number', - label: 'Total Population', - }, - - disability_people_per: { - type: 'input', - validation: 'number', - label: 'Estimated Percentage People with Disability', - }, - - people_per_urban: { - type: 'input', - validation: 'number', - label: 'Estimated Percentage (Urban to Rural)', - }, - - displaced_people: { - type: 'input', - validation: 'number', - label: 'Estimated number of People on the move (if any)', - }, - - risk_security: { - type: 'list', - label: 'Please indicate about potential operational risk for this operations and mitigation actions', - optionsKey: 'risk_security', - children: { - type: 'object', - fields: { - risk: { - type: 'input', - validation: 'string', - label: 'Risk', - }, - mitigation: { - type: 'input', - validation: 'string', - label: 'Mitigation action', - }, - }, - }, - }, - - risk_security_concern: { - type: 'input', - validation: 'string', - label: 'Please indicate any security and safety concerns for this operation', - }, - - has_child_safeguarding_risk_analysis_assessment: { - type: 'select', - optionsKey: '__boolean', - validation: 'boolean', - label: 'Has the child safeguarding risk analysis assessment been completed?', - }, - - amount_requested: { - type: 'input', - validation: 'number', - label: 'Requested Amount in CHF', - }, - - dref_planned_intervention_title: { - type: 'select', - label: 'Select the interventions that apply.', - validation: 'number', - optionsKey: 'dref_planned_intervention_title', - }, - - planned_interventions: { - type: 'list', - label: 'Planned interventions', - optionsKey: 'planned_interventions', - children: { - type: 'object', - fields: { - budget: { - type: 'input', - validation: 'number', - label: 'Budget', - }, - person_targeted: { - type: 'input', - validation: 'number', - label: 'Person targeted', - }, - description: { - type: 'input', - validation: 'string', - label: 'Description', - }, - indicators: { - type: 'list', - label: 'Indicators', - optionsKey: 'planned_interventions__indicators', - children: { - type: 'object', - fields: { - title: { - type: 'input', - validation: 'string', - label: 'Title', - }, - target: { - type: 'input', - validation: 'number', - label: 'Target', - }, - }, - }, - }, - }, - }, - }, - - human_resource: { - type: 'input', - validation: 'string', - label: 'How many staff and volunteers will be involved in this operation. Briefly describe their role.', - }, - - is_surge_personnel_deployed: { - type: 'select', - validation: 'boolean', - optionsKey: '__boolean', - label: 'Will be surge personnel be deployed?', - }, - - surge_personnel_deployed: { - type: 'input', - validation: 'string', - label: 'Description', - }, - - logistic_capacity_of_ns: { - type: 'input', - validation: 'string', - label: 'If there is procurement, will be done by National Society or IFRC?', - }, - - pmer: { - type: 'input', - validation: 'string', - label: 'How will this operation be monitored?', - }, - - communication: { - type: 'input', - validation: 'string', - label: 'Please briefly explain the National Societies communication strategy for this operation.', - }, - - // Submission - ns_request_date: { - type: 'input', - validation: 'date', - label: 'Date of National Society Application', - }, - - submission_to_geneva: { - type: 'input', - validation: 'date', - label: 'Date of Submission to GVA', - }, - - date_of_approval: { - type: 'input', - validation: 'date', - label: 'Date of Approval', - }, - - operation_timeframe: { - type: 'input', - validation: 'number', - label: 'Operation timeframe', - }, - - end_date: { - type: 'input', - validation: 'date', - label: 'End date of Operation', - }, - - publishing_date: { - type: 'input', - validation: 'date', - label: 'Date of Publishing', - }, - - appeal_code: { - type: 'input', - validation: 'string', - label: 'Appeal Code', - }, - - glide_code: { - type: 'input', - validation: 'string', - label: 'GLIDE number', - }, - - ifrc_appeal_manager_name: { - type: 'input', - validation: 'string', - label: 'IFRC Appeal Manager Name', - }, - - ifrc_appeal_manager_title: { - type: 'input', - validation: 'string', - label: 'IFRC Appeal Manager Title', - }, - - ifrc_appeal_manager_email: { - type: 'input', - validation: 'string', - label: 'IFRC Appeal Manager Email', - }, - - ifrc_appeal_manager_phone_number: { - type: 'input', - validation: 'string', - label: 'IFRC Appeal Manager Phone Number', - }, - - ifrc_project_manager_name: { - type: 'input', - validation: 'string', - label: 'IFRC Project Manager Name', - }, - - ifrc_project_manager_title: { - type: 'input', - validation: 'string', - label: 'IFRC Project Manager Title', - }, - - ifrc_project_manager_email: { - type: 'input', - validation: 'string', - label: 'IFRC Project Manager Email', - }, - - ifrc_project_manager_phone_number: { - type: 'input', - validation: 'string', - label: 'IFRC Project Manager Phone Number', - }, - - national_society_contact_name: { - type: 'input', - validation: 'string', - label: 'National Society Contact Name', - }, - - national_society_contact_title: { - type: 'input', - validation: 'string', - label: 'National Society Contact Title', - }, - - national_society_contact_email: { - type: 'input', - validation: 'string', - label: 'National Society Contact Email', - }, - - national_society_contact_phone_number: { - type: 'input', - validation: 'string', - label: 'National Society Contact Phone Number', - }, - - ifrc_emergency_name: { - type: 'input', - validation: 'string', - label: 'IFRC focal point for the emergency Name', - }, - - ifrc_emergency_title: { - type: 'input', - validation: 'string', - label: 'IFRC focal point for the emergency Title', - }, - - ifrc_emergency_email: { - type: 'input', - validation: 'string', - label: 'IFRC focal point for the emergency Email', - }, - - ifrc_emergency_phone_number: { - type: 'input', - validation: 'string', - label: 'IFRC focal point for the emergency Phone number', - }, - - regional_focal_point_name: { - type: 'input', - validation: 'string', - label: 'DREF Regional Focal Point Name', - }, - - regional_focal_point_title: { - type: 'input', - validation: 'string', - label: 'DREF Regional Focal Point Title', - }, - - regional_focal_point_email: { - type: 'input', - validation: 'string', - label: 'DREF Regional Focal Point Email', - }, - - regional_focal_point_phone_number: { - type: 'input', - validation: 'string', - label: 'DREF Regional Focal Point Phone Number', - }, - - media_contact_name: { - type: 'input', - validation: 'string', - label: 'Media Contact Name', - }, - - media_contact_title: { - type: 'input', - validation: 'string', - label: 'Media Contact Title', - }, - - media_contact_email: { - type: 'input', - validation: 'string', - label: 'Media Contact Email', - }, - - media_contact_phone_number: { - type: 'input', - validation: 'string', - label: 'Media Contact Phone Number', - }, - }, - }; - - const templateActions = createImportTemplate(drefFormSchema, optionsMap); - - async function generateTemplate() { - const workbook = new xlsx.Workbook(); - const now = new Date(); - workbook.created = now; - // workbook.description = JSON.stringify(drefFormSchema); - - const response = await fetch(ifrcLogoFile); - const buffer = await response.arrayBuffer(); - - const ifrcLogo = workbook.addImage({ - buffer, - extension: 'png', - }); - - const coverWorksheet = workbook.addWorksheet('DREF Import'); - - const overviewWorksheet = workbook.addWorksheet(SHEET_OPERATION_OVERVIEW); - const eventDetailsWorksheet = workbook.addWorksheet(SHEET_EVENT_DETAIL); - const actionsNeedsWorksheet = workbook.addWorksheet(SHEET_ACTIONS_NEEDS); - const operationWorksheet = workbook.addWorksheet(SHEET_OPERATION); - const timeframeAndContactsWorksheet = workbook.addWorksheet( - SHEET_TIMEFRAMES_AND_CONTACTS, - ); - - const sheetMap: Record = { - [SHEET_OPERATION_OVERVIEW]: overviewWorksheet, - [SHEET_EVENT_DETAIL]: eventDetailsWorksheet, - [SHEET_ACTIONS_NEEDS]: actionsNeedsWorksheet, - [SHEET_OPERATION]: operationWorksheet, - [SHEET_TIMEFRAMES_AND_CONTACTS]: timeframeAndContactsWorksheet, - }; - - const optionsWorksheet = workbook.addWorksheet('options'); - optionsWorksheet.state = 'veryHidden'; - const optionKeys = Object.keys(optionsMap) as (keyof (typeof optionsMap))[]; - - optionsWorksheet.columns = optionKeys.map((key) => ( - { header: key, key } - )); - - optionKeys.forEach((key) => { - const options = optionsMap[key]; - - if (isDefined(options)) { - const column = optionsWorksheet.getColumnKey(key); - - options.forEach((option, i) => { - const cell = optionsWorksheet.getCell(i + 2, column.number); - cell.name = String(option.key); - cell.value = option.label; - }); - } - }); - - coverWorksheet.addImage(ifrcLogo, 'A1:B6'); - coverWorksheet.getCell('C1').value = 'DISASTER RESPONSE EMERGENCY FUND'; - coverWorksheet.mergeCells('C1:L3'); - coverWorksheet.getCell('C1:L3').style = { - font: { - name: 'Montserrat', - family: 2, - bold: true, - size: 20, - color: { argb: hexToArgb(COLOR_PRIMARY_RED) }, - }, - alignment: { horizontal: 'center', vertical: 'middle' }, - }; - coverWorksheet.addRow(''); - coverWorksheet.addRow(''); - coverWorksheet.addRow(''); - coverWorksheet.addRow(''); - coverWorksheet.mergeCells('C4:L6'); - coverWorksheet.getCell('C4').value = 'Import template'; - coverWorksheet.getCell('C4').style = { - font: { - bold: true, size: 18, name: 'Montserrat', family: 2, - }, - alignment: { horizontal: 'center', vertical: 'middle' }, - }; - - const rowOffset = 2; - - templateActions.forEach((templateAction, i) => { - if (templateAction.type === 'heading') { - addRow( - overviewWorksheet, - i + rowOffset, - templateAction.outlineLevel, - String(templateAction.name), - templateAction.label, - { - ...headingStyle, - font: { - ...headingStyle.font, - }, - }, - ); - } else if (templateAction.type === 'input') { - if (templateAction.dataValidation === 'list') { - addInputRow( - overviewWorksheet, - i + rowOffset, - templateAction.outlineLevel, - String(templateAction.name), - templateAction.label, - String(templateAction.optionsKey), - optionsWorksheet, - ); - } else { - addInputRow( - overviewWorksheet, - i + rowOffset, - templateAction.outlineLevel, - String(templateAction.name), - templateAction.label, - ); - } - } - }); - - Object.values(sheetMap).forEach( - (sheet) => { - const worksheet = sheet; - worksheet.properties.defaultRowHeight = 20; - worksheet.properties.showGridLines = false; - - worksheet.columns = [ - { - key: 'field', - header: 'Field', - protection: { locked: true }, - width: 50, - }, - { - key: 'value', - header: 'Value', - width: 50, - }, - ]; - - worksheet.getRow(1).eachCell( - (cell) => { - // eslint-disable-next-line no-param-reassign - cell.style = headerRowStyle; - }, - ); - }, - ); - - await workbook.xlsx.writeBuffer().then( - (sheet) => { - FileSaver.saveAs( - new Blob([sheet], { type: 'application/vnd.ms-excel;charset=utf-8' }), - `DREF import template ${now.toLocaleString()}.xlsx`, - ); - }, - ); - - setGenerationPending(false); - } - - setGenerationPending((alreadyGenerating) => { - if (!alreadyGenerating) { - generateTemplate(); - } - - return true; - }); + const [ + showDownloadImportTemplateModal, + { + setTrue: setShowDownloadImportTemplateTrue, + setFalse: setShowDownloadImportTemplateFalse, }, - [ - countries, - disasterTypes, - nationalSocieties, - dref_planned_intervention_title, - dref_national_society_action_title, - dref_identified_need_title, - dref_dref_onset_type, - dref_dref_disaster_category, - ], - ); + ] = useBooleanState(false); return ( - + <> + + {showDownloadImportTemplateModal && ( + + )} + ); } diff --git a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/utils.ts b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/utils.ts new file mode 100644 index 0000000000..c7fdbc0d49 --- /dev/null +++ b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/utils.ts @@ -0,0 +1,297 @@ +import { + isDefined, + isTruthyString, +} from '@togglecorp/fujs'; +import xlsx, { + type Border, + type Row, + type Style, + type Workbook, + type Worksheet, +} from 'exceljs'; + +import ifrcLogoFile from '#assets/icons/ifrc-square.png'; +import { + COLOR_DARK_GREY, + COLOR_PRIMARY_BLUE, + COLOR_PRIMARY_RED, +} from '#utils/constants'; + +function hexToArgb(hexStr: string, alphaStr = 'ff') { + const hexWithoutHash = hexStr.substring(1, hexStr.length); + + return `${alphaStr}${hexWithoutHash}`; +} + +export const headerRowStyle: Partial