@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.
Files changed (59) hide show
  1. package/package.json +23 -23
  2. package/src/api-client.ts +2 -0
  3. package/src/components/account-dialog.tsx +1 -1
  4. package/src/components/centered-message.tsx +1 -1
  5. package/src/components/finance/balance-summary.tsx +1 -1
  6. package/src/components/orders/fulfillment-deliveries.tsx +1 -1
  7. package/src/components/orders/fulfillments-card.tsx +30 -20
  8. package/src/components/orders/order-cards.tsx +115 -102
  9. package/src/components/orders/order-payment-card.tsx +2 -3
  10. package/src/components/orders/post-sale-cards.tsx +79 -47
  11. package/src/components/orders/post-sale-create-dialogs.tsx +15 -0
  12. package/src/components/orders/returns-card.tsx +44 -36
  13. package/src/components/orders/shipping-label-row.tsx +1 -1
  14. package/src/components/orders/use-product-link.tsx +10 -0
  15. package/src/components/product-status-card.tsx +2 -2
  16. package/src/components/seller-address-card.tsx +2 -2
  17. package/src/components/seller-returns-location-card.tsx +2 -7
  18. package/src/components/seller-switcher.tsx +1 -1
  19. package/src/hooks/use-fulfillments.ts +33 -22
  20. package/src/hooks/use-profile.ts +38 -0
  21. package/src/locales/ar.json +35 -31
  22. package/src/locales/de.json +35 -31
  23. package/src/locales/en.json +35 -31
  24. package/src/locales/fr.json +35 -31
  25. package/src/locales/ja.json +35 -31
  26. package/src/locales/pl.json +35 -31
  27. package/src/locales/vi.json +35 -31
  28. package/src/locales/zh-CN.json +35 -31
  29. package/src/nav/default.ts +0 -10
  30. package/src/nav/settings.ts +53 -0
  31. package/src/pages/delivery-methods.tsx +47 -49
  32. package/src/pages/forgot-password.tsx +5 -5
  33. package/src/pages/general-settings.tsx +233 -0
  34. package/src/pages/home.tsx +1 -1
  35. package/src/pages/login.tsx +3 -3
  36. package/src/pages/onboarding.tsx +26 -23
  37. package/src/pages/order.tsx +2 -1
  38. package/src/pages/orders.tsx +8 -15
  39. package/src/pages/payout.tsx +7 -4
  40. package/src/pages/payouts.tsx +23 -2
  41. package/src/pages/policies.tsx +12 -8
  42. package/src/pages/product.tsx +18 -4
  43. package/src/pages/products.tsx +0 -4
  44. package/src/pages/profile-settings.tsx +221 -0
  45. package/src/pages/reset-password.tsx +3 -3
  46. package/src/pages/seller-picker.tsx +1 -1
  47. package/src/pages/taxes.tsx +40 -0
  48. package/src/pages/team.tsx +1 -1
  49. package/src/routeTree.gen.ts +63 -0
  50. package/src/routes/_authenticated/$sellerId/profile.tsx +5 -3
  51. package/src/routes/_authenticated/$sellerId/settings/general.tsx +6 -0
  52. package/src/routes/_authenticated/$sellerId/settings/profile.tsx +6 -0
  53. package/src/routes/_authenticated/$sellerId/settings/taxes.tsx +6 -0
  54. package/src/routes/_authenticated/$sellerId/settings.tsx +3 -7
  55. package/src/schemas/account.ts +1 -1
  56. package/src/tables/products.tsx +6 -5
  57. package/src/components/read-row.tsx +0 -16
  58. package/src/components/seller-business-card.tsx +0 -152
  59. 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 { currencyParts, formatMoney, isPositiveMoney, useMoneyLocale } from '@spree/dashboard-core'
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
- <Card variant="container">
72
- <CardHeader>
73
- <CardTitle>
84
+ <Section>
85
+ <SectionHeader>
86
+ <SectionTitle>
74
87
  <ArrowLeftRightIcon className="size-4" />
75
88
  {t('orders.post_sale.exchanges.title')}
76
- </CardTitle>
77
- </CardHeader>
89
+ </SectionTitle>
90
+ </SectionHeader>
78
91
 
79
- <CardContent className="flex flex-col gap-4">
92
+ <div className="flex flex-col gap-4">
80
93
  {exchanges.map((exchange) => (
81
- <Card key={exchange.id} variant="nested">
94
+ <Card key={exchange.id}>
82
95
  <CardHeader>
83
- <CardTitle className="text-sm font-medium">{exchange.number}</CardTitle>
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
- {exchange.exchange_line_items?.map((line) => (
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
- </CardContent>
164
- </Card>
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
- <Card variant="container">
205
- <CardHeader>
206
- <CardTitle>
239
+ <Section>
240
+ <SectionHeader>
241
+ <SectionTitle>
207
242
  <ShieldAlertIcon className="size-4" />
208
243
  {t('orders.post_sale.claims.title')}
209
- </CardTitle>
244
+ </SectionTitle>
210
245
  {canCreate && (
211
- <CardAction>
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
- </CardAction>
251
+ </SectionActions>
217
252
  )}
218
- </CardHeader>
253
+ </SectionHeader>
219
254
 
220
- <CardContent className="flex flex-col gap-4">
255
+ <div className="flex flex-col gap-4">
221
256
  {claims.length === 0 ? (
222
- <p className="text-muted-foreground text-sm">{t('orders.post_sale.claims.empty')}</p>
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} variant="nested">
260
+ <Card key={claim.id}>
226
261
  <CardHeader>
227
- <CardTitle className="text-sm font-medium">{claim.number}</CardTitle>
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
- <CardContent className="flex flex-col gap-1.5">
272
- {claim.claim_line_items?.map((line) => (
273
- <div key={line.id} className="flex items-center justify-between gap-3 text-sm">
274
- <span className="min-w-0 truncate">
275
- {lineLabel(line.name, line.variant, line.variant_id)}
276
- {line.description && (
277
- <span className="text-muted-foreground"> — {line.description}</span>
278
- )}
279
- </span>
280
- <span className="shrink-0 text-muted-foreground">× {line.quantity}</span>
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
- </CardContent>
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
- </Card>
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
- <Card variant="container">
74
- <CardHeader className="items-center">
75
- <CardTitle>
81
+ <Section>
82
+ <SectionHeader className="items-center">
83
+ <SectionTitle>
76
84
  <RotateCcwIcon className="size-4" />
77
85
  {t('orders.post_sale.returns.title')}
78
- </CardTitle>
86
+ </SectionTitle>
79
87
  {canCreate && (
80
- <CardAction>
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
- </CardAction>
93
+ </SectionActions>
86
94
  )}
87
- </CardHeader>
95
+ </SectionHeader>
88
96
 
89
- <CardContent className="flex flex-col gap-4">
97
+ <div className="flex flex-col gap-4">
90
98
  {returns.length === 0 ? (
91
- <p className="text-muted-foreground text-sm">{t('orders.post_sale.returns.empty')}</p>
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} variant="nested">
102
+ <Card key={returnRecord.id}>
95
103
  <CardHeader>
96
- <CardTitle className="text-sm font-medium">{returnRecord.number}</CardTitle>
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
- </CardContent>
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
- </Card>
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-sm">
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
- function ReturnLineRow({ line }: { line: ReturnLineItem }) {
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
- <div className="flex items-center justify-between gap-3 text-sm">
197
- <span className="truncate">{label}</span>
198
- <span className="flex shrink-0 items-center gap-2 text-muted-foreground">
199
- {line.received_quantity > 0 && line.received_quantity < line.quantity && (
200
- <Badge variant="outline">
201
- {t('orders.post_sale.returns.partially_received', {
202
- received: line.received_quantity,
203
- quantity: line.quantity,
204
- })}
205
- </Badge>
206
- )}
207
- {!line.resellable && (
208
- <Badge variant="outline">{t('orders.post_sale.returns.not_resellable')}</Badge>
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-sm">
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-sm">{t(`products.status_help.${status}`)}</p>
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-sm">
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-sm">{t('profile.address_not_provided')}</p>
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={address ? 'outline' : 'default'} size="sm" onClick={() => setEditing(true)}>
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-sm' : 'text-muted-foreground text-sm'}>
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-sm leading-tight">
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
+ }