import { Component, Input, OnInit } from '@angular/core'; import { FormControl, Validators } from '@angular/forms'; import { CurrencyService } from '@core/services/currency.service'; import { OpenCloseBudgetAPI } from '@core/typings/api/open-close-budget.typing'; import { Budget, BudgetDrilldownInfo, BudgetFundingSourceDetail } from '@core/typings/budget.typing'; import { TypeaheadSelectOption, TypeSafeFormBuilder, TypeSafeFormGroup } from '@yourcause/common'; import { AnalyticsService, EventType } from '@yourcause/common/analytics'; import { I18nService } from '@yourcause/common/i18n'; import { YCModalComponent } from '@yourcause/common/modals'; interface ActionGroup { actionType: OpenCloseBudgetAPI.RemainingFundsDecision; } interface FundingSourceGroup { [x: string]: number; } @Component({ selector: 'yc-open-close-budget-modal', templateUrl: './open-close-budget-modal.component.html', styleUrls: ['./open-close-budget-modal.component.scss'] }) export class OpenCloseBudgetModalComponent extends YCModalComponent implements OnInit { @Input() budgets: Budget[]; @Input() budgetDetails?: BudgetDrilldownInfo; @Input() context: 'open' | 'close'; actionFormGroup: TypeSafeFormGroup; fsFormGroup: TypeSafeFormGroup; fundingSourcesOptions: Record[]>; OpenCloseBudgetAPI = OpenCloseBudgetAPI; showFSTable = false; primaryButtonText: string; modalHeader: string; modalSubHeader: string; modalReady = false; fsWithAvailableFunds: BudgetFundingSourceDetail[]; scenarioText: string; remainingFundsDecisionOptions: TypeaheadSelectOption[]; constructor ( private i18n: I18nService, private formBuilder: TypeSafeFormBuilder, private currencyService: CurrencyService, private analyticsService: AnalyticsService ) { super(); } ngOnInit () { this.fsWithAvailableFunds = this.budgetDetails?.budgetFundingSourceDetailModel .filter((bfs) => { return bfs.budgetFundingSourceAvailableAmount > 0; }) ?? []; const fsGroups = this.fsWithAvailableFunds.reduce((acc, _, index) => { return { ...acc, [index]: new FormControl(null) }; }, {} as FundingSourceGroup); this.fsFormGroup = this.formBuilder.group( fsGroups ); this.actionFormGroup = this.formBuilder.group({ actionType: [ null, this.context === 'close' ? Validators.required : undefined ] }); this.setFundingSourceOptionsMap(); this.setModalScenario(); this.setModalText(); this.showFSTable = (this.budgets.length > 1) && Object.keys(this.fundingSourcesOptions).some((fso) => { return !!this.fundingSourcesOptions[fso].length; }); this.setActionOptions(); this.modalReady = true; } trackBy (_: number, row: BudgetFundingSourceDetail) { return row.fundingSourceName; } onCancel () { this.closeModal.emit(); } onSubmit () { const moveFSArray = [] as OpenCloseBudgetAPI.MoveFundingSourceToBudgetModel[]; const budgetIds = this.budgets.map((budget) => { return budget.id; }); if (this.actionFormGroup.value.actionType === OpenCloseBudgetAPI.RemainingFundsDecision.SELECT_BUDGET) { Object.keys(this.fsFormGroup.controls).forEach((control) => { moveFSArray.push({ fromFundingSourceId: this.fsWithAvailableFunds[+control].fundingSourceId, toBudgetId: this.fsFormGroup.get(control).value }); }); } const payload = { budgetIds, remainingFundsDecisionId: this.actionFormGroup.value.actionType || OpenCloseBudgetAPI.RemainingFundsDecision.BACK_TO_FS, moveFundingSourceToBudgetModels: moveFSArray } as OpenCloseBudgetAPI.CloseBudgetPayload; if (this.context === 'close') { this.closeModal.emit(payload); } else { this.closeModal.emit(this.budgets[0].id); } this.analyticsService.emitEvent({ eventName: 'Open close budget modal submit', eventType: EventType.Click, extras: null }); } setModalScenario () { switch (this.context) { case 'open': this.modalHeader = this.i18n.translate( 'BUDGET:textOpenBudget', {}, 'Open budget' ); this.primaryButtonText = this.i18n.translate( 'BUDGET:textOpenBudget', {}, 'Open budget' ); break; case 'close': this.modalHeader = this.i18n.translate( 'BUDGET:textCloseBudget', {}, 'Close budget' ); this.primaryButtonText = this.i18n.translate( 'BUDGET:textCloseBudget', {}, 'Close budget' ); break; } } setFundingSourceOptionsMap () { this.fundingSourcesOptions = this.fsWithAvailableFunds .reduce((acc, fs, index) => { const options = fs.budgetsUsingThisFs .filter((budget) => { return (budget.id !== this.budgets[0].id) && !budget.isClosed; }) .map((budget) => { return { value: budget.id, label: budget.name }; }); return { ...acc, [index]: options }; }, {} as Record[]>); } setActionOptions () { this.remainingFundsDecisionOptions = [{ value: OpenCloseBudgetAPI.RemainingFundsDecision.BACK_TO_FS, label: this.i18n.translate( 'BUDGET:textAddUnusedFundsBack', {}, 'Add unused funds back into each source to be available for new budgets.' ) }, { value: OpenCloseBudgetAPI.RemainingFundsDecision.DO_NOTHING, label: this.i18n.translate( 'BUDGET:textDoNothingRemainingFunds', {}, 'Do nothing. These funds will become unavailable for payment.' ) }].concat(this.showFSTable ? [{ value: OpenCloseBudgetAPI.RemainingFundsDecision.SELECT_BUDGET, label: this.i18n.translate( 'BUDGET:textAllocateRemainingFunds', {}, 'Allocate remaining funds to a new budget using the same source.' ) }] : []); } setModalText () { this.modalSubHeader = this.budgets[0].name; switch (this.context) { case 'open': if (this.budgets[0].totalUnavailableAmount > 0) { this.scenarioText = this.i18n.translate( 'BUDGET:textOpenBudgetWithFundsText', { budgetName: this.budgets[0].name, unusedFunds: this.currencyService.formatMoney( this.budgets[0].totalUnavailableAmount ) }, 'Opening the __budgetName__ budget will make the budget available for future payments. There is __unusedFunds__ allocated to the budget from attached funding sources that is unavailable for payment due to the budget being closed. Open the budget to access these funds.' ); } else { this.scenarioText = this.i18n.translate( 'BUDGET:textOpenBudgetWithoutFundsText', { budgetName: this.budgets[0].name }, 'Opening the __budgetName__ budget will make the budget available for future payments. All allocated funds to the budget have been used. Opening the budget will allow allocating additional funds.' ); } break; case 'close': if (this.budgets.length > 1) { this.scenarioText = this.i18n.translate( 'BUDGET:textCloseBudgetsWarning', { budgetName: this.budgets[0].name, remainingFunds: this.currencyService.formatMoney( this.budgets[0].totalAmountAvailable ), reservedAmount: this.currencyService.formatMoney( this.budgets[0].reservedAmount ) }, 'Closing the following budgets will prevent them from being used for future payments. Select an option below to determine what to do with the remaining funds.' ); } else { if (this.budgets[0].totalAmountAvailable > 0) { this.scenarioText = this.i18n.translate( 'BUDGET:textCloseBudgetWithFundsAndReservedText', { budgetName: this.budgets[0].name, remainingFunds: this.currencyService.formatMoney( this.budgets[0].totalAmountAvailable ), reservedAmount: this.currencyService.formatMoney( this.budgets[0].reservedAmount ) }, 'Closing the __budgetName__ budget will prevent it from being used for future payments. There is __remainingFunds__ remaining and __reservedAmount__ reserved from attached funding sources. Select an option below to determine what to do with these funds.' ); } else { this.scenarioText = this.i18n.translate( 'BUDGET:textCloseBudgetWithoutFundsAndReservedText', { budgetName: this.budgets[0].name, remainingFunds: this.currencyService.formatMoney( this.budgets[0].totalAmountAvailable ), reservedAmount: this.currencyService.formatMoney( this.budgets[0].reservedAmount ) }, 'Closing the __budgetName__ budget will prevent it from being used for future payments. There is __remainingFunds__ remaining and __reservedAmount__ reserved from attached funding sources.' ); } } break; } } }