@blocklet/payment-react 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/es/contexts/payment.js +7 -0
- package/es/history/credit/transactions-list.js +177 -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 +257 -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 +227 -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/billing/${subscriptionId}`;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
return {
|
|
@@ -84,6 +84,114 @@ const getTransactionDetailLink = (transactionId: string, inDashboard: boolean) =
|
|
|
84
84
|
};
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
const getCreditTransactionDetailLink = (transactionId: string) => {
|
|
88
|
+
const path = `/customer/credit-transaction/${transactionId}`;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
link: createLink(path),
|
|
92
|
+
connect: false,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const getMeterEventDetailLink = (meterEventId: string) => {
|
|
97
|
+
// Meter event detail page is only available in admin dashboard
|
|
98
|
+
const path = `/admin/billing/${meterEventId}`;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
link: createLink(path),
|
|
102
|
+
connect: false,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const getSubscriptionId = (item: any) =>
|
|
107
|
+
item.metadata?.subscription_id || item.subscription_id || item.invoice?.subscription_id;
|
|
108
|
+
|
|
109
|
+
const getInvoiceId = (item: any) => item.metadata?.invoice_id || item.invoice?.id;
|
|
110
|
+
const getMeterEventId = (item: any) => item.source || item.metadata?.meter_event_id;
|
|
111
|
+
|
|
112
|
+
// Extract credit activity classification logic for table columns reuse
|
|
113
|
+
const getCreditActivityFlags = (item: any) => {
|
|
114
|
+
const isGrant = item.activity_type === 'grant';
|
|
115
|
+
const isScheduled = isGrant && item.metadata?.delivery_mode === 'schedule';
|
|
116
|
+
const isDepleted = isGrant && item.status === 'depleted';
|
|
117
|
+
const isExpired = isGrant && (item.status === 'expired' || item.status === 'voided');
|
|
118
|
+
const isInactive = isDepleted || isExpired;
|
|
119
|
+
|
|
120
|
+
return { isGrant, isScheduled, isDepleted, isExpired, isInactive };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const getTransactionDetailLink = (item: any, inDashboard: boolean) => {
|
|
124
|
+
if (item.activity_type === 'grant') {
|
|
125
|
+
const invoiceId = getInvoiceId(item);
|
|
126
|
+
if (invoiceId) {
|
|
127
|
+
return getInvoiceDetailLink(invoiceId, inDashboard);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return getGrantDetailLink(item.id, inDashboard);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!inDashboard) {
|
|
134
|
+
return getCreditTransactionDetailLink(item.id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const meterEventId = getMeterEventId(item);
|
|
138
|
+
if (!meterEventId) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return getMeterEventDetailLink(meterEventId);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const getTransactionDescription = (item: any, t: Function) => {
|
|
146
|
+
const { isGrant, isScheduled, isInactive } = getCreditActivityFlags(item);
|
|
147
|
+
const isPaid = isGrant && item.category === 'paid' && (!isScheduled || item.metadata?.schedule_seq === 1);
|
|
148
|
+
|
|
149
|
+
if (!isGrant) {
|
|
150
|
+
const secondLine = item.metadata?.is_repayment ? t('common.creditActivity.repayment') : item.description || '';
|
|
151
|
+
return {
|
|
152
|
+
isGrant,
|
|
153
|
+
isInactive,
|
|
154
|
+
activityType: t('common.creditActivity.consumption'),
|
|
155
|
+
secondLine,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isPaid) {
|
|
160
|
+
let secondLine = item.description || '';
|
|
161
|
+
if (item.invoice?.total && item.invoice?.paymentCurrency) {
|
|
162
|
+
const invoiceCurrency = item.invoice.paymentCurrency;
|
|
163
|
+
const paidAmount = formatCreditAmount(
|
|
164
|
+
formatBNStr(item.invoice.total, invoiceCurrency.decimal || 0),
|
|
165
|
+
invoiceCurrency.symbol || ''
|
|
166
|
+
);
|
|
167
|
+
secondLine = t('common.creditActivity.paidAmount', { amount: paidAmount });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
isGrant,
|
|
172
|
+
isInactive,
|
|
173
|
+
activityType: t('common.creditActivity.paidGrant'),
|
|
174
|
+
secondLine,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (isScheduled) {
|
|
179
|
+
return {
|
|
180
|
+
isGrant,
|
|
181
|
+
isInactive,
|
|
182
|
+
activityType: t('common.creditActivity.resetGrant'),
|
|
183
|
+
secondLine: item.description || '',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
isGrant,
|
|
189
|
+
isInactive,
|
|
190
|
+
activityType: t('common.creditActivity.promotionalGrant'),
|
|
191
|
+
secondLine: item.description || '',
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
|
|
87
195
|
const TransactionsTable = React.memo((props: Props) => {
|
|
88
196
|
const {
|
|
89
197
|
pageSize,
|
|
@@ -101,6 +209,7 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
101
209
|
const { t, locale } = useLocaleContext();
|
|
102
210
|
const { session } = usePaymentContext();
|
|
103
211
|
const isAdmin = ['owner', 'admin'].includes(session?.user?.role || '');
|
|
212
|
+
const isDashboard = isAdmin && mode === 'dashboard';
|
|
104
213
|
const navigate = useNavigate();
|
|
105
214
|
|
|
106
215
|
// 如果没有传入 customer_id,使用当前登录用户的 DID
|
|
@@ -121,7 +230,7 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
121
230
|
end: undefined,
|
|
122
231
|
});
|
|
123
232
|
|
|
124
|
-
const handleDateRangeChange =
|
|
233
|
+
const handleDateRangeChange = (newValue: DateRangeValue) => {
|
|
125
234
|
setFilters(newValue);
|
|
126
235
|
setSearch((prev) => ({
|
|
127
236
|
...prev,
|
|
@@ -129,7 +238,7 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
129
238
|
start: newValue.start || undefined,
|
|
130
239
|
end: newValue.end || undefined,
|
|
131
240
|
}));
|
|
132
|
-
}
|
|
241
|
+
};
|
|
133
242
|
|
|
134
243
|
const { loading, data = { list: [], count: 0 } } = useRequest(
|
|
135
244
|
() =>
|
|
@@ -149,9 +258,14 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
149
258
|
// 初始化时应用默认日期筛选
|
|
150
259
|
useEffect(() => {
|
|
151
260
|
if (showTimeFilter && !search.start && !search.end) {
|
|
152
|
-
|
|
261
|
+
setSearch((prev) => ({
|
|
262
|
+
...prev,
|
|
263
|
+
page: 1,
|
|
264
|
+
start: filters.start || undefined,
|
|
265
|
+
end: filters.end || undefined,
|
|
266
|
+
}));
|
|
153
267
|
}
|
|
154
|
-
}, [showTimeFilter,
|
|
268
|
+
}, [showTimeFilter, search.start, search.end, filters.start, filters.end]);
|
|
155
269
|
|
|
156
270
|
const prevData = useRef(data);
|
|
157
271
|
|
|
@@ -164,122 +278,125 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
164
278
|
}, [data]);
|
|
165
279
|
|
|
166
280
|
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' });
|
|
281
|
+
const detail = getTransactionDetailLink(item, isDashboard);
|
|
282
|
+
if (!detail) {
|
|
283
|
+
return;
|
|
173
284
|
}
|
|
285
|
+
|
|
286
|
+
handleNavigation(e, detail.link, navigate, { target: detail.link.external ? '_blank' : '_self' });
|
|
174
287
|
};
|
|
288
|
+
const openSubscription = (e: React.MouseEvent, subscriptionId: string) => {
|
|
289
|
+
e.preventDefault();
|
|
290
|
+
const link = getSubscriptionDetailLink(subscriptionId, isDashboard);
|
|
291
|
+
handleNavigation(e, link.link, navigate);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const openInvoice = (e: React.MouseEvent, invoiceId: string) => {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
const link = getInvoiceDetailLink(invoiceId, isDashboard);
|
|
297
|
+
handleNavigation(e, link.link, navigate);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const renderActionButton = (label: string, onClick: (e: React.MouseEvent) => void) => (
|
|
301
|
+
<Button variant="text" size="small" color="primary" sx={{ whiteSpace: 'nowrap' }} onClick={onClick}>
|
|
302
|
+
{label}
|
|
303
|
+
</Button>
|
|
304
|
+
);
|
|
305
|
+
|
|
175
306
|
const columns = [
|
|
176
307
|
{
|
|
177
|
-
label: t('common.
|
|
178
|
-
name: '
|
|
179
|
-
align: 'right',
|
|
308
|
+
label: t('common.date'),
|
|
309
|
+
name: 'created_at',
|
|
180
310
|
options: {
|
|
311
|
+
setCellProps: () => ({ style: { width: '25%' } }),
|
|
181
312
|
customBodyRenderLite: (_: string, index: number) => {
|
|
182
313
|
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
314
|
return (
|
|
208
315
|
<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>
|
|
316
|
+
<Typography variant="body2" sx={{ fontSize: '0.875rem' }}>
|
|
317
|
+
{formatToDate(item.created_at, locale, 'YYYY-MM-DD HH:mm')}
|
|
318
|
+
</Typography>
|
|
226
319
|
</Box>
|
|
227
320
|
);
|
|
228
321
|
},
|
|
229
322
|
},
|
|
230
323
|
},
|
|
231
324
|
{
|
|
232
|
-
label: t('common.
|
|
233
|
-
name: '
|
|
325
|
+
label: t('common.description'),
|
|
326
|
+
name: 'description',
|
|
234
327
|
options: {
|
|
328
|
+
setCellProps: () => ({ style: { width: '25%' } }),
|
|
235
329
|
customBodyRenderLite: (_: string, index: number) => {
|
|
236
330
|
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
|
-
);
|
|
331
|
+
const { activityType, secondLine, isInactive, isGrant } = getTransactionDescription(item, t);
|
|
332
|
+
|
|
247
333
|
return (
|
|
248
|
-
<
|
|
249
|
-
direction="
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
334
|
+
<Box onClick={(e) => handleTransactionClick(e, item)} sx={{ cursor: 'pointer' }}>
|
|
335
|
+
<Stack direction="column" spacing={0.25}>
|
|
336
|
+
<Typography
|
|
337
|
+
variant="body2"
|
|
338
|
+
sx={{
|
|
339
|
+
color: isInactive ? 'text.secondary' : isGrant ? 'success.main' : 'error.main',
|
|
340
|
+
}}>
|
|
341
|
+
{activityType}
|
|
342
|
+
</Typography>
|
|
343
|
+
{secondLine && (
|
|
344
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
345
|
+
{secondLine}
|
|
346
|
+
</Typography>
|
|
347
|
+
)}
|
|
348
|
+
</Stack>
|
|
349
|
+
</Box>
|
|
260
350
|
);
|
|
261
351
|
},
|
|
262
352
|
},
|
|
263
353
|
},
|
|
264
354
|
{
|
|
265
|
-
label: t('common.
|
|
266
|
-
name: '
|
|
355
|
+
label: t('common.amount'),
|
|
356
|
+
name: 'credit_amount',
|
|
357
|
+
align: 'right',
|
|
267
358
|
options: {
|
|
359
|
+
setCellProps: () => ({ style: { width: '20%' } }),
|
|
360
|
+
customHeadLabelRender: () => <Box sx={{ pr: 5 }}>{t('common.amount')}</Box>,
|
|
268
361
|
customBodyRenderLite: (_: string, index: number) => {
|
|
269
362
|
const item = data?.list[index] as any;
|
|
270
|
-
const isGrant = item
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
{
|
|
279
|
-
|
|
280
|
-
|
|
363
|
+
const { isGrant, isDepleted, isExpired, isInactive } = getCreditActivityFlags(item);
|
|
364
|
+
const amount = isGrant ? item.amount : item.credit_amount;
|
|
365
|
+
const currency = item.paymentCurrency || item.currency;
|
|
366
|
+
const unit = !isGrant && item.meter?.unit ? item.meter.unit : currency?.symbol;
|
|
367
|
+
const displayAmount = formatCreditAmount(formatBNStr(amount, currency?.decimal || 0), unit);
|
|
368
|
+
|
|
369
|
+
if (!includeGrants) {
|
|
370
|
+
return (
|
|
371
|
+
<Box onClick={(e) => handleTransactionClick(e, item)} sx={{ pr: 5 }}>
|
|
372
|
+
<Typography>{displayAmount}</Typography>
|
|
373
|
+
</Box>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
281
376
|
|
|
282
|
-
return
|
|
377
|
+
return (
|
|
378
|
+
<Box onClick={(e) => handleTransactionClick(e, item)} sx={{ pr: 5 }}>
|
|
379
|
+
<Stack direction="column" spacing={0.25} alignItems="flex-end">
|
|
380
|
+
<Typography
|
|
381
|
+
sx={{
|
|
382
|
+
fontWeight: 500,
|
|
383
|
+
color: isInactive ? 'text.secondary' : isGrant ? 'success.main' : 'error.main',
|
|
384
|
+
whiteSpace: 'nowrap',
|
|
385
|
+
}}>
|
|
386
|
+
{isGrant ? '+' : '-'} {displayAmount}
|
|
387
|
+
</Typography>
|
|
388
|
+
{isDepleted ? (
|
|
389
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
390
|
+
{t('common.consumed')}
|
|
391
|
+
</Typography>
|
|
392
|
+
) : isExpired ? (
|
|
393
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
394
|
+
{t('common.expired')}
|
|
395
|
+
</Typography>
|
|
396
|
+
) : null}
|
|
397
|
+
</Stack>
|
|
398
|
+
</Box>
|
|
399
|
+
);
|
|
283
400
|
},
|
|
284
401
|
},
|
|
285
402
|
},
|
|
@@ -306,64 +423,27 @@ const TransactionsTable = React.memo((props: Props) => {
|
|
|
306
423
|
},
|
|
307
424
|
]
|
|
308
425
|
: []),
|
|
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
426
|
{
|
|
331
427
|
label: t('common.actions'),
|
|
332
428
|
name: 'actions',
|
|
333
429
|
options: {
|
|
430
|
+
setCellProps: () => ({ style: { width: '25%' } }),
|
|
334
431
|
customBodyRenderLite: (_: string, index: number) => {
|
|
335
432
|
const item = data?.list[index] as any;
|
|
336
|
-
const isGrant = item
|
|
337
|
-
const
|
|
433
|
+
const { isGrant, isScheduled } = getCreditActivityFlags(item);
|
|
434
|
+
const isPaid = isGrant && item.category === 'paid' && !isScheduled;
|
|
435
|
+
const subscriptionId = getSubscriptionId(item);
|
|
436
|
+
const invoiceId = isGrant ? getInvoiceId(item) : null;
|
|
437
|
+
const shouldShowSubscription = Boolean(subscriptionId) && (!isGrant || isScheduled || isPaid);
|
|
338
438
|
|
|
339
439
|
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
|
-
)}
|
|
440
|
+
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'nowrap' }}>
|
|
441
|
+
{shouldShowSubscription && subscriptionId
|
|
442
|
+
? renderActionButton(t('common.viewSubscription'), (e) => openSubscription(e, subscriptionId))
|
|
443
|
+
: null}
|
|
444
|
+
{isPaid && invoiceId
|
|
445
|
+
? renderActionButton(t('common.viewInvoice'), (e) => openInvoice(e, invoiceId))
|
|
446
|
+
: null}
|
|
367
447
|
</Box>
|
|
368
448
|
);
|
|
369
449
|
},
|
|
@@ -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',
|