import { Injectable } from '@angular/core'; import { environment } from '@environment'; import { ArrayHelpersService, AutoTableRepositoryFactory, Base64, documentEditorStyles, FileService, PaginatedResponse, PaginationOptions, TemplateMarginsForUI } 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 { uniq } from 'lodash'; import moment from 'moment'; import { DocumentTemplateResources } from './document-template.resources'; import { DocumentTemplateState } from './document-template.state'; import { BulkFetchTemplateFileTokenPayload, DocumentTemplateForUi, DocumentTemplateFromApi, ExportDocumentTemplate, FetchTemplateFileTokenPayload, TemplateMarginsFromAPI, URLSasTokenObj } from './document-template.typing'; @AttachYCState(DocumentTemplateState) @Injectable({ providedIn: 'root' }) export class DocumentTemplateService extends BaseYCService { defaultMargin = 1; constructor ( private logger: LogService, private documentTemplateResources: DocumentTemplateResources, private i18n: I18nService, private notifier: NotifierService, private autoTableFactory: AutoTableRepositoryFactory, private fileService: FileService, private arrayHelper: ArrayHelpersService ) { super(); } get detailMap () { return this.get('detailMap'); } get documentTemplates () { return this.get('documentTemplates'); } get documentTemplateOptions () { return this.get('documentTemplateOptions'); } getDocumentTemplatesPaginated ( paginationOptions: PaginationOptions ): Promise> { return this.documentTemplateResources.getDocumentTemplatesPaginated( paginationOptions ); } findDocumentTemplate (templateId: number) { return this.documentTemplates.find((temp) => { return temp.id === templateId; }); } async setDocumentTemplates () { if (!this.documentTemplates) { const options: PaginationOptions = { returnAll: true, retrieveTotalRecordCount: false, filterColumns: [], sortColumns: [{ columnName: 'id', sortAscending: false }], rowsPerPage: 1000000, pageNumber: 0 }; const results = await this.getDocumentTemplatesPaginated(options); this.set('documentTemplates', this.adaptDocumentTemplates(results.records)); this.setDocumentTemplateOptions(); } } setDocumentTemplateOptions () { const options = this.documentTemplates.map((temp) => { return { label: temp.name, value: temp.id }; }); this.set('documentTemplateOptions', this.arrayHelper.sort(options, 'label')); } resetDocumentTemplates () { this.set('documentTemplates', undefined); } resetTemplateTable () { const repo = this.autoTableFactory.getRepository('DOCUMENT_TEMPLATES'); if (repo) { repo.reset(); } } adaptDocumentTemplates ( records: DocumentTemplateFromApi[] ): DocumentTemplateForUi[] { return records.map((template) => { return { ...template, statusText: this.i18n.translate( 'GLOBAL:textCreatedByDynamic', { user: template.createdBy?.firstName + ' ' + template.createdBy?.lastName, date: moment(template.createdDate).format('ll') }, 'Created by __user__ on __date__' ) }; }); } async handleCreateDocumentTemplate (name: string) { try { const id = await this.documentTemplateResources.createDocumentTemplate(name); this.notifier.success(this.i18n.translate( 'CONFIG:textSuccessfullyCreatedDocTemp', {}, 'Successfully created the document template' )); this.resetDocumentTemplates(); return id; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorCreatingDocTemp', {}, 'There was an error creating the document template' )); return null; } } adaptTemplateHtmlString (templateHtml: string) { // Removes a weird character we were seeing when a token was used as an img src return (templateHtml || '').replace( new RegExp(String.fromCharCode(8203), 'g'), '' ); } attachStyleTag (templateHtml: string, templateMargins: TemplateMarginsForUI) { const el = document.createElement('div'); el.innerHTML = templateHtml; const styleTag = document.createElement('style'); styleTag.textContent = documentEditorStyles(templateMargins); el.appendChild(styleTag); return el.innerHTML; } async handleUpdateDocumentTemplate ( id: number, name: string, templateHtml: string, margins: TemplateMarginsForUI ) { const adaptedMargins = this.adaptTemplateMarginsForAPI(margins); try { await this.documentTemplateResources.updateDocumentTemplate( id, name, this.adaptTemplateHtmlString(templateHtml), adaptedMargins ); this.notifier.success(this.i18n.translate( 'CONFIG:textSuccessfullyUpdatedDocTemp', {}, 'Successfully updated the document template' )); this.resetDocumentTemplates(); this.resetDocumentTemplateDetail(id); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorUpdatingDocTemp', {}, 'There was an error updating the document template' )); } } async handleDeleteTemplate (id: number) { try { await this.documentTemplateResources.deleteDocumentTemplate(id); this.notifier.success(this.i18n.translate( 'CONFIG:textSuccessfullyDeletedDocTemp', {}, 'Successfully deleted the document template' )); this.resetDocumentTemplates(); return true; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorDeletingDocTemp', {}, 'There was an error deleting the document template' )); return false; } } async handleCopyTemplate (id: number): Promise { try { await this.setDocumentTemplateDetail(id); const detail = this.detailMap[id]; const copyName = detail.name + ' Copy'; const newId = await this.documentTemplateResources.createDocumentTemplate( copyName ); await this.documentTemplateResources.updateDocumentTemplate( newId, copyName, detail.templateHtml, this.adaptTemplateMarginsForAPI(detail.margins) ); this.resetDocumentTemplates(); this.notifier.success(this.i18n.translate( 'CONFIG:textSuccessfullyCopiedDocTemp', {}, 'Successfully copied the document template' )); return newId; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorCopyingDocTemp', {}, 'There was an error copying the document template' )); return null; } } extractGCFileURLS (templateHtml: string): string[] { const bodyDiv = document.createElement('div'); bodyDiv.innerHTML = templateHtml; const images = bodyDiv.querySelectorAll('img'); const imgURLs: string[] = []; images.forEach((img) => { if (img.src.includes(environment.fileUploadBases)) { imgURLs.push(img.src.split('?')[0]); } }); return uniq(imgURLs); } attachTokensToImages (templateHtml: string, tokenObj: URLSasTokenObj) { const bodyDiv = document.createElement('div'); bodyDiv.innerHTML = templateHtml; const images = bodyDiv.querySelectorAll('img'); images.forEach((img) => { if (img.src.includes(environment.fileUploadBases)) { const imgBaseURL = img.src.split('?')[0]; const adaptedSource = imgBaseURL + (tokenObj.urlsAndSasTokens[imgBaseURL] ? tokenObj.urlsAndSasTokens[imgBaseURL] : ''); img.src = adaptedSource; } }); return bodyDiv.innerHTML; } async adaptTemplateImages (templateHtml: string, templateId: number) { const URLs = this.extractGCFileURLS(templateHtml); const tokenObj = await this.bulkFetchTokensForTemplateFiles({URLs, templateId}); const adaptedTemplate = this.attachTokensToImages(templateHtml, tokenObj); return adaptedTemplate; } async setDocumentTemplateDetail (id: number) { if (!this.detailMap[id]) { const detail = await this.documentTemplateResources.getDocumentTemplateDetail( id ); const templateWithSASTokens = await this.adaptTemplateImages(detail.templateHtml, detail.id); const margins = this.adaptTemplateMarginsForUI(detail.margins); this.set('detailMap', { ...this.detailMap, [id]: { ...detail, templateHtml: templateWithSASTokens, margins } }); } } adaptTemplateMarginsForUI (margins: TemplateMarginsFromAPI): TemplateMarginsForUI { // margins are treated as numbers in the UI return { top: +(margins.top || this.defaultMargin), bottom: +(margins.bottom || this.defaultMargin), left: +(margins.left || this.defaultMargin), right: +(margins.right || this.defaultMargin) }; } adaptTemplateMarginsForAPI (margins: TemplateMarginsForUI): TemplateMarginsFromAPI { // margins are stored as strings in the database return { top: '' + margins.top, bottom: '' + margins.bottom, left: '' + margins.left, right: '' + margins.right }; } resetDocumentTemplateDetail (id: number) { this.set('detailMap', { ...this.detailMap, [id]: undefined }); } async importDocumentTemplates (templates: ExportDocumentTemplate[]) { try { await this.documentTemplateResources.importDocumentTemplates(templates); this.resetDocumentTemplates(); this.notifier.success(this.i18n.translate( 'CONFIG:notificationSuccessfullyImportedDocTemps', {}, 'Successfully imported the document templates' )); return true; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:notificationErrorImportingDocTemps', {}, 'There was an error importing the document templates' )); return false; } } async uploadFileAndReturnWithToken (file: File, templateId: number): Promise { const URL = await this.uploadDocumentTemplateFile(file); const tokenFetchPayload = { URL, templateId }; const token = await this.fetchTokenForTemplateFile(tokenFetchPayload); return URL + token; } async fetchTokenForTemplateFile (payload: FetchTemplateFileTokenPayload) { try { const token = this.documentTemplateResources.getTemplateFileSASToken(payload); return token; } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'common:textUnableToLoadImages', {}, 'Unable to load images' ) ); return null; } } async bulkFetchTokensForTemplateFiles (payload: BulkFetchTemplateFileTokenPayload) { try { const tokenRecords = this.documentTemplateResources.getBulkTemplateFileSASTokens(payload); return tokenRecords; } catch (e) { this.logger.error(e); return null; } } async uploadDocumentTemplateFile (file: File): Promise { try { const URL = await this.documentTemplateResources.uploadFile(file); this.notifier.success( this.i18n.translate( 'common:textSuccessfullyUploadedFile', {}, 'Successfully uploaded file' ) ); return URL; } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'common:textErrorUploadingImage', {}, 'There was an error uploading the selected image' ) ); return null; } } async exportDocumentTemplates (templateIds: number[]) { try { const templates = await this.documentTemplateResources.exportDocumentTemplates( templateIds ); this.fileService.downloadRaw( Base64.encode(JSON.stringify(templates)), `doc_temp_export_${moment().format('YYYYMMDDHHmmss')}.bin` ); this.notifier.success(this.i18n.translate( 'CONFIG:textSuccessfullyExportedSelectedDocumentTemplates', {}, 'Successfully exported the selected document templates' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'CONFIG:textErrorExportingSelectedDocumentTemplates', {}, 'There was an error exporting the selected document templates' )); } } }