import { Injectable } from '@angular/core'; import { StatusService } from '@core/services/status.service'; import { TimeZoneService } from '@core/services/time-zone.service'; import { TranslationService } from '@core/services/translation.service'; import { ReferenceFieldAPI } from '@core/typings/api/reference-fields.typing'; import { ApplicantFormForUI, ApplicantFormFromApi, AutomaticallyRouted, BulkRevisionRequestPayload, PortalFormAvailabilityInfo, ReminderResponse, RevisionModalPayload, SendRevisionReminderBody } from '@core/typings/application.typing'; import { ApplicationStatuses } from '@core/typings/status.typing'; import { ReferenceFieldsUI } from '@core/typings/ui/reference-fields.typing'; import { ApplicationViewFormForUI, CompletionRequirementType, FormAudience, FormComment, FormDecisionTypes, FormDefinitionForUi, FormDueEmailModalResponse, FormForApplicantForUI, FormReminderForApi, FormRequirements, FormResponse, FormResponseFromApi, FormStatuses, FormTypes, PostFormComment, RevisionPayload, SaveFormResponse, ViewFormResponse } from '@features/configure-forms/form.typing'; import { FormResponseComponent } from '@features/formio/form-renderer/form-response/form-response.component'; import { FormLogicService } from '@features/formio/services/form-logic/form-logic.service'; import { ReferenceFieldsService } from '@features/reference-fields/services/reference-fields.service'; import { EmailService } from '@features/system-emails/email.service'; import { BaseEmailOptionsModel, EmailOptionsModelForSave } from '@features/system-emails/email.typing'; import { UserService } from '@features/users/user.service'; import { ArrayHelpersService, Panel } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { ConfirmAndTakeActionService } from '@yourcause/common/modals'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import moment from 'moment'; import { ApplicationFormResources } from '../application-forms.resources'; import { ApplicationFormState } from '../application-forms.state'; @AttachYCState(ApplicationFormState) @Injectable({ providedIn: 'root' }) export class ApplicationFormService extends BaseYCService { constructor ( private i18n: I18nService, private userService: UserService, private statusService: StatusService, private timezoneService: TimeZoneService, private notifier: NotifierService, private logger: LogService, private applicationFormResources: ApplicationFormResources, private translationService: TranslationService, private arrayHelper: ArrayHelpersService, private formLogicService: FormLogicService, private emailService: EmailService, private referenceFieldsService: ReferenceFieldsService, private confirmAndTakeActionService: ConfirmAndTakeActionService ) { super(); } get editingForm () { return this.get('editingForm'); } get formsForUser () { return this.get('formsForUser'); } get optionalFormsForUser () { return this.get('optionalFormsForUser'); } /** * Set editingForm on the state * * @param editingForm: Are we editing the form? */ setEditingForm (editingForm: boolean) { this.set('editingForm', editingForm); } /** * Set formsForUser on the state * * @param formsForUser: forms for the user to complete */ setFormsForUser (formsForUser: string[]) { this.set('formsForUser', formsForUser); } /** * Set optionalFormsForUser on the state * * @param optionalForms: optional forms for the user to complete */ setOptionalFormsForUser (optionalForms: string[]) { this.set('optionalFormsForUser', optionalForms); } /** * Returns whether the form is overdue * * @param dueDate: due date * @returns is the form overdue? */ getIsFormOverdue (dueDate: string) { let isOverdue = false; if (dueDate) { const endOfToday = moment().endOf('day'); isOverdue = endOfToday.isAfter(moment(dueDate).endOf('day')); } return isOverdue; } /** * Gets the applicant forms for a given application * * @param id: application ID * @param applicationStatus: Application Status * @returns the applicant forms for the application */ async getApplicantFormsForApplication ( id: number, applicationStatus: ApplicationStatuses ): Promise { const forms = await this.applicationFormResources.getApplicationForms(id); const viewTranslations = this.translationService.viewTranslations; const formTranslationMap = viewTranslations.FormTranslation; return forms.map((form) => { const map = formTranslationMap[form.formId]; form.formName = map && map.Name ? map.Name : form.formName; if (form.dueDate) { const showDueDate = this.isDueDateRelevant( form.formStatus, applicationStatus ); if (!showDueDate) { form.dueDate = null; } } const { formDefinition } = this.formLogicService.adaptFormDefinitionForTabs( form.formDefinition, form.formId, form.formRevisionId ); const isOverdue = this.getIsFormOverdue(form.dueDate); return { ...form, name: form.formName, isOverdue, formDefinition, formResponse: this.adaptApplicantFormToResponse( form, formDefinition, isOverdue ) }; }); } /** * Adapts and returns the form for view * * @param form: the applicant form * @returns the adapted form for view */ adaptApplicantFormToViewFormResponse ( form: ApplicantFormForUI ): ViewFormResponse { return { applicationFormId: form.applicationFormId, formStatus: form.formStatus, formId: form.formId, formName: form.formName, formSubmittedOn: form.formSubmittedOn, formSubmittedBy: form.submittedBy || form.createdBy, formDefinition: form.formDefinition, formData: form.formData, formTypeId: form.formTypeId, formRevisionId: form.formRevisionId, formComments: form.formComments, decision: null, reviewerRecommendedFundingAmount: null }; } /** * Adapts the Form repsonse to the View form response * * @param form: the form response * @param formId: form ID * @param formTypeId: form type * @param formName: form name * @param formComments: form comments * @returns the adapted form for view */ adaptFormResponseToViewFormResponse ( form: FormResponse, formId: number, formTypeId: number, formName: string, formComments: FormComment[] ): ViewFormResponse { let formSubmittedBy: string; if (form.submittedBy?.firstName) { formSubmittedBy = `${form.submittedBy.firstName} ${form.submittedBy.lastName}`; } else if (form.createdBy?.firstName) { formSubmittedBy = `${form.createdBy.firstName} ${form.createdBy.lastName}`; } return { applicationFormId: form.applicationFormId, formStatus: form.applicationFormStatusId, formId, formName, formSubmittedOn: form.submittedDate, formSubmittedBy, formDefinition: form.formDefinition, formData: form.formData, decision: form.decision, reviewerRecommendedFundingAmount: form.reviewerRecommendedFundingAmount, formTypeId, formRevisionId: form.formRevisionId, formComments: formComments.map((comment) => { return { workflowLevelId: null, workflowLevelName: '', firstName: comment.commentor.firstName, lastName: comment.commentor.lastName, profileImageUrl: '', notes: comment.comment, createdDate: comment.commentDate }; }) }; } /** * Saves the form response * * @param data: payload for save form response * @param applicationId: application id * @returns save form response details */ saveFormResponse ( data: SaveFormResponse, applicationId: number ) { return this.applicationFormResources.saveFormResponse(data, applicationId); } /** * Fetches the form panels for the form-tabs component * * @param forms: the forms to adapt */ getAllApplicationFormsPanels ( forms: ApplicationViewFormForUI[] ) { return forms.map((form) => { let description = ''; let icon = ''; let iconClass = ''; if (form.audience === FormAudience.MANAGER) { const count = this.getRequiredCount(form); description = this.getPanelDescription(form, count); const iconDetails = this.getPanelIconAndClass( count, form.completionRequirementType ); icon = iconDetails.icon; iconClass = iconDetails.iconClass; } else { const responses = form.otherLevelResponses .concat(form.responses).concat(form.nominationResponses); const response = responses.length ? responses[0] : null; if (response) { description = this.getDynamicStatusString( response.applicationFormStatusId, response.updatedDate, response.submittedDate, response.revisionLastSentDate, form.portalAvailabilityDetails ); } else { description = this.i18n.translate( 'common:textNoResponse', {}, 'No response' ); } icon = response ? ( response.applicationFormStatusId === FormStatuses.Submitted ? 'check-circle' : 'exclamation-circle' ) : 'exclamation-circle'; iconClass = response ? ( response.applicationFormStatusId === FormStatuses.Submitted ? 'success' : 'warning' ) : 'warning'; if (form.dueDate) { const formDueText = this.getFormDueString( form.dueDate ); description = description + `
${formDueText}`; } } if (form.dueDate) { icon = 'hourglass-half'; iconClass = form.isOverdue ? 'danger' : 'warning'; } return { context: { form, completedByMeSection: false }, name: form.name, description, icon, iconClass }; }); } /** * Gets the form due string * * @param dueDate: due date * @returns the form due string */ getFormDueString (dueDate: string) { const date = this.timezoneService.formatDueDate(dueDate); return this.i18n.translate( 'APPLY:textFormDueOn', { date }, 'Form due on __date__' ); } /** * Gets the required count of the given form * * @param form: form * @returns the required count for that form */ getRequiredCount (form: ApplicationViewFormForUI): FormRequirements { let countRecused = 0; const countComplete = form.responses.filter((res) => { const recused = res.decision === FormDecisionTypes.Recused; if (recused) { countRecused = countRecused + 1; } return !res.isDraft && !recused; }).length; const countOtherWorkflows = form.otherLevelResponses.filter((res) => { return !res.isDraft && res.decision !== FormDecisionTypes.Recused; }).length; let countRequired: number; const managerCount = form.managersCount - countRecused; switch (form.completionRequirementType) { case CompletionRequirementType.ALL_USERS: countRequired = Math.max(managerCount, countComplete); return { countRequired, countComplete, metRequirement: countComplete >= countRequired, countOtherWorkflows }; case CompletionRequirementType.MAJORITY: countRequired = Math.max( Math.ceil((managerCount + 1) / 2), countComplete ); return { countComplete, countRequired, metRequirement: countComplete >= countRequired, countOtherWorkflows }; case CompletionRequirementType.SPECIFIC_COUNT: return { countComplete, countRequired: form.specificNumberForCompletion, metRequirement: countComplete >= form.specificNumberForCompletion, countOtherWorkflows }; case CompletionRequirementType.NONE: case CompletionRequirementType.VIEW_ONLY: return { countComplete, countRequired: 0, metRequirement: true, countOtherWorkflows }; } } /** * Gets the panels' icon and class * * @param count: form required count info * @param completionRequirementType: completion requirement type info * @returns the icon and class for the panel */ getPanelIconAndClass ( count: FormRequirements, completionRequirementType: CompletionRequirementType ) { if (count.metRequirement) { if (completionRequirementType === CompletionRequirementType.VIEW_ONLY) { return { icon: 'ban', iconClass: 'danger' }; } else if (completionRequirementType === CompletionRequirementType.NONE) { return { icon: 'exclamation-circle', iconClass: 'warning' }; } return { icon: 'check-circle', iconClass: 'success' }; } else { return { icon: 'exclamation-circle', iconClass: 'warning' }; } } /** * Gets the form panel's description * * @param form: the form * @param count: the required count info * @returns the form panel description */ getPanelDescription ( form: ApplicationViewFormForUI, count: FormRequirements ) { let returnVal = ''; if (!!count.countRequired) { returnVal = this.i18n.translate( 'GLOBAL:textNumberOutOfNumberCompleteInThisLevel', { number: count.countComplete, count: count.countRequired }, '__number__ out of __count__ complete in this level' ); } else if (!!(count.countComplete + count.countOtherWorkflows)) { returnVal = this.i18n.translate( 'GLOBAL:textNumberOfSubmittedResponses', { count: count.countComplete + count.countOtherWorkflows }, '__count__ submitted responses' ); } else { returnVal = this.i18n.translate( 'GLOBAL:textNoSubmittedResponses', {}, 'No submitted responses' ); } if (form.dueDate) { const formDueText = this.getFormDueString( form.dueDate ); returnVal = returnVal + `
${formDueText}`; } return returnVal; } /** * Gets the status string for the form panel * * @param formStatus: form status * @param updatedDate: updpated date * @param submittedDate: submitted date * @param revisionLastSentDate: revision last sent date * @param portalAvailabilityInfo: portal availability info * @returns the status string */ getDynamicStatusString ( formStatus: FormStatuses, updatedDate: string, submittedDate: string, revisionLastSentDate: string, portalAvailabilityInfo?: PortalFormAvailabilityInfo ) { return this.statusService.getDynamicStatusString( formStatus, updatedDate, submittedDate, revisionLastSentDate, portalAvailabilityInfo ); } /** * Gets "My application" form panels * * @param formsToComplete: forms for current user to complete * @returns the form panels for the forms-tab component */ getMyApplicationFormPanels ( formsToComplete: ApplicationViewFormForUI[] ): Panel[] { const notSubmittedAsOfToday = this.i18n.translate( 'GLOBAL:textNotSubmittedAsOfToday', { date: moment().format('ll') }, 'Not submitted as of __date__' ); return formsToComplete.map((form) => { const response = form.responses.find((res) => { return res.createdBy.email === this.userService.userEmail; }); let description: string; description = response ? ( response.isDraft ? notSubmittedAsOfToday : this.statusService.getDynamicStatusString( response.applicationFormStatusId, response.updatedDate, response.submittedDate, response.revisionLastSentDate, form.portalAvailabilityDetails ) ) : notSubmittedAsOfToday; if (form.completionRequirementType === CompletionRequirementType.NONE) { const optionalText = this.i18n.translate( 'common:textOptional', {}, 'Optional' ); description = `${description} (${optionalText})`; } let icon = response ? (response.isDraft ? 'exclamation-circle' : 'check-circle') : 'exclamation-circle'; let iconClass = response ? (response.isDraft ? 'warning' : 'success') : 'warning'; if (form.dueDate) { const formDueText = this.getFormDueString(form.dueDate); icon = 'hourglass-half'; iconClass = form.isOverdue ? 'danger' : 'warning'; description = description + `
${formDueText}`; } return { context: { form, completedByMeSection: true }, name: form.name, description, icon, iconClass }; }); } /** * Gets all application form panels * * @param forms applicant forms * @returns returns the form panels for the offline grants applicant forms */ getApplicantFormPanels ( forms: ApplicantFormForUI[] ): Panel[] { return forms.map((form) => { let description = ''; let icon = ''; let iconClass = ''; const response = form.formResponse; description = this.getDynamicStatusString( response.applicationFormStatusId, response.updatedDate, response.submittedDate, response.revisionLastSentDate ); icon = response.applicationFormStatusId === FormStatuses.Submitted ? 'check-circle' : 'exclamation-circle'; iconClass = response.applicationFormStatusId === FormStatuses.Submitted ? 'success' : 'warning'; if (form.dueDate) { const formDueText = this.getFormDueString(form.dueDate); description = description + `
${formDueText}`; } if (form.dueDate) { icon = 'hourglass-half'; iconClass = form.isOverdue ? 'danger' : 'warning'; } return { context: { form, completedByMeSection: false }, name: form.name, description, icon, iconClass }; }); } /** * Returns if the user can edit the form * * @param editing: is editing? * @param canEditApplicantForms: can I edit applicant forms? * @param currentResponse: the current response * @param formAudience: the form audience * @param formType: the form type * @param completionRequirementType: the completion requirement type * @param appStatus: application status * @param isNominationForm: is this a nomination form? * @param isNomination: it this a nomination? * @param isEditApplicationView: are we in edit application view? * @param isCompletedByMeSection: are they in the completed by me section? * @returns if the user can edit the form */ getCanEditForm ( editing: boolean, canEditApplicantForms: boolean, currentResponse: FormResponse, formAudience: FormAudience, formType: FormTypes, completionRequirementType: CompletionRequirementType, appStatus: ApplicationStatuses, isNominationForm: boolean, isNomination: boolean, isEditApplicationView: boolean, isCompletedByMeSection: boolean ) { const canEditThisForm = canEditApplicantForms && formAudience === FormAudience.APPLICANT && currentResponse.applicationFormStatusId !== FormStatuses.NotSent; const viewOnly = formAudience !== FormAudience.APPLICANT && (completionRequirementType === CompletionRequirementType.VIEW_ONLY); return !editing && (isCompletedByMeSection || canEditThisForm) && (currentResponse.applicationFormStatusId !== FormStatuses.NotSent) && (appStatus !== ApplicationStatuses.Canceled) && (appStatus !== ApplicationStatuses.Declined) && (appStatus !== ApplicationStatuses.Hold || isEditApplicationView) && (formType !== FormTypes.ELIGIBILITY) && (formType !== FormTypes.ROUTING) && !viewOnly && (isNominationForm ? !!isNomination : true); } /** * Adapts the applicant form to a form response * * @param form: the form * @param formDefinition: fomr definition * @param isOverdue: is overdue? * @returns adapted FormResponse */ adaptApplicantFormToResponse ( form: ApplicantFormFromApi, formDefinition: FormDefinitionForUi[], isOverdue: boolean ): FormResponse { return { applicationFormId: form.applicationFormId, isDraft: form.isDraft, createdBy: null, updatedBy: null, updatedDate: form.updatedDate, revisionNotes: '', formComments: '', submittedDate: form.formSubmittedOn, applicationFormStatusId: form.formStatus, formData: form.formData, formRevisionId: form.formRevisionId, workflowLevelName: '', revisionLastSentDate: '', decision: null, reviewerRecommendedFundingAmount: null, dueDate: form.dueDate, isOverdue, submittedByName: form.submittedBy, formDefinition }; } /** * Check the revision request criteria * * @param formAudience: form audience * @param applicationStatus: application stauts * @param applicationFormStatus: application form status * @param forResend: for resend? * @returns if form meets revision request criteria */ checkRevisionRequestCriteria ( formAudience: FormAudience, applicationStatus: ApplicationStatuses, applicationFormStatus: FormStatuses, forResend = false ) { const canResendRevisionRequestEmail = (formAudience === FormAudience.APPLICANT) && (applicationStatus === ApplicationStatuses.Hold) && ( applicationFormStatus === FormStatuses.DraftSaved || applicationFormStatus === FormStatuses.RevisionRequested ); const canRequestRevision = formAudience === FormAudience.APPLICANT && (applicationFormStatus === FormStatuses.Submitted) && (applicationStatus !== ApplicationStatuses.Declined && applicationStatus !== ApplicationStatuses.Canceled); return forResend ? canResendRevisionRequestEmail : canRequestRevision; } /** * Returns whether the due date is relevant * * @param formStatus: the form status * @param applicationStatus: the application status * @returns if the due date should be shown / relevant */ isDueDateRelevant ( formStatus: FormStatuses, applicationStatus: ApplicationStatuses ) { return (formStatus !== FormStatuses.Submitted) && (formStatus !== FormStatuses.RevisionRequested) && (applicationStatus !== ApplicationStatuses.Hold); } /** * Handles sending the form due email * * @param modalResponse: form due email modal response * @param isOverdue: is overdue? */ async handleSendFormDue ( modalResponse: FormDueEmailModalResponse, isOverdue = false ) { await this.confirmAndTakeActionService.takeAction( `api/manager/forms/SendFormDueDateRelatedEmail?pastDueEmail=${isOverdue}`, modalResponse, this.i18n.translate( isOverdue ? 'FORMS:textSuccessfullySentFormOverdueEmail' : 'FORMS:textSuccessfullySentFormDueEmail', {}, isOverdue ? 'Successfully sent the form overdue email' : 'Successfully sent the form due email' ), this.i18n.translate( isOverdue ? 'FORMS:textErrorSendingFormOverdueEmail' : 'FORMS:textErrorSendingFormDueEmail', {}, isOverdue ? 'There was an error sending the form overdue email' : 'There was an error sending the form due email' ), 'post' ); } /** * Handles extending the form due date * * @param applicationId: application id * @param applicationFormId: application form id * @param formId: form id * @param workflowLevelId: workflow level id * @param dueDate: form due date * @param isManager: is manager form */ async handleExtendFormDueDate ( applicationId: number, applicationFormId: number, formId: number, workflowLevelId: number, dueDate: string, isManager = false ) { const payload = { applicationId, applicationFormId, formId, workflowLevelId, dueDate }; await this.confirmAndTakeActionService.takeAction( isManager ? 'api/manager/applications/ExtendFormDueDateByAppManager' : 'api/manager/applications/ExtendFormDueDate', payload, this.i18n.translate( 'FORMS:textSuccessfullyExtendedDueDate', {}, 'Successfully extended the form due date' ), this.i18n.translate( 'FORMS:textThereWasAnErrorExtendingDueDate', {}, 'There was an error extending the form due date' ), 'post' ); } /** * Handles adding a comment to an application form * * @param formId: form id * @param applicationId: application id * @param comment: comment to add * @returns the the result and if it passed */ async addCommentToApplicationForm ( formId: number, applicationId: number, comment: PostFormComment ) { return this.confirmAndTakeActionService.takeAction( `api/manager/applications/${applicationId}/forms/${formId}/comments`, comment, '', this.i18n.translate( 'common:textErrorAddingComment', {}, 'There was an error adding the comment' ), 'post' ); } /** * Gets all the applicant forms for an application * * @param applicationId: application ID * @param applicationStatus: application status * @returns all applicant forms for the application */ async getAllApplicantForms ( applicationId: number, applicationStatus: ApplicationStatuses ) { const forms = await this.applicationFormResources.getAllApplicantForms(applicationId); const adapted: FormForApplicantForUI[] = forms.map((form) => { let isOverdue = false; if (form.dueDate) { const showDueDate = this.isDueDateRelevant( form.applicationFormStatusId, applicationStatus ); if (showDueDate) { isOverdue = this.getIsFormOverdue(form.dueDate); } else { // Due date not relavant if submitted form.dueDate = null; } } return { ...form, isOverdue }; }); return this.arrayHelper.sort(adapted, 'createdDate'); } /** * Returns all forms for a given application * * @param appId: application ID * @param isApplicationInClientUserWorkflowLevel: is the app in users WFL? * @param applicationStatus: application status * @returns all application forms */ async getAllApplicationForms ( appId: number, isApplicationInClientUserWorkflowLevel = false, applicationStatus: ApplicationStatuses ): Promise { const viewTranslations = this.translationService.viewTranslations; const formTranslationMap = viewTranslations.FormTranslation; const response = await this.applicationFormResources.getAllApplicationForms(appId); const forms = response.formDetails; return forms.map((form) => { const map = formTranslationMap[form.formId]; form.name = map && map.Name ? map.Name : form.name; const responses = this.setIsOverdueOnFormResponsesAndAdaptTabs( form.responses, form.formId, form.formRevisionId ); const otherLevelResponses = this.setIsOverdueOnFormResponsesAndAdaptTabs( form.otherLevelResponses, form.formId, form.formRevisionId ); const nominationResponses = this.setIsOverdueOnFormResponsesAndAdaptTabs( form.nominationResponses, form.formId, form.formRevisionId ); const { dueDate, isOverdue } = this.getDueDateFromResponseArray( responses, form.audience === FormAudience.MANAGER, isApplicationInClientUserWorkflowLevel, form.dueDate, applicationStatus ); return { ...form, dueDate, isOverdue, responses, otherLevelResponses, nominationResponses }; }); } /** * Returns the due date and whether it's overdue * * @param responses: form responses * @param isManagerForm: is this a manager form? * @param isInMyWfl: is this app in my WFL? * @param formDueDate: form due date * @param applicationStatus: application status * @returns the due date */ getDueDateFromResponseArray ( responses: FormResponse[], isManagerForm = false, isInMyWfl = false, formDueDate: string, applicationStatus: ApplicationStatuses ) { let foundResponse: FormResponse; if (isManagerForm && isInMyWfl) { const myEmail = this.userService.userEmail; foundResponse = responses.find((res) => { return res.createdBy.email === myEmail; }); // For scenarios where the GM hasn't started form yet, use formDueDate if (!foundResponse && formDueDate) { return { dueDate: formDueDate, isOverdue: this.getIsFormOverdue(formDueDate) }; } } else { // Applicant forms should only have 1 response foundResponse = responses[0]; } const showDueDate = this.isDueDateRelevant( foundResponse?.applicationFormStatusId, applicationStatus ); if (showDueDate) { return { dueDate: foundResponse?.dueDate, isOverdue: foundResponse?.isOverdue ?? false }; } else { if (foundResponse) { foundResponse.dueDate = null; foundResponse.isOverdue = false; } } return { dueDate: null, isOverdue: false }; } /** * Adapts responsesto include due date and is overdue * * @param responses: form responses * @param formId: form ID * @param revisionId: revision ID * @returns the adapted responses */ setIsOverdueOnFormResponsesAndAdaptTabs ( responses: FormResponseFromApi[], formId: number, revisionId: number ): FormResponse[] { return responses.map((response) => { if (response.dueDate) { if (response.applicationFormStatusId !== FormStatuses.Submitted) { response.isOverdue = this.getIsFormOverdue(response.dueDate); } else { response.dueDate = null; } } const { formDefinition } = this.formLogicService.adaptFormDefinitionForTabs( response.formDefinition, formId, revisionId ); return { ...response, formDefinition }; }); } /** * Displays the automatically routed toastr * * @param isNomination: is nomination? */ showAutomaticallyRoutedToaster (isNomination = false) { this.notifier.success(this.i18n.translate( isNomination ? 'APPLICATION:textRoutingRuleCriteriaMetNomination' : 'APPLICATION:textRoutingRuleCriteriaMetApplication', {}, isNomination ? 'Routing rule criteria met. Nomination has been routed to the next workflow level.' : 'Routing rule criteria met. Application has been routed to the next workflow level.' )); } /** * Handles sending the review reminder * * @param applicationId: application ID * @param response: modal response */ async sendReviewReminder ( applicationId: number, response: ReminderResponse ) { const users = response.users; const single = users.length === 1; const endpoint = `api/manager/applications/${applicationId}/reminders/review`; try { await Promise.all((users.map((user) => { return this.applicationFormResources.sendReviewReminder( endpoint, user, response.clientEmailTemplateId, response.emailOptionsModel ); }))); this.notifier.success(this.i18n.translate( single ? 'MANAGE:textSuccessfullySentUserReminder' : 'MANAGE:textSuccessfullySentUsersReminder', {}, single ? 'Successfully sent reminder email to user' : 'Successfully sent reminder emails to users' )); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( single ? 'MANAGE:textErrorSendingUserReminder' : 'MANAGE:textErrorSendingUsersReminder', {}, single ? 'There was an error sending the selected user a reminder' : 'There was an error sending the selected users a reminder' )); } } /** * Handles sending the form reminder email * * @param payload: payload for send form reminder */ async sendFormReminder (payload: FormReminderForApi) { await this.confirmAndTakeActionService.genericTakeAction( () => this.applicationFormResources.sendFormReminder(payload), this.i18n.translate( 'FORMS:textSuccessfullySentFormReminder', {}, 'Successfully sent form reminder' ), this.i18n.translate( 'FORMS:textErrorSendingFormReminder', {}, 'There was an error sending a form reminder' ) ); } /** * Handles requesting a revision on a form * * @param applicationId: application ID * @param applicationFormId: application form ID * @param response: revision modal response */ async handleRequestRevision ( applicationId: number, applicationFormId: number, response: RevisionModalPayload ) { await this.confirmAndTakeActionService.takeAction( `api/manager/applications/${applicationId}/forms/${applicationFormId}/revisions`, response, this.i18n.translate( 'FORMS:textSuccessfullyRequestedRevision', {}, 'Successfully sent form to applicant for revision' ), this.i18n.translate( 'FORMS:textSuccessfullyRequestedRevision', {}, 'Successfully sent form to applicant for revision' ), 'post' ); } /** * Handles bulk revision requests * * @param applicationIds: application IDs * @param formId: form ID * @param response: revision modal response * @param grantProgramId: program ID */ async handleBulkRequestRevision ( applicationIds: number[], formId: number, response: RevisionModalPayload, grantProgramId: number ) { const payload: BulkRevisionRequestPayload = { applicationIds, message: response.notes, clientEmailTemplateId: response.clientEmailTemplateId, formId, grantProgramId }; await this.confirmAndTakeActionService.genericTakeAction( () => this.applicationFormResources.requestBulkApplicationFormRevision(payload), this.i18n.translate( 'FORMS:textSuccessfullyRequestedRevision', {}, 'Successfully sent form to applicant for revision' ), this.i18n.translate( 'FORMS:textErrorSendingFormToAppForRevision', {}, 'There was an error sending the form to the applicant for revision' ) ); } /** * Handles sending a revision reminder * * @param applicationId: application ID * @param formId: form ID * @param customMessage: custom message * @param clientEmailTemplateId: client email template ID to send * @param emailOptionsModel: email options */ async handleRevisionReminder ( applicationId: number, formId: number, customMessage: string, clientEmailTemplateId: number, emailOptionsModel: EmailOptionsModelForSave ) { const payload: SendRevisionReminderBody = { formId, customMessage, clientEmailTemplateId, emailOptionsModel }; await this.confirmAndTakeActionService.takeAction( `api/manager/applications/${applicationId}/reminders/revision`, payload, this.i18n.translate( 'FORMS:textSuccessfullySentReminderEmail', {}, 'Successfully sent revision reminder email' ), this.i18n.translate( 'FORMS:textErrorSendingRevisionReminderEmail', {}, 'There was an error sending the revision reminder email' ), 'post' ); } /** * Handles bulk revision reminders * * @param applicationIds: application IDs * @param formId: form ID * @param customMessage: custom message * @param clientEmailTemplateId: client email template ID to send * @param grantProgramId: program ID */ async handleBulkRevisionReminder ( applicationIds: number[], formId: number, customMessage: string, clientEmailTemplateId: number, grantProgramId: number ) { const payload: BulkRevisionRequestPayload = { applicationIds, message: customMessage, clientEmailTemplateId, formId, grantProgramId }; await this.confirmAndTakeActionService.genericTakeAction( () => this.applicationFormResources.sendBulkRevisionReminder(payload), this.i18n.translate( 'FORMS:textSuccessfullySentReminderEmail', {}, 'Successfully sent revision reminder email' ), this.i18n.translate( 'FORMS:textErrorSendingRevisionReminderEmail', {}, 'There was an error sending the revision reminder email' ) ); } /** * Handles canceling a revision request * * @param payload: the cancel revision ayload * @param applicationId: application ID * @param applicationFormId: application form ID */ async handleCancelRevision ( payload: RevisionPayload, applicationId: number, applicationFormId: number ) { const attachments = await this.emailService.returnIdsFromMixedAttachments( payload.emailOptionsModel.attachments, applicationId ); const emailOptionsModel = { ...payload.emailOptionsModel, attachments }; await this.confirmAndTakeActionService.takeAction( `api/manager/applications/${applicationId}/forms/${applicationFormId}/revisions/delete?clientEmailTemplateId=${payload.clientEmailTemplateId}`, emailOptionsModel, this.i18n.translate( 'FORMS:textSuccessfullyCanceledRequestedRevision', {}, 'Successfully canceled the requested revision' ), this.i18n.translate( 'FORMS:textErrorCancelingRequestedREvision', {}, 'There was an error canceling the requested revision' ), 'post', true ); } /** * Handle sending the form to the applicant * * @param applicationId: application ID * @param applicationFormId: application form ID * @param emailOptionsModel: email options * @param clientEmailTemplateId: client email template ID to send * @param isNomination: is nomination? */ async handleSendFormToApplicant ( applicationId: number, applicationFormId: number, emailOptionsModel: EmailOptionsModelForSave, clientEmailTemplateId: number, isNomination = false ) { const payload = { ccEmails: emailOptionsModel.ccEmails, bccEmails: emailOptionsModel.bccEmails, attachments: emailOptionsModel.attachments, clientEmailTemplateId }; const result = await this.confirmAndTakeActionService.takeAction( `api/manager/applications/${applicationId}/forms/${applicationFormId}/send`, payload, this.i18n.translate( isNomination ? 'FORMS:textSuccessfullySendFormToNominator' : 'FORMS:textSuccessfullySendFormToApplicant', {}, isNomination ? 'Successfully sent the form to the nominator' : 'Successfully sent the form to the applicant' ), this.i18n.translate( 'FORMS:textErrorSendingFormToApplicant', {}, 'There was an error sending the form' ), 'post' ); if (result.passed && result.endpointResponse.automaticallyRouted) { this.showAutomaticallyRoutedToaster(isNomination); } } /** * Saves reference field responses * * @param applicationId: application ID * @param formId: form ID * @param formRevisionId: form revision ID * @param applicationFormId: application form ID * @param referenceFieldValues: reference field values to save * @param isManagerSavingApplicantForm: is a manager saving an applicant form? * @param isEdit: is this edit application? * @returns if we successfully saved the values */ async saveReferenceFieldValues ( applicationId: number, formId: number, formRevisionId: number, applicationFormId: number, referenceFieldValues: ReferenceFieldAPI.ApplicationRefFieldResponseForApi[], isManagerSavingApplicantForm = false, isEdit = false ) { try { await this.applicationFormResources.saveReferenceFields( applicationId, formId, formRevisionId, applicationFormId, referenceFieldValues, null, null, isManagerSavingApplicantForm, isEdit ); return true; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'APPLY:textErrorSavingApplication', {}, 'There was an error saving the application' )); return false; } } /** * Saves table field responses * * @param tableChanges: table changes * @param applicationId: application ID * @param formId: form ID * @param formRevisionId: form revision ID * @param applicationFormId: application form ID * @param isManagerSavingApplicantForm: is the manager saving an applicant form? * @returns if passed */ async saveTableFieldValues ( tableChanges: ReferenceFieldAPI.TableChangeResponse, applicationId: number, formId: number, formRevisionId: number, applicationFormId: number, isManagerSavingApplicantForm = false ): Promise<{ passed: boolean; rowsToUpdate: ReferenceFieldAPI.TableChangeValues[]; }> { let passed = true; const rowsToUpdate: ReferenceFieldAPI.TableChangeValues[] = []; try { for (const update of tableChanges.updates) { const res = await this.applicationFormResources.saveReferenceFields( applicationId, formId, formRevisionId, applicationFormId, update.values, update.tableReferenceFieldId, update.rowId, isManagerSavingApplicantForm ); if (!update.rowId) { update.rowId = res.rowId; rowsToUpdate.push(update); } } for (const deletion of tableChanges.deletions) { await this.applicationFormResources.deleteTableRow(deletion); } } catch (e) { this.logger.error(e); passed = false; this.notifier.error(this.i18n.translate( 'common:textErrorSavingTableChanges', {}, 'There was an error saving the table changes' )); } return { passed, rowsToUpdate }; } /** * Do the table row updates * * @param changes: changes to make * @param applicationId: application ID * @param formId: form ID * @param formRevisionId: form revision ID * @param applicationFormId: application form ID * @returns if the updates passed */ async doTableRowUpdates ( changes: ReferenceFieldsUI.AdaptRefChangesResponse, applicationId: number, formId: number, formRevisionId: number, applicationFormId: number ): Promise { const res = await this.saveTableFieldValues( changes.tableChangeValues, applicationId, formId, formRevisionId, applicationFormId ); if (res.passed) { /* Rows to update means a row was added and we need to add rowId to it */ res.rowsToUpdate.forEach((row) => { const field = this.referenceFieldsService.referenceFieldMapById[ row.tableReferenceFieldId ]; const updatedRows = [ ...(changes.tableChangeMap[field.key] as ReferenceFieldsUI.TableResponseRowForUi[]) .map((_row, _index) => { if ( !_row.rowId && row.isNewRecord && _index === row.index ) { _row.rowId = row.rowId; } return _row; }) ]; changes.tableChangeMap = { ...changes.tableChangeMap, [field.key]: updatedRows }; }); /* Successfully saved table changes, so update the map with the newest values */ /* Gc Table field and Subset field reacts to these changes */ this.referenceFieldsService.setApplicationFormTableRowsMap( applicationFormId, changes.tableChangeMap ); } return res.passed; } /** * Handles the save of changes fields * * @param changes: changes to save * @param applicationId: application ID * @param formId: form ID * @param formRevisionId: form revision ID * @param applicationFormId: application form ID * @param isManagerSavingApplicantForm: is the manager saving an applicant form? * @param isEdit: is this edit app view? * @returns if it passed */ async handleSaveOfChangedFields ( changes: ReferenceFieldsUI.AdaptRefChangesResponse, applicationId: number, formId: number, formRevisionId: number, applicationFormId: number, isManagerSavingApplicantForm: boolean, isEdit: boolean ): Promise<{ standardPassed: boolean; tablePassed: boolean; }> { let standardPassed = true; let tablePassed = true; if (changes.standardChangeValues.length > 0) { standardPassed = await this.saveReferenceFieldValues( applicationId, formId, formRevisionId, applicationFormId, changes.standardChangeValues, isManagerSavingApplicantForm, isEdit ); } if ( changes.tableChangeValues.deletions.length > 0 || changes.tableChangeValues.updates.length > 0 ) { tablePassed = await this.doTableRowUpdates( changes, applicationId, formId, formRevisionId, applicationFormId ); } return { standardPassed, tablePassed }; } /** * Handles the recall of an application form * * @param applicationId: application ID * @param applicationFormId: application form ID * @param clientEmailTemplateId: client email to send * @param emailOptionsModel: email options * @param isNomination: is nomination? */ async handleRecallApplicationForm ( applicationId: number, applicationFormId: number, clientEmailTemplateId: number, emailOptionsModel: BaseEmailOptionsModel, isNomination: boolean ) { const result = await this.confirmAndTakeActionService.takeAction( `api/manager/applications/${applicationId}/forms/${applicationFormId}/recall?clientEmailTemplateId=${clientEmailTemplateId}`, emailOptionsModel, this.i18n.translate( 'FORMS:textSuccessfullyRecalledForm', {}, 'Successfully recalled the form' ), this.i18n.translate( 'FORMS:textErrorRecallingForm', {}, 'There was an error recalling the form' ), 'post' ); if (result.passed && result.endpointResponse.automaticallyRouted) { this.showAutomaticallyRoutedToaster(isNomination); } } }