@vendure/dashboard 3.3.6-master-202507031258 → 3.3.6-master-202507040234

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.
@@ -32,7 +32,7 @@ export function vendureDashboardPlugin(options) {
32
32
  : [
33
33
  TanStackRouterVite({
34
34
  autoCodeSplitting: true,
35
- routeFileIgnorePattern: '.graphql.ts|components|hooks',
35
+ routeFileIgnorePattern: '.graphql.ts|components|hooks|utils',
36
36
  routesDirectory: path.join(packageRoot, 'src/app/routes'),
37
37
  generatedRouteTree: path.join(packageRoot, 'src/app/routeTree.gen.ts'),
38
38
  }),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.3.6-master-202507031258",
4
+ "version": "3.3.6-master-202507040234",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -86,8 +86,8 @@
86
86
  "@types/react-dom": "^19.0.4",
87
87
  "@types/react-grid-layout": "^1.3.5",
88
88
  "@uidotdev/usehooks": "^2.4.1",
89
- "@vendure/common": "^3.3.6-master-202507031258",
90
- "@vendure/core": "^3.3.6-master-202507031258",
89
+ "@vendure/common": "^3.3.6-master-202507040234",
90
+ "@vendure/core": "^3.3.6-master-202507040234",
91
91
  "@vitejs/plugin-react": "^4.3.4",
92
92
  "awesome-graphql-client": "^2.1.0",
93
93
  "class-variance-authority": "^0.7.1",
@@ -130,5 +130,5 @@
130
130
  "lightningcss-linux-arm64-musl": "^1.29.3",
131
131
  "lightningcss-linux-x64-musl": "^1.29.1"
132
132
  },
133
- "gitHead": "6033754f1547a7a1f9c49b1b97c645eda8405973"
133
+ "gitHead": "6634367c8f5aeb5c25502c51c15686d1d4fa807c"
134
134
  }
@@ -0,0 +1,191 @@
1
+ import {
2
+ RelationSelector,
3
+ createRelationSelectorConfig,
4
+ } from '@/vdb/components/data-input/relation-selector.js';
5
+ import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
6
+ import { Button } from '@/vdb/components/ui/button.js';
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogFooter,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from '@/vdb/components/ui/dialog.js';
15
+ import { Form } from '@/vdb/components/ui/form.js';
16
+ import { Input } from '@/vdb/components/ui/input.js';
17
+ import { api } from '@/vdb/graphql/api.js';
18
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
19
+ import { Trans, useLingui } from '@/vdb/lib/trans.js';
20
+ import { useMutation } from '@tanstack/react-query';
21
+ import { useState } from 'react';
22
+ import { useForm } from 'react-hook-form';
23
+ import { toast } from 'sonner';
24
+ import {
25
+ addManualPaymentToOrderDocument,
26
+ paymentMethodsDocument
27
+ } from '../orders.graphql.js';
28
+ import { Order } from '../utils/order-types.js';
29
+ import { calculateOutstandingPaymentAmount } from '../utils/order-utils.js';
30
+
31
+ interface AddManualPaymentDialogProps {
32
+ order: Order;
33
+ onSuccess?: () => void;
34
+ }
35
+
36
+ interface FormData {
37
+ method: string;
38
+ transactionId: string;
39
+ }
40
+
41
+ export function AddManualPaymentDialog({ order, onSuccess }: Readonly<AddManualPaymentDialogProps>) {
42
+ const { i18n } = useLingui();
43
+ const { formatCurrency } = useLocalFormat();
44
+ const [isSubmitting, setIsSubmitting] = useState(false);
45
+ const [open, setOpen] = useState(false);
46
+
47
+ const addManualPaymentMutation = useMutation({
48
+ mutationFn: api.mutate(addManualPaymentToOrderDocument),
49
+ onSuccess: (result: any) => {
50
+ const { addManualPaymentToOrder } = result;
51
+ if (addManualPaymentToOrder.__typename === 'Order') {
52
+ toast(i18n.t('Successfully added payment to order'));
53
+ onSuccess?.();
54
+ } else {
55
+ toast(i18n.t('Failed to add payment'), {
56
+ description: addManualPaymentToOrder.message,
57
+ });
58
+ }
59
+ },
60
+ onError: error => {
61
+ toast(i18n.t('Failed to add payment'), {
62
+ description: error instanceof Error ? error.message : 'Unknown error',
63
+ });
64
+ },
65
+ });
66
+
67
+ const form = useForm<FormData>({
68
+ defaultValues: {
69
+ method: '',
70
+ transactionId: '',
71
+ },
72
+ });
73
+ const method = form.watch('method');
74
+
75
+ const handleSubmit = async (data: FormData) => {
76
+ setIsSubmitting(true);
77
+ try {
78
+ addManualPaymentMutation.mutate({
79
+ input: {
80
+ orderId: order.id,
81
+ method: data.method,
82
+ transactionId: data.transactionId,
83
+ metadata: {},
84
+ },
85
+ });
86
+ setOpen(false);
87
+ form.reset();
88
+ } catch (error) {
89
+ toast(i18n.t('Failed to add payment'), {
90
+ description: error instanceof Error ? error.message : 'Unknown error',
91
+ });
92
+ } finally {
93
+ setIsSubmitting(false);
94
+ }
95
+ };
96
+
97
+ const handleCancel = () => {
98
+ form.reset();
99
+ setOpen(false);
100
+ };
101
+
102
+ const outstandingAmount = calculateOutstandingPaymentAmount(order);
103
+ const currencyCode = order.currencyCode;
104
+
105
+ // Create relation selector config for payment methods
106
+ const paymentMethodSelectorConfig = createRelationSelectorConfig({
107
+ listQuery: paymentMethodsDocument,
108
+ idKey: 'code',
109
+ labelKey: 'name',
110
+ placeholder: i18n.t('Search payment methods...'),
111
+ multiple: false,
112
+ label: (method: any) => `${method.name} (${method.code})`,
113
+ });
114
+
115
+ return (
116
+ <>
117
+ <Button
118
+ onClick={e => {
119
+ e.stopPropagation();
120
+ setOpen(true);
121
+ }}
122
+ className="mr-2"
123
+ >
124
+ <Trans>Add payment to order ({formatCurrency(outstandingAmount, currencyCode)})</Trans>
125
+ </Button>
126
+ <Dialog open={open}>
127
+ <DialogContent className="sm:max-w-[500px]">
128
+ <DialogHeader>
129
+ <DialogTitle>
130
+ <Trans>Add payment to order</Trans>
131
+ </DialogTitle>
132
+ <DialogDescription>
133
+ <Trans>
134
+ Add a manual payment of {formatCurrency(outstandingAmount, currencyCode)}
135
+ </Trans>
136
+ </DialogDescription>
137
+ </DialogHeader>
138
+ <Form {...form}>
139
+ <form onSubmit={e => {
140
+ e.stopPropagation();
141
+ form.handleSubmit(handleSubmit)(e);
142
+ }} className="space-y-4">
143
+ <FormFieldWrapper
144
+ control={form.control}
145
+ name="method"
146
+ label={<Trans>Payment method</Trans>}
147
+ rules={{ required: i18n.t('Payment method is required') }}
148
+ render={({ field }) => (
149
+ <RelationSelector
150
+ config={paymentMethodSelectorConfig}
151
+ value={field.value}
152
+ onChange={field.onChange}
153
+ disabled={isSubmitting}
154
+ />
155
+ )}
156
+ />
157
+ <FormFieldWrapper
158
+ control={form.control}
159
+ name="transactionId"
160
+ label={<Trans>Transaction ID</Trans>}
161
+ rules={{ required: i18n.t('Transaction ID is required') }}
162
+ render={({ field }) => (
163
+ <Input {...field} placeholder={i18n.t('Enter transaction ID')} />
164
+ )}
165
+ />
166
+ <DialogFooter>
167
+ <Button type="button" variant="outline" onClick={handleCancel}>
168
+ <Trans>Cancel</Trans>
169
+ </Button>
170
+ <Button
171
+ type="submit"
172
+ disabled={
173
+ !form.formState.isValid || isSubmitting || !method
174
+ }
175
+ >
176
+ {isSubmitting ? (
177
+ <Trans>Adding...</Trans>
178
+ ) : (
179
+ <Trans>
180
+ Add payment ({formatCurrency(outstandingAmount, currencyCode)})
181
+ </Trans>
182
+ )}
183
+ </Button>
184
+ </DialogFooter>
185
+ </form>
186
+ </Form>
187
+ </DialogContent>
188
+ </Dialog>
189
+ </>
190
+ );
191
+ }
@@ -54,7 +54,7 @@ export function EditOrderTable({
54
54
  onApplyCouponCode,
55
55
  onRemoveCouponCode,
56
56
  orderLineForm,
57
- }: OrderTableProps) {
57
+ }: Readonly<OrderTableProps>) {
58
58
  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
59
59
  const [couponCode, setCouponCode] = useState('');
60
60
 
@@ -130,7 +130,7 @@ export function EditOrderTable({
130
130
  {
131
131
  header: 'Total',
132
132
  accessorKey: 'linePriceWithTax',
133
- cell: ({ cell, row }) => {
133
+ cell: ({ row }) => {
134
134
  const value = row.original.linePriceWithTax;
135
135
  const netValue = row.original.linePrice;
136
136
  return <MoneyGrossNet priceWithTax={value} price={netValue} currencyCode={currencyCode} />;
@@ -0,0 +1,320 @@
1
+ import { ConfigurableOperationInput } from '@/vdb/components/shared/configurable-operation-input.js';
2
+ import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
3
+ import { Button } from '@/vdb/components/ui/button.js';
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from '@/vdb/components/ui/dialog.js';
12
+ import { Form } from '@/vdb/components/ui/form.js';
13
+ import { Input } from '@/vdb/components/ui/input.js';
14
+ import { Label } from '@/vdb/components/ui/label.js';
15
+ import { api } from '@/vdb/graphql/api.js';
16
+ import { graphql } from '@/vdb/graphql/graphql.js';
17
+ import { Trans, useLingui } from '@/vdb/lib/trans.js';
18
+ import { useMutation, useQuery } from '@tanstack/react-query';
19
+ import { ConfigurableOperationInput as ConfigurableOperationInputType } from '@vendure/common/lib/generated-types';
20
+ import { useState } from 'react';
21
+ import { useForm } from 'react-hook-form';
22
+ import { toast } from 'sonner';
23
+ import { fulfillmentHandlersDocument, fulfillOrderDocument } from '../orders.graphql.js';
24
+ import { Order } from '../utils/order-types.js';
25
+
26
+ interface FulfillOrderDialogProps {
27
+ order: Order;
28
+ onSuccess?: () => void;
29
+ }
30
+
31
+ interface FormData {
32
+ handler: ConfigurableOperationInputType;
33
+ }
34
+
35
+ interface FulfillmentQuantity {
36
+ fulfillCount: number;
37
+ max: number;
38
+ }
39
+
40
+ export function FulfillOrderDialog({ order, onSuccess }: Readonly<FulfillOrderDialogProps>) {
41
+ const { i18n } = useLingui();
42
+ const [isSubmitting, setIsSubmitting] = useState(false);
43
+ const [open, setOpen] = useState(false);
44
+ const [fulfillmentQuantities, setFulfillmentQuantities] = useState<{
45
+ [lineId: string]: FulfillmentQuantity;
46
+ }>({});
47
+
48
+ // Get fulfillment handlers
49
+ const { data: fulfillmentHandlersData } = useQuery({
50
+ queryKey: ['fulfillmentHandlers'],
51
+ queryFn: () => api.query(fulfillmentHandlersDocument),
52
+ staleTime: 1000 * 60 * 60 * 5,
53
+ });
54
+
55
+ // Get global settings for inventory tracking
56
+ const { data: globalSettingsData } = useQuery({
57
+ queryKey: ['globalSettings'],
58
+ queryFn: () =>
59
+ api.query(
60
+ graphql(`
61
+ query GetGlobalSettings {
62
+ globalSettings {
63
+ trackInventory
64
+ }
65
+ }
66
+ `),
67
+ ),
68
+ staleTime: 1000 * 60 * 60 * 5,
69
+ });
70
+
71
+ const fulfillOrderMutation = useMutation({
72
+ mutationFn: api.mutate(fulfillOrderDocument),
73
+ onSuccess: (result: any) => {
74
+ const { addFulfillmentToOrder } = result;
75
+ if (addFulfillmentToOrder.__typename === 'Fulfillment') {
76
+ toast(i18n.t('Successfully fulfilled order'));
77
+ onSuccess?.();
78
+ } else {
79
+ toast(i18n.t('Failed to fulfill order'), {
80
+ description: addFulfillmentToOrder.message,
81
+ });
82
+ }
83
+ },
84
+ onError: error => {
85
+ toast(i18n.t('Failed to fulfill order'), {
86
+ description: error instanceof Error ? error.message : 'Unknown error',
87
+ });
88
+ },
89
+ });
90
+
91
+ const form = useForm<FormData>({
92
+ defaultValues: {
93
+ handler: {
94
+ code: '',
95
+ arguments: [],
96
+ },
97
+ },
98
+ });
99
+
100
+ // Initialize fulfillment quantities when dialog opens
101
+ const initializeFulfillmentQuantities = () => {
102
+ if (!globalSettingsData?.globalSettings) return;
103
+
104
+ const quantities: { [lineId: string]: FulfillmentQuantity } = {};
105
+ order.lines.forEach(line => {
106
+ const fulfillCount = getFulfillableCount(line, globalSettingsData.globalSettings.trackInventory);
107
+ quantities[line.id] = { fulfillCount, max: fulfillCount };
108
+ });
109
+ setFulfillmentQuantities(quantities);
110
+
111
+ // Set default fulfillment handler
112
+ const defaultHandler =
113
+ fulfillmentHandlersData?.fulfillmentHandlers.find(
114
+ h => h.code === order.shippingLines[0]?.shippingMethod?.fulfillmentHandlerCode,
115
+ ) ?? fulfillmentHandlersData?.fulfillmentHandlers[0];
116
+
117
+ if (defaultHandler) {
118
+ form.setValue('handler', {
119
+ code: defaultHandler.code,
120
+ arguments: defaultHandler.args.map(arg => ({
121
+ name: arg.name,
122
+ value: arg.defaultValue ?? '',
123
+ })),
124
+ });
125
+ }
126
+ };
127
+
128
+ const getFulfillableCount = (line: Order['lines'][number], globalTrackInventory: boolean): number => {
129
+ const { trackInventory, stockOnHand } = line.productVariant;
130
+ const effectiveTrackInventory =
131
+ trackInventory === 'INHERIT' ? globalTrackInventory : trackInventory === 'TRUE';
132
+
133
+ const unfulfilledCount = getUnfulfilledCount(line);
134
+ return effectiveTrackInventory ? Math.min(unfulfilledCount, stockOnHand) : unfulfilledCount;
135
+ };
136
+
137
+ const getUnfulfilledCount = (line: Order['lines'][number]): number => {
138
+ const fulfilled =
139
+ order.fulfillments
140
+ ?.filter(f => f.state !== 'Cancelled')
141
+ .map(f => f.lines)
142
+ .flat()
143
+ .filter(row => row.orderLineId === line.id)
144
+ .reduce((sum, row) => sum + row.quantity, 0) ?? 0;
145
+ return line.quantity - fulfilled;
146
+ };
147
+
148
+ const updateFulfillmentQuantity = (lineId: string, fulfillCount: number) => {
149
+ setFulfillmentQuantities(prev => ({
150
+ ...prev,
151
+ [lineId]: { ...prev[lineId], fulfillCount },
152
+ }));
153
+ };
154
+
155
+ const canSubmit = (): boolean => {
156
+ const totalCount = Object.values(fulfillmentQuantities).reduce(
157
+ (total, { fulfillCount }) => total + fulfillCount,
158
+ 0,
159
+ );
160
+ const fulfillmentQuantityIsValid = Object.values(fulfillmentQuantities).every(
161
+ ({ fulfillCount, max }) => fulfillCount <= max && fulfillCount >= 0,
162
+ );
163
+ const formIsValid = form.formState.isValid;
164
+ return formIsValid && totalCount > 0 && fulfillmentQuantityIsValid;
165
+ };
166
+
167
+ const handleSubmit = async (data: FormData) => {
168
+ setIsSubmitting(true);
169
+ try {
170
+ const lines = Object.entries(fulfillmentQuantities)
171
+ .filter(([, { fulfillCount }]) => fulfillCount > 0)
172
+ .map(([orderLineId, { fulfillCount }]) => ({
173
+ orderLineId,
174
+ quantity: fulfillCount,
175
+ }));
176
+
177
+ fulfillOrderMutation.mutate({
178
+ input: {
179
+ lines,
180
+ handler: data.handler,
181
+ },
182
+ });
183
+ setOpen(false);
184
+ form.reset();
185
+ setFulfillmentQuantities({});
186
+ } catch (error) {
187
+ toast(i18n.t('Failed to fulfill order'), {
188
+ description: error instanceof Error ? error.message : 'Unknown error',
189
+ });
190
+ } finally {
191
+ setIsSubmitting(false);
192
+ }
193
+ };
194
+
195
+ const handleCancel = () => {
196
+ form.reset();
197
+ setFulfillmentQuantities({});
198
+ setOpen(false);
199
+ };
200
+
201
+ const handleOpen = () => {
202
+ setOpen(true);
203
+ // Initialize quantities after a short delay to ensure data is loaded
204
+ setTimeout(initializeFulfillmentQuantities, 100);
205
+ };
206
+
207
+ const fulfillmentHandlers = fulfillmentHandlersData?.fulfillmentHandlers;
208
+ const selectedHandler = fulfillmentHandlers?.find(h => h.code === form.watch('handler.code'));
209
+
210
+ return (
211
+ <>
212
+ <Button
213
+ onClick={e => {
214
+ e.stopPropagation();
215
+ handleOpen();
216
+ }}
217
+ className="mr-2"
218
+ >
219
+ <Trans>Fulfill order</Trans>
220
+ </Button>
221
+ <Dialog open={open}>
222
+ <DialogContent className="sm:max-w-[600px]">
223
+ <DialogHeader>
224
+ <DialogTitle>
225
+ <Trans>Fulfill order</Trans>
226
+ </DialogTitle>
227
+ <DialogDescription>
228
+ <Trans>Select quantities to fulfill and configure the fulfillment handler</Trans>
229
+ </DialogDescription>
230
+ </DialogHeader>
231
+ <Form {...form}>
232
+ <form
233
+ onSubmit={e => {
234
+ e.stopPropagation();
235
+ form.handleSubmit(handleSubmit)(e);
236
+ }}
237
+ className="space-y-4"
238
+ >
239
+ <div className="space-y-4">
240
+ <div className="font-medium">
241
+ <Trans>Order lines</Trans>
242
+ </div>
243
+ {order.lines.map(line => {
244
+ const quantity = fulfillmentQuantities[line.id];
245
+ if (!quantity || quantity.max <= 0) return null;
246
+
247
+ return (
248
+ <div
249
+ key={line.id}
250
+ className="flex items-center justify-between p-3 border rounded-md"
251
+ >
252
+ <div className="flex-1">
253
+ <div className="font-medium">{line.productVariant.name}</div>
254
+ <div className="text-sm text-muted-foreground">
255
+ SKU: {line.productVariant.sku}
256
+ </div>
257
+ <div className="text-sm text-muted-foreground">
258
+ <Trans>
259
+ {quantity.max} of {line.quantity} available to fulfill
260
+ </Trans>
261
+ </div>
262
+ </div>
263
+ <div className="flex items-center space-x-2">
264
+ <Label htmlFor={`quantity-${line.id}`}>
265
+ <Trans>Quantity</Trans>
266
+ </Label>
267
+ <Input
268
+ id={`quantity-${line.id}`}
269
+ type="number"
270
+ min="0"
271
+ max={quantity.max}
272
+ value={quantity.fulfillCount}
273
+ onChange={e => {
274
+ const value = parseInt(e.target.value) || 0;
275
+ updateFulfillmentQuantity(line.id, value);
276
+ }}
277
+ className="w-20"
278
+ />
279
+ </div>
280
+ </div>
281
+ );
282
+ })}
283
+ </div>
284
+
285
+ {selectedHandler && (
286
+ <FormFieldWrapper
287
+ control={form.control}
288
+ name="handler"
289
+ label={<Trans>Fulfillment handler</Trans>}
290
+ render={({ field }) => (
291
+ <ConfigurableOperationInput
292
+ operationDefinition={selectedHandler}
293
+ value={field.value}
294
+ onChange={field.onChange}
295
+ readonly={false}
296
+ removable={false}
297
+ />
298
+ )}
299
+ />
300
+ )}
301
+
302
+ <DialogFooter>
303
+ <Button type="button" variant="outline" onClick={handleCancel}>
304
+ <Trans>Cancel</Trans>
305
+ </Button>
306
+ <Button type="submit" disabled={!canSubmit() || isSubmitting}>
307
+ {isSubmitting ? (
308
+ <Trans>Fulfilling...</Trans>
309
+ ) : (
310
+ <Trans>Fulfill order</Trans>
311
+ )}
312
+ </Button>
313
+ </DialogFooter>
314
+ </form>
315
+ </Form>
316
+ </DialogContent>
317
+ </Dialog>
318
+ </>
319
+ );
320
+ }
@@ -0,0 +1,173 @@
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 {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from '@/vdb/components/ui/dropdown-menu.js';
10
+ import { api } from '@/vdb/graphql/api.js';
11
+ import { ResultOf } from '@/vdb/graphql/graphql.js';
12
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
13
+ import { Trans, useLingui } from '@/vdb/lib/trans.js';
14
+ import { useMutation } from '@tanstack/react-query';
15
+ import { ChevronDown } from 'lucide-react';
16
+ import { toast } from 'sonner';
17
+ import {
18
+ fulfillmentFragment,
19
+ orderDetailFragment,
20
+ transitionFulfillmentToStateDocument,
21
+ } from '../orders.graphql.js';
22
+
23
+ type Order = NonNullable<ResultOf<typeof orderDetailFragment>>;
24
+
25
+ type FulfillmentDetailsProps = {
26
+ order: Order;
27
+ fulfillment: ResultOf<typeof fulfillmentFragment>;
28
+ onSuccess?: () => void;
29
+ };
30
+
31
+ export function FulfillmentDetails({ order, fulfillment, onSuccess }: Readonly<FulfillmentDetailsProps>) {
32
+ const { formatDate } = useLocalFormat();
33
+ const { i18n } = useLingui();
34
+
35
+ // Create a map of order lines by ID for quick lookup
36
+ const orderLinesMap = new Map(order.lines.map(line => [line.id, line]));
37
+
38
+ const transitionFulfillmentMutation = useMutation({
39
+ mutationFn: api.mutate(transitionFulfillmentToStateDocument),
40
+ onSuccess: (result: ResultOf<typeof transitionFulfillmentToStateDocument>) => {
41
+ const fulfillment = result.transitionFulfillmentToState;
42
+ if (fulfillment.__typename === 'Fulfillment') {
43
+ toast.success(i18n.t('Fulfillment state updated successfully'));
44
+ onSuccess?.();
45
+ } else {
46
+ toast.error(fulfillment.message ?? i18n.t('Failed to update fulfillment state'));
47
+ }
48
+ },
49
+ onError: error => {
50
+ toast.error(i18n.t('Failed to update fulfillment state'));
51
+ },
52
+ });
53
+
54
+ const nextSuggestedState = (): string | undefined => {
55
+ const { nextStates } = fulfillment;
56
+ const namedStateOrDefault = (targetState: string) =>
57
+ nextStates.includes(targetState) ? targetState : nextStates[0];
58
+
59
+ switch (fulfillment.state) {
60
+ case 'Pending':
61
+ return namedStateOrDefault('Shipped');
62
+ case 'Shipped':
63
+ return namedStateOrDefault('Delivered');
64
+ default:
65
+ return nextStates.find(s => s !== 'Cancelled');
66
+ }
67
+ };
68
+
69
+ const nextOtherStates = (): string[] => {
70
+ const suggested = nextSuggestedState();
71
+ return fulfillment.nextStates.filter(s => s !== suggested);
72
+ };
73
+
74
+ const handleStateTransition = (state: string) => {
75
+ transitionFulfillmentMutation.mutate({
76
+ id: fulfillment.id,
77
+ state,
78
+ });
79
+ };
80
+
81
+ return (
82
+ <div className="space-y-1 p-3 border rounded-md">
83
+ <div className="space-y-1">
84
+ <LabeledData label={<Trans>Fulfillment ID</Trans>} value={fulfillment.id.slice(-8)} />
85
+ <LabeledData label={<Trans>Method</Trans>} value={fulfillment.method} />
86
+ <LabeledData label={<Trans>State</Trans>} value={fulfillment.state} />
87
+ {fulfillment.trackingCode && (
88
+ <LabeledData label={<Trans>Tracking code</Trans>} value={fulfillment.trackingCode} />
89
+ )}
90
+ <LabeledData label={<Trans>Created</Trans>} value={formatDate(fulfillment.createdAt)} />
91
+ </div>
92
+
93
+ {fulfillment.lines.length > 0 && (
94
+ <div className="mt-3 pt-3 border-t">
95
+ <Collapsible>
96
+ <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">
97
+ <Trans>
98
+ Fulfilled items (
99
+ {fulfillment.lines.reduce((acc, line) => acc + line.quantity, 0)})
100
+ </Trans>
101
+ <ChevronDown className="h-4 w-4 transition-transform duration-200 data-[state=open]:rotate-180" />
102
+ </CollapsibleTrigger>
103
+ <CollapsibleContent className="mt-2 space-y-1">
104
+ {fulfillment.lines.map((line) => {
105
+ const orderLine = orderLinesMap.get(line.orderLineId);
106
+ const productName = orderLine?.productVariant?.name ?? 'Unknown product';
107
+ const sku = orderLine?.productVariant?.sku;
108
+
109
+ return (
110
+ <div key={line.orderLineId} className="text-sm text-muted-foreground">
111
+ <div className="font-medium text-foreground text-xs">
112
+ {productName}
113
+ </div>
114
+ <div className="flex items-center gap-2 text-xs">
115
+ <span>Qty: {line.quantity}</span>
116
+ {sku && <span>SKU: {sku}</span>}
117
+ </div>
118
+ </div>
119
+ );
120
+ })}
121
+ </CollapsibleContent>
122
+ </Collapsible>
123
+ </div>
124
+ )}
125
+
126
+ {fulfillment.nextStates.length > 0 && (
127
+ <div className="mt-3 pt-3 border-t">
128
+ <div className="flex">
129
+ <Button
130
+ variant="outline"
131
+ size="sm"
132
+ disabled={transitionFulfillmentMutation.isPending}
133
+ className="rounded-r-none flex-1 justify-start shadow-none"
134
+ >
135
+ <Trans>State: {fulfillment.state}</Trans>
136
+ </Button>
137
+ <DropdownMenu>
138
+ <DropdownMenuTrigger asChild>
139
+ <Button
140
+ variant="outline"
141
+ size="sm"
142
+ disabled={transitionFulfillmentMutation.isPending}
143
+ className="rounded-l-none border-l-0 shadow-none"
144
+ >
145
+ <ChevronDown className="h-4 w-4" />
146
+ </Button>
147
+ </DropdownMenuTrigger>
148
+ <DropdownMenuContent align="end">
149
+ {nextSuggestedState() && (
150
+ <DropdownMenuItem
151
+ onClick={() => handleStateTransition(nextSuggestedState()!)}
152
+ disabled={transitionFulfillmentMutation.isPending}
153
+ >
154
+ <Trans>Transition to {nextSuggestedState()}</Trans>
155
+ </DropdownMenuItem>
156
+ )}
157
+ {nextOtherStates().map(state => (
158
+ <DropdownMenuItem
159
+ key={state}
160
+ onClick={() => handleStateTransition(state)}
161
+ disabled={transitionFulfillmentMutation.isPending}
162
+ >
163
+ <Trans>Transition to {state}</Trans>
164
+ </DropdownMenuItem>
165
+ ))}
166
+ </DropdownMenuContent>
167
+ </DropdownMenu>
168
+ </div>
169
+ </div>
170
+ )}
171
+ </div>
172
+ );
173
+ }
@@ -24,11 +24,11 @@ export function OrderAddress({ address }: Readonly<{ address?: OrderAddress }>)
24
24
  } = address;
25
25
 
26
26
  return (
27
- <div className="space-y-2">
27
+ <div className="space-y-1 text-sm">
28
28
  {fullName && <p className="font-medium">{fullName}</p>}
29
29
  {company && <p className="text-sm text-muted-foreground">{company}</p>}
30
30
 
31
- <div className="text-sm">
31
+ <div>
32
32
  {streetLine1 && <p>{streetLine1}</p>}
33
33
  {streetLine2 && <p>{streetLine2}</p>}
34
34
  <p>{[city, province].filter(Boolean).join(', ')}</p>
@@ -1,7 +1,7 @@
1
1
  import { api } from '@/vdb/graphql/api.js';
2
2
  import { graphql, ResultOf } from '@/vdb/graphql/graphql.js';
3
3
  import { useLingui } from '@/vdb/lib/trans.js';
4
- import { useInfiniteQuery, useMutation } from '@tanstack/react-query';
4
+ import { QueryKey, useInfiniteQuery, useMutation } from '@tanstack/react-query';
5
5
  import { useState } from 'react';
6
6
  import { toast } from 'sonner';
7
7
 
@@ -55,6 +55,10 @@ export interface UseOrderHistoryResult {
55
55
  hasNextPage: boolean;
56
56
  }
57
57
 
58
+ export function orderHistoryQueryKey(orderId: string): QueryKey {
59
+ return ['OrderHistory', orderId];
60
+ }
61
+
58
62
  export function useOrderHistory({
59
63
  orderId,
60
64
  pageSize = 10,
@@ -83,7 +87,7 @@ export function useOrderHistory({
83
87
  take: pageSize,
84
88
  },
85
89
  }),
86
- queryKey: ['OrderHistory', orderId],
90
+ queryKey: orderHistoryQueryKey(orderId),
87
91
  initialPageParam: 0,
88
92
  getNextPageParam: (lastPage, _pages, lastPageParam) => {
89
93
  const totalItems = lastPage.order?.history?.totalItems ?? 0;
@@ -1,13 +1,10 @@
1
1
  import { TableCell, TableRow } from '@/vdb/components/ui/table.js';
2
- import { ResultOf } from '@/vdb/graphql/graphql.js';
3
2
  import { Trans } from '@/vdb/lib/trans.js';
4
- import { orderDetailDocument } from '../orders.graphql.js';
3
+ import { Order } from '../utils/order-types.js';
5
4
  import { MoneyGrossNet } from './money-gross-net.js';
6
5
 
7
- type OrderFragment = NonNullable<ResultOf<typeof orderDetailDocument>['order']>;
8
-
9
6
  export interface OrderTableTotalsProps {
10
- order: OrderFragment;
7
+ order: Order;
11
8
  columnCount: number;
12
9
  }
13
10
 
@@ -1,10 +1,9 @@
1
1
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/vdb/components/ui/table.js';
2
2
  import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
3
3
  import { Trans } from '@/vdb/lib/trans.js';
4
- import { ResultOf } from 'gql.tada';
5
- import { orderDetailFragment } from '../orders.graphql.js';
4
+ import { Order } from '../utils/order-types.js';
6
5
 
7
- export function OrderTaxSummary({ order }: Readonly<{ order: ResultOf<typeof orderDetailFragment> }>) {
6
+ export function OrderTaxSummary({ order }: Readonly<{ order: Order }>) {
8
7
  const { formatCurrency } = useLocalFormat();
9
8
  return (
10
9
  <div>
@@ -1,7 +1,11 @@
1
+ import { LabeledData } from '@/vdb/components/labeled-data.js';
1
2
  import { ResultOf } from '@/vdb/graphql/graphql.js';
2
3
  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';
3
6
  import { Trans } from '@/vdb/lib/trans.js';
4
7
  import { paymentWithRefundsFragment } from '../orders.graphql.js';
8
+ import { JsonEditor } from 'json-edit-react';
5
9
 
6
10
  type PaymentDetailsProps = {
7
11
  payment: ResultOf<typeof paymentWithRefundsFragment>;
@@ -13,17 +17,13 @@ export function PaymentDetails({ payment, currencyCode }: Readonly<PaymentDetail
13
17
  const t = (key: string) => key;
14
18
 
15
19
  return (
16
- <div className="space-y-2">
20
+ <div className="space-y-1 p-3 border rounded-md">
17
21
  <LabeledData label={<Trans>Payment method</Trans>} value={payment.method} />
18
-
19
22
  <LabeledData label={<Trans>Amount</Trans>} value={formatCurrency(payment.amount, currencyCode)} />
20
-
21
23
  <LabeledData label={<Trans>Created at</Trans>} value={formatDate(payment.createdAt)} />
22
-
23
24
  {payment.transactionId && (
24
25
  <LabeledData label={<Trans>Transaction ID</Trans>} value={payment.transactionId} />
25
26
  )}
26
-
27
27
  {/* We need to check if there is errorMessage field in the Payment type */}
28
28
  {payment.errorMessage && (
29
29
  <LabeledData
@@ -32,30 +32,15 @@ export function PaymentDetails({ payment, currencyCode }: Readonly<PaymentDetail
32
32
  className="text-destructive"
33
33
  />
34
34
  )}
35
-
36
- <LabeledData
37
- label={<Trans>Payment metadata</Trans>}
38
- value={
39
- <pre className="max-h-96 overflow-auto rounded-md bg-muted p-4 text-sm">
40
- {JSON.stringify(payment.metadata, null, 2)}
41
- </pre>
42
- }
43
- />
44
- </div>
45
- );
46
- }
47
-
48
- type LabeledDataProps = {
49
- label: string | React.ReactNode;
50
- value: React.ReactNode;
51
- className?: string;
52
- };
53
-
54
- function LabeledData({ label, value, className }: LabeledDataProps) {
55
- return (
56
- <div className="">
57
- <span className="font-medium text-muted-foreground text-sm">{label}</span>
58
- <div className={`col-span-2 ${className}`}>{value}</div>
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>
59
44
  </div>
60
45
  );
61
46
  }
@@ -1,4 +1,8 @@
1
- import { assetFragment, errorResultFragment } from '@/vdb/graphql/fragments.js';
1
+ import {
2
+ assetFragment,
3
+ configurableOperationDefFragment,
4
+ errorResultFragment,
5
+ } from '@/vdb/graphql/fragments.js';
2
6
  import { graphql } from '@/vdb/graphql/graphql.js';
3
7
 
4
8
  export const orderListDocument = graphql(`
@@ -20,7 +24,6 @@ export const orderListDocument = graphql(`
20
24
  total
21
25
  totalWithTax
22
26
  currencyCode
23
-
24
27
  shippingLines {
25
28
  shippingMethod {
26
29
  name
@@ -508,3 +511,98 @@ export const transitionOrderToStateDocument = graphql(
508
511
  `,
509
512
  [errorResultFragment],
510
513
  );
514
+
515
+ export const paymentMethodsDocument = graphql(`
516
+ query GetPaymentMethods($options: PaymentMethodListOptions!) {
517
+ paymentMethods(options: $options) {
518
+ items {
519
+ id
520
+ createdAt
521
+ updatedAt
522
+ name
523
+ code
524
+ description
525
+ enabled
526
+ }
527
+ totalItems
528
+ }
529
+ }
530
+ `);
531
+
532
+ export const addManualPaymentToOrderDocument = graphql(
533
+ `
534
+ mutation AddManualPaymentToOrder($input: ManualPaymentInput!) {
535
+ addManualPaymentToOrder(input: $input) {
536
+ __typename
537
+ ... on Order {
538
+ id
539
+ state
540
+ payments {
541
+ id
542
+ amount
543
+ method
544
+ state
545
+ }
546
+ }
547
+ ...ErrorResult
548
+ }
549
+ }
550
+ `,
551
+ [errorResultFragment],
552
+ );
553
+
554
+ export const fulfillmentHandlersDocument = graphql(
555
+ `
556
+ query GetFulfillmentHandlers {
557
+ fulfillmentHandlers {
558
+ ...ConfigurableOperationDef
559
+ }
560
+ }
561
+ `,
562
+ [configurableOperationDefFragment],
563
+ );
564
+
565
+ export const fulfillOrderDocument = graphql(
566
+ `
567
+ mutation FulfillOrder($input: FulfillOrderInput!) {
568
+ addFulfillmentToOrder(input: $input) {
569
+ __typename
570
+ ... on Fulfillment {
571
+ id
572
+ state
573
+ method
574
+ trackingCode
575
+ lines {
576
+ orderLineId
577
+ quantity
578
+ }
579
+ }
580
+ ...ErrorResult
581
+ }
582
+ }
583
+ `,
584
+ [errorResultFragment],
585
+ );
586
+
587
+ export const transitionFulfillmentToStateDocument = graphql(
588
+ `
589
+ mutation TransitionFulfillmentToState($id: ID!, $state: String!) {
590
+ transitionFulfillmentToState(id: $id, state: $state) {
591
+ __typename
592
+ ... on Fulfillment {
593
+ id
594
+ state
595
+ nextStates
596
+ method
597
+ trackingCode
598
+ lines {
599
+ orderLineId
600
+ quantity
601
+ }
602
+ }
603
+ ...ErrorResult
604
+ }
605
+ }
606
+ `,
607
+ [errorResultFragment],
608
+ );
@@ -18,12 +18,18 @@ import { Trans, useLingui } from '@/vdb/lib/trans.js';
18
18
  import { Link, createFileRoute, redirect } from '@tanstack/react-router';
19
19
  import { User } from 'lucide-react';
20
20
  import { toast } from 'sonner';
21
+ import { AddManualPaymentDialog } from './components/add-manual-payment-dialog.js';
22
+ import { FulfillOrderDialog } from './components/fulfill-order-dialog.js';
23
+ import { FulfillmentDetails } from './components/fulfillment-details.js';
21
24
  import { OrderAddress } from './components/order-address.js';
22
25
  import { OrderHistoryContainer } from './components/order-history/order-history-container.js';
23
26
  import { OrderTable } from './components/order-table.js';
24
27
  import { OrderTaxSummary } from './components/order-tax-summary.js';
25
28
  import { PaymentDetails } from './components/payment-details.js';
26
29
  import { orderDetailDocument } from './orders.graphql.js';
30
+ import { canAddFulfillment, shouldShowAddManualPaymentButton } from './utils/order-utils.js';
31
+ import { useQueryClient } from '@tanstack/react-query';
32
+ import { orderHistoryQueryKey } from './components/order-history/use-order-history.js';
27
33
 
28
34
  const pageId = 'order-detail';
29
35
 
@@ -59,8 +65,8 @@ export const Route = createFileRoute('/_authenticated/_orders/orders_/$id')({
59
65
  function OrderDetailPage() {
60
66
  const params = Route.useParams();
61
67
  const { i18n } = useLingui();
62
-
63
- const { form, submitHandler, entity, isPending, resetForm } = useDetailPage({
68
+ const queryClient = useQueryClient();
69
+ const { form, submitHandler, entity, isPending, refreshEntity } = useDetailPage({
64
70
  pageId,
65
71
  queryDocument: orderDetailDocument,
66
72
  setValuesForUpdate: entity => {
@@ -85,11 +91,35 @@ function OrderDetailPage() {
85
91
  return null;
86
92
  }
87
93
 
94
+ const showAddPaymentButton = shouldShowAddManualPaymentButton(entity);
95
+ const showFulfillButton = canAddFulfillment(entity);
96
+
88
97
  return (
89
98
  <Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
90
99
  <PageTitle>{entity?.code ?? ''}</PageTitle>
91
100
  <PageActionBar>
92
101
  <PageActionBarRight>
102
+ {showAddPaymentButton && (
103
+ <PermissionGuard requires={['UpdateOrder']}>
104
+ <AddManualPaymentDialog
105
+ order={entity}
106
+ onSuccess={() => {
107
+ refreshEntity();
108
+ }}
109
+ />
110
+ </PermissionGuard>
111
+ )}
112
+ {showFulfillButton && (
113
+ <PermissionGuard requires={['UpdateOrder']}>
114
+ <FulfillOrderDialog
115
+ order={entity}
116
+ onSuccess={() => {
117
+ refreshEntity();
118
+ queryClient.refetchQueries({ queryKey: orderHistoryQueryKey(entity.id) });
119
+ }}
120
+ />
121
+ </PermissionGuard>
122
+ )}
93
123
  <PermissionGuard requires={['UpdateProduct', 'UpdateCatalog']}>
94
124
  <Button
95
125
  type="submit"
@@ -149,6 +179,32 @@ function OrderDetailPage() {
149
179
  />
150
180
  ))}
151
181
  </PageBlock>
182
+
183
+ <PageBlock
184
+ column="side"
185
+ blockId="fulfillment-details"
186
+ title={<Trans>Fulfillment details</Trans>}
187
+ >
188
+ {entity?.fulfillments?.length && entity.fulfillments.length > 0 ? (
189
+ <div className="space-y-2">
190
+ {entity?.fulfillments?.map(fulfillment => (
191
+ <FulfillmentDetails
192
+ key={fulfillment.id}
193
+ order={entity}
194
+ fulfillment={fulfillment}
195
+ onSuccess={() => {
196
+ refreshEntity();
197
+ queryClient.refetchQueries({ queryKey: orderHistoryQueryKey(entity.id) });
198
+ }}
199
+ />
200
+ ))}
201
+ </div>
202
+ ) : (
203
+ <div className="text-muted-foreground text-xs font-medium p-3 border rounded-md">
204
+ <Trans>No fulfillments</Trans>
205
+ </div>
206
+ )}
207
+ </PageBlock>
152
208
  </PageLayout>
153
209
  </Page>
154
210
  );
@@ -0,0 +1,7 @@
1
+ import { ResultOf } from '@/vdb/graphql/graphql.js';
2
+
3
+ import { orderDetailDocument } from '../orders.graphql.js';
4
+
5
+ export type Order = NonNullable<ResultOf<typeof orderDetailDocument>['order']>;
6
+ export type Payment = NonNullable<NonNullable<Order>['payments']>[number];
7
+ export type Fulfillment = NonNullable<NonNullable<Order>['fulfillments']>[number];
@@ -0,0 +1,77 @@
1
+ import { Fulfillment, Order, Payment } from './order-types.js';
2
+
3
+ /**
4
+ * Calculates the outstanding payment amount for an order
5
+ */
6
+ export function calculateOutstandingPaymentAmount(order: Order): number {
7
+ if (!order) return 0;
8
+
9
+ const paymentIsValid = (p: Payment): boolean =>
10
+ p.state !== 'Cancelled' && p.state !== 'Declined' && p.state !== 'Error';
11
+
12
+ let amountCovered = 0;
13
+ for (const payment of order.payments?.filter(paymentIsValid) ?? []) {
14
+ const refunds = payment.refunds.filter(r => r.state !== 'Failed') ?? [];
15
+ const refundsTotal = refunds.reduce((sum, refund) => sum + (refund.total || 0), 0);
16
+ amountCovered += payment.amount - refundsTotal;
17
+ }
18
+ return order.totalWithTax - amountCovered;
19
+ }
20
+
21
+ /**
22
+ * Checks if an order has unsettled modifications
23
+ */
24
+ export function hasUnsettledModifications(order: Order): boolean {
25
+ if (!order) return false;
26
+ return order.modifications.some(m => !m.isSettled);
27
+ }
28
+
29
+ /**
30
+ * Determines if the add manual payment button should be displayed
31
+ */
32
+ export function shouldShowAddManualPaymentButton(order: Order): boolean {
33
+ if (!order) return false;
34
+
35
+ return (
36
+ order.type !== 'Aggregate' &&
37
+ (order.state === 'ArrangingPayment' || order.state === 'ArrangingAdditionalPayment') &&
38
+ (hasUnsettledModifications(order) || calculateOutstandingPaymentAmount(order) > 0)
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Determines if we can add a fulfillment to an order
44
+ */
45
+ export function canAddFulfillment(order: Order): boolean {
46
+ if (!order) return false;
47
+
48
+ // Get all fulfillment lines from non-cancelled fulfillments
49
+ const allFulfillmentLines: Fulfillment['lines'] = (order.fulfillments ?? [])
50
+ .filter(fulfillment => fulfillment.state !== 'Cancelled')
51
+ .reduce((all, fulfillment) => [...all, ...fulfillment.lines], [] as Fulfillment['lines']);
52
+
53
+ // Check if all items are already fulfilled
54
+ let allItemsFulfilled = true;
55
+ for (const line of order.lines) {
56
+ const totalFulfilledCount = allFulfillmentLines
57
+ .filter(row => row.orderLineId === line.id)
58
+ .reduce((sum, row) => sum + row.quantity, 0);
59
+ if (totalFulfilledCount < line.quantity) {
60
+ allItemsFulfilled = false;
61
+ break;
62
+ }
63
+ }
64
+
65
+ // Check if order is in a fulfillable state
66
+ const isFulfillableState =
67
+ order.nextStates.includes('Shipped') ||
68
+ order.nextStates.includes('PartiallyShipped') ||
69
+ order.nextStates.includes('Delivered');
70
+
71
+ return (
72
+ !allItemsFulfilled &&
73
+ !hasUnsettledModifications(order) &&
74
+ calculateOutstandingPaymentAmount(order) === 0 &&
75
+ isFulfillableState
76
+ );
77
+ }
@@ -1,9 +1,6 @@
1
1
  import { graphql } from '@/vdb/graphql/graphql.js';
2
2
  import { createRelationSelectorConfig, RelationSelector } from './relation-selector.js';
3
3
 
4
- // Re-export for convenience
5
- export { createRelationSelectorConfig };
6
-
7
4
  /**
8
5
  * Single relation input component
9
6
  */
@@ -0,0 +1,21 @@
1
+ type LabeledDataProps = {
2
+ label: string | React.ReactNode;
3
+ value: React.ReactNode;
4
+ className?: string;
5
+ };
6
+
7
+ /**
8
+ * @description
9
+ * Used to display a value with a label, like
10
+ *
11
+ * Order Code
12
+ * QWERTY
13
+ */
14
+ export function LabeledData({ label, value, className }: Readonly<LabeledDataProps>) {
15
+ return (
16
+ <div className="">
17
+ <span className="font-medium text-muted-foreground text-xs">{label}</span>
18
+ <div className={`col-span-2 text-sm ${className}`}>{value}</div>
19
+ </div>
20
+ );
21
+ }
@@ -111,7 +111,7 @@ export function vendureDashboardPlugin(options: VitePluginVendureDashboardOption
111
111
  : [
112
112
  TanStackRouterVite({
113
113
  autoCodeSplitting: true,
114
- routeFileIgnorePattern: '.graphql.ts|components|hooks',
114
+ routeFileIgnorePattern: '.graphql.ts|components|hooks|utils',
115
115
  routesDirectory: path.join(packageRoot, 'src/app/routes'),
116
116
  generatedRouteTree: path.join(packageRoot, 'src/app/routeTree.gen.ts'),
117
117
  }),