import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { ConfigService } from '@arrow/bom/config'; import { Subject, Observable } from 'rxjs'; // import { Row } from '@arrow/bom/list'; import { take } from 'rxjs/operators'; import { DialogService } from './dialog/dialog.service'; export class ItemSelection { selectionInverted: boolean; rowIds: string[]; } @Injectable() export class BomItemService { //event sources private _deleteItemClickSource = new Subject(); private _newItemAddedSource = new Subject(); private _toggledAlternatesSource = new Subject(); private _itemLoadingSource = new Subject(); private _hasUnallocatedSource = new Subject(); private _rowInfoUpdatedSource = new Subject(); private _rowPricingUpdatedSource = new Subject(); private _selectAllToggleSource = new Subject(); private _selectionChangedSource = new Subject(); private _userChangedSelectionSource = new Subject(); private _indexShiftSource = new Subject<{ startIndex: number; shiftAmount: number; }>(); private _selectionIds: string[] = []; private _selectionInverted: boolean = false; private _allSelected: boolean; deleteItemClick$ = this._deleteItemClickSource.asObservable(); newItemAdded$ = this._newItemAddedSource.asObservable(); rowInfoUpdated$ = this._rowInfoUpdatedSource.asObservable(); rowPricingUpdated$ = this._rowPricingUpdatedSource.asObservable(); selectAllToggle$ = this._selectAllToggleSource.asObservable(); selectionChanged$ = this._selectionChangedSource.asObservable(); userChangedSelection$ = this._userChangedSelectionSource.asObservable(); indexShifted$ = this._indexShiftSource.asObservable(); get allSelected() { return this._allSelected; } set allSelected(value: boolean) { this._allSelected = value; } constructor( private http: HttpClient, private config: ConfigService, private dialogService: DialogService ) {} /** * Remove an item from selection when deleted if it was there * @param id */ removeFromSelections(id: string) { if (this._selectionInverted) return; const idx = this._selectionIds.findIndex(rowId => rowId === id); if (idx) this._selectionIds.slice(idx, 1); } /** * Event triggered when the delete row button icon is clicked (=> open modal) * @param bom The Bom Model * @param item - item to be deleted */ deleteItemClicked(bom: any, item?: any) { this.dialogService .confirm( 'Delete', 'Please note that once you apply filters, search for a part or leave this page you will not be able to undo this change.', { buttons: { confirm: 'DELETE', cancel: 'CANCEL' } } ) .subscribe((response: boolean) => { if (response && item) this.deleteConfirmed(item, bom.id); if (response && !item) this.deleteMultipleConfirmed(bom); }); } /** * display delete confirmation modal * @param item * @param bomId */ deleteConfirmed(item: any, bomId: string) { if (item) { item.isLoading = true; this.setItemLoading(item.rowId, true); this.saveDeleteStatus(bomId, [item.rowId], true) .pipe(take(1)) .subscribe(response => { if (response?.status?.statusCode === 200) { item.isDeleted = true; // Timeout is needed to ensure UI is refreshed setTimeout(() => { const undoPartDeleteButton = document.getElementById("undo-delete-" + item.rowId); if (undoPartDeleteButton) undoPartDeleteButton.focus(); }, 1000); } else { // TODO: handle api errors if needed } }); this._selectionChangedSource.next(this.getSelection()); } } /** * delete multiple parts * @param bom */ deleteMultipleConfirmed(bom: any) { this.saveDeleteStatus(bom.id) .pipe(take(1)) .subscribe(() => { var deletedBomRows = bom.rows.filter(row => { const rowId = this._selectionIds.find(id => { return id === row.rowId; }); return this._selectionInverted ? !rowId : rowId; }); deletedBomRows.forEach(row => (row.isDeleted = true)); if (!this._selectionInverted) this._selectionIds = []; this._selectionChangedSource.next(this.getSelection()); // Timeout is needed to ensure UI is refreshed setTimeout(() => { const undoPartDeleteButton = document.getElementById("undo-delete-" + deletedBomRows[0].rowId); if (undoPartDeleteButton) undoPartDeleteButton.focus(); }, 1000); }); } /** * Triggered when a NEW row is being appended to the BOM */ newItemAdded() { this._newItemAddedSource.next(); } /** * Event used to communicate between list item and its children with information about opening/closing * the list of alternates * @param rowId - item.rowId * @param hiddenAlts - new alternates toggle state */ toggleAlternates(rowId: string, hiddenAlts: boolean) { this._toggledAlternatesSource.next({ rowId, hiddenAlts }); } /** * Event used to communicate between list item and its children with information about the item's loading state * @param rowId - item.rowId * @param isLoading - new item loading state */ setItemLoading(rowId: string, isLoading: boolean) { this._itemLoadingSource.next({ rowId, isLoading }); } /** * Used to notify any subscribers that the row's "info" properties have just been updated * @param rowId */ notifyRowInfoUpdated(rowId: string): void { this._rowInfoUpdatedSource.next(rowId); } /** * Used to notify any subscribers that the row's "pricing" properties have just been updated * @param rowId */ notifyRowPricingUpdated(rowId: string): void { this._rowPricingUpdatedSource.next(rowId); } /** * update allocated bo * @param rowId * @param hasUnallocated */ updateUnallocatedBuyingOptions(rowId: string, hasUnallocated: boolean) { this._hasUnallocatedSource.next({ rowId, hasUnallocated }); } /** * @param rows * @param state */ toggleRowsSelection(rows: any[], state?: boolean): any[] { if (state !== undefined && state !== this.allSelected) { this.allSelected = state; this._selectionInverted = state; this._selectionIds = []; this._selectionChangedSource.next(this.getSelection()); return rows.map((row: any) => ({ ...row, selected: state })); } else { return rows.map((row: any): any => { const selected = this._selectionIds.find((id: string) => id === row.id) !== undefined; return { ...row, selected: this._selectionInverted ? !selected : selected }; }); } } /** * Adds or remove parts from the selection array * @param rowId * @param status * @param bomRowCount Total bom rows */ updateSelection(rowId: string, status: boolean, bomRowCount: number) { if (this._selectionInverted) { if (status) { this._selectionIds = this._selectionIds.filter(id => id !== rowId); } else { this._selectionIds = [...this._selectionIds, rowId]; } } else { if (status) { this._selectionIds = [...this._selectionIds, rowId]; } else { this._selectionIds = this._selectionIds.filter(id => id !== rowId); } } this.allSelected = (this._selectionInverted && !this._selectionIds.length) || (!this._selectionInverted && this._selectionIds.length === bomRowCount); this._selectionChangedSource.next(this.getSelection()); } /** * Return the current selection status */ getSelection(): ItemSelection { return { selectionInverted: this._selectionInverted, rowIds: this._selectionIds.slice() }; } resetSelection() { this._selectionInverted = false; this._allSelected = false; this._selectionIds = []; } /** * Return selected price index * @param itemQty number The requested quantity * @param priceRanges any[] The price rages * @returns number The index */ getPriceSelectedIndex(itemQty: number, priceRanges: any[]): number { let position: number = 0; if (!priceRanges) position = 0; priceRanges.forEach((priceRange: any, index: number) => { const nextPriceRange = priceRanges[index + 1]; const isCurrentPriceRange = priceRange.rangeLimit <= itemQty && ((nextPriceRange && nextPriceRange.rangeLimit > itemQty) || !nextPriceRange); if (isCurrentPriceRange) position = index; }); return position; } /** * determinate if all parts are selected or is partial selection * @param bomTotalParts */ isPartialSelection(bomTotalParts: number): boolean { if (this._selectionInverted) return true; if ( !!this._selectionIds.length && this._selectionIds.length < bomTotalParts ) return true; return false; } /** * Delete rows * @param bomId * @param rowIds * @param isDelete */ public saveDeleteStatus( bomId: string, rowIds?: string[], isDelete?: boolean ): Observable { let payload = {}; if (rowIds) { payload = { rowIds: rowIds, selectionInverted: false, isDelete: isDelete }; } else { payload = { rowIds: this._selectionIds, selectionInverted: this._selectionInverted, isDelete: true }; } return this.http.patch( `${ this.config.apiUrlWithoutVersion }/bom/api/bom/${bomId}/rows?clientid=${this.config.getSocketsClientId()}`, payload, { withCredentials: true } ); } }