@vendure/dashboard 3.3.6-master-202507031127 → 3.3.6-master-202507031620

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 (22) hide show
  1. package/dist/plugin/vite-plugin-vendure-dashboard.js +1 -1
  2. package/package.json +5 -5
  3. package/src/app/routes/_authenticated/_orders/components/add-manual-payment-dialog.tsx +191 -0
  4. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +2 -2
  5. package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +320 -0
  6. package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +173 -0
  7. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -2
  8. package/src/app/routes/_authenticated/_orders/components/order-history/use-order-history.ts +6 -2
  9. package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +2 -5
  10. package/src/app/routes/_authenticated/_orders/components/order-tax-summary.tsx +2 -3
  11. package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +14 -29
  12. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +100 -2
  13. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +58 -2
  14. package/src/app/routes/_authenticated/_orders/utils/order-types.ts +7 -0
  15. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +77 -0
  16. package/src/lib/components/data-input/relation-input.tsx +0 -3
  17. package/src/lib/components/labeled-data.tsx +21 -0
  18. package/src/lib/components/shared/translatable-form-field.tsx +2 -1
  19. package/src/lib/components/ui/select.tsx +151 -129
  20. package/src/lib/framework/page/detail-page.tsx +26 -20
  21. package/src/lib/graphql/graphql-env.d.ts +1 -1
  22. package/vite/vite-plugin-vendure-dashboard.ts +1 -1
@@ -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-202507031127",
4
+ "version": "3.3.6-master-202507031620",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -65,7 +65,7 @@
65
65
  "@radix-ui/react-label": "^2.1.2",
66
66
  "@radix-ui/react-popover": "^1.1.6",
67
67
  "@radix-ui/react-scroll-area": "^1.2.3",
68
- "@radix-ui/react-select": "^2.1.6",
68
+ "@radix-ui/react-select": "^2.2.5",
69
69
  "@radix-ui/react-separator": "^1.1.2",
70
70
  "@radix-ui/react-slot": "^1.1.2",
71
71
  "@radix-ui/react-switch": "^1.1.3",
@@ -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-202507031127",
90
- "@vendure/core": "^3.3.6-master-202507031127",
89
+ "@vendure/common": "^3.3.6-master-202507031620",
90
+ "@vendure/core": "^3.3.6-master-202507031620",
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": "82c67a6665c77f59b6bb2e652e73d4580ca4f291"
133
+ "gitHead": "d90e8efa64f0de407ec7402470ad21613885904c"
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
+ }