payment-kit 1.24.2 → 1.24.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/api/src/crons/index.ts +5 -5
- package/api/src/crons/overdue-detection.ts +734 -0
- package/api/src/libs/env.ts +2 -0
- package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +222 -0
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +20 -63
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +57 -7
- package/api/src/locales/en.ts +74 -36
- package/api/src/locales/zh.ts +77 -39
- package/api/src/queues/auto-recharge.ts +7 -0
- package/api/src/queues/credit-consume.ts +23 -2
- package/api/src/queues/notification.ts +140 -1
- package/api/src/routes/credit-transactions.ts +85 -7
- package/api/src/routes/invoices.ts +12 -0
- package/api/src/routes/meter-events.ts +172 -0
- package/blocklet.yml +1 -1
- package/package.json +6 -6
- package/src/components/customer/credit-overview.tsx +7 -1
- package/src/locales/en.tsx +20 -0
- package/src/locales/zh.tsx +20 -0
- package/src/pages/admin/billing/index.tsx +4 -0
- package/src/pages/admin/billing/meter-events/index.tsx +588 -0
- package/src/pages/admin/billing/overdue/index.tsx +289 -0
- package/src/pages/admin/customers/customers/credit-transaction/detail.tsx +15 -0
- package/src/pages/admin/overview.tsx +129 -1
- package/src/pages/customer/credit-transaction/detail.tsx +12 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/* eslint-disable react/no-unstable-nested-components */
|
|
2
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
|
+
import { api, formatBNStr, formatCreditAmount, Table, useDefaultPageSize } from '@blocklet/payment-react';
|
|
4
|
+
import type { TCustomer, TPaymentCurrency } from '@blocklet/payment-types';
|
|
5
|
+
import { Add, Close } from '@mui/icons-material';
|
|
6
|
+
import { Box, Button, CircularProgress, Menu, MenuItem, styled } from '@mui/material';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { Link, useSearchParams } from 'react-router-dom';
|
|
9
|
+
import CustomerLink from '../../../../components/customer/link';
|
|
10
|
+
|
|
11
|
+
type OverdueRecord = {
|
|
12
|
+
customer: TCustomer;
|
|
13
|
+
currency: TPaymentCurrency;
|
|
14
|
+
total_pending: string;
|
|
15
|
+
event_count: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type SearchProps = {
|
|
19
|
+
pageSize: number;
|
|
20
|
+
page: number;
|
|
21
|
+
currency_id: string;
|
|
22
|
+
q?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type ApiResponse = {
|
|
26
|
+
list: OverdueRecord[];
|
|
27
|
+
count: number;
|
|
28
|
+
paging: { page: number; pageSize: number };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const fetchData = (params: Record<string, any> = {}): Promise<ApiResponse> => {
|
|
32
|
+
const search = new URLSearchParams();
|
|
33
|
+
Object.keys(params).forEach((key) => {
|
|
34
|
+
const v = params[key];
|
|
35
|
+
if (v !== undefined && v !== null && v !== '') {
|
|
36
|
+
search.set(key, String(v));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return api.get(`/api/meter-events/overdue-customers?${search.toString()}`).then((res) => res.data);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const fetchCreditCurrencies = (): Promise<TPaymentCurrency[]> => {
|
|
43
|
+
return api.get('/api/payment-currencies?credit=true').then((res) => {
|
|
44
|
+
// Filter to only return credit type currencies
|
|
45
|
+
return (res.data || []).filter((c: TPaymentCurrency) => c.type === 'credit');
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default function OverdueList() {
|
|
50
|
+
const listKey = 'overdue-customers';
|
|
51
|
+
|
|
52
|
+
const { t } = useLocaleContext();
|
|
53
|
+
const defaultPageSize = useDefaultPageSize(20);
|
|
54
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
55
|
+
|
|
56
|
+
const [currencies, setCurrencies] = useState<TPaymentCurrency[]>([]);
|
|
57
|
+
const [search, setSearch] = useState<SearchProps>({
|
|
58
|
+
pageSize: defaultPageSize,
|
|
59
|
+
page: 1,
|
|
60
|
+
currency_id: '',
|
|
61
|
+
});
|
|
62
|
+
const [data, setData] = useState<ApiResponse | null>(null);
|
|
63
|
+
const [loading, setLoading] = useState(true);
|
|
64
|
+
|
|
65
|
+
// Load credit currencies on mount
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
fetchCreditCurrencies().then((list) => {
|
|
68
|
+
setCurrencies(list);
|
|
69
|
+
// Check if currency_id is provided in URL
|
|
70
|
+
const urlCurrencyId = searchParams.get('currency_id');
|
|
71
|
+
if (urlCurrencyId && list.some((c) => c.id === urlCurrencyId)) {
|
|
72
|
+
// Use URL parameter and clear it from URL
|
|
73
|
+
setSearch((prev: any) => ({ ...prev, currency_id: urlCurrencyId, page: 1 }));
|
|
74
|
+
setSearchParams({}, { replace: true });
|
|
75
|
+
}
|
|
76
|
+
// Do not auto-select first currency, keep empty by default
|
|
77
|
+
});
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
// Fetch data regardless of currency_id - API supports empty currency_id to return all
|
|
82
|
+
setLoading(true);
|
|
83
|
+
fetchData(search)
|
|
84
|
+
.then((res) => setData(res))
|
|
85
|
+
.finally(() => setLoading(false));
|
|
86
|
+
}, [search]);
|
|
87
|
+
|
|
88
|
+
const columns = [
|
|
89
|
+
{
|
|
90
|
+
label: t('common.customer'),
|
|
91
|
+
name: 'customer',
|
|
92
|
+
options: {
|
|
93
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
94
|
+
const item = data?.list[index] as OverdueRecord | undefined;
|
|
95
|
+
if (!item) return null;
|
|
96
|
+
return <CustomerLink customer={item.customer} />;
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
label: t('admin.overdue.selectCurrency'),
|
|
102
|
+
name: 'currency',
|
|
103
|
+
options: {
|
|
104
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
105
|
+
const item = data?.list[index] as OverdueRecord | undefined;
|
|
106
|
+
if (!item?.currency) return null;
|
|
107
|
+
return item.currency.symbol;
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
label: t('admin.overdue.pendingAmount'),
|
|
113
|
+
name: 'total_pending',
|
|
114
|
+
options: {
|
|
115
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
116
|
+
const item = data?.list[index] as OverdueRecord | undefined;
|
|
117
|
+
if (!item) return null;
|
|
118
|
+
const { currency } = item;
|
|
119
|
+
return (
|
|
120
|
+
<Box component="span" sx={{ fontWeight: 600, color: 'error.main' }}>
|
|
121
|
+
-{formatCreditAmount(formatBNStr(item.total_pending, currency?.decimal || 18), currency?.symbol || '')}
|
|
122
|
+
</Box>
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
label: t('admin.overdue.eventCount'),
|
|
129
|
+
name: 'event_count',
|
|
130
|
+
options: {
|
|
131
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
132
|
+
const item = data?.list[index] as OverdueRecord | undefined;
|
|
133
|
+
if (!item) return null;
|
|
134
|
+
if (!item.customer?.id) {
|
|
135
|
+
return item.event_count;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const params = new URLSearchParams({
|
|
139
|
+
customer_id: item.customer.id,
|
|
140
|
+
status: 'requires_capture,requires_action',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return <Link to={`/admin/billing/meter-events?${params.toString()}`}>{item.event_count}</Link>;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const onTableChange = ({ page, rowsPerPage }: any) => {
|
|
150
|
+
if (search!.pageSize !== rowsPerPage) {
|
|
151
|
+
setSearch((x: any) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
|
|
152
|
+
} else if (search!.page !== page + 1) {
|
|
153
|
+
setSearch((x: any) => ({ ...x, page: page + 1 }));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleCurrencyChange = (currencyId: string) => {
|
|
158
|
+
setSearch((prev: any) => ({ ...prev, currency_id: currencyId, page: 1 }));
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (loading && !data) {
|
|
162
|
+
return <CircularProgress />;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<Table
|
|
167
|
+
data={data?.list || []}
|
|
168
|
+
durable={`__${listKey}__`}
|
|
169
|
+
durableKeys={['page', 'rowsPerPage', 'searchText']}
|
|
170
|
+
columns={columns}
|
|
171
|
+
loading={loading}
|
|
172
|
+
onChange={onTableChange}
|
|
173
|
+
options={{
|
|
174
|
+
count: data?.count || 0,
|
|
175
|
+
page: search!.page - 1,
|
|
176
|
+
rowsPerPage: search!.pageSize,
|
|
177
|
+
onSearchChange: (text: string) => {
|
|
178
|
+
setSearch((prev: any) => ({
|
|
179
|
+
...prev,
|
|
180
|
+
q: text || undefined,
|
|
181
|
+
page: 1,
|
|
182
|
+
}));
|
|
183
|
+
},
|
|
184
|
+
}}
|
|
185
|
+
title={
|
|
186
|
+
<CurrencyFilter
|
|
187
|
+
currencies={currencies}
|
|
188
|
+
selectedCurrencyId={search?.currency_id || ''}
|
|
189
|
+
onChange={handleCurrencyChange}
|
|
190
|
+
/>
|
|
191
|
+
}
|
|
192
|
+
emptyNodeText={t('admin.overdue.noOverdue')}
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function CurrencyFilter({
|
|
198
|
+
currencies,
|
|
199
|
+
selectedCurrencyId,
|
|
200
|
+
onChange,
|
|
201
|
+
}: {
|
|
202
|
+
currencies: TPaymentCurrency[];
|
|
203
|
+
selectedCurrencyId: string;
|
|
204
|
+
onChange: (currencyId: string) => void;
|
|
205
|
+
}) {
|
|
206
|
+
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
|
207
|
+
const { t } = useLocaleContext();
|
|
208
|
+
const selectedCurrency = currencies.find((c) => c.id === selectedCurrencyId);
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<Root>
|
|
212
|
+
<Box className="table-toolbar-left">
|
|
213
|
+
<section>
|
|
214
|
+
<Button className="option-btn" variant="text" onClick={(e) => setAnchorEl(e.currentTarget)}>
|
|
215
|
+
{selectedCurrencyId ? (
|
|
216
|
+
<Close
|
|
217
|
+
sx={{ color: 'text.secondary', cursor: 'pointer', fontSize: '1.05rem' }}
|
|
218
|
+
onClick={(e) => {
|
|
219
|
+
e.stopPropagation();
|
|
220
|
+
onChange('');
|
|
221
|
+
setAnchorEl(null);
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
) : (
|
|
225
|
+
<Add sx={{ color: 'text.secondary', cursor: 'pointer', fontSize: '1.05rem' }} />
|
|
226
|
+
)}
|
|
227
|
+
{t('admin.overdue.selectCurrency')}
|
|
228
|
+
<span>{selectedCurrency?.symbol}</span>
|
|
229
|
+
</Button>
|
|
230
|
+
<Menu
|
|
231
|
+
anchorEl={anchorEl}
|
|
232
|
+
open={Boolean(anchorEl)}
|
|
233
|
+
onClose={() => setAnchorEl(null)}
|
|
234
|
+
className="status-options">
|
|
235
|
+
{currencies.map((currency) => (
|
|
236
|
+
<MenuItem
|
|
237
|
+
key={currency.id}
|
|
238
|
+
onClick={() => {
|
|
239
|
+
onChange(currency.id);
|
|
240
|
+
setAnchorEl(null);
|
|
241
|
+
}}>
|
|
242
|
+
{currency.name} ({currency.symbol})
|
|
243
|
+
</MenuItem>
|
|
244
|
+
))}
|
|
245
|
+
</Menu>
|
|
246
|
+
</section>
|
|
247
|
+
</Box>
|
|
248
|
+
</Root>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const Root = styled(Box)`
|
|
253
|
+
.table-toolbar-left {
|
|
254
|
+
display: flex;
|
|
255
|
+
align-items: center;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.table-toolbar-left section {
|
|
259
|
+
position: relative;
|
|
260
|
+
list-style: none;
|
|
261
|
+
font-size: 14px;
|
|
262
|
+
font-weight: normal;
|
|
263
|
+
padding: 0 5px;
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.option-btn {
|
|
268
|
+
display: flex;
|
|
269
|
+
align-items: center;
|
|
270
|
+
border-radius: 25px;
|
|
271
|
+
background: ${({ theme }) => theme.palette.grey[100]};
|
|
272
|
+
padding: 5px 10px;
|
|
273
|
+
color: ${({ theme }) => theme.palette.text.secondary};
|
|
274
|
+
font-size: 14px;
|
|
275
|
+
line-height: 14px;
|
|
276
|
+
overflow: visible;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.option-btn span {
|
|
280
|
+
color: #3773f2;
|
|
281
|
+
padding: 0 3px;
|
|
282
|
+
overflow: visible;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.status-options {
|
|
286
|
+
max-height: 300px;
|
|
287
|
+
overflow-y: auto;
|
|
288
|
+
}
|
|
289
|
+
`;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
api,
|
|
4
4
|
formatBNStr,
|
|
5
5
|
formatError,
|
|
6
|
+
formatTime,
|
|
6
7
|
getCustomerAvatar,
|
|
7
8
|
TxLink,
|
|
8
9
|
SourceDataViewer,
|
|
@@ -295,6 +296,20 @@ export default function AdminCreditTransactionDetail({ id }: { id: string }) {
|
|
|
295
296
|
}
|
|
296
297
|
/>
|
|
297
298
|
)}
|
|
299
|
+
<InfoRow
|
|
300
|
+
label={t('admin.meterEvent.reportedAt')}
|
|
301
|
+
value={formatTime(
|
|
302
|
+
data.meterEvent?.timestamp
|
|
303
|
+
? data.meterEvent.timestamp * 1000
|
|
304
|
+
: data.meterEvent?.created_at || data.created_at
|
|
305
|
+
)}
|
|
306
|
+
/>
|
|
307
|
+
<InfoRow
|
|
308
|
+
label={t('admin.meterEvent.processedAt')}
|
|
309
|
+
value={formatTime(
|
|
310
|
+
data.meterEvent?.processed_at ? data.meterEvent.processed_at * 1000 : data.created_at
|
|
311
|
+
)}
|
|
312
|
+
/>
|
|
298
313
|
</InfoRowGroup>
|
|
299
314
|
</Box>
|
|
300
315
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* eslint-disable react/require-default-props */
|
|
2
2
|
import DID from '@arcblock/ux/lib/DID';
|
|
3
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
4
|
-
import { api, formatBNStr, formatToDate, usePaymentContext } from '@blocklet/payment-react';
|
|
4
|
+
import { api, formatBNStr, formatCreditAmount, formatToDate, usePaymentContext } from '@blocklet/payment-react';
|
|
5
5
|
import { BN } from '@ocap/util';
|
|
6
6
|
import type { GroupedBN, TPaymentCurrency, TPaymentMethod, TPaymentStat } from '@blocklet/payment-types';
|
|
7
7
|
import {
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
PaymentsOutlined,
|
|
38
38
|
Refresh,
|
|
39
39
|
SellOutlined,
|
|
40
|
+
WarningAmber,
|
|
40
41
|
} from '@mui/icons-material';
|
|
41
42
|
import Chart, { TCurrencyMap, TCurrency } from '../../components/chart';
|
|
42
43
|
import CurrencySelect from '../../components/price/currency-select';
|
|
@@ -302,8 +303,31 @@ export default function Overview() {
|
|
|
302
303
|
}
|
|
303
304
|
);
|
|
304
305
|
|
|
306
|
+
const overdueSummary = useRequest<
|
|
307
|
+
{
|
|
308
|
+
list: Array<{
|
|
309
|
+
currency: TPaymentCurrency;
|
|
310
|
+
total_pending: string;
|
|
311
|
+
customer_count: number;
|
|
312
|
+
event_count: number;
|
|
313
|
+
}>;
|
|
314
|
+
},
|
|
315
|
+
any
|
|
316
|
+
>(
|
|
317
|
+
async () => {
|
|
318
|
+
const result = await api.get('/api/meter-events/overdue-summary');
|
|
319
|
+
return result.data;
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
refreshDeps: [],
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
|
|
305
326
|
const summaryLoading = summary.loading;
|
|
306
327
|
const trendLoading = trend.loading;
|
|
328
|
+
const overdueLoading = overdueSummary.loading;
|
|
329
|
+
const overdueList = overdueSummary.data?.list || [];
|
|
330
|
+
const hasOverdue = overdueList.length > 0;
|
|
307
331
|
const summaryData = summary.data;
|
|
308
332
|
const summarySummary = summaryData?.summary;
|
|
309
333
|
const addresses = summaryData?.addresses;
|
|
@@ -954,6 +978,8 @@ export default function Overview() {
|
|
|
954
978
|
</Stack>
|
|
955
979
|
);
|
|
956
980
|
|
|
981
|
+
const showOverdueCard = overdueLoading || hasOverdue;
|
|
982
|
+
|
|
957
983
|
const renderBusinessMonitoringSection = () => (
|
|
958
984
|
<Stack direction="column" spacing={3}>
|
|
959
985
|
<Typography variant="h5" sx={{ fontWeight: 600 }}>
|
|
@@ -1072,6 +1098,108 @@ export default function Overview() {
|
|
|
1072
1098
|
</Stack>
|
|
1073
1099
|
</Card>
|
|
1074
1100
|
)}
|
|
1101
|
+
{showOverdueCard && (
|
|
1102
|
+
<Card
|
|
1103
|
+
variant="outlined"
|
|
1104
|
+
sx={{
|
|
1105
|
+
p: 2,
|
|
1106
|
+
borderRadius: 1,
|
|
1107
|
+
}}>
|
|
1108
|
+
<Stack
|
|
1109
|
+
direction="row"
|
|
1110
|
+
sx={{
|
|
1111
|
+
alignItems: 'center',
|
|
1112
|
+
mb: 2,
|
|
1113
|
+
}}>
|
|
1114
|
+
<Box
|
|
1115
|
+
sx={{
|
|
1116
|
+
width: 40,
|
|
1117
|
+
height: 40,
|
|
1118
|
+
borderRadius: 1,
|
|
1119
|
+
display: 'flex',
|
|
1120
|
+
alignItems: 'center',
|
|
1121
|
+
justifyContent: 'center',
|
|
1122
|
+
backgroundColor: alpha(theme.palette.warning.main, 0.12),
|
|
1123
|
+
mr: 1.5,
|
|
1124
|
+
}}>
|
|
1125
|
+
<WarningAmber sx={{ color: 'warning.main', fontSize: 24 }} />
|
|
1126
|
+
</Box>
|
|
1127
|
+
<Box>
|
|
1128
|
+
<Typography component="h3" variant="h5">
|
|
1129
|
+
{t('admin.overviewPage.overdue.title')}
|
|
1130
|
+
</Typography>
|
|
1131
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
1132
|
+
{t('admin.overviewPage.overdue.subtitle')}
|
|
1133
|
+
</Typography>
|
|
1134
|
+
</Box>
|
|
1135
|
+
</Stack>
|
|
1136
|
+
<Stack spacing={1}>
|
|
1137
|
+
{overdueLoading
|
|
1138
|
+
? BALANCE_SKELETON_KEYS.slice(0, 2).map((placeholder) => (
|
|
1139
|
+
<Skeleton key={`overdue-skeleton-${placeholder}`} variant="rounded" height={56} />
|
|
1140
|
+
))
|
|
1141
|
+
: overdueList.map((item) => (
|
|
1142
|
+
<Stack
|
|
1143
|
+
key={item.currency.id}
|
|
1144
|
+
component={Link}
|
|
1145
|
+
to={`/admin/billing/overdue?currency_id=${item.currency.id}`}
|
|
1146
|
+
direction="row"
|
|
1147
|
+
spacing={1}
|
|
1148
|
+
sx={{
|
|
1149
|
+
alignItems: 'center',
|
|
1150
|
+
textDecoration: 'none',
|
|
1151
|
+
backgroundColor: theme.mode === 'dark' ? 'grey.100' : 'grey.50',
|
|
1152
|
+
color: 'inherit',
|
|
1153
|
+
p: 1.5,
|
|
1154
|
+
borderRadius: 1.5,
|
|
1155
|
+
transition: 'background-color 0.2s ease-in-out',
|
|
1156
|
+
'&:hover': {
|
|
1157
|
+
backgroundColor: 'action.hover',
|
|
1158
|
+
'& .arrow-icon': {
|
|
1159
|
+
opacity: 1,
|
|
1160
|
+
transform: 'translateX(0)',
|
|
1161
|
+
},
|
|
1162
|
+
},
|
|
1163
|
+
}}>
|
|
1164
|
+
<Avatar src={item.currency.logo} alt={item.currency.symbol} sx={{ width: 28, height: 28 }} />
|
|
1165
|
+
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
1166
|
+
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
|
1167
|
+
{item.currency.name}
|
|
1168
|
+
</Typography>
|
|
1169
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
1170
|
+
{t('admin.overviewPage.overdue.customers', { count: item.customer_count })}
|
|
1171
|
+
{' · '}
|
|
1172
|
+
{t('admin.overviewPage.overdue.events', { count: item.event_count })}
|
|
1173
|
+
</Typography>
|
|
1174
|
+
</Box>
|
|
1175
|
+
<Typography
|
|
1176
|
+
variant="body2"
|
|
1177
|
+
sx={{
|
|
1178
|
+
fontWeight: 600,
|
|
1179
|
+
textAlign: 'right',
|
|
1180
|
+
color: 'warning.main',
|
|
1181
|
+
}}>
|
|
1182
|
+
-
|
|
1183
|
+
{formatCreditAmount(
|
|
1184
|
+
formatBNStr(item.total_pending, item.currency.decimal ?? 0),
|
|
1185
|
+
item.currency.symbol
|
|
1186
|
+
)}
|
|
1187
|
+
</Typography>
|
|
1188
|
+
<ArrowForward
|
|
1189
|
+
className="arrow-icon"
|
|
1190
|
+
fontSize="small"
|
|
1191
|
+
sx={{
|
|
1192
|
+
opacity: 0,
|
|
1193
|
+
transform: 'translateX(-10px)',
|
|
1194
|
+
transition: 'all 0.2s ease-in-out',
|
|
1195
|
+
color: 'text.secondary',
|
|
1196
|
+
}}
|
|
1197
|
+
/>
|
|
1198
|
+
</Stack>
|
|
1199
|
+
))}
|
|
1200
|
+
</Stack>
|
|
1201
|
+
</Card>
|
|
1202
|
+
)}
|
|
1075
1203
|
{showMetricsCard && (
|
|
1076
1204
|
<Card
|
|
1077
1205
|
variant="outlined"
|
|
@@ -270,6 +270,18 @@ export default function CustomerCreditTransactionDetail() {
|
|
|
270
270
|
}
|
|
271
271
|
/>
|
|
272
272
|
)}
|
|
273
|
+
<InfoRow
|
|
274
|
+
label={t('admin.meterEvent.reportedAt')}
|
|
275
|
+
value={formatTime(
|
|
276
|
+
data.meterEvent?.timestamp
|
|
277
|
+
? data.meterEvent.timestamp * 1000
|
|
278
|
+
: data.meterEvent?.created_at || data.created_at
|
|
279
|
+
)}
|
|
280
|
+
/>
|
|
281
|
+
<InfoRow
|
|
282
|
+
label={t('admin.meterEvent.processedAt')}
|
|
283
|
+
value={formatTime(data.meterEvent?.processed_at ? data.meterEvent.processed_at * 1000 : data.created_at)}
|
|
284
|
+
/>
|
|
273
285
|
</InfoRowGroup>
|
|
274
286
|
</Box>
|
|
275
287
|
|