@spree/seller-dashboard 1.0.0-rc.2 → 1.0.0-rc.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 +4 -4
- package/src/api-client.ts +13 -8
- package/src/components/finance/balance-summary.tsx +31 -23
- package/src/components/orders/fulfillment-edit-dialog.tsx +10 -3
- package/src/components/orders/fulfillment-label-upload-dialog.tsx +3 -2
- package/src/components/orders/order-cards.tsx +31 -23
- package/src/components/orders/order-earnings-card.tsx +4 -2
- package/src/components/orders/order-payment-card.tsx +16 -8
- package/src/components/orders/post-sale-cards.tsx +7 -10
- package/src/components/orders/post-sale-create-dialogs.tsx +2 -3
- package/src/components/orders/returns-card.tsx +15 -8
- package/src/components/orders/shipping-label-row.tsx +15 -3
- package/src/components/seller-returns-location-card.tsx +4 -4
- package/src/components/seller-tax-identifiers-card.tsx +1 -1
- package/src/hooks/use-fulfillments.ts +3 -2
- package/src/hooks/use-store-timezone.ts +1 -1
- package/src/locales/ar.json +1 -0
- package/src/locales/de.json +1 -0
- package/src/locales/en.json +1 -0
- package/src/locales/fr.json +1 -0
- package/src/locales/pl.json +1 -0
- package/src/locales/zh-CN.json +1 -0
- package/src/pages/delivery-methods.tsx +34 -31
- package/src/pages/onboarding.tsx +2 -1
- package/src/pages/payout.tsx +14 -4
- package/src/pages/product.tsx +3 -3
- package/src/pages/team.tsx +2 -2
- package/src/tables/delivery-methods.tsx +1 -1
- package/src/tables/orders.tsx +2 -2
- package/src/tables/payouts.tsx +3 -3
- package/src/tables/policies.tsx +1 -1
- package/src/tables/products.tsx +3 -3
- package/src/tables/transfers.tsx +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spree/seller-dashboard",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Spree marketplace seller panel — the seller-facing counterpart to the admin dashboard.",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"react-hook-form": "^7.65.0",
|
|
50
50
|
"react-i18next": "^17.0.8",
|
|
51
51
|
"zod": "^4.4.3",
|
|
52
|
-
"@spree/dashboard-
|
|
53
|
-
"@spree/
|
|
54
|
-
"@spree/
|
|
52
|
+
"@spree/dashboard-ui": "1.0.0-rc.3",
|
|
53
|
+
"@spree/seller-sdk": "1.0.0-rc.3",
|
|
54
|
+
"@spree/dashboard-core": "1.0.0-rc.3"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"react": "^19.0.0",
|
package/src/api-client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { setApiClient } from '@spree/dashboard-core'
|
|
1
|
+
import { runtimeListParams, setApiClient } from '@spree/dashboard-core'
|
|
2
2
|
import type { SellerApiClient, SellerExportCreateParams } from '@spree/seller-sdk'
|
|
3
3
|
import { createSellerClient } from '@spree/seller-sdk'
|
|
4
4
|
|
|
@@ -71,7 +71,7 @@ export function createSellerApiClient({
|
|
|
71
71
|
// The shared address form reads countries through the registered client,
|
|
72
72
|
// so it works in a panel that has no admin credential.
|
|
73
73
|
listCountries: () => sellerClient().countries.list(),
|
|
74
|
-
|
|
74
|
+
uploadFile: (file, options) => sellerClient().files.upload(file, options),
|
|
75
75
|
// A file download is a bare fetch, so it does not go through the client
|
|
76
76
|
// and picks up none of its headers. Without the seller header the Seller
|
|
77
77
|
// API refuses the request before the action runs, so an export would
|
|
@@ -89,7 +89,7 @@ export function createSellerApiClient({
|
|
|
89
89
|
// the marketplace and translations of catalog copy are its merchandising,
|
|
90
90
|
// so the Seller API refuses both and the wizard offers neither.
|
|
91
91
|
imports: {
|
|
92
|
-
list: (params) => sellerClient().imports.list(params),
|
|
92
|
+
list: (params) => sellerClient().imports.list(runtimeListParams(params)),
|
|
93
93
|
get: (id) => sellerClient().imports.get(id),
|
|
94
94
|
create: (params) =>
|
|
95
95
|
sellerClient().imports.create({
|
|
@@ -101,7 +101,10 @@ export function createSellerApiClient({
|
|
|
101
101
|
completeMapping: (id, params) => sellerClient().imports.completeMapping(id, params),
|
|
102
102
|
retryFailedRows: (id) => sellerClient().imports.retryFailedRows(id),
|
|
103
103
|
delete: (id) => sellerClient().imports.delete(id),
|
|
104
|
-
rows: {
|
|
104
|
+
rows: {
|
|
105
|
+
list: (importId, params) =>
|
|
106
|
+
sellerClient().imports.rows.list(importId, runtimeListParams(params)),
|
|
107
|
+
},
|
|
105
108
|
types: ['products'],
|
|
106
109
|
// This panel's product list is keyed `seller-products`, so core's
|
|
107
110
|
// default `products` invalidation would miss it and the list would sit
|
|
@@ -115,7 +118,7 @@ export function createSellerApiClient({
|
|
|
115
118
|
// stock levels and is named on historical fulfillments, so the Seller API
|
|
116
119
|
// does not offer it and the page hides the action accordingly.
|
|
117
120
|
stockLocations: {
|
|
118
|
-
list: (params) => sellerClient().stockLocations.list(params),
|
|
121
|
+
list: (params) => sellerClient().stockLocations.list(runtimeListParams(params)),
|
|
119
122
|
get: (id) => sellerClient().stockLocations.get(id),
|
|
120
123
|
create: (params) => sellerClient().stockLocations.create(params),
|
|
121
124
|
update: (id, params) => sellerClient().stockLocations.update(id, params),
|
|
@@ -126,16 +129,18 @@ export function createSellerApiClient({
|
|
|
126
129
|
// shared Categorization card offers just these two
|
|
127
130
|
// (docs/plans/6.0-multi-vendor-marketplace.md, Decision 13).
|
|
128
131
|
productTypes: {
|
|
129
|
-
list: (params) => sellerClient().productTypes.list(params),
|
|
132
|
+
list: (params) => sellerClient().productTypes.list(runtimeListParams(params)),
|
|
130
133
|
get: (id) => sellerClient().productTypes.get(id),
|
|
131
134
|
},
|
|
132
|
-
deliveryProfiles: {
|
|
135
|
+
deliveryProfiles: {
|
|
136
|
+
list: (params) => sellerClient().deliveryProfiles.list(runtimeListParams(params)),
|
|
137
|
+
},
|
|
133
138
|
// What this seller packs into: their own boxes and cartons, plus the
|
|
134
139
|
// marketplace's shared packaging, which the API lists read-only. Backs
|
|
135
140
|
// both the packaging settings page and the variant editor's carton
|
|
136
141
|
// picker (docs/plans/6.0-seller-package-types.md).
|
|
137
142
|
packageTypes: {
|
|
138
|
-
list: (params) => sellerClient().packageTypes.list(params),
|
|
143
|
+
list: (params) => sellerClient().packageTypes.list(runtimeListParams(params)),
|
|
139
144
|
get: (id) => sellerClient().packageTypes.get(id),
|
|
140
145
|
create: (params) => sellerClient().packageTypes.create(params),
|
|
141
146
|
update: (id, params) => sellerClient().packageTypes.update(id, params),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatMoney, useMoneyLocale } from '@spree/dashboard-core'
|
|
1
2
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@spree/dashboard-ui'
|
|
2
3
|
import type { Balance } from '@spree/seller-sdk'
|
|
3
4
|
import { useTranslation } from 'react-i18next'
|
|
@@ -16,6 +17,7 @@ import { useTranslation } from 'react-i18next'
|
|
|
16
17
|
*/
|
|
17
18
|
export function BalanceSummary({ balances }: { balances: Balance[] }) {
|
|
18
19
|
const { t } = useTranslation()
|
|
20
|
+
const locale = useMoneyLocale()
|
|
19
21
|
|
|
20
22
|
if (balances.length === 0) {
|
|
21
23
|
return (
|
|
@@ -30,31 +32,37 @@ export function BalanceSummary({ balances }: { balances: Balance[] }) {
|
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
34
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
33
|
-
{balances.map((balance) =>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
</CardHeader>
|
|
45
|
-
<CardContent className="flex flex-col gap-3">
|
|
46
|
-
<span className="text-2xl font-semibold tabular-nums">{balance.display_balance}</span>
|
|
47
|
-
<dl className="flex flex-col gap-1 text-sm">
|
|
48
|
-
<Figure label={t('earnings.balance.earned')} value={balance.display_earned} />
|
|
35
|
+
{balances.map((balance) => {
|
|
36
|
+
// Earned and pending are what the sales were worth; the rest is what
|
|
37
|
+
// the account holds, in the currency it settles in.
|
|
38
|
+
const sold = (amount: string) => formatMoney(amount, balance.currency, locale)
|
|
39
|
+
const held = (amount: string) => formatMoney(amount, balance.settlement_currency, locale)
|
|
40
|
+
return (
|
|
41
|
+
<Card key={`${balance.currency}-${balance.settlement_currency}`}>
|
|
42
|
+
<CardHeader>
|
|
43
|
+
<CardTitle>
|
|
44
|
+
{t('earnings.balance.owed', { currency: balance.settlement_currency })}
|
|
45
|
+
</CardTitle>
|
|
49
46
|
{balance.converted && (
|
|
50
|
-
<
|
|
47
|
+
<CardDescription>
|
|
48
|
+
{t('earnings.balance.converted', { currency: balance.currency })}
|
|
49
|
+
</CardDescription>
|
|
51
50
|
)}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
</CardHeader>
|
|
52
|
+
<CardContent className="flex flex-col gap-3">
|
|
53
|
+
<span className="text-2xl font-semibold tabular-nums">{held(balance.balance)}</span>
|
|
54
|
+
<dl className="flex flex-col gap-1 text-sm">
|
|
55
|
+
<Figure label={t('earnings.balance.earned')} value={sold(balance.earned)} />
|
|
56
|
+
{balance.converted && (
|
|
57
|
+
<Figure label={t('earnings.balance.payable')} value={held(balance.payable)} />
|
|
58
|
+
)}
|
|
59
|
+
<Figure label={t('earnings.balance.paid')} value={held(balance.paid)} />
|
|
60
|
+
<Figure label={t('earnings.balance.pending')} value={sold(balance.pending)} />
|
|
61
|
+
</dl>
|
|
62
|
+
</CardContent>
|
|
63
|
+
</Card>
|
|
64
|
+
)
|
|
65
|
+
})}
|
|
58
66
|
</div>
|
|
59
67
|
)
|
|
60
68
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { formatMoney, useMoneyLocale } from '@spree/dashboard-core'
|
|
1
2
|
import { FulfillmentEditDialog as SharedFulfillmentEditDialog } from '@spree/dashboard-ui'
|
|
2
|
-
import { type Fulfillment, SpreeError } from '@spree/seller-sdk'
|
|
3
|
+
import { type Fulfillment, isZeroMoney, SpreeError } from '@spree/seller-sdk'
|
|
3
4
|
import { useState } from 'react'
|
|
4
5
|
import { useTranslation } from 'react-i18next'
|
|
5
6
|
import { useFulfillmentActions } from '../../hooks/use-fulfillments'
|
|
@@ -27,7 +28,8 @@ export function FulfillmentEditDialog({
|
|
|
27
28
|
const { t } = useTranslation()
|
|
28
29
|
const { update } = useFulfillmentActions(orderId)
|
|
29
30
|
const { data: locationsData } = useStockLocations(open)
|
|
30
|
-
const { refetch: refetchOrder } = useOrder(orderId)
|
|
31
|
+
const { data: order, refetch: refetchOrder } = useOrder(orderId)
|
|
32
|
+
const locale = useMoneyLocale()
|
|
31
33
|
const [errorMessage, setErrorMessage] = useState<string>()
|
|
32
34
|
|
|
33
35
|
const originOptions = (locationsData?.data ?? []).map((location) => ({
|
|
@@ -38,7 +40,12 @@ export function FulfillmentEditDialog({
|
|
|
38
40
|
const rateOptions = (fulfillment.delivery_rates ?? []).map((rate) => ({
|
|
39
41
|
value: rate.id,
|
|
40
42
|
label: `${rate.name} — ${
|
|
41
|
-
|
|
43
|
+
rate.unpriced
|
|
44
|
+
? t('orders.fulfillments.quoted_after_review')
|
|
45
|
+
: isZeroMoney(rate.cost)
|
|
46
|
+
? t('orders.fulfillments.free')
|
|
47
|
+
: // A rate is quoted in its order's currency.
|
|
48
|
+
formatMoney(rate.cost, order?.currency ?? '', locale) || rate.cost
|
|
42
49
|
}`,
|
|
43
50
|
}))
|
|
44
51
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
mapSpreeErrorsToForm,
|
|
10
10
|
} from '@spree/dashboard-core'
|
|
11
11
|
import { LabelUploadDialog } from '@spree/dashboard-ui'
|
|
12
|
+
import type { CurrencyCode } from '@spree/seller-sdk'
|
|
12
13
|
import { useState } from 'react'
|
|
13
14
|
import { useForm } from 'react-hook-form'
|
|
14
15
|
import { useTranslation } from 'react-i18next'
|
|
@@ -25,7 +26,7 @@ export function FulfillmentLabelUploadDialog({
|
|
|
25
26
|
}: {
|
|
26
27
|
orderId: string
|
|
27
28
|
fulfillmentId: string
|
|
28
|
-
currency:
|
|
29
|
+
currency: CurrencyCode
|
|
29
30
|
open: boolean
|
|
30
31
|
onOpenChange: (open: boolean) => void
|
|
31
32
|
}) {
|
|
@@ -59,7 +60,7 @@ export function FulfillmentLabelUploadDialog({
|
|
|
59
60
|
try {
|
|
60
61
|
await uploadLabel.mutateAsync({
|
|
61
62
|
fulfillmentId,
|
|
62
|
-
|
|
63
|
+
file_signed_id: file.signedId,
|
|
63
64
|
tracking_number: values.tracking_number.trim(),
|
|
64
65
|
carrier: values.carrier.trim() || undefined,
|
|
65
66
|
cost: values.cost.trim() || undefined,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AddressFormDialog,
|
|
3
|
+
formatMoney,
|
|
4
|
+
formatStoreDateTime,
|
|
5
|
+
isPositiveMoney,
|
|
6
|
+
useMoneyLocale,
|
|
7
|
+
} from '@spree/dashboard-core'
|
|
2
8
|
import {
|
|
3
9
|
AddressBlock,
|
|
4
10
|
Button,
|
|
@@ -15,6 +21,7 @@ import {
|
|
|
15
21
|
} from '@spree/dashboard-ui'
|
|
16
22
|
import { EllipsisVerticalIcon, PencilIcon } from '@spree/dashboard-ui/icons'
|
|
17
23
|
import type { Order, OrderAddressParams } from '@spree/seller-sdk'
|
|
24
|
+
import { isZeroMoney } from '@spree/seller-sdk'
|
|
18
25
|
import { type ReactNode, useState } from 'react'
|
|
19
26
|
import { useTranslation } from 'react-i18next'
|
|
20
27
|
import { sellerClient } from '../../api-client'
|
|
@@ -69,10 +76,11 @@ function formatDate(iso: string | null | undefined, timezone: string) {
|
|
|
69
76
|
*/
|
|
70
77
|
export function OrderSummaryCard({ order }: { order: Order }) {
|
|
71
78
|
const { t } = useTranslation()
|
|
79
|
+
const locale = useMoneyLocale()
|
|
80
|
+
const money = (amount: string | null) => formatMoney(amount, order.currency, locale)
|
|
72
81
|
const timezone = useStoreTimezone()
|
|
73
82
|
|
|
74
|
-
const
|
|
75
|
-
const outstanding = amount(order.amount_due)
|
|
83
|
+
const outstanding = isPositiveMoney(order.amount_due)
|
|
76
84
|
const placed = Boolean(order.completed_at)
|
|
77
85
|
|
|
78
86
|
return (
|
|
@@ -118,43 +126,43 @@ export function OrderSummaryCard({ order }: { order: Order }) {
|
|
|
118
126
|
|
|
119
127
|
<Separator />
|
|
120
128
|
|
|
121
|
-
<SummaryRow label={t('orders.summary.subtotal')} value={order.
|
|
129
|
+
<SummaryRow label={t('orders.summary.subtotal')} value={money(order.item_total)} />
|
|
122
130
|
|
|
123
|
-
{
|
|
124
|
-
<SummaryRow label={t('orders.summary.shipping')} value={order.
|
|
131
|
+
{isPositiveMoney(order.delivery_total) && (
|
|
132
|
+
<SummaryRow label={t('orders.summary.shipping')} value={money(order.delivery_total)} />
|
|
125
133
|
)}
|
|
126
134
|
|
|
127
|
-
{
|
|
128
|
-
<SummaryRow label={t('orders.summary.promotions')} value={order.
|
|
135
|
+
{!isZeroMoney(order.discount_total) && (
|
|
136
|
+
<SummaryRow label={t('orders.summary.promotions')} value={money(order.discount_total)} />
|
|
129
137
|
)}
|
|
130
138
|
|
|
131
|
-
{
|
|
139
|
+
{!isZeroMoney(order.adjustment_total) && (
|
|
132
140
|
<SummaryRow
|
|
133
141
|
label={t('orders.summary.adjustments')}
|
|
134
|
-
value={order.
|
|
142
|
+
value={money(order.adjustment_total)}
|
|
135
143
|
/>
|
|
136
144
|
)}
|
|
137
145
|
|
|
138
|
-
{
|
|
146
|
+
{isPositiveMoney(order.included_tax_total) && (
|
|
139
147
|
<SummaryRow
|
|
140
148
|
label={t('orders.summary.tax_included')}
|
|
141
|
-
value={order.
|
|
149
|
+
value={money(order.included_tax_total)}
|
|
142
150
|
/>
|
|
143
151
|
)}
|
|
144
152
|
|
|
145
153
|
{/* Shown on a placed order even at zero, so "no tax was charged" reads
|
|
146
154
|
as an answer rather than a missing row. */}
|
|
147
|
-
{(
|
|
148
|
-
(placed &&
|
|
155
|
+
{(isPositiveMoney(order.additional_tax_total) ||
|
|
156
|
+
(placed && isZeroMoney(order.included_tax_total))) && (
|
|
149
157
|
<SummaryRow
|
|
150
158
|
label={t('orders.summary.tax_additional')}
|
|
151
|
-
value={order.
|
|
159
|
+
value={money(order.additional_tax_total)}
|
|
152
160
|
/>
|
|
153
161
|
)}
|
|
154
162
|
|
|
155
163
|
<Separator />
|
|
156
164
|
|
|
157
|
-
<SummaryRow label={t('orders.summary.total')} value={order.
|
|
165
|
+
<SummaryRow label={t('orders.summary.total')} value={money(order.total)} bold />
|
|
158
166
|
|
|
159
167
|
{/* What the marketplace charged the seller on this sale. Below the
|
|
160
168
|
order total because it is not part of what the buyer paid — it is
|
|
@@ -178,19 +186,19 @@ export function OrderSummaryCard({ order }: { order: Order }) {
|
|
|
178
186
|
|
|
179
187
|
<SummaryRow
|
|
180
188
|
label={t('orders.summary.commission_fee')}
|
|
181
|
-
value={order.
|
|
189
|
+
value={money(order.commission_amount_total)}
|
|
182
190
|
/>
|
|
183
191
|
|
|
184
|
-
{
|
|
192
|
+
{isPositiveMoney(order.commission_tax_total) && (
|
|
185
193
|
<SummaryRow
|
|
186
194
|
label={t('orders.summary.commission_tax')}
|
|
187
|
-
value={order.
|
|
195
|
+
value={money(order.commission_tax_total)}
|
|
188
196
|
/>
|
|
189
197
|
)}
|
|
190
198
|
|
|
191
199
|
<SummaryRow
|
|
192
200
|
label={t('orders.summary.commission_total')}
|
|
193
|
-
value={order.
|
|
201
|
+
value={money(order.commission_total)}
|
|
194
202
|
bold
|
|
195
203
|
/>
|
|
196
204
|
</>
|
|
@@ -200,14 +208,14 @@ export function OrderSummaryCard({ order }: { order: Order }) {
|
|
|
200
208
|
|
|
201
209
|
<SummaryRow
|
|
202
210
|
label={t('orders.summary.payment_total')}
|
|
203
|
-
value={order.
|
|
211
|
+
value={money(order.payment_total)}
|
|
204
212
|
highlight
|
|
205
213
|
/>
|
|
206
214
|
<SummaryRow
|
|
207
215
|
label={t('orders.summary.outstanding_balance')}
|
|
208
|
-
value={order.
|
|
216
|
+
value={money(order.amount_due)}
|
|
209
217
|
highlight
|
|
210
|
-
danger={outstanding
|
|
218
|
+
danger={outstanding}
|
|
211
219
|
/>
|
|
212
220
|
</div>
|
|
213
221
|
</Card>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatStoreDateTime } from '@spree/dashboard-core'
|
|
1
|
+
import { formatStoreDateTime, Money } from '@spree/dashboard-core'
|
|
2
2
|
import {
|
|
3
3
|
Card,
|
|
4
4
|
CardContent,
|
|
@@ -88,7 +88,9 @@ export function OrderEarningsCard({ order }: { order: Order }) {
|
|
|
88
88
|
</span>
|
|
89
89
|
)}
|
|
90
90
|
</TableCell>
|
|
91
|
-
<TableCell className="text-right tabular-nums">
|
|
91
|
+
<TableCell className="text-right tabular-nums">
|
|
92
|
+
<Money amount={transfer.amount} currency={transfer.currency} />
|
|
93
|
+
</TableCell>
|
|
92
94
|
</TableRow>
|
|
93
95
|
))}
|
|
94
96
|
</TableBody>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatMoney, useMoneyLocale } from '@spree/dashboard-core'
|
|
1
2
|
import {
|
|
2
3
|
Card,
|
|
3
4
|
CardAction,
|
|
@@ -31,7 +32,10 @@ import { ReadRow } from '../read-row'
|
|
|
31
32
|
*/
|
|
32
33
|
export function OrderPaymentCard({ order }: { order: Order }) {
|
|
33
34
|
const { t } = useTranslation()
|
|
35
|
+
const locale = useMoneyLocale()
|
|
34
36
|
const splits = order.payment_splits ?? []
|
|
37
|
+
const money = (amount: string | null | undefined, currency = order.currency) =>
|
|
38
|
+
formatMoney(amount, currency, locale)
|
|
35
39
|
|
|
36
40
|
return (
|
|
37
41
|
<Card>
|
|
@@ -53,10 +57,8 @@ export function OrderPaymentCard({ order }: { order: Order }) {
|
|
|
53
57
|
</CardHeader>
|
|
54
58
|
|
|
55
59
|
<CardContent className="flex flex-col gap-3">
|
|
56
|
-
<ReadRow label={t('orders.summary.payment_total')}>{order.
|
|
57
|
-
<ReadRow label={t('orders.summary.outstanding_balance')}>
|
|
58
|
-
{order.display_amount_due}
|
|
59
|
-
</ReadRow>
|
|
60
|
+
<ReadRow label={t('orders.summary.payment_total')}>{money(order.payment_total)}</ReadRow>
|
|
61
|
+
<ReadRow label={t('orders.summary.outstanding_balance')}>{money(order.amount_due)}</ReadRow>
|
|
60
62
|
</CardContent>
|
|
61
63
|
|
|
62
64
|
{splits.length > 0 && (
|
|
@@ -77,11 +79,17 @@ export function OrderPaymentCard({ order }: { order: Order }) {
|
|
|
77
79
|
<TableBody>
|
|
78
80
|
{splits.map((split) => (
|
|
79
81
|
<TableRow key={split.id}>
|
|
80
|
-
<TableCell className="tabular-nums">
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
<TableCell className="tabular-nums">
|
|
83
|
+
{money(split.authorized_amount, split.currency)}
|
|
84
|
+
</TableCell>
|
|
85
|
+
<TableCell className="tabular-nums">
|
|
86
|
+
{money(split.captured_amount, split.currency)}
|
|
87
|
+
</TableCell>
|
|
88
|
+
<TableCell className="tabular-nums">
|
|
89
|
+
{money(split.refunded_amount, split.currency)}
|
|
90
|
+
</TableCell>
|
|
83
91
|
<TableCell className="text-right tabular-nums">
|
|
84
|
-
{split.
|
|
92
|
+
{money(split.refundable_amount, split.currency)}
|
|
85
93
|
</TableCell>
|
|
86
94
|
</TableRow>
|
|
87
95
|
))}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { currencyParts } from '@spree/dashboard-core'
|
|
1
|
+
import { currencyParts, formatMoney, isPositiveMoney, useMoneyLocale } from '@spree/dashboard-core'
|
|
2
2
|
import {
|
|
3
3
|
Button,
|
|
4
4
|
Card,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
ShieldAlertIcon,
|
|
24
24
|
} from '@spree/dashboard-ui/icons'
|
|
25
25
|
import type { Claim, Order } from '@spree/seller-sdk'
|
|
26
|
+
import { sumMoney } from '@spree/seller-sdk'
|
|
26
27
|
import i18n from 'i18next'
|
|
27
28
|
import { useState } from 'react'
|
|
28
29
|
import { useTranslation } from 'react-i18next'
|
|
@@ -47,6 +48,7 @@ const CLAIM_ACTIONABLE = ['open', 'approved']
|
|
|
47
48
|
*/
|
|
48
49
|
export function ExchangesCard({ order }: { order: Order }) {
|
|
49
50
|
const { t } = useTranslation()
|
|
51
|
+
const locale = useMoneyLocale()
|
|
50
52
|
const confirm = useConfirm()
|
|
51
53
|
const { data } = useOrderExchanges(order.id)
|
|
52
54
|
const { approve, receive, fulfill, cancel } = useExchangeActions(order.id)
|
|
@@ -113,7 +115,7 @@ export function ExchangesCard({ order }: { order: Order }) {
|
|
|
113
115
|
// A dearer replacement leaves the customer owing the
|
|
114
116
|
// difference, which fulfilling puts on the order.
|
|
115
117
|
if (
|
|
116
|
-
|
|
118
|
+
isPositiveMoney(exchange.price_difference) &&
|
|
117
119
|
!(await confirm({
|
|
118
120
|
message: t('orders.post_sale.exchanges.balance_due_confirm'),
|
|
119
121
|
confirmLabel: t('orders.post_sale.exchanges.fulfill'),
|
|
@@ -152,7 +154,7 @@ export function ExchangesCard({ order }: { order: Order }) {
|
|
|
152
154
|
))}
|
|
153
155
|
<p className="text-muted-foreground text-sm">
|
|
154
156
|
{t('orders.post_sale.exchanges.price_difference', {
|
|
155
|
-
amount: exchange.
|
|
157
|
+
amount: formatMoney(exchange.price_difference, order.currency, locale),
|
|
156
158
|
})}
|
|
157
159
|
</p>
|
|
158
160
|
</CardContent>
|
|
@@ -322,14 +324,9 @@ function ResolveClaimDialog({
|
|
|
322
324
|
// A claim opened without per-item amounts has a refund_total of zero, and
|
|
323
325
|
// the workflow refuses to refund nothing — offer what the customer paid for
|
|
324
326
|
// the claimed items instead, which is also the ceiling it enforces.
|
|
325
|
-
const
|
|
326
|
-
const paid = lines.reduce((sum, line) => sum + Number(line.paid_amount ?? 0), 0)
|
|
327
|
+
const paid = sumMoney(lines.map((line) => line.paid_amount))
|
|
327
328
|
const defaultAmount =
|
|
328
|
-
|
|
329
|
-
? claim.refund_total
|
|
330
|
-
: paid > 0
|
|
331
|
-
? paid.toFixed(2)
|
|
332
|
-
: claim.refund_total
|
|
329
|
+
isPositiveMoney(claim.refund_total) || !isPositiveMoney(paid) ? claim.refund_total : paid
|
|
333
330
|
|
|
334
331
|
return (
|
|
335
332
|
<ClaimResolveDialog
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { currencyParts } from '@spree/dashboard-core'
|
|
1
|
+
import { currencyParts, paidForUnits } from '@spree/dashboard-core'
|
|
2
2
|
import {
|
|
3
3
|
Field,
|
|
4
4
|
FieldLabel,
|
|
@@ -137,8 +137,7 @@ export function CreateClaimDialog({
|
|
|
137
137
|
id: item.id,
|
|
138
138
|
label: unitLabel(item),
|
|
139
139
|
quantity: item.quantity,
|
|
140
|
-
|
|
141
|
-
additionalTaxTotal: item.additional_tax_total,
|
|
140
|
+
paidFor: (quantity: number) => paidForUnits(item, quantity, order.currency),
|
|
142
141
|
}))}
|
|
143
142
|
currencySymbol={currencySymbol}
|
|
144
143
|
reasonField={<ClaimReasonField value={reasonId} onChange={setReasonId} />}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
currencyParts,
|
|
3
|
+
formatMoney,
|
|
4
|
+
isPositiveMoney,
|
|
5
|
+
returnOwesNothing,
|
|
6
|
+
returnRefundSummary,
|
|
7
|
+
useMoneyLocale,
|
|
8
|
+
} from '@spree/dashboard-core'
|
|
2
9
|
import {
|
|
3
10
|
Badge,
|
|
4
11
|
Button,
|
|
@@ -14,8 +21,6 @@ import {
|
|
|
14
21
|
DropdownMenuTrigger,
|
|
15
22
|
ReturnReceiveDialog,
|
|
16
23
|
ReturnRefundDialog,
|
|
17
|
-
returnOwesNothing,
|
|
18
|
-
returnRefundSummary,
|
|
19
24
|
StatusBadge,
|
|
20
25
|
useConfirm,
|
|
21
26
|
} from '@spree/dashboard-ui'
|
|
@@ -138,7 +143,7 @@ export function ReturnsCard({ order }: { order: Order }) {
|
|
|
138
143
|
{returnRecord.return_line_items?.map((line) => (
|
|
139
144
|
<ReturnLineRow key={line.id} line={line} />
|
|
140
145
|
))}
|
|
141
|
-
<RefundSummary returnRecord={returnRecord} />
|
|
146
|
+
<RefundSummary returnRecord={returnRecord} currency={order.currency} />
|
|
142
147
|
</CardContent>
|
|
143
148
|
</Card>
|
|
144
149
|
))
|
|
@@ -165,9 +170,10 @@ export function ReturnsCard({ order }: { order: Order }) {
|
|
|
165
170
|
)
|
|
166
171
|
}
|
|
167
172
|
|
|
168
|
-
function RefundSummary({ returnRecord }: { returnRecord: Return }) {
|
|
173
|
+
function RefundSummary({ returnRecord, currency }: { returnRecord: Return; currency: string }) {
|
|
169
174
|
const { t } = useTranslation()
|
|
170
|
-
const
|
|
175
|
+
const locale = useMoneyLocale()
|
|
176
|
+
const summary = returnRefundSummary(returnRecord, currency, locale)
|
|
171
177
|
|
|
172
178
|
return (
|
|
173
179
|
<p className="text-muted-foreground text-sm">
|
|
@@ -260,9 +266,10 @@ function RefundDialog({
|
|
|
260
266
|
return (
|
|
261
267
|
<ReturnRefundDialog
|
|
262
268
|
refundableTotal={returnRecord.refundable_total}
|
|
269
|
+
owesNothing={returnOwesNothing(returnRecord.refundable_total)}
|
|
263
270
|
refundTaxTotal={
|
|
264
|
-
|
|
265
|
-
? returnRecord.
|
|
271
|
+
isPositiveMoney(returnRecord.refund_tax_total)
|
|
272
|
+
? formatMoney(returnRecord.refund_tax_total, order.currency, i18n.language)
|
|
266
273
|
: undefined
|
|
267
274
|
}
|
|
268
275
|
currencySymbol={currencySymbol}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
downloadFromApi,
|
|
3
|
+
formatMoney,
|
|
4
|
+
getApiClient,
|
|
5
|
+
useAuth,
|
|
6
|
+
useMoneyLocale,
|
|
7
|
+
} from '@spree/dashboard-core'
|
|
2
8
|
import {
|
|
3
9
|
Badge,
|
|
4
10
|
Button,
|
|
@@ -19,6 +25,7 @@ import {
|
|
|
19
25
|
import type { ShippingLabel } from '@spree/seller-sdk'
|
|
20
26
|
import { useTranslation } from 'react-i18next'
|
|
21
27
|
import { useFulfillmentActions } from '../../hooks/use-fulfillments'
|
|
28
|
+
import { useOrder } from '../../hooks/use-order'
|
|
22
29
|
|
|
23
30
|
/**
|
|
24
31
|
* The label bound to a parcel: what it cost and how to print it.
|
|
@@ -41,6 +48,11 @@ export function ShippingLabelRow({
|
|
|
41
48
|
const { token } = useAuth()
|
|
42
49
|
const confirm = useConfirm()
|
|
43
50
|
const { deleteLabel } = useFulfillmentActions(orderId)
|
|
51
|
+
const locale = useMoneyLocale()
|
|
52
|
+
const { data: order } = useOrder(orderId)
|
|
53
|
+
// A label with no currency of its own was bought in the order's.
|
|
54
|
+
const labelCurrency = label.currency ?? order?.currency
|
|
55
|
+
const cost = labelCurrency ? formatMoney(label.cost, labelCurrency, locale) : ''
|
|
44
56
|
|
|
45
57
|
const deletable = label.source === 'uploaded'
|
|
46
58
|
|
|
@@ -83,9 +95,9 @@ export function ShippingLabelRow({
|
|
|
83
95
|
{label.source === 'uploaded' && (
|
|
84
96
|
<Badge variant="secondary">{t('orders.fulfillments.label_uploaded')}</Badge>
|
|
85
97
|
)}
|
|
86
|
-
{
|
|
98
|
+
{cost && (
|
|
87
99
|
<span className="text-muted-foreground">
|
|
88
|
-
{t('orders.fulfillments.label_cost', { amount:
|
|
100
|
+
{t('orders.fulfillments.label_cost', { amount: cost })}
|
|
89
101
|
</span>
|
|
90
102
|
)}
|
|
91
103
|
</div>
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
toastManager,
|
|
18
18
|
} from '@spree/dashboard-ui'
|
|
19
19
|
import { PencilIcon, PlusIcon } from '@spree/dashboard-ui/icons'
|
|
20
|
-
import type {
|
|
20
|
+
import type { StockLocationUpdateParams } from '@spree/seller-sdk'
|
|
21
21
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
22
22
|
import { useParams } from '@tanstack/react-router'
|
|
23
23
|
import { useState } from 'react'
|
|
@@ -46,11 +46,11 @@ export function SellerReturnsLocationCard({ headless = false }: { headless?: boo
|
|
|
46
46
|
// more locations than one page holds would otherwise see one address while
|
|
47
47
|
// the server sent their returns to another, with nothing on screen saying
|
|
48
48
|
// so — and they would edit the wrong one.
|
|
49
|
-
const activeParams = {
|
|
49
|
+
const activeParams = { active_eq: true, limit: 1 }
|
|
50
50
|
const { data: takesReturns, isError: returnsFailed } = useQuery({
|
|
51
51
|
queryKey: ['seller', sellerId, 'stock-locations', 'returns'],
|
|
52
52
|
queryFn: () =>
|
|
53
|
-
sellerClient().stockLocations.list({ ...activeParams,
|
|
53
|
+
sellerClient().stockLocations.list({ ...activeParams, returns_enabled_eq: true }),
|
|
54
54
|
})
|
|
55
55
|
const hasReturnsLocation = (takesReturns?.data.length ?? 0) > 0
|
|
56
56
|
const { data: anyActive, isError: anyFailed } = useQuery({
|
|
@@ -67,7 +67,7 @@ export function SellerReturnsLocationCard({ headless = false }: { headless?: boo
|
|
|
67
67
|
const title = t('profile.returns_address')
|
|
68
68
|
|
|
69
69
|
const save = useMutation({
|
|
70
|
-
mutationFn: (params:
|
|
70
|
+
mutationFn: (params: StockLocationUpdateParams) => {
|
|
71
71
|
if (!location) throw new Error(t('common.error'))
|
|
72
72
|
return sellerClient().stockLocations.update(location.id, params)
|
|
73
73
|
},
|
|
@@ -37,7 +37,7 @@ export function SellerTaxIdentifiersCard() {
|
|
|
37
37
|
onSuccess: refresh,
|
|
38
38
|
})
|
|
39
39
|
const remove = useMutation({
|
|
40
|
-
mutationFn: (id: string) => sellerClient().taxIdentifiers.
|
|
40
|
+
mutationFn: (id: string) => sellerClient().taxIdentifiers.delete(id),
|
|
41
41
|
onSuccess: refresh,
|
|
42
42
|
})
|
|
43
43
|
const validate = useMutation({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useResourceKeyBuilder } from '@spree/dashboard-core'
|
|
2
2
|
import { toastManager } from '@spree/dashboard-ui'
|
|
3
|
+
import type { CurrencyCode } from '@spree/seller-sdk'
|
|
3
4
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
4
5
|
import { sellerClient } from '../api-client'
|
|
5
6
|
|
|
@@ -125,14 +126,14 @@ export function useFulfillmentActions(orderId: string) {
|
|
|
125
126
|
...params
|
|
126
127
|
}: {
|
|
127
128
|
fulfillmentId: string
|
|
128
|
-
|
|
129
|
+
file_signed_id: string
|
|
129
130
|
// Required: a label with no tracking number is refused server-side,
|
|
130
131
|
// since nothing could then follow the parcel it prints.
|
|
131
132
|
tracking_number: string
|
|
132
133
|
carrier?: string
|
|
133
134
|
service?: string
|
|
134
135
|
cost?: string
|
|
135
|
-
currency?:
|
|
136
|
+
currency?: CurrencyCode
|
|
136
137
|
}) => sellerClient().orders.fulfillments.labels.create(orderId, fulfillmentId, params),
|
|
137
138
|
)
|
|
138
139
|
|
|
@@ -22,5 +22,5 @@ export function useStoreTimezone(): string {
|
|
|
22
22
|
enabled: Boolean(sellerId),
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
return profile?.
|
|
25
|
+
return profile?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC'
|
|
26
26
|
}
|
package/src/locales/ar.json
CHANGED
|
@@ -474,6 +474,7 @@
|
|
|
474
474
|
"packing_slip_title": "قائمة التعبئة — الطلب {{number}}",
|
|
475
475
|
"print_label": "طباعة الملصق",
|
|
476
476
|
"print_packing_slip": "طباعة قائمة التعبئة",
|
|
477
|
+
"quoted_after_review": "يُسعَّر بعد المراجعة",
|
|
477
478
|
"ship_to": "الشحن إلى",
|
|
478
479
|
"ships_from_label": "يُشحن من",
|
|
479
480
|
"split": "تقسيم",
|
package/src/locales/de.json
CHANGED
|
@@ -470,6 +470,7 @@
|
|
|
470
470
|
"packing_slip_title": "Lieferschein — Bestellung {{number}}",
|
|
471
471
|
"print_label": "Etikett drucken",
|
|
472
472
|
"print_packing_slip": "Lieferschein drucken",
|
|
473
|
+
"quoted_after_review": "Preis nach Prüfung",
|
|
473
474
|
"ship_to": "Lieferung an",
|
|
474
475
|
"ships_from_label": "Versand ab",
|
|
475
476
|
"split": "Teilen",
|
package/src/locales/en.json
CHANGED
|
@@ -470,6 +470,7 @@
|
|
|
470
470
|
"packing_slip_title": "Packing slip — order {{number}}",
|
|
471
471
|
"print_label": "Print label",
|
|
472
472
|
"print_packing_slip": "Print packing slip",
|
|
473
|
+
"quoted_after_review": "Quoted after review",
|
|
473
474
|
"ship_to": "Ship to",
|
|
474
475
|
"ships_from_label": "Ships from",
|
|
475
476
|
"split": "Split",
|
package/src/locales/fr.json
CHANGED
|
@@ -470,6 +470,7 @@
|
|
|
470
470
|
"packing_slip_title": "Bon de livraison — commande {{number}}",
|
|
471
471
|
"print_label": "Imprimer l’étiquette",
|
|
472
472
|
"print_packing_slip": "Imprimer le bon de livraison",
|
|
473
|
+
"quoted_after_review": "Prix après vérification",
|
|
473
474
|
"ship_to": "Livrer à",
|
|
474
475
|
"ships_from_label": "Expédié depuis",
|
|
475
476
|
"split": "Scinder",
|
package/src/locales/pl.json
CHANGED
|
@@ -472,6 +472,7 @@
|
|
|
472
472
|
"packing_slip_title": "List przewozowy — zamówienie {{number}}",
|
|
473
473
|
"print_label": "Drukuj etykietę",
|
|
474
474
|
"print_packing_slip": "Drukuj list przewozowy",
|
|
475
|
+
"quoted_after_review": "Wycena po weryfikacji",
|
|
475
476
|
"ship_to": "Wysyłka do",
|
|
476
477
|
"ships_from_label": "Wysyłka z",
|
|
477
478
|
"split": "Podziel",
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
defaultPreferences,
|
|
3
|
+
filterPreferenceSchema,
|
|
3
4
|
PageHeader,
|
|
4
5
|
PreferencesForm,
|
|
5
6
|
type ResourceSearch,
|
|
@@ -37,7 +38,7 @@ import {
|
|
|
37
38
|
useRowClickBridge,
|
|
38
39
|
} from '@spree/dashboard-ui'
|
|
39
40
|
import { PlusIcon, TrashIcon } from '@spree/dashboard-ui/icons'
|
|
40
|
-
import type { DeliveryMethod,
|
|
41
|
+
import type { DeliveryMethod, PreferenceSchema } from '@spree/seller-sdk'
|
|
41
42
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
42
43
|
import { useParams } from '@tanstack/react-router'
|
|
43
44
|
import { useEffect, useState } from 'react'
|
|
@@ -166,8 +167,7 @@ interface DeliveryMethodFormValues {
|
|
|
166
167
|
storefront_visible: boolean
|
|
167
168
|
delivery_profile_id: string
|
|
168
169
|
delivery_zone_id: string
|
|
169
|
-
|
|
170
|
-
calculator_preferences: Record<string, unknown>
|
|
170
|
+
calculator: { type: string; preferences: Record<string, unknown> }
|
|
171
171
|
rules: RuleDraft[]
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -177,8 +177,7 @@ const DEFAULTS: DeliveryMethodFormValues = {
|
|
|
177
177
|
storefront_visible: true,
|
|
178
178
|
delivery_profile_id: '',
|
|
179
179
|
delivery_zone_id: '',
|
|
180
|
-
|
|
181
|
-
calculator_preferences: {},
|
|
180
|
+
calculator: { type: '', preferences: {} },
|
|
182
181
|
rules: [],
|
|
183
182
|
}
|
|
184
183
|
|
|
@@ -231,7 +230,7 @@ function DeliveryMethodSheet({
|
|
|
231
230
|
|
|
232
231
|
const form = useForm<DeliveryMethodFormValues>({ defaultValues: DEFAULTS })
|
|
233
232
|
const profileId = form.watch('delivery_profile_id')
|
|
234
|
-
const calculatorType = form.watch('
|
|
233
|
+
const calculatorType = form.watch('calculator.type')
|
|
235
234
|
const rules = form.watch('rules')
|
|
236
235
|
|
|
237
236
|
// A zone only means something under its profile, so the picker follows
|
|
@@ -252,8 +251,10 @@ function DeliveryMethodSheet({
|
|
|
252
251
|
storefront_visible: method.storefront_visible,
|
|
253
252
|
delivery_profile_id: method.delivery_profile_id ?? '',
|
|
254
253
|
delivery_zone_id: method.delivery_zone_id ?? '',
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
calculator: {
|
|
255
|
+
type: method.calculator?.type ?? '',
|
|
256
|
+
preferences: method.calculator?.preferences ?? {},
|
|
257
|
+
},
|
|
257
258
|
rules: (method.rules ?? []).map((rule) => ({
|
|
258
259
|
id: rule.id,
|
|
259
260
|
type: rule.type,
|
|
@@ -274,19 +275,18 @@ function DeliveryMethodSheet({
|
|
|
274
275
|
// the picker blank would show an empty Pricing box while the server
|
|
275
276
|
// quietly created the method free.
|
|
276
277
|
useEffect(() => {
|
|
277
|
-
if (methodId || form.getValues('
|
|
278
|
+
if (methodId || form.getValues('calculator.type')) return
|
|
278
279
|
const flatRate = calculators?.data.find((calculator) => calculator.type === 'flat_rate')
|
|
279
280
|
if (!flatRate) return
|
|
280
281
|
|
|
281
|
-
form.setValue('
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
)
|
|
282
|
+
form.setValue('calculator', {
|
|
283
|
+
type: flatRate.type,
|
|
284
|
+
preferences: defaultPreferences(renderableSchema(flatRate.schema)),
|
|
285
|
+
})
|
|
286
286
|
}, [calculators, methodId, form])
|
|
287
287
|
|
|
288
288
|
const calculatorSchema = renderableSchema(
|
|
289
|
-
calculators?.data.find((calculator) => calculator.type === calculatorType)?.
|
|
289
|
+
calculators?.data.find((calculator) => calculator.type === calculatorType)?.schema,
|
|
290
290
|
)
|
|
291
291
|
|
|
292
292
|
const save = useMutation({
|
|
@@ -297,8 +297,10 @@ function DeliveryMethodSheet({
|
|
|
297
297
|
storefront_visible: values.storefront_visible,
|
|
298
298
|
delivery_profile_id: values.delivery_profile_id || undefined,
|
|
299
299
|
delivery_zone_id: values.delivery_zone_id || null,
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
calculator: {
|
|
301
|
+
...(values.calculator.type ? { type: values.calculator.type } : {}),
|
|
302
|
+
preferences: values.calculator.preferences,
|
|
303
|
+
},
|
|
302
304
|
// The array replaces the whole set: a rule dropped here is deleted,
|
|
303
305
|
// which is why every surviving row is re-sent with its id.
|
|
304
306
|
rules: values.rules.map((rule) => ({
|
|
@@ -329,9 +331,7 @@ function DeliveryMethodSheet({
|
|
|
329
331
|
const { errors } = form.formState
|
|
330
332
|
|
|
331
333
|
function addRule(type: string) {
|
|
332
|
-
const schema = renderableSchema(
|
|
333
|
-
ruleTypes?.data.find((rule) => rule.type === type)?.preference_schema,
|
|
334
|
-
)
|
|
334
|
+
const schema = renderableSchema(ruleTypes?.data.find((rule) => rule.type === type)?.schema)
|
|
335
335
|
form.setValue('rules', [...rules, { type, preferences: defaultPreferences(schema) }])
|
|
336
336
|
}
|
|
337
337
|
|
|
@@ -482,7 +482,7 @@ function DeliveryMethodSheet({
|
|
|
482
482
|
</FieldLabel>
|
|
483
483
|
<Controller
|
|
484
484
|
control={form.control}
|
|
485
|
-
name="
|
|
485
|
+
name="calculator.type"
|
|
486
486
|
render={({ field }) => (
|
|
487
487
|
<Select
|
|
488
488
|
value={field.value}
|
|
@@ -490,9 +490,9 @@ function DeliveryMethodSheet({
|
|
|
490
490
|
field.onChange(next)
|
|
491
491
|
const schema = renderableSchema(
|
|
492
492
|
calculators?.data.find((calculator) => calculator.type === next)
|
|
493
|
-
?.
|
|
493
|
+
?.schema,
|
|
494
494
|
)
|
|
495
|
-
form.setValue('
|
|
495
|
+
form.setValue('calculator.preferences', defaultPreferences(schema))
|
|
496
496
|
}}
|
|
497
497
|
>
|
|
498
498
|
<SelectTrigger id="delivery-method-calculator">
|
|
@@ -521,7 +521,7 @@ function DeliveryMethodSheet({
|
|
|
521
521
|
|
|
522
522
|
<Controller
|
|
523
523
|
control={form.control}
|
|
524
|
-
name="
|
|
524
|
+
name="calculator.preferences"
|
|
525
525
|
render={({ field }) => (
|
|
526
526
|
<PreferencesForm
|
|
527
527
|
schema={calculatorSchema}
|
|
@@ -582,7 +582,7 @@ function DeliveryMethodSheet({
|
|
|
582
582
|
</div>
|
|
583
583
|
|
|
584
584
|
<PreferencesForm
|
|
585
|
-
schema={renderableSchema(definition?.
|
|
585
|
+
schema={renderableSchema(definition?.schema)}
|
|
586
586
|
currencyOptions={currencyOptions}
|
|
587
587
|
values={rule.preferences}
|
|
588
588
|
onChange={(next) =>
|
|
@@ -632,16 +632,19 @@ function DeliveryMethodSheet({
|
|
|
632
632
|
}
|
|
633
633
|
|
|
634
634
|
/**
|
|
635
|
-
* The
|
|
635
|
+
* The part of a preference schema the shared form can render.
|
|
636
636
|
*
|
|
637
|
-
* Drops
|
|
637
|
+
* Drops keyed maps — today the flat-rate calculator's
|
|
638
638
|
* per-currency `amounts` map, which the operator edits through a dedicated
|
|
639
639
|
* multi-currency control and the generic form would show as the hash itself
|
|
640
640
|
* in a text box. A seller quotes in the store's currency, so `amount` and
|
|
641
641
|
* `currency` are the whole question.
|
|
642
642
|
*/
|
|
643
|
-
function renderableSchema(
|
|
644
|
-
schema
|
|
645
|
-
|
|
646
|
-
return (
|
|
643
|
+
function renderableSchema(schema: PreferenceSchema | undefined): PreferenceSchema | undefined {
|
|
644
|
+
if (!schema) return undefined
|
|
645
|
+
|
|
646
|
+
return filterPreferenceSchema(
|
|
647
|
+
schema,
|
|
648
|
+
(_key, property) => typeof property.additionalProperties !== 'object',
|
|
649
|
+
)
|
|
647
650
|
}
|
package/src/pages/onboarding.tsx
CHANGED
|
@@ -244,7 +244,7 @@ function RequirementAction({ requirement }: { requirement: RequirementStatus })
|
|
|
244
244
|
mutationFn: () =>
|
|
245
245
|
sellerClient().requirementSubmissions.create(requirement.id, {
|
|
246
246
|
note: note || undefined,
|
|
247
|
-
|
|
247
|
+
file_signed_id: file.signedId || undefined,
|
|
248
248
|
}),
|
|
249
249
|
onSuccess: () => {
|
|
250
250
|
setNote('')
|
|
@@ -349,6 +349,7 @@ function RequirementAction({ requirement }: { requirement: RequirementStatus })
|
|
|
349
349
|
value={file}
|
|
350
350
|
onChange={setFile}
|
|
351
351
|
accept={requirement.accepted_content_types?.join(',')}
|
|
352
|
+
private
|
|
352
353
|
label={t('onboarding.document_label')}
|
|
353
354
|
help={t('onboarding.document_help')}
|
|
354
355
|
/>
|
package/src/pages/payout.tsx
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
formatMoney,
|
|
3
|
+
formatStoreDateTime,
|
|
4
|
+
Money,
|
|
5
|
+
PageHeader,
|
|
6
|
+
Slot,
|
|
7
|
+
useMoneyLocale,
|
|
8
|
+
} from '@spree/dashboard-core'
|
|
2
9
|
import {
|
|
3
10
|
Card,
|
|
4
11
|
CardHeader,
|
|
@@ -32,6 +39,7 @@ import { useStoreTimezone } from '../hooks/use-store-timezone'
|
|
|
32
39
|
*/
|
|
33
40
|
export function PayoutPage() {
|
|
34
41
|
const { t } = useTranslation()
|
|
42
|
+
const locale = useMoneyLocale()
|
|
35
43
|
const { sellerId, payoutId } = useParams({ from: '/_authenticated/$sellerId/payouts/$payoutId' })
|
|
36
44
|
|
|
37
45
|
const timezone = useStoreTimezone()
|
|
@@ -54,7 +62,7 @@ export function PayoutPage() {
|
|
|
54
62
|
<ResourceLayout
|
|
55
63
|
header={
|
|
56
64
|
<PageHeader
|
|
57
|
-
title={payout.
|
|
65
|
+
title={formatMoney(payout.amount, payout.currency, locale)}
|
|
58
66
|
subtitle={formatStoreDateTime(payout.created_at, timezone)}
|
|
59
67
|
backTo="payouts"
|
|
60
68
|
badges={
|
|
@@ -121,7 +129,7 @@ export function PayoutPage() {
|
|
|
121
129
|
{t(`earnings.kinds.${transfer.kind}`, { defaultValue: transfer.kind })}
|
|
122
130
|
</TableCell>
|
|
123
131
|
<TableCell className="text-right tabular-nums">
|
|
124
|
-
{transfer.
|
|
132
|
+
<Money amount={transfer.amount} currency={transfer.currency} />
|
|
125
133
|
</TableCell>
|
|
126
134
|
</TableRow>
|
|
127
135
|
))}
|
|
@@ -140,7 +148,9 @@ export function PayoutPage() {
|
|
|
140
148
|
<CardTitle>{t('payouts.detail.settlement')}</CardTitle>
|
|
141
149
|
</CardHeader>
|
|
142
150
|
<div className="flex flex-col gap-3 px-6 pb-6">
|
|
143
|
-
<ReadRow label={t('payouts.columns.amount')}>
|
|
151
|
+
<ReadRow label={t('payouts.columns.amount')}>
|
|
152
|
+
<Money amount={payout.amount} currency={payout.currency} />
|
|
153
|
+
</ReadRow>
|
|
144
154
|
<ReadRow label={t('payouts.columns.reference')}>{payout.reference}</ReadRow>
|
|
145
155
|
<ReadRow label={t('payouts.detail.period')}>
|
|
146
156
|
{payout.period_start && payout.period_end
|
package/src/pages/product.tsx
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
variantToWirePayload,
|
|
20
20
|
} from '@spree/dashboard-core'
|
|
21
21
|
import { FormActions, ResourceLayout, StatusBadge, toastManager } from '@spree/dashboard-ui'
|
|
22
|
-
import type {
|
|
22
|
+
import type { ProductCreateParams } from '@spree/seller-sdk'
|
|
23
23
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
24
24
|
import { useNavigate, useParams } from '@tanstack/react-router'
|
|
25
25
|
import { useEffect } from 'react'
|
|
@@ -119,7 +119,7 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
|
|
|
119
119
|
values: ProductFormValues
|
|
120
120
|
extensionValues: Record<string, unknown>
|
|
121
121
|
}) => {
|
|
122
|
-
const payload:
|
|
122
|
+
const payload: ProductCreateParams = {
|
|
123
123
|
...extensionValues,
|
|
124
124
|
name: values.name,
|
|
125
125
|
description: values.description,
|
|
@@ -150,7 +150,7 @@ export function ProductPage({ mode }: { mode: 'new' | 'edit' }) {
|
|
|
150
150
|
})),
|
|
151
151
|
variants: (values.variants ?? []).map((variant, index) =>
|
|
152
152
|
variantToWirePayload(variant, index),
|
|
153
|
-
) as
|
|
153
|
+
) as ProductCreateParams['variants'],
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
return productId
|
package/src/pages/team.tsx
CHANGED
|
@@ -175,7 +175,7 @@ function MemberRow({ member, lastOne }: { member: TeamMember; lastOne: boolean }
|
|
|
175
175
|
const confirm = useConfirm()
|
|
176
176
|
|
|
177
177
|
const remove = useMutation({
|
|
178
|
-
mutationFn: (id: string) => sellerClient().team.
|
|
178
|
+
mutationFn: (id: string) => sellerClient().team.delete(id),
|
|
179
179
|
onSuccess: () => {
|
|
180
180
|
toastManager.add({ type: 'success', title: t('team.messages.removed') })
|
|
181
181
|
void queryClient.invalidateQueries({ queryKey: ['seller', sellerId, 'team'] })
|
|
@@ -302,7 +302,7 @@ function InvitationRow({ invitation }: { invitation: Invitation }) {
|
|
|
302
302
|
})
|
|
303
303
|
|
|
304
304
|
const revoke = useMutation({
|
|
305
|
-
mutationFn: (id: string) => sellerClient().invitations.
|
|
305
|
+
mutationFn: (id: string) => sellerClient().invitations.delete(id),
|
|
306
306
|
onSuccess: () => {
|
|
307
307
|
toastManager.add({ type: 'success', title: t('team.messages.revoked') })
|
|
308
308
|
void invalidate()
|
|
@@ -6,7 +6,7 @@ import i18n from 'i18next'
|
|
|
6
6
|
|
|
7
7
|
defineTable<DeliveryMethod>('seller-delivery-methods', {
|
|
8
8
|
title: i18n.t('delivery_methods.title'),
|
|
9
|
-
searchParam: '
|
|
9
|
+
searchParam: 'search',
|
|
10
10
|
searchPlaceholder: i18n.t('delivery_methods.search_placeholder'),
|
|
11
11
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
12
12
|
emptyIcon: <TruckIcon className="size-8 text-muted-foreground" />,
|
package/src/tables/orders.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineTable, formatStoreDateTime } from '@spree/dashboard-core'
|
|
1
|
+
import { defineTable, formatMoney, formatStoreDateTime } from '@spree/dashboard-core'
|
|
2
2
|
import { ResourceNameCell, StatusBadge } from '@spree/dashboard-ui'
|
|
3
3
|
import { PackageIcon } from '@spree/dashboard-ui/icons'
|
|
4
4
|
import type { Order } from '@spree/seller-sdk'
|
|
@@ -110,7 +110,7 @@ defineTable<Order>('seller-orders', {
|
|
|
110
110
|
key: 'total',
|
|
111
111
|
label: i18n.t('orders.columns.total'),
|
|
112
112
|
default: true,
|
|
113
|
-
render: (order) => order.
|
|
113
|
+
render: (order) => formatMoney(order.total, order.currency, i18n.language),
|
|
114
114
|
},
|
|
115
115
|
],
|
|
116
116
|
})
|
package/src/tables/payouts.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineTable, formatStoreDateTime } from '@spree/dashboard-core'
|
|
1
|
+
import { defineTable, formatMoney, formatStoreDateTime } from '@spree/dashboard-core'
|
|
2
2
|
import { ResourceNameCell, StatusBadge } from '@spree/dashboard-ui'
|
|
3
3
|
import { HandCoinsIcon } from '@spree/dashboard-ui/icons'
|
|
4
4
|
import type { Payout } from '@spree/seller-sdk'
|
|
@@ -22,7 +22,7 @@ const STATUSES = ['pending', 'processing', 'completed', 'failed', 'unresolved']
|
|
|
22
22
|
|
|
23
23
|
defineTable<Payout>('seller-payouts', {
|
|
24
24
|
title: i18n.t('payouts.title'),
|
|
25
|
-
searchParam: '
|
|
25
|
+
searchParam: 'search',
|
|
26
26
|
searchPlaceholder: i18n.t('payouts.search_placeholder'),
|
|
27
27
|
defaultSort: { field: 'created_at', direction: 'desc' },
|
|
28
28
|
emptyIcon: <HandCoinsIcon className="size-8 text-muted-foreground" />,
|
|
@@ -81,7 +81,7 @@ defineTable<Payout>('seller-payouts', {
|
|
|
81
81
|
sortable: true,
|
|
82
82
|
default: true,
|
|
83
83
|
className: 'text-right tabular-nums',
|
|
84
|
-
render: (payout) => payout.
|
|
84
|
+
render: (payout) => formatMoney(payout.amount, payout.currency, i18n.language),
|
|
85
85
|
},
|
|
86
86
|
],
|
|
87
87
|
})
|
package/src/tables/policies.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import i18n from 'i18next'
|
|
|
6
6
|
|
|
7
7
|
defineTable<Policy>('seller-policies', {
|
|
8
8
|
title: i18n.t('policies.title'),
|
|
9
|
-
searchParam: '
|
|
9
|
+
searchParam: 'search',
|
|
10
10
|
searchPlaceholder: i18n.t('policies.search_placeholder'),
|
|
11
11
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
12
12
|
emptyIcon: <ScrollTextIcon className="size-8 text-muted-foreground" />,
|
package/src/tables/products.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineTable } from '@spree/dashboard-core'
|
|
1
|
+
import { defineTable, formatPrice } from '@spree/dashboard-core'
|
|
2
2
|
import { ResourceNameCell, StatusBadge, Thumbnail } from '@spree/dashboard-ui'
|
|
3
3
|
import { PackageIcon } from '@spree/dashboard-ui/icons'
|
|
4
4
|
import type { Product } from '@spree/seller-sdk'
|
|
@@ -9,7 +9,7 @@ const PRODUCT_STATUSES = ['active', 'draft', 'proposed', 'rejected', 'archived']
|
|
|
9
9
|
|
|
10
10
|
defineTable<Product>('seller-products', {
|
|
11
11
|
title: i18n.t('products.title'),
|
|
12
|
-
searchParam: '
|
|
12
|
+
searchParam: 'search',
|
|
13
13
|
searchPlaceholder: i18n.t('products.search_placeholder'),
|
|
14
14
|
defaultSort: { field: 'created_at', direction: 'desc' },
|
|
15
15
|
emptyIcon: <PackageIcon className="size-8 text-muted-foreground" />,
|
|
@@ -50,7 +50,7 @@ defineTable<Product>('seller-products', {
|
|
|
50
50
|
key: 'price',
|
|
51
51
|
label: i18n.t('products.columns.price'),
|
|
52
52
|
default: true,
|
|
53
|
-
render: (product) => product.price
|
|
53
|
+
render: (product) => formatPrice(product.price ?? null),
|
|
54
54
|
},
|
|
55
55
|
],
|
|
56
56
|
})
|
package/src/tables/transfers.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineTable, formatStoreDateTime, useTenantId } from '@spree/dashboard-core'
|
|
1
|
+
import { defineTable, formatMoney, formatStoreDateTime, useTenantId } from '@spree/dashboard-core'
|
|
2
2
|
import { StatusBadge } from '@spree/dashboard-ui'
|
|
3
3
|
import { BanknoteIcon } from '@spree/dashboard-ui/icons'
|
|
4
4
|
import type { Transfer } from '@spree/seller-sdk'
|
|
@@ -51,7 +51,7 @@ defineTable<Transfer>('seller-transfers', {
|
|
|
51
51
|
// Without this the toolbar sends `name_cont`, which the ledger does not
|
|
52
52
|
// whitelist — Ransack drops an unknown condition, so the box would look
|
|
53
53
|
// like it worked and return everything.
|
|
54
|
-
searchParam: '
|
|
54
|
+
searchParam: 'search',
|
|
55
55
|
searchPlaceholder: i18n.t('earnings.search_placeholder'),
|
|
56
56
|
defaultSort: { field: 'created_at', direction: 'desc' },
|
|
57
57
|
emptyIcon: <BanknoteIcon className="size-8 text-muted-foreground" />,
|
|
@@ -112,7 +112,7 @@ defineTable<Transfer>('seller-transfers', {
|
|
|
112
112
|
sortable: true,
|
|
113
113
|
default: true,
|
|
114
114
|
className: 'text-right tabular-nums',
|
|
115
|
-
render: (transfer) => transfer.
|
|
115
|
+
render: (transfer) => formatMoney(transfer.amount, transfer.currency, i18n.language),
|
|
116
116
|
},
|
|
117
117
|
],
|
|
118
118
|
})
|