@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
@@ -142,6 +142,29 @@ export class PickingExecution extends PickingExecutionBase {
142
142
  background: none;
143
143
  color: #f3f2f7;
144
144
  }
145
+ .picking-banner {
146
+ background-color: #fff3cd;
147
+ border: 1px solid #ffc107;
148
+ border-radius: 4px;
149
+ padding: 12px 16px;
150
+ margin: 10px;
151
+ display: flex;
152
+ justify-content: space-between;
153
+ align-items: center;
154
+ color: #856404;
155
+ }
156
+ .picking-banner button {
157
+ background-color: #007e7f;
158
+ color: white;
159
+ border: none;
160
+ padding: 6px 12px;
161
+ border-radius: 4px;
162
+ cursor: pointer;
163
+ font-weight: bold;
164
+ }
165
+ .picking-banner button:hover {
166
+ background-color: #005f60;
167
+ }
145
168
  `
146
169
  ]
147
170
  }
@@ -212,6 +235,41 @@ export class PickingExecution extends PickingExecutionBase {
212
235
  }
213
236
  }
214
237
 
238
+ /**
239
+ * Determines if the "Go to PICKED" banner should be shown.
240
+ * Banner appears when:
241
+ * - Current tab is PICKING
242
+ * - PICKING tab has no items (pickingData.length === 0) - uses SAME source as Total SKU
243
+ * - PICKED tab has items (pickedData.length > 0)
244
+ *
245
+ * Uses the same data source as Total SKU display to ensure consistency.
246
+ */
247
+ _shouldShowPickingBanner() {
248
+ // Banner only shows on PICKING tab
249
+ if (this.state?.activeTab !== 'PICKING') {
250
+ return false
251
+ }
252
+
253
+ // Use the SAME source as Total SKU display: this.pickingData?.length
254
+ const pickingItemCount = this.pickingData?.length || 0
255
+ const pickedItemCount = this.pickedData?.length || 0
256
+
257
+ // Show banner when PICKING is empty but PICKED has items
258
+ const shouldShow = pickingItemCount === 0 && pickedItemCount > 0
259
+
260
+ // Console assert: catch mismatch if Total SKU > 0 but banner shows
261
+ if (shouldShow && pickingItemCount > 0) {
262
+ console.error('BANNER MISMATCH: Banner showing but Total SKU > 0', {
263
+ pickingItemCount,
264
+ pickedItemCount,
265
+ pickingDataLength: this.pickingData?.length,
266
+ pickedDataLength: this.pickedData?.length
267
+ })
268
+ }
269
+
270
+ return shouldShow
271
+ }
272
+
215
273
  get pickedListItemConfig() {
216
274
  return {
217
275
  columnLeft: {
@@ -371,6 +429,23 @@ export class PickingExecution extends PickingExecutionBase {
371
429
  </div>
372
430
  <!-- Tab Info Section End -->
373
431
 
432
+ ${
433
+ this._shouldShowPickingBanner()
434
+ ? html`
435
+ <div class="picking-banner">
436
+ <span
437
+ >${i18next.t('text.all_items_picked_proceed_to_picked', {
438
+ defaultValue: 'All items picked. Proceed to PICKED to complete picking.'
439
+ })}</span
440
+ >
441
+ <button @click=${async e => await this.openTab(e, 'PICKED')}>
442
+ ${i18next.t('button.go_to_picked', { defaultValue: 'Go to PICKED' })}
443
+ </button>
444
+ </div>
445
+ `
446
+ : ''
447
+ }
448
+
374
449
  <!-- Picking Item Section -->
375
450
  <div id="picking" class="tab-content ${this.state?.activeTab == 'PICKING' ? 'active' : ''}">
376
451
  <product-list-view .config=${this.pickingListItemConfig} .data=${this.pickingData}
@@ -0,0 +1,253 @@
1
+ import '@things-factory/grist-ui'
2
+ import '@material/mwc-button'
3
+
4
+ import { css, html, LitElement } from 'lit-element'
5
+
6
+ import { MultiColumnFormStyles, SingleColumnFormStyles } from '@things-factory/form-ui'
7
+ import { i18next, localize } from '@things-factory/i18n-base'
8
+
9
+ class RouteLabelHistoryDialog extends localize(i18next)(LitElement) {
10
+ static get styles() {
11
+ return [
12
+ MultiColumnFormStyles,
13
+ SingleColumnFormStyles,
14
+ css`
15
+ :host {
16
+ padding: 10px;
17
+ display: flex;
18
+ flex-direction: column;
19
+ overflow: hidden;
20
+ background-color: var(--main-section-background-color);
21
+ height: 100%;
22
+ max-height: calc(100vh - 200px);
23
+ }
24
+ .history-dialog-content {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: 12px;
28
+ flex: 1;
29
+ min-height: 0;
30
+ overflow: hidden;
31
+ }
32
+ .history-grist {
33
+ flex: 1;
34
+ min-height: 0;
35
+ border: 1px solid var(--form-border-color);
36
+ background: var(--main-section-background-color);
37
+ overflow: hidden;
38
+ }
39
+ .history-grist data-grist {
40
+ height: 100%;
41
+ overflow-y: auto;
42
+ }
43
+ .button-container {
44
+ padding: 10px 0 12px 0;
45
+ text-align: center;
46
+ flex-shrink: 0;
47
+ }
48
+ `
49
+ ]
50
+ }
51
+
52
+ static get properties() {
53
+ return {
54
+ target: Object,
55
+ logs: Array,
56
+ appendSeqField: String,
57
+ routeLabel: Object,
58
+ onReprint: Function
59
+ }
60
+ }
61
+
62
+ constructor() {
63
+ super()
64
+ this.logs = []
65
+ this._historyReprintSeq = ''
66
+ }
67
+
68
+ get historyGrist() {
69
+ return this.shadowRoot.querySelector('#history-grist')
70
+ }
71
+
72
+ _showToast({ type, message }) {
73
+ document.dispatchEvent(
74
+ new CustomEvent('notify', {
75
+ detail: {
76
+ type,
77
+ message
78
+ }
79
+ })
80
+ )
81
+ }
82
+
83
+ _validateSequence(seq) {
84
+ if (!seq || !seq.trim()) {
85
+ return { valid: false, error: '' }
86
+ }
87
+
88
+ const seqTrimmed = seq.trim()
89
+
90
+ // Check for decimal points in the input
91
+ // Split by comma first to check each part separately
92
+ const parts = seqTrimmed.split(',')
93
+ for (const part of parts) {
94
+ if (!part) continue
95
+ const trimmedPart = part.trim()
96
+ // Check if the part contains a decimal point
97
+ if (trimmedPart.includes('.')) {
98
+ return {
99
+ valid: false,
100
+ error: i18next.t('text.print_qty_must_be_integer')
101
+ }
102
+ }
103
+ // Check if it's a range (contains '-')
104
+ if (trimmedPart.includes('-')) {
105
+ const rangeParts = trimmedPart.split('-')
106
+ for (const rangePart of rangeParts) {
107
+ const trimmedRangePart = rangePart.trim()
108
+ if (trimmedRangePart.includes('.')) {
109
+ return {
110
+ valid: false,
111
+ error: i18next.t('text.print_qty_must_be_integer')
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ // Parse the input to support ranges (e.g., "1-2") and specific numbers (e.g., "1" or "1,3,5-7")
119
+ const parseSeqInput = input => {
120
+ if (!input || typeof input !== 'string') return []
121
+ const cleaned = input.replace(/\s/g, '')
122
+ if (!cleaned) return []
123
+ const parts = cleaned.split(',')
124
+ const numbers = new Set()
125
+ for (const part of parts) {
126
+ if (!part) continue
127
+ const range = part.split('-')
128
+ if (range.length === 1) {
129
+ const n = parseInt(range[0], 10)
130
+ if (Number.isFinite(n) && n > 0) numbers.add(n)
131
+ } else if (range.length === 2) {
132
+ const a = parseInt(range[0], 10)
133
+ const b = parseInt(range[1], 10)
134
+ if (Number.isFinite(a) && Number.isFinite(b) && a > 0 && b > 0) {
135
+ const from = Math.min(a, b)
136
+ const to = Math.max(a, b)
137
+ for (let i = from; i <= to; i++) numbers.add(i)
138
+ }
139
+ }
140
+ }
141
+ return Array.from(numbers).sort((x, y) => x - y)
142
+ }
143
+
144
+ const parsedSeqs = parseSeqInput(seqTrimmed)
145
+ if (parsedSeqs.length === 0) {
146
+ return { valid: false, error: i18next.t('text.invalid_sequence_number') }
147
+ }
148
+
149
+ // Check if all parsed sequences fall within any of the history ranges
150
+ const allValid = parsedSeqs.every(seqNum => {
151
+ return (this.logs || []).some(log => {
152
+ const from = parseInt(log.from, 10)
153
+ const to = parseInt(log.to, 10)
154
+ return !isNaN(from) && !isNaN(to) && seqNum >= from && seqNum <= to
155
+ })
156
+ })
157
+
158
+ if (!allValid) {
159
+ const ranges = (this.logs || []).map(log => `${log.from}-${log.to}`).join(', ')
160
+ return {
161
+ valid: false,
162
+ error: i18next.t('text.sequence_out_of_range', { ranges })
163
+ }
164
+ }
165
+
166
+ return { valid: true, error: '' }
167
+ }
168
+
169
+ render() {
170
+ return html`
171
+ <div class="history-dialog-content">
172
+ <form class="multi-column-form">
173
+ <label>${i18next.t('field.ro')}</label>
174
+ <input disabled .value="${this.target?.name || ''}" />
175
+
176
+ <label>${i18next.t('field.sequence_reference')}</label>
177
+ <input
178
+ disabled
179
+ .value="${this.appendSeqField === 'refNo'
180
+ ? i18next.t('field.ref_no')
181
+ : this.appendSeqField === 'refNo2'
182
+ ? i18next.t('field.ref_no_2')
183
+ : i18next.t('field.ref_no_3')}"
184
+ />
185
+
186
+ <label>${i18next.t('field.reprint_seq')}</label>
187
+ <input
188
+ placeholder="${i18next.t('text.reprint_seq_placeholder')}"
189
+ .value="${this._historyReprintSeq || ''}"
190
+ @input="${e => {
191
+ this._historyReprintSeq = e.currentTarget.value
192
+ }}"
193
+ />
194
+ </form>
195
+
196
+ <div class="history-grist">
197
+ <data-grist
198
+ id="history-grist"
199
+ .mode=${'GRID'}
200
+ .config=${{
201
+ pagination: { infinite: true },
202
+ rows: { appendable: false, selectable: false },
203
+ list: { fields: ['range', 'printedAt', 'creatorName'] },
204
+ columns: [
205
+ { type: 'gutter', gutterName: 'sequence' },
206
+ { type: 'string', name: 'range', header: i18next.t('field.range'), width: 160 },
207
+ { type: 'string', name: 'printedAt', header: i18next.t('field.printed_at'), width: 190 },
208
+ { type: 'string', name: 'creatorName', header: i18next.t('field.last_printed_by'), width: 140 }
209
+ ]
210
+ }}
211
+ .data=${{ records: this.logs || [], total: (this.logs || []).length }}
212
+ ></data-grist>
213
+ </div>
214
+ </div>
215
+
216
+ <div class="button-container">
217
+ <mwc-button
218
+ raised
219
+ danger
220
+ @click="${async () => {
221
+ if (this.onReprint) {
222
+ const reprintSeq = (this._historyReprintSeq || '').trim()
223
+ if (!reprintSeq) {
224
+ // Show toast message - handled in _reprintSelectedFromHistory
225
+ await this.onReprint(reprintSeq)
226
+ return
227
+ }
228
+
229
+ // Validate sequence before reprinting
230
+ const validation = this._validateSequence(reprintSeq)
231
+ if (!validation.valid) {
232
+ if (validation.error) {
233
+ this._showToast({
234
+ type: 'error',
235
+ message: validation.error
236
+ })
237
+ }
238
+ return
239
+ }
240
+
241
+ await this.onReprint(reprintSeq)
242
+ }
243
+ }}"
244
+ >
245
+ ${i18next.t('button.reprint')}
246
+ </mwc-button>
247
+ <mwc-button @click="${() => history.back()}">${i18next.t('button.cancel')}</mwc-button>
248
+ </div>
249
+ `
250
+ }
251
+ }
252
+
253
+ window.customElements.define('route-label-history-dialog', RouteLabelHistoryDialog)