import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { FormControl, Validators } from '@angular/forms'; import { DomSanitizer } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { UnsavedChangesModalComponent } from '@core/components/unsaved-changes-modal/unsaved-changes-modal.component'; import { FormMaskingService } from '@core/services/form-masking.service'; import { PolicyService } from '@core/services/policy.service'; import { SpecialHandlingService } from '@core/services/special-handling.service'; import { SpinnerService } from '@core/services/spinner.service'; import { StatusService } from '@core/services/status.service'; import { TranslationService } from '@core/services/translation.service'; import { ValidatorsService } from '@core/services/validators.service'; import { ApplicantFormForUI, ApplicationEditDetail, ApplicationEditMap, ApplicationForUi, ApplicationViewDetail, ApplicationViewPage, NominationForm, Nominee } from '@core/typings/application.typing'; import { UserTypes } from '@core/typings/client-user.typing'; import { ApplicationInfoForPDF, FormInfoForPDF, OrgInfoForPDF } from '@core/typings/pdf.typing'; import { ApplicationStatuses } from '@core/typings/status.typing'; import { ReferenceFieldsUI } from '@core/typings/ui/reference-fields.typing'; import { UserSignature } from '@core/typings/user.typing'; import { ApplicationDownloadService } from '@features/application-download/application-download.service'; import { ApplicationFormService } from '@features/application-forms/services/application-forms.service'; import { ExtendFormDueDateModalComponent } from '@features/application-manager/extend-form-due-date-modal/extend-form-due-date-modal.component'; import { FormReminderModalComponent } from '@features/application-manager/form-reminder-modal/form-reminder-modal.component'; import { ApplicationActionService } from '@features/application-manager/services/application-actions/application-actions.service'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { CollaborationService } from '@features/collaboration/collaboration.service'; import { ApplicationFormSignature, ApplicationViewFormForUI, BaseApplicationForLogic, CompletionRequirementType, FormAudience, FormComment, FormData, FormioChanges, FormResponse, FormStatuses, FormTypes, ResponseVisibilityOptions, SaveFormResponse } from '@features/configure-forms/form.typing'; import { DashboardsService } from '@features/dashboards/dashboards.service'; import { GCDashboards } from '@features/dashboards/dashboards.typing'; import { FormResponseComponent } from '@features/formio/form-renderer/form-response/form-response.component'; import { FormHelperService } from '@features/formio/services/form-helper/form-helper.service'; import { FormLogicService } from '@features/formio/services/form-logic/form-logic.service'; import { ReportFieldService } from '@features/formio/services/report-field/report-field.service'; import { InKindRequestedItem } from '@features/in-kind/in-kind.typing'; import { LogicState } from '@features/logic-builder/logic-builder.typing'; import { NonprofitService } from '@features/nonprofit/nonprofit.service'; import { OfflineGrantsSubmitApplicationModalComponent } from '@features/offline-grants/offline-grants-submit-application/offline-grants-submit-application-modal/offline-grants-submit-application-modal.component'; import { OfflineGrantsService } from '@features/offline-grants/offline-grants.service'; import { ProgramService } from '@features/programs/program.service'; import { ReferenceFieldsService } from '@features/reference-fields/services/reference-fields.service'; import { SignatureService } from '@features/signature/signature.service'; import { EmailService } from '@features/system-emails/email.service'; import { SystemTagsService } from '@features/system-tags/system-tags.service'; import { UserService } from '@features/users/user.service'; import { WorkflowService } from '@features/workflow/workflow.service'; import { ArrayHelpersService, CallMaker, CallMakerFactory, Comment, FileService, Panel, PanelSection, SimpleStringMap, TextFriendlySpecialCharCleaner, YcFile, YCTwoWayEmitter } from '@yourcause/common'; import { AnalyticsService, EventType } from '@yourcause/common/analytics'; import { CachedAttr, CACHE_TYPES } from '@yourcause/common/cache'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { ModalFactory } from '@yourcause/common/modals'; import { NotifierService } from '@yourcause/common/notifier'; import { Bypass, SignatureModalComponent, SignatureModalResponse, ViewSignatureModalComponent } from '@yourcause/common/signature'; import { isUndefined } from 'lodash'; import moment from 'moment'; import { BehaviorSubject, map, Subscription } from 'rxjs'; import { ApplicationViewPageComponent } from '../application-view-page/application-view-page.component'; import { ApplicationViewService } from '../application-view.service'; import { CancelRequestRevisionModalComponent } from '../cancel-request-revision-modal/cancel-request-revision-modal.component'; import { RecallFormModalComponent } from '../recall-form-modal/recall-form-modal.component'; import { RequestRevisionModalComponent } from '../request-revision-modal/request-revision-modal.component'; import { SendFormToApplicantModalComponent } from '../send-form-to-applicant-modal/send-form-to-applicant-modal.component'; @Component({ selector: 'gc-forms-tab', templateUrl: './forms-tab.component.html', styleUrls: ['./forms-tab.component.scss'], encapsulation: ViewEncapsulation.None }) export class FormsTabComponent implements OnInit, OnDestroy { sub: Subscription; viewTranslations = this.translationService.viewTranslations; programTranslationMap = this.viewTranslations.Grant_Program; cycleTranslationMap = this.viewTranslations.Grant_Program_Cycle; id: number; pendingFormioChanges: FormioChanges[] = []; FormAudience = FormAudience; ApplicationStatuses = ApplicationStatuses; CompletionRequirementOptions = CompletionRequirementType; masked$ = new BehaviorSubject(true); masked = true; showMaskToggle = false; FormStatuses = FormStatuses; FormTypes = FormTypes; completedByMeSection = true; editing: boolean; formComments: Comment[] = []; canSendToApplicant = false; noPanelsMessageOne = this.i18n.translate( 'FORMS:textNoFormsToComplete', {}, 'There are no forms for you to complete' ); noPanelsMessageTwo = this.i18n.translate( 'FORMS:textNoFormsToView', {}, 'There are no forms for you to view' ); showNoFormsToComplete = false; showNoFormsToView = false; panelSections: PanelSection[] = [{ name: this.i18n.translate( 'FORMS:textFormsForYouToComplete', {}, 'Forms for you to complete' ), panels: [], noPanelsMessage: this.noPanelsMessageOne }, { name: this.i18n.translate( this.isNomination ? 'FORMS:textNominationForms' : 'GLOBAL:textApplicationForms', {}, this.isNomination ? 'Nomination forms' : 'Application forms' ), panels: [], noPanelsMessage: this.noPanelsMessageTwo }]; editModeDraftAlertWithSend = this.i18n.translate( this.isNomination ? 'common:textAlertForEditModeDraftSendToNominator' : 'common:textAlertForEditModeDraftSendToApplicant', {}, this.isNomination ? `Edit the default form below. Select "Send to nominator" to save any changes made to the form and allow the nominator to submit the nomination. If you want to submit the nomination yourself and have the nomination enter a workflow level, click "Submit".` : `Edit the default form below. Select "Send to applicant" to save any changes made to the form and allow the applicant to submit the application. If you want to submit the application yourself and have the application enter a workflow level, click "Submit".` ); editModeDraftAlertNoSend = this.i18n.translate( this.isNomination ? 'common:textAlertForEditModeDraftApp' : 'common:textAlertForEditModeDraftNom', {}, this.isNomination ? `Edit the default form below. If you want to submit the nomination yourself and have the nomination enter a workflow level, click "Submit".` : `Edit the default form below. If you want to submit the application yourself and have the application enter a workflow level, click "Submit".` ); form: ApplicationViewFormForUI|ApplicantFormForUI; formSignature: ApplicationFormSignature; formSubmit = new YCTwoWayEmitter>(); application: ApplicationViewPage; currentResponse: FormResponse; allCurrentResponses: FormResponse[]; blankCurrentResponse: FormResponse; // For when the user hasn't started the form yet savingForm = false; formStatusText: string; formStatusIcon: string; foundPanel: Panel; hidePostComment: boolean; isCommentSliderOpen = false; formsToComplete: ApplicationViewFormForUI[]; applicantForms: ApplicantFormForUI[]; canRequestRevision = false; nominationForm: NominationForm; nomination: ApplicationForUi; @CachedAttr( CACHE_TYPES.SESSION, false, undefined, undefined, (self: ApplicationViewPageComponent) => { return `_yc_ShowMaskedApplicant_${self.id}`; } ) showMaskedApplicant: boolean; userEmail = this.userService.userEmail; loaded = false; canEditApplicantForms = false; nomineeFormValid: boolean; markForValidity: boolean; hideForm = false; formStatusBeforeEdit: FormStatuses; isManagerEditingApplicantForm = false; hasAllPermission = this.policyService.grantApplication.canManageAllApplications() || this.policyService.grantApplication.canTakeActionsOnAllApps(); conditionalVisibilityState: LogicState; savedSignatureFile: YcFile; userSignature: UserSignature; callMaker: CallMaker = CallMakerFactory.create( (changes) => this.handleAutoSave(changes), 500, false, true ); translations: SimpleStringMap = {}; richTextTranslations: SimpleStringMap = {}; forceDefaultCurrency: boolean; constructor ( private logger: LogService, private translationService: TranslationService, private statusService: StatusService, private referenceFieldService: ReferenceFieldsService, private activatedRoute: ActivatedRoute, private i18n: I18nService, private userService: UserService, private spinnerService: SpinnerService, private notifier: NotifierService, private modalFactory: ModalFactory, private applicationDownloadService: ApplicationDownloadService, private router: Router, private clientSettingsService: ClientSettingsService, private emailService: EmailService, private collaborationService: CollaborationService, private systemTagsService: SystemTagsService, private formHelperService: FormHelperService, private policyService: PolicyService, private workflowService: WorkflowService, private dashboardService: DashboardsService, private programService: ProgramService, private reportFieldService: ReportFieldService, private formMaskingService: FormMaskingService, private offlineGrantsService: OfflineGrantsService, private arrayHelper: ArrayHelpersService, private signatureService: SignatureService, private analyticsService: AnalyticsService, private nonprofitService: NonprofitService, private fileService: FileService, private applicationFormService: ApplicationFormService, private sanitizer: DomSanitizer, private formLogicService: FormLogicService, private applicationViewService: ApplicationViewService, private specialHandlingService: SpecialHandlingService, private applicationActionService: ApplicationActionService, private validatorsService: ValidatorsService ) { } get formId () { return this.activatedRoute.snapshot.params.formId; } get isEdit (): boolean { return this.activatedRoute.snapshot.data.isEdit; } get applicationMap () { return this.isEdit ? this.applicationViewService.applicationEditMap : this.applicationViewService.applicationViewMap; } get currentResponseIndex () { if (this.currentResponse) { return this.allCurrentResponses.indexOf(this.currentResponse); } return -1; } get applicantName () { return this.application.applicantFirstName + ' ' + this.application.applicantLastName; } get currentFormAudience () { if ('audience' in this.form) { return this.form.audience; } return FormAudience.APPLICANT; } get currentFormType () { if ('formType' in this.form) { return this.form.formType; } else if ('formTypeId' in this.form) { return this.form.formTypeId; } return null; } get detailForms (): ApplicantFormForUI[] { return (this.applicationMap[this.id] as ApplicationEditDetail).formsForEdit || []; } get isNomination () { return location.pathname.includes('nomination'); } get nominee () { return this.application ? (this.application.nominee || {} as Nominee) : {} as Nominee; } get isNominationForm () { if (this.form) { return this.currentFormType === FormTypes.NOMINATION; } return false; } get isMasked () { return this.application ? this.application.isMasked : true; } get clientBranding () { return this.clientSettingsService.get('clientBranding'); } get basePath () { if (this.isEdit) { return `/management/manage-${this.isNomination ? 'nominations' : 'applications'}/${this.isNomination ? 'nomination' : 'application'}/${this.id}`; } return `/management/${this.isNomination ? 'nomination-' : 'application-'}view/${this.id}`; } get showSendToApplicant () { if (this.isEdit) { return (this.applicationMap as ApplicationEditMap)[this.id]?.showSendToApplicant; } return false; } get showSaveAsDraft () { if (this.isEdit) { return (this.applicationMap as ApplicationEditMap)[this.id]?.showSaveAsDraft; } return false; } get completionRequirementType () { if (this.isEdit) { return null; } return (this.form as ApplicationViewFormForUI).completionRequirementType; } get isEditModeForDraft () { return this.isEdit && this.application.applicationStatus === ApplicationStatuses.Draft; } get editModeDraftAlert () { return this.showSendToApplicant ? this.editModeDraftAlertWithSend : this.editModeDraftAlertNoSend; } get formsForAppView () { if (this.applicationMap[this.id]) { return (this.applicationMap[this.id] as ApplicationViewDetail).formsForAppView; } return []; } get formsForEdit () { if (this.applicationMap[this.id]) { return (this.applicationMap[this.id] as ApplicationEditDetail).formsForEdit; } return []; } async ngOnInit () { await this.updateApplication(true); if (!this.sub) { this.sub = this.activatedRoute.params.pipe( map(p => p.formId)) .subscribe((formId) => { if (formId === 'reload') { this.ngOnInit(); } }); } } async updateApplication (isInit = false) { this.spinnerService.startSpinner(); this.setApplication(); await this.setPanels(isInit); this.setPermissionsForEdit(); this.setNominationAttrs(); this.spinnerService.stopSpinner(); } setPermissionsForEdit () { const appPolicyService = this.policyService.grantApplication; const canTakeAllActions = appPolicyService.canManageAllApplications() || appPolicyService.canTakeActionsOnAllApps(); const myWorkflowActions = this.workflowService.myWorkflowManagerRolesMap; const isWfManager = !!myWorkflowActions[this.application.currentWorkFlowId]; const hasEditPermission = this.policyService.grantApplication.canCreateOrEditApplications(); this.canEditApplicantForms = this.isEdit || (canTakeAllActions || (isWfManager && hasEditPermission)); } setNominationAttrs () { this.nominationForm = { email: this.nominee.email, firstName: this.nominee.firstName, lastName: this.nominee.lastName, phoneNumber: this.nominee.phoneNumber, position: this.nominee.position }; this.nomineeFormValid = !!this.nominee.email && !!this.nominee.firstName && !!this.nominee.lastName && !Validators.email(new FormControl(this.nominee.email)); } private setApplication () { this.id = this.activatedRoute.snapshot.parent.params.id; this.application = this.applicationMap[this.id] ? this.applicationMap[this.id].application : null; this.nomination = this.applicationMap[this.id] ? this.applicationMap[this.id].nomination : null; } async setPanels (isInit = false) { this.formsToComplete = []; this.applicantForms = []; if (this.isEdit) { this.panelSections = [{ name: this.i18n.translate( this.isNomination ? 'common:hdrNominatorForms' : 'common:hdrApplicantForms', {}, this.isNomination ? 'Nominator Forms' : 'Applicant Forms' ), panels: [], noPanelsMessage: this.noPanelsMessageTwo }]; this.applicantForms = this.formsForEdit; this.panelSections[0].panels = this.applicationFormService.getApplicantFormPanels( this.applicantForms ); } else { if (this.application.isApplicationInClientUserWorkflowLevel) { this.formsToComplete = this.formsForAppView.filter((form) => { return ( form.audience === FormAudience.MANAGER && form.completionRequirementType !== CompletionRequirementType.VIEW_ONLY ); }); } this.panelSections[0].panels = this.applicationFormService.getMyApplicationFormPanels( this.formsToComplete ); const formsForAll = this.formsForAppView.filter((form) => { return ( form.audience === FormAudience.APPLICANT || form.grantManagerActionType !== ResponseVisibilityOptions.VIEW_NONE ); }); const sortedForms = this.arrayHelper.sort(formsForAll, 'sortOrder'); this.panelSections[1].panels = this.applicationFormService.getAllApplicationFormsPanels( sortedForms ); } await this.setCurrentPanel(); this.setFormsForUser(); this.setOptionalFormsForUser(); if ( isInit && this.isEdit && this.foundPanel && (this.application.applicationStatus === ApplicationStatuses.Draft) ) { await this.edit(); } this.loaded = true; } async setCurrentPanel () { const panels = this.isEdit ? this.panelSections[0].panels : this.panelSections[0].panels.concat(this.panelSections[1].panels); this.foundPanel = panels.find((panel) => { return +this.formId === +panel.context.form.formId; }) || this.panelSections[0].panels[0] || (this.isEdit ? null : this.panelSections[1].panels[0]); if (this.foundPanel) { await this.onPanelClick(this.foundPanel.context); } else { this.onEmptyPanelClick(this.panelSections[0]); } } async downloadPDF () { this.spinnerService.startSpinner(); const guid = this.application.nonprofitGuid; let orgPhone = ''; if (guid) { const org = await this.nonprofitService.getNonprofitAdditionalDataByGuid( this.application.nonprofitGuid ); if (org.nonprofitDetail) { orgPhone = org.nonprofitDetail.displayNumber; } } const [ referenceFields, reportFieldResponse, signature, tags ] = await Promise.all([ // reference fields this.referenceFieldService.getReferenceFieldResponses( this.id, this.currentResponse.applicationFormId, this.currentResponse.formDefinition, this.formHelperService.getTableAndSubsetIdsFromFormDefinition( [this.currentResponse.formDefinition] ), null, true, false, null, this.currentResponse.formData ), // report field response this.reportFieldService.handleReportFieldData( this.currentResponse.formDefinition, this.application.applicationId ), // signature this.signatureService.setFormSignature( this.application.applicationId, this.currentResponse.applicationFormId, true ), // tags this.systemTagsService.getCurrentTagsForPDF( this.id ) ]); const application: ApplicationInfoForPDF = { appId: this.application.applicationId, programName: this.application.programName, isMasked: this.application.isMasked, amountRequested: this.application.amountRequested, currencyRequested: this.application.currencyRequested, currencyRequestedAmountEquivalent: this.application.currencyRequestedAmountEquivalent, inKindItems: (this.application.inKindItems || []).filter((item) => { return !!item.itemIdentification && +item.count > 0; }), specialHandling: this.application.specialHandling, cycleName: this.cycleTranslationMap[this.application.grantProgramCycle.id]?.Name, status: this.statusService.applicationStatusMap?.[this.application.applicationStatus]?.translated ?? '', careOf: this.application.careOf, submittedDate: this.application.submittedDate, workflowLevelName: this.application.currentWorkFlowLevelName, workflowName: this.application.currentWorkflowName, tags, reportFieldResponse, employeeInfo: this.application.employeeInfo, designation: this.application.designation }; const orgInfo: OrgInfoForPDF = { name: this.application.organizationName, addressString: this.application.organizationAddress, registrationId: this.application.organizationIdentification, phone: orgPhone }; let formSubmittedBy: string; if (this.currentResponse.submittedBy?.firstName) { formSubmittedBy = this.currentResponse.submittedBy.firstName + ' ' + this.currentResponse.submittedBy.lastName; } else if (this.currentResponse.createdBy?.firstName) { formSubmittedBy = this.currentResponse.createdBy.firstName + ' ' + this.currentResponse.createdBy.lastName; } const formInfo: FormInfoForPDF = { formName: this.form.name, formSubmittedOn: this.currentResponse.submittedDate, formSubmittedBy, formDefinition: this.currentResponse.formDefinition, formData: this.currentResponse.formData, formId: this.formId, formStatus: this.currentResponse.applicationFormStatusId, applicationFormId: this.currentResponse.applicationFormId, referenceFields, decision: this.currentResponse.decision, specialHandling: this.application.specialHandling, reviewerRecommendedFundingAmount: this.currentResponse.reviewerRecommendedFundingAmount, signature, translations: this.translations, richTextTranslations: this.richTextTranslations }; await this.formHelperService.prepareComponentsForRenderForm( [this.currentResponse.formDefinition], [this.formId] ); const { fileUploads, tableCsvs } = this.formHelperService.extractFilesAndTablesForPdf([formInfo]); await this.applicationDownloadService.buildApplicationPDF( await this.collaborationService.getApplicantsForPDF( this.id, this.application.isMasked && !this.application.canViewMaskedApplicantInfo ), application, this.nominee, orgInfo, this.isNomination, [formInfo], fileUploads, tableCsvs, null, null, this.masked, this.clientBranding.name, await this.programService.getProgramLogo( this.application.programId ), null, null, null, this.application.isArchived, false ); this.spinnerService.stopSpinner(); } setFormsForUser () { const formsForUser: string[] = this.formsToComplete.filter((form) => { let shouldReturn = false; form.responses.forEach((res) => { if ( (res.createdBy?.email === this.userEmail) && (res.applicationFormStatusId !== FormStatuses.Submitted) && (form.completionRequirementType !== CompletionRequirementType.NONE) ) { shouldReturn = true; } }); return shouldReturn; }).map((f) => f.name); this.applicationFormService.setFormsForUser(formsForUser); } setOptionalFormsForUser () { const formsForUser: string[] = this.formsToComplete.filter((form) => { let shouldReturn = false; form.responses.forEach((res) => { if ( (res.createdBy?.email === this.userEmail) && (res.applicationFormStatusId !== FormStatuses.Submitted) && (form.completionRequirementType === CompletionRequirementType.NONE) ) { shouldReturn = true; } }); return shouldReturn; }).map((f) => f.name); this.applicationFormService.setOptionalFormsForUser(formsForUser); } getInKindItemsForSave (inKindItems: InKindRequestedItem[]) { return (inKindItems || []).filter((item) => { return !!item.itemIdentification && +item.count > 0; }); } setFormComments () { const isArchived = this.application.isArchived; this.hidePostComment = isArchived; this.isCommentSliderOpen = false; let comments: FormComment[] = []; if ('comments' in this.form) { comments = this.form.comments; } else if ('formComments' in this.form) { comments = this.form.formComments.map((comment) => { return { commentor: { firstName: comment.firstName, lastName: comment.lastName, email: '', userType: UserTypes.MANAGER, impersonatedBy: '', fullName: comment.firstName + ' ' + comment.lastName }, commentDate: '', comment: comment.notes }; }); } this.formComments = comments.map((comment) => { if ( !isArchived && comment.commentor.email === this.userEmail ) { this.hidePostComment = true; } return { name: comment.commentor.firstName + ' ' + comment.commentor.lastName, comment: comment.comment, date: comment.commentDate }; }); } toggleMask (newMaskValue = !this.masked) { this.masked = newMaskValue; this.masked$.next(newMaskValue); } setShouldShowMask () { const maskResult = this.currentResponse?.formDefinition && this.formMaskingService.checkShouldShowMaskToggleForFormView( this.currentResponse.formDefinition ); this.showMaskToggle = maskResult?.shouldShow ?? false; this.masked = maskResult?.defaultSetting ?? false; } commentToggle (isOpen: boolean) { this.isCommentSliderOpen = isOpen; } async addFormComment (notes: string) { const result = await this.applicationFormService.addCommentToApplicationForm( this.form.formId, this.id, { notes, formRevisionId: this.currentResponse.formRevisionId } ); if (result.passed) { this.spinnerService.startSpinner(); await this.applicationViewService.setApplicationViewMap( this.id, this.isEdit ); await this.updateApplication(); const forms = this.isEdit ? this.formsForEdit : this.formsForAppView; forms.forEach((f: ApplicationViewFormForUI|ApplicantFormForUI) => { if (+f.formId === +this.form.formId) { this.form = f; } }); this.setFormComments(); this.spinnerService.stopSpinner(); } } async recallModal () { const deps = { isNomination: this.isNomination, programId: this.application.programId }; const recallPayload = await this.modalFactory.open( RecallFormModalComponent, deps ); if (recallPayload || recallPayload.clientEmailTemplateId === 0) { await this.applicationFormService.handleRecallApplicationForm( this.id, this.currentResponse.applicationFormId, recallPayload.clientEmailTemplateId, recallPayload.emailOptionsModel, this.isNomination ); this.spinnerService.startSpinner(); await this.updatePanels(); this.spinnerService.stopSpinner(); } } async revisionModal () { const response = await this.modalFactory.open( RequestRevisionModalComponent, { programId: this.application.programId, isNomination: this.isNomination } ); if (response) { this.spinnerService.startSpinner(); const attachments = await this.emailService.returnIdsFromMixedAttachments( response.emailOptionsRequest.attachments, this.id ); this.spinnerService.stopSpinner(); const emailOptionsRequest = { ...response.emailOptionsRequest, attachments }; const payload = { ...response, emailOptionsRequest }; await this.applicationFormService.handleRequestRevision( this.id, this.currentResponse.applicationFormId, payload ); await this.updatePanels(); } } async revisionReminderModal () { const response = await this.modalFactory.open( RequestRevisionModalComponent, { programId: this.application.programId, isNomination: this.isNomination, isReminder: true } ); if (response) { this.spinnerService.startSpinner(); const attachments = await this.emailService.returnIdsFromMixedAttachments( response.emailOptionsRequest.attachments, this.id ); const emailOptionsModel = { ...response.emailOptionsRequest, attachments }; await this.applicationFormService.handleRevisionReminder( this.id, this.form.formId, response.notes, response.clientEmailTemplateId, emailOptionsModel ); await this.updatePanels(); this.spinnerService.stopSpinner(); } } async cancelRevisionModal () { const deps = { programId: this.application.programId, isNomination: this.isNomination }; const payload = await this.modalFactory.open( CancelRequestRevisionModalComponent, deps ); if (payload) { this.spinnerService.startSpinner(); await this.applicationFormService.handleCancelRevision( payload, this.id, this.currentResponse.applicationFormId ); await this.updatePanels(); this.spinnerService.stopSpinner(); } } async formReminderModal () { const response = await this.modalFactory.open( FormReminderModalComponent, { isNomination: this.isNomination, formName: this.form.name, programId: this.application.programId } ); if (response) { this.spinnerService.startSpinner(); const attachments = await this.emailService.returnIdsFromMixedAttachments( response.emailOptionsModel.attachments, this.id ); const emailOptionsModel = { ...response.emailOptionsModel, attachments }; this.spinnerService.stopSpinner(); await this.applicationFormService.sendFormReminder({ ...response, applicationId: this.application.applicationId, formId: this.form.formId, emailOptionsModel }); } } async sendToApplicantModal () { const response = await this.modalFactory.open( SendFormToApplicantModalComponent, { programId: this.application.programId, isNomination: this.isNomination, applicationId: this.id, formName: this.form.name } ); if (response) { await this.applicationFormService.handleSendFormToApplicant( this.id, this.currentResponse.applicationFormId, { ccEmails: response.ccEmails, bccEmails: response.bccEmails, attachments: response.attachments }, response.clientEmailTemplateId, this.isNomination ); this.spinnerService.startSpinner(); await this.updatePanels(); this.spinnerService.stopSpinner(); } } async extendFormDueDateModal () { const response = await this.modalFactory.open( ExtendFormDueDateModalComponent, { dueDate: this.currentResponse.dueDate, formName: this.form.name } ); if (response) { await this.applicationFormService.handleExtendFormDueDate( this.id, this.currentResponse.applicationFormId, this.formId, this.application.currentWorkFlowLevelId, response.dueDate, false ); this.spinnerService.startSpinner(); await this.updatePanels(); this.spinnerService.stopSpinner(); } } viewNextResponse () { this.toggleMask(true); this.setCurrentResponse(this.allCurrentResponses[this.currentResponseIndex + 1]); } viewPreviousResponse () { this.toggleMask(true); this.setCurrentResponse(this.allCurrentResponses[this.currentResponseIndex - 1]); } async setCurrentResponseOnFirstEdit () { const { applicationFormId } = await this.applicationFormService.saveFormResponse( { formId: this.form.formId, formRevisionId: this.form.formRevisionId, fileIds: [], isDraft: true, revisionNotes: '', applicationFormId: 0, amountRequested: this.application.amountRequested || 0, currencyRequested: this.application.currencyRequested || this.clientSettingsService.defaultCurrency, saveAmountRequestedInDefaultCurrency: this.forceDefaultCurrency, paymentDesignation: TextFriendlySpecialCharCleaner(this.application.designation), specialHandlingName: this.application.specialHandling?.name ?? '', specialHandlingAddress1: this.application.specialHandling?.address1 ?? '', specialHandlingAddress2: this.application.specialHandling?.address2 ?? '', specialHandlingCity: this.application.specialHandling?.city ?? '', specialHandlingStateProvinceRegion: this.application.specialHandling?.state ?? '', specialHandlingCountry: this.application.specialHandling?.country ?? '', specialHandlingPostalCode: this.application.specialHandling?.postalCode ?? '', specialHandlingNotes: this.application.specialHandling?.notes ?? '', specialHandlingReason: this.application.specialHandling?.reason ?? '', specialHandlingFileUrl: this.application.specialHandling?.fileUrl ?? '', inKindItems: this.getInKindItemsForSave(this.application.inKindItems), careOf: this.application.careOf, reviewerRecommendedFundingAmount: undefined, requiredReferenceFieldKeys: [], reviewerRecommendedFundingAmountRequired: false, decisionRequired: false, amountRequestedRequired: false, careOfRequired: false, paymentDesignationRequired: false, editingApplicationView: this.isEdit, submittingApplication: false, workflowLevelId: null, userSignatureId: null, userSignatureBypassed: false }, this.id ); const allForms = await this.applicationFormService.getAllApplicationForms( this.id, this.application.isApplicationInClientUserWorkflowLevel, this.application.applicationStatus ); const thisForm = allForms.find((form) => { return form.formId === this.form.formId && form.formRevisionId === this.form.formRevisionId; }); if (thisForm) { const currentResponse = thisForm.responses.find((res) => { return +res.applicationFormId === +applicationFormId; }); await this.setCurrentResponse(currentResponse); } } async setCurrentResponse (response: FormResponse) { await this.setReferenceFields(response); this.setReviewerRecommendedFundingAndDecision(response); this.currentResponse = response; if (response?.applicationFormId) { const signature = await this.signatureService.setFormSignature( this.id, response.applicationFormId, false ); this.formSignature = signature?.userSignatureUrl ? signature : null; } else { this.formSignature = null; } if (!response && this.completedByMeSection) { const form = await this.formLogicService.getAndSetForm( this.form.formId, this.form.formRevisionId ); const formDefinition = form.formDefinition; this.blankCurrentResponse = { applicationFormId: null, isDraft: true, revisionNotes: '', formComments: '', formData: {}, formDefinition, formRevisionId: this.form.formRevisionId, workflowLevelName: '', revisionLastSentDate: null, decision: null, reviewerRecommendedFundingAmount: null, dueDate: null }; } else { this.blankCurrentResponse = undefined; } this.setShouldShowMask(); } setReviewerRecommendedFundingAndDecision (response: FormResponse) { const reviewerRecommendedFundingAmount = response?.reviewerRecommendedFundingAmount; const decision = response?.decision; this.application = { ...this.application, reviewerRecommendedFundingAmount, decision }; } async setReferenceFields (currentResponse: FormResponse) { if (currentResponse) { const referenceFields = await this.referenceFieldService.getReferenceFieldResponses( this.application.applicationId, currentResponse.applicationFormId, currentResponse.formDefinition, this.formHelperService.getTableAndSubsetIdsFromFormDefinition( [currentResponse.formDefinition] ), null, false, false, null, currentResponse.formData ); this.application = { ...this.application, referenceFields }; } else { this.application = { ...this.application, referenceFields: {} }; } } async edit () { this.spinnerService.startSpinner(); this.hideForm = true; await Promise.all([ this.form.requireSignature ? this.signatureService.setSignature() : null, !this.currentResponse ? this.setCurrentResponseOnFirstEdit() : null ]); if (this.form.requireSignature) { this.userSignature = this.signatureService.userSignature; this.savedSignatureFile = this.signatureService.savedSignatureFile; } this.formStatusBeforeEdit = this.currentResponse.applicationFormStatusId; if (this.foundPanel) { this.foundPanel.description = this.i18n.translate( 'GLOBAL:textNotSubmittedAsOfToday', { date: moment().format('ll') }, 'Not submitted as of __date__' ); } this.setEditing(true); // if the mask toggle is present // show the values on edit if (this.showMaskToggle) { this.toggleMask(false); } this.hideForm = false; this.spinnerService.stopSpinner(); } setEditing (editing: boolean) { this.editing = editing; this.applicationFormService.setEditingForm(this.editing); } cancelEditMode (goToMyApplications = false) { this.setEditing(false); this.formStatusBeforeEdit = null; if (goToMyApplications) { this.router.navigate([ `management/manage-${this.isNomination ? 'nominations' : 'applications'}/${this.isNomination ? 'nominations' : 'applications'}` ]); } } nominationFormChanged (form: NominationForm) { this.nominationForm = form; // Will trigger application change because isReferenceField is false. // Other fields are irrelevant. this.onChange([{ key: '', type: '', isReferenceField: false, value: null }]); } onNomineeValidityChange (valid: boolean) { this.nomineeFormValid = valid; } formInvalidError () { this.toggleSaving(false); this.notifier.error(this.i18n.translate( 'APPLY:textFormIsInvalid', {}, 'Form is invalid or incomplete. Fix errors to continue.' )); } async saveAsDraft () { this.spinnerService.startSpinner(); this.cancelEditMode(); await this.updatePanels(); this.toggleMask(true); this.spinnerService.stopSpinner(); } formDataChanged (formData: FormData) { this.currentResponse.formData = formData; } externalFieldsChanged (externalFields: Partial) { if (!isUndefined(externalFields.decision)) { this.currentResponse.decision = externalFields.decision; } } onChange (changes: FormioChanges[]) { if (this.editing) { this.pendingFormioChanges = [ ...this.pendingFormioChanges, ...changes ]; this.callMaker.invoke(() => this.pendingFormioChanges); } } toggleSaving (saving: boolean) { if (saving) { this.formStatusText = this.i18n.translate( 'GLOBAL:textSaving' ); this.formStatusIcon = 'ellipsis-h-alt'; this.savingForm = true; } else { this.savingForm = false; this.formStatusText = this.i18n.translate( 'GLOBAL:textSaved' ); this.formStatusIcon = 'check-circle'; } } async handleAutoSave (changes: FormioChanges[]) { try { this.toggleSaving(true); const response = this.referenceFieldService.handleChangeTracking( changes, this.application ); const appNeedsUpdated = response.appNeedsUpdated; const refChangeTracker = response.refChangeTracker; let needToSaveFileIds = false; if (Object.keys(refChangeTracker).length > 0) { const res = await this.saveRefFields(refChangeTracker); if (res === ReferenceFieldsUI.SaveRefFieldsResponse.NeedToSaveFileIds) { needToSaveFileIds = true; } } if (appNeedsUpdated || needToSaveFileIds) { await this.saveApplication(true, false); } this.pendingFormioChanges = this.pendingFormioChanges.filter(change => { return !changes.includes(change); }); // this makes sure that any onChanges listeners are aware of the changes to this.application this.application = { ...this.application }; this.toggleSaving(false); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'common:textErrorSavingChanges', {}, 'There was an error saving the changes' )); } return true; } async saveRefFields ( refChangeTracker: ReferenceFieldsUI.RefResponseMap ): Promise { const response = this.referenceFieldService.adaptFormioChangesForSave( refChangeTracker, this.currentResponse.applicationFormId, this.id, this.currentResponse.formRevisionId, false, this.currentFormAudience === FormAudience.MANAGER ); const passResponse = await this.applicationFormService.handleSaveOfChangedFields( response, this.id, this.form.formId, this.currentResponse.formRevisionId, this.currentResponse.applicationFormId, this.isManagerEditingApplicantForm, this.isEdit ); this.toggleSaving(false); if (passResponse.standardPassed && passResponse.tablePassed) { return response.needToSaveFileIds ? ReferenceFieldsUI.SaveRefFieldsResponse.NeedToSaveFileIds : ReferenceFieldsUI.SaveRefFieldsResponse.NoNeedToSaveFileIds; } else { return ReferenceFieldsUI.SaveRefFieldsResponse.Error; } } async saveApplication ( isDraft = true, submittingApplication = false, workflowLevelId?: number, userSignatureId?: number, userSignatureBypassed?: boolean ): Promise { const specialHandling = this.specialHandlingService.getSpecialHandlingForSave( this.application.specialHandling, this.application.defaultSpecialHandling ); const designationValid = this.validatorsService.getDesignationValidity(this.application.designation); const specialHandlingValid = this.specialHandlingService.isSpecialHandlingValid( this.application.specialHandling ); const nominationValid = !this.isNominationForm || this.nomineeFormValid; if (specialHandlingValid && nominationValid && designationValid) { // save fails if not valid const { reviewerRecommendedFundingAmountRequired, decisionRequired, amountRequestedRequired, careOfRequired, paymentDesignationRequired } = this.formHelperService.getRequiredStandardFields( this.currentResponse.formDefinition ); const currencyRequested = this.application.currencyRequested || this.clientSettingsService.defaultCurrency; const data: SaveFormResponse = { ...specialHandling, formId: this.form.formId, formRevisionId: this.currentResponse.formRevisionId, fileIds: this.formHelperService.getFileIDsFromForm( this.currentResponse.formDefinition, specialHandling.specialHandlingFileUrl, this.application.referenceFields ), nominee: this.isNominationForm ? this.nominationForm : null, isDraft, applicationFormId: this.currentResponse.applicationFormId, revisionNotes: '', careOf: this.application.careOf, amountRequested: this.application.amountRequestedForEdit || 0, saveAmountRequestedInDefaultCurrency: this.forceDefaultCurrency, currencyRequested, paymentDesignation: TextFriendlySpecialCharCleaner( this.application.designation || this.application.paymentDesignation ), decision: this.application.decision, reviewerRecommendedFundingAmount: this.application.reviewerRecommendedFundingAmount, inKindItems: this.getInKindItemsForSave( this.application.inKindItems ), requiredReferenceFieldKeys: this.formHelperService.getRequiredReferenceFieldKeys( this.conditionalVisibilityState, this.currentResponse.formDefinition ), reviewerRecommendedFundingAmountRequired, decisionRequired, amountRequestedRequired, careOfRequired, paymentDesignationRequired, editingApplicationView: this.isEdit, submittingApplication, workflowLevelId, userSignatureId, userSignatureBypassed }; if (this.isNominationForm) { data.nominee = { ...this.nominationForm, orgId: this.application.organizationId }; } const id = (!this.isNomination && this.isNominationForm) ? this.nomination.applicationId : this.id; const { automaticallyRouted } = await this.applicationFormService.saveFormResponse( data, id ); if (automaticallyRouted) { this.applicationActionService.showAutomaticallyRoutedToaster( this.isNomination ); } } } async submit (): Promise { let passed = false; this.toggleSaving(true); const isValid = await this.formSubmit.emit(); if (this.isNominationForm && !this.nomineeFormValid) { this.markForValidity = true; } else { this.markForValidity = false; } if (isValid) { if (this.markForValidity) { this.formInvalidError(); this.hideForm = true; // Formio thinks submit was successful so it hides the form / submit button. // Set timeout will re-render and force it to show. setTimeout(() => { this.hideForm = false; }); passed = false; } else { passed = await this.proceedWithSubmission(); } } else { this.formInvalidError(); passed = false; } this.toggleMask(true); this.toggleSaving(false); return passed; } async viewSignatureModal () { this.spinnerService.startSpinner(); const blob = await this.fileService.getBlob(this.formSignature.userSignatureUrl); const blobUrl = this.fileService.convertFileToUrl(blob as File); // This content is from a trusted source (our API) const cleanUrl = this.sanitizer.bypassSecurityTrustUrl(blobUrl) as string; this.spinnerService.stopSpinner(); return this.modalFactory.open( ViewSignatureModalComponent, { signatureUrl: cleanUrl, signedDateString: this.formSignature.signedDate, signedByName: this.formSignature.signedByName, signedByEmail: this.formSignature.signedByEmail, ipAddress: this.formSignature.ipAddress } ); } signatureModal (): Promise { const { supportsBypass, signatureDescription } = this.signatureService.getSignatureFormDescriptionManagerPortal( this.form.name, this.currentFormAudience, this.form.signatureDescription, this.isNomination ); return this.modalFactory.open( SignatureModalComponent, { savedSignature: this.savedSignatureFile, supportsBypass, signatureDescription } ); } async handleSignature () { let signatureIsValid = !this.form.requireSignature; let userSignatureId: number; let userSignatureBypassed: boolean; let proceed = true; if (this.form.requireSignature) { const signatureResponse = await this.signatureModal(); if (signatureResponse) { if (signatureResponse.type !== Bypass) { /** Handle Signature */ this.spinnerService.startSpinner(); userSignatureId = await this.signatureService.handleSignatureModalResponse( signatureResponse ); this.spinnerService.stopSpinner(); signatureIsValid = !!userSignatureId; } else { /** Bypass */ signatureIsValid = true; userSignatureBypassed = true; } } else { /** Cancel */ proceed = false; } } return { signatureIsValid, userSignatureBypassed, userSignatureId, proceed }; } async proceedWithSubmission (): Promise { let submitPassed = false; const { signatureIsValid, userSignatureBypassed, userSignatureId, proceed } = await this.handleSignature(); if (proceed) { if (signatureIsValid) { if (this.isEditModeForDraft) { const response = await this.openSubmitApplicationModal( userSignatureId, userSignatureBypassed ); if (response) { this.cancelEditMode(); } } else { this.cancelEditMode(); this.spinnerService.startSpinner(); submitPassed = await this.doSubmission( null, userSignatureId, userSignatureBypassed ); this.spinnerService.stopSpinner(); } } } return submitPassed; } async doSubmission ( workflowLevelId?: number, userSignatureId?: number, userSignatureBypassed?: boolean ) { let submitPassed = false; const submittingApplication = this.isEditModeForDraft; try { await this.saveApplication( false, submittingApplication, workflowLevelId, userSignatureId, userSignatureBypassed ); if (submittingApplication) { this.notifier.success(this.i18n.translate( this.isNomination ? 'APPLY:textSuccessSubmittingNomination' : 'APPLY:textSuccessSubmittingApplication', {}, this.isNomination ? 'Successfully submitted the nomination' : 'Successfully submitted the application' )); } else { this.notifier.success(this.i18n.translate( 'APPLICATION:textSuccessfullySubmittedForm', {}, 'Successfully submitted the form' )); } submitPassed = true; } catch (e) { this.logger.error(e); if (submittingApplication) { this.notifier.error(this.i18n.translate( this.isNomination ? 'APPLY:textErrorSubmittingNomination' : 'APPLY:textErrorSubmittingApplication', {}, this.isNomination ? 'There was an error submitting the nomination' : 'There was an error submitting the application' )); } else { this.notifier.error(this.i18n.translate( 'APPLICATION:textErrorSubmittingForm', {}, 'There was an error submitting the form' )); } } await this.updatePanels(); return submitPassed; } async updatePanels () { const canView = this.isEdit ? true : await this.applicationViewService.canViewApplication(this.id); if (!canView && !this.hasAllPermission) { this.router.navigate([this.dashboardService.homeRoute]); } else { await this.applicationViewService.setApplicationViewMap( this.id, this.isEdit ); await this.updateApplication(); } } async setFormAndCurrentResponse ( formToSet: ApplicationViewFormForUI|ApplicantFormForUI ) { if (formToSet) { this.form = formToSet; // When in edit mode, GMs can answer in the requested currency // But we force GMs in app view to use default currency this.isManagerEditingApplicantForm = !this.isEdit && (this.currentFormAudience === FormAudience.APPLICANT); this.forceDefaultCurrency = this.formHelperService.shouldForceDefaultCurrencyInAmountRequested( this.currentFormAudience === FormAudience.MANAGER, this.isManagerEditingApplicantForm ); this.application = { ...this.application, amountRequestedForEdit: this.forceDefaultCurrency ? this.application.amountRequested : this.application.currencyRequestedAmountEquivalent }; if (this.isEdit) { const response = (formToSet as ApplicantFormForUI).formResponse; await this.setCurrentResponse((formToSet as ApplicantFormForUI).formResponse); this.allCurrentResponses = [response]; } else if ('responses' in formToSet) { if (this.completedByMeSection) { const found = formToSet.responses.find((res) => { return res.createdBy?.email === this.userEmail; }); await this.setCurrentResponse(found); } else { if (this.isNomination) { await this.setCurrentResponse( formToSet.responses[0] || formToSet.otherLevelResponses[0] ); } else { await this.setCurrentResponse( formToSet.formType === FormTypes.NOMINATION ? formToSet.nominationResponses[0] : formToSet.responses[0] || formToSet.otherLevelResponses[0] ); } } this.allCurrentResponses = ( this.form as ApplicationViewFormForUI).responses.concat( (this.form as ApplicationViewFormForUI).otherLevelResponses ).concat((this.form as ApplicationViewFormForUI).nominationResponses); } const foundForm = this.detailForms.find((form) => { return form.formId === this.form.formId; }); this.canRequestRevision = foundForm ? foundForm.canRequestRevision : false; this.setCanSendToApplicant(); } } setCanSendToApplicant () { this.canSendToApplicant = !this.editing && (this.currentFormAudience === FormAudience.APPLICANT) && (this.application.applicationStatus !== ApplicationStatuses.Declined) && !this.application.isArchived && this.currentResponse && (this.currentResponse.applicationFormStatusId === FormStatuses.NotSent); } async onPanelClick ({ form, completedByMeSection }: Partial) { this.spinnerService.startSpinner(); this.setApplication(); this.goToForm(form.formId); this.completedByMeSection = completedByMeSection; await this.setFormAndCurrentResponse(form); this.setFormComments(); this.showNoFormsToComplete = false; this.showNoFormsToView = false; this.spinnerService.stopSpinner(); this.analyticsService.emitEvent({ eventName: 'From selected', eventType: EventType.Click, extras: { form: form.name } }); } goToForm (formId: number|string) { if (+this.formId !== +formId) { const route = `${this.basePath}/form/${formId}`; this.router.navigate([route], { replaceUrl: true }); } } onEmptyPanelClick (section: PanelSection) { this.form = null; this.goToForm('no-form'); if (section.noPanelsMessage === this.noPanelsMessageOne) { this.showNoFormsToComplete = true; this.showNoFormsToView = false; } else { this.showNoFormsToView = true; this.showNoFormsToComplete = false; } } async sendDraftToApplicant () { this.spinnerService.startSpinner(); const passed = await this.offlineGrantsService.sendDraftToApplicant(this.id); if (passed) { this.cancelEditMode(true); } this.spinnerService.stopSpinner(); } async openSubmitApplicationModal ( userSignatureId: number, userSignatureBypassed: boolean ) { const defaultWorkflowLevelId = await this.programService.getDefaultWflId( this.application.programId ); const response = await this.modalFactory.open( OfflineGrantsSubmitApplicationModalComponent, { isNomination: this.isNomination, workflow: await this.workflowService.getAndSetWorkflow( this.application.currentWorkFlowId ), defaultWorkflowLevelId } ); if (response) { this.spinnerService.startSpinner(); const passes = await this.doSubmission( response.workflowLevelId, userSignatureId, userSignatureBypassed ); this.spinnerService.stopSpinner(); if (passes && response.approveAfterSubmit) { this.applicationViewService.triggerApproveModal(); } } return response; } async canDeactivate () { if ( this.editing && this.formStatusBeforeEdit === FormStatuses.Submitted ) { const response = await this.modalFactory.open( UnsavedChangesModalComponent, { confirmText: this.i18n.translate( 'common:textUnsavedChangesSubmittedForm', {}, 'You have unsaved changes made to your submitted form. Do you want to leave the current page?' ) } ); if (response) { switch (response) { case GCDashboards.UnsavedChangesResponse.SAVE_AND_LEAVE: const passed = await this.submit(); if (passed) { return true; } return false; case GCDashboards.UnsavedChangesResponse.STAY: default: return false; } } else { return false; } } else { return true; } } handleSaveAsDraftForEditMode () { this.cancelEditMode(true); } ngOnDestroy () { if (this.sub) { this.sub.unsubscribe(); } } }