@vendure/dashboard 3.3.6-master-202507100236 → 3.3.6-master-202507110238
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/package.json +4 -4
- 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 +268 -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 -17
- 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/products_.$id.tsx +0 -1
- 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/shared/paginated-list-data-table.tsx +15 -286
- package/src/lib/components/shared/product-variant-selector.tsx +28 -4
- package/src/lib/framework/component-registry/dynamic-component.tsx +3 -3
- 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/vite/tests/barrel-exports.spec.ts +1 -1
|
@@ -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,
|
|
@@ -396,7 +380,6 @@ function DraftOrderPage() {
|
|
|
396
380
|
couponCode: e.couponCode,
|
|
397
381
|
})
|
|
398
382
|
}
|
|
399
|
-
orderLineForm={orderLineForm}
|
|
400
383
|
/>
|
|
401
384
|
</PageBlock>
|
|
402
385
|
<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 &&
|
|
@@ -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
|
}
|
|
@@ -10,6 +10,12 @@ export interface SingleRelationInputProps<T = any> {
|
|
|
10
10
|
config: Parameters<typeof createRelationSelectorConfig<T>>[0];
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
className?: string;
|
|
13
|
+
/**
|
|
14
|
+
* @description
|
|
15
|
+
* Custom text for the selector label,
|
|
16
|
+
* defaults to `Select item` or `Select items`
|
|
17
|
+
*/
|
|
18
|
+
selectorLabel?: React.ReactNode;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export function SingleRelationInput<T>({
|
|
@@ -18,6 +24,7 @@ export function SingleRelationInput<T>({
|
|
|
18
24
|
config,
|
|
19
25
|
disabled,
|
|
20
26
|
className,
|
|
27
|
+
selectorLabel,
|
|
21
28
|
}: Readonly<SingleRelationInputProps<T>>) {
|
|
22
29
|
const singleConfig = createRelationSelectorConfig<T>({
|
|
23
30
|
...config,
|
|
@@ -28,6 +35,7 @@ export function SingleRelationInput<T>({
|
|
|
28
35
|
<RelationSelector
|
|
29
36
|
config={singleConfig}
|
|
30
37
|
value={value}
|
|
38
|
+
selectorLabel={selectorLabel}
|
|
31
39
|
onChange={newValue => onChange(newValue as string)}
|
|
32
40
|
disabled={disabled}
|
|
33
41
|
className={className}
|
|
@@ -44,6 +52,7 @@ export interface MultiRelationInputProps<T = any> {
|
|
|
44
52
|
config: Parameters<typeof createRelationSelectorConfig<T>>[0];
|
|
45
53
|
disabled?: boolean;
|
|
46
54
|
className?: string;
|
|
55
|
+
selectorLabel?: React.ReactNode;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
export function MultiRelationInput<T>({
|
|
@@ -52,6 +61,7 @@ export function MultiRelationInput<T>({
|
|
|
52
61
|
config,
|
|
53
62
|
disabled,
|
|
54
63
|
className,
|
|
64
|
+
selectorLabel,
|
|
55
65
|
}: Readonly<MultiRelationInputProps<T>>) {
|
|
56
66
|
const multiConfig = createRelationSelectorConfig<T>({
|
|
57
67
|
...config,
|
|
@@ -65,6 +75,7 @@ export function MultiRelationInput<T>({
|
|
|
65
75
|
onChange={newValue => onChange(newValue as string[])}
|
|
66
76
|
disabled={disabled}
|
|
67
77
|
className={className}
|
|
78
|
+
selectorLabel={selectorLabel}
|
|
68
79
|
/>
|
|
69
80
|
);
|
|
70
81
|
}
|