payment-kit 1.19.1 → 1.19.2
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/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +1 -1
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -1
- package/api/src/libs/util.ts +3 -1
- package/api/src/queues/credit-consume.ts +15 -2
- package/api/src/routes/customers.ts +34 -5
- package/api/src/routes/payment-currencies.ts +6 -0
- package/api/src/routes/webhook-endpoints.ts +0 -3
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/src/components/conditional-section.tsx +87 -0
- package/src/components/customer/credit-overview.tsx +30 -17
- package/src/components/customer/form.tsx +2 -1
- package/src/components/metadata/form.tsx +2 -2
- package/src/components/meter/add-usage-dialog.tsx +2 -2
- package/src/components/meter/form.tsx +2 -2
- package/src/components/meter/products.tsx +2 -2
- package/src/components/payment-link/item.tsx +2 -2
- package/src/components/payouts/portal/list.tsx +6 -11
- package/src/components/price/currency-select.tsx +13 -9
- package/src/components/price/form.tsx +47 -16
- package/src/components/product/form.tsx +3 -8
- package/src/locales/en.tsx +6 -3
- package/src/locales/zh.tsx +6 -3
- package/src/pages/admin/customers/customers/detail.tsx +5 -13
- package/src/pages/customer/index.tsx +17 -15
|
@@ -52,7 +52,7 @@ export class CustomerCreditGrantLowBalanceEmailTemplate
|
|
|
52
52
|
// 计算百分比
|
|
53
53
|
const available = new BN(creditGrant.remaining_amount);
|
|
54
54
|
const total = new BN(creditGrant.amount);
|
|
55
|
-
const percentage = total.gt(0) ? available.mul(new BN(100)).div(total).toString() : '0';
|
|
55
|
+
const percentage = total.gt(new BN(0)) ? available.mul(new BN(100)).div(total).toString() : '0';
|
|
56
56
|
|
|
57
57
|
return {
|
|
58
58
|
locale,
|
|
@@ -58,7 +58,7 @@ export class CustomerCreditInsufficientEmailTemplate
|
|
|
58
58
|
const at = formatTime(Date.now());
|
|
59
59
|
|
|
60
60
|
// 检查是否完全耗尽(可用额度为0或负数)
|
|
61
|
-
const isExhausted = new BN(this.options.availableAmount).lte(0);
|
|
61
|
+
const isExhausted = new BN(this.options.availableAmount).lte(new BN(0));
|
|
62
62
|
|
|
63
63
|
// 如果有订阅ID,获取订阅信息
|
|
64
64
|
let productName: string | undefined;
|
package/api/src/libs/util.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { joinURL, withQuery, withTrailingSlash } from 'ufo';
|
|
|
12
12
|
import axios from 'axios';
|
|
13
13
|
import { ethers } from 'ethers';
|
|
14
14
|
import { fromUnitToToken } from '@ocap/util';
|
|
15
|
+
import get from 'lodash/get';
|
|
15
16
|
import dayjs from './dayjs';
|
|
16
17
|
import { blocklet, wallet } from './auth';
|
|
17
18
|
import type { PaymentCurrency, PaymentMethod, Subscription } from '../store/models';
|
|
@@ -268,11 +269,12 @@ export async function getUserOrAppInfo(
|
|
|
268
269
|
}
|
|
269
270
|
const { user } = await blocklet.getUser(address);
|
|
270
271
|
if (user) {
|
|
272
|
+
const locale = get(user, 'locale', 'en');
|
|
271
273
|
return {
|
|
272
274
|
name: user?.fullName,
|
|
273
275
|
avatar: joinURL(process.env.BLOCKLET_APP_URL!, user?.avatar),
|
|
274
276
|
type: 'user',
|
|
275
|
-
url: getCustomerProfileUrl({ userDid: address, locale
|
|
277
|
+
url: getCustomerProfileUrl({ userDid: address, locale }),
|
|
276
278
|
};
|
|
277
279
|
}
|
|
278
280
|
return {
|
|
@@ -227,9 +227,9 @@ async function consumeAvailableCredits(
|
|
|
227
227
|
metadata: {
|
|
228
228
|
meter_event_id: context.meterEvent.id,
|
|
229
229
|
meter_event_name: context.meterEvent.event_name,
|
|
230
|
-
required_amount:
|
|
230
|
+
required_amount: remainingToConsume.toString(),
|
|
231
231
|
available_amount: totalAvailable.toString(),
|
|
232
|
-
consumed_amount:
|
|
232
|
+
consumed_amount: consumed.toString(),
|
|
233
233
|
pending_amount: pendingAmount,
|
|
234
234
|
currency_id: currencyId,
|
|
235
235
|
subscription_id: context.subscription?.id,
|
|
@@ -258,6 +258,19 @@ async function consumeAvailableCredits(
|
|
|
258
258
|
},
|
|
259
259
|
});
|
|
260
260
|
}
|
|
261
|
+
} else if (remainingBalance === '0') {
|
|
262
|
+
await createEvent('Customer', 'customer.credit.insufficient', context.customer, {
|
|
263
|
+
metadata: {
|
|
264
|
+
meter_event_id: context.meterEvent.id,
|
|
265
|
+
meter_event_name: context.meterEvent.event_name,
|
|
266
|
+
required_amount: remainingToConsume.toString(),
|
|
267
|
+
available_amount: '0',
|
|
268
|
+
consumed_amount: consumed.toString(),
|
|
269
|
+
pending_amount: pendingAmount,
|
|
270
|
+
currency_id: currencyId,
|
|
271
|
+
subscription_id: context.subscription?.id,
|
|
272
|
+
},
|
|
273
|
+
}).catch(console.error);
|
|
261
274
|
}
|
|
262
275
|
|
|
263
276
|
return {
|
|
@@ -45,9 +45,6 @@ router.get('/', auth, async (req, res) => {
|
|
|
45
45
|
const { page, pageSize, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
|
|
46
46
|
const where = getWhereFromKvQuery(query.q);
|
|
47
47
|
|
|
48
|
-
if (typeof query.livemode === 'boolean') {
|
|
49
|
-
where.livemode = query.livemode;
|
|
50
|
-
}
|
|
51
48
|
if (query.did) {
|
|
52
49
|
where.did = query.did;
|
|
53
50
|
}
|
|
@@ -386,16 +383,48 @@ router.get('/payer-token', sessionMiddleware({ accessKey: true }), async (req, r
|
|
|
386
383
|
});
|
|
387
384
|
|
|
388
385
|
router.get('/:id', auth, async (req, res) => {
|
|
386
|
+
if (!req.params.id) {
|
|
387
|
+
return res.status(400).json({ error: 'Customer ID is required' });
|
|
388
|
+
}
|
|
389
389
|
try {
|
|
390
390
|
const doc = await Customer.findByPkOrDid(req.params.id as string);
|
|
391
391
|
if (doc) {
|
|
392
392
|
res.json(doc);
|
|
393
393
|
} else {
|
|
394
|
-
|
|
394
|
+
if (req.body.create) {
|
|
395
|
+
if (!req.user) {
|
|
396
|
+
return res.status(403).json({ error: 'Unauthorized' });
|
|
397
|
+
}
|
|
398
|
+
const { user } = await blocklet.getUser(req.params.id);
|
|
399
|
+
if (!user) {
|
|
400
|
+
return res.status(404).json({ error: 'User not found' });
|
|
401
|
+
}
|
|
402
|
+
const customer = await Customer.create({
|
|
403
|
+
livemode: true,
|
|
404
|
+
did: user.did,
|
|
405
|
+
name: user.fullName,
|
|
406
|
+
email: user.email,
|
|
407
|
+
phone: user.phone,
|
|
408
|
+
address: Customer.formatAddressFromUser(user),
|
|
409
|
+
description: user.remark,
|
|
410
|
+
metadata: {},
|
|
411
|
+
balance: '0',
|
|
412
|
+
next_invoice_sequence: 1,
|
|
413
|
+
delinquent: false,
|
|
414
|
+
invoice_prefix: Customer.getInvoicePrefix(),
|
|
415
|
+
});
|
|
416
|
+
logger.info('customer created', {
|
|
417
|
+
customerId: customer.id,
|
|
418
|
+
did: customer.did,
|
|
419
|
+
});
|
|
420
|
+
return res.json(customer);
|
|
421
|
+
}
|
|
422
|
+
return res.status(404).json(null);
|
|
395
423
|
}
|
|
424
|
+
return res.status(404).json(null);
|
|
396
425
|
} catch (err) {
|
|
397
426
|
logger.error(err);
|
|
398
|
-
res.status(500).json({ error: `Failed to get customer: ${err.message}` });
|
|
427
|
+
return res.status(500).json({ error: `Failed to get customer: ${err.message}` });
|
|
399
428
|
}
|
|
400
429
|
});
|
|
401
430
|
|
|
@@ -148,6 +148,12 @@ router.get('/', auth, async (req, res) => {
|
|
|
148
148
|
if (typeof query.livemode === 'string') {
|
|
149
149
|
where.livemode = JSON.parse(query.livemode);
|
|
150
150
|
}
|
|
151
|
+
where.type = 'standard';
|
|
152
|
+
if (query.credit) {
|
|
153
|
+
where.type = {
|
|
154
|
+
[Op.in]: ['standard', 'credit'],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
151
157
|
const list = await PaymentCurrency.findAll({
|
|
152
158
|
where,
|
|
153
159
|
order: [['created_at', 'DESC']],
|
|
@@ -43,9 +43,6 @@ router.get('/', auth, async (req, res) => {
|
|
|
43
43
|
const { page, pageSize, status, ...query } = await schema.validateAsync(req.query, { stripUnknown: true });
|
|
44
44
|
const where: WhereOptions<WebhookEndpoint> = {};
|
|
45
45
|
|
|
46
|
-
if (typeof query.livemode === 'boolean') {
|
|
47
|
-
where.livemode = query.livemode;
|
|
48
|
-
}
|
|
49
46
|
if (status) {
|
|
50
47
|
where.status = status
|
|
51
48
|
.split(',')
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.2",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@blocklet/did-space-js": "^1.0.62",
|
|
55
55
|
"@blocklet/js-sdk": "^1.16.44",
|
|
56
56
|
"@blocklet/logger": "^1.16.44",
|
|
57
|
-
"@blocklet/payment-react": "1.19.
|
|
57
|
+
"@blocklet/payment-react": "1.19.2",
|
|
58
58
|
"@blocklet/sdk": "^1.16.44",
|
|
59
59
|
"@blocklet/ui-react": "^3.0.1",
|
|
60
60
|
"@blocklet/uploader": "^0.1.97",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"devDependencies": {
|
|
123
123
|
"@abtnode/types": "^1.16.44",
|
|
124
124
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
125
|
-
"@blocklet/payment-types": "1.19.
|
|
125
|
+
"@blocklet/payment-types": "1.19.2",
|
|
126
126
|
"@types/cookie-parser": "^1.4.9",
|
|
127
127
|
"@types/cors": "^2.8.19",
|
|
128
128
|
"@types/debug": "^4.1.12",
|
|
@@ -168,5 +168,5 @@
|
|
|
168
168
|
"parser": "typescript"
|
|
169
169
|
}
|
|
170
170
|
},
|
|
171
|
-
"gitHead": "
|
|
171
|
+
"gitHead": "741c897204afc412721a942201516932bff59235"
|
|
172
172
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Box } from '@mui/material';
|
|
2
|
+
import { useState, ReactNode, useEffect, createContext, useContext, useMemo, useRef, useCallback } from 'react';
|
|
3
|
+
|
|
4
|
+
const ConditionalSectionContext = createContext<{
|
|
5
|
+
hideRender: (hide?: boolean) => void;
|
|
6
|
+
} | null>(null);
|
|
7
|
+
|
|
8
|
+
// 导出hook供子组件使用
|
|
9
|
+
export const useConditionalSection = () => {
|
|
10
|
+
const context = useContext(ConditionalSectionContext);
|
|
11
|
+
return context;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface ConditionalSectionProps {
|
|
15
|
+
skeleton: boolean;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
skeletonComponent?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 条件渲染组件 - 完全防闪现版本
|
|
22
|
+
*
|
|
23
|
+
* 彻底解决闪现问题的方案:
|
|
24
|
+
* 1. skeleton=true 时显示骨架屏组件
|
|
25
|
+
* 2. skeleton=false 时完全隐藏(display: none)渲染子组件,让其执行逻辑
|
|
26
|
+
* 3. 等待子组件执行完毕,如果没有调用hideRender则显示
|
|
27
|
+
* 4. 整个过程用户看不到任何闪现
|
|
28
|
+
*
|
|
29
|
+
* 使用方式:
|
|
30
|
+
* - 在任意深度的子组件中调用 useConditionalSection()?.hideRender()
|
|
31
|
+
*/
|
|
32
|
+
export default function ConditionalSection({ skeleton, children, skeletonComponent = null }: ConditionalSectionProps) {
|
|
33
|
+
const [renderState, setRenderState] = useState<'hidden' | 'visible' | 'none'>('hidden');
|
|
34
|
+
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
|
35
|
+
|
|
36
|
+
const handleHideRender = useCallback((hide: boolean = true) => {
|
|
37
|
+
if (timerRef.current) {
|
|
38
|
+
clearTimeout(timerRef.current);
|
|
39
|
+
timerRef.current = null;
|
|
40
|
+
}
|
|
41
|
+
setRenderState(hide ? 'none' : 'visible');
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const contextValue = useMemo(() => ({ hideRender: handleHideRender }), [handleHideRender]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!skeleton) {
|
|
48
|
+
timerRef.current = setTimeout(() => {
|
|
49
|
+
setRenderState('visible');
|
|
50
|
+
timerRef.current = null;
|
|
51
|
+
}, 3000);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 清理定时器
|
|
55
|
+
return () => {
|
|
56
|
+
if (timerRef.current) {
|
|
57
|
+
clearTimeout(timerRef.current);
|
|
58
|
+
timerRef.current = null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}, [skeleton]);
|
|
62
|
+
|
|
63
|
+
if (skeleton) {
|
|
64
|
+
return skeletonComponent;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (renderState === 'none') {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<ConditionalSectionContext.Provider value={contextValue}>
|
|
73
|
+
<Box
|
|
74
|
+
sx={{
|
|
75
|
+
position: renderState === 'hidden' ? 'absolute' : 'static',
|
|
76
|
+
left: renderState === 'hidden' ? '-9999px' : 'auto',
|
|
77
|
+
top: renderState === 'hidden' ? '-9999px' : 'auto',
|
|
78
|
+
visibility: renderState === 'hidden' ? 'hidden' : 'visible',
|
|
79
|
+
width: renderState === 'hidden' ? '0' : 'auto',
|
|
80
|
+
height: renderState === 'hidden' ? '0' : 'auto',
|
|
81
|
+
overflow: 'hidden',
|
|
82
|
+
}}>
|
|
83
|
+
{children}
|
|
84
|
+
</Box>
|
|
85
|
+
</ConditionalSectionContext.Provider>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { formatBNStr, CreditGrantsList, CreditTransactionsList, api } from '@blocklet/payment-react';
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import { Avatar, Box, Card, CardContent, Stack, Typography, Tabs, Tab } from '@mui/material';
|
|
4
|
-
import { useState } from 'react';
|
|
4
|
+
import { useMemo, useState } from 'react';
|
|
5
5
|
import type { TPaymentCurrency } from '@blocklet/payment-types';
|
|
6
6
|
import { useRequest } from 'ahooks';
|
|
7
|
+
import { useConditionalSection } from '../conditional-section';
|
|
7
8
|
|
|
8
9
|
enum CreditTab {
|
|
9
10
|
OVERVIEW = 'overview',
|
|
@@ -43,9 +44,30 @@ const fetchCreditSummary = async (customerId: string) => {
|
|
|
43
44
|
export default function CreditOverview({ customerId, settings, mode = 'portal' }: CreditOverviewProps) {
|
|
44
45
|
const { t } = useLocaleContext();
|
|
45
46
|
const [creditTab, setCreditTab] = useState<CreditTab>(CreditTab.OVERVIEW);
|
|
47
|
+
const conditionalSection = useConditionalSection();
|
|
48
|
+
|
|
49
|
+
const creditCurrencies = useMemo(() => {
|
|
50
|
+
return (
|
|
51
|
+
settings?.paymentMethods
|
|
52
|
+
?.filter((method: any) => method.type === 'arcblock')
|
|
53
|
+
?.flatMap((method: any) => method.payment_currencies)
|
|
54
|
+
?.filter((currency: TPaymentCurrency) => {
|
|
55
|
+
return currency.type === 'credit';
|
|
56
|
+
}) || []
|
|
57
|
+
);
|
|
58
|
+
}, [settings]);
|
|
59
|
+
|
|
46
60
|
const { data: creditSummary } = useRequest(fetchCreditSummary, {
|
|
47
61
|
defaultParams: [customerId],
|
|
48
62
|
refreshDeps: [creditTab === CreditTab.OVERVIEW],
|
|
63
|
+
onSuccess: (data) => {
|
|
64
|
+
if (creditTab === CreditTab.OVERVIEW) {
|
|
65
|
+
const filteredCurrencies = creditCurrencies.filter((currency: TPaymentCurrency) => {
|
|
66
|
+
return data.grants?.[currency.id];
|
|
67
|
+
});
|
|
68
|
+
conditionalSection?.hideRender(filteredCurrencies.length === 0);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
49
71
|
});
|
|
50
72
|
|
|
51
73
|
// 渲染信用概览卡片
|
|
@@ -141,20 +163,11 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
141
163
|
);
|
|
142
164
|
};
|
|
143
165
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
?.filter((currency: TPaymentCurrency) => {
|
|
150
|
-
const currencyId = currency.id as string;
|
|
151
|
-
const grantData = creditSummary?.grants?.[currencyId];
|
|
152
|
-
return grantData;
|
|
153
|
-
}) || [];
|
|
154
|
-
|
|
155
|
-
if (creditCurrencies.length === 0) {
|
|
156
|
-
return null;
|
|
157
|
-
}
|
|
166
|
+
const filteredCreditCurrencies = useMemo(() => {
|
|
167
|
+
return creditCurrencies.filter((currency: TPaymentCurrency) => {
|
|
168
|
+
return creditSummary?.grants?.[currency.id];
|
|
169
|
+
});
|
|
170
|
+
}, [creditCurrencies, creditSummary?.grants]);
|
|
158
171
|
|
|
159
172
|
return (
|
|
160
173
|
<Stack sx={{ width: '100%' }}>
|
|
@@ -192,10 +205,10 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
192
205
|
gridTemplateColumns: 'repeat(1, 1fr)',
|
|
193
206
|
},
|
|
194
207
|
}}>
|
|
195
|
-
{
|
|
208
|
+
{filteredCreditCurrencies.map(renderCreditOverviewCard)}
|
|
196
209
|
</Box>
|
|
197
210
|
|
|
198
|
-
{
|
|
211
|
+
{filteredCreditCurrencies.length === 0 && (
|
|
199
212
|
<Box
|
|
200
213
|
sx={{
|
|
201
214
|
display: 'flex',
|
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
validatePhoneNumber,
|
|
9
9
|
getPhoneUtil,
|
|
10
10
|
validatePostalCode,
|
|
11
|
+
FormLabel,
|
|
11
12
|
} from '@blocklet/payment-react';
|
|
12
|
-
import {
|
|
13
|
+
import { Stack } from '@mui/material';
|
|
13
14
|
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
14
15
|
import isEmail from 'validator/es/lib/isEmail';
|
|
15
16
|
import { useMount } from 'ahooks';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
-
import { FormInput } from '@blocklet/payment-react';
|
|
2
|
+
import { FormInput, FormLabel } from '@blocklet/payment-react';
|
|
3
3
|
import { AddOutlined, Autorenew, DeleteOutlineOutlined, FormatAlignLeft } from '@mui/icons-material';
|
|
4
|
-
import { Box, Button, Divider, IconButton, Stack, TextField, InputAdornment, Tooltip
|
|
4
|
+
import { Box, Button, Divider, IconButton, Stack, TextField, InputAdornment, Tooltip } from '@mui/material';
|
|
5
5
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
6
6
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
|
7
7
|
import { isObject, debounce } from 'lodash';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
-
import { api, formatError, useMobile, getCustomerAvatar } from '@blocklet/payment-react';
|
|
2
|
+
import { api, formatError, useMobile, getCustomerAvatar, FormLabel } from '@blocklet/payment-react';
|
|
3
3
|
import type { TCustomer, TPaymentCurrency } from '@blocklet/payment-types';
|
|
4
|
-
import { Box, Stack, Autocomplete, TextField, Button, Avatar,
|
|
4
|
+
import { Box, Stack, Autocomplete, TextField, Button, Avatar, Typography } from '@mui/material';
|
|
5
5
|
import { useSetState } from 'ahooks';
|
|
6
6
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
7
7
|
import Dialog from '@arcblock/ux/lib/Dialog';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
-
import { FormInput } from '@blocklet/payment-react';
|
|
3
|
-
import { FormControl, Select, MenuItem, Typography, Box, FormHelperText, Stack
|
|
2
|
+
import { FormInput, FormLabel } from '@blocklet/payment-react';
|
|
3
|
+
import { FormControl, Select, MenuItem, Typography, Box, FormHelperText, Stack } from '@mui/material';
|
|
4
4
|
import { useFormContext, useWatch } from 'react-hook-form';
|
|
5
5
|
import { InfoOutlined } from '@mui/icons-material';
|
|
6
6
|
|
|
@@ -38,7 +38,7 @@ export default function MeterProducts({ meterId, meter = undefined }: MeterProdu
|
|
|
38
38
|
const [loading, setLoading] = useState(false);
|
|
39
39
|
const [creating, setCreating] = useState(false);
|
|
40
40
|
const [error, setError] = useState<string | null>(null);
|
|
41
|
-
const [activeTab, setActiveTab] = useState<ProductType>('
|
|
41
|
+
const [activeTab, setActiveTab] = useState<ProductType>('credit');
|
|
42
42
|
|
|
43
43
|
const loadProducts = async (type: ProductType = activeTab) => {
|
|
44
44
|
setLoading(true);
|
|
@@ -183,8 +183,8 @@ export default function MeterProducts({ meterId, meter = undefined }: MeterProdu
|
|
|
183
183
|
color: 'primary.main',
|
|
184
184
|
},
|
|
185
185
|
}}>
|
|
186
|
-
<Tab label={t('admin.meter.products.meterService')} value="meter" />
|
|
187
186
|
<Tab label={t('admin.meter.products.creditCharge')} value="credit" />
|
|
187
|
+
<Tab label={t('admin.meter.products.meterService')} value="meter" />
|
|
188
188
|
</Tabs>
|
|
189
189
|
</Stack>
|
|
190
190
|
<Stack
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
3
|
-
import { api, findCurrency, formatError, formatPrice, usePaymentContext } from '@blocklet/payment-react';
|
|
3
|
+
import { api, findCurrency, formatError, formatPrice, usePaymentContext, FormLabel } from '@blocklet/payment-react';
|
|
4
4
|
import type { TPrice, TProduct, TProductExpanded } from '@blocklet/payment-types';
|
|
5
|
-
import { Box, Checkbox, FormControlLabel,
|
|
5
|
+
import { Box, Checkbox, FormControlLabel, Stack, TextField, Typography } from '@mui/material';
|
|
6
6
|
import { useSetState } from 'ahooks';
|
|
7
7
|
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
8
8
|
|
|
@@ -10,6 +10,7 @@ import { Link } from 'react-router-dom';
|
|
|
10
10
|
import { styled } from '@mui/system';
|
|
11
11
|
import { debounce } from '../../../libs/util';
|
|
12
12
|
import CustomerLink from '../../customer/link';
|
|
13
|
+
import { useConditionalSection } from '../../conditional-section';
|
|
13
14
|
|
|
14
15
|
const fetchData = (
|
|
15
16
|
params: Record<string, any> = {}
|
|
@@ -47,7 +48,6 @@ type ListProps = {
|
|
|
47
48
|
status?: string;
|
|
48
49
|
customer_id?: string;
|
|
49
50
|
currency_id?: string;
|
|
50
|
-
setHasRevenues?: (hasRevenues: boolean) => void;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
const getListKey = (props: ListProps) => {
|
|
@@ -57,14 +57,10 @@ const getListKey = (props: ListProps) => {
|
|
|
57
57
|
return 'payouts-mine';
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
export default function CustomerRevenueList({
|
|
61
|
-
currency_id = '',
|
|
62
|
-
status = '',
|
|
63
|
-
customer_id = '',
|
|
64
|
-
setHasRevenues = () => {},
|
|
65
|
-
}: ListProps) {
|
|
60
|
+
export default function CustomerRevenueList({ currency_id = '', status = '', customer_id = '' }: ListProps) {
|
|
66
61
|
const { t } = useLocaleContext();
|
|
67
62
|
const { isMobile } = useMobile('sm');
|
|
63
|
+
const conditionalSection = useConditionalSection();
|
|
68
64
|
|
|
69
65
|
const listKey = getListKey({ customer_id });
|
|
70
66
|
const defaultPageSize = useDefaultPageSize(10);
|
|
@@ -87,12 +83,11 @@ export default function CustomerRevenueList({
|
|
|
87
83
|
debounce(() => {
|
|
88
84
|
fetchData(search).then((res: any) => {
|
|
89
85
|
setData(res);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
86
|
+
const hasData = res.count > 0;
|
|
87
|
+
conditionalSection?.hideRender(!hasData);
|
|
93
88
|
});
|
|
94
89
|
}, 300)();
|
|
95
|
-
}, [search]);
|
|
90
|
+
}, [search, conditionalSection]);
|
|
96
91
|
|
|
97
92
|
const columns = [
|
|
98
93
|
{
|
|
@@ -18,6 +18,7 @@ type Props = {
|
|
|
18
18
|
disabled?: boolean;
|
|
19
19
|
selectSX?: SxProps;
|
|
20
20
|
currencyFilter?: (currency: any) => boolean;
|
|
21
|
+
hideMethod?: boolean;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export default function CurrencySelect({
|
|
@@ -29,6 +30,7 @@ export default function CurrencySelect({
|
|
|
29
30
|
disabled = false,
|
|
30
31
|
selectSX = {},
|
|
31
32
|
currencyFilter = () => true,
|
|
33
|
+
hideMethod = false,
|
|
32
34
|
}: Props) {
|
|
33
35
|
const { t } = useLocaleContext();
|
|
34
36
|
const { settings } = usePaymentContext();
|
|
@@ -80,7 +82,7 @@ export default function CurrencySelect({
|
|
|
80
82
|
justifyContent: 'flex-end',
|
|
81
83
|
textAlign: 'right',
|
|
82
84
|
}}>
|
|
83
|
-
{selectedCurrency?.symbol} ({selectedPaymentMethod?.name})
|
|
85
|
+
{selectedCurrency?.symbol} {hideMethod ? '' : `(${selectedPaymentMethod?.name})`}
|
|
84
86
|
{canSelect && <ArrowDropDown sx={{ color: 'text.secondary', fontSize: 21 }} />}
|
|
85
87
|
</Typography>
|
|
86
88
|
);
|
|
@@ -97,7 +99,7 @@ export default function CurrencySelect({
|
|
|
97
99
|
value={value}
|
|
98
100
|
renderValue={() => (
|
|
99
101
|
<Typography variant="body1" sx={{ display: 'inline-flex', fontSize: '12px', color: 'text.secondary' }}>
|
|
100
|
-
{selectedCurrency?.symbol} ({selectedPaymentMethod?.name})
|
|
102
|
+
{selectedCurrency?.symbol} {hideMethod ? '' : `(${selectedPaymentMethod?.name})`}
|
|
101
103
|
</Typography>
|
|
102
104
|
)}
|
|
103
105
|
onChange={handleSelect}
|
|
@@ -113,18 +115,20 @@ export default function CurrencySelect({
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
return [
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
hideMethod ? null : (
|
|
119
|
+
<ListSubheader
|
|
120
|
+
key={method.id}
|
|
121
|
+
sx={{ fontSize: '0.875rem', color: 'text.secondary', lineHeight: '2.1875rem' }}>
|
|
122
|
+
{method.name}
|
|
123
|
+
</ListSubheader>
|
|
124
|
+
),
|
|
121
125
|
...filteredCurrencies.map((currency) => (
|
|
122
126
|
<MenuItem key={currency.id} sx={{ pl: 3 }} value={currency.id}>
|
|
123
127
|
<Stack direction="row" sx={{ width: '100%', justifyContent: 'space-between', gap: 2 }}>
|
|
124
|
-
<Currency logo={currency.logo} name={currency.name} />
|
|
128
|
+
{hideMethod ? null : <Currency logo={currency.logo} name={currency.name} />}
|
|
125
129
|
<Typography
|
|
126
130
|
sx={{
|
|
127
|
-
fontWeight: 'bold',
|
|
131
|
+
fontWeight: hideMethod ? 'normal' : 'bold',
|
|
128
132
|
}}>
|
|
129
133
|
{currency.symbol}
|
|
130
134
|
</Typography>
|
|
@@ -50,7 +50,6 @@ import ProductSelect from '../payment-link/product-select';
|
|
|
50
50
|
import Collapse from '../collapse';
|
|
51
51
|
import { useProductsContext } from '../../contexts/products';
|
|
52
52
|
import CurrencySelect from './currency-select';
|
|
53
|
-
import MetadataForm from '../metadata/form';
|
|
54
53
|
import { getProductByPriceId } from '../../libs/util';
|
|
55
54
|
import InfoCard from '../info-card';
|
|
56
55
|
|
|
@@ -225,8 +224,11 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
225
224
|
if (value !== null) {
|
|
226
225
|
setValue(field.name, value);
|
|
227
226
|
}
|
|
228
|
-
if (value === 'one_time'
|
|
227
|
+
if (value === 'one_time') {
|
|
229
228
|
setValue(getFieldName('model'), 'standard');
|
|
229
|
+
setValue(getFieldName('currency_id'), settings.baseCurrency?.id);
|
|
230
|
+
setValue(getFieldName('recurring.meter_id'), '');
|
|
231
|
+
setValue(getFieldName('recurring.usage_type'), 'licensed');
|
|
230
232
|
}
|
|
231
233
|
}}
|
|
232
234
|
exclusive
|
|
@@ -299,7 +301,7 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
299
301
|
fullWidth
|
|
300
302
|
size="small"
|
|
301
303
|
onChange={(e) => {
|
|
302
|
-
if (e.target.value === 'standard'
|
|
304
|
+
if (e.target.value === 'standard') {
|
|
303
305
|
setValue(getFieldName('currency_id'), settings.baseCurrency?.id);
|
|
304
306
|
}
|
|
305
307
|
field.onChange(e.target.value);
|
|
@@ -313,13 +315,15 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
313
315
|
}}>
|
|
314
316
|
<MenuItem value="standard">{t('admin.price.models.standard')}</MenuItem>
|
|
315
317
|
<MenuItem value="package">{t('admin.price.models.package')}</MenuItem>
|
|
318
|
+
<MenuItem value="credit_metered" disabled={isCreditMode}>
|
|
319
|
+
{t('admin.price.models.creditMetered')}
|
|
320
|
+
</MenuItem>
|
|
316
321
|
<MenuItem value="graduated" disabled>
|
|
317
322
|
{t('admin.price.models.graduated')}
|
|
318
323
|
</MenuItem>
|
|
319
324
|
<MenuItem value="volume" disabled>
|
|
320
325
|
{t('admin.price.models.volume')}
|
|
321
326
|
</MenuItem>
|
|
322
|
-
<MenuItem value="credit_metered">{t('admin.price.models.creditMetered')}</MenuItem>
|
|
323
327
|
</Select>
|
|
324
328
|
</Box>
|
|
325
329
|
)}
|
|
@@ -771,7 +775,7 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
771
775
|
}
|
|
772
776
|
expanded={isLocked}
|
|
773
777
|
style={{ width: INPUT_WIDTH }}>
|
|
774
|
-
<Box sx={{ width: INPUT_WIDTH, mb: 2 }}>
|
|
778
|
+
<Box sx={{ width: INPUT_WIDTH, mb: 2, pl: 2, pr: 1 }}>
|
|
775
779
|
{/* Credit 数量配置 */}
|
|
776
780
|
<Controller
|
|
777
781
|
name={getFieldName('metadata')}
|
|
@@ -815,6 +819,7 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
815
819
|
}}
|
|
816
820
|
value={field.value?.credit_config?.currency_id || creditCurrencies?.[0]?.id}
|
|
817
821
|
disabled={isLocked}
|
|
822
|
+
hideMethod
|
|
818
823
|
selectSX={{ '.MuiOutlinedInput-notchedOutline': { border: 'none' } }}
|
|
819
824
|
/>
|
|
820
825
|
</InputAdornment>
|
|
@@ -1045,16 +1050,17 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
1045
1050
|
},
|
|
1046
1051
|
}}>
|
|
1047
1052
|
<Stack
|
|
1048
|
-
spacing={2}
|
|
1049
1053
|
sx={{
|
|
1050
1054
|
alignItems: 'flex-start',
|
|
1051
1055
|
width: INPUT_WIDTH,
|
|
1056
|
+
pl: 2,
|
|
1057
|
+
pr: 1,
|
|
1052
1058
|
}}>
|
|
1053
1059
|
<Controller
|
|
1054
1060
|
name={getFieldName('quantity_available')}
|
|
1055
1061
|
control={control}
|
|
1056
1062
|
render={({ field }) => (
|
|
1057
|
-
|
|
1063
|
+
<Box sx={{ width: '100%', mb: 2 }}>
|
|
1058
1064
|
<FormLabel>{t('admin.price.quantityAvailable.label')}</FormLabel>
|
|
1059
1065
|
<TextField
|
|
1060
1066
|
{...field}
|
|
@@ -1065,14 +1071,23 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
1065
1071
|
error={!quantityPositive(field.value)}
|
|
1066
1072
|
helperText={!quantityPositive(field.value) && t('admin.price.quantity.tip')}
|
|
1067
1073
|
/>
|
|
1068
|
-
|
|
1074
|
+
<Typography
|
|
1075
|
+
variant="caption"
|
|
1076
|
+
sx={{
|
|
1077
|
+
color: 'text.secondary',
|
|
1078
|
+
display: 'block',
|
|
1079
|
+
mt: 0.5,
|
|
1080
|
+
}}>
|
|
1081
|
+
{t('admin.price.quantityAvailable.description')}
|
|
1082
|
+
</Typography>
|
|
1083
|
+
</Box>
|
|
1069
1084
|
)}
|
|
1070
1085
|
/>
|
|
1071
1086
|
<Controller
|
|
1072
1087
|
name={getFieldName('quantity_limit_per_checkout')}
|
|
1073
1088
|
control={control}
|
|
1074
1089
|
render={({ field }) => (
|
|
1075
|
-
|
|
1090
|
+
<Box sx={{ width: '100%', mb: 2 }}>
|
|
1076
1091
|
<FormLabel>{t('admin.price.quantityLimitPerCheckout.label')}</FormLabel>
|
|
1077
1092
|
<TextField
|
|
1078
1093
|
{...field}
|
|
@@ -1084,7 +1099,16 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
1084
1099
|
error={!quantityPositive(field.value)}
|
|
1085
1100
|
helperText={!quantityPositive(field.value) && t('admin.price.quantity.tip')}
|
|
1086
1101
|
/>
|
|
1087
|
-
|
|
1102
|
+
<Typography
|
|
1103
|
+
variant="caption"
|
|
1104
|
+
sx={{
|
|
1105
|
+
color: 'text.secondary',
|
|
1106
|
+
display: 'block',
|
|
1107
|
+
mt: 0.5,
|
|
1108
|
+
}}>
|
|
1109
|
+
{t('admin.price.quantityLimitPerCheckout.description')}
|
|
1110
|
+
</Typography>
|
|
1111
|
+
</Box>
|
|
1088
1112
|
)}
|
|
1089
1113
|
/>
|
|
1090
1114
|
<Controller
|
|
@@ -1097,10 +1121,10 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
1097
1121
|
},
|
|
1098
1122
|
}}
|
|
1099
1123
|
render={({ field }) => (
|
|
1100
|
-
|
|
1124
|
+
<Box sx={{ width: '100%', mb: 2 }}>
|
|
1101
1125
|
<FormLabel>{t('admin.price.nickname.label')}</FormLabel>
|
|
1102
1126
|
<TextField {...field} size="small" sx={{ width: INPUT_WIDTH }} inputProps={{ maxLength: 64 }} />
|
|
1103
|
-
|
|
1127
|
+
</Box>
|
|
1104
1128
|
)}
|
|
1105
1129
|
/>
|
|
1106
1130
|
<Controller
|
|
@@ -1113,14 +1137,21 @@ export default function PriceForm({ prefix = '', simple = false, productType = u
|
|
|
1113
1137
|
},
|
|
1114
1138
|
}}
|
|
1115
1139
|
render={({ field }) => (
|
|
1116
|
-
|
|
1140
|
+
<Box sx={{ width: '100%', mb: 2 }}>
|
|
1117
1141
|
<FormLabel>{t('admin.price.lookup_key.label')}</FormLabel>
|
|
1118
1142
|
<TextField {...field} size="small" sx={{ width: INPUT_WIDTH }} inputProps={{ maxLength: 64 }} />
|
|
1119
|
-
|
|
1143
|
+
<Typography
|
|
1144
|
+
variant="caption"
|
|
1145
|
+
sx={{
|
|
1146
|
+
color: 'text.secondary',
|
|
1147
|
+
display: 'block',
|
|
1148
|
+
mt: 0.5,
|
|
1149
|
+
}}>
|
|
1150
|
+
{t('admin.price.lookup_key.description')}
|
|
1151
|
+
</Typography>
|
|
1152
|
+
</Box>
|
|
1120
1153
|
)}
|
|
1121
1154
|
/>
|
|
1122
|
-
{/* 元数据 */}
|
|
1123
|
-
<MetadataForm title={t('common.metadata.label')} color="inherit" name={getFieldName('metadata')} />
|
|
1124
1155
|
</Stack>
|
|
1125
1156
|
</Collapse>
|
|
1126
1157
|
</>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/* eslint-disable no-nested-ternary */
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
-
import { FormInput } from '@blocklet/payment-react';
|
|
3
|
+
import { FormInput, FormLabel } from '@blocklet/payment-react';
|
|
4
4
|
import type { InferFormType, TProduct } from '@blocklet/payment-types';
|
|
5
|
-
import { Box, Stack, Typography,
|
|
5
|
+
import { Box, Stack, Typography, Select, MenuItem } from '@mui/material';
|
|
6
6
|
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
|
7
7
|
|
|
8
8
|
import Collapse from '../collapse';
|
|
@@ -46,7 +46,7 @@ export default function ProductForm({ simple = false }: Props) {
|
|
|
46
46
|
rules={{ required: true }}
|
|
47
47
|
render={({ field }) => (
|
|
48
48
|
<Box sx={{ width: '100%' }}>
|
|
49
|
-
<FormLabel sx={{ color: 'text.primary' }}>{t('admin.product.type.label')}</FormLabel>
|
|
49
|
+
<FormLabel sx={{ color: 'text.primary', fontSize: '0.875rem' }}>{t('admin.product.type.label')}</FormLabel>
|
|
50
50
|
<Select {...field} fullWidth size="small">
|
|
51
51
|
<MenuItem value="good">{t('admin.product.type.good')}</MenuItem>
|
|
52
52
|
<MenuItem value="service">{t('admin.product.type.service')}</MenuItem>
|
|
@@ -135,11 +135,6 @@ export default function ProductForm({ simple = false }: Props) {
|
|
|
135
135
|
<FormInput
|
|
136
136
|
name="unit_label"
|
|
137
137
|
label={t('admin.product.unit_label.label')}
|
|
138
|
-
placeholder={
|
|
139
|
-
productType === 'credit'
|
|
140
|
-
? t('admin.creditProduct.unitLabel.placeholder')
|
|
141
|
-
: t('admin.product.unit_label.placeholder')
|
|
142
|
-
}
|
|
143
138
|
rules={{
|
|
144
139
|
maxLength: { value: 12, message: t('common.maxLength', { len: 12 }) },
|
|
145
140
|
}}
|
package/src/locales/en.tsx
CHANGED
|
@@ -384,7 +384,7 @@ export default flat({
|
|
|
384
384
|
},
|
|
385
385
|
unit_label: {
|
|
386
386
|
label: 'Unit label',
|
|
387
|
-
placeholder: '
|
|
387
|
+
placeholder: 'Unit',
|
|
388
388
|
},
|
|
389
389
|
billingType: {
|
|
390
390
|
label: 'Billing type',
|
|
@@ -452,6 +452,7 @@ export default flat({
|
|
|
452
452
|
lookup_key: {
|
|
453
453
|
label: 'Lookup key',
|
|
454
454
|
placeholder: '',
|
|
455
|
+
description: 'Lookup key is used to identify the price in the API',
|
|
455
456
|
},
|
|
456
457
|
recurring: {
|
|
457
458
|
interval: 'Billing period',
|
|
@@ -526,6 +527,7 @@ export default flat({
|
|
|
526
527
|
format: 'Available {num} pieces',
|
|
527
528
|
noLimit: 'No limit on available quantity',
|
|
528
529
|
valid: 'Available quantity must be greater than or equal to sold quantity',
|
|
530
|
+
description: 'Enter the number of units that can be sold, 0 means unlimited',
|
|
529
531
|
},
|
|
530
532
|
quantitySold: {
|
|
531
533
|
label: 'Sold quantity',
|
|
@@ -536,6 +538,7 @@ export default flat({
|
|
|
536
538
|
placeholder: '0 means unlimited',
|
|
537
539
|
format: 'Limit {num} pieces per checkout',
|
|
538
540
|
noLimit: 'No limit on quantity per checkout',
|
|
541
|
+
description: 'Enter the number of units that can be purchased in a single checkout, 0 means unlimited',
|
|
539
542
|
},
|
|
540
543
|
inventory: 'Inventory Settings',
|
|
541
544
|
},
|
|
@@ -987,7 +990,7 @@ export default flat({
|
|
|
987
990
|
viewAllActivity: 'View All Activity',
|
|
988
991
|
pendingAmount: 'Outstanding Charges',
|
|
989
992
|
grantCount: 'Grant Count',
|
|
990
|
-
noGrantsDescription: "You don't have any credit grants yet.
|
|
993
|
+
noGrantsDescription: "You don't have any credit grants yet.",
|
|
991
994
|
addCredit: 'Add Credit',
|
|
992
995
|
},
|
|
993
996
|
},
|
|
@@ -1162,7 +1165,7 @@ export default flat({
|
|
|
1162
1165
|
totalAmount: 'Total Amount',
|
|
1163
1166
|
pendingAmount: 'Pending Amount',
|
|
1164
1167
|
grantCount: 'Grant Count',
|
|
1165
|
-
noGrantsDescription: "You don't have any credit grants yet.
|
|
1168
|
+
noGrantsDescription: "You don't have any credit grants yet.",
|
|
1166
1169
|
status: {
|
|
1167
1170
|
granted: 'Active',
|
|
1168
1171
|
pending: 'Pending',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -361,7 +361,7 @@ export default flat({
|
|
|
361
361
|
},
|
|
362
362
|
unit_label: {
|
|
363
363
|
label: '单位标签',
|
|
364
|
-
placeholder: '
|
|
364
|
+
placeholder: '单位',
|
|
365
365
|
},
|
|
366
366
|
billingType: {
|
|
367
367
|
label: '计费类型',
|
|
@@ -424,6 +424,7 @@ export default flat({
|
|
|
424
424
|
lookup_key: {
|
|
425
425
|
label: '查找键',
|
|
426
426
|
placeholder: '',
|
|
427
|
+
description: '查找键用于在API中识别价格',
|
|
427
428
|
},
|
|
428
429
|
recurring: {
|
|
429
430
|
interval: '计费周期',
|
|
@@ -495,6 +496,7 @@ export default flat({
|
|
|
495
496
|
format: '可售{num}件',
|
|
496
497
|
noLimit: '不限制可售数量',
|
|
497
498
|
valid: '可售数量不得少于已售数量',
|
|
499
|
+
description: '输入可售数量,0表示不限制可售数量',
|
|
498
500
|
},
|
|
499
501
|
quantitySold: {
|
|
500
502
|
label: '已售数量',
|
|
@@ -505,6 +507,7 @@ export default flat({
|
|
|
505
507
|
placeholder: '0表示无限制',
|
|
506
508
|
format: '单次最多购买{num}件',
|
|
507
509
|
noLimit: '不限制单次购买数量',
|
|
510
|
+
description: '输入限制单次购买的最大数量, 0表示无限制',
|
|
508
511
|
},
|
|
509
512
|
inventory: '库存设置',
|
|
510
513
|
},
|
|
@@ -945,7 +948,7 @@ export default flat({
|
|
|
945
948
|
totalAmount: '总额度',
|
|
946
949
|
pendingAmount: '欠费额度',
|
|
947
950
|
grantCount: '额度数量',
|
|
948
|
-
noGrantsDescription: '
|
|
951
|
+
noGrantsDescription: '您还没有任何信用额度',
|
|
949
952
|
status: {
|
|
950
953
|
granted: '生效中',
|
|
951
954
|
pending: '待生效',
|
|
@@ -1121,7 +1124,7 @@ export default flat({
|
|
|
1121
1124
|
totalAmount: '总额度',
|
|
1122
1125
|
pendingAmount: '欠费额度',
|
|
1123
1126
|
grantCount: '额度数量',
|
|
1124
|
-
noGrantsDescription: '
|
|
1127
|
+
noGrantsDescription: '您还没有任何信用额度',
|
|
1125
1128
|
status: {
|
|
1126
1129
|
granted: '生效中',
|
|
1127
1130
|
pending: '待生效',
|
|
@@ -42,10 +42,6 @@ const fetchData = async (
|
|
|
42
42
|
): Promise<{
|
|
43
43
|
customer: TCustomerExpanded;
|
|
44
44
|
summary: { [key: string]: GroupedBN };
|
|
45
|
-
creditSummary?: {
|
|
46
|
-
grants?: { [currencyId: string]: { totalAmount: string; remainingAmount: string } };
|
|
47
|
-
pendingAmount?: { [currencyId: string]: string };
|
|
48
|
-
} | null;
|
|
49
45
|
}> => {
|
|
50
46
|
const [customer, summary] = await Promise.all([
|
|
51
47
|
api.get(`/api/customers/${id}`).then((res) => res.data),
|
|
@@ -311,15 +307,11 @@ export default function CustomerDetail(props: { id: string }) {
|
|
|
311
307
|
}}>
|
|
312
308
|
<Box className="payment-link-column-1" sx={{ flex: 1, gap: 2.5, display: 'flex', flexDirection: 'column' }}>
|
|
313
309
|
{/* 信用管理区域 */}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
</Box>
|
|
320
|
-
<Divider />
|
|
321
|
-
</>
|
|
322
|
-
)}
|
|
310
|
+
<Box className="section">
|
|
311
|
+
<SectionHeader title={t('admin.creditGrants.title')} mb={0} />
|
|
312
|
+
<CreditOverview customerId={props.id} settings={settings} mode="dashboard" />
|
|
313
|
+
</Box>
|
|
314
|
+
<Divider />
|
|
323
315
|
|
|
324
316
|
<Box className="section" sx={{ containerType: 'inline-size' }}>
|
|
325
317
|
<SectionHeader title={t('admin.details')} />
|
|
@@ -40,6 +40,7 @@ import { memo, useEffect, useState } from 'react';
|
|
|
40
40
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
41
41
|
import { joinURL } from 'ufo';
|
|
42
42
|
import CreditOverview from '../../components/customer/credit-overview';
|
|
43
|
+
import ConditionalSection from '../../components/conditional-section';
|
|
43
44
|
|
|
44
45
|
import { useTransitionContext } from '../../components/progress-bar';
|
|
45
46
|
import CurrentSubscriptions from '../../components/subscription/portal/list';
|
|
@@ -214,7 +215,6 @@ export default function CustomerHome() {
|
|
|
214
215
|
const navigate = useNavigate();
|
|
215
216
|
const [subscriptionStatus, setSubscriptionStatus] = useState(false);
|
|
216
217
|
const [hasSubscriptions, setHasSubscriptions] = useState(false);
|
|
217
|
-
const [hasRevenues, setHasRevenues] = useState(false);
|
|
218
218
|
const { startTransition } = useTransitionContext();
|
|
219
219
|
const {
|
|
220
220
|
data,
|
|
@@ -480,15 +480,15 @@ export default function CustomerHome() {
|
|
|
480
480
|
);
|
|
481
481
|
|
|
482
482
|
// 独立的Credit Card组件
|
|
483
|
-
const CreditCard =
|
|
484
|
-
<
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
483
|
+
const CreditCard = (
|
|
484
|
+
<ConditionalSection skeleton={loadingCard}>
|
|
485
|
+
<Box className="base-card section section-credit">
|
|
486
|
+
<Box className="section-header" sx={{ mb: 2 }}>
|
|
487
|
+
<Typography variant="h3">{t('admin.creditGrants.title')}</Typography>
|
|
488
|
+
</Box>
|
|
489
|
+
<CreditOverview customerId={data?.id || ''} settings={settings} />
|
|
489
490
|
</Box>
|
|
490
|
-
|
|
491
|
-
</Box>
|
|
491
|
+
</ConditionalSection>
|
|
492
492
|
);
|
|
493
493
|
|
|
494
494
|
const InvoiceCard = loadingCard ? (
|
|
@@ -519,13 +519,15 @@ export default function CustomerHome() {
|
|
|
519
519
|
</Box>
|
|
520
520
|
);
|
|
521
521
|
|
|
522
|
-
const RevenueCard =
|
|
523
|
-
<
|
|
524
|
-
<Box className="section-
|
|
525
|
-
<
|
|
522
|
+
const RevenueCard = (
|
|
523
|
+
<ConditionalSection skeleton={loadingCard}>
|
|
524
|
+
<Box className="base-card section section-revenue">
|
|
525
|
+
<Box className="section-header">
|
|
526
|
+
<Typography variant="h3">{t('customer.payout.title')}</Typography>
|
|
527
|
+
</Box>
|
|
528
|
+
<CustomerRevenueList />
|
|
526
529
|
</Box>
|
|
527
|
-
|
|
528
|
-
</Box>
|
|
530
|
+
</ConditionalSection>
|
|
529
531
|
);
|
|
530
532
|
|
|
531
533
|
return (
|