import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { SpinnerService } from '@core/services/spinner.service'; import { EmailTemplate } from '@core/typings/program.typing'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { FormAudience } from '@features/configure-forms/form.typing'; import { CopyEmailModalComponent } from '@features/system-emails/copy-email-modal/copy-email-modal.component'; import { ClientEmailTemplateFromAPI, Email, EmailForUI, EmailNotificationType, EmailTemplateCopyForAPI, EmailUsageTypes, ProgramEmailTemplateForUI, SimpleEmail } from '@features/system-emails/email.typing'; import { ALL_SKIP_FILTER, AutoTableRepository, AutoTableRepositoryFactory, TopLevelFilter, TypeSafeFormBuilder, TypeSafeFormGroup, ValueComparisonService } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { ConfirmationModalComponent, ModalFactory } from '@yourcause/common/modals'; import { NotifierService } from '@yourcause/common/notifier'; import { Subscription } from 'rxjs'; import { EmailResources } from '../email.resources'; import { EmailService } from '../email.service'; import { ToggleActivateSystemEmailModalComponent } from '../toggle-activate-system-email-modal/toggle-activate-system-email-modal.component'; import { ViewEmailModalComponent } from '../view-email-modal/view-email-modal.component'; interface EmailGroup { [x: string]: any; } @Component({ selector: 'gc-emails-accordion', templateUrl: './emails-accordion.component.html', styleUrls: ['./emails-accordion.component.scss'] }) export class EmailsAccordionComponent implements OnInit, OnDestroy { @Input() isSystem: boolean; @Input() isNomination = false; @Input() activeProgramMap: { [x: string]: number; }; @Input() disabledProgramMap: { [x: string]: boolean; }; @Input() programId: number; @Output() onSetCopyAsDefault = new EventEmitter(); @Output() onSetCopyActive = new EventEmitter(); @Output() onNewProgramEmail = new EventEmitter(); @Output() onToggleEmailActive = new EventEmitter<{ type: EmailNotificationType; isExcluded: boolean; }>(); ccBccHeader = this.i18n.translate( 'GLOBAL:textCCBCC', {}, 'CC/BCC' ); FormAudience = FormAudience; rowsPerPage = 10; loading = false; repository: AutoTableRepository; UsageTypes = EmailUsageTypes; formGroup: TypeSafeFormGroup; topLevelFilters = [ new TopLevelFilter( 'text', 'subject', '', this.i18n.translate( 'GLOBAL:textSearchByEmailSubjectOrName', {}, 'Search by email subject or name' ), undefined, undefined, [{ column: 'subject', filterType: 'cn' }, { column: 'emailNumber', filterType: 'cn' }] ), new TopLevelFilter( 'typeaheadSingleEquals', 'audienceType', ALL_SKIP_FILTER, undefined, { selectOptions: [{ display: this.i18n.translate( 'common:textAllAudiences', {}, 'All audiences' ), value: ALL_SKIP_FILTER }, { display: this.i18n.translate( 'common:lblApplicant', {}, 'Applicant' ), value: FormAudience.APPLICANT }, { display: this.i18n.translate( 'GLOBAL:textGrantManager', {}, 'Grant manager' ), value: FormAudience.MANAGER }] }, this.i18n.translate( 'common:textAudience', {}, 'Audience' ) ), new TopLevelFilter( 'typeaheadSingleEquals', 'active', ALL_SKIP_FILTER, undefined, { selectOptions: [{ display: this.i18n.translate( 'common:textAllCopies', {}, 'All copies' ), value: ALL_SKIP_FILTER }, { display: this.i18n.translate( 'common:textActiveCopies', {}, 'Active copies' ), value: true }] }, this.i18n.translate( 'common:textActiveCopies', {}, 'Active copies' ) ) ]; sub = new Subscription(); drilldownOpenMap: { [i: string]: boolean; } = {}; programMap: { [type: string]: { [id: string]: ProgramEmailTemplateForUI[]; }; } = {}; hasInternational = this.clientSettingsService.clientSettings.hasInternational; hasLangs = !this.clientSettingsService.noSelectedLanguages; canReserveFunds = this.clientSettingsService.clientSettings.reserveFunds; drilldownCopyRows: (ClientEmailTemplateFromAPI|ProgramEmailTemplateForUI)[] = []; currentDrilldownType: EmailNotificationType; activeStatus = ALL_SKIP_FILTER; constructor ( private logger: LogService, private i18n: I18nService, private modalFactory: ModalFactory, private emailResources: EmailResources, private notifier: NotifierService, private spinnerService: SpinnerService, private autoTableFactory: AutoTableRepositoryFactory, private notifierService: NotifierService, private valueComparisonService: ValueComparisonService, private formBuilder: TypeSafeFormBuilder, private emailService: EmailService, private clientSettingsService: ClientSettingsService ) { this.sub.add(this.emailService.changesTo$('templateMap').subscribe(() => { this.setProgramTemplates(); })); } get templateMap () { return this.emailService.templateMap; } ngOnInit () { const rows = this.getRows(); this.formGroup = this.formBuilder.group( this.topLevelFilters.reduce((formGroup, filter) => ({ ...formGroup, [filter.column]: [filter.value] }), {}) ); this.repository = this.autoTableFactory.create({ key: this.isSystem ? 'SYSTEM_EMAILS' : ( this.isNomination ? 'NOMINATION_EMAILS' : 'PROGRAM_EMAILS' ), columns: [], notifier: this.notifierService, rowsPerPage: this.isSystem ? this.rowsPerPage : 500, valueComparisonService: this.valueComparisonService, rows }); this.sub.add(this.repository.loading.subscribe((val) => { this.loading = val; })); } getRows () { let rows: EmailForUI[]; if (this.isSystem) { rows = this.emailService.emails.filter((email) => { if (!this.canReserveFunds) { return email.emailNotificationType !== EmailNotificationType.ApplicationAutomaticallyDeclined; } return true; }).map((email) => { return { ...email, emailNumber: `GC-${email.emailNotificationType}`, active: true }; }); } else { rows = this.emailService.emails.filter((email) => { let passes = false; if (this.isNomination) { passes = email.emailUsageType !== EmailUsageTypes.GrantProgram; } else { passes = email.emailUsageType !== EmailUsageTypes.NominationProgram; } let passesReserveFunds = true; if (!this.canReserveFunds) { passesReserveFunds = email.emailNotificationType !== EmailNotificationType.ApplicationAutomaticallyDeclined; } return passes && (email.emailUsageType !== EmailUsageTypes.System) && !email.isDisabled && passesReserveFunds; }).map((email) => { return { ...email, emailNumber: `GC-${email.emailNotificationType}`, active: true // this is so the top level filter retuns all emails. // We then use this filter value to drive the drilldown copies table }; }); } return rows; } onTopLevelFilterChange (filter: TopLevelFilter) { if (filter.column === 'active') { this.activeStatus = filter.value; if (this.currentDrilldownType) { this.setDrilldownCopyRows(this.currentDrilldownType); } } } setProgramTemplates () { const programTemplates: { [type: string]: { [id: string]: ProgramEmailTemplateForUI[]; }; } = {}; Object.keys(this.templateMap).forEach((type) => { const templates = this.templateMap[type].programTemplates || {}; programTemplates[type] = programTemplates[type] || {}; Object.keys(templates).forEach((id) => { programTemplates[type][id] = templates[id] || []; }); }); this.programMap = programTemplates; if (this.currentDrilldownType) { this.setDrilldownCopyRows(this.currentDrilldownType); } } getProgramCopies (type: EmailNotificationType) { const templateDetail = this.templateMap[type]; if (templateDetail && templateDetail.programTemplates) { return templateDetail.programTemplates[this.programId] || []; } return []; } getType (email: EmailForUI|ClientEmailTemplateFromAPI) { return this.emailService.getType(email); } getSimpleEmail ( email: EmailForUI|ClientEmailTemplateFromAPI, isEdit = false ): SimpleEmail { const type = this.getType(email); const found = this.emailService.emails.find((item) => { return item.emailNotificationType === type; }); return { id: (email as ClientEmailTemplateFromAPI).id, subject: email.subject, emailNumber: email.emailNumber, emailNotificationType: type, audienceType: found.audienceType, title: email.title || found.title, isEdit, body: (email as ClientEmailTemplateFromAPI).body || this.templateMap[found.emailNotificationType].template, description: email.description, ccEmails: (email as ClientEmailTemplateFromAPI).ccEmails || [], bccEmails: (email as ClientEmailTemplateFromAPI).bccEmails || [], attachments: (email as ClientEmailTemplateFromAPI).emailAttachments || [], allowCarbonCopyUpdates: (email as ClientEmailTemplateFromAPI).allowCarbonCopyUpdates, allowDocumentTemplates: found.allowDocumentTemplates, supportsCarbonCopy: found.supportsCarbonCopy, documentTemplates: (email as ClientEmailTemplateFromAPI).documentTemplates || [] }; } getSimpleProgramEmail ( email: ProgramEmailTemplateForUI ): SimpleEmail { const found = this.emailService.emails.find((item) => { return item.emailNotificationType === email.clientEmailTemplate.emailNotificationTypeId; }); return { id: email.clientEmailTemplateId, subject: email.clientEmailTemplate.subject, emailNumber: email.clientEmailTemplate.emailNumber, emailNotificationType: email.clientEmailTemplate.emailNotificationTypeId, audienceType: found.audienceType, title: email.clientEmailTemplate.title || found.title, isEdit: true, body: email.clientEmailTemplate.body, description: email.clientEmailTemplate.description, ccEmails: email.clientEmailTemplate.ccEmails || [], bccEmails: email.clientEmailTemplate.bccEmails || [], attachments: email.clientEmailTemplate.attachments || [], allowCarbonCopyUpdates: email.clientEmailTemplate.allowCarbonCopyUpdates, allowDocumentTemplates: found.allowDocumentTemplates, supportsCarbonCopy: found.supportsCarbonCopy, documentTemplates: email.clientEmailTemplate.documentTemplates || [] }; } async setTemplateMap (row: EmailForUI) { this.spinnerService.startSpinner(); const type = this.getType(row); try { await this.emailService.setTemplateMap(type); if (!this.isSystem) { await this.emailService.setProgramTemplatesByType( this.programId, type ); } } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'GLOBAL:textErrorLoadingEmail', {}, 'There was an error loading the copies' )); } this.spinnerService.stopSpinner(); } async resetAll (type: EmailNotificationType, isCopy: boolean) { await this.emailService.resetAll( type, isCopy, this.programId ); } setAsDefault (template: ClientEmailTemplateFromAPI, email: Email) { const copies = this.getProgramCopies(email.emailNotificationType); const index = copies.findIndex((copy) => { return copy.id === template.id; }); const newTemplate = { emailNotificationType: email.emailNotificationType, templates: copies.map((copy, idx) => { return { id: copy.id, grantProgramId: this.programId, clientEmailTemplateId: copy.clientEmailTemplateId, active: copy.active, default: index === idx ? true : false }; }) }; this.onSetCopyAsDefault.emit(newTemplate); this.updateProgramMap(newTemplate); } updateProgramMap (newTemplate: EmailTemplate) { const type = newTemplate.emailNotificationType; this.programMap[type][this.programId].forEach((temp) => { const found = newTemplate.templates.find((newTemp) => { return newTemp.id === temp.id; }); if (found) { temp.active = found.active; temp.default = found.default; } }); } async toggleActivate ( row: EmailForUI|ClientEmailTemplateFromAPI, isCopy = false, email?: EmailForUI ) { if (this.programId && isCopy) { this.handleProgramToggleActivate(row as ClientEmailTemplateFromAPI, email); } else { let isDisabled = false; if (this.programId) { isDisabled = this.disabledProgramMap[ (row as Email).emailNotificationType ]; } else { isDisabled = isCopy ? !(row as ClientEmailTemplateFromAPI).active : (row as Email).isDisabled; } const activateText = isDisabled ? 'Activate' : 'Deactivate'; const isActivate = activateText === 'Activate'; let confirmText = ''; const modalHeader = this.i18n.translate( isActivate ? 'GLOBAL:hdrActivateEmail' : 'GLOBAL:textDeactivateEmail', {}, isActivate ? 'Activate Email' : 'Deactivate Email' ); const primaryButtonText = this.i18n.translate( isActivate ? 'GLOBAL:textActivate' : 'GLOBAL:btnDeactivate', {}, isActivate ? 'Activate' : 'Deactivate' ); if (isActivate) { confirmText = this.i18n.translate( this.programId ? 'GLOBAL:textAreYourSureActivateProgram' : 'GLOBAL:textAreYourSureActivate', {}, this.programId ? 'Are you sure you want to activate this email for the current program?' : 'Are you sure you want to activate this email?' ); } else { confirmText = this.i18n.translate( this.programId ? 'GLOBAL:textAreYourSureDeactivateProgram' : 'GLOBAL:textAreYourSureDeactivate', {}, this.programId ? 'Are you sure you want to deactivate this email for the current program?' : 'Are you sure you want to deactivate this email?' ); } let emailsInUse: (ClientEmailTemplateFromAPI | ProgramEmailTemplateForUI)[]; let emailsInUseByPrograms: (ClientEmailTemplateFromAPI | ProgramEmailTemplateForUI)[]; if ('emailNotificationType' in row && !isCopy && !isActivate) { await this.setTemplateMap(row); emailsInUse = this.setDrilldownCopyRows(row.emailNotificationType); emailsInUseByPrograms = emailsInUse.filter((copy) => { if('grantPrograms' in copy) { return copy.grantPrograms.length > 0; } else { return false; } }); } const deps = { confirmText, modalHeader, primaryButtonText, copyEmailRows: emailsInUseByPrograms, isActivate }; const proceed = await this.modalFactory.open( ToggleActivateSystemEmailModalComponent, deps ); if (proceed) { if (this.programId) { this.onToggleEmailActive.emit({ type: (row as Email).emailNotificationType, isExcluded: !isDisabled }); } else { await this.handleActivateModal( row, isCopy, activateText ); } }; } } async handleProgramToggleActivate ( row: ClientEmailTemplateFromAPI, email: EmailForUI ) { if ( !row.active && email.maxCopies === 1 ) { const deps = { confirmText: this.i18n.translate( 'GLOBAL:textAreYourSureOneActiveTemplate', {}, 'Are you sure you want to make this template active? Only one template of this type can be active for the program. Doing so will make the other copies inactive.' ), modalHeader: this.i18n.translate( 'GLOBAL:hdrActivateEmail', {}, 'Activate Email' ), confirmButtonText: this.i18n.translate( 'GLOBAL:textActivate', {}, 'Activate' ) }; const proceed = await this.modalFactory.open( ConfirmationModalComponent, deps ); if (proceed) { this.doProgramToggleActivate(row, email, true); } } else { this.doProgramToggleActivate(row, email, false); } } doProgramToggleActivate ( template: ClientEmailTemplateFromAPI, email: Email, oneActive: boolean ) { const copies = this.getProgramCopies(email.emailNotificationType); const index = copies.findIndex((copy) => { return copy.id === template.id; }); const newTemplate = { emailNotificationType: email.emailNotificationType, templates: copies.map((copy, idx) => { const active = oneActive ? index === idx : index === idx ? !template.active : copy.active; return { id: copy.id, grantProgramId: this.programId, clientEmailTemplateId: copy.clientEmailTemplateId, active, default: oneActive && active ? true : oneActive && !active ? false : copy.default }; }) }; this.onSetCopyActive.emit(newTemplate); this.updateProgramMap(newTemplate); } async handleActivateModal ( row: EmailForUI|ClientEmailTemplateFromAPI, isCopy: boolean, activateText: 'Activate'|'Deactivate' ) { this.spinnerService.startSpinner(); const isActivate = activateText === 'Activate'; try { if (isCopy) { await this.emailService.toggleActivateClientEmail( (row as ClientEmailTemplateFromAPI).id, activateText === 'Activate' ); } else { await this.emailResources.toggleActivateEmail( (row as EmailForUI).emailNotificationType ); } this.notifier.success(this.i18n.translate( isActivate ? 'GLOBAL:textSuccessActivateEmail' : 'GLOBAL:textSuccessDeactivateEmail', {}, `Successfully ${ activateText === 'Activate' ? 'activated' : 'deactivated' } the email` )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( isActivate ? 'GLOBAL:textErrorActivateEmail' : 'GLOBAL:textErrorDeactivateEmail', {}, `There was an error ${ activateText === 'Activate' ? 'activating' : 'deactivating' } the email` )); } await this.resetAll(this.getType(row), isCopy); this.repository.rows = this.getRows(); this.spinnerService.stopSpinner(); } viewProgramEmail (email: ProgramEmailTemplateForUI) { this.viewEmail(this.getSimpleProgramEmail(email), true); } async viewNonProgramEmail ( email: EmailForUI|ClientEmailTemplateFromAPI, isCopy = false ) { await this.emailService.setTemplateMap(this.getType(email)); this.viewEmail(this.getSimpleEmail(email), isCopy); } async viewEmail (email: SimpleEmail, isCopy = false) { const deps = { email, isCopy, programId: this.programId, editEmailsArea: true }; await this.modalFactory.open( ViewEmailModalComponent, deps, { backdrop: true, class: 'modal-xl' } ); } editCopyForProgram (email: ProgramEmailTemplateForUI, isTranslation = false) { if (isTranslation) { this.copyModal(this.getSimpleProgramEmail(email), true, true); } else { this.copyModal(this.getSimpleProgramEmail(email), true); } } async copyModalForProgram (email: EmailForUI) { if (!this.templateMap[email.emailNotificationType]) { await this.setTemplateMap(email); } const simpleEmail = this.getSimpleEmail(email); this.copyModal(simpleEmail); } async copyModalForNonProgram ( email: EmailForUI|ClientEmailTemplateFromAPI, isEdit = false, isTranslation = false ) { await this.emailService.setTemplateMap(this.getType(email)); if (isTranslation) { this.copyModal( this.getSimpleEmail(email, isEdit), isEdit, true ); } else { this.copyModal( this.getSimpleEmail(email, isEdit), isEdit ); } } async copyModal (email: SimpleEmail, isEdit = false, isTranslation = false) { const deps = { email, isTranslation }; const copyModalReturn = await this.modalFactory.open( CopyEmailModalComponent, deps ); if (copyModalReturn) { this.spinnerService.startSpinner(); if (isTranslation) { await this.emailService.handleBulkAddTranslationToClientEmail( email, copyModalReturn ); } else { const copy = copyModalReturn.templates[0]; // Grab the attachments that are uploaded already const attachments = copyModalReturn.attachments; const attachmentArray = await this.emailService.returnIdsFromMixedAttachments( attachments, null, email.id ); const copyPayload: EmailTemplateCopyForAPI = { ...copy, attachments: attachmentArray }; if (isEdit) { copyPayload.id = email.id; } const id = await this.emailService.handleCreateOrUpdateCopy(copyPayload); if (this.programId) { const type = copyPayload.emailNotificationTypeId; if (isEdit) { await this.updateProgramOnMap(id, type, copyPayload); } else { this.addNewCopyForProgram(type, id, email); } } else { await this.resetAll(email.emailNotificationType, true); this.repository.rows = this.getRows(); } } this.spinnerService.stopSpinner(); } } async updateProgramOnMap ( id: number, type: EmailNotificationType, copyPayload: EmailTemplateCopyForAPI ) { const foundProgramTempIndex = this.programMap[type][this.programId].findIndex((item) => { return item.clientEmailTemplateId === copyPayload.id; }); const foundProgramTemp = this.programMap[type][this.programId][foundProgramTempIndex]; foundProgramTemp.clientEmailTemplate.body = copyPayload.body; foundProgramTemp.clientEmailTemplate.title = copyPayload.title; foundProgramTemp.clientEmailTemplate.subject = copyPayload.subject; foundProgramTemp.clientEmailTemplate.description = copyPayload.description; foundProgramTemp.clientEmailTemplate.documentTemplates = copyPayload.documentTemplates; foundProgramTemp.clientEmailTemplate.ccEmails = copyPayload.ccEmails; foundProgramTemp.clientEmailTemplate.bccEmails = copyPayload.bccEmails; foundProgramTemp.clientEmailTemplate.allowCarbonCopyUpdates = copyPayload.allowCarbonCopyUpdates; // Update Program template with updated attachments array const updatedAttachments = await this.emailService.getUpdatedAttachments( type, id ); foundProgramTemp.clientEmailTemplate.attachments = updatedAttachments; this.programMap = { ...this.programMap, [type]: { ...this.programMap[type], [this.programId]: [ ...this.programMap[type][this.programId].slice(0, foundProgramTempIndex), foundProgramTemp, ...this.programMap[type][this.programId].slice(foundProgramTempIndex + 1) ] } }; } async addNewCopyForProgram ( type: EmailNotificationType, id: number, email: SimpleEmail ) { const templates = await this.emailService.getAdaptedProgramTempsByEmailType( this.programId, type ); const found = templates.find((temp) => { return temp.clientEmailTemplateId === id; }); if (found) { const programCopies = this.programMap[type][this.programId]; const programTemplates = [ ...programCopies, found ]; this.programMap[type][this.programId] = programTemplates; const clientTemplates = await this.emailResources.getClientTemplatesForEmail( type ); this.emailService.setTemplateMapOnState({ ...this.templateMap[type], clientTemplates, programTemplates: { ...this.templateMap[type].programTemplates, [this.programId]: programTemplates } }, type); const newTemplate = { emailNotificationType: email.emailNotificationType, templates: programTemplates.map((copy) => { return { id: copy.id, grantProgramId: this.programId, clientEmailTemplateId: copy.clientEmailTemplateId, active: copy.active, default: copy.default }; }) }; this.onNewProgramEmail.emit(newTemplate); } } async toggleDrilldown (row: EmailForUI, open = true) { const type = row.emailNotificationType; this.currentDrilldownType = type; this.drilldownOpenMap[type] = open; if (open) { Object.keys(this.drilldownOpenMap).forEach((key) => { if (+key !== type) { this.drilldownOpenMap[key] = false; } }); } await this.setTemplateMap(row); this.setDrilldownCopyRows(this.currentDrilldownType); } setDrilldownCopyRows (emailNotificationType: EmailNotificationType) { let copies: (ClientEmailTemplateFromAPI|ProgramEmailTemplateForUI)[] = []; if (this.isSystem) { copies = this.templateMap[emailNotificationType]?.clientTemplates; } else { copies = this.programMap[emailNotificationType] ? this.programMap[emailNotificationType][this.programId] : []; } this.drilldownCopyRows = (copies || []).filter((copy) => { if (this.activeStatus === ALL_SKIP_FILTER) { return true; } else { return copy.active === true; } }); return this.drilldownCopyRows; } ngOnDestroy () { this.sub.unsubscribe(); } }