import '@things-factory/barcode-ui'
import '@things-factory/grist-ui'
import './outbound-reusable-pallet'
import './picking-replacement-popup'
import './show-my-worksheet-list-popup'
import '../components/worksheet-information-popup'
import './serial-number-popup'
import './tote-popup'
import './vas-list-popup'
import './picking-same-carton-popup'

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

import { sleep } from '@operato/utils'
import { MultiColumnFormStyles } from '@things-factory/form-ui'
import { i18next, localize } from '@things-factory/i18n-base'
import { openPopup } from '@things-factory/layout-base'
import { client, CustomAlert, gqlContext, PageView, store } from '@things-factory/shell'
import { CommonButtonStyles } from '@things-factory/styles'
import { isMobileDevice } from '@things-factory/utils'

import { fetchLocationSortingRule } from '../../fetch-location-sorting-rule'
import { fetchSettingRule } from '../../fetch-setting-value'
import { decodeQR, showToast, fetchPageSettings, isValidHttpUrl } from '../../util/index'
import {
  BATCH_PICKING,
  LOCATION_SORTING_RULE,
  LOCATION_TYPE,
  ORDER_SOURCES,
  PICKING,
  WORKSHEET_TYPE
} from '../constants'

const PICKING_TYPES = { PICKING, BATCH_PICKING }

const VALIDATE_QC_SEALS = gql`
  query validateQcSeals($releaseGoodNo: String!) {
    validateQcSeals(releaseGoodNo: $releaseGoodNo) {
      valid
      error
      minimumSealNumber
    }
  }
`

class PickingProduct extends connect(store)(localize(i18next)(PageView)) {
  static get properties() {
    return {
      _worksheetLoaded: Boolean,
      _bizplaceName: String,
      _crossDocking: Boolean,
      _currentBinLocation: String,
      _enableProductScanning: Boolean,
      _qrProcessing: Number,
      _hasBinPicking: Boolean,
      _incompleteLocationName: Boolean,
      _isCartonLabel: Boolean,
      _isExecuting: Boolean,
      _isReplacement: Boolean,
      _isRequireSerialNumberScanningOutbound: Boolean,
      _locationName: String,
      _reusablePalletList: Object,
      _worksheetInfo: Object,
      _locationName: String,
      _scannable: Boolean,
      _enableInputQty: Boolean,
      _releaseGoodType: String,
      _isLastUnit: Boolean,
      config: Object,
      data: Object,
      isWholePicking: Boolean,
      locationNameSplit: Array,
      _isReleaseGoodRecall: Boolean,
      locations: Array,
      orderNo: String,
      refPickingType: String,
      releaseGoodNo: String,
      pickingOrderId: String,
      worksheetNo: String,
      toteNo: String,
      _pageSettings: Object,
      _isReplenishment: Boolean,
      _hasVas: Boolean,
      _enableToteScanningValue: Number
    }
  }

  static get styles() {
    return [
      MultiColumnFormStyles,
      css`
        :host {
          display: flex;
          flex-direction: column;
          overflow: hidden;
        }
        data-grist {
          overflow: auto;
          flex: 1;
        }
        h2 {
          padding: var(--subtitle-padding);
          font: var(--subtitle-font);
          color: var(--subtitle-text-color);
          border-bottom: var(--subtitle-border-bottom);
        }
        legend mwc-icon {
          vertical-align: middle;
          margin: var(--grist-title-icon-margin);
          font-size: var(--grist-title-icon-size);
          color: var(--grist-title-icon-color);
          cursor: pointer;
        }
      `
    ]
  }

  get binLocationInput() {
    return this.shadowRoot.querySelector('barcode-scanable-input[name=binLocation]')?.shadowRoot.querySelector('input')
  }

  get cartonInput() {
    return this.shadowRoot.querySelector('barcode-scanable-input[name=cartonId]')?.shadowRoot.querySelector('input')
  }

  get completed() {
    return this.data.records.every(record => record?.completed)
  }

  get context() {
    var actions = []
    if (this._hasBinPicking) {
      actions.push({
        title: i18next.t('button.get_task'),
        action: this.getTask.bind(this),
        ...CommonButtonStyles.assignment
      })
    }

    actions.push({
      title: i18next.t('button.my_worksheet_list'),
      action: this.showWorksheetList.bind(this),
      ...CommonButtonStyles.inspect
    })

    if (this._hasVas) {
      actions.push({
        title: i18next.t('button.vas_list'),
        action: this.openVasListPopup.bind(this),
        ...CommonButtonStyles.reorder
      })
    }

    if (this._selectedOrderInventory && this._isRequireSerialNumberScanningOutbound) {
      actions.push({
        title: i18next.t('button.serial_number'),
        action: this.openSerialNumberPopup.bind(this),
        ...CommonButtonStyles.activate
      })
    }

    // Tote button only appears when enable-tote-scanning is exactly 1 (not 2 or higher)
    if (
      this.refPickingType === PICKING_TYPES.PICKING.value &&
      this.releaseGoodNo &&
      this._enableToteScanningValue === 1 &&
      !this._isReplenishment
    ) {
      actions.push({
        title: i18next.t('button.tote'),
        action: this.openTotePopup.bind(this),
        ...CommonButtonStyles.activate
      })
    }

    if (this._worksheetLoaded && !this._isReplenishment) {
      actions.push({
        title: i18next.t('button.plastic_pallet'),
        action: this.openPalletOutbound.bind(this),
        ...CommonButtonStyles.reorder
      })
    }

    if (
      !this._disableMissingButton &&
      (this.pickingOrderId || this.taskNo) &&
      this._releaseGoodType === 'b2b' &&
      this._selectedOrderInventory?.status !== 'MISSING' &&
      this._selectedOrderInventory?.hasMissingInventoryChanges !== true &&
      this._selectedOrderInventory?.status &&
      this._selectedOrderInventory?.status !== 'DONE'
    ) {
      actions.push({
        title: i18next.t('button.missing_stock'),
        action: this.missingStock.bind(this),
        ...CommonButtonStyles.submit
      })
    }

    if (this._worksheetLoaded && this.data.records.every(record => record.completed)) {
      actions.push({
        title: i18next.t('button.complete'),
        action: this.complete.bind(this),
        ...CommonButtonStyles.submit
      })
    }

    return {
      title: i18next.t('title.picking'),
      help: 'operato-wms/outbound/picking-product',
      actions
    }
  }

  get infoForm() {
    return this.shadowRoot.querySelector('form#info-form')
  }

  get inputForm() {
    return this.shadowRoot.querySelector('form#input-form')
  }

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

  get locationInput() {
    return this.shadowRoot.querySelector('barcode-scanable-input[name=locationName]')?.shadowRoot.querySelector('input')
  }

  get newLocationInput() {
    return this.shadowRoot.querySelector('select[name=newLocationCode]')
  }

  get releaseGoodNoInput() {
    return this.shadowRoot
      .querySelector('barcode-scanable-input[name=releaseGoodNo]')
      ?.shadowRoot?.querySelector('input')
  }

  get taskNoInput() {
    return this.shadowRoot.querySelector('barcode-scanable-input[name=taskNo]')?.shadowRoot.querySelector('input')
  }

  get palletInput() {
    return this.shadowRoot.querySelector('barcode-scanable-input[name=palletId]')?.shadowRoot.querySelector('input')
  }

  get productBarcodeInput() {
    return this.shadowRoot
      ?.querySelector('barcode-scanable-input[name=productBarcode]')
      ?.shadowRoot.querySelector('input')
  }

  get releaseQtyInput() {
    return this.shadowRoot.querySelector('input[name=confirmedQty]')
  }

  get serialInput() {
    return this.shadowRoot
      ?.querySelector('barcode-scanable-input[name=serialNumber]')
      ?.shadowRoot.querySelector('input')
  }

  _focusOnNewLocationInput() {
    setTimeout(() => this.newLocationInput.focus(), 50)
  }

  _focusOnLocationNameInput() {
    setTimeout(() => this.locationInput.focus(), 50)
  }

  _focusOnReleaseGoodNoInput() {
    setTimeout(() => this.releaseGoodNoInput.focus(), 50)
  }

  _focusOnTaskNoInput() {
    setTimeout(() => this.taskNoInput.focus(), 50)
  }

  _focusOnBinLocationInput() {
    setTimeout(() => this.binLocationInput.focus(), 50)
  }

  _focusOnLotNoInput() {
    if (this.palletInput) {
      setTimeout(() => this.palletInput.focus(), 50)
    }
  }

  _focusOnCartonNoInput() {
    if (this.cartonInput) {
      setTimeout(() => this.cartonInput.focus(), 50)
    }
  }

  _focusOnProductBarcodeInput() {
    if (this._enableProductScanning && this?.productBarcodeInput) {
      setTimeout(() => this.productBarcodeInput.focus(), 50)
    }
  }

  _focusOnSerialInput() {
    if (this.serialInput) {
      setTimeout(() => this.serialInput.focus(), 50)
    }
  }

  _focusOnReleaseQtyInput() {
    if (this.releaseQtyInput) {
      setTimeout(() => this.releaseQtyInput.focus(), 50)
    }
  }

  //set the initial value of object attributes
  constructor() {
    super()
    this._worksheetLoaded = false
    this.data = { records: [] }
    this.locationSortingRules = []
    this.locationNameSplit = []
    this.refPickingType = PICKING_TYPES.PICKING.value
    this.releaseGoodNo = ''
    this.pickingOrderId = ''
    this.taskNo = ''
    this._incompleteLocationName = false
    this._isCartonLabel = false
    this._disableMissingButton = false
    /* A flag to indicate either PICKING process is executing */
    this._isExecuting = false
    this._isReplacement = false
    this._isRequireSerialNumberScanningOutbound = false
    this._crossDocking = false
    this._enableProductScanning = false
    this._qrProcessing = 0
    this._hasBinPicking = false
    this._reusablePalletList = { records: [] }
    this._selectedOrderInventory = null
    this.scannable = false
    this._isLastUnit = false
    this._isReplenishment = false
    this._hasVas = false
    this._enableToteScanningValue = 0
  }

  render() {
    return html`
      <form id="info-form" class="multi-column-form">
        <fieldset>
          <label>${i18next.t('label.picking_type')}</label>
          <select name="pickingType" @change="${e => this.pickingTypeChangeHandler(e.currentTarget.value)}">
            ${Object.keys(PICKING_TYPES).map(
              key =>
                html`
                  <option
                    value="${PICKING_TYPES[key].value}"
                    ?selected=${this.refPickingType === PICKING_TYPES[key].value}
                  >
                    ${i18next.t(`label.${PICKING_TYPES[key].name}`)}
                  </option>
                `
            )}
          </select>
          <label>
            ${this.refPickingType === PICKING_TYPES.PICKING.value
              ? i18next.t('label.order_no')
              : i18next.t('label.task_no')}
          </label>
          <barcode-scanable-input
            name=${this.refPickingType === PICKING_TYPES.PICKING.value ? 'releaseGoodNo' : 'taskNo'}
            custom-input
            @keypress="${async e => {
              try {
                if (e.keyCode === 13) {
                  e.preventDefault()
                  if (this.binLocationInput) this.binLocationInput.value = ''
                  if (this.productBarcodeInput) this.productBarcodeInput.value = ''
                  if (this.cartonInput) this.cartonInput.value = ''
                  if (this.palletInput) this.palletInput.value = ''
                  if (this.serialInput) this.serialInput.value = ''
                  this._selectedOrderInventory = null
                  this._currentBinLocation = null
                  if (this.refPickingType === PICKING_TYPES.PICKING.value) {
                    if (this.releaseGoodNoInput.value) {
                      this.releaseGoodNo = this.releaseGoodNoInput.value
                      await this.fetchAll(this.releaseGoodNo)
                      this.completeHandler()
                    }
                  } else {
                    if (this.taskNoInput.value) {
                      this.taskNo = this.taskNoInput.value
                      await this.fetchAll(this.taskNo)
                      this.completeHandler()
                    }
                  }
                }
              } catch (e) {}
            }}"
          >
          </barcode-scanable-input>
        </fieldset>
      </form>
      <form id="input-form" class="multi-column-form">
        ${this.scannable
          ? html`
              <fieldset>
                ${this._hasBinPicking && (!this.toteNo || this.tote == '')
                  ? html`
                      <label>${i18next.t('label.bin_location')}</label>
                      <barcode-scanable-input
                        name="binLocation"
                        custom-input
                        ?scan-only=${this._pageSettings?.cameraScanOnly}
                        @keypress="${async e => {
                          if (e.keyCode === 13) {
                            e.preventDefault()
                            this._currentBinLocation = this.binLocationInput.value
                            await this.picking(e)
                          }
                        }}"
                      ></barcode-scanable-input>
                    `
                  : ''}
                ${this._isCartonLabel
                  ? html`
                      <label>${i18next.t('label.carton_id')}</label>
                      <barcode-scanable-input
                        name="cartonId"
                        custom-input
                        ?scan-only=${this._pageSettings?.cameraScanOnly}
                        @keypress="${async e => {
                          if (e.keyCode === 13) {
                            e.preventDefault()
                            await this.picking(e)
                          }
                        }}"
                      ></barcode-scanable-input>
                    `
                  : html`
                      <label>${i18next.t('label.lot_id')}</label>
                      <barcode-scanable-input
                        name="palletId"
                        custom-input
                        ?scan-only=${this._pageSettings?.cameraScanOnly}
                        @keypress="${async e => {
                          if (e.keyCode === 13) {
                            e.preventDefault()
                            await this.picking(e)
                          }
                        }}"
                      ></barcode-scanable-input>
                    `}
                ${this._enableProductScanning && this._isCartonLabel
                  ? html`
                      <label>${i18next.t('label.product_barcode')}</label>
                      <barcode-scanable-input
                        name="productBarcode"
                        custom-input
                        ?scan-only=${this._pageSettings?.cameraScanOnly}
                        @keypress="${async e => {
                          if (e.keyCode === 13) {
                            e.preventDefault()
                            await this.picking(e)
                          }
                        }}"
                      ></barcode-scanable-input>
                    `
                  : ''}
                ${!this._isRequireSerialNumberScanningOutbound && (this._enableInputQty !== 0 || !this._isCartonLabel)
                  ? html`${(this._enableInputQty == 1 && this._releaseGoodType) ||
                    (this._enableInputQty == 2 && !this._releaseGoodType) ||
                    this._enableInputQty == 3 ||
                    !this._isCartonLabel ||
                    (this._isCartonLabel && !this._enableProductScanning)
                      ? html`<label>${i18next.t('label.picked_qty')}</label>
                          <input
                            type="number"
                            min="0.001"
                            step="0.001"
                            name="confirmedQty"
                            @keypress="${async e => {
                              if (e.keyCode === 13) {
                                e.preventDefault()
                                await this.picking(e)
                              }
                            }}"
                          /> `
                      : ''} `
                  : ''}
                ${this._isRequireSerialNumberScanningOutbound
                  ? html`
                      <label>${i18next.t('label.serial_number')}</label>
                      <barcode-scanable-input
                        name="serialNumber"
                        custom-input
                        ?scan-only=${this._pageSettings?.cameraScanOnly}
                        @keypress="${async e => {
                          if (e.keyCode === 13) {
                            e.preventDefault()
                            await this.picking(e)
                          }
                        }}"
                      ></barcode-scanable-input>
                    `
                  : ``}
                ${this.isWholePicking ||
                this._crossDocking ||
                this._isCartonLabel ||
                (this._enableProductScanning && this._isCartonLabel)
                  ? ''
                  : html`
                      ${this._incompleteLocationName
                        ? ''
                        : html`${(this._isRequireSerialNumberScanningOutbound && this._isLastUnit) ||
                          !this._isRequireSerialNumberScanningOutbound
                            ? html` <label>${i18next.t('label.return_location')}</label>
                                <barcode-scanable-input
                                  name="locationName"
                                  custom-input
                                  ?scan-only=${this._pageSettings?.cameraScanOnly}
                                  @keypress="${async e => {
                                    if (e.keyCode === 13) {
                                      e.preventDefault()
                                      await this.picking(e)
                                    }
                                  }}"
                                ></barcode-scanable-input>`
                            : ''}`}
                      <select
                        ?hidden="${!this._incompleteLocationName}"
                        name="newLocationCode"
                        @change="${e => (this._locationName = e.currentTarget.value)}"
                      >
                        <option value="">-- ${i18next.t('text.please_select_the_location')} --</option>
                        ${(this.locations || []).map(
                          location =>
                            html`
                              <option value="${location && location.name}">
                                ${location && location.name}
                                ${location && location.status ? ` (${location && location.status})` : ''}
                              </option>
                            `
                        )}
                      </select>
                    `}
              </fieldset>
            `
          : ''}
      </form>
      <data-grist .mode=${isMobileDevice() ? 'LIST' : 'GRID'} .config=${this.config} .data=${this.data}
        ><div slot="headroom" id="form-section">
          <form id="info-form" class="multi-column-form">
            <fieldset>
              <legend>
                ${i18next.t('title.order_no')}: ${this._orderNo}
                ${this._orderNo
                  ? html`(${this._bizplaceName})
                      <mwc-icon @click="${this._openWorksheetInformation.bind(this)}">info</mwc-icon>`
                  : ''}
              </legend>
            </fieldset>
          </form>
        </div></data-grist
      >
    `
  }

  async pageUpdated(changes, lifecycle) {
    if (this.active) {
      this.orderNo = lifecycle.params?.orderNo || null
      if (this.orderNo) await this.autoFetchPickingTasks(this.orderNo)
      this._focusOnReleaseGoodNoInput()
    }
  }

  async pageInitialized() {
    //fetch domain page settings
    let pageSettings = await fetchPageSettings({
      settings: {
        name: 'name',
        operator: 'in',
        value: ['enable-bin-picking', 'camera-scan-only', 'enable-picking-random-seq', 'disable-missing-button', 'display-conversion']
      }
    })

    const settingsMap = new Map(pageSettings.settings.map(setting => [setting.name, setting.value]))

    let enableBinPickingTask = settingsMap.get('enable-bin-picking') === 'true'
    let cameraScanOnly = settingsMap.get('camera-scan-only') === 'true'
    let enablePickingRandomSeq = settingsMap.get('enable-picking-random-seq') === 'true'
    let disableMissingButton = settingsMap.get('disable-missing-button') === 'true'
    let displayConversion = settingsMap.get('display-conversion') === 'true'

    const locationSortingTask = fetchLocationSortingRule(LOCATION_SORTING_RULE.PICKING_PRODUCT.value)

    this.config = {
      rows: {
        appendable: false,
        classifier: (record, rowIndex) => {
          return {
            emphasized: record.completed == true
          }
        },
        handlers: {
          click: async (columns, data, column, record, rowIndex) => {
            try {
              this.grist.showSpinner()
              if (data.records.length && record) {
                this._isRequireSerialNumberScanningOutbound = record.product.isRequireSerialNumberScanningOutbound
                this.inputForm.reset()
                this._isReplacement = false

                if (this.productBarcodeInput) this.productBarcodeInput.value = ''
                if (this.cartonInput) this.cartonInput.value = ''
                if (this.palletInput) this.palletInput.value = ''
                if (this.serialInput) this.serialInput.value = ''

                await this.fetchInventories(
                  this.refPickingType === PICKING_TYPES.PICKING.value ? this.releaseGoodNo : this.taskNo
                )

                this.focusOnInput()
                let updatedRecords = this._selectedOrderInventory
                if (updatedRecords.releaseQty === updatedRecords.pickedQty) {
                  updatedRecords.status = 'DONE'
                } else {
                  updatedRecords.status = 'PICKING'
                }
                this.updateSelectedItemProp(record)
                this.updateContext()
              }
            } catch (ex) {
              console.log(ex)
            } finally {
              this.grist.hideSpinner()
            }
          }
        }
      },
      pagination: { infinite: true },
      list: {
        fields: [
          'completed',
          'locationName',
          'palletId',
          'cartonId',
          'sku',
          'product',
          'packingType',
          'packingSize',
          'expirationDate',
          'batchId',
          'releaseProgress',
          'stockStatus'
        ]
      },
      columns: [
        { type: 'gutter', gutterName: 'sequence' },
        {
          type: 'boolean',
          name: 'completed',
          header: i18next.t('field.done'),
          width: 40
        },
        {
          type: 'string',
          name: 'locationName',
          header: i18next.t('field.location'),
          label: true,
          width: 120
        },
        {
          type: 'string',
          name: 'palletId',
          header: i18next.t('field.lot_id'),
          label: true,
          width: 140
        },
        {
          type: 'string',
          name: 'cartonId',
          header: i18next.t('field.carton_id'),
          label: true,
          width: 140
        },
        {
          type: 'string',
          name: 'sku',
          header: i18next.t('field.sku'),
          label: true,
          width: 120
        },
        {
          type: 'object',
          name: 'product',
          header: i18next.t('field.product'),
          label: true,
          width: 120
        },
        {
          type: 'string',
          name: 'packingType',
          header: i18next.t('field.packing_type'),
          label: true,
          width: 120
        },
        {
          type: 'integer',
          name: 'packingSize',
          header: i18next.t('field.packing_size'),
          label: true,
          width: 100
        },
        {
          type: 'date',
          name: 'expirationDate',
          header: i18next.t('field.exp_date'),
          label: true,
          width: 140
        },
        {
          type: 'string',
          name: 'batchId',
          header: i18next.t('field.batch_id'),
          label: true,
          width: 140
        },
        {
          type: 'string',
          name: 'conversion',
          header: i18next.t('field.pack_conversion'),
          label: true,
          hidden: !displayConversion,
          width: 140
        },
        {
          type: 'string',
          name: 'releaseProgress',
          header: i18next.t('field.release_qty'),
          label: true,
          width: 100
        },
        {
          type: 'string',
          name: 'stockStatus',
          header: i18next.t('field.missing_stock'),
          label: true,
          width: 160
        }
      ]
    }

    this.locationSortingRules = await locationSortingTask
    this._hasBinPicking = await enableBinPickingTask
    this._disableMissingButton = (await disableMissingButton) || false

    this._pageSettings = {
      ...this._pageSettings,
      enablePickingRandomSeq: (await enablePickingRandomSeq) || false,
      cameraScanOnly: (await cameraScanOnly) || false,
      displayConversion: (await displayConversion) || false
    }
  }

  formatWorksheetDetailInfos(wsdInfos) {
    return wsdInfos
      .map(record => {
        if (record.pickedQty == record.releaseQty || record.status == 'MISSING') {
          record.completed = true
        } else {
          record.completed = false
        }
        record.locationName = record?.location?.name
        record.product = record?.relatedOrderInv && record?.relatedOrderInv?.product
        record.productDetail = record?.relatedOrderInv.productDetail

        if (this._crossDocking) {
          record.batchId = record.relatedOrderInv.batchId
          record.packingType = record.relatedOrderInv.packingType
        }

        if (this._hasBinPicking) {
          record.binLocation = record.binLocation?.name
        }

        if (record?.cartonId) {
          record.hasCartonLabel = true
        }

        record.releaseProgress = `${record.pickedQty} / ${record.releaseQty}`
        record.sku = record?.relatedOrderInv.product?.sku
        record.stockStatus = record.hasMissingInventoryChanges === true ? 'PENDING_APPROVAL' : null

        return record
      })
      .sort((a, b) => a.completed - b.completed)
      .sort((a, b) => a.hasMissingInventoryChanges - b.hasMissingInventoryChanges)
  }

  clearView() {
    this.data = { records: [] }
    this.infoForm.reset()
    this.inputForm.reset()
    this._isRequireSerialNumberScanningOutbound = false
    this.releaseGoodNo = null
    this.pickingOrderId = null
    this.taskNo = null
    this._crossDocking = false
    this._selectedOrderInventory = null
    this._incompleteLocationName = false
    this._isExecuting = false
    this._locationName = null
    this.toteNo = ''
    this._worksheetLoaded = false
  }

  fillUpForm(form, data, fieldType) {
    this._incompleteLocationName = false
    form.reset()
    for (let key in data) {
      Array.from(form.querySelectorAll(fieldType)).forEach(field => {
        if (field.name === key && field.type === 'checkbox') {
          field.checked = data[key]
        } else if (field.name === key && field.type === 'datetime-local') {
          const datetime = Number(data[key])
          const timezoneOffset = new Date(datetime).getTimezoneOffset() * 60000
          field.value = new Date(datetime - timezoneOffset).toISOString().slice(0, -1)
        } else if (field.name === key) {
          if (data[key] instanceof Object) {
            const objectData = data[key]
            field.value = `${objectData.name} ${objectData.description ? `(${objectData.description})` : ''}`
          } else {
            field.value = data[key]
          }
        }
      })
    }
  }

  async picking(e) {
    if (e.keyCode !== 13) return
    e.preventDefault()
    try {
      //qr scanning and input is a url
      let isBarcode = isValidHttpUrl(this.productBarcodeInput?.value)
      if (
        this._enableProductScanning &&
        this._qrProcessing > 0 &&
        (isBarcode || isValidHttpUrl(this.serialInput?.value))
      ) {
        //assign url to input
        let input = ''
        //need to update this logic if using qr scanning without serial number
        if (this.refPickingType === PICKING_TYPES.PICKING.value && this._isRequireSerialNumberScanningOutbound) {
          input = isBarcode ? this.productBarcodeInput.value : this.serialInput.value
        } else if (this.refPickingType === PICKING_TYPES.BATCH_PICKING.value) {
          input = this.productBarcodeInput?.value.trim()
        }

        //get gtin and serial number from qr
        let config = JSON.parse(
          await fetchSettingRule('qr-data-parsing-configuration', this._worksheetInfo.partnerDomainId)
        )

        let data = await decodeQR(input, config)

        // Fill up form
        if (this.productBarcodeInput && data.productBarcode) {
          this.productBarcodeInput.value = data.productBarcode
        }
        if (this.serialInput && data.serialNumber) {
          this.serialInput.value = data.serialNumber
        }

        await sleep(1010)
      }

      let answer = await this.validatePicking()
      if (answer) {
        if (!answer?.value) return
      }
      let orderNo = this.refPickingType === PICKING_TYPES.PICKING.value ? this.releaseGoodNo : this.taskNo

      let response = {}
      if (this.crossDocking) {
        response = await this.pickCrossDocking()
      } else if (this._enableProductScanning && this._isCartonLabel && !this._isReplenishment) {
        response = await this.productBarcodePicking()
      } else if (this._isReplacement && !this._isCartonLabel) {
        if (this._isReleaseGoodRecall) {
          throw new Error(i18next.t('text.lot_replacement_is_not_applicable_on_recall_release_order'))
        } else {
          this.openPalletReplacementPopup()
        }
        return
      } else if (this._isReplenishment) {
        response = await this.replenishmentPicking()
      } else {
        response = await this.pickCommonPicking()
      }

      if (!response.errors) {
        if (this._hasBinPicking && (!this.toteNo || this.tote == '')) {
          this._currentBinLocation = this.binLocationInput.value
        }
        if (this._isRequireSerialNumberScanningOutbound) {
          this.serialInput.value = ''
        } else if (this._enableProductScanning) {
          if (this.productBarcodeInput) this.productBarcodeInput.value = ''
        } else {
          this.releaseQtyInput.value = ''
        }

        if (!this._isCartonLabel && !this._isRequireSerialNumberScanningOutbound) {
          this.palletInput.value = ''
        }

        if (!this.isWholePicking && !this._crossDocking && !this._isCartonLabel && this.locationInput) {
          this.locationInput.value = ''
          this._locationName = null
        }
      }

      await this.fetchInventories(orderNo)

      this.focusOnInput()

      this.isReplacement = false
    } catch (e) {
      showToast(e)
    }
  }

  openPalletReplacementPopup() {
    openPopup(
      html`
        <picking-replacement-popup
          .orderInventory="${this._selectedOrderInventory}"
          .refPickingType="${this.refPickingType}"
          .replacePalletId="${this.palletInput.value}"
          @completed="${() => {
            this._selectedOrderInventory = null
            if (this.refPickingType === PICKING_TYPES.PICKING.value) {
              this.fetchInventories(this.releaseGoodNoInput.value)
              this.fetchPalletsHandler(this.releaseGoodNoInput.value)
            } else {
              this.fetchInventories(this.taskNoInput.value)
              this.fetchPalletsHandler(this.taskNoInput.value)
            }
            // this._selectedTaskStatus = null
            this._isReplacement = false
            this._isExecuting = false
          }}"
        ></picking-replacement-popup>
      `,
      {
        backdrop: true,
        size: 'large',
        title: i18next.t('title.pallet_replacement')
      }
    )
  }

  async openSerialNumberPopup() {
    await sleep(1010)
    let qrConfig = null
    if (this._qrProcessing > 0) {
      qrConfig = JSON.parse(
        await fetchSettingRule('qr-data-parsing-configuration', this._worksheetInfo.partnerDomainId)
      )
    }
    openPopup(
      html`
        <serial-number-popup
          .orderInventory="${this?._selectedOrderInventory?.relatedOrderInv}"
          .orderType="${i18next.t('text.outbound')}"
          .orderSource="${ORDER_SOURCES.PICKING.value}"
          .orderId="${this.pickingOrderId}"
          .data="${this?._selectedOrderInventory}"
          .qrProcessing="${this._qrProcessing}"
          .qrConfig="${qrConfig}"
          @close-serial-number-popup="${async e => {
            await this.fetchAll(this.releaseGoodNo)
          }}"
        ></serial-number-popup>
      `,
      {
        closable: false,
        backdrop: false,
        escapable: false,
        size: 'large',
        title: i18next.t('title.serial_number_list')
      }
    )
  }

  async openTotePopup() {
    await sleep(1010)

    // Query for minimum seal number setting
    let minimumSealNumber = 0
    try {
      const validationResponse = await client.query({
        query: VALIDATE_QC_SEALS,
        variables: { releaseGoodNo: this.releaseGoodNo },
        context: gqlContext()
      })

      if (!validationResponse.errors && validationResponse.data?.validateQcSeals) {
        minimumSealNumber = validationResponse.data.validateQcSeals.minimumSealNumber || 0
      }
    } catch (err) {
      // If query fails, default to 0 (don't block tote scanning)
      console.warn('Failed to fetch minimum seal number:', err)
    }

    openPopup(
      html`
        <tote-popup
          .orderNo="${this.releaseGoodNo}"
          .toteNo="${this.toteNo}"
          .bizplace="${this._bizplaceName}"
          .minimumSealNumber="${minimumSealNumber}"
          @completed="${async e => {
            this.toteNo = e.detail
            // Refresh the item list to reflect any undone items
            await this.fetchInventories(
              this.refPickingType === PICKING_TYPES.PICKING.value ? this.releaseGoodNo : this.taskNo
            )
          }}"
        ></tote-popup>
      `,
      {
        closable: false,
        backdrop: false,
        escapable: false,
        size: 'large',
        title: i18next.t('title.tote_scanning')
      }
    )
  }

  _openWorksheetInformation() {
    openPopup(
      html`
        <worksheet-information-popup .worksheetInformation="${this._worksheetInfo}"></worksheet-information-popup>
      `,
      {
        backdrop: true,
        size: 'large',
        title: i18next.t('title.order_information')
      }
    )
  }

  async pick_crossDocking() {
    return await client.mutate({
      mutation: gql`
        mutation crossDockPicking(
          $worksheetDetailName: String!
          $worksheetType: String!
          $palletId: String!
          $releaseQty: Float!
        ) {
          crossDockPicking(
            worksheetDetailName: $worksheetDetailName
            worksheetType: $worksheetType
            palletId: $palletId
            releaseQty: $releaseQty
          )
        }
      `,
      variables: {
        worksheetDetailName: this._selectedOrderInventory.name,
        worksheetType: this.refPickingType,
        palletId: this.palletInput.value,
        releaseQty: parseFloat(this.releaseQtyInput.value)
      }
    })
  }

  async pickCommonPicking() {
    if (this._isExecuting) throw new Error(i18next.t('text.previous_scan_is_executing'))
    this._isExecuting = true

    const qty = this.releaseQtyInput?.value ? parseFloat(this.releaseQtyInput.value) : 1
    if (isNaN(qty)) throw new Error(i18next.t('text.invalid_qty'))

    try {
      switch (this.refPickingType) {
        case PICKING_TYPES.PICKING.value:
          return await client.mutate({
            mutation: gql`
              mutation picking(
                $worksheetDetailName: String!
                $worksheetType: String!
                $palletId: String
                $locationName: String!
                $releaseQty: Float!
                $binLocation: String
                $serialNumber: String
                $toteNo: String
              ) {
                picking(
                  worksheetDetailName: $worksheetDetailName
                  worksheetType: $worksheetType
                  palletId: $palletId
                  locationName: $locationName
                  releaseQty: $releaseQty
                  binLocation: $binLocation
                  serialNumber: $serialNumber
                  toteNo: $toteNo
                )
              }
            `,
            variables: {
              worksheetDetailName: this._selectedOrderInventory.name,
              worksheetType: this.refPickingType,
              palletId: this.palletInput.value.trim(),
              locationName: this._locationName ? this._locationName : this._selectedOrderInventory?.location.name,
              releaseQty: this._isRequireSerialNumberScanningOutbound ? 1 : qty,
              binLocation:
                this._hasBinPicking && (!this.toteNo || this.tote == '') ? this.binLocationInput.value : null,
              serialNumber: this._isRequireSerialNumberScanningOutbound ? this.serialInput.value : null,
              toteNo: this._enableToteScanningValue >= 1 && this.toteNo !== '' ? this.toteNo : null
            }
          })

        case PICKING_TYPES.BATCH_PICKING.value:
          return await client.mutate({
            mutation: gql`
              mutation batchPicking(
                $taskNo: String!
                $worksheetType: String!
                $palletId: String!
                $locationName: String
                $binLocationName: String
                $releaseQty: Float!
              ) {
                batchPicking(
                  taskNo: $taskNo
                  worksheetType: $worksheetType
                  palletId: $palletId
                  locationName: $locationName
                  binLocationName: $binLocationName
                  releaseQty: $releaseQty
                )
              }
            `,
            variables: {
              taskNo: this.taskNo,
              worksheetType: this.refPickingType,
              palletId: this.palletInput.value.trim(),
              locationName: this._locationName ? this._locationName : this._selectedOrderInventory?.location.name,
              binLocationName:
                this._hasBinPicking && (!this.toteNo || this.tote == '') ? this.binLocationInput.value : null,
              releaseQty: qty
            }
          })
      }
    } catch (e) {
      showToast(e)
    } finally {
      this._isExecuting = false
    }
  }

  async productBarcodePicking() {
    if (this._isExecuting) throw new Error(i18next.t('text.previous_scan_is_executing'))
    this._isExecuting = true

    let qty = 1
    if (this._selectedOrderInventory.product?.isInventoryDecimal) {
      qty = this._selectedOrderInventory?.releaseQty
    }
    qty = this.releaseQtyInput?.value ? parseFloat(this.releaseQtyInput.value) : qty

    if (isNaN(qty)) throw new Error(i18next.t('text.invalid_qty'))

    try {
      switch (this.refPickingType) {
        case PICKING_TYPES.PICKING.value:
          return await client.mutate({
            mutation: gql`
              mutation scanProductPicking(
                $worksheetDetailName: String!
                $worksheetType: String!
                $productBarcode: String!
                $cartonId: String!
                $binLocation: String
                $serialNumber: String
                $toteNo: String
                $pickingQty: Float
              ) {
                scanProductPicking(
                  worksheetDetailName: $worksheetDetailName
                  worksheetType: $worksheetType
                  productBarcode: $productBarcode
                  cartonId: $cartonId
                  binLocation: $binLocation
                  serialNumber: $serialNumber
                  toteNo: $toteNo
                  pickingQty: $pickingQty
                )
              }
            `,
            variables: {
              worksheetDetailId: this._selectedOrderInventory.id,
              worksheetDetailName: this._selectedOrderInventory.name,
              worksheetType: this.refPickingType,
              productBarcode: this.productBarcodeInput.value.trim(),
              cartonId: this.cartonInput.value.trim(),
              serialNumber: this._isRequireSerialNumberScanningOutbound ? this.serialInput.value : null,
              binLocation:
                this._hasBinPicking && (!this.toteNo || this.tote == '') ? this.binLocationInput.value : null,
              toteNo: this._enableToteScanningValue >= 1 && this.toteNo !== '' ? this.toteNo : null,
              pickingQty: qty
            }
          })

        case PICKING_TYPES.BATCH_PICKING.value:
          return await client.mutate({
            mutation: gql`
              mutation scanProductBatchPicking(
                $taskNo: String!
                $worksheetType: String!
                $cartonId: String!
                $inventory: InventoryPatch!
                $productBarcode: String!
                $binLocationName: String
                $pickingQty: Float
              ) {
                scanProductBatchPicking(
                  taskNo: $taskNo
                  worksheetType: $worksheetType
                  cartonId: $cartonId
                  inventory: $inventory
                  productBarcode: $productBarcode
                  binLocationName: $binLocationName
                  pickingQty: $pickingQty
                )
              }
            `,
            variables: {
              taskNo: this.taskNo,
              worksheetType: this.refPickingType,
              cartonId: this.cartonInput.value.trim(),
              productBarcode: this.productBarcodeInput.value.trim(),
              inventory: {
                batchId: this._selectedOrderInventory.batchId,
                packingType: this._selectedOrderInventory.packingType,
                packingSize: this._selectedOrderInventory.packingSize,
                productDetail: { id: this._selectedOrderInventory.productDetail.id }
              },
              binLocationName:
                this._hasBinPicking && (!this.toteNo || this.tote == '') ? this.binLocationInput.value : null,
              pickingQty: qty
            }
          })
      }
    } catch (e) {
      throw e
    } finally {
      this._isExecuting = false
    }
  }

  async replenishmentPicking() {
    if (this._isExecuting) throw new Error(i18next.t('text.previous_scan_is_executing'))
    this._isExecuting = true

    const qty = this.releaseQtyInput?.value ? parseFloat(this.releaseQtyInput.value) : 1
    if (isNaN(qty)) throw new Error(i18next.t('text.invalid_qty'))

    try {
      return await client.mutate({
        mutation: gql`
          mutation replenishmentPicking(
            $worksheetDetailName: String!
            $worksheetType: String!
            $productBarcode: String
            $cartonId: String!
            $binLocation: String
            $serialNumber: String
            $pickingQty: Float
          ) {
            replenishmentPicking(
              worksheetDetailName: $worksheetDetailName
              worksheetType: $worksheetType
              productBarcode: $productBarcode
              cartonId: $cartonId
              binLocation: $binLocation
              serialNumber: $serialNumber
              pickingQty: $pickingQty
            )
          }
        `,
        variables: {
          worksheetDetailName: this._selectedOrderInventory.name,
          worksheetType: WORKSHEET_TYPE.PICKING_REPLENISHMENT.value,
          productBarcode: this._enableProductScanning ? this.productBarcodeInput.value.trim() : null,
          cartonId: this.cartonInput.value.trim(),
          serialNumber: this._isRequireSerialNumberScanningOutbound ? this.serialInput.value : null,
          binLocation: this._hasBinPicking && (!this.toteNo || this.tote == '') ? this.binLocationInput.value : null,
          pickingQty: qty
        }
      })
    } catch (e) {
      throw e
    } finally {
      this._isExecuting = false
    }
  }

  async validatePicking() {
    // Support random picking sequence: auto-select row by scanned identifiers (carton/pallet) and product barcode
    if (this._pageSettings?.enablePickingRandomSeq === true) {
      // 1) Carton-based (carton label flow)
      if (this.cartonInput?.value) {
        const scannedCartonId = this.cartonInput.value.trim()
        if (scannedCartonId) {
          let candidates = (this.data?.records || []).filter(
            rec =>
              rec.cartonId === scannedCartonId &&
              rec.status !== 'DONE' &&
              rec.status !== 'MISSING' &&
              !rec.completed &&
              rec.pickedQty < rec.releaseQty
          )
          // Validate carton id: must have at least one candidate
          if (!candidates || candidates.length === 0) {
            this._focusOnCartonNoInput()
            const hasFullyPickedForCarton = (this.data?.records || []).some(
              rec =>
                rec.cartonId === scannedCartonId &&
                rec.status !== 'DONE' &&
                rec.status !== 'MISSING' &&
                !rec.completed &&
                rec.pickedQty >= rec.releaseQty
            )
            throw new Error(
              i18next.t(hasFullyPickedForCarton ? 'text.qty_exceed_release_qty' : 'text.invalid_carton_id')
            )
          }

          if (this._enableProductScanning) {
            // Require product barcode when product scanning enabled
            if (!this.productBarcodeInput?.value) {
              this._focusOnProductBarcodeInput()
              throw new Error(i18next.t('text.empty_product_barcode'))
            }

            this._popupCancelled = false
            const matchedRecord = await this._selectRecordByGtinFromCandidates(
              this.productBarcodeInput.value.trim(),
              candidates
            )

            if (this._popupCancelled) return { value: false }

            if (!matchedRecord) {
              this._focusOnProductBarcodeInput()
              this.productBarcodeInput.value = ''
              throw new Error(i18next.t('text.incorrect_product_barcode'))
            }

            if (!this._selectedOrderInventory || this._selectedOrderInventory.name !== matchedRecord.name) {
              this.updateSelectedItemProp(matchedRecord)
              this.scannableField()
            }
          } else {
            // Product scanning disabled: pick a deterministic candidate
            const matchedRecord = this._pickBestCandidate(this._filterCandidatesByInputQty(candidates))
            if (
              matchedRecord &&
              (!this._selectedOrderInventory || this._selectedOrderInventory.name !== matchedRecord.name)
            ) {
              this.updateSelectedItemProp(matchedRecord)
              this.scannableField()
            }
          }
        }
      }
    }

    if ((!this.palletInput?.value || !this.cartonInput?.value) && this._isRequireSerialNumberScanningOutbound) {
      if (!this.serialInput?.value) {
        this._focusOnSerialInput()
        throw new Error(i18next.t('text.empty_serial_number'))
      }
    }

    if (!this._isCartonLabel) {
      if (!this.palletInput.value) {
        this._focusOnLotNoInput()
        throw new Error(i18next.t('text.lot_id_is_empty'))
      } else {
        if (
          this._selectedOrderInventory.palletId !== this.palletInput.value &&
          !this._isRequireSerialNumberScanningOutbound
        ) {
          const isInProgressing = await this.checkProgressingPallet(this.palletInput.value)
          if (isInProgressing) throw new Error(i18next.t('text.pallet_is_in_progressing'))

          this._isReplacement = await this.checkProductIdenticality(
            this._selectedOrderInventory.palletId,
            this.palletInput.value
          )

          if (!this._isReplacement) {
            this._focusOnLotNoInput()
            throw new Error(i18next.t('text.wrong_lot_id'))
          }
        }
      }
    } else {
      if (this._hasBinPicking && (!this.toteNo || this.tote == '')) {
        if (!this.binLocationInput?.value) {
          this._focusOnBinLocationInput()
          throw new Error(i18next.t('text.bin_location_is_empty'))
        }
      }

      if (!this.cartonInput?.value) {
        this._focusOnCartonNoInput()
        throw new Error(i18next.t('text.carton_id_is_empty'))
      }

      if (this._selectedOrderInventory.cartonId) {
        if (this._selectedOrderInventory.cartonId !== this.cartonInput.value) {
          let foundRecord = {}

          // Auto Find matching carton for pick scanning row without forcing the user to choose
          if (this._pageSettings.enablePickingRandomSeq == true) {
            let cartonInputValue = this.cartonInput.value
            let candidates = this.data.records.filter(
              rec =>
                rec.cartonId === cartonInputValue &&
                rec.status !== 'DONE' &&
                rec.status !== 'MISSING' &&
                !rec.completed &&
                rec.pickedQty < rec.releaseQty
            )
            // If no candidates remain, check if it's because all matching items are already fully picked
            if (!candidates.length) {
              const hasFullyPickedForCarton = this.data.records.some(
                rec =>
                  rec.cartonId === cartonInputValue &&
                  rec.status !== 'DONE' &&
                  rec.status !== 'MISSING' &&
                  !rec.completed &&
                  rec.pickedQty >= rec.releaseQty
              )
              if (hasFullyPickedForCarton) {
                this._focusOnCartonNoInput()
                throw new Error(i18next.t('text.qty_exceed_release_qty'))
              }
            }

            if (this._enableProductScanning && this.productBarcodeInput?.value) {
              this._popupCancelled = false
              foundRecord = await this._selectRecordByGtinFromCandidates(
                this.productBarcodeInput.value.trim(),
                candidates
              )
              if (this._popupCancelled) return { value: false }
            }
            // If product barcode is provided but no GTIN match, stop without fallback
            if (this._enableProductScanning && this.productBarcodeInput?.value && !foundRecord) {
              this._focusOnProductBarcodeInput()
              this.productBarcodeInput.value = ''
              throw new Error(i18next.t('text.incorrect_product_barcode'))
            }

            // No product barcode: deterministically choose best candidate among duplicates
            if (!foundRecord && (!this._enableProductScanning || !this.productBarcodeInput?.value)) {
              foundRecord = this._pickBestCandidate(this._filterCandidatesByInputQty(candidates))
            }

            if (foundRecord) {
              this.updateSelectedItemProp(foundRecord)
            }
          } else {
            foundRecord = null
          }

          if (!foundRecord) {
            //carton replacement?
            if (this.productBarcodeInput?.value) {
              this.productBarcodeInput.value = ''
            }
            if (this.serialInput?.value) {
              this.serialInput.value = ''
            }
            throw new Error(i18next.t('text.invalid_carton_id'))
          }
        }

        if (this._enableProductScanning) {
          if (!this.productBarcodeInput?.value) {
            this._focusOnProductBarcodeInput()
            throw new Error(i18next.t('text.empty_product_barcode'))
          }
        }
      }
    }

    if (!this._isReplacement) {
      if (!this._isRequireSerialNumberScanningOutbound) {
        if (this.releaseQtyInput) {
          if (!parseFloat(this.releaseQtyInput?.value)) {
            this._focusOnReleaseQtyInput()
            throw new Error(i18next.t('text.release_qty_is_empty'))
          }

          if (!this.releaseQtyInput?.checkValidity()) throw new Error(i18next.t('text.release_qty_invalid'))

          // typed qty should be matched with release qty.
          if (
            parseFloat(this.releaseQtyInput.value) !==
              this._selectedOrderInventory.releaseQty -
                (this._selectedOrderInventory.pickedQty ? this._selectedOrderInventory.pickedQty : 0) &&
            !this._isCartonLabel
          ) {
            this._focusOnReleaseQtyInput()
            throw new Error(i18next.t('text.wrong_release_qty'))
          }
        }
      }

      if (!this._isCartonLabel) {
        // location id existing
        if (!this.isWholePicking && this.locationInput) {
          if (!this.locationInput.value) {
            this._focusOnLocationNameInput()
            throw new Error(i18next.t('text.location_id_is_empty'))
          }
          // check if actual return location
          this._locationName =
            this.isWholePicking || !this.locationInput
              ? this._selectedOrderInventory.location.name
              : this.locationInput.value
          // 1. Check whether location is changed
          if (this._selectedOrderInventory.location.name !== this._locationName) {
            this.locationNameSplit = this.locationInput.value.split('-')
            if (this.locationNameSplit.length < 3) {
              this._focusOnLocationNameInput()
              throw new Error(i18next.t('text.invalid_return_location'))
            }
            const zonePortions = this.locationNameSplit[0].match(/[a-zA-Z0-9]+/g)
            const rowPortions = this.locationNameSplit[1].match(/[a-zA-Z0-9]+/g)
            const columnPortions = this.locationNameSplit[2].match(/[a-zA-Z0-9]+/g)
            const shelfPortions = this.locationNameSplit[3].match(/[a-zA-Z0-9]+/g)
            if (this.locationNameSplit.length === 3) {
              this.locations = await this.fetchLocations(zonePortions[0], rowPortions[0], columnPortions[0])
              this._focusOnNewLocationInput()

              const selectedLocations = this.locations.find(res => {
                return res.name === this._locationName
              })

              if (this._isReleaseGoodRecall) {
                if (![selectedLocations.type].includes(LOCATION_TYPE.QUARANTINE.value, LOCATION_TYPE.RESERVE.value)) {
                  throw new Error(i18next.t('text.location_scanned_is_not_a_quarantine_zone_or_reserved_zone'))
                }
              }

              if (this.locations.length > 0) {
                this._incompleteLocationName = true
                throw new Error(i18next.t('text.please_select_the_location_again'))
              } else {
                throw new Error(i18next.t('text.invalid_return_location'))
              }
            } else if (this.locationNameSplit.length === 4) {
              this.locations = await this.fetchLocations(
                zonePortions[0],
                rowPortions[0],
                columnPortions[0],
                shelfPortions[0]
              )

              const selectedLocations = this.locations.find(res => {
                return res.name === this._locationName
              })

              if (this._isReleaseGoodRecall) {
                if (![selectedLocations.type].includes(LOCATION_TYPE.QUARANTINE.value, LOCATION_TYPE.RESERVE.value)) {
                  throw new Error(i18next.t('text.location_scanned_is_not_a_quarantine_zone_or_reserved_zone'))
                }
              }

              let relocateConfirmation = await CustomAlert({
                title: i18next.t('title.relocate'),
                text: i18next.t('text.are_you_sure'),
                confirmButton: { text: i18next.t('button.relocate') },
                cancelButton: { text: i18next.t('button.cancel') }
              })

              if (!relocateConfirmation.isConfirmed) {
                this._incompleteLocationName = false
                this.locationNameSplit = []
                return relocateConfirmation
              }
            }
          }
        }
      }
    }

    if (this._crossDocking) {
      // typed qty should be smaller than release qty.
      if (parseFloat(this.releaseQtyInput.value) > this._selectedOrderInventory.releaseQty) {
        this._focusOnReleaseQtyInput()
        throw new Error(i18next.t('text.wrong_release_qty'))
      }
    }
  }

  async checkProgressingPallet(palletId) {
    const response = await client.query({
      query: gql`
        query checkProgressingPallet($palletId: String!) {
          checkProgressingPallet(palletId: $palletId)
        }
      `,
      variables: { palletId }
    })

    if (!response.errors) {
      return response.data.checkProgressingPallet
    }
  }

  async checkProductIdenticality(palletA, palletB) {
    const response = await client.query({
      query: gql`
        query checkProductIdenticality($palletA: String!, $palletB: String!) {
          checkProductIdenticality(palletA: $palletA, palletB: $palletB)
        }
      `,
      variables: { palletA, palletB }
    })

    if (!response.errors) {
      return response.data.checkProductIdenticality
    }
  }

  async checkCartonIdenticality(cartonA, palletA, cartonB) {
    const response = await client.query({
      query: gql`
        query checkCartonIdenticality($cartonA: String!, $palletA: String!, $cartonB: String!) {
          checkCartonIdenticality(cartonA: $cartonA, palletA: $palletA, cartonB: $cartonB)
        }
      `,
      variables: { cartonA, palletA, cartonB }
    })

    if (!response.errors) {
      return response.data.checkCartonIdenticality
    }
  }

  async fetchLocations(zone, row, column) {
    const sortings = [{ name: 'name', desc: false }]
    const filters = [
      { name: 'zone', operator: 'eq', value: zone },
      { name: 'row', operator: 'eq', value: row },
      { name: 'column', operator: 'eq', value: column }
    ]

    if (filters) {
      const response = await client.query({
        query: gql`
          query locations($filters: [Filter!], $sortings: [Sorting!]) {
            locations(filters: $filters, sortings: $sortings) {
              items {
                id
                name
                zone
                row
                column
                shelf
                status
                type
              }
            }
          }
        `,
        variables: { filters, sortings }
      })

      if (!response.errors) {
        return response.data.locations.items || []
      }
    }
  }

  async autoFetchPickingTasks(orderNo) {
    this._selectedOrderInventory = null
    this.releaseGoodNoInput.value = orderNo
    this.releaseGoodNo = this.releaseGoodNoInput.value
    await this.fetchAll(this.releaseGoodNo)
    this.completeHandler()
  }

  async fetchInventories(pickingRefNo) {
    if (!pickingRefNo) return

    //this.releaseGoodNo will be incorrect if worksheet list is used, therefore it's assigned here
    //this._selectedOrderInventory still has the value of previous order, therefore it's cleared here
    //it should check if this.releaseGoodNo exist or not first, if it exist then only will run this logic, this logic is only suitable for single picking
    if (this.releaseGoodNo && this.releaseGoodNo !== pickingRefNo) {
      this.releaseGoodNo = pickingRefNo
      this._selectedOrderInventory = null
    }

    const data = this._selectedOrderInventory
    let response
    switch (this.refPickingType) {
      case PICKING_TYPES.PICKING.value:
        response = await client.query({
          query: gql`
            query pickingWorksheet($orderNo: String!, $locationSortingRules: [Sorting!]) {
              pickingWorksheet(orderNo: $orderNo, locationSortingRules: $locationSortingRules) {
                worksheetInfo {
                  releaseGood {
                    id
                    name
                    recall
                    crossDocking
                    type
                    rtm
                  }
                  replenishment {
                    id
                    name
                  }
                  bizplaceName
                  partnerDomainId
                  customerCompanyDomainId
                  refNo
                  refNo2
                  refNo3
                  startedAt
                  containerNo
                  marketplaceStatus
                  shippingProvider
                  trackingNo
                }
                worksheetDetailInfos {
                  hasMissingInventoryChanges
                  name
                  palletId
                  cartonId
                  batchId
                  batchIdRef
                  product {
                    id
                    sku
                    name
                    description
                    isInventoryDecimal
                    isRequireSerialNumberScanningOutbound
                  }
                  qty
                  releaseQty
                  pickedQty
                  expirationDate
                  status
                  description
                  targetName
                  packingType
                  packingSize
                  conversion
                  location {
                    name
                    description
                  }
                  binLocation {
                    name
                    description
                  }
                  relatedOrderInv {
                    id
                    batchId
                    batchIdRef
                    packingType
                    binLocation {
                      id
                      name
                    }
                    product {
                      id
                      sku
                      name
                      description
                      isRequireSerialNumberScanningOutbound
                    }
                    productDetail {
                      id
                    }
                  }
                }
              }
            }
          `,
          variables: { orderNo: pickingRefNo, locationSortingRules: this.locationSortingRules }
        })

        if (!response.errors) {
          let releaseGood = response.data.pickingWorksheet.worksheetInfo?.releaseGood
          let replenishment = response.data.pickingWorksheet.worksheetInfo?.replenishment

          this._worksheetLoaded = true

          if (releaseGood) {
            this._isReleaseGoodRecall = response.data.pickingWorksheet.worksheetInfo.releaseGood.recall
            this.pickingOrderId = releaseGood.id
            this._releaseGoodType = releaseGood.type
          } else {
            this.pickingOrderId = replenishment.id
            this._isReplenishment = true
          }

          if (!response.data?.pickingWorksheet) return
          const { worksheetInfo, worksheetDetailInfos } = response.data.pickingWorksheet
          this._crossDocking = worksheetInfo.releaseGood?._crossDocking
          this._bizplaceName = worksheetInfo?.bizplaceName
          this._orderNo = this.releaseGoodNo
          this._worksheetInfo = { ...worksheetInfo, rtm: worksheetInfo.releaseGood?.rtm }
          let data = { records: this.formatWorksheetDetailInfos(worksheetDetailInfos) }
          this.data = { ...data }

          if (!this._selectedOrderInventory) {
            this._selectedOrderInventory = data.records[0]
          } else {
            let currentData = (data?.records || data).find(record => record.name === this._selectedOrderInventory.name)

            if (currentData?.completed) {
              if (this.cartonInput) this.cartonInput.value = ''
              if (this.palletInput) this.palletInput.value = ''
              let nextData = data.records.filter(rec => !rec.completed)
              if (nextData.length > 0) {
                this._selectedOrderInventory = nextData[0]
              } else {
                this._selectedOrderInventory = null
              }
            }
          }

          if (this._selectedOrderInventory) {
            const record = data.records.find(record => record.name === this._selectedOrderInventory.name)
            this._selectedOrderInventory = record
            this.updateSelectedItemProp(record)
            await this.updateComplete
            await this.grist.reset()
            this.fillUpForm(
              this.inputForm,
              {
                ...record,
                binLocation: record?.binLocation ? record.binLocation : ''
              },
              'input'
            )

            if (record.pickedQty == record.releaseQty) {
              if (this.cartonInput) this.cartonInput.value = ''
              if (this.palletInput) this.palletInput.value = ''
            } else {
              // pre-set bin location from last scan if none was set
              if (
                this._hasBinPicking &&
                this.binLocationInput &&
                (!this._currentBinLocation || this._currentBinLocation == '')
              ) {
                this._currentBinLocation = record?.binLocation ? record.binLocation : ''
              }
            }

            if (
              record.releaseQty - record.pickedQty == 1 &&
              !this._isCartonLabel &&
              this._isRequireSerialNumberScanningOutbound
            ) {
              this._isLastUnit = true
            } else {
              this._isLastUnit = false
            }
          }

          this.scannableField()

          if (data) {
            if (!this._isCartonLabel) {
              const findPalletId = worksheetDetailInfos.find(res => res.palletId == data.palletId)

              if (data && findPalletId?.hasMissingInventoryChanges) {
                this.scannable = false
              }
            } else {
              const findCartonId = worksheetDetailInfos.find(res => res.cartonId == data.cartonId)

              if (data && findCartonId?.hasMissingInventoryChanges) {
                this.scannable = false
              }
            }
          }
        }

        break

      case PICKING_TYPES.BATCH_PICKING.value:
        response = await client.query({
          query: gql`
            query batchPickingWorksheet($taskNo: String!, $locationSortingRules: [Sorting!]) {
              batchPickingWorksheet(taskNo: $taskNo, locationSortingRules: $locationSortingRules) {
                worksheetInfo {
                  bizplaceName
                  partnerDomainId
                  customerCompanyDomainId
                  startedAt
                  rtm
                  refNo
                  refNo2
                  refNo3
                  shippingProvider
                  trackingNo
                  marketplaceStatus
                  containerNo
                }
                worksheetDetailInfos {
                  hasMissingInventoryChanges
                  palletId
                  cartonId
                  batchId
                  product {
                    id
                    name
                    description
                    sku
                    isInventoryDecimal
                    isRequireSerialNumberScanning
                  }
                  qty
                  releaseQty
                  pickedQty
                  expirationDate
                  status
                  packingType
                  packingSize
                  conversion
                  location {
                    name
                  }
                  binLocation {
                    name
                  }
                  relatedOrderInv {
                    batchId
                    packingType
                    product {
                      id
                      name
                      description
                      sku
                      isInventoryDecimal
                    }
                    productDetail {
                      id
                    }
                  }
                }
              }
            }
          `,
          variables: { taskNo: pickingRefNo, locationSortingRules: this.locationSortingRules }
        })

        if (!response.errors) {
          //if (!this._selectedOrderInventory) this.clearView()
          this.taskNo = pickingRefNo

          this._worksheetLoaded = true

          if (!response.data?.batchPickingWorksheet) return

          const { worksheetInfo, worksheetDetailInfos } = response.data.batchPickingWorksheet
          await this.updateComplete
          this._bizplaceName = worksheetInfo.bizplaceName
          this._orderNo = this.taskNo
          this._worksheetInfo = worksheetInfo
          this.data = { records: this.formatWorksheetDetailInfos(worksheetDetailInfos) }

          if (!this._selectedOrderInventory) {
            this._selectedOrderInventory = this.data.records[0]
          } else {
            let currentData = this.data.records.find(
              record =>
                record.product.sku === this._selectedOrderInventory.product.sku &&
                record.palletId === this._selectedOrderInventory.palletId
            )
            if (currentData.completed) {
              if (this.cartonInput) this.cartonInput.value = ''
              if (this.palletInput) this.palletInput.value = ''
              let nextData = this.data.records.filter(recs => !recs.completed)
              if (nextData.length > 0) {
                this._selectedOrderInventory = nextData[0]
              } else {
                this._selectedOrderInventory = null
              }
            }
          }
          this.scannableField()

          if (!this._isCartonLabel && data) {
            const findPalletId = worksheetDetailInfos.find(res => res.palletId == data.palletId)

            if (data && findPalletId?.hasMissingInventoryChanges) {
              this.scannable = false
            }
          }

          if (this._isCartonLabel && data) {
            const findCartonId = worksheetDetailInfos.find(res => res.cartonId == data.cartonId)

            if (data && findCartonId?.hasMissingInventoryChanges) {
              this.scannable = false
            }
          }

          if (this._selectedOrderInventory) {
            const record = this.data.records.find(
              record =>
                record.product.sku === this._selectedOrderInventory.product.sku &&
                record.palletId === this._selectedOrderInventory.palletId
            )

            this._selectedOrderInventory = record
            this.updateSelectedItemProp(record)

            if (record.pickedQty == record.releaseQty) {
              if (this._isCartonLabel) {
                //need to relook into clearview logic
                if (this.cartonInput) this.cartonInput.value = ''
                if (this.productBarcodeInput) this.productBarcodeInput.value = ''
              }
              this.fillUpForm(this.inputForm, record, 'input')
            } else {
              this.fillUpForm(this.inputForm, record, 'input')
            }

            if (
              record.releaseQty - record.pickedQty == 1 &&
              !this._isCartonLabel &&
              this._isRequireSerialNumberScanningOutbound
            ) {
              this._isLastUnit = true
            } else {
              this._isLastUnit = false
            }
          }
        }
        break
    }

    this.updateContext()
    // if (this.data.records.every(record => record.completed))
  }

  async fetchPalletsHandler(releaseGoodNo) {
    const response = await client.query({
      query: gql`
        query pallets($filters: [Filter!]!, $pagination: Pagination) {
          pallets(filters: $filters, pagination: $pagination) {
            items {
              id
              name
              owner {
                id
                name
                description
              }
              holder {
                id
                name
                description
              }
              status
            }
          }
        }
      `,
      variables: {
        filters: [{ name: 'refOrderNo', value: releaseGoodNo, operator: 'eq' }],
        pagination: { page: 1, limit: 9999999 }
      }
    })

    if (!response.errors) {
      this._reusablePalletList = { records: response.data.pallets.items }
    }
  }

  async fetchOrderVas(orderId) {
    if (!orderId) return

    let filters = [{ name: 'releaseGood', operator: 'eq', value: orderId }]

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

    if (!response.errors) {
      const total = response.data.orderVass.total
      if (total > 0) {
        this._hasVas = true
      } else {
        this._hasVas = false
      }
    } else {
      this._hasVas = false
    }
    this.updateContext()
  }

  async openVasListPopup() {
    await sleep(1010)
    openPopup(
      html` <vas-list-popup .orderId="${this.pickingOrderId}" .orderType="${'RELEASE_GOODS'}"></vas-list-popup> `,
      {
        closable: false,
        backdrop: false,
        escapable: false,
        size: 'large',
        title: i18next.t('title.vas')
      }
    )
  }

  async completeHandler() {
    if (!this.data?.records?.length || this.data.records.some(record => !record.completed)) return

    this.updateContext()
    const result = await CustomAlert({
      title: i18next.t('title.picking'),
      text: i18next.t('text.do_you_want_to_complete'),
      confirmButton: { text: i18next.t('button.complete') },
      cancelButton: { text: i18next.t('button.cancel') }
    })

    if (!result?.value) {
      return
    }

    if (this._isExecuting) {
      return
    } else {
      await this.complete()
    }
  }

  async complete() {
    if (this._isExecuting) {
      return
    }

    this._isExecuting = true

    try {
      let response
      switch (this.refPickingType) {
        case PICKING_TYPES.PICKING.value:
          response = await client.mutate({
            mutation: gql`
              mutation completePicking($releaseGoodNo: String!) {
                completePicking(releaseGoodNo: $releaseGoodNo) {
                  obsolete
                }
              }
            `,
            variables: { releaseGoodNo: this.releaseGoodNo }
          })
          break

        case PICKING_TYPES.BATCH_PICKING.value:
          response = await client.mutate({
            mutation: gql`
              mutation completeBatchPicking($taskNo: String!) {
                completeBatchPicking(taskNo: $taskNo) {
                  obsolete
                }
              }
            `,
            variables: { taskNo: this.taskNo }
          })
          break
      }

      if (!response.errors) {
        if (response.data?.completePicking?.obsolete || response.data?.completeBatchPicking?.obsolete) {
          showToast({ message: i18next.t('text.obsolete_inventory_found_please_reassign') })
        } else {
          this.clearView()
          this._orderNo = ''
          this._bizplaceName = ''
          this._currentBinLocation = ''
          this._selectedOrderInventory = null
          await CustomAlert({
            title: i18next.t('title.picking'),
            text: i18next.t('text.picking_is_completed'),
            confirmButton: { text: i18next.t('button.confirm') }
          })

          if (!this.orderNo) {
            if (this.refPickingType === PICKING_TYPES.PICKING.value) {
              this.releaseGoodNoInput.value = ''
            } else {
              this.taskNoInput.value = ''
            }
          }
          this.updateContext()
        }
      }
    } catch (e) {
      showToast(e)
    } finally {
      this._isExecuting = false
    }
  }

  async missingStock() {
    const selectOrderInventory = this._selectedOrderInventory

    const patchesArr = [selectOrderInventory]

    const patches = patchesArr.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) {
      try {
        const response = await client.mutate({
          mutation: gql`
            mutation submitInventoryChanges($patches: [InventoryPatch!]!) {
              submitInventoryChanges(patches: $patches)
            }
          `,
          variables: { patches }
        })

        if (!response.error) {
          await this.fetchInventories(this.releaseGoodNo)
        }
      } catch (error) {
        throw new Error(error)
      }
    }
  }

  async openPalletOutbound() {
    await sleep(1010)
    openPopup(
      html`
        <outbound-reusable-pallet
          .palletData="${this._reusablePalletList}"
          .releaseGoodNo="${this.releaseGoodNo}"
          @reusable-pallet-info="${e => {
            this._reusablePalletList = e.detail
            const plasticPallet = this._reusablePalletList.records.map(itm => itm.name)
            this._worksheetInfo.plasticPallet = plasticPallet.toString()
          }}"
        ></outbound-reusable-pallet>
      `,
      {
        backdrop: true,
        size: 'large',
        title: i18next.t('title.outbound_reusable_pallet')
      }
    )
  }

  pickingTypeChangeHandler(currentType) {
    this.refPickingType = currentType
    this.clearView()
    this.updateContext()

    if (this.refPickingType === PICKING_TYPES.PICKING.value) this._focusOnReleaseGoodNoInput()
    else this._focusOnTaskNoInput()
  }

  async getTask() {
    try {
      const response = await client.mutate({
        mutation: gql`
          mutation fetchAndAssignPickingTask {
            fetchAndAssignPickingTask {
              worksheetInfo {
                bizplaceName
                partnerDomainId
                refNo
                refNo2
                refNo3
                startedAt
                releaseGood {
                  id
                  name
                  crossDocking
                }
              }
              worksheetDetailInfos {
                name
                palletId
                cartonId
                batchId
                batchIdRef
                product {
                  sku
                  name
                  description
                  isInventoryDecimal
                }
                qty
                releaseQty
                pickedQty
                expirationDate
                status
                description
                targetName
                packingType
                packingSize
                conversion
                location {
                  name
                  description
                }
                relatedOrderInv {
                  batchId
                  batchIdRef
                  packingType
                  product {
                    sku
                    name
                    description
                    isInventoryDecimal
                  }
                  productDetail {
                    id
                  }
                }
              }
            }
          }
        `,
        variables: {},
        context: gqlContext()
      })

      if (!response.errors) {
        if (!this._selectedOrderInventory) this.clearView()
        if (!response.data?.fetchAndAssignPickingTask) return

        const { worksheetInfo, worksheetDetailInfos } = response.data.fetchAndAssignPickingTask
        this._bizplaceName = worksheetInfo.bizplaceName
        this._crossDocking = worksheetInfo.releaseGood?._crossDocking
        this.releaseGoodNo = worksheetInfo.releaseGood?.name
        this.pickingOrderId = worksheetInfo.releaseGood?.id
        this._orderNo = this.releaseGoodNo
        this._worksheetInfo = worksheetInfo
        this.data = { records: this.formatWorksheetDetailInfos(worksheetDetailInfos) }

        //fetch partner page settings
        let pageSettings = await fetchPageSettings({
          settings: {
            name: 'name',
            operator: 'in',
            value: ['enable-product-scanning', 'qr-code-scanning', 'enable-tote-scanning', 'enable-input-qty']
          },
          settingsDomain: this._worksheetInfo.partnerDomainId
        })

        const settingsMap = new Map(pageSettings.settings.map(setting => [setting.name, setting.value]))

        this._enableProductScanning = settingsMap.get('enable-product-scanning') === 'true'
        this._enableToteScanningValue = parseInt(settingsMap.get('enable-tote-scanning') || '0', 10)
        this._enableInputQty = settingsMap.get('enable-input-qty')
        this._qrProcessing = settingsMap.get('qr-code-scanning')

        this._selectedOrderInventory = this.data?.records[0] || undefined

        if (this._selectedOrderInventory) {
          this.updateSelectedItemProp(this._selectedOrderInventory)
          if (this._selectedOrderInventory?.pickedQty == this._selectedOrderInventory?.releaseQty) {
            if (this._isCartonLabel) {
              if (this.cartonInput) this.cartonInput.value = ''
              this._focusOnCartonNoInput()
            }
            this._selectedOrderInventory = null
            // this._selectedTaskStatus = null
            this.inputForm.reset()
          } else {
            this._focusOnProductBarcodeInput()
            this.fillUpForm(this.inputForm, this._selectedOrderInventory, 'input')
          }
        }
        this.scannableField()
      }
    } catch (e) {
      showToast(e)
    }
  }

  async fetchAll(orderNo) {
    await this.fetchInventories(orderNo)
    await this.fetchPalletsHandler(orderNo)
    await this.fetchOrderVas(this.pickingOrderId)
    //fetch partner page settings
    let pageSettings = await fetchPageSettings({
      settings: {
        name: 'name',
        operator: 'in',
        value: ['enable-product-scanning', 'enable-tote-scanning', 'enable-input-qty', 'qr-code-scanning']
      },
      settingsDomain: this._worksheetInfo.partnerDomainId
    })

    const settingsMap = new Map(pageSettings.settings.map(setting => [setting.name, setting.value]))

    this._enableProductScanning = settingsMap.get('enable-product-scanning') === 'true'
    this._enableToteScanningValue = parseInt(settingsMap.get('enable-tote-scanning') || '0', 10)
    this._enableInputQty = settingsMap.get('enable-input-qty')
    this._qrProcessing = settingsMap.get('qr-code-scanning')

    // Update context after settings are loaded to ensure tote button visibility is correct
    this.updateContext()
    this.focusOnInput()
  }

  async showWorksheetList() {
    await sleep(1010)
    openPopup(html` <show-my-worksheet-list-popup .type=${this.refPickingType}></show-my-worksheet-list-popup> `, {
      backdrop: true,
      size: 'small',
      title: i18next.t('title.my_worksheet_list')
    })
  }

  // Prioritized input focus
  focusOnInput() {
    if (this.scannable) {
      // set bin location value if already has value else focus on bin input if bin picking setting is active
      if (this._currentBinLocation && this.binLocationInput) {
        this.binLocationInput.value = this._currentBinLocation
      }

      if (this._hasBinPicking && this.binLocationInput && this.binLocationInput.value == '') {
        return this._focusOnBinLocationInput()
      } else {
        if (this._isCartonLabel) {
          if (!this._enableProductScanning) this.cartonInput.value = ''

          if (this.cartonInput.value == '') {
            return this._focusOnCartonNoInput()
          }
        } else {
          if (!this._enableProductScanning && !this._isRequireSerialNumberScanningOutbound) this.palletInput.value = ''

          if (this.palletInput.value == '') {
            return this._focusOnLotNoInput()
          }
        }
      }

      if (this._enableProductScanning && this._isCartonLabel && !this._isRequireSerialNumberScanningOutbound) {
        this.productBarcodeInput.value = ''
        return this._focusOnProductBarcodeInput()
      }

      if (this._isRequireSerialNumberScanningOutbound) {
        if (this._isLastUnit) {
          return this._focusOnNewLocationInput()
        } else {
          this.serialInput.value = ''
          return this._focusOnSerialInput()
        }
      }
    }
  }

  updateSelectedItemProp(selectedRecords) {
    if (selectedRecords?.releaseQty === selectedRecords.qty) {
      this.isWholePicking = true
      this._selectedOrderInventory = selectedRecords
    } else {
      this.isWholePicking = false
      this._selectedOrderInventory = selectedRecords
    }

    this._isCartonLabel = selectedRecords.hasCartonLabel
    this._isRequireSerialNumberScanningOutbound = selectedRecords?.product?.isRequireSerialNumberScanningOutbound
  }

  scannableField() {
    if (
      this._selectedOrderInventory &&
      this._selectedOrderInventory.pickedQty != this._selectedOrderInventory.releaseQty &&
      !this._selectedOrderInventory?.hasMissingInventoryChanges &&
      this._selectedOrderInventory.status !== 'MISSING'
    ) {
      this.scannable = true
    } else {
      this.scannable = false
    }
  }

  massageReleaseGood(response) {}

  _getTypedQty() {
    if (!this.releaseQtyInput || !this.releaseQtyInput.value) return null
    const qty = parseFloat(this.releaseQtyInput.value)
    if (isNaN(qty) || qty <= 0) return null
    return qty
  }

  _filterCandidatesByInputQty(candidates) {
    const typedQty = this._getTypedQty()
    if (!typedQty || !Array.isArray(candidates) || !candidates.length) return candidates
    const matches = candidates.filter(
      rec => (parseFloat(rec?.releaseQty) - parseFloat(rec?.pickedQty ?? 0)) === typedQty
    )
    return matches.length ? matches : candidates
  }

  _pickBestCandidate(candidates) {
    if (!Array.isArray(candidates) || !candidates.length) return null

    const sorted = [...candidates].sort((a, b) => {
      const aCompleted = !!a?.completed
      const bCompleted = !!b?.completed
      if (aCompleted !== bCompleted) return aCompleted - bCompleted // prefer incomplete

      const aMissing = a?.status === 'MISSING'
      const bMissing = b?.status === 'MISSING'
      if (aMissing !== bMissing) return aMissing - bMissing // prefer not missing

      const aPending = !!a?.hasMissingInventoryChanges
      const bPending = !!b?.hasMissingInventoryChanges
      if (aPending !== bPending) return aPending - bPending // prefer not pending

      const aRemain = (a?.releaseQty || 0) - (a?.pickedQty || 0)
      const bRemain = (b?.releaseQty || 0) - (b?.pickedQty || 0)
      if (aRemain !== bRemain) return bRemain - aRemain // prefer greater remaining qty

      if (a?.name && b?.name && a.name !== b.name) return a.name.localeCompare(b.name)
      return 0
    })

    return sorted[0] || null
  }

  async _selectRecordByGtinFromCandidates(gtin, candidates) {
    try {
      if (!gtin || !candidates?.length) return null

      // Use checkGtin to resolve product-packing info to match candidate rows
      const response = await client.query({
        query: gql`
          query checkGtin($gtin: String!, $customerCompanyDomainId: String!) {
            checkGtin(gtin: $gtin, customerCompanyDomainId: $customerCompanyDomainId) {
              items {
                id
                name
                packingType
                packingSize
                gtin
                product {
                  id
                }
              }
              total
            }
          }
        `,
        variables: {
          gtin,
          customerCompanyDomainId: this._worksheetInfo?.customerCompanyDomainId
        }
      })

      if (response?.errors) return null

      const items = response?.data?.checkGtin?.items || []
      if (!items.length) return null

      // First try exact product+packing match, then fall back to product-only match
      const exactMatches = candidates.filter(rec =>
        items.some(
          itm =>
            itm.product?.id === rec?.product?.id &&
            itm.packingType === rec.packingType &&
            itm.packingSize === rec.packingSize
        )
      )
      if (exactMatches.length) return await this._pickBestCandidateOrShowPopup(exactMatches)

      const productOnlyMatches = candidates.filter(rec => items.some(itm => itm.product?.id === rec?.product?.id))
      if (productOnlyMatches.length) return await this._pickBestCandidateOrShowPopup(productOnlyMatches)

      return null
    } catch (e) {
      return null
    }
  }

  _hasDuplicateSameProduct(candidates) {
    return this._getDuplicateSameProductRows(candidates).length > 0
  }

  _getDuplicateSameProductRows(candidates) {
    if (!Array.isArray(candidates) || candidates.length <= 1) return []

    // Group by cartonId + SKU, return only rows that belong to a duplicate group
    const groups = {}
    candidates.forEach(c => {
      const key = `${c?.cartonId || ''}|${c?.product?.sku || ''}`
      if (!groups[key]) groups[key] = []
      groups[key].push(c)
    })

    const duplicateRows = []
    for (const rows of Object.values(groups)) {
      if (rows.length > 1) duplicateRows.push(...rows)
    }
    return duplicateRows
  }

  _showPickingSameCartonPopup(candidates) {
    return new Promise(resolve => {
      openPopup(
        html`
          <picking-same-carton-popup
            .pickingInventories="${candidates}"
            @selection="${e => {
              resolve(e.detail)
            }}"
          ></picking-same-carton-popup>
        `,
        {
          backdrop: true,
          size: 'large',
          title: i18next.t('title.inventory_selection')
        }
      )
    })
  }

  async _pickBestCandidateOrShowPopup(candidates) {
    if (!Array.isArray(candidates) || !candidates.length) return null

    // If multiple candidates have the same cartonId + SKU, show popup for user to choose
    const duplicateRows = this._getDuplicateSameProductRows(candidates)
    if (duplicateRows.length) {
      const selectedRecord = await this._showPickingSameCartonPopup(duplicateRows)
      if (!selectedRecord) {
        // User cancelled the popup — signal callers to do nothing
        this._popupCancelled = true
        return null
      }
      return selectedRecord
    }

    // Otherwise, use the existing logic to pick the best candidate
    return this._pickBestCandidate(candidates)
  }
}

window.customElements.define('picking-product', PickingProduct)
