@vendure/dashboard 3.3.6-master-202507090236 → 3.3.6-master-202507110238
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/dist/plugin/tests/barrel-exports.spec.js +1 -1
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +30 -37
- package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +33 -53
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +14 -7
- package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +23 -12
- package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +364 -0
- package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +222 -0
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +146 -85
- package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +268 -31
- package/src/app/routes/_authenticated/_orders/components/settle-refund-dialog.tsx +80 -0
- package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +102 -0
- package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +144 -0
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +118 -2
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +144 -52
- package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +550 -0
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +0 -17
- package/src/app/routes/_authenticated/_orders/utils/order-types.ts +5 -2
- package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +4 -3
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +0 -1
- package/src/lib/components/data-display/date-time.tsx +7 -1
- package/src/lib/components/data-input/relation-input.tsx +11 -0
- package/src/lib/components/data-input/relation-selector.tsx +9 -2
- package/src/lib/components/data-table/data-table-utils.ts +34 -0
- package/src/lib/components/data-table/data-table-view-options.tsx +2 -2
- package/src/lib/components/data-table/data-table.tsx +5 -2
- package/src/lib/components/data-table/use-generated-columns.tsx +307 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +15 -286
- package/src/lib/components/shared/product-variant-selector.tsx +28 -4
- package/src/lib/framework/component-registry/dynamic-component.tsx +3 -3
- package/src/lib/framework/document-introspection/get-document-structure.spec.ts +321 -2
- package/src/lib/framework/document-introspection/get-document-structure.ts +149 -31
- package/src/lib/framework/extension-api/types/layout.ts +21 -6
- package/src/lib/framework/layout-engine/layout-extensions.ts +1 -4
- package/src/lib/framework/layout-engine/page-layout.tsx +61 -10
- package/src/lib/framework/page/use-detail-page.ts +10 -7
- package/vite/tests/barrel-exports.spec.ts +1 -1
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import { Money } from '@/vdb/components/data-display/money.js';
|
|
2
|
+
import { getColumnVisibility } from '@/vdb/components/data-table/data-table-utils.js';
|
|
3
|
+
import { DataTable } from '@/vdb/components/data-table/data-table.js';
|
|
4
|
+
import { useGeneratedColumns } from '@/vdb/components/data-table/use-generated-columns.js';
|
|
5
|
+
import { DetailPageButton } from '@/vdb/components/shared/detail-page-button.js';
|
|
1
6
|
import { VendureImage } from '@/vdb/components/shared/vendure-image.js';
|
|
2
|
-
import {
|
|
7
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
8
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/vdb/components/ui/dropdown-menu.js';
|
|
9
|
+
import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
|
|
10
|
+
import { getFieldsFromDocumentNode } from '@/vdb/framework/document-introspection/get-document-structure.js';
|
|
3
11
|
import { ResultOf } from '@/vdb/graphql/graphql.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
VisibilityState,
|
|
10
|
-
} from '@tanstack/react-table';
|
|
11
|
-
import { useState } from 'react';
|
|
12
|
+
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
|
|
13
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
14
|
+
import { JsonEditor } from 'json-edit-react';
|
|
15
|
+
import { EllipsisVertical } from 'lucide-react';
|
|
16
|
+
import { useMemo } from 'react';
|
|
12
17
|
import { orderDetailDocument, orderLineFragment } from '../orders.graphql.js';
|
|
13
18
|
import { MoneyGrossNet } from './money-gross-net.js';
|
|
14
19
|
import { OrderTableTotals } from './order-table-totals.js';
|
|
@@ -18,111 +23,167 @@ type OrderLineFragment = ResultOf<typeof orderLineFragment>;
|
|
|
18
23
|
|
|
19
24
|
export interface OrderTableProps {
|
|
20
25
|
order: OrderFragment;
|
|
26
|
+
pageId: string;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const columns: ColumnDef<OrderLineFragment>[] = [
|
|
29
|
-
{
|
|
30
|
-
header: 'Image',
|
|
29
|
+
// Factory function to create customizeColumns with inline components
|
|
30
|
+
function createCustomizeColumns(currencyCode: string) {
|
|
31
|
+
return {
|
|
32
|
+
featuredAsset: {
|
|
33
|
+
header: () => <Trans>Image</Trans>,
|
|
31
34
|
accessorKey: 'featuredAsset',
|
|
32
|
-
cell: ({ row }) => {
|
|
35
|
+
cell: ({ row }: { row: any }) => {
|
|
33
36
|
const asset = row.original.featuredAsset;
|
|
34
37
|
return <VendureImage asset={asset} preset="tiny" />;
|
|
35
38
|
},
|
|
36
39
|
},
|
|
37
|
-
{
|
|
38
|
-
header:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
productVariant: {
|
|
41
|
+
header: () => <Trans>Product</Trans>,
|
|
42
|
+
cell: ({ row }: { row: any }) => {
|
|
43
|
+
const productVariant = row.original.productVariant;
|
|
44
|
+
return (
|
|
45
|
+
<DetailPageButton
|
|
46
|
+
id={productVariant.id}
|
|
47
|
+
label={productVariant.name}
|
|
48
|
+
href={`/product-variants/${productVariant.id}`}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
},
|
|
44
52
|
},
|
|
45
|
-
{
|
|
46
|
-
header:
|
|
53
|
+
unitPriceWithTax: {
|
|
54
|
+
header: () => <Trans>Unit price</Trans>,
|
|
47
55
|
accessorKey: 'unitPriceWithTax',
|
|
48
|
-
cell: ({
|
|
56
|
+
cell: ({ row }: { row: any }) => {
|
|
49
57
|
const value = row.original.unitPriceWithTax;
|
|
50
58
|
const netValue = row.original.unitPrice;
|
|
51
59
|
return <MoneyGrossNet priceWithTax={value} price={netValue} currencyCode={currencyCode} />;
|
|
52
60
|
},
|
|
53
61
|
},
|
|
54
|
-
{
|
|
55
|
-
|
|
62
|
+
fulfillmentLines: {
|
|
63
|
+
cell: ({ row }: { row: any }) => (
|
|
64
|
+
<DropdownMenu>
|
|
65
|
+
<DropdownMenuTrigger asChild>
|
|
66
|
+
<Button variant="ghost" size="icon">
|
|
67
|
+
<EllipsisVertical />
|
|
68
|
+
</Button>
|
|
69
|
+
</DropdownMenuTrigger>
|
|
70
|
+
<DropdownMenuContent>
|
|
71
|
+
<JsonEditor data={row.original.fulfillmentLines} viewOnly rootFontSize={12} />
|
|
72
|
+
</DropdownMenuContent>
|
|
73
|
+
</DropdownMenu>
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
quantity: {
|
|
77
|
+
header: () => <Trans>Quantity</Trans>,
|
|
56
78
|
accessorKey: 'quantity',
|
|
57
79
|
},
|
|
58
|
-
{
|
|
59
|
-
|
|
80
|
+
unitPrice: {
|
|
81
|
+
cell: ({ row }: { row: any }) => (
|
|
82
|
+
<Money currencyCode={currencyCode} value={row.original.unitPrice} />
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
proratedUnitPrice: {
|
|
86
|
+
cell: ({ row }: { row: any }) => (
|
|
87
|
+
<Money currencyCode={currencyCode} value={row.original.proratedUnitPrice} />
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
proratedUnitPriceWithTax: {
|
|
91
|
+
cell: ({ row }: { row: any }) => (
|
|
92
|
+
<Money currencyCode={currencyCode} value={row.original.proratedUnitPriceWithTax} />
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
linePrice: {
|
|
96
|
+
cell: ({ row }: { row: any }) => (
|
|
97
|
+
<Money currencyCode={currencyCode} value={row.original.linePrice} />
|
|
98
|
+
),
|
|
99
|
+
},
|
|
100
|
+
discountedLinePrice: {
|
|
101
|
+
cell: ({ row }: { row: any }) => (
|
|
102
|
+
<Money currencyCode={currencyCode} value={row.original.discountedLinePrice} />
|
|
103
|
+
),
|
|
104
|
+
},
|
|
105
|
+
lineTax: {
|
|
106
|
+
cell: ({ row }: { row: any }) => (
|
|
107
|
+
<Money currencyCode={currencyCode} value={row.original.lineTax} />
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
linePriceWithTax: {
|
|
111
|
+
header: () => <Trans>Total</Trans>,
|
|
60
112
|
accessorKey: 'linePriceWithTax',
|
|
61
|
-
cell: ({
|
|
113
|
+
cell: ({ row }: { row: any }) => {
|
|
62
114
|
const value = row.original.linePriceWithTax;
|
|
63
115
|
const netValue = row.original.linePrice;
|
|
64
116
|
return <MoneyGrossNet priceWithTax={value} price={netValue} currencyCode={currencyCode} />;
|
|
65
117
|
},
|
|
66
118
|
},
|
|
119
|
+
discountedLinePriceWithTax: {
|
|
120
|
+
cell: ({ row }: { row: any }) => (
|
|
121
|
+
<Money currencyCode={currencyCode} value={row.original.discountedLinePriceWithTax} />
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function OrderTable({ order, pageId }: Readonly<OrderTableProps>) {
|
|
128
|
+
const { setTableSettings, settings } = useUserSettings();
|
|
129
|
+
const tableSettings = pageId ? settings.tableSettings?.[pageId] : undefined;
|
|
130
|
+
|
|
131
|
+
const defaultColumnVisibility = tableSettings?.columnVisibility ?? {
|
|
132
|
+
featuredAsset: true,
|
|
133
|
+
productVariant: true,
|
|
134
|
+
unitPriceWithTax: true,
|
|
135
|
+
quantity: true,
|
|
136
|
+
linePriceWithTax: true,
|
|
137
|
+
};
|
|
138
|
+
const columnOrder = tableSettings?.columnOrder ?? [
|
|
139
|
+
'featuredAsset',
|
|
140
|
+
'productVariant',
|
|
141
|
+
'unitPriceWithTax',
|
|
142
|
+
'quantity',
|
|
143
|
+
'linePriceWithTax',
|
|
67
144
|
];
|
|
145
|
+
const currencyCode = order.currencyCode;
|
|
68
146
|
|
|
69
|
-
const
|
|
147
|
+
const fields = getFieldsFromDocumentNode(addCustomFields(orderDetailDocument), ['order', 'lines']);
|
|
70
148
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
},
|
|
149
|
+
const customizeColumns = useMemo(() => createCustomizeColumns(currencyCode), [currencyCode]);
|
|
150
|
+
|
|
151
|
+
const { columns, customFieldColumnNames } = useGeneratedColumns({
|
|
152
|
+
fields,
|
|
153
|
+
rowActions: [],
|
|
154
|
+
customizeColumns: customizeColumns as any,
|
|
155
|
+
deleteMutation: undefined,
|
|
156
|
+
defaultColumnOrder: columnOrder,
|
|
157
|
+
additionalColumns: {},
|
|
158
|
+
includeSelectionColumn: false,
|
|
159
|
+
includeActionsColumn: false,
|
|
160
|
+
enableSorting: false,
|
|
80
161
|
});
|
|
81
162
|
|
|
163
|
+
const columnVisibility = getColumnVisibility(fields, defaultColumnVisibility, customFieldColumnNames);
|
|
164
|
+
const visibleColumnCount = Object.values(columnVisibility).filter(Boolean).length;
|
|
165
|
+
const data = order.lines;
|
|
166
|
+
|
|
82
167
|
return (
|
|
83
168
|
<div className="w-full">
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
))}
|
|
103
|
-
</TableHeader>
|
|
104
|
-
<TableBody>
|
|
105
|
-
{table.getRowModel().rows?.length ? (
|
|
106
|
-
table.getRowModel().rows.map(row => (
|
|
107
|
-
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
|
108
|
-
{row.getVisibleCells().map(cell => (
|
|
109
|
-
<TableCell key={cell.id}>
|
|
110
|
-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
111
|
-
</TableCell>
|
|
112
|
-
))}
|
|
113
|
-
</TableRow>
|
|
114
|
-
))
|
|
115
|
-
) : (
|
|
116
|
-
<TableRow>
|
|
117
|
-
<TableCell colSpan={columns.length} className="h-24 text-center">
|
|
118
|
-
No results.
|
|
119
|
-
</TableCell>
|
|
120
|
-
</TableRow>
|
|
121
|
-
)}
|
|
122
|
-
<OrderTableTotals order={order} columnCount={columns.length} />
|
|
123
|
-
</TableBody>
|
|
124
|
-
</Table>
|
|
125
|
-
</div>
|
|
169
|
+
<DataTable
|
|
170
|
+
columns={columns as any}
|
|
171
|
+
data={data as any}
|
|
172
|
+
totalItems={data.length}
|
|
173
|
+
disableViewOptions={false}
|
|
174
|
+
defaultColumnVisibility={columnVisibility}
|
|
175
|
+
onColumnVisibilityChange={(_, columnVisibility) => {
|
|
176
|
+
setTableSettings(pageId, 'columnVisibility', columnVisibility);
|
|
177
|
+
}}
|
|
178
|
+
setTableOptions={options => ({
|
|
179
|
+
...options,
|
|
180
|
+
manualPagination: false,
|
|
181
|
+
manualSorting: false,
|
|
182
|
+
manualFiltering: false,
|
|
183
|
+
})}
|
|
184
|
+
>
|
|
185
|
+
<OrderTableTotals order={order} columnCount={visibleColumnCount} />
|
|
186
|
+
</DataTable>
|
|
126
187
|
</div>
|
|
127
188
|
);
|
|
128
189
|
}
|
|
@@ -1,46 +1,283 @@
|
|
|
1
1
|
import { LabeledData } from '@/vdb/components/labeled-data.js';
|
|
2
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/vdb/components/ui/collapsible.js';
|
|
4
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
2
5
|
import { ResultOf } from '@/vdb/graphql/graphql.js';
|
|
3
6
|
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { Trans } from '@/vdb/lib/trans.js';
|
|
7
|
-
import { paymentWithRefundsFragment } from '../orders.graphql.js';
|
|
7
|
+
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
8
|
+
import { useMutation } from '@tanstack/react-query';
|
|
8
9
|
import { JsonEditor } from 'json-edit-react';
|
|
10
|
+
import { ChevronDown } from 'lucide-react';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { toast } from 'sonner';
|
|
13
|
+
import {
|
|
14
|
+
cancelPaymentDocument,
|
|
15
|
+
paymentWithRefundsFragment,
|
|
16
|
+
settlePaymentDocument,
|
|
17
|
+
settleRefundDocument,
|
|
18
|
+
transitionPaymentToStateDocument,
|
|
19
|
+
} from '../orders.graphql.js';
|
|
20
|
+
import { SettleRefundDialog } from './settle-refund-dialog.js';
|
|
21
|
+
import { getTypeForState, StateTransitionControl } from './state-transition-control.js';
|
|
9
22
|
|
|
10
23
|
type PaymentDetailsProps = {
|
|
11
24
|
payment: ResultOf<typeof paymentWithRefundsFragment>;
|
|
12
25
|
currencyCode: string;
|
|
26
|
+
onSuccess?: () => void;
|
|
13
27
|
};
|
|
14
28
|
|
|
15
|
-
export function PaymentDetails({ payment, currencyCode }: Readonly<PaymentDetailsProps>) {
|
|
29
|
+
export function PaymentDetails({ payment, currencyCode, onSuccess }: Readonly<PaymentDetailsProps>) {
|
|
16
30
|
const { formatCurrency, formatDate } = useLocalFormat();
|
|
17
|
-
const
|
|
31
|
+
const { i18n } = useLingui();
|
|
32
|
+
const [settleRefundDialogOpen, setSettleRefundDialogOpen] = useState(false);
|
|
33
|
+
const [selectedRefundId, setSelectedRefundId] = useState<string | null>(null);
|
|
34
|
+
|
|
35
|
+
const settlePaymentMutation = useMutation({
|
|
36
|
+
mutationFn: api.mutate(settlePaymentDocument),
|
|
37
|
+
onSuccess: (result: ResultOf<typeof settlePaymentDocument>) => {
|
|
38
|
+
if (result.settlePayment.__typename === 'Payment') {
|
|
39
|
+
toast.success(i18n.t('Payment settled successfully'));
|
|
40
|
+
onSuccess?.();
|
|
41
|
+
} else {
|
|
42
|
+
toast.error(result.settlePayment.message ?? i18n.t('Failed to settle payment'));
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
onError: () => {
|
|
46
|
+
toast.error(i18n.t('Failed to settle payment'));
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const transitionPaymentMutation = useMutation({
|
|
51
|
+
mutationFn: api.mutate(transitionPaymentToStateDocument),
|
|
52
|
+
onSuccess: (result: ResultOf<typeof transitionPaymentToStateDocument>) => {
|
|
53
|
+
if (result.transitionPaymentToState.__typename === 'Payment') {
|
|
54
|
+
toast.success(i18n.t('Payment state updated successfully'));
|
|
55
|
+
onSuccess?.();
|
|
56
|
+
} else {
|
|
57
|
+
toast.error(
|
|
58
|
+
result.transitionPaymentToState.message ?? i18n.t('Failed to update payment state'),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
onError: () => {
|
|
63
|
+
toast.error(i18n.t('Failed to update payment state'));
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const cancelPaymentMutation = useMutation({
|
|
68
|
+
mutationFn: api.mutate(cancelPaymentDocument),
|
|
69
|
+
onSuccess: (result: ResultOf<typeof cancelPaymentDocument>) => {
|
|
70
|
+
if (result.cancelPayment.__typename === 'Payment') {
|
|
71
|
+
toast.success(i18n.t('Payment cancelled successfully'));
|
|
72
|
+
onSuccess?.();
|
|
73
|
+
} else {
|
|
74
|
+
toast.error(result.cancelPayment.message ?? i18n.t('Failed to cancel payment'));
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
onError: () => {
|
|
78
|
+
toast.error(i18n.t('Failed to cancel payment'));
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const settleRefundMutation = useMutation({
|
|
83
|
+
mutationFn: api.mutate(settleRefundDocument),
|
|
84
|
+
onSuccess: (result: ResultOf<typeof settleRefundDocument>) => {
|
|
85
|
+
if (result.settleRefund.__typename === 'Refund') {
|
|
86
|
+
toast.success(i18n.t('Refund settled successfully'));
|
|
87
|
+
onSuccess?.();
|
|
88
|
+
setSettleRefundDialogOpen(false);
|
|
89
|
+
} else {
|
|
90
|
+
toast.error(result.settleRefund.message ?? i18n.t('Failed to settle refund'));
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
onError: () => {
|
|
94
|
+
toast.error(i18n.t('Failed to settle refund'));
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const handlePaymentStateTransition = (state: string) => {
|
|
99
|
+
if (state === 'Cancelled') {
|
|
100
|
+
cancelPaymentMutation.mutate({ id: payment.id });
|
|
101
|
+
} else {
|
|
102
|
+
transitionPaymentMutation.mutate({ id: payment.id, state });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleSettlePayment = () => {
|
|
107
|
+
settlePaymentMutation.mutate({ id: payment.id });
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const handleSettleRefund = (refundId: string) => {
|
|
111
|
+
setSelectedRefundId(refundId);
|
|
112
|
+
setSettleRefundDialogOpen(true);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleSettleRefundConfirm = (transactionId: string) => {
|
|
116
|
+
if (selectedRefundId) {
|
|
117
|
+
settleRefundMutation.mutate({
|
|
118
|
+
input: {
|
|
119
|
+
id: selectedRefundId,
|
|
120
|
+
transactionId,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const nextOtherStates = (): string[] => {
|
|
127
|
+
if (!payment.nextStates) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
return payment.nextStates.filter(s => s !== 'Settled' && s !== 'Error');
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const getPaymentActions = () => {
|
|
134
|
+
const actions = [];
|
|
135
|
+
|
|
136
|
+
if (payment.nextStates?.includes('Settled')) {
|
|
137
|
+
actions.push({
|
|
138
|
+
label: 'Settle payment',
|
|
139
|
+
onClick: handleSettlePayment,
|
|
140
|
+
type: 'success',
|
|
141
|
+
disabled: settlePaymentMutation.isPending,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
nextOtherStates().forEach(state => {
|
|
146
|
+
actions.push({
|
|
147
|
+
label: state === 'Cancelled' ? 'Cancel payment' : `Transition to ${state}`,
|
|
148
|
+
type: getTypeForState(state),
|
|
149
|
+
onClick: () => handlePaymentStateTransition(state),
|
|
150
|
+
disabled: transitionPaymentMutation.isPending || cancelPaymentMutation.isPending,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return actions;
|
|
155
|
+
};
|
|
18
156
|
|
|
19
157
|
return (
|
|
20
|
-
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
158
|
+
<>
|
|
159
|
+
<div className="space-y-1 p-3 border rounded-md">
|
|
160
|
+
<div className="grid lg:grid-cols-2 gap-2">
|
|
161
|
+
<LabeledData label={<Trans>Payment method</Trans>} value={payment.method} />
|
|
162
|
+
<LabeledData
|
|
163
|
+
label={<Trans>Amount</Trans>}
|
|
164
|
+
value={formatCurrency(payment.amount, currencyCode)}
|
|
165
|
+
/>
|
|
166
|
+
<LabeledData
|
|
167
|
+
label={<Trans>Created at</Trans>}
|
|
168
|
+
value={formatDate(payment.createdAt, { dateStyle: 'short', timeStyle: 'short' })}
|
|
169
|
+
/>
|
|
170
|
+
{payment.transactionId && (
|
|
171
|
+
<LabeledData label={<Trans>Transaction ID</Trans>} value={payment.transactionId} />
|
|
172
|
+
)}
|
|
173
|
+
{/* We need to check if there is errorMessage field in the Payment type */}
|
|
174
|
+
{payment.errorMessage && (
|
|
175
|
+
<LabeledData
|
|
176
|
+
label={<Trans>Error message</Trans>}
|
|
177
|
+
value={payment.errorMessage}
|
|
178
|
+
className="text-destructive"
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
<Collapsible className="mt-2 border-t pt-2">
|
|
183
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full text-sm hover:underline text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md p-1 -m-1">
|
|
184
|
+
<Trans>Payment metadata</Trans>
|
|
185
|
+
<ChevronDown className="h-4 w-4 transition-transform duration-200 data-[state=open]:rotate-180" />
|
|
186
|
+
</CollapsibleTrigger>
|
|
187
|
+
<CollapsibleContent className="mt-2">
|
|
188
|
+
<JsonEditor
|
|
189
|
+
viewOnly
|
|
190
|
+
rootFontSize={12}
|
|
191
|
+
minWidth={100}
|
|
192
|
+
rootName=""
|
|
193
|
+
data={payment.metadata}
|
|
194
|
+
collapse
|
|
195
|
+
/>
|
|
196
|
+
</CollapsibleContent>
|
|
197
|
+
</Collapsible>
|
|
198
|
+
{payment.refunds && payment.refunds.length > 0 && (
|
|
199
|
+
<Collapsible className="mt-2 border-t pt-2">
|
|
200
|
+
<CollapsibleTrigger className="flex items-center justify-between w-full text-sm hover:underline text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md p-1 -m-1">
|
|
201
|
+
<Trans>Refunds ({payment.refunds.length})</Trans>
|
|
202
|
+
<ChevronDown className="h-4 w-4 transition-transform duration-200 data-[state=open]:rotate-180" />
|
|
203
|
+
</CollapsibleTrigger>
|
|
204
|
+
<CollapsibleContent className="mt-2 space-y-3">
|
|
205
|
+
{payment.refunds.map(refund => (
|
|
206
|
+
<div key={refund.id} className="p-3 border rounded-md bg-muted/50">
|
|
207
|
+
<div className="space-y-1">
|
|
208
|
+
<LabeledData label={<Trans>Refund ID</Trans>} value={refund.id} />
|
|
209
|
+
<LabeledData label={<Trans>State</Trans>} value={refund.state} />
|
|
210
|
+
<LabeledData
|
|
211
|
+
label={<Trans>Created at</Trans>}
|
|
212
|
+
value={formatDate(refund.createdAt, {
|
|
213
|
+
dateStyle: 'short',
|
|
214
|
+
timeStyle: 'short',
|
|
215
|
+
})}
|
|
216
|
+
/>
|
|
217
|
+
<LabeledData
|
|
218
|
+
label={<Trans>Total</Trans>}
|
|
219
|
+
value={formatCurrency(refund.total, currencyCode)}
|
|
220
|
+
/>
|
|
221
|
+
{refund.reason && (
|
|
222
|
+
<LabeledData
|
|
223
|
+
label={<Trans>Reason</Trans>}
|
|
224
|
+
value={refund.reason}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
{refund.transactionId && (
|
|
228
|
+
<LabeledData
|
|
229
|
+
label={<Trans>Transaction ID</Trans>}
|
|
230
|
+
value={refund.transactionId}
|
|
231
|
+
/>
|
|
232
|
+
)}
|
|
233
|
+
{refund.metadata && Object.keys(refund.metadata).length > 0 && (
|
|
234
|
+
<div className="mt-2">
|
|
235
|
+
<LabeledData label={<Trans>Metadata</Trans>} value="" />
|
|
236
|
+
<JsonEditor
|
|
237
|
+
viewOnly
|
|
238
|
+
rootFontSize={11}
|
|
239
|
+
minWidth={100}
|
|
240
|
+
rootName=""
|
|
241
|
+
data={refund.metadata}
|
|
242
|
+
collapse
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
{refund.state === 'Pending' && (
|
|
248
|
+
<div className="mt-3 pt-3 border-t">
|
|
249
|
+
<Button
|
|
250
|
+
size="sm"
|
|
251
|
+
onClick={() => handleSettleRefund(refund.id)}
|
|
252
|
+
disabled={settleRefundMutation.isPending}
|
|
253
|
+
>
|
|
254
|
+
<Trans>Settle refund</Trans>
|
|
255
|
+
</Button>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
))}
|
|
260
|
+
</CollapsibleContent>
|
|
261
|
+
</Collapsible>
|
|
262
|
+
)}
|
|
263
|
+
<div className="mt-3 pt-3 border-t">
|
|
264
|
+
<StateTransitionControl
|
|
265
|
+
currentState={payment.state}
|
|
266
|
+
actions={getPaymentActions()}
|
|
267
|
+
isLoading={
|
|
268
|
+
settlePaymentMutation.isPending ||
|
|
269
|
+
transitionPaymentMutation.isPending ||
|
|
270
|
+
cancelPaymentMutation.isPending
|
|
271
|
+
}
|
|
272
|
+
/>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
<SettleRefundDialog
|
|
276
|
+
open={settleRefundDialogOpen}
|
|
277
|
+
onOpenChange={setSettleRefundDialogOpen}
|
|
278
|
+
onSettle={handleSettleRefundConfirm}
|
|
279
|
+
isLoading={settleRefundMutation.isPending}
|
|
280
|
+
/>
|
|
281
|
+
</>
|
|
45
282
|
);
|
|
46
283
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogDescription,
|
|
6
|
+
DialogFooter,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from '@/vdb/components/ui/dialog.js';
|
|
10
|
+
import { Input } from '@/vdb/components/ui/input.js';
|
|
11
|
+
import { Label } from '@/vdb/components/ui/label.js';
|
|
12
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
|
|
15
|
+
type SettleRefundDialogProps = {
|
|
16
|
+
open: boolean;
|
|
17
|
+
onOpenChange: (open: boolean) => void;
|
|
18
|
+
onSettle: (transactionId: string) => void;
|
|
19
|
+
isLoading?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function SettleRefundDialog({
|
|
23
|
+
open,
|
|
24
|
+
onOpenChange,
|
|
25
|
+
onSettle,
|
|
26
|
+
isLoading,
|
|
27
|
+
}: Readonly<SettleRefundDialogProps>) {
|
|
28
|
+
const [transactionId, setTransactionId] = useState('');
|
|
29
|
+
|
|
30
|
+
const handleSettle = () => {
|
|
31
|
+
if (transactionId.trim()) {
|
|
32
|
+
onSettle(transactionId.trim());
|
|
33
|
+
setTransactionId('');
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleOpenChange = (newOpen: boolean) => {
|
|
38
|
+
if (!newOpen) {
|
|
39
|
+
setTransactionId('');
|
|
40
|
+
}
|
|
41
|
+
onOpenChange(newOpen);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
46
|
+
<DialogContent>
|
|
47
|
+
<DialogHeader>
|
|
48
|
+
<DialogTitle>
|
|
49
|
+
<Trans>Settle refund</Trans>
|
|
50
|
+
</DialogTitle>
|
|
51
|
+
<DialogDescription>
|
|
52
|
+
<Trans>Enter the transaction ID for this refund settlement</Trans>
|
|
53
|
+
</DialogDescription>
|
|
54
|
+
</DialogHeader>
|
|
55
|
+
<div className="space-y-4">
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
<Label htmlFor="transaction-id">
|
|
58
|
+
<Trans>Transaction ID</Trans>
|
|
59
|
+
</Label>
|
|
60
|
+
<Input
|
|
61
|
+
id="transaction-id"
|
|
62
|
+
value={transactionId}
|
|
63
|
+
onChange={e => setTransactionId(e.target.value)}
|
|
64
|
+
placeholder="Enter transaction ID..."
|
|
65
|
+
disabled={isLoading}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<DialogFooter>
|
|
70
|
+
<Button variant="outline" onClick={() => handleOpenChange(false)} disabled={isLoading}>
|
|
71
|
+
<Trans>Cancel</Trans>
|
|
72
|
+
</Button>
|
|
73
|
+
<Button onClick={handleSettle} disabled={!transactionId.trim() || isLoading}>
|
|
74
|
+
<Trans>Settle refund</Trans>
|
|
75
|
+
</Button>
|
|
76
|
+
</DialogFooter>
|
|
77
|
+
</DialogContent>
|
|
78
|
+
</Dialog>
|
|
79
|
+
);
|
|
80
|
+
}
|