@spree/seller-dashboard 6.0.3 → 6.0.4
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 +23 -23
- package/src/api-client.ts +2 -0
- package/src/components/account-dialog.tsx +1 -1
- package/src/components/centered-message.tsx +1 -1
- package/src/components/finance/balance-summary.tsx +1 -1
- package/src/components/orders/fulfillment-deliveries.tsx +1 -1
- package/src/components/orders/fulfillments-card.tsx +30 -20
- package/src/components/orders/order-cards.tsx +115 -102
- package/src/components/orders/order-payment-card.tsx +2 -3
- package/src/components/orders/post-sale-cards.tsx +79 -47
- package/src/components/orders/post-sale-create-dialogs.tsx +15 -0
- package/src/components/orders/returns-card.tsx +44 -36
- package/src/components/orders/shipping-label-row.tsx +1 -1
- package/src/components/orders/use-product-link.tsx +10 -0
- package/src/components/product-status-card.tsx +2 -2
- package/src/components/seller-address-card.tsx +2 -2
- package/src/components/seller-returns-location-card.tsx +2 -7
- package/src/components/seller-switcher.tsx +1 -1
- package/src/hooks/use-fulfillments.ts +33 -22
- package/src/hooks/use-profile.ts +38 -0
- package/src/locales/ar.json +35 -31
- package/src/locales/de.json +35 -31
- package/src/locales/en.json +35 -31
- package/src/locales/fr.json +35 -31
- package/src/locales/ja.json +35 -31
- package/src/locales/pl.json +35 -31
- package/src/locales/vi.json +35 -31
- package/src/locales/zh-CN.json +35 -31
- package/src/nav/default.ts +0 -10
- package/src/nav/settings.ts +53 -0
- package/src/pages/delivery-methods.tsx +47 -49
- package/src/pages/forgot-password.tsx +5 -5
- package/src/pages/general-settings.tsx +233 -0
- package/src/pages/home.tsx +1 -1
- package/src/pages/login.tsx +3 -3
- package/src/pages/onboarding.tsx +26 -23
- package/src/pages/order.tsx +2 -1
- package/src/pages/orders.tsx +8 -15
- package/src/pages/payout.tsx +7 -4
- package/src/pages/payouts.tsx +23 -2
- package/src/pages/policies.tsx +12 -8
- package/src/pages/product.tsx +18 -4
- package/src/pages/products.tsx +0 -4
- package/src/pages/profile-settings.tsx +221 -0
- package/src/pages/reset-password.tsx +3 -3
- package/src/pages/seller-picker.tsx +1 -1
- package/src/pages/taxes.tsx +40 -0
- package/src/pages/team.tsx +1 -1
- package/src/routeTree.gen.ts +63 -0
- package/src/routes/_authenticated/$sellerId/profile.tsx +5 -3
- package/src/routes/_authenticated/$sellerId/settings/general.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/settings/profile.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/settings/taxes.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/settings.tsx +3 -7
- package/src/schemas/account.ts +1 -1
- package/src/tables/products.tsx +6 -5
- package/src/components/read-row.tsx +0 -16
- package/src/components/seller-business-card.tsx +0 -152
- package/src/pages/profile.tsx +0 -395
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { CLAIM_STATUSES, EXCHANGE_STATUSES, subsetOf, sumMoney } from '@spree/core'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
currencyParts,
|
|
4
|
+
formatMoney,
|
|
5
|
+
isPositiveMoney,
|
|
6
|
+
postSaleItemRow,
|
|
7
|
+
useMoneyLocale,
|
|
8
|
+
} from '@spree/dashboard-core'
|
|
3
9
|
import {
|
|
4
10
|
Button,
|
|
5
11
|
Card,
|
|
@@ -13,6 +19,11 @@ import {
|
|
|
13
19
|
DropdownMenuItem,
|
|
14
20
|
DropdownMenuSeparator,
|
|
15
21
|
DropdownMenuTrigger,
|
|
22
|
+
FulfillmentItemList,
|
|
23
|
+
Section,
|
|
24
|
+
SectionActions,
|
|
25
|
+
SectionHeader,
|
|
26
|
+
SectionTitle,
|
|
16
27
|
StatusBadge,
|
|
17
28
|
useConfirm,
|
|
18
29
|
} from '@spree/dashboard-ui'
|
|
@@ -35,6 +46,7 @@ import {
|
|
|
35
46
|
} from '../../hooks/use-post-sale'
|
|
36
47
|
import { lineLabel } from './line-label'
|
|
37
48
|
import { CreateClaimDialog } from './post-sale-create-dialogs'
|
|
49
|
+
import { useProductLink } from './use-product-link'
|
|
38
50
|
|
|
39
51
|
const EXCHANGE_ACTIONABLE = subsetOf(EXCHANGE_STATUSES, ['requested', 'approved', 'received'])
|
|
40
52
|
const CLAIM_ACTIONABLE = subsetOf(CLAIM_STATUSES, ['open', 'approved'])
|
|
@@ -47,6 +59,7 @@ const CLAIM_ACTIONABLE = subsetOf(CLAIM_STATUSES, ['open', 'approved'])
|
|
|
47
59
|
* is opened by the operator or the customer and worked here.
|
|
48
60
|
*/
|
|
49
61
|
export function ExchangesCard({ order }: { order: Order }) {
|
|
62
|
+
const productLink = useProductLink()
|
|
50
63
|
const { t } = useTranslation()
|
|
51
64
|
const locale = useMoneyLocale()
|
|
52
65
|
const confirm = useConfirm()
|
|
@@ -68,19 +81,19 @@ export function ExchangesCard({ order }: { order: Order }) {
|
|
|
68
81
|
}
|
|
69
82
|
|
|
70
83
|
return (
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
<
|
|
84
|
+
<Section>
|
|
85
|
+
<SectionHeader>
|
|
86
|
+
<SectionTitle>
|
|
74
87
|
<ArrowLeftRightIcon className="size-4" />
|
|
75
88
|
{t('orders.post_sale.exchanges.title')}
|
|
76
|
-
</
|
|
77
|
-
</
|
|
89
|
+
</SectionTitle>
|
|
90
|
+
</SectionHeader>
|
|
78
91
|
|
|
79
|
-
<
|
|
92
|
+
<div className="flex flex-col gap-4">
|
|
80
93
|
{exchanges.map((exchange) => (
|
|
81
|
-
<Card key={exchange.id}
|
|
94
|
+
<Card key={exchange.id}>
|
|
82
95
|
<CardHeader>
|
|
83
|
-
<CardTitle className="text-
|
|
96
|
+
<CardTitle className="text-base font-medium">{exchange.number}</CardTitle>
|
|
84
97
|
<CardAction className="flex items-center gap-2">
|
|
85
98
|
<StatusBadge
|
|
86
99
|
status={exchange.status}
|
|
@@ -142,17 +155,38 @@ export function ExchangesCard({ order }: { order: Order }) {
|
|
|
142
155
|
</CardAction>
|
|
143
156
|
</CardHeader>
|
|
144
157
|
|
|
158
|
+
<FulfillmentItemList
|
|
159
|
+
productLink={productLink}
|
|
160
|
+
rows={(exchange.exchange_line_items ?? []).map((line) => {
|
|
161
|
+
const row = postSaleItemRow(
|
|
162
|
+
line.id,
|
|
163
|
+
{
|
|
164
|
+
line_item_id: line.line_item_id,
|
|
165
|
+
variant_id: line.original_variant_id,
|
|
166
|
+
name: line.name,
|
|
167
|
+
variant: line.original_variant,
|
|
168
|
+
quantity: line.quantity,
|
|
169
|
+
},
|
|
170
|
+
order.items ?? [],
|
|
171
|
+
)
|
|
172
|
+
return {
|
|
173
|
+
...row,
|
|
174
|
+
replacement: {
|
|
175
|
+
name:
|
|
176
|
+
line.new_variant_name || line.new_variant?.sku || line.new_variant_id || '',
|
|
177
|
+
optionsText: line.new_variant?.options_text,
|
|
178
|
+
thumbnailUrl: line.new_variant?.thumbnail_url,
|
|
179
|
+
productId: line.new_variant?.product_id,
|
|
180
|
+
displayPrice: formatMoney(line.new_variant_price, order.currency, locale, {
|
|
181
|
+
unitPrice: true,
|
|
182
|
+
}),
|
|
183
|
+
label: t('admin.pages.orders.detail.claims.resolutions.replacement'),
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
})}
|
|
187
|
+
/>
|
|
145
188
|
<CardContent className="flex flex-col gap-1.5">
|
|
146
|
-
|
|
147
|
-
<div key={line.id} className="flex items-center justify-between gap-3 text-sm">
|
|
148
|
-
<span className="truncate">
|
|
149
|
-
{lineLabel(line.name, line.original_variant, line.original_variant_id)} →{' '}
|
|
150
|
-
{lineLabel(line.new_variant_name, line.new_variant, line.new_variant_id)}
|
|
151
|
-
</span>
|
|
152
|
-
<span className="shrink-0 text-muted-foreground">× {line.quantity}</span>
|
|
153
|
-
</div>
|
|
154
|
-
))}
|
|
155
|
-
<p className="text-muted-foreground text-sm">
|
|
189
|
+
<p className="text-muted-foreground text-base">
|
|
156
190
|
{t('orders.post_sale.exchanges.price_difference', {
|
|
157
191
|
amount: formatMoney(exchange.price_difference, order.currency, locale),
|
|
158
192
|
})}
|
|
@@ -160,13 +194,14 @@ export function ExchangesCard({ order }: { order: Order }) {
|
|
|
160
194
|
</CardContent>
|
|
161
195
|
</Card>
|
|
162
196
|
))}
|
|
163
|
-
</
|
|
164
|
-
</
|
|
197
|
+
</div>
|
|
198
|
+
</Section>
|
|
165
199
|
)
|
|
166
200
|
}
|
|
167
201
|
|
|
168
202
|
/** Something went wrong with a delivery, and the seller puts it right. */
|
|
169
203
|
export function ClaimsCard({ order }: { order: Order }) {
|
|
204
|
+
const productLink = useProductLink()
|
|
170
205
|
const { t } = useTranslation()
|
|
171
206
|
const confirm = useConfirm()
|
|
172
207
|
const { data } = useOrderClaims(order.id)
|
|
@@ -201,30 +236,30 @@ export function ClaimsCard({ order }: { order: Order }) {
|
|
|
201
236
|
}
|
|
202
237
|
|
|
203
238
|
return (
|
|
204
|
-
<
|
|
205
|
-
<
|
|
206
|
-
<
|
|
239
|
+
<Section>
|
|
240
|
+
<SectionHeader>
|
|
241
|
+
<SectionTitle>
|
|
207
242
|
<ShieldAlertIcon className="size-4" />
|
|
208
243
|
{t('orders.post_sale.claims.title')}
|
|
209
|
-
</
|
|
244
|
+
</SectionTitle>
|
|
210
245
|
{canCreate && (
|
|
211
|
-
<
|
|
246
|
+
<SectionActions>
|
|
212
247
|
<Button variant="outline" size="sm" onClick={() => setCreateOpen(true)}>
|
|
213
248
|
<PlusIcon className="size-4" />
|
|
214
249
|
{t('orders.post_sale.claims.create')}
|
|
215
250
|
</Button>
|
|
216
|
-
</
|
|
251
|
+
</SectionActions>
|
|
217
252
|
)}
|
|
218
|
-
</
|
|
253
|
+
</SectionHeader>
|
|
219
254
|
|
|
220
|
-
<
|
|
255
|
+
<div className="flex flex-col gap-4">
|
|
221
256
|
{claims.length === 0 ? (
|
|
222
|
-
<p className="text-muted-foreground text-
|
|
257
|
+
<p className="text-muted-foreground text-base">{t('orders.post_sale.claims.empty')}</p>
|
|
223
258
|
) : (
|
|
224
259
|
claims.map((claim) => (
|
|
225
|
-
<Card key={claim.id}
|
|
260
|
+
<Card key={claim.id}>
|
|
226
261
|
<CardHeader>
|
|
227
|
-
<CardTitle className="text-
|
|
262
|
+
<CardTitle className="text-base font-medium">{claim.number}</CardTitle>
|
|
228
263
|
<CardAction className="flex items-center gap-2">
|
|
229
264
|
<StatusBadge
|
|
230
265
|
status={claim.status}
|
|
@@ -268,23 +303,20 @@ export function ClaimsCard({ order }: { order: Order }) {
|
|
|
268
303
|
</CardAction>
|
|
269
304
|
</CardHeader>
|
|
270
305
|
|
|
271
|
-
<
|
|
272
|
-
{
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
</div>
|
|
282
|
-
))}
|
|
283
|
-
</CardContent>
|
|
306
|
+
<FulfillmentItemList
|
|
307
|
+
productLink={productLink}
|
|
308
|
+
rows={(claim.claim_line_items ?? []).map((line) => {
|
|
309
|
+
const row = postSaleItemRow(line.id, line, order.items ?? [])
|
|
310
|
+
return {
|
|
311
|
+
...row,
|
|
312
|
+
optionsText: [row.optionsText, line.description].filter(Boolean).join(' — '),
|
|
313
|
+
}
|
|
314
|
+
})}
|
|
315
|
+
/>
|
|
284
316
|
</Card>
|
|
285
317
|
))
|
|
286
318
|
)}
|
|
287
|
-
</
|
|
319
|
+
</div>
|
|
288
320
|
|
|
289
321
|
<CreateClaimDialog order={order} open={createOpen} onOpenChange={setCreateOpen} />
|
|
290
322
|
{resolving && (
|
|
@@ -295,7 +327,7 @@ export function ClaimsCard({ order }: { order: Order }) {
|
|
|
295
327
|
onOpenChange={() => setResolving(null)}
|
|
296
328
|
/>
|
|
297
329
|
)}
|
|
298
|
-
</
|
|
330
|
+
</Section>
|
|
299
331
|
)
|
|
300
332
|
}
|
|
301
333
|
|
|
@@ -22,11 +22,21 @@ import { unitLabel } from './line-label'
|
|
|
22
22
|
|
|
23
23
|
/** Units that actually went out, which is all that can come back. */
|
|
24
24
|
export function fulfilledUnits(order: Order) {
|
|
25
|
+
const lineItems = new Map((order.items ?? []).map((item) => [item.id, item]))
|
|
26
|
+
|
|
25
27
|
return (order.fulfillments ?? []).flatMap((fulfillment) =>
|
|
26
28
|
(fulfillment.fulfillment_items ?? []).map((item) => ({
|
|
27
29
|
id: item.id,
|
|
28
30
|
label: unitLabel(item),
|
|
29
31
|
quantity: item.quantity,
|
|
32
|
+
product: {
|
|
33
|
+
name: item.name ?? item.id,
|
|
34
|
+
optionsText: item.options_text,
|
|
35
|
+
// The fulfillment item carries no image; its order line does.
|
|
36
|
+
thumbnailUrl: item.line_item_id
|
|
37
|
+
? lineItems.get(item.line_item_id)?.thumbnail_url
|
|
38
|
+
: undefined,
|
|
39
|
+
},
|
|
30
40
|
})),
|
|
31
41
|
)
|
|
32
42
|
}
|
|
@@ -137,6 +147,11 @@ export function CreateClaimDialog({
|
|
|
137
147
|
id: item.id,
|
|
138
148
|
label: unitLabel(item),
|
|
139
149
|
quantity: item.quantity,
|
|
150
|
+
product: {
|
|
151
|
+
name: item.name,
|
|
152
|
+
optionsText: item.options_text,
|
|
153
|
+
thumbnailUrl: item.thumbnail_url,
|
|
154
|
+
},
|
|
140
155
|
paidFor: (quantity: number) => paidForUnits(item, quantity, order.currency),
|
|
141
156
|
}))}
|
|
142
157
|
currencySymbol={currencySymbol}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
currencyParts,
|
|
4
4
|
formatMoney,
|
|
5
5
|
isPositiveMoney,
|
|
6
|
+
postSaleItemRow,
|
|
6
7
|
returnOwesNothing,
|
|
7
8
|
returnRefundSummary,
|
|
8
9
|
useMoneyLocale,
|
|
@@ -20,8 +21,13 @@ import {
|
|
|
20
21
|
DropdownMenuItem,
|
|
21
22
|
DropdownMenuSeparator,
|
|
22
23
|
DropdownMenuTrigger,
|
|
24
|
+
FulfillmentItemList,
|
|
23
25
|
ReturnReceiveDialog,
|
|
24
26
|
ReturnRefundDialog,
|
|
27
|
+
Section,
|
|
28
|
+
SectionActions,
|
|
29
|
+
SectionHeader,
|
|
30
|
+
SectionTitle,
|
|
25
31
|
StatusBadge,
|
|
26
32
|
useConfirm,
|
|
27
33
|
} from '@spree/dashboard-ui'
|
|
@@ -33,6 +39,7 @@ import { useTranslation } from 'react-i18next'
|
|
|
33
39
|
import { useOrderReturns, useReturnActions } from '../../hooks/use-post-sale'
|
|
34
40
|
import { lineLabel } from './line-label'
|
|
35
41
|
import { CreateReturnDialog, fulfilledUnits } from './post-sale-create-dialogs'
|
|
42
|
+
import { useProductLink } from './use-product-link'
|
|
36
43
|
|
|
37
44
|
// Refunded and canceled returns are finished; offering a menu on them would
|
|
38
45
|
// open onto nothing.
|
|
@@ -46,6 +53,7 @@ const ACTIONABLE = subsetOf(RETURN_STATUSES, ['requested', 'approved', 'received
|
|
|
46
53
|
* refunding decides where the money goes.
|
|
47
54
|
*/
|
|
48
55
|
export function ReturnsCard({ order }: { order: Order }) {
|
|
56
|
+
const productLink = useProductLink()
|
|
49
57
|
const { t } = useTranslation()
|
|
50
58
|
const confirm = useConfirm()
|
|
51
59
|
const { data } = useOrderReturns(order.id)
|
|
@@ -70,30 +78,30 @@ export function ReturnsCard({ order }: { order: Order }) {
|
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
return (
|
|
73
|
-
<
|
|
74
|
-
<
|
|
75
|
-
<
|
|
81
|
+
<Section>
|
|
82
|
+
<SectionHeader className="items-center">
|
|
83
|
+
<SectionTitle>
|
|
76
84
|
<RotateCcwIcon className="size-4" />
|
|
77
85
|
{t('orders.post_sale.returns.title')}
|
|
78
|
-
</
|
|
86
|
+
</SectionTitle>
|
|
79
87
|
{canCreate && (
|
|
80
|
-
<
|
|
88
|
+
<SectionActions>
|
|
81
89
|
<Button variant="outline" size="sm" onClick={() => setCreateOpen(true)}>
|
|
82
90
|
<PlusIcon className="size-4" />
|
|
83
91
|
{t('orders.post_sale.returns.create')}
|
|
84
92
|
</Button>
|
|
85
|
-
</
|
|
93
|
+
</SectionActions>
|
|
86
94
|
)}
|
|
87
|
-
</
|
|
95
|
+
</SectionHeader>
|
|
88
96
|
|
|
89
|
-
<
|
|
97
|
+
<div className="flex flex-col gap-4">
|
|
90
98
|
{returns.length === 0 ? (
|
|
91
|
-
<p className="text-muted-foreground text-
|
|
99
|
+
<p className="text-muted-foreground text-base">{t('orders.post_sale.returns.empty')}</p>
|
|
92
100
|
) : (
|
|
93
101
|
returns.map((returnRecord) => (
|
|
94
|
-
<Card key={returnRecord.id}
|
|
102
|
+
<Card key={returnRecord.id}>
|
|
95
103
|
<CardHeader>
|
|
96
|
-
<CardTitle className="text-
|
|
104
|
+
<CardTitle className="text-base font-medium">{returnRecord.number}</CardTitle>
|
|
97
105
|
<CardAction className="flex items-center gap-2">
|
|
98
106
|
<StatusBadge
|
|
99
107
|
status={returnRecord.status}
|
|
@@ -140,16 +148,20 @@ export function ReturnsCard({ order }: { order: Order }) {
|
|
|
140
148
|
</CardAction>
|
|
141
149
|
</CardHeader>
|
|
142
150
|
|
|
151
|
+
<FulfillmentItemList
|
|
152
|
+
productLink={productLink}
|
|
153
|
+
rows={(returnRecord.return_line_items ?? []).map((line) => ({
|
|
154
|
+
...postSaleItemRow(line.id, line, order.items ?? []),
|
|
155
|
+
badges: <ReturnLineBadges line={line} />,
|
|
156
|
+
}))}
|
|
157
|
+
/>
|
|
143
158
|
<CardContent className="flex flex-col gap-1.5">
|
|
144
|
-
{returnRecord.return_line_items?.map((line) => (
|
|
145
|
-
<ReturnLineRow key={line.id} line={line} />
|
|
146
|
-
))}
|
|
147
159
|
<RefundSummary returnRecord={returnRecord} currency={order.currency} />
|
|
148
160
|
</CardContent>
|
|
149
161
|
</Card>
|
|
150
162
|
))
|
|
151
163
|
)}
|
|
152
|
-
</
|
|
164
|
+
</div>
|
|
153
165
|
|
|
154
166
|
<CreateReturnDialog order={order} open={createOpen} onOpenChange={setCreateOpen} />
|
|
155
167
|
{receiving && (
|
|
@@ -167,7 +179,7 @@ export function ReturnsCard({ order }: { order: Order }) {
|
|
|
167
179
|
onOpenChange={() => setRefunding(null)}
|
|
168
180
|
/>
|
|
169
181
|
)}
|
|
170
|
-
</
|
|
182
|
+
</Section>
|
|
171
183
|
)
|
|
172
184
|
}
|
|
173
185
|
|
|
@@ -177,7 +189,7 @@ function RefundSummary({ returnRecord, currency }: { returnRecord: Return; curre
|
|
|
177
189
|
const summary = returnRefundSummary(returnRecord, currency, locale)
|
|
178
190
|
|
|
179
191
|
return (
|
|
180
|
-
<p className="text-muted-foreground text-
|
|
192
|
+
<p className="text-muted-foreground text-base">
|
|
181
193
|
{summary.kind === 'owed' &&
|
|
182
194
|
t('orders.post_sale.returns.refund_total', { amount: summary.amount })}
|
|
183
195
|
{summary.kind === 'refunded' &&
|
|
@@ -188,28 +200,24 @@ function RefundSummary({ returnRecord, currency }: { returnRecord: Return; curre
|
|
|
188
200
|
)
|
|
189
201
|
}
|
|
190
202
|
|
|
191
|
-
|
|
203
|
+
/** What arrived back against a line. */
|
|
204
|
+
function ReturnLineBadges({ line }: { line: ReturnLineItem }) {
|
|
192
205
|
const { t } = useTranslation()
|
|
193
|
-
const label = lineLabel(line.name, line.variant, line.variant_id)
|
|
194
206
|
|
|
195
207
|
return (
|
|
196
|
-
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)}
|
|
210
|
-
× {line.quantity}
|
|
211
|
-
</span>
|
|
212
|
-
</div>
|
|
208
|
+
<>
|
|
209
|
+
{line.received_quantity > 0 && line.received_quantity < line.quantity && (
|
|
210
|
+
<Badge variant="outline">
|
|
211
|
+
{t('orders.post_sale.returns.partially_received', {
|
|
212
|
+
received: line.received_quantity,
|
|
213
|
+
quantity: line.quantity,
|
|
214
|
+
})}
|
|
215
|
+
</Badge>
|
|
216
|
+
)}
|
|
217
|
+
{!line.resellable && (
|
|
218
|
+
<Badge variant="outline">{t('orders.post_sale.returns.not_resellable')}</Badge>
|
|
219
|
+
)}
|
|
220
|
+
</>
|
|
213
221
|
)
|
|
214
222
|
}
|
|
215
223
|
|
|
@@ -88,7 +88,7 @@ export function ShippingLabelRow({
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
return (
|
|
91
|
-
<CardContent className="flex flex-wrap items-center justify-between gap-2 border-border-subtle border-b py-3 text-
|
|
91
|
+
<CardContent className="mx-4 px-0 flex flex-wrap items-center justify-between gap-2 border-border-subtle border-b py-3.5 text-base">
|
|
92
92
|
<div className="flex flex-wrap items-center gap-2">
|
|
93
93
|
<ReceiptTextIcon className="size-4 text-muted-foreground" />
|
|
94
94
|
<span className="text-muted-foreground">{t('orders.fulfillments.label_title')}</span>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ProductLinkRender } from '@spree/dashboard-ui'
|
|
2
|
+
import { Link, useParams } from '@tanstack/react-router'
|
|
3
|
+
|
|
4
|
+
/** Links an item list's products to the seller's own product pages. */
|
|
5
|
+
export function useProductLink(): ProductLinkRender {
|
|
6
|
+
const { sellerId } = useParams({ from: '/_authenticated/$sellerId' })
|
|
7
|
+
return (productId) => (
|
|
8
|
+
<Link to="/$sellerId/products/$productId" params={{ sellerId, productId }} />
|
|
9
|
+
)
|
|
10
|
+
}
|
|
@@ -51,14 +51,14 @@ export function ProductStatusCard({ product, onDone }: { product: Product; onDon
|
|
|
51
51
|
<CardContent className="flex flex-col items-start gap-3">
|
|
52
52
|
<StatusBadge status={status} label={t(`products.statuses.${status}`)} />
|
|
53
53
|
|
|
54
|
-
<p className="text-muted-foreground text-
|
|
54
|
+
<p className="text-muted-foreground text-base">{t(`products.status_help.${status}`)}</p>
|
|
55
55
|
|
|
56
56
|
{/* Why the marketplace turned it down, so the seller knows what to
|
|
57
57
|
fix. It lives on the submission rather than the product: a note
|
|
58
58
|
the seller could overwrite by saving their own product was no
|
|
59
59
|
record of anything. */}
|
|
60
60
|
{status === 'rejected' && product.submission?.review_note && (
|
|
61
|
-
<p className="text-
|
|
61
|
+
<p className="text-base">
|
|
62
62
|
{t('products.rejection_reason', { reason: product.submission.review_note })}
|
|
63
63
|
</p>
|
|
64
64
|
)}
|
|
@@ -69,11 +69,11 @@ export function SellerAddressCard({
|
|
|
69
69
|
const body = address ? (
|
|
70
70
|
<AddressBlock address={address} />
|
|
71
71
|
) : (
|
|
72
|
-
<p className="text-muted-foreground text-
|
|
72
|
+
<p className="text-muted-foreground text-base">{t('profile.address_not_provided')}</p>
|
|
73
73
|
)
|
|
74
74
|
|
|
75
75
|
const editButton = (
|
|
76
|
-
<Button variant=
|
|
76
|
+
<Button variant="outline" size="sm" onClick={() => setEditing(true)}>
|
|
77
77
|
{address ? <PencilIcon className="size-4" /> : <PlusIcon className="size-4" />}
|
|
78
78
|
{address ? t('profile.edit') : t('profile.add_address')}
|
|
79
79
|
</Button>
|
|
@@ -90,18 +90,13 @@ export function SellerReturnsLocationCard({ headless = false }: { headless?: boo
|
|
|
90
90
|
hasAddress && location ? (
|
|
91
91
|
<AddressBlock address={stockLocationToAddressBlock(location)} />
|
|
92
92
|
) : (
|
|
93
|
-
<p className={failed ? 'text-destructive text-
|
|
93
|
+
<p className={failed ? 'text-destructive text-base' : 'text-muted-foreground text-base'}>
|
|
94
94
|
{t(failed ? 'common.error' : 'profile.address_not_provided')}
|
|
95
95
|
</p>
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
const editButton = (
|
|
99
|
-
<Button
|
|
100
|
-
variant={hasAddress ? 'outline' : 'default'}
|
|
101
|
-
size="sm"
|
|
102
|
-
disabled={!location}
|
|
103
|
-
onClick={() => setEditing(true)}
|
|
104
|
-
>
|
|
99
|
+
<Button variant="outline" size="sm" disabled={!location} onClick={() => setEditing(true)}>
|
|
105
100
|
{hasAddress ? <PencilIcon className="size-4" /> : <PlusIcon className="size-4" />}
|
|
106
101
|
{hasAddress ? t('profile.edit') : t('profile.add_address')}
|
|
107
102
|
</Button>
|
|
@@ -69,7 +69,7 @@ export function SellerSwitcher() {
|
|
|
69
69
|
<AvatarFallback>{initials}</AvatarFallback>
|
|
70
70
|
</Avatar>
|
|
71
71
|
{!isCollapsed && (
|
|
72
|
-
<div className="grid flex-1 text-left text-
|
|
72
|
+
<div className="grid flex-1 text-left text-base leading-tight">
|
|
73
73
|
<span className="truncate font-medium text-foreground">{current?.name}</span>
|
|
74
74
|
</div>
|
|
75
75
|
)}
|
|
@@ -12,28 +12,8 @@ import { sellerClient } from '../api-client'
|
|
|
12
12
|
* variant and a quantity.
|
|
13
13
|
*/
|
|
14
14
|
export function useFulfillmentActions(orderId: string) {
|
|
15
|
-
const queryClient = useQueryClient()
|
|
16
|
-
const buildKey = useResourceKeyBuilder()
|
|
17
|
-
|
|
18
|
-
function useFulfillmentMutation<TParams>(mutationFn: (params: TParams) => Promise<unknown>) {
|
|
19
|
-
return useMutation({
|
|
20
|
-
mutationFn,
|
|
21
|
-
onSuccess: () => {
|
|
22
|
-
queryClient.invalidateQueries({ queryKey: buildKey('seller-order', orderId) })
|
|
23
|
-
queryClient.invalidateQueries({ queryKey: buildKey('seller-orders') })
|
|
24
|
-
},
|
|
25
|
-
onError: (error) => {
|
|
26
|
-
// A rejected workflow — a parcel that cannot ship, a split that would
|
|
27
|
-
// empty its source — otherwise looks like a dead button.
|
|
28
|
-
toastManager.add({
|
|
29
|
-
type: 'error',
|
|
30
|
-
title: error instanceof Error ? error.message : String(error),
|
|
31
|
-
})
|
|
32
|
-
},
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
|
|
36
15
|
const fulfill = useFulfillmentMutation(
|
|
16
|
+
orderId,
|
|
37
17
|
({
|
|
38
18
|
fulfillmentId,
|
|
39
19
|
...params
|
|
@@ -47,6 +27,7 @@ export function useFulfillmentActions(orderId: string) {
|
|
|
47
27
|
)
|
|
48
28
|
|
|
49
29
|
const update = useFulfillmentMutation(
|
|
30
|
+
orderId,
|
|
50
31
|
({
|
|
51
32
|
fulfillmentId,
|
|
52
33
|
...params
|
|
@@ -59,11 +40,12 @@ export function useFulfillmentActions(orderId: string) {
|
|
|
59
40
|
}) => sellerClient().orders.fulfillments.update(orderId, fulfillmentId, params),
|
|
60
41
|
)
|
|
61
42
|
|
|
62
|
-
const cancel = useFulfillmentMutation((fulfillmentId: string) =>
|
|
43
|
+
const cancel = useFulfillmentMutation(orderId, (fulfillmentId: string) =>
|
|
63
44
|
sellerClient().orders.fulfillments.cancel(orderId, fulfillmentId),
|
|
64
45
|
)
|
|
65
46
|
|
|
66
47
|
const split = useFulfillmentMutation(
|
|
48
|
+
orderId,
|
|
67
49
|
({
|
|
68
50
|
fulfillmentId,
|
|
69
51
|
...params
|
|
@@ -79,6 +61,7 @@ export function useFulfillmentActions(orderId: string) {
|
|
|
79
61
|
// boxes or a pallet under a freight PRO number are the same shape, and each
|
|
80
62
|
// carries its own tracking number and carrier status.
|
|
81
63
|
const createDelivery = useFulfillmentMutation(
|
|
64
|
+
orderId,
|
|
82
65
|
({
|
|
83
66
|
fulfillmentId,
|
|
84
67
|
...params
|
|
@@ -92,6 +75,7 @@ export function useFulfillmentActions(orderId: string) {
|
|
|
92
75
|
)
|
|
93
76
|
|
|
94
77
|
const updateDelivery = useFulfillmentMutation(
|
|
78
|
+
orderId,
|
|
95
79
|
({
|
|
96
80
|
fulfillmentId,
|
|
97
81
|
deliveryId,
|
|
@@ -113,6 +97,7 @@ export function useFulfillmentActions(orderId: string) {
|
|
|
113
97
|
)
|
|
114
98
|
|
|
115
99
|
const deleteDelivery = useFulfillmentMutation(
|
|
100
|
+
orderId,
|
|
116
101
|
({ fulfillmentId, deliveryId }: { fulfillmentId: string; deliveryId: string }) =>
|
|
117
102
|
sellerClient().orders.fulfillments.deliveries.delete(orderId, fulfillmentId, deliveryId),
|
|
118
103
|
)
|
|
@@ -121,6 +106,7 @@ export function useFulfillmentActions(orderId: string) {
|
|
|
121
106
|
// refunding run through the operator's carrier account, so neither is
|
|
122
107
|
// offered — the endpoint refuses both.
|
|
123
108
|
const uploadLabel = useFulfillmentMutation(
|
|
109
|
+
orderId,
|
|
124
110
|
({
|
|
125
111
|
fulfillmentId,
|
|
126
112
|
...params
|
|
@@ -138,6 +124,7 @@ export function useFulfillmentActions(orderId: string) {
|
|
|
138
124
|
)
|
|
139
125
|
|
|
140
126
|
const deleteLabel = useFulfillmentMutation(
|
|
127
|
+
orderId,
|
|
141
128
|
({ fulfillmentId, labelId }: { fulfillmentId: string; labelId: string }) =>
|
|
142
129
|
sellerClient().orders.fulfillments.labels.delete(orderId, fulfillmentId, labelId),
|
|
143
130
|
)
|
|
@@ -154,3 +141,27 @@ export function useFulfillmentActions(orderId: string) {
|
|
|
154
141
|
deleteLabel,
|
|
155
142
|
}
|
|
156
143
|
}
|
|
144
|
+
|
|
145
|
+
function useFulfillmentMutation<TParams>(
|
|
146
|
+
orderId: string,
|
|
147
|
+
mutationFn: (params: TParams) => Promise<unknown>,
|
|
148
|
+
) {
|
|
149
|
+
const queryClient = useQueryClient()
|
|
150
|
+
const buildKey = useResourceKeyBuilder()
|
|
151
|
+
|
|
152
|
+
return useMutation({
|
|
153
|
+
mutationFn,
|
|
154
|
+
onSuccess: () => {
|
|
155
|
+
queryClient.invalidateQueries({ queryKey: buildKey('seller-order', orderId) })
|
|
156
|
+
queryClient.invalidateQueries({ queryKey: buildKey('seller-orders') })
|
|
157
|
+
},
|
|
158
|
+
onError: (error) => {
|
|
159
|
+
// A rejected workflow — a parcel that cannot ship, a split that would
|
|
160
|
+
// empty its source — otherwise looks like a dead button.
|
|
161
|
+
toastManager.add({
|
|
162
|
+
type: 'error',
|
|
163
|
+
title: error instanceof Error ? error.message : String(error),
|
|
164
|
+
})
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { toastManager } from '@spree/dashboard-ui'
|
|
2
|
+
import type { ProfileUpdateParams } from '@spree/seller-sdk'
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
4
|
+
import { useParams } from '@tanstack/react-router'
|
|
5
|
+
import { useTranslation } from 'react-i18next'
|
|
6
|
+
import { sellerClient } from '../api-client'
|
|
7
|
+
|
|
8
|
+
export function useProfile({ enabled = true }: { enabled?: boolean } = {}) {
|
|
9
|
+
const { sellerId } = useParams({ from: '/_authenticated/$sellerId' })
|
|
10
|
+
|
|
11
|
+
return useQuery({
|
|
12
|
+
queryKey: ['seller', sellerId, 'profile'],
|
|
13
|
+
queryFn: () => sellerClient().profile.get(),
|
|
14
|
+
enabled,
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Saves part of the seller's own record, wherever in the panel it is edited. */
|
|
19
|
+
export function useUpdateProfile() {
|
|
20
|
+
const { t } = useTranslation()
|
|
21
|
+
const { sellerId } = useParams({ from: '/_authenticated/$sellerId' })
|
|
22
|
+
const queryClient = useQueryClient()
|
|
23
|
+
|
|
24
|
+
return useMutation({
|
|
25
|
+
mutationFn: (params: ProfileUpdateParams) => sellerClient().profile.update(params),
|
|
26
|
+
onSuccess: (updated) => {
|
|
27
|
+
queryClient.setQueryData(['seller', sellerId, 'profile'], updated)
|
|
28
|
+
// Onboarding requirements read the profile, so the checklist re-evaluates.
|
|
29
|
+
void queryClient.invalidateQueries({ queryKey: ['seller', sellerId, 'onboarding'] })
|
|
30
|
+
toastManager.add({ type: 'success', title: t('profile.saved') })
|
|
31
|
+
},
|
|
32
|
+
onError: (err) =>
|
|
33
|
+
toastManager.add({
|
|
34
|
+
type: 'error',
|
|
35
|
+
title: err instanceof Error && err.message ? err.message : t('common.error'),
|
|
36
|
+
}),
|
|
37
|
+
})
|
|
38
|
+
}
|