@things-factory/operato-wms 4.3.752 → 4.3.755

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/client/pages/inbound/unload-product.js +30 -33
  2. package/client/pages/outbound/loading-product.js +2 -3
  3. package/client/pages/outbound/loading-v2/loading-execution-base.js +568 -56
  4. package/client/pages/outbound/loading-v2/loading-execution.js +258 -8
  5. package/client/pages/outbound/loading-v2/loading.js +82 -12
  6. package/client/pages/outbound/picking-product.js +11 -6
  7. package/client/pages/outbound/picking-v2/picking-execution-base.js +8 -8
  8. package/client/pages/outbound/picking-v2/picking-execution.js +75 -0
  9. package/client/pages/outbound/route-label-history-dialog.js +253 -0
  10. package/client/pages/outbound/route-label-popup.js +637 -131
  11. package/client/pages/outbound/show-ro-list-popup.js +8 -4
  12. package/client/pages/outbound/single-outbound-worksheet.js +55 -6
  13. package/client/pages/outbound/sorting-product.js +71 -25
  14. package/client/pages/outbound/zone-worksheet.js +62 -3
  15. package/dist-server/entities/index.js +9 -0
  16. package/dist-server/entities/index.js.map +1 -0
  17. package/dist-server/entities/route-label-sequence-counter.js +75 -0
  18. package/dist-server/entities/route-label-sequence-counter.js.map +1 -0
  19. package/dist-server/entities/route-label-sequence-log.js +74 -0
  20. package/dist-server/entities/route-label-sequence-log.js.map +1 -0
  21. package/dist-server/graphql/resolvers/index.js +3 -0
  22. package/dist-server/graphql/resolvers/index.js.map +1 -1
  23. package/dist-server/graphql/resolvers/outbound/complete-load.js +40 -3
  24. package/dist-server/graphql/resolvers/outbound/complete-load.js.map +1 -1
  25. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.js +50 -11
  26. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.js.map +1 -1
  27. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good.js +90 -48
  28. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good.js.map +1 -1
  29. package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js +15 -7
  30. package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js.map +1 -1
  31. package/dist-server/graphql/resolvers/outbound/sort-item.js +95 -16
  32. package/dist-server/graphql/resolvers/outbound/sort-item.js.map +1 -1
  33. package/dist-server/graphql/resolvers/route-label/index.js +7 -0
  34. package/dist-server/graphql/resolvers/route-label/index.js.map +1 -0
  35. package/dist-server/graphql/resolvers/route-label/route-label.js +220 -0
  36. package/dist-server/graphql/resolvers/route-label/route-label.js.map +1 -0
  37. package/dist-server/graphql/resolvers/zone-worksheet/index.js.map +1 -1
  38. package/dist-server/graphql/resolvers/zone-worksheet/zone-picking.js +123 -27
  39. package/dist-server/graphql/resolvers/zone-worksheet/zone-picking.js.map +1 -1
  40. package/dist-server/graphql/resolvers/zone-worksheet/zone-worksheet.js.map +1 -1
  41. package/dist-server/graphql/types/index.js +4 -0
  42. package/dist-server/graphql/types/index.js.map +1 -1
  43. package/dist-server/graphql/types/outbound/index.js +7 -1
  44. package/dist-server/graphql/types/outbound/index.js.map +1 -1
  45. package/dist-server/graphql/types/route-label/index.js +14 -0
  46. package/dist-server/graphql/types/route-label/index.js.map +1 -0
  47. package/dist-server/graphql/types/route-label/route-label-sequence.js +53 -0
  48. package/dist-server/graphql/types/route-label/route-label-sequence.js.map +1 -0
  49. package/dist-server/index.js +5 -0
  50. package/dist-server/index.js.map +1 -1
  51. package/dist-server/validators/setting-validators.js +519 -0
  52. package/dist-server/validators/setting-validators.js.map +1 -0
  53. package/helps/operato-wms/outbound/picking-product.en.md +156 -158
  54. package/package.json +50 -50
  55. package/server/entities/index.ts +6 -0
  56. package/server/entities/route-label-sequence-counter.ts +56 -0
  57. package/server/entities/route-label-sequence-log.ts +47 -0
  58. package/server/graphql/resolvers/index.ts +3 -0
  59. package/server/graphql/resolvers/outbound/complete-load.ts +42 -1
  60. package/server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.ts +56 -13
  61. package/server/graphql/resolvers/outbound/find-loadable-release-good.ts +120 -57
  62. package/server/graphql/resolvers/outbound/loading-worksheet-v2.ts +9 -2
  63. package/server/graphql/resolvers/outbound/sort-item.ts +125 -21
  64. package/server/graphql/resolvers/route-label/index.ts +16 -0
  65. package/server/graphql/resolvers/route-label/route-label.ts +264 -0
  66. package/server/graphql/resolvers/zone-worksheet/index.ts +2 -2
  67. package/server/graphql/resolvers/zone-worksheet/zone-picking.ts +156 -39
  68. package/server/graphql/resolvers/zone-worksheet/zone-worksheet.ts +1 -2
  69. package/server/graphql/types/index.ts +4 -0
  70. package/server/graphql/types/outbound/index.ts +7 -1
  71. package/server/graphql/types/route-label/index.ts +18 -0
  72. package/server/graphql/types/route-label/route-label-sequence.ts +50 -0
  73. package/server/index.ts +7 -1
  74. package/server/validators/setting-validators.ts +668 -0
  75. package/translations/en.json +280 -252
  76. package/translations/ko.json +29 -1
  77. package/translations/ms.json +28 -0
  78. package/translations/zh.json +28 -0
@@ -4,6 +4,7 @@ import './release-good-info'
4
4
  import './delivery-order-info'
5
5
  import '../show-ro-list-popup'
6
6
  import '../transport-vehicles-popup'
7
+ import '../tote-popup'
7
8
  import './load-by-qty'
8
9
  import './warehouse-return-popup'
9
10
 
@@ -21,7 +22,8 @@ import { showToast } from '../../../util'
21
22
  // Constants
22
23
  const SETTINGS = {
23
24
  QC_SKU: 'qc-sku',
24
- DISABLE_QC: 'disable-QC-in-loading'
25
+ DISABLE_QC: 'disable-QC-in-loading',
26
+ ENABLE_TOTE_SCANNING: 'enable-tote-scanning'
25
27
  }
26
28
 
27
29
  const TAB_TYPES = {
@@ -51,6 +53,18 @@ const FIND_LOADABLE_RELEASE_GOOD = gql`
51
53
  total
52
54
  pending
53
55
  }
56
+ tabCounts {
57
+ LOADING {
58
+ total
59
+ completed
60
+ pending
61
+ }
62
+ LOADED {
63
+ total
64
+ completed
65
+ pending
66
+ }
67
+ }
54
68
  }
55
69
  }
56
70
  `
@@ -58,6 +72,22 @@ const FIND_LOADABLE_RELEASE_GOOD = gql`
58
72
  const FIND_LOADABLE_RELEASE_GOOD_BY_BIN = gql`
59
73
  query findLoadableReleaseGoodByBin($orderNo: String!) {
60
74
  findLoadableReleaseGoodByBin(orderNo: $orderNo) {
75
+ worksheetInfo {
76
+ binLocationName
77
+ isReleaseGoodScan
78
+ releaseGood {
79
+ id
80
+ name
81
+ attentionCompany
82
+ attentionTo
83
+ deliveryAddress1
84
+ deliveryAddress2
85
+ status
86
+ refNo
87
+ refNo2
88
+ refNo3
89
+ }
90
+ }
61
91
  worksheetDetailInfos {
62
92
  id
63
93
  name
@@ -90,6 +120,7 @@ const FIND_LOADABLE_RELEASE_GOOD_BY_BIN = gql`
90
120
  id
91
121
  batchId
92
122
  packingType
123
+ refWorksheetId
93
124
  product {
94
125
  id
95
126
  sku
@@ -150,6 +181,7 @@ const LOADING_WORKSHEET_V2 = gql`
150
181
  id
151
182
  batchId
152
183
  packingType
184
+ refWorksheetId
153
185
  product {
154
186
  id
155
187
  sku
@@ -191,12 +223,19 @@ const GET_DELIVERY_ORDERS = gql`
191
223
  `
192
224
 
193
225
  const SORT_ITEM = gql`
194
- mutation sortItem($releaseGoodNo: String!, $productBarcode: String!, $qty: Float!, $worksheetDetails: [Object!]) {
226
+ mutation sortItem(
227
+ $releaseGoodNo: String!
228
+ $productBarcode: String!
229
+ $qty: Float!
230
+ $worksheetDetails: [Object!]
231
+ $toteNo: String
232
+ ) {
195
233
  sortItem(
196
234
  releaseGoodNo: $releaseGoodNo
197
235
  productBarcode: $productBarcode
198
236
  qty: $qty
199
237
  worksheetDetails: $worksheetDetails
238
+ toteNo: $toteNo
200
239
  )
201
240
  }
202
241
  `
@@ -213,6 +252,16 @@ const COMPLETE_LOAD_PARTIALLY = gql`
213
252
  }
214
253
  `
215
254
 
255
+ const VALIDATE_QC_SEALS = gql`
256
+ query validateQcSeals($releaseGoodNo: String!) {
257
+ validateQcSeals(releaseGoodNo: $releaseGoodNo) {
258
+ valid
259
+ error
260
+ minimumSealNumber
261
+ }
262
+ }
263
+ `
264
+
216
265
  export class LoadingExecutionBase extends LitElement {
217
266
  static get properties() {
218
267
  return {
@@ -237,13 +286,24 @@ export class LoadingExecutionBase extends LitElement {
237
286
  loadedData: Array,
238
287
  _executionData: Object,
239
288
  _disableQC: Boolean,
240
- _useQcSku: Boolean
289
+ _useQcSku: Boolean,
290
+ _enableToteScanning: Boolean,
291
+ _enableToteScanningValue: Number,
292
+ _hasRefWorksheetId: Boolean,
293
+ toteNo: String,
294
+ _tabCounts: Object,
295
+ _needsSealing: Boolean
241
296
  }
242
297
  }
243
298
 
244
299
  constructor() {
245
300
  super()
246
301
  this._useQcSku = false
302
+ this._enableToteScanning = false
303
+ this._enableToteScanningValue = 0
304
+ this._hasRefWorksheetId = false
305
+ this.toteNo = ''
306
+ this._needsSealing = false
247
307
  this._worksheetState = null
248
308
  this._executionData = {
249
309
  ownCollection: false,
@@ -332,7 +392,12 @@ export class LoadingExecutionBase extends LitElement {
332
392
  title: i18next.t('button.order_list'),
333
393
  action: async () => {
334
394
  try {
335
- const orderNo = this._useQcSku ? this.worksheet?.binLocationName : this.worksheet?.releaseGood?.name
395
+ // If isReleaseGoodScan is true, use release good name; otherwise use binLocationName if QC SKU mode
396
+ const orderNo = this.worksheet?.isReleaseGoodScan
397
+ ? this.worksheet?.releaseGood?.name
398
+ : this._useQcSku
399
+ ? this.worksheet?.binLocationName
400
+ : this.worksheet?.releaseGood?.name
336
401
 
337
402
  if (!orderNo) {
338
403
  throw new Error(i18next.t('text.no_order_selected'))
@@ -344,7 +409,8 @@ export class LoadingExecutionBase extends LitElement {
344
409
  orderNo,
345
410
  tabName: this.state?.activeTab || TAB_TYPES.LOADING
346
411
  },
347
- context: gqlContext()
412
+ context: gqlContext(),
413
+ fetchPolicy: 'network-only' // Always fetch fresh data to show updated item counts
348
414
  })
349
415
 
350
416
  if (!response.errors) {
@@ -363,6 +429,57 @@ export class LoadingExecutionBase extends LitElement {
363
429
  }
364
430
  }
365
431
 
432
+ get toteButton() {
433
+ return {
434
+ title: i18next.t('button.tote'),
435
+ action: async () => {
436
+ await this.openTotePopup()
437
+ },
438
+ ...CommonButtonStyles.activate
439
+ }
440
+ }
441
+
442
+ async openTotePopup() {
443
+ try {
444
+ const orderNo = this._currentReleaseGood?.releaseGood?.name
445
+ const bizplace = this._currentReleaseGood?.bizplaceName
446
+
447
+ if (!orderNo) {
448
+ throw new Error(i18next.t('text.no_release_order_selected'))
449
+ }
450
+
451
+ openPopup(
452
+ html`
453
+ <tote-popup
454
+ .orderNo="${orderNo}"
455
+ .toteNo="${this.toteNo}"
456
+ .bizplace="${bizplace}"
457
+ @completed="${async e => {
458
+ this.toteNo = e.detail
459
+ // Check seal status after tote is sealed (may have changed)
460
+ await this._checkAndUpdateSealStatus()
461
+ // Update button visibility after sealing (Tote button may need to be hidden if sealing is complete)
462
+ this.getButton()
463
+ // Update QC banner state after tote is sealed
464
+ if (this._updateQcBannerState) {
465
+ await this._updateQcBannerState()
466
+ }
467
+ }}"
468
+ ></tote-popup>
469
+ `,
470
+ {
471
+ closable: false,
472
+ backdrop: false,
473
+ escapable: false,
474
+ size: 'large',
475
+ title: i18next.t('title.tote_scanning')
476
+ }
477
+ )
478
+ } catch (e) {
479
+ showToast(e)
480
+ }
481
+ }
482
+
366
483
  // Input Getters
367
484
  get productBarcodeInput() {
368
485
  return this.shadowRoot
@@ -396,30 +513,86 @@ export class LoadingExecutionBase extends LitElement {
396
513
 
397
514
  // Core Methods
398
515
  async openTab(e, tabName) {
516
+ // Validate seals before allowing switch to LOADING tab
517
+ // Validation applies when tote scanning is enabled and switching from QC or LOADED to LOADING
518
+ // The validation query checks if totes exist and are properly sealed based on minimum-seal-number
519
+ if (
520
+ tabName === TAB_TYPES.LOADING &&
521
+ (this.state?.activeTab === TAB_TYPES.QC || this.state?.activeTab === TAB_TYPES.LOADED) &&
522
+ this._enableToteScanning
523
+ ) {
524
+ const orderNo = this._currentReleaseGood?.releaseGood?.name || this.worksheet?.releaseGood?.name
525
+ if (orderNo) {
526
+ try {
527
+ const validationResponse = await client.query({
528
+ query: VALIDATE_QC_SEALS,
529
+ variables: { releaseGoodNo: orderNo },
530
+ context: gqlContext()
531
+ })
532
+
533
+ if (!validationResponse.errors && validationResponse.data?.validateQcSeals) {
534
+ const { valid, error, minimumSealNumber } = validationResponse.data.validateQcSeals
535
+ // Only block if valid is false AND minimumSealNumber > 0
536
+ // If minimumSealNumber is 0, allow switching even if valid is false
537
+ if (!valid && (minimumSealNumber || 0) > 0) {
538
+ throw new Error(error || 'Seal validation failed')
539
+ }
540
+ }
541
+ } catch (e) {
542
+ showToast(e)
543
+ // Don't change tab on validation failure
544
+ return
545
+ }
546
+ }
547
+ }
548
+
399
549
  this.state = { ...this.state, activeTab: tabName }
400
550
  await this.updateComplete
401
551
 
402
552
  if (tabName === TAB_TYPES.QC) {
403
- const qcData = await this.fetchData(
404
- this._useQcSku ? this.worksheet?.binLocationName : this.worksheet?.releaseGood?.name,
405
- 'qc',
406
- this._useQcSku
407
- )
553
+ // If isReleaseGoodScan is true, use release good name; otherwise use binLocationName if QC SKU mode
554
+ const orderNoForQc = this.worksheet?.isReleaseGoodScan
555
+ ? this.worksheet?.releaseGood?.name
556
+ : this._useQcSku
557
+ ? this.worksheet?.binLocationName
558
+ : this.worksheet?.releaseGood?.name
559
+ const useQcSkuForFetch = this._useQcSku && !this.worksheet?.isReleaseGoodScan
560
+ // Fetch both QC and LOADING data so banner can check if items are ready for loading
561
+ const [qcData, loadingData] = await Promise.all([
562
+ this.fetchData(orderNoForQc, 'qc', useQcSkuForFetch),
563
+ this.fetchData(orderNoForQc, 'loading', useQcSkuForFetch)
564
+ ])
408
565
  this.qcData = qcData.filter(data => data?.sortedQty != data?.pickedQty) || []
566
+ this.loadingData =
567
+ loadingData.filter(data => data?.sortedQty > 0 && data.status != WORKSHEET_STATUS.DONE.value) || []
568
+ // Check seal status when opening QC tab (needed for Tote button visibility)
569
+ await this._checkAndUpdateSealStatus()
570
+ // Update button visibility after fetching QC data (which updates _hasRefWorksheetId)
571
+ this.getButton()
572
+ // Update QC banner state after fetching QC data
573
+ if (this._updateQcBannerState) {
574
+ await this._updateQcBannerState()
575
+ }
409
576
  } else if (tabName === TAB_TYPES.LOADING) {
410
- if (this._useQcSku && this.worksheet?.binLocationName) {
577
+ // Use bin-based query only if QC SKU mode AND it's not a release good scan
578
+ if (this._useQcSku && this.worksheet?.binLocationName && !this.worksheet?.isReleaseGoodScan) {
411
579
  const response = await client.query({
412
580
  query: FIND_LOADABLE_RELEASE_GOOD,
413
581
  variables: {
414
582
  orderNo: this.worksheet?.binLocationName,
415
583
  tabName: this.state?.activeTab || TAB_TYPES.LOADING
416
584
  },
417
- context: gqlContext()
585
+ context: gqlContext(),
586
+ fetchPolicy: 'network-only' // Always fetch fresh data to show updated item counts
418
587
  })
419
588
 
420
589
  if (!response.errors) {
421
590
  let records = response.data.findLoadableReleaseGood
422
591
  if (records.length) {
592
+ // Store tabCounts from first record if available
593
+ if (records[0]?.tabCounts) {
594
+ this._tabCounts = records[0].tabCounts
595
+ }
423
596
  await this.selectReleaseOrderPopup(records, this.state?.activeTab)
424
597
  if (this.state.activeTab !== TAB_TYPES.LOADING) {
425
598
  this.state = { ...this.state, activeTab: TAB_TYPES.LOADING }
@@ -431,9 +604,50 @@ export class LoadingExecutionBase extends LitElement {
431
604
  }
432
605
  } else {
433
606
  this.loadingData = await this.fetchData(this.worksheet?.releaseGood?.name, 'loading', this._useQcSku)
607
+ // Refresh tabCounts when switching to LOADING tab
608
+ if (this._currentReleaseGood?.releaseGood?.name || this.worksheet?.releaseGood?.name) {
609
+ const orderNo = this._currentReleaseGood?.releaseGood?.name || this.worksheet?.releaseGood?.name
610
+ try {
611
+ const response = await client.query({
612
+ query: FIND_LOADABLE_RELEASE_GOOD,
613
+ variables: {
614
+ orderNo,
615
+ tabName: TAB_TYPES.LOADING
616
+ },
617
+ context: gqlContext(),
618
+ fetchPolicy: 'network-only' // Always fetch fresh data to show updated counts
619
+ })
620
+ if (!response.errors && response.data.findLoadableReleaseGood.length > 0) {
621
+ const releaseGood = response.data.findLoadableReleaseGood[0]
622
+ if (releaseGood.tabCounts) {
623
+ this._tabCounts = releaseGood.tabCounts
624
+ }
625
+ }
626
+ } catch (e) {
627
+ console.error('Error refreshing tabCounts:', e)
628
+ }
629
+ }
434
630
  }
435
631
  } else if (tabName === TAB_TYPES.LOADED) {
436
- if (!this._currentReleaseGood?.releaseGood?.name || this._useQcSku) {
632
+ // Ensure _currentReleaseGood is set from worksheet if not already set
633
+ if (!this._currentReleaseGood?.releaseGood?.name && this.worksheet?.releaseGood?.name) {
634
+ this._currentReleaseGood = {
635
+ releaseGood: this.worksheet.releaseGood,
636
+ bizplaceName: this.worksheet?.bizplaceName,
637
+ startedAt: this.worksheet?.startedAt
638
+ }
639
+ }
640
+
641
+ // If we have a current release good, use reload() which works with order number (no bin required)
642
+ if (this._currentReleaseGood?.releaseGood?.name) {
643
+ await this.reload()
644
+ // reload() already calls getButton(), so we can return here
645
+ return
646
+ }
647
+
648
+ // If using QC SKU mode and need to query by bin -> require bin location
649
+ // Only enforce bin selection when actually needed for bin-based operations
650
+ if (this._useQcSku) {
437
651
  if (!this.worksheet?.binLocationName) {
438
652
  throw new Error(i18next.t('text.no_bin_location_selected'))
439
653
  }
@@ -444,27 +658,33 @@ export class LoadingExecutionBase extends LitElement {
444
658
  orderNo: this.worksheet.binLocationName,
445
659
  tabName: this.state?.activeTab || TAB_TYPES.LOADED
446
660
  },
447
- context: gqlContext()
661
+ context: gqlContext(),
662
+ fetchPolicy: 'network-only' // Always fetch fresh data to show updated item counts
448
663
  })
449
664
 
450
665
  if (!response.errors) {
451
666
  let records = response.data.findLoadableReleaseGood
452
667
  if (records.length) {
453
- await this.selectReleaseOrderPopup(records, this.state?.activeTab)
454
- if (this._currentReleaseGood?.releaseGood?.name) {
455
- this.loadedData = await fetchDeliveryOrders(this._currentReleaseGood?.releaseGood?.name)
456
- if (!this.worksheet?.worksheet?.id) {
457
- await this.fetchData(this._currentReleaseGood.releaseGood.name, 'loading', this._useQcSku)
458
- }
668
+ // Store tabCounts from first record if available
669
+ if (records[0]?.tabCounts) {
670
+ this._tabCounts = records[0].tabCounts
459
671
  }
672
+ await this.selectReleaseOrderPopup(records, this.state?.activeTab)
673
+ // selectReleaseOrderPopup will handle data fetch and getButton() call
674
+ return
460
675
  } else {
461
676
  throw new Error(i18next.t('text.no_release_order_found'))
462
677
  }
463
678
  }
464
679
  } else {
465
- this.loadedData = await fetchDeliveryOrders(this._currentReleaseGood?.releaseGood?.name)
466
- if (!this.worksheet?.worksheet?.id) {
467
- await this.fetchData(this._currentReleaseGood.releaseGood.name, 'loading', this._useQcSku)
680
+ // Not using QC SKU mode and no current release good -> try to use order number from worksheet
681
+ // Allow switching to LOADED tab even without bin selection
682
+ if (this.worksheet?.releaseGood?.name) {
683
+ await this.reload()
684
+ return
685
+ } else {
686
+ // No order selected at all -> show error
687
+ throw new Error(i18next.t('text.no_release_order_selected'))
468
688
  }
469
689
  }
470
690
  }
@@ -498,27 +718,66 @@ export class LoadingExecutionBase extends LitElement {
498
718
  startedAt: this.worksheet?.startedAt
499
719
  }
500
720
 
721
+ // Reset tote selection when switching RO
722
+ this.toteNo = ''
723
+ // Reset refWorksheetId flag - will be updated when data is fetched
724
+ this._hasRefWorksheetId = false
725
+ // Reset seal status - will be updated after data is fetched
726
+ this._needsSealing = false
727
+
501
728
  this._executionData = {
502
729
  ...this._executionData,
503
730
  orderId: selectedRO.id
504
731
  }
505
732
 
506
- this.state = { ...this.state, activeTab: tabName }
733
+ // Store tabCounts for banner display
734
+ if (selectedRO.tabCounts) {
735
+ this._tabCounts = selectedRO.tabCounts
736
+ }
737
+
738
+ // UX-safe tab selection: Auto-switch to LOADED if LOADING is empty but LOADED has items
739
+ let finalTabName = tabName
740
+ if (selectedRO.tabCounts) {
741
+ const loadingCounts = selectedRO.tabCounts.LOADING || { total: 0, pending: 0 }
742
+ const loadedCounts = selectedRO.tabCounts.LOADED || { total: 0, pending: 0 }
743
+
744
+ // If current tab is LOADING and it's empty, but LOADED has items, switch to LOADED
745
+ if (tabName === TAB_TYPES.LOADING && loadingCounts.total === 0 && loadedCounts.total > 0) {
746
+ finalTabName = TAB_TYPES.LOADED
747
+ }
748
+ }
749
+
750
+ this.state = { ...this.state, activeTab: finalTabName }
507
751
  await this.updateComplete
508
752
 
509
- if (tabName === TAB_TYPES.LOADING) {
753
+ if (finalTabName === TAB_TYPES.LOADING) {
510
754
  this.loadingData = await this.fetchData(selectedRO.name, 'loading', false)
511
- } else if (tabName === TAB_TYPES.LOADED) {
755
+ } else if (finalTabName === TAB_TYPES.LOADED) {
512
756
  this.loadedData = await fetchDeliveryOrders(selectedRO.name)
513
757
  if (!this.worksheet?.worksheet?.id) {
514
758
  await this.fetchData(selectedRO.name, 'loading', this._useQcSku)
515
759
  }
516
- } else if (tabName === TAB_TYPES.QC) {
517
- this.qcData = await this.fetchData(selectedRO.name, 'qc', this._useQcSku)
760
+ } else if (finalTabName === TAB_TYPES.QC) {
761
+ // Fetch both QC and LOADING data so banner can check if items are ready for loading
762
+ const [qcData, loadingData] = await Promise.all([
763
+ this.fetchData(selectedRO.name, 'qc', this._useQcSku),
764
+ this.fetchData(selectedRO.name, 'loading', this._useQcSku)
765
+ ])
766
+ this.qcData = qcData.filter(data => data?.sortedQty < data?.pickedQty) || []
767
+ this.loadingData =
768
+ loadingData.filter(data => data?.sortedQty > 0 && data.status != WORKSHEET_STATUS.DONE.value) || []
769
+ // fetchData updates _hasRefWorksheetId, so update buttons after data is fetched
518
770
  }
519
771
 
520
772
  await this.updateComplete
773
+ // Check seal status after order is selected (needed for Tote button visibility)
774
+ await this._checkAndUpdateSealStatus()
775
+ // Update button visibility after data is fetched (which updates _hasRefWorksheetId)
521
776
  this.getButton()
777
+ // Update QC banner state after data is fetched
778
+ if (this._updateQcBannerState) {
779
+ await this._updateQcBannerState()
780
+ }
522
781
  }}
523
782
  ></show-ro-list-popup>
524
783
  `,
@@ -546,6 +805,8 @@ export class LoadingExecutionBase extends LitElement {
546
805
  async resetView() {
547
806
  this._disableQC = await fetchSettingRule(SETTINGS.DISABLE_QC)
548
807
  this._useQcSku = await fetchSettingRule(SETTINGS.QC_SKU)
808
+ this._enableToteScanningValue = parseInt((await fetchSettingRule(SETTINGS.ENABLE_TOTE_SCANNING)) || '0', 10)
809
+ this._enableToteScanning = this._enableToteScanningValue >= 1
549
810
 
550
811
  let activeTab = TAB_TYPES.QC
551
812
  if (this._disableQC && !this._useQcSku) {
@@ -556,7 +817,14 @@ export class LoadingExecutionBase extends LitElement {
556
817
 
557
818
  const currentWorksheet = this.worksheet
558
819
 
559
- this.qcData = this.worksheetDetails.filter(data => data?.sortedQty != data?.pickedQty) || []
820
+ // Check if any order inventory has refWorksheetId (when data comes from props)
821
+ if (this.worksheetDetails && this.worksheetDetails.length > 0) {
822
+ this._hasRefWorksheetId = this.worksheetDetails.some(
823
+ item => item?.relatedOrderInv?.refWorksheetId != null && item?.relatedOrderInv?.refWorksheetId !== ''
824
+ )
825
+ }
826
+
827
+ this.qcData = this.worksheetDetails.filter(data => data?.sortedQty < data?.pickedQty) || []
560
828
  this.loadingData =
561
829
  this.worksheetDetails.filter(data => data?.sortedQty > 0 && data.status != WORKSHEET_STATUS.DONE.value) || []
562
830
  this.loadedData = this.deliveryOrdersInfo || []
@@ -567,6 +835,9 @@ export class LoadingExecutionBase extends LitElement {
567
835
  worksheetId: currentWorksheet?.worksheet?.id
568
836
  }
569
837
 
838
+ // reset tote when resetting view
839
+ this.toteNo = ''
840
+
570
841
  if (!this.worksheet && this.worksheetDetails?.length > 0) {
571
842
  this.worksheet = {
572
843
  ...currentWorksheet,
@@ -577,10 +848,175 @@ export class LoadingExecutionBase extends LitElement {
577
848
  this.getButton()
578
849
  }
579
850
 
851
+ /**
852
+ * Reloads all data and counts after mutations (load, undo, etc.)
853
+ * This ensures banner and button visibility are always accurate
854
+ * Always fetches counts for BOTH LOADING and LOADED tabs
855
+ */
856
+ async reload() {
857
+ // If no order selected, clear stale state and show error
858
+ if (!this._currentReleaseGood?.releaseGood?.name && !this.worksheet?.releaseGood?.name) {
859
+ // Clear stale state to prevent incorrect UI
860
+ this._tabCounts = null
861
+ this.loadedData = null
862
+ showToast({ type: 'error', message: i18next.t('text.no_release_order_selected') })
863
+ this.getButton()
864
+ return
865
+ }
866
+
867
+ const orderNo = this._currentReleaseGood?.releaseGood?.name || this.worksheet?.releaseGood?.name
868
+
869
+ try {
870
+ // Query for BOTH LOADING and LOADED tabs to ensure complete tabCounts
871
+ const [loadingResponse, loadedResponse] = await Promise.all([
872
+ client.query({
873
+ query: FIND_LOADABLE_RELEASE_GOOD,
874
+ variables: {
875
+ orderNo,
876
+ tabName: TAB_TYPES.LOADING
877
+ },
878
+ context: gqlContext(),
879
+ fetchPolicy: 'network-only' // Always fetch fresh data to show updated counts
880
+ }),
881
+ client.query({
882
+ query: FIND_LOADABLE_RELEASE_GOOD,
883
+ variables: {
884
+ orderNo,
885
+ tabName: TAB_TYPES.LOADED
886
+ },
887
+ context: gqlContext(),
888
+ fetchPolicy: 'network-only' // Always fetch fresh data to show updated counts
889
+ })
890
+ ])
891
+
892
+ // Merge tabCounts from both queries
893
+ let mergedTabCounts = {
894
+ LOADING: null,
895
+ LOADED: null
896
+ }
897
+
898
+ // Extract LOADING counts
899
+ if (!loadingResponse.errors && loadingResponse.data.findLoadableReleaseGood.length > 0) {
900
+ const loadingReleaseGood = loadingResponse.data.findLoadableReleaseGood[0]
901
+ if (loadingReleaseGood.tabCounts?.LOADING) {
902
+ mergedTabCounts.LOADING = loadingReleaseGood.tabCounts.LOADING
903
+ }
904
+ // Also get LOADED counts from LOADING query if available (backend returns both)
905
+ if (loadingReleaseGood.tabCounts?.LOADED) {
906
+ mergedTabCounts.LOADED = loadingReleaseGood.tabCounts.LOADED
907
+ }
908
+ }
909
+
910
+ // Extract LOADED counts (prefer from LOADED query if LOADING query didn't have it)
911
+ if (!loadedResponse.errors && loadedResponse.data.findLoadableReleaseGood.length > 0) {
912
+ const loadedReleaseGood = loadedResponse.data.findLoadableReleaseGood[0]
913
+ if (loadedReleaseGood.tabCounts?.LOADED) {
914
+ mergedTabCounts.LOADED = loadedReleaseGood.tabCounts.LOADED
915
+ }
916
+ // Also get LOADING counts from LOADED query if available (backend returns both)
917
+ if (loadedReleaseGood.tabCounts?.LOADING && !mergedTabCounts.LOADING) {
918
+ mergedTabCounts.LOADING = loadedReleaseGood.tabCounts.LOADING
919
+ }
920
+ }
921
+
922
+ // Set merged tabCounts (only if we got at least one count)
923
+ if (mergedTabCounts.LOADING || mergedTabCounts.LOADED) {
924
+ this._tabCounts = mergedTabCounts
925
+ } else {
926
+ // Both queries returned empty or had errors - clear stale tabCounts
927
+ this._tabCounts = null
928
+ }
929
+
930
+ // Refresh loadedData
931
+ if (orderNo) {
932
+ this.loadedData = await fetchDeliveryOrders(orderNo)
933
+ }
934
+
935
+ // Always refresh loadingData (not just when on LOADING tab)
936
+ // This ensures banner condition (which uses loadingData.length) is accurate after undo/load mutations
937
+ // If a release good is selected (from popup), use release good query, not bin query
938
+ if (orderNo) {
939
+ const useQcSkuForReload =
940
+ this._useQcSku && !this.worksheet?.isReleaseGoodScan && !this._currentReleaseGood?.releaseGood?.name
941
+ this.loadingData = await this.fetchData(orderNo, 'loading', useQcSkuForReload)
942
+ }
943
+
944
+ // Update button visibility
945
+ await this.updateComplete
946
+ this.getButton()
947
+
948
+ // Update QC banner state after reload (in case we're on QC tab)
949
+ if (this._updateQcBannerState) {
950
+ await this._updateQcBannerState()
951
+ }
952
+
953
+ // Request update to refresh banner visibility after data reload
954
+ this.requestUpdate()
955
+ } catch (e) {
956
+ console.error('Error reloading data:', e)
957
+ // Clear stale state on error to prevent incorrect UI
958
+ this._tabCounts = null
959
+ this.loadedData = null
960
+ showToast({
961
+ type: 'error',
962
+ message: i18next.t('text.failed_to_refresh_data', { defaultValue: 'Failed to refresh data' })
963
+ })
964
+ this.getButton()
965
+ }
966
+ }
967
+
968
+ /**
969
+ * Checks if sealing is needed and updates _needsSealing property
970
+ * This can be overridden by child classes to provide custom seal checking logic
971
+ */
972
+ async _checkAndUpdateSealStatus() {
973
+ if (!this._enableToteScanning) {
974
+ this._needsSealing = false
975
+ return
976
+ }
977
+
978
+ const orderNo = this._currentReleaseGood?.releaseGood?.name || this.worksheet?.releaseGood?.name
979
+ if (!orderNo) {
980
+ this._needsSealing = false
981
+ return
982
+ }
983
+
984
+ try {
985
+ const validationResponse = await client.query({
986
+ query: VALIDATE_QC_SEALS,
987
+ variables: { releaseGoodNo: orderNo },
988
+ context: gqlContext()
989
+ })
990
+
991
+ if (!validationResponse.errors && validationResponse.data?.validateQcSeals) {
992
+ const { valid, minimumSealNumber } = validationResponse.data.validateQcSeals
993
+ const minSealNum = minimumSealNumber || 0
994
+ // Sealing is needed if valid is false AND minimumSealNumber > 0
995
+ this._needsSealing = !valid && minSealNum > 0
996
+ } else {
997
+ this._needsSealing = false
998
+ }
999
+ } catch (e) {
1000
+ // On error, assume sealing might be needed to be safe
1001
+ this._needsSealing = true
1002
+ }
1003
+ }
1004
+
580
1005
  getButton() {
581
1006
  let activeTab = this.state?.activeTab
582
1007
 
583
1008
  let buttons = [this.backButton]
1009
+ // Tote button visibility logic for QC tab:
1010
+ if (activeTab === TAB_TYPES.QC && this._enableToteScanning && this._useQcSku === false) {
1011
+ const shouldShowToteButton =
1012
+ (this._disableQC === false && this._hasRefWorksheetId) ||
1013
+ (this._disableQC === false && this._enableToteScanningValue === 2 && !this._hasRefWorksheetId) ||
1014
+ this._needsSealing === true
1015
+
1016
+ if (shouldShowToteButton) {
1017
+ buttons.unshift(this.toteButton)
1018
+ }
1019
+ }
584
1020
  if (activeTab === TAB_TYPES.LOADING) {
585
1021
  if (this.loadingData?.length > 0) {
586
1022
  if (this._useQcSku) {
@@ -593,10 +1029,20 @@ export class LoadingExecutionBase extends LitElement {
593
1029
  if (this._useQcSku) {
594
1030
  buttons.unshift(this.orderListButton)
595
1031
  }
596
- if (this.loadedData?.completable) {
1032
+ // Show COMPLETE button based on:
1033
+ // 1. Prefer _tabCounts.LOADED.total > 0 (most reliable)
1034
+ // 2. Only block if loadedData.completable === false explicitly
1035
+ // 3. Also check loadedData.deliveryOrders as fallback
1036
+ const hasLoadedItems =
1037
+ this._tabCounts?.LOADED?.total > 0 ||
1038
+ (this.loadedData?.deliveryOrders && this.loadedData.deliveryOrders.length > 0)
1039
+ const canComplete = this.loadedData?.completable !== false
1040
+
1041
+ if (hasLoadedItems && canComplete) {
597
1042
  buttons.unshift(this.completeButton)
598
1043
  } else if (
599
- this.loadedData?.deliveryOrders.length > 0 &&
1044
+ this.loadedData?.deliveryOrders &&
1045
+ this.loadedData.deliveryOrders.length > 0 &&
600
1046
  this._currentReleaseGood?.releaseGood?.status !== 'LOADING'
601
1047
  ) {
602
1048
  buttons.unshift(this.partialButton)
@@ -665,7 +1111,8 @@ export class LoadingExecutionBase extends LitElement {
665
1111
  .data="${targetDO}"
666
1112
  @undo=${async e => {
667
1113
  showToast({ type: 'success', message: 'undo loading completed' })
668
- this.loadedData = await fetchDeliveryOrders(this._currentReleaseGood.releaseGood.name)
1114
+ // Reload all data and counts to update banner and button visibility
1115
+ await this.reload()
669
1116
  }}
670
1117
  ></delivery-order-info>
671
1118
  `,
@@ -685,13 +1132,21 @@ export class LoadingExecutionBase extends LitElement {
685
1132
  await this.validateSorting()
686
1133
  let selectedData = this.qcData.filter(data => this.qcDataView.selectedItems.includes(data.id))
687
1134
 
1135
+ // If isReleaseGoodScan is true, use release good name; otherwise use binLocationName if QC SKU mode
1136
+ const releaseGoodNo = this.worksheet?.isReleaseGoodScan
1137
+ ? this.worksheet?.releaseGood?.name
1138
+ : this._useQcSku
1139
+ ? this.worksheet?.binLocationName
1140
+ : this.worksheet?.releaseGood?.name
1141
+
688
1142
  const response = await client.mutate({
689
1143
  mutation: SORT_ITEM,
690
1144
  variables: {
691
- releaseGoodNo: this._useQcSku ? this.worksheet?.binLocationName : this.worksheet?.releaseGood?.name,
1145
+ releaseGoodNo,
692
1146
  productBarcode: this.productBarcodeInput.value,
693
1147
  qty: parseFloat(this.qtyInput.value),
694
- worksheetDetails: selectedData
1148
+ worksheetDetails: selectedData,
1149
+ toteNo: this._enableToteScanning && this.toteNo !== '' ? this.toteNo : null
695
1150
  }
696
1151
  })
697
1152
 
@@ -700,13 +1155,28 @@ export class LoadingExecutionBase extends LitElement {
700
1155
  this.qtyInput.value = ''
701
1156
  this._focusOnProductBarcodeInput()
702
1157
 
703
- const updatedData = await this.fetchData(
704
- this._useQcSku ? this.worksheet?.binLocationName : this.worksheet?.releaseGood?.name,
705
- 'qc',
706
- this._useQcSku
707
- )
708
-
709
- this.qcData = updatedData.filter(data => data?.sortedQty < data?.pickedQty) || []
1158
+ // Fetch data for both QC and LOADING tabs to ensure banner condition is accurate
1159
+ // If isReleaseGoodScan is true, use release good name; otherwise use binLocationName if QC SKU mode
1160
+ const orderNoForRefresh = this.worksheet?.isReleaseGoodScan
1161
+ ? this.worksheet?.releaseGood?.name
1162
+ : this._useQcSku
1163
+ ? this.worksheet?.binLocationName
1164
+ : this.worksheet?.releaseGood?.name
1165
+ const useQcSkuForRefresh = this._useQcSku && !this.worksheet?.isReleaseGoodScan
1166
+
1167
+ const [qcData, loadingData] = await Promise.all([
1168
+ this.fetchData(orderNoForRefresh, 'qc', useQcSkuForRefresh),
1169
+ this.fetchData(orderNoForRefresh, 'loading', useQcSkuForRefresh)
1170
+ ])
1171
+
1172
+ this.qcData = qcData.filter(data => data?.sortedQty < data?.pickedQty) || []
1173
+ this.loadingData =
1174
+ loadingData.filter(data => data?.sortedQty > 0 && data.status != WORKSHEET_STATUS.DONE.value) || []
1175
+
1176
+ // Update QC banner state after data changes
1177
+ if (this._updateQcBannerState) {
1178
+ await this._updateQcBannerState()
1179
+ }
710
1180
  }
711
1181
  } catch (e) {
712
1182
  showToast(e)
@@ -750,11 +1220,8 @@ export class LoadingExecutionBase extends LitElement {
750
1220
  ._executionData=${loadData}
751
1221
  @loading=${async e => {
752
1222
  showToast({ type: 'success', message: 'loaded successfully' })
753
- this.loadingData = await this.fetchData(
754
- this._currentReleaseGood.releaseGood.name,
755
- 'loading',
756
- this._useQcSku
757
- )
1223
+ // Reload all data and counts to update banner and button visibility
1224
+ await this.reload()
758
1225
  }}
759
1226
  ></load-by-qty>
760
1227
  `,
@@ -802,10 +1269,23 @@ export class LoadingExecutionBase extends LitElement {
802
1269
  // --- Clear previous order and UI data ---
803
1270
  this._currentReleaseGood = null
804
1271
  this.loadedData = []
1272
+ this.loadingData = []
1273
+ this.qcData = []
1274
+ this.worksheet = null
1275
+ this.worksheetDetails = null
1276
+ this._tabCounts = null
1277
+ this._hasRefWorksheetId = false
1278
+ this.toteNo = ''
1279
+ this.state = null
805
1280
 
806
1281
  setTimeout(async () => {
807
1282
  try {
808
- const orderNo = this._useQcSku ? this.worksheet?.binLocationName : this.worksheet?.releaseGood?.name
1283
+ // If isReleaseGoodScan is true, use release good name; otherwise use binLocationName if QC SKU mode
1284
+ const orderNo = this.worksheet?.isReleaseGoodScan
1285
+ ? this.worksheet?.releaseGood?.name
1286
+ : this._useQcSku
1287
+ ? this.worksheet?.binLocationName
1288
+ : this.worksheet?.releaseGood?.name
809
1289
  if (!orderNo) {
810
1290
  resetContext(this.parent)
811
1291
  return
@@ -817,14 +1297,18 @@ export class LoadingExecutionBase extends LitElement {
817
1297
  orderNo,
818
1298
  tabName: this.state?.activeTab || TAB_TYPES.LOADING
819
1299
  },
820
- context: gqlContext()
1300
+ context: gqlContext(),
1301
+ fetchPolicy: 'network-only' // Always fetch fresh data to show updated item counts
821
1302
  })
822
1303
 
823
1304
  if (!response.errors) {
824
1305
  const orders = response.data.findLoadableReleaseGood
825
- if (orders.length) {
826
- await this.selectReleaseOrderPopup(orders, this.state?.activeTab)
1306
+ // Filter out orders that are already DONE
1307
+ const activeOrders = orders.filter(order => order.status !== 'DONE')
1308
+ if (activeOrders.length) {
1309
+ await this.selectReleaseOrderPopup(activeOrders, this.state?.activeTab)
827
1310
  } else {
1311
+ // No more active orders, reset to initial scan screen
828
1312
  resetContext(this.parent)
829
1313
  }
830
1314
  }
@@ -872,10 +1356,23 @@ export class LoadingExecutionBase extends LitElement {
872
1356
  // --- Clear previous order and UI data ---
873
1357
  this._currentReleaseGood = null
874
1358
  this.loadedData = []
1359
+ this.loadingData = []
1360
+ this.qcData = []
1361
+ this.worksheet = null
1362
+ this.worksheetDetails = null
1363
+ this._tabCounts = null
1364
+ this._hasRefWorksheetId = false
1365
+ this.toteNo = ''
1366
+ this.state = null
875
1367
 
876
1368
  setTimeout(async () => {
877
1369
  try {
878
- const orderNo = this._useQcSku ? this.worksheet?.binLocationName : this.worksheet?.releaseGood?.name
1370
+ // If isReleaseGoodScan is true, use release good name; otherwise use binLocationName if QC SKU mode
1371
+ const orderNo = this.worksheet?.isReleaseGoodScan
1372
+ ? this.worksheet?.releaseGood?.name
1373
+ : this._useQcSku
1374
+ ? this.worksheet?.binLocationName
1375
+ : this.worksheet?.releaseGood?.name
879
1376
  if (!orderNo) {
880
1377
  resetContext(this.parent)
881
1378
  return
@@ -887,14 +1384,18 @@ export class LoadingExecutionBase extends LitElement {
887
1384
  orderNo,
888
1385
  tabName: this.state?.activeTab || TAB_TYPES.LOADING
889
1386
  },
890
- context: gqlContext()
1387
+ context: gqlContext(),
1388
+ fetchPolicy: 'network-only' // Always fetch fresh data to show updated item counts
891
1389
  })
892
1390
 
893
1391
  if (!response.errors) {
894
1392
  const orders = response.data.findLoadableReleaseGood
895
- if (orders.length) {
896
- await this.selectReleaseOrderPopup(orders, this.state?.activeTab)
1393
+ // Filter out orders that are already DONE
1394
+ const activeOrders = orders.filter(order => order.status !== 'DONE')
1395
+ if (activeOrders.length) {
1396
+ await this.selectReleaseOrderPopup(activeOrders, this.state?.activeTab)
897
1397
  } else {
1398
+ // No more active orders, reset to initial scan screen
898
1399
  resetContext(this.parent)
899
1400
  }
900
1401
  }
@@ -934,7 +1435,8 @@ export class LoadingExecutionBase extends LitElement {
934
1435
 
935
1436
  async fetchData(orderNo, tab, useQcSku) {
936
1437
  try {
937
- if (tab === 'qc' && useQcSku) {
1438
+ // Otherwise, use FIND_LOADABLE_RELEASE_GOOD_BY_BIN for both QC and LOADING tabs when useQcSku is true (bin scan)
1439
+ if (useQcSku && !this.worksheet?.isReleaseGoodScan) {
938
1440
  const response = await client.query({
939
1441
  query: FIND_LOADABLE_RELEASE_GOOD_BY_BIN,
940
1442
  variables: { orderNo }
@@ -942,6 +1444,10 @@ export class LoadingExecutionBase extends LitElement {
942
1444
 
943
1445
  if (!response.errors) {
944
1446
  const data = response.data.findLoadableReleaseGoodByBin.worksheetDetailInfos
1447
+ // Check if any order inventory has refWorksheetId
1448
+ this._hasRefWorksheetId = data.some(
1449
+ item => item?.relatedOrderInv?.refWorksheetId != null && item?.relatedOrderInv?.refWorksheetId !== ''
1450
+ )
945
1451
  return combineSameProducts(data)
946
1452
  }
947
1453
  } else {
@@ -973,7 +1479,13 @@ export class LoadingExecutionBase extends LitElement {
973
1479
  worksheetId: worksheetInfo.worksheet.id
974
1480
  }
975
1481
 
976
- return response.data.loadingWorksheetv2.worksheetDetailInfos
1482
+ const worksheetDetailInfos = response.data.loadingWorksheetv2.worksheetDetailInfos
1483
+ // Check if any order inventory has refWorksheetId
1484
+ this._hasRefWorksheetId = worksheetDetailInfos.some(
1485
+ item => item?.relatedOrderInv?.refWorksheetId != null && item?.relatedOrderInv?.refWorksheetId !== ''
1486
+ )
1487
+
1488
+ return worksheetDetailInfos
977
1489
  }
978
1490
  }
979
1491
  } catch (e) {