@things-factory/operato-wms 4.3.760 → 4.3.762

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 (26) hide show
  1. package/client/pages/outbound/loading-v2/delivery-order-info.js +21 -7
  2. package/client/pages/outbound/loading-v2/loading-execution-base.js +82 -18
  3. package/client/pages/outbound/loading-v2/loading-execution.js +3 -1
  4. package/client/pages/outbound/picking-product.js +34 -0
  5. package/client/pages/outbound/sorting-product.js +30 -0
  6. package/client/pages/outbound/tote-popup.js +226 -36
  7. package/dist-server/graphql/resolvers/outbound/index.js +2 -1
  8. package/dist-server/graphql/resolvers/outbound/index.js.map +1 -1
  9. package/dist-server/graphql/resolvers/outbound/undo-load.js +2 -1
  10. package/dist-server/graphql/resolvers/outbound/undo-load.js.map +1 -1
  11. package/dist-server/graphql/resolvers/outbound/unsort-item.js +65 -0
  12. package/dist-server/graphql/resolvers/outbound/unsort-item.js.map +1 -0
  13. package/dist-server/graphql/resolvers/reports/tote-loading-qc-report.js +19 -10
  14. package/dist-server/graphql/resolvers/reports/tote-loading-qc-report.js.map +1 -1
  15. package/dist-server/graphql/types/outbound/index.js +4 -0
  16. package/dist-server/graphql/types/outbound/index.js.map +1 -1
  17. package/package.json +3 -3
  18. package/server/graphql/resolvers/outbound/index.ts +2 -0
  19. package/server/graphql/resolvers/outbound/undo-load.ts +2 -1
  20. package/server/graphql/resolvers/outbound/unsort-item.ts +78 -0
  21. package/server/graphql/resolvers/reports/tote-loading-qc-report.ts +34 -11
  22. package/server/graphql/types/outbound/index.ts +4 -0
  23. package/translations/en.json +7 -0
  24. package/translations/ko.json +6 -0
  25. package/translations/ms.json +6 -0
  26. package/translations/zh.json +6 -0
@@ -106,7 +106,7 @@ class DeliveryOrderInfo extends localize(i18next)(LitElement) {
106
106
  {
107
107
  type: 'row',
108
108
  content: [
109
- { header: 'Exp Date', field: 'expirationDate', type: 'string' },
109
+ { header: 'Exp Date', field: 'inventory.expirationDate', type: 'string' },
110
110
  { header: 'Batch No', field: 'batchId', type: 'string' }
111
111
  ]
112
112
  },
@@ -166,6 +166,11 @@ class DeliveryOrderInfo extends localize(i18next)(LitElement) {
166
166
 
167
167
  async undoLoad() {
168
168
  try {
169
+ // Validate data exists before proceeding
170
+ if (!this.data?.id) {
171
+ throw new Error('Delivery order data is not available')
172
+ }
173
+
169
174
  const result = await CustomAlert({
170
175
  title: i18next.t('title.are_you_sure'),
171
176
  text: i18next.t('text.proceed_undo_loading'),
@@ -175,23 +180,31 @@ class DeliveryOrderInfo extends localize(i18next)(LitElement) {
175
180
 
176
181
  if (!result.value) return
177
182
 
183
+ const doId = this.data.id
184
+
178
185
  const response = await client.mutate({
179
186
  mutation: gql`
180
187
  mutation undoLoad($doId: String!) {
181
188
  undoLoad(doId: $doId)
182
189
  }
183
190
  `,
184
- variables: { doId: this.data.id }
191
+ variables: { doId }
185
192
  })
186
193
 
187
194
  if (!response.errors) {
188
- this.dispatchEvent(new CustomEvent('undo', { detail: { doId: this.data.id } }))
189
- history.back()
195
+ // Dispatch event with bubbles and composed to ensure proper propagation through shadow DOM
196
+ this.dispatchEvent(new CustomEvent('undo', {
197
+ detail: { doId },
198
+ bubbles: true,
199
+ composed: true
200
+ }))
201
+ // Small delay to allow event handler to process before closing popup
202
+ setTimeout(() => history.back(), 100)
190
203
  } else {
191
204
  throw new Error(response.errors[0]?.message || 'Failed to undo loading')
192
205
  }
193
206
  } catch (e) {
194
- showToast(e)
207
+ showToast({ type: 'error', message: e.message || 'Failed to undo loading' })
195
208
  }
196
209
  }
197
210
 
@@ -215,8 +228,9 @@ class DeliveryOrderInfo extends localize(i18next)(LitElement) {
215
228
  const groupedMap = new Map()
216
229
 
217
230
  this.data.orderInventories.forEach(oi => {
218
- // Create a unique key based on product details
219
- const key = `${oi.product?.sku || ''}_${oi.batchId || ''}_${oi.expirationDate || ''}_${oi.packingType || ''}_${oi.packingSize || ''}`
231
+ // Create a unique key based on product details (expirationDate is on inventory relation)
232
+ const expirationDate = oi.inventory?.expirationDate || ''
233
+ const key = `${oi.product?.sku || ''}_${oi.batchId || ''}_${expirationDate}_${oi.packingType || ''}_${oi.packingSize || ''}`
220
234
 
221
235
  if (groupedMap.has(key)) {
222
236
  const existing = groupedMap.get(key)
@@ -448,19 +448,58 @@ export class LoadingExecutionBase extends LitElement {
448
448
  throw new Error(i18next.t('text.no_release_order_selected'))
449
449
  }
450
450
 
451
+ // Query for minimum seal number setting
452
+ let minimumSealNumber = 0
453
+ try {
454
+ const validationResponse = await client.query({
455
+ query: VALIDATE_QC_SEALS,
456
+ variables: { releaseGoodNo: orderNo },
457
+ context: gqlContext()
458
+ })
459
+
460
+ if (!validationResponse.errors && validationResponse.data?.validateQcSeals) {
461
+ minimumSealNumber = validationResponse.data.validateQcSeals.minimumSealNumber || 0
462
+ }
463
+ } catch (err) {
464
+ // If query fails, default to 0 (don't block tote scanning)
465
+ console.warn('Failed to fetch minimum seal number:', err)
466
+ }
467
+
451
468
  openPopup(
452
469
  html`
453
470
  <tote-popup
454
471
  .orderNo="${orderNo}"
455
472
  .toteNo="${this.toteNo}"
456
473
  .bizplace="${bizplace}"
474
+ .minimumSealNumber="${minimumSealNumber}"
457
475
  @completed="${async e => {
458
476
  this.toteNo = e.detail
459
477
  // Check seal status after tote is sealed (may have changed)
460
478
  await this._checkAndUpdateSealStatus()
479
+
480
+ // Refresh data for both QC and LOADING tabs to reflect any undone items
481
+ // Use the same logic as validateSorting to determine which order/bin to refresh
482
+ const orderNoForRefresh = this.worksheet?.isReleaseGoodScan
483
+ ? this.worksheet?.releaseGood?.name
484
+ : this._useQcSku
485
+ ? this.worksheet?.binLocationName
486
+ : this.worksheet?.releaseGood?.name
487
+ const useQcSkuForRefresh = this._useQcSku && !this.worksheet?.isReleaseGoodScan
488
+
489
+ const [qcData, loadingData] = await Promise.all([
490
+ this.fetchData(orderNoForRefresh, 'qc', useQcSkuForRefresh),
491
+ this.fetchData(orderNoForRefresh, 'loading', useQcSkuForRefresh)
492
+ ])
493
+
494
+ // QC tab: items that still need QC (sortedQty < pickedQty)
495
+ this.qcData = qcData.filter(data => data?.sortedQty < data?.pickedQty) || []
496
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
497
+ this.loadingData =
498
+ loadingData.filter(data => data?.sortedQty >= data?.pickedQty && data.status != WORKSHEET_STATUS.DONE.value) || []
499
+
461
500
  // Update button visibility after sealing (Tote button may need to be hidden if sealing is complete)
462
501
  this.getButton()
463
- // Update QC banner state after tote is sealed
502
+ // Update QC banner state after data is refreshed
464
503
  if (this._updateQcBannerState) {
465
504
  await this._updateQcBannerState()
466
505
  }
@@ -562,9 +601,13 @@ export class LoadingExecutionBase extends LitElement {
562
601
  this.fetchData(orderNoForQc, 'qc', useQcSkuForFetch),
563
602
  this.fetchData(orderNoForQc, 'loading', useQcSkuForFetch)
564
603
  ])
604
+
605
+ // QC tab: items that still need QC (sortedQty != pickedQty)
565
606
  this.qcData = qcData.filter(data => data?.sortedQty != data?.pickedQty) || []
607
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
566
608
  this.loadingData =
567
- loadingData.filter(data => data?.sortedQty > 0 && data.status != WORKSHEET_STATUS.DONE.value) || []
609
+ loadingData.filter(data => data?.sortedQty >= data?.pickedQty && data.status != WORKSHEET_STATUS.DONE.value) || []
610
+
568
611
  // Check seal status when opening QC tab (needed for Tote button visibility)
569
612
  await this._checkAndUpdateSealStatus()
570
613
  // Update button visibility after fetching QC data (which updates _hasRefWorksheetId)
@@ -585,8 +628,9 @@ export class LoadingExecutionBase extends LitElement {
585
628
  // Filter to only show items for the currently selected release order AND from this bin
586
629
  // Note: FIND_LOADABLE_RELEASE_GOOD_BY_BIN returns OrderInventory status ('LOADED'),
587
630
  // not WorksheetDetail status ('DONE'), so we filter by 'LOADED'
631
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
588
632
  this.loadingData = (binData || []).filter(
589
- data => data?.status !== 'LOADED' && data?.releaseGood?.name === this._currentReleaseGood.releaseGood.name
633
+ data => data?.sortedQty >= data?.pickedQty && data?.status !== 'LOADED' && data?.releaseGood?.name === this._currentReleaseGood.releaseGood.name
590
634
  )
591
635
  }
592
636
 
@@ -617,9 +661,13 @@ export class LoadingExecutionBase extends LitElement {
617
661
  }
618
662
  }
619
663
  } else {
620
- this.loadingData = await this.fetchData(this.worksheet?.releaseGood?.name, 'loading', this._useQcSku)
621
- // Filter out items that are already LOADED (safety check in case backend returns them)
622
- this.loadingData = (this.loadingData || []).filter(data => data?.status !== WORKSHEET_STATUS.DONE.value)
664
+ const rawLoadingData = await this.fetchData(this.worksheet?.releaseGood?.name, 'loading', this._useQcSku)
665
+
666
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
667
+ this.loadingData = (rawLoadingData || []).filter(
668
+ data => data?.sortedQty >= data?.pickedQty && data?.status !== WORKSHEET_STATUS.DONE.value
669
+ )
670
+
623
671
  // Refresh tabCounts when switching to LOADING tab
624
672
  if (this._currentReleaseGood?.releaseGood?.name || this.worksheet?.releaseGood?.name) {
625
673
  const orderNo = this._currentReleaseGood?.releaseGood?.name || this.worksheet?.releaseGood?.name
@@ -774,13 +822,16 @@ export class LoadingExecutionBase extends LitElement {
774
822
 
775
823
  // Filter to only show items for the selected release order from this bin
776
824
  // Note: FIND_LOADABLE_RELEASE_GOOD_BY_BIN returns OrderInventory status ('LOADED')
825
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
777
826
  this.loadingData = (binData || []).filter(
778
- data => data?.status !== 'LOADED' && data?.releaseGood?.name === selectedRO.name
827
+ data => data?.sortedQty >= data?.pickedQty && data?.status !== 'LOADED' && data?.releaseGood?.name === selectedRO.name
779
828
  )
780
829
  } else {
781
830
  this.loadingData = await this.fetchData(selectedRO.name, 'loading', false)
782
- // Filter out items that are already LOADED (safety check)
783
- this.loadingData = (this.loadingData || []).filter(data => data?.status !== WORKSHEET_STATUS.DONE.value)
831
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
832
+ this.loadingData = (this.loadingData || []).filter(
833
+ data => data?.sortedQty >= data?.pickedQty && data?.status !== WORKSHEET_STATUS.DONE.value
834
+ )
784
835
  }
785
836
  } else if (finalTabName === TAB_TYPES.LOADED) {
786
837
  this.loadedData = await fetchDeliveryOrders(selectedRO.name)
@@ -801,19 +852,22 @@ export class LoadingExecutionBase extends LitElement {
801
852
  data?.releaseGood?.name === selectedRO.name
802
853
  )
803
854
  // Also fetch loading data for banner
855
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
804
856
  let binLoadingData = await this.fetchData(this.worksheet.binLocationName, 'loading', true)
805
857
  this.loadingData = (binLoadingData || []).filter(
806
858
  data =>
807
- data?.sortedQty > 0 && data?.status !== 'LOADED' && data?.releaseGood?.name === selectedRO.name
859
+ data?.sortedQty >= data?.pickedQty && data?.status !== 'LOADED' && data?.releaseGood?.name === selectedRO.name
808
860
  )
809
861
  } else {
810
862
  const [qcData, loadingData] = await Promise.all([
811
863
  this.fetchData(selectedRO.name, 'qc', this._useQcSku),
812
864
  this.fetchData(selectedRO.name, 'loading', this._useQcSku)
813
865
  ])
866
+ // QC tab: items that still need QC (sortedQty < pickedQty)
814
867
  this.qcData = qcData.filter(data => data?.sortedQty < data?.pickedQty) || []
868
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
815
869
  this.loadingData =
816
- loadingData.filter(data => data?.sortedQty > 0 && data.status != WORKSHEET_STATUS.DONE.value) || []
870
+ loadingData.filter(data => data?.sortedQty >= data?.pickedQty && data.status != WORKSHEET_STATUS.DONE.value) || []
817
871
  }
818
872
  // fetchData updates _hasRefWorksheetId, so update buttons after data is fetched
819
873
  }
@@ -873,9 +927,11 @@ export class LoadingExecutionBase extends LitElement {
873
927
  )
874
928
  }
875
929
 
930
+ // QC tab: items that still need QC (sortedQty < pickedQty)
876
931
  this.qcData = this.worksheetDetails.filter(data => data?.sortedQty < data?.pickedQty) || []
932
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
877
933
  this.loadingData =
878
- this.worksheetDetails.filter(data => data?.sortedQty > 0 && data.status != WORKSHEET_STATUS.DONE.value) || []
934
+ this.worksheetDetails.filter(data => data?.sortedQty >= data?.pickedQty && data.status != WORKSHEET_STATUS.DONE.value) || []
879
935
  this.loadedData = this.deliveryOrdersInfo || []
880
936
 
881
937
  this._executionData = {
@@ -990,14 +1046,17 @@ export class LoadingExecutionBase extends LitElement {
990
1046
  let binData = await this.fetchData(this.worksheet.binLocationName, 'loading', true)
991
1047
  // Filter to only show items for the selected release order from this bin
992
1048
  // Note: FIND_LOADABLE_RELEASE_GOOD_BY_BIN returns OrderInventory status ('LOADED')
1049
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
993
1050
  this.loadingData = (binData || []).filter(
994
- data => data?.status !== 'LOADED' && data?.releaseGood?.name === orderNo
1051
+ data => data?.sortedQty >= data?.pickedQty && data?.status !== 'LOADED' && data?.releaseGood?.name === orderNo
995
1052
  )
996
1053
  } else {
997
1054
  // Standard mode: fetch by release order name
998
1055
  this.loadingData = await this.fetchData(orderNo, 'loading', false)
999
- // Filter out items that are already LOADED (safety check)
1000
- this.loadingData = (this.loadingData || []).filter(data => data?.status !== WORKSHEET_STATUS.DONE.value)
1056
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
1057
+ this.loadingData = (this.loadingData || []).filter(
1058
+ data => data?.sortedQty >= data?.pickedQty && data?.status !== WORKSHEET_STATUS.DONE.value
1059
+ )
1001
1060
  }
1002
1061
  }
1003
1062
 
@@ -1240,9 +1299,11 @@ export class LoadingExecutionBase extends LitElement {
1240
1299
  this.fetchData(orderNoForRefresh, 'loading', useQcSkuForRefresh)
1241
1300
  ])
1242
1301
 
1302
+ // QC tab: items that still need QC (sortedQty < pickedQty)
1243
1303
  this.qcData = qcData.filter(data => data?.sortedQty < data?.pickedQty) || []
1304
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
1244
1305
  this.loadingData =
1245
- loadingData.filter(data => data?.sortedQty > 0 && data.status != WORKSHEET_STATUS.DONE.value) || []
1306
+ loadingData.filter(data => data?.sortedQty >= data?.pickedQty && data.status != WORKSHEET_STATUS.DONE.value) || []
1246
1307
 
1247
1308
  // Update QC banner state after data changes
1248
1309
  if (this._updateQcBannerState) {
@@ -1493,8 +1554,10 @@ export class LoadingExecutionBase extends LitElement {
1493
1554
  @warehouse-return=${async e => {
1494
1555
  showToast({ type: 'success', message: 'Returning worksheet generated' })
1495
1556
  this.loadingData = await this.fetchData(this.worksheet?.releaseGood?.name, 'loading', this._useQcSku)
1496
- // Filter out items that are already LOADED (safety check)
1497
- this.loadingData = (this.loadingData || []).filter(data => data?.status !== WORKSHEET_STATUS.DONE.value)
1557
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
1558
+ this.loadingData = (this.loadingData || []).filter(
1559
+ data => data?.sortedQty >= data?.pickedQty && data?.status !== WORKSHEET_STATUS.DONE.value
1560
+ )
1498
1561
  }}
1499
1562
  ></warehouse-return-popup>
1500
1563
  `,
@@ -1557,6 +1620,7 @@ export class LoadingExecutionBase extends LitElement {
1557
1620
  }
1558
1621
 
1559
1622
  const worksheetDetailInfos = response.data.loadingWorksheetv2.worksheetDetailInfos
1623
+
1560
1624
  // Check if any order inventory has refWorksheetId
1561
1625
  this._hasRefWorksheetId = worksheetDetailInfos.some(
1562
1626
  item => item?.relatedOrderInv?.refWorksheetId != null && item?.relatedOrderInv?.refWorksheetId !== ''
@@ -424,9 +424,11 @@ export class LoadingExecution extends LoadingExecutionBase {
424
424
  }
425
425
 
426
426
  // Filter data for different tabs
427
+ // QC tab: items that still need QC (sortedQty < pickedQty)
427
428
  this.qcData = processedDetails.filter(data => data?.sortedQty < data?.pickedQty && data.status !== 'LOADED') || []
429
+ // Loading tab: items that have completed QC (sortedQty >= pickedQty) and are not yet loaded
428
430
  this.loadingData =
429
- processedDetails.filter(data => data?.sortedQty > 0 && data.status != WORKSHEET_STATUS.DONE.value) || []
431
+ processedDetails.filter(data => data?.sortedQty >= data?.pickedQty && data.status != WORKSHEET_STATUS.DONE.value) || []
430
432
  this.loadedData = this.deliveryOrdersInfo || []
431
433
 
432
434
  // Update execution data
@@ -33,6 +33,17 @@ import {
33
33
  } from '../constants'
34
34
 
35
35
  const PICKING_TYPES = { PICKING, BATCH_PICKING }
36
+
37
+ const VALIDATE_QC_SEALS = gql`
38
+ query validateQcSeals($releaseGoodNo: String!) {
39
+ validateQcSeals(releaseGoodNo: $releaseGoodNo) {
40
+ valid
41
+ error
42
+ minimumSealNumber
43
+ }
44
+ }
45
+ `
46
+
36
47
  class PickingProduct extends connect(store)(localize(i18next)(PageView)) {
37
48
  static get properties() {
38
49
  return {
@@ -976,14 +987,37 @@ class PickingProduct extends connect(store)(localize(i18next)(PageView)) {
976
987
 
977
988
  async openTotePopup() {
978
989
  await sleep(1010)
990
+
991
+ // Query for minimum seal number setting
992
+ let minimumSealNumber = 0
993
+ try {
994
+ const validationResponse = await client.query({
995
+ query: VALIDATE_QC_SEALS,
996
+ variables: { releaseGoodNo: this.releaseGoodNo },
997
+ context: gqlContext()
998
+ })
999
+
1000
+ if (!validationResponse.errors && validationResponse.data?.validateQcSeals) {
1001
+ minimumSealNumber = validationResponse.data.validateQcSeals.minimumSealNumber || 0
1002
+ }
1003
+ } catch (err) {
1004
+ // If query fails, default to 0 (don't block tote scanning)
1005
+ console.warn('Failed to fetch minimum seal number:', err)
1006
+ }
1007
+
979
1008
  openPopup(
980
1009
  html`
981
1010
  <tote-popup
982
1011
  .orderNo="${this.releaseGoodNo}"
983
1012
  .toteNo="${this.toteNo}"
984
1013
  .bizplace="${this._bizplaceName}"
1014
+ .minimumSealNumber="${minimumSealNumber}"
985
1015
  @completed="${async e => {
986
1016
  this.toteNo = e.detail
1017
+ // Refresh the item list to reflect any undone items
1018
+ await this.fetchInventories(
1019
+ this.refPickingType === PICKING_TYPES.PICKING.value ? this.releaseGoodNo : this.taskNo
1020
+ )
987
1021
  }}"
988
1022
  ></tote-popup>
989
1023
  `,
@@ -21,6 +21,16 @@ import { fetchSettingRule } from '../../fetch-setting-value'
21
21
  import { WORKSHEET_STATUS } from '../constants'
22
22
  import { decodeQR, fetchPageSettings, isValidHttpUrl } from '../../util'
23
23
 
24
+ const VALIDATE_QC_SEALS = gql`
25
+ query validateQcSeals($releaseGoodNo: String!) {
26
+ validateQcSeals(releaseGoodNo: $releaseGoodNo) {
27
+ valid
28
+ error
29
+ minimumSealNumber
30
+ }
31
+ }
32
+ `
33
+
24
34
  class SortingProduct extends connect(store)(localize(i18next)(PageView)) {
25
35
  static get properties() {
26
36
  return {
@@ -515,14 +525,34 @@ class SortingProduct extends connect(store)(localize(i18next)(PageView)) {
515
525
 
516
526
  async _openTotePopup() {
517
527
  await sleep(1010)
528
+
529
+ // Query for minimum seal number setting
530
+ let minimumSealNumber = 0
531
+ try {
532
+ const validationResponse = await client.query({
533
+ query: VALIDATE_QC_SEALS,
534
+ variables: { releaseGoodNo: this._selectedReleaseGood }
535
+ })
536
+
537
+ if (!validationResponse.errors && validationResponse.data?.validateQcSeals) {
538
+ minimumSealNumber = validationResponse.data.validateQcSeals.minimumSealNumber || 0
539
+ }
540
+ } catch (err) {
541
+ // If query fails, default to 0 (don't block tote scanning)
542
+ console.warn('Failed to fetch minimum seal number:', err)
543
+ }
544
+
518
545
  openPopup(
519
546
  html`
520
547
  <tote-popup
521
548
  .orderNo="${this._selectedReleaseGood}"
522
549
  .toteNo="${this.toteNo}"
523
550
  .bizplace="${this.bizplaceName}"
551
+ .minimumSealNumber="${minimumSealNumber}"
524
552
  @completed="${async e => {
525
553
  this.toteNo = e.detail
554
+ // Refresh the item list to reflect any undone items
555
+ await this._fetchItemList(this._selectedReleaseGood)
526
556
  }}"
527
557
  ></tote-popup>
528
558
  `,