@vendure/dashboard 3.3.6-master-202507100236 → 3.3.6-master-202507120238
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin/tests/barrel-exports.spec.js +1 -1
- package/dist/plugin/utils/config-loader.js +2 -2
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_orders/components/customer-address-selector.tsx +0 -1
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +30 -37
- package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +33 -53
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +14 -7
- package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +23 -12
- package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +364 -0
- package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +222 -0
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +146 -85
- package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +272 -31
- package/src/app/routes/_authenticated/_orders/components/settle-refund-dialog.tsx +80 -0
- package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +102 -0
- package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +144 -0
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +118 -2
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +144 -52
- package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +550 -0
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +0 -18
- package/src/app/routes/_authenticated/_orders/utils/order-types.ts +5 -2
- package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +4 -3
- package/src/app/routes/_authenticated/_products/components/option-value-input.tsx +1 -1
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +0 -1
- package/src/app/routes/_authenticated/_zones/components/zone-countries-table.tsx +0 -7
- package/src/lib/components/data-display/date-time.tsx +7 -1
- package/src/lib/components/data-input/relation-input.tsx +11 -0
- package/src/lib/components/data-input/relation-selector.tsx +9 -2
- package/src/lib/components/data-table/data-table-utils.ts +34 -0
- package/src/lib/components/data-table/data-table-view-options.tsx +2 -2
- package/src/lib/components/data-table/data-table.tsx +5 -2
- package/src/lib/components/data-table/use-generated-columns.tsx +307 -0
- package/src/lib/components/layout/content-language-selector.tsx +1 -1
- package/src/lib/components/shared/asset/asset-preview.tsx +0 -6
- package/src/lib/components/shared/option-value-input.tsx +1 -1
- package/src/lib/components/shared/paginated-list-data-table.tsx +15 -286
- package/src/lib/components/shared/product-variant-selector.tsx +29 -5
- package/src/lib/components/ui/calendar.tsx +1 -1
- package/src/lib/framework/component-registry/dynamic-component.tsx +3 -3
- package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +1 -1
- package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +0 -2
- package/src/lib/framework/document-introspection/get-document-structure.spec.ts +321 -2
- package/src/lib/framework/document-introspection/get-document-structure.ts +149 -31
- package/src/lib/framework/extension-api/types/layout.ts +21 -6
- package/src/lib/framework/layout-engine/layout-extensions.ts +1 -4
- package/src/lib/framework/layout-engine/page-layout.tsx +61 -10
- package/src/lib/framework/page/use-detail-page.ts +10 -7
- package/src/lib/hooks/use-extended-list-query.ts +32 -30
- package/vite/tests/barrel-exports.spec.ts +1 -1
- package/vite/utils/config-loader.ts +2 -2
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import { ErrorPage } from '@/vdb/components/shared/error-page.js';
|
|
2
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
3
|
+
import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
|
|
4
|
+
import {
|
|
5
|
+
Page,
|
|
6
|
+
PageActionBar,
|
|
7
|
+
PageActionBarRight,
|
|
8
|
+
PageBlock,
|
|
9
|
+
PageLayout,
|
|
10
|
+
PageTitle,
|
|
11
|
+
} from '@/vdb/framework/layout-engine/page-layout.js';
|
|
12
|
+
import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
|
|
13
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
14
|
+
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
15
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
16
|
+
import { createFileRoute, Link, redirect, useNavigate } from '@tanstack/react-router';
|
|
17
|
+
import { ResultOf, VariablesOf } from 'gql.tada';
|
|
18
|
+
import { User } from 'lucide-react';
|
|
19
|
+
import { useEffect, useState } from 'react';
|
|
20
|
+
import { toast } from 'sonner';
|
|
21
|
+
import { CustomerAddressSelector } from './components/customer-address-selector.js';
|
|
22
|
+
import { EditOrderTable } from './components/edit-order-table.js';
|
|
23
|
+
import { OrderAddress } from './components/order-address.js';
|
|
24
|
+
import { OrderModificationPreviewDialog } from './components/order-modification-preview-dialog.js';
|
|
25
|
+
import { OrderModificationSummary } from './components/order-modification-summary.js';
|
|
26
|
+
import { useTransitionOrderToState } from './components/use-transition-order-to-state.js';
|
|
27
|
+
import {
|
|
28
|
+
draftOrderEligibleShippingMethodsDocument,
|
|
29
|
+
modifyOrderDocument,
|
|
30
|
+
orderDetailDocument,
|
|
31
|
+
} from './orders.graphql.js';
|
|
32
|
+
import { AddressFragment, Order } from './utils/order-types.js';
|
|
33
|
+
|
|
34
|
+
const pageId = 'order-modify';
|
|
35
|
+
type ModifyOrderInput = VariablesOf<typeof modifyOrderDocument>['input'];
|
|
36
|
+
|
|
37
|
+
export const Route = createFileRoute('/_authenticated/_orders/orders_/$id_/modify')({
|
|
38
|
+
component: ModifyOrderPage,
|
|
39
|
+
loader: async ({ context, params }) => {
|
|
40
|
+
if (!params.id) {
|
|
41
|
+
throw new Error('ID param is required');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result: ResultOf<typeof orderDetailDocument> = await context.queryClient.ensureQueryData(
|
|
45
|
+
getDetailQueryOptions(addCustomFields(orderDetailDocument), { id: params.id }),
|
|
46
|
+
{ id: params.id },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (!result.order) {
|
|
50
|
+
throw new Error(`Order with the ID ${params.id} was not found`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (result.order.state === 'Draft') {
|
|
54
|
+
throw redirect({
|
|
55
|
+
to: `/orders/draft/${params.id}`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (result.order.state !== 'Modifying') {
|
|
59
|
+
throw redirect({
|
|
60
|
+
to: `/orders/${params.id}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
breadcrumb: [{ path: '/orders', label: 'Orders' }, result.order.code, { label: 'Modify' }],
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
errorComponent: ({ error }) => <ErrorPage message={error.message} />,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// --- AddedLine type for added items ---
|
|
72
|
+
interface AddedLine {
|
|
73
|
+
id: string;
|
|
74
|
+
featuredAsset?: any;
|
|
75
|
+
productVariant: {
|
|
76
|
+
id: string;
|
|
77
|
+
name: string;
|
|
78
|
+
sku: string;
|
|
79
|
+
};
|
|
80
|
+
unitPrice: number;
|
|
81
|
+
unitPriceWithTax: number;
|
|
82
|
+
quantity: number;
|
|
83
|
+
linePrice: number;
|
|
84
|
+
linePriceWithTax: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- ProductVariantInfo type ---
|
|
88
|
+
type ProductVariantInfo = {
|
|
89
|
+
productVariantId: string;
|
|
90
|
+
productVariantName: string;
|
|
91
|
+
sku: string;
|
|
92
|
+
productAsset: {
|
|
93
|
+
preview: string;
|
|
94
|
+
};
|
|
95
|
+
price?: number;
|
|
96
|
+
priceWithTax?: number;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function ModifyOrderPage() {
|
|
100
|
+
const params = Route.useParams();
|
|
101
|
+
const navigate = useNavigate({ from: '/orders/$id/modify' });
|
|
102
|
+
const { i18n } = useLingui();
|
|
103
|
+
const queryClient = useQueryClient();
|
|
104
|
+
const { form, submitHandler, entity } = useDetailPage({
|
|
105
|
+
pageId,
|
|
106
|
+
queryDocument: orderDetailDocument,
|
|
107
|
+
setValuesForUpdate: entity => {
|
|
108
|
+
return {
|
|
109
|
+
id: entity.id,
|
|
110
|
+
customFields: entity.customFields,
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
params: { id: params.id },
|
|
114
|
+
onSuccess: async () => {
|
|
115
|
+
toast(i18n.t('Successfully updated order'));
|
|
116
|
+
form.reset(form.getValues());
|
|
117
|
+
},
|
|
118
|
+
onError: err => {
|
|
119
|
+
toast(i18n.t('Failed to update order'), {
|
|
120
|
+
description: err instanceof Error ? err.message : 'Unknown error',
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const { data: eligibleShippingMethods } = useQuery({
|
|
126
|
+
queryKey: ['eligibleShippingMethods', entity?.id],
|
|
127
|
+
queryFn: () => api.query(draftOrderEligibleShippingMethodsDocument, { orderId: entity?.id ?? '' }),
|
|
128
|
+
enabled: !!entity?.shippingAddress?.streetLine1,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const { transitionToPreModifyingState, ManuallySelectNextState, selectNextState, transitionToState } =
|
|
132
|
+
useTransitionOrderToState(entity?.id ?? '');
|
|
133
|
+
|
|
134
|
+
// --- Modification intent state ---
|
|
135
|
+
|
|
136
|
+
const [modifyOrderInput, setModifyOrderInput] = useState<ModifyOrderInput>({
|
|
137
|
+
orderId: '',
|
|
138
|
+
addItems: [],
|
|
139
|
+
adjustOrderLines: [],
|
|
140
|
+
surcharges: [],
|
|
141
|
+
note: '',
|
|
142
|
+
couponCodes: [],
|
|
143
|
+
options: {
|
|
144
|
+
recalculateShipping: true,
|
|
145
|
+
},
|
|
146
|
+
dryRun: true,
|
|
147
|
+
} satisfies ModifyOrderInput);
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
setModifyOrderInput(prev => ({
|
|
151
|
+
...prev,
|
|
152
|
+
orderId: entity?.id ?? '',
|
|
153
|
+
couponCodes: entity?.couponCodes ?? [],
|
|
154
|
+
}));
|
|
155
|
+
}, [entity?.id]);
|
|
156
|
+
|
|
157
|
+
// --- Added variants info state ---
|
|
158
|
+
const [addedVariants, setAddedVariants] = useState<Map<string, ProductVariantInfo>>(new Map());
|
|
159
|
+
|
|
160
|
+
// --- Handlers update modifyOrderInput ---
|
|
161
|
+
function handleAddItem(variant: ProductVariantInfo) {
|
|
162
|
+
setModifyOrderInput(prev => ({
|
|
163
|
+
...prev,
|
|
164
|
+
addItems: [...(prev.addItems ?? []), { productVariantId: variant.productVariantId, quantity: 1 }],
|
|
165
|
+
}));
|
|
166
|
+
setAddedVariants(prev => {
|
|
167
|
+
const newMap = new Map(prev);
|
|
168
|
+
newMap.set(variant.productVariantId, variant);
|
|
169
|
+
return newMap;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function handleAdjustLine({
|
|
174
|
+
lineId,
|
|
175
|
+
quantity,
|
|
176
|
+
customFields,
|
|
177
|
+
}: {
|
|
178
|
+
lineId: string;
|
|
179
|
+
quantity: number;
|
|
180
|
+
customFields: Record<string, any>;
|
|
181
|
+
}) {
|
|
182
|
+
// Check if this is an added line
|
|
183
|
+
if (lineId.startsWith('added-')) {
|
|
184
|
+
const productVariantId = lineId.replace('added-', '');
|
|
185
|
+
setModifyOrderInput(prev => ({
|
|
186
|
+
...prev,
|
|
187
|
+
addItems: (prev.addItems ?? []).map(item =>
|
|
188
|
+
item.productVariantId === productVariantId ? { ...item, quantity } : item,
|
|
189
|
+
),
|
|
190
|
+
}));
|
|
191
|
+
} else {
|
|
192
|
+
let normalizedCustomFields: any = customFields;
|
|
193
|
+
delete normalizedCustomFields.__entityId__;
|
|
194
|
+
if (Object.keys(normalizedCustomFields).length === 0) {
|
|
195
|
+
normalizedCustomFields = undefined;
|
|
196
|
+
}
|
|
197
|
+
setModifyOrderInput(prev => {
|
|
198
|
+
const existing = (prev.adjustOrderLines ?? []).find(l => l.orderLineId === lineId);
|
|
199
|
+
const adjustOrderLines = existing
|
|
200
|
+
? (prev.adjustOrderLines ?? []).map(l =>
|
|
201
|
+
l.orderLineId === lineId
|
|
202
|
+
? { ...l, quantity, customFields: normalizedCustomFields }
|
|
203
|
+
: l,
|
|
204
|
+
)
|
|
205
|
+
: [
|
|
206
|
+
...(prev.adjustOrderLines ?? []),
|
|
207
|
+
{ orderLineId: lineId, quantity, customFields: normalizedCustomFields },
|
|
208
|
+
];
|
|
209
|
+
return { ...prev, adjustOrderLines };
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function handleRemoveLine({ lineId }: { lineId: string }) {
|
|
215
|
+
if (lineId.startsWith('added-')) {
|
|
216
|
+
const productVariantId = lineId.replace('added-', '');
|
|
217
|
+
setModifyOrderInput(prev => ({
|
|
218
|
+
...prev,
|
|
219
|
+
addItems: (prev.addItems ?? []).filter(item => item.productVariantId !== productVariantId),
|
|
220
|
+
}));
|
|
221
|
+
setAddedVariants(prev => {
|
|
222
|
+
const newMap = new Map(prev);
|
|
223
|
+
newMap.delete(productVariantId);
|
|
224
|
+
return newMap;
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
setModifyOrderInput(prev => {
|
|
228
|
+
const existingAdjustment = (prev.adjustOrderLines ?? []).find(l => l.orderLineId === lineId);
|
|
229
|
+
const adjustOrderLines = existingAdjustment
|
|
230
|
+
? (prev.adjustOrderLines ?? []).map(l =>
|
|
231
|
+
l.orderLineId === lineId ? { ...l, quantity: 0 } : l,
|
|
232
|
+
)
|
|
233
|
+
: [...(prev.adjustOrderLines ?? []), { orderLineId: lineId, quantity: 0 }];
|
|
234
|
+
return {
|
|
235
|
+
...prev,
|
|
236
|
+
adjustOrderLines,
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function handleSetShippingMethod({ shippingMethodId }: { shippingMethodId: string }) {
|
|
243
|
+
setModifyOrderInput(prev => ({
|
|
244
|
+
...prev,
|
|
245
|
+
shippingMethodIds: [shippingMethodId],
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function handleApplyCouponCode({ couponCode }: { couponCode: string }) {
|
|
250
|
+
setModifyOrderInput(prev => ({
|
|
251
|
+
...prev,
|
|
252
|
+
couponCodes: prev.couponCodes?.includes(couponCode)
|
|
253
|
+
? prev.couponCodes
|
|
254
|
+
: [...(prev.couponCodes ?? []), couponCode],
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function handleRemoveCouponCode({ couponCode }: { couponCode: string }) {
|
|
259
|
+
setModifyOrderInput(prev => ({
|
|
260
|
+
...prev,
|
|
261
|
+
couponCodes: (prev.couponCodes ?? []).filter(code => code !== couponCode),
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- Address editing state ---
|
|
266
|
+
const [editingShippingAddress, setEditingShippingAddress] = useState(false);
|
|
267
|
+
const [editingBillingAddress, setEditingBillingAddress] = useState(false);
|
|
268
|
+
|
|
269
|
+
function orderAddressToModifyOrderInput(
|
|
270
|
+
address: AddressFragment,
|
|
271
|
+
): ModifyOrderInput['updateShippingAddress'] {
|
|
272
|
+
return {
|
|
273
|
+
streetLine1: address.streetLine1,
|
|
274
|
+
streetLine2: address.streetLine2,
|
|
275
|
+
city: address.city,
|
|
276
|
+
countryCode: address.country.code,
|
|
277
|
+
fullName: address.fullName,
|
|
278
|
+
postalCode: address.postalCode,
|
|
279
|
+
province: address.province,
|
|
280
|
+
company: address.company,
|
|
281
|
+
phoneNumber: address.phoneNumber,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// --- Address selection handlers ---
|
|
286
|
+
function handleSelectShippingAddress(address: AddressFragment) {
|
|
287
|
+
setModifyOrderInput(prev => ({
|
|
288
|
+
...prev,
|
|
289
|
+
updateShippingAddress: orderAddressToModifyOrderInput(address),
|
|
290
|
+
}));
|
|
291
|
+
setEditingShippingAddress(false);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function handleSelectBillingAddress(address: AddressFragment) {
|
|
295
|
+
setModifyOrderInput(prev => ({
|
|
296
|
+
...prev,
|
|
297
|
+
updateBillingAddress: orderAddressToModifyOrderInput(address),
|
|
298
|
+
}));
|
|
299
|
+
setEditingBillingAddress(false);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// --- Utility: compute pending order for display ---
|
|
303
|
+
function computePendingOrder(input: ModifyOrderInput): Order | null {
|
|
304
|
+
if (!entity) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
// Adjust lines
|
|
308
|
+
const lines = entity.lines.map(line => {
|
|
309
|
+
const adjust = input.adjustOrderLines?.find(l => l.orderLineId === line.id);
|
|
310
|
+
return adjust
|
|
311
|
+
? { ...line, quantity: adjust.quantity, customFields: (adjust as any).customFields }
|
|
312
|
+
: line;
|
|
313
|
+
});
|
|
314
|
+
// Add new items (as AddedLine)
|
|
315
|
+
const addedLines = input.addItems
|
|
316
|
+
?.map(item => {
|
|
317
|
+
const variantInfo = addedVariants.get(item.productVariantId);
|
|
318
|
+
return variantInfo
|
|
319
|
+
? ({
|
|
320
|
+
id: `added-${item.productVariantId}`,
|
|
321
|
+
featuredAsset: variantInfo.productAsset ?? null,
|
|
322
|
+
productVariant: {
|
|
323
|
+
id: variantInfo.productVariantId,
|
|
324
|
+
name: variantInfo.productVariantName,
|
|
325
|
+
sku: variantInfo.sku,
|
|
326
|
+
},
|
|
327
|
+
unitPrice: variantInfo.price ?? 0,
|
|
328
|
+
unitPriceWithTax: variantInfo.priceWithTax ?? 0,
|
|
329
|
+
quantity: item.quantity,
|
|
330
|
+
linePrice: (variantInfo.price ?? 0) * item.quantity,
|
|
331
|
+
linePriceWithTax: (variantInfo.priceWithTax ?? 0) * item.quantity,
|
|
332
|
+
} as unknown as Order['lines'][number])
|
|
333
|
+
: null;
|
|
334
|
+
})
|
|
335
|
+
.filter(x => x != null);
|
|
336
|
+
return {
|
|
337
|
+
...entity,
|
|
338
|
+
lines: [...lines, ...(addedLines ?? [])],
|
|
339
|
+
couponCodes: input.couponCodes ?? [],
|
|
340
|
+
shippingLines: input.shippingMethodIds
|
|
341
|
+
? input.shippingMethodIds
|
|
342
|
+
.map(shippingMethodId => {
|
|
343
|
+
const shippingMethod =
|
|
344
|
+
eligibleShippingMethods?.eligibleShippingMethodsForDraftOrder.find(
|
|
345
|
+
method => method.id === shippingMethodId,
|
|
346
|
+
);
|
|
347
|
+
if (!shippingMethod) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
shippingMethod: {
|
|
352
|
+
...shippingMethod,
|
|
353
|
+
fulfillmentHandlerCode: 'manual',
|
|
354
|
+
},
|
|
355
|
+
discountedPriceWithTax: shippingMethod?.priceWithTax ?? 0,
|
|
356
|
+
id: shippingMethodId,
|
|
357
|
+
};
|
|
358
|
+
})
|
|
359
|
+
.filter(x => x !== undefined)
|
|
360
|
+
: entity.shippingLines,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const [previewOpen, setPreviewOpen] = useState(false);
|
|
365
|
+
|
|
366
|
+
if (!entity) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const pendingOrder = computePendingOrder(modifyOrderInput);
|
|
371
|
+
const hasModifications =
|
|
372
|
+
(modifyOrderInput.addItems?.length ?? 0) > 0 ||
|
|
373
|
+
(modifyOrderInput.adjustOrderLines?.length ?? 0) > 0 ||
|
|
374
|
+
(modifyOrderInput.couponCodes?.length ?? 0) > 0 ||
|
|
375
|
+
(modifyOrderInput.shippingMethodIds?.length ?? 0) > 0 ||
|
|
376
|
+
modifyOrderInput.updateShippingAddress ||
|
|
377
|
+
modifyOrderInput.updateBillingAddress;
|
|
378
|
+
|
|
379
|
+
if (!pendingOrder) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// On successful state transition, invalidate the order detail query and navigate to the order detail page
|
|
384
|
+
const onSuccess = () => {
|
|
385
|
+
const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
|
|
386
|
+
queryClient.invalidateQueries({ queryKey });
|
|
387
|
+
navigate({ to: `/orders/$id`, params: { id: entity?.id } });
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const handleCancelModificationClick = async () => {
|
|
391
|
+
const transitionError = await transitionToPreModifyingState();
|
|
392
|
+
if (!transitionError) {
|
|
393
|
+
onSuccess();
|
|
394
|
+
} else {
|
|
395
|
+
selectNextState({ onSuccess });
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const handleModificationSubmit = async (priceDifference?: number) => {
|
|
400
|
+
const transitionError =
|
|
401
|
+
priceDifference && priceDifference > 0
|
|
402
|
+
? await transitionToState('ArrangingAdditionalPayment')
|
|
403
|
+
: await transitionToPreModifyingState();
|
|
404
|
+
if (!transitionError) {
|
|
405
|
+
const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
|
|
406
|
+
await queryClient.invalidateQueries({ queryKey });
|
|
407
|
+
setPreviewOpen(false);
|
|
408
|
+
await navigate({ to: `/orders/$id`, params: { id: entity?.id } });
|
|
409
|
+
} else {
|
|
410
|
+
selectNextState({ onSuccess });
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const shippingAddress = modifyOrderInput.updateShippingAddress ?? entity.shippingAddress;
|
|
415
|
+
const billingAddress = modifyOrderInput.updateBillingAddress ?? entity.billingAddress;
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
|
|
419
|
+
<PageTitle>
|
|
420
|
+
<Trans>Modify order</Trans>
|
|
421
|
+
</PageTitle>
|
|
422
|
+
<PageActionBar>
|
|
423
|
+
<PageActionBarRight>
|
|
424
|
+
<Button type="button" variant="secondary" onClick={handleCancelModificationClick}>
|
|
425
|
+
<Trans>Cancel modification</Trans>
|
|
426
|
+
</Button>
|
|
427
|
+
</PageActionBarRight>
|
|
428
|
+
</PageActionBar>
|
|
429
|
+
<PageLayout>
|
|
430
|
+
<PageBlock column="main" blockId="order-lines" title={<Trans>Order lines</Trans>}>
|
|
431
|
+
<EditOrderTable
|
|
432
|
+
order={pendingOrder}
|
|
433
|
+
eligibleShippingMethods={
|
|
434
|
+
eligibleShippingMethods?.eligibleShippingMethodsForDraftOrder ?? []
|
|
435
|
+
}
|
|
436
|
+
onAddItem={handleAddItem}
|
|
437
|
+
onAdjustLine={handleAdjustLine}
|
|
438
|
+
onRemoveLine={handleRemoveLine}
|
|
439
|
+
onSetShippingMethod={handleSetShippingMethod}
|
|
440
|
+
onApplyCouponCode={handleApplyCouponCode}
|
|
441
|
+
onRemoveCouponCode={handleRemoveCouponCode}
|
|
442
|
+
displayTotals={false}
|
|
443
|
+
/>
|
|
444
|
+
</PageBlock>
|
|
445
|
+
<PageBlock
|
|
446
|
+
column="side"
|
|
447
|
+
blockId="modification-summary"
|
|
448
|
+
title={<Trans>Summary of modifications</Trans>}
|
|
449
|
+
>
|
|
450
|
+
<OrderModificationSummary
|
|
451
|
+
originalOrder={entity}
|
|
452
|
+
modifyOrderInput={modifyOrderInput}
|
|
453
|
+
addedVariants={addedVariants}
|
|
454
|
+
eligibleShippingMethods={
|
|
455
|
+
eligibleShippingMethods?.eligibleShippingMethodsForDraftOrder?.map(m => ({
|
|
456
|
+
id: m.id,
|
|
457
|
+
name: m.name,
|
|
458
|
+
})) ?? []
|
|
459
|
+
}
|
|
460
|
+
/>
|
|
461
|
+
<div className="mt-4 flex justify-end">
|
|
462
|
+
<Button
|
|
463
|
+
type="button"
|
|
464
|
+
onClick={() => setPreviewOpen(true)}
|
|
465
|
+
disabled={!hasModifications}
|
|
466
|
+
>
|
|
467
|
+
<Trans>Preview changes</Trans>
|
|
468
|
+
</Button>
|
|
469
|
+
</div>
|
|
470
|
+
<OrderModificationPreviewDialog
|
|
471
|
+
open={previewOpen}
|
|
472
|
+
onOpenChange={setPreviewOpen}
|
|
473
|
+
orderSnapshot={entity}
|
|
474
|
+
modifyOrderInput={modifyOrderInput}
|
|
475
|
+
onResolve={handleModificationSubmit}
|
|
476
|
+
/>
|
|
477
|
+
</PageBlock>
|
|
478
|
+
<PageBlock column="side" blockId="customer" title={<Trans>Customer</Trans>}>
|
|
479
|
+
{entity.customer ? (
|
|
480
|
+
<Button variant="ghost" asChild>
|
|
481
|
+
<Link to={`/customers/${entity?.customer?.id}`}>
|
|
482
|
+
<User className="w-4 h-4" />
|
|
483
|
+
{entity?.customer?.firstName} {entity?.customer?.lastName}
|
|
484
|
+
</Link>
|
|
485
|
+
</Button>
|
|
486
|
+
) : (
|
|
487
|
+
<div className="text-muted-foreground text-xs font-medium p-3 border rounded-md">
|
|
488
|
+
<Trans>No customer</Trans>
|
|
489
|
+
</div>
|
|
490
|
+
)}
|
|
491
|
+
</PageBlock>
|
|
492
|
+
<PageBlock column="side" blockId="addresses" title={<Trans>Addresses</Trans>}>
|
|
493
|
+
<div className="mb-4">
|
|
494
|
+
<div className="mb-1">
|
|
495
|
+
<Trans>Shipping address</Trans>:
|
|
496
|
+
<Button
|
|
497
|
+
variant="ghost"
|
|
498
|
+
size="sm"
|
|
499
|
+
className="ml-2"
|
|
500
|
+
onClick={() => setEditingShippingAddress(true)}
|
|
501
|
+
>
|
|
502
|
+
<Trans>Edit</Trans>
|
|
503
|
+
</Button>
|
|
504
|
+
</div>
|
|
505
|
+
{editingShippingAddress ? (
|
|
506
|
+
<CustomerAddressSelector
|
|
507
|
+
customerId={entity.customer?.id}
|
|
508
|
+
onSelect={handleSelectShippingAddress}
|
|
509
|
+
/>
|
|
510
|
+
) : null}
|
|
511
|
+
{shippingAddress && !editingShippingAddress ? (
|
|
512
|
+
<OrderAddress address={shippingAddress} />
|
|
513
|
+
) : (
|
|
514
|
+
<div className="text-muted-foreground text-xs font-medium">
|
|
515
|
+
<Trans>No shipping address</Trans>
|
|
516
|
+
</div>
|
|
517
|
+
)}
|
|
518
|
+
</div>
|
|
519
|
+
<div>
|
|
520
|
+
<div className="mb-1">
|
|
521
|
+
<Trans>Billing address</Trans>:
|
|
522
|
+
<Button
|
|
523
|
+
variant="ghost"
|
|
524
|
+
size="sm"
|
|
525
|
+
className="ml-2"
|
|
526
|
+
onClick={() => setEditingBillingAddress(true)}
|
|
527
|
+
>
|
|
528
|
+
<Trans>Edit</Trans>
|
|
529
|
+
</Button>
|
|
530
|
+
</div>
|
|
531
|
+
{editingBillingAddress ? (
|
|
532
|
+
<CustomerAddressSelector
|
|
533
|
+
customerId={entity.customer?.id}
|
|
534
|
+
onSelect={handleSelectBillingAddress}
|
|
535
|
+
/>
|
|
536
|
+
) : null}
|
|
537
|
+
{billingAddress && !editingBillingAddress ? (
|
|
538
|
+
<OrderAddress address={billingAddress} />
|
|
539
|
+
) : (
|
|
540
|
+
<div className="text-muted-foreground text-xs font-medium">
|
|
541
|
+
<Trans>No billing address</Trans>
|
|
542
|
+
</div>
|
|
543
|
+
)}
|
|
544
|
+
</div>
|
|
545
|
+
</PageBlock>
|
|
546
|
+
</PageLayout>
|
|
547
|
+
<ManuallySelectNextState availableStates={entity.nextStates} />
|
|
548
|
+
</Page>
|
|
549
|
+
);
|
|
550
|
+
}
|
|
@@ -90,22 +90,6 @@ function DraftOrderPage() {
|
|
|
90
90
|
params: { id: params.id },
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
const { form: orderLineForm } = useGeneratedForm({
|
|
94
|
-
document: addCustomFields(adjustDraftOrderLineDocument),
|
|
95
|
-
varName: undefined,
|
|
96
|
-
entity: entity?.lines[0],
|
|
97
|
-
setValues: entity => {
|
|
98
|
-
return {
|
|
99
|
-
orderId: entity.id,
|
|
100
|
-
input: {
|
|
101
|
-
quantity: entity.quantity,
|
|
102
|
-
orderLineId: entity.id,
|
|
103
|
-
customFields: entity.customFields,
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
|
|
109
93
|
const { form: orderCustomFieldsForm } = useGeneratedForm({
|
|
110
94
|
document: setDraftOrderCustomFieldsDocument,
|
|
111
95
|
varName: undefined,
|
|
@@ -265,7 +249,6 @@ function DraftOrderPage() {
|
|
|
265
249
|
const { mutate: removeCouponCodeForDraftOrder } = useMutation({
|
|
266
250
|
mutationFn: api.mutate(removeCouponCodeFromDraftOrderDocument),
|
|
267
251
|
onSuccess: (result: ResultOf<typeof removeCouponCodeFromDraftOrderDocument>) => {
|
|
268
|
-
const order = result.removeCouponCodeFromDraftOrder;
|
|
269
252
|
toast.success(i18n.t('Coupon code removed from order'));
|
|
270
253
|
refreshEntity();
|
|
271
254
|
},
|
|
@@ -396,7 +379,6 @@ function DraftOrderPage() {
|
|
|
396
379
|
couponCode: e.couponCode,
|
|
397
380
|
})
|
|
398
381
|
}
|
|
399
|
-
orderLineForm={orderLineForm}
|
|
400
382
|
/>
|
|
401
383
|
</PageBlock>
|
|
402
384
|
<PageBlock column="main" blockId="order-custom-fields" title={<Trans>Custom fields</Trans>}>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { ResultOf } from '@/vdb/graphql/graphql.js';
|
|
1
|
+
import { FragmentOf, ResultOf } from '@/vdb/graphql/graphql.js';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { addressFragment } from '../../_customers/customers.graphql.js';
|
|
4
|
+
import { orderAddressFragment, orderDetailDocument } from '../orders.graphql.js';
|
|
4
5
|
|
|
5
6
|
export type Order = NonNullable<ResultOf<typeof orderDetailDocument>['order']>;
|
|
6
7
|
export type Payment = NonNullable<NonNullable<Order>['payments']>[number];
|
|
7
8
|
export type Fulfillment = NonNullable<NonNullable<Order>['fulfillments']>[number];
|
|
9
|
+
export type OrderAddressFragment = FragmentOf<typeof orderAddressFragment>;
|
|
10
|
+
export type AddressFragment = FragmentOf<typeof addressFragment>;
|
|
@@ -64,9 +64,10 @@ export function canAddFulfillment(order: Order): boolean {
|
|
|
64
64
|
|
|
65
65
|
// Check if order is in a fulfillable state
|
|
66
66
|
const isFulfillableState =
|
|
67
|
-
order.nextStates.includes('Shipped') ||
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
(order.nextStates.includes('Shipped') ||
|
|
68
|
+
order.nextStates.includes('PartiallyShipped') ||
|
|
69
|
+
order.nextStates.includes('Delivered')) &&
|
|
70
|
+
order.state !== 'ArrangingAdditionalPayment';
|
|
70
71
|
|
|
71
72
|
return (
|
|
72
73
|
!allItemsFulfilled &&
|
|
@@ -37,7 +37,7 @@ export function OptionValueInput({
|
|
|
37
37
|
groupIndex,
|
|
38
38
|
disabled = false,
|
|
39
39
|
}: Readonly<OptionValueInputProps>) {
|
|
40
|
-
const { control
|
|
40
|
+
const { control } = useFormContext<FormValues>();
|
|
41
41
|
const { fields, append, remove } = useFieldArray({
|
|
42
42
|
control,
|
|
43
43
|
name: `optionGroups.${groupIndex}.values`,
|
|
@@ -28,13 +28,6 @@ export function ZoneCountriesTable({ zoneId, canAddCountries = false }: Readonly
|
|
|
28
28
|
},
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
const { mutate: removeCountryFromZone } = useMutation({
|
|
32
|
-
mutationFn: api.mutate(removeCountryFromZoneMutation),
|
|
33
|
-
onSuccess: () => {
|
|
34
|
-
refetch();
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
|
|
38
31
|
const [page, setPage] = useState(1);
|
|
39
32
|
const [pageSize, setPageSize] = useState(10);
|
|
40
33
|
|
|
@@ -3,11 +3,17 @@ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
|
3
3
|
export function DateTime({ value }: Readonly<{ value: string | Date }>) {
|
|
4
4
|
const { formatDate } = useLocalFormat();
|
|
5
5
|
let renderedDate: string;
|
|
6
|
+
let renderedTime: string;
|
|
6
7
|
try {
|
|
7
8
|
renderedDate = formatDate(value);
|
|
9
|
+
renderedTime = formatDate(value, { timeStyle: 'long' });
|
|
8
10
|
} catch (e) {
|
|
9
11
|
renderedDate = value.toString();
|
|
12
|
+
renderedTime = '';
|
|
10
13
|
console.error(e);
|
|
11
14
|
}
|
|
12
|
-
return
|
|
15
|
+
return <div className="flex flex-col">
|
|
16
|
+
<div className="text-sm">{renderedDate}</div>
|
|
17
|
+
<div className="text-xs text-muted-foreground">{renderedTime}</div>
|
|
18
|
+
</div>;
|
|
13
19
|
}
|