import { Component, OnInit, Inject, ViewChild, ElementRef, NgZone, HostListener } from '@angular/core'; import { ListItemModel, UserService, Utils, BomListDataSource, BomListColumn } from '@arrow/bom/core'; import { WPT_DIALOG_DATA, WptDialogRef } from '@arrow/warpaint/dialog'; import { Row, ListService } from '@arrow/bom/list'; import { PageView, AlternativesType } from './alternativesModal.enums'; import { FeatureFlagService } from '@arrow/bom/feature-flag'; import { ConfigService } from '@arrow/bom/config'; import { finalize } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { CrossesFilterChangeModel } from '@arrow/bom/controls'; enum IPreviousPartColumns { PN = 'Part Number', MFR = 'Manufacturer', PRICE = 'Price', TOTAL = 'Total', TARIFF = 'Tariff', STOCK = 'Stock', LIFECYCLE = 'lifecycle', } const mobileResolutionWidth: number = 600; @Component({ selector: 'app-alternatives-modal', templateUrl: 'alternatives-modal.component.html', styleUrls: ['alternatives-modal.component.scss'] }) export class AlternativesModalComponent implements OnInit { columnDefs: Array = [ IPreviousPartColumns.PN, IPreviousPartColumns.MFR, IPreviousPartColumns.PRICE, IPreviousPartColumns.TOTAL, IPreviousPartColumns.TARIFF, IPreviousPartColumns.STOCK ]; public viewType = PageView; public fixedColumnHeaders: string[]; public rowId: string; public bomId: string; public alternateParams: any; public data: any; public rowsDataSource: Row[]; public PreviousDataSource:any; public rowData: Row; public alternativeType: string; public isQuantityValid: boolean; public listData: BomListDataSource; public selectedOption: any; public isMyArrow = false; public RIGHT_COLUMN_HEADERS: BomListColumn[]; public altPricesCount: number = 0; public stockLoading: boolean = true; public selectedPreviousPart: boolean = false; public isLowResolution = false; public inactivateDatasheetLinkOnCrosses = false; private _originalRows: Partial[]; private readonly _arrowHeaders: BomListColumn[]; private readonly _myArrowHeaders: BomListColumn[]; //Not sure if its something that changed during angular 9 // @ViewChild('scrollWrapper', { static: true }) scrollWrapper: ElementRef; // @ViewChild('container', { static: true }) container: ElementRef; @ViewChild('scrollWrapper') scrollWrapper: ElementRef; @ViewChild('container') container: ElementRef; @ViewChild('scrollRightBtn') scrollRightBtnEl: ElementRef; @ViewChild('scrollLeftBtn') scrollLeftBtnEl: ElementRef; public selectionCallback: (data: { current: ListItemModel; next: ListItemModel; isPrevious?:boolean }) => void; public adjustHeightCallback: (data: { rowId: string; pricesCount: number; bo: any; hasTariff: boolean; }) => void; public trackDataSheetCallback: (id: number) => void; public collapseColumns: boolean = false; public filterGroups: any[] = [ { name : this.translate.instant('stock'), key : 'stock', options : [{ value : 'in-stock', viewValue : this.translate.instant('In-Stock') }] } ]; constructor( @Inject(WPT_DIALOG_DATA) public modalParameters: any, public dialogRef: WptDialogRef, private _listService: ListService, public ffService: FeatureFlagService, public userService: UserService, public utils: Utils, public configService: ConfigService, private translate: TranslateService, private _zone: NgZone ) { this.rowsDataSource = [modalParameters.rowData]; this.PreviousDataSource = [modalParameters.previousPart]; this.alternateParams = modalParameters.alternateParams; this.bomId = modalParameters.bomId; this.fixedColumnHeaders = modalParameters.fixedColumnHeaders; this.alternativeType = modalParameters.alternativeType === AlternativesType.crosses ? 'Crosses' : 'Alternatives'; this.isQuantityValid = modalParameters.isQuantityValid; this.selectionCallback = modalParameters.selection; this.adjustHeightCallback = modalParameters.adjustHeight; this.trackDataSheetCallback = modalParameters.trackDataSheet; this.data = this.rowsDataSource[0].item; this.rowId = this.data.rowId; this._arrowHeaders = BomListColumn.getAlternatesRightColumnsArrowHeaders().sort(c => c.position); this._myArrowHeaders = BomListColumn.getAlternatesRightColumnsMyArrowHeaders().sort(c => c.position); } ngOnInit() { this.isMyArrow = this.configService.getTarget() === 'myarrow'; this.RIGHT_COLUMN_HEADERS = this.isMyArrow ? this._myArrowHeaders : this._arrowHeaders; this._listService.getAlternates(this.bomId,this.alternateParams) .pipe( finalize(() => { this._zone.run(() => { this.rowsDataSource[0].item.alternates = this._initialSortRow( this.rowsDataSource[0].item.alternates ); this._originalRows = this.rowsDataSource[0].item.alternates .map((item, idx) => ({ id: String(idx), item: item, part: { number: item.partName }, price: item.priceRanges })) .slice(); this.listData = { parts: this.rowsDataSource.length, selection: { rowIds: [], selectionInverted: false }, scrollableColumns: this.RIGHT_COLUMN_HEADERS, rows: this._originalRows.slice(0, 25), config: { stickyHeader: true, stickyTopPx: 0, wrapperEl: this.container.nativeElement } }; this._filtersSetup(this.listData.rows.map(r => r.item)); this.stockLoading = false; }) }) ) .subscribe((pricingData: any) => { pricingData.forEach(pricingDataItem => { const matchedAlt = this.rowsDataSource[0].item.alternates.find( (altModel: ListItemModel) => altModel.partId === pricingDataItem.partId ); if (matchedAlt) { matchedAlt.fillAltPriceInfo(pricingDataItem, pricingDataItem.priceSources); this.altPricesCount++; // If all requested alt prices loaded // flag that alt prices are already there for the item, no need to get them again on dropdown open if ( this.rowsDataSource[0].item.alternates && this.altPricesCount === this.rowsDataSource[0].item.alternates.length ) { this.rowsDataSource[0].item.altPrices = true; } } this._fillPreviousPartPricingInfo(pricingDataItem); }); }); } onSelect() { const nextItem = this.selectedPreviousPart ? this._getPreviousPartListModel() : this.rowsDataSource[0].item.alternates.find((item, idx) => idx === Number(this.selectedOption.rowId)); const data = { current: this.rowsDataSource[0].item, next: nextItem, isPrevious:this.selectedPreviousPart }; this.selectionCallback(data); this.dialogRef.close(); } updateSelection(selectionObj: { rowId: string; status: boolean }) { this.selectedOption = selectionObj; } updateCollapsableColumns() { this.collapseColumns = !this.collapseColumns; } saveAlternate(data: { current: ListItemModel; next: ListItemModel }): void { this.selectionCallback(data); this.dialogRef.close(); } trackDataSheet(id: number): void { this.trackDataSheetCallback(id); } scrollRight(): void { this.scrollWrapper.nativeElement.scrollLeft += 1000; this.scrollLeftBtnEl.nativeElement.focus(); if (!this.isLowResolution) // Deactivate 'Datasheet' link accessibility if not in mobile phone resolution and element is not visible to the users this.inactivateDatasheetLinkOnCrosses = true; } scrollLeft(): void { this.scrollWrapper.nativeElement.scrollLeft -= 1000; this.scrollRightBtnEl.nativeElement.focus(); let scrollPosition = this.scrollWrapper.nativeElement.scrollLeft; if (scrollPosition < 395) // Reactivate 'Datasheet' link accessibility if element becomes visible to the users (regardless screen resolution mode) this.inactivateDatasheetLinkOnCrosses = false; } onSort(e: any): void { this.listData = { ...this.listData, rows: this._sortRows(this.listData.rows.slice(), e) }; } onFilter(crossesFilterChangeModel: CrossesFilterChangeModel): void { const newRows = this._originalRows?.slice().filter(r => { const partStatus = []; if (r.item.stock > 0) { partStatus.push('in-stock'); } if (r.item.grade) { partStatus.push(this._makeKey(r.item.grade)); } if (r.item.matchType) { partStatus.push(this._makeKey(r.item.matchType)); } partStatus.push(this._makeKey(r.item.manufacturer)); return crossesFilterChangeModel.selectedOptions .map(item => (partStatus.indexOf(item) === -1 ? false : true)) .reduce((acc, cur) => acc && cur, true); }); this.listData = { ...this.listData, parts: newRows?.length || 0, rows: newRows }; } onResetFilter(): void { this.listData = { ...this.listData, parts: this._originalRows?.length || 0, rows: this._originalRows }; } @HostListener('window:resize', ['$event']) onResize(event?) { this.isLowResolution = window.outerWidth < mobileResolutionWidth; // Control 'Datasheet' link accessibility as element's visibility changes let scrollPosition = this.scrollWrapper.nativeElement.scrollLeft; if (!this.isLowResolution && scrollPosition >= 395) this.inactivateDatasheetLinkOnCrosses = true; else this.inactivateDatasheetLinkOnCrosses = false; } private _getPreviousPartListModel() : ListItemModel{ if(this.PreviousDataSource && this.PreviousDataSource.length > 0){ const previousPart = this.PreviousDataSource[0]; const currentPart = this.rowsDataSource[0].item; const params = { rowId: currentPart.rowId, data: null }; const listItem = new ListItemModel(params); listItem.partId = previousPart.partId; listItem.partName = previousPart.fullPart; listItem.manufacturer = previousPart.mfrName; return listItem; } return null; } private _fillPreviousPartPricingInfo(pricingItem: any){ const row = this.rowsDataSource[0]; const previousPartId = row.item?.previousPart?.partId; const previousPart = this.PreviousDataSource[0]; if(previousPart && previousPartId && pricingItem?.partId === previousPartId){ const pricingSummary = pricingItem; previousPart.totalPrice = pricingSummary.totalPrice; previousPart.pricePerPart = pricingSummary.pricePerPart; previousPart.tarifFlag = pricingSummary.tarifFlag; previousPart.totalStock = pricingSummary.totalStock; } } private _sortRows(rows, conf = { direction: 'asc', active: '' }): any[] { let result; let key; switch (conf.active) { case 'grade': key = 'grade'; break; case 'part number no cpn': key = 'partName'; break; case 'manufacturer': key = 'manufacturer'; break; case 'difference': key = 'difference'; break; case 'match-type': key = 'matchType'; break; case 'lifecycle': key = 'partStatus'; break; case 'price right': key = 'priceRanges'; break; case 'total': key = 'totalPrice'; break; case 'tariff': key = 'hasTariff'; break; case 'stock right': key = 'stock'; break; default: return rows; } result = rows.sort((a, b) => { let gradeA; let gradeB; switch (key) { case 'stock': gradeA = a.item[key] ? a.item[key] : 0; gradeB = b.item[key] ? b.item[key] : 0; break; case 'priceRanges': gradeA = a.item.priceRanges.length ? this._getMin(a.item.priceRanges).rangePrice : 0; gradeB = b.item.priceRanges.length ? this._getMin(b.item.priceRanges).rangePrice : 0; break; case 'totalPrice': gradeA = a.item[key] ? a.item[key] : 0; gradeB = b.item[key] ? b.item[key] : 0; break; case 'hasTariff': gradeA = a.item[key] ? 'Yes' : 'No'; gradeB = b.item[key] ? 'Yes' : 'No'; break; default: gradeA = a.item[key] ? a.item[key].toUpperCase() : 'Z'; gradeB = b.item[key] ? b.item[key].toUpperCase() : 'Z'; break; } if (gradeA < gradeB) { return -1; } if (gradeA > gradeB) { return 1; } return 0; }); return conf.direction === 'desc' ? result.reverse() : result; } _initialSortRow(rows): any[] { let result; result = rows.sort(function(a, b) { const gradeA = a.grade ? a.grade : 'Z'; const gradeB = b.grade ? b.grade : 'Z'; const stockA = a.stock; const stockB = b.stock; if (gradeA > gradeB) { return 1; } else if (gradeA === gradeB) { if (stockA > stockB) { return -1; } else if (stockA === stockB) { return 0; } else { return 1; } } else { return -1; } }); return result; } private _filtersSetup(items: any[]) { const gradeSet = []; const matchSet = []; const manufacturerSet = {}; items.forEach(i => { // Grade if (i.grade && gradeSet.indexOf(i.grade) === -1) { gradeSet.push(i.grade); } // Match Type if (i.matchType && matchSet.indexOf(i.matchType) === -1) { matchSet.push(i.matchType); } // Manufacturer if (manufacturerSet[i.manufacturer]) { manufacturerSet[i.manufacturer].count++; } else { manufacturerSet[i.manufacturer] = { name: i.manufacturer, count: 1 }; } }); if (gradeSet.length > 1) { this.filterGroups.push({ name: this.translate.instant('grade'), key: 'grade', options: Array.from(gradeSet).map(grade => ({ value: this._makeKey(grade), viewValue: grade })) }); } if (matchSet.length > 1) { this.filterGroups.push({ name: this.translate.instant('match-type'), key: 'match', options: Array.from(matchSet).map(matchType => ({ value: this._makeKey(matchType), viewValue: matchType })) }); } if (Object.keys(manufacturerSet).length > 1) { this.filterGroups.push({ name: this.translate.instant('manufacturer'), key: 'manufacturer', options: Object.keys(manufacturerSet).map(mfr => ({ value: this._makeKey(mfr), viewValue: `${mfr} (${manufacturerSet[mfr].count})` })) }); } } private _makeKey(value: string): string { return value .trim() .replace(' ', '-') .toLowerCase(); } private _getMin = function(array) { if (array.length > 1) { return array.reduce(function(prev, curr) { return prev.rangePrice < curr.rangePrice ? prev : curr; }); } else { return array.length ? array[0] : 0; } }; }