import { Injectable } from '@angular/core'; import { ApplicationFileService } from '@core/services/application-file.service'; import { TranslationService } from '@core/services/translation.service'; import { CommunicationsService } from '@features/communications/communications.service'; import { CommunicationVisibility } from '@features/communications/communications.typing'; import { DocumentTemplateService } from '@features/document-templates/document-template.service'; import { DistributeOptions } from '@features/document-templates/document-template.typing'; import { FileService, SimpleStringMap, TypeaheadSelectOption, YcFile } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import moment from 'moment'; import printJS from 'print-js'; import { ApplicationAttachmentResources } from './application-attachment.resources'; import { ApplicationAttachmentState } from './application-attachments.state'; import { ApplicationAttachmentForUI, ApplicationAttachmentFromApi, ApplicationAttachmentRecord, AttachmentType, CommFileInfo, EmailFileInfo, ExternalFileModalResponse, ExternalOrMergeFileInfo, FormFileInfo, MailMergeModalResponse, MergeDocumenteModalResponse, OpenCommunicationFileParams } from './application-attachments.typing'; @AttachYCState(ApplicationAttachmentState) @Injectable({ providedIn: 'root' }) export class ApplicationAttachmentService extends BaseYCService { constructor ( private logger: LogService, private applicationAttachmentResources: ApplicationAttachmentResources, private translationService: TranslationService, private i18n: I18nService, private notifier: NotifierService, private documentTemplateService: DocumentTemplateService, private fileService: FileService, private applicationFileService: ApplicationFileService, private communicationsService: CommunicationsService ) { super(); } getAttachmentTypeMap (): SimpleStringMap { return { [AttachmentType.EMAIL]: this.i18n.translate( 'GLOBAL:textEmailAttachment', {}, 'Email attachment' ), [AttachmentType.EXTERNAL]: this.i18n.translate( 'GLOBAL:textExternalFile', {}, 'External file' ), [AttachmentType.FORM]: this.i18n.translate( 'GLOBAL:textFormAttachment', {}, 'Form attachment' ), [AttachmentType.MERGE]: this.i18n.translate( 'GLOBAL:textMergeDocument', {}, 'Merge document' ), [AttachmentType.COMMUNICATION]: this.i18n.translate( 'GLOBAL:textCommunication', {}, 'Communication' ) }; } getDocumentVisibilityOptions ( isNomination = false ): TypeaheadSelectOption[] { return [ { label: this.i18n.translate( isNomination ? 'GLOBAL:textAllGrantManagerOnNomCanViewDoc' : 'GLOBAL:textAllGrantManagerOnAppCanViewDoc', {}, isNomination ? 'All grant managers on the nomination can view the document' : 'All grant managers on the application can view the document' ), value: CommunicationVisibility.ALL_GRANT_MANAGERS }, { label: this.i18n.translate( 'GLOBAL:textOnlyGrantManagerInWFLCanViewDoc', {}, 'Only grant manager in the current workflow level can view the document' ), value: CommunicationVisibility.MANAGERS_IN_LEVEL }, { label: this.i18n.translate( 'GLOBAL:textOnlyICanViewTheDocument', {}, 'Only I can view the document' ), value: CommunicationVisibility.ONLY_ME } ]; } async getApplicationAttachments ( applicationId: number ): Promise { const response = await this.applicationAttachmentResources.getApplicationAttachments( applicationId ); return this.adaptApplicationAttachmentsForUI(response); } async getMatchingCommRecordForAttachment ( fileUploadId: number, appId: number ) { const communicationRecords = await this.communicationsService.getCommunicationsForApplication(appId, false); const matchingCommRecord = communicationRecords .reduce((total, curr) => { return [ ...total, ...curr ]; }, []) .filter((comm) => !!comm?.files) .find((comm) => { return comm.files.some((file) => { return file.fileUploadId === fileUploadId; }); }); return matchingCommRecord; } filterFileInfoToUniq ( fileInfo: (CommFileInfo|FormFileInfo)[], fileUploadIds: number[] ) { return fileInfo.filter((file) => { if (!!file.fileUploadId) { if (!fileUploadIds.includes(file.fileUploadId)) { fileUploadIds.push(file.fileUploadId); return true; } } return false; }); } adaptApplicationAttachmentsForUI (response: ApplicationAttachmentFromApi) { const fileUploadIds: number[] = []; response.formFileInfo = response.formFileInfo.map((info) => { return { ...info, fileInfo: this.filterFileInfoToUniq(info.fileInfo, fileUploadIds), multiValueFileInfo: this.filterFileInfoToUniq( info.multiValueFileInfo, fileUploadIds ) }; }); const attachments: ApplicationAttachmentRecord[] = [ ...response.communicationFileInfo, ...response.emailFileInfo, ...response.formFileInfo.reduce((acc, formFileInfo) => { if (formFileInfo.multiValueFileInfo.length === 0) { return [ ...acc, formFileInfo ]; } else { return [ ...acc, { ...formFileInfo, fileInfo: [ ...formFileInfo.fileInfo, ...formFileInfo.multiValueFileInfo ] } ]; } }, []), ...response.externalFileInfo, ...response.mergeDocumentFileInfo ]; const viewTranslations = this.translationService.viewTranslations; const formTranslationMap = viewTranslations.FormTranslation; return attachments.map((attachment) => { const { statusDate, statusText, statusTooltip } = this.getStatusInfoForAttachment(attachment); let formName: string; let formId: number; let applicantCanView = false; let canRemoveAttachment = false; const attachmentType = attachment.attachmentType; switch (attachmentType) { case AttachmentType.EXTERNAL: case AttachmentType.MERGE: applicantCanView = (attachment as ExternalOrMergeFileInfo).applicantCanView; canRemoveAttachment = (attachment as ExternalOrMergeFileInfo).canRemoveAttachment; if (attachmentType === AttachmentType.MERGE) { const templateName = (attachment as ExternalOrMergeFileInfo).documentTemplateName; attachment.fileInfo.forEach((file) => { file.fileName = templateName ? `${templateName}.pdf` : file.fileName; }); } break; case AttachmentType.FORM: formId = (attachment as FormFileInfo).formId; const map = formTranslationMap[formId]; formName = map?.Name ?? (attachment as FormFileInfo).formName; break; } return { attachmentId: attachment.attachmentId, attachmentType: attachment.attachmentType, emailNotificationType: 'emailNotificationType' in attachment ? attachment.emailNotificationType : null, emailSentDate: 'emailSentDate' in attachment ? attachment.emailSentDate : null, formId, formName, documentVisibility: 'documentVisibility' in attachment ? attachment.documentVisibility : null, applicantCanView, uploadedDate: 'uploadedDate' in attachment ? attachment.uploadedDate : null, uploadedBy: 'uploadedBy' in attachment ? attachment.uploadedBy : null, canRemoveAttachment, statusDate, statusText, statusTooltip, fileInfo: attachment.fileInfo }; }); } getStatusInfoForAttachment ( attachment: ApplicationAttachmentRecord ) { let statusDate = ''; let statusText = ''; let statusTooltip = ''; const uploadedBy = attachment.fileInfo[0]?.uploadedBy; switch (attachment.attachmentType) { case AttachmentType.EMAIL: statusDate = (attachment as EmailFileInfo).emailSentDate; statusText = this.i18n.translate( 'GLOBAL:textEmailSentOnDate', { date: moment(statusDate).format('ll') }, 'Email sent on __date__' ); break; case AttachmentType.EXTERNAL: case AttachmentType.FORM: case AttachmentType.COMMUNICATION: statusDate = attachment.fileInfo[0].uploadedDate; statusText = this.i18n.translate( 'GLOBAL:textUploadedByUserOnDate', { userName: `${uploadedBy?.firstName} ${uploadedBy?.lastName}`, date: moment(statusDate).format('ll') }, 'Uploaded by __userName__ on __date__' ); statusTooltip = uploadedBy?.impersonatedBy; break; case AttachmentType.MERGE: statusDate = attachment.fileInfo[0].uploadedDate; statusText = this.i18n.translate( 'GLOBAL:textAddedByUserOnDate', { userName: `${uploadedBy?.firstName} ${uploadedBy?.lastName}`, date: moment(statusDate).format('ll') }, 'Added by __userName__ on __date__' ); statusTooltip = uploadedBy?.impersonatedBy; break; } return { statusDate, statusText, statusTooltip }; } async handleRemoveAttachment ( fileUploadIds: number[], applicationId: number, attachmentType: AttachmentType ) { try { await Promise.all([ fileUploadIds.map(async (uploadId: number) => { await this.applicationAttachmentResources.removeAttachment( uploadId, applicationId, attachmentType ); }) ]); this.notifier.success(this.i18n.translate( 'GLOBAL:textSuccessfullyRemovedAttachment', {}, 'Successfully removed the attachment' )); return true; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'GLOBAL:textErrorRemovingAttachment', {}, 'There was an error removing the attachment' )); return false; } } async handleAttachExternalFile ( modalResponse: ExternalFileModalResponse, applicationId: number ) { try { await this.applicationAttachmentResources.attachExternalFileToApplication( modalResponse.selectedFile, applicationId, modalResponse.documentVisibility, modalResponse.applicantCanView ); this.notifier.success(this.i18n.translate( 'GLOBAL:textSuccessfullyAddedTheExternalFile', {}, 'Successfully added the external file' )); return true; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'GLOBAL:textErrorAddingExternalFile', {}, 'There was an error adding the external file' )); return false; } } async handleAttachMergeDocument ( modalResponse: MergeDocumenteModalResponse, applicationId: number ) { try { await this.applicationAttachmentResources.attachMergeDocumentToApplication( modalResponse.documentTemplateId, applicationId, modalResponse.documentVisibility, modalResponse.applicantCanView ); this.notifier.success(this.i18n.translate( 'GLOBAL:textSuccessfullyAddedTheMergeDocument', {}, 'Successfully added the merge document' )); return true; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'GLOBAL:textErrorAddingMergeDocument', {}, 'There was an error adding the merge document' )); return false; } } async downloadFileForApplicant ( fileUploadId: number, fileName: string ) { const url = await this.applicationAttachmentResources.getFileAccessUrlForApplicant( fileUploadId ); await this.fileService.downloadUrlAs(url, fileName); } async downloadFileForManager ( fileUploadId: number, applicationId: number, attachmentType: AttachmentType, fileName: string ) { try { const url = await this.getAccessUrlForManager( fileUploadId, applicationId, attachmentType ); await this.fileService.downloadUrlAs(url, fileName); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorOpeningFile', {}, 'There was an error opening the file' )); } } getAccessUrlForManager ( fileUploadId: number, applicationId: number, attachmentType: AttachmentType ) { return this.applicationAttachmentResources.getFileAccessUrlForManager( fileUploadId, applicationId, attachmentType ); } async handleMailMergeModalResponse ( response: MailMergeModalResponse, isNomination = false ) { const isPrint = response.distributeOptionId === DistributeOptions.PRINT; const isDownload = response.distributeOptionId === DistributeOptions.DOWNLOAD; if (isPrint || isDownload) { const accessURL = await this.attachMergeDocumentToApplicationBulk( response, isNomination ); const foundTemplate = this.documentTemplateService.findDocumentTemplate( response.documentTemplateId ); if (accessURL) { if (isPrint) { await this.printSelectedMergeDocuments( accessURL, foundTemplate.name ); } else { await this.fileService.downloadUrlAs(accessURL, `${foundTemplate.name}.pdf`); } } else { this.notifier.error(this.i18n.translate( isPrint ? 'CONFIG:textErrorPrintingPDF' : 'CONFIG:textErrorDownloadingTheFile', {}, isPrint ? 'There was an error printing the PDF' : 'There was an error downloading the file' )); } } else { await this.handleAttachAndSendDocument(response); } } isPrintJsSupported () { const userAgent = navigator.userAgent; return userAgent.includes('Chrome') || userAgent.includes('Opera') || userAgent.includes('Safari'); } async printSelectedMergeDocuments ( accessURL: string, documentTitle: string ) { try { return printJS({ printable: accessURL, type: 'pdf', documentTitle: `${documentTitle}.pdf` }); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorPrintingTheSelectedMergeDocument', {}, 'There was an error printing the selected merge document' )); } } async attachMergeDocumentToApplicationBulk ( response: MailMergeModalResponse, isNomination = false ): Promise { try { const accessURL = await this.applicationAttachmentResources.attachMergeDocumentToApplicationBulk( response ); return accessURL; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( isNomination ? 'CONFIG:textErrorAttachingMergeDocsToNoms' : 'CONFIG:textErrorAttachingMergeDocsToApps', {}, isNomination ? 'There was an error attaching the merge document to the selected nominations' : 'There was an error attaching the merge document to the selected applications' )); return null; } } async handleAttachAndSendDocument (response: MailMergeModalResponse) { try { await this.applicationAttachmentResources.sendMailMergeBulk( response ); this.notifier.success(this.i18n.translate( 'GLOBAL:textSuccessBulkMailMerge', {}, 'Successfully attached and sent the merge document' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'GLOBAL:textErrorBulkMailMerge', {}, 'There was an error attaching and sending the merge document' )); } } getMergePreview ( templateId: number, applicationId: number ): Promise { try { return this.applicationAttachmentResources.getMergePreview( templateId, applicationId ); } catch (e) { this.logger.error(e); return null; } } async tryOpenReferenceFileUpload (file: YcFile) { const url = file?.fileUrl; await this.tryOpenReferenceFieldFromUrl(url); } async tryOpenReferenceFieldFromUrl (url: string) { try { if (url) { const details = this.applicationFileService.breakDownloadUrlDownToObject(url); await this.applicationFileService.openFile( +details.applicationId, +details.fileId, +details.applicationFormId ); } } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorDownloadingTheFile', {}, 'There was an error downloading the file' )); } } async downloadReferenceFileUpload (file: YcFile) { const url = file?.fileUrl; await this.doDownloadReferenceFile(url); } async doDownloadReferenceFile (url: string) { try { if (url) { const details = this.applicationFileService.breakDownloadUrlDownToObject(url); await this.applicationFileService.downloadFile( +details.applicationId, +details.fileId, details.fileName, +details.applicationFormId ); } } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorDownloadingTheFile', {}, 'There was an error downloading the file' )); } } async openFileForManager (params: OpenCommunicationFileParams): Promise { try { const accessUrl = await this.getAccessUrlForManager( params.file.fileUploadId, params.applicationId, AttachmentType.COMMUNICATION ); const blob = await this.fileService.getBlob(accessUrl) as File; const blobUrl = this.fileService.convertFileToUrl(blob); window.open(blobUrl, '_blank'); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorOpeningFile', {}, 'There was an error opening the file' )); } } async openCommunicationFile (params: OpenCommunicationFileParams): Promise { if (params.isForNonprofit) { await this.communicationsService.openFileForNonprofitCommunication( params.joinCommunicationId, params.file.fileUploadId ); } else { await this.openFileForManager(params); } } async downloadUrlAs ( url: string, fileName: string ) { await this.fileService.downloadUrlAs(url, fileName); } /** * * @param params used for determining how we fetch the file */ async downloadCommunicationFile (params: OpenCommunicationFileParams): Promise { if (params.isForNonprofit) { await this.communicationsService.downloadAccessUrlForNonprofitCommunication( params.joinCommunicationId, params.file.fileUploadId, params.file.fileName ); } else { await this.downloadFileForManager( params.file.fileUploadId, params.applicationId, AttachmentType.COMMUNICATION, params.file.fileName ); } } }