import { Component, Input, OnInit } from '@angular/core'; import { Validators } from '@angular/forms'; import { FundingSourceTypes } from '@core/typings/budget.typing'; import { BudgetService } from '@features/budgets/budget.service'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { ProgramService } from '@features/programs/program.service'; import { ArrayHelpersService, TypeaheadSelectOption, TypeSafeFormBuilder, TypeSafeFormGroup } from '@yourcause/common'; import { AnalyticsService, EventType } from '@yourcause/common/analytics'; import { YCModalComponent } from '@yourcause/common/modals'; import { uniqBy } from 'lodash'; interface UpdateBudgetGroup { budgetIdFundingSource: string; reserveFunds: boolean; } @Component({ selector: 'gc-update-budget-assignment-modal', templateUrl: './update-budget-assignment.component.html', styleUrls: ['./update-budget-assignment.component.scss'] }) export class UpdateBudgetAssignmentModalComponent extends YCModalComponent<{ budgetId: number; fundingSourceId: number; reserveFunds: boolean; }> implements OnInit { @Input() programName: string; @Input() programId: number; @Input() cycleName: string; @Input() cycleId: number; @Input() currentBudgetName: string; @Input() currentBudgetId: number; @Input() currentFundingSourceName: string; @Input() currentFundingSourceId: number; @Input() amountRequested: number; @Input() isCurrentlyReserved: boolean; @Input() applicationIds: number[]; defaultCurrency = this.clientSettingsService.defaultCurrency; clientAllowsReserveFunds = this.clientSettingsService.clientSettings.reserveFunds; budgetRemainingMap: Record = {}; budgetFundingSourceOptions: TypeaheadSelectOption[]; formGroup: TypeSafeFormGroup = this.formBuilder.group({ budgetIdFundingSource: [null, Validators.required], reserveFunds: false }); showReserveFunds = false; constructor ( private formBuilder: TypeSafeFormBuilder, private clientSettingsService: ClientSettingsService, private budgetService: BudgetService, private programService: ProgramService, private arrayHelper: ArrayHelpersService, private analyticsService: AnalyticsService ) { super(); } async ngOnInit () { this.budgetFundingSourceOptions = null; const program = await this.programService.getProgram('' + this.programId); const cycle = program.cycles.find(_cycle => _cycle.id === this.cycleId); const budgets = await Promise.all(cycle.budgetIds.map(async (budgetId) => { return this.budgetService.getBudget(budgetId); })); const options = budgets // they should only be able to pick an open budget .filter(budget => !budget.isClosed) .reduce((acc, budget) => { return [ ...acc, ...budget.budgetFundingSources // they should only be able to pick a Cash FS .filter(budgetFs => { return budgetFs.fundingSourceType === FundingSourceTypes.DOLLARS; }) .map(budgetFs => { const value = this.getKey(budget.id, budgetFs.fundingSourceId); const label = `${budget.name} | ${budgetFs.fundingSourceName}`; // the fund reservation is at the budget level, every funding source option for a budget will show the same unreserved amount // the available funds IS total amount available (after payments) - reserved amount this.budgetRemainingMap[value] = budgetFs.amountRemaining; return { label, value }; }) .filter(budgetFs => { const optionIsCurrentBudgetFs = budgetFs.value === this.getKey(this.currentBudgetId, this.currentFundingSourceId); const optionHasEnoughToCover = (this.budgetRemainingMap[budgetFs.value] >= this.amountRequested); // omit the current budget if it isn't currently reserved // and the current one doesn't have enough funds to cover it if ( optionIsCurrentBudgetFs && !this.isCurrentlyReserved && !optionHasEnoughToCover ) { return false; } return true; }) ]; }, []); this.budgetFundingSourceOptions = this.arrayHelper.sort(uniqBy(options, 'value'), 'label'); // preselect if only one option if (this.budgetFundingSourceOptions.length === 1) { const value = this.budgetFundingSourceOptions[0].value; this.formGroup.get('budgetIdFundingSource').setValue(value); this.onSelected(value); } } private getKey (budgetId: number, fsId: number) { return budgetId + '-' + fsId; } onSelected (value: string) { let remaining = this.budgetRemainingMap[value]; // if they pick the current budget/fs, we default in the fact that it's currently reserved if (value === this.getKey(this.currentBudgetId, this.currentFundingSourceId)) { this.formGroup.get('reserveFunds').setValue(this.isCurrentlyReserved); } else { this.formGroup.get('reserveFunds').setValue(false); } // if this application is reserved and on this budget // add the amount requested back to the available // so that they can get the option to unreserve the application if ( this.isCurrentlyReserved && this.getKey(this.currentBudgetId, this.currentFundingSourceId) === value ) { remaining += this.amountRequested; } const amountIsAvailable = remaining >= this.amountRequested; this.setShowReserveFunds(amountIsAvailable); } setShowReserveFunds (reserveAmountIsAvailable: boolean) { if (this.clientAllowsReserveFunds) { this.showReserveFunds = reserveAmountIsAvailable; } else { this.showReserveFunds = false; } } onSubmit () { const [budgetId, fundingSource] = this.formGroup?.value?.budgetIdFundingSource?.split('-'); this.closeModal.emit({ budgetId: +budgetId, fundingSourceId: +fundingSource, reserveFunds: this.showReserveFunds && this.formGroup?.value?.reserveFunds }); this.analyticsService.emitEvent({ eventName: 'Update budget assignment submit', eventType: EventType.Click, extras: null }); } }