import { Injectable } from '@angular/core'; import { JsZipService } from '@core/services/js-zip.service'; import { PDFService } from '@core/services/pdf.service'; import { PolicyService } from '@core/services/policy.service'; import { SpecialHandlingService } from '@core/services/special-handling.service'; import { SpinnerService } from '@core/services/spinner.service'; import { StatusService } from '@core/services/status.service'; import { TranslationService } from '@core/services/translation.service'; import { ReferenceFieldAPI } from '@core/typings/api/reference-fields.typing'; import { AppDetailForDownload, ApplicationForUi, ApplicationFromPaginated, BaseApplication, Nominee } from '@core/typings/application.typing'; import { BulkCollaboratorResponse } from '@core/typings/collaboration.typing'; import { ApplicantInfoForPDF, ApplicationInfoForPDF, CsvForDownload, FileUploadForPDF, FormInfoForPDF, OrgInfoForPDF, TableCsvForPdf } from '@core/typings/pdf.typing'; import { AdHocReportingUI } from '@core/typings/ui/ad-hoc-reporting.typing'; import { ReferenceFieldsUI } from '@core/typings/ui/reference-fields.typing'; import { WorkflowLevel } from '@core/typings/workflow.typing'; import { ApplicationActivityService } from '@features/application-activity/application-activity.service'; import { ApplicationFormService } from '@features/application-forms/services/application-forms.service'; import { ApplicationManagerService } from '@features/application-manager/services/application-manager/application-manager.service'; import { ApplicationViewService } from '@features/application-view/application-view.service'; import { AwardService } from '@features/awards/award.service'; import { Award, AwardFromApi } from '@features/awards/typings/award.typing'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { CollaborationService } from '@features/collaboration/collaboration.service'; import { ApplicationViewFormForUI, ApplicationViewFormFromApi, BulkDownloadGroup, FormDefinitionForUi, FormResponse, FormResponseFromApi, FormStatuses, SubmissionMap, WorkflowLevelSubmissionForm } from '@features/configure-forms/form.typing'; import { FormsService } from '@features/configure-forms/services/forms/forms.service'; import { EmployeeSSOFieldsService } from '@features/employee-sso-fields/employee-sso-fields.service'; import { EmployeeSSOFieldsData } from '@features/employee-sso-fields/employee-sso-fields.typing'; import { SpecialHandling } from '@features/formio/formio-components/standard-formio-components/gc-special-handling/gc-special-handling.component'; import { FormHelperService } from '@features/formio/services/form-helper/form-helper.service'; import { FormLogicEvaluatorService } from '@features/formio/services/form-logic-evaluator/form-logic-evaluator.service'; import { FormLogicService } from '@features/formio/services/form-logic/form-logic.service'; import { ReportFieldService } from '@features/formio/services/report-field/report-field.service'; import { InKindRequestedItem } from '@features/in-kind/in-kind.typing'; import { NonprofitService } from '@features/nonprofit/nonprofit.service'; import { ProgramService } from '@features/programs/program.service'; import { ReferenceFieldsService } from '@features/reference-fields/services/reference-fields.service'; import { SignatureService } from '@features/signature/signature.service'; import { EmailService } from '@features/system-emails/email.service'; import { ApplicationEmailPdf, EmailPdfType } from '@features/system-emails/email.typing'; import { ExportEmailService } from '@features/system-emails/export-emails/export-emails.service'; import { SystemTagsService } from '@features/system-tags/system-tags.service'; import { UserService } from '@features/users/user.service'; import { WorkflowResolver } from '@features/workflow/resolvers/workflow.resolver'; import { WorkflowService } from '@features/workflow/workflow.service'; import { AddressFormatterService, FileService, SimpleStringMap } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { ModalFactory } from '@yourcause/common/modals'; import { NotifierService } from '@yourcause/common/notifier'; import JSZip from 'jszip'; import { uniq } from 'lodash'; import { ApplicationDownloadResources } from './application-download.resources'; import { AppDownloadBulkModalResponse, BulkDownloadApplicationModalComponent } from './bulk-download-application-modal/bulk-download-application-modal.component'; import { DownloadApplicationPDFModalComponent, DownloadAppModalResponse } from './download-application-pdf-modal/download-application-pdf-modal.component'; @Injectable({ providedIn: 'root' }) export class ApplicationDownloadService { constructor ( private workflowService: WorkflowService, private programService: ProgramService, private translationService: TranslationService, private applicationManagerService: ApplicationManagerService, private workflowResolver: WorkflowResolver, private formService: FormsService, private policyService: PolicyService, private applicationDownloadResources: ApplicationDownloadResources, private statusService: StatusService, private systemTagsService: SystemTagsService, private formHelperService: FormHelperService, private referenceFieldsService: ReferenceFieldsService, private reportFieldService: ReportFieldService, private collaborationService: CollaborationService, private employeeSsoFieldsService: EmployeeSSOFieldsService, private signatureService: SignatureService, private i18n: I18nService, private nonprofitService: NonprofitService, private applicationActivityService: ApplicationActivityService, private exportEmailService: ExportEmailService, private emailService: EmailService, private modalFactory: ModalFactory, private spinnerService: SpinnerService, private notifier: NotifierService, private logger: LogService, private addressFormatter: AddressFormatterService, private clientSettingsService: ClientSettingsService, private pdfService: PDFService, private fileService: FileService, private jsZipService: JsZipService, private userService: UserService, private formLogicService: FormLogicService, private applicationFormService: ApplicationFormService, private formLogicEvaluatorService: FormLogicEvaluatorService, private specialHandlingService: SpecialHandlingService, private applicationViewService: ApplicationViewService, private awardService: AwardService ) { } /** * * @param application: The application to download * @param isApplicantMasked: Is the applicant masked? * @param isNomination: Is nomination? * @param awards: Awards for download */ async downloadApplicationModal ( application: ApplicationForUi|ApplicationFromPaginated, isApplicantMasked: boolean, canViewMaskedApplicants: boolean, isNomination: boolean, fromAppManager: boolean, awards: AwardFromApi[] ) { this.spinnerService.startSpinner(); const { workflowId, workflowLevelName, workflowName, defaultForm, forCurrentWfl, submissions, refResponseMapByAppFormId, applicationForUi, applicationAwards } = await this.prepareDownloadApplicationModal(application, fromAppManager, awards); this.spinnerService.stopSpinner(); const response = await this.modalFactory.open( DownloadApplicationPDFModalComponent, { applicationId: application.applicationId, workflowId, forCurrentWfl, defaultForm, isNomination, submissions, applicationForUi, applicationAwards, applicantIsMaskedAndCantView: isApplicantMasked && !canViewMaskedApplicants } ); if (response) { await this.handleDownloadApplicationResponse( response, applicationForUi, isNomination, workflowLevelName, workflowName, defaultForm?.formId, isApplicantMasked, refResponseMapByAppFormId, applicationAwards ); } } /** * * @param response: download app modal response * @param application: the application * @param isNomination: is nomination? * @param workflowLevelName: workflow level name * @param workflowName: workflow name * @param defaultFormId: the default form id * @param isApplicantMasked: is applicant masked? * @param refResponseMapByAppFormId: ref responses mapped * @param awards: Awards for download */ async handleDownloadApplicationResponse ( response: DownloadAppModalResponse, application: ApplicationForUi|ApplicationFromPaginated, isNomination: boolean, workflowLevelName: string, workflowName: string, defaultFormId: number, isApplicantMasked: boolean, refResponseMapByAppFormId: Record, awards: AwardFromApi[] ) { const includedSubmissions = response.submissions; this.spinnerService.startSpinner(); this.spinnerService.setLoadingMessage(this.i18n.translate( 'GLOBAL:textDoNotCloseOrRefreshThisPage', {}, 'Do not close or refresh this page until complete' )); await this.handleDownloadApplication( application, includedSubmissions, isNomination, workflowLevelName, workflowName, defaultFormId, response.isMasked, isApplicantMasked, refResponseMapByAppFormId, response.showActivityTrail, response.showEmails, response.showAwards, awards ); this.spinnerService.setLoadingMessage(''); this.spinnerService.stopSpinner(); } /** * * @param application: the application * @returns workflow attributes */ getWorkflowAttrs (application: ApplicationFromPaginated | ApplicationForUi) { const workflowId = 'currentWorkFlowId' in application ? application.currentWorkFlowId : application.workflowId; const workflowLevelName = 'currentWorkFlowLevelName' in application ? application.currentWorkFlowLevelName : application.currentWorkflowLevelName; const workflowName = 'workflowName' in application ? application.workflowName : application.currentWorkflowName; return { workflowId, workflowLevelName, workflowName }; } /** * * @param apps: Applications for bulk download * @param isNomination: Is nomination * @param applicantsAreMasked: Are applicants masked? */ async bulkDownloadApplicationModal ( apps: ApplicationFromPaginated[], isNomination: boolean, applicantsAreMasked: boolean ) { const response = await this.modalFactory.open( BulkDownloadApplicationModalComponent, { applicationIds: apps.map((app) => app.applicationId), isNomination, programId: apps[0].programId, workflowId: apps[0].workflowId } ); if (response) { this.spinnerService.startSpinner(); this.spinnerService.setLoadingMessage(this.i18n.translate( 'GLOBAL:textDoNotCloseOrRefreshThisPage', {}, 'Do not close or refresh this page until complete' )); await this.handleBulkDownloadApplications( apps, response, isNomination, applicantsAreMasked ); this.spinnerService.setLoadingMessage(''); this.spinnerService.stopSpinner(); } } /** * * @param apps: applications to bulk download * @param response: modal response from the bulk download * @param isNomination: is nomination? * @param applicantsAreMasked: Masking is on */ async handleBulkDownloadApplications ( apps: ApplicationFromPaginated[], response: AppDownloadBulkModalResponse, isNomination: boolean, applicantsAreMasked: boolean ) { try { await this.downloadMultipleApplicationsAsPdf( apps, response, isNomination, applicantsAreMasked ); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'common:textErrorDownloadPDF', {}, 'There was an error downloading the PDF' )); } } /** * Download the selected form responses for an application * * @param application: The application to download * @param submissions: The workflow level / form submissions to download * @param isNomination: Is nomination * @param workflowLevelName: Workflow level name * @param workflowName: Workflow name * @param defaultFormId: Default form ID for the program * @param formFieldsMasked: Should we mask form fields? * @param isApplicantMasked: Is the applicant masked? * @param refResponseMapByAppFormId: reference field responses, * @param showActivityTrail: include activity trail? * @param showEmails: include emails? * @param showAwards: include awards? * @param awards: Awards to download */ async handleDownloadApplication ( application: ApplicationFromPaginated|ApplicationForUi, submissions: SubmissionMap[], isNomination: boolean, workflowLevelName: string, workflowName: string, defaultFormId: number, formFieldsMasked: boolean, isApplicantMasked: boolean, refResponseMapByAppFormId: Record, showActivityTrail: boolean, showEmails: boolean, showAwards: boolean, awards: AwardFromApi[] ) { try { const { applicants, tags, reportFieldResponse, employeeInfo, programLogo, inKindItems, specialHandling, orgPhone, activities, emails, nominee } = await this.prepSingleApplicationForDownload( application, submissions, isApplicantMasked, showActivityTrail, showEmails, isNomination ); const { appForPdf, orgForPdf, formInfos, fileUploads, tableCsvs } = await this.prepApplicationForDownload( application, submissions, workflowLevelName, workflowName, tags, employeeInfo, reportFieldResponse, inKindItems, refResponseMapByAppFormId, orgPhone, specialHandling, isApplicantMasked ); const emailPdfBlob = await this.getEmailPdfBlob( application.applicationId, showEmails, emails ); const activitiesCsv = this.applicationActivityService.convertActivitiesToCsv( activities, isNomination ); await this.buildApplicationPDF( applicants, appForPdf, nominee, orgForPdf, isNomination, formInfos, fileUploads, tableCsvs, activitiesCsv, emailPdfBlob, formFieldsMasked, this.clientSettingsService.clientBranding.name, programLogo, defaultFormId, null, showAwards ? this.awardService.constructAwards(awards) : null, application.isArchived, false ); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'common:textErrorDownloadPDF', {}, 'There was an error downloading the PDF' )); } } /** * * @param applicationId: application id * @param showEmails: show emails? * @param emails: emails for pdf * @returns the email pdf blob */ async getEmailPdfBlob ( applicationId: number, showEmails: boolean, emails: ApplicationEmailPdf[] ) { if (showEmails && emails.length > 0) { return this.pdfService.getEmailPdfBlobForApplication( applicationId, emails, EmailPdfType.Application ); } return null; } /** * Build the PDF for the application * * @param applicants: Applicants on the app * @param application: Application record * @param nominee: Nominee on the app * @param orgInfo: Org info * @param isNomination: Is nomination * @param formInfos: Detailed form info for download * @param fileUploads: File uploads for download * @param tableCsvs: Table Csvs for download * @param activitiesCsv: The activities csv string * @param emailPdfBlob: Email PDF blob to download * @param formFieldsMasked: Are form fields masked? * @param clientName: Client name * @param logo: Program Logo * @param defaultFormId: Default Form ID on program * @param bulkZip: If bulk, this is the parent zip * @param awards: Awards for download * @param isArchived: is the application archived? * @param isForApplicantPortal? is this in the applicant portal? * @returns the zip */ async buildApplicationPDF ( applicants: ApplicantInfoForPDF[], application: ApplicationInfoForPDF, nominee: Nominee, orgInfo: OrgInfoForPDF, isNomination: boolean, formInfos: FormInfoForPDF[], fileUploads: FileUploadForPDF[], tableCsvs: TableCsvForPdf[], activitiesCsv: string, emailPdfBlob: Blob, formFieldsMasked: boolean, clientName: string, logo: string, defaultFormId: number, bulkZip: JSZip, awards: Award[], // pass this if you want to show awards in PDF isArchived: boolean, isForApplicantPortal: boolean ) { const payload = { clientName, nominee: { name: nominee?.firstName + ' ' + nominee?.lastName, address: '', phoneNumber: nominee?.phoneNumber, email: nominee?.email }, isNomination, isMasked: application.isMasked, applicants, applicationId: application.appId, applicationInfo: application, forms: formInfos, orgInfo, logo, defaultFormId, responsesMasked: formFieldsMasked, awards, isArchived, isForApplicantPortal }; const applicantFooterText = this.getApplicantFooterText(applicants, application.isMasked); const downloadUrl = await this.pdfService.generateApplicationPdf( payload, applicantFooterText ); if (downloadUrl) { return this.handleAttachmentsAndDownload( application.appId, downloadUrl, fileUploads, tableCsvs, activitiesCsv, emailPdfBlob, bulkZip ); } return null; } /** * * @param applicants: the applicants * @param dontShowApplicantName: if we are hiding applicant name * @returns pdf footer text */ getApplicantFooterText ( applicants: ApplicantInfoForPDF[], dontShowApplicantName: boolean ) { if (!dontShowApplicantName) { let applicantString = this.i18n.translate( 'common:lblApplicantsOpt', {}, 'Applicant(s)' ) + ':'; applicants.forEach((applicant, index) => { if (index !== 0) { applicantString = applicantString + ' \|'; } applicantString = applicantString + ' ' + applicant.name + ' \(' + applicant.email + '\)'; }); return applicantString; } return ''; } /** * * @param appId: Application ID to download * @param downloadUrl: PDF download URL * @param fileUploads: File Uploads for PDF * @param tableCsvs: Table CSVs for PDF * @param activitiesCsv: Activites csv string for download * @param emailPdfBlob: Email PDF blob for download * @param bulkZip: If bulk, this is the parent zip * @returns the zip */ async handleAttachmentsAndDownload ( appId: number, downloadUrl: string, fileUploads: FileUploadForPDF[], tableCsvs: TableCsvForPdf[], activitiesCsv: string, emailPdfBlob: Blob, bulkZip: JSZip ) { const hasFilesOrCsvs = fileUploads.length > 0 || tableCsvs.length > 0 || !!activitiesCsv || !!emailPdfBlob; const applicationText = this.i18n.translate( 'common:lblApplication', {}, 'Application' ); if (bulkZip || hasFilesOrCsvs) { const ApplicationPdfBlob = await this.pdfService.getPdfBlob( downloadUrl ); const zip = bulkZip || this.jsZipService.createZip(); let applicationFolderForBulk: JSZip; let attachmentsFolder: JSZip; const attachmentsText = this.i18n.translate( 'common:textAttachments', {}, 'Attachments' ); const applicationAppText = `${applicationText} ${appId}`; const hasAttachmentsFolder = fileUploads.length > 0 || tableCsvs.length > 0; if (bulkZip) { applicationFolderForBulk = this.jsZipService.addFolder(zip, applicationAppText); this.jsZipService.addFile( applicationFolderForBulk, `${applicationAppText}.pdf`, ApplicationPdfBlob ); if (hasAttachmentsFolder) { attachmentsFolder = this.jsZipService.addFolder( applicationFolderForBulk, attachmentsText ); } } else { this.jsZipService.addFile( zip, `${applicationAppText}.pdf`, ApplicationPdfBlob ); if (hasAttachmentsFolder) { attachmentsFolder = this.jsZipService.addFolder(zip, attachmentsText); } } await this.pdfService.handleFileUploads(fileUploads, attachmentsFolder); this.handleTableCsvs(tableCsvs, attachmentsFolder); this.handleActivities(activitiesCsv, applicationFolderForBulk || zip); this.handleEmails(emailPdfBlob, applicationFolderForBulk || zip); if (!bulkZip) { await this.pdfService.handleZipDownload(zip, `${applicationText}.zip`); } return zip; } else { this.fileService.downloadUrlAs(downloadUrl, `${applicationText}.pdf`); return null; } } /** * * @param activitiesCsv: activites csv string to download * @param applicationFolder: the folder to put the csv in */ handleActivities ( activitiesCsv: string, applicationFolder: JSZip ) { if (!!activitiesCsv) { const activityText = this.i18n.translate( 'common:textActivity', {}, 'Activity' ); applicationFolder.file(activityText + '.csv', activitiesCsv); } } /** * * @param emailPdfBlob: Email PDF blob to download * @param applicationFolder: Folder to put the pdf in */ handleEmails ( emailPdfBlob: Blob, applicationFolder: JSZip ) { if (!!emailPdfBlob) { const applicationEmailText = this.i18n.translate( 'common:textApplicationEmails', {}, 'Application emails' ); applicationFolder.file(applicationEmailText + '.pdf', emailPdfBlob); } } /** * * @param tableCsvs: Table CSVs to download * @param attachmentsFolder: Folder to put the table csvs */ handleTableCsvs ( tableCsvs: CsvForDownload[], attachmentsFolder: JSZip ) { for (const tableCsv of tableCsvs) { attachmentsFolder.file(tableCsv.fileName, tableCsv.csv); } } /** * Fetches all the necessary data and downloads the zip * * @param apps: Applications to download * @param modalResponse: Bulk download modal response * @param isNomination: Is nomination */ async downloadMultipleApplicationsAsPdf ( apps: ApplicationFromPaginated[], modalResponse: AppDownloadBulkModalResponse, isNomination: boolean, applicantsAreMasked: boolean ) { const detailMap = await this.prepMultipleApplicationsForDownload( apps, modalResponse.formIds, modalResponse.program.defaultFormId, applicantsAreMasked ); let applicationsZip = this.jsZipService.createZip(); await Promise.all(apps.map(async (app) => { const detail = detailMap[app.applicationId]; const submissions = this.adaptToSubmissionsForBulk( modalResponse.groups, detail ); const applicantIsMasked = app.isMasked && applicantsAreMasked; const { appForPdf, orgForPdf, formInfos, fileUploads, tableCsvs } = await this.prepApplicationForDownload( app, submissions, app.currentWorkflowLevelName, app.workflowName, detail.tags, detail.employeeInfo, detail.reportFieldResponse, detail.appDetail.inKindItems, detail.refResponseMapByAppFormId, app.orgPhoneNumber, this.specialHandlingService.getSpecialHandling(detail.appDetail), applicantIsMasked ); const applicants = this.getApplicantsForDownload(detail.collaborators, applicantIsMasked); const appZipResult = await this.buildApplicationPDF( applicants, appForPdf, null, orgForPdf, isNomination, formInfos, fileUploads, tableCsvs, null, null, modalResponse.isMasked, this.clientSettingsService.clientBranding.name, modalResponse.program.logoUrl, modalResponse.program.defaultFormId, applicationsZip, null, false, false ); if (appZipResult) { applicationsZip = appZipResult; } })); const applicationText = this.i18n.translate( 'common:lblApplication', {}, 'Application' ); await this.pdfService.handleZipDownload( applicationsZip, `${applicationText}.zip` ); } /** * * @param collaborators: collaborators * @param isMasked: is masked? * @returns the adaptped applicants for the application */ getApplicantsForDownload ( collaborators: BulkCollaboratorResponse[], isMasked: boolean ) { if (!isMasked) { return collaborators.map((collab) => { return { name: collab.firstName + ' ' + collab.lastName, addressString: this.addressFormatter.format( { ...collab.address, address1: collab.address.address, stateProvRegCode: collab.address.state }, true ), email: collab.email, phone: collab.phoneNumber }; }); } return []; } /** * * @param application: application record * @param fromAppManager this action taken from app manager? * @param awards: Awards for download * @returns the details needed for the Download Application Modal */ async prepareDownloadApplicationModal ( application: ApplicationForUi|ApplicationFromPaginated, fromAppManager: boolean, awards: AwardFromApi[] ) { const { workflowId, workflowLevelName, workflowName } = this.getWorkflowAttrs(application); const [ applicationAwards, appForUi ] = await Promise.all([ fromAppManager ? this.awardService.getAwards(application.applicationId) : { awards }, fromAppManager ? this.applicationViewService.getApplicationDetail(application.applicationId) : application as ApplicationForUi ]); await this.workflowResolver.resolve(); const forms = await this.applicationFormService.getApplicantFormsForApplication( application.applicationId, application.applicationStatus ); const defaultForm = forms.find((form) => { return form.isDefault; }); const res = await this.fetchFormResponsesForApplicationDownload( application.applicationId, workflowId, defaultForm.formId ); return { workflowId, workflowLevelName, workflowName, defaultForm, forCurrentWfl: res.forCurrentWfl, submissions: res.submissions, refResponseMapByAppFormId: res.refResponseMapByAppFormId, applicationAwards: applicationAwards.awards, applicationForUi: appForUi }; } /** * * @param applicationId: Application ID to fetch responses for * @param workflowId: Workflow ID of the application * @param defaultFormId: Default Form ID on the program * @returns the form response info for the application */ async fetchFormResponsesForApplicationDownload ( applicationId: number, workflowId: number, defaultFormId: number ) { const canTakeActions = this.policyService.grantApplication.canManageAllApplications() || this.policyService.grantApplication.canTakeActionsOnAllApps(); const isWfManager = !!this.workflowService.myWorkflowManagerRolesMap[workflowId]; const forCurrentWfl = !canTakeActions && !isWfManager; const { forms, refResponseMapByAppFormId } = await this.getFormResponsesForDownload( applicationId, forCurrentWfl, defaultFormId ); return { forCurrentWfl, submissions: forms, refResponseMapByAppFormId }; } /** * * @param applicationId: Application ID to get form responses for * @param forCurrentWfl: Are these responses for the current WFL only? * @param defaultFormId: Default Form ID for the program * @returns the form responses */ async getFormResponsesForDownload ( applicationId: number, forCurrentWfl: boolean, defaultFormId: number ) { const funcName = forCurrentWfl ? 'getAllFormResponsesForCurrentWorkflowLevel' : 'getAllFormResponsesForApplication'; const allResponses = await this.applicationDownloadResources[funcName](applicationId); const applicationFormIds: number[] = []; const forms = allResponses.formDetails.filter((form) => { return this.filterFormBasedOnResponses(form, forCurrentWfl, applicationFormIds); }).map((form) => { return this.adaptFormWithResponses(form, forCurrentWfl, defaultFormId); }); const refResponseMapByAppFormId = this.getRefResponseMap( allResponses.applicationReferenceFieldResponses, applicationFormIds ); return { forms, refResponseMapByAppFormId }; } /** * * @param form: the form * @param forCurrentWfl: is this for the current wfl only? * @param applicationFormIds: application form ids array * @returns the filtered forms */ filterFormBasedOnResponses ( form: ApplicationViewFormFromApi, forCurrentWfl: boolean, applicationFormIds: number[] ) { const responses = [ ...form.responses, ...(forCurrentWfl ? [] : form.otherLevelResponses) ]; responses.forEach((res) => { if (!applicationFormIds.includes(res.applicationFormId)) { applicationFormIds.push(res.applicationFormId); } }); return responses.length > 0; } /** * * @param form: the form * @param forCurrentWfl: is for current wfl only? * @param defaultFormId: the default form id * @returns adapted form and responses */ adaptFormWithResponses ( form: ApplicationViewFormFromApi, forCurrentWfl: boolean, defaultFormId: number ) { const viewTranslations = this.translationService.viewTranslations; const formTranslationMap = viewTranslations.FormTranslation; const isDefaultForm = form.formId === defaultFormId; const _allResponses = form.responses.concat( forCurrentWfl ? [] : form.otherLevelResponses ); const responses = this.filterToSubmittedResponses( this.adaptFormResponseForUi( _allResponses, form.dueDate, form.formId, form.formRevisionId ), isDefaultForm ); const otherLevelResponses = this.filterToSubmittedResponses( this.adaptFormResponseForUi( form.otherLevelResponses, form.dueDate, form.formId, form.formRevisionId ), isDefaultForm ); const nominationResponses = this.filterToSubmittedResponses( this.adaptFormResponseForUi( form.nominationResponses, form.dueDate, form.formId, form.formRevisionId ), isDefaultForm ); const adapted: ApplicationViewFormForUI = { ...form, name: formTranslationMap[form.formId]?.Name ?? form.name, responses, otherLevelResponses, nominationResponses, isOverdue: this.applicationFormService.getIsFormOverdue(form.dueDate) }; return adapted; } /** * * @param referenceFieldResponses: Reference field responses to map * @param applicationFormIds: application form ids array * @returns map of reference field responses by application form ID */ getRefResponseMap ( referenceFieldResponses: ReferenceFieldAPI.ApplicationRefFieldResponse[], applicationFormIds: number[] ): Record { const sharedResponses: ReferenceFieldAPI.ApplicationRefFieldResponse[] = []; const map: Record = {}; referenceFieldResponses.forEach((res) => { const key = res.applicationFormId; if (key) { const existingResponses = map[key]; if (existingResponses) { map[key] = [ ...existingResponses, res ]; } else { map[key] = [res]; } } else { sharedResponses.push(res); } }); Object.keys(map).forEach((key) => { const responses = map[+key]; map[+key] = [ ...responses, ...sharedResponses ]; }); // Make sure all application form IDs get on the map applicationFormIds.forEach((applicationFormId) => { if (!map[applicationFormId]) { map[applicationFormId] = [ ...sharedResponses ]; } }); return map; } /** * * @param responses: Form responses * @param dueDate: Due date * @param formId: Form ID * @param revisionId: Revision ID * @returns the adapted form responses */ adaptFormResponseForUi ( responses: FormResponseFromApi[], dueDate: string, formId: number, revisionId: number ) { return responses.map((response) => { const { formDefinition } = this.formLogicService.adaptFormDefinitionForTabs( response.formDefinition, formId, revisionId ); return { ...response, formDefinition, isOverdue: this.applicationFormService.getIsFormOverdue(dueDate) }; }); } /** * * @param responses: Form responses to filter * @param isDefaultForm: Are responses for the default form? * @returns the filtered responses */ filterToSubmittedResponses ( responses: FormResponse[], isDefaultForm: boolean ): FormResponse[] { return responses.filter((response) => { return isDefaultForm || response.applicationFormStatusId === FormStatuses.Submitted; }); } /** * * @param programId: Program ID of apps to download * @param workflowId: Workflow ID of apps to download * @returns the details needed for the Bulk Download Application Modal */ async prepareBulkDownloadModal ( programId: number, workflowId: number ) { const [ program, forms, workflowDetail ] = await Promise.all([ this.programService.getProgram(programId + ''), this.workflowService.getWorkflowForms(programId), this.workflowService.getAndSetWorkflowMap(workflowId) ]); const workflowFormsMap = this.programService.constructFormsForEdit( workflowId, forms, program.defaultFormId ); const defaultForm = forms.find((_form) => { return _form.formId === program.defaultFormId; }); const levels: WorkflowLevel[] = []; workflowDetail.levels.forEach((level) => { levels.push(level); Object.keys(level).forEach((key) => { if (key === 'subLevels') { level[key].forEach((sub) => { sub.parentName = level.name; levels.push(sub); }); } }); }); const viewTranslations = this.translationService.viewTranslations; const formTranslationMap = viewTranslations.FormTranslation; const bulkDownloadGroups: BulkDownloadGroup[] = [{ isDefaultFormGroup: true, workflowLevelId: null, workflowLevelName: this.i18n.translate( 'PROGRAM:textDefaultForm', {}, 'Default form' ), parentWorkflowLevelName: '', forms: [{ ...defaultForm, formName: formTranslationMap[defaultForm.formId]?.Name, selected: true }] }]; levels.forEach((level) => { const levelForms = workflowFormsMap[level.id]; const adapted = levelForms.filter((_form) => { return !_form.isDefaultForm; }).map((_form) => { return { ..._form, formName: formTranslationMap[_form.formId]?.Name, selected: false }; }); if (adapted.length > 0) { bulkDownloadGroups.push({ workflowLevelName: level.name, workflowLevelId: level.id, parentWorkflowLevelName: level.parentName, forms: adapted, isDefaultFormGroup: false }); } }); return { program, workflowFormsMap, bulkDownloadGroups }; } /** * * @param application: Application to prepare for download * @param submissions: Form responses to prepare for download * @param isApplicantMasked: Is the applicant masked? * @param showActivityTrail: include activites in download? * @param showEmails: include emails in download? * @param isNomination: is nomination? * @returns download application helper data */ async prepSingleApplicationForDownload ( application: ApplicationFromPaginated|ApplicationForUi, submissions: SubmissionMap[], isApplicantMasked: boolean, showActivityTrail: boolean, showEmails: boolean, isNomination: boolean ) { const formDefs: FormDefinitionForUi[][] = []; const formIds: number[] = []; submissions.forEach((submission) => { submission.forms.forEach((form) => { formIds.push(form.formId); form.submissions.forEach((response) => { formDefs.push(response.formDefinition); }); }); }); let inKindItems: InKindRequestedItem[]; let specialHandling: SpecialHandling; if ('inKindItems' in application) { inKindItems = application.inKindItems; } if ('specialHandling' in application) { specialHandling = application.specialHandling; } if (showEmails) { await this.emailService.setEmails(); } const [ applicants, tags, reportFieldResponse, employeeInfo, programLogo, appDetail, org, activities, emails, nominee ] = await Promise.all([ this.collaborationService.getApplicantsForPDF( application.applicationId, isApplicantMasked ), this.systemTagsService.getCurrentTagsForPDF( application.applicationId ), this.getReportFieldResponseForMultipleForms( formDefs, application.applicationId ), this.employeeSsoFieldsService.getEmployeeSSOFieldsForApp( application.applicationId ), this.programService.getProgramLogo( application.programId ), !inKindItems || !specialHandling ? this.applicationViewService.getApplicationDetail( application.applicationId ) : null, application.nonprofitGuid && !('orgPhoneNumber' in application) ? this.nonprofitService.getNonprofitAdditionalDataByGuid( application.nonprofitGuid ) : undefined, showActivityTrail ? this.applicationActivityService.getAllAppActivity(application.applicationId) : [], showEmails ? this.exportEmailService.getApplicationEmails( application.applicationId, this.emailService.emails ) : [], isNomination ? this.applicationManagerService.getNomineeInfo( application.applicationId ) : undefined ]); await this.formHelperService.prepareComponentsForRenderForm( formDefs, uniq(formIds) ); if (!inKindItems) { inKindItems = appDetail.inKindItems; } if (!specialHandling) { specialHandling = appDetail.specialHandling; } let orgPhone = ''; if ('orgPhoneNumber' in application) { orgPhone = application.orgPhoneNumber; } else if (org) { orgPhone = org.nonprofitDetail?.displayNumber; } return { applicants, tags, reportFieldResponse, employeeInfo, programLogo, inKindItems, orgPhone, activities, emails, nominee, specialHandling }; } /** * * @param application: Application to download * @param submissions: Form responses to download * @param workflowLevelName: Workflow level name * @param workflowName: Workflow name * @param tags: Application tags * @param employeeInfo: Employee SSO Data * @param reportFieldResponse: Report field response data * @param inKindItems: In kind items for download * @param refResponseMapByAppFormId: Map with application form ID to reference field responses * @param orgPhone: org phone number * @param specialHandling: Special handling * @param isApplicantMasked: is applicant masked? * @returns helper data to complete download */ async prepApplicationForDownload ( application: ApplicationFromPaginated | ApplicationForUi, submissions: SubmissionMap[], workflowLevelName: string, workflowName: string, tags: string[], employeeInfo: EmployeeSSOFieldsData, reportFieldResponse: AdHocReportingUI.ReportResponseRow, inKindItems: InKindRequestedItem[], refResponseMapByAppFormId: Record, orgPhone: string, specialHandling: SpecialHandling, isApplicantMasked: boolean ) { const orgForPdf: OrgInfoForPDF = { name: application.organizationName, addressString: application.organizationAddress, registrationId: application.organizationIdentification, phone: orgPhone }; const appForPdf: ApplicationInfoForPDF = { appId: application.applicationId, programName: application.programName, isMasked: isApplicantMasked, amountRequested: application.amountRequested, currencyRequested: application.currencyRequested, currencyRequestedAmountEquivalent: application.currencyRequestedAmountEquivalent, inKindItems: (inKindItems || []).filter((item) => { return !!item.itemIdentification && +item.count > 0; }), specialHandling, cycleName: this.translationService.viewTranslations?.Grant_Program_Cycle?.[application.grantProgramCycle.id]?.Name ?? application.grantProgramCycle.name, status: this.statusService.applicationStatusMap?.[application.applicationStatus]?.translated ?? '', submittedDate: application.submittedDate, careOf: application.careOf, workflowLevelName, workflowName, tags, reportFieldResponse: null, employeeInfo, designation: application.designation }; const formInfos: FormInfoForPDF[] = []; const refMap: Record = {}; for (const submission of submissions) { for (const form of submission.forms) { for (const response of form.submissions) { const submittedBy = response.submittedBy?.fullName ?? ''; const wasSubmittedByApplicant = !submittedBy || submittedBy === `${application.applicantFirstName} ${application.applicantLastName}`; const dontShowApplicantName = wasSubmittedByApplicant && isApplicantMasked; let translations: SimpleStringMap = {}; let richTextTranslations: SimpleStringMap = {}; const formLang = this.formService.getDefaultLangFromFormId(form.formId); const userLang = this.userService.getCurrentUserCulture(); if (userLang !== formLang) { const { richTextMap, standardMap } = await this.translationService.getFormTranslationsByLanguage( form.formId ); translations = standardMap; richTextTranslations = richTextMap; } const formDefinition = response.formDefinition; const signature = !dontShowApplicantName ? await this.signatureService.setFormSignature( application.applicationId, response.applicationFormId, true ) : null; const formSubmittedBy = this.getFormSubmittedBy(response, dontShowApplicantName); let referenceFields = refMap[response.applicationFormId]; if (!referenceFields) { referenceFields = await this.referenceFieldsService.getReferenceFieldResponses( application.applicationId, response.applicationFormId, response.formDefinition, this.formHelperService.getTableAndSubsetIdsFromFormDefinition( [response.formDefinition] ), null, true, false, refResponseMapByAppFormId[response.applicationFormId] ); refMap[response.applicationFormId] = referenceFields; } // For download application, we don't actually display the form, so "isHidden" is not set // We need to run conditional logic to populate this on the form definition const res = this.formLogicService.initFormDefinitionLogic( formDefinition, this.getExternalFieldsFromApp( application, form, response, referenceFields, reportFieldResponse, employeeInfo, inKindItems, specialHandling ), reportFieldResponse ); const customExecutions = this.formLogicEvaluatorService.getFunctionCalls( response.formDefinition ); if (customExecutions.length > 0) { const app = this.getPartialBaseApplicationForPdf( appForPdf, referenceFields, response.reviewerRecommendedFundingAmount ); this.formLogicEvaluatorService.executeJSFunctions( customExecutions, true, true, this.formHelperService.generateDataForFormDefinition(app, response.formDefinition) ); } formDefinition.forEach((tab) => { this.formLogicService.applyComponentLogicResults( tab, res.setValueState, res.conditionalVisibilityState, res.formulaState, true, form.formAudience ); }); const formInfo: FormInfoForPDF = { formName: form.formName, formSubmittedOn: response.submittedDate, formSubmittedBy, formDefinition, formData: response.formData, formId: form.formId, formStatus: response.applicationFormStatusId, applicationFormId: response.applicationFormId, referenceFields, decision: response.decision, specialHandling, reviewerRecommendedFundingAmount: response.reviewerRecommendedFundingAmount, signature, translations, richTextTranslations }; formInfos.push(formInfo); } } } const { fileUploads, tableCsvs } = this.formHelperService.extractFilesAndTablesForPdf(formInfos); appForPdf.reportFieldResponse = reportFieldResponse; return { appForPdf, orgForPdf, formInfos, fileUploads, tableCsvs }; } /** * Gets a partial base app for PDF * * @param application: the application * @param referenceFields: the reference field responses * @param reviewerRecommendedFundingAmount: reviewer recommended funding * @returns the partial base application needed for pdf */ getPartialBaseApplicationForPdf ( application: ApplicationInfoForPDF, referenceFields: ReferenceFieldsUI.RefResponseMap, reviewerRecommendedFundingAmount: number ): Partial { return { applicationId: application.appId, amountRequested: +application.amountRequested, currencyRequestedAmountEquivalent: application.currencyRequestedAmountEquivalent, currencyRequested: application.currencyRequested, careOf: application.careOf, specialHandling: application.specialHandling, reviewerRecommendedFundingAmount, referenceFields, reportFieldResponse: application.reportFieldResponse }; } /** * * @param response: the form response * @param dontShowApplicantName: dont show if it's masked * @returns the form submitted by string */ getFormSubmittedBy ( response: FormResponse, dontShowApplicantName: boolean ) { let formSubmittedBy = ''; if (!dontShowApplicantName) { if (response.submittedBy?.firstName || response.submittedBy?.fullName) { formSubmittedBy = response.submittedBy.fullName || `${response.submittedBy.firstName} ${response.submittedBy.lastName}`; } else if (response.createdBy?.firstName) { formSubmittedBy = `${response.createdBy.firstName} ${response.createdBy.lastName}`; } } return formSubmittedBy; } /** * * @param appDetailMap: Application Details per app * @param formIds: Form IDs we are downloading */ async prepCdtsForBulkDownload ( appDetailMap: Record, formIds: number[] ) { const formDefMap: Record = {}; const formDefs: FormDefinitionForUi[][] = []; Object.keys(appDetailMap).forEach((key) => { const appDetail = appDetailMap[+key]; appDetail.forms.forEach((form) => { form.responses.forEach((response) => { const uniqueId = `${form.formId}_${form.formRevisionId}`; if (!formDefMap[uniqueId]) { formDefMap[uniqueId] = response.formDefinition; formDefs.push(response.formDefinition); } }); }); }); await this.formHelperService.prepareComponentsForRenderForm( formDefs, formIds ); } /** * * @param application: application record * @param form: form record * @param response: form response record * @param referenceFields: reference fields for application * @param reportFieldResponse: report field response for application * @param employeeInfo: employee info for application * @param inKindItems: in kind items * @param specialHandling: Special handling * @returns BaseApplication model */ getExternalFieldsFromApp ( application: ApplicationFromPaginated | ApplicationForUi, form: WorkflowLevelSubmissionForm, response: FormResponse, referenceFields: ReferenceFieldsUI.RefResponseMap, reportFieldResponse: AdHocReportingUI.ReportResponseRow, employeeInfo: EmployeeSSOFieldsData, inKindItems: InKindRequestedItem[], specialHandling: SpecialHandling ): BaseApplication { return { applicationId: application.applicationId, formType: null, applicationFormId: response.applicationFormId, formId: form.formId, revisionId: response.formRevisionId, amountRequested: application.amountRequested, amountRequestedForEdit: application.currencyRequestedAmountEquivalent, currencyRequestedAmountEquivalent: application.currencyRequestedAmountEquivalent, designation: application.designation, currencyRequested: application.currencyRequested, careOf: application.careOf, specialHandling, programId: application.programId, reviewerRecommendedFundingAmount: response.reviewerRecommendedFundingAmount, referenceFields, reportFieldResponse, employeeInfo, decision: response.decision, inKindItems }; } /** * * @param formDefs: Form definitions we are downloading * @param applicationId: Application ID we are downloading * @returns report field response data */ async getReportFieldResponseForMultipleForms ( formDefs: FormDefinitionForUi[][], applicationId: number ): Promise { const someFormContainsReportFields = formDefs.some((formDef) => { return this.reportFieldService.formContainsReportField(formDef); }); if (someFormContainsReportFields) { return this.reportFieldService.getReportFieldResponse( applicationId ); } return null; } /** * * @param apps: Applications for download * @param formIds: Form IDs for download * @param defaultFormId: Default Form ID on the program * @param isMasked: is the applicant masked? * @returns Detail map by application ID */ async prepMultipleApplicationsForDownload ( apps: ApplicationFromPaginated[], formIds: number[], defaultFormId: number, isMasked: boolean ) { const appDetailMap = await this.getAppDetailMap( apps, formIds, defaultFormId, isMasked ); await this.prepCdtsForBulkDownload( appDetailMap, formIds ); return appDetailMap; } /** * * @param apps: Applications for map * @param formIds: Form IDs we are downloadinig * @param defaultFormId: Default form ID on the program * @param isMasked: if the applicant is masked * @returns Detail map by application ID */ async getAppDetailMap ( apps: ApplicationFromPaginated[], formIds: number[], defaultFormId: number, isMasked: boolean ) { const appIds = apps.map((app) => app.applicationId); const [ collaborators, applicationTagMap, employeeInfo, reportFieldResponses, signatures, appDetails ] = await Promise.all([ !isMasked ? this.collaborationService.fetchCollaboratorsBulk(appIds) : [], this.systemTagsService.getTagsBulk(appIds), this.employeeSsoFieldsService.getEmployeeSsoFieldsBulk(appIds), this.reportFieldService.getReportFieldResponseBulk(appIds), this.signatureService.getSignaturesBulk(appIds, formIds), this.applicationManagerService.getBulkApplicationDetails(appIds) ]); const appDetailMap: Record = {}; for (const app of apps) { const { forms, refResponseMapByAppFormId } = await this.getFormResponsesForDownload( app.applicationId, false, defaultFormId ); appDetailMap[app.applicationId] = { defaultFormId, forms: forms.filter((form) => { return formIds.includes(form.formId); }), collaborators: collaborators.filter((collab) => { return collab.applicationId === app.applicationId; }), tags: applicationTagMap[ app.applicationId ], employeeInfo: employeeInfo.find((info) => { return info.applicationId === app.applicationId; }), reportFieldResponse: reportFieldResponses.find((res) => { return +res.application.id === app.applicationId; }), signatures: signatures.filter((item) => { return item.applicationId === app.applicationId; }), appDetail: appDetails.find((detail) => { return detail.applicationId === app.applicationId; }), application: app, refResponseMapByAppFormId }; } return appDetailMap; } /** * * @param groups: Workflow Level Groups with selected forms * @param appDetail: Detail map by application ID * @returns the adapted responses for download */ adaptToSubmissionsForBulk ( groups: BulkDownloadGroup[], appDetail: AppDetailForDownload ): SubmissionMap[] { const submissions: SubmissionMap[] = []; groups.forEach((group) => { const forms: WorkflowLevelSubmissionForm[] = []; group.forms.forEach((workflowLevelForm) => { // Find all the forms that match this formId / workflowLevelId combo appDetail.forms.forEach((formWithResponse) => { if (workflowLevelForm.formId === formWithResponse.formId) { let responses: FormResponse[] = []; if (group.isDefaultFormGroup) { const found = formWithResponse.responses[0]; if (found) { responses = [found]; } } else { responses = formWithResponse.responses.filter((response) => { return response.workflowLevelId === group.workflowLevelId; }); } if (responses.length > 0) { const adaptedForm: WorkflowLevelSubmissionForm = { formId: workflowLevelForm.formId, formName: workflowLevelForm.formName, formAudience: this.formService.getAudienceFromFormType( workflowLevelForm.formType ), submittedOn: '', // Only used in download PDF modal submissions: responses, selected: true }; const alreadyHaveThisForm = forms.find((form) => { return form.formId === workflowLevelForm.formId; }); if (!alreadyHaveThisForm) { forms.push(adaptedForm); } } } }); if (forms.length > 0) { submissions.push({ parentWorkflowLevelName: group.parentWorkflowLevelName, workflowLevelName: group.workflowLevelName, workflowLevelId: group.workflowLevelId, forms }); } }); }); return submissions; } }