import '@things-factory/form-ui'
import '@things-factory/grist-ui'
import '@things-factory/import-ui'
import '../components/popup-note'
import './inventory-history-by-pallet'
import '../inventory/serial-number-list-popup'
import '../components/select-product-popup'

import gql from 'graphql-tag'
import { css, html } from 'lit-element'
import { connect } from 'pwa-helpers/connect-mixin'

import { DirectPrinter } from '@things-factory/barcode-base'
import { i18next, localize } from '@things-factory/i18n-base'
import { openPopup } from '@things-factory/layout-base'
import { client, CustomAlert, PageView, store } from '@things-factory/shell'
import { CommonButtonStyles, ScrollbarStyles } from '@things-factory/styles'
import { isMobileDevice } from '@things-factory/utils'

import { fetchSettingRule } from '../../fetch-setting-value'
import { fetchPageSettings } from '../../util/index'
import {
  ADJUSTMENT_CODE,
  CARTON_LABEL_SETTING_KEY,
  CONDITION_OF_GOODS,
  INVENTORY_ITEM_CHANGE_TYPE,
  PALLET_LABEL_SETTING_KEY
} from '../constants'

// Excel/import often provides numbers as strings ("0", "1.5", "").
// GraphQL Float rejects NaN, so normalize invalid/blank to undefined.
const toFloatOrUndefined = v => {
  if (v === null || v === undefined) return undefined
  if (typeof v === 'string' && v.trim() === '') return undefined
  const n = Number(v)
  return Number.isFinite(n) ? n : undefined
}

// Import UI may produce human headers (e.g. "packing type") instead of GraphQL input keys (e.g. "packingType").
// GraphQL rejects unknown fields, so normalize/sanitize patches before submitting.
const INVENTORY_PATCH_KEYS = new Set([
  'id',
  'name',
  'bizplace',
  'refInventory',
  'bizplaceName',
  'palletId',
  'cartonId',
  'batchId',
  'batchIdRef',
  'product',
  'productDetail',
  'sku',
  'brand',
  'productName',
  'productDetailName',
  'productDetailId',
  'reusablePallet',
  'reusablePalletId',
  'location',
  'locationName',
  'warehouse',
  'warehouseName',
  'packingType',
  'packingSize',
  'otherRef',
  'transactionType',
  'details',
  'zone',
  'weight',
  'uom',
  'uomValue',
  'qty',
  'status',
  'description',
  'remark',
  'expirationDate',
  'manufactureDate',
  'unitCost',
  'adjustmentCode',
  'adjustmentNote',
  'inventoryItemChangesJson',
  'serialNumbers',
  'updatedAt',
  'updater',
  'cuFlag',
  'transferQty',
  'transferUomValue',
  'obsolete',
  'conditionOfGoods'
])

const toCamelKey = key =>
  String(key)
    .trim()
    .replace(/[_-]+/g, ' ')
    .replace(/\s+([a-zA-Z0-9])/g, (_, c) => String(c).toUpperCase())
    .replace(/^\w/, c => c.toLowerCase())

const normalizeInventoryPatch = patch => {
  if (!patch || typeof patch !== 'object') return patch

  const headerKeyMap = {
    'packing type': 'packingType',
    'packing size': 'packingSize',
    'uom value': 'uomValue',
    'unit cost': 'unitCost',
    'batch id': 'batchId',
    'batch no': 'batchId',
    'batch no.': 'batchId',
    'batch id ref': 'batchIdRef',
    'batch no ref': 'batchIdRef',
    'batch no. ref': 'batchIdRef'
  }

  const normalized = {}

  Object.entries(patch).forEach(([rawKey, value]) => {
    const lower = String(rawKey).trim().toLowerCase()
    let key = headerKeyMap[lower] || rawKey

    if (!INVENTORY_PATCH_KEYS.has(key)) {
      const camel = toCamelKey(rawKey)
      if (INVENTORY_PATCH_KEYS.has(camel)) key = camel
    }

    if (!INVENTORY_PATCH_KEYS.has(key)) return

    // prefer already-correct keys if both exist
    if (normalized[key] === undefined) normalized[key] = value
  })

  // sanitize nested refs to what GraphQL expects
  if (normalized.bizplace && normalized.bizplace.id) normalized.bizplace = { id: normalized.bizplace.id }
  if (normalized.location && normalized.location.id) {
    normalized.location = { id: normalized.location.id, name: normalized.location.name }
  }
  if (normalized.product && normalized.product.id) normalized.product = { id: normalized.product.id }
  if (normalized.productDetail && normalized.productDetail.id)
    normalized.productDetail = { id: normalized.productDetail.id }

  // ScalarDate cannot parse empty string; treat blank as undefined.
  if (normalized.manufactureDate === '') delete normalized.manufactureDate

  return normalized
}

class InventoryAdjustment extends connect(store)(localize(i18next)(PageView)) {
  static get styles() {
    return [
      ScrollbarStyles,
      css`
        :host {
          display: flex;
          flex-direction: column;
          overflow: hidden;
        }

        search-form {
          overflow: visible;
        }

        data-grist {
          overflow-y: auto;
          flex: 1;
        }
      `
    ]
  }

  static get properties() {
    return {
      _searchFields: Array,
      config: Object,
      _palletLabel: Object,
      _cartonLabel: Object,
      _inventory: Object,
      _data: Object,
      _strictProductSelection: Boolean,
      _partnerDomainId: String
    }
  }

  get dataGrist() {
    return this.shadowRoot.querySelector('data-grist')
  }

  get searchForm() {
    return this.shadowRoot.querySelector('search-form')
  }

  get columns() {
    return this.config.columns
  }

  get context() {
    return {
      title: i18next.t('title.inventory_adjustment'),
      actions: [
        {
          title: i18next.t('button.lot_label_print'),
          action: this.printLotLabel.bind(this),
          ...CommonButtonStyles.label
        },
        {
          title: i18next.t('button.carton_label_print'),
          action: this.printCartonLabel.bind(this),
          ...CommonButtonStyles.label
        },
        {
          title: i18next.t('button.missing_stock'),
          action: this.missingStock.bind(this),
          ...CommonButtonStyles.save
        },
        {
          title: i18next.t('button.save'),
          action: this.saveInventories.bind(this),
          ...CommonButtonStyles.save
        }
      ],
      printable: {
        accept: ['preview'],
        content: this
      },
      exportable: {
        name: i18next.t('title.inventory_adjustment'),
        data: this.exportableData.bind(this)
      },
      importable: {
        handler: this.importableData.bind(this)
      },
      help: 'operato-wms/inventory/inventory-adjustment'
    }
  }

  stateChanged(state) {
    let palletLabelSetting = state.dashboard[PALLET_LABEL_SETTING_KEY]
    let cartonLabelSetting = state.dashboard[CARTON_LABEL_SETTING_KEY]
    this._palletLabel = (palletLabelSetting && palletLabelSetting.board) || {}
    this._cartonLabel = (cartonLabelSetting && cartonLabelSetting.board) || {}
  }

  pageUpdated(changes, lifecycle) {
    if (this.active) {
      this.dataGrist.fetch()
    }
  }

  render() {
    return html`
      <search-form id="search-form" .fields=${this._searchFields} @submit=${e => this.dataGrist.fetch()}></search-form>

      <data-grist
        .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
        .config=${this.config}
        .fetchHandler="${this.fetchHandler.bind(this)}"
        @record-change="${this.customerChange.bind(this)}"
        @field-change="${this.fieldChange.bind(this)}"
      ></data-grist>
    `
  }

  constructor() {
    super()
    this._partnerDomainId = ''
    this._strictProductSelection = false
  }

  async pageInitialized() {
    this._configureInventoryGrist()
    this._strictProductSelection = await fetchSettingRule('strict-product-selection', this._partnerDomainId)
  }

  async _configureInventoryGrist() {
    //fetch all page setting in 1 action
    let pageSettings = await fetchPageSettings({
      codes: [{ name: 'name', operator: 'in', value: ['PACKING_TYPES', 'UOM'] }],
      partners: []
    })
    let [partners, packingType, uom] = [
      pageSettings.partners || [],
      (packingType = pageSettings.codes?.find(x => x.name == 'PACKING_TYPES').settingDetails || []),
      (uom = pageSettings.codes?.find(x => x.name == 'UOM').settingDetails || [])
    ]
    let strictEditable = !this._strictProductSelection

    this._searchFields = [
      {
        label: i18next.t('field.company'),
        name: 'bizplace',
        type: 'select',
        options: [
          { value: '' },
          ...partners
            .map(partner => {
              return {
                name: `${partner.name} ${partner.description ? `(${partner.description})` : ''}`,
                value: partner.id
              }
            })
            .sort(this.compareValues('name', 'asc'))
        ],
        handlers: {
          change: () => {
            this.searchForm.submit()
          }
        },
        props: { searchOper: 'eq' }
      },
      {
        label: i18next.t('field.product_info'),
        name: 'productInfo',
        type: 'text',
        props: { searchOper: 'eq' }
      },
      {
        label: i18next.t('field.batch_no'),
        name: 'batchId',
        type: 'text',
        props: { searchOper: 'i_like' }
      },
      {
        label: i18next.t('field.batch_no_ref'),
        name: 'batchIdRef',
        type: 'text',
        props: { searchOper: 'i_like' }
      },
      {
        label: i18next.t('field.lot_id'),
        name: 'palletId',
        type: 'text',
        props: { searchOper: 'i_like' }
      },
      {
        label: i18next.t('field.carton_id'),
        name: 'cartonId',
        type: 'text',
        props: { searchOper: 'i_like' }
      },
      {
        label: i18next.t('field.packing_type'),
        name: 'packingType',
        type: 'select',
        options: [
          { value: '' },
          ...packingType.map(packingType => {
            return {
              name: packingType.name,
              value: packingType.name
            }
          })
        ],
        props: { searchOper: 'eq' }
      },
      {
        label: i18next.t('field.location'),
        name: 'location',
        type: 'object',
        queryName: 'locations',
        field: 'name'
      }
    ]

    this.config = {
      list: {
        fields: [
          'palletId',
          'cartonId',
          'batchId',
          'product',
          'bizplace',
          'packingType',
          'qty',
          'location',
          'expirationDate'
        ]
      },
      rows: {
        handlers: { click: this.setProductRefCondition.bind(this) },
        selectable: { multiple: true }
      },
      pagination: { limit: 50, pages: [50, 100, 200, 500] },
      columns: [
        { type: 'gutter', gutterName: 'sequence' },
        { type: 'gutter', gutterName: 'row-selector', multiple: true },
        {
          type: 'gutter',
          gutterName: 'button',
          icon: 'reorder',
          handlers: { click: this.showInventoryMovement.bind(this) }
        },
        { type: 'string', name: 'inventoryItemChanges', hidden: true },
        { type: 'string', name: 'productName', hidden: true },
        {
          type: 'string',
          name: 'palletId',
          header: i18next.t('field.lot_id'),
          imex: { header: i18next.t('field.lot_id'), key: 'palletId', width: 25, type: 'string' },
          sortable: true,
          width: 150
        },
        {
          type: 'string',
          name: 'cartonId',
          header: i18next.t('field.carton_id'),
          imex: { header: i18next.t('field.carton_id'), key: 'cartonId', width: 25, type: 'string' },
          sortable: true,
          width: 150
        },
        {
          type: 'string',
          name: 'batchId',
          header: i18next.t('field.batch_no'),
          record: { editable: true },
          imex: { header: i18next.t('field.*_batch_no'), key: 'batchId', width: 30, type: 'string' },
          sortable: true,
          width: 120
        },
        {
          type: 'object',
          name: 'bizplace',
          header: i18next.t('field.company'),
          record: {
            editable: true,
            options: { queryName: 'partnersBizplaces' }
          },
          imex: {
            header: i18next.t('field.*_company'),
            key: 'bizplace.name',
            width: 50,
            type: 'array',
            arrData: []
          },
          sortable: true,
          width: 300
        },
        {
          type: 'string',
          name: 'sku',
          header: i18next.t('field.sku'),
          imex: { header: i18next.t('field.*_sku'), key: 'sku', width: 30, type: 'string' },
          sortable: true,
          width: 150
        },
        {
          type: 'object',
          name: 'product',
          header: i18next.t('field.product'),
          record: {
            editable: true,
            options: {
              queryName: 'productDetails',
              basicArgs: {
                filters: [{ name: 'bizplace_id', operator: 'eq', value: '' }]
              },
              select: [
                { name: 'id', hidden: true },
                { name: 'productId', hidden: true },
                { name: 'sku', header: i18next.t('field.sku'), width: 150 },
                { name: 'name', header: i18next.t('field.name'), width: 250 },
                { name: 'brand', header: i18next.t('field.brand'), width: 150 },
                { name: 'type', header: i18next.t('field.type'), width: 200 },
                { name: 'packingType', header: i18next.t('field.packing_type'), width: 200 },
                { name: 'packingSize', header: i18next.t('field.packing_size'), width: 150 },
                { name: 'uom', header: i18next.t('field.uom'), width: 150 },
                { name: 'costPrice', header: i18next.t('field.unit_cost'), width: 150 },
                {
                  name: 'isRequireSerialNumberScanningInbound',
                  header: i18next.t('field.require_serial_number_scanning_inbound'),
                  width: 150
                },
                {
                  name: 'isRequireSerialNumberScanningOutbound',
                  header: i18next.t('field.require_serial_number_scanning_outbound'),
                  width: 150
                }
              ]
            }
          },
          imex: {
            header: i18next.t('field.product'),
            key: 'productName',
            width: 150,
            type: 'string'
          },
          sortable: true,
          width: 500
        },
        {
          type: 'string',
          name: 'brand',
          header: i18next.t('field.brand'),
          width: 120
        },
        {
          type: 'code',
          name: 'packingType',
          header: i18next.t('field.packing_type'),
          imex: {
            header: i18next.t('field.packing_type'),
            key: 'packingType',
            width: 50,
            type: 'array',
            arrData: [{ name: '' }, ...packingType]
          },
          record: { editable: strictEditable, codeName: 'PACKING_TYPES' },
          width: 150
        },
        {
          type: 'number',
          name: 'packingSize',
          header: i18next.t('field.packing_size'),
          record: { editable: strictEditable },
          imex: {
            header: i18next.t('field.packing_size'),
            key: 'packingSize',
            width: 50,
            type: 'string'
          },
          width: 150
        },
        {
          type: 'float',
          name: 'qty',
          header: i18next.t('field.qty'),
          record: { editable: true },
          sortable: true,
          imex: {
            header: i18next.t('field.*_qty'),
            key: 'qty',
            width: 10,
            type: 'float'
          },
          width: 80
        },
        {
          type: 'float',
          name: 'transferQty',
          header: i18next.t('field.transfer_qty'),
          sortable: true,
          imex: {
            header: i18next.t('field.transfer_qty'),
            key: 'transferQty',
            width: 10,
            type: 'float'
          },
          width: 80
        },
        {
          type: 'float',
          name: 'transferUomValue',
          header: i18next.t('field.transfer_uom_value'),
          sortable: true,
          imex: {
            header: i18next.t('field.transfer_uom_value'),
            key: 'transferUomValue',
            width: 10,
            type: 'float'
          },
          width: 80
        },
        {
          type: 'string',
          name: 'serialNumbers',
          header: i18next.t('field.serial_number'),
          hidden: true,
          imex: { header: i18next.t('field.serial_number'), key: 'serialNumbers', width: 100, type: 'string' },
          width: 100
        },
        {
          type: 'number',
          name: 'inventoryItemCount',
          header: i18next.t('field.serial_number'),
          sortable: false,
          handlers: {
            click: (columns, data, column, record, rowIndex) => {
              if (record.qty > 0) this.openSerialNumberPopup(record, rowIndex, data.records)
            }
          },
          width: 80
        },
        {
          type: 'number',
          name: 'lockedQty',
          header: i18next.t('field.release_qty'),
          sortable: false,
          width: 80
        },
        {
          type: 'number',
          name: 'remainQty',
          header: i18next.t('field.remain_qty'),
          sortable: false,
          width: 80
        },
        {
          type: 'float',
          name: 'uomValue',
          header: i18next.t('field.total_uom_value'),
          record: { editable: true },
          sortable: true,
          imex: {
            header: i18next.t('field.*_total_uom_value'),
            key: 'uomValue',
            width: 10,
            type: 'float'
          },
          width: 80
        },
        {
          type: 'code',
          name: 'uom',
          header: i18next.t('field.uom'),
          record: { editable: strictEditable, codeName: 'UOM' },
          imex: {
            header: i18next.t('field.uom'),
            key: 'uom',
            width: 25,
            type: 'array',
            arrData: [{ name: '' }, ...uom]
          },
          sortable: true,
          width: 80
        },
        {
          type: 'object',
          name: 'location',
          header: i18next.t('field.location'),
          record: {
            editable: true,
            options: {
              queryName: 'locations',
              select: [
                { name: 'id', hidden: true },
                { name: 'description', hidden: true },
                { name: 'name', header: i18next.t('field.name'), sortable: true },
                { name: 'zone', header: i18next.t('field.zone'), sortable: true },
                { name: 'row', header: i18next.t('field.row'), sortable: true },
                { name: 'column', header: i18next.t('field.column'), sortable: true }
              ],
              list: { fields: ['name', 'zone', 'row', 'column'] }
            }
          },
          imex: {
            header: i18next.t('field.*_location'),
            key: 'location.name',
            width: 15,
            type: 'array',
            arrData: []
          },
          sortable: true,
          width: 120
        },
        {
          type: 'select',
          name: 'conditionOfGoods',
          header: i18next.t('field.inventory_quality'),
          record: {
            editable: true,
            options: [
              { display: '', value: '' },
              ...Object.values(CONDITION_OF_GOODS).map(c => ({
                display: i18next.t(`label.${c.name}`),
                value: c.value
              }))
            ]
          },
          imex: {
            header: i18next.t('field.inventory_quality'),
            key: 'conditionOfGoods',
            width: 20,
            type: 'array',
            arrData: Object.values(CONDITION_OF_GOODS).map(c => {
              return { name: c.value }
            })
          },
          sortable: true,
          width: 120
        },
        {
          type: 'boolean',
          name: 'obsolete',
          header: i18next.t('field.obsolete'),
          record: { editable: false },
          width: 80
        },
        {
          type: 'date',
          name: 'expirationDate',
          header: i18next.t('field.exp_date'),
          imex: { header: i18next.t('field.exp_date'), key: 'expirationDate', width: 30, type: 'date' },
          record: { editable: true },
          sortable: true,
          width: 150
        },
        {
          type: 'date',
          name: 'manufactureDate',
          header: i18next.t('field.manufacture_date'),
          imex: { header: i18next.t('field.manufacture_date'), key: 'manufactureDate', width: 40, type: 'date' },
          record: { editable: true },
          sortable: true,
          width: 120
        },
        {
          type: 'string',
          name: 'remark',
          header: i18next.t('field.remark'),
          sortable: true,
          width: 200,
          handlers: {
            click: this.openRemarkNote.bind(this)
          }
        },
        {
          type: 'string',
          name: 'batchIdRef',
          header: i18next.t('field.batch_no_ref'),
          record: { editable: true },
          imex: { header: i18next.t('field.batch_no_ref'), key: 'batchIdRef', width: 30, type: 'string' },
          sortable: true,
          width: 120
        },
        {
          type: 'float',
          name: 'unitCost',
          header: i18next.t('field.unit_cost'),
          record: { editable: true },
          sortable: true,
          imex: {
            header: i18next.t('field.unit_cost'),
            key: 'unitCost',
            width: 10,
            type: 'float'
          },
          width: 80
        },
        {
          type: 'select',
          name: 'adjustmentCode',
          header: i18next.t('field.adjustment_code'),
          record: {
            editable: true,
            options: [
              '',
              ...Object.values(ADJUSTMENT_CODE)
                .map(ac => ac.value)
                .sort()
            ]
          },
          imex: {
            header: i18next.t('field.*_adjustment_code'),
            key: 'adjustmentCode',
            width: 30,
            type: 'array',
            arrData: Object.values(ADJUSTMENT_CODE).map(ac => {
              return {
                name: ac.name
              }
            })
          },
          sortable: true,
          width: 200
        },
        {
          type: 'string',
          name: 'adjustmentNote',
          header: i18next.t('field.adjustment_note'),
          imex: { header: i18next.t('field.*_adjustment_note'), key: 'adjustmentNote', width: 30, type: 'string' },
          record: { editable: true },
          sortable: true,
          width: 200
        },
        {
          type: 'datetime',
          name: 'updatedAt',
          header: i18next.t('field.updated_at'),
          sortable: true,
          width: 150
        },
        {
          type: 'object',
          name: 'updater',
          header: i18next.t('field.updater'),
          sortable: true,
          width: 150
        }
      ]
    }
  }

  // Combined export and grist fetch in single function
  async fetchHandler({ page, limit, sorters = [] }, exportItem = false) {
    const filters = await this.searchForm.getQueryFilters()

    const query =
      exportItem == false
        ? gql`
            query inventories(
              $filters: [Filter!]
              $pagination: Pagination
              $sortings: [Sorting!]
              $exportItem: Boolean
            ) {
              inventories(filters: $filters, pagination: $pagination, sortings: $sortings, exportItem: $exportItem) {
                items {
                  id
                  palletId
                  cartonId
                  batchId
                  batchIdRef
                  packingType
                  packingSize
                  uom
                  uomValue
                  manufactureDate
                  manufactureYear
                  serialNumbers
                  obsolete
                  transferQty
                  transferUomValue
                  bizplace {
                    id
                    name
                    description
                  }
                  partnerDomain {
                    id
                  }
                  product {
                    id
                    name
                    sku
                    productInformation
                    brand
                    auxUnit1
                    auxValue1
                    description
                    type
                    isRequireSerialNumberScanningOutbound
                    isRequireSerialNumberScanningInbound
                    isInventoryDecimal
                    costPrice
                  }
                  productDetail {
                    id
                    name
                    costPrice
                  }
                  qty
                  lockedQty
                  warehouse {
                    id
                    name
                    description
                  }
                  zone
                  location {
                    id
                    name
                    description
                  }
                  createdAt
                  updatedAt
                  updater {
                    name
                    description
                  }
                  remark
                  expirationDate
                  unitCost
                  inventoryItemCount
                  purchaseOrderNo
                  orderProductRemark
                  refOrderId
                  arrivalNoticeRefNo
                  arrivalNoticeNo
                  adjustmentCode
                  adjustmentNote
                  conditionOfGoods
                }
                total
              }
            }
          `
        : gql`
            query inventories(
              $filters: [Filter!]!
              $pagination: Pagination
              $sortings: [Sorting!]!
              $exportItem: Boolean
            ) {
              inventories(filters: $filters, pagination: $pagination, sortings: $sortings, exportItem: $exportItem) {
                items {
                  id
                  palletId
                  cartonId
                  batchId
                  batchIdRef
                  packingType
                  packingSize
                  uom
                  uomValue
                  remark
                  expirationDate
                  manufactureDate
                  manufactureYear
                  unitCost
                  adjustmentCode
                  adjustmentNote
                  serialNumbers
                  obsolete
                  conditionOfGoods
                  refOrderId
                  arrivalNoticeRefNo
                  partnerDomain {
                    id
                  }
                  bizplace {
                    id
                    name
                    description
                  }
                  product {
                    id
                    sku
                    brand
                    name
                    description
                    productInformation
                    costPrice
                    isInventoryDecimal
                  }
                  productDetail {
                    id
                    name
                    costPrice
                  }
                  qty
                  lockedQty
                  warehouse {
                    id
                    name
                    description
                  }
                  zone
                  location {
                    id
                    name
                    description
                  }
                  updatedAt
                }
                total
              }
            }
          `

    const response = await client.query({
      query,
      variables: {
        filters: [...filters, { name: 'status', operator: 'eq', value: 'STORED' }],
        pagination: { page, limit },
        sortings: sorters,
        exportItem: exportItem
      }
    })

    response.data.inventories.items = response.data.inventories.items.map(item => {
      return {
        ...item,
        sku: item.product.sku,
        productName: item.product.name,
        brand: item.product.brand,
        unitCost: parseFloat(item.unitCost == null ? 0 : item.unitCost).toFixed(2),
        lockedQty: item.lockedQty || 0,
        remainQty: item.qty - (item.lockedQty || 0)
      }
    })

    this._partnerDomainId = response.data.inventories.items[0]?.partnerDomain.id

    if (exportItem == false) {
      return {
        total: response.data.inventories.total || 0,
        records: response.data.inventories.items || []
      }
    } else {
      return response.data.inventories.items || []
    }
  }

  setProductRefCondition(columns, data, column, record, rowIndex) {
    this.config.columns.map(column => {
      if (column.name === 'product') {
        if (record && record.bizplace && record.bizplace.id) {
          column.record.options.basicArgs = {
            filters: [{ name: 'bizplace_id', operator: 'eq', value: record.bizplace.id }]
          }
        }
      }
    })
  }

  customerChange(event) {
    const record = event.detail
    const columnName = event.detail.column.name

    if (
      columnName === 'bizplace' &&
      record.before?.bizplace?.id != record.after?.bizplace?.id &&
      this.dataGrist._data.records[record.row].lockedQty <= 0
    ) {
      delete this.dataGrist._data.records[event.detail.row].product
      delete this.dataGrist._data.records[event.detail.row].sku
      delete this.dataGrist._data.records[event.detail.row].brand
      delete this.dataGrist._data.records[event.detail.row].packingType
      delete this.dataGrist._data.records[event.detail.row].packingSize
      delete this.dataGrist._data.records[event.detail.row].uom
    }
  }

  fieldChange(e) {
    let popUpDataGrist = e.target?.shadowRoot.querySelector('data-grist')

    let item = e.target.localName == 'import-pop-up' ? popUpDataGrist : this.dataGrist

    if (
      item._data.records[e.detail.row].lockedQty > 0 &&
      (e.detail.column.name == 'batchId' || e.detail.column.name == 'product' || e.detail.column.name == 'bizplace')
    ) {
      this.showToast({ level: 'error', message: i18next.t('text.there_is_release_qty_no_amendment_is_allowed') })
      item._data.records[e.detail.row][e.detail.column.name] = e.detail.before
      return
    }

    switch (e.detail.column.name) {
      case 'batchId':
        if (e.detail.after == '' || e.detail.after == null || e.detail.after.toString() == NaN.toString()) {
          item._data.records[e.detail.row].batchId = e.detail.before
          this.showToast({ level: 'error', message: i18next.t('text.invalid_batch_no') })
        }
      case 'uomValue':
        if (e.detail.after < 0 || e.detail.after.toString() == NaN.toString()) {
          item._data.records[e.detail.row].uomValue = e.detail.before
          this.showToast({ level: 'error', message: i18next.t('text.invalid_uom_value') })
        }
        break
      case 'qty':
        if (e.detail.after < 0 || isNaN(e.detail.after) || !Number.isFinite(e.detail.after)) {
          item._data.records[e.detail.row].qty = e.detail.before
          this.showToast({ level: 'error', message: i18next.t('text.invalid_qty') })
        }
        if (!item._data.records[e.detail.row].product.isInventoryDecimal && e.detail.after % 1 != 0) {
          item._data.records[e.detail.row].qty = e.detail.before
          this.showToast({ level: 'error', message: i18next.t('text.decimal_quantities_not_allowed') })
        } else if (item._data.records[e.detail.row].product.isInventoryDecimal) {
          const decimalPlaces = e.detail.after.toString().split('.')[1]?.length || 0
          if (decimalPlaces > 3) {
            item._data.records[e.detail.row].qty = e.detail.before
            this.showToast({
              level: 'error',
              message: i18next.t('text.qty_cannot_exceed_three_decimal_places')
            })
          }
        }
        break
      case 'packingType':
        if (e.detail.after.trim() == '') {
          item._data.records[e.detail.row].packingType = e.detail.before
          this.showToast({ level: 'error', message: i18next.t('text.invalid_packing_type') })
        }
        break
      case 'packingSize':
        if (e.detail.after <= 0 || !Number.isInteger(e.detail.after)) {
          item._data.records[e.detail.row].packingSize = e.detail.before
          this.showToast({ level: 'error', message: i18next.t('text.invalid_packing_size') })
        }
        break
      case 'unitCost':
        if (e.detail.after < 0 || isNaN(e.detail.after)) {
          item._data.records[e.detail.row].unitCost = e.detail.before
          this.showToast({ level: 'error', message: i18next.t('text.invalid_unit_cost') })
        }
        break
      case 'expirationDate':
        if (new Date(e.detail.after) < new Date()) {
          this.showToast({ message: i18next.t('text.expiration_date_will_be_earlier_than_today') })
        }
        break
      case 'manufactureDate':
        if (new Date(e.detail.after) > new Date()) {
          this.showToast({ message: i18next.t('text.invalid_manufacture_date') })
        }
        break
      case 'adjustmentCode':
        if (e.detail.after == '' || e.detail.after == null || e.detail.after.toString() == NaN.toString()) {
          item._data.records[e.detail.row].adjustmentCode = e.detail.before
          this.showToast({ level: 'error', message: i18next.t('text.invalid_adjustment_code') })
        }
        break
      case 'adjustmentNote':
        if (e.detail.after == '' || e.detail.after == null || e.detail.after.toString() == NaN.toString()) {
          item._data.records[e.detail.row].adjustmentNote = e.detail.before
          this.showToast({ level: 'error', message: i18next.t('text.invalid_adjustment_note') })
        }
        break
      case 'product':
        item._data.records[e.detail.row].uom = e.detail.after?.uom ? e.detail.after.uom : ''
        item._data.records[e.detail.row].packingType = e.detail.after?.packingType ? e.detail.after.packingType : ''
        item._data.records[e.detail.row].packingSize = e.detail.after?.packingSize ? e.detail.after.packingSize : 1
        item._data.records[e.detail.row].brand = e.detail.after?.brand ? e.detail.after.brand : ''
        item._data.records[e.detail.row].sku = e.detail.after?.sku ? e.detail.after.sku : ''

        if (e.detail.before == undefined) {
          item._data.records[e.detail.row].unitCost =
            e.detail.after?.costPrice >= 0 ? e.detail.after.costPrice : undefined
        }
        break

      case 'bizplace':
        item._data.records[e.detail.row].uom = ''
        item._data.records[e.detail.row].packingType = ''
        item._data.records[e.detail.row].packingSize = 1
        item._data.records[e.detail.row].brand = ''
        item._data.records[e.detail.row].sku = ''
        item._data.records[e.detail.row].qty = e.detail.record.qty
        item._data.records[e.detail.row].uomValue = e.detail.record.uomValue
        break
      default:
        break
    }

    if (item._data.records[e.detail.row]?.inventoryItemChanges) {
      let inventoryItemChanges = item._data.records[e.detail.row]?.inventoryItemChanges
      item._data.records[e.detail.row].inventoryItemChanges = inventoryItemChanges
      item.dirtyData.records[e.detail.row].inventoryItemChanges = inventoryItemChanges
      item.dirtyData.records[e.detail.row].__dirtyfields__ = {
        ...item.dirtyData.records[e.detail.row].__dirtyfields__,
        inventoryItemChanges: { before: '', after: inventoryItemChanges }
      }
    }
  }

  async missingStock() {
    const selectOrderInventory = this.dataGrist.selected

    try {
      if (selectOrderInventory.length <= 0) {
        this.showToast({
          type: 'error',
          message: `${i18next.t('text.no_record_selected')}`
        })
        throw new Error(`${i18next.t('text.no_record_selected')}`)
      }

      const patches = selectOrderInventory.map(p => {
        return {
          palletId: p.palletId,
          transactionType: 'MISSING'
        }
      })

      const result = await CustomAlert({
        title: i18next.t('title.are_you_sure'),
        text: i18next.t('text.do_you_want_to_mark_it_as_missing'),
        confirmButton: { text: i18next.t('button.confirm') },
        cancelButton: { text: i18next.t('button.cancel') }
      })

      if (result.isConfirmed) {
        const response = await client.mutate({
          mutation: gql`
            mutation submitInventoryChanges($patches: [InventoryPatch!]!) {
              submitInventoryChanges(patches: $patches)
            }
          `,
          variables: { patches }
        })

        if (!response.error) {
          this.showToast({ message: i18next.t('text.data_inventory_adjustment_has_been_submitted_for_approval') })
          this.updateContext()
        }
      }
    } catch (error) {
      throw new Error(error)
    }
  }

  async saveInventories() {
    try {
      var patches = this.dataGrist.exportPatchList({ flagName: 'cuFlag' })
      patches = patches.map(row => {
        delete row.productUomValue
        delete row.inventoryItemCount

        if (new Date(row.expirationDate) < new Date()) {
          this.showToast({
            message: `${i18next.t('text.expiration_date_will_be_earlier_than_today')}`
          })
        }

        if (row.cuFlag == '+') {
          this.validation(row)

          if (!row.product || !row.uom || row?.uom.trim() == '') {
            throw new Error(i18next.t('text.empty_value_in_list'))
          }
        }

        if (row.cuFlag == 'M') {
          if (
            (row.hasOwnProperty('uom') && row.uom?.trim() == '') ||
            (row.hasOwnProperty('packingType') && row.packingType?.trim() == '') ||
            (row.hasOwnProperty('unitCost') && (row.unitCost == null || row.unitCost < 0)) ||
            (row.hasOwnProperty('packingSize') && (row.packingSize == null || row.packingSize <= 0)) ||
            (row.hasOwnProperty('adjustmentCode') && row.adjustmentCode?.trim() == '') ||
            (row.hasOwnProperty('adjustmentNote') && row.adjustmentNote?.trim() == '')
          ) {
            throw new Error(i18next.t('text.empty_value_in_list'))
          }

          let currentItem = this.dataGrist?.data?.records.find(itm => itm.id === row.id)
          //if user is not updating adjustment code or note, check if current data has them
          if (!row.hasOwnProperty('adjustmentNote')) {
            if (!currentItem.adjustmentNote) {
              throw new Error(
                i18next.t('text.adjustment_note_is_required_kindly_fill_in_the_adjustment_note_to_continue')
              )
            }
          }

          if (!row.hasOwnProperty('adjustmentCode')) {
            if (!currentItem.adjustmentCode) {
              throw new Error(
                i18next.t('text.adjustment_code_is_required_kindly_fill_in_the_adjustment_code_to_continue')
              )
            }
          }
        }

        row.bizplace = row.bizplace ? { id: row.bizplace.id } : undefined
        row.location = row.location ? { id: row.location.id } : undefined
        row.productDetailId = row.product ? row.product.id : undefined
        row.product = row.product ? { id: row.product.productId } : undefined
        row.uomValue = toFloatOrUndefined(row.uomValue)
        row.unitCost = toFloatOrUndefined(row.unitCost)
        row.packingSize = row.packingSize ? parseInt(row.packingSize) : undefined
        row.transferQty = toFloatOrUndefined(row.transferQty)
        row.transferUomValue = toFloatOrUndefined(row.transferUomValue)

        if (row?.batchId) row.batchId = row.batchId.trim()
        if (row?.batchIdRef) row.batchIdRef = row.batchIdRef.trim()

        if (row?.inventoryItemChanges) {
          row.inventoryItemChangesJson = row.inventoryItemChanges
          delete row.inventoryItemChanges
        }
        return row
      })

      if (patches.length == 0) {
        this.showToast({ message: i18next.t('text.nothing_updated') })
      }

      if (patches && patches.length) {
        const validateInventoryChanges = await client.query({
          query: gql`
            query validateOrderInventory($patches: [InventoryPatch!]!) {
              validateOrderInventory(patches: $patches)
            }
          `,
          variables: { patches }
        })

        if (validateInventoryChanges.data.validateOrderInventory) {
          const response = await client.mutate({
            mutation: gql`
              mutation submitInventoryChanges($patches: [InventoryPatch!]!) {
                submitInventoryChanges(patches: $patches)
              }
            `,
            variables: { patches }
          })

          if (!response.errors) {
            this.dataGrist.fetch()
            this.showToast({ message: i18next.t('text.data_inventory_adjustment_has_been_submitted_for_approval') })
          }
        } else {
          this.showToast({
            type: 'error',
            message: `${i18next.t('text.complete_the_picking_process_before_adjust_the_batch_number')}`
          })
        }
      }
    } catch (e) {
      this.showToast(e)
    }
  }

  async importHandler(patches) {
    try {
      patches = patches.map(patch => normalizeInventoryPatch(patch))

      const validAdjustmentCodes = new Set(
        Object.values(ADJUSTMENT_CODE).map(ac => ac.value)
      )

      // Fetch fresh locations for client-side validation (avoid stale cache)
      await this.fetchLocation()
      const locationNameSet = new Set((this._locations || []).map(l => l.name))

      patches.forEach(itm => {
        const errors = []
        delete itm.errMsg

        if (toFloatOrUndefined(itm?.uomValue) < 0) {
          errors.push(i18next.t('text.invalid_uom_value'))
        }
        if (toFloatOrUndefined(itm?.qty) < 0) {
          errors.push(i18next.t('text.invalid_qty'))
        }
        if (itm?.packingType?.trim() == '') {
          errors.push(i18next.t('text.invalid_packing_type'))
        }
        if (toFloatOrUndefined(itm?.unitCost) < 0) {
          errors.push(i18next.t('text.invalid_unit_cost'))
        }
        if (new Date(itm?.manufactureDate) > new Date()) {
          errors.push(i18next.t('text.invalid_manufacture_date'))
        }
        if (!itm?.adjustmentCode?.trim() || !validAdjustmentCodes.has(itm.adjustmentCode.trim())) {
          errors.push(i18next.t('text.adjustment_code_is_required_kindly_fill_in_the_adjustment_code_to_continue'))
        }
        if (itm?.adjustmentNote?.trim() == '') {
          errors.push(i18next.t('text.invalid_adjustment_note'))
        }

        // Validate location for all rows
        if (!itm?.location || !itm?.location?.name) {
          errors.push(i18next.t('text.location_is_required_kindly_fill_in_the_location_to_continue'))
        } else if (!locationNameSet.has(itm.location.name)) {
          errors.push(`${i18next.t('text.location_not_found')}: ${itm.location.name}`)
        }

        if (itm.cuFlag == '+') {
          if (!itm?.batchId) {
            errors.push(i18next.t('text.batch_no_is_required_kindly_fill_in_the_batch_no_to_continue'))
          }
          if (!itm?.bizplace || !itm?.bizplace?.id) {
            errors.push(i18next.t('text.company_is_required_kindly_fill_in_the_company_to_continue'))
          }
          if (!itm?.qty) {
            errors.push(i18next.t('text.qty_is_required_kindly_fill_in_the_qty_to_continue'))
          }
          if (!itm?.uomValue) {
            errors.push(i18next.t('text.total_uom_value_is_required_kindly_fill_in_the_total_uom_value_to_continue'))
          }
          if (!itm?.adjustmentCode) {
            // already checked above, but keep for cuFlag '+' specific check
          }
          if (!itm?.adjustmentNote) {
            errors.push(i18next.t('text.adjustment_note_is_required_kindly_fill_in_the_adjustment_note_to_continue'))
          }

          if (!itm.sku) {
            errors.push(i18next.t('text.sku_is_required_kindly_fill_in_the_sku_to_continue'))
          }

          if (itm.qty < 0 || itm.unitCost < 0 || itm.uomValue < 0) {
            errors.push(i18next.t('text.invalid_value_in_list'))
          }

          if (
            itm.manufactureDate &&
            (itm.manufactureDate.getFullYear < 1000 || itm.manufactureDate.getTime > new Date().getTime)
          ) {
            errors.push(i18next.t('text.invalid_value_in_list'))
          }
        }

        if (itm.serialNumbers) {
          let serialNumbers = itm.serialNumbers
          itm.serialNumbers = serialNumbers.replace(' ', '')

          if (itm.serialNumbers.split(',').length != itm.qty) {
            errors.push(i18next.t('text.serial_number_not_tally_with_qty'))
          }
        }

        if (errors.length) {
          itm.errMsg = errors.join('; ')
        }
      })

      const hasErrors = patches.some(p => p.errMsg)
      if (hasErrors) {
        return patches
      }

      // Save original patches for error display if the server mutation fails
      const originalPatches = patches.map(p => ({ ...p }))

      // All validations passed — apply transformations and submit
      patches.forEach(itm => {
        itm.qty = toFloatOrUndefined(itm.qty)
        itm.packingSize = toFloatOrUndefined(itm.packingSize)
        itm.uomValue = toFloatOrUndefined(itm.uomValue)
        itm.transferQty = toFloatOrUndefined(itm.transferQty)
        itm.transferUomValue = toFloatOrUndefined(itm.transferUomValue)

        if (itm.unitCost == null && itm.unitCost != 0) {
          itm.unitCost = null
        }
        if (itm.unitCost != null) {
          itm.unitCost = toFloatOrUndefined(itm.unitCost)
        }

        if (itm.location && (itm.location.zone || itm.location.row || itm.location.column)) {
          delete itm.location.zone
          delete itm.location.row
          delete itm.location.column
        }

        if (itm?.product?.productId) {
          itm.product.id = itm.product.productId
          delete itm.product.productId
          delete itm.product.brand
          delete itm.product.packingType
          delete itm.product.packingSize
          delete itm.product.sku
          delete itm.product.type
          delete itm.product.uom
        }

        if (itm.expirationDate) {
          let dt = new Date(itm.expirationDate)
          itm.expirationDate = `${dt.getFullYear()}-${dt.getMonth() + 1}-${dt.getDate()}`
        }

        if (itm.manufactureDate) {
          let mdt = new Date(itm.manufactureDate)
          itm.manufactureDate = `${mdt.getFullYear()}-${mdt.getMonth() + 1}-${mdt.getDate()}`
        }

        delete itm.errMsg
      })

      const response = await client.mutate({
        mutation: gql`
          mutation submitInventoryChanges($patches: [InventoryPatch!]!) {
            submitInventoryChanges(patches: $patches)
          }
        `,
        variables: { patches }
      })

      if (!response.errors) {
        this.dataGrist.fetch()
        this.showToast({ message: i18next.t('text.data_inventory_adjustment_has_been_submitted_for_approval') })
      } else {
        // Server returned errors — show in the error column on original patches
        const errMsg = response.errors.map(e => e.message).join('; ')
        originalPatches.forEach(p => {
          p.errMsg = errMsg
        })
        return originalPatches
      }
    } catch (e) {
      this.showToast(e)
    }
  }

  importableData(records) {
    records.map(record => {
      if (typeof record.unitCost == 'object') {
        record.unitCost = 0
      }
    })

    let columns = this.config.columns
      .filter(column => column.imex !== undefined)
      .map(column => {
        if (column.name == 'serialNumbers') {
          delete column.hidden
        }

        if (column.name == 'product') {
          column.name = 'productName'
          column.type = 'string'
        }

        return { ...column }
      })

    columns.push({
      type: 'string',
      name: 'errMsg',
      header: i18next.t('field.error_message'),
      width: 300,
      record: { editable: false }
    })

    setTimeout(() => {
      openPopup(
        html`
          <import-pop-up
            .records=${records}
            .config=${{
            rows: this.config.rows,
            columns: columns
          }}
            .enableRowErrors=${true}
            .importHandler="${this.importHandler.bind(this)}"
            @field-change="${this.fieldChange.bind(this)}"
          ></import-pop-up>
        `,
        {
          backdrop: true,
          size: 'large',
          title: i18next.t('title.import')
        }
      )
    }, 500)
  }

  async exportableData() {
    try {
      let data = []

      var headerSetting = [
        ...this.dataGrist._config.columns
          .filter(column => column.type !== 'gutter' && column.record !== undefined && column.imex !== undefined)
          .map(column => {
            return column.imex
          })
      ]

      const bizplaceFilters = (await this.searchForm.getQueryFilters()).filter(x => x.name === 'bizplace')

      if (bizplaceFilters.length == 0) {
        throw new Error(`Please select a customer for export.`)
      }

      if (this.dataGrist.selected && this.dataGrist.selected.length > 0) {
        data = this.dataGrist.selected
      } else {
        data = await this.fetchHandler({}, true)
      }

      let [products, bizplaces, locations] = await Promise.all([
        this.fetchProduct(bizplaceFilters),
        this.fetchBizplaces([{ ...bizplaceFilters[0], name: 'bizplace' }]),
        this.fetchLocation()
      ])

      headerSetting = headerSetting.map(column => {
        switch (column.key) {
          case 'bizplace.name':
            column.arrData = bizplaces
            break
          case 'location.name':
            column.arrData = locations
            break
          default:
            break
        }
        return column
      })

      data = data.map(item => {
        if (item?.expirationDate !== null && item?.expirationDate !== '') {
          item.expirationDate = this.toUtcTimestamp(item.expirationDate)
        }

        if (item?.manufactureDate !== null && item?.manufactureDate !== '') {
          item.manufactureDate = this.toUtcTimestamp(item.manufactureDate)
        }

        return {
          id: item.id,
          ...this.columns
            .filter(column => column.type !== 'gutter' && column.imex !== undefined)
            .reduce((record, column) => {
              record[column.imex.key] = column.imex.key.split('.').reduce((obj, key) => {
                return obj && obj[key] !== 'undefined' ? obj[key] : undefined
              }, item)

              return record
            }, {})
        }
      })

      return { header: headerSetting, data: data }
    } catch (e) {
      this.showToast(e)
    }
  }

  async fetchBizplaces(bizplace = []) {
    const response = await client.query({
      query: gql`
        query bizplaces($filters: [Filter!]!) {
          bizplaces(filters: $filters) {
            items {
              id
              name
              description
            }
          }
        }
      `,
      variables: { filters: [...bizplace] }
    })
    return response.data.bizplaces.items
  }

  async fetchProduct(bizplace = []) {
    try {
      bizplace[0].name = 'bizplace'
      const response = await client.query({
        query: gql`
          query pureProductsByBizplace($filters: [Filter!]!) {
            pureProductsByBizplace(filters: $filters) {
              items {
                id
                productInformation
              }
            }
          }
        `,
        variables: { filters: [...bizplace] }
      })
      return response.data.pureProductsByBizplace.items.map(itm => {
        return { id: itm.id, name: itm.productInformation }
      })
    } catch (ex) {
      throw ex
    }
  }

  async fetchLocation() {
    const response = await client.query({
      query: gql`
        query pureLocations($filters: [Filter!]!) {
          pureLocations(filters: $filters) {
            items {
              id
              name
            }
          }
        }
      `,
      variables: { filters: [] }
    })
    this._locations = response.data.pureLocations.items
    return response.data.pureLocations.items
  }

  async updateRemark(id, remark) {
    const response = await client.mutate({
      mutation: gql`
        mutation updateInventoryRemark($id: String!, $remark: String!) {
          updateInventoryRemark(id: $id, remark: $remark) {
            id
            remark
          }
        }
      `,
      variables: { id, remark }
    })
    if (!response.errors) {
      this.showToast({ message: i18next.t('text.remark_updated') })
    }
  }

  showInventoryMovement(columns, data, column, record, rowIndex) {
    openPopup(html` <inventory-history-by-pallet .palletId="${record.palletId}"></inventory-history-by-pallet> `, {
      backdrop: true,
      size: 'large',
      title: `${record.palletId} - History`
    })
  }

  openRemarkNote(columns, data, column, record, rowIndex) {
    openPopup(
      html`
        <popup-note
          .title="${i18next.t('title.inventory_remark')}"
          .value="${record.remark}"
          @submit="${async e => {
          await this.updateRemark(record.id, e.detail.value)

          this.dataGrist._data = {
            records: this.dataGrist._data.records.map(updateRecord => {
              if (updateRecord.id === record.id) updateRecord.remark = e.detail.value
              return updateRecord
            })
          }
        }}"
        ></popup-note>
      `,
      {
        backdrop: true,
        size: 'large',
        title: i18next.t('title.inventory_remark')
      }
    )
  }

  openSerialNumberPopup(inventory, rowIndex, records) {
    const removable =
      inventory?.product?.isRequireSerialNumberScanningOutbound &&
        inventory?.product?.isRequireSerialNumberScanningInbound
        ? true
        : false

    const updatable =
      inventory?.product?.isRequireSerialNumberScanningOutbound &&
        inventory?.product?.isRequireSerialNumberScanningInbound
        ? true
        : false

    openPopup(
      html`
        <serial-number-list-popup
          .title="${i18next.t('title.serial_number_list')}"
          .inventory=${inventory}
          .adjustmentType=${'ADJUSTMENT'}
          .enableRemove=${removable}
          .enableUpdateBtn=${updatable}
          @confirm-serial-number-list="${e => {
          let totalSerialNumber = 0
          let newRecords = []
          for (let i = 0; i < e.detail.inventoryItemChanges.length; i++) {
            let inventoryItemChange = e.detail.inventoryItemChanges[i]
            newRecords.push(inventoryItemChange)

            if (inventoryItemChange.type == INVENTORY_ITEM_CHANGE_TYPE.NEW.value) totalSerialNumber++

            if (inventoryItemChange.type == INVENTORY_ITEM_CHANGE_TYPE.REMOVED.value) totalSerialNumber--
          }

          let inventoryItemChangesJson = JSON.stringify(newRecords)
          let currentInventoryItemCount = records[rowIndex]?.inventoryItemCount || 0

          this.dataGrist.dirtyData.records[rowIndex].inventoryItemChanges = inventoryItemChangesJson
          this.dataGrist.dirtyData.records[rowIndex].inventoryItemCount =
            currentInventoryItemCount + totalSerialNumber
          if (!this.dataGrist?.dirtyData?.records[rowIndex]?.__dirtyfields__) {
            this.dataGrist.dirtyData.records[rowIndex].__dirtyfields__ = {
              inventoryItemChanges: { before: '', after: inventoryItemChangesJson }
            }
          } else {
            this.dataGrist.dirtyData.records[rowIndex].__dirtyfields__ = {
              ...this.dataGrist.dirtyData.records[rowIndex].__dirtyfields__,
              inventoryItemChanges: {
                before: '',
                after: inventoryItemChangesJson
              }
            }
          }
          this.dataGrist.dirtyData.records[rowIndex].__dirty__ = 'M'

          if (records[rowIndex]) {
            records[rowIndex].inventoryItemChanges = inventoryItemChangesJson
            records[rowIndex].inventoryItemCount = currentInventoryItemCount + totalSerialNumber
          }
        }}"
        ></serial-number-list-popup>
      `,
      {
        backdrop: false,
        size: 'large',
        title: i18next.t('title.serial_number_list')
      }
    )
  }

  async printLotLabel() {
    const records = this.dataGrist.selected
    let bulkRefNo = records.map(itm => itm.refOrderId)
    var labelId = this._palletLabel && this._palletLabel.id

    const filters = [
      {
        name: 'refOrderId',
        operator: 'eq',
        value: bulkRefNo
      }
    ]

    const response = await client.query({
      query: gql`
        query arrivalNotices($filters: [Filter!]) {
          arrivalNotices(filters: $filters) {
            items {
              id
              refNo
              refNo2
              refNo3
            }
            total
          }
        }
      `,
      variables: {
        filters
      }
    })

    const arrivalNotices = response.data.arrivalNotices.items
    const arrivalNoticesMap = {}

    arrivalNotices.forEach(itm => {
      arrivalNoticesMap[itm.id] = itm
    })

    records.forEach(record => {
      const matchingArrivalNotice = arrivalNoticesMap[record.refOrderId]
      if (matchingArrivalNotice) {
        record.arrivalNotice = {
          id: matchingArrivalNotice.id,
          refNo: matchingArrivalNotice.refNo,
          refNo2: matchingArrivalNotice.refNo2,
          refNo3: matchingArrivalNotice.refNo3
        }
      }
    })

    if (!labelId) {
      this.showToast({
        type: 'error',
        message: `${i18next.t('text.no_label_setting_was_found')}. ${i18next.t('text.please_check_your_setting')}`
      })
    } else {
      for (var record of records) {
        let dateIn = this.getDateStr(record.createdAt)
        var searchParams = new URLSearchParams()

        /* for pallet record mapping */
        searchParams.append('pallet', record.palletId)
        searchParams.append('batch', record.batchId)
        searchParams.append('product', record.product.name)
        searchParams.append('sku', record.product.sku)
        searchParams.append('auxUnit1', record.product.auxUnit1)
        searchParams.append('auxValue1', record.product.auxValue1)
        searchParams.append('type', record.product.type)
        searchParams.append('description', record.product.description)
        searchParams.append('customer', record.bizplace.name)
        searchParams.append('packing', record.packingType)
        searchParams.append('packingSize', record.packingSize)
        searchParams.append('uom', record.uom)
        searchParams.append('uomValue', record.uomValue)
        searchParams.append('date', dateIn)
        searchParams.append('remark', record.remark)
        searchParams.append('batchRef', record.batchIdRef)
        searchParams.append('refNo', record.arrivalNoticeRefNo ? record.arrivalNoticeRefNo : '')
        searchParams.append('refNo2', record.arrivalNotice ? record.arrivalNotice.refNo2 : '')
        searchParams.append('refNo3', record.arrivalNotice ? record.arrivalNotice.refNo3 : '')
        searchParams.append('expirationDate', record.expirationDate)
        searchParams.append('purchaseOrderNo', record.purchaseOrderNo)
        searchParams.append('orderProductRemark', record.orderProductRemark)
        searchParams.append('refNo', record.arrivalNoticeRefNo)
        searchParams.append('ganNo', record.arrivalNoticeNo)
        if (typeof record.manufactureDate === 'number') {
          let date = new Date(record.manufactureDate)
          let year = date.getFullYear()

          searchParams.append('manufactureYear', year.toString())
        }

        try {
          const response = await fetch(`label-command/${labelId}?${searchParams.toString()}`, {
            method: 'GET'
          })

          if (response.status !== 200) {
            throw `Error : Can't get label command from server (response: ${response.status})`
          }

          var command = await response.text()

          if (!this.printer) {
            this.printer = new DirectPrinter()
          }

          await this.printer.print(command)
        } catch (e) {
          this.showToast(e)

          delete this.printer
          break
        }
      }
    }
  }

  async printCartonLabel() {
    const records = this.dataGrist.selected
    const labelId = this._cartonLabel && this._cartonLabel.id

    if (!labelId) {
      this.showToast({
        type: 'error',
        message: `${i18next.t('text.no_label_setting_was_found')}. ${i18next.t('text.please_check_your_setting')}`
      })
    } else {
      for (var record of records) {
        var searchParams = new URLSearchParams()
        if (!record?.cartonId) throw new Error(i18next.t('text.selected_inventory_not_using_carton_label'))

        /* for pallet record mapping */
        searchParams.append('cartonId', record.cartonId)
        searchParams.append('carton', record.cartonId)
        searchParams.append('customer', record.bizplace.name)

        try {
          const response = await fetch(`label-command/${labelId}?${searchParams.toString()}`, {
            method: 'GET'
          })

          if (response.status !== 200) {
            throw `Error : Can't get label command from server (response: ${response.status})`
          }

          var command = await response.text()

          if (!this.printer) {
            this.printer = new DirectPrinter()
          }

          await this.printer.print(command)
          setTimeout(50)
        } catch (e) {
          this.showToast(e)

          delete this.printer
          break
        }
      }
    }
  }

  getDateStr(date) {
    const newDate = new Date(parseInt(date))

    const month = String(newDate.getMonth() + 1).padStart(2, '0')
    const day = String(newDate.getDate()).padStart(2, '0')
    const year = newDate.getFullYear()

    date = [year, month, day].join('/')
    return date
  }

  compareValues(key, order = 'asc') {
    return function innerSort(a, b) {
      if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) {
        return 0
      }

      const varA = typeof a[key] === 'string' ? a[key].toUpperCase() : a[key]
      const varB = typeof b[key] === 'string' ? b[key].toUpperCase() : b[key]

      let comparison = 0
      if (varA > varB) {
        comparison = 1
      } else if (varA < varB) {
        comparison = -1
      }
      return order === 'desc' ? comparison * -1 : comparison
    }
  }

  showToast({ level, type, message }) {
    document.dispatchEvent(
      new CustomEvent('notify', {
        detail: {
          level,
          type,
          message
        }
      })
    )
  }

  //Validation if user not filled up the required column
  validation(row) {
    if (!row?.batchId) {
      throw new Error(i18next.t('text.batch_no_is_required_kindly_fill_in_the_batch_no_to_continue'))
    }

    if (!row?.bizplace || !row?.bizplace?.id) {
      throw new Error(i18next.t('text.company_is_required_kindly_fill_in_the_company_to_continue'))
    }

    if (!row?.qty) {
      throw new Error(i18next.t('text.qty_is_required_kindly_fill_in_the_qty_to_continue'))
    }

    if (!row?.uomValue) {
      throw new Error(i18next.t('text.total_uom_value_is_required_kindly_fill_in_the_total_uom_value_to_continue'))
    }

    if (!row?.location) {
      throw new Error(i18next.t('text.location_is_required_kindly_fill_in_the_location_to_continue'))
    }

    if (!row?.adjustmentCode) {
      throw new Error(i18next.t('text.adjustment_code_is_required_kindly_fill_in_the_adjustment_code_to_continue'))
    }

    if (!row?.adjustmentNote) {
      throw new Error(i18next.t('text.adjustment_note_is_required_kindly_fill_in_the_adjustment_note_to_continue'))
    }
  }

  toUtcTimestamp(dateInput) {
    const date = new Date(dateInput)
    const utcDate = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0))
    return utcDate.getTime()
  }
}

window.customElements.define('inventory-adjustment', InventoryAdjustment)
