@vendure/dashboard 3.4.3-master-202509200226 → 3.4.3-master-202509240228
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/package.json +4 -4
- package/src/app/routes/_authenticated/_countries/countries.graphql.ts +2 -0
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-container.tsx +2 -2
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-types.ts +5 -0
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history-utils.tsx +124 -0
- package/src/app/routes/_authenticated/_customers/components/customer-history/customer-history.tsx +91 -59
- package/src/app/routes/_authenticated/_customers/components/customer-history/default-customer-history-components.tsx +176 -0
- package/src/app/routes/_authenticated/_customers/components/customer-history/index.ts +4 -2
- package/src/app/routes/_authenticated/_customers/customers.graphql.ts +2 -0
- package/src/app/routes/_authenticated/_orders/components/order-detail-shared.tsx +302 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/default-order-history-components.tsx +98 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-container.tsx +9 -7
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-types.ts +5 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx +173 -0
- package/src/app/routes/_authenticated/_orders/components/order-history/order-history.tsx +64 -408
- package/src/app/routes/_authenticated/_orders/components/order-table-totals.tsx +16 -0
- package/src/app/routes/_authenticated/_orders/components/seller-orders-card.tsx +61 -0
- package/src/app/routes/_authenticated/_orders/components/use-transition-order-to-state.tsx +17 -10
- package/src/app/routes/_authenticated/_orders/orders.graphql.ts +35 -0
- package/src/app/routes/_authenticated/_orders/orders_.$aggregateOrderId_.seller-orders.$sellerOrderId.tsx +50 -0
- package/src/app/routes/_authenticated/_orders/orders_.$id.tsx +17 -290
- package/src/app/routes/_authenticated/_orders/orders_.$id_.modify.tsx +7 -39
- package/src/app/routes/_authenticated/_orders/orders_.draft.$id.tsx +4 -26
- package/src/app/routes/_authenticated/_orders/utils/order-detail-loaders.tsx +129 -0
- package/src/app/routes/_authenticated/_orders/utils/order-utils.ts +8 -0
- package/src/app/routes/_authenticated/_roles/components/permissions-table-grid.tsx +251 -0
- package/src/app/routes/_authenticated/_roles/roles_.$id.tsx +5 -3
- package/src/lib/components/shared/detail-page-button.tsx +6 -4
- package/src/lib/components/shared/history-timeline/history-note-entry.tsx +65 -0
- package/src/lib/components/shared/history-timeline/history-timeline-with-grouping.tsx +141 -0
- package/src/lib/components/shared/history-timeline/use-history-note-editor.ts +26 -0
- package/src/lib/framework/extension-api/define-dashboard-extension.ts +5 -0
- package/src/lib/framework/extension-api/extension-api-types.ts +7 -0
- package/src/lib/framework/extension-api/logic/history-entries.ts +24 -0
- package/src/lib/framework/extension-api/logic/index.ts +1 -0
- package/src/lib/framework/extension-api/types/history-entries.ts +120 -0
- package/src/lib/framework/extension-api/types/index.ts +1 -0
- package/src/lib/framework/history-entry/history-entry-extensions.ts +11 -0
- package/src/lib/framework/history-entry/history-entry.tsx +129 -0
- package/src/lib/framework/layout-engine/page-layout.tsx +96 -9
- package/src/lib/framework/registry/registry-types.ts +2 -0
- package/src/lib/index.ts +12 -1
- package/src/app/routes/_authenticated/_roles/components/permissions-grid.tsx +0 -120
- package/src/lib/components/shared/history-timeline/history-entry.tsx +0 -188
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { CustomFieldsForm } from '@/vdb/components/shared/custom-fields-form.js';
|
|
2
|
+
import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
|
|
3
|
+
import { Button } from '@/vdb/components/ui/button.js';
|
|
4
|
+
import { DropdownMenuItem } from '@/vdb/components/ui/dropdown-menu.js';
|
|
5
|
+
import {
|
|
6
|
+
Page,
|
|
7
|
+
PageActionBar,
|
|
8
|
+
PageActionBarRight,
|
|
9
|
+
PageBlock,
|
|
10
|
+
PageLayout,
|
|
11
|
+
PageTitle,
|
|
12
|
+
} from '@/vdb/framework/layout-engine/page-layout.js';
|
|
13
|
+
import { getDetailQueryOptions, useDetailPage } from '@/vdb/framework/page/use-detail-page.js';
|
|
14
|
+
import { api } from '@/vdb/graphql/api.js';
|
|
15
|
+
import { useCustomFieldConfig } from '@/vdb/hooks/use-custom-field-config.js';
|
|
16
|
+
import { Trans, useLingui } from '@/vdb/lib/trans.js';
|
|
17
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
18
|
+
import { Link, useNavigate } from '@tanstack/react-router';
|
|
19
|
+
import { ResultOf } from 'gql.tada';
|
|
20
|
+
import { Pencil, User } from 'lucide-react';
|
|
21
|
+
import { useMemo } from 'react';
|
|
22
|
+
import { toast } from 'sonner';
|
|
23
|
+
import {
|
|
24
|
+
orderDetailDocument,
|
|
25
|
+
setOrderCustomFieldsDocument,
|
|
26
|
+
transitionOrderToStateDocument,
|
|
27
|
+
} from '../orders.graphql.js';
|
|
28
|
+
import { canAddFulfillment, shouldShowAddManualPaymentButton } from '../utils/order-utils.js';
|
|
29
|
+
import { AddManualPaymentDialog } from './add-manual-payment-dialog.js';
|
|
30
|
+
import { FulfillOrderDialog } from './fulfill-order-dialog.js';
|
|
31
|
+
import { FulfillmentDetails } from './fulfillment-details.js';
|
|
32
|
+
import { OrderAddress } from './order-address.js';
|
|
33
|
+
import { OrderHistoryContainer } from './order-history/order-history-container.js';
|
|
34
|
+
import { orderHistoryQueryKey } from './order-history/use-order-history.js';
|
|
35
|
+
import { OrderTable } from './order-table.js';
|
|
36
|
+
import { OrderTaxSummary } from './order-tax-summary.js';
|
|
37
|
+
import { PaymentDetails } from './payment-details.js';
|
|
38
|
+
import { getTypeForState, StateTransitionControl } from './state-transition-control.js';
|
|
39
|
+
import { useTransitionOrderToState } from './use-transition-order-to-state.js';
|
|
40
|
+
|
|
41
|
+
export type OrderDetail = NonNullable<ResultOf<typeof orderDetailDocument>['order']>;
|
|
42
|
+
|
|
43
|
+
export interface OrderDetailSharedProps {
|
|
44
|
+
// Required props
|
|
45
|
+
pageId: string;
|
|
46
|
+
orderId: string;
|
|
47
|
+
// Title customization
|
|
48
|
+
titleSlot?: (order: OrderDetail) => React.ReactNode;
|
|
49
|
+
// Optional content slots
|
|
50
|
+
beforeOrderTable?: (order: OrderDetail) => React.ReactNode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function DefaultOrderTitle({ entity }: { entity: any }) {
|
|
54
|
+
return <>{entity?.code ?? ''}</>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @description
|
|
59
|
+
* Shared functionality between the order and seller order detail pages.
|
|
60
|
+
*/
|
|
61
|
+
export function OrderDetailShared({
|
|
62
|
+
pageId,
|
|
63
|
+
orderId,
|
|
64
|
+
titleSlot,
|
|
65
|
+
beforeOrderTable,
|
|
66
|
+
}: Readonly<OrderDetailSharedProps>) {
|
|
67
|
+
const { i18n } = useLingui();
|
|
68
|
+
const navigate = useNavigate();
|
|
69
|
+
const queryClient = useQueryClient();
|
|
70
|
+
|
|
71
|
+
const { form, submitHandler, entity, refreshEntity } = useDetailPage({
|
|
72
|
+
pageId,
|
|
73
|
+
queryDocument: orderDetailDocument,
|
|
74
|
+
updateDocument: setOrderCustomFieldsDocument,
|
|
75
|
+
setValuesForUpdate: (entity: any) => {
|
|
76
|
+
return {
|
|
77
|
+
id: entity.id,
|
|
78
|
+
customFields: entity.customFields,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
params: { id: orderId },
|
|
82
|
+
onSuccess: async () => {
|
|
83
|
+
toast(i18n.t('Successfully updated order'));
|
|
84
|
+
form.reset(form.getValues());
|
|
85
|
+
},
|
|
86
|
+
onError: err => {
|
|
87
|
+
toast(i18n.t('Failed to update order'), {
|
|
88
|
+
description: err instanceof Error ? err.message : 'Unknown error',
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const { transitionToState } = useTransitionOrderToState(entity?.id);
|
|
94
|
+
const transitionOrderToStateMutation = useMutation({
|
|
95
|
+
mutationFn: api.mutate(transitionOrderToStateDocument),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const customFieldConfig = useCustomFieldConfig('Order');
|
|
99
|
+
|
|
100
|
+
const stateTransitionActions = useMemo(() => {
|
|
101
|
+
if (!entity) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
return entity.nextStates.map((state: string) => ({
|
|
105
|
+
label: `Transition to ${state}`,
|
|
106
|
+
type: getTypeForState(state),
|
|
107
|
+
onClick: async () => {
|
|
108
|
+
const transitionError = await transitionToState(state);
|
|
109
|
+
if (transitionError) {
|
|
110
|
+
toast(i18n.t('Failed to transition order to state'), {
|
|
111
|
+
description: transitionError,
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
refreshOrderAndHistory();
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
}));
|
|
118
|
+
}, [entity, transitionToState, i18n]);
|
|
119
|
+
|
|
120
|
+
if (!entity) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const handleModifyClick = async () => {
|
|
125
|
+
try {
|
|
126
|
+
await transitionOrderToStateMutation.mutateAsync({
|
|
127
|
+
id: entity.id,
|
|
128
|
+
state: 'Modifying',
|
|
129
|
+
});
|
|
130
|
+
const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
|
|
131
|
+
await queryClient.invalidateQueries({ queryKey });
|
|
132
|
+
await navigate({ to: `/orders/$id/modify`, params: { id: entity.id } });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
toast(i18n.t('Failed to modify order'), {
|
|
135
|
+
description: error instanceof Error ? error.message : 'Unknown error',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const nextStates = entity.nextStates;
|
|
141
|
+
const showAddPaymentButton = shouldShowAddManualPaymentButton(entity);
|
|
142
|
+
const showFulfillButton = canAddFulfillment(entity);
|
|
143
|
+
|
|
144
|
+
async function refreshOrderAndHistory() {
|
|
145
|
+
if (entity) {
|
|
146
|
+
const queryKey = getDetailQueryOptions(orderDetailDocument, { id: entity.id }).queryKey;
|
|
147
|
+
await queryClient.invalidateQueries({ queryKey });
|
|
148
|
+
queryClient.refetchQueries({ queryKey: orderHistoryQueryKey(entity.id) });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<Page pageId={pageId} form={form} submitHandler={submitHandler} entity={entity}>
|
|
154
|
+
<PageTitle>{titleSlot?.(entity) || <DefaultOrderTitle entity={entity} />}</PageTitle>
|
|
155
|
+
<PageActionBar>
|
|
156
|
+
<PageActionBarRight
|
|
157
|
+
dropdownMenuItems={[
|
|
158
|
+
...(nextStates.includes('Modifying')
|
|
159
|
+
? [
|
|
160
|
+
{
|
|
161
|
+
component: () => (
|
|
162
|
+
<DropdownMenuItem onClick={handleModifyClick}>
|
|
163
|
+
<Pencil className="w-4 h-4" />
|
|
164
|
+
<Trans>Modify</Trans>
|
|
165
|
+
</DropdownMenuItem>
|
|
166
|
+
),
|
|
167
|
+
},
|
|
168
|
+
]
|
|
169
|
+
: []),
|
|
170
|
+
]}
|
|
171
|
+
>
|
|
172
|
+
{showAddPaymentButton && (
|
|
173
|
+
<PermissionGuard requires={['UpdateOrder']}>
|
|
174
|
+
<AddManualPaymentDialog
|
|
175
|
+
order={entity}
|
|
176
|
+
onSuccess={() => {
|
|
177
|
+
refreshEntity();
|
|
178
|
+
}}
|
|
179
|
+
/>
|
|
180
|
+
</PermissionGuard>
|
|
181
|
+
)}
|
|
182
|
+
{showFulfillButton && (
|
|
183
|
+
<PermissionGuard requires={['UpdateOrder']}>
|
|
184
|
+
<FulfillOrderDialog
|
|
185
|
+
order={entity}
|
|
186
|
+
onSuccess={() => {
|
|
187
|
+
refreshOrderAndHistory();
|
|
188
|
+
}}
|
|
189
|
+
/>
|
|
190
|
+
</PermissionGuard>
|
|
191
|
+
)}
|
|
192
|
+
</PageActionBarRight>
|
|
193
|
+
</PageActionBar>
|
|
194
|
+
<PageLayout>
|
|
195
|
+
{/* Main Column Blocks */}
|
|
196
|
+
{beforeOrderTable?.(entity)}
|
|
197
|
+
<PageBlock column="main" blockId="order-table">
|
|
198
|
+
<OrderTable order={entity} pageId={pageId} />
|
|
199
|
+
</PageBlock>
|
|
200
|
+
<PageBlock column="main" blockId="tax-summary" title={<Trans>Tax summary</Trans>}>
|
|
201
|
+
<OrderTaxSummary order={entity} />
|
|
202
|
+
</PageBlock>
|
|
203
|
+
{customFieldConfig?.length ? (
|
|
204
|
+
<PageBlock column="main" blockId="custom-fields">
|
|
205
|
+
<CustomFieldsForm entityType="Order" control={form.control} />
|
|
206
|
+
<div className="flex justify-end">
|
|
207
|
+
<Button
|
|
208
|
+
type="submit"
|
|
209
|
+
disabled={!form.formState.isDirty || !form.formState.isValid}
|
|
210
|
+
>
|
|
211
|
+
<Trans>Save</Trans>
|
|
212
|
+
</Button>
|
|
213
|
+
</div>
|
|
214
|
+
</PageBlock>
|
|
215
|
+
) : null}
|
|
216
|
+
<PageBlock column="main" blockId="payment-details" title={<Trans>Payment details</Trans>}>
|
|
217
|
+
<div className="grid lg:grid-cols-2 gap-4">
|
|
218
|
+
{entity?.payments?.map((payment: any) => (
|
|
219
|
+
<PaymentDetails
|
|
220
|
+
key={payment.id}
|
|
221
|
+
payment={payment}
|
|
222
|
+
currencyCode={entity.currencyCode}
|
|
223
|
+
onSuccess={refreshOrderAndHistory}
|
|
224
|
+
/>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
</PageBlock>
|
|
228
|
+
<PageBlock column="main" blockId="order-history" title={<Trans>Order history</Trans>}>
|
|
229
|
+
<OrderHistoryContainer orderId={orderId} />
|
|
230
|
+
</PageBlock>
|
|
231
|
+
|
|
232
|
+
{/* Side Column Blocks */}
|
|
233
|
+
<PageBlock column="side" blockId="state">
|
|
234
|
+
<StateTransitionControl
|
|
235
|
+
currentState={entity?.state}
|
|
236
|
+
actions={stateTransitionActions}
|
|
237
|
+
isLoading={transitionOrderToStateMutation.isPending}
|
|
238
|
+
/>
|
|
239
|
+
</PageBlock>
|
|
240
|
+
<PageBlock column="side" blockId="customer" title={<Trans>Customer</Trans>}>
|
|
241
|
+
{entity?.customer ? (
|
|
242
|
+
<Button variant="ghost" asChild>
|
|
243
|
+
<Link to={`/customers/${entity.customer.id}`}>
|
|
244
|
+
<User className="w-4 h-4" />
|
|
245
|
+
{entity.customer.firstName} {entity.customer.lastName}
|
|
246
|
+
</Link>
|
|
247
|
+
</Button>
|
|
248
|
+
) : (
|
|
249
|
+
<div className="text-muted-foreground text-xs font-medium p-3 border rounded-md">
|
|
250
|
+
<Trans>No customer</Trans>
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
<div className="mt-4 divide-y">
|
|
254
|
+
{entity?.shippingAddress && (
|
|
255
|
+
<div className="pb-6">
|
|
256
|
+
<div className="font-medium">
|
|
257
|
+
<Trans>Shipping address</Trans>
|
|
258
|
+
</div>
|
|
259
|
+
<OrderAddress address={entity.shippingAddress} />
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
{entity?.billingAddress && (
|
|
263
|
+
<div className="pt-4">
|
|
264
|
+
<div className="font-medium">
|
|
265
|
+
<Trans>Billing address</Trans>
|
|
266
|
+
</div>
|
|
267
|
+
<OrderAddress address={entity.billingAddress} />
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
</PageBlock>
|
|
272
|
+
<PageBlock
|
|
273
|
+
column="side"
|
|
274
|
+
blockId="fulfillment-details"
|
|
275
|
+
title={<Trans>Fulfillment details</Trans>}
|
|
276
|
+
>
|
|
277
|
+
{entity?.fulfillments?.length && entity.fulfillments.length > 0 ? (
|
|
278
|
+
<div className="space-y-2">
|
|
279
|
+
{entity?.fulfillments?.map((fulfillment: any) => (
|
|
280
|
+
<FulfillmentDetails
|
|
281
|
+
key={fulfillment.id}
|
|
282
|
+
order={entity}
|
|
283
|
+
fulfillment={fulfillment}
|
|
284
|
+
onSuccess={() => {
|
|
285
|
+
refreshEntity();
|
|
286
|
+
queryClient.refetchQueries({
|
|
287
|
+
queryKey: orderHistoryQueryKey(entity.id),
|
|
288
|
+
});
|
|
289
|
+
}}
|
|
290
|
+
/>
|
|
291
|
+
))}
|
|
292
|
+
</div>
|
|
293
|
+
) : (
|
|
294
|
+
<div className="text-muted-foreground text-xs font-medium p-3 border rounded-md">
|
|
295
|
+
<Trans>No fulfillments</Trans>
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
</PageBlock>
|
|
299
|
+
</PageLayout>
|
|
300
|
+
</Page>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { HistoryEntry, HistoryEntryProps } from '@/vdb/framework/history-entry/history-entry.js';
|
|
2
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
3
|
+
|
|
4
|
+
export function OrderStateTransitionComponent(props: Readonly<HistoryEntryProps>) {
|
|
5
|
+
const { entry } = props;
|
|
6
|
+
if (entry.data.from === 'Created') return null;
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<HistoryEntry {...props}>
|
|
10
|
+
<p className="text-xs text-muted-foreground">
|
|
11
|
+
<Trans>
|
|
12
|
+
From {entry.data.from} to {entry.data.to}
|
|
13
|
+
</Trans>
|
|
14
|
+
</p>
|
|
15
|
+
</HistoryEntry>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function OrderPaymentTransitionComponent(props: Readonly<HistoryEntryProps>) {
|
|
20
|
+
const { entry } = props;
|
|
21
|
+
return (
|
|
22
|
+
<HistoryEntry {...props}>
|
|
23
|
+
<p className="text-xs text-muted-foreground">
|
|
24
|
+
<Trans>
|
|
25
|
+
Payment #{entry.data.paymentId} transitioned to {entry.data.to}
|
|
26
|
+
</Trans>
|
|
27
|
+
</p>
|
|
28
|
+
</HistoryEntry>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function OrderRefundTransitionComponent(props: Readonly<HistoryEntryProps>) {
|
|
33
|
+
const { entry } = props;
|
|
34
|
+
return (
|
|
35
|
+
<HistoryEntry {...props}>
|
|
36
|
+
<p className="text-xs text-muted-foreground">
|
|
37
|
+
<Trans>
|
|
38
|
+
Refund #{entry.data.refundId} transitioned to {entry.data.to}
|
|
39
|
+
</Trans>
|
|
40
|
+
</p>
|
|
41
|
+
</HistoryEntry>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function OrderFulfillmentTransitionComponent(props: Readonly<HistoryEntryProps>) {
|
|
46
|
+
const { entry } = props;
|
|
47
|
+
return (
|
|
48
|
+
<HistoryEntry {...props}>
|
|
49
|
+
<p className="text-xs text-muted-foreground">
|
|
50
|
+
<Trans>
|
|
51
|
+
Fulfillment #{entry.data.fulfillmentId} from {entry.data.from} to {entry.data.to}
|
|
52
|
+
</Trans>
|
|
53
|
+
</p>
|
|
54
|
+
</HistoryEntry>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function OrderFulfillmentComponent(props: Readonly<HistoryEntryProps>) {
|
|
59
|
+
const { entry } = props;
|
|
60
|
+
return (
|
|
61
|
+
<HistoryEntry {...props}>
|
|
62
|
+
<p className="text-xs text-muted-foreground">
|
|
63
|
+
<Trans>Fulfillment #{entry.data.fulfillmentId} created</Trans>
|
|
64
|
+
</p>
|
|
65
|
+
</HistoryEntry>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function OrderModifiedComponent(props: Readonly<HistoryEntryProps>) {
|
|
70
|
+
const { entry } = props;
|
|
71
|
+
return (
|
|
72
|
+
<HistoryEntry {...props}>
|
|
73
|
+
<p className="text-xs text-muted-foreground">
|
|
74
|
+
<Trans>Order modification #{entry.data.modificationId}</Trans>
|
|
75
|
+
</p>
|
|
76
|
+
</HistoryEntry>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function OrderCustomerUpdatedComponent(props: Readonly<HistoryEntryProps>) {
|
|
81
|
+
return (
|
|
82
|
+
<HistoryEntry {...props}>
|
|
83
|
+
<p className="text-xs text-muted-foreground">
|
|
84
|
+
<Trans>Customer information updated</Trans>
|
|
85
|
+
</p>
|
|
86
|
+
</HistoryEntry>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function OrderCancellationComponent(props: Readonly<HistoryEntryProps>) {
|
|
91
|
+
return (
|
|
92
|
+
<HistoryEntry {...props}>
|
|
93
|
+
<p className="text-xs text-muted-foreground">
|
|
94
|
+
<Trans>Order cancelled</Trans>
|
|
95
|
+
</p>
|
|
96
|
+
</HistoryEntry>
|
|
97
|
+
);
|
|
98
|
+
}
|
package/src/app/routes/_authenticated/_orders/components/order-history/order-history-container.tsx
CHANGED
|
@@ -61,13 +61,15 @@ export function OrderHistoryContainer({ orderId }: Readonly<OrderHistoryContaine
|
|
|
61
61
|
|
|
62
62
|
return (
|
|
63
63
|
<>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
{order ? (
|
|
65
|
+
<OrderHistory
|
|
66
|
+
order={order}
|
|
67
|
+
historyEntries={historyEntries ?? []}
|
|
68
|
+
onAddNote={addNote}
|
|
69
|
+
onUpdateNote={updateNote}
|
|
70
|
+
onDeleteNote={deleteNote}
|
|
71
|
+
/>
|
|
72
|
+
) : null}
|
|
71
73
|
{hasNextPage && (
|
|
72
74
|
<Button type="button" variant="outline" onClick={() => fetchNextPage()}>
|
|
73
75
|
<Trans>Load more</Trans>
|
package/src/app/routes/_authenticated/_orders/components/order-history/order-history-utils.tsx
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { HistoryEntryItem } from '@/vdb/framework/extension-api/types/index.js';
|
|
2
|
+
import { Trans } from '@/vdb/lib/trans.js';
|
|
3
|
+
import {
|
|
4
|
+
ArrowRightToLine,
|
|
5
|
+
Ban,
|
|
6
|
+
CheckIcon,
|
|
7
|
+
CreditCardIcon,
|
|
8
|
+
Edit3,
|
|
9
|
+
SquarePen,
|
|
10
|
+
Truck,
|
|
11
|
+
UserX,
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
import { OrderHistoryOrderDetail } from './order-history-types.js';
|
|
14
|
+
|
|
15
|
+
export function orderHistoryUtils(order: OrderHistoryOrderDetail) {
|
|
16
|
+
const getTimelineIcon = (entry: HistoryEntryItem) => {
|
|
17
|
+
switch (entry.type) {
|
|
18
|
+
case 'ORDER_PAYMENT_TRANSITION':
|
|
19
|
+
return <CreditCardIcon className="h-4 w-4" />;
|
|
20
|
+
case 'ORDER_REFUND_TRANSITION':
|
|
21
|
+
return <CreditCardIcon className="h-4 w-4" />;
|
|
22
|
+
case 'ORDER_NOTE':
|
|
23
|
+
return <SquarePen className="h-4 w-4" />;
|
|
24
|
+
case 'ORDER_STATE_TRANSITION':
|
|
25
|
+
if (entry.data.to === 'Delivered') {
|
|
26
|
+
return <CheckIcon className="h-4 w-4" />;
|
|
27
|
+
}
|
|
28
|
+
if (entry.data.to === 'Cancelled') {
|
|
29
|
+
return <Ban className="h-4 w-4" />;
|
|
30
|
+
}
|
|
31
|
+
return <ArrowRightToLine className="h-4 w-4" />;
|
|
32
|
+
case 'ORDER_FULFILLMENT_TRANSITION':
|
|
33
|
+
if (entry.data.to === 'Shipped' || entry.data.to === 'Delivered') {
|
|
34
|
+
return <Truck className="h-4 w-4" />;
|
|
35
|
+
}
|
|
36
|
+
return <ArrowRightToLine className="h-4 w-4" />;
|
|
37
|
+
case 'ORDER_FULFILLMENT':
|
|
38
|
+
return <Truck className="h-4 w-4" />;
|
|
39
|
+
case 'ORDER_MODIFIED':
|
|
40
|
+
return <Edit3 className="h-4 w-4" />;
|
|
41
|
+
case 'ORDER_CUSTOMER_UPDATED':
|
|
42
|
+
return <UserX className="h-4 w-4" />;
|
|
43
|
+
case 'ORDER_CANCELLATION':
|
|
44
|
+
return <Ban className="h-4 w-4" />;
|
|
45
|
+
default:
|
|
46
|
+
return <CheckIcon className="h-4 w-4" />;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const getTitle = (entry: HistoryEntryItem) => {
|
|
51
|
+
switch (entry.type) {
|
|
52
|
+
case 'ORDER_PAYMENT_TRANSITION':
|
|
53
|
+
if (entry.data.to === 'Settled') {
|
|
54
|
+
return <Trans>Payment settled</Trans>;
|
|
55
|
+
}
|
|
56
|
+
if (entry.data.to === 'Authorized') {
|
|
57
|
+
return <Trans>Payment authorized</Trans>;
|
|
58
|
+
}
|
|
59
|
+
if (entry.data.to === 'Declined' || entry.data.to === 'Cancelled') {
|
|
60
|
+
return <Trans>Payment failed</Trans>;
|
|
61
|
+
}
|
|
62
|
+
return <Trans>Payment transitioned</Trans>;
|
|
63
|
+
case 'ORDER_REFUND_TRANSITION':
|
|
64
|
+
if (entry.data.to === 'Settled') {
|
|
65
|
+
return <Trans>Refund settled</Trans>;
|
|
66
|
+
}
|
|
67
|
+
return <Trans>Refund transitioned</Trans>;
|
|
68
|
+
case 'ORDER_NOTE':
|
|
69
|
+
return <Trans>Note added</Trans>;
|
|
70
|
+
case 'ORDER_STATE_TRANSITION': {
|
|
71
|
+
if (entry.data.from === 'Created') {
|
|
72
|
+
return <Trans>Order placed</Trans>;
|
|
73
|
+
}
|
|
74
|
+
if (entry.data.to === 'Delivered') {
|
|
75
|
+
return <Trans>Order fulfilled</Trans>;
|
|
76
|
+
}
|
|
77
|
+
if (entry.data.to === 'Cancelled') {
|
|
78
|
+
return <Trans>Order cancelled</Trans>;
|
|
79
|
+
}
|
|
80
|
+
if (entry.data.to === 'Shipped') {
|
|
81
|
+
return <Trans>Order shipped</Trans>;
|
|
82
|
+
}
|
|
83
|
+
return <Trans>Order transitioned</Trans>;
|
|
84
|
+
}
|
|
85
|
+
case 'ORDER_FULFILLMENT_TRANSITION':
|
|
86
|
+
if (entry.data.to === 'Shipped') {
|
|
87
|
+
return <Trans>Order shipped</Trans>;
|
|
88
|
+
}
|
|
89
|
+
if (entry.data.to === 'Delivered') {
|
|
90
|
+
return <Trans>Order delivered</Trans>;
|
|
91
|
+
}
|
|
92
|
+
return <Trans>Fulfillment transitioned</Trans>;
|
|
93
|
+
case 'ORDER_FULFILLMENT':
|
|
94
|
+
return <Trans>Fulfillment created</Trans>;
|
|
95
|
+
case 'ORDER_MODIFIED':
|
|
96
|
+
return <Trans>Order modified</Trans>;
|
|
97
|
+
case 'ORDER_CUSTOMER_UPDATED':
|
|
98
|
+
return <Trans>Customer updated</Trans>;
|
|
99
|
+
case 'ORDER_CANCELLATION':
|
|
100
|
+
return <Trans>Order cancelled</Trans>;
|
|
101
|
+
default:
|
|
102
|
+
return <Trans>{entry.type.replace(/_/g, ' ').toLowerCase()}</Trans>;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const getIconColor = ({ type, data }: HistoryEntryItem) => {
|
|
107
|
+
const success = 'bg-success text-success-foreground';
|
|
108
|
+
const destructive = 'bg-danger text-danger-foreground';
|
|
109
|
+
const regular = 'bg-muted text-muted-foreground';
|
|
110
|
+
|
|
111
|
+
if (type === 'ORDER_PAYMENT_TRANSITION' && data.to === 'Settled') {
|
|
112
|
+
return success;
|
|
113
|
+
}
|
|
114
|
+
if (type === 'ORDER_STATE_TRANSITION' && data.to === 'Delivered') {
|
|
115
|
+
return success;
|
|
116
|
+
}
|
|
117
|
+
if (type === 'ORDER_FULFILLMENT_TRANSITION' && data.to === 'Delivered') {
|
|
118
|
+
return success;
|
|
119
|
+
}
|
|
120
|
+
if (type === 'ORDER_CANCELLATION') {
|
|
121
|
+
return destructive;
|
|
122
|
+
}
|
|
123
|
+
if (type === 'ORDER_STATE_TRANSITION' && data.to === 'Cancelled') {
|
|
124
|
+
return destructive;
|
|
125
|
+
}
|
|
126
|
+
if (type === 'ORDER_PAYMENT_TRANSITION' && (data.to === 'Declined' || data.to === 'Cancelled')) {
|
|
127
|
+
return destructive;
|
|
128
|
+
}
|
|
129
|
+
return regular;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const getActorName = (entry: HistoryEntryItem) => {
|
|
133
|
+
if (entry.administrator) {
|
|
134
|
+
return `${entry.administrator.firstName} ${entry.administrator.lastName}`;
|
|
135
|
+
} else if (order?.customer) {
|
|
136
|
+
return `${order.customer.firstName} ${order.customer.lastName}`;
|
|
137
|
+
}
|
|
138
|
+
return '';
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const isPrimaryEvent = (entry: HistoryEntryItem) => {
|
|
142
|
+
switch (entry.type) {
|
|
143
|
+
case 'ORDER_STATE_TRANSITION':
|
|
144
|
+
return (
|
|
145
|
+
entry.data.to === 'Delivered' ||
|
|
146
|
+
entry.data.to === 'Cancelled' ||
|
|
147
|
+
entry.data.to === 'Settled' ||
|
|
148
|
+
entry.data.from === 'Created'
|
|
149
|
+
);
|
|
150
|
+
case 'ORDER_REFUND_TRANSITION':
|
|
151
|
+
return entry.data.to === 'Settled';
|
|
152
|
+
case 'ORDER_PAYMENT_TRANSITION':
|
|
153
|
+
return entry.data.to === 'Settled' || entry.data.to === 'Cancelled';
|
|
154
|
+
case 'ORDER_FULFILLMENT_TRANSITION':
|
|
155
|
+
return entry.data.to === 'Delivered' || entry.data.to === 'Shipped';
|
|
156
|
+
case 'ORDER_NOTE':
|
|
157
|
+
case 'ORDER_MODIFIED':
|
|
158
|
+
case 'ORDER_CUSTOMER_UPDATED':
|
|
159
|
+
case 'ORDER_CANCELLATION':
|
|
160
|
+
return true;
|
|
161
|
+
default:
|
|
162
|
+
return false; // All other events are secondary
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
getTimelineIcon,
|
|
168
|
+
getTitle,
|
|
169
|
+
getIconColor,
|
|
170
|
+
getActorName,
|
|
171
|
+
isPrimaryEvent,
|
|
172
|
+
};
|
|
173
|
+
}
|