import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { ApplicantFromSearch } from '@core/typings/applicant.typing'; import { BaseEmailOptionsModel, EmailNotificationType, EmailOptionsModelForSave } from '@features/system-emails/email.typing'; import { ArrayHelpersService, AutoTableRepositoryFactory, FileService, PaginationOptions, TypeaheadSelectOption } 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 * as parse from 'papaparse'; import { DistributionListImportValidationModel } from './distribution-list-import-modal/distribution-list-import-modal.component'; import { InvitationResources } from './invitation.resources'; import { InvitationState } from './invitation.state'; import { AvailableApplicant, DistributionListApplicant, DistributionListRecord, InvitationDetailResponse, InvitationRecord, InvitationResponse, InvitationStatus, ScheduleInvitee, ScheduleRecord, SendInviteIndividualForSave, SendInviteListForSave, SendInviteListModalResponse } from './invitation.typing'; @AttachYCState(InvitationState) @Injectable({ providedIn: 'root' }) export class InvitationService extends BaseYCService { reminderEmailType = EmailNotificationType.InvitationReminder; invitationEmail = EmailNotificationType.InvitationToApply; inviteStatusOptions: TypeaheadSelectOption[] = [{ label: this.i18n.translate( 'GLOBAL:textSent', {}, 'Sent' ), value: InvitationStatus.Sent }, { label: this.i18n.translate( 'GLOBAL:textScheduled', {}, 'Scheduled' ), value: InvitationStatus.Scheduled }]; constructor ( private logger: LogService, private invitationResources: InvitationResources, private fileService: FileService, private i18n: I18nService, private notifier: NotifierService, private autoTableFactory: AutoTableRepositoryFactory, private arrayHelper: ArrayHelpersService ) { super(); } get distributionLists () { return this.get('distributionLists'); } async resetDistributionListData () { await this.resetAllDistributionLists(); const repo = this.autoTableFactory.getRepository('DISTRIBUTION_LISTS'); if (repo) { repo.reset(); } } resetInvitationListRepo () { const repo = this.autoTableFactory.getRepository('INVITATIONS'); if (repo) { repo.reset(); } } resetScheduleListRepo () { const repo = this.autoTableFactory.getRepository('SCHEDULES'); if (repo) { repo.reset(); } } resetDistributionListDetailRepos ( distributionListId: number ) { const listRepo = this.getDistributionListRepo(distributionListId); if (listRepo) { listRepo.reset(); } const applicantRepo = this.getAvailableApplicantsListRepo(distributionListId); if (applicantRepo) { applicantRepo.reset(); } } getDistributionListRepoKey (distributionListId: number) { return `DISTRIBUTION_LIST_APPLICANTS_${distributionListId}`; } getDistributionListRepo (distributionListId: number) { const repo = this.autoTableFactory.getRepository( this.getDistributionListRepoKey(distributionListId) ); return repo; } getAvailableApplicantsRepoKey (distributionListId: number) { return `AVAILABLE_APPLICANTS_${distributionListId}`; } getAvailableApplicantsListRepo (distributionListId: number) { const repo = this.autoTableFactory.getRepository( this.getAvailableApplicantsRepoKey(distributionListId) ); return repo; } async getInvitations ( paginationOptions: PaginationOptions ) { const result = await this.invitationResources.getInvitations(paginationOptions); result.records.forEach((record) => { switch (record.invitationStatus) { case InvitationStatus.Scheduled: record.inviteStatusText = this.i18n.translate( 'PROGRAM:textScheduledForDate', { date: moment(record.scheduledDate).format('ll') }, 'Scheduled for __date__' ); break; case InvitationStatus.Sent: record.inviteStatusText = this.i18n.translate( 'PROGRAM:textInviteSentOnDate', { date: moment(record.sentDate).format('ll') }, 'Invite sent on __date__' ); break; default: record.inviteStatusText = ''; } if (record.applicationCreatedDate) { record.openedText = this.i18n.translate( 'PROGRAM:textOpenedOnDate', { date: moment(record.applicationCreatedDate).format('ll') }, 'Opened on __date__' ); } if (record.reminderDate) { record.reminderSentText = this.i18n.translate( 'PROGRAM:textReminderSentOnDate', { date: moment(record.reminderDate).format('ll') }, 'Reminder sent on __date__' ); } }); return result; } getSchedules ( paginationOptions: PaginationOptions ) { return this.invitationResources.getSchedules(paginationOptions); } async handleResendInvitation ( invitationId: number, emailOptionsModel: BaseEmailOptionsModel ) { try { await this.invitationResources.resendInvitation(invitationId, emailOptionsModel); this.notifier.success(this.i18n.translate( 'PROGRAM:textSuccessResendingInvitation', {}, 'Successfully resent the invitation' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorResendingInvitation', {}, 'There was an error resending the invitation' )); } } async handleInvitationReminderModal ( invitationId: number, clientEmailTemplateId: number, emailOptionsModel: EmailOptionsModelForSave ) { try { await this.invitationResources.sendInvitationReminder( invitationId, clientEmailTemplateId, emailOptionsModel ); this.notifier.success(this.i18n.translate( 'PROGRAM:textSuccessSendInvitationReminder', {}, 'Successfully sent the invitation reminder' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorSendingInvitationReminder', {}, 'There was an error sending the invitation reminder' )); } } resetAllDistributionLists () { this.set('distributionLists', undefined); return this.setAllDistributionLists(); } async setAllDistributionLists () { if (!this.distributionLists) { const lists = await this.invitationResources.getAllDistributionLists(); this.set('distributionLists', this.arrayHelper.sort(lists, 'name')); } } async handleUpdateSchedule ( response: SendInviteListModalResponse, scheduleId: number ) { try { await this.invitationResources.updateSchedule( response, scheduleId ); this.notifier.success(this.i18n.translate( 'PROGRAM:textSuccessUpdatedTheSchedule', {}, 'Successfully updated the schedule' )); this.resetInvitationListRepo(); this.resetScheduleListRepo(); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorUpdatingTheSchedule', {}, 'There was an error updating the schedule' )); } } async handleSendInvitationToIndividual ( response: SendInviteIndividualForSave ) { const applicant = response.selectedApplicant; try { if (applicant.id) { await this.invitationResources.sendInvitationToExistingApplicant({ grantProgramId: response.programId, grantProgramCycleId: response.cycleId, clientEmailTemplateId: response.clientEmailTemplateId || null, emailOptionsModel: response.emailOptionsModel, applicantId: applicant.id, firstName: applicant.firstName, lastName: applicant.lastName, email: applicant.email }); } else { await this.invitationResources.sendInvitationToNewApplicant({ grantProgramId: response.programId, grantProgramCycleId: response.cycleId, clientEmailTemplateId: response.clientEmailTemplateId || null, emailOptionsModel: response.emailOptionsModel, firstName: applicant.firstName.trim(), lastName: applicant.lastName.trim(), email: applicant.email, isEmployeeOfClient: applicant.isEmployeeOfClient }); } this.resetInvitationListRepo(); this.notifier.success(this.i18n.translate( 'PROGRAM:textSuccessSentInvitation', {}, 'Successfully sent the invitation' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorSendingInvitation', {}, 'There was an error sending the invitation' )); } } async handleSendInvitationToList ( response: SendInviteListForSave ) { try { const payload = { distributionListId: response.distributionListId, grantProgramId: response.programId, grantProgramCycleId: response.cycleId, clientEmailTemplateId: response.clientEmailTemplateId || null, emailOptionsModel: response.emailOptionsModel, scheduledDate: response.sendNow ? null : response.scheduledDate }; await this.invitationResources.sendInvitationToDistributionList( payload ); this.notifier.success(this.i18n.translate( 'PROGRAM:textSuccessSentInvitationToDistributionList', {}, 'Successfully sent the invitation to the distribution list' )); this.resetInvitationListRepo(); this.resetScheduleListRepo(); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorSendingInvitationToTheDistributionList', {}, 'There was an error sending the invitation to the distribution list' )); } } async handleCreateOrUpdateDistributionList ( name: string, description: string, distributionListId?: number ) { const isUpdate = !!distributionListId; try { const id = await this.invitationResources.createOrUpdateDistributionList( name, description, distributionListId ); await this.resetDistributionListData(); this.notifier.success(this.i18n.translate( isUpdate ? 'PROGRAM:textSuccessUpdateDistributionList' : 'PROGRAM:textSuccessCreateDistributionList', {}, isUpdate ? 'Successfully updated the distribution list' : 'Successfully created the distribution list' )); return id; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( isUpdate ? 'PROGRAM:textErrorUpdatingDistributionList' : 'PROGRAM:textErrorCreatingDistributionList', {}, isUpdate ? 'There was an error updating the distribution list' : 'There was an error creating the distribution list' )); return null; } } getDistributionLists ( paginationOptions: PaginationOptions ) { return this.invitationResources.getDistributionLists(paginationOptions); } async handleCopyDistributionList (id: number) { try { const list = this.distributionLists.find((item) => { return +id === item.id; }); const newId = await this.invitationResources.createOrUpdateDistributionList( list.name + ' Copy', list.description ); const paginationOptions: PaginationOptions = { rowsPerPage: 1000, pageNumber: 1, sortColumns: [], filterColumns: [], orFilterColumns: [], retrieveTotalRecordCount: true, returnAll: true }; const response = await this.getDistributionListApplicants( id, paginationOptions ); const applicantIds = response.records.map((item) => { return item.applicantId; }); await this.handleAddApplicantsToList(newId, applicantIds); this.notifier.success(this.i18n.translate( 'PROGRAM:textSuccessCopyingDistributionList', {}, 'Successfully copied the distribution list' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorCopyingDistributionList', {}, 'There was an error copying the distribution list' )); } } getDistributionListApplicants ( id: number, paginationOptions: PaginationOptions ) { return this.invitationResources.getDistributionListApplicants( id, paginationOptions ); } async handleRemoveApplicantsFromList ( distributionListId: number, distributionListApplicantIds: number[] ) { try { await this.invitationResources.removeApplicantsFromList( distributionListId, distributionListApplicantIds ); this.notifier.success(this.i18n.translate( distributionListApplicantIds.length === 1 ? 'PROGRAM:textSuccessRemovingApplicantFromList' : 'PROGRAM:textSuccessRemovingApplicantsFromList', {}, distributionListApplicantIds.length === 1 ? 'Successfully removed the applicant from the distribution list' : 'Successfully removed the applicants from the distribution list' )); await this.resetDistributionListData(); this.resetDistributionListDetailRepos(distributionListId); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( distributionListApplicantIds.length === 1 ? 'PROGRAM:textErrorRemovingApplicantFromList' : 'PROGRAM:textErrorRemovingApplicantsFromList', {}, distributionListApplicantIds.length === 1 ? 'There was an error removing the applicant from the distribution list' : 'There was an error removing the applicants from the distribution list' )); } } async handleAddApplicantsToList ( distributionListId: number, applicantIds: number[] ) { try { await this.invitationResources.addExistingApplicantsToList( distributionListId, uniq(applicantIds) ); this.notifier.success(this.i18n.translate( applicantIds.length === 1 ? 'PROGRAM:textSuccessAddingApplicantToList' : 'PROGRAM:textSuccessAddingApplicantsToList', {}, applicantIds.length === 1 ? 'Successfully added the applicant to the distribution list' : 'Successfully added the applicants to the distribution list' )); await this.resetDistributionListData(); this.resetDistributionListDetailRepos(distributionListId); } catch (err) { const e = err as HttpErrorResponse; this.logger.error(e); if ( e.error.message === 'Internal applicant already exists on this distribution list.' ) { this.notifier.error(this.i18n.translate( 'PROGRAM:textCannotAddApplicantThatAlreadyExistsOnTheList', {}, 'The applicant selected already exists on the distribution list' )); } else { this.notifier.error(this.i18n.translate( applicantIds.length === 1 ? 'PROGRAM:textErrorAddingApplicantToList' : 'PROGRAM:textErrorAddingApplicantsToList', {}, applicantIds.length === 1 ? 'There was an error adding the applicant to the distribution list' : 'There was an error adding the applicants to the distribution list' )); } } } async handleAddNewApplicantToList ( distributionListId: number, applicant: ApplicantFromSearch ) { try { await this.invitationResources.addNewApplicantToList( distributionListId, applicant ); this.notifier.success(this.i18n.translate( 'PROGRAM:textSuccessAddingApplicantToList', {}, 'Successfully added the applicant to the distribution list' )); await this.resetDistributionListData(); this.resetDistributionListDetailRepos(distributionListId); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorAddingApplicantToList', {}, 'There was an error adding the applicant to the distribution list' )); } } getAvailableApplicants ( distributionListId: number, options: PaginationOptions, programId: number ) { return this.invitationResources.getAvailableApplicants( distributionListId, options, programId ); } async handleAddApplicantModal ( distributionListId: number, applicant: ApplicantFromSearch ) { if (applicant.id) { await this.handleAddApplicantsToList( distributionListId, [applicant.id] ); } else { await this.handleAddNewApplicantToList( distributionListId, applicant ); } } async handleAddAvailableApplicants ( distributionListId: number ) { const repo = this.getAvailableApplicantsListRepo(distributionListId); if (repo) { try { const paginationOptions = repo.getPaginationOptions(); const response = this.extractProgramIdFromOptions( paginationOptions ); const result = await this.getAvailableApplicants( distributionListId, { ...response.options, returnAll: true }, response.programId ); const applicantIds = result.records.map((row) => { return row.applicant.applicantId; }); await this.handleAddApplicantsToList( distributionListId, uniq(applicantIds) ); } catch (e) { this.logger.error(e); this.addAvailableApplicantsError(); } } else { this.addAvailableApplicantsError(); } } addAvailableApplicantsError () { this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorAddingAvailableApplicants', {}, 'There was an error adding available applicants' )); } async handleClearDistributionList ( distributionListId: number ) { const repo = this.getDistributionListRepo(distributionListId); if (repo) { try { const paginationOptions = repo.getPaginationOptions(); const result = await this.getDistributionListApplicants( distributionListId, { ...paginationOptions, returnAll: true } ); const distributionListApplicantIds = result.records.map((record) => { return record.distributionListApplicantId; }); await this.handleRemoveApplicantsFromList( distributionListId, uniq(distributionListApplicantIds) ); } catch (e) { this.logger.error(e); this.clearDistributionListError(); } } else { this.clearDistributionListError(); } } clearDistributionListError () { this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorClearingDistributionList', {}, 'There was an error clearing the distribution list' )); } getScheduleInvitees ( paginationOptions: PaginationOptions, scheduleId: number, distributionListId: number ) { return this.invitationResources.getScheduleInvitees( paginationOptions, scheduleId, distributionListId ); } async deleteSchedule (id: number) { try { await this.invitationResources.deleteSchedule(id); this.resetInvitationListRepo(); this.resetScheduleListRepo(); this.notifier.success(this.i18n.translate( 'PROGRAM:textSuccessDeletingSchedule', {}, 'Successfully deleted the schedule' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorDeletingSchedule', {}, 'There was an error deleting the schedule' )); } } async handleDeleteDistributionList ( distributionListId: number ) { try { await this.invitationResources.deleteDistributionList( distributionListId ); this.notifier.success(this.i18n.translate( 'PROGRAM:textSuccessfullyDeletedDistributionList', {}, 'Successfully deleted the distribution list' )); await this.resetDistributionListData(); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorDeletingDistributionList', {}, 'There was an error deleting the distribution list' )); } } extractProgramIdFromOptions ( options: PaginationOptions ) { let programId: number; options = { ...options, filterColumns: options.filterColumns.filter((col) => { if (col.columnName === 'grantProgramId') { programId = col.filters[0].filterValue as number; return false; } return true; }) }; return { options, programId }; } async getInvitationDetail ( invitationId: number ): Promise { if (invitationId) { try { const detail = await this.invitationResources.getInvitationInfo(invitationId); return { detail, response: InvitationResponse.Pass }; } catch (err) { const e = err as HttpErrorResponse; if (e?.error?.message === 'Invitation used.') { return { detail: null, response: InvitationResponse.Invitation_Used }; } else { this.logger.error(e); return { detail: null, response: InvitationResponse.Fail }; } } } else { return null; } } /** * * @param distributionListId: Distribution List ID * @returns applicants on this list */ async downloadTemplateForImportDistributionList ( distributionListId: number ) { const applicants = await this.invitationResources.exportDistributionList(distributionListId); const adapted = applicants.map((applicant) => { return { 'First Name': applicant.firstName, 'Last Name': applicant.lastName, Email: applicant.email, 'Is Employee': applicant.isEmployee }; }); if (adapted.length > 0) { const csv = this.parse(adapted); this.fileService.downloadCSV(csv); } else { const input = 'First Name,Last Name,Email,Is Employee'; this.fileService.downloadString( input, 'text/csv', 'template.csv' ); } } /** * * @param applicants: Applicants to parse * @returns csv string */ parse (applicants: DistributionListImportValidationModel[]) { return parse.unparse(applicants); } /** * Imports the distribution list * * @param distributionListId: Distribution List ID */ async importDistributionList ( distributionListId: number, file: Blob ) { try { await this.invitationResources.importDistributionList( distributionListId, file ); this.notifier.success(this.i18n.translate( 'common:textSuccessImportDistList', {}, 'Successfully imported the distribution list' )); this.resetDistributionListDetailRepos(distributionListId); return true; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'common:textErrorImportingDistList', {}, 'There was an error importing the distribution list' )); return false; } } }