payment-kit 1.20.13 → 1.20.14
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/routes/checkout-sessions.ts +15 -2
- package/api/src/routes/coupons.ts +7 -0
- package/api/src/routes/credit-grants.ts +8 -1
- package/api/src/routes/credit-transactions.ts +153 -13
- package/api/src/routes/invoices.ts +35 -1
- package/api/src/routes/meter-events.ts +31 -3
- package/api/src/routes/meters.ts +4 -0
- package/api/src/routes/payment-currencies.ts +2 -1
- package/api/src/routes/promotion-codes.ts +2 -2
- package/api/src/routes/subscription-items.ts +4 -0
- package/api/src/routes/webhook-endpoints.ts +4 -0
- package/api/src/store/migrations/20250919-add-source-data.ts +20 -0
- package/api/src/store/models/credit-transaction.ts +5 -0
- package/api/src/store/models/meter-event.ts +22 -12
- package/api/src/store/models/types.ts +18 -0
- package/blocklet.yml +1 -1
- package/package.json +5 -5
- package/src/components/customer/credit-overview.tsx +1 -1
- package/src/components/customer/related-credit-grants.tsx +194 -0
- package/src/components/meter/add-usage-dialog.tsx +8 -0
- package/src/components/meter/events-list.tsx +93 -96
- package/src/components/product/form.tsx +0 -1
- package/src/locales/en.tsx +9 -0
- package/src/locales/zh.tsx +9 -0
- package/src/pages/admin/billing/invoices/detail.tsx +21 -2
- package/src/pages/customer/invoice/detail.tsx +11 -2
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import { formatBNStr, formatToDate, Table, usePaymentContext } from '@blocklet/payment-react';
|
|
4
|
+
import type { TCreditGrantExpanded } from '@blocklet/payment-types';
|
|
5
|
+
import { Box, Chip, Divider, styled, Typography } from '@mui/material';
|
|
6
|
+
import { useNavigate } from 'react-router-dom';
|
|
7
|
+
|
|
8
|
+
interface RelatedCreditGrantsProps {
|
|
9
|
+
grants: TCreditGrantExpanded[];
|
|
10
|
+
showDivider?: boolean;
|
|
11
|
+
mode?: 'dashboard' | 'portal';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function StatusChip({ status, label = '' }: { status: string; label?: string }) {
|
|
15
|
+
const getStatusColor = (statusValue: string) => {
|
|
16
|
+
switch (statusValue) {
|
|
17
|
+
case 'granted':
|
|
18
|
+
return 'success';
|
|
19
|
+
case 'pending':
|
|
20
|
+
return 'warning';
|
|
21
|
+
case 'expired':
|
|
22
|
+
return 'default';
|
|
23
|
+
case 'depleted':
|
|
24
|
+
return 'default';
|
|
25
|
+
case 'voided':
|
|
26
|
+
return 'default';
|
|
27
|
+
default:
|
|
28
|
+
return 'default';
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return <Chip label={label || status} size="small" color={getStatusColor(status) as any} />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function RelatedCreditGrants({ grants, showDivider = true, mode = 'portal' }: RelatedCreditGrantsProps) {
|
|
36
|
+
const { t, locale } = useLocaleContext();
|
|
37
|
+
const { session } = usePaymentContext();
|
|
38
|
+
const navigate = useNavigate();
|
|
39
|
+
|
|
40
|
+
const isAdmin = ['owner', 'admin'].includes(session?.user?.role || '');
|
|
41
|
+
|
|
42
|
+
const inDashboard = mode === 'dashboard' && isAdmin;
|
|
43
|
+
|
|
44
|
+
if (!grants?.length) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleShowGrantDetail = (grant: TCreditGrantExpanded) => {
|
|
49
|
+
let path = `/customer/credit-grant/${grant.id}`;
|
|
50
|
+
if (inDashboard) {
|
|
51
|
+
path = `/admin/customers/${grant.id}`;
|
|
52
|
+
}
|
|
53
|
+
navigate(path);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const columns = [
|
|
57
|
+
{
|
|
58
|
+
label: t('common.name'),
|
|
59
|
+
name: 'name',
|
|
60
|
+
options: {
|
|
61
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
62
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
63
|
+
return <Box onClick={() => handleShowGrantDetail(grant)}>{grant.name || grant.id}</Box>;
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
label: t('common.status'),
|
|
69
|
+
name: 'status',
|
|
70
|
+
options: {
|
|
71
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
72
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
73
|
+
return (
|
|
74
|
+
<Box onClick={() => handleShowGrantDetail(grant)}>
|
|
75
|
+
<StatusChip status={grant.status} label={t(`admin.customer.creditGrants.status.${grant.status}`)} />
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: t('common.remainingCredit'),
|
|
83
|
+
name: 'remaining_amount',
|
|
84
|
+
align: 'right',
|
|
85
|
+
options: {
|
|
86
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
87
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
88
|
+
return (
|
|
89
|
+
<Box onClick={() => handleShowGrantDetail(grant)}>
|
|
90
|
+
<Typography variant="body2">
|
|
91
|
+
{formatBNStr(grant.remaining_amount, grant.paymentCurrency?.decimal || 0)}{' '}
|
|
92
|
+
{grant.paymentCurrency?.symbol}
|
|
93
|
+
</Typography>
|
|
94
|
+
</Box>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: t('common.scope'),
|
|
101
|
+
name: 'scope',
|
|
102
|
+
options: {
|
|
103
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
104
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
105
|
+
let scope = 'general';
|
|
106
|
+
if (grant.applicability_config?.scope?.prices) {
|
|
107
|
+
scope = 'specific';
|
|
108
|
+
}
|
|
109
|
+
return (
|
|
110
|
+
<Box onClick={() => handleShowGrantDetail(grant)}>
|
|
111
|
+
{scope === 'specific' ? t('common.specific') : t('common.general')}
|
|
112
|
+
</Box>
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
label: t('common.effectiveDate'),
|
|
119
|
+
name: 'effective_at',
|
|
120
|
+
options: {
|
|
121
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
122
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
123
|
+
const effectiveAt = grant.effective_at ? grant.effective_at * 1000 : grant.created_at;
|
|
124
|
+
return (
|
|
125
|
+
<Box onClick={() => handleShowGrantDetail(grant)}>
|
|
126
|
+
{formatToDate(effectiveAt, locale, 'YYYY-MM-DD HH:mm')}
|
|
127
|
+
</Box>
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
label: t('common.expirationDate'),
|
|
134
|
+
name: 'expires_at',
|
|
135
|
+
options: {
|
|
136
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
137
|
+
const grant = grants[index] as TCreditGrantExpanded;
|
|
138
|
+
return (
|
|
139
|
+
<Box onClick={() => handleShowGrantDetail(grant)}>
|
|
140
|
+
<Typography variant="body2">
|
|
141
|
+
{grant.expires_at ? formatToDate(grant.expires_at * 1000, locale, 'YYYY-MM-DD HH:mm') : '-'}
|
|
142
|
+
</Typography>
|
|
143
|
+
</Box>
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
{showDivider && <Divider />}
|
|
153
|
+
<TableRoot className="section">
|
|
154
|
+
<Typography
|
|
155
|
+
variant="h3"
|
|
156
|
+
className="section-header"
|
|
157
|
+
sx={{
|
|
158
|
+
mb: 2,
|
|
159
|
+
}}>
|
|
160
|
+
{t('admin.customer.creditGrants.relatedGrants')}
|
|
161
|
+
</Typography>
|
|
162
|
+
<Table
|
|
163
|
+
data={grants}
|
|
164
|
+
columns={columns}
|
|
165
|
+
options={{
|
|
166
|
+
count: grants.length,
|
|
167
|
+
page: 0,
|
|
168
|
+
rowsPerPage: grants.length,
|
|
169
|
+
pagination: false,
|
|
170
|
+
}}
|
|
171
|
+
loading={false}
|
|
172
|
+
toolbar={false}
|
|
173
|
+
footer={false}
|
|
174
|
+
emptyNodeText={t('admin.customer.creditGrants.noGrants')}
|
|
175
|
+
/>
|
|
176
|
+
</TableRoot>
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const TableRoot = styled(Box)`
|
|
182
|
+
@media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
|
|
183
|
+
.MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
|
|
184
|
+
> div {
|
|
185
|
+
width: fit-content;
|
|
186
|
+
flex: inherit;
|
|
187
|
+
font-size: 14px;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
.invoice-summary {
|
|
191
|
+
padding-right: 20px;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
`;
|
|
@@ -41,6 +41,14 @@ const addMeterEvent = (data: any): Promise<any> => {
|
|
|
41
41
|
source: 'manual',
|
|
42
42
|
created_by: 'admin',
|
|
43
43
|
},
|
|
44
|
+
source_data: [
|
|
45
|
+
{
|
|
46
|
+
key: 'origin',
|
|
47
|
+
label: 'created_by',
|
|
48
|
+
value: 'Admin',
|
|
49
|
+
type: 'text',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
44
52
|
})
|
|
45
53
|
.then((res) => res.data);
|
|
46
54
|
};
|
|
@@ -5,7 +5,6 @@ import type { TCustomer, TPaymentCurrency, TMeterEventExpanded } from '@blocklet
|
|
|
5
5
|
import {
|
|
6
6
|
Box,
|
|
7
7
|
Typography,
|
|
8
|
-
Alert,
|
|
9
8
|
Stack,
|
|
10
9
|
Card,
|
|
11
10
|
Autocomplete,
|
|
@@ -19,7 +18,7 @@ import {
|
|
|
19
18
|
} from '@mui/material';
|
|
20
19
|
import { CalendarTodayOutlined, Add } from '@mui/icons-material';
|
|
21
20
|
import { useEffect, useMemo } from 'react';
|
|
22
|
-
import { useSetState } from 'ahooks';
|
|
21
|
+
import { useSetState, useLocalStorageState, useRequest } from 'ahooks';
|
|
23
22
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
|
24
23
|
import { fromUnitToToken } from '@ocap/util';
|
|
25
24
|
import { Link } from 'react-router-dom';
|
|
@@ -34,6 +33,14 @@ interface MeterEventsListProps {
|
|
|
34
33
|
paymentCurrency: TPaymentCurrency;
|
|
35
34
|
}
|
|
36
35
|
|
|
36
|
+
type SearchProps = {
|
|
37
|
+
pageSize: number;
|
|
38
|
+
page: number;
|
|
39
|
+
customer_id?: string;
|
|
40
|
+
start?: number;
|
|
41
|
+
end?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
37
44
|
interface TMeterEventStats {
|
|
38
45
|
date: string;
|
|
39
46
|
event_count: number;
|
|
@@ -42,22 +49,9 @@ interface TMeterEventStats {
|
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
interface EventsState {
|
|
45
|
-
events: TMeterEventExpanded[];
|
|
46
52
|
stats: TMeterEventStats[];
|
|
47
53
|
customers: TCustomer[];
|
|
48
|
-
loading: boolean;
|
|
49
54
|
statsLoading: boolean;
|
|
50
|
-
error: string | null;
|
|
51
|
-
pagination: {
|
|
52
|
-
page: number;
|
|
53
|
-
limit: number;
|
|
54
|
-
total: number;
|
|
55
|
-
};
|
|
56
|
-
filters: {
|
|
57
|
-
customer_id?: string;
|
|
58
|
-
start?: string;
|
|
59
|
-
end?: string;
|
|
60
|
-
};
|
|
61
55
|
anchorEl: any;
|
|
62
56
|
startDate: Date;
|
|
63
57
|
endDate: Date;
|
|
@@ -66,14 +60,30 @@ interface EventsState {
|
|
|
66
60
|
addUsageDialog: boolean;
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
const fetchEvents = (
|
|
63
|
+
const fetchEvents = (
|
|
64
|
+
meterId: string,
|
|
65
|
+
params: SearchProps = {} as SearchProps
|
|
66
|
+
): Promise<{ list: TMeterEventExpanded[]; count: number }> => {
|
|
70
67
|
const searchParams = new URLSearchParams();
|
|
71
68
|
searchParams.append('meter_id', meterId);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
69
|
+
|
|
70
|
+
// 确保 start 和 end 参数被正确添加
|
|
71
|
+
if (params.page !== undefined) {
|
|
72
|
+
searchParams.append('page', String(params.page));
|
|
73
|
+
}
|
|
74
|
+
if (params.pageSize !== undefined) {
|
|
75
|
+
searchParams.append('pageSize', String(params.pageSize));
|
|
76
|
+
}
|
|
77
|
+
if (params.start !== undefined) {
|
|
78
|
+
searchParams.append('start', String(params.start));
|
|
79
|
+
}
|
|
80
|
+
if (params.end !== undefined) {
|
|
81
|
+
searchParams.append('end', String(params.end));
|
|
82
|
+
}
|
|
83
|
+
if (params.customer_id) {
|
|
84
|
+
searchParams.append('customer_id', params.customer_id);
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
return api.get(`/api/meter-events?${searchParams.toString()}`).then((res: any) => res.data);
|
|
78
88
|
};
|
|
79
89
|
|
|
@@ -143,19 +153,31 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
143
153
|
const { isMobile } = useMobile('md');
|
|
144
154
|
const { livemode } = usePaymentContext();
|
|
145
155
|
const maxDate = dayjs().endOf('day').toDate();
|
|
156
|
+
|
|
157
|
+
const [search, setSearch] = useLocalStorageState<SearchProps>(`meter-events-${meterId}`, {
|
|
158
|
+
defaultValue: {
|
|
159
|
+
pageSize: 10,
|
|
160
|
+
page: 1,
|
|
161
|
+
start: dayjs().subtract(30, 'day').startOf('day').unix(),
|
|
162
|
+
end: dayjs().endOf('day').unix(),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const {
|
|
167
|
+
data = {
|
|
168
|
+
list: [],
|
|
169
|
+
count: 0,
|
|
170
|
+
},
|
|
171
|
+
refresh,
|
|
172
|
+
loading,
|
|
173
|
+
} = useRequest(() => fetchEvents(meterId, search), {
|
|
174
|
+
refreshDeps: [search],
|
|
175
|
+
});
|
|
176
|
+
|
|
146
177
|
const [state, setState] = useSetState<EventsState>({
|
|
147
|
-
events: [],
|
|
148
178
|
stats: [],
|
|
149
179
|
customers: [],
|
|
150
|
-
loading: false,
|
|
151
180
|
statsLoading: false,
|
|
152
|
-
error: null,
|
|
153
|
-
pagination: {
|
|
154
|
-
page: 0,
|
|
155
|
-
limit: 10,
|
|
156
|
-
total: 0,
|
|
157
|
-
},
|
|
158
|
-
filters: {},
|
|
159
181
|
anchorEl: null,
|
|
160
182
|
startDate: dayjs().subtract(30, 'day').startOf('day').toDate(),
|
|
161
183
|
endDate: maxDate,
|
|
@@ -209,35 +231,6 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
209
231
|
return emptyData.map((empty) => dataMap.get(empty.date) || empty);
|
|
210
232
|
}, [state.stats, state.statsLoading, state.startDate, state.endDate, granularity]);
|
|
211
233
|
|
|
212
|
-
const loadEvents = async (page = 0, limit = 10) => {
|
|
213
|
-
setState({ loading: true, error: null });
|
|
214
|
-
try {
|
|
215
|
-
const params: any = {
|
|
216
|
-
page,
|
|
217
|
-
limit,
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
if (state.startDate && state.endDate) {
|
|
221
|
-
params.start = dayjs(state.startDate).unix();
|
|
222
|
-
params.end = dayjs(state.endDate).unix();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (state.selectedCustomer) {
|
|
226
|
-
params.customer_id = state.selectedCustomer.id;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const result = await fetchEvents(meterId, params);
|
|
230
|
-
setState({
|
|
231
|
-
events: result.list,
|
|
232
|
-
pagination: { page, limit, total: result.count },
|
|
233
|
-
loading: false,
|
|
234
|
-
});
|
|
235
|
-
} catch (err) {
|
|
236
|
-
console.error('Failed to fetch events:', err);
|
|
237
|
-
setState({ error: 'Failed to load events', loading: false });
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
|
|
241
234
|
const loadStats = async () => {
|
|
242
235
|
setState({ statsLoading: true });
|
|
243
236
|
try {
|
|
@@ -259,28 +252,43 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
259
252
|
try {
|
|
260
253
|
const result = await fetchCustomers();
|
|
261
254
|
setState({ customers: result.list });
|
|
255
|
+
|
|
256
|
+
// Initialize selected customer if exists in search
|
|
257
|
+
if (search!.customer_id && result.list.length > 0) {
|
|
258
|
+
const selectedCustomer = result.list.find((c) => c.id === search!.customer_id);
|
|
259
|
+
if (selectedCustomer) {
|
|
260
|
+
setState({ selectedCustomer });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
262
263
|
} catch (err) {
|
|
263
264
|
console.error('Failed to fetch customers:', err);
|
|
264
265
|
}
|
|
265
266
|
};
|
|
266
267
|
|
|
267
268
|
useEffect(() => {
|
|
268
|
-
loadEvents();
|
|
269
|
-
loadStats();
|
|
270
269
|
loadCustomers();
|
|
271
270
|
}, [meterId]);
|
|
272
271
|
|
|
272
|
+
// Initialize state dates from search params
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (search?.start && search?.end) {
|
|
275
|
+
setState({
|
|
276
|
+
startDate: dayjs.unix(search.start).toDate(),
|
|
277
|
+
endDate: dayjs.unix(search.end).toDate(),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}, [search?.start, search?.end]);
|
|
281
|
+
|
|
273
282
|
useEffect(() => {
|
|
274
|
-
loadEvents(0, state.pagination.limit);
|
|
275
283
|
loadStats();
|
|
276
284
|
}, [state.startDate, state.endDate, state.selectedCustomer, granularity]);
|
|
277
285
|
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
286
|
+
const onTableChange = ({ page, rowsPerPage }: any) => {
|
|
287
|
+
if (search!.pageSize !== rowsPerPage) {
|
|
288
|
+
setSearch((x) => ({ ...x!, pageSize: rowsPerPage, page: 1 }));
|
|
289
|
+
} else if (search!.page !== page + 1) {
|
|
290
|
+
setSearch((x) => ({ ...x!, page: page + 1 }));
|
|
291
|
+
}
|
|
284
292
|
};
|
|
285
293
|
|
|
286
294
|
const onTogglePicker = (e: any) => {
|
|
@@ -292,6 +300,10 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
292
300
|
};
|
|
293
301
|
|
|
294
302
|
const onRangeChange = (range: any) => {
|
|
303
|
+
const newStart = dayjs(range.startDate).startOf('day').unix();
|
|
304
|
+
const newEnd = dayjs(range.endDate).endOf('day').unix();
|
|
305
|
+
|
|
306
|
+
setSearch((x) => ({ ...x!, start: newStart, end: newEnd, page: 1 }));
|
|
295
307
|
setState({
|
|
296
308
|
startDate: range.startDate,
|
|
297
309
|
endDate: dayjs(range.endDate).endOf('day').toDate(),
|
|
@@ -306,6 +318,7 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
306
318
|
};
|
|
307
319
|
|
|
308
320
|
const handleCustomerChange = (customer: TCustomer | null) => {
|
|
321
|
+
setSearch((x) => ({ ...x!, customer_id: customer?.id, page: 1 }));
|
|
309
322
|
setState({ selectedCustomer: customer });
|
|
310
323
|
};
|
|
311
324
|
|
|
@@ -322,18 +335,10 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
322
335
|
};
|
|
323
336
|
|
|
324
337
|
const handleAddUsageSuccess = () => {
|
|
325
|
-
|
|
338
|
+
refresh();
|
|
326
339
|
loadStats();
|
|
327
340
|
};
|
|
328
341
|
|
|
329
|
-
if (state.error) {
|
|
330
|
-
return (
|
|
331
|
-
<Alert severity="error" sx={{ mt: 1 }}>
|
|
332
|
-
{state.error}
|
|
333
|
-
</Alert>
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
342
|
const open = Boolean(state.anchorEl);
|
|
338
343
|
const id = open ? 'date-range-picker-popover' : undefined;
|
|
339
344
|
|
|
@@ -345,7 +350,7 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
345
350
|
filter: false,
|
|
346
351
|
sort: false,
|
|
347
352
|
customBodyRenderLite: (_: string, index: number) => {
|
|
348
|
-
const item =
|
|
353
|
+
const item = data.list[index];
|
|
349
354
|
if (!item) return null;
|
|
350
355
|
const value = fromUnitToToken(item.payload.value || '0', paymentCurrency.decimal);
|
|
351
356
|
return <Link to={`/admin/billing/${item.id}`}>{`${value} ${paymentCurrency.symbol}`}</Link>;
|
|
@@ -359,7 +364,7 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
359
364
|
filter: false,
|
|
360
365
|
sort: false,
|
|
361
366
|
customBodyRenderLite: (_: string, index: number) => {
|
|
362
|
-
const item =
|
|
367
|
+
const item = data.list[index];
|
|
363
368
|
if (!item || !item?.customer) return '-';
|
|
364
369
|
return <CustomerLink customer={item.customer} size="small" />;
|
|
365
370
|
},
|
|
@@ -372,7 +377,7 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
372
377
|
filter: false,
|
|
373
378
|
sort: false,
|
|
374
379
|
customBodyRenderLite: (_: string, index: number) => {
|
|
375
|
-
const item =
|
|
380
|
+
const item = data.list[index];
|
|
376
381
|
if (!item) return '-';
|
|
377
382
|
const subscriptionId = (item as any).payload?.subscription_id;
|
|
378
383
|
if (!subscriptionId) return '-';
|
|
@@ -393,7 +398,7 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
393
398
|
filter: false,
|
|
394
399
|
sort: false,
|
|
395
400
|
customBodyRenderLite: (_: string, index: number) => {
|
|
396
|
-
const item =
|
|
401
|
+
const item = data.list[index];
|
|
397
402
|
return <Link to={`/admin/billing/${item?.id}`}>{item ? formatTime(item.created_at) : '-'}</Link>;
|
|
398
403
|
},
|
|
399
404
|
},
|
|
@@ -610,28 +615,20 @@ export default function MeterEventsList({ meterId, paymentCurrency }: MeterEvent
|
|
|
610
615
|
sx={{
|
|
611
616
|
color: 'text.secondary',
|
|
612
617
|
}}>
|
|
613
|
-
{t('admin.meter.events.count', { count:
|
|
618
|
+
{t('admin.meter.events.count', { count: data.count })}
|
|
614
619
|
</Typography>
|
|
615
620
|
</Box>
|
|
616
621
|
<Table
|
|
617
|
-
data={
|
|
622
|
+
data={data.list}
|
|
618
623
|
columns={columns}
|
|
624
|
+
onChange={onTableChange}
|
|
619
625
|
options={{
|
|
620
|
-
count:
|
|
621
|
-
page:
|
|
622
|
-
rowsPerPage:
|
|
623
|
-
onChangePage: handlePageChange,
|
|
624
|
-
onChangeRowsPerPage: handleRowsPerPageChange,
|
|
625
|
-
search: false,
|
|
626
|
-
filter: false,
|
|
627
|
-
sort: false,
|
|
628
|
-
viewColumns: false,
|
|
629
|
-
download: false,
|
|
630
|
-
print: false,
|
|
631
|
-
selectableRows: 'none',
|
|
626
|
+
count: data.count,
|
|
627
|
+
page: search!.page - 1,
|
|
628
|
+
rowsPerPage: search!.pageSize,
|
|
632
629
|
responsive: isMobile ? 'vertical' : 'standard',
|
|
633
630
|
}}
|
|
634
|
-
loading={
|
|
631
|
+
loading={loading}
|
|
635
632
|
emptyNodeText={t('admin.meter.events.empty')}
|
|
636
633
|
/>
|
|
637
634
|
{/* 日期选择器弹窗 */}
|
package/src/locales/en.tsx
CHANGED
|
@@ -74,9 +74,11 @@ export default flat({
|
|
|
74
74
|
saturday: 'Saturday',
|
|
75
75
|
},
|
|
76
76
|
name: 'Name',
|
|
77
|
+
type: 'Type',
|
|
77
78
|
status: 'Status',
|
|
78
79
|
remainingCredit: 'Remaining Credit',
|
|
79
80
|
scope: 'Scope',
|
|
81
|
+
source: 'Source',
|
|
80
82
|
effectiveDate: 'Effective Date',
|
|
81
83
|
expirationDate: 'Expiration Date',
|
|
82
84
|
creditGrant: 'Grant',
|
|
@@ -85,8 +87,13 @@ export default flat({
|
|
|
85
87
|
meterEvent: 'Meter Event',
|
|
86
88
|
creditAmount: 'Credit',
|
|
87
89
|
createdAt: 'Created At',
|
|
90
|
+
expiresAt: 'Expires At',
|
|
88
91
|
general: 'All usage-based prices',
|
|
89
92
|
specific: 'Specific usage-based prices',
|
|
93
|
+
paid: 'Paid',
|
|
94
|
+
promotional: 'Promotional',
|
|
95
|
+
viewInvoice: 'View Invoice',
|
|
96
|
+
viewSourceData: 'View Source',
|
|
90
97
|
},
|
|
91
98
|
notification: {
|
|
92
99
|
preferences: {
|
|
@@ -1317,6 +1324,8 @@ export default flat({
|
|
|
1317
1324
|
backToGrants: 'Back to Credit Grants',
|
|
1318
1325
|
viewDetails: 'View Details',
|
|
1319
1326
|
overview: 'Overview',
|
|
1327
|
+
relatedGrants: 'Credit Grants',
|
|
1328
|
+
category: 'Category',
|
|
1320
1329
|
overviewDescription: 'Monitor all currency credit balances, usage, and outstanding debt.',
|
|
1321
1330
|
availableBalance: 'Available Balance',
|
|
1322
1331
|
viewGrants: 'View Grants',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -73,9 +73,11 @@ export default flat({
|
|
|
73
73
|
},
|
|
74
74
|
maxAmount: '最大金额为 {max}',
|
|
75
75
|
name: '名称',
|
|
76
|
+
type: '类型',
|
|
76
77
|
status: '状态',
|
|
77
78
|
remainingCredit: '剩余额度',
|
|
78
79
|
scope: '适用范围',
|
|
80
|
+
source: '来源',
|
|
79
81
|
effectiveDate: '生效时间',
|
|
80
82
|
expirationDate: '过期时间',
|
|
81
83
|
creditGrant: '信用额度',
|
|
@@ -84,8 +86,13 @@ export default flat({
|
|
|
84
86
|
meterEvent: '计量事件',
|
|
85
87
|
creditAmount: '额度',
|
|
86
88
|
createdAt: '创建时间',
|
|
89
|
+
expiresAt: '过期时间',
|
|
87
90
|
general: '通用',
|
|
88
91
|
specific: '指定使用范围',
|
|
92
|
+
paid: '付费',
|
|
93
|
+
promotional: '促销',
|
|
94
|
+
viewInvoice: '查看账单',
|
|
95
|
+
viewSourceData: '查看来源',
|
|
89
96
|
},
|
|
90
97
|
notification: {
|
|
91
98
|
preferences: {
|
|
@@ -1282,6 +1289,8 @@ export default flat({
|
|
|
1282
1289
|
pendingAmount: '欠费额度',
|
|
1283
1290
|
grantCount: '额度数量',
|
|
1284
1291
|
noGrantsDescription: '您还没有任何信用额度',
|
|
1292
|
+
relatedGrants: '信用额度',
|
|
1293
|
+
category: '分类',
|
|
1285
1294
|
status: {
|
|
1286
1295
|
granted: '生效中',
|
|
1287
1296
|
pending: '待生效',
|
|
@@ -13,7 +13,13 @@ import {
|
|
|
13
13
|
getInvoiceStatusColor,
|
|
14
14
|
useMobile,
|
|
15
15
|
} from '@blocklet/payment-react';
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
TCheckoutSession,
|
|
18
|
+
TCreditGrantExpanded,
|
|
19
|
+
TInvoice,
|
|
20
|
+
TInvoiceExpanded,
|
|
21
|
+
TPaymentLink,
|
|
22
|
+
} from '@blocklet/payment-types';
|
|
17
23
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
18
24
|
import { Alert, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
|
|
19
25
|
import { styled } from '@mui/system';
|
|
@@ -37,8 +43,18 @@ import SectionHeader from '../../../../components/section/header';
|
|
|
37
43
|
import { goBackOrFallback } from '../../../../libs/util';
|
|
38
44
|
import InfoMetric from '../../../../components/info-metric';
|
|
39
45
|
import InfoRowGroup from '../../../../components/info-row-group';
|
|
46
|
+
import RelatedCreditGrants from '../../../../components/customer/related-credit-grants';
|
|
40
47
|
|
|
41
|
-
const fetchData = (
|
|
48
|
+
const fetchData = (
|
|
49
|
+
id: string
|
|
50
|
+
): Promise<
|
|
51
|
+
TInvoiceExpanded & {
|
|
52
|
+
relatedInvoice?: TInvoiceExpanded;
|
|
53
|
+
checkoutSession: TCheckoutSession;
|
|
54
|
+
paymentLink: TPaymentLink;
|
|
55
|
+
relatedCreditGrants?: TCreditGrantExpanded[];
|
|
56
|
+
}
|
|
57
|
+
> => {
|
|
42
58
|
return api.get(`/api/invoices/${id}`).then((res) => res.data);
|
|
43
59
|
};
|
|
44
60
|
|
|
@@ -366,6 +382,9 @@ export default function InvoiceDetail(props: { id: string }) {
|
|
|
366
382
|
)}
|
|
367
383
|
</InfoRowGroup>
|
|
368
384
|
</Box>
|
|
385
|
+
{data?.relatedCreditGrants && data.relatedCreditGrants.length > 0 && (
|
|
386
|
+
<RelatedCreditGrants grants={data.relatedCreditGrants} mode="dashboard" />
|
|
387
|
+
)}
|
|
369
388
|
<Divider />
|
|
370
389
|
<Box className="section">
|
|
371
390
|
<SectionHeader title={t('admin.summary')} />
|