diff --git a/app/src/utils/importTemplate.ts b/app/src/utils/importTemplate.ts index 4881e4902..44d0d8460 100644 --- a/app/src/utils/importTemplate.ts +++ b/app/src/utils/importTemplate.ts @@ -130,7 +130,7 @@ export function getCombinedKey( return `${String(parentKey)}__${String(key)}`; } -type TemplateField = HeadingTemplateField | InputTemplateField; +export type TemplateField = HeadingTemplateField | InputTemplateField; // TODO: add test export function createImportTemplate< diff --git a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/index.tsx b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/index.tsx index 60f58652c..7796d4765 100644 --- a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/index.tsx +++ b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/index.tsx @@ -22,6 +22,7 @@ import FileSaver from 'file-saver'; import useGlobalEnums from '#hooks/domain/useGlobalEnums'; import { + COLOR_PRIMARY_RED, DREF_TYPE_RESPONSE, TypeOfDrefEnum, } from '#utils/constants'; @@ -33,7 +34,10 @@ import { SHEET_OPERATION_OVERVIEW, SHEET_TIMEFRAMES_AND_CONTACTS, } from '#utils/domain/dref'; -import { createImportTemplate } from '#utils/importTemplate'; +import { + createImportTemplate, + type TemplateField, +} from '#utils/importTemplate'; import { actionsTabFields, eventDetailTabFields, @@ -47,6 +51,7 @@ import { addInputRow, buildCoverWorksheet, headerRowStyle, + hexToArgb, } from '../utils'; import useImportTemplateSchema from './useImportTemplateSchema'; @@ -56,6 +61,263 @@ function typeOfDrefKeySelector(option: { key: TypeOfDrefEnum }) { return option.key; } +async function generateTemplate( + templateActions: TemplateField[], + optionsMap: ReturnType['optionsMap'], + drefTypeLabelMap: Record | undefined, + typeOfDref: TypeOfDrefEnum, + callback: () => void, +) { + 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', + { properties: { tabColor: { argb: hexToArgb(COLOR_PRIMARY_RED, '10') } } }, + ); + await buildCoverWorksheet(coverWorksheet, workbook, typeOfDrefLabel); + + const overviewWorksheet = workbook.addWorksheet( + SHEET_OPERATION_OVERVIEW, + { properties: { tabColor: { argb: hexToArgb(COLOR_PRIMARY_RED, '10') } } }, + ); + // TODO: Add red color to all the sheet tabs + const eventDetailsWorksheet = workbook.addWorksheet( + SHEET_EVENT_DETAIL, + { properties: { tabColor: { argb: hexToArgb(COLOR_PRIMARY_RED, '10') } } }, + ); + const actionsNeedsWorksheet = workbook.addWorksheet( + SHEET_ACTIONS_NEEDS, + { properties: { tabColor: { argb: hexToArgb(COLOR_PRIMARY_RED, '10') } } }, + ); + const operationWorksheet = workbook.addWorksheet( + SHEET_OPERATION, + { properties: { tabColor: { argb: hexToArgb(COLOR_PRIMARY_RED, '10') } } }, + ); + const timeframeAndContactsWorksheet = workbook.addWorksheet( + SHEET_TIMEFRAMES_AND_CONTACTS, + { properties: { tabColor: { argb: hexToArgb(COLOR_PRIMARY_RED, '10') } } }, + ); + + 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 }) => { + let lastHeadingIndex = 0; + actions.forEach((templateAction, i) => { + if (templateAction.type === 'heading') { + addHeadingRow( + worksheet, + i + ROW_OFFSET, + templateAction.outlineLevel, + String(templateAction.name), + templateAction.label, + templateAction.description, + ); + worksheet.mergeCells(i + ROW_OFFSET, 1, i + ROW_OFFSET, 3); + lastHeadingIndex = i + 1; + } else if (templateAction.type === 'input') { + const mode = (i - lastHeadingIndex) % 2 === 0 ? 'one' : 'two'; + if (templateAction.dataValidation === 'list') { + addInputRow( + mode, + worksheet, + i + ROW_OFFSET, + templateAction.outlineLevel, + String(templateAction.name), + templateAction.label, + templateAction.description, + 'list', + String(templateAction.optionsKey), + optionsWorksheet, + ); + } else { + addInputRow( + mode, + worksheet, + i + ROW_OFFSET, + templateAction.outlineLevel, + String(templateAction.name), + templateAction.label, + templateAction.description, + templateAction.dataValidation, + ); + } + } + }); + }); + + 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: 100, + style: { alignment: { wrapText: true } }, + }, + { + key: 'description', + header: 'Description', + width: 80, + }, + ]; + + worksheet.getRow(1).eachCell( + (cell) => { + // eslint-disable-next-line no-param-reassign + cell.style = headerRowStyle; + }, + ); + + /* + worksheet.eachRow({ includeEmpty: true }, (row, rowNumber) => { + if (rowNumber <= 1) { // Skip the header row + return; + } + + const fillColor = rowNumber % 2 === 0 + ? hexToArgb(COLOR_PRIMARY_RED, '10') + : ; + + row.eachCell((cell) => { + const fill: xlsx.FillPattern = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: fillColor }, + }; + // eslint-disable-next-line no-param-reassign + cell.style = { + font: { + color: { argb: 'FFFFFFFF' }, + }, + fill, + }; + }); + }); + */ + }, + ); + + 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, + ); + }, + ); + + callback(); +} + interface Props { onComplete: () => void; } @@ -88,210 +350,18 @@ function DownloadImportTemplateModal(props: Props) { 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, - templateAction.description, - ); - } else if (templateAction.type === 'input') { - if (templateAction.dataValidation === 'list') { - addInputRow( - worksheet, - i + ROW_OFFSET, - templateAction.outlineLevel, - String(templateAction.name), - templateAction.label, - templateAction.description, - 'list', - String(templateAction.optionsKey), - optionsWorksheet, - ); - } else { - addInputRow( - worksheet, - i + ROW_OFFSET, - templateAction.outlineLevel, - String(templateAction.name), - templateAction.label, - templateAction.description, - templateAction.dataValidation, - ); - } - } - }); - }); - - 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, - }, - { - key: 'description', - header: 'Description', - width: 80, - }, - ]; - - 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(); + generateTemplate( + templateActions, + optionsMap, + drefTypeLabelMap, + typeOfDref, + () => { + setGenerationPending(false); + onComplete(); + }, + ); } return true; diff --git a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/useImportTemplateSchema.ts b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/useImportTemplateSchema.ts index 83b6467d0..7f0daeada 100644 --- a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/useImportTemplateSchema.ts +++ b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/DownloadImportTemplateModal/useImportTemplateSchema.ts @@ -53,21 +53,27 @@ function useImportTemplateSchema() { { key: 'source__0', label: 'Source #1' }, { key: 'source__1', label: 'Source #2' }, { key: 'source__2', label: 'Source #3' }, + { key: 'source__3', label: 'Source #4' }, + { key: 'source__4', label: 'Source #5' }, ], planned_interventions__indicators: [ { key: 'indicator__0', label: 'Indicator #1' }, { key: 'indicator__1', label: 'Indicator #2' }, { key: 'indicator__2', label: 'Indicator #3' }, + { key: 'indicator__3', label: 'Indicator #4' }, + { key: 'indicator__4', label: 'Indicator #5' }, ], risk_security: [ { key: 'risk__0', label: 'Risk #1' }, { key: 'risk__1', label: 'Risk #2' }, { key: 'risk__2', label: 'Risk #3' }, + { key: 'risk__3', label: 'Risk #4' }, + { key: 'risk__4', label: 'Risk #5' }, ], national_society_actions: dref_national_society_action_title?.map( ({ key, value }) => ({ key, label: value }), ) ?? [], - identified_needs: dref_identified_need_title?.map( + needs_identified: dref_identified_need_title?.map( ({ key, value }) => ({ key, label: value }), ) ?? [], }), [ @@ -189,6 +195,7 @@ function useImportTemplateSchema() { 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', + description: 'Enter MDR code and year. Example: MDR', }, lessons_learned: { @@ -200,7 +207,7 @@ function useImportTemplateSchema() { event_date: { type: 'input', - label: 'Date of the Event', + label: 'Date when the trigger was met', validation: 'date', }, @@ -260,6 +267,12 @@ function useImportTemplateSchema() { label: 'Has the National Society started any actions?', }, + ns_respond_date: { + type: 'input', + validation: 'date', + label: 'Start date of National Society actions', + }, + national_society_actions: { type: 'list', label: 'National Society Actions', @@ -325,10 +338,16 @@ function useImportTemplateSchema() { label: 'Are there major coordination mechanisms in place?', }, + major_coordination_mechanism: { + type: 'input', + validation: 'string', + label: 'List coordination mechanisms/platform in place at local/district and national level. Indicate the lead authorities/agencies. How the National Society is involved/positioned in this coordination. Does the NS in any lead/co-lead role? Any identified gap/overlap in the coordination (e.g., sector missing…)?', + }, + needs_identified: { type: 'list', label: 'Identified Needs', - optionsKey: 'identified_needs', + optionsKey: 'needs_identified', children: { type: 'object', fields: { @@ -488,6 +507,7 @@ function useImportTemplateSchema() { label: 'Person targeted', }, description: { + description: 'A list should start with an \' * \' followed by a space. There are no limits to the number of lists that can be included. Eg:* Activity XYZ* Activity ABC', type: 'input', validation: 'string', label: 'List of activities', @@ -590,171 +610,6 @@ function useImportTemplateSchema() { label: 'Date of Publishing', description: 'Added by Regional Office', }, - - appeal_code: { - type: 'input', - validation: 'string', - label: 'Appeal Code', - description: 'Added by the regional PMER', - }, - - glide_code: { - type: 'input', - validation: 'string', - label: 'GLIDE number', - }, - - ifrc_appeal_manager_name: { - type: 'input', - validation: 'string', - label: 'IFRC Appeal Manager: Name', - description: 'Added by the regional office', - }, - - ifrc_appeal_manager_title: { - type: 'input', - validation: 'string', - label: 'IFRC Appeal Manager: Title', - description: 'Added by the regional office', - }, - - ifrc_appeal_manager_email: { - type: 'input', - validation: 'string', - label: 'IFRC Appeal Manager: Email', - description: 'Added by the regional office', - }, - - ifrc_appeal_manager_phone_number: { - type: 'input', - validation: 'string', - label: 'IFRC Appeal Manager: Phone Number', - description: 'Added by the regional office', - }, - - ifrc_project_manager_name: { - type: 'input', - validation: 'string', - label: 'IFRC Project Manager: Name', - description: 'Added by the regional office', - }, - - ifrc_project_manager_title: { - type: 'input', - validation: 'string', - label: 'IFRC Project Manager: Title', - description: 'Added by the regional office', - }, - - ifrc_project_manager_email: { - type: 'input', - validation: 'string', - label: 'IFRC Project Manager: Email', - description: 'Added by the regional office', - }, - - ifrc_project_manager_phone_number: { - type: 'input', - validation: 'string', - label: 'IFRC Project Manager: Phone Number', - description: 'Added by the regional office', - }, - - 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', - }, }, }), []); diff --git a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/utils.ts b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/utils.ts index 8c9688ff7..b88e1f6b4 100644 --- a/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/utils.ts +++ b/app/src/views/AccountMyFormsDref/DownloadImportTemplateButton/utils.ts @@ -3,7 +3,6 @@ import { isTruthyString, } from '@togglecorp/fujs'; import xlsx, { - type Border, type Row, type Style, type Workbook, @@ -17,7 +16,7 @@ import { COLOR_PRIMARY_RED, } from '#utils/constants'; -function hexToArgb(hexStr: string, alphaStr = 'ff') { +export function hexToArgb(hexStr: string, alphaStr = 'ff') { const hexWithoutHash = hexStr.substring(1); return `${alphaStr}${hexWithoutHash}`; @@ -42,48 +41,46 @@ export const headerRowStyle: Partial