import { AfterViewInit, Component, OnDestroy } from '@angular/core'; import { FormControl, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { SearchAllOrgsModalComponent } from '@core/components/search-all-orgs-modal/search-all-orgs-modal.component'; import { ApplicantService } from '@core/services/auth-user/applicant.service'; import { MixpanelService } from '@core/services/mixpanel.service'; import { SpecialHandlingService } from '@core/services/special-handling.service'; import { SpinnerService } from '@core/services/spinner.service'; import { StatusService } from '@core/services/status.service'; import { TimeZoneService } from '@core/services/time-zone.service'; import { TranslationService } from '@core/services/translation.service'; import { Applicant } from '@core/typings/applicant.typing'; import { ApplicationApplicantFile, ApplyPageApplication, BaseApplication, EligibilityResponse, NominationForm, Nominee, SaveApplication, SubmitEligibility } from '@core/typings/application.typing'; import { ProcessingTypes } from '@core/typings/payment.typing'; import { ApplicationInfoForPDF, FormInfoForPDF, OrgInfoForPDF } from '@core/typings/pdf.typing'; import { ProgramApplicantType } from '@core/typings/program.typing'; import { ApplicationStatuses } from '@core/typings/status.typing'; import { ReferenceFieldsUI } from '@core/typings/ui/reference-fields.typing'; import { ContactSupport } from '@core/typings/ui/support.typing'; import { UserSignature } from '@core/typings/user.typing'; import { AddOrganizationService } from '@features/add-organization/add-organization.service'; import { ApplicationDownloadService } from '@features/application-download/application-download.service'; import { ApplicationFormService } from '@features/application-forms/services/application-forms.service'; import { ApplicationAttachmentService } from '@features/application-view/application-attachments/application-attachments.service'; import { AwardService } from '@features/awards/award.service'; import { MyAward } from '@features/awards/typings/award.typing'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { CollaborationService } from '@features/collaboration/collaboration.service'; import { BaseApplicationForLogic, FormData, FormForApplicantForUI, FormForApplicantFromApi, FormioChanges, FormStatuses, FormTypes, MyApplicationFormUI, ProgramFormForUi } from '@features/configure-forms/form.typing'; import { CyclesService } from '@features/cycles/cycles.service'; import { BlankSpecialHandling } from '@features/formio/formio-components/standard-formio-components/gc-special-handling/gc-special-handling.component'; import { ComponentHelperService } from '@features/formio/services/component-helper/component-helper.service'; import { FormHelperService } from '@features/formio/services/form-helper/form-helper.service'; import { ReportFieldService } from '@features/formio/services/report-field/report-field.service'; import { InKindService } from '@features/in-kind/in-kind.service'; import { InKindCategoryItemStat } from '@features/in-kind/in-kind.typing'; import { LogicState } from '@features/logic-builder/logic-builder.typing'; import { NonprofitService } from '@features/nonprofit/nonprofit.service'; import { ProgramAutomationService } from '@features/programs/program-automation/program-automation.service'; import { ReferenceFieldsService } from '@features/reference-fields/services/reference-fields.service'; import { SignatureService } from '@features/signature/signature.service'; import { SupportService } from '@features/support/support.service'; import { UserService } from '@features/users/user.service'; import { AddressFormatterService, CallMaker, CallMakerFactory, OrganizationEligibleForGivingStatus, SearchResult, SimpleStringMap, TextFriendlySpecialCharCleaner, YcFile } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { ConfirmActionResult, ConfirmationModalWithSecondaryActionComponent, ModalFactory } from '@yourcause/common/modals'; import { NotifierService } from '@yourcause/common/notifier'; import { SignatureModalResponse } from '@yourcause/common/signature'; import moment from 'moment'; import { ApplicationApplicantService } from '../application-applicant.service'; import { ApplicationAwardsModalComponent } from '../application-awards-modal/application-awards-modal.component'; @Component({ selector: 'gc-apply-page', templateUrl: './apply-page.component.html', styleUrls: ['./apply-page.component.scss'] }) export class ApplyPageComponent implements AfterViewInit, OnDestroy { programOrRoutingId = this.activatedRoute.snapshot.params.programId; formId = this.activatedRoute.snapshot.params.formId; applicationId = this.activatedRoute.snapshot.params.applicationId; copyOfId = this.activatedRoute.snapshot.queryParams.copyOf; submitting: boolean; FormTypes = FormTypes; FormStatuses = FormStatuses; ProgramApplicantType = ProgramApplicantType; applicant: Applicant; applicantAddressString: string; isApplicationDraft: boolean; application: ApplyPageApplication = {} as ApplyPageApplication; _showForm: boolean; _showEligibilityForm: boolean; programNotStarted: boolean; formData: FormData; eligibilityFormData: FormData; selectedOrg: SearchResult; orgId: number; hideSelectOrg = false; eligibilityPassed = this.i18n.translate( 'APPLY:textEligibilityPassed', {}, 'Eligibility Passed' ); eligibilityFailed = this.i18n.translate( 'APPLY:textQualificationsNotMet', {}, 'Qualifications Not Met' ); instructions = ''; isNomination = false; nominationForm: NominationForm; formStatusMap = this.statusService.formStatusMap; statusText = ''; statusIcon = ''; pendingFormioChanges: FormioChanges[] = []; nomineeFormValid: boolean; markForValidity: boolean; revisionRequested = false; canEditForm = false; ApplicationStatuses = ApplicationStatuses; portalTranslations = this.translationService.viewTranslations; formTranslationMap = this.portalTranslations.FormTranslation; cycleTranslations = this.portalTranslations.Grant_Program_Cycle; currentFormName: string; currentFormDescription: string; showDates = false; acceptingNewSubmissions = false; programAcceptanceText = ''; programIsAcceptingText = this.i18n.translate( 'APPLY:textProgramIsAcceptingApplications', {}, 'Program is accepting applications' ); programIsAcceptingNominations = this.i18n.translate( 'APPLY:textProgramIsAcceptingNominations', {}, 'Program is accepting nominations' ); programIsNotAcceptingText = this.i18n.translate( 'APPLY:textProgramIsNotAccepting', {}, 'Program is not accepting applications' ); programIsNotAcceptingNominations = this.i18n.translate( 'APPLY:textProgramIsNotAcceptingNominations', {}, 'Program is not accepting nominations' ); isAfterInit = false; formAlert: MyApplicationFormUI; files: ApplicationApplicantFile[] = []; appSubmittedDate: string; refFieldsToCopyAfterEligibility: ReferenceFieldsUI.RefResponseMap; updatingOrg = false; autoSaving = false; forms: FormForApplicantForUI[]; conditionalVisibilityState: LogicState; savedSignatureFile: YcFile; applicantSignature: UserSignature; callMaker: CallMaker = CallMakerFactory.create( (changes) => this.handleAutoSave(changes), 500, false, true ); translations: SimpleStringMap = {}; richTextTranslations: SimpleStringMap = {}; awards: MyAward[]; awardTotalText: string; constructor ( private mixpanel: MixpanelService, private logger: LogService, private referenceFieldsService: ReferenceFieldsService, private activatedRoute: ActivatedRoute, private userService: UserService, private applicantService: ApplicantService, private router: Router, private modalFactory: ModalFactory, private spinnerService: SpinnerService, private notifier: NotifierService, private i18n: I18nService, private applicationApplicantService: ApplicationApplicantService, private applicationDownloadService: ApplicationDownloadService, private addressFormatter: AddressFormatterService, private statusService: StatusService, private nonprofitService: NonprofitService, private clientSettingsService: ClientSettingsService, private translationService: TranslationService, private inKindService: InKindService, private cyclesService: CyclesService, private notifierService: NotifierService, private specialHandlingService: SpecialHandlingService, private collaborationService: CollaborationService, private applicationAttachmentService: ApplicationAttachmentService, private supportService: SupportService, private formHelperService: FormHelperService, private componentHelper: ComponentHelperService, private addOrgService: AddOrganizationService, private reportFieldService: ReportFieldService, private timezoneService: TimeZoneService, private signatureService: SignatureService, private programAutomationService: ProgramAutomationService, private applicationFormService: ApplicationFormService, private awardService: AwardService ) { } get isRouting () { return this.activatedRoute.snapshot.data.isRouting; } get showForm () { return this._showForm; } set showForm (val) { this._showForm = val; } get showEligibilityForm () { return this._showEligibilityForm; } set showEligibilityForm (val) { this._showEligibilityForm = val; } async ngAfterViewInit () { await this.init(); } async init () { this.spinnerService.startSpinner(); const orgId = this.activatedRoute.snapshot.queryParamMap.get('organizationId'); this.application = await this.applicationApplicantService.getPortalApplication( this.applicationId, this.formId, this.programOrRoutingId, this.copyOfId, orgId ? +orgId : null, this.isRouting ); this.setHelpers(); if (this.application.form?.requireSignature) { await this.signatureService.setSignature(); this.applicantSignature = this.signatureService.userSignature; this.savedSignatureFile = this.signatureService.savedSignatureFile; } if (this.applicationId) { this.forms = await this.applicationFormService.getAllApplicantForms( this.applicationId, this.application.statusId ); } if (this.copyOfId) { this.pendingFormioChanges = this.formHelperService.getFormioChangesForCopyApp( this.application.referenceFields ); this.referenceFieldsService.clearOutTableRowIdsForCopy( this.application.referenceFields ); this.refFieldsToCopyAfterEligibility = { ...this.application.referenceFields }; } await Promise.all([ this.clientSettingsService.setClientSettingsForApplicant( this.application.clientId ), this.setInKindCategories() ]); const [ clientAffiliateInfo ] = await Promise.all([ this.applicantService.getClientAffiliateInfoWithFriendlyNames(this.clientSettingsService?.clientBranding?.name), this.clientSettingsService.setClientIntAuthorities() ]); this.mixpanel.register({ ...clientAffiliateInfo, 'Client Name': this.application.clientName, 'Program Name': this.application.grantProgramName }); await this.handleApplicantType(); this.setInstructions(); this.setFormAlertAndFiles(); this.setContactSupportPrepopData(); this.isAfterInit = true; this.spinnerService.stopSpinner(); if (this.showForm) { if (!this.isRouting) { this.callMaker.invoke(() => this.pendingFormioChanges); } } } setHelpers () { this.setApplicant(); this.setNominationAttrs(); this.formData = this.application.form?.formData; this.eligibilityFormData = this.application.eligibilityForm?.formData; this.isApplicationDraft = !this.applicationId || this.application.form.isDraft; this.revisionRequested = this.application.form.revisionNotes && this.application.form.isDraft && this.application.form.applicationFormStatusId !== FormStatuses.Submitted; this.showDates = !this.application.hideCycleDatesInApplicantPortal; this.awards = (this.awardService.myAwards || []).filter((award) => { return award.applicationId === +this.applicationId; }); this.awardTotalText = this.awardService.getAwardTotalText(this.awards); this.checkProgramCycles(); } viewAwards () { this.modalFactory.open( ApplicationAwardsModalComponent, { applicationId: this.applicationId, awards: this.awards } ); } async setInKindCategories () { await this.inKindService.getCategoriesForApplicant(this.application.clientId); } setApplicant () { this.applicant = this.userService.get('applicant'); this.applicantAddressString = this.addressFormatter.formatSimpleGrantsAddressToSingleLine({ address: this.applicant.address, address2: this.applicant.address2, city: this.applicant.city, state: this.applicant.state, country: this.applicant.country, postalCode: this.applicant.postalCode }); } setContactSupportPrepopData () { const app = this.application; this.supportService.setPrepopData({ contactReason: null as ContactSupport.ContactReasonFullList, firstName: this.applicant.firstName, lastName: this.applicant.lastName, email: this.applicant.email, phoneNumber: this.applicant.phoneNumber, clientName: app.clientName, applicationId: app.id, programName: app?.grantProgramName, organizationName: app.organization?.name, taxId: app.organization?.orgIdentification, description: '' }); } setNominationAttrs () { this.isNomination = this.application.form.formType === FormTypes.NOMINATION; if (this.isNomination) { const nominee: Nominee = this.application.nominee || ({} as Nominee); this.nominationForm = { email: nominee.email, firstName: (nominee.firstName || '').trim(), lastName: (nominee.lastName || '').trim(), phoneNumber: nominee.phoneNumber, position: nominee.position }; this.nomineeFormValid = !!nominee.email && !!nominee.firstName && !!nominee.lastName && !Validators.email(new FormControl(nominee.email)); } } checkProgramCycles () { if (this.isRouting) { this.canEditForm = true; } else { const cycleHelpers = this.cyclesService.getCycleDateHelpers( this.applicationId ? [this.application.cycle] : this.application.cycles ); const notSubmittedYet = this.application.statusId === ApplicationStatuses.Draft; if (notSubmittedYet) { if (this.applicationId && cycleHelpers.currentCycle) { this.acceptingNewSubmissions = +this.application.cycle.id === +cycleHelpers.currentCycle.id; } else { this.acceptingNewSubmissions = !!cycleHelpers.currentCycle; } } if (!this.acceptingNewSubmissions) { const isDefaultFormWithNoRevisionRequest = !this.revisionRequested && this.application.form.formId === this.application.defaultFormId; this.canEditForm = !notSubmittedYet && !this.application.isArchived && !isDefaultFormWithNoRevisionRequest && ( this.revisionRequested || this.application.form.applicationFormStatusId !== FormStatuses.Submitted ); } else { this.canEditForm = this.isApplicationDraft; } if (notSubmittedYet) { if (this.acceptingNewSubmissions) { if (this.showDates) { this.programAcceptanceText = this.i18n.translate( this.isNomination ? 'APPLY:textAcceptingNominationsUntilDate' : 'APPLY:textAcceptingApplicationsUntilDate', { date: this.getDateForDisplay(cycleHelpers.currentCycle.endDate) }, this.isNomination ? 'Accepting nominations until __date__' : 'Accepting applications until __date__' ); } else { this.programAcceptanceText = this.programIsAcceptingText; } } else { this.programAcceptanceText = this.programIsNotAcceptingText; } } } } getDateForDisplay (date: string) { return this.timezoneService.returnDateTimeAndTZ(moment(date).format('LLL'), 'lll'); } setInstructions () { if (this.isRouting) { this.instructions = this.i18n.translate( 'common:textApplicantRoutingInstructions', {}, 'Fill out and submit the form on the right. Based on your responses you will be routed to a grant program where you will complete your grant application.' ); } else { const fillOutFormInstructions = this.i18n.translate( 'APPLY:textFillOutFormInstructions', {}, 'Fill out the form to your right. You can save progress and return later to submit the form.' ); const alreadySubmittedInstructions = this.i18n.translate( 'APPLY:textAlreadySubmittedForm', {}, 'Your form has already been submitted. You can review your responses here.' ); if (this.revisionRequested) { this.instructions = this.i18n.translate( 'APPLY:FORM_REQUIRES_REVIEW' ); } else if (this.acceptingNewSubmissions) { if (this.isApplicationDraft) { if (this.showEligibilityForm) { this.instructions = this.i18n.translate( 'APPLY:textCompleteEligibilityInstructions', {}, 'Complete the eligibility form below to access the grant application forms.' ); } else if (this.showForm) { this.instructions = fillOutFormInstructions; } } else { this.instructions = alreadySubmittedInstructions; } } else if ( this.application.statusId === ApplicationStatuses.Draft && this.application.form.formType === FormTypes.REQUEST ) { this.instructions = this.i18n.translate( 'APPLY:textProgramExpiredInstructions', {}, 'This application cannot be submitted because the program is no longer accepting applications.' ); } else { if ( this.application.statusId !== ApplicationStatuses.Draft && this.isApplicationDraft ) { this.instructions = fillOutFormInstructions; } else { this.instructions = alreadySubmittedInstructions; } } } } setFormAlertAndFiles () { const formAlerts = this.applicationApplicantService.formsPastDue; this.formAlert = formAlerts.find((form) => { return +form.id === +this.application.applicationFormId; }); const thisApp = this.applicationApplicantService.myApplications.find((app) => { return +app.applicationId === +this.applicationId; }); this.files = thisApp?.files || []; const defaultForm = thisApp?.applicationForms.find((form) => { return form.formId === this.application.defaultFormId; }); this.appSubmittedDate = defaultForm?.submissionDate; } async handleApplicantType () { if (this.application.programApplicantType === ProgramApplicantType.ORGS) { if (!this.application.organization) { this.spinnerService.stopSpinner(); await this.showSelectOrgModal(); } else { await this.setOrganization(); await this.handleEligibility(); } } else { this.hideSelectOrg = true; await this.handleEligibility(); this.setApplicantAddressAsHandling(); } } setShowFormAttr (attr: 'showForm'|'showEligibilityForm', show: boolean) { this[attr] = show; let form; if (this.showForm) { form = this.application.form; } else if (this.showEligibilityForm) { form = this.application.eligibilityForm; } if (form) { this.currentFormName = form.name; this.currentFormDescription = form.description; } else { this.currentFormName = ''; this.currentFormDescription = ''; } } async setOrganization () { const org = this.application.organization; if (org) { let address1; let address2; let city; let stateProvRegCode; let country; let postalCode; let addressString; let displayName; let email; let phone; let parentName; let parentGuid; const registrationId = org.orgIdentification; if (org.nonprofitGuid) { const response = await this.nonprofitService.getNonprofitAdditionalDataByGuid( org.nonprofitGuid ); if (response.nonprofitDetail) { const address = this.nonprofitService.getNonprofitAddressFields( response.nonprofitDetail ); address1 = address.address1; address2 = address.address2; city = address.city; stateProvRegCode = address.stateProvRegCode; country = address.country; postalCode = address.postalCode; addressString = this.addressFormatter.returnSingleLine( address ); displayName = response.nonprofitDetail.displayName; email = response.nonprofitDetail.displayEmail; phone = response.nonprofitDetail.displayNumber; parentName = response.nonprofitDetail.parentNonprofitName; parentGuid = response.nonprofitDetail.parentNonprofitGuid; } } this.setSelectedOrg( SearchResult.construct({ text: org.name, document: { id: '' + org.id, address1: address1 || org.address, address2: address2 || org.address2, city: city || org.city, country: country || org.country, iconURL: org.imageUrl, name: org.name, displayName: displayName || org.name, email, phone, postalCode: postalCode || org.postalCode, registrationId, isPrivateOrg: org.isPrivateOrg, stateProvRegCode: stateProvRegCode || org.state, addressString: addressString || this.addressFormatter.formatSimpleGrantsAddressToSingleLine({ address: org.address, address2: org.address2, city: org.city, state: org.state, country: org.country, postalCode: org.postalCode }), parentName, parentGuid } }) ); } } setSelectedOrg (org: SearchResult) { this.selectedOrg = org; if (org) { this.setOrgAddressAsHandling(org); } else { this.setApplicantAddressAsHandling(); } } setOrgAddressAsHandling (org: SearchResult) { this.application.defaultSpecialHandling = { name: org.document.name || '', address1: org.document.address1 || '', address2: org.document.address2 || '', city: org.document.city || '', state: org.document.stateProvRegCode || '', country: org.document.country || '', postalCode: org.document.postalCode || '', notes: '', reason: '', fileUrl: '' }; } setApplicantAddressAsHandling () { this.application.defaultSpecialHandling = { name: this.applicant.firstName + ' ' + this.applicant.lastName, address1: this.applicant.address || '', address2: this.applicant.address2 || '', city: this.applicant.city || '', state: this.applicant.state || '', country: this.applicant.country || '', postalCode: this.applicant.postalCode || '', notes: '', reason: '', fileUrl: '' }; } async showSelectOrgModal (isUpdate = false) { this.spinnerService.startSpinner(); const favoriteOrgs = await this.applicantService.getApplicantOrganizations( this.application.clientId ); this.spinnerService.stopSpinner(); const forceOrgSelect = !this.application.organization && (this.application.programApplicantType === ProgramApplicantType.ORGS) && !this.selectedOrg; const response = await this.modalFactory.open( SearchAllOrgsModalComponent, { hideAddOrg: !this.application.allowAddOrg, favoriteOrgs, processorType: this.application.cycle.isClientProcessing ? ProcessingTypes.Client : ProcessingTypes.YourCause, charityBucketId: this.application.charityBucketId, forceOrgSelect, clientId: this.application.clientId, orgSearchGuidelines: this.application.charityBucketDescription, showPrivateOrgs: this.application.cycle.isClientProcessing, saveNewlyCreatedOrgs: true, isNomination: this.isNomination, exitRouterLink: forceOrgSelect ? '/apply/applications' : '', exitLinkText: forceOrgSelect ? this.i18n.translate( 'GLOBAL:textGoToMyApplications', {}, 'Go to my applications' ) : '' }, { class: 'modal-xl', keyboard: forceOrgSelect ? false : true } ); if (response) { this.spinnerService.startSpinner(); if (isUpdate) { this.updatingOrg = true; } await this.handleOrgSelect(response.selectedOrg, isUpdate); if (isUpdate) { await this.updateVettingStatus(); } this.spinnerService.stopSpinner(); } } async updateVettingStatus () { const vettingStatus = await this.applicationApplicantService.getLatestVettingStatusForApplication( this.applicationId, this.application.form.formId ); this.application.latestVettingRequestStatusForOrg = vettingStatus; } async handleOrgSelect ( org: SearchResult, isUpdate: boolean ) { if (isUpdate) { this.application = { ...this.application, specialHandling: BlankSpecialHandling }; } this.setSelectedOrg(org); if ( this.selectedOrg.document.eligibleForGivingStatusId !== OrganizationEligibleForGivingStatus.ELIGIBLE ) { this.application.latestVettingRequestStatusForOrg = null; } // We need to trigger a bogus change in order to force the application to update. Designation field can be present or absent on the form and this will safely save the existing value this.onChange([{ key: 'designation', type: 'designation', isReferenceField: false, value: this.application.designation }]); if (!isUpdate) { await this.handleEligibility(); } } async handleEligibility () { const formIsEligibility = this.application?.form?.formType === FormTypes.ELIGIBILITY; if ( (!this.applicationId || formIsEligibility) && this.application.eligibilityForm ) { this.setShowFormAttr('showEligibilityForm', true); } else { this.setShowFormAttr('showForm', true); } this.setInstructions(); } changeForm (form: FormForApplicantForUI) { this.formId = form.formId; this.router.navigate( [`/apply/application/${this.applicationId}/forms/${form.formId}`] ); this.setShowFormAttr('showForm', false); this.setShowFormAttr('showEligibilityForm', false); this.init(); } handleErrorSave ( e: Error, isEligibility = false, isSubmit = false ) { if (isSubmit) { this.notifier.error(this.i18n.translate( isEligibility ? 'APPLY:textErrorSubmittingEligibility' : 'APPLY:textErrorSubmittingApplication', {}, isEligibility ? 'There was an error submitting eligibility' : 'There was an error submitting the application' )); } else { this.notifier.error(this.i18n.translate( isEligibility ? 'APPLY:textErrorSavingEligibility' : 'APPLY:textErrorSavingApplication', {}, isEligibility ? 'There was an error submitting eligibility' : 'There was an error submitting the application' )); } this.logger.error(e); this.spinnerService.stopSpinner(); } async getOrgId () { if (this.selectedOrg) { if (this.selectedOrg.document.nonprofitGuid) { try { this.selectedOrg.document.id = await this.nonprofitService.getIdByNonprofitGuid( this.selectedOrg.document.nonprofitGuid ); } catch (e) { this.handleErrorSave(e as Error); } } return +this.selectedOrg.document.id; } return null; } nominationFormChanged (form: NominationForm) { this.nominationForm = form; this.setFormData(this.formData); } onNomineeValidityChange (valid: boolean) { this.nomineeFormValid = valid; } goToMyApplications () { this.router.navigate(['/apply/applications']); } setFormData (data: FormData) { if (this.canEditForm) { if (this.showForm) { this.formData = data; } else if (this.showEligibilityForm) { this.eligibilityFormData = data; } } } onChange (changes: FormioChanges[] = []) { if (this.canEditForm) { if (this.showForm) { this.pendingFormioChanges = [ ...this.pendingFormioChanges, ...changes ]; if (!this.isRouting) { this.callMaker.invoke(() => this.pendingFormioChanges); } } } } async handleAutoSave (changes: FormioChanges[]) { this.setFloaterText(); const response = this.referenceFieldsService.handleChangeTracking( changes, this.application as Partial ); const appNeedsUpdated = response.appNeedsUpdated; const refChangeTracker = response.refChangeTracker; if (!this.applicationId) { // Scenario where app has not been created yet this.spinnerService.startSpinner(); await this.doSave(true, false, false, refChangeTracker); this.spinnerService.stopSpinner(); } else { let needToSaveFileIds = false; if (Object.keys(refChangeTracker).length > 0) { const res = await this.saveRefFields(refChangeTracker); if (res === ReferenceFieldsUI.SaveRefFieldsResponse.NeedToSaveFileIds) { needToSaveFileIds = true; } } if (appNeedsUpdated || needToSaveFileIds || this.updatingOrg) { await this.doSave(true, false, false); this.updatingOrg = false; // we set org ID here so that app is updated and then report fields can be refetched this.orgId = +this.selectedOrg?.document?.id; } } this.pendingFormioChanges = this.pendingFormioChanges.filter(change => { return !changes.includes(change); }); this.setFloaterText(true); return true; } async submitRouting () { const response = this.referenceFieldsService.handleChangeTracking( this.pendingFormioChanges, this.application as Partial ); const changes = this.referenceFieldsService.adaptFormioChangesForSave( response.refChangeTracker, null, null, null, false, false ); this.spinnerService.startSpinner(); const result = await this.programAutomationService.submitProgramRouting( this.application.clientId, this.programOrRoutingId, { formId: this.application.form.formId, formRevisionId: this.application.form.formRevisionId, formResponse: { amountRequested: this.application.amountRequestedForEdit || 0, currencyRequested: this.application.currencyRequested, referenceFields: changes.standardChangeValues.map((change) => { return { id: change.referenceFieldId, value: change.value }; }) } } ); this.spinnerService.stopSpinner(); if (result) { if (result.noProgramToRouteTo && !!result.message) { await this.programAutomationService.showNoProgramsAvailable(result); this.goToMyApplications(); } else if (!!result.grantProgramGuid) { this.applicationApplicantService.setRoutingAppForSave({ formId: this.application.form.formId, formRevisionId: this.application.form.formRevisionId, amountRequested: this.application.amountRequestedForEdit || 0, currencyRequested: this.application.currencyRequested, referenceFields: this.application.referenceFields, programId: result.grantProgramId, programGuid: result.grantProgramGuid }); this.router.navigate([`/apply/programs/${result.grantProgramGuid}`]); } } } async saveAndNavigate (signatureResponse: SignatureModalResponse) { let signatureFileId: number; if (this.application.form.requireSignature) { signatureFileId = await this.signatureService.handleSignatureModalResponse(signatureResponse); if (!signatureFileId) { // If no file ID, signature failed to upload return; } } /* Attempt to save all reference fields before submission */ if (!this.submitting) { if (this.isNomination && !this.nomineeFormValid) { this.showForm = false; this.markForValidity = true; this.notifierService.error(this.i18n.translate( 'APPLY:textFormIsInvalidFixErrorsBelow', {}, 'Form is invalid. Please fix the errors below.' )); // Formio thinks submit was successful so it hides the form / submit button. // Set timeout will re-render and force it to show. setTimeout(() => { this.showForm = true; }); } else { this.spinnerService.startSpinner(); this.submitting = true; this.canEditForm = false; const errorOnSave = await this.doSave( false, true, false, undefined, signatureFileId ); this.submitting = false; if (!errorOnSave) { this.notifier.success(this.i18n.translate( this.isNomination ? 'APPLY:textSuccessfullySubmittedNomination' : 'APPLY:textSuccessfullySubmittedApp', {}, this.isNomination ? 'Successfully submitted the nomination' : 'Successfully submitted the application' )); this.applicationApplicantService.setMyApplications(undefined); this.goToMyApplications(); } else { this.canEditForm = true; } this.spinnerService.stopSpinner(); } } } async returnOrgInfoForSave () { let needToHandleOrgAfterSave = false; let orgId; // if we don't have a vetting request, check if we need one if ( this.selectedOrg && !this.application.latestVettingRequestStatusForOrg && this.selectedOrg.document?.eligibleForGivingStatusId !== OrganizationEligibleForGivingStatus.ELIGIBLE ) { // We can only call AddOrVetOrganization if application id exists, // so check if we need to call this function again needToHandleOrgAfterSave = !this.applicationId; orgId = await this.addOrgService.handleOrgForApplication( this.application.cycle.id, this.application.cycle.isClientProcessing ? ProcessingTypes.Client : ProcessingTypes.YourCause, this.selectedOrg.document.id ? +this.selectedOrg.document.id : null, this.applicationId, this.selectedOrg.document.eligibleForGivingStatusId, this.application.latestVettingRequestStatusForOrg, this.application.clientId ); if (orgId) { // this could come back null and we don't want to stringify this.selectedOrg.document.id = '' + orgId; } else { this.selectedOrg.document.id = null; } } else { orgId = await this.getOrgId(); } return { orgId, needToHandleOrgAfterSave }; } async saveRefFields ( refChangeTracker: ReferenceFieldsUI.RefResponseMap, applicationFormId?: number ): Promise { if (applicationFormId) { const fileUploadResponse = await this.referenceFieldsService.ensureFormFieldFilesAreUploadedForCopy( this.applicationId, applicationFormId, refChangeTracker, this.application.referenceFields ); refChangeTracker = fileUploadResponse.refChangeTracker; } const response = this.referenceFieldsService.adaptFormioChangesForSave( refChangeTracker, this.application.applicationFormId, this.applicationId, this.application.form.formRevisionId, false, false ); const passResponse = await this.applicationFormService.handleSaveOfChangedFields( response, this.applicationId, this.application.form.formId, this.application.form.formRevisionId, this.application.applicationFormId, false, false ); if (passResponse.standardPassed && passResponse.tablePassed) { return response.needToSaveFileIds ? ReferenceFieldsUI.SaveRefFieldsResponse.NeedToSaveFileIds : ReferenceFieldsUI.SaveRefFieldsResponse.NoNeedToSaveFileIds; } else { return ReferenceFieldsUI.SaveRefFieldsResponse.Error; } } async doSave ( isDraft = true, force = false, refreshVettingStatus = false, refResponseMap?: ReferenceFieldsUI.RefResponseMap, userSignatureId?: number ): Promise { // handle vetting and return orgID if ((this.canEditForm && !this.submitting) || force) { const { orgId, needToHandleOrgAfterSave } = await this.returnOrgInfoForSave(); const specialHandling = this.specialHandlingService.getSpecialHandlingForSave( this.application.specialHandling, this.application.defaultSpecialHandling ); const specialHandlingValid = this.specialHandlingService.isSpecialHandlingValid( this.application.specialHandling ); if (specialHandlingValid) { // save fails if not valid const { amountRequestedRequired, careOfRequired, paymentDesignationRequired } = this.formHelperService.getRequiredStandardFields( this.application.form.formDefinition ); const fileIds = this.formHelperService.getFileIDsFromForm( this.application.form.formDefinition, specialHandling.specialHandlingFileUrl, this.application.referenceFields ); const data: SaveApplication = { ...specialHandling, applicationId: this.applicationId, amountRequested: this.application.amountRequestedForEdit || 0, currencyRequested: this.application.currencyRequested, grantProgramId: this.application.programId, isDraft, form: { formId: this.application.form.formId, formRevisionId: this.application.form.formRevisionId, formData: this.formData || {}, isDraft, fileIds }, organizationId: orgId, paymentDesignation: this.application.designation ? TextFriendlySpecialCharCleaner(this.application.designation) : '', careOf: this.application?.careOf, inKindAmountRequested: (this.application.inKindItems || []).filter((item) => { return !!item.itemIdentification && +item.count > 0; }), requiredReferenceFieldKeys: this.formHelperService.getRequiredReferenceFieldKeys( this.conditionalVisibilityState, this.application.form.formDefinition ), amountRequestedRequired, careOfRequired, paymentDesignationRequired, userSignatureId, userSignatureBypassed: false }; if (this.isNomination) { data.nominee = { ...this.nominationForm, orgId }; } let errorOnSave = false; try { const response = await this.applicationApplicantService.saveApplication(data); if (!this.applicationId) { await this.handleInitialSave( response.id, response.applicationFormId, needToHandleOrgAfterSave, refreshVettingStatus, refResponseMap, data ); } } catch (e) { this.handleErrorSave(e as Error, false, !isDraft); errorOnSave = true; } return errorOnSave; } } return false; } async handleInitialSave ( newId: number, applicationFormId: number, needToHandleOrgAfterSave = false, refreshVettingStatus = false, refChangeTracker: ReferenceFieldsUI.RefResponseMap, savePayload: SaveApplication ) { // We are going to navigate to the new app, so call maker can stop and re-init after this.callMaker.complete(); this.translationService.resetViewTranslations(false); if (!!this.copyOfId && refChangeTracker) { await this.formHelperService.prepareComponentsForRenderForm( [this.application.form.formDefinition], [this.application.form.formId] ); this.referenceFieldsService.clearOutInactiveCdtResponses(refChangeTracker); } if ( !!this.applicationApplicantService.routingAppForSave && this.application.programId === this.applicationApplicantService.routingAppForSave.programId ) { // If we were routed here by program automation, tie the responses the the new application await this.applicationApplicantService.saveApplicantRoutingResponsesToApp( newId, savePayload ); } this.applicationId = newId; this.application.applicationFormId = applicationFormId; if (needToHandleOrgAfterSave) { await this.addOrgService.handleOrgForApplication( this.application.cycle.id, this.application.cycle.isClientProcessing ? ProcessingTypes.Client : ProcessingTypes.YourCause, this.selectedOrg.document.id ? +this.selectedOrg.document.id : null, this.applicationId, this.selectedOrg.document.eligibleForGivingStatusId, this.application.latestVettingRequestStatusForOrg, this.application.clientId ); } if (refreshVettingStatus) { await this.updateVettingStatus(); } if (this.copyOfId) { await this.collaborationService.copyCollaboratorsToApplication( this.copyOfId, newId ); this.referenceFieldsService.updateApplicationFormTableRowsMapAfterCopy( this.application.copyApplicationFormId, applicationFormId, this.formHelperService.getTableAndSubsetIdsFromFormDefinition( [this.application.form.formDefinition] ) ); } if (refChangeTracker && Object.keys(refChangeTracker).length > 0) { const response = await this.saveRefFields( refChangeTracker, applicationFormId ); if (response === ReferenceFieldsUI.SaveRefFieldsResponse.NeedToSaveFileIds) { await this.doSave(true, false, false); } } this.navigateToApplication(); } private setFloaterText (toSaved = false) { if (toSaved) { this.statusText = this.i18n.translate('GLOBAL:textSaved'); this.statusIcon = 'check-circle'; } else { this.statusText = this.i18n.translate('GLOBAL:textSaving'); this.statusIcon = 'ellipsis-h-alt'; } } async openFile (file: ApplicationApplicantFile) { this.spinnerService.startSpinner(); await this.applicationAttachmentService.downloadFileForApplicant( file.fileUploadId, file.fileName ); this.spinnerService.stopSpinner(); } async saveEligibility () { if (!this.submitting) { this.spinnerService.startSpinner(); this.submitting = true; const formData: FormData = {}; this.application.eligibilityForm.formDefinition.forEach((tab) => { this.componentHelper.eachComponent(tab.components, (comp) => { const refKey = this.componentHelper.getRefFieldKeyFromCompType( comp.type ); if (!!refKey) { formData[comp.key] = this.application.referenceFields[refKey]; } else { formData[comp.key] = (this.application as any)[comp.type]; } }); }); const { orgId } = await this.returnOrgInfoForSave(); const data: SubmitEligibility = { id: this.applicationId, formRevisionId: this.application.eligibilityForm.formRevisionId, grantProgramId: this.application.programId, isDraft: false, formData: this.componentHelper.removeContentFromFormData( formData, this.application.eligibilityForm.formDefinition ), organizationId: orgId }; let response; try { response = await this.applicationApplicantService.submitEligibility(data); } catch (e) { this.handleErrorSave(e as Error, true, true); } this.submitting = false; this.spinnerService.stopSpinner(); this.showPassFailModal(response); } } async showPassFailModal (response: EligibilityResponse) { const passed = response.matchesMasterResponse; const deps = { primaryCustomColor: this.application.clientBranding.brandPrimary, secondaryCustomColor: this.application.clientBranding.brandSecondary, modalHeader: passed ? this.eligibilityPassed : this.eligibilityFailed, confirmButtonText: passed ? this.i18n.translate( 'APPLY:textContinueToApp', {}, 'Continue to application' ) : this.i18n.translate( 'GLOBAL:textGoToMyApplications', {}, 'Go to my applications' ), confirmText: passed ? this.application.passMessage : this.application.failMessage, secondaryButtonText: passed ? null : this.i18n.translate( 'APPLY:textStartOver', {}, 'Start over' ) }; const modalAction = await this.modalFactory.open( ConfirmationModalWithSecondaryActionComponent, deps, { class: 'modal-small'} ); if (modalAction === ConfirmActionResult.Primary) { this.spinnerService.startSpinner(); // Reset reference fields // The eligibility ones shouldn't be saved on the request form // TODO: Figure out how/if we are going to save eligibility reference field answers if (this.refFieldsToCopyAfterEligibility) { this.application = { ...this.application, referenceFields: { ...this.refFieldsToCopyAfterEligibility } }; } else { this.application = { ...this.application, referenceFields: {} }; } if (!this.isRouting) { this.callMaker.invoke(() => this.pendingFormioChanges); } this.spinnerService.stopSpinner(); } else if (modalAction === ConfirmActionResult.Secondary) { this.setShowFormAttr('showEligibilityForm', false); this.eligibilityFormData = {}; setTimeout(() => { this.setShowFormAttr('showEligibilityForm', true); }, 100); } else { this.goToMyApplications(); } } navigateToApplication () { this.formId = this.application.form.formId; const route = `/apply/application/${this.applicationId}/forms/${this.formId}`; this.router.navigate([route]); } async downloadPDF () { this.spinnerService.startSpinner(); let form: ProgramFormForUi; if (this.showForm) { form = this.application.form; } else if (this.showEligibilityForm) { form = this.application.eligibilityForm; } let foundForm: FormForApplicantFromApi; if (form) { foundForm = this.forms.find((f) => f.formId === form.formId); } let availableItemsForApplicant: InKindCategoryItemStat[] = []; if (this.application.inKindItems) { const ids = this.application.inKindItems.map((item) => item.itemIdentification); availableItemsForApplicant = await this.inKindService.getItemsById( ids, this.application.programId, this.userService.currentUser.culture ); } const formDefinition = this.showForm ? this.application.form.formDefinition : this.application.eligibilityForm.formDefinition; const [ referenceFields, reportFieldResponse, signature ] = await Promise.all([ // reference fields this.referenceFieldsService.getReferenceFieldResponses( this.applicationId, this.application.applicationFormId, formDefinition, this.formHelperService.getTableAndSubsetIdsFromFormDefinition( [formDefinition] ), null, true, false, null, form.formData ), // report field response this.reportFieldService.handleReportFieldData( form.formDefinition, this.applicationId ), // signature this.signatureService.setFormSignature( this.application.applicationId, this.application.applicationFormId, true ) ]); const application: ApplicationInfoForPDF = { appId: this.applicationId, programName: this.application.grantProgramName, isMasked: false, amountRequested: this.application.amountRequested || 0, currencyRequested: this.application.currencyRequested, currencyRequestedAmountEquivalent: this.application.currencyRequestedAmountEquivalent, inKindItems: this.application.inKindItems || [], availableItemsForApplicant, specialHandling: this.application.specialHandling, cycleName: this.cycleTranslations[this.application.cycle.id]?.Name, workflowLevelName: '', careOf: this.application.careOf, workflowName: '', status: this.statusService.applicationStatusMap?.[this.application.statusId]?.translated ?? '', tags: [], // Not necessary for applicants submittedDate: this.appSubmittedDate, reportFieldResponse, employeeInfo: this.application.employeeInfo, designation: this.application.designation }; const orgInfo: OrgInfoForPDF = { name: this.selectedOrg ? this.selectedOrg.document.name : '', addressString: this.selectedOrg ? this.selectedOrg.document.addressString : '', registrationId: this.selectedOrg?.document?.registrationId ?? '', phone: this.selectedOrg ? this.selectedOrg.document.phone : '' }; const formId = foundForm ? foundForm.formId : null; const formInfo: FormInfoForPDF = { formName: this.showForm ? this.application.form.name : this.application.eligibilityForm.name, formSubmittedOn: foundForm ? foundForm.submittedDate : '', formSubmittedBy: foundForm.submittedBy, formDefinition, formData: this.showForm ? this.formData : this.eligibilityFormData, formId, formStatus: foundForm.applicationFormStatusId, applicationFormId: this.application.applicationFormId, referenceFields, decision: null, specialHandling: this.application.specialHandling, reviewerRecommendedFundingAmount: null, signature, translations: this.translations, richTextTranslations: this.richTextTranslations }; await this.formHelperService.prepareComponentsForRenderForm( [formDefinition], [formId] ); const { fileUploads, tableCsvs } = this.formHelperService.extractFilesAndTablesForPdf([formInfo]); await this.applicationDownloadService.buildApplicationPDF( await this.collaborationService.getApplicantsForPDF( this.applicationId, false ), application, this.isNomination ? this.nominationForm : null, orgInfo, this.isNomination, [formInfo], fileUploads, tableCsvs, null, null, false, this.application.clientName, this.application.clientLogoUrl, null, null, this.awards.length > 0 ? this.awardService.constructAwards(this.awards) : null, this.application.isArchived, true ); this.spinnerService.stopSpinner(); } ngOnDestroy () { this.mixpanel.unregister('Program Name'); this.mixpanel.unregister('Client Name'); this.applicationApplicantService.clearClientBranding(); } }