import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { SpinnerService } from '@core/services/spinner.service'; import { Payment } from '@core/typings/payment.typing'; import { InKindService } from '@features/in-kind/in-kind.service'; import { InKindAwardedItemApi, InKindCategoryItemStat, InKindItemToAwardOrPay } from '@features/in-kind/in-kind.typing'; import { ArrayHelpersService, AutoTableRepository, AutoTableRepositoryFactory, SimpleStringMap, TypeaheadSelectOption, ValueComparisonService } from '@yourcause/common'; import { FormError } from '@yourcause/common/core-forms'; import { I18nService } from '@yourcause/common/i18n'; import { NotifierService } from '@yourcause/common/notifier'; @Component({ selector: 'gc-in-kind-items-selector-table', templateUrl: 'in-kind-items-selector-table.component.html', styleUrls: ['./in-kind-items-selector-table.component.scss'] }) export class InKindItemsSelectorTableComponent implements OnInit, OnChanges { @Input() isAward = true; @Input() totalText = this.i18n.translate( 'GLOBAL:textTotalAmount', {}, 'Total amount' ); @Input() inKindItems: InKindItemToAwardOrPay[] = []; // items for this display @Input() awardAndPaymentSame = false; // when true, we do not need to do validation @Input() awardedItems: InKindItemToAwardOrPay[]; // Necessary when isAward is false @Input() paidItems: InKindItemToAwardOrPay[]; // Necessary when isAward is true @Input() alreadyAwardedItems: InKindAwardedItemApi[]; @Input() alreadyPaidItems: InKindAwardedItemApi[]; @Input() originalPayment: Payment; // for validation around inUse @Input() disabled = false; @Output() onEditChange = new EventEmitter(); @Output() onItemsChange = new EventEmitter(); @Output() onTotalChange = new EventEmitter(); repository: AutoTableRepository; allItems: InKindCategoryItemStat[] = []; availableItems: TypeaheadSelectOption[] = []; categoryOptions: TypeaheadSelectOption[] = []; totalMap: SimpleStringMap = {}; totalUnits = 0; totalValue = 0; loaded = false; editingIndex: number; addingItem = false; itemsToPayMap: SimpleStringMap = {}; awardedItemsMap: SimpleStringMap = {}; paidItemsMap: SimpleStringMap = {}; alreadyPaidItemsMap: SimpleStringMap = {}; alreadyAwardedItemsMap: SimpleStringMap = {}; errorMap: SimpleStringMap = {}; noItemsFoundText = this.i18n.translate( 'common:textNoItemsFound', {}, 'No items found' ); greaterThanZeroError = this.i18n.translate( 'AWARDS:textUnitsMustBeGreaterThanZero', {}, 'Units must be greater than zero' ); itemNoLongerInUseError = this.i18n.translate( 'AWARDS:textCannotAddUnitsToInactiveItems', {}, 'Cannot add units to inactive items' ); selectedCategory: number = null; categoriesNameMap: Record = {}; categoryPlaceholder = this.i18n.translate( 'common:textSelectACategory', {}, 'Select a category' ); constructor ( private inKindService: InKindService, private autoTableFactory: AutoTableRepositoryFactory, private valueComparisonService: ValueComparisonService, private notifierService: NotifierService, private spinnerService: SpinnerService, private i18n: I18nService, private arrayHelper: ArrayHelpersService ) { } get categories () { return this.inKindService.categories; } get isEditing () { return this.editingIndex !== undefined; } async ngOnInit () { this.spinnerService.startSpinner(); if (!this.awardAndPaymentSame && !this.isAward) { this.noItemsFoundText = this.i18n.translate( 'AWARDS:textNoItemsAvailableToSelectForAward', {}, 'All available items from the award have been used. To add a new item, add it to the award first.' ); } this.inKindItems.forEach((item) => { this.totalMap[item.identification] = +item.unitsEntered * +item.value; }); this.categoriesNameMap = this.categories.reduce((acc, category) => { return { ...acc, [category.id]: category.name }; }, {}); this.categoryOptions = this.categories.map((cat) => { return { label: cat.name, value: cat.id }; }); this.allItems = this.inKindService.allItems; this.setAvailableItems(); this.repository = this.autoTableFactory.create({ key: `IN_KIND_ITEMS_SELECTED_${this.isAward ? 'AWARD' : 'PAYMENT'}`, columns: [], notifier: this.notifierService, valueComparisonService: this.valueComparisonService, rows: this.inKindItems, rowsPerPage: 1000000, skipAddToState: true }); this.setTotalsLine(); this.loaded = true; this.spinnerService.stopSpinner(); } ngOnChanges (changes: SimpleChanges) { if (changes.paidItems) { this.paidItemsMap = {}; this.paidItems.forEach((item) => { this.paidItemsMap = { ...this.paidItemsMap, [item.identification]: item }; }); } if (changes.alreadyPaidItems) { this.alreadyPaidItemsMap = {}; (this.alreadyPaidItems || []).forEach((item) => { let countForItem = this.alreadyPaidItemsMap[item.itemIdentification]; if (!countForItem) { countForItem = +item.count; } else { countForItem = countForItem + +item.count; } this.alreadyPaidItemsMap = { ...this.alreadyPaidItemsMap, [item.itemIdentification]: countForItem }; }); } if (changes.alreadyAwardedItems) { this.alreadyAwardedItemsMap = {}; (this.alreadyAwardedItems || []).forEach((item) => { this.alreadyAwardedItemsMap = { ...this.alreadyAwardedItemsMap, [item.itemIdentification]: item.count }; }); } if (changes.awardedItems) { this.awardedItemsMap = {}; this.awardedItems.forEach((item) => { this.awardedItemsMap = { ...this.awardedItemsMap, [item.identification]: item }; }); this.setItemsToPayMap(); this.setAvailableItems(); } } trackBy (_: number, row: InKindItemToAwardOrPay) { return row.identification; } categoryChange (category: number) { this.selectedCategory = category; this.setAvailableItems(); } setAvailableItems () { const usedItems = this.inKindItems.map((item) => item.identification); const availableItems = this.allItems.filter((item) => { if (!item.inUse) { return false; } const hasNotBeenUsed = !usedItems.includes(item.identification); if (!this.awardAndPaymentSame && !this.isAward) { const availFromAward = this.awardedItems.filter((avail) => { const found = this.alreadyPaidItems.find((paid) => { return paid.itemIdentification === avail.identification; }); if (found) { return +avail.unitsEntered > found.count; } return true; }).map((i) => i.identification); return hasNotBeenUsed && availFromAward.includes(item.identification); } return hasNotBeenUsed; }); this.setCategoryOptions(availableItems); const availableItemOptions = availableItems.filter((item) => { return this.selectedCategory === item.categoryId; }).map((item) => { return { label: item.name, value: item.identification }; }); this.availableItems = this.arrayHelper.sort(availableItemOptions, 'label'); } setCategoryOptions (availableItems: InKindCategoryItemStat[]) { const allCategories = this.inKindService.categories; const categoriesInUse = allCategories.filter((category) => { return availableItems.map((item) => item.categoryId).includes(category.id); }); const categoryOptions = this.arrayHelper.sort(categoriesInUse.map((cat) => { return { value: cat.id, label: cat.name }; }), 'label'); this.categoryOptions = categoryOptions; } setItemsToPayMap () { if (!this.awardAndPaymentSame && !this.isAward) { this.inKindItems.forEach((item) => { const awarded = +this.awardedItemsMap[item.identification].unitsEntered; const paid = this.alreadyPaidItemsMap[item.identification] || 0; let toPay = awarded - paid - item.unitsEntered; if (toPay < 0) { toPay = 0; } this.itemsToPayMap[item.identification] = toPay; }); } } addAnItem () { this.addingItem = true; this.setAvailableItems(); const blankItem: InKindItemToAwardOrPay = { identification: null, name: '', value: 0, unitsRemaining: 0, categoryId: 0, unitsEntered: 0, inUse: true }; this.repository.rows = [ blankItem, ...this.inKindItems ]; this.editingIndex = 0; this.onEditChange.emit(this.isEditing); } saveRow (item: InKindItemToAwardOrPay) { if (this.addingItem) { this.addingItem = false; } this.editingIndex = undefined; this.onEditChange.emit(this.isEditing); let found = false; this.inKindItems.forEach((inKindItem) => { if (inKindItem.identification === item.identification) { inKindItem.unitsEntered = item.unitsEntered; found = true; } }); if (!found) { // If not found, it is a new item. this.inKindItems = [ item, ...this.inKindItems ]; this.repository.rows = this.inKindItems; } this.setItemsToPayMap(); this.onItemsChange.emit(this.inKindItems); this.onTotalChange.emit(this.totalValue); } removeRow (item: InKindItemToAwardOrPay, index: number) { if (this.addingItem) { this.addingItem = false; } this.editingIndex = undefined; this.setTotalPerRowMap(item, 0); this.onTotalChange.emit(this.totalValue); item.unitsEntered = 0; this.inKindItems = [ ...this.repository.currentSet.slice(0, index), ...this.repository.currentSet.slice(index + 1) ]; this.repository.rows = this.inKindItems; this.onItemsChange.emit(this.inKindItems); this.onEditChange.emit(this.isEditing); } itemSelected (item: InKindItemToAwardOrPay, identification: string) { if (identification) { const found = this.allItems.find((itm) => { return identification === itm.identification; }); item.name = found.name; item.value = found.value; item.unitsRemaining = found.unitsRemaining; item.categoryId = found.categoryId; item.identification = identification; item.unitsEntered = 0; this.onItemsEntered( item, item.unitsEntered ); } } onItemsEntered ( item: InKindItemToAwardOrPay, unitsEntered = item.unitsEntered || 0 ) { this.setTotalPerRowMap(item, unitsEntered); this.errorMap = { ...this.errorMap, [item.identification]: this.getError(item, unitsEntered) }; } getError ( item: InKindItemToAwardOrPay, unitsEntered = 0 ): FormError { if (!this.awardAndPaymentSame) { if (this.isAward) { const paidItem = this.paidItemsMap[item.identification]; const alreadyPaidItemCount = this.alreadyPaidItemsMap[item.identification]; const totalEntered = (paidItem ? +paidItem.unitsEntered : 0) + (alreadyPaidItemCount || 0); let isValid = +unitsEntered >= totalEntered; if (!isValid) { return { errorMessage: this.getMinUnitsError(totalEntered), value: { [item.identification]: '' } }; } const originalAwardedItemCount = this.alreadyAwardedItemsMap[item.identification]; if (!item.inUse && originalAwardedItemCount) { // If no longer in use, you can not add more units to item isValid = originalAwardedItemCount >= +unitsEntered; if (!isValid) { return { errorMessage: this.itemNoLongerInUseError, value: { [item.identification]: '' } }; } } } else { // Check if existing payment where item is no longer in use const found = this.originalPayment ? this.originalPayment.inKindItems.find((i) => { return i.itemIdentification === item.identification; }) : null; if (found && !item.inUse) { const originalPaymentCount = found.count; if (+unitsEntered > originalPaymentCount) { return { errorMessage: this.itemNoLongerInUseError, value: { [item.identification]: '' } }; } } // Check if item count is greater than what's been awarded const awardedItem = this.awardedItemsMap[item.identification]; const alreadyPaidCount = this.alreadyPaidItemsMap[item.identification] || 0; const avail = +awardedItem.unitsEntered - alreadyPaidCount; const isValid = +unitsEntered <= avail; if (!isValid) { return { errorMessage: this.getMaxUnitsError(avail), value: { [item.identification]: '' } }; } } } if (unitsEntered <= 0) { return { errorMessage: this.greaterThanZeroError, value: { [item.identification]: '' } }; } return null; } getMinUnitsError (min: number) { return this.i18n.translate( 'AWARDS:textUnitsCannotBeLessThanValue', { min }, 'Cannot be less than __min__' ); } getMaxUnitsError (max: number) { return this.i18n.translate( 'AWARDS:textUnitsCannotBeGreaterThanValue', { max }, 'Cannot be greater than __max__' ); } setTotalPerRowMap ( item: InKindItemToAwardOrPay, unitsEntered = item.unitsEntered ) { if ((+item.unitsEntered !== +unitsEntered)) { const valueDiff = (+unitsEntered * item.value) - (+item.unitsEntered * item.value); this.totalValue = this.totalValue + valueDiff; const unitDiff = +unitsEntered - +item.unitsEntered; this.totalUnits = this.totalUnits + unitDiff; } this.totalMap[item.identification] = +unitsEntered * +item.value; } setTotalsLine () { this.totalValue = Object.keys(this.totalMap).reduce((acc, key) => { return acc + this.totalMap[key]; }, 0); this.totalUnits = this.inKindItems.reduce((acc, item: InKindItemToAwardOrPay) => { return acc + +item.unitsEntered; }, 0); this.onTotalChange.emit(this.totalValue); } }