@things-factory/operato-mms 3.7.6 → 3.7.10

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.
package/client/menu.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import '@material/mwc-icon'
2
- import { store } from '@things-factory/shell'
2
+
3
3
  import { updateMenuTemplate } from '@things-factory/lite-menu'
4
+ import { store } from '@things-factory/shell'
4
5
 
5
- var stores
6
+ var stores, platforms
6
7
 
7
8
  store.subscribe(() => {
8
9
  var state = store.getState()
@@ -13,6 +14,14 @@ store.subscribe(() => {
13
14
 
14
15
  stores = state.mms.stores
15
16
 
17
+ // At the moment, only Sales by Platform will use this variable
18
+ // and currently only support for lazada and shopee
19
+ platforms = stores
20
+ .map(store => store.platform)
21
+ .filter((v, i, a) => a.indexOf(v) === i)
22
+ .filter(v => ['lazada', 'shopee'].indexOf(v) !== -1)
23
+ .sort()
24
+
16
25
  updateMenuTemplate(
17
26
  getMenuTemplate({
18
27
  stores: stores || []
@@ -103,6 +112,35 @@ function getMenuTemplate({ stores }) {
103
112
  path: 'mms-inventory-product',
104
113
  icon: 'storage'
105
114
  },
115
+ {
116
+ name: 'report',
117
+ type: 'group'
118
+ },
119
+ {
120
+ name: 'sales by platform',
121
+ path: 'mms-report-sales-by-platform-home',
122
+ icon: 'assessment',
123
+ menus: [
124
+ ...platforms.map(platform => {
125
+ return {
126
+ name: platform,
127
+ path: `${platform}-sales-report`,
128
+ icon: 'store'
129
+ }
130
+ })
131
+ // to be removed. it was here for testing purpose
132
+ /* {
133
+ name: 'lazada',
134
+ path: 'lazada-sales-report',
135
+ icon: 'store'
136
+ },
137
+ {
138
+ name: 'shopee',
139
+ path: 'shopee-sales-report',
140
+ icon: 'store'
141
+ } */
142
+ ]
143
+ },
106
144
  {
107
145
  name: 'store integration',
108
146
  type: 'group'
@@ -0,0 +1,144 @@
1
+ import { css, html } from 'lit-element'
2
+ import { connect } from 'pwa-helpers/connect-mixin.js'
3
+
4
+ import { PageView, store } from '@things-factory/shell'
5
+
6
+ class SalesByPlatformHome extends connect(store)(PageView) {
7
+ static get styles() {
8
+ return [
9
+ css`
10
+ :host {
11
+ display: block;
12
+ background-color: var(--main-section-background-color);
13
+ padding: var(--padding-wide);
14
+ overflow: auto;
15
+ --bizplace-cards-size: 150px;
16
+ }
17
+ h2 {
18
+ margin: var(--title-margin);
19
+ font: var(--title-font);
20
+ color: var(--title-text-color);
21
+ }
22
+ input {
23
+ display: block;
24
+ }
25
+ [page-description] {
26
+ margin: var(--page-description-margin);
27
+ font: var(--page-description-font);
28
+ color: var(--page-description-color);
29
+ }
30
+ [button-primary] {
31
+ background-color: var(--button-primary-background-color);
32
+ border: var(--button-border);
33
+ border-radius: var(--button-border-radius);
34
+ padding: var(--button-padding);
35
+ color: var(--button-primary-color);
36
+ font: var(--button-primary-font);
37
+ text-transform: var(--button-text-transform);
38
+ text-decoration: none;
39
+ }
40
+ [button-primary]:hover {
41
+ background-color: var(--button-primary-active-background-color);
42
+ box-shadow: var(--button-active-box-shadow);
43
+ }
44
+ img {
45
+ max-width: 45%;
46
+ float: right;
47
+ }
48
+ ul {
49
+ margin: 0 0 var(--margin-wide) 0;
50
+ padding: 0;
51
+ overflow: hidden;
52
+ list-style: none;
53
+ }
54
+ li {
55
+ display: block;
56
+ float: left;
57
+ background-color: var(--theme-white-color);
58
+ border-radius: var(--border-radius);
59
+ border: 1px solid var(--theme-white-color);
60
+ box-shadow: var(--box-shadow);
61
+ margin: var(--margin-narrow) var(--margin-default) var(--margin-default) 0;
62
+ padding: var(--padding-default) var(--padding-default) var(--padding-narrow) var(--padding-default);
63
+ text-align: center;
64
+ }
65
+ li:hover {
66
+ border: 1px solid var(--primary-color);
67
+ }
68
+ * {
69
+ display: block;
70
+ }
71
+ a {
72
+ width: var(--bizplace-cards-size);
73
+ cursor: pointer;
74
+ text-decoration: none;
75
+ font-size: var(--fontsize-small);
76
+ color: var(--secondary-color);
77
+ }
78
+ mwc-icon {
79
+ color: var(--primary-color);
80
+ }
81
+ strong {
82
+ max-width: 100%;
83
+
84
+ font-size: 1.2em;
85
+ color: var(--secondary-text-color);
86
+ }
87
+ a,
88
+ strong @media screen and (max-width: 600px) {
89
+ [field] {
90
+ grid-column: span 2;
91
+ }
92
+ img {
93
+ display: none;
94
+ }
95
+ }
96
+ `
97
+ ]
98
+ }
99
+
100
+ static get properties() {
101
+ return {
102
+ _platforms: Array
103
+ }
104
+ }
105
+
106
+ get context() {
107
+ return {
108
+ title: `ecommerce products`
109
+ }
110
+ }
111
+
112
+ render() {
113
+ const platforms = this._platforms || []
114
+
115
+ return html`
116
+ <h2>Sales Report</h2>
117
+ <p page-description>View your day to day sales report.</p>
118
+
119
+ <img src="/assets/images/image-products.png" alt="products home graphic image" />
120
+
121
+ <ul marketplaceStore>
122
+ ${platforms.map(
123
+ platform => html`
124
+ <li>
125
+ <a href="${platform}-sales-report">
126
+ <mwc-icon>${'widgets'}</mwc-icon>
127
+ <strong>${platform}</strong>
128
+ </a>
129
+ </li>
130
+ `
131
+ )}
132
+ </ul>
133
+ `
134
+ }
135
+
136
+ stateChanged(state) {
137
+ this._platforms = state.mms?.stores
138
+ .map(store => store.platform)
139
+ .filter((v, i, a) => a.indexOf(v) === i)
140
+ .filter(v => ['lazada', 'shopee'].indexOf(v) !== -1)
141
+ }
142
+ }
143
+
144
+ window.customElements.define('mms-report-sales-by-platform-home', SalesByPlatformHome)
@@ -0,0 +1,3 @@
1
+ export * from './home'
2
+ export * from './lazada-sales-report'
3
+ export * from './shopee-sales-report'
@@ -0,0 +1,379 @@
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
+ import { connect } from 'pwa-helpers/connect-mixin.js'
7
+
8
+ import { i18next } from '@things-factory/i18n-base'
9
+ import { client, PageView, store } from '@things-factory/shell'
10
+ import { ScrollbarStyles } from '@things-factory/styles'
11
+ import { isMobileDevice } from '@things-factory/utils'
12
+
13
+ class LazadaSalesReport extends connect(store)(PageView) {
14
+ static get styles() {
15
+ return [
16
+ ScrollbarStyles,
17
+ css`
18
+ :host {
19
+ display: flex;
20
+ flex-direction: column;
21
+
22
+ overflow: hidden;
23
+ }
24
+
25
+ search-form {
26
+ overflow: visible;
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
+ }
41
+ }
42
+
43
+ get context() {
44
+ return {
45
+ title: i18next.t('title.lazada_sales_report'),
46
+ exportable: {
47
+ name: i18next.t('title.lazada_sales_report'),
48
+ data: this._exportableData.bind(this)
49
+ }
50
+ }
51
+ }
52
+
53
+ get _searchForm() {
54
+ return this.shadowRoot.querySelector('search-form')
55
+ }
56
+
57
+ get _dataGrist() {
58
+ return this.shadowRoot.querySelector('data-grist')
59
+ }
60
+
61
+ get _toDateInput() {
62
+ return this._searchForm.shadowRoot.querySelector('input[name=toDate]')
63
+ }
64
+
65
+ render() {
66
+ return html`
67
+ <search-form id="search-form" .fields=${this._searchFields} @submit=${e => this._dataGrist.fetch()}></search-form>
68
+ <data-grist
69
+ .mode=${isMobileDevice() ? 'LIST' : 'GRID'}
70
+ .config=${this._config}
71
+ .fetchHandler="${this.fetchHandler.bind(this)}"
72
+ ></data-grist>
73
+ `
74
+ }
75
+
76
+ async pageInitialized() {
77
+ this._searchFields = [
78
+ {
79
+ label: i18next.t('field.store'),
80
+ name: 'marketplaceStoreId',
81
+ type: 'select',
82
+ options: [{ value: '' }],
83
+ props: { searchOper: 'eq' }
84
+ },
85
+ {
86
+ label: i18next.t('field.from_date'),
87
+ name: 'fromDate',
88
+ type: 'date',
89
+ props: {
90
+ searchOper: 'eq',
91
+ max: new Date().toISOString().split('T')[0]
92
+ },
93
+ value: (() => {
94
+ let date = new Date()
95
+ date.setMonth(date.getMonth() - 1)
96
+ return date.toISOString().split('T')[0]
97
+ })(),
98
+ handlers: { change: this._modifyDateRange.bind(this) }
99
+ },
100
+ {
101
+ label: i18next.t('field.to_date'),
102
+ name: 'toDate',
103
+ type: 'date',
104
+ props: {
105
+ searchOper: 'eq',
106
+ min: (() => {
107
+ let date = new Date()
108
+ date.setMonth(date.getMonth() - 1)
109
+ return date.toISOString().split('T')[0]
110
+ })(),
111
+ max: new Date().toISOString().split('T')[0]
112
+ },
113
+ value: new Date().toISOString().split('T')[0]
114
+ }
115
+ ]
116
+
117
+ this._config = {
118
+ rows: { appendable: false },
119
+ pagination: { limit: 50, pages: [50, 100, 200, 500, 1000] },
120
+ columns: [
121
+ { type: 'gutter', gutterName: 'sequence' },
122
+ {
123
+ type: 'string',
124
+ name: 'marketplaceStore',
125
+ header: i18next.t('field.store_name'),
126
+ sortable: true,
127
+ imex: { header: i18next.t('field.store_name'), key: 'marketplaceStore', width: 50, type: 'string' },
128
+ width: 200
129
+ },
130
+ {
131
+ type: 'string',
132
+ name: 'orderNo',
133
+ header: i18next.t('field.order_no'),
134
+ sortable: true,
135
+ imex: { header: i18next.t('field.order_no'), key: 'orderNo', width: 50, type: 'string' },
136
+ width: 200
137
+ },
138
+ {
139
+ type: 'string',
140
+ name: 'marketplaceOrderItem',
141
+ header: i18next.t('field.item'),
142
+ sortable: false,
143
+ imex: { header: i18next.t('field.item'), key: 'marketplaceOrderItem', width: 120, type: 'string' },
144
+ width: 400
145
+ },
146
+ {
147
+ type: 'datetime',
148
+ name: 'orderCreatedAt',
149
+ header: i18next.t('field.order_created_date'),
150
+ sortable: false,
151
+ imex: { header: i18next.t('field.order_created_date'), key: 'orderCreatedAt', width: 30, type: 'string' },
152
+ width: 200
153
+ },
154
+ {
155
+ type: 'float',
156
+ name: 'originalPrice',
157
+ header: i18next.t('field.original_price'),
158
+ sortable: false,
159
+ imex: {
160
+ header: i18next.t('field.original_price'),
161
+ key: 'originalPrice',
162
+ width: 30,
163
+ type: 'float'
164
+ },
165
+ width: 200
166
+ },
167
+ {
168
+ type: 'float',
169
+ name: 'shippingFee',
170
+ header: i18next.t('field.shipping_fee'),
171
+ sortable: false,
172
+ imex: { header: i18next.t('field.shipping_fee'), key: 'shippingFee', width: 30, type: 'float' },
173
+ width: 200
174
+ },
175
+ {
176
+ type: 'float',
177
+ name: 'actualShippingFee',
178
+ header: i18next.t('field.actual_shipping_fee'),
179
+ sortable: false,
180
+ imex: {
181
+ header: i18next.t('field.actual_shipping_fee'),
182
+ key: 'actualShippingFee',
183
+ width: 30,
184
+ type: 'float'
185
+ },
186
+ width: 200
187
+ },
188
+ {
189
+ type: 'float',
190
+ name: 'paymentFee',
191
+ header: i18next.t('field.payment_fee'),
192
+ sortable: false,
193
+ imex: { header: i18next.t('field.payment_fee'), key: 'paymentFee', width: 30, type: 'float' },
194
+ width: 200
195
+ },
196
+ {
197
+ type: 'float',
198
+ name: 'marketplaceShippingFeeVoucher',
199
+ header: i18next.t('field.shipping_fee_voucher'),
200
+ sortable: false,
201
+ imex: {
202
+ header: i18next.t('field.shipping_fee_voucher'),
203
+ key: 'marketplaceShippingFeeVoucher',
204
+ width: 30,
205
+ type: 'float'
206
+ },
207
+ width: 200
208
+ },
209
+ {
210
+ type: 'float',
211
+ name: 'sellerShippingFeeVoucher',
212
+ header: i18next.t('field.seller_shipping_fee_voucher'),
213
+ sortable: false,
214
+ imex: {
215
+ header: i18next.t('field.seller_shipping_fee_voucher'),
216
+ key: 'sellerShippingFeeVoucher',
217
+ width: 30,
218
+ type: 'float'
219
+ },
220
+ width: 200
221
+ },
222
+ {
223
+ type: 'float',
224
+ name: 'paidPrice',
225
+ header: i18next.t('field.paid_price'),
226
+ sortable: false,
227
+ imex: { header: i18next.t('field.paid_price'), key: 'paidPrice', width: 30, type: 'float' },
228
+ width: 200
229
+ },
230
+ {
231
+ type: 'float',
232
+ name: 'totalAmount',
233
+ header: i18next.t('field.total_amount'),
234
+ sortable: false,
235
+ imex: { header: i18next.t('field.total_amount'), key: 'totalAmount', width: 30, type: 'float' },
236
+ width: 200
237
+ }
238
+ ]
239
+ }
240
+ }
241
+
242
+ async fetchHandler({ page, limit, sorters = [{ name: 'marketplaceStore' }, { name: 'orderNo' }] }) {
243
+ let filters = [...this._searchForm.queryFilters, { name: 'platform', operator: 'eq', value: 'lazada' }]
244
+
245
+ const pagination = { page, limit }
246
+ const sortings = sorters
247
+
248
+ const response = await client.query({
249
+ query: gql`
250
+ query marketplaceSalesReports($filters: [Filter], $pagination: Pagination, $sortings: [Sorting]) {
251
+ marketplaceSalesReports(filters: $filters, pagination: $pagination, sortings: $sortings) {
252
+ items {
253
+ orderNo
254
+ orderCreatedAt
255
+ buyerUsername
256
+ payTime
257
+ totalAmount
258
+ refundAmount
259
+ shippingProvider
260
+ storeName
261
+ storeDescription
262
+ promotionId
263
+ originalPrice
264
+ paidPrice
265
+ sku
266
+ variationSku
267
+ productName
268
+ productSku
269
+ productDiscount
270
+ paymentFee
271
+ shippingFee
272
+ actualShippingFee
273
+ marketplaceShippingFeeVoucher
274
+ sellerShippingFeeVoucher
275
+ }
276
+ total
277
+ }
278
+ }
279
+ `,
280
+ variables: { filters, pagination, sortings }
281
+ })
282
+
283
+ if (!response.errors) {
284
+ return {
285
+ total: response.data.marketplaceSalesReports.total,
286
+ records: response.data.marketplaceSalesReports.items.map(item => {
287
+ return {
288
+ ...item,
289
+ marketplaceOrderItem: `${item.variationSku} ${item.sku ? `(${item.sku})` : ''}`,
290
+ marketplaceStore: `${item.storeName} ${item.storeDescription ? `(${item.storeDescription})` : ''}`,
291
+ originalPrice: this._getRoundedValue(item.originalPrice),
292
+ shippingFee: this._getRoundedValue(item.shippingFee),
293
+ actualShippingFee: this._getRoundedValue(item.actualShippingFee),
294
+ paymentFee: this._getRoundedValue(item.paymentFee),
295
+ marketplaceShippingFeeVoucher: this._getRoundedValue(item.marketplaceShippingFeeVoucher),
296
+ sellerShippingFeeVoucher: this._getRoundedValue(item.sellerShippingFeeVoucher),
297
+ paidPrice: this._getRoundedValue(item.paidPrice),
298
+ totalAmount: this._getRoundedValue(item.totalAmount)
299
+ }
300
+ })
301
+ }
302
+ }
303
+ }
304
+
305
+ stateChanged(state) {
306
+ if (state.mms && state.mms.stores && state.mms.stores.length) {
307
+ const _lazadaStores = state.mms.stores.filter(store => store.platform.toLowerCase() == 'lazada')
308
+ this._updateSearchFields(_lazadaStores)
309
+ this._dataGrist?.fetch()
310
+ }
311
+ }
312
+
313
+ _updateSearchFields(stores) {
314
+ if (!this._searchFields?.length) return
315
+
316
+ this._searchFields = this._searchFields.map(searchField => {
317
+ if (searchField.name === 'marketplaceStoreId') {
318
+ searchField.options = [
319
+ { value: '' },
320
+ ...stores.map(store => {
321
+ return {
322
+ name: `${store.name} ${store.description ? `(${store.description})` : ''}`,
323
+ value: store.id
324
+ }
325
+ })
326
+ ]
327
+ }
328
+
329
+ return searchField
330
+ })
331
+ }
332
+
333
+ _exportableData() {
334
+ const headerSetting = this._config.columns
335
+ .filter(column => column.type !== 'gutter' && column.imex !== undefined)
336
+ .map(column => {
337
+ return column.imex
338
+ })
339
+
340
+ const data = this._dataGrist.data.records.map(item => {
341
+ return {
342
+ ...this._config.columns
343
+ .filter(column => column.type !== 'gutter' && column.imex !== undefined)
344
+ .reduce((record, column) => {
345
+ record[column.imex.key] = column.imex.key
346
+ .split('.')
347
+ .reduce((obj, key) => (obj && obj[key] !== 'undefined' ? obj[key] : undefined), item)
348
+
349
+ if (column.type === 'datetime' && record[column.imex.key]) {
350
+ record[column.imex.key] = new Date(Number(record[column.imex.key])).toLocaleString()
351
+ }
352
+ return record
353
+ }, {})
354
+ }
355
+ })
356
+
357
+ return { header: headerSetting, data: data }
358
+ }
359
+
360
+ _modifyDateRange(e) {
361
+ const fromDate = e.currentTarget.value
362
+
363
+ if (this._toDateInput.value < fromDate) this._toDateInput.value = fromDate
364
+
365
+ let min = new Date(fromDate)
366
+ let today = new Date()
367
+ today.setHours(0, 0, 0, 0)
368
+
369
+ min = min.toISOString().split('T')[0]
370
+
371
+ this._toDateInput.min = min
372
+ }
373
+
374
+ _getRoundedValue(value = 0) {
375
+ return Math.round((value + Number.EPSILON) * 100) / 100
376
+ }
377
+ }
378
+
379
+ customElements.define('lazada-sales-report', LazadaSalesReport)