import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { SpecialHandlingService } from '@core/services/special-handling.service'; import { SpinnerService } from '@core/services/spinner.service'; import { TranslationService } from '@core/services/translation.service'; import { CyclesAPI } from '@core/typings/api/cycles.typing'; import { ApplicationByFormFromApi, ApplicationCycle, ApplyPageApplication, EligibilityResponse, MyApplicationFromApi, SaveApplication, SaveApplicationResponse, SubmitEligibility } from '@core/typings/application.typing'; import { ClientBrandingFromApi, ColorPaletteType } from '@core/typings/branding.typing'; import { ApplicantOrganization } from '@core/typings/organization.typing'; import { ProcessingTypes } from '@core/typings/payment.typing'; import { ProgramApplicantType, ProgramDetailApi } from '@core/typings/program.typing'; import { ApplicationStatuses } from '@core/typings/status.typing'; import { FormTranslations } from '@core/typings/translation.typing'; import { CancelApplicationPayload } from '@core/typings/ui/cancel-application.typing'; import { ReferenceFieldsUI } from '@core/typings/ui/reference-fields.typing'; import { ApplicationFormService } from '@features/application-forms/services/application-forms.service'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { AdaptedForm, FormStatuses, FormTypes, MyApplicationFormUI, ProgramFormForUi, ProgramFormFromApi } from '@features/configure-forms/form.typing'; import { CyclesService } from '@features/cycles/cycles.service'; import { EmployeeSSOFieldsService } from '@features/employee-sso-fields/employee-sso-fields.service'; import { EmployeeSSOFieldsData } from '@features/employee-sso-fields/employee-sso-fields.typing'; import { FormHelperService } from '@features/formio/services/form-helper/form-helper.service'; import { FormLogicService } from '@features/formio/services/form-logic/form-logic.service'; import { NonprofitService } from '@features/nonprofit/nonprofit.service'; import { ProgramAutomationService } from '@features/programs/program-automation/program-automation.service'; import { ProgramAutomationForApplicantUi, RoutingAppForSave } from '@features/programs/program-automation/program-automation.typing'; import { ProgramService } from '@features/programs/program.service'; import { ReferenceFieldsService } from '@features/reference-fields/services/reference-fields.service'; import { ArrayHelpersService, Base64, TextFriendlySpecialCharCleaner } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { ConfirmAndTakeActionService } from '@yourcause/common/modals'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import moment from 'moment'; import { ApplicationApplicantResources } from './application-applicant.resources'; import { ApplicationApplicantState } from './application-applicant.state'; @AttachYCState(ApplicationApplicantState) @Injectable({ providedIn: 'root' }) export class ApplicationApplicantService extends BaseYCService< ApplicationApplicantState > { constructor ( private logger: LogService, private programService: ProgramService, private clientSettingsService: ClientSettingsService, private spinnerService: SpinnerService, private router: Router, private referenceFieldsService: ReferenceFieldsService, private translationService: TranslationService, private cyclesService: CyclesService, private applicationApplicantResources: ApplicationApplicantResources, private i18n: I18nService, private arrayHelper: ArrayHelpersService, private notifier: NotifierService, private formHelperService: FormHelperService, private nonprofitService: NonprofitService, private programAutomationService: ProgramAutomationService, private applicationFormService: ApplicationFormService, private formLogicService: FormLogicService, private employeeSSOFieldsService: EmployeeSSOFieldsService, private specialHandlingService: SpecialHandlingService, private confirmAndTakeActionService: ConfirmAndTakeActionService ) { super(); } get myApplications () { return this.get('myApplications'); } get revisionAlerts () { return this.get('revisionAlerts'); } get formsPastDue () { return this.get('formsPastDue'); } get hasNominations () { return this.get('hasNominations'); } get routingAppForSave (): RoutingAppForSave { const b64 = sessionStorage.getItem('routingAppForSave'); if (b64) { return JSON.parse(Base64.decode(b64)); } return null; } /** * Sets the Routing App for Save * * @param app: Application to set */ setRoutingAppForSave (app: RoutingAppForSave) { const b64 = Base64.encode(JSON.stringify(app)); sessionStorage.setItem('routingAppForSave', b64); } /** * Clears the Routing App for Save */ clearRoutingAppForSave () { sessionStorage.removeItem('routingAppForSave'); } /** * Sets My Applications * * @param myApplications: My Applications to set */ setMyApplications (myApplications: MyApplicationFromApi[]) { this.set('myApplications', myApplications); } /** * Sets Revision Alerts * * @param revisionAlerts: Revision Alerts to set */ setRevisionAlerts (revisionAlerts: MyApplicationFromApi[]) { this.set('revisionAlerts', revisionAlerts); } /** * Gets the latest vetting info for the application / org * * @param appId: Application ID * @param formId: Form ID * @returns the latest vetting request status of the application */ async getLatestVettingStatusForApplication (appId: number, formId: number) { const application = await this.getApplicationByForm( appId, formId ); return application.latestVettingRequestStatusForOrg; } /** * Returns the application with form info * * @param appId: Application ID * @param formId: Form ID * @returns the application with form info */ async getApplicationByForm ( appId: number, formId: number ) { try { const application = await this.applicationApplicantResources.getApplicationByForm( appId, formId ); return application; } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'APPLY:textThereWasAnErrorLoadingTheApplication', {}, 'There was an error loading the application' ) ); return null; } } /** * Returns the application for the apply page * * @param appId: Application ID * @param formId: Form ID * @param programOrRoutingId: Program Routing ID * @param copyOfId: Copy of Application ID * @param orgId: Organization ID * @param isRouting: is for routing? * @returns the portal application for the apply page */ async getPortalApplication ( appId: number, formId: number, programOrRoutingId: string, copyOfId: number, orgId: number, isRouting: boolean ): Promise { let application: ApplicationByFormFromApi; let appToCopy: ApplicationByFormFromApi; let program: ProgramDetailApi; let routing: ProgramAutomationForApplicantUi; let employeeInfo = {} as EmployeeSSOFieldsData; let referenceFields: ReferenceFieldsUI.RefResponseMap = {}; let clientBranding: ClientBrandingFromApi; let clientId: number; let formRevisionId: number; let eligibilityRevisionId: number; let cycle: ApplicationCycle; let form: AdaptedForm; let eligibilityForm: ProgramFormForUi; let organization: ApplicantOrganization; try { if (appId) { application = await this.applicationApplicantResources.getApplicationByForm( appId, formId ); } // Standard Program Apply - not routing if (!isRouting) { const progId = (programOrRoutingId || application.grantProgramId); program = await this.programService.getProgramForApplicant(progId); clientId = program.clientId; if (copyOfId) { appToCopy = await this.applicationApplicantResources.getApplicationByForm( copyOfId, program.form.formId ); } cycle = this.getCycleForApplyPage( application?.cycle, program.grantProgramId, program.cycles ); await this.clientSettingsService.setAvailableApplicantCurrencies( program.grantProgramId ); formRevisionId = application?.form?.formRevisionId || appToCopy?.form?.formRevisionId || program.form.formRevisionId; eligibilityRevisionId = !application || appToCopy ? program.eligibilityForm?.formRevisionId : undefined; await this.prepareReferenceFields( formRevisionId, eligibilityRevisionId, this.routingAppForSave?.formRevisionId ); eligibilityForm = this.getEligibilityForm(program, application); const _form = application?.form || program?.form; form = this.formLogicService.adaptFormForTabs( _form, _form.formId, _form.formRevisionId ); if (application || appToCopy) { employeeInfo = await this.employeeSSOFieldsService.getEmployeeSSOFieldsForApp( appId || copyOfId ); referenceFields = await this.referenceFieldsService.getReferenceFieldResponses( appId || copyOfId, application?.form?.applicationFormId || appToCopy.form.applicationFormId, form.formDefinition, this.formHelperService.getTableAndSubsetIdsFromFormDefinition( [form.formDefinition] ), null, false, false, undefined, eligibilityForm?.formData ); const translations = this.translationService.viewTranslations.FormTranslation[ formId || program.form.formId ]; this.appendFormTranslationsToApp( application || appToCopy, translations ); } organization = application ? application.organization : (appToCopy ? appToCopy.organization : null); if (!appId && orgId) { organization = await this.nonprofitService.getApplicantOrganizationFromOrgId( +orgId ); } } else { // Routing Scenario routing = await this.programAutomationService.getRoutingInfoForApplicant(programOrRoutingId); formRevisionId = routing.formRevisionId; await this.prepareReferenceFields(null, null, formRevisionId); clientId = routing.clientId; form = routing.form; } clientBranding = await this.getClientBranding(clientId); return this.adaptToPortalApplication( appId, application, appToCopy, program, organization, clientId, form, eligibilityForm, cycle, employeeInfo, referenceFields, clientBranding, routing ); } catch (e) { this.logger.error(e); this.spinnerService.stopSpinner(); this.router.navigate([ '/apply/applications' ]); return null; } } /** * Returns the relevant eligibility form * * @param program: Program * @param application: Application * @returns the relevant eligibility form */ getEligibilityForm ( program: ProgramDetailApi, application: ApplicationByFormFromApi ) { let eligibilityForm = program?.eligibilityForm ? this.formLogicService.adaptFormForTabs( program.eligibilityForm, program.eligibilityForm.formId, program.eligibilityForm.formRevisionId ) : null; const isExistingEligibility = application?.form?.formType === FormTypes.ELIGIBILITY; if (isExistingEligibility) { eligibilityForm = this.formLogicService.adaptFormForTabs( application.form, application.form.formId, application.form.formRevisionId ); } return eligibilityForm; } /** * Adapts to the portal application for the apply page * * @param id: Application ID * @param application: Application * @param appToCopy; Application to Copy * @param program: Program * @param organization: Organization * @param clientId: Client ID * @param form: Form * @param eligibilityForm: Eligibility Form * @param cycle: Cycle * @param employeeInfo: Employee SSO Info * @param referenceFields: Reference Field answers * @param clientBranding: Client Branding info * @param routing: Routing info * @returns the adapted application for apply page */ adaptToPortalApplication ( id: number, application: ApplicationByFormFromApi, appToCopy: ApplicationByFormFromApi, program: ProgramDetailApi, organization: ApplicantOrganization, clientId: number, form: AdaptedForm, eligibilityForm: ProgramFormForUi, cycle: ApplicationCycle, employeeInfo: EmployeeSSOFieldsData, referenceFields: ReferenceFieldsUI.RefResponseMap, clientBranding: ClientBrandingFromApi, routing: ProgramAutomationForApplicantUi ): ApplyPageApplication { const amountRequested = application ? application.amountRequested : (appToCopy ? appToCopy.amountRequested : null); const currencyRequestedAmountEquivalent = application ? application.currencyRequestedAmountEquivalent : (appToCopy ? appToCopy.currencyRequestedAmountEquivalent : null); return { id, applicationId: id, applicationFormId: application?.form?.applicationFormId, copyApplicationFormId: appToCopy?.form?.applicationFormId, cycles: program?.cycles ?? [], isApplicationDraft: application ? application.isApplicationDraft : true, organization, programId: program?.grantProgramId, fallbackProgramGuid: routing?.fallbackGrantProgramGuid, ruleSetFailureMessage: routing?.ruleSetFailureMessage, charityBucketId: program?.charityBucketId, charityBucketDescription: program?.charityBucketDescription, grantProgramName: program?.grantProgramName, grantProgramDescription: program?.grantProgramDescription, programApplicantType: program?.programApplicantType ?? ProgramApplicantType.INDIVIDUAL, clientName: program?.clientName || routing?.clientName, clientLogoUrl: program?.logoUrl || program?.clientLogoUrl || routing?.clientLogoUrl, clientId, form, formId: form.formId, revisionId: form.formRevisionId, eligibilityForm, programAllowCollaboration: !id ? null : application.programAllowCollaboration, programSendEmailsToCollaborators: !id ? null : application.programSendEmailsToCollaborators, hasDraftApplication: program?.hasDraftApplication ?? false, passMessage: program?.eligibilityPassMessage || this.programService.passMessage, failMessage: program?.eligibilityFailMessage || this.programService.failMessage, clientOrganizationsProcessingTypeId: cycle?.isClientProcessing ? ProcessingTypes.Client : ProcessingTypes.YourCause, formType: application ? application.form.formType : FormTypes.REQUEST, hideCycleDatesInApplicantPortal: program?.hideCycleDatesInApplicantPortal, amountRequested, currencyRequestedAmountEquivalent, amountRequestedForEdit: currencyRequestedAmountEquivalent, currencyRequested: application ? application.currencyRequested : (appToCopy ? appToCopy.currencyRequested : null), allowAddOrg: typeof program?.allowAddOrg !== 'boolean' ? true : program?.allowAddOrg, nominee: application ? application.nominee : (appToCopy ? appToCopy.nominee : null), statusId: application ? (application.applicationStatusId || 0) : 0, nominationApplicationId: application ? application.nominationApplicationId : null, designation: application ? application.paymentDesignation : (appToCopy ? TextFriendlySpecialCharCleaner(appToCopy.paymentDesignation) : ''), careOf: application ? application.careOf : (appToCopy ? appToCopy.careOf : ''), specialHandling: this.specialHandlingService.getSpecialHandling(application || appToCopy), employeeInfo, isArchived: application ? application.isArchived : false, isProgramArchived: program?.isArchived, referenceFields, inKindAmountRequested: application ? application.inKindAmountRequested : (appToCopy ? appToCopy.inKindAmountRequested : 0), inKindItems: application ? application.inKindItems : (appToCopy ? appToCopy.inKindItems : []), cycle, defaultFormId: program?.form.formId, latestVettingRequestStatusForOrg: application?.latestVettingRequestStatusForOrg, invitationOrganizationId: application?.invitationOrganizationId, clientBranding }; } /** * Fetches necessary reference field info based on the revision id(s) * * @param formRevisionId: Form revision ID * @param eligibilityFormRevisionId: Eligibility form revision ID * @param routingFormRevisionId: Routing form revision ID */ async prepareReferenceFields ( formRevisionId: number, eligibilityFormRevisionId: number, routingFormRevisionId: number ) { const [ formFields, eligibilityFields, routingFields ] = await Promise.all([ this.referenceFieldsService.fetchFieldsByFormRevisionId(formRevisionId), this.referenceFieldsService.fetchFieldsByFormRevisionId(eligibilityFormRevisionId), this.referenceFieldsService.fetchFieldsByFormRevisionId(routingFormRevisionId) ]); this.referenceFieldsService.setAllReferenceFields([ ...formFields, ...eligibilityFields, ...routingFields ]); } /** * Finds the relevant cycle when applying for a program * * @param appCycle: Application Cycle * @param grantProgramId: Grant Program ID * @param programCycles: Program Cycles * @returns the relevant cycle when applying */ getCycleForApplyPage ( appCycle: ApplicationCycle, grantProgramId: number, programCycles: CyclesAPI.BaseProgramCycle[] ) { let cycle: ApplicationCycle; if (appCycle) { cycle = appCycle; } else { const { currentCycle } = this.cyclesService.getCycleDateHelpers(programCycles); if (currentCycle) { cycle = { id: currentCycle.id, startDate: currentCycle.startDate, endDate: currentCycle.endDate, grantProgramId, isArchived: currentCycle.isArchived, isClientProcessing: currentCycle.isClientProcessing, clientOrganizationsProcessingTypeId: currentCycle.clientOrganizationsProcessingTypeId }; } } return cycle; } /** * Adds the form translation values to the application forms * * @param application: Application to adapt * @param translations: translations to append */ appendFormTranslationsToApp ( application: ApplicationByFormFromApi, translations: FormTranslations ) { application.form.name = translations && translations.Name ? translations.Name : application.form.name; application.form.description = translations && translations.Description ? translations.Description : application.form.description; } /** * Returns client branding info by client ID * * @param clientId: Client ID * @returns client branding info */ async getClientBranding (clientId: number) { const branding = await this.applicationApplicantResources.getClientBranding(clientId); this.clientSettingsService.setClientBranding({ brandPrimary: branding.brandPrimary, brandSecondary: branding.brandSecondary, brandUtility: branding.brandUtility, logoUrl: branding.logoUrl, chartPrimary: branding.brandPrimary, chartSecondary: branding.brandSecondary, chartUtility: branding.brandUtility, colorPalette: ColorPaletteType.STANDARD }); return branding; } /** * Clears client branding */ clearClientBranding () { this.clientSettingsService.setClientBranding(undefined); } /** * Gets My Applications * * @param force: force the endpoint call? */ async getMyApplications (force = false) { if (force || !this.myApplications) { const data = await this.applicationApplicantResources.getApplications( 1, 1000, true ); const { rows, hasNominations, revisionAlerts, formsPastDue } = this.adaptMyApplications(data.data); this.set('myApplications', rows); this.set('revisionAlerts', revisionAlerts); this.set('formsPastDue', formsPastDue); this.set('hasNominations', hasNominations); } } /** * Adapts My Application records * * @param records: Records to adapt * @returns the adapted records */ adaptMyApplications ( records: MyApplicationFromApi[] ): { rows: MyApplicationFromApi[]; hasNominations: boolean; revisionAlerts: MyApplicationFromApi[]; formsPastDue: MyApplicationFormUI[]; } { let hasNominations = false; const revisionAlerts: MyApplicationFromApi[] = []; const formsPastDue: MyApplicationFormUI[] = []; const rows = records.map((app) => { if (!!app.nominee) { hasNominations = true; } app.organizationName = app.organizationName || this.i18n.translate('common:textNone'); if (!app.status) { app.status = ApplicationStatuses.Draft; } app.files = [ ...app.files, ...app.mergeDocuments.map((item) => { return { fileName: item.documentTemplateName ? `${item.documentTemplateName}.pdf` : item.fileName, uploadedDate: item.uploadedDate, uploadedBy: item.uploadedBy, attachmentId: item.attachmentId, fileUploadId: item.fileUploadId, attachmentType: item.attachmentType }; }) ]; app.applicationForms.forEach((form) => { if (!form.status) { form.status = FormStatuses.DraftSaved; } if (form.status === FormStatuses.RevisionRequested) { revisionAlerts.push({ ...app, applicationForms: [form] }); } if (form.dueDate) { const showDueDate = this.applicationFormService.isDueDateRelevant( form.status, app.status ); if (showDueDate) { const isOverdue = this.applicationFormService.getIsFormOverdue(form.dueDate); if (isOverdue) { formsPastDue.push({ ...form, applicationId: app.applicationId, grantProgramName: app.grantProgramName, grantProgramId: app.grantProgramId }); } } else { // Due date not relavant if submitted form.dueDate = null; } } }); app.applicationForms = this.arrayHelper.sort(app.applicationForms, 'id'); return app; }).sort((a, b) => { if (a.status === ApplicationStatuses.Hold) { return -1; } if (b.status === ApplicationStatuses.Hold) { return 1; } if (moment(a.createdDate).isSame(moment(b.createdDate))) { return 0; } return moment(a.createdDate).isAfter(moment(b.createdDate)) ? -1 : 1; }); return { rows, hasNominations, revisionAlerts, formsPastDue }; } /** * Handles confirm modal and delete of the application * * @param applicationId: Application ID * @param isNomination: is nomination? * @returns if the delete application passed */ async handleDeleteApplication ( applicationId: number, isNomination = false ) { const response = await this.confirmAndTakeActionService.genericConfirmAndTakeAction( () => this.applicationApplicantResources.deleteApplication(applicationId), this.i18n.translate( isNomination ? 'APPLY:hdrDeleteNomination' : 'APPLY:hdrDeleteApplication', {}, isNomination ? 'Delete Nomination' : 'Delete Application' ), '', this.i18n.translate( isNomination ? 'APPLY:textAreYouSureDeleteNomination' : 'APPLY:textAreYouSureDeleteApplication', {}, isNomination ? 'Are you sure you want to delete this nomination? This action cannot be undone.' : 'Are you sure you want to delete this application? This action cannot be undone.' ), this.i18n.translate( 'common:btnDelete', {}, 'Delete' ), this.i18n.translate( isNomination ? 'APPLY:textSuccessfullyDeletedNomination' : 'APPLY:textSuccessfullyDeletedApplication', {}, isNomination ? 'Successfully deleted the nomination' : 'Successfully deleted the application' ), this.i18n.translate( isNomination ? 'APPLY:textErrorDeletingNomination' : 'APPLY:textErrorDeletingApplication', {}, isNomination ? 'There was an error deleting the nomination' : 'There was an error deleting the application' ) ); if (response?.passed) { this.spinnerService.startSpinner(); await this.getMyApplications(true); this.spinnerService.stopSpinner(); } } /** * Remove the given application from My Applications * * @param applicationId: Application ID */ removeFromMyApplications (applicationId: number) { const myApps = this.myApplications; const myRevisions = this.revisionAlerts; const index = myApps.findIndex((app) => { return +app.applicationId === +applicationId; }); if (index > -1) { this.set('myApplications', [ ...myApps.slice(0, index), ...myApps.slice(index + 1) ]); } const revisionIndex = myRevisions.findIndex((app) => { return +app.applicationId === +applicationId; }); if (revisionIndex > -1) { this.set('revisionAlerts', [ ...myRevisions.slice(0, revisionIndex), ...myRevisions.slice(revisionIndex + 1) ]); } } /** * Gets the draft applications for a program * * @param grantProgramGuid: Grant program guid * @param programCycles: Grant program cycles * @returns the draft applications */ getDraftAppsForProgram ( grantProgramGuid: string, programCycles: CyclesAPI.BaseProgramCycle[] ): MyApplicationFromApi[] { const cycleMap: Record = {}; programCycles.forEach((cycle) => { cycleMap[cycle.id] = cycle; }); return this.myApplications.filter((app) => { const cycle = cycleMap[app.cycleId]; return app.isDraft && !app.isArchived && app.grantProgramGuid === grantProgramGuid && !cycle?.isArchived && moment(cycle?.endDate).isAfter(moment()); }); } /** * Handles saving the application changes * * @param data: Payload for save * @returns Endpoint response - SaveApplicationResponse model */ saveApplication (data: SaveApplication): Promise { return this.applicationApplicantResources.saveApplication(data); } /** * Handles submitting eligibility answers * * @param data: Payload for submitting eligibility * @returns the eligibility response */ submitEligibility (data: SubmitEligibility): Promise { return this.applicationApplicantResources.submitEligibility(data); } /** * Returns an existing submitted apps for a program * * @param programId: Program ID * @returns the existing submitted applications for the program */ checkForExistingSubmittedApps (programId: number) { return this.myApplications.some((app) => { return +app.grantProgramId === +programId && !app.isDraft; }); } /** * Handles canceling an application * * @param payload: Payload for cancel Application */ async cancelApplication (payload: CancelApplicationPayload) { const response = await this.confirmAndTakeActionService.genericTakeAction( () => this.applicationApplicantResources.cancelApplication(payload), this.i18n.translate( 'common:textSuccessfullyCancelledApplication', {}, 'Successfully cancelled application' ), this.i18n.translate( 'common:textUnableToCancelApplication', {}, 'There was a problem canceling this application' ) ); if (response.passed) { this.spinnerService.startSpinner(); await this.getMyApplications(true); this.spinnerService.stopSpinner(); } } /** * This ties the routing response to the application when it is created * * @param applicationId: Application ID * @param appSavePayload: The payload used to save the Application */ async saveApplicantRoutingResponsesToApp ( applicationId: number, appSavePayload: SaveApplication ) { const routingAppForSave = this.routingAppForSave; const routingFormId = routingAppForSave.formId; const routingRevisionId = routingAppForSave.formRevisionId; const data: SaveApplication = { ...appSavePayload, applicationId, amountRequested: routingAppForSave.amountRequested || 0, currencyRequested: routingAppForSave.currencyRequested, requiredReferenceFieldKeys: [], amountRequestedRequired: false, careOfRequired: false, paymentDesignationRequired: false, form: { ...appSavePayload.form, formId: routingFormId, formRevisionId: routingRevisionId, isDraft: false } }; const res = await this.saveApplication(data); const response = this.referenceFieldsService.adaptFormioChangesForSave( routingAppForSave.referenceFields, null, null, null, false, false ); await this.applicationFormService.handleSaveOfChangedFields( response, applicationId, routingFormId, routingRevisionId, res.applicationFormId, false, false ); this.clearRoutingAppForSave(); } }