@spree/seller-dashboard 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/vite-plugin/index.js +51 -0
- package/package.json +64 -0
- package/src/api-client.ts +203 -0
- package/src/components/centered-message.tsx +8 -0
- package/src/components/finance/balance-summary.tsx +69 -0
- package/src/components/onboarding-nav-badge.tsx +38 -0
- package/src/components/orders/fulfillment-deliveries.tsx +116 -0
- package/src/components/orders/fulfillment-delivery-dialog.tsx +82 -0
- package/src/components/orders/fulfillment-edit-dialog.tsx +95 -0
- package/src/components/orders/fulfillment-fulfill-form.tsx +132 -0
- package/src/components/orders/fulfillment-label-upload-dialog.tsx +95 -0
- package/src/components/orders/fulfillment-split-dialog.tsx +142 -0
- package/src/components/orders/fulfillments-card.tsx +289 -0
- package/src/components/orders/line-label.ts +28 -0
- package/src/components/orders/order-cancel-dialog.tsx +74 -0
- package/src/components/orders/order-cards.tsx +297 -0
- package/src/components/orders/order-earnings-card.tsx +99 -0
- package/src/components/orders/order-notes-cards.tsx +83 -0
- package/src/components/orders/order-payment-card.tsx +94 -0
- package/src/components/orders/post-sale-cards.tsx +347 -0
- package/src/components/orders/post-sale-create-dialogs.tsx +201 -0
- package/src/components/orders/returns-card.tsx +258 -0
- package/src/components/orders/shipping-label-row.tsx +124 -0
- package/src/components/product-status-card.tsx +111 -0
- package/src/components/read-row.tsx +16 -0
- package/src/components/retryable-error.tsx +23 -0
- package/src/components/seller-address-card.tsx +123 -0
- package/src/components/seller-business-card.tsx +152 -0
- package/src/components/seller-returns-location-card.tsx +151 -0
- package/src/components/seller-switcher.tsx +124 -0
- package/src/components/seller-tax-identifiers-card.tsx +63 -0
- package/src/create-router.ts +32 -0
- package/src/hooks/use-fulfillments.ts +155 -0
- package/src/hooks/use-ledger.ts +46 -0
- package/src/hooks/use-order.ts +62 -0
- package/src/hooks/use-post-sale.ts +202 -0
- package/src/hooks/use-reasons.ts +34 -0
- package/src/hooks/use-stock-locations.ts +23 -0
- package/src/hooks/use-store-timezone.ts +26 -0
- package/src/i18n.ts +26 -0
- package/src/index.ts +27 -0
- package/src/lib/packing-slip.ts +109 -0
- package/src/locales/en.json +841 -0
- package/src/nav/default.ts +108 -0
- package/src/nav/settings.ts +114 -0
- package/src/pages/accept-invitation.tsx +127 -0
- package/src/pages/delivery-methods.tsx +647 -0
- package/src/pages/earnings.tsx +44 -0
- package/src/pages/forgot-password.tsx +100 -0
- package/src/pages/home.tsx +132 -0
- package/src/pages/login.tsx +94 -0
- package/src/pages/onboarding.tsx +539 -0
- package/src/pages/order.tsx +119 -0
- package/src/pages/orders.tsx +33 -0
- package/src/pages/payout.tsx +154 -0
- package/src/pages/payouts.tsx +32 -0
- package/src/pages/policies.tsx +292 -0
- package/src/pages/product.tsx +250 -0
- package/src/pages/products.tsx +228 -0
- package/src/pages/profile.tsx +389 -0
- package/src/pages/reset-password.tsx +105 -0
- package/src/pages/seller-picker.tsx +46 -0
- package/src/pages/team.tsx +468 -0
- package/src/panel-chrome.tsx +36 -0
- package/src/routeTree.gen.ts +590 -0
- package/src/routes/__root.tsx +15 -0
- package/src/routes/_authenticated/$sellerId/earnings/index.tsx +15 -0
- package/src/routes/_authenticated/$sellerId/index.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/onboarding.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/orders/$orderId.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/orders/index.tsx +15 -0
- package/src/routes/_authenticated/$sellerId/payouts/$payoutId.tsx +7 -0
- package/src/routes/_authenticated/$sellerId/payouts/index.tsx +15 -0
- package/src/routes/_authenticated/$sellerId/products/$productId.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/products/index.tsx +23 -0
- package/src/routes/_authenticated/$sellerId/products/new.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/profile.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/settings/delivery-methods.tsx +14 -0
- package/src/routes/_authenticated/$sellerId/settings/index.tsx +18 -0
- package/src/routes/_authenticated/$sellerId/settings/package-types.tsx +21 -0
- package/src/routes/_authenticated/$sellerId/settings/policies.tsx +14 -0
- package/src/routes/_authenticated/$sellerId/settings/stock-locations.tsx +22 -0
- package/src/routes/_authenticated/$sellerId/settings/team.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/settings.tsx +18 -0
- package/src/routes/_authenticated/$sellerId.tsx +49 -0
- package/src/routes/_authenticated/index.tsx +48 -0
- package/src/routes/_authenticated.tsx +13 -0
- package/src/routes/accept-invitation.$invitationId.tsx +19 -0
- package/src/routes/forgot-password.tsx +15 -0
- package/src/routes/login.tsx +15 -0
- package/src/routes/reset-password.tsx +21 -0
- package/src/schemas/fulfillment.ts +31 -0
- package/src/seller-dashboard.tsx +50 -0
- package/src/styles.css +18 -0
- package/src/tables/delivery-methods.tsx +68 -0
- package/src/tables/orders.tsx +116 -0
- package/src/tables/payouts.tsx +87 -0
- package/src/tables/policies.tsx +37 -0
- package/src/tables/products.tsx +56 -0
- package/src/tables/transfers.tsx +118 -0
- package/src/vite/index.ts +87 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useResourceKeyBuilder } from '@spree/dashboard-core'
|
|
2
|
+
import { toastManager } from '@spree/dashboard-ui'
|
|
3
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
4
|
+
import { sellerClient } from '../api-client'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Everything a seller does to a parcel.
|
|
8
|
+
*
|
|
9
|
+
* Each verb is its own endpoint because each carries different arguments —
|
|
10
|
+
* shipping takes a tracking number and which units went, splitting takes a
|
|
11
|
+
* variant and a quantity.
|
|
12
|
+
*/
|
|
13
|
+
export function useFulfillmentActions(orderId: string) {
|
|
14
|
+
const queryClient = useQueryClient()
|
|
15
|
+
const buildKey = useResourceKeyBuilder()
|
|
16
|
+
|
|
17
|
+
function useFulfillmentMutation<TParams>(mutationFn: (params: TParams) => Promise<unknown>) {
|
|
18
|
+
return useMutation({
|
|
19
|
+
mutationFn,
|
|
20
|
+
onSuccess: () => {
|
|
21
|
+
queryClient.invalidateQueries({ queryKey: buildKey('seller-order', orderId) })
|
|
22
|
+
queryClient.invalidateQueries({ queryKey: buildKey('seller-orders') })
|
|
23
|
+
},
|
|
24
|
+
onError: (error) => {
|
|
25
|
+
// A rejected workflow — a parcel that cannot ship, a split that would
|
|
26
|
+
// empty its source — otherwise looks like a dead button.
|
|
27
|
+
toastManager.add({
|
|
28
|
+
type: 'error',
|
|
29
|
+
title: error instanceof Error ? error.message : String(error),
|
|
30
|
+
})
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fulfill = useFulfillmentMutation(
|
|
36
|
+
({
|
|
37
|
+
fulfillmentId,
|
|
38
|
+
...params
|
|
39
|
+
}: {
|
|
40
|
+
fulfillmentId: string
|
|
41
|
+
tracking?: string
|
|
42
|
+
tracking_carrier?: string
|
|
43
|
+
notify_customer?: boolean
|
|
44
|
+
items?: Array<{ item_id: string; quantity: number }>
|
|
45
|
+
}) => sellerClient().orders.fulfillments.fulfill(orderId, fulfillmentId, params),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const update = useFulfillmentMutation(
|
|
49
|
+
({
|
|
50
|
+
fulfillmentId,
|
|
51
|
+
...params
|
|
52
|
+
}: {
|
|
53
|
+
fulfillmentId: string
|
|
54
|
+
tracking?: string
|
|
55
|
+
tracking_carrier?: string
|
|
56
|
+
stock_location_id?: string
|
|
57
|
+
selected_delivery_rate_id?: string
|
|
58
|
+
}) => sellerClient().orders.fulfillments.update(orderId, fulfillmentId, params),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const cancel = useFulfillmentMutation((fulfillmentId: string) =>
|
|
62
|
+
sellerClient().orders.fulfillments.cancel(orderId, fulfillmentId),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const split = useFulfillmentMutation(
|
|
66
|
+
({
|
|
67
|
+
fulfillmentId,
|
|
68
|
+
...params
|
|
69
|
+
}: {
|
|
70
|
+
fulfillmentId: string
|
|
71
|
+
variant_id: string
|
|
72
|
+
quantity: number
|
|
73
|
+
stock_location_id?: string
|
|
74
|
+
}) => sellerClient().orders.fulfillments.split(orderId, fulfillmentId, params),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// A parcel's consignments. One parcel usually travels as one, but three
|
|
78
|
+
// boxes or a pallet under a freight PRO number are the same shape, and each
|
|
79
|
+
// carries its own tracking number and carrier status.
|
|
80
|
+
const createDelivery = useFulfillmentMutation(
|
|
81
|
+
({
|
|
82
|
+
fulfillmentId,
|
|
83
|
+
...params
|
|
84
|
+
}: {
|
|
85
|
+
fulfillmentId: string
|
|
86
|
+
tracking_number: string
|
|
87
|
+
carrier?: string
|
|
88
|
+
service?: string
|
|
89
|
+
tracking_url?: string
|
|
90
|
+
}) => sellerClient().orders.fulfillments.deliveries.create(orderId, fulfillmentId, params),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const updateDelivery = useFulfillmentMutation(
|
|
94
|
+
({
|
|
95
|
+
fulfillmentId,
|
|
96
|
+
deliveryId,
|
|
97
|
+
...params
|
|
98
|
+
}: {
|
|
99
|
+
fulfillmentId: string
|
|
100
|
+
deliveryId: string
|
|
101
|
+
tracking_number?: string
|
|
102
|
+
carrier?: string
|
|
103
|
+
service?: string
|
|
104
|
+
tracking_url?: string
|
|
105
|
+
}) =>
|
|
106
|
+
sellerClient().orders.fulfillments.deliveries.update(
|
|
107
|
+
orderId,
|
|
108
|
+
fulfillmentId,
|
|
109
|
+
deliveryId,
|
|
110
|
+
params,
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const deleteDelivery = useFulfillmentMutation(
|
|
115
|
+
({ fulfillmentId, deliveryId }: { fulfillmentId: string; deliveryId: string }) =>
|
|
116
|
+
sellerClient().orders.fulfillments.deliveries.delete(orderId, fulfillmentId, deliveryId),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
// Postage the seller bought elsewhere and is recording here. Buying and
|
|
120
|
+
// refunding run through the operator's carrier account, so neither is
|
|
121
|
+
// offered — the endpoint refuses both.
|
|
122
|
+
const uploadLabel = useFulfillmentMutation(
|
|
123
|
+
({
|
|
124
|
+
fulfillmentId,
|
|
125
|
+
...params
|
|
126
|
+
}: {
|
|
127
|
+
fulfillmentId: string
|
|
128
|
+
file: string
|
|
129
|
+
// Required: a label with no tracking number is refused server-side,
|
|
130
|
+
// since nothing could then follow the parcel it prints.
|
|
131
|
+
tracking_number: string
|
|
132
|
+
carrier?: string
|
|
133
|
+
service?: string
|
|
134
|
+
cost?: string
|
|
135
|
+
currency?: string
|
|
136
|
+
}) => sellerClient().orders.fulfillments.labels.create(orderId, fulfillmentId, params),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
const deleteLabel = useFulfillmentMutation(
|
|
140
|
+
({ fulfillmentId, labelId }: { fulfillmentId: string; labelId: string }) =>
|
|
141
|
+
sellerClient().orders.fulfillments.labels.delete(orderId, fulfillmentId, labelId),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
fulfill,
|
|
146
|
+
update,
|
|
147
|
+
cancel,
|
|
148
|
+
split,
|
|
149
|
+
createDelivery,
|
|
150
|
+
updateDelivery,
|
|
151
|
+
deleteDelivery,
|
|
152
|
+
uploadLabel,
|
|
153
|
+
deleteLabel,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useResourceKey } from '@spree/dashboard-core'
|
|
2
|
+
import { useQuery } from '@tanstack/react-query'
|
|
3
|
+
import { sellerClient } from '../api-client'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where this seller stands, one row per currency.
|
|
7
|
+
*
|
|
8
|
+
* Nothing is ever converted between currencies, so a seller trading in two
|
|
9
|
+
* has two positions and the panel shows both rather than a total.
|
|
10
|
+
*/
|
|
11
|
+
export function useBalances() {
|
|
12
|
+
return useQuery({
|
|
13
|
+
queryKey: useResourceKey('seller-balances'),
|
|
14
|
+
queryFn: () => sellerClient().balances.list(),
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One settlement, for its own page. */
|
|
19
|
+
export function usePayout(payoutId: string) {
|
|
20
|
+
return useQuery({
|
|
21
|
+
queryKey: useResourceKey('seller-payout', payoutId),
|
|
22
|
+
queryFn: () => sellerClient().payouts.get(payoutId),
|
|
23
|
+
enabled: !!payoutId,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The earnings a settlement covers, or the ones an order produced.
|
|
29
|
+
*
|
|
30
|
+
* Both read the same list filtered a different way, so the pages that ask
|
|
31
|
+
* either question share one hook and one query key shape.
|
|
32
|
+
*
|
|
33
|
+
* Paged rather than capped: the server clamps `limit` at 100, and a payout
|
|
34
|
+
* covering more than that must not render one page and call it the whole
|
|
35
|
+
* settlement.
|
|
36
|
+
*/
|
|
37
|
+
export function useTransfers(filter: { payout_id_eq?: string; order_id_eq?: string }, page = 1) {
|
|
38
|
+
const key = filter.payout_id_eq ?? filter.order_id_eq ?? 'all'
|
|
39
|
+
|
|
40
|
+
return useQuery({
|
|
41
|
+
queryKey: useResourceKey('seller-transfers', key, String(page)),
|
|
42
|
+
queryFn: () => sellerClient().transfers.list({ ...filter, limit: 100, page }),
|
|
43
|
+
enabled: !!(filter.payout_id_eq || filter.order_id_eq),
|
|
44
|
+
placeholderData: (previous) => previous,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useResourceKey, useResourceKeyBuilder } from '@spree/dashboard-core'
|
|
2
|
+
import { toastManager } from '@spree/dashboard-ui'
|
|
3
|
+
import type { Order } from '@spree/seller-sdk'
|
|
4
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
5
|
+
import { sellerClient } from '../api-client'
|
|
6
|
+
|
|
7
|
+
/** One order, with everything the page renders about it. */
|
|
8
|
+
export function useOrder(orderId: string) {
|
|
9
|
+
return useQuery({
|
|
10
|
+
queryKey: useResourceKey('seller-order', orderId),
|
|
11
|
+
// The shares of the basket's payment are expand-gated, and the payment
|
|
12
|
+
// card is what reads them.
|
|
13
|
+
queryFn: () => sellerClient().orders.get(orderId, { expand: ['payment_splits'] }),
|
|
14
|
+
enabled: !!orderId,
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Withdrawing from an order this seller cannot fulfil. */
|
|
19
|
+
export function useCancelOrder(orderId: string) {
|
|
20
|
+
return useOrderMutation(
|
|
21
|
+
orderId,
|
|
22
|
+
(params?: {
|
|
23
|
+
cancel_reason_id?: string
|
|
24
|
+
cancel_note?: string
|
|
25
|
+
refund_payments?: boolean
|
|
26
|
+
notify_customer?: boolean
|
|
27
|
+
}) => sellerClient().orders.cancel(orderId, params),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The seller's working note, and what the buyer asked for. */
|
|
32
|
+
export function useUpdateOrderNotes(orderId: string) {
|
|
33
|
+
return useOrderMutation(orderId, (params: { customer_note?: string; internal_note?: string }) =>
|
|
34
|
+
sellerClient().orders.notes.update(orderId, params),
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A write against the order itself. Refreshes the order and the list behind
|
|
40
|
+
* it — a canceled order reads differently in both places.
|
|
41
|
+
*/
|
|
42
|
+
export function useOrderMutation<TParams, TResult = Order>(
|
|
43
|
+
orderId: string,
|
|
44
|
+
mutationFn: (params: TParams) => Promise<TResult>,
|
|
45
|
+
) {
|
|
46
|
+
const queryClient = useQueryClient()
|
|
47
|
+
const buildKey = useResourceKeyBuilder()
|
|
48
|
+
|
|
49
|
+
return useMutation({
|
|
50
|
+
mutationFn,
|
|
51
|
+
onSuccess: () => {
|
|
52
|
+
queryClient.invalidateQueries({ queryKey: buildKey('seller-order', orderId) })
|
|
53
|
+
queryClient.invalidateQueries({ queryKey: buildKey('seller-orders') })
|
|
54
|
+
},
|
|
55
|
+
onError: (error) => {
|
|
56
|
+
toastManager.add({
|
|
57
|
+
type: 'error',
|
|
58
|
+
title: error instanceof Error ? error.message : String(error),
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { useResourceKey, useResourceKeyBuilder } from '@spree/dashboard-core'
|
|
2
|
+
import { toastManager } from '@spree/dashboard-ui'
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
4
|
+
import { sellerClient } from '../api-client'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What has come back, or gone wrong, on one order.
|
|
8
|
+
*
|
|
9
|
+
* Fetched separately from the order so its payload stays lean — most orders
|
|
10
|
+
* have none of these.
|
|
11
|
+
*/
|
|
12
|
+
export function useOrderReturns(orderId: string) {
|
|
13
|
+
return useQuery({
|
|
14
|
+
queryKey: useResourceKey('seller-order-returns', orderId),
|
|
15
|
+
queryFn: () => sellerClient().orders.returns.list(orderId),
|
|
16
|
+
enabled: !!orderId,
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useOrderExchanges(orderId: string) {
|
|
21
|
+
return useQuery({
|
|
22
|
+
queryKey: useResourceKey('seller-order-exchanges', orderId),
|
|
23
|
+
queryFn: () => sellerClient().orders.exchanges.list(orderId),
|
|
24
|
+
enabled: !!orderId,
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useOrderClaims(orderId: string) {
|
|
29
|
+
return useQuery({
|
|
30
|
+
queryKey: useResourceKey('seller-order-claims', orderId),
|
|
31
|
+
queryFn: () => sellerClient().orders.claims.list(orderId),
|
|
32
|
+
enabled: !!orderId,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Refreshes the list this write belongs to and the order itself — refunding
|
|
38
|
+
* changes what the order is owed.
|
|
39
|
+
*/
|
|
40
|
+
function usePostSaleMutation<TParams>(
|
|
41
|
+
orderId: string,
|
|
42
|
+
resourceKey: string,
|
|
43
|
+
mutationFn: (params: TParams) => Promise<unknown>,
|
|
44
|
+
) {
|
|
45
|
+
const queryClient = useQueryClient()
|
|
46
|
+
const buildKey = useResourceKeyBuilder()
|
|
47
|
+
|
|
48
|
+
return useMutation({
|
|
49
|
+
mutationFn,
|
|
50
|
+
onSuccess: () => {
|
|
51
|
+
queryClient.invalidateQueries({ queryKey: buildKey(resourceKey, orderId) })
|
|
52
|
+
queryClient.invalidateQueries({ queryKey: buildKey('seller-order', orderId) })
|
|
53
|
+
},
|
|
54
|
+
onError: (error) => {
|
|
55
|
+
toastManager.add({
|
|
56
|
+
type: 'error',
|
|
57
|
+
title: error instanceof Error ? error.message : String(error),
|
|
58
|
+
})
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function useReturnActions(orderId: string) {
|
|
64
|
+
const key = 'seller-order-returns'
|
|
65
|
+
const returns = () => sellerClient().orders.returns
|
|
66
|
+
|
|
67
|
+
const create = usePostSaleMutation(
|
|
68
|
+
orderId,
|
|
69
|
+
key,
|
|
70
|
+
(params: {
|
|
71
|
+
items: Array<{ fulfillment_item_id: string; quantity: number }>
|
|
72
|
+
memo?: string
|
|
73
|
+
reason_id?: string
|
|
74
|
+
stock_location_id?: string
|
|
75
|
+
}) => returns().create(orderId, params),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
const approve = usePostSaleMutation(orderId, key, (id: string) => returns().approve(orderId, id))
|
|
79
|
+
|
|
80
|
+
const receive = usePostSaleMutation(
|
|
81
|
+
orderId,
|
|
82
|
+
key,
|
|
83
|
+
({
|
|
84
|
+
returnId,
|
|
85
|
+
items,
|
|
86
|
+
}: {
|
|
87
|
+
returnId: string
|
|
88
|
+
items?: Array<{ return_line_item_id: string; quantity: number; resellable?: boolean }>
|
|
89
|
+
}) => returns().receive(orderId, returnId, items ? { items } : undefined),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const refund = usePostSaleMutation(
|
|
93
|
+
orderId,
|
|
94
|
+
key,
|
|
95
|
+
({
|
|
96
|
+
returnId,
|
|
97
|
+
refundMethod,
|
|
98
|
+
amount,
|
|
99
|
+
}: {
|
|
100
|
+
returnId: string
|
|
101
|
+
refundMethod?: 'original_payment' | 'store_credit'
|
|
102
|
+
amount?: string
|
|
103
|
+
}) => returns().refund(orderId, returnId, { refund_method: refundMethod, amount }),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const cancel = usePostSaleMutation(orderId, key, (id: string) => returns().cancel(orderId, id))
|
|
107
|
+
|
|
108
|
+
return { create, approve, receive, refund, cancel }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function useExchangeActions(orderId: string) {
|
|
112
|
+
const key = 'seller-order-exchanges'
|
|
113
|
+
const exchanges = () => sellerClient().orders.exchanges
|
|
114
|
+
|
|
115
|
+
const create = usePostSaleMutation(
|
|
116
|
+
orderId,
|
|
117
|
+
key,
|
|
118
|
+
(params: {
|
|
119
|
+
items: Array<{ fulfillment_item_id: string; new_variant_id: string; quantity: number }>
|
|
120
|
+
memo?: string
|
|
121
|
+
reason_id?: string
|
|
122
|
+
}) => exchanges().create(orderId, params),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const approve = usePostSaleMutation(orderId, key, (id: string) =>
|
|
126
|
+
exchanges().approve(orderId, id),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const receive = usePostSaleMutation(
|
|
130
|
+
orderId,
|
|
131
|
+
key,
|
|
132
|
+
({
|
|
133
|
+
exchangeId,
|
|
134
|
+
items,
|
|
135
|
+
}: {
|
|
136
|
+
exchangeId: string
|
|
137
|
+
items?: Array<{ exchange_line_item_id: string; quantity: number; resellable?: boolean }>
|
|
138
|
+
}) => exchanges().receive(orderId, exchangeId, items ? { items } : undefined),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const fulfill = usePostSaleMutation(
|
|
142
|
+
orderId,
|
|
143
|
+
key,
|
|
144
|
+
({
|
|
145
|
+
exchangeId,
|
|
146
|
+
refundMethod,
|
|
147
|
+
}: {
|
|
148
|
+
exchangeId: string
|
|
149
|
+
refundMethod?: 'original_payment' | 'store_credit'
|
|
150
|
+
}) => exchanges().fulfill(orderId, exchangeId, { refund_method: refundMethod }),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const cancel = usePostSaleMutation(orderId, key, (id: string) => exchanges().cancel(orderId, id))
|
|
154
|
+
|
|
155
|
+
return { create, approve, receive, fulfill, cancel }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function useClaimActions(orderId: string) {
|
|
159
|
+
const key = 'seller-order-claims'
|
|
160
|
+
const claims = () => sellerClient().orders.claims
|
|
161
|
+
|
|
162
|
+
const create = usePostSaleMutation(
|
|
163
|
+
orderId,
|
|
164
|
+
key,
|
|
165
|
+
(params: {
|
|
166
|
+
items: Array<{ line_item_id: string; quantity: number; description?: string }>
|
|
167
|
+
memo?: string
|
|
168
|
+
reason_id?: string
|
|
169
|
+
}) => claims().create(orderId, params),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const approve = usePostSaleMutation(orderId, key, (id: string) => claims().approve(orderId, id))
|
|
173
|
+
|
|
174
|
+
const resolve = usePostSaleMutation(
|
|
175
|
+
orderId,
|
|
176
|
+
key,
|
|
177
|
+
({
|
|
178
|
+
claimId,
|
|
179
|
+
resolution,
|
|
180
|
+
refundMethod,
|
|
181
|
+
amount,
|
|
182
|
+
replacementLineItemIds,
|
|
183
|
+
}: {
|
|
184
|
+
claimId: string
|
|
185
|
+
resolution: 'refund' | 'replacement' | 'refund_and_replacement'
|
|
186
|
+
refundMethod?: 'original_payment' | 'store_credit'
|
|
187
|
+
amount?: string
|
|
188
|
+
replacementLineItemIds?: string[]
|
|
189
|
+
}) =>
|
|
190
|
+
claims().resolve(orderId, claimId, {
|
|
191
|
+
resolution,
|
|
192
|
+
refund_method: refundMethod,
|
|
193
|
+
amount,
|
|
194
|
+
replacement_line_item_ids: replacementLineItemIds,
|
|
195
|
+
}),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const deny = usePostSaleMutation(orderId, key, (id: string) => claims().deny(orderId, id))
|
|
199
|
+
const cancel = usePostSaleMutation(orderId, key, (id: string) => claims().cancel(orderId, id))
|
|
200
|
+
|
|
201
|
+
return { create, approve, resolve, deny, cancel }
|
|
202
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useResourceKey } from '@spree/dashboard-core'
|
|
2
|
+
import { useQuery } from '@tanstack/react-query'
|
|
3
|
+
import { sellerClient } from '../api-client'
|
|
4
|
+
|
|
5
|
+
export type ReasonKind = 'return-reasons' | 'claim-reasons' | 'order-cancellation-reasons'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The marketplace's vocabulary for why goods came back, what went wrong, or
|
|
9
|
+
* why an order was called off. Operator-owned and slow-moving, so one fetch
|
|
10
|
+
* serves every dialog.
|
|
11
|
+
*/
|
|
12
|
+
export function useReasons(kind: ReasonKind, enabled = true) {
|
|
13
|
+
return useQuery({
|
|
14
|
+
queryKey: useResourceKey(kind),
|
|
15
|
+
queryFn: () => {
|
|
16
|
+
const client = sellerClient()
|
|
17
|
+
if (kind === 'return-reasons') return client.returnReasons.list()
|
|
18
|
+
if (kind === 'claim-reasons') return client.claimReasons.list()
|
|
19
|
+
return client.orderCancellationReasons.list()
|
|
20
|
+
},
|
|
21
|
+
staleTime: 5 * 60 * 1000,
|
|
22
|
+
enabled,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Registry data — it only changes on deploy. */
|
|
27
|
+
export function useTrackingCarriers(enabled = true) {
|
|
28
|
+
return useQuery({
|
|
29
|
+
queryKey: useResourceKey('seller-tracking-carriers'),
|
|
30
|
+
queryFn: () => sellerClient().trackingCarriers.list(),
|
|
31
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
32
|
+
enabled,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useResourceKey } from '@spree/dashboard-core'
|
|
2
|
+
import { useQuery } from '@tanstack/react-query'
|
|
3
|
+
import { sellerClient } from '../api-client'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where this seller keeps stock.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately not `useStockLocations` from `@spree/dashboard-core`: that one
|
|
9
|
+
* reads the admin client and would answer with the marketplace's warehouses
|
|
10
|
+
* too, so a seller moving a parcel could pick a shelf that is not theirs. The
|
|
11
|
+
* seller endpoint is rooted in `current_seller.stock_locations`, so the list
|
|
12
|
+
* is theirs by construction rather than by filtering here.
|
|
13
|
+
*
|
|
14
|
+
* Slow-moving, so one fetch serves every dialog on the page.
|
|
15
|
+
*/
|
|
16
|
+
export function useStockLocations(enabled = true) {
|
|
17
|
+
return useQuery({
|
|
18
|
+
queryKey: useResourceKey('seller-stock-locations'),
|
|
19
|
+
queryFn: () => sellerClient().stockLocations.list({ limit: 100 }),
|
|
20
|
+
staleTime: 5 * 60 * 1000,
|
|
21
|
+
enabled,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { useParams } from '@tanstack/react-router'
|
|
3
|
+
import { sellerClient } from '../api-client'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The marketplace's timezone, as every date in this panel is read in.
|
|
7
|
+
*
|
|
8
|
+
* A seller keeps no clock of their own — a settlement period is decided by
|
|
9
|
+
* the marketplace's day, so a timestamp rendered in whichever zone the
|
|
10
|
+
* seller's browser sits in would put an earning on the wrong date for
|
|
11
|
+
* anyone trading away from the marketplace.
|
|
12
|
+
*
|
|
13
|
+
* Falls back to the browser's zone only until the profile arrives, matching
|
|
14
|
+
* what the operator dashboard's `useStore` does.
|
|
15
|
+
*/
|
|
16
|
+
export function useStoreTimezone(): string {
|
|
17
|
+
const { sellerId } = useParams({ from: '/_authenticated/$sellerId' })
|
|
18
|
+
|
|
19
|
+
const { data: profile } = useQuery({
|
|
20
|
+
queryKey: ['seller', sellerId, 'profile'],
|
|
21
|
+
queryFn: () => sellerClient().profile.get(),
|
|
22
|
+
enabled: Boolean(sellerId),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return profile?.preferred_timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC'
|
|
26
|
+
}
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Extends the framework's i18n rather than standing up a second instance.
|
|
2
|
+
//
|
|
3
|
+
// The side-effect import initialises i18next and registers the framework's
|
|
4
|
+
// own copy — the strings baked into shared components like `RowActions` and
|
|
5
|
+
// the confirm dialog. A panel that called `init()` itself would win the race
|
|
6
|
+
// and leave those components rendering raw keys
|
|
7
|
+
// (`admin.row_actions.menu_label`) wherever they appear. It must come first:
|
|
8
|
+
// side-effect imports are ordering barriers.
|
|
9
|
+
import '@spree/dashboard-core/lib/i18n'
|
|
10
|
+
// The instance comes from core too, not a bare `i18next` import: a bundled
|
|
11
|
+
// build can resolve a second copy of the package, and `addResourceBundle` on
|
|
12
|
+
// that one throws. Core re-exports the object it initialised for exactly this.
|
|
13
|
+
import { i18n } from '@spree/dashboard-core'
|
|
14
|
+
import en from './locales/en.json'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The panel's own copy, layered on top.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately separate from the admin dashboard's: sellers and marketplace
|
|
20
|
+
* staff read different words for the same records — "your products" rather
|
|
21
|
+
* than "products", "your payouts" rather than a seller's. The deep + overwrite
|
|
22
|
+
* flags merge these in without dropping the framework keys underneath.
|
|
23
|
+
*/
|
|
24
|
+
i18n.addResourceBundle('en', 'translation', en, true, true)
|
|
25
|
+
|
|
26
|
+
export default i18n
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The i18n bootstrap runs on import and must come first: route modules and
|
|
2
|
+
// table definitions call `i18n.t(...)` at module load, so a bundler that
|
|
3
|
+
// evaluated them before this would bake raw keys into the UI. Side-effect
|
|
4
|
+
// imports are ordering barriers, which is exactly what this relies on.
|
|
5
|
+
import './i18n'
|
|
6
|
+
// The built-in sidebar entries. Registered on import — after i18n, since the
|
|
7
|
+
// labels are translated at registration time — and before any host plugin
|
|
8
|
+
// runs, so a plugin's `nav.remove('team')` finds the entry to remove.
|
|
9
|
+
import './nav/default'
|
|
10
|
+
import './nav/settings'
|
|
11
|
+
|
|
12
|
+
// Plugin facade re-export — lets a marketplace register in-app customisations
|
|
13
|
+
// (nav entries, slot widgets, table columns) without declaring
|
|
14
|
+
// @spree/dashboard-core as a direct dependency. Same facade the operator's
|
|
15
|
+
// dashboard exposes: one API for extending either panel.
|
|
16
|
+
export * from '@spree/dashboard-core/plugin'
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
createSellerApiClient,
|
|
20
|
+
rememberedSeller,
|
|
21
|
+
rememberSeller,
|
|
22
|
+
setActiveSeller,
|
|
23
|
+
} from './api-client'
|
|
24
|
+
export { CenteredMessage } from './components/centered-message'
|
|
25
|
+
export type { SellerRouterOptions } from './create-router'
|
|
26
|
+
export { createSellerRouter } from './create-router'
|
|
27
|
+
export { SellerDashboard } from './seller-dashboard'
|