@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.
- package/dist/plugin/dashboard.plugin.d.ts +1 -1
- package/dist/plugin/dashboard.plugin.js +1 -1
- package/dist/vite/utils/compiler.js +50 -24
- package/dist/vite/utils/path-transformer.d.ts +20 -0
- package/dist/vite/utils/path-transformer.js +116 -0
- package/dist/vite/utils/plugin-discovery.js +3 -2
- package/dist/vite/utils/ui-config.js +15 -1
- package/dist/vite/vite-plugin-lingui-babel.d.ts +15 -2
- package/dist/vite/vite-plugin-lingui-babel.js +92 -17
- package/dist/vite/vite-plugin-translations.js +2 -2
- package/dist/vite/vite-plugin-ui-config.d.ts +31 -0
- package/package.json +10 -6
- package/src/app/common/delete-bulk-action.tsx +1 -1
- package/src/app/common/duplicate-bulk-action.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/collections.graphql.ts +1 -3
- package/src/app/routes/_authenticated/_collections/collections.tsx +169 -48
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +36 -5
- package/src/app/routes/_authenticated/_collections/components/collection-bulk-actions.tsx +1 -1
- package/src/app/routes/_authenticated/_collections/components/collection-filters-selector.tsx +7 -1
- package/src/app/routes/_authenticated/_customers/components/customer-history/default-customer-history-components.tsx +31 -29
- package/src/app/routes/_authenticated/_customers/customers.graphql.ts +1 -0
- package/src/app/routes/_authenticated/_customers/customers.tsx +3 -0
- package/src/app/routes/_authenticated/_global-settings/global-settings.tsx +1 -1
- package/src/app/routes/_authenticated/_orders/components/draft-order-status.tsx +48 -0
- package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +8 -5
- package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +79 -54
- package/src/app/routes/_authenticated/_orders/components/order-history/default-order-history-components.tsx +43 -3
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +19 -3
- package/src/app/routes/_authenticated/_orders/components/order-table.tsx +1 -0
- package/src/app/routes/_authenticated/_orders/components/refund-order-dialog.tsx +372 -0
- package/src/app/routes/_authenticated/_orders/hooks/use-refund-order.ts +345 -0
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +41 -0
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +22 -6
- package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +51 -0
- package/src/app/routes/_authenticated/_orders/utils/refund-utils.ts +100 -0
- package/src/app/routes/_authenticated/_orders/utils/use-modify-order.ts +1 -1
- package/src/app/routes/_authenticated/_payment-methods/components/payment-eligibility-checker-selector.tsx +4 -1
- package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx +7 -1
- package/src/app/routes/_authenticated/_payment-methods/payment-methods_.$id.tsx +18 -2
- package/src/app/routes/_authenticated/_product-variants/components/product-variant-bulk-actions.tsx +1 -1
- package/src/app/routes/_authenticated/_product-variants/components/variant-price-detail.tsx +6 -2
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +9 -3
- package/src/app/routes/_authenticated/_product-variants/product-variants_.$id.tsx +49 -30
- package/src/app/routes/_authenticated/_products/components/product-bulk-actions.tsx +1 -1
- package/src/app/routes/_authenticated/_profile/profile.graphql.ts +7 -0
- package/src/app/routes/_authenticated/_profile/profile.tsx +25 -1
- package/src/app/routes/_authenticated/_promotions/components/promotion-actions-selector.tsx +7 -1
- package/src/app/routes/_authenticated/_promotions/components/promotion-conditions-selector.tsx +7 -1
- package/src/app/routes/_authenticated/_promotions/promotions_.$id.tsx +18 -2
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-calculator-selector.tsx +7 -1
- package/src/app/routes/_authenticated/_shipping-methods/components/shipping-eligibility-checker-selector.tsx +4 -1
- package/src/app/routes/_authenticated/_shipping-methods/shipping-methods_.$id.tsx +14 -2
- package/src/i18n/common-strings.ts +7 -0
- package/src/i18n/locales/ar.po +669 -399
- package/src/i18n/locales/bg.po +1889 -46
- package/src/i18n/locales/cs.po +676 -406
- package/src/i18n/locales/de.po +676 -406
- package/src/i18n/locales/en.po +669 -399
- package/src/i18n/locales/es.po +676 -406
- package/src/i18n/locales/fa.po +676 -406
- package/src/i18n/locales/fr.po +676 -406
- package/src/i18n/locales/he.po +676 -406
- package/src/i18n/locales/hr.po +676 -406
- package/src/i18n/locales/it.po +676 -406
- package/src/i18n/locales/ja.po +676 -406
- package/src/i18n/locales/nb.po +676 -406
- package/src/i18n/locales/ne.po +676 -406
- package/src/i18n/locales/pl.po +676 -406
- package/src/i18n/locales/pt_BR.po +676 -406
- package/src/i18n/locales/pt_PT.po +676 -406
- package/src/i18n/locales/ru.po +676 -406
- package/src/i18n/locales/sv.po +676 -406
- package/src/i18n/locales/tr.po +676 -406
- package/src/i18n/locales/uk.po +676 -406
- package/src/i18n/locales/zh_Hans.po +676 -406
- package/src/i18n/locales/zh_Hant.po +676 -406
- package/src/lib/components/data-input/facet-value-input.tsx +2 -2
- package/src/lib/components/data-input/index.ts +1 -0
- package/src/lib/components/data-input/select-with-options.tsx +23 -7
- package/src/lib/components/data-input/struct-form-input.tsx +53 -21
- package/src/lib/components/data-input/text-input.tsx +1 -1
- package/src/lib/components/data-table/data-table-bulk-actions.tsx +2 -1
- package/src/lib/components/data-table/data-table-context.tsx +2 -10
- package/src/lib/components/data-table/data-table-utils.ts +34 -12
- package/src/lib/components/data-table/data-table.tsx +68 -30
- package/src/lib/components/data-table/global-views-bar.tsx +1 -1
- package/src/lib/components/data-table/my-views-button.tsx +1 -1
- package/src/lib/components/data-table/save-view-button.tsx +1 -1
- package/src/lib/components/data-table/use-generated-columns.tsx +9 -2
- package/src/lib/components/data-table/views-sheet.tsx +1 -1
- package/src/lib/components/layout/channel-switcher.tsx +16 -17
- package/src/lib/components/layout/manage-languages-dialog.tsx +1 -1
- package/src/lib/components/shared/assign-to-channel-bulk-action.tsx +1 -1
- package/src/lib/components/shared/configurable-operation-input.tsx +23 -0
- package/src/lib/components/shared/configurable-operation-multi-selector.tsx +45 -0
- package/src/lib/components/shared/configurable-operation-selector.tsx +5 -0
- package/src/lib/components/shared/paginated-list-context.ts +10 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +6 -32
- package/src/lib/components/shared/remove-from-channel-bulk-action.tsx +1 -1
- package/src/lib/components/ui/alert.tsx +2 -0
- package/src/lib/constants.ts +7 -319
- package/src/lib/framework/dashboard-widget/base-widget.tsx +3 -12
- package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +1 -1
- package/src/lib/framework/dashboard-widget/metrics-widget/chart.tsx +1 -1
- package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +1 -1
- package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +1 -1
- package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +2 -20
- package/src/lib/framework/extension-api/input-component-extensions.tsx +4 -0
- package/src/lib/framework/form-engine/custom-form-component.tsx +13 -3
- package/src/lib/framework/form-engine/form-engine-types.ts +3 -5
- package/src/lib/framework/form-engine/form-schema-tools.ts +4 -1
- package/src/lib/framework/form-engine/use-generated-form.tsx +6 -2
- package/src/lib/framework/form-engine/utils.spec.ts +129 -2
- package/src/lib/framework/form-engine/utils.ts +36 -9
- package/src/lib/framework/form-engine/value-transformers.ts +6 -0
- package/src/lib/framework/page/detail-page-route-loader.tsx +6 -4
- package/src/lib/framework/page/detail-page.tsx +22 -37
- package/src/lib/framework/page/list-page.stories.tsx +41 -2
- package/src/lib/framework/page/list-page.tsx +8 -0
- package/src/lib/graphql/graphql-env.d.ts +33 -16
- package/src/lib/graphql/schema-enums.ts +13 -0
- package/src/lib/hooks/use-alerts-context.ts +10 -0
- package/src/lib/hooks/use-alerts.ts +1 -1
- package/src/lib/hooks/use-data-table-context.ts +11 -0
- package/src/lib/hooks/use-dynamic-translations.ts +7 -0
- package/src/lib/hooks/use-job-queue-polling.ts +160 -0
- package/src/lib/hooks/use-paginated-list.ts +28 -0
- package/src/lib/hooks/use-widget-dimensions.ts +12 -0
- package/src/lib/hooks/use-widget-filters.ts +21 -0
- package/src/lib/index.ts +12 -0
- package/src/lib/providers/alerts-provider.tsx +3 -11
- package/src/lib/virtual.d.ts +5 -0
- 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
|
-
|
|
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
|
}
|
package/src/app/routes/_authenticated/_payment-methods/components/payment-handler-selector.tsx
CHANGED
|
@@ -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({
|
|
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
|
}
|