Passed
Push — trunk ( 3009d4...5cf225 )
by Christian
15:03 queued 01:27
created

index.ts ➔ data   A

Complexity

Conditions 1

Size

Total Lines 22
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 20
dl 0
loc 22
rs 9.4
c 0
b 0
f 0
1
import type EntityCollection from '@shopware-ag/admin-extension-sdk/es/data/_internals/EntityCollection';
2
import template from './sw-dashboard-statistics.html.twig';
3
import './sw-dashboard-statistics.scss';
4
5
const { Criteria } = Shopware.Data;
6
7
type OrderEntity = EntitySchema.order;
8
9
type HistoryDateRange = {
10
    label: string,
11
    range: number,
12
    interval: 'hour' | 'day',
13
    aggregate: 'hour' | 'day',
14
}
15
16
type BucketData = {
17
    key: string,
18
    count: number,
19
    totalAmount: {
20
        sum: number,
21
    },
22
}
23
24
type HistoryOrderDataCount = {
25
    apiAlias: 'order_count_bucket_aggregation',
26
    buckets: Array<BucketData>,
27
    name: 'order_count_bucket',
28
}
29
30
type HistoryOrderDataSum = {
31
    apiAlias: 'order_sum_bucket_aggregation',
32
    buckets: Array<BucketData>,
33
    name: 'order_sum_bucket',
34
}
35
36
type HistoryOrderData = HistoryOrderDataCount | HistoryOrderDataSum | null;
37
38
interface ComponentData {
39
    historyOrderDataCount: HistoryOrderDataCount | null,
40
    historyOrderDataSum: HistoryOrderDataSum | null,
41
    todayOrderData: EntityCollection<'order'> | null,
42
    todayOrderDataLoaded: boolean
43
    todayOrderDataSortBy: 'orderDateTime',
44
    todayOrderDataSortDirection: 'DESC' | 'ASC',
45
    ordersDateRange: HistoryDateRange,
46
    turnoverDateRange: HistoryDateRange,
47
    isLoading: boolean,
48
}
49
50
/**
51
 * @package merchant-services
52
 *
53
 * @private
54
 */
55
export default Shopware.Component.wrapComponentConfig({
56
    template,
57
58
    inject: [
59
        'repositoryFactory',
60
        'stateStyleDataProviderService',
61
        'acl',
62
    ],
63
64
    data(): ComponentData {
65
        return {
66
            historyOrderDataCount: null,
67
            historyOrderDataSum: null,
68
            todayOrderData: null,
69
            todayOrderDataLoaded: false,
70
            todayOrderDataSortBy: 'orderDateTime',
71
            todayOrderDataSortDirection: 'DESC',
72
            ordersDateRange: {
73
                label: '30Days',
74
                range: 30,
75
                interval: 'day',
76
                aggregate: 'day',
77
            },
78
            turnoverDateRange: {
79
                label: '30Days',
80
                range: 30,
81
                interval: 'day',
82
                aggregate: 'day',
83
            },
84
            isLoading: true,
85
        };
86
    },
87
88
    computed: {
89
        rangesValueMap(): Array<HistoryDateRange> {
90
            return [{
91
                label: '30Days',
92
                range: 30,
93
                interval: 'day',
94
                aggregate: 'day',
95
            }, {
96
                label: '14Days',
97
                range: 14,
98
                interval: 'day',
99
                aggregate: 'day',
100
            }, {
101
                label: '7Days',
102
                range: 7,
103
                interval: 'day',
104
                aggregate: 'day',
105
            }, {
106
                label: '24Hours',
107
                range: 24,
108
                interval: 'hour',
109
                aggregate: 'hour',
110
            }, {
111
                label: 'yesterday',
112
                range: 1,
113
                interval: 'day',
114
                aggregate: 'hour',
115
            }];
116
        },
117
118
        availableRanges(): string[] {
119
            return this.rangesValueMap.map((range) => range.label);
120
        },
121
122
        chartOptionsOrderCount() {
123
            return {
124
                xaxis: {
125
                    type: 'datetime',
126
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
127
                    min: this.getDateAgo(this.ordersDateRange).getTime(),
128
                    labels: {
129
                        datetimeUTC: false,
130
                    },
131
                },
132
                yaxis: {
133
                    min: 0,
134
                    tickAmount: 3,
135
                    labels: {
136
                        formatter: (value: string) => { return parseInt(value, 10); },
137
                    },
138
                },
139
            };
140
        },
141
142
        chartOptionsOrderSum() {
143
            return {
144
                xaxis: {
145
                    type: 'datetime',
146
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
147
                    min: this.getDateAgo(this.turnoverDateRange).getTime(),
148
                    labels: {
149
                        datetimeUTC: false,
150
                    },
151
                },
152
                yaxis: {
153
                    min: 0,
154
                    tickAmount: 5,
155
                    labels: {
156
                        // price aggregations do not support currencies yet, see NEXT-5069
157
                        formatter: (value: string) => Shopware.Utils.format.currency(
158
                            Number.parseFloat(value),
159
                            Shopware.Context.app.systemCurrencyISOCode as string,
160
                            2,
161
                        ),
162
                    },
163
                },
164
            };
165
        },
166
167
        orderRepository() {
168
            return this.repositoryFactory.create('order');
169
        },
170
171
        orderCountSeries() {
172
            if (!this.historyOrderDataCount) {
173
                return [];
174
            }
175
176
            // format data for chart
177
            const seriesData = this.historyOrderDataCount.buckets.map((data: BucketData) => {
178
                return { x: this.parseDate(data.key), y: data.count };
179
            });
180
181
            // add empty value for today if there isn't any order, otherwise today would be missing
182
            if (!this.todayBucketCount) {
183
                seriesData.push({ x: this.today.getTime(), y: 0 });
184
            }
185
186
            return [{ name: this.$tc('sw-dashboard.monthStats.numberOfOrders'), data: seriesData }];
187
        },
188
189
        orderCountToday() {
190
            if (this.todayBucketCount) {
191
                return this.todayBucketCount.count;
192
            }
193
            return 0;
194
        },
195
196
        orderSumMonthSeries() {
197
            return this.orderSumSeries;
198
        },
199
200
        orderSumSeries() {
201
            if (!this.historyOrderDataSum) {
202
                return [];
203
            }
204
205
            // format data for chart
206
            const seriesData = this.historyOrderDataSum.buckets.map((data) => {
207
                return { x: this.parseDate(data.key), y: data.totalAmount.sum };
208
            });
209
210
            // add empty value for today if there isn't any order, otherwise today would be missing
211
            if (!this.todayBucketSum) {
212
                seriesData.push({ x: this.today.getTime(), y: 0 });
213
            }
214
215
            return [{ name: this.$tc('sw-dashboard.monthStats.totalTurnover'), data: seriesData }];
216
        },
217
218
        orderSumToday() {
219
            if (this.todayBucketCount) {
220
                return this.todayBucketCount.totalAmount.sum;
221
            }
222
            return 0;
223
        },
224
225
        hasOrderToday() {
226
            return this.todayOrderData && this.todayOrderData.length > 0;
227
        },
228
229
        hasOrderInMonth() {
230
            return !!this.historyOrderDataCount && !!this.historyOrderDataSum;
231
        },
232
233
        today() {
234
            const today = Shopware.Utils.format.dateWithUserTimezone();
235
            today.setHours(0, 0, 0, 0);
236
            return today;
237
        },
238
239
        todayBucketCount(): BucketData | null {
240
            return this.calculateTodayBucket(this.historyOrderDataCount);
241
        },
242
243
        todayBucketSum(): BucketData | null {
244
            return this.calculateTodayBucket(this.historyOrderDataSum);
245
        },
246
247
        systemCurrencyISOCode() {
248
            return Shopware.Context.app.systemCurrencyISOCode;
249
        },
250
251
        isSessionLoaded() {
252
            return !Shopware.State.get('session')?.userPending;
253
        },
254
    },
255
256
    watch: {
257
        isSessionLoaded: {
258
            immediate: true,
259
            async handler() {
260
                if (this.isSessionLoaded) {
261
                    await this.initializeOrderData();
262
                }
263
            },
264
        },
265
    },
266
267
    methods: {
268
        calculateTodayBucket(aggregation: HistoryOrderData): BucketData | null {
269
            const buckets = aggregation?.buckets;
270
271
            if (!buckets) {
272
                return null;
273
            }
274
275
            const today = this.today;
276
            // search for stats with same timestamp as today
277
            const findDateStats = buckets.find((dateCount) => {
278
                // when date exists
279
                if (dateCount.key) {
280
                    // if time is today
281
                    const date = new Date(dateCount.key);
282
283
                    return date.setHours(0, 0, 0, 0) === today.setHours(0, 0, 0, 0);
284
                }
285
286
                return false;
287
            });
288
289
            if (findDateStats) {
290
                return findDateStats;
291
            }
292
            return null;
293
        },
294
295
        async initializeOrderData() {
296
            if (!this.acl.can('order.viewer')) {
297
                this.isLoading = false;
298
299
                return;
300
            }
301
302
            this.todayOrderDataLoaded = false;
303
304
            await this.getHistoryOrderData();
305
            this.todayOrderData = await this.fetchTodayData();
306
            this.todayOrderDataLoaded = true;
307
            this.isLoading = false;
308
        },
309
310
        getHistoryOrderData() {
311
            return Promise.all([
312
                this.fetchHistoryOrderDataCount().then((response) => {
313
                    if (response.aggregations) {
314
                        // @ts-expect-error
315
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
316
                        this.historyOrderDataCount = response.aggregations.order_count_bucket;
317
                    }
318
                }),
319
                this.fetchHistoryOrderDataSum().then((response) => {
320
                    if (response.aggregations) {
321
                        // @ts-expect-error
322
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
323
                        this.historyOrderDataSum = response.aggregations.order_sum_bucket;
324
                    }
325
                }),
326
            ]);
327
        },
328
329
        fetchHistoryOrderDataCount() {
330
            const criteria = new Criteria(1, 1);
331
332
            criteria.addAggregation(
333
                Criteria.histogram(
334
                    'order_count_bucket',
335
                    'orderDateTime',
336
                    this.ordersDateRange.aggregate,
337
                    null,
338
                    Criteria.sum('totalAmount', 'amountTotal'),
339
                    // eslint-disable-next-line max-len
340
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
341
                    Shopware.State.get('session').currentUser?.timeZone ?? 'UTC',
342
                ),
343
            );
344
345
            criteria.addFilter(Criteria.range('orderDate', {
346
                gte: this.formatDateToISO(this.getDateAgo(this.ordersDateRange)),
347
            }));
348
349
            return this.orderRepository.search(criteria);
350
        },
351
352
        fetchHistoryOrderDataSum() {
353
            const criteria = new Criteria(1, 1);
354
355
            criteria.addAggregation(
356
                Criteria.histogram(
357
                    'order_sum_bucket',
358
                    'orderDateTime',
359
                    this.turnoverDateRange.aggregate,
360
                    null,
361
                    Criteria.sum('totalAmount', 'amountTotal'),
362
                    // eslint-disable-next-line max-len
363
                    // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
364
                    Shopware.State.get('session').currentUser?.timeZone ?? 'UTC',
365
                ),
366
            );
367
368
            criteria.addAssociation('stateMachineState');
369
370
            criteria.addFilter(Criteria.equals('transactions.stateMachineState.technicalName', 'paid'));
371
            criteria.addFilter(Criteria.range('orderDate', {
372
                gte: this.formatDateToISO(this.getDateAgo(this.turnoverDateRange)),
373
            }));
374
375
            return this.orderRepository.search(criteria);
376
        },
377
378
        fetchTodayData() {
379
            const criteria = new Criteria(1, 10);
380
381
            criteria.addAssociation('currency');
382
383
            criteria.addFilter(Criteria.equals('orderDate', this.formatDateToISO(new Date())));
384
            criteria.addSorting(Criteria.sort(this.todayOrderDataSortBy, this.todayOrderDataSortDirection));
385
386
            return this.orderRepository.search(criteria);
387
        },
388
389
        formatDateToISO(date: Date) {
390
            return Shopware.Utils.format.toISODate(date, false);
391
        },
392
393
        formatChartHeadlineDate(date: Date) {
394
            const lastKnownLang = Shopware.Application.getContainer('factory').locale.getLastKnownLocale();
395
396
            return date.toLocaleDateString(lastKnownLang, {
397
                day: 'numeric',
398
                month: 'short',
399
            });
400
        },
401
402
        orderGridColumns() {
403
            return [{
404
                property: 'orderNumber',
405
                label: 'sw-order.list.columnOrderNumber',
406
                routerLink: 'sw.order.detail',
407
                allowResize: true,
408
                primary: true,
409
            }, {
410
                property: 'orderDateTime',
411
                dataIndex: 'orderDateTime',
412
                label: 'sw-dashboard.todayStats.orderTime',
413
                allowResize: true,
414
                primary: false,
415
            }, {
416
                property: 'orderCustomer.firstName',
417
                dataIndex: 'orderCustomer.firstName,orderCustomer.lastName',
418
                label: 'sw-order.list.columnCustomerName',
419
                allowResize: true,
420
            }, {
421
                property: 'stateMachineState.name',
422
                label: 'sw-order.list.columnState',
423
                allowResize: true,
424
            }, {
425
                property: 'amountTotal',
426
                label: 'sw-order.list.columnAmount',
427
                align: 'right',
428
                allowResize: true,
429
            }];
430
        },
431
432
        getVariantFromOrderState(order: OrderEntity): string {
433
            /* eslint-disable */
434
            return this.stateStyleDataProviderService.getStyle(
435
                'order.state',
436
                order.stateMachineState?.technicalName,
437
            ).variant;
438
            /* eslint-enable */
439
        },
440
441
        parseDate(date: string): number {
442
            const parsedDate = new Date(date.replace(/-/g, '/').replace('T', ' ').replace(/\..*|\+.*/, ''));
443
            return parsedDate.valueOf();
444
        },
445
446
        async onOrdersRangeUpdate(range: string): Promise<void> {
447
            const ordersDateRange = this.rangesValueMap.find((item: HistoryDateRange) => item.label === range);
448
449
            if (!ordersDateRange) {
450
                throw Error('Range not found');
451
            }
452
453
            this.ordersDateRange = ordersDateRange;
454
455
            const response = await this.fetchHistoryOrderDataCount();
456
457
            if (response.aggregations) {
458
                // @ts-expect-error
459
                // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
460
                this.historyOrderDataCount = response.aggregations.order_count_bucket;
461
            }
462
        },
463
464
        async onTurnoverRangeUpdate(range: string): Promise<void> {
465
            const turnoverDateRange = this.rangesValueMap.find((item: HistoryDateRange) => item.label === range);
466
467
            if (!turnoverDateRange) {
468
                throw Error('Range not found');
469
            }
470
471
            this.turnoverDateRange = turnoverDateRange;
472
473
            const response = await this.fetchHistoryOrderDataSum();
474
475
            if (response.aggregations) {
476
                // @ts-expect-error
477
                // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
478
                this.historyOrderDataSum = response.aggregations.order_sum_bucket;
479
            }
480
        },
481
482
        getCardSubtitle(range: HistoryDateRange): string {
483
            return `${this.formatChartHeadlineDate(this.getDateAgo(range))} - ${this.formatChartHeadlineDate(this.today)}`;
484
        },
485
486
        getDateAgo(range: HistoryDateRange): Date {
487
            const date = Shopware.Utils.format.dateWithUserTimezone();
488
489
            if (range.interval === 'hour') {
490
                date.setHours(date.getHours() - range.range);
491
492
                return date;
493
            }
494
495
            date.setDate(date.getDate() - range.range);
496
            date.setHours(0, 0, 0, 0);
497
498
            return date;
499
        },
500
    },
501
});
502
503
/**
504
 * @private
505
 */
506
export type { HistoryDateRange };
507