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();
}
}