import { Injectable } from '@angular/core'; import { AggregationService } from '@core/services/aggregation.service'; import { SpecialHandlingService } from '@core/services/special-handling.service'; import { TranslationService } from '@core/services/translation.service'; import { ApplicationEditDetail, ApplicationForUi, ApplicationViewDetail, ApplicationViewPage, RecommendedFundingInfoForUI, SpecialHandlingResponse } from '@core/typings/application.typing'; import { BudgetDetail, BudgetFundingSource } from '@core/typings/budget.typing'; import { OrganizationForInfoPanel } from '@core/typings/organization.typing'; import { ApplicationStatuses } from '@core/typings/status.typing'; import { ApplicationFormService } from '@features/application-forms/services/application-forms.service'; import { ApplicationActionService } from '@features/application-manager/services/application-actions/application-actions.service'; import { AwardService } from '@features/awards/award.service'; import { BudgetService } from '@features/budgets/budget.service'; import { CommunicationsService } from '@features/communications/communications.service'; import { FormStatuses, FormTypes } from '@features/configure-forms/form.typing'; import { EmployeeSSOFieldsService } from '@features/employee-sso-fields/employee-sso-fields.service'; import { FormHelperService } from '@features/formio/services/form-helper/form-helper.service'; import { EmailNotificationType } from '@features/system-emails/email.typing'; import { SystemTagsService } from '@features/system-tags/system-tags.service'; import { SystemTags } from '@features/system-tags/typings/system-tags.typing'; import { TextFriendlySpecialCharCleaner } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import { ApplicationViewResources } from './application-view.resources'; import { ApplicationViewState } from './application-view.state'; @AttachYCState(ApplicationViewState) @Injectable({ providedIn: 'root' }) export class ApplicationViewService extends BaseYCService { constructor ( private applicationViewResources: ApplicationViewResources, private logger: LogService, private i18n: I18nService, private translationService: TranslationService, private applicationFormService: ApplicationFormService, private employeeSSOFieldsService: EmployeeSSOFieldsService, private systemTagsService: SystemTagsService, private communicationService: CommunicationsService, private budgetService: BudgetService, private formHelperService: FormHelperService, private notifier: NotifierService, private aggregationService: AggregationService, private specialHandlingService: SpecialHandlingService, private applicationActionService: ApplicationActionService, private awardService: AwardService ) { super(); } get applicationViewMap () { return this.get('applicationViewMap'); } get applicationEditMap () { return this.get('applicationEditMap'); } /** * Gets the application detail given the ID * * @param appId: Application ID * @returns the application detail */ async getApplicationDetail (appId: number): Promise { const detail = await this.applicationViewResources.getApplication(appId); detail.specialHandling = this.specialHandlingService.getSpecialHandling(detail); return { ...detail, amountRequestedForEdit: detail.currencyRequestedAmountEquivalent, designation: detail.paymentDesignation }; } /** * Resolves the Application View / Edit Page */ async resolveApplicationView (id: number) { try { await this.setApplicationViewMap(id); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'MANAGE:textErrorLoadingApplication', {}, 'There was an error loading the application' )); throw e; } const application = this.applicationViewMap[id].application; if (application.charityId) { try { await this.setCsrData(id, application.charityId); } catch (e) { this.logger.error(e); } } } /** * Sets CSR Data for a given App * * @param appId: Application ID * @param charityId: Charity ID */ async setCsrData (appId: number, charityId: number) { const csrData = await this.aggregationService.getCharityDonationsByYear( charityId ); this.set('applicationViewMap', { ...this.applicationViewMap, [appId]: { ...this.applicationViewMap[appId], csrData } }); } /** * Appends the permissions to the application * * @param app: Application * @returns the application with the permissions appended */ addPermissionsForApplicationViewMap (app: ApplicationForUi) { const applicationActionFlags = this.applicationActionService.getApplicationActionFlags(app); const appWithPermissions = { ...app, ...applicationActionFlags }; return appWithPermissions; } /** * Sets the Application View / Edit Map for a given application * * @param id: Application ID * @param isEdit: for edit view? * @param isNewApp: is this a new app? */ async setApplicationViewMap ( id: number, isEdit = false, isNewApp = false ) { const viewTranslations = this.translationService.viewTranslations; const programTranslationMap = viewTranslations.Grant_Program; let applicationDetail = await this.getApplicationDetail(id); if (!applicationDetail.applicationStatus) { applicationDetail.applicationStatus = ApplicationStatuses.Draft; } const programTranslation = programTranslationMap[applicationDetail.programId]; applicationDetail.programName = programTranslation ? programTranslation.Name : applicationDetail.programName; applicationDetail.designation = TextFriendlySpecialCharCleaner( applicationDetail.paymentDesignation ); applicationDetail.specialHandling = { name: applicationDetail.specialHandlingName || '', address1: applicationDetail.specialHandlingAddress1 || '', address2: applicationDetail.specialHandlingAddress2 || '', city: applicationDetail.specialHandlingCity || '', state: applicationDetail.specialHandlingStateProvinceRegion || '', postalCode: applicationDetail.specialHandlingPostalCode || '', country: applicationDetail.specialHandlingCountry || '', notes: applicationDetail.specialHandlingNotes || '', reason: applicationDetail.specialHandlingReason || '', fileUrl: applicationDetail.specialHandlingFileUrl || '' }; applicationDetail = this.addPermissionsForApplicationViewMap(applicationDetail); const [ specialHandlingResponse, formsForAppView, applicantForms, awards, nomination, showSendToApplicant, employeeInfo ] = await Promise.all([ // specialHandlingResponse this.specialHandlingService.getDefaultSpecialHandling( applicationDetail.organizationId, applicationDetail.nonprofitGuid, applicationDetail.organizationName, applicationDetail.applicantId, applicationDetail.applicantFirstName, applicationDetail.applicantLastName, applicationDetail.applicantEmail, applicationDetail.applicantPhoneNumber ), // formsForAppView !isEdit ? this.applicationFormService.getAllApplicationForms( id, applicationDetail.isApplicationInClientUserWorkflowLevel, applicationDetail.applicationStatus ) : null, // applicant forms this.applicationFormService.getApplicantFormsForApplication(id, applicationDetail.applicationStatus), // awards this.awardService.getAwards(id), // nomination applicationDetail.nominationApplicationId ? this.getApplicationDetail( applicationDetail.nominationApplicationId ) : null, // showSendToApplicant isEdit ? this.getShowSendToApplicant( id, applicationDetail.applicantCanReceiveEmails, applicationDetail.applicationStatus === ApplicationStatuses.Draft ) : null, // employeeInfo this.employeeSSOFieldsService.getEmployeeSSOFieldsForApp(id), this.systemTagsService.fetchTagsForRecord( SystemTags.Buckets.Applicant, applicationDetail.applicantId ) ]); const organization = this.getOrgForInfoPanel( applicationDetail, specialHandlingResponse ); applicationDetail.defaultSpecialHandling = specialHandlingResponse.defaultAddress; applicationDetail.orgAddressString = specialHandlingResponse.orgAddressString; let budgetFundingSource: BudgetFundingSource; let budgetDetail: BudgetDetail; if (applicationDetail.budgetId) { budgetDetail = await this.budgetService.getBudgetDetail( applicationDetail.budgetId ); budgetFundingSource = budgetDetail.budgetFundingSources.find((budgetFS) => { return budgetFS.fundingSourceId === applicationDetail.fundingSourceId; }); } const amountRequested = isNewApp ? null : applicationDetail.amountRequested; const application: ApplicationViewPage = { ...applicationDetail, amountRequested, employeeInfo, inKindItems: applicationDetail.inKindItems.map((item) => { return { itemIdentification: item.itemIdentification, count: item.count }; }) }; const primaryApplicant = this.adaptPrimaryApplicant(applicationDetail); if (isEdit) { const filteredForms = applicantForms.filter((form) => { const isEligibility = form.formTypeId === FormTypes.ELIGIBILITY; const isRouting = form.formTypeId === FormTypes.ROUTING; const isNotSent = form.formStatus === FormStatuses.NotSent; return !isEligibility && !isRouting && !isNotSent; }); const recordForEdit: ApplicationEditDetail = { application, showSendToApplicant, showSaveAsDraft: !showSendToApplicant && application.applicationStatus === ApplicationStatuses.Draft, formsForEdit: filteredForms, defaultFormId: this.formHelperService.getDefaultForm(applicantForms).formId, nominee: applicationDetail.nominee, nomination, budget: budgetDetail, budgetFundingSource, awards, organization, primaryApplicant }; this.set('applicationEditMap', { ...this.applicationEditMap, [id]: recordForEdit }); } else { const recordForView: ApplicationViewDetail = { application, formsForAppView, nominee: applicationDetail.nominee, formsForEdit: applicantForms, nomination, csrData: null, budget: budgetDetail, budgetFundingSource, awards, organization, primaryApplicant }; this.set('applicationViewMap', { ...this.applicationViewMap, [id]: recordForView }); } } /** * Returns the organization info for the panel * * @param applicationDetail: Application Detail * @param specialHandlingResponse: Special Handling Response * @returns the organization info for panel */ getOrgForInfoPanel ( applicationDetail: ApplicationForUi, specialHandlingResponse: SpecialHandlingResponse ): OrganizationForInfoPanel { return applicationDetail.organizationId ? { id: applicationDetail.organizationId, guid: applicationDetail.nonprofitGuid, name: applicationDetail.organizationName, image: applicationDetail.orgnizationImageUrl, canUpdate: false, nameIsLink: true, imageUrl: applicationDetail.orgnizationImageUrl, address: null, isPrivateOrg: applicationDetail.isPrivateOrg, addressString: applicationDetail.organizationAddress, identification: applicationDetail.organizationIdentification, eligibleForGivingStatusId: applicationDetail.organizationEligibleForGivingStatus, isInternational: applicationDetail.orgIsInternational, parentGuid: specialHandlingResponse.nonprofitDetail?.parentNonprofitGuid, parentName: specialHandlingResponse.nonprofitDetail?.parentNonprofitName } : null; } /** * Returns the adapted primay applicant info * * @param applicationDetail: Application Detail * @returns the adapted primary applicant */ adaptPrimaryApplicant (applicationDetail: ApplicationForUi) { return { id: applicationDetail.applicantId, firstName: applicationDetail.applicantFirstName, lastName: applicationDetail.applicantLastName, email: applicationDetail.applicantEmail, canManageApplicants: true, canReceiveEmails: applicationDetail.applicantCanReceiveEmails, isApplicationOwner: true, phoneNumber: applicationDetail.applicantPhoneNumber, addressString: applicationDetail.applicantAddress }; } /** * Returns whether we should show the send to applicant action on edit application view * * @param applicationId: Application ID * @param notifyApplicant: Notify applicant setting * @param isDraft: is this a draft? * @returns */ async getShowSendToApplicant ( applicationId: number, notifyApplicant: boolean, isDraft: boolean ): Promise { if (isDraft && notifyApplicant) { let showSendApplication = true; const comms = await this.communicationService.getCommunicationsForApplication( applicationId, true ); const filtered = comms.map((groupedComms) => { return groupedComms[0]; }).filter((comm) => { return comm.emailNotificationType === EmailNotificationType.ApplicantAddedToApplication; }); await Promise.all(filtered.map(async (comm) => { const detail = await this.communicationService.getEmailDetails( comm.joinCommunicationId ); if (detail.applicationId === +applicationId) { showSendApplication = false; } })); return showSendApplication; } return false; } /** * Returns whether the current user is allowed to view the given app * * @param id: Application ID * @returns if the current user is allowed to view */ canViewApplication (id: number) { return this.applicationViewResources.canViewApplication(id); } /** * Triggers the Approve Modal to Open */ triggerApproveModal () { this.triggerChange('triggerApproveModal'); } /** * Triggers the Activity Tab to Update */ triggerActivityTabUpdate () { this.triggerChange('triggerActivityTabUpdate'); } /** * Gets reviewer recommended funding info for the app * * @param applicationId: app ID * @returns recommended funding info for the app */ async getReviewerRecommendedFundingInfo ( applicationId: number ): Promise { const res = await this.applicationViewResources.getReviewerRecommendedFundingInfo( applicationId ); return res.reviewerRecommendations.map((item) => { return { recommendedFundingAmount: item.reviewerRecommendedFundingAmount, decision: item.decision, createdBy: item.createdBy, createdDate: item.createdDate, workflowLevelName: item.workflowLevelName }; }); } /** * Gets the application audit trail * * @param applicationId: Application ID * @returns the activity audit trail */ getApplicationAuditTrail ( applicationId: number ) { return this.applicationViewResources.getApplicationAuditTrail(applicationId); } }