@vendure/dashboard 3.3.6-master-202507031127 → 3.3.6-master-202507031620
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/vite-plugin-vendure-dashboard.js +1 -1
- package/package.json +5 -5
- package/src/app/routes/_authenticated/_orders/components/add-manual-payment-dialog.tsx +191 -0
- package/src/app/routes/_authenticated/_orders/components/edit-order-table.tsx +2 -2
- package/src/app/routes/_authenticated/_orders/components/fulfill-order-dialog.tsx +320 -0
- package/src/app/routes/_authenticated/_orders/components/fulfillment-details.tsx +173 -0
- package/src/app/routes/_authenticated/_orders/components/order-address.tsx +2 -2
- package/src/app/routes/_authenticated/_orders/components/order-history/use-order-history.ts +6 -2
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +2 -5
- package/src/app/routes/_authenticated/_orders/components/order-tax-summary.tsx +2 -3
- package/src/app/routes/_authenticated/_orders/components/payment-details.tsx +14 -29
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +100 -2
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +58 -2
- package/src/app/routes/_authenticated/_orders/utils/order-types.ts +7 -0
- package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +77 -0
- package/src/lib/components/data-input/relation-input.tsx +0 -3
- package/src/lib/components/labeled-data.tsx +21 -0
- package/src/lib/components/shared/translatable-form-field.tsx +2 -1
- package/src/lib/components/ui/select.tsx +151 -129
- package/src/lib/framework/page/detail-page.tsx +26 -20
- package/src/lib/graphql/graphql-env.d.ts +1 -1
- package/vite/vite-plugin-vendure-dashboard.ts +1 -1
|
@@ -32,7 +32,7 @@ export function vendureDashboardPlugin(options) {
|
|
|
32
32
|
: [
|
|
33
33
|
TanStackRouterVite({
|
|
34
34
|
autoCodeSplitting: true,
|
|
35
|
-
routeFileIgnorePattern: '.graphql.ts|components|hooks',
|
|
35
|
+
routeFileIgnorePattern: '.graphql.ts|components|hooks|utils',
|
|
36
36
|
routesDirectory: path.join(packageRoot, 'src/app/routes'),
|
|
37
37
|
generatedRouteTree: path.join(packageRoot, 'src/app/routeTree.gen.ts'),
|
|
38
38
|
}),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure/dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "3.3.6-master-
|
|
4
|
+
"version": "3.3.6-master-202507031620",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@radix-ui/react-label": "^2.1.2",
|
|
66
66
|
"@radix-ui/react-popover": "^1.1.6",
|
|
67
67
|
"@radix-ui/react-scroll-area": "^1.2.3",
|
|
68
|
-
"@radix-ui/react-select": "^2.
|
|
68
|
+
"@radix-ui/react-select": "^2.2.5",
|
|
69
69
|
"@radix-ui/react-separator": "^1.1.2",
|
|
70
70
|
"@radix-ui/react-slot": "^1.1.2",
|
|
71
71
|
"@radix-ui/react-switch": "^1.1.3",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"@types/react-dom": "^19.0.4",
|
|
87
87
|
"@types/react-grid-layout": "^1.3.5",
|
|
88
88
|
"@uidotdev/usehooks": "^2.4.1",
|
|
89
|
-
"@vendure/common": "^3.3.6-master-
|
|
90
|
-
"@vendure/core": "^3.3.6-master-
|
|
89
|
+
"@vendure/common": "^3.3.6-master-202507031620",
|
|
90
|
+
"@vendure/core": "^3.3.6-master-202507031620",
|
|
91
91
|
"@vitejs/plugin-react": "^4.3.4",
|
|
92
92
|
"awesome-graphql-client": "^2.1.0",
|
|
93
93
|
"class-variance-authority": "^0.7.1",
|
|
@@ -130,5 +130,5 @@
|
|
|
130
130
|
"lightningcss-linux-arm64-musl": "^1.29.3",
|
|
131
131
|
"lightningcss-linux-x64-musl": "^1.29.1"
|
|
132
132
|
},
|
|
133
|
-
"gitHead": "
|
|
133
|
+
"gitHead": "d90e8efa64f0de407ec7402470ad21613885904c"
|
|
134
134
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RelationSelector,
|
|
3
|
+
createRelationSelectorConfig,
|
|
4
|
+
} from '@/vdb/components/data-input/relation-selector.js';
|
|
5
|
+
import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
|
|
6
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle,
|
|
14
|
+
} from '@/vdb/components/ui/dialog.js';
|
|
15
|
+
import { Form } from '@/vdb/components/ui/form.js';
|
|
16
|
+
import { Input } from '@/vdb/components/ui/input.js';
|
|
17
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
18
|
+
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
|
|
19
|
+
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
20
|
+
import { useMutation } from '@tanstack/react-query';
|
|
21
|
+
import { useState } from 'react';
|
|
22
|
+
import { useForm } from 'react-hook-form';
|
|
23
|
+
import { toast } from 'sonner';
|
|
24
|
+
import {
|
|
25
|
+
addManualPaymentToOrderDocument,
|
|
26
|
+
paymentMethodsDocument
|
|
27
|
+
} from '../orders.graphql.js';
|
|
28
|
+
import { Order } from '../utils/order-types.js';
|
|
29
|
+
import { calculateOutstandingPaymentAmount } from '../utils/order-utils.js';
|
|
30
|
+
|
|
31
|
+
interface AddManualPaymentDialogProps {
|
|
32
|
+
order: Order;
|
|
33
|
+
onSuccess?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface FormData {
|
|
37
|
+
method: string;
|
|
38
|
+
transactionId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function AddManualPaymentDialog({ order, onSuccess }: Readonly<AddManualPaymentDialogProps>) {
|
|
42
|
+
const { i18n } = useLingui();
|
|
43
|
+
const { formatCurrency } = useLocalFormat();
|
|
44
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
45
|
+
const [open, setOpen] = useState(false);
|
|
46
|
+
|
|
47
|
+
const addManualPaymentMutation = useMutation({
|
|
48
|
+
mutationFn: api.mutate(addManualPaymentToOrderDocument),
|
|
49
|
+
onSuccess: (result: any) => {
|
|
50
|
+
const { addManualPaymentToOrder } = result;
|
|
51
|
+
if (addManualPaymentToOrder.__typename === 'Order') {
|
|
52
|
+
toast(i18n.t('Successfully added payment to order'));
|
|
53
|
+
onSuccess?.();
|
|
54
|
+
} else {
|
|
55
|
+
toast(i18n.t('Failed to add payment'), {
|
|
56
|
+
description: addManualPaymentToOrder.message,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
onError: error => {
|
|
61
|
+
toast(i18n.t('Failed to add payment'), {
|
|
62
|
+
description: error instanceof Error ? error.message : 'Unknown error',
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const form = useForm<FormData>({
|
|
68
|
+
defaultValues: {
|
|
69
|
+
method: '',
|
|
70
|
+
transactionId: '',
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const method = form.watch('method');
|
|
74
|
+
|
|
75
|
+
const handleSubmit = async (data: FormData) => {
|
|
76
|
+
setIsSubmitting(true);
|
|
77
|
+
try {
|
|
78
|
+
addManualPaymentMutation.mutate({
|
|
79
|
+
input: {
|
|
80
|
+
orderId: order.id,
|
|
81
|
+
method: data.method,
|
|
82
|
+
transactionId: data.transactionId,
|
|
83
|
+
metadata: {},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
setOpen(false);
|
|
87
|
+
form.reset();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
toast(i18n.t('Failed to add payment'), {
|
|
90
|
+
description: error instanceof Error ? error.message : 'Unknown error',
|
|
91
|
+
});
|
|
92
|
+
} finally {
|
|
93
|
+
setIsSubmitting(false);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const handleCancel = () => {
|
|
98
|
+
form.reset();
|
|
99
|
+
setOpen(false);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const outstandingAmount = calculateOutstandingPaymentAmount(order);
|
|
103
|
+
const currencyCode = order.currencyCode;
|
|
104
|
+
|
|
105
|
+
// Create relation selector config for payment methods
|
|
106
|
+
const paymentMethodSelectorConfig = createRelationSelectorConfig({
|
|
107
|
+
listQuery: paymentMethodsDocument,
|
|
108
|
+
idKey: 'code',
|
|
109
|
+
labelKey: 'name',
|
|
110
|
+
placeholder: i18n.t('Search payment methods...'),
|
|
111
|
+
multiple: false,
|
|
112
|
+
label: (method: any) => `${method.name} (${method.code})`,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
<Button
|
|
118
|
+
onClick={e => {
|
|
119
|
+
e.stopPropagation();
|
|
120
|
+
setOpen(true);
|
|
121
|
+
}}
|
|
122
|
+
className="mr-2"
|
|
123
|
+
>
|
|
124
|
+
<Trans>Add payment to order ({formatCurrency(outstandingAmount, currencyCode)})</Trans>
|
|
125
|
+
</Button>
|
|
126
|
+
<Dialog open={open}>
|
|
127
|
+
<DialogContent className="sm:max-w-[500px]">
|
|
128
|
+
<DialogHeader>
|
|
129
|
+
<DialogTitle>
|
|
130
|
+
<Trans>Add payment to order</Trans>
|
|
131
|
+
</DialogTitle>
|
|
132
|
+
<DialogDescription>
|
|
133
|
+
<Trans>
|
|
134
|
+
Add a manual payment of {formatCurrency(outstandingAmount, currencyCode)}
|
|
135
|
+
</Trans>
|
|
136
|
+
</DialogDescription>
|
|
137
|
+
</DialogHeader>
|
|
138
|
+
<Form {...form}>
|
|
139
|
+
<form onSubmit={e => {
|
|
140
|
+
e.stopPropagation();
|
|
141
|
+
form.handleSubmit(handleSubmit)(e);
|
|
142
|
+
}} className="space-y-4">
|
|
143
|
+
<FormFieldWrapper
|
|
144
|
+
control={form.control}
|
|
145
|
+
name="method"
|
|
146
|
+
label={<Trans>Payment method</Trans>}
|
|
147
|
+
rules={{ required: i18n.t('Payment method is required') }}
|
|
148
|
+
render={({ field }) => (
|
|
149
|
+
<RelationSelector
|
|
150
|
+
config={paymentMethodSelectorConfig}
|
|
151
|
+
value={field.value}
|
|
152
|
+
onChange={field.onChange}
|
|
153
|
+
disabled={isSubmitting}
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
/>
|
|
157
|
+
<FormFieldWrapper
|
|
158
|
+
control={form.control}
|
|
159
|
+
name="transactionId"
|
|
160
|
+
label={<Trans>Transaction ID</Trans>}
|
|
161
|
+
rules={{ required: i18n.t('Transaction ID is required') }}
|
|
162
|
+
render={({ field }) => (
|
|
163
|
+
<Input {...field} placeholder={i18n.t('Enter transaction ID')} />
|
|
164
|
+
)}
|
|
165
|
+
/>
|
|
166
|
+
<DialogFooter>
|
|
167
|
+
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
168
|
+
<Trans>Cancel</Trans>
|
|
169
|
+
</Button>
|
|
170
|
+
<Button
|
|
171
|
+
type="submit"
|
|
172
|
+
disabled={
|
|
173
|
+
!form.formState.isValid || isSubmitting || !method
|
|
174
|
+
}
|
|
175
|
+
>
|
|
176
|
+
{isSubmitting ? (
|
|
177
|
+
<Trans>Adding...</Trans>
|
|
178
|
+
) : (
|
|
179
|
+
<Trans>
|
|
180
|
+
Add payment ({formatCurrency(outstandingAmount, currencyCode)})
|
|
181
|
+
</Trans>
|
|
182
|
+
)}
|
|
183
|
+
</Button>
|
|
184
|
+
</DialogFooter>
|
|
185
|
+
</form>
|
|
186
|
+
</Form>
|
|
187
|
+
</DialogContent>
|
|
188
|
+
</Dialog>
|
|
189
|
+
</>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
@@ -54,7 +54,7 @@ export function EditOrderTable({
|
|
|
54
54
|
onApplyCouponCode,
|
|
55
55
|
onRemoveCouponCode,
|
|
56
56
|
orderLineForm,
|
|
57
|
-
}: OrderTableProps) {
|
|
57
|
+
}: Readonly<OrderTableProps>) {
|
|
58
58
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
|
59
59
|
const [couponCode, setCouponCode] = useState('');
|
|
60
60
|
|
|
@@ -130,7 +130,7 @@ export function EditOrderTable({
|
|
|
130
130
|
{
|
|
131
131
|
header: 'Total',
|
|
132
132
|
accessorKey: 'linePriceWithTax',
|
|
133
|
-
cell: ({
|
|
133
|
+
cell: ({ row }) => {
|
|
134
134
|
const value = row.original.linePriceWithTax;
|
|
135
135
|
const netValue = row.original.linePrice;
|
|
136
136
|
return <MoneyGrossNet priceWithTax={value} price={netValue} currencyCode={currencyCode} />;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { ConfigurableOperationInput } from '@/vdb/components/shared/configurable-operation-input.js';
|
|
2
|
+
import { FormFieldWrapper } from '@/vdb/components/shared/form-field-wrapper.js';
|
|
3
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from '@/vdb/components/ui/dialog.js';
|
|
12
|
+
import { Form } from '@/vdb/components/ui/form.js';
|
|
13
|
+
import { Input } from '@/vdb/components/ui/input.js';
|
|
14
|
+
import { Label } from '@/vdb/components/ui/label.js';
|
|
15
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
16
|
+
import { graphql } from '@/vdb/graphql/graphql.js';
|
|
17
|
+
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
18
|
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
19
|
+
import { ConfigurableOperationInput as ConfigurableOperationInputType } from '@vendure/common/lib/generated-types';
|
|
20
|
+
import { useState } from 'react';
|
|
21
|
+
import { useForm } from 'react-hook-form';
|
|
22
|
+
import { toast } from 'sonner';
|
|
23
|
+
import { fulfillmentHandlersDocument, fulfillOrderDocument } from '../orders.graphql.js';
|
|
24
|
+
import { Order } from '../utils/order-types.js';
|
|
25
|
+
|
|
26
|
+
interface FulfillOrderDialogProps {
|
|
27
|
+
order: Order;
|
|
28
|
+
onSuccess?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface FormData {
|
|
32
|
+
handler: ConfigurableOperationInputType;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface FulfillmentQuantity {
|
|
36
|
+
fulfillCount: number;
|
|
37
|
+
max: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function FulfillOrderDialog({ order, onSuccess }: Readonly<FulfillOrderDialogProps>) {
|
|
41
|
+
const { i18n } = useLingui();
|
|
42
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
43
|
+
const [open, setOpen] = useState(false);
|
|
44
|
+
const [fulfillmentQuantities, setFulfillmentQuantities] = useState<{
|
|
45
|
+
[lineId: string]: FulfillmentQuantity;
|
|
46
|
+
}>({});
|
|
47
|
+
|
|
48
|
+
// Get fulfillment handlers
|
|
49
|
+
const { data: fulfillmentHandlersData } = useQuery({
|
|
50
|
+
queryKey: ['fulfillmentHandlers'],
|
|
51
|
+
queryFn: () => api.query(fulfillmentHandlersDocument),
|
|
52
|
+
staleTime: 1000 * 60 * 60 * 5,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Get global settings for inventory tracking
|
|
56
|
+
const { data: globalSettingsData } = useQuery({
|
|
57
|
+
queryKey: ['globalSettings'],
|
|
58
|
+
queryFn: () =>
|
|
59
|
+
api.query(
|
|
60
|
+
graphql(`
|
|
61
|
+
query GetGlobalSettings {
|
|
62
|
+
globalSettings {
|
|
63
|
+
trackInventory
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
`),
|
|
67
|
+
),
|
|
68
|
+
staleTime: 1000 * 60 * 60 * 5,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const fulfillOrderMutation = useMutation({
|
|
72
|
+
mutationFn: api.mutate(fulfillOrderDocument),
|
|
73
|
+
onSuccess: (result: any) => {
|
|
74
|
+
const { addFulfillmentToOrder } = result;
|
|
75
|
+
if (addFulfillmentToOrder.__typename === 'Fulfillment') {
|
|
76
|
+
toast(i18n.t('Successfully fulfilled order'));
|
|
77
|
+
onSuccess?.();
|
|
78
|
+
} else {
|
|
79
|
+
toast(i18n.t('Failed to fulfill order'), {
|
|
80
|
+
description: addFulfillmentToOrder.message,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
onError: error => {
|
|
85
|
+
toast(i18n.t('Failed to fulfill order'), {
|
|
86
|
+
description: error instanceof Error ? error.message : 'Unknown error',
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const form = useForm<FormData>({
|
|
92
|
+
defaultValues: {
|
|
93
|
+
handler: {
|
|
94
|
+
code: '',
|
|
95
|
+
arguments: [],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Initialize fulfillment quantities when dialog opens
|
|
101
|
+
const initializeFulfillmentQuantities = () => {
|
|
102
|
+
if (!globalSettingsData?.globalSettings) return;
|
|
103
|
+
|
|
104
|
+
const quantities: { [lineId: string]: FulfillmentQuantity } = {};
|
|
105
|
+
order.lines.forEach(line => {
|
|
106
|
+
const fulfillCount = getFulfillableCount(line, globalSettingsData.globalSettings.trackInventory);
|
|
107
|
+
quantities[line.id] = { fulfillCount, max: fulfillCount };
|
|
108
|
+
});
|
|
109
|
+
setFulfillmentQuantities(quantities);
|
|
110
|
+
|
|
111
|
+
// Set default fulfillment handler
|
|
112
|
+
const defaultHandler =
|
|
113
|
+
fulfillmentHandlersData?.fulfillmentHandlers.find(
|
|
114
|
+
h => h.code === order.shippingLines[0]?.shippingMethod?.fulfillmentHandlerCode,
|
|
115
|
+
) ?? fulfillmentHandlersData?.fulfillmentHandlers[0];
|
|
116
|
+
|
|
117
|
+
if (defaultHandler) {
|
|
118
|
+
form.setValue('handler', {
|
|
119
|
+
code: defaultHandler.code,
|
|
120
|
+
arguments: defaultHandler.args.map(arg => ({
|
|
121
|
+
name: arg.name,
|
|
122
|
+
value: arg.defaultValue ?? '',
|
|
123
|
+
})),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const getFulfillableCount = (line: Order['lines'][number], globalTrackInventory: boolean): number => {
|
|
129
|
+
const { trackInventory, stockOnHand } = line.productVariant;
|
|
130
|
+
const effectiveTrackInventory =
|
|
131
|
+
trackInventory === 'INHERIT' ? globalTrackInventory : trackInventory === 'TRUE';
|
|
132
|
+
|
|
133
|
+
const unfulfilledCount = getUnfulfilledCount(line);
|
|
134
|
+
return effectiveTrackInventory ? Math.min(unfulfilledCount, stockOnHand) : unfulfilledCount;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const getUnfulfilledCount = (line: Order['lines'][number]): number => {
|
|
138
|
+
const fulfilled =
|
|
139
|
+
order.fulfillments
|
|
140
|
+
?.filter(f => f.state !== 'Cancelled')
|
|
141
|
+
.map(f => f.lines)
|
|
142
|
+
.flat()
|
|
143
|
+
.filter(row => row.orderLineId === line.id)
|
|
144
|
+
.reduce((sum, row) => sum + row.quantity, 0) ?? 0;
|
|
145
|
+
return line.quantity - fulfilled;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const updateFulfillmentQuantity = (lineId: string, fulfillCount: number) => {
|
|
149
|
+
setFulfillmentQuantities(prev => ({
|
|
150
|
+
...prev,
|
|
151
|
+
[lineId]: { ...prev[lineId], fulfillCount },
|
|
152
|
+
}));
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const canSubmit = (): boolean => {
|
|
156
|
+
const totalCount = Object.values(fulfillmentQuantities).reduce(
|
|
157
|
+
(total, { fulfillCount }) => total + fulfillCount,
|
|
158
|
+
0,
|
|
159
|
+
);
|
|
160
|
+
const fulfillmentQuantityIsValid = Object.values(fulfillmentQuantities).every(
|
|
161
|
+
({ fulfillCount, max }) => fulfillCount <= max && fulfillCount >= 0,
|
|
162
|
+
);
|
|
163
|
+
const formIsValid = form.formState.isValid;
|
|
164
|
+
return formIsValid && totalCount > 0 && fulfillmentQuantityIsValid;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const handleSubmit = async (data: FormData) => {
|
|
168
|
+
setIsSubmitting(true);
|
|
169
|
+
try {
|
|
170
|
+
const lines = Object.entries(fulfillmentQuantities)
|
|
171
|
+
.filter(([, { fulfillCount }]) => fulfillCount > 0)
|
|
172
|
+
.map(([orderLineId, { fulfillCount }]) => ({
|
|
173
|
+
orderLineId,
|
|
174
|
+
quantity: fulfillCount,
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
fulfillOrderMutation.mutate({
|
|
178
|
+
input: {
|
|
179
|
+
lines,
|
|
180
|
+
handler: data.handler,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
setOpen(false);
|
|
184
|
+
form.reset();
|
|
185
|
+
setFulfillmentQuantities({});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
toast(i18n.t('Failed to fulfill order'), {
|
|
188
|
+
description: error instanceof Error ? error.message : 'Unknown error',
|
|
189
|
+
});
|
|
190
|
+
} finally {
|
|
191
|
+
setIsSubmitting(false);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const handleCancel = () => {
|
|
196
|
+
form.reset();
|
|
197
|
+
setFulfillmentQuantities({});
|
|
198
|
+
setOpen(false);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const handleOpen = () => {
|
|
202
|
+
setOpen(true);
|
|
203
|
+
// Initialize quantities after a short delay to ensure data is loaded
|
|
204
|
+
setTimeout(initializeFulfillmentQuantities, 100);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const fulfillmentHandlers = fulfillmentHandlersData?.fulfillmentHandlers;
|
|
208
|
+
const selectedHandler = fulfillmentHandlers?.find(h => h.code === form.watch('handler.code'));
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<>
|
|
212
|
+
<Button
|
|
213
|
+
onClick={e => {
|
|
214
|
+
e.stopPropagation();
|
|
215
|
+
handleOpen();
|
|
216
|
+
}}
|
|
217
|
+
className="mr-2"
|
|
218
|
+
>
|
|
219
|
+
<Trans>Fulfill order</Trans>
|
|
220
|
+
</Button>
|
|
221
|
+
<Dialog open={open}>
|
|
222
|
+
<DialogContent className="sm:max-w-[600px]">
|
|
223
|
+
<DialogHeader>
|
|
224
|
+
<DialogTitle>
|
|
225
|
+
<Trans>Fulfill order</Trans>
|
|
226
|
+
</DialogTitle>
|
|
227
|
+
<DialogDescription>
|
|
228
|
+
<Trans>Select quantities to fulfill and configure the fulfillment handler</Trans>
|
|
229
|
+
</DialogDescription>
|
|
230
|
+
</DialogHeader>
|
|
231
|
+
<Form {...form}>
|
|
232
|
+
<form
|
|
233
|
+
onSubmit={e => {
|
|
234
|
+
e.stopPropagation();
|
|
235
|
+
form.handleSubmit(handleSubmit)(e);
|
|
236
|
+
}}
|
|
237
|
+
className="space-y-4"
|
|
238
|
+
>
|
|
239
|
+
<div className="space-y-4">
|
|
240
|
+
<div className="font-medium">
|
|
241
|
+
<Trans>Order lines</Trans>
|
|
242
|
+
</div>
|
|
243
|
+
{order.lines.map(line => {
|
|
244
|
+
const quantity = fulfillmentQuantities[line.id];
|
|
245
|
+
if (!quantity || quantity.max <= 0) return null;
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<div
|
|
249
|
+
key={line.id}
|
|
250
|
+
className="flex items-center justify-between p-3 border rounded-md"
|
|
251
|
+
>
|
|
252
|
+
<div className="flex-1">
|
|
253
|
+
<div className="font-medium">{line.productVariant.name}</div>
|
|
254
|
+
<div className="text-sm text-muted-foreground">
|
|
255
|
+
SKU: {line.productVariant.sku}
|
|
256
|
+
</div>
|
|
257
|
+
<div className="text-sm text-muted-foreground">
|
|
258
|
+
<Trans>
|
|
259
|
+
{quantity.max} of {line.quantity} available to fulfill
|
|
260
|
+
</Trans>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
<div className="flex items-center space-x-2">
|
|
264
|
+
<Label htmlFor={`quantity-${line.id}`}>
|
|
265
|
+
<Trans>Quantity</Trans>
|
|
266
|
+
</Label>
|
|
267
|
+
<Input
|
|
268
|
+
id={`quantity-${line.id}`}
|
|
269
|
+
type="number"
|
|
270
|
+
min="0"
|
|
271
|
+
max={quantity.max}
|
|
272
|
+
value={quantity.fulfillCount}
|
|
273
|
+
onChange={e => {
|
|
274
|
+
const value = parseInt(e.target.value) || 0;
|
|
275
|
+
updateFulfillmentQuantity(line.id, value);
|
|
276
|
+
}}
|
|
277
|
+
className="w-20"
|
|
278
|
+
/>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
})}
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
{selectedHandler && (
|
|
286
|
+
<FormFieldWrapper
|
|
287
|
+
control={form.control}
|
|
288
|
+
name="handler"
|
|
289
|
+
label={<Trans>Fulfillment handler</Trans>}
|
|
290
|
+
render={({ field }) => (
|
|
291
|
+
<ConfigurableOperationInput
|
|
292
|
+
operationDefinition={selectedHandler}
|
|
293
|
+
value={field.value}
|
|
294
|
+
onChange={field.onChange}
|
|
295
|
+
readonly={false}
|
|
296
|
+
removable={false}
|
|
297
|
+
/>
|
|
298
|
+
)}
|
|
299
|
+
/>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
<DialogFooter>
|
|
303
|
+
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
304
|
+
<Trans>Cancel</Trans>
|
|
305
|
+
</Button>
|
|
306
|
+
<Button type="submit" disabled={!canSubmit() || isSubmitting}>
|
|
307
|
+
{isSubmitting ? (
|
|
308
|
+
<Trans>Fulfilling...</Trans>
|
|
309
|
+
) : (
|
|
310
|
+
<Trans>Fulfill order</Trans>
|
|
311
|
+
)}
|
|
312
|
+
</Button>
|
|
313
|
+
</DialogFooter>
|
|
314
|
+
</form>
|
|
315
|
+
</Form>
|
|
316
|
+
</DialogContent>
|
|
317
|
+
</Dialog>
|
|
318
|
+
</>
|
|
319
|
+
);
|
|
320
|
+
}
|