@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.
Files changed (37) hide show
  1. package/dist/plugin/tests/barrel-exports.spec.js +1 -1
  2. package/package.json +4 -4
  3. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +30 -37
  4. package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +33 -53
  5. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +14 -7
  6. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +23 -12
  7. package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +364 -0
  8. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +222 -0
  9. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +146 -85
  10. package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +268 -31
  11. package/src/app/routes/_authenticated/_orders/components/settle-refund-dialog.tsx +80 -0
  12. package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +102 -0
  13. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +144 -0
  14. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +118 -2
  15. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +144 -52
  16. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +550 -0
  17. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +0 -17
  18. package/src/app/routes/_authenticated/_orders/utils/order-types.ts +5 -2
  19. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +4 -3
  20. package/src/app/routes/_authenticated/_products/products_.$id.tsx +0 -1
  21. package/src/lib/components/data-display/date-time.tsx +7 -1
  22. package/src/lib/components/data-input/relation-input.tsx +11 -0
  23. package/src/lib/components/data-input/relation-selector.tsx +9 -2
  24. package/src/lib/components/data-table/data-table-utils.ts +34 -0
  25. package/src/lib/components/data-table/data-table-view-options.tsx +2 -2
  26. package/src/lib/components/data-table/data-table.tsx +5 -2
  27. package/src/lib/components/data-table/use-generated-columns.tsx +307 -0
  28. package/src/lib/components/shared/paginated-list-data-table.tsx +15 -286
  29. package/src/lib/components/shared/product-variant-selector.tsx +28 -4
  30. package/src/lib/framework/component-registry/dynamic-component.tsx +3 -3
  31. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +321 -2
  32. package/src/lib/framework/document-introspection/get-document-structure.ts +149 -31
  33. package/src/lib/framework/extension-api/types/layout.ts +21 -6
  34. package/src/lib/framework/layout-engine/layout-extensions.ts +1 -4
  35. package/src/lib/framework/layout-engine/page-layout.tsx +61 -10
  36. package/src/lib/framework/page/use-detail-page.ts +10 -7
  37. 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/vdb/components/ui/table.js';
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
- ColumnDef,
6
- flexRender,
7
- getCoreRowModel,
8
- useReactTable,
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
- export function OrderTable({ order }: Readonly<OrderTableProps>) {
24
- const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
25
-
26
- const currencyCode = order.currencyCode;
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: 'Product',
39
- accessorKey: 'productVariant.name',
40
- },
41
- {
42
- header: 'SKU',
43
- accessorKey: 'productVariant.sku',
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: 'Unit price',
53
+ unitPriceWithTax: {
54
+ header: () => <Trans>Unit price</Trans>,
47
55
  accessorKey: 'unitPriceWithTax',
48
- cell: ({ cell, row }) => {
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
- header: 'Quantity',
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
- header: 'Total',
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: ({ cell, row }) => {
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 data = order.lines;
147
+ const fields = getFieldsFromDocumentNode(addCustomFields(orderDetailDocument), ['order', 'lines']);
70
148
 
71
- const table = useReactTable({
72
- data,
73
- columns,
74
- getCoreRowModel: getCoreRowModel(),
75
- rowCount: data.length,
76
- onColumnVisibilityChange: setColumnVisibility,
77
- state: {
78
- columnVisibility,
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
- <div className="">
85
- <Table>
86
- <TableHeader>
87
- {table.getHeaderGroups().map(headerGroup => (
88
- <TableRow key={headerGroup.id}>
89
- {headerGroup.headers.map(header => {
90
- return (
91
- <TableHead key={header.id}>
92
- {header.isPlaceholder
93
- ? null
94
- : flexRender(
95
- header.column.columnDef.header,
96
- header.getContext(),
97
- )}
98
- </TableHead>
99
- );
100
- })}
101
- </TableRow>
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 { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/vdb/components/ui/collapsible.js';
5
- import { ChevronDown } from 'lucide-react';
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 t = (key: string) => key;
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
- <div className="space-y-1 p-3 border rounded-md">
21
- <LabeledData label={<Trans>Payment method</Trans>} value={payment.method} />
22
- <LabeledData label={<Trans>Amount</Trans>} value={formatCurrency(payment.amount, currencyCode)} />
23
- <LabeledData label={<Trans>Created at</Trans>} value={formatDate(payment.createdAt)} />
24
- {payment.transactionId && (
25
- <LabeledData label={<Trans>Transaction ID</Trans>} value={payment.transactionId} />
26
- )}
27
- {/* We need to check if there is errorMessage field in the Payment type */}
28
- {payment.errorMessage && (
29
- <LabeledData
30
- label={<Trans>Error message</Trans>}
31
- value={payment.errorMessage}
32
- className="text-destructive"
33
- />
34
- )}
35
- <Collapsible className="mt-2 border-t pt-2">
36
- <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">
37
- <Trans>Payment metadata</Trans>
38
- <ChevronDown className="h-4 w-4 transition-transform duration-200 data-[state=open]:rotate-180" />
39
- </CollapsibleTrigger>
40
- <CollapsibleContent className="mt-2">
41
- <JsonEditor viewOnly rootFontSize={12} minWidth={100} rootName='' data={payment.metadata} collapse />
42
- </CollapsibleContent>
43
- </Collapsible>
44
- </div>
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
+ }