import { Injectable } from '@angular/core'; import { ApplicationFileService } from '@core/services/application-file.service'; import { ApplicantService } from '@core/services/auth-user/applicant.service'; import { SpecialHandlingService } from '@core/services/special-handling.service'; import { ApplicantForApi, ApplicantFromSearch } from '@core/typings/applicant.typing'; import { ApplicantFormForUI, ApplicationViewPage } from '@core/typings/application.typing'; import { ProcessingTypes } from '@core/typings/payment.typing'; import { ApplicationStatuses } from '@core/typings/status.typing'; import { ReferenceFieldsUI } from '@core/typings/ui/reference-fields.typing'; import { AddOrganizationService } from '@features/add-organization/add-organization.service'; import { ApplicationFormService } from '@features/application-forms/services/application-forms.service'; import { OfflineGrantsResources } from '@features/application-manager/resources/offline-grants.resources'; import { ApplicationViewService } from '@features/application-view/application-view.service'; import { CollaborationService } from '@features/collaboration/collaboration.service'; import { SaveFormResponse } from '@features/configure-forms/form.typing'; import { FormHelperService } from '@features/formio/services/form-helper/form-helper.service'; import { NonprofitService } from '@features/nonprofit/nonprofit.service'; import { ProgramService } from '@features/programs/program.service'; import { ReferenceFieldsService } from '@features/reference-fields/services/reference-fields.service'; import { FileService, TextFriendlySpecialCharCleaner } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { NotifierService } from '@yourcause/common/notifier'; import { CreateEditApplicationForApi, CreateEditApplicationModalResponse, OfflineCreateModalAction, SubmitApplicationOptions } from './offline-grants.typing'; @Injectable({ providedIn: 'root' }) export class OfflineGrantsService { constructor ( private offlineGrantsResources: OfflineGrantsResources, private logService: LogService, private notifier: NotifierService, private i18n: I18nService, private applicationViewService: ApplicationViewService, private nonprofitService: NonprofitService, private specialHandlingService: SpecialHandlingService, private applicantService: ApplicantService, private addOrgService: AddOrganizationService, private programService: ProgramService, private referenceFieldService: ReferenceFieldsService, private formHelperService: FormHelperService, private collaborationService: CollaborationService, private applicationFormService: ApplicationFormService, private applicationFileService: ApplicationFileService, private fileService: FileService ) { } async getApplicationForEdit ( id: number, isNewApp: boolean ) { await this.applicationViewService.setApplicationViewMap( id, true, isNewApp ); } async handleCreateEditApplication ( response: CreateEditApplicationModalResponse, applicationId: number, isNomination: boolean, skipToastr = false ) { const isCreate = !applicationId; const modalAction = response.modalAction; try { const orgResponse = await this.getOrganizationId( response, applicationId ); const payload: CreateEditApplicationForApi = { applicationId, grantProgramId: response.grantProgramId, grantProgramCycleId: response.cycleId, applicantId: await this.getApplicantId( response.selectedApplicant, response.applicantCanReceiveEmails ), organizationId: orgResponse.organizationId, applicantCanReceiveEmails: response.applicantCanReceiveEmails, sendEmailToApplicant: !applicationId && modalAction === OfflineCreateModalAction.SEND_TO_APPLICANT }; const id = await this.offlineGrantsResources.createEditApplication( payload ); if (orgResponse.needToHandleOrgAfterSave) { await this.handleOrgForApplication( response, id || applicationId ); } if (!skipToastr) { const toastrText = this.getCreateEditAppToastrText( true, isCreate, modalAction, isNomination, id ); this.notifier.success(toastrText); } return id; } catch (e) { this.logService.error(e); const toastrText = this.getCreateEditAppToastrText( false, isCreate, modalAction, isNomination, null ); this.notifier.error(toastrText); return null; } } async getApplicantId ( applicant: ApplicantFromSearch, notifyApplicant: boolean ): Promise { if (applicant.id) { return applicant.id; } const adapted: ApplicantForApi = { ...applicant, ...applicant.address, clientId: null, isEmployeeOfClient: applicant.isEmployeeOfClient, firstName: applicant.firstName.trim(), lastName: applicant.lastName.trim(), sendEmail: notifyApplicant }; const response = await this.applicantService.createApplicant(adapted); return response.applicantId; } async getOrganizationId ( response: CreateEditApplicationModalResponse, applicationId: number ): Promise<{ organizationId: number; needToHandleOrgAfterSave: boolean; }> { let needToHandleOrgAfterSave = false; if (response.selectedOrg) { // We can only call AddOrVetOrganization if application id exists, // so check if we need to call this function again needToHandleOrgAfterSave = !applicationId; const organizationId = await this.handleOrgForApplication( response, applicationId ); return { organizationId, needToHandleOrgAfterSave }; } return { organizationId: null, needToHandleOrgAfterSave }; } handleOrgForApplication ( response: CreateEditApplicationModalResponse, applicationId: number ): Promise { return this.addOrgService.handleOrgForApplication( response.cycleId, response.cycle.isClientProcessing ? ProcessingTypes.Client : ProcessingTypes.YourCause, +response.selectedOrg.document.id, applicationId, response.selectedOrg.document.eligibleForGivingStatusId, response.vettingInfo?.latestVettingRequestStatusForOrg, response.program.clientId, response.vettingInfo?.vettingRequestContactName, response.vettingInfo?.vettingRequestContactEmail, response.vettingInfo?.vettingRequestContactWebsite ); } getCreateEditAppToastrText ( success: boolean, isCreate: boolean, modalAction: OfflineCreateModalAction, isNomination: boolean, id: number ) { if (success) { if (isCreate) { if (modalAction === OfflineCreateModalAction.SEND_TO_APPLICANT) { return this.i18n.translate( isNomination ? 'common:textNomCreatedAndSent' : 'common:textAppCreatedAndSent', { id }, isNomination ? 'Nomination __id__ created and sent to nominator.' : 'Application __id__ created and sent to applicant.' ); } else if (modalAction === OfflineCreateModalAction.SAVE_AND_EDIT) { return this.i18n.translate( isNomination ? 'common:textNominationSavedToastr' : 'common:textApplicationSavedToastr', {}, isNomination ? 'Nomination saved. Make changes to the default form and send to the nominator or submit on their behalf.' : 'Application saved. Make changes to the default form and send to the applicant or submit on their behalf.' ); } else { return this.i18n.translate( isNomination ? 'common:NomSavedAsDraft' : 'common:textAppSavedAsDraft', { id }, isNomination ? 'Nomination __id__ saved as draft.' : 'Application __id__ saved as draft.' ); } } else { return this.i18n.translate( isNomination ? 'common:textSuccessfullyUpdatedTheNomination' : 'common:textSuccessfullyUpdatedTheApplication', {}, isNomination ? 'Successfully updated the nomination.' : 'Successfully updated the application.' ); } } else { if (isCreate) { return this.i18n.translate( isNomination ? 'common:textErrorCreatingNomination' : 'common:textErrorCreatingApplication', {}, isNomination ? 'There was an error creating the nomination.' : 'There was an error creating the application.' ); } else { return this.i18n.translate( isNomination ? 'common:textErrorUpdatingNomination' : 'common:textErrorUpdatingApplication', {}, isNomination ? 'There was an error updating the nomination.' : 'There was an error updating the application.' ); } } } async sendDraftToApplicant ( applicationId: number, isNomination = false ): Promise { try { await this.offlineGrantsResources.sendDraftToApplicant(applicationId); this.notifier.success(this.i18n.translate( isNomination ? 'common:textSuccessSendingToNominator' : 'common:textSuccessSendingToApplicant', {}, isNomination ? 'Successfully sent to the nominator' : 'Successfully sent to the applicant' )); return true; } catch (e) { this.logService.error(e); this.notifier.error(this.i18n.translate( isNomination ? 'common:textErrorSendingToNominator' : 'common:textErrorSendingToApplicant', {}, isNomination ? 'There was an error sending to the nominator' : 'There was an error sending to the applicant' )); return false; } } getSubmitOptions (isNomination: boolean) { return [{ label: this.i18n.translate( 'common:textEnterTheDefaultLevel', {}, 'Enter the default workflow level' ), value: SubmitApplicationOptions.ENTER_DEFAULT_LEVEL }, { label: this.i18n.translate( isNomination ? 'common:textSelectWflForNomToEnter' : 'common:textSelectWflForAppToEnter', {}, isNomination ? 'Select a workflow level for the nomination to enter' : 'Select a workflow level for the application to enter' ), value: SubmitApplicationOptions.SELECT_A_LEVEL }]; } async handleCopyApplication ( applicationIdToCopy: number, isNomination: boolean ): Promise { try { await this.getApplicationForEdit(applicationIdToCopy, false); const detail = this.applicationViewService.applicationEditMap[ applicationIdToCopy ]; const app = detail.application; const [ program, selectedOrg ] = await Promise.all([ this.programService.getOfflineProgramDetail(app.programId), this.nonprofitService.constructSearchResult( app ) ]); // Create Copy const newId = await this.handleCreateEditApplication( { program, cycle: program?.cycles.find((cycle) => { return cycle.id === app.grantProgramCycle.id; }), grantProgramId: app.programId, cycleId: app.grantProgramCycle.id, selectedApplicant: { id: app.applicantId }, selectedOrg, applicantCanReceiveEmails: app.applicantCanReceiveEmails, vettingInfo: null, modalAction: app.applicantCanReceiveEmails ? OfflineCreateModalAction.SEND_TO_APPLICANT : OfflineCreateModalAction.SAVE_AND_EDIT } as CreateEditApplicationModalResponse, null, isNomination ); if (newId) { // Prepare Forms and Copy Collaborators const [ formsFromOriginalApp, formsFromNewApp ] = await Promise.all([ this.applicationFormService.getApplicantFormsForApplication( applicationIdToCopy, app.applicationStatus ), this.applicationFormService.getApplicantFormsForApplication( newId, ApplicationStatuses.Draft ), this.collaborationService.copyCollaboratorsToApplication( applicationIdToCopy, newId ) ]); const originalDefaultForm = this.formHelperService.getDefaultForm( formsFromOriginalApp ); const newDefaultForm = this.formHelperService.getDefaultForm( formsFromNewApp ); // Save Ref Fields and Standard Fields const referenceFieldValues = await this.saveReferenceFieldsForCopy( applicationIdToCopy, newId, originalDefaultForm, newDefaultForm ); await this.saveAppFieldsForCopy( newId, app, newDefaultForm, referenceFieldValues ); this.notifier.success(this.i18n.translate( isNomination ? 'common:textSuccessCopyNom' : 'common:textSuccessCopyApp', {}, isNomination ? 'Successfully copied the nomination' : 'Successfully copied the application' )); return newId; } return null; } catch (e) { this.logService.error(e); this.notifier.error(this.i18n.translate( isNomination ? 'common:textErrorCopyNom' : 'common:textErrorCopyApp', {}, isNomination ? 'There was an error copying the nomination' : 'There was an error copying the applicatioin' )); return null; } } async saveAppFieldsForCopy ( newId: number, application: ApplicationViewPage, newDefaultForm: ApplicantFormForUI, referenceFieldValues: ReferenceFieldsUI.RefResponseMap ) { const specialHandling = this.specialHandlingService.getSpecialHandlingForSave( application.specialHandling, application.defaultSpecialHandling ); // Need to re-upload the special handling file for the new application if (specialHandling.specialHandlingFileUrl) { const fileDetails = this.applicationFileService.breakDownloadUrlDownToObject( specialHandling.specialHandlingFileUrl ); const blob = await this.fileService.getBlob(specialHandling.specialHandlingFileUrl); const file = this.fileService.convertBlobToFile( blob, fileDetails.fileName ); specialHandling.specialHandlingFileUrl = await this.applicationFileService.uploadFile( newId, newDefaultForm.applicationFormId, file, fileDetails.fileName, null ); } const data: SaveFormResponse = { ...specialHandling, formId: newDefaultForm.formId, formRevisionId: newDefaultForm.formRevisionId, fileIds: this.formHelperService.getFileIDsFromForm( newDefaultForm.formDefinition, specialHandling.specialHandlingFileUrl, referenceFieldValues ), nominee: application.nominee, isDraft: true, applicationFormId: newDefaultForm.applicationFormId, revisionNotes: '', careOf: application.careOf, amountRequested: application.currencyRequestedAmountEquivalent, saveAmountRequestedInDefaultCurrency: false, currencyRequested: application.currencyRequested, paymentDesignation: TextFriendlySpecialCharCleaner( application.designation || application.paymentDesignation ), decision: null, reviewerRecommendedFundingAmount: null, inKindItems: application.inKindItems, // below are not needed for drafts requiredReferenceFieldKeys: [], reviewerRecommendedFundingAmountRequired: false, decisionRequired: false, amountRequestedRequired: false, careOfRequired: false, paymentDesignationRequired: false, editingApplicationView: true, submittingApplication: false, workflowLevelId: null, userSignatureId: null, userSignatureBypassed: false }; await this.applicationFormService.saveFormResponse( data, newId ); } async saveReferenceFieldsForCopy ( applicationIdToCopy: number, newId: number, originalDefaultForm: ApplicantFormForUI, newDefaultForm: ApplicantFormForUI ): Promise { await this.formHelperService.prepareComponentsForRenderForm( [newDefaultForm.formDefinition], [newDefaultForm.formId] ); let refResponses = await this.referenceFieldService.getReferenceFieldResponses( applicationIdToCopy, originalDefaultForm.applicationFormId, newDefaultForm.formDefinition, this.formHelperService.getTableAndSubsetIdsFromFormDefinition( [originalDefaultForm.formDefinition] ), null ); this.referenceFieldService.updateApplicationFormTableRowsMapAfterCopy( originalDefaultForm.applicationFormId, newDefaultForm.applicationFormId, this.formHelperService.getTableAndSubsetIdsFromFormDefinition( [newDefaultForm.formDefinition] ) ); this.referenceFieldService.clearOutTableRowIdsForCopy(refResponses); this.referenceFieldService.clearOutInactiveCdtResponses(refResponses); const updatedMaps = await this.referenceFieldService.reuploadReferenceFilesOnCopy( refResponses, newId, newDefaultForm.applicationFormId, {} ); refResponses = updatedMaps.refChangeTracker; const response = this.referenceFieldService.adaptFormioChangesForSave( refResponses, newDefaultForm.applicationFormId, newId, newDefaultForm.formRevisionId, false, false ); await this.applicationFormService.handleSaveOfChangedFields( response, newId, newDefaultForm.formId, newDefaultForm.formRevisionId, newDefaultForm.applicationFormId, true, true ); return refResponses; } }