import { Component, Input, OnInit, Inject, ViewChildren, QueryList, AfterViewInit, ElementRef, OnDestroy, NgZone } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { DownloadService, AnalyticsService, AnalyticsEvent, ApiSocketEmitAction, ApiSocketListenAction } from '@arrow/bom/core'; import { Modal, ModalService, Utils, multiEmailValidator, BomItemService, DialogService, SocketsService, UserService } from '@arrow/bom/core'; import { LoggerService } from '@arrow/bom/logger'; import { WPT_DIALOG_DATA } from '@arrow/warpaint/dialog'; import { Subject } from 'rxjs'; import { take, takeUntil } from 'rxjs/operators'; import { cloneDeep } from 'lodash'; import { DownloadSettingsPayload, SettingsService } from '@arrow/bom/shared'; const DW_COL_ID = 32; const EMAIL_ERROR_STATUS = 'error'; const EMAIL_OK_STATUS = 'ok'; enum FileRequestType{ INVALID = 0, REGULAR = 1, EMAIL = 2 } interface FileGeneratedResponse{ fileId: string; requestType: FileRequestType; hasError: boolean; } @Component({ selector: 'bom-download', templateUrl: 'download-bom.component.html', styleUrls: [ '../../../core/src/style/modal.scss', './download-bom.component.scss' ] }) export class DownloadBomComponent extends Modal implements OnInit, AfterViewInit, OnDestroy { @ViewChildren('checkboxList') _checkboxLists: QueryList; @Input() content: string; invalid: boolean = false; export: Array; cols: Array; backupCols: Array; form: FormGroup; emailForm: FormGroup; selectToggle: boolean; selectToggleCustom: boolean; downloading: boolean = false; downloadedOrSent: boolean = false; emailing: boolean = false; emailStatus: string = ''; downloadStatus: string = ''; includeCrosses: boolean = false; saveSelection: boolean = false; isIndeterminate: boolean; isIndeterminateCustom: boolean; totalDefault: number; totalSelectedDefaultOptional: number; totalOptional: number; totalDefaultOptional: number; totalCustom: number; totalSelectedCustom: number; openEmail: boolean = false; displayWDWMessage: boolean = false; private _killSig$$ = new Subject(); constructor( @Inject(WPT_DIALOG_DATA) public data: any, public userService: UserService, public analyticsService: AnalyticsService, private _modalService: ModalService, private _userSettingsService: SettingsService, private _dialog: DialogService, private _downloadService: DownloadService, private _utils: Utils, private _bomItemService: BomItemService, private _loggerService: LoggerService, private _sockets: SocketsService, private _zone: NgZone ) { super(_modalService); } ngOnInit(): void { this.cols = this.data.bomData.exportableColumns.sort(this._compare); this.backupCols = cloneDeep(this.cols); this.totalDefault = this._countColumnType(this.cols, 1); this.totalOptional = this._countColumnType(this.cols, 2); this.totalCustom = this._countColumnType(this.cols, 3); this.totalDefaultOptional = this.totalDefault + this.totalOptional; this.totalSelectedCustom = this._countSelectedUser(this.cols, 3); this.totalSelectedDefaultOptional = this._countSelectedUser(this.cols, 1) + this._countSelectedUser(this.cols, 2); this.includeCrosses = this.data.userSettings.includeCrosses; this.selectToggle = this.totalDefaultOptional === this.totalSelectedDefaultOptional ? true : false; this.selectToggleCustom = this.totalCustom === this.totalSelectedCustom ? true : false; this.isIndeterminate = this._isPartialSelected( this.totalDefaultOptional, this.totalSelectedDefaultOptional ); this.isIndeterminateCustom = this._isPartialSelected( this.totalCustom, this.totalSelectedCustom ); // Create control name, can't use name as it sometimes has spaces this.cols = this.cols.map(col => { col['controlName'] = this._makeNameSafe(col.name); return col; }); this.form = this._createFormGroup(this.cols); this.export = this.createExportData(this.form); // sub to formGroup changes this.form.valueChanges.subscribe(() => { this.export = this.createExportData(this.form); this.invalid = !this.export.length; }); // Create email form group this.emailForm = new FormGroup({ recipient: new FormControl('', [ Validators.required, multiEmailValidator(this._utils.emailRegex) ]), sender: new FormControl(), message: new FormControl() }); if (!this.userService.isAnonymous) { this._setUsersEmailToField(this.userService.email || ''); } this._sockets .observeAction(ApiSocketListenAction.DOWNLOAD_FILE_GENERATED) .pipe(takeUntil(this._killSig$$)) .subscribe((response: FileGeneratedResponse) => { this._zone.run(() => { this._processGeneratedFile(response); }); }); } ngOnDestroy() { this._killSig$$.next(); this._killSig$$.complete(); this.restoreCols(); } ngAfterViewInit(): void { const fieldSetHeightPx = 29; let maxHeightPx = 0; this._checkboxLists.forEach((elRef: ElementRef) => { switch (elRef.nativeElement.id) { case 'default-columns': maxHeightPx = (fieldSetHeightPx * this.totalDefault) / 4; break; case 'optional-columns': maxHeightPx = (fieldSetHeightPx * this.totalOptional) / 4; break; case 'custom-columns': maxHeightPx = (fieldSetHeightPx * this.totalCustom) / 4; break; } elRef.nativeElement.style.maxHeight = maxHeightPx + 'px'; }); } /** * If user is logged in, this sets the sender input field value to their user name * @param email */ private _setUsersEmailToField(email: string): void { if (this.emailForm.controls['sender'].pristine) { this.emailForm.controls['sender'].setValue(email); } } /** * Used to create safe names for control names * @param name */ private _makeNameSafe(name: string): string { const regexp = /([_+-.,!@#$%^&*();\/|<>"' ])/g; return name.replace(regexp, ''); } /** * This is the compare function for the col array sorting. * http://stackoverflow.com/questions/1129216 */ private _compare(a: any, b: any): number { if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; return 0; } /** * Create form group of all bom columns * @param columnName */ private _createFormGroup(columnName: Array): FormGroup { const columns: any = {}; columnName.forEach( col => (columns[col.controlName] = new FormControl(true)) ); return new FormGroup(columns); } /** * Calculates number of default or custom rows * @param totalColumns * @param columnType */ private _countColumnType(totalColumns: Array, columnType: number) { return totalColumns.filter(function(x) { return x.columnType === columnType && x.name !== 'Part Number'; }).length; } /** * Calculates number of rows previously selected by the user * @param totalColumns * @param columnType */ private _countSelectedUser(totalColumns: Array, columnType: number) { return totalColumns.filter(function(x) { return ( x.columnType === columnType && x.isUserSelected === true && x.name !== 'Part Number' ); }).length; } /** * Checks if checkboxes are partially selected */ private _isPartialSelected(total: number, totalSelected: number) { if (totalSelected < total && totalSelected > 0) { return true; } else { return false; } } private _processGeneratedFile(fileResponse: FileGeneratedResponse): void { if(fileResponse.requestType === FileRequestType.REGULAR && !fileResponse.hasError) { this._downloadService .downloadGeneratedBomFile(fileResponse.fileId) .pipe(take(1)) .subscribe(() => { this.downloading = false; }); } else if(fileResponse.requestType === FileRequestType.EMAIL) { this.emailing = false; if(fileResponse.hasError) { this.emailStatus = EMAIL_ERROR_STATUS; } else { this.emailStatus = EMAIL_OK_STATUS; } } if(fileResponse.hasError) { this.downloading = false; this.emailing = false; this._dialog.alert('Error', 'Error during file request'); } } /** * Toggles all column checkboxes to be checked/unchecked * @param state */ toggleSelectAll(isDefaultSection: boolean): void { if (isDefaultSection) { this.selectToggle = !this.selectToggle; this.isIndeterminate = false; this.totalSelectedDefaultOptional = this.selectToggle ? this.totalDefaultOptional : 0; this.cols.forEach((col: any) => { if (col.columnType < 3 && col.controlName !== 'PartNumber') { this.form.controls[col.controlName].setValue(this.selectToggle); } }); } else { this.selectToggleCustom = !this.selectToggleCustom; this.isIndeterminateCustom = false; this.totalSelectedCustom = this.selectToggleCustom ? this.totalCustom : 0; this.cols.forEach((col: any) => { if (col.columnType === 3) { this.form.controls[col.controlName].setValue(this.selectToggleCustom); } }); } } /** * Updates number of checkboxes selected * @param state * @param type */ updateTotalSelected( state: boolean, type: string, columnName: string, colId?: number ): void { //check if DWE col is selected colId === DW_COL_ID && state === true ? (this.displayWDWMessage = true) : (this.displayWDWMessage = false); if (type === 'default' && !state) { this.analyticsService.googleTrackEvent(AnalyticsEvent.CLICK, { category: 'BOM', action: 'DL Default unchecked', label: columnName }); } if (type === 'optional' && state) { this.analyticsService.googleTrackEvent(AnalyticsEvent.CLICK, { category: 'BOM', action: 'DL Additional selected', label: columnName }); } if (type === 'default' || type === 'optional') { this.totalSelectedDefaultOptional = state ? this.totalSelectedDefaultOptional + 1 : this.totalSelectedDefaultOptional - 1; this.isIndeterminate = this._isPartialSelected( this.totalDefaultOptional, this.totalSelectedDefaultOptional ); this.selectToggle = !this.isIndeterminate && this.totalSelectedDefaultOptional !== 0 ? true : false; } else { this.totalSelectedCustom = state ? this.totalSelectedCustom + 1 : this.totalSelectedCustom - 1; this.isIndeterminateCustom = this._isPartialSelected( this.totalCustom, this.totalSelectedCustom ); this.selectToggleCustom = !this.isIndeterminateCustom && this.totalSelectedCustom !== 0 ? true : false; } } /** * Creates an array of checked columns * @param form * @returns Array of checked column data */ createExportData(form: FormGroup): Array { return this.cols.filter((col: any) => form.controls[col.controlName].value); } /** * Show email form so user can email BOM * @param Email form HTML element. * @param el */ toggleShowEmailSection(event: Event): void { event.preventDefault(); this.openEmail = !this.openEmail; } /** * Extracted selected columns. */ setExportColumnData(): Array { return this.export.map((col: any) => { return { colId: col.colId }; }); } /** * Prepare export column data. */ confirm(): void { this.downloading = true; this.downloadedOrSent = true; this.analyticsService.googleTrackEvent(AnalyticsEvent.CLICK, { category: 'BOM', action: 'View BOM Export', label: this.data.bomData.id }); if (this.includeCrosses) { const label = this.includeCrosses ? 'Yes' : 'No'; this.analyticsService.googleTrackEvent(AnalyticsEvent.CLICK, { category: 'BOM', action: 'Crosses download', label: label }); } const exportData = this.setExportColumnData(); if (this.saveSelection) { this.saveUserDownloadSettings( this.data.bomData.id, exportData.map(el => el.colId), this.includeCrosses ); } this.downloadBom(exportData, this.includeCrosses); } requestEmailBom(bomData: any, emailForm: FormGroup): void{ this.emailing = true; const colIds = this.setExportColumnData().map(el => el.colId); // DO NOT REMOVE if we want to add the save preferences to the send email action just remove the comments // this.downloadedOrSent = true; // if (this.saveSelection) { // this.saveUserDownloadSettings( // this.data.bomData.id, // colIds, // this.includeCrosses // ); // } if (this.includeCrosses) { const evenData = { category: 'BOM', action: 'Crosses download', label: this.includeCrosses ? 'Yes' : 'No' }; this.analyticsService.googleTrackEvent(AnalyticsEvent.CLICK, evenData); } if (emailForm.value.recipient) { const recipientCount = emailForm.value.recipient.split(',').length; this.analyticsService.googleTrackEvent(AnalyticsEvent.CLICK, { category: 'BOM', action: 'Email', label: recipientCount }); } const emailModel = { ...emailForm.value, columnIds: colIds, includeCrosses: this.includeCrosses }; const targetEmails = emailModel.recipient.split(',').map(email => email.trim()) const senderEmail = emailModel.sender; const optionalMessage = emailModel.message; const rowIds = this._bomItemService.getSelection().rowIds; const body = { emailRequest:{ bomId: bomData.id, colIds, rowIds, includeCrosses: this.includeCrosses, targetEmails, senderEmail, optionalMessage } } this._sockets.emitAction(ApiSocketEmitAction.DOWNLOAD_REQUEST, body); } /** * Prepare export columns to be exported. */ sendEmail(bomData: any, emailForm: FormGroup): void { if (this.includeCrosses) { const evenData = { category: 'BOM', action: 'Crosses download', label: this.includeCrosses ? 'Yes' : 'No' }; this.analyticsService.googleTrackEvent(AnalyticsEvent.CLICK, evenData); } if (emailForm.value.recipient) { const recipientCount = emailForm.value.recipient.split(',').length; this.analyticsService.googleTrackEvent(AnalyticsEvent.CLICK, { category: 'BOM', action: 'Email', label: recipientCount }); } this.requestEmailBom(bomData, emailForm); } downloadBom(cols: Array, includeCrosses: Boolean): void { this.downloadBomRequest(cols, includeCrosses); } downloadBomRequest(cols: Array, includeCrosses: Boolean): void{ const rowIds = this._bomItemService.getSelection().rowIds; const body = { regularRequest:{ bomId: this.data.bomData.id, colIds: cols.map(el => el.colId), rowIds, includeCrosses } } this._sockets.emitAction(ApiSocketEmitAction.DOWNLOAD_REQUEST, body); } saveUserDownloadSettings( bomId: string, columns: Array, includeCrosses: Boolean ): void { this.analyticsService.googleTrackEvent(AnalyticsEvent.CLICK, { category: 'BOM', action: 'Save preference', label: 'Saved' }); const payload = Object.assign(new DownloadSettingsPayload(), { bomId: bomId, includeCrosses: includeCrosses, columnsSelected: columns }); this._userSettingsService.saveDownloadSettings(payload).subscribe( (response) => { this.downloading = false; this.data.userSettings.includeCrosses = this.includeCrosses = response.includeCrosses; }, error => { this.downloading = false; this._loggerService.error('error saving user settings' + error); } ); } // restore the preferences on download modal if the uset close the modal // without having the save preferences checked // and without downloading the bom // to save preferences must check the save and download the bom restoreCols() { if (!this.saveSelection || (this.saveSelection && !this.downloadedOrSent)) { this.cols.forEach((col, index) => { col.isUserSelected = this.backupCols[index].isUserSelected; }); } } }