import { Component, Input, OnInit } from '@angular/core'; import { AbstractControl, ValidatorFn, Validators } from '@angular/forms'; import { TimeZoneService } from '@core/services/time-zone.service'; import { TranslationService } from '@core/services/translation.service'; import { ApplicantFromSearch } from '@core/typings/applicant.typing'; import { Program, ProgramTypes } from '@core/typings/program.typing'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { CyclesService } from '@features/cycles/cycles.service'; import { ProgramService } from '@features/programs/program.service'; import { ArrayHelpersService, DuplicateCheckValidator, EmailValidator, GenericFile, TypeaheadSelectOption, TypeSafeFormBuilder, TypeSafeFormGroup } from '@yourcause/common'; import { AnalyticsService, EventType } from '@yourcause/common/analytics'; import { I18nService } from '@yourcause/common/i18n'; import { YCModalComponent } from '@yourcause/common/modals'; import moment from 'moment'; import { InvitationService } from '../invitation.service'; import { ScheduleRecord, SendInviteListWithOptions, SendInviteModalResponseWithAttachments } from '../invitation.typing'; interface SendInviteGroup { distributionListId: number; programId: number; cycleId: number; sendNow: boolean; scheduledDate: string|moment.Moment; clientEmailTemplateId: number; attachments: GenericFile[]; cc: string[]; bcc: string[]; } @Component({ selector: 'gc-send-invitation-modal', templateUrl: './send-invitation-modal.component.html', styleUrls: ['./send-invitation-modal.component.scss'] }) export class SendInvitationModalComponent extends YCModalComponent< SendInviteModalResponseWithAttachments|SendInviteListWithOptions > implements OnInit { @Input() toIndividual = true; @Input() schedule: ScheduleRecord; @Input() forceFormGroupValueToDefault = true; selectedApplicant: ApplicantFromSearch; newApplicant: ApplicantFromSearch; formGroup: TypeSafeFormGroup; programOptions: TypeaheadSelectOption[] = []; cycleOptions: TypeaheadSelectOption[] = []; distributionListOptions: TypeaheadSelectOption[] = []; viewTranslations = this.translationService.viewTranslations; programTranslationMap = this.viewTranslations.Grant_Program; cycleTranslationMap = this.viewTranslations.Grant_Program_Cycle; programCycleMap = this.cycleService.programCycleMap; availablePrograms = this.programService.allActiveManagerPrograms.filter((prog) => { return prog.programType === ProgramTypes.GRANT; }); previewOpen = false; clientName = this.clientSettingsService.clientBranding.name; sendOptions: TypeaheadSelectOption[] = [{ label: this.i18n.translate( 'GLOBAL:textSendNow', {}, 'Send now' ), value: true }, { label: this.i18n.translate( 'GLOBAL:textSendLater', {}, 'Send later' ), value: false }]; isApplicantValid = false; emailType = this.invitationService.invitationEmail; tomorrow = moment().add(1, 'day'); attachments: GenericFile[]; constructor ( private i18n: I18nService, private formBuilder: TypeSafeFormBuilder, private programService: ProgramService, private translationService: TranslationService, private arrayHelper: ArrayHelpersService, private cycleService: CyclesService, private clientSettingsService: ClientSettingsService, private invitationService: InvitationService, private timeZoneService: TimeZoneService, private analyticsService: AnalyticsService ) { super(); } async ngOnInit () { this.setProgramOptions(); this.setDistributionListOptions(); let validators: ValidatorFn[] = [ this.futureDateValidator() ]; const scheduledDateRequired = !this.toIndividual; const distributionListRequired = !this.toIndividual && !this.schedule; const scheduledDate = this.toIndividual ? null : this.schedule?.scheduledDate || this.tomorrow; let sendNow = true; if (this.toIndividual) { validators = []; } if (this.schedule) { sendNow = false; validators = [ this.futureOrCurrentDateValidator() ]; } this.formGroup = this.formBuilder.group({ distributionListId: [ this.schedule?.distributionListId ?? null, distributionListRequired ? Validators.required : null ], programId: [ this.schedule?.grantProgramId ?? null, Validators.required ], cycleId: this.schedule?.grantProgramCycleId ?? null, sendNow, scheduledDate: [ scheduledDate, scheduledDateRequired ? Validators.required : null ], clientEmailTemplateId: this.schedule?.clientEmailTemplateId ?? 0, attachments: [], cc: [ [], EmailValidator() ], bcc: [ [], EmailValidator() ] }, { validators: [ ...validators, DuplicateCheckValidator('cc', 'bcc') ] }); if (this.schedule) { this.setCycleOptions(); } } getDateString (start: string, end: string, timeZoneId: string) { const timeZone = this.timeZoneService.returnTimeZoneFromID(timeZoneId); const startDisplay = this.timeZoneService.adaptDateForDisplayIfDST(start, timeZone.offset); const endDisplay = this.timeZoneService.adaptDateForDisplayIfDST(end, timeZone.offset); return this.i18n.translate( 'common:textDateToDate', { start: startDisplay, end: endDisplay }, '__start__ to __end__' ); } getTimezoneFromProgramId (programId: number) { const found = this.availablePrograms.find((prog) => { return +prog.grantProgramId === +programId; }); if (found) { return found.timezoneId || 'UTC'; } return 'UTC'; } setProgramOptions () { const options = this.availablePrograms.map((prog: Program) => { const dateString = this.getDateString( prog.startDate, prog.endDate, prog.timezoneId ); const name = this.programTranslationMap[prog.grantProgramId] ? this.programTranslationMap[prog.grantProgramId].Name : prog.grantProgramName; return { option: `
${name}
${dateString}
${ prog.timezoneId || 'UTC' }
`, value: prog.grantProgramId, label: name }; }); this.programOptions = this.arrayHelper.sort(options, 'label'); } setCycleOptions () { const programId = this.formGroup.value.programId; if (programId) { const timeZoneId = this.getTimezoneFromProgramId(programId); this.cycleOptions = this.programCycleMap[programId] .filter((cycle) => { return moment(cycle.endDate).isAfter(moment()); }) .map((cycle) => { const dateString = this.getDateString( cycle.startDate, cycle.endDate, timeZoneId ); const obj = this.cycleTranslationMap[cycle.id]; const cycleName = obj && obj.Name ? obj.Name : cycle.name; return { option: `
${cycleName}
${dateString}
${ timeZoneId }
`, value: cycle.id, label: cycleName }; }); } else { this.cycleOptions = []; } } setDistributionListOptions () { this.distributionListOptions = this.invitationService.distributionLists .filter((item) => { return item.distributionListApplicantCount > 0; }) .map((item) => { return { label: item.name, value: item.id }; }); } onProgramChange () { this.setCycleOptions(); } setApplicantFromSearch (applicant: ApplicantFromSearch) { this.selectedApplicant = applicant; } setNewApplicant (applicant: ApplicantFromSearch) { this.newApplicant = applicant; } setValidityOfApplicant (valid = false) { this.isApplicantValid = valid; } onSubmit () { const formValue = this.formGroup.value; const programId = formValue.programId; const cycleId = formValue.cycleId; const clientEmailTemplateId = formValue.clientEmailTemplateId || null; if (this.toIndividual) { const selectedApplicant = this.selectedApplicant || this.newApplicant; this.closeModal.emit({ programId, cycleId, clientEmailTemplateId, selectedApplicant, emailOptionsModel: { ccEmails: formValue.cc, bccEmails: formValue.bcc, attachments: formValue.attachments } }); } else { this.closeModal.emit({ programId, cycleId, clientEmailTemplateId, sendNow: formValue.sendNow, scheduledDate: formValue.scheduledDate, distributionListId: formValue.distributionListId, emailOptionsModel: { ccEmails: formValue.cc, bccEmails: formValue.bcc, attachments: formValue.attachments } }); } this.analyticsService.emitEvent({ eventName: 'Send invitation modal submit', eventType: EventType.Click, extras: null }); } futureDateValidator () { // This validator is for creating a new schedule return (group: AbstractControl) => { if (!group.value.sendNow) { const scheduledDate = group.value.scheduledDate; const tomorrow = moment().add(1, 'day'); if (moment(scheduledDate).isBefore(tomorrow, 'day')) { return { scheduledDate: { // control name futureDate: { // type of error i18nKey: 'GLOBAL:textMustBeFutureDate', defaultValue: 'Date must be in future' } } }; } } return null; }; } futureOrCurrentDateValidator () { // This validator is for updating an existing schedule return (group: AbstractControl) => { const currentScheduledDate = moment(this.schedule.scheduledDate); const currentIsToday = currentScheduledDate.isSame(moment(), 'day'); const scheduledDate = moment(group.value.scheduledDate); if (currentIsToday && currentScheduledDate.isSame(scheduledDate, 'day')) { return null; } const tomorrow = moment().add(1, 'day'); if (moment(scheduledDate).isBefore(tomorrow, 'day')) { return { scheduledDate: { // control name futureDate: { // type of error i18nKey: 'GLOBAL:textMustBeFutureDate', defaultValue: 'Date must be in future' } } }; } return null; }; } }