@blocklet/payment-react 1.24.1 → 1.24.3
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/es/contexts/payment.js +7 -0
- package/es/history/credit/transactions-list.js +167 -138
- package/es/history/invoice/list.js +88 -19
- package/es/locales/en.js +20 -4
- package/es/locales/zh.js +20 -4
- package/es/payment/product-item.js +2 -0
- package/lib/contexts/payment.js +4 -0
- package/lib/history/credit/transactions-list.js +247 -142
- package/lib/history/invoice/list.js +113 -28
- package/lib/locales/en.js +20 -4
- package/lib/locales/zh.js +20 -4
- package/lib/payment/product-item.js +2 -0
- package/package.json +3 -3
- package/src/contexts/payment.tsx +9 -0
- package/src/history/credit/transactions-list.tsx +214 -147
- package/src/history/invoice/list.tsx +114 -28
- package/src/locales/en.tsx +18 -2
- package/src/locales/zh.tsx +18 -2
- package/src/payment/product-item.tsx +4 -0
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
/* eslint-disable react/no-unstable-nested-components */
|
|
7
7
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
8
8
|
import type { Paginated, TCreditTransactionExpanded } from '@blocklet/payment-types';
|
|
9
|
-
import { Box, Typography, Grid, Stack, Link, Button
|
|
9
|
+
import { Box, Typography, Grid, Stack, Link, Button } from '@mui/material';
|
|
10
10
|
import { useRequest } from 'ahooks';
|
|
11
11
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
12
12
|
import { useNavigate } from 'react-router-dom';
|
|
13
|
-
import React, {
|
|
13
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
14
14
|
import { styled } from '@mui/system';
|
|
15
15
|
import { joinURL } from 'ufo';
|
|
16
16
|
import DateRangePicker, { type DateRangeValue } from '../../components/date-range-picker';
|
|
@@ -72,10 +72,10 @@ const getInvoiceDetailLink = (invoiceId: string, inDashboard: boolean) => {
|
|
|
72
72
|
};
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
const
|
|
76
|
-
let path = `/customer/
|
|
75
|
+
const getSubscriptionDetailLink = (subscriptionId: string, inDashboard: boolean) => {
|
|
76
|
+
let path = `/customer/subscription/${subscriptionId}`;
|
|
77
77
|
if (inDashboard) {
|
|
78
|
-
path = `/admin/
|
|
78
|
+
path = `/admin/subscriptions/${subscriptionId}`;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
return {
|
|
@@ -84,6 +84,101 @@ const getTransactionDetailLink = (transactionId: string, inDashboard: boolean) =
|
|
|
84
84
|
};
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
const getMeterEventDetailLink = (meterEventId: string) => {
|
|
88
|
+
// Meter event detail page is only available in admin dashboard
|
|
89
|
+
const path = `/admin/billing/${meterEventId}`;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
link: createLink(path),
|
|
93
|
+
connect: false,
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const getSubscriptionId = (item: any) =>
|
|
98
|
+
item.metadata?.subscription_id || item.subscription_id || item.invoice?.subscription_id;
|
|
99
|
+
|
|
100
|
+
const getInvoiceId = (item: any) => item.metadata?.invoice_id || item.invoice?.id;
|
|
101
|
+
const getMeterEventId = (item: any) => item.source || item.metadata?.meter_event_id;
|
|
102
|
+
|
|
103
|
+
// Extract credit activity classification logic for table columns reuse
|
|
104
|
+
const getCreditActivityFlags = (item: any) => {
|
|
105
|
+
const isGrant = item.activity_type === 'grant';
|
|
106
|
+
const isScheduled = isGrant && item.metadata?.delivery_mode === 'schedule';
|
|
107
|
+
const isDepleted = isGrant && item.status === 'depleted';
|
|
108
|
+
const isExpired = isGrant && (item.status === 'expired' || item.status === 'voided');
|
|
109
|
+
const isInactive = isDepleted || isExpired;
|
|
110
|
+
|
|
111
|
+
return { isGrant, isScheduled, isDepleted, isExpired, isInactive };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const getTransactionDetailLink = (item: any, inDashboard: boolean) => {
|
|
115
|
+
if (item.activity_type === 'grant') {
|
|
116
|
+
const invoiceId = getInvoiceId(item);
|
|
117
|
+
if (invoiceId) {
|
|
118
|
+
return getInvoiceDetailLink(invoiceId, inDashboard);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return getGrantDetailLink(item.id, inDashboard);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const meterEventId = getMeterEventId(item);
|
|
125
|
+
if (!meterEventId) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return getMeterEventDetailLink(meterEventId);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const getTransactionDescription = (item: any, t: Function) => {
|
|
133
|
+
const { isGrant, isScheduled, isInactive } = getCreditActivityFlags(item);
|
|
134
|
+
const isPaid = isGrant && item.category === 'paid' && (!isScheduled || item.metadata?.schedule_seq === 1);
|
|
135
|
+
|
|
136
|
+
if (!isGrant) {
|
|
137
|
+
const secondLine = item.metadata?.is_repayment ? t('common.creditActivity.repayment') : item.description || '';
|
|
138
|
+
return {
|
|
139
|
+
isGrant,
|
|
140
|
+
isInactive,
|
|
141
|
+
activityType: t('common.creditActivity.consumption'),
|
|
142
|
+
secondLine,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (isPaid) {
|
|
147
|
+
let secondLine = item.description || '';
|
|
148
|
+
if (item.invoice?.total && item.invoice?.paymentCurrency) {
|
|
149
|
+
const invoiceCurrency = item.invoice.paymentCurrency;
|
|
150
|
+
const paidAmount = formatCreditAmount(
|
|
151
|
+
formatBNStr(item.invoice.total, invoiceCurrency.decimal || 0),
|
|
152
|
+
invoiceCurrency.symbol || ''
|
|
153
|
+
);
|
|
154
|
+
secondLine = t('common.creditActivity.paidAmount', { amount: paidAmount });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
isGrant,
|
|
159
|
+
isInactive,
|
|
160
|
+
activityType: t('common.creditActivity.paidGrant'),
|
|
161
|
+
secondLine,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (isScheduled) {
|
|
166
|
+
return {
|
|
167
|
+
isGrant,
|
|
168
|
+
isInactive,
|
|
169
|
+
activityType: t('common.creditActivity.resetGrant'),
|
|
170
|
+
secondLine: item.description || '',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
isGrant,
|
|
176
|
+
isInactive,
|
|
177
|
+
activityType: t('common.creditActivity.promotionalGrant'),
|
|
178
|
+
secondLine: item.description || '',
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
87
182
|
const TransactionsTable = React.memo((props: Props) => {
|
|
88
183
|
const {
|
|
89
184
|
pageSize,
|
|
@@ -101,6 +196,7 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
101
196
|
const { t, locale } = useLocaleContext();
|
|
102
197
|
const { session } = usePaymentContext();
|
|
103
198
|
const isAdmin = ['owner', 'admin'].includes(session?.user?.role || '');
|
|
199
|
+
const isDashboard = isAdmin && mode === 'dashboard';
|
|
104
200
|
const navigate = useNavigate();
|
|
105
201
|
|
|
106
202
|
// 如果没有传入 customer_id,使用当前登录用户的 DID
|
|
@@ -121,7 +217,7 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
121
217
|
end: undefined,
|
|
122
218
|
});
|
|
123
219
|
|
|
124
|
-
const handleDateRangeChange =
|
|
220
|
+
const handleDateRangeChange = (newValue: DateRangeValue) => {
|
|
125
221
|
setFilters(newValue);
|
|
126
222
|
setSearch((prev) => ({
|
|
127
223
|
...prev,
|
|
@@ -129,7 +225,7 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
129
225
|
start: newValue.start || undefined,
|
|
130
226
|
end: newValue.end || undefined,
|
|
131
227
|
}));
|
|
132
|
-
}
|
|
228
|
+
};
|
|
133
229
|
|
|
134
230
|
const { loading, data = { list: [], count: 0 } } = useRequest(
|
|
135
231
|
() =>
|
|
@@ -149,9 +245,14 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
149
245
|
// 初始化时应用默认日期筛选
|
|
150
246
|
useEffect(() => {
|
|
151
247
|
if (showTimeFilter && !search.start && !search.end) {
|
|
152
|
-
|
|
248
|
+
setSearch((prev) => ({
|
|
249
|
+
...prev,
|
|
250
|
+
page: 1,
|
|
251
|
+
start: filters.start || undefined,
|
|
252
|
+
end: filters.end || undefined,
|
|
253
|
+
}));
|
|
153
254
|
}
|
|
154
|
-
}, [showTimeFilter,
|
|
255
|
+
}, [showTimeFilter, search.start, search.end, filters.start, filters.end]);
|
|
155
256
|
|
|
156
257
|
const prevData = useRef(data);
|
|
157
258
|
|
|
@@ -164,122 +265,125 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
164
265
|
}, [data]);
|
|
165
266
|
|
|
166
267
|
const handleTransactionClick = (e: React.MouseEvent, item: any) => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
} else {
|
|
171
|
-
const { link } = getTransactionDetailLink(item.id, isAdmin && mode === 'dashboard');
|
|
172
|
-
handleNavigation(e, link, navigate, { target: link.external ? '_blank' : '_self' });
|
|
268
|
+
const detail = getTransactionDetailLink(item, isDashboard);
|
|
269
|
+
if (!detail) {
|
|
270
|
+
return;
|
|
173
271
|
}
|
|
272
|
+
|
|
273
|
+
handleNavigation(e, detail.link, navigate, { target: detail.link.external ? '_blank' : '_self' });
|
|
274
|
+
};
|
|
275
|
+
const openSubscription = (e: React.MouseEvent, subscriptionId: string) => {
|
|
276
|
+
e.preventDefault();
|
|
277
|
+
const link = getSubscriptionDetailLink(subscriptionId, isDashboard);
|
|
278
|
+
handleNavigation(e, link.link, navigate);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const openInvoice = (e: React.MouseEvent, invoiceId: string) => {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
const link = getInvoiceDetailLink(invoiceId, isDashboard);
|
|
284
|
+
handleNavigation(e, link.link, navigate);
|
|
174
285
|
};
|
|
286
|
+
|
|
287
|
+
const renderActionButton = (label: string, onClick: (e: React.MouseEvent) => void) => (
|
|
288
|
+
<Button variant="text" size="small" color="primary" sx={{ whiteSpace: 'nowrap' }} onClick={onClick}>
|
|
289
|
+
{label}
|
|
290
|
+
</Button>
|
|
291
|
+
);
|
|
292
|
+
|
|
175
293
|
const columns = [
|
|
176
294
|
{
|
|
177
|
-
label: t('common.
|
|
178
|
-
name: '
|
|
179
|
-
align: 'right',
|
|
295
|
+
label: t('common.date'),
|
|
296
|
+
name: 'created_at',
|
|
180
297
|
options: {
|
|
298
|
+
setCellProps: () => ({ style: { width: '25%' } }),
|
|
181
299
|
customBodyRenderLite: (_: string, index: number) => {
|
|
182
300
|
const item = data?.list[index] as any;
|
|
183
|
-
const isGrant = item.activity_type === 'grant';
|
|
184
|
-
const isExpiredGrant = isGrant && item.status === 'expired';
|
|
185
|
-
const amount = isGrant ? item.amount : item.credit_amount;
|
|
186
|
-
const currency = item.paymentCurrency || item.currency;
|
|
187
|
-
const unit = !isGrant && item.meter?.unit ? item.meter.unit : currency?.symbol;
|
|
188
|
-
const displayAmount = formatCreditAmount(formatBNStr(amount, currency?.decimal || 0), unit);
|
|
189
|
-
|
|
190
|
-
if (!includeGrants) {
|
|
191
|
-
return (
|
|
192
|
-
<Box onClick={(e) => handleTransactionClick(e, item)}>
|
|
193
|
-
<Typography>{displayAmount}</Typography>
|
|
194
|
-
</Box>
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const amountNode = (
|
|
199
|
-
<Typography
|
|
200
|
-
sx={{
|
|
201
|
-
color: isGrant ? (isExpiredGrant ? 'text.disabled' : 'success.main') : 'error.main',
|
|
202
|
-
}}>
|
|
203
|
-
{isGrant ? '+' : '-'} {displayAmount}
|
|
204
|
-
</Typography>
|
|
205
|
-
);
|
|
206
|
-
|
|
207
301
|
return (
|
|
208
302
|
<Box onClick={(e) => handleTransactionClick(e, item)}>
|
|
209
|
-
<
|
|
210
|
-
{
|
|
211
|
-
|
|
212
|
-
label={t('admin.creditGrants.status.expired')}
|
|
213
|
-
size="small"
|
|
214
|
-
variant="outlined"
|
|
215
|
-
sx={{
|
|
216
|
-
mr: 2,
|
|
217
|
-
height: 18,
|
|
218
|
-
fontSize: '12px',
|
|
219
|
-
color: 'text.disabled',
|
|
220
|
-
borderColor: 'text.disabled',
|
|
221
|
-
}}
|
|
222
|
-
/>
|
|
223
|
-
)}
|
|
224
|
-
{amountNode}
|
|
225
|
-
</Stack>
|
|
303
|
+
<Typography variant="body2" sx={{ fontSize: '0.875rem' }}>
|
|
304
|
+
{formatToDate(item.created_at, locale, 'YYYY-MM-DD HH:mm')}
|
|
305
|
+
</Typography>
|
|
226
306
|
</Box>
|
|
227
307
|
);
|
|
228
308
|
},
|
|
229
309
|
},
|
|
230
310
|
},
|
|
231
311
|
{
|
|
232
|
-
label: t('common.
|
|
233
|
-
name: '
|
|
312
|
+
label: t('common.description'),
|
|
313
|
+
name: 'description',
|
|
234
314
|
options: {
|
|
315
|
+
setCellProps: () => ({ style: { width: '25%' } }),
|
|
235
316
|
customBodyRenderLite: (_: string, index: number) => {
|
|
236
317
|
const item = data?.list[index] as any;
|
|
237
|
-
const isGrant = item
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const grantName = isGrant ? item.name : item.creditGrant.name;
|
|
241
|
-
const grantId = isGrant ? item.id : item.credit_grant_id;
|
|
242
|
-
const nameNode = (
|
|
243
|
-
<Typography variant="body2" sx={{ cursor: 'pointer', color: isExpiredGrant ? 'text.disabled' : undefined }}>
|
|
244
|
-
{grantName || `Grant ${grantId.slice(-6)}`}
|
|
245
|
-
</Typography>
|
|
246
|
-
);
|
|
318
|
+
const { activityType, secondLine, isInactive, isGrant } = getTransactionDescription(item, t);
|
|
319
|
+
|
|
247
320
|
return (
|
|
248
|
-
<
|
|
249
|
-
direction="
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
321
|
+
<Box onClick={(e) => handleTransactionClick(e, item)} sx={{ cursor: 'pointer' }}>
|
|
322
|
+
<Stack direction="column" spacing={0.25}>
|
|
323
|
+
<Typography
|
|
324
|
+
variant="body2"
|
|
325
|
+
sx={{
|
|
326
|
+
color: isInactive ? 'text.secondary' : isGrant ? 'success.main' : 'error.main',
|
|
327
|
+
}}>
|
|
328
|
+
{activityType}
|
|
329
|
+
</Typography>
|
|
330
|
+
{secondLine && (
|
|
331
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
332
|
+
{secondLine}
|
|
333
|
+
</Typography>
|
|
334
|
+
)}
|
|
335
|
+
</Stack>
|
|
336
|
+
</Box>
|
|
260
337
|
);
|
|
261
338
|
},
|
|
262
339
|
},
|
|
263
340
|
},
|
|
264
341
|
{
|
|
265
|
-
label: t('common.
|
|
266
|
-
name: '
|
|
342
|
+
label: t('common.amount'),
|
|
343
|
+
name: 'credit_amount',
|
|
344
|
+
align: 'right',
|
|
267
345
|
options: {
|
|
346
|
+
setCellProps: () => ({ style: { width: '20%' } }),
|
|
347
|
+
customHeadLabelRender: () => <Box sx={{ pr: 5 }}>{t('common.amount')}</Box>,
|
|
268
348
|
customBodyRenderLite: (_: string, index: number) => {
|
|
269
349
|
const item = data?.list[index] as any;
|
|
270
|
-
const isGrant = item
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
{
|
|
279
|
-
|
|
280
|
-
|
|
350
|
+
const { isGrant, isDepleted, isExpired, isInactive } = getCreditActivityFlags(item);
|
|
351
|
+
const amount = isGrant ? item.amount : item.credit_amount;
|
|
352
|
+
const currency = item.paymentCurrency || item.currency;
|
|
353
|
+
const unit = !isGrant && item.meter?.unit ? item.meter.unit : currency?.symbol;
|
|
354
|
+
const displayAmount = formatCreditAmount(formatBNStr(amount, currency?.decimal || 0), unit);
|
|
355
|
+
|
|
356
|
+
if (!includeGrants) {
|
|
357
|
+
return (
|
|
358
|
+
<Box onClick={(e) => handleTransactionClick(e, item)} sx={{ pr: 5 }}>
|
|
359
|
+
<Typography>{displayAmount}</Typography>
|
|
360
|
+
</Box>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
281
363
|
|
|
282
|
-
return
|
|
364
|
+
return (
|
|
365
|
+
<Box onClick={(e) => handleTransactionClick(e, item)} sx={{ pr: 5 }}>
|
|
366
|
+
<Stack direction="column" spacing={0.25} alignItems="flex-end">
|
|
367
|
+
<Typography
|
|
368
|
+
sx={{
|
|
369
|
+
fontWeight: 500,
|
|
370
|
+
color: isInactive ? 'text.secondary' : isGrant ? 'success.main' : 'error.main',
|
|
371
|
+
whiteSpace: 'nowrap',
|
|
372
|
+
}}>
|
|
373
|
+
{isGrant ? '+' : '-'} {displayAmount}
|
|
374
|
+
</Typography>
|
|
375
|
+
{isDepleted ? (
|
|
376
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
377
|
+
{t('common.consumed')}
|
|
378
|
+
</Typography>
|
|
379
|
+
) : isExpired ? (
|
|
380
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
381
|
+
{t('common.expired')}
|
|
382
|
+
</Typography>
|
|
383
|
+
) : null}
|
|
384
|
+
</Stack>
|
|
385
|
+
</Box>
|
|
386
|
+
);
|
|
283
387
|
},
|
|
284
388
|
},
|
|
285
389
|
},
|
|
@@ -306,64 +410,27 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
306
410
|
},
|
|
307
411
|
]
|
|
308
412
|
: []),
|
|
309
|
-
{
|
|
310
|
-
label: t('common.date'),
|
|
311
|
-
name: 'created_at',
|
|
312
|
-
options: {
|
|
313
|
-
customBodyRenderLite: (_: string, index: number) => {
|
|
314
|
-
const item = data?.list[index] as any;
|
|
315
|
-
return (
|
|
316
|
-
<Box onClick={(e) => handleTransactionClick(e, item)}>
|
|
317
|
-
<Typography
|
|
318
|
-
variant="body2"
|
|
319
|
-
sx={{
|
|
320
|
-
color: 'text.secondary',
|
|
321
|
-
fontSize: '0.875rem',
|
|
322
|
-
}}>
|
|
323
|
-
{formatToDate(item.created_at, locale, 'YYYY-MM-DD HH:mm')}
|
|
324
|
-
</Typography>
|
|
325
|
-
</Box>
|
|
326
|
-
);
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
413
|
{
|
|
331
414
|
label: t('common.actions'),
|
|
332
415
|
name: 'actions',
|
|
333
416
|
options: {
|
|
417
|
+
setCellProps: () => ({ style: { width: '25%' } }),
|
|
334
418
|
customBodyRenderLite: (_: string, index: number) => {
|
|
335
419
|
const item = data?.list[index] as any;
|
|
336
|
-
const isGrant = item
|
|
337
|
-
const
|
|
420
|
+
const { isGrant, isScheduled } = getCreditActivityFlags(item);
|
|
421
|
+
const isPaid = isGrant && item.category === 'paid' && !isScheduled;
|
|
422
|
+
const subscriptionId = getSubscriptionId(item);
|
|
423
|
+
const invoiceId = isGrant ? getInvoiceId(item) : null;
|
|
424
|
+
const shouldShowSubscription = Boolean(subscriptionId) && (!isGrant || isScheduled || isPaid);
|
|
338
425
|
|
|
339
426
|
return (
|
|
340
|
-
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
|
341
|
-
{
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
e.preventDefault();
|
|
348
|
-
const link = getInvoiceDetailLink(invoiceId, isAdmin && mode === 'dashboard');
|
|
349
|
-
handleNavigation(e, link.link, navigate);
|
|
350
|
-
}}>
|
|
351
|
-
{t('common.viewInvoice')}
|
|
352
|
-
</Button>
|
|
353
|
-
)}
|
|
354
|
-
{!isGrant && (
|
|
355
|
-
<Button
|
|
356
|
-
variant="text"
|
|
357
|
-
size="small"
|
|
358
|
-
color="primary"
|
|
359
|
-
onClick={(e) => {
|
|
360
|
-
e.preventDefault();
|
|
361
|
-
const link = getTransactionDetailLink(item.id, isAdmin && mode === 'dashboard');
|
|
362
|
-
handleNavigation(e, link.link, navigate);
|
|
363
|
-
}}>
|
|
364
|
-
{t('common.viewDetail')}
|
|
365
|
-
</Button>
|
|
366
|
-
)}
|
|
427
|
+
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'nowrap' }}>
|
|
428
|
+
{shouldShowSubscription && subscriptionId
|
|
429
|
+
? renderActionButton(t('common.viewSubscription'), (e) => openSubscription(e, subscriptionId))
|
|
430
|
+
: null}
|
|
431
|
+
{isPaid && invoiceId
|
|
432
|
+
? renderActionButton(t('common.viewInvoice'), (e) => openInvoice(e, invoiceId))
|
|
433
|
+
: null}
|
|
367
434
|
</Box>
|
|
368
435
|
);
|
|
369
436
|
},
|
|
@@ -23,6 +23,7 @@ import api from '../../libs/api';
|
|
|
23
23
|
import StripePaymentAction from '../../components/stripe-payment-action';
|
|
24
24
|
import {
|
|
25
25
|
formatBNStr,
|
|
26
|
+
formatCreditAmount,
|
|
26
27
|
formatError,
|
|
27
28
|
formatToDate,
|
|
28
29
|
formatToDatetime,
|
|
@@ -112,6 +113,7 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
|
|
|
112
113
|
const listKey = 'invoice-table';
|
|
113
114
|
const { t, locale } = useLocaleContext();
|
|
114
115
|
const navigate = useNavigate();
|
|
116
|
+
const { getCurrency } = usePaymentContext();
|
|
115
117
|
|
|
116
118
|
const [search, setSearch] = useState<{ pageSize: number; page: number }>({
|
|
117
119
|
pageSize: pageSize || 10,
|
|
@@ -256,25 +258,126 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
|
|
|
256
258
|
...(relatedSubscription
|
|
257
259
|
? [
|
|
258
260
|
{
|
|
259
|
-
label: t('common.
|
|
260
|
-
name: '
|
|
261
|
+
label: t('common.purchaseItems'),
|
|
262
|
+
name: 'purchase_items',
|
|
261
263
|
options: {
|
|
262
264
|
customBodyRenderLite: (_: string, index: number) => {
|
|
263
265
|
const invoice = data?.list[index] as TInvoiceExpanded;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
{
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
266
|
+
const lines = (invoice as any).lines || [];
|
|
267
|
+
const items = lines
|
|
268
|
+
.map((line: any) => {
|
|
269
|
+
const name = line.price?.product?.name || line.description;
|
|
270
|
+
if (!name) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const quantity = Number(line.quantity || 0);
|
|
274
|
+
const label = Number.isFinite(quantity) && quantity > 1 ? `${name} x${quantity}` : name;
|
|
275
|
+
const lineKey = line.id || line.price?.id || line.price?.product?.id || line.description || name;
|
|
276
|
+
return { key: String(lineKey), label };
|
|
277
|
+
})
|
|
278
|
+
.filter(Boolean) as Array<{ key: string; label: string }>;
|
|
279
|
+
|
|
280
|
+
if (items.length === 0 && invoice.subscription?.description) {
|
|
281
|
+
items.push({
|
|
282
|
+
key: `subscription-${invoice.subscription_id || invoice.id}`,
|
|
283
|
+
label: invoice.subscription.description,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const isSubscription = Boolean(invoice.subscription_id);
|
|
287
|
+
const clickableProps = isSubscription
|
|
288
|
+
? { onClick: (e: React.MouseEvent) => handleRelatedSubscriptionClick(e, invoice) }
|
|
289
|
+
: {};
|
|
290
|
+
|
|
291
|
+
if (items.length === 0) {
|
|
292
|
+
return (
|
|
293
|
+
<Box sx={{ color: 'text.lighter' }}>
|
|
294
|
+
<Typography sx={{ fontSize: 14 }}>{t('common.none')}</Typography>
|
|
295
|
+
</Box>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<Box {...clickableProps} sx={isSubscription ? { cursor: 'pointer' } : undefined}>
|
|
301
|
+
<Stack spacing={0.5}>
|
|
302
|
+
{items.map((item) => (
|
|
303
|
+
<Typography
|
|
304
|
+
key={`${invoice.id}-item-${item.key}`}
|
|
305
|
+
sx={{ fontSize: 14, color: isSubscription ? 'text.link' : 'text.primary' }}
|
|
306
|
+
noWrap>
|
|
307
|
+
{item.label}
|
|
308
|
+
</Typography>
|
|
309
|
+
))}
|
|
310
|
+
</Stack>
|
|
273
311
|
</Box>
|
|
274
312
|
);
|
|
275
313
|
},
|
|
276
314
|
},
|
|
277
315
|
},
|
|
316
|
+
{
|
|
317
|
+
label: t('common.credits'),
|
|
318
|
+
name: 'credits',
|
|
319
|
+
options: {
|
|
320
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
321
|
+
const invoice = data?.list[index] as TInvoiceExpanded;
|
|
322
|
+
const lines = (invoice as any).lines || [];
|
|
323
|
+
const creditItems: Array<{ key: string; label: string }> = [];
|
|
324
|
+
|
|
325
|
+
lines.forEach((line: any) => {
|
|
326
|
+
const lineKey = String(
|
|
327
|
+
line.id || line.price?.id || line.price?.product?.id || line.description || invoice.id
|
|
328
|
+
);
|
|
329
|
+
const pushCreditItem = (suffix: string, label: string) => {
|
|
330
|
+
creditItems.push({ key: `${lineKey}-${suffix}`, label });
|
|
331
|
+
};
|
|
332
|
+
const creditConfig = line.price?.metadata?.credit_config;
|
|
333
|
+
const creditAmount = Number(creditConfig?.credit_amount || 0);
|
|
334
|
+
if (creditAmount > 0) {
|
|
335
|
+
const quantity = Number(line.quantity || 0);
|
|
336
|
+
const totalAmount = creditAmount * (Number.isFinite(quantity) && quantity > 0 ? quantity : 1);
|
|
337
|
+
const currencySymbol =
|
|
338
|
+
getCurrency(creditConfig?.currency_id)?.symbol || creditConfig?.currency_id || 'Credits';
|
|
339
|
+
pushCreditItem('amount', `+${formatCreditAmount(String(totalAmount), currencySymbol)}`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const scheduleConfig = creditConfig?.schedule;
|
|
344
|
+
const scheduledAmount = Number(scheduleConfig?.amount_per_grant || 0);
|
|
345
|
+
if (scheduleConfig?.enabled && scheduleConfig?.delivery_mode === 'schedule' && scheduledAmount > 0) {
|
|
346
|
+
const quantity = Number(line.quantity || 0);
|
|
347
|
+
const totalAmount = scheduledAmount * (Number.isFinite(quantity) && quantity > 0 ? quantity : 1);
|
|
348
|
+
const currencySymbol =
|
|
349
|
+
getCurrency(creditConfig?.currency_id)?.symbol || creditConfig?.currency_id || 'Credits';
|
|
350
|
+
pushCreditItem('schedule', `+${formatCreditAmount(String(totalAmount), currencySymbol)}`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const creditInfo = (line.price as any)?.credit;
|
|
355
|
+
const creditInfoAmount = Number(creditInfo?.amount || 0);
|
|
356
|
+
if (creditInfoAmount > 0) {
|
|
357
|
+
const currencySymbol = creditInfo.currency?.symbol || 'Credits';
|
|
358
|
+
pushCreditItem('credit', `+${formatCreditAmount(String(creditInfoAmount), currencySymbol)}`);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if (creditItems.length === 0) {
|
|
363
|
+
return '-';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<Stack spacing={0.5}>
|
|
368
|
+
{creditItems.map((creditItem) => (
|
|
369
|
+
<Typography
|
|
370
|
+
key={`${invoice.id}-credit-${creditItem.key}`}
|
|
371
|
+
sx={{ fontSize: 14, color: 'success.main' }}
|
|
372
|
+
noWrap>
|
|
373
|
+
{creditItem.label}
|
|
374
|
+
</Typography>
|
|
375
|
+
))}
|
|
376
|
+
</Stack>
|
|
377
|
+
);
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
278
381
|
]
|
|
279
382
|
: []),
|
|
280
383
|
{
|
|
@@ -296,23 +399,6 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
|
|
|
296
399
|
},
|
|
297
400
|
},
|
|
298
401
|
},
|
|
299
|
-
|
|
300
|
-
{
|
|
301
|
-
label: t('common.description'),
|
|
302
|
-
name: '',
|
|
303
|
-
options: {
|
|
304
|
-
sort: false,
|
|
305
|
-
customBodyRenderLite: (val: string, index: number) => {
|
|
306
|
-
const invoice = data?.list[index] as TInvoiceExpanded;
|
|
307
|
-
|
|
308
|
-
return (
|
|
309
|
-
<Box onClick={(e) => handleLinkClick(e, invoice)} sx={linkStyle}>
|
|
310
|
-
{getInvoiceDescriptionAndReason(invoice, locale)?.description || invoice.id}
|
|
311
|
-
</Box>
|
|
312
|
-
);
|
|
313
|
-
},
|
|
314
|
-
},
|
|
315
|
-
},
|
|
316
402
|
{
|
|
317
403
|
label: t('common.status'),
|
|
318
404
|
name: 'status',
|