import { Injectable } from '@angular/core'; import { TranslationService } from '@core/services/translation.service'; import { AutomationAPI } from '@core/typings/api/automation.typing'; import { ProgramApplicantType } from '@core/typings/program.typing'; import { NoProgramsAvailableModalComponent } from '@features/apply/no-programs-available-modal/no-programs-available-modal.component'; import { FormDefinitionForUi, FormTypes } from '@features/configure-forms/form.typing'; import { ComponentHelperService } from '@features/formio/services/component-helper/component-helper.service'; import { FormLogicService } from '@features/formio/services/form-logic/form-logic.service'; import { APIResult, ArrayHelpersService, AutoTableRepositoryFactory, PaginationOptions } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { ConfirmAndTakeActionService, ModalFactory } from '@yourcause/common/modals'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import { ProgramAutomationResources } from './program-automation.resources'; import { ProgramAutomationState } from './program-automation.state'; import { ProgramAutomationDetailForUi, ProgramAutomationDetailFromApi, ProgramAutomationForApplicantUi, ProgramAutomationForm, ProgramAutomationRowForUi, ProgramAutomationRulesetForUi, ProgramAutomationTableAction, ProgramRoutingApplyPayload, ProgramRoutingApplyResponse, Program_Automation_Table_Key, RulesetSequencePayload, SaveAutomationApi, SaveRuleset } from './program-automation.typing'; @AttachYCState(ProgramAutomationState) @Injectable({ providedIn: 'root' }) export class ProgramAutomationService extends BaseYCService { constructor ( private programAutomationResources: ProgramAutomationResources, private arrayHelper: ArrayHelpersService, private confirmAndTakeActionService: ConfirmAndTakeActionService, private i18n: I18nService, private autoTableRepositoryFactory: AutoTableRepositoryFactory, private translationService: TranslationService, private logger: LogService, private notifier: NotifierService, private componentHelper: ComponentHelperService, private formLogicService: FormLogicService, private modalFactory: ModalFactory ) { super(); } get programAutomationRules () { return this.get('programAutomationRules'); } get activeProgramAutomationOptions () { return this.get('activeProgramAutomationOptions'); } get allProgramAutomationOptions () { return this.get('allProgramAutomationOptions'); } get newRouteClicked () { return this.get('newRouteClicked'); } get evaluationOrderClicked () { return this.get('evaluationOrderClicked'); } /** * * @param id: ID of program automation record * @returns the detail of that record */ async getProgramAutomationDetail (id: number): Promise { if (id) { const detail = await this.programAutomationResources.getProgramAutomationDetail(id); return { ...detail, grantProgramRoutingAutomationRuleSets: this.arrayHelper.sort(detail.grantProgramRoutingAutomationRuleSets.map((ruleset) => { return { ...ruleset, programName: this.translationService.viewTranslations.Grant_Program[ruleset.routeToGrantProgramId]?.Name, valid: true }; }), 'sequence') }; } else { return this.getBlankProgramAutomationDetail(); } } /** * * @returns a blank program automation detail for new scenario */ getBlankProgramAutomationDetail (): ProgramAutomationDetailForUi { return { id: null, name: '', fallbackGrantProgramId: null, ruleSetFailureMessage: '', description: '', formId: null, complete: false, draft: true, isArchived: false, landingLinkGuid: '', grantProgramRoutingAutomationRuleSets: [], programApplicantType: ProgramApplicantType.ORGS }; } /** * Get Program Automation Rules based off Pagination Options * * @param options: pagination options */ async getAutomationRules ( options: PaginationOptions ): Promise> { const response = await this.programAutomationResources.getProgramAutomationRecords(options); const formTranslations = this.translationService.viewTranslations.FormTranslation; return { success: true, data: { records: response.records.map((record) => { return { ...record, routingFormName: formTranslations[record.formId]?.Name }; }), recordCount: response.recordCount } }; } /** * Get All Program Automation Rules */ async getAllAutomationRules () { const options: PaginationOptions = { returnAll: true, rowsPerPage: 1, pageNumber: 1, sortColumns: [{ columnName: 'name', sortAscending: true }], filterColumns: [], retrieveTotalRecordCount: true }; const response = await this.getAutomationRules(options); return response.data.records; } /** * Sets all Program Automation Rules on the state if not already set */ async setAllProgramAutomationRules () { if (!this.programAutomationRules) { const rules = await this.getAllAutomationRules(); this.setProgramAutomationRules(rules); } } /** * Sets the rules on the state as well as the options * * @param rules: Program automation rules */ setProgramAutomationRules (rules: ProgramAutomationRowForUi[]) { this.set('programAutomationRules', rules); const options = this.arrayHelper.sort(rules.map((rule) => { return { label: rule.name, value: rule.id }; }), 'label'); this.set('allProgramAutomationOptions', options); const activeOptions = this.arrayHelper.sort(rules.filter((rule) => { return !rule.isArchived; }).map((rule) => { return { label: rule.name, value: rule.id }; })); this.set('activeProgramAutomationOptions', activeOptions); } /** * Resets all Program Automation Rules */ resetProgramAutomationRules () { this.set('programAutomationRules', undefined); this.set('allProgramAutomationOptions', undefined); this.set('activeProgramAutomationOptions', undefined); } /** * Resets the table view */ resetProgramAutomationTableRepo () { const repo = this.autoTableRepositoryFactory.getRepository(Program_Automation_Table_Key); if (repo) { repo.reset(); } } /** * * @param detail: Automation detail to save * @returns grantProgramRoutingDetailsId & grantProgramRoutingLinkGuid */ async saveProgramAutomation (detail: ProgramAutomationDetailFromApi) { try { const payload: SaveAutomationApi = { grantProgramRoutingDetailsId: detail.id, draft: detail.draft, complete: detail.complete, formId: detail.formId, fallbackGrantProgramId: detail.fallbackGrantProgramId, name: detail.name, description: detail.description, failureMessage: detail.ruleSetFailureMessage, programApplicantType: detail.programApplicantType }; const response = await this.programAutomationResources.saveProgramAutomation(payload); return response; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'PROGRAM:textErrorSavingProgramAutomation', {}, 'There was an error saving the program automation' )); return null; } } /** * * @param row: Row to update * @param action: Action taken */ updateAutomationRecord ( row: ProgramAutomationRowForUi, action: ProgramAutomationTableAction ) { if (this.programAutomationRules) { const foundIndex = this.programAutomationRules.findIndex((rule) => { return rule.id === row.id; }); if (foundIndex > -1) { const thisRecord = this.programAutomationRules[foundIndex]; let updatedRecord: ProgramAutomationRowForUi; switch (action) { case ProgramAutomationTableAction.PUBLISH: updatedRecord = { ...thisRecord, draft: false }; break; case ProgramAutomationTableAction.ARCHIVE: updatedRecord = { ...thisRecord, isArchived: !row.isArchived }; break; case ProgramAutomationTableAction.DELETE: // Do nothing and the record will be deleted and filtered out below break; } const updatedRules = [ ...this.programAutomationRules.slice(0, foundIndex), updatedRecord, ...this.programAutomationRules.slice(foundIndex + 1) ].filter((item) => !!item); this.setProgramAutomationRules(updatedRules); } } } /** * * @param row: Row to take action * @param action: Action to take */ async handleTableAction ( row: ProgramAutomationRowForUi, action: ProgramAutomationTableAction ) { let passed = false; switch (action) { case ProgramAutomationTableAction.PUBLISH: passed = await this.handlePublish(row); break; case ProgramAutomationTableAction.ARCHIVE: passed = await this.handleArchive(row); break; case ProgramAutomationTableAction.DELETE: passed = await this.handleDelete(row); break; } if (passed) { this.resetProgramAutomationTableRepo(); this.updateAutomationRecord(row, action); } } /** * * @param row: Row to publish * @returns if it passed */ async handlePublish ( row: ProgramAutomationRowForUi ) { const publishText = this.i18n.translate( 'GLOBAL:textPublish', {}, 'Publish' ); const result = await this.confirmAndTakeActionService.confirmAndTakeAction( `api/manager/GrantProgramRoutingAutomation/${row.id}/Publish`, {}, publishText, row.name, this.i18n.translate( 'common:textConfirmPublishProgramAutomation', {}, 'Publishing the program automation will allow anyone with a link to it to apply. This action cannot be undone. Are you sure you want to publish the program automation?' ), publishText, this.i18n.translate( 'common:textSuccessPublishProgramAutomation', {}, 'Successfully published program automation' ), this.i18n.translate( 'common:textErrorPublishProgramAutomation', {}, 'There was an error publishing program automation' ), 'get' ); return result?.passed; } /** * * @param row: Row to archive or unarchive * @returns if it passed */ async handleArchive (row: ProgramAutomationRowForUi) { const archiveText = this.i18n.translate( row.isArchived ? 'GLOBAL:textUnarchive' : 'GLOBAL:textArchive', {}, row.isArchived ? 'Unarchive' : 'Archive' ); const result = await this.confirmAndTakeActionService.confirmAndTakeAction( row.isArchived ? `api/manager/GrantProgramRoutingAutomation/${row.id}/Unarchive` : `api/manager/GrantProgramRoutingAutomation/${row.id}/Archive`, {}, archiveText, row.name, this.i18n.translate( row.isArchived ? 'common:textAreYouSureUnarchiveProgramAutomation2' : 'common:textAreYouSureArchiveProgramAutomation2', {}, row.isArchived ? 'Unarchiving this program automation will reactivate all routes created for it. Applicants will once again be able to apply using the link shared with them. Are you sure you want to unarchive the program automation?' : 'Archiving this program automation will deactivate all routes created for it. Applicants will no longer be able to apply using the link shared with them. Are you sure you want to archive the program automation?' ), archiveText, this.i18n.translate( row.isArchived ? 'common:textSuccessUnarchiveProgramAutomation' : 'common:textSuccessArchiveProgramAutomation', {}, row.isArchived ? 'Successfully unarchived the program automation' : 'Successfully archived the program automation' ), this.i18n.translate( row.isArchived ? 'common:textErrorUnarchivingProgramAutomation' : 'common:textErrorArchivingProgramAutomation', {}, row.isArchived ? 'There was an error unarchiving the program automation' : 'There was an error archiving the program automation' ), 'get' ); return result?.passed; } /** * * @param row: Row to delete * @returns if it passed */ async handleDelete (row: ProgramAutomationRowForUi) { const deleteText = this.i18n.translate( 'common:btnDelete', {}, 'Delete' ); const result = await this.confirmAndTakeActionService.confirmAndTakeAction( `api/manager/GrantProgramRoutingAutomation/${row.id}`, {}, deleteText, row.name, this.i18n.translate( 'common:textConfirmDeleteProgramAutomation2', {}, 'Are you sure you want to delete the program automation? This action cannot be undone.' ), deleteText, this.i18n.translate( 'common:textSuccessDeleteProgramAutomation', {}, 'Successfully deleted the program automation' ), this.i18n.translate( 'common:textErrorDeleteProgramAutomation', {}, 'There was an error deleting the program automation' ), 'delete' ); return result?.passed; } /** * * @param id: Program Automation ID * @returns if passed */ publishAutomationFromModal (id: number) { return this.confirmAndTakeActionService.takeAction( `api/manager/GrantProgramRoutingAutomation/${id}/Publish`, null, '', this.i18n.translate( 'common:textErrorPublishProgramAutomation', {}, 'There was an error publishing program automation' ), 'get', true ); } /** * * @param detail: Current automation detail * @param rulesetId: Ruleset ID * @param programId: Route to Program ID * @param routeName: Route Name * @param applyRulesWithOr: Apply rules with or? * @param rules: the rules * @returns the ruleset */ async handleSaveRuleset ( grantProgramRoutingDetailsId: number, formId: number, rulesetId: number, programId: number, routeName: string, applyRulesWithOr: boolean, rules: AutomationAPI.SaveAutomationRuleSetExpressionModel[] ): Promise { try { const payload: SaveRuleset = { grantProgramRoutingDetailsId, grantProgramRoutingAutomationRuleSetId: rulesetId, routeToGrantProgramId: programId, name: routeName, description: '', applyRulesWithOr, formId, rules }; const result = await this.programAutomationResources.saveRuleset(payload); return { ...result, programName: this.translationService.viewTranslations.Grant_Program[result.routeToGrantProgramId]?.Name, valid: result.rules.length > 0 }; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'common:textErrorSavingChanges', {}, 'There was an error saving the changes' )); return null; } } /** * * @param newRouteClicked: New Route button clicked boolean */ setNewRouteClicked (newRouteClicked: boolean) { this.set('newRouteClicked', newRouteClicked); } /** * * @param evaluationOrderClicked: Evaluation Order button clicked boolean */ setEvaluationOrderClicked (evaluationOrderClicked: boolean) { this.set('evaluationOrderClicked', evaluationOrderClicked); } /** * * @param grantProgramRoutingDetailsId: Details ID * @param grantProgramRoutingAutomationRuleSetId: Ruleset ID */ async handleDeleteRuleset ( grantProgramRoutingDetailsId: number, grantProgramRoutingAutomationRuleSetId: number ) { return this.confirmAndTakeActionService.takeAction( `/api/manager/GrantProgramRoutingAutomation/${grantProgramRoutingDetailsId}/${grantProgramRoutingAutomationRuleSetId}`, null, '', this.i18n.translate( 'common:textErrorDeletingRoute', {}, 'There was an error deleting the route' ), 'delete', true ); } /** * * @param payload: Sequence payload * @returns if passed */ async handleSequenceRulesets (payload: RulesetSequencePayload) { return this.confirmAndTakeActionService.takeAction( 'api/manager/GrantProgramRoutingAutomation/Sequence', payload, '', this.i18n.translate( 'common:textErrorSequencing', {}, 'There was an error sequencing the rulesets' ), 'post', true ); } /** * * @param formDefinition: FormDefinition * @returns if we should allow rules for application fields */ allowApplicationRules ( formDefinition: FormDefinitionForUi[] ) { let shouldShow = false; formDefinition.forEach((tab) => { this.componentHelper.eachComponent(tab.components, (comp) => { const hasStandardComp = [ 'amountRequested', 'inKindItems' ].includes(comp.type); if (hasStandardComp) { shouldShow = true; } }); }); return shouldShow; } /** * Fetched when an applicant navigates to the routing link. * * @param landingLinkGuid: Landing Link Guid * @returns the program automation routing details */ async getRoutingInfoForApplicant ( landingLinkGuid: string ): Promise { const details = await this.programAutomationResources.getRoutingInfoForApplicant(landingLinkGuid); details.formDefinition = this.formLogicService.adaptFormDefinitionForTabs( details.formDefinition, details.formId, details.formRevisionId ).formDefinition; return { ...details, form: { applicationFormId: null, formId: details.formId, formRevisionId: details.formRevisionId, name: details.name, description: details.description, formData: null, isDraft: true, canEdit: true, revisionNotes: '', formType: FormTypes.ROUTING, applicationFormStatusId: null, defaultLanguageId: '', requireSignature: false, signatureDescription: '', formDefinition: details.formDefinition } }; } /** * Submit the routing form answers and see which program they should be routed to * * @param clientId: Client ID * @param landingLinkGuid: Landing Link Guid * @param form: Form payload * @returns details on which program the applicant should be routed to */ async submitProgramRouting ( clientId: number, landingLinkGuid: string, form: ProgramAutomationForm ) { const payload: ProgramRoutingApplyPayload = { clientId, landingLinkGuid, form }; try { const result = await this.programAutomationResources.submitProgramRouting(payload); return result; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'APPLICATION:textErrorSubmittingForm', {}, 'There was an error submitting the form' )); return null; } } /** * Shown to applicants who do not meet the requirements for any program * * @param result: Program Routing Result from API * @returns boolean when modal is closed */ showNoProgramsAvailable (result: ProgramRoutingApplyResponse) { return this.modalFactory.open( NoProgramsAvailableModalComponent, { failMessage: result.message } ); } }