@vendure/dashboard 3.3.6-master-202507100236 → 3.3.6-master-202507120238

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 (49) hide show
  1. package/dist/plugin/tests/barrel-exports.spec.js +1 -1
  2. package/dist/plugin/utils/config-loader.js +2 -2
  3. package/package.json +4 -4
  4. package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +0 -1
  5. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +30 -37
  6. package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +33 -53
  7. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +14 -7
  8. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +23 -12
  9. package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +364 -0
  10. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +222 -0
  11. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +146 -85
  12. package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +272 -31
  13. package/src/app/routes/_authenticated/_orders/components/settle-refund-dialog.tsx +80 -0
  14. package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +102 -0
  15. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +144 -0
  16. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +118 -2
  17. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +144 -52
  18. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +550 -0
  19. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +0 -18
  20. package/src/app/routes/_authenticated/_orders/utils/order-types.ts +5 -2
  21. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +4 -3
  22. package/src/app/routes/_authenticated/_products/components/option-value-input.tsx +1 -1
  23. package/src/app/routes/_authenticated/_products/products_.$id.tsx +0 -1
  24. package/src/app/routes/_authenticated/_zones/components/zone-countries-table.tsx +0 -7
  25. package/src/lib/components/data-display/date-time.tsx +7 -1
  26. package/src/lib/components/data-input/relation-input.tsx +11 -0
  27. package/src/lib/components/data-input/relation-selector.tsx +9 -2
  28. package/src/lib/components/data-table/data-table-utils.ts +34 -0
  29. package/src/lib/components/data-table/data-table-view-options.tsx +2 -2
  30. package/src/lib/components/data-table/data-table.tsx +5 -2
  31. package/src/lib/components/data-table/use-generated-columns.tsx +307 -0
  32. package/src/lib/components/layout/content-language-selector.tsx +1 -1
  33. package/src/lib/components/shared/asset/asset-preview.tsx +0 -6
  34. package/src/lib/components/shared/option-value-input.tsx +1 -1
  35. package/src/lib/components/shared/paginated-list-data-table.tsx +15 -286
  36. package/src/lib/components/shared/product-variant-selector.tsx +29 -5
  37. package/src/lib/components/ui/calendar.tsx +1 -1
  38. package/src/lib/framework/component-registry/dynamic-component.tsx +3 -3
  39. package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +1 -1
  40. package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +0 -2
  41. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +321 -2
  42. package/src/lib/framework/document-introspection/get-document-structure.ts +149 -31
  43. package/src/lib/framework/extension-api/types/layout.ts +21 -6
  44. package/src/lib/framework/layout-engine/layout-extensions.ts +1 -4
  45. package/src/lib/framework/layout-engine/page-layout.tsx +61 -10
  46. package/src/lib/framework/page/use-detail-page.ts +10 -7
  47. package/src/lib/hooks/use-extended-list-query.ts +32 -30
  48. package/vite/tests/barrel-exports.spec.ts +1 -1
  49. package/vite/utils/config-loader.ts +2 -2
@@ -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,287 @@
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 {
22
+ getTypeForState,
23
+ StateTransitionAction,
24
+ StateTransitionControl,
25
+ } from './state-transition-control.js';
9
26
 
10
27
  type PaymentDetailsProps = {
11
28
  payment: ResultOf<typeof paymentWithRefundsFragment>;
12
29
  currencyCode: string;
30
+ onSuccess?: () => void;
13
31
  };
14
32
 
15
- export function PaymentDetails({ payment, currencyCode }: Readonly<PaymentDetailsProps>) {
33
+ export function PaymentDetails({ payment, currencyCode, onSuccess }: Readonly<PaymentDetailsProps>) {
16
34
  const { formatCurrency, formatDate } = useLocalFormat();
17
- const t = (key: string) => key;
35
+ const { i18n } = useLingui();
36
+ const [settleRefundDialogOpen, setSettleRefundDialogOpen] = useState(false);
37
+ const [selectedRefundId, setSelectedRefundId] = useState<string | null>(null);
38
+
39
+ const settlePaymentMutation = useMutation({
40
+ mutationFn: api.mutate(settlePaymentDocument),
41
+ onSuccess: (result: ResultOf<typeof settlePaymentDocument>) => {
42
+ if (result.settlePayment.__typename === 'Payment') {
43
+ toast.success(i18n.t('Payment settled successfully'));
44
+ onSuccess?.();
45
+ } else {
46
+ toast.error(result.settlePayment.message ?? i18n.t('Failed to settle payment'));
47
+ }
48
+ },
49
+ onError: () => {
50
+ toast.error(i18n.t('Failed to settle payment'));
51
+ },
52
+ });
53
+
54
+ const transitionPaymentMutation = useMutation({
55
+ mutationFn: api.mutate(transitionPaymentToStateDocument),
56
+ onSuccess: (result: ResultOf<typeof transitionPaymentToStateDocument>) => {
57
+ if (result.transitionPaymentToState.__typename === 'Payment') {
58
+ toast.success(i18n.t('Payment state updated successfully'));
59
+ onSuccess?.();
60
+ } else {
61
+ toast.error(
62
+ result.transitionPaymentToState.message ?? i18n.t('Failed to update payment state'),
63
+ );
64
+ }
65
+ },
66
+ onError: () => {
67
+ toast.error(i18n.t('Failed to update payment state'));
68
+ },
69
+ });
70
+
71
+ const cancelPaymentMutation = useMutation({
72
+ mutationFn: api.mutate(cancelPaymentDocument),
73
+ onSuccess: (result: ResultOf<typeof cancelPaymentDocument>) => {
74
+ if (result.cancelPayment.__typename === 'Payment') {
75
+ toast.success(i18n.t('Payment cancelled successfully'));
76
+ onSuccess?.();
77
+ } else {
78
+ toast.error(result.cancelPayment.message ?? i18n.t('Failed to cancel payment'));
79
+ }
80
+ },
81
+ onError: () => {
82
+ toast.error(i18n.t('Failed to cancel payment'));
83
+ },
84
+ });
85
+
86
+ const settleRefundMutation = useMutation({
87
+ mutationFn: api.mutate(settleRefundDocument),
88
+ onSuccess: (result: ResultOf<typeof settleRefundDocument>) => {
89
+ if (result.settleRefund.__typename === 'Refund') {
90
+ toast.success(i18n.t('Refund settled successfully'));
91
+ onSuccess?.();
92
+ setSettleRefundDialogOpen(false);
93
+ } else {
94
+ toast.error(result.settleRefund.message ?? i18n.t('Failed to settle refund'));
95
+ }
96
+ },
97
+ onError: () => {
98
+ toast.error(i18n.t('Failed to settle refund'));
99
+ },
100
+ });
101
+
102
+ const handlePaymentStateTransition = (state: string) => {
103
+ if (state === 'Cancelled') {
104
+ cancelPaymentMutation.mutate({ id: payment.id });
105
+ } else {
106
+ transitionPaymentMutation.mutate({ id: payment.id, state });
107
+ }
108
+ };
109
+
110
+ const handleSettlePayment = () => {
111
+ settlePaymentMutation.mutate({ id: payment.id });
112
+ };
113
+
114
+ const handleSettleRefund = (refundId: string) => {
115
+ setSelectedRefundId(refundId);
116
+ setSettleRefundDialogOpen(true);
117
+ };
118
+
119
+ const handleSettleRefundConfirm = (transactionId: string) => {
120
+ if (selectedRefundId) {
121
+ settleRefundMutation.mutate({
122
+ input: {
123
+ id: selectedRefundId,
124
+ transactionId,
125
+ },
126
+ });
127
+ }
128
+ };
129
+
130
+ const nextOtherStates = (): string[] => {
131
+ if (!payment.nextStates) {
132
+ return [];
133
+ }
134
+ return payment.nextStates.filter(s => s !== 'Settled' && s !== 'Error');
135
+ };
136
+
137
+ const getPaymentActions = () => {
138
+ const actions: StateTransitionAction[] = [];
139
+
140
+ if (payment.nextStates?.includes('Settled')) {
141
+ actions.push({
142
+ label: 'Settle payment',
143
+ onClick: handleSettlePayment,
144
+ type: 'success',
145
+ disabled: settlePaymentMutation.isPending,
146
+ });
147
+ }
148
+
149
+ nextOtherStates().forEach(state => {
150
+ actions.push({
151
+ label: state === 'Cancelled' ? 'Cancel payment' : `Transition to ${state}`,
152
+ type: getTypeForState(state),
153
+ onClick: () => handlePaymentStateTransition(state),
154
+ disabled: transitionPaymentMutation.isPending || cancelPaymentMutation.isPending,
155
+ });
156
+ });
157
+
158
+ return actions;
159
+ };
18
160
 
19
161
  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>
162
+ <>
163
+ <div className="space-y-1 p-3 border rounded-md">
164
+ <div className="grid lg:grid-cols-2 gap-2">
165
+ <LabeledData label={<Trans>Payment method</Trans>} value={payment.method} />
166
+ <LabeledData
167
+ label={<Trans>Amount</Trans>}
168
+ value={formatCurrency(payment.amount, currencyCode)}
169
+ />
170
+ <LabeledData
171
+ label={<Trans>Created at</Trans>}
172
+ value={formatDate(payment.createdAt, { dateStyle: 'short', timeStyle: 'short' })}
173
+ />
174
+ {payment.transactionId && (
175
+ <LabeledData label={<Trans>Transaction ID</Trans>} value={payment.transactionId} />
176
+ )}
177
+ {/* We need to check if there is errorMessage field in the Payment type */}
178
+ {payment.errorMessage && (
179
+ <LabeledData
180
+ label={<Trans>Error message</Trans>}
181
+ value={payment.errorMessage}
182
+ className="text-destructive"
183
+ />
184
+ )}
185
+ </div>
186
+ <Collapsible className="mt-2 border-t pt-2">
187
+ <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">
188
+ <Trans>Payment metadata</Trans>
189
+ <ChevronDown className="h-4 w-4 transition-transform duration-200 data-[state=open]:rotate-180" />
190
+ </CollapsibleTrigger>
191
+ <CollapsibleContent className="mt-2">
192
+ <JsonEditor
193
+ viewOnly
194
+ rootFontSize={12}
195
+ minWidth={100}
196
+ rootName=""
197
+ data={payment.metadata}
198
+ collapse
199
+ />
200
+ </CollapsibleContent>
201
+ </Collapsible>
202
+ {payment.refunds && payment.refunds.length > 0 && (
203
+ <Collapsible className="mt-2 border-t pt-2">
204
+ <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">
205
+ <Trans>Refunds ({payment.refunds.length})</Trans>
206
+ <ChevronDown className="h-4 w-4 transition-transform duration-200 data-[state=open]:rotate-180" />
207
+ </CollapsibleTrigger>
208
+ <CollapsibleContent className="mt-2 space-y-3">
209
+ {payment.refunds.map(refund => (
210
+ <div key={refund.id} className="p-3 border rounded-md bg-muted/50">
211
+ <div className="space-y-1">
212
+ <LabeledData label={<Trans>Refund ID</Trans>} value={refund.id} />
213
+ <LabeledData label={<Trans>State</Trans>} value={refund.state} />
214
+ <LabeledData
215
+ label={<Trans>Created at</Trans>}
216
+ value={formatDate(refund.createdAt, {
217
+ dateStyle: 'short',
218
+ timeStyle: 'short',
219
+ })}
220
+ />
221
+ <LabeledData
222
+ label={<Trans>Total</Trans>}
223
+ value={formatCurrency(refund.total, currencyCode)}
224
+ />
225
+ {refund.reason && (
226
+ <LabeledData
227
+ label={<Trans>Reason</Trans>}
228
+ value={refund.reason}
229
+ />
230
+ )}
231
+ {refund.transactionId && (
232
+ <LabeledData
233
+ label={<Trans>Transaction ID</Trans>}
234
+ value={refund.transactionId}
235
+ />
236
+ )}
237
+ {refund.metadata && Object.keys(refund.metadata).length > 0 && (
238
+ <div className="mt-2">
239
+ <LabeledData label={<Trans>Metadata</Trans>} value="" />
240
+ <JsonEditor
241
+ viewOnly
242
+ rootFontSize={11}
243
+ minWidth={100}
244
+ rootName=""
245
+ data={refund.metadata}
246
+ collapse
247
+ />
248
+ </div>
249
+ )}
250
+ </div>
251
+ {refund.state === 'Pending' && (
252
+ <div className="mt-3 pt-3 border-t">
253
+ <Button
254
+ size="sm"
255
+ onClick={() => handleSettleRefund(refund.id)}
256
+ disabled={settleRefundMutation.isPending}
257
+ >
258
+ <Trans>Settle refund</Trans>
259
+ </Button>
260
+ </div>
261
+ )}
262
+ </div>
263
+ ))}
264
+ </CollapsibleContent>
265
+ </Collapsible>
266
+ )}
267
+ <div className="mt-3 pt-3 border-t">
268
+ <StateTransitionControl
269
+ currentState={payment.state}
270
+ actions={getPaymentActions()}
271
+ isLoading={
272
+ settlePaymentMutation.isPending ||
273
+ transitionPaymentMutation.isPending ||
274
+ cancelPaymentMutation.isPending
275
+ }
276
+ />
277
+ </div>
278
+ </div>
279
+ <SettleRefundDialog
280
+ open={settleRefundDialogOpen}
281
+ onOpenChange={setSettleRefundDialogOpen}
282
+ onSettle={handleSettleRefundConfirm}
283
+ isLoading={settleRefundMutation.isPending}
284
+ />
285
+ </>
45
286
  );
46
287
  }