import { Component, Input, OnInit } from '@angular/core'; import { AbstractControl, Validators } from '@angular/forms'; import { SpinnerService } from '@core/services/spinner.service'; import { ConfigureProgramMap } from '@core/typings/program.typing'; import { WorkflowLevel } from '@core/typings/workflow.typing'; import { AvailabilityOptions, CompletionRequirementType, DateOption, DueDateOptions, FormAudience, FormDueDate, FormTypes, PortalAvailabilityDetails, ProgramFormModalFormState, RelativeFormDueDateFields, ResponseVisibilityOptions, WorkflowLevelFormApi } from '@features/configure-forms/form.typing'; import { FormStatusService } from '@features/configure-forms/services/form-status/form-status.service'; import { FormsService } from '@features/configure-forms/services/forms/forms.service'; import { EmailService } from '@features/system-emails/email.service'; import { EmailNotificationType } from '@features/system-emails/email.typing'; import { ArrayHelpersService, SelectOption, Tab, 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 { Subscription } from 'rxjs'; import { ProgramService } from '../program.service'; enum ProgramFormTabOptions { GRANT_MANAGER = 1, AVAILABILITY = 2, DUE_DATE = 3, EMAIL_NOTIFICATION = 4 } @Component({ selector: 'gc-program-form-modal', templateUrl: './program-form-modal.component.html', styleUrls: ['./program-form-modal.component.scss'] }) export class ProgramFormModalComponent extends YCModalComponent implements OnInit { @Input() form: WorkflowLevelFormApi; @Input() workflowLevel: WorkflowLevel; @Input() defaultForm: WorkflowLevelFormApi; @Input() programId: number; @Input() isNomination = false; emailModalOpen = false; AvailabilityOptions = AvailabilityOptions; CompletionRequirementOptions = CompletionRequirementType; formGroup: TypeSafeFormGroup; FormTypes = FormTypes; FormAudience = FormAudience; DueDateOptions = DueDateOptions; audience: FormAudience; currentTab: ProgramFormTabOptions; ProgramFormTabOptions = ProgramFormTabOptions; showApplicantOptions = false; showManagerOptions = false; maximumNumberRequiredForCompletion = 99; formType: FormTypes; modalHeader = this.i18n.translate( 'PROGRAM:hdrAddForm', {}, 'Add Form' ); defaultSelectForm = this.i18n.translate( 'GLOBAL:textSelectAForm', {}, 'Select a form' ); formOptions: TypeaheadSelectOption[]; onOrAfterDateInfo = this.i18n.translate( 'PROGRAM:textOnOrAfterDateInfo', {}, 'Date form is sent to applicant. If the date has already passed the form will be sent today.' ); dueDateHeader = this.i18n.translate( 'common:hdrDueDate', {}, 'Due Date' ); emailNotificationText = this.i18n.translate( 'common:textEmailNotification', {}, 'Email notification' ); nominationFormAddedText = this.i18n.translate( 'PROGRAM:textNominationFormAddedEmailText', {}, 'GC-23 Nomination Form Added' ); applicationFormAddedText = this.i18n.translate( 'PROGRAM:textApplicationFormAdded', {}, 'GC-18 Application Form Added' ); emailTypeText: string; modalSubHeader: string; grantManagerOptionsText: string; daysText = this.i18n.translate('GLOBAL:textDaysLowercase', {}, 'days'); dateOptions: TypeaheadSelectOption[] = [{ label: this.i18n.translate( 'GLOBAL:lblLastAwardDate', {}, 'Last award date' ), value: DateOption.AWARD_DATE }, { label: this.i18n.translate( 'GLOBAL:textApplicationSubmittedDate', {}, 'Application submitted date' ), value: DateOption.APPLICATION_SUBMITTED_DATE }, { label: this.i18n.translate( 'GLOBAL:textApplicationApprovalDate', {}, 'Application approval date' ), value: DateOption.APPLICATION_APPROVAL_DATE }, { label: this.i18n.translate( 'GLOBAL:textWorkflowLevelEntryDate', {}, 'Workflow level entry date' ), value: DateOption.WORKFLOW_LEVEL_ENTRY_DATE }]; responseVisibilityOptions: SelectOption[] = [{ display: this.i18n.translate('common:textNo'), value: ResponseVisibilityOptions.VIEW_NONE }, { display: this.i18n.translate( 'PROGRAM:textYesTheyCanSeeResponsesAtThisWorkflowLevel', {}, 'Yes, they can see responses added at this workflow level' ), value: ResponseVisibilityOptions.VIEW_AT_THIS_WORKFLOW }, { display: this.i18n.translate( 'PROGRAM:textYesTheyCanSeeAllResponsesWorkflowLevel', {}, 'Yes, they can see all responses from this and previous workflow levels' ), value: ResponseVisibilityOptions.VIEW_AT_ALL_WORKFLOW }]; availabilityOptions: SelectOption[] = [{ icon: 'bolt', display: this.i18n.translate( 'PROGRAM:textAvailabilityAuto', {}, 'Automatically when the application enters this workflow level' ), value: AvailabilityOptions.AUTO }, { icon: 'hand-pointer', display: this.i18n.translate( 'PROGRAM:textAvailabilityManual', {}, 'Manually when a Grant Manager sends it to the applicant portal' ), value: AvailabilityOptions.MANUAL }, { icon: 'calendar', display: this.i18n.translate( 'common:textOnOrAfter', {}, 'On or after' ), value: AvailabilityOptions.DATE }, { icon: 'calendar', display: this.i18n.translate( 'common:textOnOrAfter', {}, 'On or after' ), value: AvailabilityOptions.DYNAMIC_DATE }]; completionRequirementOptions: SelectOption[] = [{ display: this.i18n.translate( 'PROGRAM:textAllUsers', {}, 'All users' ), value: CompletionRequirementType.ALL_USERS }, { display: this.i18n.translate( 'PROGRAM:textMajorityUsers', {}, 'Majority of users' ), value: CompletionRequirementType.MAJORITY }, { display: this.i18n.translate( 'PROGRAM:textNoneFormIsOptional', {}, 'None. The form is optional to complete.' ), value: CompletionRequirementType.NONE }, { display: this.i18n.translate( 'GLOBAL:textViewOnly', {}, 'View only' ), value: CompletionRequirementType.VIEW_ONLY }, { display: '', value: CompletionRequirementType.SPECIFIC_COUNT }]; dueDateOptions: SelectOption[] = [{ display: this.i18n.translate( 'PROGRAM:textNoDueDate', {}, 'No due date' ), value: DueDateOptions.NONE }, { display: '', value: DueDateOptions.SPECIFIC }]; relativeFormDueDateOptions = this.formStatusService.relativeFormDueDateOptions; existingForms: WorkflowLevelFormApi[] = []; tabs: Tab[] = []; noFormsAvailable = false; currentEmailActive: boolean; sub = new Subscription(); emailDisabledInEdit = false; configureProgramMap: ConfigureProgramMap; activeTempsFromChangesMap: number[]; constructor ( private formBuilder: TypeSafeFormBuilder, private i18n: I18nService, private formService: FormsService, private arrayHelper: ArrayHelpersService, private spinnerService: SpinnerService, private emailService: EmailService, private programService: ProgramService, private analyticsService: AnalyticsService, private formStatusService: FormStatusService ) { super(); this.sub.add(this.programService.changesTo$('configureProgramMap') .subscribe((changeMap) => { this.configureProgramMap = changeMap; })); } get formTypeMap () { return this.formService.formTypeMap; } get formTypes () { return this.formService.formTypes; } get emailType (): EmailNotificationType { return this.isNomination ? EmailNotificationType.NominationFormSent : EmailNotificationType.ApplicationFormSent; } async ngOnInit () { this.spinnerService.startSpinner(); await this.setFormOptions(); this.spinnerService.stopSpinner(); if (this.form) { this.modalHeader = this.i18n.translate( 'PROGRAM:textEditForm', {}, 'Edit Form' ); } this.modalSubHeader = this.workflowLevel.parentName ? (this.workflowLevel.parentName + ' | ' + this.workflowLevel.name) : this.workflowLevel.name; const formId = this.form ? this.form.formId : this.formOptions[0]?.value; const date = this.form?.portalAvailabilityDetails?.date ?? moment().format(); const dateOffset = this.form?.portalAvailabilityDetails?.dateOffset ?? 7; const dateOption = this.form?.portalAvailabilityDetails?.dateOption ?? DateOption.APPLICATION_APPROVAL_DATE; const specificNumberForCompletion = this.form?.specificNumberForCompletion ?? 1; this.formGroup = this.formBuilder.group({ formId: [formId, Validators.required], managerActionType: [ this.form?.managerActionType ?? ResponseVisibilityOptions.VIEW_NONE ], completionRequirementType: [ this.form?.completionRequirementType ?? CompletionRequirementType.ALL_USERS, this.validateCompletionRequirementType() ], portalAvailability: [ this.form ? this.form.portalAvailability : AvailabilityOptions.AUTO ], date: [date], dateOption: [dateOption], dateOffset: [dateOffset], specificNumberForCompletion, dueDateOption: [ !!this.form?.dueDateDetails ? DueDateOptions.SPECIFIC : DueDateOptions.NONE ], relativeDateField: [ this.form?.dueDateDetails?.relativeDateField ?? RelativeFormDueDateFields.ENTERED_WFL_DATE ], dueDateOffset: this.form?.dueDateDetails?.dateOffset ?? 30, clientEmailTemplateId: this.form?.clientEmailTemplateId || 0 }, { validator: [ this.validateNumberForCompletion() ] } ); this.setGrantManagerOptionsText(); this.formChanged(); this.currentEmailActive = await this.emailService.isProgramEmailActive( this.emailType, this.programId ); this.setTabs(); } getEmailPrefFromConfigurationMap () { if (this.programId) { // get changes from map for this program const configMap = this.configureProgramMap[this.programId]; // get clientTemplates obj belonging to this emailType const clientTemplatesObjOnConfigMap = configMap?.clientTemplates .find((clientTemplate) => clientTemplate.emailNotificationType === this.emailType); // get templates array off clientTemplates obj const templatesOnClientTemplateObj = clientTemplatesObjOnConfigMap?.templates; // filter out inactive templates and map to array of IDs this.activeTempsFromChangesMap = templatesOnClientTemplateObj ? templatesOnClientTemplateObj .filter((template) => template.active) .map((t) => t.clientEmailTemplateId) : []; // check for the default email on emailNotificationTypesPrefs this.emailDisabledInEdit = configMap.emailNotificationTypesPreferences.some((emailPref) => emailPref.id === this.emailType && emailPref.isExcluded); } } setGrantManagerOptionsText () { this.grantManagerOptionsText = this.i18n.translate( 'PROGRAM:textGrantLevelRulesDescUpdated2', { users: this.workflowLevel.workflowLevelUsers.length || 0 }, 'Select the number of users who must complete this form at this workflow level. Currently, the level has __users__ users.' ); } async setFormOptions () { await this.i18n.loadNamespaces('FORMS'); const forms = this.arrayHelper.sort( this.formService.published, 'name' ); this.formOptions = forms.filter((form) => { const existingFormIds = this.existingForms.map((f) => f.formId); return !existingFormIds.includes(form.formId); }).map((form) => { const formTypeText = this.i18n.translate( 'common:hdrType' ); const formAudienceText = this.i18n.translate( 'common:textAudience', {}, 'Audience' ); const type = this.formTypes.find((t) => +t.id === +form.formType); const audience = type.audience === FormAudience.MANAGER ? this.i18n.translate('GLOBAL:textGrantManager', {}, 'Grant manager') : this.i18n.translate('common:lblApplicant'); return { option: `
${form.name}
${formTypeText + ': ' + this.formTypeMap[form.formType]} ${formAudienceText + ': ' + audience} `, label: form.name + ' - ' + this.formTypeMap[form.formType], value: form.formId }; }); this.noFormsAvailable = this.formOptions.length === 0 && !this.form; } formChanged () { const form = this.formService.published.find((f) => { return +f.formId === +this.formGroup.value.formId; }); if (form) { this.audience = this.formService.getAudienceFromFormType(form.formType); this.formType = form.formType; } else { this.audience = null; } this.setOptionsForView(); this.setTabs(); } setTabs () { // we need to check our change map in case email prefs have been updated this.getEmailPrefFromConfigurationMap(); if (this.showManagerOptions) { this.tabs = [{ label: this.i18n.translate( 'common:hdrGrantManagerOptions', {}, 'Grant Manager Options' ), active: true, context: ProgramFormTabOptions.GRANT_MANAGER }, { label: this.dueDateHeader, active: false, context: ProgramFormTabOptions.DUE_DATE }]; this.currentTab = ProgramFormTabOptions.GRANT_MANAGER; } else if (this.showApplicantOptions) { this.tabs = [{ label: this.i18n.translate( 'PROGRAM:textAvailability', {}, 'Availability' ), active: true, context: ProgramFormTabOptions.AVAILABILITY }, { label: this.dueDateHeader, active: false, context: ProgramFormTabOptions.DUE_DATE }]; // only add email notification tab if email is active if ( this.currentEmailActive && this.formGroup.value.portalAvailability !== AvailabilityOptions.MANUAL && !this.emailDisabledInEdit ) { this.tabs = [ ...this.tabs, { label: this.emailNotificationText, active: false, context: ProgramFormTabOptions.EMAIL_NOTIFICATION } ]; } // set text for email type this.emailTypeText = this.isNomination ? this.nominationFormAddedText : this.applicationFormAddedText; this.currentTab = ProgramFormTabOptions.AVAILABILITY; } else { this.tabs = []; } if (this.audience !== FormAudience.APPLICANT) { this.formGroup.get('clientEmailTemplateId').setValue(null); } } setCurrentTab (index: number) { this.currentTab = this.tabs[index]?.context || ProgramFormTabOptions.DUE_DATE; } setOptionsForView () { const isApplicant = this.audience === FormAudience.APPLICANT; const hasForm = !!this.formGroup.value.formId; const isRelevant = ![ FormTypes.NOMINATION, FormTypes.ELIGIBILITY, FormTypes.ROUTING ].includes(this.formType); const notDefaultForm = +this.formGroup.value.formId !== +this.defaultForm.formId; this.showApplicantOptions = hasForm && isApplicant && isRelevant && notDefaultForm; this.showManagerOptions = hasForm && !isApplicant; } validateNumberForCompletion () { return (group: AbstractControl) => { const numberForCompletion = group.get('specificNumberForCompletion').value; if ( group.value.completionRequirement && (numberForCompletion || numberForCompletion === 0) ) { const numberIsNotGreaterThanZero = !(numberForCompletion > 0); const numberIsGreaterThanMax = numberForCompletion > this.maximumNumberRequiredForCompletion; if (numberIsGreaterThanMax) { return { specificNumberForCompletion: { exceedsMaximumRequried: { i18nKey: 'GLOBAL:textCannotExceed99', defaultValue: 'Cannot exceed 99' } } }; } else if (numberIsNotGreaterThanZero) { return { specificNumberForCompletion: { mustBeAboveZero: { i18nKey: 'GLOBAL:textCannotBeZero', defaultValue: 'This number cannot be zero' } } }; } } else if (group && group.value.completionRequirement && !numberForCompletion) { return { specificNumberForCompletion: { mustBeAboveZero: { i18nKey: 'GLOBAL:textANumberOfUsersIsRequired', defaultValue: 'A number of users is required for this option' } } }; } return null; }; } validateCompletionRequirementType () { return (control: AbstractControl) => { const type = control.value as CompletionRequirementType; if (!type && this.showManagerOptions) { return { required: { i18nKey: 'common:textThisInputIsRequired', defaultValue: 'This input is required' } }; } return null; }; } adaptFormValue (): WorkflowLevelFormApi { const formValue = this.formGroup.value; let dueDate: FormDueDate = null; const dueDateAllowed = ( formValue.completionRequirementType !== CompletionRequirementType.NONE && formValue.completionRequirementType !== CompletionRequirementType.VIEW_ONLY ); if (this.showApplicantOptions || dueDateAllowed) { if (formValue.dueDateOption === DueDateOptions.SPECIFIC) { dueDate = { relativeDateField: formValue.relativeDateField, dateOffset: formValue.dueDateOffset }; } } let specificNumberForCompletion: number = null; if (formValue.completionRequirementType === CompletionRequirementType.SPECIFIC_COUNT) { specificNumberForCompletion = formValue.specificNumberForCompletion; } const portalAvailabilityDetails: PortalAvailabilityDetails = { dateOption: null, dateOffset: null, date: null }; if (formValue.portalAvailability === AvailabilityOptions.DATE) { portalAvailabilityDetails.date = formValue.date; } else if (formValue.portalAvailability === AvailabilityOptions.DYNAMIC_DATE) { portalAvailabilityDetails.dateOption = formValue.dateOption; portalAvailabilityDetails.dateOffset = formValue.dateOffset; } let adaptedClientEmailTemplateId = formValue.clientEmailTemplateId; if ( formValue.clientEmailTemplateId === 0 || this.formGroup.value.portalAvailability === AvailabilityOptions.MANUAL ) { adaptedClientEmailTemplateId = null; } return { formId: +formValue.formId, workflowLevelId: this.workflowLevel.id, completionRequirementType: formValue.completionRequirementType, specificNumberForCompletion, managerActionType: formValue.managerActionType, portalAvailability: formValue.portalAvailability, portalAvailabilityDetails, notifyApplicantOfFormAvailability: true, dueDateDetails: dueDate, grantProgramId: this.programId, isDefaultForm: (+formValue.formId !== +this.defaultForm?.formId), formType: this.formService.getFormTypeFromFormId(formValue.formId), clientEmailTemplateId: adaptedClientEmailTemplateId, sortOrder: this.existingForms.length + 1 }; } onSubmit () { const adaptedFormValue = this.adaptFormValue(); this.closeModal.emit(adaptedFormValue); this.analyticsService.emitEvent({ eventName: 'Program forms modal save', eventType: EventType.Click, extras: null }); } onCancel () { this.closeModal.emit(); } }