@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
@@ -1,6 +1,8 @@
1
1
  import '@things-factory/barcode-ui'
2
2
  import '@things-factory/grist-ui'
3
3
  import '@material/mwc-icon'
4
+ import '@material/mwc-button'
5
+ import './route-label-history-dialog'
4
6
 
5
7
  import gql from 'graphql-tag'
6
8
  import { css, html, LitElement } from 'lit-element'
@@ -9,6 +11,7 @@ import { connect } from 'pwa-helpers/connect-mixin'
9
11
  import { DirectPrinter } from '@things-factory/barcode-base'
10
12
  import { MultiColumnFormStyles, SingleColumnFormStyles } from '@things-factory/form-ui'
11
13
  import { i18next, localize } from '@things-factory/i18n-base'
14
+ import { openPopup } from '@things-factory/layout-base'
12
15
  import { client, store } from '@things-factory/shell'
13
16
  import { isMobileDevice } from '@things-factory/utils'
14
17
 
@@ -63,14 +66,9 @@ class RouteLabelPopup extends connect(store)(localize(i18next)(LitElement)) {
63
66
  color: var(--search-form-icon-color);
64
67
  }
65
68
 
66
- .checkbox-container {
67
- display: flex;
68
- align-items: center;
69
- gap: 10px; /* Adjust gap as needed */
70
- }
71
-
72
- .checkbox-container label {
73
- margin-left: 5px; /* Adjust margin as needed */
69
+ .grid-break {
70
+ grid-column: span 24;
71
+ height: 0;
74
72
  }
75
73
  `
76
74
  ]
@@ -83,6 +81,7 @@ class RouteLabelPopup extends connect(store)(localize(i18next)(LitElement)) {
83
81
  partnerId: String,
84
82
  refNoFilter: String,
85
83
  worksheetType: String,
84
+ sourceWorksheetType: String,
86
85
  _binNumberList: Array,
87
86
  _searchFields: Array,
88
87
  _priorityList: Array,
@@ -91,7 +90,12 @@ class RouteLabelPopup extends connect(store)(localize(i18next)(LitElement)) {
91
90
  _selectedBinNumber: String,
92
91
  _enableToteScanning: Boolean,
93
92
  _enableBinScanning: Boolean,
94
- orderInventories: Object
93
+ orderInventories: Object,
94
+ _viewMode: String,
95
+ _appendSeqField: String,
96
+ _historyTarget: Object,
97
+ _historyLogs: Array,
98
+ _historyReprintSeq: String
95
99
  }
96
100
  }
97
101
 
@@ -119,84 +123,160 @@ class RouteLabelPopup extends connect(store)(localize(i18next)(LitElement)) {
119
123
 
120
124
  constructor() {
121
125
  super()
126
+ this._viewMode = 'SKU_TOTE'
127
+ this._appendSeqField = 'refNo'
128
+ }
129
+
130
+ async _sleep(ms = 0) {
131
+ await new Promise(resolve => setTimeout(resolve, ms))
132
+ }
133
+
134
+ _formatDateTime(value) {
135
+ try {
136
+ if (!value) return ''
137
+ if (value instanceof Date) return value.toLocaleString()
138
+ const d = new Date(value)
139
+ if (!Number.isNaN(d.getTime())) return d.toLocaleString()
140
+ const n = typeof value === 'number' ? value : parseInt(value, 10)
141
+ if (Number.isFinite(n)) {
142
+ const dn = new Date(n)
143
+ if (!Number.isNaN(dn.getTime())) return dn.toLocaleString()
144
+ }
145
+ return String(value)
146
+ } catch (e) {
147
+ return ''
148
+ }
122
149
  }
123
150
 
124
151
  render() {
125
152
  return html`
126
153
  <div>
127
154
  <form class="multi-column-form">
128
- ${this.worksheetType == 'merged'
155
+ <label>${i18next.t('field.filter_by')}</label>
156
+ <select
157
+ name="viewMode"
158
+ @change="${e => {
159
+ this._viewMode = e.currentTarget.value
160
+ this._setupConfig()
161
+ this.requestUpdate()
162
+ this.dataGrist.fetch()
163
+ }}"
164
+ >
165
+ <option value="SKU_TOTE" ?selected=${this._viewMode === 'SKU_TOTE'}>SKU/Tote</option>
166
+ <option value="RO" ?selected=${this._viewMode === 'RO'}>RO</option>
167
+ </select>
168
+ <span class="grid-break"></span>
169
+
170
+ ${this._viewMode === 'SKU_TOTE'
129
171
  ? html`
130
- <label>${i18next.t('field.priority')}</label>
131
- <select
132
- name="priority"
133
- @change="${e => {
134
- this._selectedPriority = this._priorityList.filter(itm => itm.value == e.currentTarget.value)
172
+ ${this.worksheetType == 'merged'
173
+ ? html`
174
+ <label>${i18next.t('field.priority')}</label>
175
+ <select
176
+ name="priority"
177
+ @change="${e => {
178
+ this._selectedPriority = this._priorityList.filter(itm => itm.value == e.currentTarget.value)
179
+ }}"
180
+ >
181
+ ${(this._priorityList || []).map(
182
+ list => html` <option value=${list?.value}>${list?.name}</option> `
183
+ )}
184
+ </select>
185
+ `
186
+ : ''}
187
+ ${this._enableBinScanning
188
+ ? html`
189
+ <label>${i18next.t('field.bin_number')}</label>
190
+ <select
191
+ name="binNumber"
192
+ @change="${e => {
193
+ this._selectedBinNumber = this._binNumberList.filter(itm => itm.id == e.currentTarget.value)
194
+ if (this._selectedBinNumber.length == 0) this._selectedBinNumber = e.currentTarget.value
195
+ this._changeCheckbox()
196
+ }}"
197
+ >
198
+ <option value="" selected></option>
199
+ <option value="all">Show All</option>
200
+ ${(this._binNumberList || []).map(
201
+ list => html` <option value=${list.id}>${list.name}</option> `
202
+ )}
203
+ </select>
204
+ `
205
+ : ''}
206
+
207
+ <label for="refNo">${i18next.t('field.ref_no')}</label>
208
+ <input
209
+ type="text"
210
+ name="refNo"
211
+ @input="${e => {
212
+ this.refNoFilter = e.currentTarget.value
135
213
  }}"
136
- >
137
- ${(this._priorityList || []).map(list => html` <option value=${list?.value}>${list?.name}</option> `)}
138
- </select>
214
+ @keypress="${e => {
215
+ if (e.key === 'Enter') {
216
+ this.dataGrist.fetch()
217
+ e.preventDefault()
218
+ }
219
+ }}"
220
+ />
221
+
222
+ <div options-wrap>
223
+ <input
224
+ type="checkbox"
225
+ checked
226
+ id="withSku"
227
+ name="withSku"
228
+ @change="${() => this._changeCheckbox()}"
229
+ />
230
+ <label for="withSku">${i18next.t('field.sku')}</label>
231
+
232
+ ${this._enableToteScanning
233
+ ? html`
234
+ <input
235
+ type="checkbox"
236
+ id="withTote"
237
+ name="withTote"
238
+ @change="${() => this._changeCheckbox()}"
239
+ />
240
+ <label for="withTote">${i18next.t('field.tote')}</label>
241
+ `
242
+ : ''}
243
+ </div>
139
244
  `
140
- : ''}
141
- ${this._enableBinScanning
142
- ? html`
143
- <label>${i18next.t('field.bin_number')}</label>
245
+ : html`
246
+ <label>${i18next.t('field.sequence_reference')}</label>
144
247
  <select
145
- name="binNumber"
146
- @change="${e => {
147
- this._selectedBinNumber = this._binNumberList.filter(itm => itm.id == e.currentTarget.value)
148
- if (this._selectedBinNumber.length == 0) this._selectedBinNumber = e.currentTarget.value
149
- this._changeCheckbox()
248
+ name="appendSeqField"
249
+ @change="${async e => {
250
+ this._appendSeqField = e.currentTarget.value
251
+ await this.dataGrist.fetch()
150
252
  }}"
151
253
  >
152
- <option value="" selected></option>
153
- <option value="all">Show All</option>
154
- ${(this._binNumberList || []).map(list => html` <option value=${list.id}>${list.name}</option> `)}
254
+ <option value="refNo" ?selected=${this._appendSeqField === 'refNo'}>
255
+ ${i18next.t('field.ref_no')}
256
+ </option>
257
+ <option value="refNo2" ?selected=${this._appendSeqField === 'refNo2'}>
258
+ ${i18next.t('field.ref_no_2')}
259
+ </option>
260
+ <option value="refNo3" ?selected=${this._appendSeqField === 'refNo3'}>
261
+ ${i18next.t('field.ref_no_3')}
262
+ </option>
155
263
  </select>
156
- `
157
- : ''}
158
-
159
- <label for="refNo">${i18next.t('field.ref_no')}</label>
160
- <input
161
- type="text"
162
- name="refNo"
163
- @input="${e => {
164
- this.refNoFilter = e.currentTarget.value
165
- }}"
166
- @keypress="${e => {
167
- if (e.key === 'Enter') {
168
- this.dataGrist.fetch()
169
- e.preventDefault()
170
- }
171
- }}"
172
- }}
173
- />
174
- <div class="checkbox-container">
175
- <input
176
- type="checkbox"
177
- checked
178
- id="withSku"
179
- name="withSku"
180
- @change="${e => {
181
- this._changeCheckbox()
182
- }}"
183
- />
184
- <label for="withSku">${i18next.t('field.sku')}</label>
185
-
186
- ${this._enableToteScanning
187
- ? html`
188
- <input
189
- type="checkbox"
190
- id="withTote"
191
- name="withTote"
192
- @change="${e => {
193
- this._changeCheckbox()
194
- }}"
195
- />
196
- <label for="withTote">${i18next.t('field.tote')}</label>
197
- `
198
- : ''}
199
- </div>
264
+
265
+ <label for="refNo">${i18next.t('field.ref_no')}</label>
266
+ <input
267
+ type="text"
268
+ name="refNo"
269
+ @input="${e => {
270
+ this.refNoFilter = e.currentTarget.value
271
+ }}"
272
+ @keypress="${e => {
273
+ if (e.key === 'Enter') {
274
+ this.dataGrist.fetch()
275
+ e.preventDefault()
276
+ }
277
+ }}"
278
+ />
279
+ `}
200
280
  </form>
201
281
  <mwc-icon id="search" @click=${e => this.dataGrist.fetch()} search>search</mwc-icon>
202
282
  </div>
@@ -206,7 +286,7 @@ class RouteLabelPopup extends connect(store)(localize(i18next)(LitElement)) {
206
286
  .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
207
287
  .config=${this.config}
208
288
  .fetchHandler=${this.fetchHandler.bind(this)}
209
- @field-change="${this._packingSizeChange.bind(this)}"
289
+ @field-change="${this._onFieldChange.bind(this)}"
210
290
  ></data-grist>
211
291
  </div>
212
292
 
@@ -232,7 +312,7 @@ class RouteLabelPopup extends connect(store)(localize(i18next)(LitElement)) {
232
312
 
233
313
  async firstUpdated() {
234
314
  await this.fetchLocations()
235
- this._enableToteScanning = await fetchSettingRule('enable-tote-scanning')
315
+ this._enableToteScanning = parseInt((await fetchSettingRule('enable-tote-scanning')) || '0', 10) >= 1
236
316
  this._enableBinScanning = await fetchSettingRule('enable-bin-picking')
237
317
 
238
318
  this._priorityList = [
@@ -247,6 +327,47 @@ class RouteLabelPopup extends connect(store)(localize(i18next)(LitElement)) {
247
327
  }
248
328
  ]
249
329
 
330
+ this._setupConfig()
331
+ }
332
+
333
+ _setupConfig() {
334
+ if (this._viewMode === 'RO') {
335
+ this.config = {
336
+ rows: { appendable: false, selectable: { multiple: true } },
337
+ pagination: { infinite: true },
338
+ list: {
339
+ fields: ['name', 'refNo', 'refNo2', 'refNo3', 'printQty', 'lastSeq', 'history']
340
+ },
341
+ columns: [
342
+ { type: 'gutter', gutterName: 'sequence' },
343
+ { type: 'gutter', gutterName: 'row-selector', multiple: true },
344
+ {
345
+ type: 'gutter',
346
+ gutterName: 'button',
347
+ icon: 'history',
348
+ handlers: {
349
+ click: (_columns, _data, _column, record) => this._openHistoryDialog(record)
350
+ }
351
+ },
352
+ { type: 'string', name: 'name', header: i18next.t('field.ro'), sortable: true, width: 160 },
353
+ { type: 'string', name: 'refNo', header: i18next.t('field.ref_no'), sortable: true, width: 150 },
354
+ { type: 'string', name: 'refNo2', header: i18next.t('field.ref_no_2'), sortable: true, width: 150 },
355
+ { type: 'string', name: 'refNo3', header: i18next.t('field.ref_no_3'), sortable: true, width: 150 },
356
+ {
357
+ type: 'string',
358
+ name: 'printQty',
359
+ header: i18next.t('field.print_qty'),
360
+ record: { editable: true },
361
+ width: 120
362
+ },
363
+ { type: 'integer', name: 'lastSeq', header: i18next.t('field.last_seq'), sortable: true, width: 110 },
364
+ { type: 'string', name: 'history', header: i18next.t('field.last_printed_by'), width: 220 }
365
+ ]
366
+ }
367
+ return
368
+ }
369
+
370
+ // Default: SKU/Tote mode (existing behavior)
250
371
  let configColumns = [
251
372
  { type: 'gutter', gutterName: 'sequence' },
252
373
  { type: 'gutter', gutterName: 'row-selector', multiple: true }
@@ -381,6 +502,62 @@ class RouteLabelPopup extends connect(store)(localize(i18next)(LitElement)) {
381
502
  }
382
503
 
383
504
  async fetchHandler({ sorters = [] }) {
505
+ if (this._viewMode === 'RO') {
506
+ try {
507
+ const response = await client.query({
508
+ query: gql`
509
+ query routeLabelReleaseGoodsInExecutingWorksheet(
510
+ $partnerId: String!
511
+ $refNo: String
512
+ $worksheetType: String!
513
+ $seqField: String
514
+ ) {
515
+ routeLabelReleaseGoodsInExecutingWorksheet(
516
+ partnerId: $partnerId
517
+ refNo: $refNo
518
+ worksheetType: $worksheetType
519
+ seqField: $seqField
520
+ ) {
521
+ id
522
+ name
523
+ refNo
524
+ refNo2
525
+ refNo3
526
+ routeId
527
+ storeId
528
+ storeName
529
+ stopId
530
+ createdAt
531
+ lastSeq
532
+ lastPrintedBy
533
+ }
534
+ }
535
+ `,
536
+ variables: {
537
+ partnerId: this.partnerId,
538
+ refNo: this.refNoFilter || null,
539
+ worksheetType: this.sourceWorksheetType || 'PICKING',
540
+ seqField: this._appendSeqField
541
+ }
542
+ })
543
+
544
+ if (!response.errors) {
545
+ const items = response.data.routeLabelReleaseGoodsInExecutingWorksheet || []
546
+ return {
547
+ total: items.length,
548
+ records: items.map(rg => ({
549
+ ...rg,
550
+ printQty: 1,
551
+ history: rg.lastPrintedBy || ''
552
+ }))
553
+ }
554
+ }
555
+ } catch (e) {
556
+ this._showToast(e)
557
+ }
558
+ return { total: 0, records: [] }
559
+ }
560
+
384
561
  const filters = []
385
562
 
386
563
  if (this.worksheetType == 'merged') {
@@ -492,12 +669,76 @@ class RouteLabelPopup extends connect(store)(localize(i18next)(LitElement)) {
492
669
  }
493
670
  }
494
671
 
495
- async _packingSizeChange(e) {
496
- const selectedColumn = e.detail.column.name
672
+ async _onFieldChange(e) {
673
+ const { column, row, after, before } = e.detail
497
674
 
498
- if (selectedColumn === 'packingType') {
499
- this.dataGrist._data.records[e.detail.row].packingSize = e.detail.record.productDetails.find(
500
- item => item.packingType == e.detail.after
675
+ // Handle printQty validation for RO mode
676
+ if (this._viewMode === 'RO' && column.name === 'printQty') {
677
+ const printQtyStr = String(after || '').trim()
678
+ const previousValue = before !== undefined && before !== null ? before : 1
679
+
680
+ // Check if empty
681
+ if (!printQtyStr || printQtyStr === '') {
682
+ this.dataGrist._data.records[row].printQty = previousValue
683
+ this.dataGrist.render()
684
+ this._showToast({
685
+ type: 'error',
686
+ message: i18next.t('text.print_qty_is_required')
687
+ })
688
+ return
689
+ }
690
+
691
+ // Check if it contains decimal point
692
+ if (printQtyStr.includes('.')) {
693
+ this.dataGrist._data.records[row].printQty = previousValue
694
+ this.dataGrist.render()
695
+ this._showToast({
696
+ type: 'error',
697
+ message: i18next.t('text.print_qty_must_be_integer')
698
+ })
699
+ return
700
+ }
701
+
702
+ const qtyNum = parseInt(printQtyStr, 10)
703
+ if (isNaN(qtyNum)) {
704
+ this.dataGrist._data.records[row].printQty = previousValue
705
+ this.dataGrist.render()
706
+ this._showToast({
707
+ type: 'error',
708
+ message: i18next.t('text.print_qty_must_be_valid_number')
709
+ })
710
+ return
711
+ }
712
+
713
+ // Check if the parsed value matches the original (to catch cases like "123abc")
714
+ if (qtyNum.toString() !== printQtyStr) {
715
+ this.dataGrist._data.records[row].printQty = previousValue
716
+ this.dataGrist.render()
717
+ this._showToast({
718
+ type: 'error',
719
+ message: i18next.t('text.print_qty_must_be_valid_integer')
720
+ })
721
+ return
722
+ }
723
+
724
+ if (qtyNum < 1) {
725
+ this.dataGrist._data.records[row].printQty = previousValue
726
+ this.dataGrist.render()
727
+ this._showToast({
728
+ type: 'error',
729
+ message: i18next.t('text.print_qty_must_be_at_least_one')
730
+ })
731
+ return
732
+ }
733
+
734
+ // If validation passes, convert to integer
735
+ this.dataGrist._data.records[row].printQty = qtyNum
736
+ }
737
+
738
+ // Handle packingSize change for SKU_TOTE mode
739
+ if (this._viewMode === 'SKU_TOTE' && column.name === 'packingType') {
740
+ this.dataGrist._data.records[row].packingSize = e.detail.record.productDetails.find(
741
+ item => item.packingType == after
501
742
  ).packingSize
502
743
  }
503
744
  }
@@ -508,73 +749,259 @@ class RouteLabelPopup extends connect(store)(localize(i18next)(LitElement)) {
508
749
 
509
750
  if (!records || records.length == 0) {
510
751
  this._showToast({ message: i18next.t('text.there_is_no_selected_items') })
752
+ return
511
753
  }
512
754
  if (!labelId) {
513
755
  this._showToast({
514
756
  type: 'error',
515
757
  message: `${i18next.t('text.no_label_setting_was_found')}. ${i18next.t('text.please_check_your_setting')}`
516
758
  })
517
- } else {
518
- let index = 1
519
- for (let record of records) {
520
- let max = record.releaseQty ? record.releaseQty / record.packingSize : 1
521
- let commands
522
- for (let qty = 1; qty <= max; qty++) {
523
- let searchParams = new URLSearchParams()
524
-
525
- /* for pallet record mapping */
526
- searchParams.append('roName', record.roName)
527
- if (record?.binNumber) {
528
- searchParams.append('binNumber', record.binNumber)
759
+ return
760
+ }
761
+
762
+ // Validate printQty for RO mode
763
+ if (this._viewMode === 'RO') {
764
+ for (const record of records) {
765
+ const printQty = record.printQty
766
+ if (printQty === undefined || printQty === null || printQty === '') {
767
+ this._showToast({
768
+ type: 'error',
769
+ message: i18next.t('text.print_qty_is_required')
770
+ })
771
+ return
772
+ }
773
+
774
+ // Check if it contains decimal point
775
+ if (String(printQty).includes('.')) {
776
+ this._showToast({
777
+ type: 'error',
778
+ message: i18next.t('text.print_qty_must_be_integer')
779
+ })
780
+ return
781
+ }
782
+
783
+ const qtyNum = parseInt(printQty, 10)
784
+ if (isNaN(qtyNum)) {
785
+ this._showToast({
786
+ type: 'error',
787
+ message: i18next.t('text.print_qty_must_be_valid_number')
788
+ })
789
+ return
790
+ }
791
+
792
+ // Check if the parsed value matches the original (to catch cases like "123abc")
793
+ if (qtyNum.toString() !== String(printQty).trim()) {
794
+ this._showToast({
795
+ type: 'error',
796
+ message: i18next.t('text.print_qty_must_be_valid_integer')
797
+ })
798
+ return
799
+ }
800
+
801
+ if (qtyNum < 1) {
802
+ this._showToast({
803
+ type: 'error',
804
+ message: i18next.t('text.print_qty_must_be_at_least_one')
805
+ })
806
+ return
807
+ }
808
+ }
809
+
810
+ await this._printRouteLabelByRo(records, labelId)
811
+ return
812
+ }
813
+
814
+ let index = 1
815
+ for (let record of records) {
816
+ let max = record.releaseQty ? record.releaseQty / record.packingSize : 1
817
+ let commands = ''
818
+ for (let qty = 1; qty <= max; qty++) {
819
+ let searchParams = new URLSearchParams()
820
+
821
+ /* for pallet record mapping */
822
+ searchParams.append('roName', record.roName)
823
+ if (record?.binNumber) {
824
+ searchParams.append('binNumber', record.binNumber)
825
+ }
826
+ searchParams.append('locationName', record.locationName)
827
+ if (!record?.toteNumber) {
828
+ searchParams.append('qty', record.qty)
829
+ searchParams.append('releaseQty', record.releaseQty)
830
+ searchParams.append('counter', `${qty} of ${max}`)
831
+ searchParams.append('productSku', record.productSku)
832
+ searchParams.append('productName', record.productName)
833
+ } else {
834
+ searchParams.append('productName', record.toteNumber)
835
+ searchParams.append('counter', `${index} of ${records.length}`)
836
+ }
837
+ searchParams.append('stopId', record.stopId)
838
+ searchParams.append('routeId', record.routeId)
839
+ searchParams.append('storeId', record.storeId)
840
+ searchParams.append('storeName', record.storeName)
841
+ searchParams.append('str', `STR: ${record.storeId}`)
842
+ searchParams.append('createdAt', `P/D: ${record.createdAt}`)
843
+ searchParams.append(
844
+ 'storeInformation',
845
+ `(Store ID) ${record.storeId}, (Store Name) ${record.storeName}, (Route ID) ${record.routeId}, (Stop ID) ${record.stopId}`
846
+ )
847
+ try {
848
+ const response = await fetch(`label-command/${labelId}?${searchParams.toString()}`, {
849
+ method: 'GET'
850
+ })
851
+
852
+ if (response.status !== 200) {
853
+ throw `Error : Can't get label command from server (response: ${response.status})`
529
854
  }
530
- searchParams.append('locationName', record.locationName)
531
- if (!record?.toteNumber) {
532
- searchParams.append('qty', record.qty)
533
- searchParams.append('releaseQty', record.releaseQty)
534
- searchParams.append('counter', `${qty} of ${max}`)
535
- searchParams.append('productSku', record.productSku)
536
- searchParams.append('productName', record.productName)
537
- } else {
538
- searchParams.append('productName', record.toteNumber)
539
- searchParams.append('counter', `${index} of ${records.length}`)
855
+
856
+ let command = await response.text()
857
+
858
+ if (!this.printer) {
859
+ this.printer = new DirectPrinter()
540
860
  }
541
- searchParams.append('stopId', record.stopId)
542
- searchParams.append('routeId', record.routeId)
543
- searchParams.append('storeId', record.storeId)
544
- searchParams.append('storeName', record.storeName)
545
- searchParams.append('str', `STR: ${record.storeId}`)
546
- searchParams.append('createdAt', `P/D: ${record.createdAt}`)
547
- searchParams.append(
548
- 'storeInformation',
549
- `(Store ID) ${record.storeId}, (Store Name) ${record.storeName}, (Route ID) ${record.routeId}, (Stop ID) ${record.stopId}`
550
- )
551
- try {
552
- const response = await fetch(`label-command/${labelId}?${searchParams.toString()}`, {
553
- method: 'GET'
554
- })
555
861
 
556
- if (response.status !== 200) {
557
- throw `Error : Can't get label command from server (response: ${response.status})`
558
- }
862
+ commands += command + ' '
863
+ } catch (e) {
864
+ this._showToast(e)
865
+
866
+ delete this.printer
867
+ break
868
+ }
869
+ }
870
+ if (commands && this.printer) {
871
+ this.printer.print(commands)
872
+ await this._sleep(50)
873
+ }
874
+ index++
875
+ }
876
+ }
559
877
 
560
- let command = await response.text()
878
+ async _printRouteLabelByRo(records, labelId) {
879
+ try {
880
+ const pad3 = n => String(n).padStart(3, '0')
561
881
 
562
- if (!this.printer) {
563
- this.printer = new DirectPrinter()
882
+ const parseSeqInput = input => {
883
+ if (!input || typeof input !== 'string') return []
884
+ const cleaned = input.replace(/\s/g, '')
885
+ if (!cleaned) return []
886
+ const parts = cleaned.split(',')
887
+ const numbers = new Set()
888
+ for (const part of parts) {
889
+ if (!part) continue
890
+ const range = part.split('-')
891
+ if (range.length === 1) {
892
+ const n = parseInt(range[0], 10)
893
+ if (Number.isFinite(n) && n > 0) numbers.add(n)
894
+ } else if (range.length === 2) {
895
+ const a = parseInt(range[0], 10)
896
+ const b = parseInt(range[1], 10)
897
+ if (Number.isFinite(a) && Number.isFinite(b) && a > 0 && b > 0) {
898
+ const from = Math.min(a, b)
899
+ const to = Math.max(a, b)
900
+ for (let i = from; i <= to; i++) numbers.add(i)
564
901
  }
902
+ }
903
+ }
904
+ return Array.from(numbers).sort((x, y) => x - y)
905
+ }
565
906
 
566
- commands += command + ' '
567
- } catch (e) {
568
- this._showToast(e)
907
+ for (const record of records) {
908
+ const field = this._appendSeqField || 'refNo'
909
+ const base = (record[field] || '').toString()
569
910
 
570
- delete this.printer
571
- break
911
+ let seqsToPrint = []
912
+
913
+ // Check if reprintSeq is provided (for reprinting specific sequences)
914
+ if (record.reprintSeq) {
915
+ const reprintSeqs = parseSeqInput(record.reprintSeq)
916
+ if (reprintSeqs.length > 0) {
917
+ // Reprint specific sequence numbers (does not increment counter).
918
+ seqsToPrint = reprintSeqs
572
919
  }
573
920
  }
574
- this.printer.print(commands)
575
- index++
576
- setTimeout(50)
921
+
922
+ // If no reprint sequences, reserve a new range (increments counter).
923
+ if (seqsToPrint.length === 0) {
924
+ const printQtyRaw = Number(record.printQty ?? 1)
925
+ const printQty = Number.isFinite(printQtyRaw) && printQtyRaw > 0 ? Math.floor(printQtyRaw) : 1
926
+
927
+ const reserveResp = await client.mutate({
928
+ mutation: gql`
929
+ mutation reserveRouteLabelSequence($releaseGoodId: String!, $field: String!, $qty: Int!) {
930
+ reserveRouteLabelSequence(releaseGoodId: $releaseGoodId, field: $field, qty: $qty) {
931
+ releaseGoodId
932
+ field
933
+ from
934
+ to
935
+ }
936
+ }
937
+ `,
938
+ variables: { releaseGoodId: record.id, field, qty: printQty }
939
+ })
940
+
941
+ if (reserveResp.errors) {
942
+ throw reserveResp.errors[0]
943
+ }
944
+
945
+ const { from, to } = reserveResp.data.reserveRouteLabelSequence
946
+ for (let n = from; n <= to; n++) seqsToPrint.push(n)
947
+
948
+ // Update UI fields locally (best-effort) only when reserving new range
949
+ record.lastSeq = to
950
+ record.history = [record.history, `${from}-${to}`].filter(Boolean).join(', ')
951
+ }
952
+
953
+ let commands = ''
954
+
955
+ for (let idx = 0; idx < seqsToPrint.length; idx++) {
956
+ const seq = seqsToPrint[idx]
957
+ const seqPadded = pad3(seq)
958
+ const appendedValue = `${base}-${seqPadded}`
959
+ const searchParams = new URLSearchParams()
960
+ searchParams.append('roName', record.name)
961
+ searchParams.append('refNo', record.refNo || '')
962
+ searchParams.append('refNo2', record.refNo2 || '')
963
+ searchParams.append('refNo3', record.refNo3 || '')
964
+
965
+ // Override the selected field with appended sequence
966
+ if (field === 'refNo') searchParams.set('refNo', appendedValue)
967
+ if (field === 'refNo2') searchParams.set('refNo2', appendedValue)
968
+ if (field === 'refNo3') searchParams.set('refNo3', appendedValue)
969
+
970
+ searchParams.append('seq', String(seq))
971
+ searchParams.append('seqPadded', seqPadded)
972
+ searchParams.append('counter', `${idx + 1} of ${seqsToPrint.length}`)
973
+
974
+ if (record.stopId != null) searchParams.append('stopId', record.stopId)
975
+ if (record.routeId != null) searchParams.append('routeId', record.routeId)
976
+ if (record.storeId != null) searchParams.append('storeId', record.storeId)
977
+ if (record.storeName != null) searchParams.append('storeName', record.storeName)
978
+ if (record.storeId != null) searchParams.append('str', `STR: ${record.storeId}`)
979
+ if (record.createdAt) searchParams.append('createdAt', `P/D: ${record.createdAt}`)
980
+ searchParams.append(
981
+ 'storeInformation',
982
+ `(Store ID) ${record.storeId || ''}, (Store Name) ${record.storeName || ''}, (Route ID) ${
983
+ record.routeId || ''
984
+ }, (Stop ID) ${record.stopId || ''}`
985
+ )
986
+
987
+ const response = await fetch(`label-command/${labelId}?${searchParams.toString()}`, { method: 'GET' })
988
+ if (response.status !== 200) {
989
+ throw `Error : Can't get label command from server (response: ${response.status})`
990
+ }
991
+ commands += (await response.text()) + ' '
992
+ }
993
+
994
+ if (commands) {
995
+ if (!this.printer) {
996
+ this.printer = new DirectPrinter()
997
+ }
998
+ this.printer.print(commands)
999
+ await this._sleep(50)
1000
+ }
577
1001
  }
1002
+ } catch (e) {
1003
+ this._showToast(e)
1004
+ delete this.printer
578
1005
  }
579
1006
  }
580
1007
 
@@ -700,6 +1127,85 @@ class RouteLabelPopup extends connect(store)(localize(i18next)(LitElement)) {
700
1127
  })
701
1128
  )
702
1129
  }
1130
+
1131
+ async _openHistoryDialog(record) {
1132
+ try {
1133
+ if (!record?.id) return
1134
+
1135
+ const resp = await client.query({
1136
+ query: gql`
1137
+ query routeLabelSequenceLogs($releaseGoodId: String!, $field: String!) {
1138
+ routeLabelSequenceLogs(releaseGoodId: $releaseGoodId, field: $field) {
1139
+ id
1140
+ releaseGoodId
1141
+ field
1142
+ from
1143
+ to
1144
+ qty
1145
+ createdAt
1146
+ creatorName
1147
+ }
1148
+ }
1149
+ `,
1150
+ variables: { releaseGoodId: record.id, field: this._appendSeqField || 'refNo' }
1151
+ })
1152
+
1153
+ if (resp.errors) throw resp.errors[0]
1154
+ const historyLogs = (resp.data.routeLabelSequenceLogs || []).map(l => {
1155
+ return {
1156
+ ...l,
1157
+ range: `${l.from}-${l.to}`,
1158
+ printedAt: this._formatDateTime(l.createdAt)
1159
+ }
1160
+ })
1161
+
1162
+ openPopup(
1163
+ html`
1164
+ <route-label-history-dialog
1165
+ .target="${record}"
1166
+ .logs="${historyLogs}"
1167
+ .appendSeqField="${this._appendSeqField || 'refNo'}"
1168
+ .routeLabel="${this._routeLabel}"
1169
+ .onReprint="${async reprintSeq => {
1170
+ await this._reprintSelectedFromHistory(record, reprintSeq)
1171
+ }}"
1172
+ ></route-label-history-dialog>
1173
+ `,
1174
+ {
1175
+ backdrop: true,
1176
+ size: 'medium',
1177
+ title: i18next.t('text.print_history')
1178
+ }
1179
+ )
1180
+ } catch (e) {
1181
+ this._showToast(e)
1182
+ }
1183
+ }
1184
+
1185
+ async _reprintSelectedFromHistory(target, reprintSeq) {
1186
+ try {
1187
+ if (!target) return
1188
+
1189
+ if (!reprintSeq || !reprintSeq.trim()) {
1190
+ this._showToast({ message: i18next.t('text.please_enter_reprint_seq') })
1191
+ return
1192
+ }
1193
+
1194
+ const labelId = this._routeLabel && this._routeLabel.id
1195
+ if (!labelId) {
1196
+ this._showToast({
1197
+ type: 'error',
1198
+ message: `${i18next.t('text.no_label_setting_was_found')}. ${i18next.t('text.please_check_your_setting')}`
1199
+ })
1200
+ return
1201
+ }
1202
+
1203
+ await this._printRouteLabelByRo([{ ...target, reprintSeq: reprintSeq.trim() }], labelId)
1204
+ history.back()
1205
+ } catch (e) {
1206
+ this._showToast(e)
1207
+ }
1208
+ }
703
1209
  }
704
1210
 
705
1211
  window.customElements.define('route-label-popup', RouteLabelPopup)