@things-factory/operato-wms 4.3.761 → 4.3.764

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.
@@ -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
  }
@@ -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
  `,
@@ -6,7 +6,7 @@ import { css, html, LitElement } from 'lit-element'
6
6
 
7
7
  import { MultiColumnFormStyles, SingleColumnFormStyles } from '@things-factory/form-ui'
8
8
  import { i18next, localize } from '@things-factory/i18n-base'
9
- import { client } from '@things-factory/shell'
9
+ import { client, CustomAlert } from '@things-factory/shell'
10
10
  import { ScrollbarStyles } from '@things-factory/styles'
11
11
  import { isMobileDevice } from '@things-factory/utils'
12
12
  import { TOTE_STATUS } from '../constants'
@@ -20,6 +20,7 @@ class TotePopup extends localize(i18next)(LitElement) {
20
20
  data: Object,
21
21
  orderNo: String,
22
22
  toteNo: String,
23
+ minimumSealNumber: Number,
23
24
  _totalSeal: Number,
24
25
  _selectedToteNo: String,
25
26
  _toteNoList: Object,
@@ -76,11 +77,26 @@ class TotePopup extends localize(i18next)(LitElement) {
76
77
  .break {
77
78
  word-wrap: break-word;
78
79
  }
80
+ .mobile-hint {
81
+ padding: 10px;
82
+ background-color: var(--primary-light-color, #f0f0f0);
83
+ border-left: 4px solid var(--primary-color, #2196f3);
84
+ margin: 10px;
85
+ font-size: 0.9em;
86
+ }
87
+ .refresh-button {
88
+ margin: 10px;
89
+ display: flex;
90
+ justify-content: center;
91
+ }
79
92
  `
80
93
  ]
81
94
  }
82
95
 
83
96
  render() {
97
+ const shouldShowSealSection = this._shouldShowSealSection()
98
+ const sealInputDisabled = !this.toteNo || this.toteNo === ''
99
+
84
100
  return html`
85
101
  <form class="multi-column-form">
86
102
  <fieldset>
@@ -121,34 +137,46 @@ class TotePopup extends localize(i18next)(LitElement) {
121
137
  </fieldset>
122
138
  </form>
123
139
 
124
- <form class="multi-column-form">
125
- <fieldset>
126
- <label>${i18next.t('field.seal_no')}</label>
127
- <barcode-scanable-input
128
- name="sealNo"
129
- custom-input
130
- @keypress="${e => {
131
- if (e.keyCode === 13) {
132
- e.preventDefault()
133
- this.sealNo = this.sealNoInput.value
134
- if (this.toteNo) {
135
- this._sealTote()
136
- } else {
137
- this._showToast({ message: i18next.t('text.tote_is_not_scanned') })
138
- }
139
- }
140
- }}"
141
- ></barcode-scanable-input>
142
- </fieldset>
143
-
144
- <label>${i18next.t('field.scanned')} (${this._totalSeal || 0})</label>
145
- <select name="sealNoList">
146
- <option value="">-- ${i18next.t('text.seal_number')} --</option>
147
- ${(this._sealNoList?.orderToteSeals || []).map(
148
- list => html` <option value="${list.name}">${list.name}</option> `
149
- )}
150
- </select>
151
- </form>
140
+ ${shouldShowSealSection
141
+ ? html`
142
+ <form class="multi-column-form">
143
+ <fieldset>
144
+ <label>${i18next.t('field.seal_no')}</label>
145
+ <barcode-scanable-input
146
+ name="sealNo"
147
+ custom-input
148
+ ?disabled="${sealInputDisabled}"
149
+ @keypress="${e => {
150
+ if (e.keyCode === 13) {
151
+ e.preventDefault()
152
+ this.sealNo = this.sealNoInput.value
153
+ if (this.toteNo) {
154
+ this._sealTote()
155
+ } else {
156
+ this._showToast({ message: i18next.t('text.tote_is_not_scanned') })
157
+ }
158
+ }
159
+ }}"
160
+ ></barcode-scanable-input>
161
+ </fieldset>
162
+
163
+ <label>${i18next.t('field.scanned')} (${this._totalSeal || 0})</label>
164
+ <select name="sealNoList">
165
+ <option value="">-- ${i18next.t('text.seal_number')} --</option>
166
+ ${(this._sealNoList?.orderToteSeals || []).map(
167
+ list => html` <option value="${list.name}">${list.name}</option> `
168
+ )}
169
+ </select>
170
+ </form>
171
+ `
172
+ : ''}
173
+ ${isMobileDevice() && this._selectedToteNo?.length > 0
174
+ ? html`
175
+ <div class="mobile-hint">
176
+ ${i18next.t('text.viewing_items_in_tote')}: <strong>${this._selectedToteNo[0].name}</strong>
177
+ </div>
178
+ `
179
+ : ''}
152
180
 
153
181
  <div class="grist-container">
154
182
  <div class="grist">
@@ -161,9 +189,35 @@ class TotePopup extends localize(i18next)(LitElement) {
161
189
  </div>
162
190
  </div>
163
191
 
192
+ ${isMobileDevice()
193
+ ? html`
194
+ <div class="refresh-button">
195
+ <mwc-button
196
+ icon="refresh"
197
+ @click=${async () => {
198
+ if (this.grist) {
199
+ await this.grist.fetch()
200
+ this._showToast({ type: 'info', message: i18next.t('text.list_refreshed') })
201
+ }
202
+ }}
203
+ label="${i18next.t('button.refresh')}"
204
+ ></mwc-button>
205
+ </div>
206
+ `
207
+ : ''}
208
+
164
209
  <div class="button-container">
210
+ ${this._shouldShowUndoButton()
211
+ ? html`
212
+ <mwc-button
213
+ @click=${this._undoSelectedItems.bind(this)}
214
+ raised
215
+ style="--mdc-theme-primary: var(--status-warning-color, #ff9800); margin-right: 10px;"
216
+ label="${i18next.t('button.undo')}"
217
+ ></mwc-button>
218
+ `
219
+ : ''}
165
220
  <mwc-button
166
- danger
167
221
  @click=${() => {
168
222
  this.dispatchEvent(
169
223
  new CustomEvent('completed', {
@@ -188,11 +242,25 @@ class TotePopup extends localize(i18next)(LitElement) {
188
242
  }
189
243
 
190
244
  get sealNoInput() {
191
- return this.shadowRoot.querySelector('barcode-scanable-input[name=sealNo]').shadowRoot.querySelector('input')
245
+ return this.shadowRoot.querySelector('barcode-scanable-input[name=sealNo]')?.shadowRoot?.querySelector('input')
246
+ }
247
+
248
+ _shouldShowSealSection() {
249
+ // Only show seal section if minimumSealNumber is greater than 0
250
+ return (this.minimumSealNumber || 0) > 0
251
+ }
252
+
253
+ _shouldShowUndoButton() {
254
+ // Show undo button only if selected tote is not sealed
255
+ const selectedTote = this._selectedToteNo?.[0]
256
+ return selectedTote && !selectedTote?.closedDate
192
257
  }
193
258
 
194
259
  _focusOnSealNoInput() {
195
- setTimeout(() => this.sealNoInput.focus(), 50)
260
+ const sealInput = this.sealNoInput
261
+ if (sealInput) {
262
+ setTimeout(() => sealInput.focus(), 50)
263
+ }
196
264
  }
197
265
 
198
266
  _focusOnToteNoInput() {
@@ -201,28 +269,36 @@ class TotePopup extends localize(i18next)(LitElement) {
201
269
 
202
270
  async firstUpdated() {
203
271
  this.config = {
272
+ list: { fields: ['sku', 'packingType', 'pickedQty'] },
204
273
  pagination: { pages: [10, 20, 50, 100] },
205
274
  rows: {
206
- appendable: false
275
+ appendable: false,
276
+ selectable: {
277
+ multiple: true
278
+ }
207
279
  },
208
280
  columns: [
209
281
  { type: 'gutter', gutterName: 'sequence' },
282
+ { type: 'gutter', gutterName: 'row-selector', multiple: true },
210
283
  {
211
284
  type: 'string',
212
285
  name: 'sku',
213
286
  header: i18next.t('field.sku'),
287
+ label: true,
214
288
  width: 180
215
289
  },
216
290
  {
217
291
  type: 'string',
218
292
  name: 'packingType',
219
293
  header: i18next.t('field.packing_type'),
294
+ label: true,
220
295
  width: 180
221
296
  },
222
297
  {
223
298
  type: 'string',
224
299
  name: 'pickedQty',
225
300
  header: i18next.t('field.picked_qty'),
301
+ label: true,
226
302
  width: 180
227
303
  }
228
304
  ]
@@ -269,7 +345,8 @@ class TotePopup extends localize(i18next)(LitElement) {
269
345
  toteNo: this._selectedToteNo[0].name,
270
346
  sortings,
271
347
  orderNo: this.orderNo
272
- }
348
+ },
349
+ fetchPolicy: 'no-cache'
273
350
  })
274
351
 
275
352
  if (!response.errors) {
@@ -279,7 +356,10 @@ class TotePopup extends localize(i18next)(LitElement) {
279
356
  }
280
357
 
281
358
  if (this.toteNo && this.toteNo !== '') {
282
- this._focusOnSealNoInput()
359
+ // Only focus on seal input if seal section is visible
360
+ if (this._shouldShowSealSection()) {
361
+ this._focusOnSealNoInput()
362
+ }
283
363
  } else {
284
364
  this._focusOnToteNoInput()
285
365
  }
@@ -306,6 +386,21 @@ class TotePopup extends localize(i18next)(LitElement) {
306
386
 
307
387
  if (result) {
308
388
  await this._checkToteAvailability()
389
+
390
+ // After successful tote scan, refresh the tote list and auto-select the scanned tote
391
+ if (this.toteNo && this.toteNo !== '') {
392
+ await this._fetchToteList()
393
+
394
+ // Auto-select the scanned tote for PDA/mobile users
395
+ const scannedTote = this._toteNoList?.find(tote => tote.name === this.toteNo)
396
+ if (scannedTote) {
397
+ this._selectedToteNo = [scannedTote]
398
+ // Trigger grist to fetch items for the selected tote
399
+ if (this.grist) {
400
+ await this.grist.fetch()
401
+ }
402
+ }
403
+ }
309
404
  } else {
310
405
  this.toteNoInput.value = ''
311
406
  this.toteNo = ''
@@ -315,7 +410,10 @@ class TotePopup extends localize(i18next)(LitElement) {
315
410
  }
316
411
 
317
412
  if (this.toteNo && this.toteNo !== '') {
318
- this._focusOnSealNoInput()
413
+ // Only focus on seal input if seal section is visible
414
+ if (this._shouldShowSealSection()) {
415
+ this._focusOnSealNoInput()
416
+ }
319
417
  } else {
320
418
  this._focusOnToteNoInput()
321
419
  }
@@ -420,6 +518,7 @@ class TotePopup extends localize(i18next)(LitElement) {
420
518
  items {
421
519
  id
422
520
  name
521
+ closedDate
423
522
  orderToteSeals {
424
523
  id
425
524
  name
@@ -446,6 +545,97 @@ class TotePopup extends localize(i18next)(LitElement) {
446
545
  }
447
546
  }
448
547
 
548
+ async _undoSelectedItems() {
549
+ // Get selected rows from grist
550
+ const selectedRecords = this.grist.selected
551
+
552
+ if (!selectedRecords || selectedRecords.length === 0) {
553
+ await CustomAlert({
554
+ type: 'info',
555
+ title: i18next.t('text.nothing_selected'),
556
+ text: i18next.t('text.please_select_items_to_undo')
557
+ })
558
+ return
559
+ }
560
+
561
+ // Confirm before deleting
562
+ const itemCount = selectedRecords.length
563
+ const itemText =
564
+ itemCount > 1
565
+ ? `${itemCount} items`
566
+ : `${selectedRecords[0].sku} (${selectedRecords[0].pickedQty} ${selectedRecords[0].packingType})`
567
+
568
+ const answer = await CustomAlert({
569
+ type: 'warning',
570
+ title: i18next.t('button.undo'),
571
+ text: i18next.t('text.are_you_sure_to_remove_x_from_tote', { x: itemText }),
572
+ confirmButton: {
573
+ text: i18next.t('button.confirm'),
574
+ color: '#ff9800'
575
+ },
576
+ cancelButton: {
577
+ text: i18next.t('button.cancel'),
578
+ color: '#cfcfcf'
579
+ }
580
+ })
581
+
582
+ if (!answer.value) return
583
+
584
+ try {
585
+ // Undo all selected items
586
+ for (const record of selectedRecords) {
587
+ await this._unsortItem(record.id, false) // Don't show individual toasts
588
+ }
589
+
590
+ // Show single success message for all items
591
+ this._showToast({
592
+ type: 'info',
593
+ message:
594
+ itemCount > 1
595
+ ? i18next.t('text.items_removed_from_tote', { count: itemCount })
596
+ : i18next.t('text.item_removed_from_tote')
597
+ })
598
+
599
+ // Refresh once after all items are removed
600
+ await this._fetchToteList()
601
+ await this.grist.fetch()
602
+ } catch (e) {
603
+ this._showToast(e)
604
+ }
605
+ }
606
+
607
+ async _unsortItem(orderToteItemId, showToast = true) {
608
+ try {
609
+ const response = await client.mutate({
610
+ mutation: gql`
611
+ mutation unsortItem($orderToteItemId: String!, $reason: String) {
612
+ unsortItem(orderToteItemId: $orderToteItemId, reason: $reason)
613
+ }
614
+ `,
615
+ variables: {
616
+ orderToteItemId,
617
+ reason: 'Removed by user from tote popup'
618
+ }
619
+ })
620
+
621
+ if (!response.errors) {
622
+ if (showToast) {
623
+ this._showToast({
624
+ type: 'info',
625
+ message: i18next.t('text.item_removed_from_tote')
626
+ })
627
+
628
+ // Refresh the tote list and grist
629
+ await this._fetchToteList()
630
+ await this.grist.fetch()
631
+ }
632
+ }
633
+ } catch (e) {
634
+ this._showToast(e)
635
+ throw e // Re-throw to stop batch processing
636
+ }
637
+ }
638
+
449
639
  _showToast({ type, message }) {
450
640
  document.dispatchEvent(
451
641
  new CustomEvent('notify', {