@vendure/dashboard 3.4.2-master-202509030226 → 3.4.2-master-202509040226
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/package.json +4 -4
- package/src/app/routes/_authenticated/_facets/facets.graphql.ts +0 -1
- package/src/app/routes/_authenticated/_facets/facets.tsx +55 -35
- package/src/app/routes/_authenticated/_orders/orders.tsx +12 -32
- package/src/app/routes/_authenticated/_products/components/create-product-variants-dialog.tsx +4 -8
- package/src/app/routes/_authenticated/_products/components/create-product-variants.tsx +1 -0
- package/src/lib/components/data-table/filters/data-table-datetime-filter.tsx +33 -3
- package/src/lib/components/shared/asset/asset-gallery.tsx +4 -1
- package/src/lib/components/shared/table-cell/order-table-cell-components.tsx +38 -0
- package/src/lib/components/shared/table-cell/table-cell-types.ts +33 -0
- package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +11 -7
- package/src/lib/framework/dashboard-widget/metrics-widget/chart.tsx +21 -5
- package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +18 -9
- package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +22 -20
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.4.2-master-
|
|
4
|
+
"version": "3.4.2-master-202509040226",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
"@types/react": "^19.0.10",
|
|
101
101
|
"@types/react-dom": "^19.0.4",
|
|
102
102
|
"@uidotdev/usehooks": "^2.4.1",
|
|
103
|
-
"@vendure/common": "^3.4.2-master-
|
|
104
|
-
"@vendure/core": "^3.4.2-master-
|
|
103
|
+
"@vendure/common": "^3.4.2-master-202509040226",
|
|
104
|
+
"@vendure/core": "^3.4.2-master-202509040226",
|
|
105
105
|
"@vitejs/plugin-react": "^4.3.4",
|
|
106
106
|
"acorn": "^8.11.3",
|
|
107
107
|
"acorn-walk": "^8.3.2",
|
|
@@ -152,5 +152,5 @@
|
|
|
152
152
|
"lightningcss-linux-arm64-musl": "^1.29.3",
|
|
153
153
|
"lightningcss-linux-x64-musl": "^1.29.1"
|
|
154
154
|
},
|
|
155
|
-
"gitHead": "
|
|
155
|
+
"gitHead": "1c3baa9187d1e7b08aa6f1b798aad54770bbed53"
|
|
156
156
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
|
|
2
2
|
import { FacetValueChip } from '@/vdb/components/shared/facet-value-chip.js';
|
|
3
3
|
import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
|
|
4
|
+
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
4
5
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
5
6
|
import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
|
|
6
7
|
import { ListPage } from '@/vdb/framework/page/list-page.js';
|
|
@@ -16,12 +17,49 @@ import {
|
|
|
16
17
|
} from './components/facet-bulk-actions.js';
|
|
17
18
|
import { FacetValuesSheet } from './components/facet-values-sheet.js';
|
|
18
19
|
import { deleteFacetDocument, facetListDocument } from './facets.graphql.js';
|
|
20
|
+
import { DataTableCellComponent } from '@/vdb/components/shared/table-cell/table-cell-types.js';
|
|
19
21
|
|
|
20
22
|
export const Route = createFileRoute('/_authenticated/_facets/facets')({
|
|
21
23
|
component: FacetListPage,
|
|
22
24
|
loader: () => ({ breadcrumb: () => <Trans>Facets</Trans> }),
|
|
23
25
|
});
|
|
24
26
|
|
|
27
|
+
const FacetValuesCell: DataTableCellComponent<ResultOf<
|
|
28
|
+
typeof facetListDocument
|
|
29
|
+
>['facets']['items'][0]> = ({ row }) => {
|
|
30
|
+
const value = row.original.valueList;
|
|
31
|
+
if (!value) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const list = value;
|
|
35
|
+
return (
|
|
36
|
+
<div className="flex flex-wrap gap-2 items-center">
|
|
37
|
+
{list.items.map(item => {
|
|
38
|
+
return (
|
|
39
|
+
<FacetValueChip
|
|
40
|
+
key={item.id}
|
|
41
|
+
facetValue={item}
|
|
42
|
+
removable={false}
|
|
43
|
+
displayFacetName={false}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
<FacetValuesSheet
|
|
48
|
+
facetId={row.original.id}
|
|
49
|
+
facetName={row.original.name}
|
|
50
|
+
>
|
|
51
|
+
{list.totalItems > 3 ? (
|
|
52
|
+
<div>
|
|
53
|
+
<Trans>+ {list.totalItems - 3} more</Trans>
|
|
54
|
+
</div>
|
|
55
|
+
) : (
|
|
56
|
+
<Trans>View values</Trans>
|
|
57
|
+
)}
|
|
58
|
+
</FacetValuesSheet>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
25
63
|
function FacetListPage() {
|
|
26
64
|
return (
|
|
27
65
|
<ListPage
|
|
@@ -29,49 +67,31 @@ function FacetListPage() {
|
|
|
29
67
|
title="Facets"
|
|
30
68
|
listQuery={facetListDocument}
|
|
31
69
|
deleteMutation={deleteFacetDocument}
|
|
70
|
+
defaultVisibility={{
|
|
71
|
+
name: true,
|
|
72
|
+
isPrivate: true,
|
|
73
|
+
valueList: true,
|
|
74
|
+
}}
|
|
32
75
|
customizeColumns={{
|
|
33
76
|
name: {
|
|
34
|
-
header:
|
|
77
|
+
header: () => <Trans>Facet Name</Trans>,
|
|
35
78
|
cell: ({ row }) => <DetailPageButton id={row.original.id} label={row.original.name} />,
|
|
36
79
|
},
|
|
37
|
-
|
|
38
|
-
header: () => <Trans>
|
|
39
|
-
cell: ({
|
|
40
|
-
const
|
|
41
|
-
if (!value) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
const list = value as any as ResultOf<
|
|
45
|
-
typeof facetListDocument
|
|
46
|
-
>['facets']['items'][0]['valueList'];
|
|
80
|
+
isPrivate: {
|
|
81
|
+
header: () => <Trans>Visibility</Trans>,
|
|
82
|
+
cell: ({ row }) => {
|
|
83
|
+
const isPrivate = row.original.isPrivate;
|
|
47
84
|
return (
|
|
48
|
-
<
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
<FacetValueChip
|
|
52
|
-
key={item.id}
|
|
53
|
-
facetValue={item}
|
|
54
|
-
removable={false}
|
|
55
|
-
displayFacetName={false}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
})}
|
|
59
|
-
<FacetValuesSheet
|
|
60
|
-
facetId={cell.row.original.id}
|
|
61
|
-
facetName={cell.row.original.name}
|
|
62
|
-
>
|
|
63
|
-
{list.totalItems > 3 ? (
|
|
64
|
-
<div>
|
|
65
|
-
<Trans>+ {list.totalItems - 3} more</Trans>
|
|
66
|
-
</div>
|
|
67
|
-
) : (
|
|
68
|
-
<Trans>View values</Trans>
|
|
69
|
-
)}
|
|
70
|
-
</FacetValuesSheet>
|
|
71
|
-
</div>
|
|
85
|
+
<Badge variant={isPrivate ? 'destructive' : 'success'}>
|
|
86
|
+
{isPrivate ? <Trans>private</Trans> : <Trans>public</Trans>}
|
|
87
|
+
</Badge>
|
|
72
88
|
);
|
|
73
89
|
},
|
|
74
90
|
},
|
|
91
|
+
valueList: {
|
|
92
|
+
header: () => <Trans>Values</Trans>,
|
|
93
|
+
cell: FacetValuesCell,
|
|
94
|
+
},
|
|
75
95
|
}}
|
|
76
96
|
onSearchTermChange={searchTerm => {
|
|
77
97
|
return {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { Money } from '@/vdb/components/data-display/money.js';
|
|
2
1
|
import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
|
|
3
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CustomerCell,
|
|
4
|
+
OrderMoneyCell,
|
|
5
|
+
OrderStateCell,
|
|
6
|
+
} from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
|
|
4
7
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
5
8
|
import { PageActionBarRight } from '@/vdb/framework/layout-engine/page-layout.js';
|
|
6
9
|
import { ListPage } from '@/vdb/framework/page/list-page.js';
|
|
@@ -9,7 +12,7 @@ import { ResultOf } from '@/vdb/graphql/graphql.js';
|
|
|
9
12
|
import { useServerConfig } from '@/vdb/hooks/use-server-config.js';
|
|
10
13
|
import { Trans } from '@/vdb/lib/trans.js';
|
|
11
14
|
import { useMutation } from '@tanstack/react-query';
|
|
12
|
-
import { createFileRoute,
|
|
15
|
+
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
13
16
|
import { PlusIcon } from 'lucide-react';
|
|
14
17
|
import { createDraftOrderDocument, orderListDocument } from './orders.graphql.js';
|
|
15
18
|
|
|
@@ -58,26 +61,15 @@ function OrderListPage() {
|
|
|
58
61
|
customizeColumns={{
|
|
59
62
|
total: {
|
|
60
63
|
header: 'Total',
|
|
61
|
-
cell:
|
|
62
|
-
const value = cell.getValue();
|
|
63
|
-
const currencyCode = row.original.currencyCode;
|
|
64
|
-
return <Money value={value} currency={currencyCode} />;
|
|
65
|
-
},
|
|
64
|
+
cell: OrderMoneyCell,
|
|
66
65
|
},
|
|
67
66
|
totalWithTax: {
|
|
68
67
|
header: 'Total with Tax',
|
|
69
|
-
cell:
|
|
70
|
-
const value = cell.getValue();
|
|
71
|
-
const currencyCode = row.original.currencyCode;
|
|
72
|
-
return <Money value={value} currency={currencyCode} />;
|
|
73
|
-
},
|
|
68
|
+
cell: OrderMoneyCell,
|
|
74
69
|
},
|
|
75
70
|
state: {
|
|
76
71
|
header: 'State',
|
|
77
|
-
cell:
|
|
78
|
-
const value = cell.getValue() as string;
|
|
79
|
-
return <Badge variant="outline">{value}</Badge>;
|
|
80
|
-
},
|
|
72
|
+
cell: OrderStateCell,
|
|
81
73
|
},
|
|
82
74
|
code: {
|
|
83
75
|
header: 'Code',
|
|
@@ -89,24 +81,12 @@ function OrderListPage() {
|
|
|
89
81
|
},
|
|
90
82
|
customer: {
|
|
91
83
|
header: 'Customer',
|
|
92
|
-
cell:
|
|
93
|
-
const value = cell.getValue();
|
|
94
|
-
if (!value) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
return (
|
|
98
|
-
<Button asChild variant="ghost">
|
|
99
|
-
<Link to={`/customers/${value.id}`}>
|
|
100
|
-
{value.firstName} {value.lastName}
|
|
101
|
-
</Link>
|
|
102
|
-
</Button>
|
|
103
|
-
);
|
|
104
|
-
},
|
|
84
|
+
cell: CustomerCell,
|
|
105
85
|
},
|
|
106
86
|
shippingLines: {
|
|
107
87
|
header: 'Shipping',
|
|
108
|
-
cell: ({
|
|
109
|
-
const value =
|
|
88
|
+
cell: ({ row }) => {
|
|
89
|
+
const value = row.original.shippingLines;
|
|
110
90
|
return <div>{value.map(line => line.shippingMethod.name).join(', ')}</div>;
|
|
111
91
|
},
|
|
112
92
|
},
|
package/src/app/routes/_authenticated/_products/components/create-product-variants-dialog.tsx
CHANGED
|
@@ -135,6 +135,7 @@ export function CreateProductVariantsDialog({
|
|
|
135
135
|
({ data }: { data: VariantConfiguration }) => setVariantData(data),
|
|
136
136
|
[],
|
|
137
137
|
);
|
|
138
|
+
const createCount = Object.values(variantData?.variants ?? {}).filter(v => v.enabled).length;
|
|
138
139
|
|
|
139
140
|
return (
|
|
140
141
|
<>
|
|
@@ -168,7 +169,8 @@ export function CreateProductVariantsDialog({
|
|
|
168
169
|
!variantData ||
|
|
169
170
|
createOptionGroupMutation.isPending ||
|
|
170
171
|
addOptionGroupToProductMutation.isPending ||
|
|
171
|
-
createProductVariantsMutation.isPending
|
|
172
|
+
createProductVariantsMutation.isPending ||
|
|
173
|
+
createCount === 0
|
|
172
174
|
}
|
|
173
175
|
>
|
|
174
176
|
{createOptionGroupMutation.isPending ||
|
|
@@ -176,13 +178,7 @@ export function CreateProductVariantsDialog({
|
|
|
176
178
|
createProductVariantsMutation.isPending ? (
|
|
177
179
|
<Trans>Creating...</Trans>
|
|
178
180
|
) : (
|
|
179
|
-
<Trans>
|
|
180
|
-
Create{' '}
|
|
181
|
-
{variantData
|
|
182
|
-
? Object.values(variantData.variants).filter(v => v.enabled).length
|
|
183
|
-
: 0}{' '}
|
|
184
|
-
variants
|
|
185
|
-
</Trans>
|
|
181
|
+
<Trans>Create {createCount} variants</Trans>
|
|
186
182
|
)}
|
|
187
183
|
</Button>
|
|
188
184
|
</DialogFooter>
|
|
@@ -53,6 +53,24 @@ export function DataTableDateTimeFilter({
|
|
|
53
53
|
}
|
|
54
54
|
}, [operator, value, startDate, endDate]);
|
|
55
55
|
|
|
56
|
+
const parseToDate = (input: unknown): Date | undefined => {
|
|
57
|
+
if (input instanceof Date) {
|
|
58
|
+
return input;
|
|
59
|
+
}
|
|
60
|
+
if (typeof input === 'string' && input !== '') {
|
|
61
|
+
return new Date(input);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const dashboardComponentProps = {
|
|
67
|
+
name: 'date',
|
|
68
|
+
onBlur: () => {
|
|
69
|
+
/* */
|
|
70
|
+
},
|
|
71
|
+
ref: () => null,
|
|
72
|
+
};
|
|
73
|
+
|
|
56
74
|
return (
|
|
57
75
|
<div className="flex flex-col gap-2">
|
|
58
76
|
<div className="flex flex-col md:flex-row gap-2">
|
|
@@ -71,11 +89,23 @@ export function DataTableDateTimeFilter({
|
|
|
71
89
|
{operator !== 'isNull' &&
|
|
72
90
|
(operator === 'between' ? (
|
|
73
91
|
<div className="space-y-2">
|
|
74
|
-
<DateTimeInput
|
|
75
|
-
|
|
92
|
+
<DateTimeInput
|
|
93
|
+
{...dashboardComponentProps}
|
|
94
|
+
value={startDate}
|
|
95
|
+
onChange={val => setStartDate(parseToDate(val))}
|
|
96
|
+
/>
|
|
97
|
+
<DateTimeInput
|
|
98
|
+
{...dashboardComponentProps}
|
|
99
|
+
value={endDate}
|
|
100
|
+
onChange={val => setEndDate(parseToDate(val))}
|
|
101
|
+
/>
|
|
76
102
|
</div>
|
|
77
103
|
) : (
|
|
78
|
-
<DateTimeInput
|
|
104
|
+
<DateTimeInput
|
|
105
|
+
{...dashboardComponentProps}
|
|
106
|
+
value={value}
|
|
107
|
+
onChange={val => setValue(parseToDate(val))}
|
|
108
|
+
/>
|
|
79
109
|
))}
|
|
80
110
|
</div>
|
|
81
111
|
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
@@ -360,7 +360,10 @@ export function AssetGallery({
|
|
|
360
360
|
{formatFileSize(asset.fileSize)}
|
|
361
361
|
</p>
|
|
362
362
|
)}
|
|
363
|
-
<DetailPageButton
|
|
363
|
+
<DetailPageButton
|
|
364
|
+
href={`/assets/${asset.id}`}
|
|
365
|
+
label={<Trans>Edit</Trans>}
|
|
366
|
+
/>
|
|
364
367
|
</div>
|
|
365
368
|
</CardContent>
|
|
366
369
|
</Card>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Money } from '@/vdb/components/data-display/money.js';
|
|
2
|
+
import { DataTableCellComponent } from '@/vdb/components/shared/table-cell/table-cell-types.js';
|
|
3
|
+
import { Badge } from '@/vdb/components/ui/badge.js';
|
|
4
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
5
|
+
import { Link } from '@tanstack/react-router';
|
|
6
|
+
|
|
7
|
+
type CustomerCellData = {
|
|
8
|
+
customer: {
|
|
9
|
+
id: string;
|
|
10
|
+
firstName: string;
|
|
11
|
+
lastName: string;
|
|
12
|
+
} | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const CustomerCell: DataTableCellComponent<CustomerCellData> = ({ row }) => {
|
|
16
|
+
const value = row.original.customer;
|
|
17
|
+
if (!value) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return (
|
|
21
|
+
<Button asChild variant="ghost">
|
|
22
|
+
<Link to={`/customers/${value.id}`}>
|
|
23
|
+
{value.firstName} {value.lastName}
|
|
24
|
+
</Link>
|
|
25
|
+
</Button>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const OrderStateCell: DataTableCellComponent<{ state: string }> = ({ row }) => {
|
|
30
|
+
const value = row.original.state;
|
|
31
|
+
return <Badge variant="outline">{value}</Badge>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const OrderMoneyCell: DataTableCellComponent<{ currencyCode: string }> = ({ cell, row }) => {
|
|
35
|
+
const value = cell.getValue();
|
|
36
|
+
const currencyCode = row.original.currencyCode;
|
|
37
|
+
return <Money value={value} currency={currencyCode} />;
|
|
38
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CellContext } from '@tanstack/table-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @description
|
|
5
|
+
* This type is used to define re-usable components that can render a table cell in a
|
|
6
|
+
* DataTable.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* type CustomerCellData = {
|
|
11
|
+
* customer: {
|
|
12
|
+
* id: string;
|
|
13
|
+
* firstName: string;
|
|
14
|
+
* lastName: string;
|
|
15
|
+
* } | null;
|
|
16
|
+
* };
|
|
17
|
+
*
|
|
18
|
+
* export const CustomerCell: DataTableCellComponent<CustomerCellData> = ({ row }) => {
|
|
19
|
+
* const value = row.original.customer;
|
|
20
|
+
* if (!value) {
|
|
21
|
+
* return null;
|
|
22
|
+
* }
|
|
23
|
+
* return (
|
|
24
|
+
* <Button asChild variant="ghost">
|
|
25
|
+
* <Link to={`/customers/${value.id}`}>
|
|
26
|
+
* {value.firstName} {value.lastName}
|
|
27
|
+
* </Link>
|
|
28
|
+
* </Button>
|
|
29
|
+
* );
|
|
30
|
+
* };
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export type DataTableCellComponent<T> = <Data extends T>(context: CellContext<Data, any>) => any;
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { PaginatedListDataTable } from '@/vdb/components/shared/paginated-list-data-table.js';
|
|
2
|
+
import {
|
|
3
|
+
CustomerCell,
|
|
4
|
+
OrderMoneyCell,
|
|
5
|
+
OrderStateCell,
|
|
6
|
+
} from '@/vdb/components/shared/table-cell/order-table-cell-components.js';
|
|
2
7
|
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
8
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
4
9
|
import { Link } from '@tanstack/react-router';
|
|
@@ -25,7 +30,6 @@ export function LatestOrdersWidget() {
|
|
|
25
30
|
return (
|
|
26
31
|
<DashboardBaseWidget id={WIDGET_ID} title="Latest Orders" description="Your latest orders">
|
|
27
32
|
<PaginatedListDataTable
|
|
28
|
-
disableViewOptions
|
|
29
33
|
page={page}
|
|
30
34
|
transformVariables={variables => ({
|
|
31
35
|
...variables,
|
|
@@ -38,6 +42,7 @@ export function LatestOrdersWidget() {
|
|
|
38
42
|
state: {
|
|
39
43
|
notIn: ['Cancelled', 'Draft'],
|
|
40
44
|
},
|
|
45
|
+
...(variables.options?.filter ?? {}),
|
|
41
46
|
},
|
|
42
47
|
},
|
|
43
48
|
})}
|
|
@@ -56,7 +61,7 @@ export function LatestOrdersWidget() {
|
|
|
56
61
|
header: 'Placed At',
|
|
57
62
|
cell: ({ row }) => {
|
|
58
63
|
return (
|
|
59
|
-
<span>
|
|
64
|
+
<span className="capitalize">
|
|
60
65
|
{formatRelative(row.original.orderPlacedAt ?? new Date(), new Date())}
|
|
61
66
|
</span>
|
|
62
67
|
);
|
|
@@ -64,12 +69,11 @@ export function LatestOrdersWidget() {
|
|
|
64
69
|
},
|
|
65
70
|
total: {
|
|
66
71
|
header: 'Total',
|
|
67
|
-
cell:
|
|
68
|
-
return (
|
|
69
|
-
<span>{formatCurrency(row.original.total, row.original.currencyCode)}</span>
|
|
70
|
-
);
|
|
71
|
-
},
|
|
72
|
+
cell: OrderMoneyCell,
|
|
72
73
|
},
|
|
74
|
+
totalWithTax: { cell: OrderMoneyCell },
|
|
75
|
+
state: { cell: OrderStateCell },
|
|
76
|
+
customer: { cell: CustomerCell },
|
|
73
77
|
}}
|
|
74
78
|
itemsPerPage={pageSize}
|
|
75
79
|
sorting={sorting}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { Area, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
|
2
|
-
|
|
3
|
-
import { AreaChart } from 'recharts';
|
|
1
|
+
import { Area, AreaChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
|
4
2
|
import { useWidgetDimensions } from '../base-widget.js';
|
|
5
3
|
|
|
6
4
|
export function MetricsChart({
|
|
@@ -14,11 +12,29 @@ export function MetricsChart({
|
|
|
14
12
|
|
|
15
13
|
return (
|
|
16
14
|
<AreaChart width={width} height={height} data={chartData}>
|
|
15
|
+
<defs>
|
|
16
|
+
<linearGradient id="gradientFill" x1="0" y1="0" x2="0" y2="1">
|
|
17
|
+
<stop offset="5%" stopColor="var(--color-brand)" stopOpacity={0.9} />
|
|
18
|
+
<stop offset="100%" stopColor="var(--color-brand)" stopOpacity={0.05} />
|
|
19
|
+
</linearGradient>
|
|
20
|
+
</defs>
|
|
17
21
|
<CartesianGrid strokeDasharray="4 4" stroke="var(--color-border)" />
|
|
18
22
|
<XAxis className="text-xs" color="var(--color-foreground)" dataKey="name" interval={2} />
|
|
19
23
|
<YAxis className="text-xs" color="var(--color-foreground)" tickFormatter={formatValue} />
|
|
20
|
-
<Tooltip
|
|
21
|
-
|
|
24
|
+
<Tooltip
|
|
25
|
+
formatter={formatValue}
|
|
26
|
+
contentStyle={{ borderRadius: 4, padding: 4, paddingLeft: 8, paddingRight: 8 }}
|
|
27
|
+
labelStyle={{ fontSize: 12 }}
|
|
28
|
+
itemStyle={{ fontSize: 14 }}
|
|
29
|
+
/>
|
|
30
|
+
<Area
|
|
31
|
+
type="monotone"
|
|
32
|
+
dataKey="sales"
|
|
33
|
+
stroke="var(--color-brand)"
|
|
34
|
+
strokeWidth={2}
|
|
35
|
+
strokeOpacity={0.8}
|
|
36
|
+
fill={'url(#gradientFill)'}
|
|
37
|
+
/>
|
|
22
38
|
</AreaChart>
|
|
23
39
|
);
|
|
24
40
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
1
2
|
import { Tabs, TabsList, TabsTrigger } from '@/vdb/components/ui/tabs.js';
|
|
2
3
|
import { api } from '@/vdb/graphql/api.js';
|
|
3
4
|
import { useChannel } from '@/vdb/hooks/use-channel.js';
|
|
4
5
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
5
6
|
import { useQuery } from '@tanstack/react-query';
|
|
7
|
+
import { RefreshCw } from 'lucide-react';
|
|
6
8
|
import { useMemo, useState } from 'react';
|
|
7
9
|
import { DashboardBaseWidget } from '../base-widget.js';
|
|
8
10
|
import { MetricsChart } from './chart.js';
|
|
@@ -19,7 +21,7 @@ export function MetricsWidget() {
|
|
|
19
21
|
const { activeChannel } = useChannel();
|
|
20
22
|
const [dataType, setDataType] = useState<DATA_TYPES>(DATA_TYPES.OrderTotal);
|
|
21
23
|
|
|
22
|
-
const { data } = useQuery({
|
|
24
|
+
const { data, isRefetching, refetch } = useQuery({
|
|
23
25
|
queryKey: ['dashboard-order-metrics', dataType],
|
|
24
26
|
queryFn: () => {
|
|
25
27
|
return api.query(orderChartDataQuery, {
|
|
@@ -53,15 +55,22 @@ export function MetricsWidget() {
|
|
|
53
55
|
<DashboardBaseWidget
|
|
54
56
|
id="metrics-widget"
|
|
55
57
|
title="Metrics"
|
|
56
|
-
description="
|
|
58
|
+
description="Order metrics"
|
|
57
59
|
actions={
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
<div className="flex gap-1">
|
|
61
|
+
<Tabs defaultValue={dataType} onValueChange={value => setDataType(value as DATA_TYPES)}>
|
|
62
|
+
<TabsList>
|
|
63
|
+
<TabsTrigger value={DATA_TYPES.OrderCount}>Order Count</TabsTrigger>
|
|
64
|
+
<TabsTrigger value={DATA_TYPES.OrderTotal}>Order Total</TabsTrigger>
|
|
65
|
+
<TabsTrigger value={DATA_TYPES.AverageOrderValue}>
|
|
66
|
+
Average Order Value
|
|
67
|
+
</TabsTrigger>
|
|
68
|
+
</TabsList>
|
|
69
|
+
</Tabs>
|
|
70
|
+
<Button variant={'ghost'} onClick={() => refetch()}>
|
|
71
|
+
<RefreshCw className={isRefetching ? 'animate-rotate' : ''} />
|
|
72
|
+
</Button>
|
|
73
|
+
</div>
|
|
65
74
|
}
|
|
66
75
|
>
|
|
67
76
|
{chartData && (
|
|
@@ -137,26 +137,28 @@ export function OrdersSummaryWidget() {
|
|
|
137
137
|
</Tabs>
|
|
138
138
|
}
|
|
139
139
|
>
|
|
140
|
-
<div className="
|
|
141
|
-
<div className="flex flex-col
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
140
|
+
<div className="@container h-full">
|
|
141
|
+
<div className="flex flex-col h-full @md:flex-row gap-8 items-center justify-center @md:justify-evenly text-center tabular-nums">
|
|
142
|
+
<div className="flex flex-col lg:gap-2">
|
|
143
|
+
<p className="lg:text-lg text-muted-foreground">Total Orders</p>
|
|
144
|
+
<p className="text-xl @md:text-3xl font-semibold">
|
|
145
|
+
<AnimatedNumber
|
|
146
|
+
animationConfig={{ mass: 0.01, stiffness: 90, damping: 3 }}
|
|
147
|
+
value={currentTotalOrders}
|
|
148
|
+
/>
|
|
149
|
+
</p>
|
|
150
|
+
<PercentageChange value={orderChange} />
|
|
151
|
+
</div>
|
|
152
|
+
<div className="flex flex-col lg:gap-2">
|
|
153
|
+
<p className="lg:text-lg text-muted-foreground">Total Revenue</p>
|
|
154
|
+
<p className="text-xl @md:text-3xl font-semibold">
|
|
155
|
+
<AnimatedCurrency
|
|
156
|
+
animationConfig={{ mass: 0.01, stiffness: 90, damping: 3 }}
|
|
157
|
+
value={currentRevenue}
|
|
158
|
+
/>
|
|
159
|
+
</p>
|
|
160
|
+
<PercentageChange value={revenueChange} />
|
|
161
|
+
</div>
|
|
160
162
|
</div>
|
|
161
163
|
</div>
|
|
162
164
|
</DashboardBaseWidget>
|