import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { CurrencyService } from '@core/services/currency.service'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { FormDefinitionComponent, FormioAnswerValues, FormioChanges, ValueLogicResult } from '@features/configure-forms/form.typing'; import { ComponentHelperService } from '@features/formio/services/component-helper/component-helper.service'; import { UserService } from '@features/users/user.service'; import { SelectOption, TypeaheadSelectOption, TypeSafeFormBuilder, TypeSafeFormGroup, TypeToken } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogicBuilderService } from '../logic-builder.service'; import { ConditionalLogicResultType, EvaluationType, GlobalValueLogicGroup, LogicColumn, LogicColumnDisplay, LogicGroupType, LogicValueFormatType } from '../logic-builder.typing'; export interface ValueLogicBuilderFormGroup { resultType: ConditionalLogicResultType; setResult: LogicColumn; staticResult: V; applyCalculation: boolean; }; export interface CalculationOptionsFormGroup { operator: 'plus'|'minus'; constant: number; constantUnits: 'days'|'weeks'|'years'; } @Component({ selector: 'gc-value-logic-builder', templateUrl: './value-logic-builder.component.html', styleUrls: ['./value-logic-builder.component.scss'] }) export class ValueLogicBuilderComponent implements OnInit, OnChanges { @Input() formId: number; @Input() availableColumns: LogicColumnDisplay[]; @Input() component: FormDefinitionComponent; @Input() sourceColumn: LogicColumnDisplay; @Input() options: (TypeaheadSelectOption|SelectOption)[] = []; @Input() logicValueFormatType: LogicValueFormatType; @Input() logic: GlobalValueLogicGroup>; @Input() maxDepth: number; @Input() alwaysTrueAvailable: boolean; @Output() logicChanged = new EventEmitter>>(); @Output() validChange = new EventEmitter(); componentForView: FormDefinitionComponent; conditionsValid: boolean; showCalculationCheckbox: boolean; calculationLabel: string; resultTypeOptions = this.logicBuilderService.getResultTypeOptions(); operatorOptionItems: TypeaheadSelectOption[] = [{ label: `+`, value: 'plus' }, { label: `-`, value: 'minus' }]; constantUnitOptionItems: TypeaheadSelectOption[] = [{ label: this.i18n.translate('common:lblDays', {}, 'Days'), value: 'days' }, { label: this.i18n.translate('common:lblWeeks', {}, 'Weeks'), value: 'weeks' }, { label: this.i18n.translate('common:lblYears', {}, 'Years'), value: 'years' }]; setValueOfComponentOptions: TypeaheadSelectOption>[]; ConditionalLogicResultType = ConditionalLogicResultType; formGroup: TypeSafeFormGroup>>; calculationOptionsFormGroup: TypeSafeFormGroup; $formAnswerType = new TypeToken(); isAfterInit = false; constructor ( private logicBuilderService: LogicBuilderService, private i18n: I18nService, private formBuilder: TypeSafeFormBuilder, private componentHelper: ComponentHelperService, private currencyService: CurrencyService, private clientSettingsService: ClientSettingsService, private userService: UserService ) { } get isOtherColumn () { return this.logic?.resultType === ConditionalLogicResultType.OTHER_COLUMN; } ngOnInit () { if (!this.logic || this.availableColumns.length === 0) { this.setLogicToDefault(); } this.handleCurrencyResults(); if (this.component) { this.setValueOfComponentOptions = this.sourceColumn?.otherColumnOptions; this.adaptModalFormToDataType(); this.setFormGroup(); this.componentForView = { ...this.component, label: this.i18n.translate( 'common:textValueOfComponent', {}, 'Value of component' ) }; } this.isAfterInit = true; } ngOnChanges (changes: SimpleChanges) { if (changes.logic && this.isAfterInit) { if (this.logic.resultType !== this.formGroup.value.resultType) { this.formGroup.get('resultType').setValue(this.logic.resultType); } this.handleCurrencyResults(); } } handleCurrencyResults () { if (this.logicValueFormatType === 'currency' && !this.logic.result && !this.isOtherColumn) { const currencyOptions = this.currencyService.getCurrencyOptionsForComponent( '', this.component.useCustomCurrency, this.component.customCurrency ); this.logic = { ...this.logic, result: { amountEquivalent: null, amountForControl: null, amountInDefaultCurrency: null, currency: this.componentHelper.getCurrencyForFormFieldControl( '', this.component.useCustomCurrency, this.component.customCurrency, currencyOptions, this.clientSettingsService.defaultCurrency, this.userService.lastSelectedCurrency ) } }; this.logicChanged.emit(this.logic); } } adaptModalFormToDataType () { if (this.logicValueFormatType === 'date') { this.resultTypeOptions = this.resultTypeOptions.concat([{ label: this.i18n.translate( 'common:lblTodaysDate', {}, 'Today\'s Date' ), value: ConditionalLogicResultType.RELATIVE_DATE }]); this.calculationOptionsFormGroup = this.formBuilder.group({ operator: this.logic?.resultConfig?.operator || 'plus', constant: this.logic?.resultConfig?.constant || 1, constantUnits: this.logic?.resultConfig?.constantUnits || 'days' }); this.calculationLabel = this.i18n.translate( 'common:lblDateCalculationLabel', {}, 'Add or subtract time from the selected date' ); this.showCalculationCheckbox = this.logic.resultType !== ConditionalLogicResultType.STATIC_VALUE; } } setFormGroup () { const staticResult = !this.isOtherColumn ? this.logic?.result : null; const setResult = this.isOtherColumn ? this.logic?.result : null; const applyCalculation = !!this.logic?.resultConfig?.constant; this.formGroup = this.formBuilder.group>>({ resultType: this.logic?.resultType || ConditionalLogicResultType.STATIC_VALUE, staticResult: [staticResult as FormioAnswerValues], setResult: [setResult as LogicColumn], applyCalculation }); } setLogicToDefault () { this.logic = this.logicBuilderService.getDefaultConditionalValueLogic(); if (this.logicValueFormatType === 'currency') { this.logic = { ...this.logic, result: { amountEquivalent: null, amountForControl: null, amountInDefaultCurrency: null, currency: '' } }; } // Conditions are required for value logic this.conditionsValidChange(false); this.logicChanged.emit(this.logic); } conditionsValidChange (valid: boolean) { this.conditionsValid = valid; this.updateValidity(); } updateValidity () { this.checkForConditionlessAvailable(); const valid = this.getValidity(); this.validChange.emit(valid); } getValidity (): boolean { const hasValidValue = this.getValueValidity(); const conditionlessRulePermitted = this.logic.evaluationType === EvaluationType.AlwaysTrue && this.logic.conditions.length === 0 && this.alwaysTrueAvailable; const relativeDateOperationValid = this.formGroup?.value?.resultType === ConditionalLogicResultType.RELATIVE_DATE && (this.calculationOptionsFormGroup.value.constant || !this.formGroup.value.applyCalculation); const valid = (hasValidValue || relativeDateOperationValid) && (this.conditionsValid || conditionlessRulePermitted); return valid; } getValueValidity (): boolean { const formValues = this.formGroup?.value; const hasStaticValue = !!formValues?.staticResult || formValues?.staticResult === false || formValues?.staticResult === 0; const setResultValid = this.getSetResultValidity(formValues?.setResult); const hasValidValue = (this.isOtherColumn && setResultValid) || (!this.isOtherColumn && hasStaticValue); return hasValidValue; } getSetResultValidity (val: LogicColumn) { return (val as unknown) !== '' && val !== undefined && val !== null; } handleLogicChange (group: LogicGroupType>) { this.logic = group as GlobalValueLogicGroup>; this.logicChanged.emit(this.logic); } handleResultTypeChange (resultType: ConditionalLogicResultType) { const result = this.getResultByResultType(resultType); this.logic = { ...this.logic, resultType, result }; // reset the apply calc checkbox this.formGroup.get('applyCalculation').setValue(false); this.showCalculationCheckbox = resultType !== ConditionalLogicResultType.STATIC_VALUE && this.logicValueFormatType === 'date'; this.handleLogicChange(this.logic); this.updateValidity(); } getResultByResultType (resultType: ConditionalLogicResultType) { let result; const formValues = this.formGroup.value; switch(resultType) { case ConditionalLogicResultType.OTHER_COLUMN: result = formValues.setResult; break; case ConditionalLogicResultType.RELATIVE_DATE: result = null; break; default: case ConditionalLogicResultType.STATIC_VALUE: result = formValues.staticResult; break; } return result; } checkForConditionlessAvailable () { if (this.logic.conditions.length === 0 && this.alwaysTrueAvailable) { this.logic.evaluationType = EvaluationType.AlwaysTrue; } else if (this.logic.conditions.length !== 0) { this.logic.evaluationType = EvaluationType.ConditionallyTrue; }; } handleCalculationChange () { const formGroupVal = this.calculationOptionsFormGroup.value; this.logic = { ...this.logic, resultConfig: { operator: formGroupVal.operator, constant: formGroupVal.constant, constantUnits: formGroupVal.constantUnits } }; this.handleLogicChange(this.logic); this.updateValidity(); } handleCalculationUpdate () { const useCalculationDataOnForm = this.formGroup.value.applyCalculation; if (useCalculationDataOnForm) { this.handleCalculationChange(); } else { this.logic = { ...this.logic, resultConfig: { operator: null, constant: null, constantUnits: null } }; this.handleLogicChange(this.logic); this.updateValidity(); } } onStaticValueChange (result: FormioChanges) { const staticResult = result.value; this.formGroup.get('staticResult').setValue(staticResult); const logic: GlobalValueLogicGroup = { ...this.logic, result: result.value, resultType: this.formGroup.value.resultType }; this.handleLogicChange(logic); this.updateValidity(); } onSetValueComponentChange (column: LogicColumn) { const logic: GlobalValueLogicGroup> = { ...this.logic, result: column, resultType: this.formGroup.value.resultType }; this.handleLogicChange(logic); this.updateValidity(); } }