@things-factory/operato-wms 4.3.797 → 4.3.799

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 (39) hide show
  1. package/client/pages/adjustment/transfer-inventory.js +940 -207
  2. package/client/pages/components/worksheet-information-popup.js +60 -8
  3. package/client/pages/constants/order.js +7 -1
  4. package/client/pages/inventory/inventory-history.js +33 -5
  5. package/client/pages/order/release-order/b2b/b2b-order-list.js +17 -1
  6. package/client/pages/order/release-order/release-order-detail.js +19 -0
  7. package/client/pages/order/transport-order/print-delivery-note.js +19 -1
  8. package/client/pages/outbound/loading-v2/load-by-qty.js +10 -0
  9. package/client/pages/outbound/loading-v2/loading-execution-base.js +22 -8
  10. package/client/pages/outbound/loading-v2/loading-execution.js +37 -8
  11. package/client/pages/outbound/loading-v2/loading.js +6 -0
  12. package/client/pages/outbound/loading-v2/release-good-info.js +3 -0
  13. package/client/pages/outbound/picking-product.js +25 -3
  14. package/client/pages/outbound/picking-v2/picking-execution-base.js +20 -2
  15. package/client/pages/outbound/picking-v2/picking-execution.js +4 -3
  16. package/client/pages/outbound/picking-v2/picking.js +3 -0
  17. package/client/pages/outbound/single-outbound-worksheet.js +18 -1
  18. package/client/pages/outbound/sorting-product.js +24 -1
  19. package/client/pages/outbound/worksheet-zone-picking.js +1 -0
  20. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.js +9 -4
  21. package/dist-server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.js.map +1 -1
  22. package/dist-server/graphql/resolvers/outbound/load-by-qty.js +1 -1
  23. package/dist-server/graphql/resolvers/outbound/load-by-qty.js.map +1 -1
  24. package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js +5 -1
  25. package/dist-server/graphql/resolvers/outbound/loading-worksheet-v2.js.map +1 -1
  26. package/dist-server/graphql/resolvers/zone-worksheet/zone-worksheet.js +8 -2
  27. package/dist-server/graphql/resolvers/zone-worksheet/zone-worksheet.js.map +1 -1
  28. package/dist-server/validators/setting-validators.js +7 -1
  29. package/dist-server/validators/setting-validators.js.map +1 -1
  30. package/package.json +8 -8
  31. package/server/graphql/resolvers/outbound/find-loadable-release-good-by-bin.ts +7 -1
  32. package/server/graphql/resolvers/outbound/load-by-qty.ts +1 -1
  33. package/server/graphql/resolvers/outbound/loading-worksheet-v2.ts +9 -2
  34. package/server/graphql/resolvers/zone-worksheet/zone-worksheet.ts +32 -23
  35. package/server/validators/setting-validators.ts +13 -1
  36. package/translations/en.json +9 -0
  37. package/translations/ko.json +9 -0
  38. package/translations/ms.json +9 -0
  39. package/translations/zh.json +9 -0
@@ -9,6 +9,8 @@ import { isMobileDevice } from '@things-factory/utils'
9
9
  import gql from 'graphql-tag'
10
10
  import { css, html } from 'lit-element'
11
11
  import { connect } from 'pwa-helpers/connect-mixin.js'
12
+ import { getCodeByName } from '@things-factory/code-base'
13
+ import { fetchSettingRule } from '../../fetch-setting-value'
12
14
  import { CARTON, LOT } from '../constants'
13
15
  import '../inbound/select-location-popup'
14
16
 
@@ -23,7 +25,14 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
23
25
  refInventoryType: String,
24
26
  _data: Object,
25
27
  _gristConfig: Object,
26
- _isExecuting: Boolean
28
+ _isExecuting: Boolean,
29
+ _showTransferForm: Boolean,
30
+ _showReasonField: Boolean,
31
+ _showRemarkField: Boolean,
32
+ _toLocationData: Object,
33
+ _transferCodes: Array,
34
+ _locations: Array,
35
+ _locationRecommendationChecked: Boolean
27
36
  }
28
37
  }
29
38
 
@@ -118,6 +127,148 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
118
127
  color: var(--primary-color) !important;
119
128
  margin-botton: 5px;
120
129
  }
130
+
131
+ /* Mobile form styles - matching unload-product design */
132
+ .mobile-form {
133
+ display: flex;
134
+ flex-direction: column;
135
+ gap: 10px;
136
+ padding: 8px 0;
137
+ }
138
+ .mobile-form fieldset {
139
+ display: flex;
140
+ flex-direction: column;
141
+ align-items: flex-start !important;
142
+ border: none;
143
+ margin: 0;
144
+ padding: 0;
145
+ width: 100%;
146
+ box-sizing: border-box;
147
+ }
148
+ .mobile-form label {
149
+ display: block;
150
+ grid-column: unset !important;
151
+ text-align: left !important;
152
+ align-self: flex-start !important;
153
+ margin-bottom: 4px;
154
+ color: #333;
155
+ font-weight: bold;
156
+ font-size: 14px;
157
+ width: 100%;
158
+ text-transform: capitalize;
159
+ }
160
+ .mobile-form select,
161
+ .mobile-form input[type="text"],
162
+ .mobile-form input[type="date"] {
163
+ grid-column: unset !important;
164
+ width: 100%;
165
+ max-width: 100%;
166
+ height: 24px;
167
+ min-height: 24px;
168
+ border: 1px solid rgba(0, 0, 0, 0.2);
169
+ border-radius: 4px;
170
+ padding: 4px 8px;
171
+ font-size: 12px;
172
+ background-color: white;
173
+ box-sizing: border-box;
174
+ }
175
+ .mobile-form input[type="number"] {
176
+ grid-column: unset !important;
177
+ width: 100%;
178
+ max-width: 100%;
179
+ height: 24px;
180
+ min-height: 24px;
181
+ border: 1px solid rgba(0, 0, 0, 0.2);
182
+ border-radius: 4px;
183
+ padding: 4px 8px;
184
+ padding-right: 24px;
185
+ font-size: 12px;
186
+ background-color: white;
187
+ box-sizing: border-box;
188
+ -moz-appearance: textfield;
189
+ }
190
+ .mobile-form input[type="number"]::-webkit-inner-spin-button,
191
+ .mobile-form input[type="number"]::-webkit-outer-spin-button {
192
+ opacity: 1;
193
+ height: 16px;
194
+ }
195
+ .mobile-form select:focus,
196
+ .mobile-form input:focus {
197
+ outline: none;
198
+ border-color: #017E7F;
199
+ }
200
+ .mobile-form [custom-input] {
201
+ grid-column: unset !important;
202
+ width: 100%;
203
+ box-sizing: border-box;
204
+ border: var(--input-field-border, 1px solid rgba(0, 0, 0, 0.2));
205
+ border-radius: var(--input-field-border-radius, 4px);
206
+ }
207
+ .mobile-form barcode-scanable-input {
208
+ width: 100%;
209
+ max-width: 100%;
210
+ box-sizing: border-box;
211
+ min-width: 0;
212
+ height: 24px;
213
+ min-height: 24px;
214
+ }
215
+ .mobile-form input[disabled] {
216
+ background-color: #f5f5f5;
217
+ cursor: not-allowed;
218
+ }
219
+ .mobile-form .side-by-side-row {
220
+ display: flex;
221
+ flex-direction: row;
222
+ gap: 10px;
223
+ }
224
+ .mobile-form .side-by-side-row fieldset {
225
+ flex: 1;
226
+ min-width: 0;
227
+ }
228
+ /* End of mobile form styles */
229
+
230
+ /* Mobile info section */
231
+ .mobile-info-section {
232
+ padding: 8px 8px 12px 8px;
233
+ /* border-bottom: 1px solid #017e7f; */
234
+ margin-bottom: 4px;
235
+ background-color: #F5F7FB;
236
+ }
237
+
238
+ .mobile-info-row {
239
+ display: flex;
240
+ flex-direction: row;
241
+ align-items: center;
242
+ padding: 3px 0;
243
+ }
244
+
245
+ .mobile-info-label {
246
+ font-weight: bold;
247
+ min-width: 130px;
248
+ color: #3a5877;
249
+ font-size: 13px;
250
+ text-transform: capitalize;
251
+ }
252
+
253
+ .mobile-info-value {
254
+ flex: 1;
255
+ color: var(--primary-color);
256
+ font-size: 13px;
257
+ }
258
+
259
+ .mobile-section-title {
260
+ color: #017e7f;
261
+ font-weight: bold;
262
+ font-size: 14px;
263
+ margin: 12px 0 0 0;
264
+ padding: 6px 0;
265
+ border-bottom: 1px solid #017e7f;
266
+ }
267
+
268
+ form#input-form.mobile-form {
269
+ background-color: #ffffff;
270
+ padding: 8px;
271
+ }
121
272
  `
122
273
  ]
123
274
  }
@@ -125,14 +276,33 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
125
276
  get context() {
126
277
  var actions = []
127
278
 
128
- if (this._palletId || this._cartonId) {
129
- actions = [
130
- ...actions,
131
- {
279
+ if (isMobileDevice()) {
280
+ if (this._showTransferForm) {
281
+ actions.push({
282
+ title: i18next.t('button.transfer'),
283
+ action: this._onMobileTransfer.bind(this),
284
+ ...CommonButtonStyles.transfer,
285
+ emphasis: { raised: true }
286
+ })
287
+ actions.push({
288
+ title: i18next.t('button.back'),
289
+ action: this._onMobileBack.bind(this),
290
+ ...CommonButtonStyles.back
291
+ })
292
+ } else if (this._selectedInventory) {
293
+ actions.push({
294
+ title: i18next.t('button.select'),
295
+ action: this._onMobileSelect.bind(this),
296
+ ...CommonButtonStyles.save
297
+ })
298
+ }
299
+ } else {
300
+ if (this._palletId || this._cartonId) {
301
+ actions.push({
132
302
  title: i18next.t('button.done'),
133
303
  action: this.done.bind(this)
134
- }
135
- ]
304
+ })
305
+ }
136
306
  }
137
307
 
138
308
  return {
@@ -180,6 +350,22 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
180
350
  ?.shadowRoot.querySelector('input')
181
351
  }
182
352
 
353
+ get reasonSelect() {
354
+ return this.shadowRoot.querySelector('select[name=reason]')
355
+ }
356
+
357
+ get remarkTextarea() {
358
+ return this.shadowRoot.querySelector('textarea[name=remark]')
359
+ }
360
+
361
+ get productInfoInput() {
362
+ return this.shadowRoot.querySelector('barcode-scanable-input[name=productInfo]')?.shadowRoot.querySelector('input')
363
+ }
364
+
365
+ get searchLocationInput() {
366
+ return this.shadowRoot.querySelector('barcode-scanable-input[name=searchLocation]')?.shadowRoot.querySelector('input')
367
+ }
368
+
183
369
  focusOnPalletInput() {
184
370
  setTimeout(() => this.palletInput.focus(), 100)
185
371
  }
@@ -201,166 +387,337 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
201
387
  }
202
388
 
203
389
  render() {
390
+ const isMobile = isMobileDevice()
204
391
  return html`
205
- <form id="info-form" class="multi-column-form">
206
- <fieldset>
207
- <label>${i18next.t('label.inventory_type')}</label>
208
- <select name="inventoryType" @change="${e => this.inventoryTypeChangeHandler(e.currentTarget.value)}">
209
- ${Object.keys(INVENTORY_TYPES).map(key =>
210
- this.refInventoryType === INVENTORY_TYPES[key].value
211
- ? html`
212
- <option value="${INVENTORY_TYPES[key].value}" selected>
213
- ${i18next.t(`label.${INVENTORY_TYPES[key].name}`)}
214
- </option>
215
- `
216
- : html`
217
- <option value="${INVENTORY_TYPES[key].value}">
218
- ${i18next.t(`label.${INVENTORY_TYPES[key].name}`)}
219
- </option>
220
- `
221
- )}
222
- </select>
223
-
224
- ${this.refInventoryType === INVENTORY_TYPES.LOT.value
225
- ? html`
226
- <barcode-scanable-input
227
- name="palletId"
228
- custom-input
229
- @keypress="${e => {
230
- if (e.keyCode === 13) {
231
- e.preventDefault()
232
- if (this.palletInput.value) {
233
- this._palletId = this.palletInput.value
234
- this.fetchInventory(this.palletInput.value)
392
+ ${!isMobile
393
+ ? html`
394
+ <form id="info-form" class="multi-column-form">
395
+ <fieldset>
396
+ <label>${i18next.t('label.inventory_type')}</label>
397
+ <select name="inventoryType" @change="${e => this.inventoryTypeChangeHandler(e.currentTarget.value)}">
398
+ ${Object.keys(INVENTORY_TYPES).map(key =>
399
+ this.refInventoryType === INVENTORY_TYPES[key].value
400
+ ? html`
401
+ <option value="${INVENTORY_TYPES[key].value}" selected>
402
+ ${i18next.t(`label.${INVENTORY_TYPES[key].name}`)}
403
+ </option>
404
+ `
405
+ : html`
406
+ <option value="${INVENTORY_TYPES[key].value}">
407
+ ${i18next.t(`label.${INVENTORY_TYPES[key].name}`)}
408
+ </option>
409
+ `
410
+ )}
411
+ </select>
412
+
413
+ ${this.refInventoryType === INVENTORY_TYPES.LOT.value
414
+ ? html`
415
+ <label>${i18next.t('label.id')}</label>
416
+ <barcode-scanable-input
417
+ name="palletId"
418
+ custom-input
419
+ @keypress="${e => {
420
+ if (e.keyCode === 13) {
421
+ e.preventDefault()
422
+ if (this.palletInput.value) {
423
+ this._palletId = this.palletInput.value
424
+ }
425
+ this.fetchInventory()
426
+ }
427
+ }}"
428
+ ></barcode-scanable-input>
429
+ `
430
+ : html`
431
+ <label>${i18next.t('label.id')}</label>
432
+ <barcode-scanable-input
433
+ name="cartonId"
434
+ custom-input
435
+ @keypress="${e => {
436
+ if (e.keyCode === 13) {
437
+ e.preventDefault()
438
+ if (this.cartonInput.value) {
439
+ this._cartonId = this.cartonInput.value
440
+ }
441
+ this.fetchInventory()
442
+ }
443
+ }}"
444
+ ></barcode-scanable-input>
445
+ `}
446
+ </fieldset>
447
+ <fieldset>
448
+ <label>${i18next.t('label.product_info')}</label>
449
+ <barcode-scanable-input
450
+ name="productInfo"
451
+ custom-input
452
+ @keypress="${e => {
453
+ if (e.keyCode === 13) {
454
+ e.preventDefault()
455
+ this.fetchInventory()
235
456
  }
236
- }
237
- }}"
238
- ></barcode-scanable-input>
239
- `
240
- : html` <barcode-scanable-input
241
- name="cartonId"
242
- custom-input
243
- @keypress="${e => {
244
- if (e.keyCode === 13) {
245
- e.preventDefault()
246
- if (this.cartonInput.value) {
247
- this._cartonId = this.cartonInput.value
248
- this.fetchInventory(this.cartonInput.value)
249
- }
250
- }
251
- }}"
252
- ></barcode-scanable-input>`}
253
- </fieldset>
254
- </form>
255
-
256
- <div class="grist">
257
- <data-grist
258
- .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
259
- .config=${this._gristConfig}
260
- .data=${this._data}
261
- ></data-grist>
262
- </div>
263
-
264
- <div class="input-section">
265
- <div class="left-column">
266
- <div class="single-column-form">
267
- <fieldset>
268
- <label class="content-title">${i18next.t('label.lot_id')}</label>
269
- <label class="content">${this?._selectedInventory?.palletId || '-'}</label>
270
-
271
- <label class="content-title">${i18next.t('label.from_location')}</label>
272
- <label class="content">${this?._selectedInventory?.location?.name || '-'}</label>
273
-
274
- <label class="content-title">${i18next.t('label.product')}</label>
275
- <label class="content"
276
- >${this?._selectedInventory?.product
277
- ? '(' + this._selectedInventory.product.sku + ') ' + this?._selectedInventory?.product?.name
278
- : '-'}</label
279
- >
280
- <label class="content-title">${i18next.t('label.packing_type')}</label>
281
- <label class="content">${this?._selectedInventory?.packingType || '-'}</label>
282
-
283
- <label class="content-title">${i18next.t('label.qty')}</label>
284
- <label class="content">${(this?._selectedInventory?.qty || 0).toString()}</label>
285
-
286
- <label class="content-title">${i18next.t('label.reserve_qty')}</label>
287
- <label class="content">${(this?._selectedInventory?.lockedQty || 0).toString()}</label>
288
-
289
- <label class="content-title">${i18next.t('label.available_qty')}</label>
290
- <label class="content">${(this?._selectedInventory?.availableQty || 0).toString()}</label>
291
-
292
- <!-- <input name="fromLocationName" readonly /> -->
293
- </fieldset>
294
- </div>
295
- </div>
296
- <div class="right-column">
297
- <form id="input-form" class="single-column-form">
298
- <fieldset>
299
- <label class="content-title">${i18next.t('label.transfer_qty')}</label>
300
- <input
301
- type="number"
302
- name="qty"
303
- min="0"
304
- @change="${e => {
305
- let qtyInputValue = parseFloat(this.qtyInput.value) || null
306
- if (!Number.isInteger(parseFloat(this.qtyInput.value))) {
307
- this.qtyInput.value = qtyInputValue
308
- }
309
- if (qtyInputValue < 0) {
310
- this.qtyInput.value = 0
311
- }
312
- if (this._selectedInventory) {
313
- if (this._selectedInventory.availableQty >= qtyInputValue) {
314
- this._requireNewId = true
315
- } else {
316
- if (qtyInputValue > this._selectedInventory.availableQty) {
317
- this.qtyInput.value = null
318
- this.showToast({ message: 'Transferred quantity cannot be more than available quantity' })
457
+ }}"
458
+ ></barcode-scanable-input>
459
+
460
+ <label>${i18next.t('label.location')}</label>
461
+ <barcode-scanable-input
462
+ name="searchLocation"
463
+ custom-input
464
+ @keypress="${e => {
465
+ if (e.keyCode === 13) {
466
+ e.preventDefault()
467
+ this.fetchInventory()
319
468
  }
320
- this._requireNewId = false
469
+ }}"
470
+ ></barcode-scanable-input>
471
+ </fieldset>
472
+ </form>
473
+
474
+ <div class="grist">
475
+ <data-grist
476
+ .mode=${'GRID'}
477
+ .config=${this._gristConfig}
478
+ .data=${this._data}
479
+ ></data-grist>
480
+ </div>
481
+
482
+ <div class="input-section">
483
+ <div class="left-column">
484
+ <div class="single-column-form">
485
+ <fieldset>
486
+ <label class="content-title">${this.refInventoryType === INVENTORY_TYPES.LOT.value ? i18next.t('label.lot_id') : i18next.t('label.carton_id')}</label>
487
+ <label class="content">${this.refInventoryType === INVENTORY_TYPES.LOT.value ? this?._selectedInventory?.palletId || '-' : this?._selectedInventory?.cartonId || '-'}</label>
488
+
489
+ <label class="content-title">${i18next.t('label.from_location')}</label>
490
+ <label class="content">${this?._selectedInventory?.location?.name || '-'}</label>
491
+
492
+ <label class="content-title">${i18next.t('label.product')}</label>
493
+ <label class="content"
494
+ >${this?._selectedInventory?.product
495
+ ? '(' + this._selectedInventory.product.sku + ') ' + this?._selectedInventory?.product?.name
496
+ : '-'}</label
497
+ >
498
+ <label class="content-title">${i18next.t('label.packing_type')}</label>
499
+ <label class="content">${this?._selectedInventory?.packingType || '-'}</label>
500
+
501
+ <label class="content-title">${i18next.t('label.qty')}</label>
502
+ <label class="content">${(this?._selectedInventory?.qty || 0).toString()}</label>
503
+
504
+ <label class="content-title">${i18next.t('label.reserve_qty')}</label>
505
+ <label class="content">${(this?._selectedInventory?.lockedQty || 0).toString()}</label>
506
+
507
+ <label class="content-title">${i18next.t('label.available_qty')}</label>
508
+ <label class="content">${(this?._selectedInventory?.availableQty || 0).toString()}</label>
509
+ </fieldset>
510
+ </div>
511
+ </div>
512
+ <div class="right-column">
513
+ <form id="input-form" class="single-column-form">
514
+ <fieldset>
515
+ <label class="content-title">${i18next.t('label.transfer_qty')}</label>
516
+ <input
517
+ type="number"
518
+ name="qty"
519
+ min="0"
520
+ @change="${e => {
521
+ let qtyInputValue = parseFloat(this.qtyInput.value) || null
522
+ if (!Number.isInteger(parseFloat(this.qtyInput.value))) {
523
+ this.qtyInput.value = qtyInputValue
524
+ }
525
+ if (qtyInputValue < 0) {
526
+ this.qtyInput.value = 0
527
+ }
528
+ if (this._selectedInventory) {
529
+ if (this._selectedInventory.availableQty >= qtyInputValue) {
530
+ this._requireNewId = true
531
+ } else {
532
+ if (qtyInputValue > this._selectedInventory.availableQty) {
533
+ this.qtyInput.value = null
534
+ this.showToast({ message: 'Transferred quantity cannot be more than available quantity' })
535
+ }
536
+ this._requireNewId = false
537
+ }
538
+ }
539
+ }}"
540
+ @keypress="${e => {
541
+ if (e.keyCode === 13) {
542
+ e.preventDefault()
543
+ if (parseFloat(this.qtyInput.value) < 0) {
544
+ this.qtyInput.value = 0
545
+ } else if (this.qtyInput.value && this.toLocationNameInput.value === '') {
546
+ this.focusOnToLocationInput()
547
+ }
548
+ }
549
+ }}"
550
+ />
551
+
552
+ <label class="content-title">${i18next.t('label.to_location')}</label>
553
+ <barcode-scanable-input
554
+ name="toLocationName"
555
+ custom-input
556
+ @keypress="${this._handleToLocationEnter.bind(this)}"
557
+ @input="${() => { this._locationRecommendationChecked = false }}"
558
+ ></barcode-scanable-input>
559
+
560
+ ${this.refInventoryType === INVENTORY_TYPES.LOT.value
561
+ ? this._requireNewId
562
+ ? html`
563
+ <label class="content-title">${i18next.t('label.new_lot_id')}</label>
564
+ <barcode-scanable-input
565
+ name="newLotId"
566
+ custom-input
567
+ @keypress="${e => {
568
+ if (e.keyCode === 13) {
569
+ e.preventDefault()
570
+ this.transferInventory(e)
571
+ }
572
+ }}"
573
+ ></barcode-scanable-input>
574
+ `
575
+ : ``
576
+ : html`
577
+ <label class="content-title">${i18next.t('label.new_carton_id')}</label>
578
+ <barcode-scanable-input
579
+ name="newCartonId"
580
+ custom-input
581
+ @keypress="${e => {
582
+ if (e.keyCode === 13) {
583
+ e.preventDefault()
584
+ this.transferInventory(e)
585
+ }
586
+ }}"
587
+ ></barcode-scanable-input>
588
+ `}
589
+
590
+ ${this._showReasonField ? html`
591
+ <label class="content-title">${i18next.t('label.reason')}</label>
592
+ <select name="reason" @change="${e => {
593
+ this._showRemarkField = e.currentTarget.value.toUpperCase() === 'OTHERS'
594
+ }}">
595
+ <option value=""></option>
596
+ ${(this._transferCodes || []).map(code => html`
597
+ <option value="${code.name}">${code.description || code.name}</option>
598
+ `)}
599
+ </select>
600
+ ` : ''}
601
+
602
+ ${this._showRemarkField ? html`
603
+ <label class="content-title">${i18next.t('label.remark')}</label>
604
+ <textarea name="remark" rows="3"></textarea>
605
+ ` : ''}
606
+
607
+ <div class="button-container">
608
+ <button @click=${this.transferInventory.bind(this)}>${i18next.t('button.transfer')}</button>
609
+ </div>
610
+ </fieldset>
611
+ </form>
612
+ </div>
613
+ </div>
614
+ `
615
+ : this._showTransferForm
616
+ ? html`
617
+ <div class="mobile-info-section">
618
+ <div class="mobile-info-row">
619
+ <div class="mobile-info-label">${this.refInventoryType === INVENTORY_TYPES.LOT.value ? i18next.t('label.lot_id') : i18next.t('label.carton_id')}</div>
620
+ <div class="mobile-info-value">${this.refInventoryType === INVENTORY_TYPES.LOT.value ? this._selectedInventory?.palletId || '-' : this._selectedInventory?.cartonId || '-'}</div>
621
+ </div>
622
+ <div class="mobile-info-row">
623
+ <div class="mobile-info-label">${i18next.t('label.product')}</div>
624
+ <div class="mobile-info-value">${this._selectedInventory?.product ? '(' + this._selectedInventory.product.sku + ') ' + this._selectedInventory.product.name : '-'}</div>
625
+ </div>
626
+ <div class="mobile-info-row">
627
+ <div class="mobile-info-label">${i18next.t('label.from_location')}</div>
628
+ <div class="mobile-info-value">${this._selectedInventory?.location?.name || '-'}</div>
629
+ </div>
630
+ <div class="mobile-info-row">
631
+ <div class="mobile-info-label">${i18next.t('label.packing_type')}</div>
632
+ <div class="mobile-info-value">${this._selectedInventory?.packingType || '-'}</div>
633
+ </div>
634
+ <div class="mobile-info-row">
635
+ <div class="mobile-info-label">${i18next.t('label.qty')}</div>
636
+ <div class="mobile-info-value">${(this._selectedInventory?.qty || 0).toString()}</div>
637
+ </div>
638
+ <div class="mobile-info-row">
639
+ <div class="mobile-info-label">${i18next.t('label.reserve_qty')}</div>
640
+ <div class="mobile-info-value">${(this._selectedInventory?.lockedQty || 0).toString()}</div>
641
+ </div>
642
+ <div class="mobile-info-row">
643
+ <div class="mobile-info-label">${i18next.t('label.available_qty')}</div>
644
+ <div class="mobile-info-value">${(this._selectedInventory?.availableQty || 0).toString()}</div>
645
+ </div>
646
+ </div>
647
+
648
+ <form id="input-form" class="mobile-form">
649
+ <fieldset>
650
+ <label>${i18next.t('label.transfer_qty')}</label>
651
+ <input
652
+ type="number"
653
+ name="qty"
654
+ min="0"
655
+ @change="${_ => {
656
+ let qtyInputValue = parseFloat(this.qtyInput.value) || null
657
+ if (!Number.isInteger(parseFloat(this.qtyInput.value))) {
658
+ this.qtyInput.value = qtyInputValue
321
659
  }
322
- }
323
- }}"
324
- @keypress="${e => {
325
- if (e.keyCode === 13) {
326
- e.preventDefault()
327
- if (parseFloat(this.qtyInput.value) < 0) {
660
+ if (qtyInputValue < 0) {
328
661
  this.qtyInput.value = 0
329
- } else if (this.qtyInput.value && this.toLocationNameInput.value === '') {
330
- this.focusOnToLocationInput()
331
662
  }
332
- }
333
- }}"
334
- />
335
-
336
- <label class="content-title">${i18next.t('label.to_location')}</label>
337
- <barcode-scanable-input
338
- name="toLocationName"
339
- custom-input
340
- @keypress="${e => {
341
- if (e.keyCode === 13) {
342
- e.preventDefault()
343
- if (this.toLocationNameInput.value) {
344
- if (this.refInventoryType === INVENTORY_TYPES.LOT.value) {
345
- if (this._requireNewId) {
346
- this.focusOnNewLotInput()
347
- } else {
348
- this.transferInventory(e)
349
- }
663
+ if (this._selectedInventory) {
664
+ if (this._selectedInventory.availableQty >= qtyInputValue) {
665
+ this._requireNewId = true
350
666
  } else {
351
- this.focusOnNewCartonInput()
667
+ if (qtyInputValue > this._selectedInventory.availableQty) {
668
+ this.qtyInput.value = null
669
+ this.showToast({ message: 'Transferred quantity cannot be more than available quantity' })
670
+ }
671
+ this._requireNewId = false
352
672
  }
353
- // this.transferInventory(e)
354
673
  }
355
- }
356
- }}"
357
- ></barcode-scanable-input>
674
+ }}"
675
+ @keypress="${e => {
676
+ if (e.keyCode === 13) {
677
+ e.preventDefault()
678
+ if (parseFloat(this.qtyInput.value) < 0) {
679
+ this.qtyInput.value = 0
680
+ } else if (this.qtyInput.value && this.toLocationNameInput.value === '') {
681
+ this.focusOnToLocationInput()
682
+ }
683
+ }
684
+ }}"
685
+ />
686
+ </fieldset>
687
+
688
+ <fieldset>
689
+ <label>${i18next.t('label.to_location')}</label>
690
+ <barcode-scanable-input
691
+ name="toLocationName"
692
+ custom-input
693
+ @keypress="${this._handleToLocationEnter.bind(this)}"
694
+ @input="${() => { this._locationRecommendationChecked = false }}"
695
+ ></barcode-scanable-input>
696
+ </fieldset>
358
697
 
359
698
  ${this.refInventoryType === INVENTORY_TYPES.LOT.value
360
699
  ? this._requireNewId
361
- ? html` <label class="content-title">${i18next.t('label.new_lot_id')}</label>
700
+ ? html`
701
+ <fieldset>
702
+ <label>${i18next.t('label.new_lot_id')}</label>
703
+ <barcode-scanable-input
704
+ name="newLotId"
705
+ custom-input
706
+ @keypress="${e => {
707
+ if (e.keyCode === 13) {
708
+ e.preventDefault()
709
+ this.transferInventory(e)
710
+ }
711
+ }}"
712
+ ></barcode-scanable-input>
713
+ </fieldset>
714
+ `
715
+ : ``
716
+ : html`
717
+ <fieldset>
718
+ <label>${i18next.t('label.new_carton_id')}</label>
362
719
  <barcode-scanable-input
363
- name="newLotId"
720
+ name="newCartonId"
364
721
  custom-input
365
722
  @keypress="${e => {
366
723
  if (e.keyCode === 13) {
@@ -368,29 +725,127 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
368
725
  this.transferInventory(e)
369
726
  }
370
727
  }}"
371
- ></barcode-scanable-input>`
372
- : ``
373
- : html`
374
- <label class="content-title">${i18next.t('label.new_carton_id')}</label>
375
- <barcode-scanable-input
376
- name="newCartonId"
377
- custom-input
378
- @keypress="${e => {
379
- if (e.keyCode === 13) {
380
- e.preventDefault()
381
- this.transferInventory(e)
382
- }
383
- }}"
384
- ></barcode-scanable-input>
728
+ ></barcode-scanable-input>
729
+ </fieldset>
385
730
  `}
386
731
 
387
- <div class="button-container">
388
- <button @click=${this.transferInventory.bind(this)}>${i18next.t('button.transfer')}</button>
389
- </div>
390
- </fieldset>
391
- </form>
392
- </div>
393
- </div>
732
+ ${this._showReasonField ? html`
733
+ <fieldset>
734
+ <label>${i18next.t('label.reason')}</label>
735
+ <select name="reason" @change="${e => {
736
+ this._showRemarkField = e.currentTarget.value.toUpperCase() === 'OTHERS'
737
+ }}">
738
+ <option value=""></option>
739
+ ${(this._transferCodes || []).map(code => html`
740
+ <option value="${code.name}">${code.description || code.name}</option>
741
+ `)}
742
+ </select>
743
+ </fieldset>
744
+ ` : ''}
745
+
746
+ ${this._showRemarkField ? html`
747
+ <fieldset>
748
+ <label>${i18next.t('label.remark')}</label>
749
+ <textarea name="remark" rows="3"></textarea>
750
+ </fieldset>
751
+ ` : ''}
752
+ </form>
753
+ `
754
+ : html`
755
+ <form id="info-form" class="mobile-form">
756
+ <fieldset class="side-by-side-row">
757
+ <fieldset>
758
+ <label>${i18next.t('label.inventory_type')}</label>
759
+ <select name="inventoryType" @change="${e => this.inventoryTypeChangeHandler(e.currentTarget.value)}">
760
+ ${Object.keys(INVENTORY_TYPES).map(key =>
761
+ this.refInventoryType === INVENTORY_TYPES[key].value
762
+ ? html`
763
+ <option value="${INVENTORY_TYPES[key].value}" selected>
764
+ ${i18next.t(`label.${INVENTORY_TYPES[key].name}`)}
765
+ </option>
766
+ `
767
+ : html`
768
+ <option value="${INVENTORY_TYPES[key].value}">
769
+ ${i18next.t(`label.${INVENTORY_TYPES[key].name}`)}
770
+ </option>
771
+ `
772
+ )}
773
+ </select>
774
+ </fieldset>
775
+ <fieldset>
776
+ <label>${i18next.t('label.id')}</label>
777
+ ${this.refInventoryType === INVENTORY_TYPES.LOT.value
778
+ ? html`
779
+ <barcode-scanable-input
780
+ name="palletId"
781
+ custom-input
782
+ @keypress="${e => {
783
+ if (e.keyCode === 13) {
784
+ e.preventDefault()
785
+ if (this.palletInput.value) {
786
+ this._palletId = this.palletInput.value
787
+ }
788
+ this.fetchInventory()
789
+ }
790
+ }}"
791
+ ></barcode-scanable-input>
792
+ `
793
+ : html`
794
+ <barcode-scanable-input
795
+ name="cartonId"
796
+ custom-input
797
+ @keypress="${e => {
798
+ if (e.keyCode === 13) {
799
+ e.preventDefault()
800
+ if (this.cartonInput.value) {
801
+ this._cartonId = this.cartonInput.value
802
+ }
803
+ this.fetchInventory()
804
+ }
805
+ }}"
806
+ ></barcode-scanable-input>
807
+ `}
808
+ </fieldset>
809
+ </fieldset>
810
+ <fieldset class="side-by-side-row">
811
+ <fieldset>
812
+ <label>${i18next.t('label.product_info')}</label>
813
+ <barcode-scanable-input
814
+ name="productInfo"
815
+ custom-input
816
+ @keypress="${e => {
817
+ if (e.keyCode === 13) {
818
+ e.preventDefault()
819
+ this.fetchInventory()
820
+ }
821
+ }}"
822
+ ></barcode-scanable-input>
823
+ </fieldset>
824
+ <fieldset>
825
+ <label>${i18next.t('label.location')}</label>
826
+ <barcode-scanable-input
827
+ name="searchLocation"
828
+ custom-input
829
+ @keypress="${e => {
830
+ if (e.keyCode === 13) {
831
+ e.preventDefault()
832
+ this.fetchInventory()
833
+ }
834
+ }}"
835
+ ></barcode-scanable-input>
836
+ </fieldset>
837
+ </fieldset>
838
+ </form>
839
+
840
+ <div class="grist">
841
+ <data-grist
842
+ .mode=${'LIST'}
843
+ .config=${this._gristConfig}
844
+ .data=${this._data}
845
+ .selectedRecords=${this._selectedInventory ? [this._selectedInventory] : []}
846
+ ></data-grist>
847
+ </div>
848
+ `}
394
849
  `
395
850
  }
396
851
 
@@ -399,7 +854,22 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
399
854
  this._isExecuting = false
400
855
  }
401
856
 
402
- pageInitialized() {
857
+ async pageInitialized() {
858
+ this._transferCodes = await getCodeByName('TRANSFER_CODES')
859
+
860
+ const [locationRecommendationLevel, stackingOptionLimitStr] = await Promise.all([
861
+ fetchSettingRule('location-recommendation-level'),
862
+ fetchSettingRule('stacking-option-limit')
863
+ ])
864
+ this._locationRecommendationLevel = (locationRecommendationLevel || '').toUpperCase().trim()
865
+ try {
866
+ const parsed = JSON.parse(stackingOptionLimitStr || '{}')
867
+ this._stackingOptionLimit = { min: Number(parsed.Min) || 1, max: Number(parsed.Max) || 1 }
868
+ } catch {
869
+ this._stackingOptionLimit = { min: 1, max: 1 }
870
+ }
871
+
872
+ await this._fetchLocations()
403
873
  this.refInventoryType = INVENTORY_TYPES.LOT.value
404
874
  this._gristConfig = {
405
875
  list: {
@@ -424,9 +894,11 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
424
894
  handlers: {
425
895
  click: async (columns, data, column, record, rowIndex) => {
426
896
  try {
427
- if (this.refInventoryType !== INVENTORY_TYPES.LOT.value) {
428
- this._selectedInventory = record
429
- }
897
+ if (this._selectedInventory) this._selectedInventory['__selected__'] = false
898
+ record['__selected__'] = true
899
+ this._selectedInventory = record
900
+ this._resetReasonFields()
901
+ this.updateContext()
430
902
  } catch (ex) {
431
903
  console.log(ex)
432
904
  } finally {
@@ -535,23 +1007,48 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
535
1007
  else this.focusOnCartonInput()
536
1008
  }
537
1009
 
538
- async fetchInventory(inventoryRefId) {
1010
+ _resetReasonFields() {
1011
+ this._showReasonField = false
1012
+ this._showRemarkField = false
1013
+ this._locationRecommendationChecked = false
1014
+ if (this.reasonSelect) this.reasonSelect.value = ''
1015
+ if (this.remarkTextarea) this.remarkTextarea.value = ''
1016
+ }
1017
+
1018
+ async fetchInventory() {
539
1019
  try {
1020
+ this._resetReasonFields()
540
1021
  let filters = []
541
- if (this.refInventoryType === INVENTORY_TYPES.LOT.value) {
542
- filters.push({
543
- name: 'palletId',
544
- operator: 'eq',
545
- value: inventoryRefId
546
- })
547
- } else {
548
- filters.push({
549
- name: 'cartonId',
550
- operator: 'eq',
551
- value: inventoryRefId
552
- })
1022
+
1023
+ const idValue =
1024
+ this.refInventoryType === INVENTORY_TYPES.LOT.value ? this.palletInput?.value : this.cartonInput?.value
1025
+
1026
+ if (idValue) {
1027
+ const idField = this.refInventoryType === INVENTORY_TYPES.LOT.value ? 'palletId' : 'cartonId'
1028
+ filters.push({ name: idField, operator: 'eq', value: idValue })
553
1029
  }
554
1030
 
1031
+ const productInfoValue = this.productInfoInput?.value?.trim()
1032
+ const searchLocationValue = this.searchLocationInput?.value?.trim()
1033
+
1034
+ if (searchLocationValue) {
1035
+ await this._fetchLocations()
1036
+ const locationExists = (this._locations || []).some(l => l.name === searchLocationValue)
1037
+ if (!locationExists) {
1038
+ throw new Error(i18next.t('text.location_not_found'))
1039
+ }
1040
+ filters.push({ name: 'locationName', operator: 'eq', value: searchLocationValue })
1041
+ }
1042
+
1043
+ if (productInfoValue) {
1044
+ filters.push({ name: 'productInfo', operator: 'like', value: productInfoValue })
1045
+ }
1046
+
1047
+ if (filters.length === 0) return
1048
+
1049
+ filters.push({ name: 'status', operator: 'eq', value: 'STORED' })
1050
+ filters.push({ name: 'refInventoryType', operator: 'eq', value: this.refInventoryType })
1051
+
555
1052
  const response = await client.query({
556
1053
  query: gql`
557
1054
  query inventories($filters: [Filter!]) {
@@ -569,6 +1066,7 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
569
1066
  sku
570
1067
  name
571
1068
  description
1069
+ allowStackingOption
572
1070
  }
573
1071
  qty
574
1072
  lockedQty
@@ -584,22 +1082,24 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
584
1082
  }
585
1083
  }
586
1084
  `,
587
- variables: {
588
- filters: [...filters, { name: 'status', operator: 'eq', value: 'STORED' }]
589
- }
1085
+ variables: { filters }
590
1086
  })
591
1087
 
592
1088
  if (!response.errors) {
593
- // let foundId = response.data.inventories.total
594
1089
  if (response.data.inventories.total == 0) {
595
- if (this.refInventoryType === INVENTORY_TYPES.LOT.value) {
596
- this.focusOnPalletInput()
597
- throw new Error(i18next.t('text.lot_id_not_found'))
598
- }
599
- if (this.refInventoryType === INVENTORY_TYPES.CARTON.value) {
600
- this.focusOnCartonInput()
601
- throw new Error(i18next.t('text.carton_id_not_found'))
1090
+ if (idValue && !productInfoValue && !searchLocationValue) {
1091
+ if (this.refInventoryType === INVENTORY_TYPES.LOT.value) {
1092
+ this.focusOnPalletInput()
1093
+ throw new Error(i18next.t('text.lot_id_not_found'))
1094
+ } else {
1095
+ this.focusOnCartonInput()
1096
+ throw new Error(i18next.t('text.carton_id_not_found'))
1097
+ }
1098
+ } else if ((productInfoValue && !idValue && !searchLocationValue) || (searchLocationValue && !idValue && !productInfoValue)) {
1099
+ throw new Error(i18next.t('text.there_is_no_inventory_for_the_product'))
602
1100
  }
1101
+
1102
+ this._selectedInventory = null
603
1103
  }
604
1104
 
605
1105
  this._data = {
@@ -612,7 +1112,7 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
612
1112
  })
613
1113
  }
614
1114
 
615
- if (this.refInventoryType === INVENTORY_TYPES.LOT.value) {
1115
+ if (this.refInventoryType === INVENTORY_TYPES.LOT.value && !productInfoValue && !searchLocationValue && response.data.inventories.total > 0) {
616
1116
  this._selectedInventory = this._data.records[0]
617
1117
  }
618
1118
 
@@ -635,6 +1135,16 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
635
1135
  this._isExecuting = true
636
1136
  this._validate()
637
1137
 
1138
+ if (!this._locationRecommendationChecked) {
1139
+ const locationName = this.toLocationNameInput.value
1140
+ const confirmed = await this._checkLocationRecommendation(locationName)
1141
+ if (!confirmed) {
1142
+ this.toLocationNameInput.value = ''
1143
+ return
1144
+ }
1145
+ this._locationRecommendationChecked = true
1146
+ }
1147
+
638
1148
  const result = await CustomAlert({
639
1149
  title: i18next.t('title.are_you_sure'),
640
1150
  text: i18next.t('text.transfer_inventory_to_different_location'),
@@ -652,7 +1162,9 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
652
1162
  cartonId: this.refInventoryType === INVENTORY_TYPES.CARTON.value ? this.newCartonInput?.value : null,
653
1163
  fromLocationName: this._selectedInventory.location.name,
654
1164
  toLocationName: this.toLocationNameInput.value,
655
- qty: parseFloat(this.qtyInput.value)
1165
+ qty: parseFloat(this.qtyInput.value),
1166
+ reason: this._showReasonField ? this.reasonSelect?.value || null : null,
1167
+ remark: this._showRemarkField ? this.remarkTextarea?.value?.trim() || null : null
656
1168
  }
657
1169
 
658
1170
  const response = await client.mutate({
@@ -664,6 +1176,8 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
664
1176
  $fromLocationName: String!
665
1177
  $toLocationName: String!
666
1178
  $qty: Float!
1179
+ $reason: String
1180
+ $remark: String
667
1181
  ) {
668
1182
  inventoryTransfer(
669
1183
  originPalletId: $originPalletId
@@ -672,6 +1186,8 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
672
1186
  fromLocationName: $fromLocationName
673
1187
  toLocationName: $toLocationName
674
1188
  qty: $qty
1189
+ reason: $reason
1190
+ remark: $remark
675
1191
  )
676
1192
  }
677
1193
  `,
@@ -686,6 +1202,15 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
686
1202
  this.updateContext()
687
1203
  }
688
1204
  } catch (e) {
1205
+ // HANDLE EDGE SCENARIOS
1206
+ if (e.message === 'Please select a reason') {
1207
+ this._showReasonField = true
1208
+ this.reasonSelect?.focus()
1209
+ } else if (e.message === 'Please enter a remark') {
1210
+ this._showReasonField = true
1211
+ this._showRemarkField = true
1212
+ this.remarkTextarea?.focus()
1213
+ }
689
1214
  this.showToast(e)
690
1215
  } finally {
691
1216
  this._isExecuting = false
@@ -719,8 +1244,32 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
719
1244
  throw new Error(i18next.t('text.from_location_is_same_as_to_location'))
720
1245
  }
721
1246
 
722
- if (this.refInventoryType === INVENTORY_TYPES.CARTON.value && this?.cartonInput?.value.trim() == '') {
723
- throw new Error(i18next.t('text.required_carton_id'))
1247
+ // if (this.refInventoryType === INVENTORY_TYPES.CARTON.value && this?.cartonInput?.value.trim() == '') {
1248
+ // throw new Error(i18next.t('text.required_carton_id'))
1249
+ // }
1250
+
1251
+ const toLocationName = this.toLocationNameInput.value
1252
+ const toLocation = (this._locations || []).find(l => l.name === toLocationName)
1253
+ const toIsSpecial = this._isSpecialLocationType(toLocation?.type)
1254
+
1255
+ const fromLocationName = this._selectedInventory?.location?.name
1256
+ const fromLocationType = (this._locations || []).find(l => l.name === fromLocationName)?.type
1257
+ const fromIsSpecial = this._isSpecialLocationType(fromLocationType)
1258
+
1259
+ if (toIsSpecial !== fromIsSpecial) {
1260
+ if (!this.reasonSelect?.value) {
1261
+ this._showReasonField = true
1262
+ this.reasonSelect?.focus()
1263
+ throw new Error(i18next.t('text.please_select_a_reason'))
1264
+ }
1265
+ if (this.reasonSelect.value.toUpperCase() === 'OTHERS' && !this.remarkTextarea?.value?.trim()) {
1266
+ this._showRemarkField = true
1267
+ this.remarkTextarea?.focus()
1268
+ throw new Error(i18next.t('text.please_enter_a_remark'))
1269
+ }
1270
+ } else {
1271
+ this._showReasonField = false
1272
+ this._showRemarkField = false
724
1273
  }
725
1274
  }
726
1275
 
@@ -733,12 +1282,196 @@ class TransferInventory extends connect(store)(localize(i18next)(PageView)) {
733
1282
  this.cartonInput.value = ''
734
1283
  this.cartonId = ''
735
1284
  }
1285
+ if (this?.productInfoInput) this.productInfoInput.value = ''
1286
+ if (this?.searchLocationInput) this.searchLocationInput.value = ''
1287
+ }
1288
+
1289
+ _onMobileSelect() {
1290
+ if (!this._selectedInventory) return
1291
+ this._resetReasonFields()
1292
+ this._showTransferForm = true
1293
+ this.updateContext()
1294
+ }
1295
+
1296
+ _onMobileBack() {
1297
+ this._resetReasonFields()
1298
+ this._showTransferForm = false
1299
+ this.updateContext()
1300
+ }
1301
+
1302
+ _onMobileTransfer() {
1303
+ this.transferInventory({ stopPropagation: () => {}, preventDefault: () => {} })
1304
+ }
1305
+
1306
+ async _fetchLocations() {
1307
+ try {
1308
+ const response = await client.query({
1309
+ query: gql`
1310
+ query {
1311
+ locations {
1312
+ items {
1313
+ id
1314
+ name
1315
+ type
1316
+ status
1317
+ }
1318
+ }
1319
+ }
1320
+ `
1321
+ })
1322
+ if (!response.errors) {
1323
+ this._locations = response.data.locations.items
1324
+ }
1325
+ } catch (e) {
1326
+ console.error('Failed to fetch locations', e)
1327
+ }
1328
+ }
1329
+
1330
+ _handleToLocationEnter(e) {
1331
+ if (e.keyCode === 13) {
1332
+ e.preventDefault()
1333
+ if (this.toLocationNameInput?.value) {
1334
+ this._validateAndProceedToLocation()
1335
+ }
1336
+ }
1337
+ }
1338
+
1339
+ async _checkLocationRecommendation(toLocationName) {
1340
+ const level = this._locationRecommendationLevel
1341
+ if (!level) return true
1342
+
1343
+ const fromSku = this._selectedInventory?.product?.sku
1344
+
1345
+ const response = await client.query({
1346
+ query: gql`
1347
+ query inventories($filters: [Filter!]) {
1348
+ inventories(filters: $filters) {
1349
+ items {
1350
+ product { sku }
1351
+ }
1352
+ total
1353
+ }
1354
+ }
1355
+ `,
1356
+ variables: {
1357
+ filters: [
1358
+ { name: 'locationName', operator: 'eq', value: toLocationName },
1359
+ { name: 'status', operator: 'eq', value: 'STORED' }
1360
+ ]
1361
+ }
1362
+ })
1363
+
1364
+ if (response.errors) return true
1365
+
1366
+ const { items: inventoriesAtLocation = [], total: countAtLocation = 0 } = response.data.inventories
1367
+
1368
+ let warningMsg = null
1369
+
1370
+ switch (level) {
1371
+ case 'LEVEL 1': {
1372
+ await this._fetchLocations(); // REFRESH TO GET LATEST
1373
+ const toLocation = (this._locations || []).find(l => l.name === toLocationName)
1374
+ if (toLocation?.status?.toUpperCase() !== 'EMPTY') {
1375
+ warningMsg = i18next.t('text.reminder_location_assigned_to_another_sku')
1376
+ }
1377
+ break
1378
+ }
1379
+ case 'LEVEL 2': {
1380
+ const hasDifferentSku = inventoriesAtLocation.some(inv => inv.product?.sku !== fromSku)
1381
+ if (hasDifferentSku) {
1382
+ warningMsg = i18next.t('text.reminder_location_assigned_to_another_sku')
1383
+ }
1384
+ break
1385
+ }
1386
+ case 'LEVEL 3': {
1387
+ const hasDifferentSku = inventoriesAtLocation.some(inv => inv.product?.sku !== fromSku)
1388
+ const allowStacking = this._selectedInventory?.product?.allowStackingOption
1389
+ const stackingLimit = allowStacking ? this._stackingOptionLimit?.max : null
1390
+ const hitStackingLimit = stackingLimit != null && countAtLocation >= stackingLimit
1391
+ if (hasDifferentSku || hitStackingLimit) {
1392
+ warningMsg = i18next.t('text.reminder_location_occupied_by_different_sku_or_stacking_capacity')
1393
+ }
1394
+ break
1395
+ }
1396
+ default:
1397
+
1398
+ }
1399
+
1400
+ if (!warningMsg) return true
1401
+
1402
+ const result = await CustomAlert({
1403
+ title: i18next.t('title.reminder'),
1404
+ text: warningMsg,
1405
+ confirmButton: { text: i18next.t('button.confirm') },
1406
+ cancelButton: { text: i18next.t('button.cancel') }
1407
+ })
1408
+
1409
+ return result.value
1410
+ }
1411
+
1412
+ _isSpecialLocationType(type) {
1413
+ const t = type?.toUpperCase()
1414
+ return t === 'DAMAGE' || t === 'QUARANTINE'
1415
+ }
1416
+
1417
+ async _validateAndProceedToLocation() {
1418
+ try {
1419
+ const locationName = this.toLocationNameInput.value
1420
+ const toLocation = (this._locations || []).find(l => l.name === locationName)
1421
+
1422
+ if (!toLocation) throw new Error(i18next.t('text.location_not_found'))
1423
+
1424
+ if (!this._locationRecommendationChecked) {
1425
+ const confirmed = await this._checkLocationRecommendation(locationName)
1426
+ if (!confirmed) {
1427
+ this.toLocationNameInput.value = ''
1428
+ return
1429
+ }
1430
+ this._locationRecommendationChecked = true
1431
+ }
1432
+
1433
+ this._toLocationData = toLocation
1434
+
1435
+ const toIsSpecial = this._isSpecialLocationType(toLocation.type)
1436
+ const fromLocationName = this._selectedInventory?.location?.name
1437
+ const fromLocationType = (this._locations || []).find(l => l.name === fromLocationName)?.type
1438
+ const fromIsSpecial = this._isSpecialLocationType(fromLocationType)
1439
+ const requiresReason = toIsSpecial !== fromIsSpecial // FROM SPECIAL LOCATION -> NORMAL || NORMAL -> SPECIAL LOCATION
1440
+
1441
+ if (this.reasonSelect) this.reasonSelect.value = ''
1442
+ if (this.remarkTextarea) this.remarkTextarea.value = ''
1443
+
1444
+ if (requiresReason) {
1445
+ this._showReasonField = true
1446
+ this.reasonSelect?.focus()
1447
+ await this.updateComplete
1448
+ } else {
1449
+ this._showReasonField = false
1450
+ this._showRemarkField = false
1451
+ if (this.refInventoryType === INVENTORY_TYPES.LOT.value) {
1452
+ if (this._requireNewId) {
1453
+ this.focusOnNewLotInput()
1454
+ } else {
1455
+ this.transferInventory({ stopPropagation: () => {}, preventDefault: () => {} })
1456
+ }
1457
+ } else {
1458
+ this.focusOnNewCartonInput()
1459
+ }
1460
+ }
1461
+ } catch (e) {
1462
+ this.showToast(e)
1463
+ }
736
1464
  }
737
1465
 
738
1466
  clearView() {
739
1467
  this.inputForm?.reset()
740
1468
  this._selectedInventory = null
741
1469
  this._requireNewId = false
1470
+ this._showTransferForm = false
1471
+ this._showReasonField = false
1472
+ this._showRemarkField = false
1473
+ this._toLocationData = null
1474
+ this._locationRecommendationChecked = false
742
1475
  this._data = { records: [] }
743
1476
  if (this?.toLocationNameInput) this.toLocationNameInput.value = ''
744
1477
  if (this?.newCartonInput) this.newCartonInput.value = ''