@wilshop/dashboard 3.5.5 → 3.5.7

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 (133) hide show
  1. package/dist/plugin/dashboard.plugin.d.ts +1 -1
  2. package/dist/plugin/dashboard.plugin.js +1 -1
  3. package/dist/vite/utils/compiler.js +50 -24
  4. package/dist/vite/utils/path-transformer.d.ts +20 -0
  5. package/dist/vite/utils/path-transformer.js +116 -0
  6. package/dist/vite/utils/plugin-discovery.js +3 -2
  7. package/dist/vite/utils/ui-config.js +15 -1
  8. package/dist/vite/vite-plugin-lingui-babel.d.ts +15 -2
  9. package/dist/vite/vite-plugin-lingui-babel.js +92 -17
  10. package/dist/vite/vite-plugin-translations.js +2 -2
  11. package/dist/vite/vite-plugin-ui-config.d.ts +31 -0
  12. package/package.json +10 -6
  13. package/src/app/common/delete-bulk-action.tsx +1 -1
  14. package/src/app/common/duplicate-bulk-action.tsx +1 -1
  15. package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -3
  16. package/src/app/routes/_authenticated/_collections/collections.tsx +169 -48
  17. package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +36 -5
  18. package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +1 -1
  19. package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +7 -1
  20. package/src/app/routes/_authenticated/_customers/components/customer-history/default-customer-history-components.tsx +31 -29
  21. package/src/app/routes/_authenticated/_customers/customers.graphql.ts +1 -0
  22. package/src/app/routes/_authenticated/_customers/customers.tsx +3 -0
  23. package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -1
  24. package/src/app/routes/_authenticated/_orders/components/draft-order-status.tsx +48 -0
  25. package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +8 -5
  26. package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +79 -54
  27. package/src/app/routes/_authenticated/_orders/components/order-history/default-order-history-components.tsx +43 -3
  28. package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +19 -3
  29. package/src/app/routes/_authenticated/_orders/components/order-table.tsx +1 -0
  30. package/src/app/routes/_authenticated/_orders/components/refund-order-dialog.tsx +372 -0
  31. package/src/app/routes/_authenticated/_orders/hooks/use-refund-order.ts +345 -0
  32. package/src/app/routes/_authenticated/_orders/orders.graphql.ts +41 -0
  33. package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +22 -6
  34. package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +51 -0
  35. package/src/app/routes/_authenticated/_orders/utils/refund-utils.ts +100 -0
  36. package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +1 -1
  37. package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +4 -1
  38. package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +7 -1
  39. package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +18 -2
  40. package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +1 -1
  41. package/src/app/routes/_authenticated/_product-variants/components/variant-price-detail.tsx +6 -2
  42. package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +9 -3
  43. package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +49 -30
  44. package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +1 -1
  45. package/src/app/routes/_authenticated/_profile/profile.graphql.ts +7 -0
  46. package/src/app/routes/_authenticated/_profile/profile.tsx +25 -1
  47. package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +7 -1
  48. package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +7 -1
  49. package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +18 -2
  50. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +7 -1
  51. package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +4 -1
  52. package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +14 -2
  53. package/src/i18n/common-strings.ts +7 -0
  54. package/src/i18n/locales/ar.po +669 -399
  55. package/src/i18n/locales/bg.po +1889 -46
  56. package/src/i18n/locales/cs.po +676 -406
  57. package/src/i18n/locales/de.po +676 -406
  58. package/src/i18n/locales/en.po +669 -399
  59. package/src/i18n/locales/es.po +676 -406
  60. package/src/i18n/locales/fa.po +676 -406
  61. package/src/i18n/locales/fr.po +676 -406
  62. package/src/i18n/locales/he.po +676 -406
  63. package/src/i18n/locales/hr.po +676 -406
  64. package/src/i18n/locales/it.po +676 -406
  65. package/src/i18n/locales/ja.po +676 -406
  66. package/src/i18n/locales/nb.po +676 -406
  67. package/src/i18n/locales/ne.po +676 -406
  68. package/src/i18n/locales/pl.po +676 -406
  69. package/src/i18n/locales/pt_BR.po +676 -406
  70. package/src/i18n/locales/pt_PT.po +676 -406
  71. package/src/i18n/locales/ru.po +676 -406
  72. package/src/i18n/locales/sv.po +676 -406
  73. package/src/i18n/locales/tr.po +676 -406
  74. package/src/i18n/locales/uk.po +676 -406
  75. package/src/i18n/locales/zh_Hans.po +676 -406
  76. package/src/i18n/locales/zh_Hant.po +676 -406
  77. package/src/lib/components/data-input/facet-value-input.tsx +2 -2
  78. package/src/lib/components/data-input/index.ts +1 -0
  79. package/src/lib/components/data-input/select-with-options.tsx +23 -7
  80. package/src/lib/components/data-input/struct-form-input.tsx +53 -21
  81. package/src/lib/components/data-input/text-input.tsx +1 -1
  82. package/src/lib/components/data-table/data-table-bulk-actions.tsx +2 -1
  83. package/src/lib/components/data-table/data-table-context.tsx +2 -10
  84. package/src/lib/components/data-table/data-table-utils.ts +34 -12
  85. package/src/lib/components/data-table/data-table.tsx +68 -30
  86. package/src/lib/components/data-table/global-views-bar.tsx +1 -1
  87. package/src/lib/components/data-table/my-views-button.tsx +1 -1
  88. package/src/lib/components/data-table/save-view-button.tsx +1 -1
  89. package/src/lib/components/data-table/use-generated-columns.tsx +9 -2
  90. package/src/lib/components/data-table/views-sheet.tsx +1 -1
  91. package/src/lib/components/layout/channel-switcher.tsx +16 -17
  92. package/src/lib/components/layout/manage-languages-dialog.tsx +1 -1
  93. package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +1 -1
  94. package/src/lib/components/shared/configurable-operation-input.tsx +23 -0
  95. package/src/lib/components/shared/configurable-operation-multi-selector.tsx +45 -0
  96. package/src/lib/components/shared/configurable-operation-selector.tsx +5 -0
  97. package/src/lib/components/shared/paginated-list-context.ts +10 -0
  98. package/src/lib/components/shared/paginated-list-data-table.tsx +6 -32
  99. package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +1 -1
  100. package/src/lib/components/ui/alert.tsx +2 -0
  101. package/src/lib/constants.ts +7 -319
  102. package/src/lib/framework/dashboard-widget/base-widget.tsx +3 -12
  103. package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +1 -1
  104. package/src/lib/framework/dashboard-widget/metrics-widget/chart.tsx +1 -1
  105. package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +1 -1
  106. package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +1 -1
  107. package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +2 -20
  108. package/src/lib/framework/extension-api/input-component-extensions.tsx +4 -0
  109. package/src/lib/framework/form-engine/custom-form-component.tsx +13 -3
  110. package/src/lib/framework/form-engine/form-engine-types.ts +3 -5
  111. package/src/lib/framework/form-engine/form-schema-tools.ts +4 -1
  112. package/src/lib/framework/form-engine/use-generated-form.tsx +6 -2
  113. package/src/lib/framework/form-engine/utils.spec.ts +129 -2
  114. package/src/lib/framework/form-engine/utils.ts +36 -9
  115. package/src/lib/framework/form-engine/value-transformers.ts +6 -0
  116. package/src/lib/framework/page/detail-page-route-loader.tsx +6 -4
  117. package/src/lib/framework/page/detail-page.tsx +22 -37
  118. package/src/lib/framework/page/list-page.stories.tsx +41 -2
  119. package/src/lib/framework/page/list-page.tsx +8 -0
  120. package/src/lib/graphql/graphql-env.d.ts +33 -16
  121. package/src/lib/graphql/schema-enums.ts +13 -0
  122. package/src/lib/hooks/use-alerts-context.ts +10 -0
  123. package/src/lib/hooks/use-alerts.ts +1 -1
  124. package/src/lib/hooks/use-data-table-context.ts +11 -0
  125. package/src/lib/hooks/use-dynamic-translations.ts +7 -0
  126. package/src/lib/hooks/use-job-queue-polling.ts +160 -0
  127. package/src/lib/hooks/use-paginated-list.ts +28 -0
  128. package/src/lib/hooks/use-widget-dimensions.ts +12 -0
  129. package/src/lib/hooks/use-widget-filters.ts +21 -0
  130. package/src/lib/index.ts +12 -0
  131. package/src/lib/providers/alerts-provider.tsx +3 -11
  132. package/src/lib/virtual.d.ts +5 -0
  133. package/src/lib/utils/global-languages.ts +0 -268
@@ -0,0 +1,345 @@
1
+ import { api } from '@/vdb/graphql/api.js';
2
+ import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
3
+ import { useLingui } from '@lingui/react/macro';
4
+ import { useMutation } from '@tanstack/react-query';
5
+ import { useCallback, useEffect, useMemo, useState } from 'react';
6
+ import { toast } from 'sonner';
7
+
8
+ import { cancelOrderDocument, refundOrderDocument } from '../orders.graphql.js';
9
+ import { Order } from '../utils/order-types.js';
10
+ import {
11
+ allocateRefundsToPayments,
12
+ calculateRefundTotal,
13
+ getOrderLineInputFromSelections,
14
+ getRefundablePayments,
15
+ getTotalAmountToRefund,
16
+ getTotalRefundableAmount,
17
+ LineSelection,
18
+ RefundablePayment,
19
+ } from '../utils/refund-utils.js';
20
+
21
+ export interface UseRefundOrderReturn {
22
+ // State
23
+ lineSelections: Record<string, LineSelection>;
24
+ refundShippingLineIds: string[];
25
+ selectedReason: string;
26
+ customReason: string;
27
+ manuallySetRefundTotal: boolean;
28
+ refundTotal: number;
29
+ refundablePayments: RefundablePayment[];
30
+ isSubmitting: boolean;
31
+
32
+ // Derived (useMemo)
33
+ reason: string;
34
+ totalRefundableAmount: number;
35
+ amountToRefundTotal: number;
36
+ validationErrors: string[];
37
+ canSubmit: boolean;
38
+ isCancelling: boolean;
39
+
40
+ // Callbacks
41
+ onRefundQuantityChange: (lineId: string, quantity: number) => void;
42
+ onCancelChange: (lineId: string, cancel: boolean) => void;
43
+ toggleShippingRefund: (lineId: string) => void;
44
+ onPaymentSelected: (paymentId: string, selected: boolean) => void;
45
+ onPaymentAmountChange: (paymentId: string, amount: number) => void;
46
+ onManualRefundTotalChange: (value: number) => void;
47
+ setSelectedReason: (reason: string) => void;
48
+ setCustomReason: (reason: string) => void;
49
+ setManuallySetRefundTotal: (value: boolean) => void;
50
+ recalculateRefundTotal: () => number;
51
+
52
+ // Actions
53
+ handleSubmit: () => Promise<void>;
54
+ resetState: () => void;
55
+ }
56
+
57
+ export function useRefundOrder(order: Order, onSuccess?: () => void): UseRefundOrderReturn {
58
+ const { t } = useLingui();
59
+ const { formatCurrency } = useLocalFormat();
60
+
61
+ const [isSubmitting, setIsSubmitting] = useState(false);
62
+ const [lineSelections, setLineSelections] = useState<Record<string, LineSelection>>({});
63
+ const [refundShippingLineIds, setRefundShippingLineIds] = useState<string[]>([]);
64
+ const [selectedReason, setSelectedReason] = useState<string>('');
65
+ const [customReason, setCustomReason] = useState('');
66
+ const [manuallySetRefundTotal, setManuallySetRefundTotal] = useState(false);
67
+ const [refundTotal, setRefundTotal] = useState(0);
68
+ const [refundablePayments, setRefundablePayments] = useState<RefundablePayment[]>([]);
69
+
70
+ const reason = selectedReason === 'other' ? customReason : selectedReason;
71
+
72
+ const cancelOrderMutation = useMutation({
73
+ mutationFn: api.mutate(cancelOrderDocument),
74
+ });
75
+
76
+ const refundOrderMutation = useMutation({
77
+ mutationFn: api.mutate(refundOrderDocument),
78
+ });
79
+
80
+ const resetState = useCallback(() => {
81
+ const selections: Record<string, LineSelection> = {};
82
+ order.lines.forEach(line => {
83
+ selections[line.id] = { quantity: 0, cancel: false };
84
+ });
85
+ setLineSelections(selections);
86
+ setRefundShippingLineIds([]);
87
+ setSelectedReason('');
88
+ setCustomReason('');
89
+ setManuallySetRefundTotal(false);
90
+ setRefundTotal(0);
91
+ setRefundablePayments(getRefundablePayments(order.payments));
92
+ }, [order]);
93
+
94
+ const totalRefundableAmount = useMemo(
95
+ () => getTotalRefundableAmount(refundablePayments),
96
+ [refundablePayments],
97
+ );
98
+
99
+ const amountToRefundTotal = useMemo(
100
+ () => getTotalAmountToRefund(refundablePayments),
101
+ [refundablePayments],
102
+ );
103
+
104
+ const recalculateRefundTotal = useCallback(() => {
105
+ return calculateRefundTotal(order.lines, lineSelections, order.shippingLines, refundShippingLineIds);
106
+ }, [order.lines, order.shippingLines, lineSelections, refundShippingLineIds]);
107
+
108
+ const updateRefundTotal = useCallback(() => {
109
+ if (manuallySetRefundTotal) {
110
+ setRefundablePayments(prev => allocateRefundsToPayments(prev, refundTotal));
111
+ } else {
112
+ const calculatedTotal = recalculateRefundTotal();
113
+ setRefundTotal(calculatedTotal);
114
+ setRefundablePayments(prev => allocateRefundsToPayments(prev, calculatedTotal));
115
+ }
116
+ }, [manuallySetRefundTotal, recalculateRefundTotal, refundTotal]);
117
+
118
+ useEffect(() => {
119
+ updateRefundTotal();
120
+ }, [updateRefundTotal]);
121
+
122
+ const onRefundQuantityChange = useCallback((lineId: string, quantity: number) => {
123
+ setManuallySetRefundTotal(false);
124
+ setLineSelections(prev => {
125
+ const prevLine = prev[lineId];
126
+ if (!prevLine) return prev;
127
+
128
+ const previousQuantity = prevLine.quantity;
129
+ let cancel = prevLine.cancel;
130
+
131
+ if (quantity === 0) {
132
+ cancel = false;
133
+ } else if (previousQuantity === 0 && quantity > 0) {
134
+ cancel = true;
135
+ }
136
+
137
+ return {
138
+ ...prev,
139
+ [lineId]: { quantity, cancel },
140
+ };
141
+ });
142
+ }, []);
143
+
144
+ const onCancelChange = useCallback((lineId: string, cancel: boolean) => {
145
+ setLineSelections(prev => ({
146
+ ...prev,
147
+ [lineId]: { ...prev[lineId], cancel },
148
+ }));
149
+ }, []);
150
+
151
+ const toggleShippingRefund = useCallback((lineId: string) => {
152
+ setManuallySetRefundTotal(false);
153
+ setRefundShippingLineIds(prev => {
154
+ if (prev.includes(lineId)) {
155
+ return prev.filter(id => id !== lineId);
156
+ }
157
+ return [...prev, lineId];
158
+ });
159
+ }, []);
160
+
161
+ const onPaymentSelected = useCallback(
162
+ (paymentId: string, selected: boolean) => {
163
+ setRefundablePayments(prev => {
164
+ const updatedPayments = prev.map(p => (p.id === paymentId ? { ...p, selected } : p));
165
+
166
+ if (selected) {
167
+ const otherAllocated = updatedPayments
168
+ .filter(p => p.id !== paymentId && p.selected)
169
+ .reduce((sum, p) => sum + p.amountToRefund, 0);
170
+ const outstanding = refundTotal - otherAllocated;
171
+ return updatedPayments.map(p => {
172
+ if (p.id === paymentId && outstanding > 0) {
173
+ return { ...p, amountToRefund: Math.min(outstanding, p.refundableAmount) };
174
+ }
175
+ return p;
176
+ });
177
+ } else {
178
+ return updatedPayments.map(p => (p.id === paymentId ? { ...p, amountToRefund: 0 } : p));
179
+ }
180
+ });
181
+ },
182
+ [refundTotal],
183
+ );
184
+
185
+ const onPaymentAmountChange = useCallback((paymentId: string, amount: number) => {
186
+ setRefundablePayments(prev =>
187
+ prev.map(p => (p.id === paymentId ? { ...p, amountToRefund: amount } : p)),
188
+ );
189
+ }, []);
190
+
191
+ const onManualRefundTotalChange = useCallback((value: number) => {
192
+ setRefundTotal(value);
193
+ setRefundablePayments(prev => allocateRefundsToPayments(prev, value));
194
+ }, []);
195
+
196
+ const validationErrors = useMemo(() => {
197
+ const errors: string[] = [];
198
+
199
+ if (refundTotal < 0) {
200
+ errors.push(t`Refund total cannot be negative`);
201
+ }
202
+
203
+ if (refundTotal > totalRefundableAmount) {
204
+ errors.push(
205
+ t`Refund total exceeds maximum refundable amount of ${formatCurrency(totalRefundableAmount, order.currencyCode)}`,
206
+ );
207
+ }
208
+
209
+ if (amountToRefundTotal !== refundTotal && refundTotal > 0) {
210
+ errors.push(t`Allocated payment amount must equal refund total`);
211
+ }
212
+
213
+ if (refundTotal > 0 && !reason) {
214
+ errors.push(t`A reason for the refund is required`);
215
+ }
216
+
217
+ return errors;
218
+ }, [
219
+ refundTotal,
220
+ totalRefundableAmount,
221
+ amountToRefundTotal,
222
+ reason,
223
+ formatCurrency,
224
+ order.currencyCode,
225
+ t,
226
+ ]);
227
+
228
+ const canSubmit = useMemo(() => {
229
+ return (
230
+ refundTotal > 0 &&
231
+ amountToRefundTotal === refundTotal &&
232
+ !!reason &&
233
+ validationErrors.length === 0
234
+ );
235
+ }, [refundTotal, amountToRefundTotal, reason, validationErrors]);
236
+
237
+ const isCancelling = useMemo(() => {
238
+ return Object.values(lineSelections).some(line => line.quantity > 0 && line.cancel);
239
+ }, [lineSelections]);
240
+
241
+ const handleSubmit = async () => {
242
+ setIsSubmitting(true);
243
+
244
+ try {
245
+ const refundLines = getOrderLineInputFromSelections(lineSelections);
246
+ const cancelLines = getOrderLineInputFromSelections(lineSelections, line => line.cancel);
247
+
248
+ if (isCancelling && cancelLines.length > 0) {
249
+ const cancelResult = await cancelOrderMutation.mutateAsync({
250
+ input: {
251
+ orderId: order.id,
252
+ lines: cancelLines,
253
+ reason,
254
+ cancelShipping: refundShippingLineIds.length > 0,
255
+ },
256
+ });
257
+
258
+ if (cancelResult.cancelOrder.__typename !== 'Order') {
259
+ toast.error(t`Failed to cancel order items`, {
260
+ description: cancelResult.cancelOrder.message,
261
+ });
262
+ setIsSubmitting(false);
263
+ return;
264
+ }
265
+ }
266
+
267
+ const paymentsToRefund = refundablePayments.filter(p => p.selected && p.amountToRefund > 0);
268
+
269
+ let successfulRefundCount = 0;
270
+
271
+ for (const payment of paymentsToRefund) {
272
+ const refundResult = await refundOrderMutation.mutateAsync({
273
+ input: {
274
+ lines: refundLines,
275
+ reason,
276
+ paymentId: payment.id,
277
+ amount: payment.amountToRefund,
278
+ shipping: 0,
279
+ adjustment: 0,
280
+ },
281
+ });
282
+
283
+ if (refundResult.refundOrder.__typename !== 'Refund') {
284
+ if (successfulRefundCount > 0) {
285
+ toast.warning(t`Partial refund completed`, {
286
+ description: t`${successfulRefundCount} payment(s) refunded before failure. Check order history for details.`,
287
+ });
288
+ }
289
+ toast.error(t`Failed to process refund`, {
290
+ description: refundResult.refundOrder.message,
291
+ });
292
+ setIsSubmitting(false);
293
+ return;
294
+ }
295
+
296
+ successfulRefundCount++;
297
+ }
298
+
299
+ toast.success(t`Refund processed successfully`);
300
+ onSuccess?.();
301
+ } catch (error) {
302
+ toast.error(t`Failed to process refund`, {
303
+ description: error instanceof Error ? error.message : t`Unknown error`,
304
+ });
305
+ } finally {
306
+ setIsSubmitting(false);
307
+ }
308
+ };
309
+
310
+ return {
311
+ // State
312
+ lineSelections,
313
+ refundShippingLineIds,
314
+ selectedReason,
315
+ customReason,
316
+ manuallySetRefundTotal,
317
+ refundTotal,
318
+ refundablePayments,
319
+ isSubmitting,
320
+
321
+ // Derived
322
+ reason,
323
+ totalRefundableAmount,
324
+ amountToRefundTotal,
325
+ validationErrors,
326
+ canSubmit,
327
+ isCancelling,
328
+
329
+ // Callbacks
330
+ onRefundQuantityChange,
331
+ onCancelChange,
332
+ toggleShippingRefund,
333
+ onPaymentSelected,
334
+ onPaymentAmountChange,
335
+ onManualRefundTotalChange,
336
+ setSelectedReason,
337
+ setCustomReason,
338
+ setManuallySetRefundTotal,
339
+ recalculateRefundTotal,
340
+
341
+ // Actions
342
+ handleSubmit,
343
+ resetState,
344
+ };
345
+ }
@@ -742,6 +742,47 @@ export const settleRefundDocument = graphql(
742
742
  [errorResultFragment],
743
743
  );
744
744
 
745
+ export const refundOrderDocument = graphql(
746
+ `
747
+ mutation RefundOrder($input: RefundOrderInput!) {
748
+ refundOrder(input: $input) {
749
+ __typename
750
+ ... on Refund {
751
+ id
752
+ state
753
+ total
754
+ reason
755
+ transactionId
756
+ method
757
+ metadata
758
+ lines {
759
+ orderLineId
760
+ quantity
761
+ }
762
+ }
763
+ ...ErrorResult
764
+ }
765
+ }
766
+ `,
767
+ [errorResultFragment],
768
+ );
769
+
770
+ export const cancelOrderDocument = graphql(
771
+ `
772
+ mutation CancelOrder($input: CancelOrderInput!) {
773
+ cancelOrder(input: $input) {
774
+ __typename
775
+ ... on Order {
776
+ id
777
+ state
778
+ }
779
+ ...ErrorResult
780
+ }
781
+ }
782
+ `,
783
+ [errorResultFragment],
784
+ );
785
+
745
786
  export const setOrderCustomFieldsDocument = graphql(`
746
787
  mutation SetOrderCustomFields($input: UpdateOrderInput!) {
747
788
  setOrderCustomFields(input: $input) {
@@ -24,6 +24,7 @@ import { ResultOf } from 'gql.tada';
24
24
  import { User } from 'lucide-react';
25
25
  import { toast } from 'sonner';
26
26
  import { CustomerAddressSelector } from './components/customer-address-selector.js';
27
+ import { DraftOrderStatus } from './components/draft-order-status.js';
27
28
  import { EditOrderTable } from './components/edit-order-table.js';
28
29
  import { OrderAddress } from './components/order-address.js';
29
30
  import {
@@ -289,6 +290,13 @@ function DraftOrderPage() {
289
290
  });
290
291
  };
291
292
 
293
+ const hasCustomer = !!entity.customer;
294
+ const hasLines = entity.lines.length > 0;
295
+ const hasShippingMethod = entity.shippingLines.length > 0;
296
+ const isDraftState = entity.state === 'Draft';
297
+
298
+ const isCompleteDraftDisabled = !hasCustomer || !hasLines || !hasShippingMethod || !isDraftState;
299
+
292
300
  return (
293
301
  <Page pageId="draft-order-detail" form={form} entity={entity}>
294
302
  <PageTitle>
@@ -312,12 +320,7 @@ function DraftOrderPage() {
312
320
  <PermissionGuard requires={['UpdateOrder']}>
313
321
  <Button
314
322
  type="button"
315
- disabled={
316
- !entity.customer ||
317
- entity.lines.length === 0 ||
318
- entity.shippingLines.length === 0 ||
319
- entity.state !== 'Draft'
320
- }
323
+ disabled={isCompleteDraftDisabled}
321
324
  onClick={() => completeDraftOrder({ id: entity.id, state: 'ArrangingPayment' })}
322
325
  >
323
326
  <Trans>Complete draft</Trans>
@@ -325,7 +328,20 @@ function DraftOrderPage() {
325
328
  </PermissionGuard>
326
329
  </PageActionBarRight>
327
330
  </PageActionBar>
331
+
328
332
  <PageLayout>
333
+ <PageBlock
334
+ column="side"
335
+ blockId="draft-order-status"
336
+ title={<Trans>Draft order status</Trans>}
337
+ >
338
+ <DraftOrderStatus
339
+ hasCustomer={hasCustomer}
340
+ hasLines={hasLines}
341
+ hasShippingMethod={hasShippingMethod}
342
+ isDraftState={isDraftState}
343
+ />
344
+ </PageBlock>
329
345
  <PageBlock column="main" blockId="order-table">
330
346
  <EditOrderTable
331
347
  order={entity}
@@ -157,3 +157,54 @@ export function computePendingOrder(
157
157
  : order.shippingLines,
158
158
  };
159
159
  }
160
+
161
+ /**
162
+ * Gets the total quantity already refunded for an order line across all payments.
163
+ */
164
+ export function getRefundedQuantity(order: Order, lineId: string): number {
165
+ return (
166
+ order.payments
167
+ ?.reduce((all, payment) => [...all, ...payment.refunds], [] as Payment['refunds'])
168
+ .filter(refund => refund.state !== 'Failed')
169
+ .reduce(
170
+ (all, refund) => [...all, ...refund.lines],
171
+ [] as Array<{ orderLineId: string; quantity: number }>,
172
+ )
173
+ .filter(refundLine => refundLine.orderLineId === lineId)
174
+ .reduce((sum, refundLine) => sum + refundLine.quantity, 0) ?? 0
175
+ );
176
+ }
177
+
178
+ /**
179
+ * Checks if an order line can still be refunded (has unrefunded quantity).
180
+ */
181
+ export function lineCanBeRefunded(order: Order, line: Order['lines'][number]): boolean {
182
+ const refundedCount = getRefundedQuantity(order, line.id);
183
+ return refundedCount < line.orderPlacedQuantity;
184
+ }
185
+
186
+ /**
187
+ * Gets the maximum refundable quantity for an order line.
188
+ */
189
+ export function getMaxRefundableQuantity(order: Order, line: Order['lines'][number]): number {
190
+ const refundedCount = getRefundedQuantity(order, line.id);
191
+ return Math.max(0, line.orderPlacedQuantity - refundedCount);
192
+ }
193
+
194
+ /**
195
+ * Checks if an order has any settled payments that can be refunded.
196
+ */
197
+ export function orderHasSettledPayments(order: Order): boolean {
198
+ return !!order.payments?.some(p => p.state === 'Settled');
199
+ }
200
+
201
+ /**
202
+ * Determines if the refund button should be shown for an order.
203
+ * Shows refund for orders with settled payments that are not in initial states.
204
+ */
205
+ export function canRefundOrder(order: Order): boolean {
206
+ if (!order || order.type === 'Aggregate') return false;
207
+ const hasSettled = orderHasSettledPayments(order);
208
+ const notInInitialState = order.state !== 'PaymentAuthorized' && order.active !== true;
209
+ return hasSettled && notInInitialState;
210
+ }
@@ -0,0 +1,100 @@
1
+ import { Order, Payment } from './order-types.js';
2
+
3
+ // === Types ===
4
+
5
+ export type RefundablePayment = Payment & {
6
+ refundableAmount: number;
7
+ amountToRefund: number;
8
+ selected: boolean;
9
+ };
10
+
11
+ export type LineSelection = { quantity: number; cancel: boolean };
12
+
13
+ // === Refundable Payment Functions ===
14
+
15
+ /**
16
+ * Filters payments to only those that are settled and calculates the refundable amount
17
+ * (payment amount minus sum of non-failed refunds).
18
+ */
19
+ export function getRefundablePayments(payments: Payment[] | undefined | null): RefundablePayment[] {
20
+ const settledPayments = (payments ?? []).filter(p => p.state === 'Settled');
21
+ return settledPayments.map((payment, index) => {
22
+ const successfulRefunds = payment.refunds.filter(r => r.state !== 'Failed');
23
+ const refundedTotal = successfulRefunds.reduce((sum, refund) => sum + (refund.total || 0), 0);
24
+ const refundableAmount = Math.max(0, payment.amount - refundedTotal);
25
+ return {
26
+ ...payment,
27
+ refundableAmount,
28
+ amountToRefund: 0,
29
+ selected: index === 0,
30
+ };
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Calculates the total refundable amount across all payments.
36
+ */
37
+ export function getTotalRefundableAmount(refundablePayments: RefundablePayment[]): number {
38
+ return refundablePayments.reduce((sum, p) => sum + p.refundableAmount, 0);
39
+ }
40
+
41
+ /**
42
+ * Calculates the total amount currently allocated to refund across all payments.
43
+ */
44
+ export function getTotalAmountToRefund(refundablePayments: RefundablePayment[]): number {
45
+ return refundablePayments.reduce((sum, p) => sum + p.amountToRefund, 0);
46
+ }
47
+
48
+ // === Refund Calculation Functions ===
49
+
50
+ /**
51
+ * Calculate total refund amount from line selections and shipping
52
+ */
53
+ export function calculateRefundTotal(
54
+ lines: Order['lines'],
55
+ lineSelections: Record<string, LineSelection>,
56
+ shippingLines: Order['shippingLines'],
57
+ refundShippingLineIds: string[],
58
+ ): number {
59
+ const itemTotal = lines.reduce((total, line) => {
60
+ const selection = lineSelections[line.id];
61
+ const refundCount = selection?.quantity || 0;
62
+ return total + line.proratedUnitPriceWithTax * refundCount;
63
+ }, 0);
64
+
65
+ const shippingTotal = shippingLines.reduce((total, line) => {
66
+ if (refundShippingLineIds.includes(line.id)) {
67
+ return total + line.discountedPriceWithTax;
68
+ }
69
+ return total;
70
+ }, 0);
71
+
72
+ return itemTotal + shippingTotal;
73
+ }
74
+
75
+ /**
76
+ * Allocate refund total across selected payments (immutable)
77
+ */
78
+ export function allocateRefundsToPayments(payments: RefundablePayment[], total: number): RefundablePayment[] {
79
+ let refundsAllocated = 0;
80
+ return payments.map(payment => {
81
+ if (!payment.selected) {
82
+ return { ...payment, amountToRefund: 0 };
83
+ }
84
+ const amountToRefund = Math.min(payment.refundableAmount, total - refundsAllocated);
85
+ refundsAllocated += amountToRefund;
86
+ return { ...payment, amountToRefund };
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Convert line selections to GraphQL input format
92
+ */
93
+ export function getOrderLineInputFromSelections(
94
+ lineSelections: Record<string, LineSelection>,
95
+ filterFn: (line: LineSelection) => boolean = () => true,
96
+ ): Array<{ orderLineId: string; quantity: number }> {
97
+ return Object.entries(lineSelections)
98
+ .filter(([, line]) => line.quantity > 0 && filterFn(line))
99
+ .map(([orderLineId, line]) => ({ orderLineId, quantity: line.quantity }));
100
+ }
@@ -85,7 +85,7 @@ export function useModifyOrder(order: Order | null | undefined): UseModifyOrderR
85
85
  const newQuantity = existingAdjustment.quantity + 1;
86
86
 
87
87
  // If back to original quantity, remove from adjustments
88
- if (newQuantity === existingLine.quantity) {
88
+ if (newQuantity === Number(existingLine.quantity)) {
89
89
  return {
90
90
  ...prev,
91
91
  adjustOrderLines: (prev.adjustOrderLines ?? []).filter(
@@ -17,12 +17,14 @@ export const paymentEligibilityCheckersDocument = graphql(
17
17
  interface PaymentEligibilityCheckerSelectorProps {
18
18
  value: ConfigurableOperationInputType | undefined;
19
19
  onChange: (value: ConfigurableOperationInputType | undefined) => void;
20
+ onValidityChange?: (isValid: boolean) => void;
20
21
  }
21
22
 
22
23
  export function PaymentEligibilityCheckerSelector({
23
24
  value,
24
25
  onChange,
25
- }: PaymentEligibilityCheckerSelectorProps) {
26
+ onValidityChange,
27
+ }: Readonly<PaymentEligibilityCheckerSelectorProps>) {
26
28
  return (
27
29
  <ConfigurableOperationSelector
28
30
  value={value}
@@ -32,6 +34,7 @@ export function PaymentEligibilityCheckerSelector({
32
34
  dataPath="paymentMethodEligibilityCheckers"
33
35
  buttonText="Select Payment Eligibility Checker"
34
36
  emptyText="No checkers found"
37
+ onValidityChange={onValidityChange}
35
38
  />
36
39
  );
37
40
  }
@@ -17,9 +17,14 @@ export const paymentHandlersDocument = graphql(
17
17
  interface PaymentHandlerSelectorProps {
18
18
  value: ConfigurableOperationInputType | undefined;
19
19
  onChange: (value: ConfigurableOperationInputType | undefined) => void;
20
+ onValidityChange?: (isValid: boolean) => void;
20
21
  }
21
22
 
22
- export function PaymentHandlerSelector({ value, onChange }: Readonly<PaymentHandlerSelectorProps>) {
23
+ export function PaymentHandlerSelector({
24
+ value,
25
+ onChange,
26
+ onValidityChange,
27
+ }: Readonly<PaymentHandlerSelectorProps>) {
23
28
  return (
24
29
  <ConfigurableOperationSelector
25
30
  value={value}
@@ -28,6 +33,7 @@ export function PaymentHandlerSelector({ value, onChange }: Readonly<PaymentHand
28
33
  queryKey="paymentMethodHandlers"
29
34
  dataPath="paymentMethodHandlers"
30
35
  buttonText="Select Payment Handler"
36
+ onValidityChange={onValidityChange}
31
37
  />
32
38
  );
33
39
  }