@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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/dist/vite-plugin/index.js +51 -0
  4. package/package.json +64 -0
  5. package/src/api-client.ts +203 -0
  6. package/src/components/centered-message.tsx +8 -0
  7. package/src/components/finance/balance-summary.tsx +69 -0
  8. package/src/components/onboarding-nav-badge.tsx +38 -0
  9. package/src/components/orders/fulfillment-deliveries.tsx +116 -0
  10. package/src/components/orders/fulfillment-delivery-dialog.tsx +82 -0
  11. package/src/components/orders/fulfillment-edit-dialog.tsx +95 -0
  12. package/src/components/orders/fulfillment-fulfill-form.tsx +132 -0
  13. package/src/components/orders/fulfillment-label-upload-dialog.tsx +95 -0
  14. package/src/components/orders/fulfillment-split-dialog.tsx +142 -0
  15. package/src/components/orders/fulfillments-card.tsx +289 -0
  16. package/src/components/orders/line-label.ts +28 -0
  17. package/src/components/orders/order-cancel-dialog.tsx +74 -0
  18. package/src/components/orders/order-cards.tsx +297 -0
  19. package/src/components/orders/order-earnings-card.tsx +99 -0
  20. package/src/components/orders/order-notes-cards.tsx +83 -0
  21. package/src/components/orders/order-payment-card.tsx +94 -0
  22. package/src/components/orders/post-sale-cards.tsx +347 -0
  23. package/src/components/orders/post-sale-create-dialogs.tsx +201 -0
  24. package/src/components/orders/returns-card.tsx +258 -0
  25. package/src/components/orders/shipping-label-row.tsx +124 -0
  26. package/src/components/product-status-card.tsx +111 -0
  27. package/src/components/read-row.tsx +16 -0
  28. package/src/components/retryable-error.tsx +23 -0
  29. package/src/components/seller-address-card.tsx +123 -0
  30. package/src/components/seller-business-card.tsx +152 -0
  31. package/src/components/seller-returns-location-card.tsx +151 -0
  32. package/src/components/seller-switcher.tsx +124 -0
  33. package/src/components/seller-tax-identifiers-card.tsx +63 -0
  34. package/src/create-router.ts +32 -0
  35. package/src/hooks/use-fulfillments.ts +155 -0
  36. package/src/hooks/use-ledger.ts +46 -0
  37. package/src/hooks/use-order.ts +62 -0
  38. package/src/hooks/use-post-sale.ts +202 -0
  39. package/src/hooks/use-reasons.ts +34 -0
  40. package/src/hooks/use-stock-locations.ts +23 -0
  41. package/src/hooks/use-store-timezone.ts +26 -0
  42. package/src/i18n.ts +26 -0
  43. package/src/index.ts +27 -0
  44. package/src/lib/packing-slip.ts +109 -0
  45. package/src/locales/en.json +841 -0
  46. package/src/nav/default.ts +108 -0
  47. package/src/nav/settings.ts +114 -0
  48. package/src/pages/accept-invitation.tsx +127 -0
  49. package/src/pages/delivery-methods.tsx +647 -0
  50. package/src/pages/earnings.tsx +44 -0
  51. package/src/pages/forgot-password.tsx +100 -0
  52. package/src/pages/home.tsx +132 -0
  53. package/src/pages/login.tsx +94 -0
  54. package/src/pages/onboarding.tsx +539 -0
  55. package/src/pages/order.tsx +119 -0
  56. package/src/pages/orders.tsx +33 -0
  57. package/src/pages/payout.tsx +154 -0
  58. package/src/pages/payouts.tsx +32 -0
  59. package/src/pages/policies.tsx +292 -0
  60. package/src/pages/product.tsx +250 -0
  61. package/src/pages/products.tsx +228 -0
  62. package/src/pages/profile.tsx +389 -0
  63. package/src/pages/reset-password.tsx +105 -0
  64. package/src/pages/seller-picker.tsx +46 -0
  65. package/src/pages/team.tsx +468 -0
  66. package/src/panel-chrome.tsx +36 -0
  67. package/src/routeTree.gen.ts +590 -0
  68. package/src/routes/__root.tsx +15 -0
  69. package/src/routes/_authenticated/$sellerId/earnings/index.tsx +15 -0
  70. package/src/routes/_authenticated/$sellerId/index.tsx +6 -0
  71. package/src/routes/_authenticated/$sellerId/onboarding.tsx +6 -0
  72. package/src/routes/_authenticated/$sellerId/orders/$orderId.tsx +6 -0
  73. package/src/routes/_authenticated/$sellerId/orders/index.tsx +15 -0
  74. package/src/routes/_authenticated/$sellerId/payouts/$payoutId.tsx +7 -0
  75. package/src/routes/_authenticated/$sellerId/payouts/index.tsx +15 -0
  76. package/src/routes/_authenticated/$sellerId/products/$productId.tsx +6 -0
  77. package/src/routes/_authenticated/$sellerId/products/index.tsx +23 -0
  78. package/src/routes/_authenticated/$sellerId/products/new.tsx +6 -0
  79. package/src/routes/_authenticated/$sellerId/profile.tsx +6 -0
  80. package/src/routes/_authenticated/$sellerId/settings/delivery-methods.tsx +14 -0
  81. package/src/routes/_authenticated/$sellerId/settings/index.tsx +18 -0
  82. package/src/routes/_authenticated/$sellerId/settings/package-types.tsx +21 -0
  83. package/src/routes/_authenticated/$sellerId/settings/policies.tsx +14 -0
  84. package/src/routes/_authenticated/$sellerId/settings/stock-locations.tsx +22 -0
  85. package/src/routes/_authenticated/$sellerId/settings/team.tsx +6 -0
  86. package/src/routes/_authenticated/$sellerId/settings.tsx +18 -0
  87. package/src/routes/_authenticated/$sellerId.tsx +49 -0
  88. package/src/routes/_authenticated/index.tsx +48 -0
  89. package/src/routes/_authenticated.tsx +13 -0
  90. package/src/routes/accept-invitation.$invitationId.tsx +19 -0
  91. package/src/routes/forgot-password.tsx +15 -0
  92. package/src/routes/login.tsx +15 -0
  93. package/src/routes/reset-password.tsx +21 -0
  94. package/src/schemas/fulfillment.ts +31 -0
  95. package/src/seller-dashboard.tsx +50 -0
  96. package/src/styles.css +18 -0
  97. package/src/tables/delivery-methods.tsx +68 -0
  98. package/src/tables/orders.tsx +116 -0
  99. package/src/tables/payouts.tsx +87 -0
  100. package/src/tables/policies.tsx +37 -0
  101. package/src/tables/products.tsx +56 -0
  102. package/src/tables/transfers.tsx +118 -0
  103. 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'