import { Component, Input, OnInit } from '@angular/core'; import { AbstractControl, Validators } from '@angular/forms'; import { SpinnerService } from '@core/services/spinner.service'; import { CyclesAPI } from '@core/typings/api/cycles.typing'; import { BudgetFundingSourceCombo, FundingSourceTypes } from '@core/typings/budget.typing'; import { ProcessingTypes } from '@core/typings/payment.typing'; import { ProgramApplicantType } from '@core/typings/program.typing'; import { BudgetService } from '@features/budgets/budget.service'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { PaymentProcessingService } from '@features/payment-processing/payment-processing.service'; import { ProgramService } from '@features/programs/program.service'; import { UserService } from '@features/users/user.service'; import { ArrayHelpersService, DateTimeRangeValidator, SimpleStringMap, 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'; import moment from 'moment'; import { CyclesService } from '../cycles.service'; interface ProgramCycleGroup { name: string; startDate: moment.Moment|string; startTime: moment.Moment|string; endDate: moment.Moment|string; endTime: moment.Moment|string; budgets: number[]; clientOrganizationsProcessingTypeId: ProcessingTypes; budgetFsCash: BudgetFundingSourceCombo; budgetFsInKind: BudgetFundingSourceCombo; } @Component({ selector: 'gc-program-cycle-modal', templateUrl: './program-cycle-modal.compnent.html', styleUrls: ['./program-cycle-modal.component.scss'] }) export class ProgramCycleModalComponent extends YCModalComponent< CyclesAPI.ConfigureProgramCycle > implements OnInit { @Input() isNew = false; @Input() isCopy = false; @Input() cycle: CyclesAPI.ConfigureProgramCycle; @Input() otherCycles: CyclesAPI.ConfigureProgramCycle[] = []; formGroup: TypeSafeFormGroup; otherCycleDates: SimpleStringMap<{ start: string; end: string; }> = {}; budgetOptions: TypeaheadSelectOption[] = []; showProcessorDropdown = false; processorOptions = [{ label: 'YourCause', value: ProcessingTypes.YourCause }, { label: this.clientSettingsService.clientBranding.name, value: ProcessingTypes.Client }]; budgets = this.budgetService.get('budgets'); simpleBudgetMap = this.budgetService.get('simpleBudgetMap'); FundingSourceTypes = FundingSourceTypes; budgetFsCashOptions: TypeaheadSelectOption[] = []; budgetFsInKindOptions: TypeaheadSelectOption[] = []; showDefaultCashBudgetFs = false; showDefaultInKindBudgetFs = false; closedText = this.i18n.translate('GLOBAL:textClosed'); cycleDatesOverlap = false; modalHeader: string; isIndividual: boolean; constructor ( private formBuilder: TypeSafeFormBuilder, private programService: ProgramService, private i18n: I18nService, private budgetService: BudgetService, private arrayHelper: ArrayHelpersService, private paymentProcessingService: PaymentProcessingService, private clientSettingsService: ClientSettingsService, private spinnerService: SpinnerService, private userService: UserService, private cyclesService: CyclesService, private analyticsService: AnalyticsService ) { super(); } get isNomination () { return location.pathname.includes('nomination'); } get programMap () { return this.programService.get('configureProgramMap'); } get activeProgramId () { return this.programService.get('activeProgramId'); } get program () { return this.programMap[this.activeProgramId]; } async ngOnInit () { if (!this.cycle) { this.cycle = {} as CyclesAPI.ConfigureProgramCycle; } this.isIndividual = this.program.applicantType === ProgramApplicantType.INDIVIDUAL; this.setModalHeader(); this.setOtherCycleDates(); if (!this.isNomination) { this.budgetOptions = this.arrayHelper.sort(this.budgets.map((budget) => { return { label: (budget.isClosed ? budget.name + ' (' + this.closedText + ') ' : budget.name), value: budget.id }; }), 'label'); if (this.cycle.budgets) { await this.setBudgetDefaultVisibility(); } } const asyncValidators = this.isNomination ? [] : [this.defaultBudgetFsValidator()]; this.formGroup = this.formBuilder.group({ name: [this.cycle.name || '', Validators.required], startDate: [ this.cycle.startDate ? moment(this.cycle.startDate) : moment(), Validators.required ], startTime: [ this.cycle.startDate ? moment(this.cycle.startDate) : moment().startOf('day') ], endDate: [ this.cycle.endDate ? moment(this.cycle.endDate) : moment().add(1, 'year'), Validators.required ], endTime: [ this.cycle.endDate ? moment(this.cycle.endDate) : moment().endOf('day') ], budgets: this.isNomination ? null : [this.cycle.budgets || [], Validators.required], clientOrganizationsProcessingTypeId: !this.isNomination && !this.isIndividual ? [this.cycle.clientOrganizationsProcessingTypeId, Validators.required] : null, budgetFsCash: this.isNomination ? null : [this.getBudgetFsValueForFormGroup(true)], budgetFsInKind: this.isNomination ? null : [this.getBudgetFsValueForFormGroup(false)] }, { validators: [ DateTimeRangeValidator() ], asyncValidators }); if (!this.isNomination) { this.onDefaultCashChange(); this.onDefaultInKindChange(); this.updateProcessor(); } this.setDate(true); this.setDate(false); this.checkCycleOverlap(); } setModalHeader () { if (this.isNew) { this.modalHeader = this.i18n.translate( 'PROGRAM:hdrAddNewCycle', {}, 'Add New Cycle' ); } else if (this.isCopy) { this.modalHeader = this.i18n.translate( 'PROGRAM:hdrCopyCycle', {}, 'Copy Cycle' ); } else { this.modalHeader = this.i18n.translate( 'PROGRAM:hdrEditCycle', {}, 'Edit Cycle' ); } } setOtherCycleDates () { this.otherCycles.forEach((cycle, index) => { this.otherCycleDates[index] = { start: this.getDateTime( cycle.startDate, cycle.startDate ), end: this.getDateTime( cycle.endDate, cycle.endDate ) }; }); } getBudgetFsValueForFormGroup (isCash = true) { const attr = isCash ? 'budgetFsCashOptions' : 'budgetFsInKindOptions'; const found = this[attr].find(option => { const budgetId = isCash ? this.cycle.defaultCashBudgetId : this.cycle.defaultInKindBudgetId; const fsId = isCash ? this.cycle.defaultCashFundingSourceId : this.cycle.defaultInKindFundingSourceId; return (option.value.budget.id === budgetId) && option.value.fundingSource.fundingSourceId === fsId; }); return found ? found.value : null; } async setBudgetDefaultVisibility () { this.spinnerService.startSpinner(); const { cashBudgets, inKindBudgets } = await this.getBudgetsSelectedByType(); this.budgetFsCashOptions = this.setBudgetDefaultOptions(cashBudgets); this.budgetFsInKindOptions = this.setBudgetDefaultOptions(inKindBudgets); if (this.formGroup) { if (this.budgetFsCashOptions.length === 1) { this.setBudgetFsCash(this.budgetFsCashOptions[0].value); } if (this.budgetFsInKindOptions.length === 1) { this.setBudgetFsInKind(this.budgetFsInKindOptions[0].value); } } this.showDefaultCashBudgetFs = this.budgetFsCashOptions.length > 0; this.showDefaultInKindBudgetFs = this.budgetFsInKindOptions.length > 0; this.spinnerService.stopSpinner(); } async getBudgetsSelectedByType () { const cashBudgets: number[] = []; const inKindBudgets: number[] = []; for (const budgetId of (this.cycle.budgets || [])) { await this.budgetService.setBudgetMapDetail(budgetId); const budget = this.simpleBudgetMap[budgetId]; const isCash = budget.fundingSourceType === FundingSourceTypes.DOLLARS; if (isCash) { cashBudgets.push(budgetId); } else { inKindBudgets.push(budgetId); } } return { cashBudgets, inKindBudgets }; } setBudgetFsCash (value: BudgetFundingSourceCombo) { this.formGroup.get('budgetFsCash').setValue(value); this.onDefaultCashChange(); } setBudgetFsInKind (value: BudgetFundingSourceCombo) { this.formGroup.get('budgetFsInKind').setValue(value); this.onDefaultInKindChange(); } setBudgetDefaultOptions (budgets: number[]) { return budgets.map(budgetId => { return this.budgetService.get('budgetMap')[budgetId]; }).reduce((acc, budget) => { return [ ...acc, ...budget.budgetFundingSources .map(source => { return { label: budget.name + ' - ' + source.fundingSourceName, value: { budget, fundingSource: source, comboId: budget.id + '-' + source.fundingSourceId, isClosed: budget.isClosed || source.isClosed } }; }) ]; }, []); } async budgetChanged () { this.cycle.budgets = this.formGroup.value.budgets || []; this.formGroup.get('clientOrganizationsProcessingTypeId').setValue(null); this.updateProcessor(); this.makeSureDefaultsStillValid(); await this.setBudgetDefaultVisibility(); } makeSureDefaultsStillValid () { const budgets: number[] = this.formGroup.value.budgets; const defaultCash: BudgetFundingSourceCombo = this.formGroup.value.budgetFsCash; const defaultInKind: BudgetFundingSourceCombo = this.formGroup.value.budgetFsInKind; const cashInvalid = defaultCash && !budgets.includes(defaultCash.budget.id); const inKindInvalid = defaultInKind && !budgets.includes(defaultInKind.budget.id); if (cashInvalid) { this.setBudgetFsCash(null); } if (inKindInvalid) { this.setBudgetFsInKind(null); } } ineligbleOrgProcessingTypeChange () { this.cycle.clientOrganizationsProcessingTypeId = this.formGroup.value.clientOrganizationsProcessingTypeId; } updateProcessor () { if ( this.cycle.budgets && this.cycle.budgets.length && !this.isIndividual ) { const { processor, hasOptions } = this.programService.getProcessorTypeForProgram( this.cycle.clientOrganizationsProcessingTypeId, this.cycle.budgets, this.paymentProcessingService.processorType, this.budgets ); this.formGroup.get('clientOrganizationsProcessingTypeId').setValue(processor); this.showProcessorDropdown = hasOptions; } else { this.showProcessorDropdown = false; } } onDefaultCashChange () { const budgetFs: BudgetFundingSourceCombo = this.formGroup.value.budgetFsCash; this.cycle.defaultCashBudgetId = budgetFs ? budgetFs.budget.id : null; this.cycle.defaultCashFundingSourceId = budgetFs ? budgetFs.fundingSource.fundingSourceId : null; } onDefaultInKindChange () { const budgetFs: BudgetFundingSourceCombo = this.formGroup.value.budgetFsInKind; this.cycle.defaultInKindBudgetId = budgetFs ? budgetFs.budget.id : null; this.cycle.defaultInKindFundingSourceId = budgetFs ? budgetFs.fundingSource.fundingSourceId : null; } datesChanged (isStart = true) { this.setDate(isStart); this.checkCycleOverlap(); } setDate (isStart = true) { const timeAttr = isStart ? 'startTime' : 'endTime'; const dateAttr = isStart ? 'startDate' : 'endDate'; const dateTime = this.getDateTime( this.formGroup.value[timeAttr], this.formGroup.value[dateAttr] ); this.cycle[dateAttr] = dateTime; } getDateTime (timeValue: string|moment.Moment, dateValue: string|moment.Moment) { const hours = moment(timeValue).hours(); const minutes = moment(timeValue).minutes(); return moment(dateValue).hour(hours).minute(minutes).seconds(0).format('YYYY-MM-DDTHH:mm:ss'); } checkCycleOverlap () { let datesOverlap = false; const cycleStart = this.cycle.startDate; const cycleEnd = this.cycle.endDate; Object.keys(this.otherCycleDates).forEach((key) => { const { start, end } = this.otherCycleDates[key]; const startIsBetween = moment(cycleStart).isBetween(start, end); const endIsBetween = moment(cycleEnd).isBetween(start, end); const startIsSame = moment(cycleStart).isSame(start) || moment(cycleStart).isSame(end); const endIsSame = moment(cycleEnd).isSame(start) || moment(cycleEnd).isSame(end); const otherStartIsBetween = moment(start).isBetween(cycleStart, cycleEnd); const otherEndIsBetween = moment(end).isBetween(cycleStart, cycleEnd); if ( startIsBetween || endIsBetween || startIsSame || endIsSame || otherStartIsBetween || otherEndIsBetween ) { datesOverlap = true; } }); this.cycleDatesOverlap = datesOverlap; } onSave () { const currentUserName = this.userService.user.firstName + ' ' + this.userService.user.lastName; if (this.isNew || this.isCopy) { this.cycle.createdBy = currentUserName; this.cycle.createdDate = moment().toString(); } this.cycle.updatedBy = currentUserName; this.cycle.updatedDate = moment().toString(); this.cycle.status = this.cyclesService.getCycleStatus( this.cycle.startDate, this.cycle.endDate ); this.closeModal.emit(this.cycle); this.analyticsService.emitEvent({ eventName: 'Program cycle modal submit', eventType: EventType.Click, extras: null }); } defaultBudgetFsValidator () { return async (group: AbstractControl) => { const response = await this.getBudgetsSelectedByType(); const cashOptions = this.setBudgetDefaultOptions(response.cashBudgets); const inKindOptions = this.setBudgetDefaultOptions(response.inKindBudgets); const cashDefault = group.value.budgetFsCash; const inKindDefault = group.value.budgetFsInKind; if (!cashDefault && cashOptions.length > 0) { return { budgetFsCash: { required: { i18nKey: 'common:textThisInputIsRequired', defaultValue: 'This input is required' } } }; } if (!inKindDefault && inKindOptions.length > 0) { return { budgetFsCash: { required: { i18nKey: 'common:textThisInputIsRequired', defaultValue: 'This input is required' } } }; } return null; }; } }