@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.
- package/index.html +11 -12
- package/package.json +4 -4
- package/src/app/common/use-page-title.test.ts +263 -0
- package/src/app/common/use-page-title.ts +86 -0
- package/src/app/routes/__root.tsx +4 -4
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-container.tsx +2 -2
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-types.ts +5 -0
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-utils.tsx +124 -0
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history.tsx +91 -59
- package/src/app/routes/_authenticated/_customers/components/customer-history/default-customer-history-components.tsx +176 -0
- package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +4 -2
- package/src/app/routes/_authenticated/_customers/customers.graphql.ts +2 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/default-order-history-components.tsx +98 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-container.tsx +9 -7
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-types.ts +5 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +173 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history.tsx +64 -408
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +4 -0
- package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +9 -4
- package/src/app/routes/_authenticated/_shipping-methods/components/metadata-badges.tsx +15 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/price-display.tsx +21 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-method-test-result-wrapper.tsx +87 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-address-form.tsx +255 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-order-builder.tsx +243 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-result.tsx +97 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-sheet.tsx +41 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods.tsx +74 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-single-method-result.tsx +90 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-single-shipping-method-sheet.tsx +56 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/test-single-shipping-method.tsx +82 -0
- package/src/app/routes/_authenticated/_shipping-methods/components/use-shipping-method-test-state.ts +67 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.graphql.ts +27 -0
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods.tsx +2 -2
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +24 -4
- package/src/lib/components/shared/history-timeline/history-note-entry.tsx +65 -0
- package/src/lib/components/shared/history-timeline/history-timeline-with-grouping.tsx +141 -0
- package/src/lib/components/shared/history-timeline/use-history-note-editor.ts +26 -0
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +5 -0
- package/src/lib/framework/extension-api/extension-api-types.ts +7 -0
- package/src/lib/framework/extension-api/logic/history-entries.ts +24 -0
- package/src/lib/framework/extension-api/logic/index.ts +1 -0
- package/src/lib/framework/extension-api/types/history-entries.ts +120 -0
- package/src/lib/framework/extension-api/types/index.ts +1 -0
- package/src/lib/framework/history-entry/history-entry-extensions.ts +11 -0
- package/src/lib/framework/history-entry/history-entry.tsx +129 -0
- package/src/lib/framework/registry/registry-types.ts +2 -0
- package/src/lib/index.ts +5 -1
- package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-method-dialog.tsx +0 -32
- 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
|
+
}
|
package/src/app/routes/_authenticated/_shipping-methods/components/test-shipping-methods-result.tsx
ADDED
|
@@ -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
|
+
}
|