@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.
Files changed (37) hide show
  1. package/dist/plugin/tests/barrel-exports.spec.js +1 -1
  2. package/package.json +4 -4
  3. package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +30 -37
  4. package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +33 -53
  5. package/src/app/routes/_authenticated/_orders/components/order-address.tsx +14 -7
  6. package/src/app/routes/_authenticated/_orders/components/order-line-custom-fields-form.tsx +23 -12
  7. package/src/app/routes/_authenticated/_orders/components/order-modification-preview-dialog.tsx +364 -0
  8. package/src/app/routes/_authenticated/_orders/components/order-modification-summary.tsx +222 -0
  9. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +146 -85
  10. package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +268 -31
  11. package/src/app/routes/_authenticated/_orders/components/settle-refund-dialog.tsx +80 -0
  12. package/src/app/routes/_authenticated/_orders/components/state-transition-control.tsx +102 -0
  13. package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +144 -0
  14. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +118 -2
  15. package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +144 -52
  16. package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +550 -0
  17. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +0 -17
  18. package/src/app/routes/_authenticated/_orders/utils/order-types.ts +5 -2
  19. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +4 -3
  20. package/src/app/routes/_authenticated/_products/products_.$id.tsx +0 -1
  21. package/src/lib/components/data-display/date-time.tsx +7 -1
  22. package/src/lib/components/data-input/relation-input.tsx +11 -0
  23. package/src/lib/components/data-input/relation-selector.tsx +9 -2
  24. package/src/lib/components/data-table/data-table-utils.ts +34 -0
  25. package/src/lib/components/data-table/data-table-view-options.tsx +2 -2
  26. package/src/lib/components/data-table/data-table.tsx +5 -2
  27. package/src/lib/components/data-table/use-generated-columns.tsx +307 -0
  28. package/src/lib/components/shared/paginated-list-data-table.tsx +15 -286
  29. package/src/lib/components/shared/product-variant-selector.tsx +28 -4
  30. package/src/lib/framework/component-registry/dynamic-component.tsx +3 -3
  31. package/src/lib/framework/document-introspection/get-document-structure.spec.ts +321 -2
  32. package/src/lib/framework/document-introspection/get-document-structure.ts +149 -31
  33. package/src/lib/framework/extension-api/types/layout.ts +21 -6
  34. package/src/lib/framework/layout-engine/layout-extensions.ts +1 -4
  35. package/src/lib/framework/layout-engine/page-layout.tsx +61 -10
  36. package/src/lib/framework/page/use-detail-page.ts +10 -7
  37. 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 { orderDetailDocument } from '../orders.graphql.js';
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
- order.nextStates.includes('PartiallyShipped') ||
69
- order.nextStates.includes('Delivered');
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 &&
@@ -158,7 +158,6 @@ function ProductDetailPage() {
158
158
  <AddProductVariantDialog
159
159
  productId={params.id}
160
160
  onSuccess={() => {
161
- console.log('onSuccess');
162
161
  refreshRef.current?.();
163
162
  }}
164
163
  />
@@ -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 renderedDate;
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
  }