import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; import { SequenceModalComponent } from '@core/components/sequence-modal/sequence-modal.component'; import { SpinnerService } from '@core/services/spinner.service'; import { AutomationAPI } from '@core/typings/api/automation.typing'; import { ProgramApplicantType } from '@core/typings/program.typing'; import { Automation } from '@core/typings/ui/automation.typing'; import { FormsService } from '@features/configure-forms/services/forms/forms.service'; import { FormLogicService } from '@features/formio/services/form-logic/form-logic.service'; import { RuleAutomationService } from '@features/rule-automation/rule-automation.service'; import { ArrayHelpersService, Panel, PanelSection } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { ConfirmationModalComponent, ModalFactory } from '@yourcause/common/modals'; import { isEqual } from 'lodash'; import { Subscription } from 'rxjs'; import { ProgramAutomationService } from '../../program-automation.service'; import { ProgramAutomationRulesetForUi, RulesetSequencePayload } from '../../program-automation.typing'; import { ProgramAutomationRouteModalComponent } from '../program-automation-route-modal/program-automation-route-modal.component'; @Component({ selector: 'gc-program-automation-builder-rules', templateUrl: './program-automation-builder-rules.component.html', styleUrls: ['./program-automation-builder-rules.component.scss'] }) export class ProgramAutomationBuilderRulesComponent implements OnInit, OnChanges, OnDestroy { @Input() grantProgramRoutingDetailsId: number; @Input() formId: number; @Input() isDraft: boolean; @Input() rulesets: ProgramAutomationRulesetForUi[]; @Input() programApplicantType: ProgramApplicantType; @Output() onRulesetUpdated = new EventEmitter(); @Output() onRulesetDeleted = new EventEmitter(); @Output() onValidChange = new EventEmitter(); @Output() onModalOpenOrClose = new EventEmitter(); @Output() onUiRulesChange = new EventEmitter(); @Output() onHasRuleChanges = new EventEmitter(); @Output() onActiveRulesetChange = new EventEmitter(); @Output() applyRulesWithOrChange = new EventEmitter(); @Output() onToggleChangesSaved = new EventEmitter(); showRules = false; uiRules: Automation.CriteriaFormState[]; currentRules: AutomationAPI.AutomationRuleSetExpressionBaseModel[]; panelSections: PanelSection[] = []; activePanel: Panel; activeRuleset: ProgramAutomationRulesetForUi; applyRulesWithOr = false; isAfterInit = false; objects: Automation.ObjectConfig[]; rulesValid = true; hasRuleChanges = false; sub = new Subscription(); constructor ( private i18n: I18nService, private spinnerService: SpinnerService, private ruleAutomationService: RuleAutomationService, private formService: FormsService, private modalFactory: ModalFactory, private programAutomationService: ProgramAutomationService, private arrayHelper: ArrayHelpersService, private formLogicService: FormLogicService ) { this.sub.add(this.programAutomationService.changesTo$('newRouteClicked').subscribe((newRouteClicked) => { if (newRouteClicked) { this.onCreateOrEditRuleset(true); this.programAutomationService.setNewRouteClicked(false); } })); this.sub.add(this.programAutomationService.changesTo$('evaluationOrderClicked').subscribe((evaluationOrderClicked) => { if (evaluationOrderClicked) { this.onEvaluationOrder(); this.programAutomationService.setEvaluationOrderClicked(false); } })); } async ngOnInit () { await this.setObjects(); this.onRulesetInit(); this.isAfterInit = true; } async ngOnChanges (changes: SimpleChanges) { if (changes.rulesets && this.isAfterInit) { this.onRulesetInit(); } if (changes.formId && this.isAfterInit) { await this.setObjects(); } } onRulesetInit () { this.rulesValid = this.rulesets.length > 0; this.setPanels(); this.emitValidity(); } setPanels ( navigateToRulesetId?: number, defaultToFirstPanel?: boolean ) { const rulesets = this.arrayHelper.sort(this.rulesets || [], 'sequence'); this.panelSections = [{ name: '', panels: rulesets.map((ruleset) => { return { context: ruleset, name: ruleset.name, description: this.i18n.translate( 'common:textRouteToDynamic', { name: ruleset.programName }, 'Route to: __name__' ), icon: ruleset.valid ? 'check' : 'exclamation-circle', iconClass: ruleset.valid ? 'success' : 'danger' }; }) }]; if (!this.activePanel || !!navigateToRulesetId || defaultToFirstPanel) { if (navigateToRulesetId) { const activePanel = this.panelSections[0].panels.find((panel) => { return panel.context.grantProgramRoutingAutomationRuleSetId === navigateToRulesetId; }); this.setActivePanel(activePanel); } else if (this.panelSections[0].panels.length > 0) { this.setActivePanel(this.panelSections[0].panels[0]); } } } async setObjects () { if (this.formId) { this.spinnerService.startSpinner(); const revisionId = this.formService.getLatestRevisionId(this.formId, true); const form = await this.formLogicService.getAndSetForm(this.formId, revisionId); const formObject = await this.ruleAutomationService.formToRuleObject(form); const showApplicationObject = this.programAutomationService.allowApplicationRules(form.formDefinition); this.objects = this.arrayHelper.sort([ showApplicationObject ? this.ruleAutomationService.getApplicationObject(false, true) : undefined, this.ruleAutomationService.getApplicantObject(), this.ruleAutomationService.getEmployeeSsoObject(), formObject ].filter((item) => !!item), 'label'); this.spinnerService.stopSpinner(); } else { this.objects = []; } } async onActivePanelClick (panel: Panel) { await this.saveRulesetChanges(); this.setActivePanel(panel); } setActivePanel (panel: Panel) { this.activePanel = panel; this.setActiveRuleset(panel.context?.grantProgramRoutingAutomationRuleSetId); this.currentRules = this.activeRuleset?.rules ?? []; const rules = this.ruleAutomationService.mapAPIRulesToCriteria( this.currentRules, this.objects ); this.setUiRules(rules); this.showRules = false; setTimeout(() => { this.showRules = true; }); } setActiveRuleset (grantProgramRoutingAutomationRuleSetId: number) { this.activeRuleset = this.rulesets.find((ruleset) => { return grantProgramRoutingAutomationRuleSetId === ruleset.grantProgramRoutingAutomationRuleSetId; }); this.applyRulesWithOr = this.activeRuleset?.applyRulesWithOr ?? false; this.applyRulesWithOrChange.emit(this.applyRulesWithOr); this.onActiveRulesetChange.emit(this.activeRuleset); } rulesChanged (rules: Automation.CriteriaFormState[]) { rules = rules.map((rule) => { return { ...rule, err: undefined, ruleLabel: undefined }; }); if (!isEqual(this.uiRules, rules)) { this.setHasRuleChanges(true); this.setUiRules(rules); const adaptedRules = this.ruleAutomationService.getAdaptedRulesForAPI(rules); const ruleset = { ...this.activeRuleset, rules: adaptedRules }; this.onRulesetUpdated.emit(ruleset); } } ruleValidationChanged (valid: boolean) { this.rulesValid = valid; this.emitValidity(); const ruleset = this.rulesets.find((set) => { return set.grantProgramRoutingAutomationRuleSetId === this.activeRuleset?.grantProgramRoutingAutomationRuleSetId; }); if (ruleset) { ruleset.valid = valid; this.updateRulesets(ruleset, false, null, false, true); } const panelIndex = this.panelSections[0].panels.findIndex((panel) => { return this.activeRuleset?.grantProgramRoutingAutomationRuleSetId === panel.context.grantProgramRoutingAutomationRuleSetId; }); if (panelIndex > -1) { this.panelSections[0].panels[panelIndex].icon = valid ? 'check' : 'exclamation-circle'; this.panelSections[0].panels[panelIndex].iconClass = valid ? 'success' : 'danger'; } } emitValidity () { this.onValidChange.emit(this.rulesValid); } getActiveRulesetIndex () { return this.rulesets.findIndex((set) => { return set.grantProgramRoutingAutomationRuleSetId === this.activePanel.context.grantProgramRoutingAutomationRuleSetId; }); } async saveRulesetChanges () { if (this.hasRuleChanges) { this.spinnerService.startSpinner(); const ruleset = await this.programAutomationService.handleSaveRuleset( this.grantProgramRoutingDetailsId, this.formId, this.activeRuleset.grantProgramRoutingAutomationRuleSetId, this.activeRuleset.routeToGrantProgramId, this.activeRuleset.name, this.applyRulesWithOr, this.ruleAutomationService.getAdaptedRulesForAPI(this.uiRules) ); if (ruleset) { this.updateRulesets(ruleset, false, undefined, false, true); this.onRulesetUpdated.emit(ruleset); this.setHasRuleChanges(false); } this.spinnerService.stopSpinner(); } } setHasRuleChanges (hasRuleChanges: boolean) { this.hasRuleChanges = hasRuleChanges; this.onHasRuleChanges.emit(hasRuleChanges); } setUiRules (rules: Automation.CriteriaFormState[]) { this.uiRules = rules; this.onUiRulesChange.emit(this.uiRules); } updateRulesets ( ruleset?: ProgramAutomationRulesetForUi, isCreate = false, navigateToRulesetId?: number, defaultToFirstPanel?: boolean, skipSetPanels = false ) { if (isCreate) { this.rulesets = [ ...this.rulesets, ruleset ]; } else { const index = this.getActiveRulesetIndex(); this.rulesets = [ ...this.rulesets.slice(0, index), ruleset, ...this.rulesets.slice(index + 1) ].filter((item) => !!item); } if (!skipSetPanels) { this.setPanels(navigateToRulesetId, defaultToFirstPanel); } } async onCreateOrEditRuleset (isCreate: boolean) { await this.saveRulesetChanges(); this.onModalOpenOrClose.emit(true); const response = await this.modalFactory.open( ProgramAutomationRouteModalComponent, { id: isCreate ? null : this.activeRuleset.grantProgramRoutingAutomationRuleSetId, programId: isCreate ? null : this.activeRuleset.routeToGrantProgramId, routeName: isCreate ? null : this.activeRuleset.name, programApplicantType: this.programApplicantType } ); this.onModalOpenOrClose.emit(false); if (response) { this.spinnerService.startSpinner(); const found = this.rulesets.find((set) => { return set.grantProgramRoutingAutomationRuleSetId === this.activeRuleset.grantProgramRoutingAutomationRuleSetId; }); const ruleset = await this.programAutomationService.handleSaveRuleset( this.grantProgramRoutingDetailsId, this.formId, isCreate ? null : this.activeRuleset.grantProgramRoutingAutomationRuleSetId, response.programId, response.routeName, isCreate ? false: this.applyRulesWithOr, isCreate ? [] : found.rules.map((rule) => { return { ...rule, isRemoved: false }; }) ); if (ruleset) { this.updateRulesets(ruleset, isCreate, ruleset.grantProgramRoutingAutomationRuleSetId); this.onRulesetUpdated.emit(ruleset); } this.spinnerService.stopSpinner(); } } async onDeleteRuleset () { this.onModalOpenOrClose.emit(true); const proceed = await this.modalFactory.open( ConfirmationModalComponent, { modalHeader: this.i18n.translate( 'common:hdrDeleteRoute', {}, 'Delete Route' ), modalSubHeader: this.activeRuleset.name, confirmButtonText: this.i18n.translate( 'common:btnDelete', {}, 'Delete' ), confirmText: this.i18n.translate( 'common:textConfirmDeleteRoute', {}, 'Are you sure you want to delete the route? This action cannot be undone.' ) } ); this.onModalOpenOrClose.emit(false); if (proceed) { this.spinnerService.startSpinner(); const result = await this.programAutomationService.handleDeleteRuleset( this.grantProgramRoutingDetailsId, this.activeRuleset.grantProgramRoutingAutomationRuleSetId ); if (result.passed) { this.onToggleChangesSaved.emit(); this.updateRulesets(undefined, false, null, true); this.onRulesetDeleted.emit(this.activeRuleset.grantProgramRoutingAutomationRuleSetId); } this.spinnerService.stopSpinner(); } } async onEvaluationOrder () { await this.saveRulesetChanges(); this.onModalOpenOrClose.emit(true); const response = await this.modalFactory.open( SequenceModalComponent, { modalHeader: this.i18n.translate( 'common:hdrEvaluationOrder', {}, 'Evaluation Order' ), description: `
${this.i18n.translate( 'common:textEvaluationOrderProgramRoutingDesc', {}, `An applicant's responses are evaluated against each route and it's routing rules when the routing form is submitted. The evaluation order is the order in which these responses are compared to each route. The first route whose criteria are met will be the program to which the applicant will be directed to apply.` )}
`, nameHeader: this.i18n.translate( 'CONFIG:hdrRoutingRules', {}, 'Routing Rules' ), items: this.rulesets.map((set) => { return { id: set.grantProgramRoutingAutomationRuleSetId, name: set.name, sequence: set.sequence, isDisabledOrInActive: false }; }) } ); this.onModalOpenOrClose.emit(false); if (response) { this.spinnerService.startSpinner(); const payload: RulesetSequencePayload = { ruleSets: response.map((item) => { return { grantProgramRoutingAutomationRuleSetId: item.id, sequence: item.sequence }; }), grantProgramRoutingDetailsId: this.grantProgramRoutingDetailsId }; const passed = await this.programAutomationService.handleSequenceRulesets(payload); if (passed) { this.onToggleChangesSaved.emit(); this.rulesets = this.arrayHelper.sort(this.rulesets.map((ruleset) => { const foundSet = payload.ruleSets.find((set) => { return set.grantProgramRoutingAutomationRuleSetId === ruleset.grantProgramRoutingAutomationRuleSetId; }); return { ...ruleset, sequence: foundSet.sequence ?? ruleset.sequence }; }), 'sequence'); this.setPanels(null, true); } this.spinnerService.stopSpinner(); } } ngOnDestroy () { this.sub.unsubscribe(); } }