import { AfterContentInit, AfterViewInit, Component, ElementRef, HostBinding, OnInit, QueryList, ViewChildren } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { UploadFile } from '@sasjs/adapter' import * as XLSX from '@sheet/crypto' import { XLMapListItem, globals } from '../_globals' import { FileUploader } from '../models/FileUploader.class' import { EventService, LicenceService, LoggerService, SasService, SasStoreService } from '../services' import { getCellAddress, getFinishingCell } from './utils/xl.utils' import { blobToFile, byteArrayToBinaryString } from './utils/file.utils' interface XLMapRule { XLMAP_ID: string XLMAP_SHEET: string XLMAP_RANGE_ID: string XLMAP_START: string XLMAP_FINISH: string } interface XLUploadEntry { LOAD_REF: string XLMAP_ID: string XLMAP_RANGE_ID: string ROW_NO: number COL_NO: number VALUE_TXT: string } enum Status { NoMapSelected, FetchingRules, ReadyToUpload, ExtractingData, ReadyToSubmit, SubmittingExtractedData, Submitting } enum Tabs { Rules, Data } @Component({ selector: 'app-xlmap', templateUrl: './xlmap.component.html', styleUrls: ['./xlmap.component.scss'] }) export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit { @HostBinding('class.content-container') contentContainerClass = true @ViewChildren('fileUploadInput') fileUploadInputCompList: QueryList = new QueryList() StatusEnum = Status TabsEnum = Tabs public selectedTab = Tabs.Rules public rulesSource = globals.dcLib + '.MPE_XLMAP_RULES' public xlmaps: XLMapListItem[] = [] public selectedXLMap: XLMapListItem | undefined = undefined public searchString = '' public xlmapsLoading = true public isLoading = false public isLoadingDesc = '' public status = Status.NoMapSelected public xlmapRulesHeaders = [ 'XLMAP_SHEET', 'XLMAP_RANGE_ID', 'XLMAP_START', 'XLMAP_FINISH' ] public xlmapRulesColumns = [ { data: 'XLMAP_SHEET' }, { data: 'XLMAP_RANGE_ID' }, { data: 'XLMAP_START' }, { data: 'XLMAP_FINISH' } ] public xlmapRules: XLMapRule[] = [] public xlUploadHeader = ['XLMAP_RANGE_ID', 'ROW_NO', 'COL_NO', 'VALUE_TXT'] public xlUploadColumns = [ { data: 'XLMAP_RANGE_ID' }, { data: 'ROW_NO' }, { data: 'COL_NO' }, { data: 'VALUE_TXT' } ] public xlData: XLUploadEntry[] = [] public showUploadModal = false public hasBaseDropZoneOver = false public filename = '' public submitLimitNotice = false public uploader: FileUploader = new FileUploader() public licenceState = this.licenceService.licenceState public hotTableLicenseKey: string | undefined = undefined public hotTableMaxRows = this.licenceState.value.viewer_rows_allowed || Infinity constructor( private eventService: EventService, private licenceService: LicenceService, private loggerService: LoggerService, private route: ActivatedRoute, private router: Router, private sasStoreService: SasStoreService, private sasService: SasService ) {} public xlmapOnClick(xlmap: XLMapListItem) { if (xlmap.id !== this.selectedXLMap?.id) { this.selectedXLMap = xlmap this.xlData = [] this.filename = '' this.uploader.queue = [] if (this.fileUploadInputCompList.first) { this.fileUploadInputCompList.first.nativeElement.value = '' } this.selectedTab = Tabs.Rules this.viewXLMapRules() this.router.navigateByUrl('/home/files/' + xlmap.id) } } public xlmapListOnFilter() { if (this.searchString.length > 0) { const array: XLMapListItem[] = globals.xlmaps this.xlmaps = array.filter((item) => item.id.toLowerCase().includes(this.searchString.toLowerCase()) ) } else { this.xlmaps = globals.xlmaps } } public isActiveXLMap(id: string) { return this.selectedXLMap?.id === id } public maxWidthChecker(width: any, col: any) { if (width > 200) return 200 else return width } public getCellConfiguration() { return { readOnly: true } } public rowHeaders() { return ' ' } public onShowUploadModal() { this.showUploadModal = true } /** * Called by FileDropDirective * @param e true if file is dragged over the drop zone */ public fileOverBase(e: boolean): void { this.hasBaseDropZoneOver = e } public getFileDesc(event: any, dropped = false) { const file = dropped ? event[0] : event.target.files[0] if (!file) return const filename = file.name this.filename = filename const fileType = filename.slice( filename.lastIndexOf('.') + 1, filename.lastIndexOf('.') + 4 ) if (fileType.toLowerCase() === 'xls') { this.showUploadModal = false this.isLoading = true this.isLoadingDesc = 'Extracting Data' this.status = Status.ExtractingData const reader = new FileReader() reader.onload = async (theFile: any) => { /* read workbook */ const bstr = byteArrayToBinaryString(theFile.target.result) let wb: XLSX.WorkBook | undefined = undefined const xlsxOptions: XLSX.ParsingOptions = { type: 'binary', cellDates: false, cellFormula: true, cellStyles: true, cellNF: false, cellText: false } try { wb = XLSX.read(bstr, { ...xlsxOptions }) } catch (err: any) { this.eventService.showAbortModal( null, err, undefined, 'Error reading file' ) } if (!wb) { this.isLoading = false this.isLoadingDesc = '' this.status = Status.ReadyToUpload this.uploader.queue.pop() return } this.extractData(wb) return } reader.readAsArrayBuffer(file) } else { this.isLoading = false this.isLoadingDesc = '' this.status = Status.ReadyToUpload this.showUploadModal = true this.uploader.queue.pop() const abortMsg = 'Invalid file type "' + this.filename + '". Please upload excel file.' this.eventService.showAbortModal(null, abortMsg) } } public discardExtractedData() { this.isLoading = false this.isLoadingDesc = '' this.status = Status.ReadyToUpload this.xlData = [] this.selectedTab = Tabs.Rules this.filename = '' this.uploader.queue = [] if (this.fileUploadInputCompList.first) { this.fileUploadInputCompList.first.nativeElement.value = '' } } /** * Submits attached excel file that is in preview mode */ public submitExcel() { if (this.licenceState.value.submit_rows_limit !== Infinity) { this.submitLimitNotice = true return } this.submit() } public submit() { if (!this.selectedXLMap || !this.xlData.length) return this.status = Status.Submitting this.isLoading = true this.isLoadingDesc = 'Submitting extracted data' const filesToUpload: UploadFile[] = [] for (const file of this.uploader.queue) { filesToUpload.push({ file: file, fileName: file.name }) } const csvContent = Object.keys(this.xlData[0]).join(',') + '\n' + this.xlData .slice(0, this.licenceState.value.submit_rows_limit) .map((row: any) => Object.values(row).join(',')) .join('\n') const blob = new Blob([csvContent], { type: 'application/csv' }) const file: File = blobToFile(blob, this.filename + '.csv') filesToUpload.push({ file: file, fileName: file.name }) const uploadUrl = 'services/editors/loadfile' this.sasService .uploadFile(uploadUrl, filesToUpload, { table: this.selectedXLMap.targetDS }) .then((res: any) => { if (res.sasjsAbort) { const abortRes = res const abortMsg = abortRes.sasjsAbort[0].MSG const macMsg = abortRes.sasjsAbort[0].MAC this.eventService.showAbortModal('', abortMsg, { SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT, SYSERRORTEXT: abortRes.SYSERRORTEXT, MAC: macMsg }) } else if (res.sasparams) { const params = res.sasparams[0] const tableId = params.DSID this.router.navigateByUrl('/stage/' + tableId) } }) .catch((err: any) => { this.eventService.catchResponseError('file upload', err) }) .finally(() => { this.status = Status.ReadyToSubmit this.isLoading = false this.isLoadingDesc = '' }) } public extractData(wb: XLSX.WorkBook) { const extractedData: XLUploadEntry[] = [] this.xlmapRules.forEach((rule) => { let sheetName = rule.XLMAP_SHEET // if sheet name is not an absolute name rather an index string like /1, /2, etc // we extract the index and find absolute sheet name for specified index if (sheetName.startsWith('/')) { const temp = sheetName.split('/')[1] const sheetIndex = parseInt(temp) - 1 sheetName = wb.SheetNames[sheetIndex] } const sheet = wb.Sheets[sheetName] const arrayOfObjects = XLSX.utils.sheet_to_json(sheet, { raw: true, header: 'A', blankrows: true }) const start = getCellAddress(rule.XLMAP_START, arrayOfObjects) const finish = getFinishingCell(start, rule.XLMAP_FINISH, arrayOfObjects) const a1Range = `${start}:${finish}` const range = XLSX.utils.decode_range(a1Range) const rangedData = XLSX.utils.sheet_to_json(sheet, { raw: true, range: a1Range, header: 'A', blankrows: true }) for (let i = 0; i < rangedData.length; i++) { const row = rangedData[i] // `range.s.c` is the index of first column in the range // `range.e.c` is the index of last column in the range // we'll iterate from first column to last column and // extract value where defined and push to extracted data array for (let j = range.s.c, x = 0; j <= range.e.c; j++, x++) { const col = XLSX.utils.encode_col(j) if (col in row) { // in excel's R1C1 notation indexing starts from 1 but in JS it starts from 0 // therefore, we'll have to add 1 to rows and cols extractedData.push({ LOAD_REF: '0', XLMAP_ID: rule.XLMAP_ID, XLMAP_RANGE_ID: rule.XLMAP_RANGE_ID, ROW_NO: i + 1, COL_NO: x + 1, VALUE_TXT: row[col] }) } } } }) this.status = Status.ReadyToSubmit this.isLoading = false this.isLoadingDesc = '' this.xlData = extractedData this.selectedTab = Tabs.Data } async viewXLMapRules() { if (!this.selectedXLMap) return this.isLoading = true this.isLoadingDesc = 'Loading excel rules' this.status = Status.FetchingRules await this.sasStoreService .getXLMapRules(this.selectedXLMap.id) .then((res) => { this.xlmapRules = res.xlmaprules this.status = Status.ReadyToUpload }) .catch((err) => { this.loggerService.error(err) }) this.isLoading = false this.isLoadingDesc = '' } private load() { this.xlmaps = globals.xlmaps this.xlmapsLoading = false const id = this.route.snapshot.params['id'] if (id) { const xlmapListItem = this.xlmaps.find((item) => item.id === id) if (xlmapListItem) { this.selectedXLMap = xlmapListItem this.viewXLMapRules() } } } ngOnInit() { this.licenceService.hot_license_key.subscribe( (hot_license_key: string | undefined) => { this.hotTableLicenseKey = hot_license_key } ) } ngAfterViewInit() { return } ngAfterContentInit(): void { if (globals.editor.startupSet) { this.load() } else { this.eventService.onStartupDataLoaded.subscribe(() => { this.load() }) } } }