import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import { Router } from '@angular/router'; 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 { ApplicationDetail, ApplicationFromPaginated, ApplicationManagerMap, AppManagerTypes, ApproveDeclineModalResponse, ApproveDeclinePayload, Nominee, NotifyOfStatusForApi, RouteModalResponse, VettingRequestStatusId } from '@core/typings/application.typing'; import { UserTypes } from '@core/typings/client-user.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 { ApplicantManagerService } from '@features/applicant/applicant-manager.service'; import { ApplicationDownloadService } from '@features/application-download/application-download.service'; import { ApplicationEligibilityService } from '@features/application-eligibility/application-eligibility.service'; import { ApplicationFormService } from '@features/application-forms/services/application-forms.service'; import { ApplicationAttachmentService } from '@features/application-view/application-attachments/application-attachments.service'; import { ApplicationViewService } from '@features/application-view/application-view.service'; import { ApproveAwardPayModalComponent } from '@features/approve-award-pay/approve-award-pay-modal/approve-award-pay-modal.component'; import { BulkApproveAwardPayModalComponent } from '@features/approve-award-pay/bulk-approve-award-pay-modal/bulk-approve-award-pay-modal.component'; import { ArchiveModalComponent } from '@features/archive/archive-modal/archive-modal.component'; import { AwardService } from '@features/awards/award.service'; import { BulkApproveAwardPayPayload, BulkApproveModalResponse } from '@features/awards/typings/award.typing'; import { BudgetAssignmentsService } from '@features/budget-assignments/budget-assignments.service'; import { CancelApplicationModalComponent } from '@features/cancel-application/cancel-application-modal/cancel-application-modal.component'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { CommunicationsModalComponent } from '@features/communications/communications-modal/communications-modal.component'; import { FormStatuses } from '@features/configure-forms/form.typing'; import { FormsService } from '@features/configure-forms/services/forms/forms.service'; import { CyclesService } from '@features/cycles/cycles.service'; import { EmployeeSSOFieldsService } from '@features/employee-sso-fields/employee-sso-fields.service'; import { SearchByDropdownValue } from '@features/my-workspace/my-workspace.typing'; import { NonprofitService } from '@features/nonprofit/nonprofit.service'; 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 { EmailNotificationType } from '@features/system-emails/email.typing'; import { ManageTagsModalComponent } from '@features/system-tags/manage-tags-modal/manage-tags-modal.component'; import { SystemTagsService } from '@features/system-tags/system-tags.service'; import { SystemTags } from '@features/system-tags/typings/system-tags.typing'; import { WorkflowService } from '@features/workflow/workflow.service'; import { AddressFormatterService, ArrayHelpersService, AutoTableRepositoryFactory, BulkAction, FilterModalTypes, MyFilter, OrganizationEligibleForGivingStatus, TableDataFactory, TableFiltersModalResponse, TopLevelCustomLink, TopLevelFilter, TypeaheadSelectOption } from '@yourcause/common'; import { CachedAttr, CACHE_TYPES } from '@yourcause/common/cache'; import { I18nService } from '@yourcause/common/i18n'; import { ConfirmationModalComponent, ModalFactory } from '@yourcause/common/modals'; import { uniq } from 'lodash'; import { skip, Subscription } from 'rxjs'; import { ApproveDeclineModalComponent } from '../approve-decline-modal/approve-decline-modal.component'; import { DeleteApplicationModalComponent } from '../delete-application-modal/delete-application-modal.component'; import { MailMergeModalComponent } from '../mail-merge-modal/mail-merge-modal.component'; import { NotifyStatusFormGroup, NotifyStatusModalComponent } from '../notify-status-modal/notify-status-modal.component'; import { RouteApplicationModalComponent } from '../route-application-modal/route-application-modal.component'; import { ApplicationActionService } from '../services/application-actions/application-actions.service'; import { ApplicationManagerService, DefaultSearchByDropdownValue } from '../services/application-manager/application-manager.service'; import { UpdateAppCycleModalComponent } from '../update-app-cycle-modal/update-app-cycle-modal.component'; import { UpdateProgramModalComponent } from '../update-program-modal/update-program-modal.component'; import { UpdateStatusModalComponent } from '../update-status-modal/update-status-modal.component'; @Component({ selector: 'gc-application-manager-table', templateUrl: './application-manager-table.component.html', styleUrls: ['./application-manager-table.component.scss'] }) export class ApplicationManagerTableComponent implements OnInit, OnChanges, OnDestroy { @Input() type: AppManagerTypes; @Input() myFilters: MyFilter[] = []; @Input() isNomination = false; @Input() programId: number; @Input() cycleIds: number[]; @Input() nonprofitId: number; @Input() applicantId: number; @Input() applicationId: number; @Input() supportsSavingFilters = false; @Output() onSaveFilter = new EventEmitter(); statusIconMap = {}; ApplicationStatuses = ApplicationStatuses; VettingRequestStatusId = VettingRequestStatusId; WorkflowManagerActions = WorkflowManagerActions; FormStatuses = FormStatuses; programOptions: TypeaheadSelectOption[] = this.programService.allActiveManagerProgramOptions; cycleOptions: TypeaheadSelectOption[] = this.cycleService.allMyCycleSelectOptions; statusMapApp = this.statusService.applicationStatusMap; tableDataFactory: TableDataFactory; topLevelFilters: TopLevelFilter[] = []; tagOptions: TypeaheadSelectOption[] = []; tableKey: string; viewOnly = false; UserTypes = UserTypes; bulkActions: BulkAction[]; canCreateOrEditApp = this.policyService.grantApplication.canCreateOrEditApplications(); showBulkActions = true; showMaskedApplicantsIcon = 'user-shield'; hideMaskedApplicantsIcon = 'user-times'; hideMaskedApplicantsTooltip = this.i18n.translate( 'MANAGE:textShowApplicantInformation', {}, 'Show applicant information' ); showMaskedApplicantsTooltip = this.i18n.translate( 'MANAGE:textHideApplicantInformation', {}, 'Hide applicant information' ); topLevelCustomLinks: TopLevelCustomLink[] = []; fromRoute = this.applicantManagerService.getFromRouteForProfile(); viewTranslations = this.translationService.viewTranslations; programTranslationMap = this.viewTranslations.Grant_Program; cycleTranslationMap = this.viewTranslations.Grant_Program_Cycle; defaultCurrency = this.clientSettingsService.defaultCurrency; OrganizationEligibleForGivingStatus = OrganizationEligibleForGivingStatus; ProcessingTypes = ProcessingTypes; existingFilterNames: string[] = []; wflOptions = this.workflowService.myWorkflowLevelOptions; sub = new Subscription(); @CachedAttr( CACHE_TYPES.LOCALSTORAGE, [], undefined, undefined, (self: ApplicationManagerTableComponent) => { return `ycStatusMulti_${self.getTableKey()}`; } ) status: (string|number)[]; @CachedAttr( CACHE_TYPES.LOCALSTORAGE, false, undefined, undefined, (self: ApplicationManagerTableComponent) => { return `ycActiveStatus_${self.getTableKey()}`; } ) activeStatus: boolean; @CachedAttr( CACHE_TYPES.LOCALSTORAGE, false, undefined, undefined, (self: ApplicationManagerTableComponent) => { return `ycShowMasked_${self.getTableKey()}`; } ) showMaskedApplicants: boolean; @CachedAttr( CACHE_TYPES.EPHEMERAL, 0, undefined, undefined, (self: ApplicationManagerTableComponent) => { return `ycReturnToPageNumber_${self.getTableKey()}`; } ) returnToPageNumber: number; @CachedAttr( CACHE_TYPES.LOCALSTORAGE, DefaultSearchByDropdownValue, undefined, undefined, (self: ApplicationManagerTableComponent) => { return `ycSearchByDropdownValue_${self.getTableKey()}`; } ) searchByDropdownValue: SearchByDropdownValue; @CachedAttr( CACHE_TYPES.LOCALSTORAGE, FilterModalTypes.Last6Months, undefined, undefined, (self: ApplicationManagerTableComponent) => { return `ycDateRangeValue_${self.getTableKey()}`; } ) dateRangeValue: FilterModalTypes; constructor ( private systemTagsService: SystemTagsService, private i18n: I18nService, private modalFactory: ModalFactory, private router: Router, private statusService: StatusService, private policyService: PolicyService, private spinnerService: SpinnerService, private arrayHelper: ArrayHelpersService, private autoTableFactory: AutoTableRepositoryFactory, private programService: ProgramService, private applicantManagerService: ApplicantManagerService, private nonprofitService: NonprofitService, private translationService: TranslationService, private awardService: AwardService, private clientSettingsService: ClientSettingsService, private applicationManagerService: ApplicationManagerService, private emailService: EmailService, private applicationAttachmentService: ApplicationAttachmentService, private budgetAssignmentService: BudgetAssignmentsService, private applicationDownloadService: ApplicationDownloadService, private addressFormatter: AddressFormatterService, private formService: FormsService, private employeeSSOFieldsService: EmployeeSSOFieldsService, private cycleService: CyclesService, private workflowService: WorkflowService, private offlineGrantsService: OfflineGrantsService, private applicationEligibilityService: ApplicationEligibilityService, private applicationFormService: ApplicationFormService, private applicationViewService: ApplicationViewService, private applicationActionService: ApplicationActionService ) { this.sub.add( this.applicationManagerService.changesTo$('triggerResetAppManagerTable') .pipe(skip(1)).subscribe(() => { this.resetTableAndStats(); }) ); this.getSsoColumnOptions = this.getSsoColumnOptions.bind(this); this.getFormColumnOptions = this.getFormColumnOptions.bind(this); this.setMaskedIconAndTooltip = this.setMaskedIconAndTooltip.bind(this); } get applicationMap () { return this.applicationManagerService.applicationManagerMap; } get isProgramDashboard () { return this.type === AppManagerTypes.ProgramDashboard; } get isNonprofitProfile () { return this.type === AppManagerTypes.NonprofitProfile; } get isColumnsOnly () { return this.type === AppManagerTypes.ColumnsOnly; } get isApplicantProfile () { return this.type === AppManagerTypes.ApplicantProfile; } get applicantRouterLink () { return this.applicantManagerService.get('applicantProfileRouterLink'); } get nonprofitRouterLink () { return this.nonprofitService.get('nonprofitProfileRouterLink'); } get hasMaskedAppsOnPage () { return this.applicationManagerService.hasMaskedAppsOnPage; } ngOnInit () { this.setCycleAndProgramOptions(); this.setExistingFilterNames(); this.viewOnly = this.isNonprofitProfile; this.tableKey = this.getTableKey(); this.setBulkActions(); this.setTableDataFactory(); this.setTopLevelFilters(); this.setTagOptions(); } ngOnChanges (changes: SimpleChanges) { if (changes.cycleIds && this.isProgramDashboard) { if (this.tableDataFactory) { this.tableDataFactory.reset.emit(); } } } getTableKey () { if (this.isApplicantProfile) { return this.isNomination ? `APPLICANT_NOMINATIONS_${this.applicantId}` : `APPLICANT_APPLICATIONS_${this.applicantId}`; } else if (this.isNonprofitProfile) { return this.isNomination ? `NONPROFIT_NOMINATIONS_${this.nonprofitId}` : `NONPROFIT_APPLICATIONS_${this.nonprofitId}`; } else if (this.isProgramDashboard) { return this.isNomination ? `PROGRAM_MANAGER_NOMS_${this.programId}` : `PROGRAM_MANAGER_APPS_${this.programId}`; } else if (this.isColumnsOnly) { return 'COLUMNS_ONLY'; } else { return this.isNomination ? 'NOMINATIONS' : 'APPLICATIONS'; } } async getFormColumnOptions () { this.spinnerService.startSpinner(); await this.formService.setMyDependentFormFilterOptions(); this.spinnerService.stopSpinner(); return this.formService.myDependentFormFilteringOptions; } async getSsoColumnOptions () { await this.employeeSSOFieldsService.setEmployeeSSOFields(); return this.employeeSSOFieldsService.dependentSsoFilteringOptions; } setTagOptions () { this.tagOptions = this.systemTagsService.getTagOptionsForAppManager(this.isNomination); } setCycleAndProgramOptions () { // Only necessary for type columns b/c we don't get facets returned if (this.type === AppManagerTypes.ColumnsOnly) { this.cycleOptions = this.cycleService.allMyCycleSelectOptions; this.programOptions = this.arrayHelper.sort( this.programService.allManagerPrograms.map((prog) => { return { label: this.programTranslationMap[prog.grantProgramId] ? this.programTranslationMap[prog.grantProgramId].Name : prog.grantProgramName, value: +prog.grantProgramId }; }), 'label' ); } } setExistingFilterNames () { if (this.myFilters) { this.existingFilterNames = this.myFilters.map((filter) => { return filter.name; }); } } setBulkActions () { if (this.viewOnly) { this.showBulkActions = false; } if (this.showBulkActions) { this.bulkActions = [{ label: this.i18n.translate('GLOBAL:btnApprove'), disabled: (apps: ApplicationFromPaginated[]) => { const programIds = uniq(apps.map((app) => app.programId)); const cycleIds = uniq(apps.map((app) => app.grantProgramCycle.id)); return !apps.every(val => { return this.applicationMap[val.applicationId].canApprove; }) || programIds.length !== 1 || cycleIds.length !== 1; }, exec: (arg: ApplicationFromPaginated[]) => { const hasArchived = arg.some((app) => app.isArchived); return this.isNomination || hasArchived ? this.approveOrDeclineModal(arg, 'Approve') : this.bulkApproveAwardPay(arg); } }, { label: this.i18n.translate('GLOBAL:btnDecline'), disabled: (apps: ApplicationFromPaginated[]) => { const programIds = uniq(apps.map((app) => app.programId)); return !apps.every(val => { return this.applicationMap[val.applicationId].canDecline; }) || programIds.length !== 1; }, exec: (arg: ApplicationFromPaginated[]) => { return this.approveOrDeclineModal(arg, 'Decline'); } }, this.isNomination ? null : { label: this.i18n.translate('common:lblAward'), disabled: (apps: ApplicationFromPaginated[]) => { const hasArchived = apps.some((app) => { return app.isArchived; }); const hasAwardPermission = apps.every(val => { return this.applicationMap[val.applicationId].canAwardPay; }); const programIds = uniq(apps.map((app) => app.programId)); const cycleIds = uniq(apps.map((app) => app.grantProgramCycle.id)); const currencies = uniq(apps.map((row) => row.currencyRequested)); const allApproved = apps.every(val => { return val.applicationStatus === ApplicationStatuses.Approved; }); if ( hasArchived || !hasAwardPermission || !allApproved || (programIds.length !== 1) || (cycleIds.length !== 1) || (currencies.length !== 1)) { return true; } const passedRequested = apps.filter((app) => { return (!!app.amountRequested) || ((!!app.inKindAmountRequested || app.inKindAmountRequested === 0)); }); if (passedRequested.length !== apps.length) { return true; } const passedVettingArray = apps.map((app) => { return this.awardService.checkVettingRequirementForAward( app.organizationEligibleForGivingStatus, app.grantProgramCycle.isClientProcessing ? ProcessingTypes.Client : ProcessingTypes.YourCause, this.getCycleBudgetIds(app.grantProgramCycle.id), app.amountRequested, app.inKindAmountRequested, app.recommendedFundingAmount ); }); if (!passedVettingArray.every((passed) => !!passed)) { return true; } return false; }, exec: (arg: ApplicationFromPaginated[]) => { return this.bulkApproveAwardPay(arg, true); } }, { label: this.i18n.translate('GLOBAL:btnNotifyOfStatus', {}, 'Notify of Status'), disabled: ((apps: ApplicationFromPaginated[]) => { return !apps.every(val => { return this.applicationMap[val.applicationId].canNotifyOfStatus; }); }), exec: (arg: ApplicationFromPaginated[]) => { return this.notifyOfStatusModal(arg); } }, { label: this.i18n.translate('GLOBAL:btnRoute'), disabled: (apps: ApplicationFromPaginated[]) => { const hasRoutePermission = apps.every(val => { return this.applicationMap[val.applicationId].canRoute; }); const programIds = uniq(apps.map((app) => app.programId)); const applicationsOnHold: ApplicationFromPaginated[] = []; apps.forEach((app: ApplicationFromPaginated) => { if (app.applicationStatus === ApplicationStatuses.Hold) { applicationsOnHold.push(app); } }); return !hasRoutePermission || !!applicationsOnHold.length || programIds.length !== 1; }, exec: (arg: ApplicationFromPaginated[]) => { return this.bulkRouteModal(arg); } }, { label: this.i18n.translate('GLOBAL:textArchive', {}, 'Archive'), disabled: ((apps: ApplicationFromPaginated[]) => { const hasArchivePermission = apps.every(val => { return this.applicationMap[val.applicationId].canArchiveUnarchive; }); const someAreArchived = apps.some((val) => { return val.isArchived; }); return !hasArchivePermission || someAreArchived; }), exec: (arg: ApplicationFromPaginated[]) => { return this.archiveModal(arg, 'archiveApp'); } }, { label: this.i18n.translate('GLOBAL:textUnarchive', {}, 'Unarchive'), disabled: ((apps: ApplicationFromPaginated[]) => { const hasArchivePermission = apps.every(val => { return this.applicationMap[val.applicationId].canArchiveUnarchive; }); const someAreUnarchived = apps.some((val) => { return !val.isArchived || val.isProgramArchived; }); return !hasArchivePermission || someAreUnarchived; }), exec: (arg: ApplicationFromPaginated[]) => { return this.archiveModal(arg, 'unarchiveApp'); } }, { label: this.i18n.translate('GLOBAL:textUpdateCycle', {}, 'Update cycle'), disabled: (apps: ApplicationFromPaginated[]) => { const canUpdateCycle = apps.every(val => { return this.applicationMap[val.applicationId].canUpdateCycle; }); const programIds = uniq(apps.map((app) => app.programId)); return !canUpdateCycle || programIds.length !== 1; }, exec: (arg: ApplicationFromPaginated[]) => { return this.updateCycle(arg); } }, { label: this.i18n.translate('CONFIG:textAddMergeDocument', {}, 'Add merge document'), disabled: (apps: ApplicationFromPaginated[]) => { const canMailMerge = apps.every(val => { return this.applicationMap[val.applicationId].canSendMailMerge; }); return !canMailMerge; }, exec: (arg: ApplicationFromPaginated[]) => { return this.mailMergeModal(arg); } }, { label: this.i18n.translate( 'BUDGET:btnBudgetAssignment', {}, 'Budget assignment' ), disabled: (apps: ApplicationFromPaginated[]) => { let haveSameProgramAndCycle = true; apps.reduce((acc, app) => { const combo = `${app.programId}-${app.grantProgramCycle.id}`; if (acc && (acc !== combo)) { haveSameProgramAndCycle = false; } return combo; }, ''); const failsOtherRequirements = apps.some((app) => { return app.applicationStatus === ApplicationStatuses.Canceled || app.isArchived || this.isNomination; }); return !haveSameProgramAndCycle || failsOtherRequirements; }, exec: (arg: ApplicationFromPaginated[]) => { return this.updateBudget(arg); } }, { label: this.i18n.translate( 'CONFIG:textAddTags', {}, 'Add tags' ), disabled: () => { return false; }, exec: (arg: ApplicationFromPaginated[]) => { return this.addTagsModal(arg); } }, { label: this.i18n.translate( 'common:textDownload', {}, 'Download' ), disabled: (apps: ApplicationFromPaginated[]) => { const programIds = uniq(apps.map((app) => app.programId)); const hasDrafts = apps.some((app) => app.isDraft); return hasDrafts || apps.length > 10 || programIds.length !== 1; }, exec: (arg: ApplicationFromPaginated[]) => { return this.bulkDownloadModal(arg); } }].filter((action) => !!action); } else { this.bulkActions = []; } } getCycleBudgetIds (id: number) { return this.programService.get('cycleBudgetsMap')[id] || []; } setTopLevelFilters () { this.topLevelFilters = this.applicationManagerService.getTopLevelFiltersForApplicationManager( this.status, this.isNomination, this.activeStatus, this.searchByDropdownValue, this.dateRangeValue ); } setTableDataFactory () { this.tableDataFactory = this.applicationManagerService.getTableDataFactory( this.type, this.cycleIds, this.type === AppManagerTypes.ApplicantProfile ? this.applicantId : null, this.activeStatus, this.isNomination, this.tableKey, this.nonprofitId, this.returnToPageNumber, this.applicationId, this.setMaskedIconAndTooltip ); } async updateBudget (records: ApplicationFromPaginated[]) { const result = await this.budgetAssignmentService.updateBudgetModal( records ); if (result) { this.spinnerService.startSpinner(); await this.budgetAssignmentService.updateApplicationBudgetFundingSource( records.map((record) => record.applicationId), result.budgetId, result.fundingSourceId, result.reserveFunds ); this.resetTableAndStats(); this.spinnerService.stopSpinner(); } } onTopLevelFilterChange (filter: TopLevelFilter) { if (filter.column === 'applicationStatus') { this.status = filter.value; } if (filter.column === 'isArchived') { this.activeStatus = filter.value; } if (filter.column === 'applicationId') { this.searchByDropdownValue = { ...filter.value, filterValue: '' }; } if (filter.column === 'updatedDate') { this.dateRangeValue = filter.value || FilterModalTypes.Last6Months; } } onNavigateToApp () { const repo = this.autoTableFactory.getRepository(this.tableKey); this.returnToPageNumber = repo.pageNumber; } setMaskedIconAndTooltip () { const tooltip = this.showMaskedApplicants ? this.showMaskedApplicantsTooltip : this.hideMaskedApplicantsTooltip; const item = { name: tooltip, tooltip, icon: this.showMaskedApplicants ? this.hideMaskedApplicantsIcon : this.showMaskedApplicantsIcon, isIcon: true, onClick: () => { this.showMaskedApplicants = !this.showMaskedApplicants; this.setMaskedIconAndTooltip(); } }; if (this.topLevelCustomLinks.length === 0) { this.topLevelCustomLinks = [item]; } else { this.topLevelCustomLinks[0] = item; } } setApplicationMap ( id: keyof ApplicationManagerMap, attr: K, value: ApplicationDetail[K] ) { this.applicationManagerService.setApplicationManagerMap(id, attr, value); } async onDrilldown (row: ApplicationFromPaginated) { const id = '' + row.applicationId; const [ forms, awards, application ] = await Promise.all([ this.applicationFormService.getApplicantFormsForApplication( +id, row.applicationStatus ), this.awardService.getAwards(+id), this.applicationViewService.getApplicationDetail(+id) ]); this.setApplicationMap(id, 'forms', forms); this.setApplicationMap(id, 'awards', awards); this.setApplicationMap(id, 'nominee', application.nominee); this.setApplicationMap( id, 'isInWorkflowLevel', application.isApplicationInClientUserWorkflowLevel ); this.setApplicationMap( id, 'canAccessNominationApplication', application.canAccessNominationApplication ); this.setApplicationMap( id, 'programTimezone', application.grantProgramTimezone ); } async approveAwardPayModal (row: ApplicationFromPaginated) { this.spinnerService.startSpinner(); const canProceed = await this.awardService.prepareApproveAwardPayModal( row.workflowId, row.currentWorkflowLevelId, row.programId, row.amountRequested, row.inKindAmountRequested, true, row.organizationEligibleForGivingStatus, row.grantProgramCycle.id, row.recommendedFundingAmount, row.isArchived ); this.spinnerService.stopSpinner(); if (canProceed) { const response = await this.modalFactory.open( ApproveAwardPayModalComponent, { applicationId: row.applicationId, programId: row.programId } ); if (response) { if (response.awards) { this.spinnerService.startSpinner(); const success = await this.awardService.handleApproveAwardPayModal( response, true ); if (success) { this.resetTableAndStats(); } this.spinnerService.stopSpinner(); } else { this.handleApproveDecline( [row], 'Approve', { comment: '', notifyApplicant: response.sendEmail, customMessage: response.customMessage, clientEmailTemplateId: response.clientEmailTemplateId, emailOptionsModel: response.emailOptionsModel } ); } } } else { await this.approveOrDeclineModal(row, 'Approve'); } } async bulkApproveAwardPay ( rows: ApplicationFromPaginated[], awardOnly = false ) { // We only need to check if all apps are not awarded if they are taking the action "Award" const checkAllNotAwarded = awardOnly; let allAppsWithAwards: number[] = []; if (checkAllNotAwarded) { this.spinnerService.startSpinner(); allAppsWithAwards = await this.applicationManagerService.getAllAppsWithAwards( rows.map((row) => row.applicationId) ); this.spinnerService.stopSpinner(); } if (allAppsWithAwards.length === 0) { this.proceedWithBulkApproveAwardPay(rows, awardOnly); } else { let confirmText = this.i18n.translate( 'common:textCannotAwardAndPayAppsHaveAwards', {}, 'Cannot award and pay the selected applications because the following already have an award:' ) + `
    `; allAppsWithAwards.forEach((id) => { confirmText = confirmText + '
  • ' + id + '
  • '; }); confirmText = confirmText + '
'; this.modalFactory.open( ConfirmationModalComponent, { modalHeader: this.i18n.translate( 'AWARDS:hdrAwardAndPayApplications', {}, 'Award and Pay Applications' ), confirmButtonText: this.i18n.translate( 'common:textClose', {}, 'Close' ), confirmText } ); } } async proceedWithBulkApproveAwardPay ( rows: ApplicationFromPaginated[], awardOnly: boolean ) { const programId = rows[0].programId; const cycleId = rows[0].grantProgramCycle.id; const currencyRequested = rows[0].currencyRequested; this.spinnerService.startSpinner(); const canProceed = await this.awardService.prepareApproveAwardPayModalBulk( rows, programId, cycleId ); const hasAwardPermission = rows.every(val => { return this.applicationMap[val.applicationId].canAwardPay; }); this.spinnerService.stopSpinner(); if (hasAwardPermission && canProceed) { const response = await this.modalFactory.open( BulkApproveAwardPayModalComponent, { applications: rows, programId, currencyRequested, awardOnly, cycleId } ); if (response) { // Bulk Approve Award Pay if ((response as BulkApproveAwardPayPayload).applications) { this.spinnerService.startSpinner(); await this.applicationActionService.handleBulkApproveAwardPay( response as BulkApproveAwardPayPayload, awardOnly ); await this.resetTableAndStats(); this.spinnerService.stopSpinner(); } else { // Just Approve this.handleApproveDecline( rows, 'Approve', { comment: (response as BulkApproveModalResponse).comment, notifyApplicant: (response as BulkApproveModalResponse).sendEmail, customMessage: (response as BulkApproveModalResponse).customMessage, clientEmailTemplateId: (response as BulkApproveModalResponse).clientEmailTemplateId, emailOptionsModel: (response as BulkApproveModalResponse).emailOptionsModel } ); } } } else { await this.approveOrDeclineModal(rows, 'Approve'); } } async approveOrDeclineModal ( row: ApplicationFromPaginated|ApplicationFromPaginated[], type: 'Approve'|'Decline' ) { return new Promise(async (resolve) => { const rows = row instanceof Array ? row : [row]; let nominees: Nominee[]; if (this.isNomination && type === 'Approve') { this.spinnerService.startSpinner(); await Promise.all(rows.map(async (app) => { const map = this.applicationMap[app.applicationId]; if (!map || !map.nominee) { await this.onDrilldown(app); } nominees = rows.map((item) => { return this.applicationMap[item.applicationId].nominee; }); })); this.spinnerService.stopSpinner(); } const deps = { type, single: rows.length === 1, isNomination: this.isNomination, programId: rows[0].programId, nominees, applicationIds: rows.map((r) => r.applicationId) }; const response = await this.modalFactory.open( ApproveDeclineModalComponent, deps ); if (response) { await this.handleApproveDecline(rows, type, response); } resolve(); }); } async handleCancelApplication (application: ApplicationFromPaginated) { const emailType = this.isNomination ? EmailNotificationType.YourNominationIsCanceled : EmailNotificationType.YourApplicationIsCanceled; const currentEmailIsActive = await this.emailService.isProgramEmailActive( emailType, application.programId ); const cancelModalResponse = await this.modalFactory.open( CancelApplicationModalComponent, { applicationId: application.applicationId, programId: application.programId, isNomination: this.isNomination, isManager: true, emailActive: currentEmailIsActive, emailType, programName: application.programName } ); if (cancelModalResponse) { this.spinnerService.startSpinner(); let payload; if (cancelModalResponse.attachments) { const attachments = await this.emailService.returnIdsFromMixedAttachments( cancelModalResponse.attachments, application.applicationId ); payload = { ...cancelModalResponse, emailOptions: { attachments, ccEmails: null, bccEmails: null } }; } else { payload = { ...cancelModalResponse, emailOptions: { attachments: null, ccEmails: null, bccEmails: null } }; } await this.applicationActionService.cancelApplication(payload); await this.resetTableAndStats(); this.spinnerService.stopSpinner(); } } async handleApproveDecline ( input: ApplicationFromPaginated[], type: 'Approve'|'Decline', response: ApproveDeclineModalResponse ) { this.spinnerService.startSpinner(); const rows = input instanceof Array ? input : [input]; const attachments = await this.emailService.returnIdsFromMixedAttachments( response.emailOptionsModel.attachments ); const emailOptionsModel = { ...response.emailOptionsModel, attachments }; const data: ApproveDeclinePayload = { applicationIds: rows.map(row => row.applicationId), 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; } await this.applicationActionService.handleApproveDeclineModal( type, data, true, this.isNomination ); await this.resetTableAndStats(); this.spinnerService.stopSpinner(); } async resetTableAndStats () { if (this.isProgramDashboard) { await this.programService.resetProgramStats(this.programId, []); } this.tableDataFactory.reset.emit(); } async bulkDownloadModal (apps: ApplicationFromPaginated[]) { if (apps.length === 1) { await this.downloadApplicationModal(apps[0]); } else { const hasMaskedApps = apps.some((app) => { return app.isMasked; }); const hasNonViewableApps = apps.some((app) => { return app.isMasked && !app.canViewMaskedApplicantInfo; }); const shouldMaskApplicants = hasMaskedApps && (!this.showMaskedApplicants || hasNonViewableApps); await this.applicationDownloadService.bulkDownloadApplicationModal( apps, this.isNomination, shouldMaskApplicants ); } } async downloadApplicationModal (app: ApplicationFromPaginated) { const response = await this.applicationDownloadService.downloadApplicationModal( app, app.isMasked && !this.showMaskedApplicants, app.canViewMaskedApplicantInfo, this.isNomination, true, null ); return response; } notifyOfStatusModal (input: ApplicationFromPaginated|ApplicationFromPaginated[]) { const inputs = input instanceof Array ? input : [input]; return new Promise(async (resolve) => { const response = await this.modalFactory.open( NotifyStatusModalComponent, { isNomination: this.isNomination, single: inputs.length === 1 } ); if (response) { await this.handleNotifyStatus(inputs.map(row => row.applicationId), response); } resolve(); }); } async handleNotifyStatus (ids: number[], 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: ids, customMessage: response.customMessage, clientEmailTemplateId: response.clientEmailTemplateId, emailOptionsRequest: payload.emailOptionsRequest }; await this.applicationActionService.handleSendApplicationStatusEmail( data, this.isNomination ); await this.resetTableAndStats(); this.spinnerService.stopSpinner(); } async routeModal (application: ApplicationFromPaginated) { this.spinnerService.startSpinner(); const details = await this.applicationManagerService.getBulkApplicationDetailsMap( [application.applicationId] ); const appRoutes = details[application.applicationId].workflowLevelRoutes; const workflowLevelRoutes = appRoutes.map((route) => { return { workflowLevelId: route.canRouteToWorkflowLevelId, name: route.canRouteToWorkflowLevelName, description: route.canRouteToWorkflowLevelDescription }; }); this.spinnerService.stopSpinner(); const deps = { workflowLevelRoutes, isNomination: this.isNomination }; const response = await this.modalFactory.open( RouteApplicationModalComponent, deps ); if (response) { await this.handleRoute(application.applicationId, response); } } async handleRoute (id: number, response: RouteModalResponse) { this.spinnerService.startSpinner(); const data = { workflowLevelId: response.level, comments: response.comment }; await this.applicationActionService.handleRouteApplication( id, data, true, this.isNomination ); this.resetTableAndStats(); this.spinnerService.stopSpinner(); } async handleBulkRoute (ids: number[], response: RouteModalResponse) { this.spinnerService.startSpinner(); const data = { applicationIds: this.isNomination ? [] : ids, nominationIds: this.isNomination ? ids : [], newWorkflowLevelId: response.level, comment: response.comment }; await this.applicationActionService.handleBulkRouteApplications( data, this.isNomination ); this.resetTableAndStats(); this.spinnerService.stopSpinner(); } async bulkRouteModal (applications: ApplicationFromPaginated[]) { this.spinnerService.startSpinner(); const appIds = applications.map((app) => app.applicationId); const details = await this.applicationManagerService.getBulkApplicationDetails( appIds ); const workflowLevelRoutes = this.arrayHelper.mapToUniq( details, 'workflowLevelRoutes', 'canRouteToWorkflowLevelId' ).map((route) => { return { workflowLevelId: route.canRouteToWorkflowLevelId, name: route.canRouteToWorkflowLevelName, description: route.canRouteToWorkflowLevelDescription }; }); this.spinnerService.stopSpinner(); const deps = { workflowLevelRoutes, isNomination: this.isNomination, numberOfApplications: appIds.length }; const response = await this.modalFactory.open( RouteApplicationModalComponent, deps ); if (response) { await this.handleBulkRoute(appIds, response); } } async edit (row: ApplicationFromPaginated, isCopy = false) { if (isCopy) { this.spinnerService.startSpinner(); const [ program, detail ] = await Promise.all([ this.programService.getProgram('' + row.programId), this.applicationViewService.getApplicationDetail( row.applicationId ) ]); const potentialErrorMessage = await this.applicationEligibilityService.checkIfCycleUpdateCanProceed({ applicationId: row.applicationId, grantProgramCycleId: row.grantProgramCycle.id, grantProgramId: row.programId, orgIdentification: row.organizationIdentification, charityBucketId: program.charityBucketId, clientId: program.clientId, latestVettingRequestStatusForOrg: detail.latestVettingRequestStatusForOrg }); if (potentialErrorMessage) { this.spinnerService.stopSpinner(); await this.applicationEligibilityService.showErrorCopyingApplication( potentialErrorMessage, this.i18n.translate( 'PROGRAM:hdrUnableToCopyApplication', {}, 'Unable to Copy Application' )); } else { const id = await this.offlineGrantsService.handleCopyApplication( row.applicationId, this.isNomination ); this.spinnerService.stopSpinner(); if (id) { this.router.navigateByUrl(this.getEditApplicationRoute(id)); } } } else { this.router.navigateByUrl(this.getEditApplicationRoute(row.applicationId)); } } getEditApplicationRoute (applicationId: number) { return `/management/manage-${this.isNomination ? 'nominations/' : 'applications/'}${ this.isNomination ? 'nomination' : 'application' }/` + applicationId + '/form/no-form'; } async deleteModal (app: ApplicationFromPaginated) { this.spinnerService.startSpinner(); const { appCanBeDeleted, applicationPaymentStats } = await this.applicationManagerService.prepareDeleteAppModal(app.applicationId); this.spinnerService.stopSpinner(); const modalResult = await this.modalFactory.open( DeleteApplicationModalComponent, { applicationId: app.applicationId, isNomination: this.isNomination, applicationPaymentStats, appCanBeDeleted } ); if (modalResult) { this.spinnerService.startSpinner(); const success = await this.applicationActionService.handleDeleteApplication( app.applicationId, this.isNomination ); this.spinnerService.stopSpinner(); if (success) { this.resetTableAndStats(); } } } async communicationsModal (row: ApplicationFromPaginated) { const deps = { isNomination: this.isNomination, applicationId: row.applicationId }; await this.modalFactory.open( CommunicationsModalComponent, deps ); } async updateCycle (rows: ApplicationFromPaginated[]) { const applicationIds = rows.map(row => row.applicationId); const programId = rows[0].programId; const { totalPayments, budgetCount, eligibleCycleIds } = await this.applicationManagerService.getUpdateCycleInfo( applicationIds, programId ); const cycleModalStats: CyclesUI.UpdateAppCycleModalData = { applicationIds, programId, totalPayments, budgetCount, eligibleCycleIds }; if (eligibleCycleIds.length <= 0) { await this.applicationEligibilityService.showErrorCopyingApplication( this.i18n.translate( 'PROGRAM:textNoEligibleCyclesBasedOnOrganizations', {}, 'There are no eligible cycles based on the organizations tied to these applications' ), this.i18n.translate( 'PROGRAM:hdrUnableUpdateApplications', {}, 'Unable to Update Applications' )); } else { const response = await this.modalFactory.open( UpdateAppCycleModalComponent, { modalData: cycleModalStats, isNomination: this.isNomination } ); if (response) { this.resetTableAndStats(); } } } async archiveModal (rows: ApplicationFromPaginated[], context: 'archiveApp'|'unarchiveApp') { const applicationIds = rows.map(row => row.applicationId); const returnArchived = context === 'archiveApp' ? false : true; const applicationStats = await this.applicationManagerService.getPaymentStats( applicationIds, returnArchived ); const applicationData = { ids: applicationIds, totalPayments: applicationStats.paymentsAmount, totalAwards: applicationStats.awarded, paymentsAvailableForProcessing: applicationStats.paymentsAvailableForProcessing }; const response = await this.modalFactory.open( ArchiveModalComponent, { modalData: applicationData, isNomination: this.isNomination, context } ); if (response) { this.spinnerService.startSpinner(); await this.applicationActionService.handleArchiveModalResponse(response, context); this.spinnerService.stopSpinner(); this.resetTableAndStats(); } } async updateStatus (row: ApplicationFromPaginated) { const response = await this.modalFactory.open( UpdateStatusModalComponent, { isNomination: this.isNomination, id: row.applicationId, currentWorkflowLevelId: row.currentWorkflowLevelId, workflowId: row.workflowId, currentStatusId: row.applicationStatus } ); if (response) { this.spinnerService.startSpinner(); response.applicationId = row.applicationId; await this.applicationActionService.changeApplicationStatus( response, this.isNomination ); this.spinnerService.stopSpinner(); this.resetTableAndStats(); } } async mailMergeModal (applications: ApplicationFromPaginated[]) { const response = await this.modalFactory.open( MailMergeModalComponent, { applicationIds: applications.map((app) => app.applicationId), isNomination: this.isNomination } ); if (response) { this.spinnerService.startSpinner(); await this.applicationAttachmentService.handleMailMergeModalResponse(response); this.spinnerService.stopSpinner(); } } async addTagsModal (rows: ApplicationFromPaginated[]) { let currentTagIds: number[] = []; const tagType = this.isNomination ? SystemTags.Buckets.Nomination : SystemTags.Buckets.Application; const singleApp = rows.length === 1; if (singleApp) { this.spinnerService.startSpinner(); const appId = rows[0].applicationId; currentTagIds = await this.systemTagsService.getAndSetTagsForRecord( tagType, appId ); this.spinnerService.stopSpinner(); } const ids = rows.map((row) => row.applicationId); const tagIds = await this.modalFactory.open( ManageTagsModalComponent, { bucket: tagType, currentTagIds, isNomination: this.isNomination, ids, modalSubHeader: ids.length > 1 ? this.i18n.translate( this.isNomination ? 'common:textNumberOfNominations' : 'common:textNumberOfApplications', { number: ids.length }, this.isNomination ? '__number__ nominations' : '__number__ applications' ) : (this.i18n.translate( this.isNomination ? 'GLOBAL:textNominationID' : 'common:textApplicationId', {}, this.isNomination ? 'Nomination ID' : 'Application ID' ) + `: ${ids[0]}`) } ); if (tagIds) { this.spinnerService.startSpinner(); if (singleApp) { const currentTags = currentTagIds.map((id) => { return this.systemTagsService.summaries.find(tag => tag.id === id); }); const { newTags, removedTags } = this.systemTagsService.getNewAndRemovedTags( currentTags, tagIds ); await this.systemTagsService.setTagsOnRecord( tagType, rows[0].applicationId, newTags, removedTags ); } else { await this.systemTagsService.handleBulkAddTags( tagIds, rows.map((row) => row.applicationId) ); } this.resetTableAndStats(); this.spinnerService.stopSpinner(); } } async updateProgram (row: ApplicationFromPaginated) { this.spinnerService.startSpinner(); const detail = await this.applicationViewService.getApplicationDetail( row.applicationId ); this.spinnerService.stopSpinner(); const response = await this.modalFactory.open( UpdateProgramModalComponent, { isNomination: this.isNomination, applicationId: row.applicationId, currentProgramName: this.programTranslationMap[row.programId]?.Name || row.programName, currentProgramId: row.programId, currentWorkflowName: row.workflowName, currentWorkflowId: row.workflowId, currentWorkflowLevelName: row.currentWorkflowLevelName, currentWorkflowLevelId: row.currentWorkflowLevelId, currentCycleName: this.cycleTranslationMap[row.grantProgramCycle.id]?.Name, currentCycleId: row.grantProgramCycle.id, organization: row.organizationId ? { id: row.organizationId, guid: row.nonprofitGuid, name: row.organizationName, image: row.organizationImageUrl, imageUrl: row.organizationImageUrl, address: null, addressString: this.addressFormatter.formatSimpleGrantsAddressToSingleLine( row.organizationAddressInfo ), identification: row.organizationIdentification, eligibleForGivingStatusId: row.organizationEligibleForGivingStatus, isInternational: row.orgIsInternational, parentGuid: row.parentOrganizationGuid, parentName: row.parentOrganizationName } : null, latestVettingRequestStatus: detail.latestVettingRequestStatusForOrg } ); if (response) { this.spinnerService.startSpinner(); await this.applicationActionService.handleUpdateProgram( response, row.applicationId, row.organizationId ); this.resetTableAndStats(); this.spinnerService.stopSpinner(); } } async copyApplication (row: ApplicationFromPaginated) { const proceed = await this.modalFactory.open( ConfirmationModalComponent, { modalHeader: this.i18n.translate( this.isNomination ? 'common:hdrCopyNomination' : 'common:hdrCopyApplication', {}, this.isNomination ? 'Copy Nomination' : 'Copy Application' ), modalSubHeader: this.i18n.translate( this.isNomination ? 'GLOBAL:textNominationID' : 'common:textApplicationId', {}, this.isNomination ? 'Nomination ID' : 'Application ID' ) + ': ' + row.applicationId, confirmButtonText: this.i18n.translate( 'common:textCopy', {}, 'Copy' ), confirmText: this.i18n.translate( this.isNomination ? 'common:textAreYouSureCopyApp' : 'common:textAreYouSureCopyApp', {}, this.isNomination ? 'Are you sure you want to copy this nomination?' : 'Are you sure you want to copy this application?' ) } ); if (proceed) { this.edit(row, true); } } ngOnDestroy () { this.sub.unsubscribe(); } }