@things-factory/operato-wms 3.7.6 → 3.7.7

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 (27) hide show
  1. package/client/pages/components/select-product-popup.js +5 -1
  2. package/client/pages/constants/order.js +10 -0
  3. package/client/pages/inventory-check/inspecting-product.js +9 -8
  4. package/client/pages/manifest/create-loading-manifest.js +314 -0
  5. package/client/pages/manifest/dispatcher-verification-popup.js +86 -0
  6. package/client/pages/manifest/loading-manifest-detail.js +345 -0
  7. package/client/pages/manifest/loading-manifest-list.js +305 -0
  8. package/client/pages/order/goods-received-note/received-note-list.js +138 -6
  9. package/client/pages/order/release-order/b2b/b2b-order-list.js +1 -1
  10. package/client/pages/order/release-order/b2b/b2b-order-requests.js +1 -1
  11. package/client/pages/order/release-order/b2c/b2c-order-requests.js +111 -14
  12. package/client/pages/order/reverse-kitting-order/reverse-kitting-order-detail.js +115 -50
  13. package/client/pages/order/reverse-kitting-order/reverse-kitting-order-list.js +1 -6
  14. package/client/pages/order/reverse-kitting-order/reverse-kitting-pallet-label-popup.js +239 -0
  15. package/client/pages/order/transport-order/delivery-order-list.js +47 -3
  16. package/client/pages/outbound/picking-product.js +200 -66
  17. package/client/pages/outbound/serial-number-popup.js +280 -0
  18. package/client/pages/vas/reverse-kitting/execute-reverse-kitting.js +30 -12
  19. package/client/pages/vas/reverse-kitting/popup-combination-sets.js +75 -5
  20. package/client/pages/vas/reverse-kitting/popup-reverse-kitting-template.js +113 -18
  21. package/client/route.js +12 -0
  22. package/package.json +10 -10
  23. package/things-factory.config.js +12 -0
  24. package/translations/en.json +42 -6
  25. package/translations/ko.json +35 -1
  26. package/translations/ms.json +36 -1
  27. package/translations/zh.json +35 -1
@@ -0,0 +1,345 @@
1
+ import '@things-factory/grist-ui'
2
+ import './dispatcher-verification-popup'
3
+
4
+ import gql from 'graphql-tag'
5
+ import { css, html } from 'lit-element'
6
+
7
+ import { GhostPrint } from '@operato/ghost-print'
8
+ import { MultiColumnFormStyles } from '@things-factory/form-ui'
9
+ import { i18next, localize } from '@things-factory/i18n-base'
10
+ import { openPopup } from '@things-factory/layout-base'
11
+ import { client, navigate, PageView } from '@things-factory/shell'
12
+ import { CommonButtonStyles } from '@things-factory/styles'
13
+ import { isMobileDevice } from '@things-factory/utils'
14
+
15
+ import { MANIFEST_STATUS } from '../constants'
16
+
17
+ class LoadingManifestDetail extends localize(i18next)(PageView) {
18
+ static get styles() {
19
+ return [
20
+ MultiColumnFormStyles,
21
+ css`
22
+ :host {
23
+ display: flex;
24
+ flex-direction: column;
25
+ overflow-x: auto;
26
+ }
27
+ .container {
28
+ flex: 1;
29
+ display: flex;
30
+ overflow-y: auto;
31
+ min-height: 50vh;
32
+ }
33
+ .grist {
34
+ background-color: var(--main-section-background-color);
35
+ display: flex;
36
+ flex-direction: column;
37
+ flex: 1;
38
+ overflow-y: auto;
39
+ }
40
+ data-grist {
41
+ overflow-y: hidden;
42
+ flex: 1;
43
+ }
44
+ h2 {
45
+ padding: var(--subtitle-padding);
46
+ font: var(--subtitle-font);
47
+ color: var(--subtitle-text-color);
48
+ border-bottom: var(--subtitle-border-bottom);
49
+ }
50
+ .grist h2 {
51
+ margin: var(--grist-title-margin);
52
+ border: var(--grist-title-border);
53
+ font: var(--grist-title-font);
54
+ color: var(--secondary-color);
55
+ }
56
+
57
+ .grist h2 mwc-icon {
58
+ vertical-align: middle;
59
+ margin: var(--grist-title-icon-margin);
60
+ font-size: var(--grist-title-icon-size);
61
+ color: var(--grist-title-icon-color);
62
+ }
63
+
64
+ h2 + data-grist {
65
+ padding-top: var(--grist-title-with-grid-padding);
66
+ }
67
+ `
68
+ ]
69
+ }
70
+
71
+ static get properties() {
72
+ return {
73
+ shippingData: Object,
74
+ shippingGristConfig: Object,
75
+ _manifestNo: String,
76
+ _status: String
77
+ }
78
+ }
79
+
80
+ get context() {
81
+ var actions = [
82
+ {
83
+ title: i18next.t('button.back'),
84
+ action: () => navigate('loading_manifest_list'),
85
+ ...CommonButtonStyles.back
86
+ },
87
+ {
88
+ title: i18next.t('button.print'),
89
+ action: this._printManifest.bind(this),
90
+ ...CommonButtonStyles.label
91
+ }
92
+ ]
93
+
94
+ if (this._status == MANIFEST_STATUS.OPEN.value) {
95
+ actions.push({
96
+ title: i18next.t('button.dispatch'),
97
+ action: this._openDispatcherVerificationPopup.bind(this),
98
+ ...CommonButtonStyles.load
99
+ })
100
+ }
101
+
102
+ return {
103
+ title: i18next.t('title.manifest'),
104
+ actions
105
+ }
106
+ }
107
+
108
+ get manifestForm() {
109
+ return this.shadowRoot.querySelector('form[name=manifest]')
110
+ }
111
+
112
+ render() {
113
+ return html`
114
+ <div class="form-container">
115
+ <form name="manifest" class="multi-column-form">
116
+ <fieldset>
117
+ <legend>${i18next.t('title.manifest_no')}: ${this._manifestNo}</legend>
118
+ <label>${i18next.t('label.shipping_provider')}</label>
119
+ <input name="shippingProvider" readonly />
120
+
121
+ <label>${i18next.t('label.dispatcher_name')}</label>
122
+ <input name="dispatcherName" readonly />
123
+
124
+ <label>${i18next.t('label.dispatcher_identity')}</label>
125
+ <input name="dispatcherIdentity" readonly />
126
+
127
+ <label>${i18next.t('label.dispatch_at')}</label>
128
+ <input name="dispatchedAt" type="datetime-local" readonly />
129
+
130
+ <label>${i18next.t('label.total_tracking_no')}</label>
131
+ <input name="totalTrackingNo" readonly />
132
+
133
+ <label>${i18next.t('label.status')}</label>
134
+ <input name="status" readonly />
135
+ </fieldset>
136
+ </form>
137
+ </div>
138
+
139
+ <div class="container">
140
+ <div class="grist">
141
+ <h2><mwc-icon>list_alt</mwc-icon>${i18next.t('title.manifest_items')}</h2>
142
+
143
+ <data-grist
144
+ .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
145
+ .config=${this.shippingGristConfig}
146
+ .data=${this.shippingData}
147
+ ></data-grist>
148
+ </div>
149
+ </div>
150
+ `
151
+ }
152
+
153
+ pageInitialized() {
154
+ this.shippingGristConfig = {
155
+ list: { fields: ['refNo', 'trackingNo', 'dispatchmentStatus'] },
156
+ pagination: { infinite: true },
157
+ columns: [
158
+ { type: 'gutter', gutterName: 'sequence' },
159
+ {
160
+ type: 'string',
161
+ name: 'refNo',
162
+ label: true,
163
+ header: i18next.t('field.order_no'),
164
+ width: 200
165
+ },
166
+ {
167
+ type: 'string',
168
+ name: 'trackingNo',
169
+ label: true,
170
+ header: i18next.t('field.tracking_no'),
171
+ width: 200
172
+ },
173
+ {
174
+ type: 'string',
175
+ name: 'attentionTo',
176
+ label: true,
177
+ header: i18next.t('field.delivery_to'),
178
+ width: 150
179
+ },
180
+ {
181
+ type: 'string',
182
+ name: 'phone',
183
+ label: true,
184
+ header: i18next.t('field.phone'),
185
+ width: 150
186
+ },
187
+ {
188
+ type: 'string',
189
+ name: 'deliveryAddress',
190
+ label: true,
191
+ header: i18next.t('field.delivery_address'),
192
+ width: 250
193
+ },
194
+ {
195
+ type: 'string',
196
+ name: 'postalCode',
197
+ label: true,
198
+ header: i18next.t('field.postal_code'),
199
+ width: 120
200
+ },
201
+ {
202
+ type: 'string',
203
+ name: 'city',
204
+ label: true,
205
+ header: i18next.t('field.city'),
206
+ width: 120
207
+ },
208
+ {
209
+ type: 'string',
210
+ name: 'dispatchmentStatus',
211
+ label: true,
212
+ header: i18next.t('field.status'),
213
+ width: 150
214
+ }
215
+ ]
216
+ }
217
+ }
218
+
219
+ async pageUpdated(changes, lifecycle, before) {
220
+ if (this.active) {
221
+ this._manifestNo = changes.resourceId || this._manifestNo || ''
222
+ await this._fetchManifestDetail()
223
+ this.updateContext()
224
+ }
225
+ }
226
+
227
+ async _fetchManifestDetail() {
228
+ if (!this._manifestNo) return
229
+
230
+ const response = await client.query({
231
+ query: gql`
232
+ query manifest($name: String!) {
233
+ manifest(name: $name) {
234
+ id
235
+ name
236
+ dispatchedAt
237
+ dispatcherName
238
+ dispatcherIdentity
239
+ status
240
+ shippingProvider
241
+ trackingNos {
242
+ trackingNo
243
+ deliveryAddress
244
+ attentionTo
245
+ phone
246
+ postalCode
247
+ city
248
+ refNo
249
+ dispatchmentStatus
250
+ }
251
+ }
252
+ }
253
+ `,
254
+ variables: { name: this._manifestNo }
255
+ })
256
+
257
+ if (!response.errors) {
258
+ const manifest = response.data.manifest
259
+
260
+ this._status = manifest.status
261
+ this._fillupForm(this.manifestForm, { ...manifest, totalTrackingNo: manifest.trackingNos?.length })
262
+ this.shippingData = { records: manifest.trackingNos }
263
+ this.updateContext()
264
+ }
265
+ }
266
+
267
+ _openDispatcherVerificationPopup() {
268
+ openPopup(
269
+ html`
270
+ <dispatcher-verification-popup
271
+ @dispatch="${e => {
272
+ this._dispatchManifest(e.detail)
273
+ }}"
274
+ ></dispatcher-verification-popup>
275
+ `,
276
+ {
277
+ size: 'medium',
278
+ title: `${i18next.t('title.dispatcher_verification')}`
279
+ }
280
+ )
281
+ }
282
+
283
+ async _dispatchManifest(patch) {
284
+ const response = await client.mutate({
285
+ mutation: gql`
286
+ mutation dispatchManifest($name: String!, $patch: ManifestPatch!) {
287
+ dispatchManifest(name: $name, patch: $patch) {
288
+ id
289
+ name
290
+ }
291
+ }
292
+ `,
293
+ variables: { name: this._manifestNo, patch }
294
+ })
295
+
296
+ if (!response.errors) {
297
+ await this._fetchManifestDetail()
298
+ this._showToast({ message: i18next.t('text.manifest_has_been_dispatched_and_closed') })
299
+ }
300
+ }
301
+
302
+ _printManifest() {
303
+ GhostPrint.print({
304
+ src: `/view_manifest/${this._manifestNo}`,
305
+ pending: 1000,
306
+ onfinish: () => {
307
+ this._showToast({ message: i18next.t('text.document_has_been_generated') })
308
+ }
309
+ })
310
+ }
311
+
312
+ _fillupForm(form, data) {
313
+ for (let key in data) {
314
+ Array.from(form.querySelectorAll('input, textarea, select')).forEach(field => {
315
+ if (field.name === key && field.type === 'checkbox') {
316
+ field.checked = data[key]
317
+ } else if (field.name === key && field.type === 'datetime-local') {
318
+ if (data[key]) {
319
+ const datetime = Number(data[key])
320
+ const timezoneOffset = new Date(datetime).getTimezoneOffset() * 60000
321
+ field.value = new Date(datetime - timezoneOffset).toISOString().slice(0, -1)
322
+ } else {
323
+ field.value = data[key]
324
+ }
325
+ } else if (field.name === key) {
326
+ field.value = data[key]
327
+ }
328
+ })
329
+ }
330
+ }
331
+
332
+ _showToast({ type, message }) {
333
+ document.dispatchEvent(
334
+ new CustomEvent('notify', {
335
+ detail: {
336
+ type,
337
+ message,
338
+ option: { timer: 1000 }
339
+ }
340
+ })
341
+ )
342
+ }
343
+ }
344
+
345
+ customElements.define('loading-manifest-detail', LoadingManifestDetail)
@@ -0,0 +1,305 @@
1
+ import '@things-factory/form-ui'
2
+ import '@things-factory/grist-ui'
3
+
4
+ import gql from 'graphql-tag'
5
+ import { css, html } from 'lit-element'
6
+
7
+ import { getCodeByName } from '@things-factory/code-base'
8
+ import { i18next, localize } from '@things-factory/i18n-base'
9
+ import { client, navigate, PageView } from '@things-factory/shell'
10
+ import { ScrollbarStyles } from '@things-factory/styles'
11
+ import { isMobileDevice } from '@things-factory/utils'
12
+
13
+ class LoadingManifestList extends localize(i18next)(PageView) {
14
+ static get styles() {
15
+ return [
16
+ ScrollbarStyles,
17
+ css`
18
+ :host {
19
+ display: flex;
20
+ flex-direction: column;
21
+ overflow: hidden;
22
+ }
23
+
24
+ search-form {
25
+ overflow: visible;
26
+ }
27
+
28
+ data-grist {
29
+ overflow-y: auto;
30
+ flex: 1;
31
+ }
32
+ `
33
+ ]
34
+ }
35
+
36
+ static get properties() {
37
+ return {
38
+ _searchFields: Array,
39
+ config: Object,
40
+ data: Object
41
+ }
42
+ }
43
+
44
+ get context() {
45
+ return {
46
+ title: i18next.t('title.manifest_list'),
47
+ exportable: {
48
+ name: i18next.t('title.manifest_list'),
49
+ data: this._exportableData.bind(this)
50
+ }
51
+ }
52
+ }
53
+
54
+ get _fromDateInput() {
55
+ return this.searchForm.shadowRoot.querySelector('input[name=fromDate]')
56
+ }
57
+
58
+ get _toDateInput() {
59
+ return this.searchForm.shadowRoot.querySelector('input[name=toDate]')
60
+ }
61
+
62
+ get searchForm() {
63
+ return this.shadowRoot.querySelector('search-form')
64
+ }
65
+
66
+ get dataGrist() {
67
+ return this.shadowRoot.querySelector('data-grist')
68
+ }
69
+
70
+ render() {
71
+ return html`
72
+ <search-form id="search-form" .fields=${this._searchFields} @submit=${e => this.dataGrist.fetch()}></search-form>
73
+
74
+ <data-grist
75
+ .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
76
+ .config=${this.config}
77
+ .fetchHandler="${this.fetchHandler.bind(this)}"
78
+ ></data-grist>
79
+ `
80
+ }
81
+
82
+ pageUpdated(changes, lifecycle) {
83
+ if (this.active) {
84
+ this.dataGrist.fetch()
85
+ }
86
+ }
87
+
88
+ async pageInitialized() {
89
+ const manifestStatus = await getCodeByName('MANIFEST_STATUS')
90
+ this._searchFields = [
91
+ {
92
+ label: i18next.t('field.manifest_no'),
93
+ name: 'name',
94
+ type: 'text',
95
+ props: { searchOper: 'i_like' }
96
+ },
97
+ {
98
+ label: i18next.t('field.shipping_provider'),
99
+ name: 'shippingProvider',
100
+ type: 'text',
101
+ props: { searchOper: 'i_like' }
102
+ },
103
+ {
104
+ label: i18next.t('field.from'),
105
+ name: 'fromDate',
106
+ type: 'date',
107
+ props: {
108
+ searchOper: 'eq'
109
+ },
110
+ value: (() => {
111
+ let date = new Date()
112
+ date.setMonth(date.getMonth() - 6)
113
+ return date.toISOString().split('T')[0]
114
+ })(),
115
+ handlers: { change: this._modifyDateRange.bind(this) }
116
+ },
117
+ {
118
+ label: i18next.t('field.to'),
119
+ name: 'toDate',
120
+ type: 'date',
121
+ props: {
122
+ searchOper: 'eq',
123
+ min: (() => {
124
+ let date = new Date()
125
+ date.setMonth(date.getMonth() - 6)
126
+ return date.toISOString().split('T')[0]
127
+ })()
128
+ },
129
+ value: new Date().toISOString().split('T')[0]
130
+ },
131
+ {
132
+ label: i18next.t('field.status'),
133
+ name: 'status',
134
+ type: 'select',
135
+ options: [
136
+ { value: '' },
137
+ ...manifestStatus.map(status => {
138
+ return {
139
+ name: i18next.t(`label.${status.description}`),
140
+ value: status.name
141
+ }
142
+ })
143
+ ],
144
+ props: { searchOper: 'eq' }
145
+ }
146
+ ]
147
+
148
+ this.config = {
149
+ list: {
150
+ fields: ['name', 'shippingProvider', 'dispatchedAt', 'status', 'createdAt', 'updatedAt', 'creator']
151
+ },
152
+ rows: { appendable: false, selectable: { multiple: true } },
153
+ columns: [
154
+ { type: 'gutter', gutterName: 'sequence' },
155
+ { type: 'gutter', gutterName: 'row-selector', multiple: true },
156
+ {
157
+ type: 'gutter',
158
+ gutterName: 'button',
159
+ icon: 'reorder',
160
+ handlers: {
161
+ click: (columns, data, column, record, rowIndex) => {
162
+ navigate(`loading_manifest_detail/${record.name}`)
163
+ }
164
+ }
165
+ },
166
+ {
167
+ type: 'string',
168
+ name: 'name',
169
+ header: i18next.t('field.manifest_no'),
170
+ label: true,
171
+ sortable: true,
172
+ width: 180
173
+ },
174
+ {
175
+ type: 'string',
176
+ name: 'shippingProvider',
177
+ header: i18next.t('field.shipping_provider'),
178
+ label: true,
179
+ sortable: true,
180
+ width: 150
181
+ },
182
+ {
183
+ type: 'datetime',
184
+ name: 'dispatchedAt',
185
+ header: i18next.t('field.dispatch_at'),
186
+ label: true,
187
+ sortable: true,
188
+ width: 150
189
+ },
190
+ {
191
+ type: 'string',
192
+ name: 'status',
193
+ header: i18next.t('field.status'),
194
+ label: true,
195
+ sortable: true,
196
+ width: 170
197
+ },
198
+ {
199
+ type: 'datetime',
200
+ name: 'createdAt',
201
+ header: i18next.t('field.created_at'),
202
+ label: true,
203
+ sortable: true,
204
+ width: 160
205
+ },
206
+ {
207
+ type: 'object',
208
+ name: 'creator',
209
+ header: i18next.t('field.created_by'),
210
+ label: true,
211
+ sortable: true,
212
+ width: 200
213
+ }
214
+ ]
215
+ }
216
+ }
217
+
218
+ async fetchHandler({ page, limit, sorters = [{ name: 'createdAt', desc: true }] }) {
219
+ const response = await client.query({
220
+ query: gql`
221
+ query manifests($filters: [Filter], $pagination: Pagination, $sortings: [Sorting]) {
222
+ manifests(filters: $filters, pagination: $pagination, sortings: $sortings) {
223
+ items {
224
+ id
225
+ name
226
+ shippingProvider
227
+ dispatchedAt
228
+ status
229
+ createdAt
230
+ creator {
231
+ id
232
+ name
233
+ description
234
+ }
235
+ }
236
+ total
237
+ }
238
+ }
239
+ `,
240
+ variables: { filters: this.searchForm.queryFilters, pagination: { page, limit }, sortings: sorters }
241
+ })
242
+
243
+ if (!response.errors) {
244
+ return {
245
+ total: response.data.manifests.total || 0,
246
+ records: response.data.manifests.items || []
247
+ }
248
+ }
249
+ }
250
+
251
+ _modifyDateRange(e) {
252
+ const fromDate = e.currentTarget.value
253
+
254
+ if (this._toDateInput.value < fromDate) this._toDateInput.value = fromDate
255
+
256
+ let min = new Date(fromDate)
257
+ let today = new Date()
258
+ today.setHours(0, 0, 0, 0)
259
+
260
+ min = min.toISOString().split('T')[0]
261
+
262
+ this._toDateInput.min = min
263
+ }
264
+
265
+ get _columns() {
266
+ return this.config.columns
267
+ }
268
+
269
+ async _exportableData() {
270
+ try {
271
+ let data = []
272
+
273
+ var headerSetting = [
274
+ { header: i18next.t('field.manifest_no'), key: 'name', width: 25, type: 'string' },
275
+ { header: i18next.t('field.shipping_provider'), key: 'shippingProvider', width: 25, type: 'string' },
276
+ { header: i18next.t('field.dispatch_at'), key: 'dispatchedAt', width: 25, type: 'date' },
277
+ { header: i18next.t('field.status'), key: 'status', width: 25, type: 'string' },
278
+ { header: i18next.t('field.created_at'), key: 'createdAt', width: 30, type: 'date' },
279
+ { header: i18next.t('field.created_by'), key: 'createdBy', width: 25, type: 'string' }
280
+ ]
281
+
282
+ if (this.dataGrist.selected && this.dataGrist.selected.length > 0) {
283
+ data = this.dataGrist.selected.map(record => {
284
+ return {
285
+ ...record,
286
+ createdBy: record.creator.name
287
+ }
288
+ })
289
+ } else {
290
+ data = (await this.fetchHandler({ page: 1, limit: 9999999 })).records.map(record => {
291
+ return {
292
+ ...record,
293
+ createdBy: record.creator.name
294
+ }
295
+ })
296
+ }
297
+
298
+ return { header: headerSetting, data }
299
+ } catch (e) {
300
+ this._showToast(e)
301
+ }
302
+ }
303
+ }
304
+
305
+ customElements.define('loading-manifest-list', LoadingManifestList)