import { Component, Input, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { ApplicantService } from '@core/services/auth-user/applicant.service'; import { PolicyService } from '@core/services/policy.service'; import { SpinnerService } from '@core/services/spinner.service'; import { StatusService } from '@core/services/status.service'; import { TranslationService } from '@core/services/translation.service'; import { ApplicationEditDetail, ApplicationEditMap, ApplicationViewDetail, ApplicationViewMap, ApplicationViewPage, ApproveDeclineModalResponse, ApproveDeclinePayload, NotifyOfStatusForApi } from '@core/typings/application.typing'; import { BudgetDetail, BudgetFundingSource, FundingSourceTypes } from '@core/typings/budget.typing'; import { UserTypes } from '@core/typings/client-user.typing'; import { Collaborator } from '@core/typings/collaboration.typing'; import { CsrConnectStats } from '@core/typings/organization.typing'; import { ProcessingTypes } from '@core/typings/payment.typing'; import { ApplicationStatuses } from '@core/typings/status.typing'; import { CyclesUI } from '@core/typings/ui/cycles.typing'; import { WorkflowManagerActions } from '@core/typings/workflow.typing'; import { ApplicationActivityFromApi } from '@features/application-activity/activity.typing'; import { ApplicationActivityService } from '@features/application-activity/application-activity.service'; import { RecentActivityModalComponent } from '@features/application-activity/recent-activity-modal/recent-activity-modal.component'; import { ApplicationDownloadService } from '@features/application-download/application-download.service'; import { ApplicationFormService } from '@features/application-forms/services/application-forms.service'; import { AppManagerAction } from '@features/application-manager/application-manager.typing'; import { DeleteApplicationModalComponent } from '@features/application-manager/delete-application-modal/delete-application-modal.component'; import { MailMergeModalComponent } from '@features/application-manager/mail-merge-modal/mail-merge-modal.component'; import { NotifyStatusFormGroup, NotifyStatusModalComponent } from '@features/application-manager/notify-status-modal/notify-status-modal.component'; import { ApplicationActionService } from '@features/application-manager/services/application-actions/application-actions.service'; import { ApplicationManagerService } from '@features/application-manager/services/application-manager/application-manager.service'; import { UpdateProgramModalComponent } from '@features/application-manager/update-program-modal/update-program-modal.component'; import { UpdateStatusModalComponent } from '@features/application-manager/update-status-modal/update-status-modal.component'; import { ReminderModalComponent } from '@features/application-view/reminder-modal/reminder-modal.component'; import { ApproveAwardPayModalComponent } from '@features/approve-award-pay/approve-award-pay-modal/approve-award-pay-modal.component'; import { AttachExternalEmailModalComponent } from '@features/attach-external-email/attach-external-email-modal/attach-external-email-modal.component'; import { AwardService } from '@features/awards/award.service'; import { SimpleAwardModalComponent } from '@features/awards/simple-award-modal/simple-award-modal.component'; import { Award, AwardModalResponse } from '@features/awards/typings/award.typing'; import { BudgetAssignmentsService } from '@features/budget-assignments/budget-assignments.service'; import { BudgetService } from '@features/budgets/budget.service'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { CollaborationService } from '@features/collaboration/collaboration.service'; import { CommunicationsService } from '@features/communications/communications.service'; import { ApplicationViewFormForUI, CompletionRequirementType, FormAudience } from '@features/configure-forms/form.typing'; import { DashboardsService } from '@features/dashboards/dashboards.service'; import { EmployeeSSOFieldsService } from '@features/employee-sso-fields/employee-sso-fields.service'; import { NonprofitService } from '@features/nonprofit/nonprofit.service'; import { CreateEditApplicationModalComponent } from '@features/offline-grants/offline-grants-create-edit-application/create-edit-application-modal/create-edit-application-modal.component'; import { OfflineGrantsService } from '@features/offline-grants/offline-grants.service'; import { ProgramService } from '@features/programs/program.service'; import { EmailService } from '@features/system-emails/email.service'; import { SystemTags } from '@features/system-tags/typings/system-tags.typing'; import { ArrayHelpersService, DebounceFactory, OrganizationEligibleForGivingStatus, PaginationOptions, Tab } from '@yourcause/common'; import { CachedAttr, CACHE_TYPES } from '@yourcause/common/cache'; import { I18nService } from '@yourcause/common/i18n'; import { ModalFactory } from '@yourcause/common/modals'; import moment from 'moment'; import { skip, Subscription } from 'rxjs'; import { ApproveDeclineModalComponent } from '../../application-manager/approve-decline-modal/approve-decline-modal.component'; import { RouteApplicationModalComponent } from '../../application-manager/route-application-modal/route-application-modal.component'; import { ArchiveModalComponent } from '../../archive/archive-modal/archive-modal.component'; import { ApplicationAttachmentService } from '../application-attachments/application-attachments.service'; import { ApplicationViewService } from '../application-view.service'; import { BudgetSummaryModalComponent } from '../budget-summary-modal/budget-summary-modal.component'; import { LinkHRDataModalComponent } from '../link-hr-data-modal/link-hr-data-modal.component'; import { RecommendFundingModalComponent } from '../recommend-funding-modal/recommend-funding-modal.component'; import { UsersInWflModalComponent } from '../users-in-wfl-modal/users-in-wfl-modal.component'; @Component({ selector: 'gc-application-view-page', templateUrl: './application-view-page.component.html', styleUrls: ['./application-view-page.component.scss'], encapsulation: ViewEncapsulation.None }) export class ApplicationViewPageComponent implements OnInit, OnDestroy { @Input() isEdit = false; @Input() idForEdit: number; Buckets = SystemTags.Buckets; showPanel: number; title = ''; description = ''; statusMap = this.statusService.applicationStatusMap; tabs: Tab[] = []; application: ApplicationViewPage; csrData: CsrConnectStats; ApplicationStatuses = ApplicationStatuses; actions: AppManagerAction[]; isMasked = false; showMaskedApplicantIcon = 'user-shield'; hideMaskedApplicantIcon = 'user-times'; WorkflowManagerActions = WorkflowManagerActions; hideMaskedApplicantTooltip = this.i18n.translate( 'MANAGE:textShowApplicantInformation', {}, 'Show applicant information' ); sendReminderText = this.i18n.translate( 'FORMS:textSendReminder', {}, 'Send reminder' ); notifyOfStatusText = this.i18n.translate( 'APPLY:textNotifyOfStatus', {}, 'Notify of status' ); showMaskedApplicantTooltip = this.i18n.translate( 'MANAGE:textHideApplicantInformation', {}, 'Hide applicant information' ); updateStatusText = this.i18n.translate( 'APPLY:textUpdateStatus', {}, 'Update status' ); mailMergeText = this.i18n.translate( 'CONFIG:textAddMergeDocument', {}, 'Add merge document' ); createAwardText = this.i18n.translate( 'APPLY:btnCreateAward', {}, 'Create award' ); budgetAssignmentText = this.i18n.translate( 'BUDGET:btnBudgetAssignment', {}, 'Budget assignment' ); linkHRDataText = this.i18n.translate( 'GLOBAL:textLinkHRData', {}, 'Link HR data' ); viewAllActivityText = this.i18n.translate( 'GLOBAL:textViewAllActivity', {}, 'View all activity' ); @CachedAttr( CACHE_TYPES.SESSION, false, undefined, undefined, (self: ApplicationViewPageComponent) => { return `_yc_ShowMaskedApplicant_${self.id}`; } ) showMaskedApplicant: boolean; topLevelCustomIcon: string; topLevelCustomTooltip = ''; createdText = ''; viewTranslations = this.translationService.viewTranslations; programTranslationMap = this.viewTranslations.Grant_Program; cycleTranslationMap = this.viewTranslations.Grant_Program_Cycle; hideAwardTab: boolean; availableAwardTypes: FundingSourceTypes[]; createAwardDisabled = false; passesVetting = false; cycle: CyclesUI.ProgramCycle; hasAllPermission = this.policyService.grantApplication.canManageAllApplications() || this.policyService.grantApplication.canTakeActionsOnAllApps(); canManageUsers = this.policyService.system.canManageUsers(); sub = new Subscription(); budgetFundClass = 'mr-2'; OrganizationEligibleForGivingStatus = OrganizationEligibleForGivingStatus; ProcessingTypes = ProcessingTypes; constructor ( private i18n: I18nService, private applicationManagerService: ApplicationManagerService, private activatedRoute: ActivatedRoute, private modalFactory: ModalFactory, private statusService: StatusService, private spinnerService: SpinnerService, private router: Router, private awardService: AwardService, private nonprofitService: NonprofitService, private budgetService: BudgetService, private programService: ProgramService, private translationService: TranslationService, private dashboardService: DashboardsService, private policyService: PolicyService, private emailService: EmailService, private applicationAttachmentService: ApplicationAttachmentService, private arrayHelpers: ArrayHelpersService, private applicationDownloadService: ApplicationDownloadService, private budgetAssignmentService: BudgetAssignmentsService, private applicantService: ApplicantService, private employeeSSOFieldsService: EmployeeSSOFieldsService, private clientSettingsService: ClientSettingsService, private collaborationService: CollaborationService, private offlineGrantsService: OfflineGrantsService, private communicationsService: CommunicationsService, private applicationActivityService: ApplicationActivityService, private applicationFormService: ApplicationFormService, private applicationViewService: ApplicationViewService, private applicationActionService: ApplicationActionService ) { } get applicationMap () { return this.isEdit ? this.applicationViewService.applicationEditMap : this.applicationViewService.applicationViewMap; } get id () { return this.isEdit ? this.idForEdit : this.activatedRoute.snapshot.params.id; } get applicantMaskedText () { return this.isNomination ? this.i18n.translate( 'GLOBAL:textNominatorNameIsMasked', {}, 'Nominator name is masked' ) : this.i18n.translate( 'GLOBAL:textApplicantNameIsMasked', {}, 'Applicant name is masked' ); } get clientSettings () { return this.clientSettingsService.clientSettings; } get formsForUser () { const forms = this.applicationFormService.formsForUser; if (forms) { return (forms || []).join(', '); } return ''; } get optionalFormsForUser () { const forms = this.applicationFormService.optionalFormsForUser; if (forms) { return (forms || []).join(','); } return ''; } get applicant () { return { id: this.application.applicantId, firstName: this.application.applicantFirstName, lastName: this.application.applicantLastName, email: this.application.applicantEmail, profileImageUrl: this.application.applicationProfileImageUrl, canManageApplicants: false, canReceiveEmails: false, isApplicationOwner: true, addressString: this.application.applicantAddress, phoneNumber: this.application.applicantPhoneNumber, routerLink: [ `${this.basePath}/applicant-profile/${this.application.applicantId}/applications` ] }; } get awards () { if (this.applicationMap[this.id]) { return this.applicationMap[this.id].awards.awards; } return []; } get existingAwardTypes () { return this.awards.map((award) => award.awardType); } 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 applicantName () { return this.application.applicantFirstName + ' ' + this.application.applicantLastName; } get orgName () { return this.application.organizationName; } get programName () { return this.application.programName; } get cycleName () { return this.cycleTranslationMap[this.application?.grantProgramCycle.id]?.Name; } get isNomination () { return location.pathname.includes('nomination'); } get budgetDetail (): BudgetDetail { if (this.applicationMap[this.id]) { return this.applicationMap[this.id].budget; } return {} as BudgetDetail; } get budgetFundingSource (): BudgetFundingSource { if (this.applicationMap[this.id]) { return this.applicationMap[this.id].budgetFundingSource; } return {} as BudgetFundingSource; } 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 []; } get showBudgetAssigned () { if (this.application) { return this.application.currentWorkflowLevelShowBudgetSummaryInfo && ( this.application.budgetAssignmentAutomationRuleSetId || this.application.budgetId ); } return false; } get editingForm () { return this.applicationFormService.editingForm; } async ngOnInit () { await this.setProgram(); this.setCsrData(); this.setTabs(); if (this.showBudgetAssigned) { this.getBudgetFundsClass(); } this.sub.add( this.applicationViewService.changesTo$('applicationViewMap') .subscribe(() => { this.applicationChanged(); }) ); this.sub.add( this.applicationViewService.changesTo$('applicationEditMap') .subscribe(() => { this.applicationChanged(); }) ); this.sub.add( this.applicationViewService.changesTo$('triggerApproveModal') .pipe(skip(1)).subscribe(() => { this.approveAwardPayModal(true); }) ); } applicationChanged () { this.setTabs(); if (this.hideAwardTab && location.pathname.includes('awards')) { this.reloadForms(); } this.actions = this.setupActions(this.application); } async setProgram () { this.spinnerService.startSpinner(); const application = this.applicationMap[this.id].application; this.cycle = await this.programService.getCycleFromProgram( application.programId, application.grantProgramCycle.id ); this.availableAwardTypes = await this.awardService.getAvailableAwardTypes( application.programId, application.grantProgramCycle.id, this.cycle ); this.spinnerService.stopSpinner(); } setCsrData () { if (!this.isEdit) { const csrData = this.applicationMap[this.id] ? (this.applicationMap as ApplicationViewMap)[this.id].csrData : null; this.csrData = this.nonprofitService.getCsrStatsWithData(csrData); } } formatDate (date: string) { if (date) { return moment(date).format('ll'); } return ''; } getRouterLinkForApplicant = (collab: Collaborator) => { return [ `${this.basePath}/applicant-profile/${collab.id}/applications` ]; }; async openBudgetSummaryModal () { const awardAmount = this.applicationMap[this.id].awards?.totalAmount > 0 ? this.applicationMap[this.id].awards?.totalAmount : 0; const deps = { budget: this.budgetDetail, programId: this.application.programId, budgetFundingSource: this.budgetFundingSource, amountRequested: this.application.amountRequested, awardAmount, applicationId: this.application.applicationId, isReserved: this.application.reservedFunds }; await this.modalFactory.open( BudgetSummaryModalComponent, deps ); } async createAwardModal (award?: Award) { const deps = { originalAward: award, applicantCanReceiveEmails: this.application.applicantCanReceiveEmails, amountRequested: this.application.amountRequested, requestedItems: this.application.inKindItems, programId: this.application.programId, appDesignation: this.application.designation, currencyRequested: this.application.currencyRequested, currencyRequestedAmountEquivalent: this.application.currencyRequestedAmountEquivalent, latestVettingRequestStatusForOrg: this.application.latestVettingRequestStatusForOrg, existingAwardTypes: this.existingAwardTypes, cycleId: this.application.grantProgramCycle.id, applicationId: +this.id, assignedBudgetId: this.application.budgetId, assignedFsId: this.application.fundingSourceId, reservedFunds: this.application.reservedFunds, specialHandling: this.application.specialHandling, organizationEligibleForGivingStatus: this.application.organizationEligibleForGivingStatus, recommendedFundingAmount: this.application.recommendedFundingAmount, isMasked: this.application.isMasked && !this.application.canViewMaskedApplicantInfo, organization: this.applicationMap[this.id].organization, applicant: this.applicationMap[this.id].primaryApplicant }; const response = await this.modalFactory.open( SimpleAwardModalComponent, deps ); if (response) { await this.handleAwardUpdate(response); } } async handleAwardUpdate (response: AwardModalResponse) { this.spinnerService.startSpinner(); const attachments = await this.emailService.returnIdsFromMixedAttachments( response.emailOptionsModel.attachments, this.id ); const emailOptionsModel = { ...response.emailOptionsModel, attachments }; const payload = { ...response, emailOptionsModel }; const awardId = await this.awardService.handleAwardModalUpdates( payload, this.id ); await this.budgetService.resetBudgetDetail(this.cycle.budgetIds); await this.updateApplication(true); if (response.addAnotherPayment) { const awards = this.awardService.constructAwards(this.awards); const found = awards.find((award) => award.id === awardId); if (found) { this.createAwardModal(found); } } this.spinnerService.stopSpinner(); } async updateApplication ( navigateToAward = false ) { const canView = 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); this.setTabs(); this.setAwardAttrs(); const goToAward = navigateToAward && !this.hideAwardTab; if (goToAward) { this.router.navigate([`${this.basePath}/awards`]); } else if (location.pathname.includes('form')) { this.reloadForms(); } this.actions = this.setupActions(this.applicationMap[this.id].application); } } private reloadForms () { const route = `${this.basePath}/form/reload`; this.router.navigate([route]); } setTabs () { this.application = this.applicationMap[this.id] ? this.applicationMap[this.id].application : null; this.passesVetting = this.awardService.checkVettingRequirementForAward( this.application.organizationEligibleForGivingStatus, this.cycle.isClientProcessing ? ProcessingTypes.Client : ProcessingTypes.YourCause, this.cycle.budgetIds, this.application.amountRequested, this.application.inKindAmountRequested, this.application.recommendedFundingAmount, true ); const canCreateAward = this.awardService.hasClientProcessorsToCreateAward( this.passesVetting, this.cycle.budgetIds, this.availableAwardTypes, this.existingAwardTypes ); this.createAwardDisabled = !canCreateAward; this.setAwardAttrs(); this.isMasked = this.application ? this.application.isMasked : false; if (this.isMasked) { this.setMaskedIconAndTooltip(); } else { this.setCreatedText(); } this.setTitleAndDescription(); if (this.application) { const formId = this.findInitalFormIdToDisplay(); this.tabs = [{ link: `${this.basePath}/form/${formId || 'no-form'}`, labelKey: this.isEdit ? (this.isNomination ? 'common:hdrNominatorForms' : 'common:hdrApplicantForms') : (this.isNomination ? 'APPLY:textNominationForms' : 'APPLY:textApplicationForms'), label: this.isEdit ? (this.isNomination ? 'Nominator Forms' : 'Applicant Forms'): (this.isNomination ? 'Nomination Forms' : 'Application Forms'), icon: 'file-alt', activeRoutes: ['no-form', 'form'] }, { link: `${this.basePath}/awards`, labelKey: 'common:hdrAwards', label: 'Awards', icon: 'gift', hidden: this.hideAwardTab }, !this.isEdit && (!this.isMasked || this.application.canViewMaskedApplicantInfo) ? { link: `${this.basePath}/communications/`, labelKey: 'GLOBAL:textCommunications', label: 'Communications', icon: 'comments' } : null, !this.isEdit ? { link: `${this.basePath}/activity`, labelKey: 'common:textActivity', label: 'Activity', icon: 'history' } : null, !this.isEdit && (!this.isMasked || this.application.canViewMaskedApplicantInfo) ? { link: `${this.basePath}/attachments`, labelKey: 'common:textAttachments', label: 'Attachments', icon: 'paperclip' } : null].filter(item => !!item); } if ( location.pathname.includes('awards') && this.hideAwardTab ) { this.router.navigate([`${this.basePath}/form/no-form`]); } } findInitalFormIdToDisplay () { if (this.isEdit) { const found = this.formsForEdit.find((form) => { return form.formId === (this.applicationMap as ApplicationEditMap)[this.id].defaultFormId; }); return found?.formId || this.formsForEdit[0]?.formId; } else { const foundForm = this.formsForAppView.find((form) => { return ( (form as ApplicationViewFormForUI).audience === FormAudience.MANAGER && (form as ApplicationViewFormForUI).completionRequirementType !== CompletionRequirementType.NONE ); }); return foundForm?.formId || this.formsForAppView[0]?.formId; } } setAwardAttrs () { this.hideAwardTab = this.awardService.shouldHideAwardTab( this.applicationMap[this.id].application, this.applicationMap[this.id].awards.awards, this.isNomination ); } toggleShowMaskedApplicants () { this.showMaskedApplicant = !this.showMaskedApplicant; this.setMaskedIconAndTooltip(); this.setTitleAndDescription(); } setMaskedIconAndTooltip () { this.topLevelCustomIcon = this.showMaskedApplicant ? this.hideMaskedApplicantIcon : this.showMaskedApplicantIcon; this.topLevelCustomTooltip = this.showMaskedApplicant ? this.showMaskedApplicantTooltip : this.hideMaskedApplicantTooltip; this.setCreatedText(); } setCreatedText () { if (this.application.createdBy) { const createdByName = this.application.createdBy.firstName + ' ' + this.application.createdBy.lastName; if ( ( this.application.canViewMaskedApplicantInfo && (!this.isMasked || this.showMaskedApplicant) ) || this.application.createdBy.userType === UserTypes.MANAGER ) { this.createdText = this.i18n.translate( 'GLOBAL:textCreatedByOnDynamic', { name: createdByName, date: this.formatDate(this.application.createdDate) }, 'Created by __name__ on __date__' ); } else { this.createdText = this.i18n.translate( 'GLOBAL:textCreatedOnDynamic', { date: this.formatDate(this.application.createdDate) }, 'Created on __date__' ); } } else { this.createdText = this.i18n.translate( 'GLOBAL:textCreatedThroughNominationProcess', { date: this.formatDate(this.application.createdDate) }, 'Created through nomination process on __date__' ); } } setTitleAndDescription () { if (this.application) { if (this.orgName) { if (!this.isMasked || this.showMaskedApplicant) { this.title = this.i18n.translate( this.isNomination ? 'APPLY:textApplicationNominatingDynamic' : 'APPLY:textApplicationOnBehalfOfDynamic', { name: this.applicantName, organization: this.orgName }, `__name__ ${ this.isNomination ? 'nominating' : 'on behalf of' } __organization__` ); } else { this.title = this.orgName + ' (' + this.applicantMaskedText + ')'; } } else { if ( this.application.canViewMaskedApplicantInfo && (!this.isMasked || this.showMaskedApplicant) ) { this.title = this.applicantName; } else { this.title = this.applicantMaskedText; } } this.description = ` ${this.i18n.translate( this.isNomination ? 'GLOBAL:textNominationID' : 'common:textApplicationId', {}, this.isNomination ? 'Nomination ID' : 'Application ID' )}: ${this.id} ${this.i18n.translate( 'common:lblProgram' )}: ${this.programName} | ${this.cycleName} `; } } async approveAwardPayModal (isAppManager = false) { this.spinnerService.startSpinner(); const canProceed = await this.awardService.prepareApproveAwardPayModal( this.application.currentWorkFlowId, this.application.currentWorkFlowLevelId, this.application.programId, this.application.amountRequested, this.application.inKindAmountRequested, isAppManager, this.application.organizationEligibleForGivingStatus, this.application.grantProgramCycle.id, this.application.recommendedFundingAmount, this.application.isArchived ); this.spinnerService.stopSpinner(); if (canProceed) { const response = await this.modalFactory.open( ApproveAwardPayModalComponent, { applicationId: this.application.applicationId, programId: this.application.programId } ); if (response) { if (response.awards) { this.spinnerService.startSpinner(); const success = await this.awardService.handleApproveAwardPayModal( response, isAppManager ); if (success) { await this.updateApplication(); } this.spinnerService.stopSpinner(); } else { this.handleApproveDeclineModal( { comment: '', notifyApplicant: response.sendEmail, customMessage: response.customMessage, clientEmailTemplateId: response.clientEmailTemplateId, emailOptionsModel: response.emailOptionsModel }, 'Approve', isAppManager ); } } } else { this.approveDeclineModal('Approve', isAppManager); } } async approveDeclineModal (type: 'Approve'|'Decline', isAppManager = false) { const deps = { type, single: true, isNomination: this.isNomination, nominees: [this.applicationMap[this.id].nominee], programId: this.application.programId, applicationIds: [+this.id] }; const response = await this.modalFactory.open( ApproveDeclineModalComponent, deps ); if (response) { await this.handleApproveDeclineModal(response, type, isAppManager); } } async handleApproveDeclineModal ( response: ApproveDeclineModalResponse, type: 'Approve'|'Decline', isAppManager = false ) { this.spinnerService.startSpinner(); const attachments = await this.emailService.returnIdsFromMixedAttachments( response.emailOptionsModel.attachments, this.id ); const emailOptionsModel = { ...response.emailOptionsModel, attachments }; const data: ApproveDeclinePayload = { applicationIds: [this.id], sendEmail: response.notifyApplicant, comments: response.comment, clientEmailTemplateId: response.notifyApplicant ? response.clientEmailTemplateId : undefined, emailOptionsModel }; if (this.isNomination && type === 'Approve') { data.nominationApprovalGrantProgramId = response.grantProgram; data.nominationInviteCustomMessage = response.notifyApplicant ? response.customMessage : ''; data.isEmployeeOfClient = response.isEmployeeOfClient; } else { data.customMessage = response.customMessage; } const success = await this.applicationActionService.handleApproveDeclineModal( type, data, isAppManager, this.isNomination ); if (success) { await this.updateApplication(); } this.spinnerService.stopSpinner(); } async routeModal (isAppManager = false) { const deps = { workflowLevelRoutes: this.application.workflowLevelRoutes, isNomination: this.isNomination }; const response = await this.modalFactory.open( RouteApplicationModalComponent, deps ); if (response) { this.spinnerService.startSpinner(); const data = { workflowLevelId: response.level, comments: response.comment }; await this.applicationActionService.handleRouteApplication( this.id, data, isAppManager ); await this.updateApplication(); this.spinnerService.stopSpinner(); } } async reminderModal () { const deps = { workflowId: this.application.currentWorkFlowId, workflowLevelId: this.application.currentWorkFlowLevelId, programId: this.application.programId, isNomination: this.isNomination, applicationId: this.id }; const response = await this.modalFactory.open( ReminderModalComponent, deps ); if (response) { this.spinnerService.startSpinner(); await this.applicationFormService.sendReviewReminder( this.id, response ); this.spinnerService.stopSpinner(); } } async activitiesModal () { await this.modalFactory.open( RecentActivityModalComponent, { showAppId: this.id, tableDataFactory: DebounceFactory.createSimple( (options: PaginationOptions) => { return this.applicationActivityService.getAppActivity(this.id, options); }, 0 ), defaultCurrency: this.clientSettingsService.defaultCurrency } ); } async linkHRDataModal () { const employeeSSOFields = this.employeeSSOFieldsService.getEmployeeSSOFieldsForLookup(); const result = await this.modalFactory.open( LinkHRDataModalComponent, { applicationId: this.application.applicationId, employeeSSOFields, clientBranding: this.clientSettingsService.clientBranding } ); if (result) { this.spinnerService.startSpinner(); await this.applicantService.linkHRData(result); await this.updateApplication(); await this.collaborationService.fetchCollaborators(this.application.applicationId); this.spinnerService.stopSpinner(); } } async archiveModal (context: 'archiveApp'|'unarchiveApp') { const returnArchived = context === 'archiveApp' ? false : true; const applicationStats = await this.applicationManagerService.getPaymentStats( [this.id], returnArchived ); const applicationData = { ids: [this.application.applicationId], totalAwards: applicationStats.awarded, totalPayments: applicationStats.paymentsAmount, paymentsAvailableForProcessing: applicationStats.paymentsAvailableForProcessing }; const response = await this.modalFactory.open( ArchiveModalComponent, { isNomination: this.isNomination, modalData: applicationData, context } ); if (response) { this.spinnerService.startSpinner(); await this.applicationActionService.handleArchiveModalResponse(response, context); await this.updateApplication(); this.spinnerService.stopSpinner(); } } getBudgetFundsClass () { const awardAmount = this.applicationMap[this.id].awards?.totalAmount > 0 ? this.applicationMap[this.id].awards?.totalAmount : 0; const amountRequestedRemaining = this.application.amountRequested - awardAmount; const reservedAmount = this.budgetFundingSource.reservedAmount ?? 0; const budgetRemaining = reservedAmount > 0 ? this.budgetFundingSource.totalAmount - this.budgetFundingSource.totalAmountPayments - reservedAmount : this.budgetFundingSource.totalAmount - this.budgetFundingSource.totalAmountPayments; if (amountRequestedRemaining <= 0 || (budgetRemaining - amountRequestedRemaining) > 0) { this.budgetFundClass = 'text-success mr-2'; } else if (budgetRemaining <= 0) { this.budgetFundClass = 'text-danger mr-2'; } else { this.budgetFundClass = 'text-warning mr-2'; } } async notifyOfStatusModal () { const response = await this.modalFactory.open( NotifyStatusModalComponent, { isNomination: this.isNomination, single: true } ); if (response) { await this.handleNotifyStatus(response); } } async handleNotifyStatus (response: NotifyStatusFormGroup) { this.spinnerService.startSpinner(); const attachments = await this.emailService.returnIdsFromMixedAttachments( response.attachments ); const emailOptionsRequest = { ccEmails: response.cc, bccEmails: response.bcc, attachments }; const payload = { ...response, emailOptionsRequest }; const data: NotifyOfStatusForApi = { applicationIds: [this.application.applicationId], customMessage: response.customMessage, clientEmailTemplateId: response.clientEmailTemplateId, emailOptionsRequest: payload.emailOptionsRequest }; await this.applicationActionService.handleSendApplicationStatusEmail( data, this.isNomination ); this.spinnerService.stopSpinner(); } setupActions ( app: ApplicationViewPage ): AppManagerAction[] { const actions = [ { icon: { icon: 'check', class: 'mr-2 text-success' }, label: this.i18n.translate( this.isNomination ? 'APPLY:textApproveNomination' : 'GLOBAL:textApproveApplication', {}, this.isNomination ? 'Approve nomination' : 'Approve application' ), breakBefore: false, hidden: !this.applicationActionService.canTakeAction( app, WorkflowManagerActions.Approve ), exec: async () => await this.approveAwardPayModal(true) }, { icon: { icon: 'times', class: 'mr-2 text-danger' }, label: this.i18n.translate( this.isNomination ? 'APPLY:textDeclineNomination' : 'GLOBAL:textDeclineApplication', {}, this.isNomination ? 'Decline nomination' : 'Decline application' ), breakBefore: false, hidden: !this.applicationActionService.canTakeAction( app, WorkflowManagerActions.Decline ), exec: async () => this.approveDeclineModal('Decline', true) }, { icon: { icon: 'code-branch', class: 'mr-2 text-link' }, label: this.i18n.translate( this.isNomination ? 'APPLY:textRouteNomination' : 'GLOBAL:textRouteApplication', {}, this.isNomination ? 'Route nomination' : 'Route application' ), breakBefore: false, hidden: !this.applicationActionService.canTakeAction( app, WorkflowManagerActions.Route ), exec: async () => await this.routeModal(true) }, { icon: { icon: 'gift', class: 'mr-2 text-success' }, label: this.createAwardText, breakBefore: false, hidden: this.createAwardDisabled || this.isNomination || (this.application.applicationStatus !== ApplicationStatuses.Approved) || !this.applicationActionService.canTakeAction( app, WorkflowManagerActions.AwardPay ), exec: async () => await this.createAwardModal() }, { icon: { icon: 'dollar-sign', class: 'mr-2 text-link' }, label: this.budgetAssignmentText, breakBefore: false, hidden: [ ApplicationStatuses.Canceled, ApplicationStatuses.Draft ].includes(this.application.applicationStatus) || app.isArchived || this.isNomination || !this.policyService.grantApplication.canAccessApplicationManager(), exec: async () => await this.updateBudget() }, { icon: { icon: 'history', class: 'mr-2 text-link' }, label: this.viewAllActivityText, breakBefore: false, hidden: false, exec: async () => this.activitiesModal() }, { icon: { icon: 'link', class: 'mr-2 text-link' }, label: this.linkHRDataText, breakBefore: false, // hidden if GM user cannot manage users, the applicant is already SSO, or the client does not have SSO enabled hidden: !this.canManageUsers || this.application.applicantIsSSO || !this.clientSettings.hasSSO || [ ApplicationStatuses.Canceled, ApplicationStatuses.Draft ].includes(this.application.applicationStatus), exec: async () => this.linkHRDataModal() }, { icon: { icon: 'download', class: 'mr-2 text-link' }, label: this.i18n.translate( this.isNomination ? 'common:textDownloadNomination' : 'APPLY:btnDownloadApplication', {}, this.isNomination ? 'Download nomination' : 'Download application' ), breakBefore: false, hidden: app.isDraft, exec: async () => { await this.applicationDownloadService.downloadApplicationModal( app, this.isMasked && !this.showMaskedApplicant, app.canViewMaskedApplicantInfo, this.isNomination, false, this.awards ); } }, { icon: { icon: 'envelope', class: 'mr-2 text-link' }, label: this.sendReminderText, breakBefore: false, hidden: !( app.isApplicationInClientUserWorkflowLevel && this.applicationActionService.canTakeAction( app, WorkflowManagerActions.SendReminder ) ), exec: async () => await this.reminderModal() }, { icon: { icon: 'envelope', class: 'mr-2 text-link' }, label: this.notifyOfStatusText, breakBefore: false, hidden: !this.applicationActionService.canTakeAction( app, WorkflowManagerActions.NotifyOfStatus ), exec: async () => this.notifyOfStatusModal() }, { icon: { icon: 'exchange', class: 'mr-2 text-link' }, label: this.updateStatusText, breakBefore: false, hidden: !this.applicationActionService.canTakeAction( app, WorkflowManagerActions.UpdateStatus ), exec: async () => this.updateStatus() }, { icon: { icon: 'rocket', class: 'mr-2 text-link' }, label: this.i18n.translate( 'CONFIG:textUpdateProgram', {}, 'Update program' ), breakBefore: false, hidden: this.isNomination || !this.applicationActionService.canTakeAction( app, WorkflowManagerActions.UpdateProgram ), exec: async () => await this.updateProgramModal() }, { icon: { icon: 'mail-bulk', class: 'mr-2 text-link' }, label: this.mailMergeText, breakBefore: false, hidden: !this.applicationActionService.canTakeAction( app, WorkflowManagerActions.MailMerge ), exec: async () => await this.mailMergeModal() }, { icon: { icon: 'mail-bulk', class: 'mr-2 text-link' }, label: this.i18n.translate( 'common:textAttachExternalEmails', {}, 'Attach external emails' ), breakBefore: false, hidden: !this.clientSettings.allowExternalEmails || (this.isMasked && !this.application.canViewMaskedApplicantInfo), exec: async () => { const emailAddress = this.communicationsService.getApplicationEmailAddress( this.application.applicationGuid ); await this.externalEmailsModal(emailAddress); } } ]; const canArchive = this.applicationActionService.canTakeAction( app, WorkflowManagerActions.ArchiveUnarchive ) || app.canArchiveAndUnarchiveApplication; const archiveAction = [{ icon: { icon: 'archive', class: 'mr-2 text-warning' }, label: this.i18n.translate( this.isNomination ? 'APPLY:textArchiveNomination' : 'APPLY:textArchiveApplication', {}, this.isNomination ? 'Archive nomination' : 'Archive application' ), breakBefore: true, hidden: app.isArchived || !canArchive, exec: async () => await this.archiveModal('archiveApp') }, { icon: { icon: 'undo', class: 'mr-2 text-link' }, label: this.i18n.translate( this.isNomination ? 'APPLY:textUnarchiveNomination' : 'APPLY:textUnarchiveApplication', {}, this.isNomination ? 'Unarchive nomination' : 'Unarchive application' ), breakBefore: true, hidden: !app.isArchived || !canArchive, exec: async () => await this.archiveModal('unarchiveApp') }]; const canDelete = this.applicationActionService.canTakeAction( app, WorkflowManagerActions.DeleteApplications ); const deleteAction: AppManagerAction[] = [{ icon: { icon: 'times', class: 'mr-2 text-danger' }, label: this.i18n.translate( this.isNomination ? 'APPLY:textDeleteNomination' : 'APPLY:textDeleteApplication', {}, this.isNomination ? 'Delete nomination' : 'Delete application' ), breakBefore: !canArchive, hidden: !canDelete, exec: () => this.deleteModal() }]; return this.arrayHelpers.sort(actions, 'label').concat(archiveAction).concat(deleteAction); } async deleteModal () { this.spinnerService.startSpinner(); const { appCanBeDeleted, applicationPaymentStats } = await this.applicationManagerService.prepareDeleteAppModal(this.id); this.spinnerService.stopSpinner(); const modalResult = await this.modalFactory.open( DeleteApplicationModalComponent, { applicationId: this.id, isNomination: this.isNomination, applicationPaymentStats, appCanBeDeleted } ); if (modalResult) { this.spinnerService.startSpinner(); const success = await this.applicationActionService.handleDeleteApplication( this.id, this.isNomination ); this.spinnerService.stopSpinner(); if (success) { this.router.navigateByUrl('/management/manage-applications/applications'); } } } async updateStatus () { const response = await this.modalFactory.open( UpdateStatusModalComponent, { isNomination: this.isNomination, id: this.application.applicationId, currentWorkflowLevelId: this.application.currentWorkFlowLevelId, workflowId: this.application.currentWorkFlowId, currentStatusId: this.application.applicationStatus } ); if (response) { response.applicationId = this.application.applicationId; await this.applicationActionService.changeApplicationStatus( response, this.isNomination ); await this.updateApplication(); } } async mailMergeModal () { const response = await this.modalFactory.open(MailMergeModalComponent, { applicationIds: [this.application.applicationId], isNomination: this.isNomination }); if (response) { this.spinnerService.startSpinner(); await this.applicationAttachmentService.handleMailMergeModalResponse(response); await this.updateApplication(); this.spinnerService.stopSpinner(); } } async updateBudget () { const result = await this.budgetAssignmentService.updateBudgetModal( [this.application], this.application ); if (result) { this.spinnerService.startSpinner(); await this.budgetAssignmentService.updateApplicationBudgetFundingSource( [this.id], result.budgetId, result.fundingSourceId, result.reserveFunds ); await this.updateApplication(); this.spinnerService.stopSpinner(); } } async externalEmailsModal (emailAddress: string) { await this.modalFactory.open( AttachExternalEmailModalComponent, { emailAddress } ); } async recommendFundingModal () { const response = await this.modalFactory.open( RecommendFundingModalComponent, { applicationId: this.id, amountRequested: this.application.amountRequested, currencyRequested: this.application.currencyRequested, currencyRequestedAmountEquivalent: this.application.currencyRequestedAmountEquivalent, recommendedFundingAmount: this.application.recommendedFundingAmount } ); if (response) { this.spinnerService.startSpinner(); await this.applicationActionService.handleSetRecommendedFunding( this.id, response.recommendedFundingAmount ); await this.updateApplication(); this.spinnerService.stopSpinner(); } } async wflUsersModal () { await this.modalFactory.open( UsersInWflModalComponent, { workflowId: this.application.currentWorkFlowId, workflowLevelId: this.application.currentWorkFlowLevelId, workflowLevelName: this.application.currentWorkFlowLevelName } ); } async updateProgramModal () { const response = await this.modalFactory.open( UpdateProgramModalComponent, { isNomination: this.isNomination, applicationId: this.application.applicationId, currentProgramName: this.programName, currentProgramId: this.application.programId, currentWorkflowName: this.application.currentWorkflowName, currentWorkflowId: this.application.currentWorkFlowId, currentWorkflowLevelName: this.application.currentWorkFlowLevelName, currentWorkflowLevelId: this.application.currentWorkFlowLevelId, currentCycleName: this.cycleName, currentCycleId: this.application.grantProgramCycle.id, organization: this.applicationMap[this.id].organization, latestVettingRequestStatus: this.application.latestVettingRequestStatusForOrg } ); if (response) { this.spinnerService.startSpinner(); await this.applicationActionService.handleUpdateProgram( response, this.application.applicationId, this.application.organizationId ); await this.updateApplication(); this.spinnerService.stopSpinner(); } } async editApplicationConfig () { const response = await this.modalFactory.open( CreateEditApplicationModalComponent, { applicationId: +this.id, programId: this.application.programId, cycleId: this.application.grantProgramCycle.id, isNomination: this.isNomination, applicantCanReceiveEmails: this.application.applicantCanReceiveEmails } ); if (response) { this.spinnerService.startSpinner(); await this.offlineGrantsService.handleCreateEditApplication( response, this.id, this.isNomination ); await this.updateApplication(); this.spinnerService.stopSpinner(); } } ngOnDestroy () { this.sub.unsubscribe(); } }