@vendure/dashboard 3.4.3-master-202509230228 → 3.4.3-master-202509250229

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/index.html +11 -12
  2. package/package.json +4 -4
  3. package/src/app/common/use-page-title.test.ts +263 -0
  4. package/src/app/common/use-page-title.ts +86 -0
  5. package/src/app/routes/__root.tsx +4 -4
  6. package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-container.tsx +2 -2
  7. package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-types.ts +5 -0
  8. package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-utils.tsx +124 -0
  9. package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history.tsx +91 -59
  10. package/src/app/routes/_authenticated/_customers/components/customer-history/default-customer-history-components.tsx +176 -0
  11. package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +4 -2
  12. package/src/app/routes/_authenticated/_customers/customers.graphql.ts +2 -0
  13. package/src/app/routes/_authenticated/_orders/components/order-history/default-order-history-components.tsx +98 -0
  14. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-container.tsx +9 -7
  15. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-types.ts +5 -0
  16. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +173 -0
  17. package/src/app/routes/_authenticated/_orders/components/order-history/order-history.tsx +64 -408
  18. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +4 -0
  19. package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +9 -4
  20. package/src/app/routes/_authenticated/_shipping-methods/components/metadata-badges.tsx +15 -0
  21. package/src/app/routes/_authenticated/_shipping-methods/components/price-display.tsx +21 -0
  22. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-test-result-wrapper.tsx +87 -0
  23. package/src/app/routes/_authenticated/_shipping-methods/components/test-address-form.tsx +255 -0
  24. package/src/app/routes/_authenticated/_shipping-methods/components/test-order-builder.tsx +243 -0
  25. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-result.tsx +97 -0
  26. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-sheet.tsx +41 -0
  27. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods.tsx +74 -0
  28. package/src/app/routes/_authenticated/_shipping-methods/components/test-single-method-result.tsx +90 -0
  29. package/src/app/routes/_authenticated/_shipping-methods/components/test-single-shipping-method-sheet.tsx +56 -0
  30. package/src/app/routes/_authenticated/_shipping-methods/components/test-single-shipping-method.tsx +82 -0
  31. package/src/app/routes/_authenticated/_shipping-methods/components/use-shipping-method-test-state.ts +67 -0
  32. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +27 -0
  33. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +2 -2
  34. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +24 -4
  35. package/src/lib/components/shared/history-timeline/history-note-entry.tsx +65 -0
  36. package/src/lib/components/shared/history-timeline/history-timeline-with-grouping.tsx +141 -0
  37. package/src/lib/components/shared/history-timeline/use-history-note-editor.ts +26 -0
  38. package/src/lib/framework/extension-api/define-dashboard-extension.ts +5 -0
  39. package/src/lib/framework/extension-api/extension-api-types.ts +7 -0
  40. package/src/lib/framework/extension-api/logic/history-entries.ts +24 -0
  41. package/src/lib/framework/extension-api/logic/index.ts +1 -0
  42. package/src/lib/framework/extension-api/types/history-entries.ts +120 -0
  43. package/src/lib/framework/extension-api/types/index.ts +1 -0
  44. package/src/lib/framework/history-entry/history-entry-extensions.ts +11 -0
  45. package/src/lib/framework/history-entry/history-entry.tsx +129 -0
  46. package/src/lib/framework/registry/registry-types.ts +2 -0
  47. package/src/lib/index.ts +5 -1
  48. package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-method-dialog.tsx +0 -32
  49. package/src/lib/components/shared/history-timeline/history-entry.tsx +0 -188
@@ -0,0 +1,87 @@
1
+ import { Alert, AlertDescription } from '@/vdb/components/ui/alert.js';
2
+ import { Button } from '@/vdb/components/ui/button.js';
3
+ import { Card, CardContent, CardHeader, CardTitle } from '@/vdb/components/ui/card.js';
4
+ import { Trans } from '@/vdb/lib/trans.js';
5
+ import { PlayIcon } from 'lucide-react';
6
+ import React from 'react';
7
+
8
+ interface ShippingMethodTestResultWrapperProps {
9
+ okToRun: boolean;
10
+ testDataUpdated: boolean;
11
+ hasTestedOnce: boolean;
12
+ onRunTest: () => void;
13
+ loading?: boolean;
14
+ children: React.ReactNode;
15
+ emptyState?: React.ReactNode;
16
+ showEmptyState?: boolean;
17
+ runTestLabel?: React.ReactNode;
18
+ loadingLabel?: React.ReactNode;
19
+ }
20
+
21
+ export function ShippingMethodTestResultWrapper({
22
+ okToRun,
23
+ testDataUpdated,
24
+ hasTestedOnce,
25
+ onRunTest,
26
+ loading = false,
27
+ children,
28
+ emptyState,
29
+ showEmptyState = false,
30
+ runTestLabel = <Trans>Run Test</Trans>,
31
+ loadingLabel = <Trans>Testing shipping method...</Trans>,
32
+ }: Readonly<ShippingMethodTestResultWrapperProps>) {
33
+ const canRunTest = okToRun && testDataUpdated;
34
+ return (
35
+ <Card>
36
+ <CardHeader>
37
+ <CardTitle className="flex items-center justify-between">
38
+ <span>
39
+ <Trans>Test Results</Trans>
40
+ </span>
41
+ {okToRun && (
42
+ <Button
43
+ onClick={onRunTest}
44
+ disabled={!canRunTest || loading}
45
+ size="sm"
46
+ className="ml-auto"
47
+ >
48
+ <PlayIcon className="mr-1 h-4 w-4" />
49
+ {runTestLabel}
50
+ </Button>
51
+ )}
52
+ </CardTitle>
53
+ </CardHeader>
54
+ <CardContent>
55
+ {!okToRun && (
56
+ <Alert>
57
+ <AlertDescription>
58
+ <Trans>
59
+ Please add products and complete the shipping address to run the test.
60
+ </Trans>
61
+ </AlertDescription>
62
+ </Alert>
63
+ )}
64
+
65
+ {okToRun && testDataUpdated && hasTestedOnce && (
66
+ <Alert variant="destructive">
67
+ <AlertDescription>
68
+ <Trans>
69
+ Test data has been updated. Click "Run Test" to see updated results.
70
+ </Trans>
71
+ </AlertDescription>
72
+ </Alert>
73
+ )}
74
+
75
+ {loading && (
76
+ <div className="text-center py-8">
77
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
78
+ <p className="mt-2 text-sm text-muted-foreground">{loadingLabel}</p>
79
+ </div>
80
+ )}
81
+
82
+ {!loading && showEmptyState && emptyState}
83
+ {!loading && !showEmptyState && children}
84
+ </CardContent>
85
+ </Card>
86
+ );
87
+ }
@@ -0,0 +1,255 @@
1
+ import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
2
+ import { AccordionContent, AccordionItem, AccordionTrigger } from '@/vdb/components/ui/accordion.js';
3
+ import { Form } from '@/vdb/components/ui/form.js';
4
+ import { Input } from '@/vdb/components/ui/input.js';
5
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
6
+ import { api } from '@/vdb/graphql/api.js';
7
+ import { graphql } from '@/vdb/graphql/graphql.js';
8
+ import { Trans } from '@/vdb/lib/trans.js';
9
+ import { useQuery } from '@tanstack/react-query';
10
+ import { useEffect, useRef } from 'react';
11
+ import { useForm } from 'react-hook-form';
12
+
13
+ // Query document to fetch available countries
14
+ const getAvailableCountriesDocument = graphql(`
15
+ query GetAvailableCountries {
16
+ countries(options: { filter: { enabled: { eq: true } } }) {
17
+ items {
18
+ id
19
+ code
20
+ name
21
+ }
22
+ }
23
+ }
24
+ `);
25
+
26
+ export interface TestAddress {
27
+ fullName: string;
28
+ company?: string;
29
+ streetLine1: string;
30
+ streetLine2?: string;
31
+ city: string;
32
+ province: string;
33
+ postalCode: string;
34
+ countryCode: string;
35
+ phoneNumber?: string;
36
+ }
37
+
38
+ interface TestAddressFormProps {
39
+ onAddressChange: (address: TestAddress) => void;
40
+ }
41
+
42
+ export function TestAddressForm({ onAddressChange }: Readonly<TestAddressFormProps>) {
43
+ const form = useForm<TestAddress>({
44
+ defaultValues: (() => {
45
+ try {
46
+ const stored = localStorage.getItem('shippingTestAddress');
47
+ return stored
48
+ ? JSON.parse(stored)
49
+ : {
50
+ fullName: '',
51
+ company: '',
52
+ streetLine1: '',
53
+ streetLine2: '',
54
+ city: '',
55
+ province: '',
56
+ postalCode: '',
57
+ countryCode: '',
58
+ phoneNumber: '',
59
+ };
60
+ } catch {
61
+ return {
62
+ fullName: '',
63
+ company: '',
64
+ streetLine1: '',
65
+ streetLine2: '',
66
+ city: '',
67
+ province: '',
68
+ postalCode: '',
69
+ countryCode: '',
70
+ phoneNumber: '',
71
+ };
72
+ }
73
+ })(),
74
+ });
75
+
76
+ // Fetch available countries
77
+ const { data: countriesData, isLoading: isLoadingCountries } = useQuery({
78
+ queryKey: ['availableCountries'],
79
+ queryFn: () => api.query(getAvailableCountriesDocument),
80
+ staleTime: 1000 * 60 * 60 * 24, // 24 hours
81
+ });
82
+
83
+ const previousValuesRef = useRef<string>('');
84
+
85
+ // Use form subscription instead of watch() to avoid infinite loops
86
+ useEffect(() => {
87
+ const subscription = form.watch(value => {
88
+ const currentValueString = JSON.stringify(value);
89
+
90
+ // Only update if values actually changed
91
+ if (currentValueString !== previousValuesRef.current) {
92
+ previousValuesRef.current = currentValueString;
93
+
94
+ try {
95
+ localStorage.setItem('shippingTestAddress', currentValueString);
96
+ } catch {
97
+ // Ignore localStorage errors
98
+ }
99
+
100
+ if (value) {
101
+ onAddressChange(value as TestAddress);
102
+ }
103
+ }
104
+ });
105
+
106
+ return () => subscription.unsubscribe();
107
+ }, [form, onAddressChange]);
108
+
109
+ useEffect(() => {
110
+ const initialAddress = form.getValues();
111
+ onAddressChange(initialAddress);
112
+ }, []);
113
+
114
+ const currentValues = form.getValues();
115
+
116
+ const getAddressSummary = () => {
117
+ const parts = [
118
+ currentValues.fullName,
119
+ currentValues.streetLine1,
120
+ currentValues.city,
121
+ currentValues.province,
122
+ currentValues.postalCode,
123
+ currentValues.countryCode,
124
+ ].filter(Boolean);
125
+ return parts.length > 0 ? parts.join(', ') : '';
126
+ };
127
+
128
+ const isComplete = !!(
129
+ currentValues.fullName &&
130
+ currentValues.streetLine1 &&
131
+ currentValues.city &&
132
+ currentValues.province &&
133
+ currentValues.postalCode &&
134
+ currentValues.countryCode
135
+ );
136
+
137
+ return (
138
+ <AccordionItem value="shipping-address">
139
+ <AccordionTrigger>
140
+ <div className="flex items-center justify-between w-full pr-2">
141
+ <span>
142
+ <Trans>Shipping Address</Trans>
143
+ </span>
144
+ {isComplete && (
145
+ <span className="text-sm text-muted-foreground truncate max-w-md">
146
+ {getAddressSummary()}
147
+ </span>
148
+ )}
149
+ </div>
150
+ </AccordionTrigger>
151
+ <AccordionContent className="px-2">
152
+ <Form {...form}>
153
+ <div className="space-y-4">
154
+ <div className="grid grid-cols-2 gap-4">
155
+ <FormFieldWrapper
156
+ control={form.control}
157
+ name="fullName"
158
+ label={<Trans>Full Name</Trans>}
159
+ render={({ field }) => <Input {...field} placeholder="John Smith" />}
160
+ />
161
+ <FormFieldWrapper
162
+ control={form.control}
163
+ name="company"
164
+ label={<Trans>Company</Trans>}
165
+ render={({ field }) => (
166
+ <Input {...field} value={field.value || ''} placeholder="Company name" />
167
+ )}
168
+ />
169
+ </div>
170
+
171
+ <FormFieldWrapper
172
+ control={form.control}
173
+ name="streetLine1"
174
+ label={<Trans>Street Address</Trans>}
175
+ render={({ field }) => <Input {...field} placeholder="123 Main Street" />}
176
+ />
177
+
178
+ <FormFieldWrapper
179
+ control={form.control}
180
+ name="streetLine2"
181
+ label={<Trans>Street Address 2</Trans>}
182
+ render={({ field }) => (
183
+ <Input
184
+ {...field}
185
+ value={field.value || ''}
186
+ placeholder="Apartment, suite, etc."
187
+ />
188
+ )}
189
+ />
190
+
191
+ <div className="grid grid-cols-3 gap-4">
192
+ <FormFieldWrapper
193
+ control={form.control}
194
+ name="city"
195
+ label={<Trans>City</Trans>}
196
+ render={({ field }) => <Input {...field} placeholder="New York" />}
197
+ />
198
+ <FormFieldWrapper
199
+ control={form.control}
200
+ name="province"
201
+ label={<Trans>State / Province</Trans>}
202
+ render={({ field }) => <Input {...field} placeholder="NY" />}
203
+ />
204
+ <FormFieldWrapper
205
+ control={form.control}
206
+ name="postalCode"
207
+ label={<Trans>Postal Code</Trans>}
208
+ render={({ field }) => <Input {...field} placeholder="10001" />}
209
+ />
210
+ </div>
211
+
212
+ <div className="grid grid-cols-2 gap-4">
213
+ <FormFieldWrapper
214
+ control={form.control}
215
+ name="countryCode"
216
+ label={<Trans>Country</Trans>}
217
+ renderFormControl={false}
218
+ render={({ field }) => (
219
+ <Select
220
+ onValueChange={field.onChange}
221
+ value={field.value}
222
+ disabled={isLoadingCountries}
223
+ >
224
+ <SelectTrigger>
225
+ <SelectValue placeholder="Select a country" />
226
+ </SelectTrigger>
227
+ <SelectContent>
228
+ {countriesData?.countries.items.map(country => (
229
+ <SelectItem key={country.code} value={country.code}>
230
+ {country.name}
231
+ </SelectItem>
232
+ ))}
233
+ </SelectContent>
234
+ </Select>
235
+ )}
236
+ />
237
+ <FormFieldWrapper
238
+ control={form.control}
239
+ name="phoneNumber"
240
+ label={<Trans>Phone Number</Trans>}
241
+ render={({ field }) => (
242
+ <Input
243
+ {...field}
244
+ value={field.value || ''}
245
+ placeholder="+1 (555) 123-4567"
246
+ />
247
+ )}
248
+ />
249
+ </div>
250
+ </div>
251
+ </Form>
252
+ </AccordionContent>
253
+ </AccordionItem>
254
+ );
255
+ }
@@ -0,0 +1,243 @@
1
+ import {
2
+ ProductVariantSelector,
3
+ ProductVariantSelectorProps,
4
+ } from '@/vdb/components/shared/product-variant-selector.js';
5
+ import { AssetLike, VendureImage } from '@/vdb/components/shared/vendure-image.js';
6
+ import { AccordionContent, AccordionItem, AccordionTrigger } from '@/vdb/components/ui/accordion.js';
7
+ import { Button } from '@/vdb/components/ui/button.js';
8
+ import { Input } from '@/vdb/components/ui/input.js';
9
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/vdb/components/ui/table.js';
10
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
11
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
12
+ import { Trans } from '@/vdb/lib/trans.js';
13
+ import {
14
+ ColumnDef,
15
+ flexRender,
16
+ getCoreRowModel,
17
+ useReactTable,
18
+ VisibilityState,
19
+ } from '@tanstack/react-table';
20
+ import { Trash2 } from 'lucide-react';
21
+ import { useEffect, useState } from 'react';
22
+
23
+ export interface TestOrderLine {
24
+ id: string;
25
+ name: string;
26
+ featuredAsset?: AssetLike;
27
+ sku: string;
28
+ unitPriceWithTax: number;
29
+ quantity: number;
30
+ }
31
+
32
+ interface TestOrderBuilderProps {
33
+ onOrderLinesChange: (lines: TestOrderLine[]) => void;
34
+ }
35
+
36
+ export function TestOrderBuilder({ onOrderLinesChange }: Readonly<TestOrderBuilderProps>) {
37
+ const { formatCurrency } = useLocalFormat();
38
+ const { activeChannel } = useChannel();
39
+ const [lines, setLines] = useState<TestOrderLine[]>(() => {
40
+ try {
41
+ const stored = localStorage.getItem('shippingTestOrder');
42
+ return stored ? JSON.parse(stored) : [];
43
+ } catch {
44
+ return [];
45
+ }
46
+ });
47
+ const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
48
+
49
+ const currencyCode = activeChannel?.defaultCurrencyCode ?? 'USD';
50
+ const subTotal = lines.reduce((sum, l) => sum + l.unitPriceWithTax * l.quantity, 0);
51
+
52
+ useEffect(() => {
53
+ try {
54
+ localStorage.setItem('shippingTestOrder', JSON.stringify(lines));
55
+ } catch {
56
+ // Ignore localStorage errors
57
+ }
58
+ onOrderLinesChange(lines);
59
+ }, [lines, onOrderLinesChange]);
60
+
61
+ const addProduct = (product: Parameters<ProductVariantSelectorProps['onProductVariantSelect']>[0]) => {
62
+ if (!lines.find(l => l.id === product.productVariantId)) {
63
+ const newLine: TestOrderLine = {
64
+ id: product.productVariantId,
65
+ name: product.productVariantName,
66
+ featuredAsset: product.productAsset ?? undefined,
67
+ quantity: 1,
68
+ sku: product.sku,
69
+ unitPriceWithTax: product.priceWithTax || 0,
70
+ };
71
+ setLines(prev => [...prev, newLine]);
72
+ }
73
+ };
74
+
75
+ const updateQuantity = (lineId: string, newQuantity: number) => {
76
+ if (newQuantity <= 0) {
77
+ removeLine(lineId);
78
+ return;
79
+ }
80
+ setLines(prev => prev.map(line => (line.id === lineId ? { ...line, quantity: newQuantity } : line)));
81
+ };
82
+
83
+ const removeLine = (lineId: string) => {
84
+ setLines(prev => prev.filter(line => line.id !== lineId));
85
+ };
86
+
87
+ const columns: ColumnDef<TestOrderLine>[] = [
88
+ {
89
+ header: 'Image',
90
+ accessorKey: 'preview',
91
+ cell: ({ row }) => {
92
+ const asset = row.original.featuredAsset ?? null;
93
+ return <VendureImage asset={asset} preset="tiny" />;
94
+ },
95
+ },
96
+ {
97
+ header: 'Product',
98
+ accessorKey: 'name',
99
+ },
100
+ {
101
+ header: 'SKU',
102
+ accessorKey: 'sku',
103
+ },
104
+ {
105
+ header: 'Unit price',
106
+ accessorKey: 'unitPriceWithTax',
107
+ cell: ({ row }) => {
108
+ return formatCurrency(row.original.unitPriceWithTax, currencyCode);
109
+ },
110
+ },
111
+ {
112
+ header: 'Quantity',
113
+ accessorKey: 'quantity',
114
+ cell: ({ row }) => {
115
+ return (
116
+ <div className="flex gap-2 items-center">
117
+ <Input
118
+ type="number"
119
+ min="1"
120
+ value={row.original.quantity}
121
+ onChange={e => updateQuantity(row.original.id, parseInt(e.target.value) || 1)}
122
+ className="w-16"
123
+ />
124
+ <Button
125
+ variant="outline"
126
+ type="button"
127
+ size="icon"
128
+ onClick={() => removeLine(row.original.id)}
129
+ className="h-8 w-8"
130
+ >
131
+ <Trash2 className="h-4 w-4" />
132
+ </Button>
133
+ </div>
134
+ );
135
+ },
136
+ },
137
+ {
138
+ header: 'Total',
139
+ accessorKey: 'total',
140
+ cell: ({ row }) => {
141
+ const total = row.original.unitPriceWithTax * row.original.quantity;
142
+ return formatCurrency(total, currencyCode);
143
+ },
144
+ },
145
+ ];
146
+
147
+ const table = useReactTable({
148
+ data: lines,
149
+ columns,
150
+ getCoreRowModel: getCoreRowModel(),
151
+ rowCount: lines.length,
152
+ onColumnVisibilityChange: setColumnVisibility,
153
+ state: {
154
+ columnVisibility,
155
+ },
156
+ });
157
+
158
+ return (
159
+ <AccordionItem value="test-order">
160
+ <AccordionTrigger>
161
+ <div className="flex items-center justify-between w-full pr-2">
162
+ <span>
163
+ <Trans>Test Order</Trans>
164
+ </span>
165
+ {lines.length > 0 && (
166
+ <span className="text-sm text-muted-foreground">
167
+ {lines.length} item{lines.length !== 1 ? 's' : ''} •{' '}
168
+ {formatCurrency(subTotal, currencyCode)}
169
+ </span>
170
+ )}
171
+ </div>
172
+ </AccordionTrigger>
173
+ <AccordionContent className="space-y-4 px-2">
174
+ {lines.length > 0 ? (
175
+ <div className="w-full">
176
+ <Table>
177
+ <TableHeader>
178
+ {table.getHeaderGroups().map(headerGroup => (
179
+ <TableRow key={headerGroup.id}>
180
+ {headerGroup.headers.map(header => {
181
+ return (
182
+ <TableHead key={header.id}>
183
+ {header.isPlaceholder
184
+ ? null
185
+ : flexRender(
186
+ header.column.columnDef.header,
187
+ header.getContext(),
188
+ )}
189
+ </TableHead>
190
+ );
191
+ })}
192
+ </TableRow>
193
+ ))}
194
+ </TableHeader>
195
+ <TableBody>
196
+ {table.getRowModel().rows.map(row => (
197
+ <TableRow key={row.id}>
198
+ {row.getVisibleCells().map(cell => (
199
+ <TableCell key={cell.id}>
200
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
201
+ </TableCell>
202
+ ))}
203
+ </TableRow>
204
+ ))}
205
+ <TableRow>
206
+ <TableCell colSpan={columns.length} className="h-12">
207
+ <div className="my-4 flex justify-center">
208
+ <div className="max-w-lg">
209
+ <ProductVariantSelector onProductVariantSelect={addProduct} />
210
+ </div>
211
+ </div>
212
+ </TableCell>
213
+ </TableRow>
214
+ <TableRow>
215
+ <TableCell
216
+ colSpan={columns.length - 1}
217
+ className="text-right font-medium"
218
+ >
219
+ <Trans>Subtotal</Trans>
220
+ </TableCell>
221
+ <TableCell className="font-medium">
222
+ {formatCurrency(subTotal, currencyCode)}
223
+ </TableCell>
224
+ </TableRow>
225
+ </TableBody>
226
+ </Table>
227
+ </div>
228
+ ) : (
229
+ <div className="space-y-4">
230
+ <div className="text-center py-8 text-muted-foreground">
231
+ <Trans>Add products to create a test order</Trans>
232
+ </div>
233
+ <div className="flex justify-center">
234
+ <div className="max-w-lg">
235
+ <ProductVariantSelector onProductVariantSelect={addProduct} />
236
+ </div>
237
+ </div>
238
+ </div>
239
+ )}
240
+ </AccordionContent>
241
+ </AccordionItem>
242
+ );
243
+ }
@@ -0,0 +1,97 @@
1
+ import { Badge } from '@/vdb/components/ui/badge.js';
2
+ import { useChannel } from '@/vdb/hooks/use-channel.js';
3
+ import { Trans } from '@/vdb/lib/trans.js';
4
+ import { Check } from 'lucide-react';
5
+ import { MetadataBadges } from './metadata-badges.js';
6
+ import { PriceDisplay } from './price-display.js';
7
+ import { ShippingMethodTestResultWrapper } from './shipping-method-test-result-wrapper.js';
8
+
9
+ export interface ShippingMethodQuote {
10
+ id: string;
11
+ name: string;
12
+ code: string;
13
+ description: string;
14
+ price: number;
15
+ priceWithTax: number;
16
+ metadata?: Record<string, any>;
17
+ }
18
+
19
+ interface ShippingEligibilityTestResultProps {
20
+ testResult?: ShippingMethodQuote[];
21
+ okToRun: boolean;
22
+ testDataUpdated: boolean;
23
+ hasTestedOnce: boolean;
24
+ onRunTest: () => void;
25
+ loading?: boolean;
26
+ }
27
+
28
+ export function TestShippingMethodsResult({
29
+ testResult,
30
+ okToRun,
31
+ testDataUpdated,
32
+ hasTestedOnce,
33
+ onRunTest,
34
+ loading = false,
35
+ }: Readonly<ShippingEligibilityTestResultProps>) {
36
+ const { activeChannel } = useChannel();
37
+ const currencyCode = activeChannel?.defaultCurrencyCode ?? 'USD';
38
+ const hasResults = testResult && testResult.length > 0;
39
+ const showEmptyState = testResult && testResult.length === 0 && !loading && !testDataUpdated;
40
+
41
+ return (
42
+ <ShippingMethodTestResultWrapper
43
+ okToRun={okToRun}
44
+ testDataUpdated={testDataUpdated}
45
+ hasTestedOnce={hasTestedOnce}
46
+ onRunTest={onRunTest}
47
+ loading={loading}
48
+ showEmptyState={showEmptyState}
49
+ emptyState={
50
+ <div className="text-center py-8">
51
+ <p className="text-muted-foreground">
52
+ <Trans>No eligible shipping methods found for this order.</Trans>
53
+ </p>
54
+ </div>
55
+ }
56
+ loadingLabel={<Trans>Testing shipping methods...</Trans>}
57
+ >
58
+ {hasResults && (
59
+ <div className="space-y-3">
60
+ <div className="flex items-center gap-2 mb-4">
61
+ <span className="text-sm font-medium">
62
+ <Trans>
63
+ Found {testResult.length} eligible shipping method
64
+ {testResult.length !== 1 ? 's' : ''}
65
+ </Trans>
66
+ </span>
67
+ </div>
68
+ {testResult.map(method => (
69
+ <div
70
+ key={method.id}
71
+ className="flex items-center text-sm justify-between p-3 rounded-lg bg-muted/50"
72
+ >
73
+ <div className="flex-1">
74
+ <div className="flex gap-1">
75
+ <Check className="h-5 w-5 text-success" />
76
+ <div className="">{method.name}</div>
77
+ </div>
78
+ <Badge variant="secondary">{method.code}</Badge>
79
+ {method.description && (
80
+ <div className="text-sm text-muted-foreground mt-1">
81
+ {method.description}
82
+ </div>
83
+ )}
84
+ <MetadataBadges metadata={method.metadata} />
85
+ </div>
86
+ <PriceDisplay
87
+ price={method.price}
88
+ priceWithTax={method.priceWithTax}
89
+ currencyCode={currencyCode}
90
+ />
91
+ </div>
92
+ ))}
93
+ </div>
94
+ )}
95
+ </ShippingMethodTestResultWrapper>
96
+ );
97
+ }