src/app/xlmap/xlmap.component.ts

Index

Properties

Properties

COL_NO
COL_NO: number
Type : number
LOAD_REF
LOAD_REF: string
Type : string
ROW_NO
ROW_NO: number
Type : number
VALUE_TXT
VALUE_TXT: string
Type : string
XLMAP_ID
XLMAP_ID: string
Type : string
XLMAP_RANGE_ID
XLMAP_RANGE_ID: string
Type : string
import {
  AfterContentInit,
  AfterViewInit,
  Component,
  ElementRef,
  HostBinding,
  OnInit,
  QueryList,
  signal,
  ViewChildren,
  ViewEncapsulation
} 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'
import Handsontable from 'handsontable'
import { UploadFileResponse } from '../models/UploadFile'

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'],
  encapsulation: ViewEncapsulation.None,
  standalone: false
})
export class XLMapComponent implements AfterContentInit, AfterViewInit, OnInit {
  @HostBinding('class.content-container') contentContainerClass = true
  @ViewChildren('fileUploadInput')
  fileUploadInputCompList: QueryList<ElementRef> = 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

  // Stable settings reference so the HOT v17 wrapper doesn't re-run
  // updateSettings on every change-detection tick (the previous getter returned a
  // new object each call, causing an infinite render loop / freeze). Rebuilt only
  // when the selected tab, or the license, changes (see setSelectedTab + ngOnInit).
  hotTableSettings = signal<Handsontable.GridSettings>(this.buildHotSettings())

  private buildHotSettings(): Handsontable.GridSettings {
    return {
      multiColumnSorting: true,
      viewportRowRenderingOffset: 50,
      colHeaders:
        this.selectedTab === this.TabsEnum.Rules
          ? this.xlmapRulesHeaders
          : this.xlUploadHeader,
      columns:
        this.selectedTab === this.TabsEnum.Rules
          ? this.xlmapRulesColumns
          : this.xlUploadColumns,
      filters: true,
      height: '100%',
      stretchH: 'all',
      afterGetColHeader: this.afterGetColHeader,
      modifyColWidth: this.maxWidthChecker,
      cells: this.getCellConfiguration,
      maxRows: this.hotTableMaxRows,
      manualColumnResize: true,
      rowHeaders: this.rowHeaders,
      rowHeaderWidth: 15,
      rowHeights: 20,
      licenseKey: this.hotTableLicenseKey,
      className: 'htDark',
      theme: 'ht-theme-classic'
    }
  }

  // Single place that changes the active tab so the settings signal stays in sync.
  private setSelectedTab(tab: Tabs) {
    this.selectedTab = tab
    this.hotTableSettings.set(this.buildHotSettings())
  }

  constructor(
    private eventService: EventService,
    private licenceService: LicenceService,
    private loggerService: LoggerService,
    private route: ActivatedRoute,
    private router: Router,
    private sasStoreService: SasStoreService,
    private sasService: SasService
  ) {}

  public afterGetColHeader(column: number, th: any) {
    // Dark mode
    th.classList.add(globals.handsontable.darkTableHeaderClass)
  }

  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.setSelectedTab(Tabs.Rules)
      this.viewXLMapRules()
      this.router.navigateByUrl('/home/excel-maps/' + 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 "<b>' +
        this.filename +
        '</b>". Please upload excel file.'
      this.eventService.showAbortModal(null, abortMsg)
    }
  }

  public discardExtractedData() {
    this.isLoading = false
    this.isLoadingDesc = ''
    this.status = Status.ReadyToUpload
    this.xlData = []
    this.setSelectedTab(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: UploadFileResponse) => {
        if (res.adapterResponse.sasjsAbort) {
          const abortRes = res.adapterResponse
          const abortMsg = abortRes.sasjsAbort[0].MSG
          const macMsg = abortRes.sasjsAbort[0].MAC

          this.eventService.showAbortModal('', abortMsg, {
            SYSWARNINGTEXT: abortRes.SYSWARNINGTEXT,
            SYSERRORTEXT: abortRes.SYSERRORTEXT,
            MAC: macMsg,
            _PROGRAM: abortRes._PROGRAM
          })
        } else if (res.adapterResponse.sasparams) {
          const params = res.adapterResponse.sasparams[0]
          const tableId = params.DSID
          this.router.navigateByUrl('/stage/' + tableId)
        }
      })
      .catch((err: any) => {
        this.eventService.catchResponseError('file upload', err.response)
      })
      .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 = <any[]>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 = <any[]>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.setSelectedTab(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
        this.hotTableSettings.set(this.buildHotSettings())
      }
    )
  }

  ngAfterViewInit() {
    return
  }

  ngAfterContentInit(): void {
    if (globals.editor.startupSet) {
      this.load()
    } else {
      this.eventService.onStartupDataLoaded.subscribe(() => {
        this.load()
      })
    }
  }
}

results matching ""

    No results matching ""