@vendure/dashboard 3.3.6-master-202507090236 → 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
package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { MoneyInput } from '@/vdb/components/data-input/money-input.js';
|
|
2
|
+
import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
|
|
3
|
+
import { Alert, AlertDescription, AlertTitle } from '@/vdb/components/ui/alert.js';
|
|
4
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/vdb/components/ui/card.js';
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogClose,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from '@/vdb/components/ui/dialog.js';
|
|
15
|
+
import { FormControl, FormField } from '@/vdb/components/ui/form.js';
|
|
16
|
+
import { Textarea } from '@/vdb/components/ui/textarea.js';
|
|
17
|
+
import { addCustomFields } from '@/vdb/framework/document-introspection/add-custom-fields.js';
|
|
18
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
19
|
+
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
20
|
+
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
21
|
+
import { useMutation } from '@tanstack/react-query';
|
|
22
|
+
import { ResultOf, VariablesOf } from 'gql.tada';
|
|
23
|
+
import { CheckIcon } from 'lucide-react';
|
|
24
|
+
import { useEffect, useRef } from 'react';
|
|
25
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
26
|
+
import { modifyOrderDocument, orderDetailDocument } from '../orders.graphql.js';
|
|
27
|
+
import { OrderTable } from './order-table.js';
|
|
28
|
+
|
|
29
|
+
export type OrderFragment = NonNullable<ResultOf<typeof orderDetailDocument>['order']>;
|
|
30
|
+
export type ModifyOrderInput = VariablesOf<typeof modifyOrderDocument>['input'];
|
|
31
|
+
|
|
32
|
+
interface OrderModificationPreviewDialogProps {
|
|
33
|
+
open: boolean;
|
|
34
|
+
onOpenChange: (open: boolean) => void;
|
|
35
|
+
orderSnapshot: OrderFragment;
|
|
36
|
+
modifyOrderInput: ModifyOrderInput;
|
|
37
|
+
/**
|
|
38
|
+
* The price difference between the order snapshot and the preview order.
|
|
39
|
+
* If the dialog is cancelled, this will be undefined.
|
|
40
|
+
*/
|
|
41
|
+
onResolve: (priceDifference?: number) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface PaymentRefundForm {
|
|
45
|
+
payments: Record<string, number>;
|
|
46
|
+
note: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function OrderModificationPreviewDialog({
|
|
50
|
+
open,
|
|
51
|
+
onOpenChange,
|
|
52
|
+
orderSnapshot,
|
|
53
|
+
modifyOrderInput,
|
|
54
|
+
onResolve,
|
|
55
|
+
}: Readonly<OrderModificationPreviewDialogProps>) {
|
|
56
|
+
const { i18n } = useLingui();
|
|
57
|
+
const { formatCurrency } = useLocalFormat();
|
|
58
|
+
// Use a ref to track the last input sent to avoid duplicate calls
|
|
59
|
+
const lastInputRef = useRef<ModifyOrderInput | null>(null);
|
|
60
|
+
const previewMutation = useMutation({
|
|
61
|
+
mutationFn: api.mutate(addCustomFields(modifyOrderDocument)),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Create form with dynamic fields for each payment
|
|
65
|
+
const refundForm = useForm<PaymentRefundForm>({
|
|
66
|
+
defaultValues: {
|
|
67
|
+
note: '',
|
|
68
|
+
payments:
|
|
69
|
+
orderSnapshot.payments?.reduce(
|
|
70
|
+
(acc, payment) => {
|
|
71
|
+
acc[payment.id] = 0;
|
|
72
|
+
return acc;
|
|
73
|
+
},
|
|
74
|
+
{} as Record<string, number>,
|
|
75
|
+
) || {},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const confirmMutation = useMutation({
|
|
80
|
+
mutationFn: (input: ModifyOrderInput) => api.mutate(addCustomFields(modifyOrderDocument))({ input }),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Trigger preview when dialog opens or input changes (while open)
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (open) {
|
|
86
|
+
// Only trigger if input actually changed
|
|
87
|
+
if (
|
|
88
|
+
!lastInputRef.current ||
|
|
89
|
+
JSON.stringify(lastInputRef.current) !== JSON.stringify(modifyOrderInput)
|
|
90
|
+
) {
|
|
91
|
+
const input = { ...modifyOrderInput, dryRun: true };
|
|
92
|
+
previewMutation.mutate({ input });
|
|
93
|
+
lastInputRef.current = modifyOrderInput;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, [open, modifyOrderInput]);
|
|
97
|
+
|
|
98
|
+
const previewOrder =
|
|
99
|
+
previewMutation.data?.modifyOrder?.__typename === 'Order' ? previewMutation.data.modifyOrder : null;
|
|
100
|
+
const error =
|
|
101
|
+
previewMutation.data && previewMutation.data.modifyOrder?.__typename !== 'Order'
|
|
102
|
+
? previewMutation.data.modifyOrder?.message || i18n.t('Unknown error')
|
|
103
|
+
: previewMutation.error?.message || null;
|
|
104
|
+
const loading = previewMutation.isPending;
|
|
105
|
+
|
|
106
|
+
// Price difference
|
|
107
|
+
const priceDifference = previewOrder ? previewOrder.totalWithTax - orderSnapshot.totalWithTax : 0;
|
|
108
|
+
const formattedDiff = previewOrder
|
|
109
|
+
? formatCurrency(Math.abs(priceDifference), previewOrder.currencyCode)
|
|
110
|
+
: '';
|
|
111
|
+
|
|
112
|
+
// Calculate total refund amount from form
|
|
113
|
+
const totalRefundAmount =
|
|
114
|
+
orderSnapshot.payments?.reduce((total, payment) => {
|
|
115
|
+
const refundAmount = refundForm.watch(`payments.${payment.id}`) || 0;
|
|
116
|
+
return total + refundAmount;
|
|
117
|
+
}, 0) || 0;
|
|
118
|
+
|
|
119
|
+
// Check if total refund matches the required amount
|
|
120
|
+
const isRefundComplete = priceDifference > 0 || totalRefundAmount >= Math.abs(priceDifference);
|
|
121
|
+
const remainingAmount = Math.abs(priceDifference) - totalRefundAmount;
|
|
122
|
+
|
|
123
|
+
// Confirm handler
|
|
124
|
+
const handleConfirm = async () => {
|
|
125
|
+
if (!previewOrder) return;
|
|
126
|
+
const input: ModifyOrderInput = { ...modifyOrderInput, dryRun: false };
|
|
127
|
+
if (priceDifference < 0) {
|
|
128
|
+
// Create refunds array from form data
|
|
129
|
+
const { note, payments } = refundForm.getValues();
|
|
130
|
+
const refunds = Object.entries(payments)
|
|
131
|
+
.filter(([_, amount]) => amount > 0)
|
|
132
|
+
.map(([paymentId, amount]) => ({
|
|
133
|
+
paymentId,
|
|
134
|
+
amount,
|
|
135
|
+
reason: note,
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
input.refunds = refunds;
|
|
139
|
+
}
|
|
140
|
+
await confirmMutation.mutateAsync(input);
|
|
141
|
+
onResolve(priceDifference);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
146
|
+
<DialogContent className="min-w-[80vw] max-h-[90vh] p-8 overflow-hidden flex flex-col">
|
|
147
|
+
<DialogHeader>
|
|
148
|
+
<DialogTitle>
|
|
149
|
+
<Trans>Preview order modifications</Trans>
|
|
150
|
+
</DialogTitle>
|
|
151
|
+
<DialogDescription>
|
|
152
|
+
<Trans>Review the changes before applying them to the order.</Trans>
|
|
153
|
+
</DialogDescription>
|
|
154
|
+
</DialogHeader>
|
|
155
|
+
<div className="space-y-4 flex-1 overflow-y-auto">
|
|
156
|
+
{loading && (
|
|
157
|
+
<div className="text-center py-8">
|
|
158
|
+
<Trans>Loading preview…</Trans>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
{error && <div className="text-destructive py-2">{error}</div>}
|
|
162
|
+
{previewOrder && !loading && !error && (
|
|
163
|
+
<>
|
|
164
|
+
<OrderTable order={previewOrder} />
|
|
165
|
+
{/* Refund/payment UI using Alert */}
|
|
166
|
+
{priceDifference < 0 && (
|
|
167
|
+
<>
|
|
168
|
+
<Alert variant="destructive">
|
|
169
|
+
<AlertTitle>
|
|
170
|
+
<Trans>Refund required</Trans>
|
|
171
|
+
</AlertTitle>
|
|
172
|
+
<AlertDescription>
|
|
173
|
+
<Trans>
|
|
174
|
+
A refund of {formattedDiff} is required. Select payment
|
|
175
|
+
amounts and enter a note to proceed.
|
|
176
|
+
</Trans>
|
|
177
|
+
</AlertDescription>
|
|
178
|
+
</Alert>
|
|
179
|
+
<FormProvider {...refundForm}>
|
|
180
|
+
<form className="space-y-4 mt-4">
|
|
181
|
+
{/* Payment Cards */}
|
|
182
|
+
<div className="space-y-3">
|
|
183
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
184
|
+
{orderSnapshot.payments?.map(payment => (
|
|
185
|
+
<Card key={payment.id} className="">
|
|
186
|
+
<CardHeader className="">
|
|
187
|
+
<CardTitle className="flex gap-2 items-baseline">
|
|
188
|
+
<span className="">{payment.method}</span>
|
|
189
|
+
<span className="text-xs text-muted-foreground">
|
|
190
|
+
ID: {payment.transactionId}
|
|
191
|
+
</span>
|
|
192
|
+
</CardTitle>
|
|
193
|
+
<Button
|
|
194
|
+
type="button"
|
|
195
|
+
size="sm"
|
|
196
|
+
variant="outline"
|
|
197
|
+
className=""
|
|
198
|
+
onClick={() => {
|
|
199
|
+
const currentRefundAmount =
|
|
200
|
+
refundForm.getValues(
|
|
201
|
+
`payments.${payment.id}`,
|
|
202
|
+
) || 0;
|
|
203
|
+
const availableAmount =
|
|
204
|
+
payment.amount;
|
|
205
|
+
const remainingNeeded =
|
|
206
|
+
Math.abs(priceDifference) -
|
|
207
|
+
totalRefundAmount;
|
|
208
|
+
|
|
209
|
+
// Calculate how much we can still refund from this payment method
|
|
210
|
+
const remainingFromThisPayment =
|
|
211
|
+
availableAmount -
|
|
212
|
+
currentRefundAmount;
|
|
213
|
+
|
|
214
|
+
// Take the minimum of what's needed and what's available
|
|
215
|
+
const amountToAdd = Math.min(
|
|
216
|
+
remainingFromThisPayment,
|
|
217
|
+
remainingNeeded,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (amountToAdd > 0) {
|
|
221
|
+
refundForm.setValue(
|
|
222
|
+
`payments.${payment.id}`,
|
|
223
|
+
currentRefundAmount +
|
|
224
|
+
amountToAdd,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
<Trans>Refund from this method</Trans>
|
|
230
|
+
</Button>
|
|
231
|
+
</CardHeader>
|
|
232
|
+
<CardContent className="pt-0">
|
|
233
|
+
<div className="flex flex-col gap-3">
|
|
234
|
+
<div className="text-sm text-muted-foreground">
|
|
235
|
+
<Trans>Available:</Trans>{' '}
|
|
236
|
+
{formatCurrency(
|
|
237
|
+
payment.amount,
|
|
238
|
+
orderSnapshot.currencyCode,
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
<div className="w-full">
|
|
242
|
+
<FormField
|
|
243
|
+
name={`payments.${payment.id}`}
|
|
244
|
+
control={refundForm.control}
|
|
245
|
+
render={({ field }) => (
|
|
246
|
+
<FormControl>
|
|
247
|
+
<MoneyInput
|
|
248
|
+
value={
|
|
249
|
+
field.value || 0
|
|
250
|
+
}
|
|
251
|
+
onChange={
|
|
252
|
+
field.onChange
|
|
253
|
+
}
|
|
254
|
+
currency={
|
|
255
|
+
orderSnapshot.currencyCode
|
|
256
|
+
}
|
|
257
|
+
/>
|
|
258
|
+
</FormControl>
|
|
259
|
+
)}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</CardContent>
|
|
264
|
+
</Card>
|
|
265
|
+
))}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Total Refund Summary */}
|
|
270
|
+
<div className="bg-muted/50 rounded-lg pb-3">
|
|
271
|
+
<div className="flex items-center justify-between p-3">
|
|
272
|
+
<div className="flex items-center gap-2">
|
|
273
|
+
<span className="text-sm font-medium">
|
|
274
|
+
<Trans>Total refund:</Trans>
|
|
275
|
+
</span>
|
|
276
|
+
<span className="text-sm">
|
|
277
|
+
{formatCurrency(
|
|
278
|
+
totalRefundAmount,
|
|
279
|
+
orderSnapshot.currencyCode,
|
|
280
|
+
)}
|
|
281
|
+
</span>
|
|
282
|
+
{isRefundComplete && (
|
|
283
|
+
<CheckIcon className="h-4 w-4 text-green-600" />
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
{!isRefundComplete && (
|
|
287
|
+
<div className="text-sm text-muted-foreground">
|
|
288
|
+
<Trans>Remaining:</Trans>{' '}
|
|
289
|
+
{formatCurrency(
|
|
290
|
+
remainingAmount,
|
|
291
|
+
orderSnapshot.currencyCode,
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
</div>
|
|
296
|
+
<div className="px-3">
|
|
297
|
+
<FormFieldWrapper
|
|
298
|
+
name="note"
|
|
299
|
+
control={refundForm.control}
|
|
300
|
+
rules={{ required: true }}
|
|
301
|
+
render={({ field }) => (
|
|
302
|
+
<Textarea
|
|
303
|
+
{...field}
|
|
304
|
+
className="bg-background"
|
|
305
|
+
placeholder={i18n.t('Enter refund note')}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
</form>
|
|
312
|
+
</FormProvider>
|
|
313
|
+
</>
|
|
314
|
+
)}
|
|
315
|
+
<div className="w-full flex justify-end">
|
|
316
|
+
<div className="max-w-l mb-2">
|
|
317
|
+
{priceDifference > 0 && (
|
|
318
|
+
<Alert variant="destructive">
|
|
319
|
+
<AlertTitle>
|
|
320
|
+
<Trans>Additional payment required</Trans>
|
|
321
|
+
</AlertTitle>
|
|
322
|
+
<AlertDescription>
|
|
323
|
+
<Trans>
|
|
324
|
+
An additional payment of {formattedDiff} will be required.
|
|
325
|
+
</Trans>
|
|
326
|
+
</AlertDescription>
|
|
327
|
+
</Alert>
|
|
328
|
+
)}
|
|
329
|
+
{priceDifference === 0 && (
|
|
330
|
+
<Alert variant="default">
|
|
331
|
+
<AlertTitle>
|
|
332
|
+
<Trans>No payment or refund required</Trans>
|
|
333
|
+
</AlertTitle>
|
|
334
|
+
<AlertDescription>
|
|
335
|
+
<Trans>
|
|
336
|
+
No payment or refund is required for these changes.
|
|
337
|
+
</Trans>
|
|
338
|
+
</AlertDescription>
|
|
339
|
+
</Alert>
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</>
|
|
344
|
+
)}
|
|
345
|
+
</div>
|
|
346
|
+
<DialogFooter>
|
|
347
|
+
<DialogClose asChild>
|
|
348
|
+
<Button type="button" variant="secondary" onClick={() => onResolve()}>
|
|
349
|
+
<Trans>Cancel</Trans>
|
|
350
|
+
</Button>
|
|
351
|
+
</DialogClose>
|
|
352
|
+
<Button
|
|
353
|
+
type="button"
|
|
354
|
+
variant="default"
|
|
355
|
+
onClick={handleConfirm}
|
|
356
|
+
disabled={loading || !!error || confirmMutation.isPending || !isRefundComplete}
|
|
357
|
+
>
|
|
358
|
+
<Trans>Confirm</Trans>
|
|
359
|
+
</Button>
|
|
360
|
+
</DialogFooter>
|
|
361
|
+
</DialogContent>
|
|
362
|
+
</Dialog>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
2
|
+
import { ResultOf, VariablesOf } from 'gql.tada';
|
|
3
|
+
import { modifyOrderDocument, orderDetailDocument } from '../orders.graphql.js';
|
|
4
|
+
|
|
5
|
+
type OrderFragment = NonNullable<ResultOf<typeof orderDetailDocument>['order']>;
|
|
6
|
+
type ModifyOrderInput = VariablesOf<typeof modifyOrderDocument>['input'];
|
|
7
|
+
|
|
8
|
+
type OrderLine = OrderFragment['lines'][number];
|
|
9
|
+
|
|
10
|
+
interface OrderModificationSummaryProps {
|
|
11
|
+
originalOrder: OrderFragment;
|
|
12
|
+
modifyOrderInput: ModifyOrderInput;
|
|
13
|
+
addedVariants: Map<string, any>; // For displaying added line info
|
|
14
|
+
eligibleShippingMethods: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface LineAdjustment {
|
|
21
|
+
orderLineId: string;
|
|
22
|
+
name: string;
|
|
23
|
+
oldQuantity: number;
|
|
24
|
+
newQuantity: number;
|
|
25
|
+
oldCustomFields: Record<string, any>;
|
|
26
|
+
newCustomFields: Record<string, any>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function OrderModificationSummary({
|
|
30
|
+
originalOrder,
|
|
31
|
+
modifyOrderInput,
|
|
32
|
+
addedVariants,
|
|
33
|
+
eligibleShippingMethods,
|
|
34
|
+
}: Readonly<OrderModificationSummaryProps>) {
|
|
35
|
+
// Map by line id for quick lookup
|
|
36
|
+
const originalLineMap = new Map(originalOrder.lines.map(line => [line.id, line]));
|
|
37
|
+
|
|
38
|
+
// Adjusted lines: in both, but quantity changed
|
|
39
|
+
const adjustedLines = (modifyOrderInput.adjustOrderLines ?? [])
|
|
40
|
+
.map((adj: NonNullable<ModifyOrderInput['adjustOrderLines']>[number] & { customFields?: any }) => {
|
|
41
|
+
const orig = originalLineMap.get(adj.orderLineId);
|
|
42
|
+
if (
|
|
43
|
+
orig &&
|
|
44
|
+
(adj.quantity !== orig.quantity ||
|
|
45
|
+
JSON.stringify(adj.customFields) !== JSON.stringify((orig as any).customFields))
|
|
46
|
+
) {
|
|
47
|
+
return {
|
|
48
|
+
orderLineId: adj.orderLineId,
|
|
49
|
+
name: orig.productVariant.name,
|
|
50
|
+
oldQuantity: orig.quantity,
|
|
51
|
+
newQuantity: adj.quantity,
|
|
52
|
+
oldCustomFields: (orig as any).customFields,
|
|
53
|
+
newCustomFields: adj.customFields,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
})
|
|
58
|
+
.filter(Boolean) as Array<LineAdjustment>;
|
|
59
|
+
|
|
60
|
+
// Added lines: from addItems
|
|
61
|
+
const addedLines = (modifyOrderInput.addItems ?? [])
|
|
62
|
+
.map(item => {
|
|
63
|
+
const variantInfo = addedVariants.get(item.productVariantId);
|
|
64
|
+
return variantInfo
|
|
65
|
+
? {
|
|
66
|
+
id: `added-${item.productVariantId}`,
|
|
67
|
+
name: variantInfo.productVariantName,
|
|
68
|
+
quantity: item.quantity,
|
|
69
|
+
}
|
|
70
|
+
: null;
|
|
71
|
+
})
|
|
72
|
+
.filter(Boolean) as Array<{ id: string; name: string; quantity: number }>;
|
|
73
|
+
|
|
74
|
+
// Removed lines: quantity set to 0 in adjustOrderLines
|
|
75
|
+
const removedLines = (modifyOrderInput.adjustOrderLines ?? [])
|
|
76
|
+
.map(adj => {
|
|
77
|
+
const orig = originalLineMap.get(adj.orderLineId);
|
|
78
|
+
if (orig && adj.quantity === 0) {
|
|
79
|
+
return orig;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
})
|
|
83
|
+
.filter(Boolean) as OrderLine[];
|
|
84
|
+
|
|
85
|
+
// Coupon code changes
|
|
86
|
+
const originalCoupons = new Set(originalOrder.couponCodes ?? []);
|
|
87
|
+
const modifiedCoupons = new Set(modifyOrderInput.couponCodes ?? []);
|
|
88
|
+
const addedCouponCodes = Array.from(modifiedCoupons).filter(code => !originalCoupons.has(code));
|
|
89
|
+
const removedCouponCodes = Array.from(originalCoupons).filter(code => !modifiedCoupons.has(code));
|
|
90
|
+
|
|
91
|
+
// Shipping method change detection
|
|
92
|
+
const originalShippingMethodId = originalOrder.shippingLines?.[0]?.shippingMethod?.id;
|
|
93
|
+
const modifiedShippingMethodId = modifyOrderInput.shippingMethodIds?.[0];
|
|
94
|
+
let shippingMethodChanged = false;
|
|
95
|
+
let newShippingMethodName = '';
|
|
96
|
+
if (modifiedShippingMethodId && modifiedShippingMethodId !== originalShippingMethodId) {
|
|
97
|
+
shippingMethodChanged = true;
|
|
98
|
+
newShippingMethodName =
|
|
99
|
+
eligibleShippingMethods.find(m => m.id === modifiedShippingMethodId)?.name ||
|
|
100
|
+
modifiedShippingMethodId;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="text-sm">
|
|
105
|
+
{/* Address changes */}
|
|
106
|
+
{modifyOrderInput.updateShippingAddress && (
|
|
107
|
+
<div className="mb-2">
|
|
108
|
+
<span className="font-medium">
|
|
109
|
+
<Trans>Shipping address changed</Trans>
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
{modifyOrderInput.updateBillingAddress && (
|
|
114
|
+
<div className="mb-2">
|
|
115
|
+
<span className="font-medium">
|
|
116
|
+
<Trans>Billing address changed</Trans>
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
{shippingMethodChanged && (
|
|
121
|
+
<div className="mb-2">
|
|
122
|
+
<span className="font-medium">
|
|
123
|
+
<Trans>Shipping method changed</Trans>: {newShippingMethodName}
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
{adjustedLines.length > 0 && (
|
|
128
|
+
<div className="mb-2">
|
|
129
|
+
<div className="font-medium">
|
|
130
|
+
<Trans>Adjusting {adjustedLines.length} lines</Trans>
|
|
131
|
+
</div>
|
|
132
|
+
<ul className="list-disc ml-4">
|
|
133
|
+
{adjustedLines.map(line => (
|
|
134
|
+
<li key={line.orderLineId} className="">
|
|
135
|
+
<div className="flex items-center gap-2">
|
|
136
|
+
{line.name}:{' '}
|
|
137
|
+
{line.oldQuantity !== line.newQuantity && (
|
|
138
|
+
<div>
|
|
139
|
+
<span className="text-muted-foreground">
|
|
140
|
+
{line.oldQuantity} →{' '}
|
|
141
|
+
</span>
|
|
142
|
+
|
|
143
|
+
{line.newQuantity}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
{JSON.stringify(line.oldCustomFields) !==
|
|
148
|
+
JSON.stringify(line.newCustomFields) && (
|
|
149
|
+
<span className="text-muted-foreground">
|
|
150
|
+
<Trans>Custom fields changed</Trans>
|
|
151
|
+
</span>
|
|
152
|
+
)}
|
|
153
|
+
</li>
|
|
154
|
+
))}
|
|
155
|
+
</ul>
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
{addedLines.length > 0 && (
|
|
159
|
+
<div className="mb-2">
|
|
160
|
+
<div className="font-medium">
|
|
161
|
+
<Trans>Adding {addedLines.length} item(s)</Trans>
|
|
162
|
+
</div>
|
|
163
|
+
<ul className="list-disc ml-4">
|
|
164
|
+
{addedLines.map(line => (
|
|
165
|
+
<li key={line.id}>
|
|
166
|
+
{line.name} x {line.quantity}
|
|
167
|
+
</li>
|
|
168
|
+
))}
|
|
169
|
+
</ul>
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
{removedLines.length > 0 && (
|
|
173
|
+
<div className="mb-2">
|
|
174
|
+
<div className="font-medium">
|
|
175
|
+
<Trans>Removing {removedLines.length} item(s)</Trans>
|
|
176
|
+
</div>
|
|
177
|
+
<ul className="list-disc ml-4">
|
|
178
|
+
{removedLines.map(line => (
|
|
179
|
+
<li key={line.id}>{line.productVariant.name}</li>
|
|
180
|
+
))}
|
|
181
|
+
</ul>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
{addedCouponCodes.length > 0 && (
|
|
185
|
+
<div className="mb-2">
|
|
186
|
+
<div className="font-medium">
|
|
187
|
+
<Trans>Added coupon codes</Trans>
|
|
188
|
+
</div>
|
|
189
|
+
<ul className="list-disc ml-4">
|
|
190
|
+
{addedCouponCodes.map(code => (
|
|
191
|
+
<li key={code}>{code}</li>
|
|
192
|
+
))}
|
|
193
|
+
</ul>
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
{removedCouponCodes.length > 0 && (
|
|
197
|
+
<div className="mb-2">
|
|
198
|
+
<div className="font-medium">
|
|
199
|
+
<Trans>Removed coupon codes</Trans>
|
|
200
|
+
</div>
|
|
201
|
+
<ul className="list-disc ml-4">
|
|
202
|
+
{removedCouponCodes.map(code => (
|
|
203
|
+
<li key={code}>{code}</li>
|
|
204
|
+
))}
|
|
205
|
+
</ul>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
{adjustedLines.length === 0 &&
|
|
209
|
+
addedLines.length === 0 &&
|
|
210
|
+
removedLines.length === 0 &&
|
|
211
|
+
addedCouponCodes.length === 0 &&
|
|
212
|
+
removedCouponCodes.length === 0 &&
|
|
213
|
+
!modifyOrderInput.updateShippingAddress &&
|
|
214
|
+
!modifyOrderInput.updateBillingAddress &&
|
|
215
|
+
!shippingMethodChanged && (
|
|
216
|
+
<div className="text-muted-foreground">
|
|
217
|
+
<Trans>No modifications made</Trans>
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|