@blocklet/payment-react 1.19.0 → 1.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/es/components/blockchain/tx.d.ts +1 -1
  2. package/es/components/blockchain/tx.js +9 -11
  3. package/es/components/country-select.d.ts +1 -1
  4. package/es/components/date-range-picker.d.ts +13 -0
  5. package/es/components/date-range-picker.js +279 -0
  6. package/es/components/input.d.ts +5 -2
  7. package/es/components/input.js +6 -2
  8. package/es/components/label.d.ts +7 -0
  9. package/es/components/label.js +50 -0
  10. package/es/components/loading-button.d.ts +1 -1
  11. package/es/history/credit/grants-list.d.ts +14 -0
  12. package/es/history/credit/grants-list.js +215 -0
  13. package/es/history/credit/transactions-list.d.ts +13 -0
  14. package/es/history/credit/transactions-list.js +254 -0
  15. package/es/history/invoice/list.js +21 -1
  16. package/es/index.d.ts +5 -1
  17. package/es/index.js +10 -1
  18. package/es/libs/util.d.ts +2 -0
  19. package/es/libs/util.js +12 -0
  20. package/es/locales/en.js +20 -2
  21. package/es/locales/zh.js +20 -2
  22. package/es/payment/form/address.js +2 -1
  23. package/es/payment/form/index.js +46 -7
  24. package/es/payment/index.js +18 -3
  25. package/es/payment/product-item.d.ts +8 -1
  26. package/es/payment/product-item.js +137 -5
  27. package/es/payment/summary.d.ts +3 -1
  28. package/es/payment/summary.js +9 -0
  29. package/lib/components/blockchain/tx.d.ts +1 -1
  30. package/lib/components/blockchain/tx.js +9 -8
  31. package/lib/components/country-select.d.ts +1 -1
  32. package/lib/components/date-range-picker.d.ts +13 -0
  33. package/lib/components/date-range-picker.js +329 -0
  34. package/lib/components/input.d.ts +5 -2
  35. package/lib/components/input.js +8 -4
  36. package/lib/components/label.d.ts +7 -0
  37. package/lib/components/label.js +62 -0
  38. package/lib/components/loading-button.d.ts +1 -1
  39. package/lib/history/credit/grants-list.d.ts +14 -0
  40. package/lib/history/credit/grants-list.js +277 -0
  41. package/lib/history/credit/transactions-list.d.ts +13 -0
  42. package/lib/history/credit/transactions-list.js +300 -0
  43. package/lib/history/invoice/list.js +24 -0
  44. package/lib/index.d.ts +5 -1
  45. package/lib/index.js +39 -0
  46. package/lib/libs/util.d.ts +2 -0
  47. package/lib/libs/util.js +14 -0
  48. package/lib/locales/en.js +20 -2
  49. package/lib/locales/zh.js +20 -2
  50. package/lib/payment/form/address.js +6 -5
  51. package/lib/payment/form/index.js +49 -9
  52. package/lib/payment/index.js +20 -2
  53. package/lib/payment/product-item.d.ts +8 -1
  54. package/lib/payment/product-item.js +144 -4
  55. package/lib/payment/summary.d.ts +3 -1
  56. package/lib/payment/summary.js +9 -0
  57. package/package.json +3 -3
  58. package/src/components/blockchain/tx.tsx +9 -15
  59. package/src/components/country-select.tsx +2 -2
  60. package/src/components/date-range-picker.tsx +310 -0
  61. package/src/components/input.tsx +14 -3
  62. package/src/components/label.tsx +59 -0
  63. package/src/components/loading-button.tsx +1 -1
  64. package/src/history/credit/grants-list.tsx +276 -0
  65. package/src/history/credit/transactions-list.tsx +316 -0
  66. package/src/history/invoice/list.tsx +18 -1
  67. package/src/index.ts +9 -0
  68. package/src/libs/util.ts +14 -0
  69. package/src/locales/en.tsx +20 -0
  70. package/src/locales/zh.tsx +19 -0
  71. package/src/payment/form/address.tsx +4 -3
  72. package/src/payment/form/index.tsx +112 -53
  73. package/src/payment/index.tsx +17 -1
  74. package/src/payment/product-item.tsx +152 -4
  75. package/src/payment/summary.tsx +13 -2
@@ -1,9 +1,10 @@
1
1
  import React, { type ReactNode, useImperativeHandle, useRef } from 'react';
2
- import { Box, FormLabel, InputAdornment, TextField, Typography } from '@mui/material';
2
+ import { Box, InputAdornment, TextField, Typography } from '@mui/material';
3
3
  import get from 'lodash/get';
4
4
  import { Controller, useFormContext } from 'react-hook-form';
5
5
  import type { TextFieldProps } from '@mui/material';
6
6
  import type { RegisterOptions } from 'react-hook-form';
7
+ import FormLabel from './label';
7
8
 
8
9
  type InputProps = TextFieldProps & {
9
10
  name: string;
@@ -12,6 +13,9 @@ type InputProps = TextFieldProps & {
12
13
  errorPosition?: 'right' | 'bottom';
13
14
  rules?: RegisterOptions;
14
15
  wrapperStyle?: React.CSSProperties;
16
+ required?: boolean;
17
+ tooltip?: ReactNode | string;
18
+ description?: ReactNode | string;
15
19
  };
16
20
 
17
21
  function FormInputError({ error }: { error: string }) {
@@ -33,9 +37,12 @@ export default function FormInput({
33
37
  errorPosition = 'bottom',
34
38
  wrapperStyle = {},
35
39
  inputProps = {},
40
+ required = false,
41
+ tooltip = '',
42
+ description = '',
36
43
  ...rest
37
44
  }: InputProps & {
38
- ref?: React.RefObject<HTMLInputElement>;
45
+ ref?: React.RefObject<HTMLInputElement | null>;
39
46
  inputProps?: TextFieldProps['inputProps'];
40
47
  }) {
41
48
  const { control, formState } = useFormContext();
@@ -52,7 +59,11 @@ export default function FormInput({
52
59
  rules={rules}
53
60
  render={({ field }) => (
54
61
  <Box sx={{ width: '100%', ...wrapperStyle }}>
55
- {!!label && <FormLabel sx={{ color: 'text.primary' }}>{label}</FormLabel>}
62
+ {!!label && (
63
+ <FormLabel required={required} tooltip={tooltip} description={description}>
64
+ {label}
65
+ </FormLabel>
66
+ )}
56
67
  <TextField
57
68
  fullWidth
58
69
  error={!!get(formState.errors, name)}
@@ -0,0 +1,59 @@
1
+ import { InfoOutlined } from '@mui/icons-material';
2
+ import { Box, FormLabel, Tooltip, Typography } from '@mui/material';
3
+ import type { FormLabelProps } from '@mui/material';
4
+ import type { ReactNode } from 'react';
5
+
6
+ export default function CustomFormLabel({
7
+ children,
8
+ required = false,
9
+ tooltip = '',
10
+ description = '',
11
+ ...props
12
+ }: FormLabelProps & { required?: boolean; tooltip?: ReactNode | string; description?: ReactNode | string }) {
13
+ return (
14
+ <Box sx={{ mb: 1, width: '100%' }}>
15
+ <FormLabel
16
+ {...props}
17
+ sx={{
18
+ display: 'flex',
19
+ alignItems: 'center',
20
+ gap: 0.5,
21
+ fontSize: '0.875rem',
22
+ fontWeight: 500,
23
+ color: 'text.primary',
24
+ '&.MuiFormLabel-root': {
25
+ display: 'flex',
26
+ alignItems: 'center',
27
+ gap: 0.5,
28
+ fontWeight: 500,
29
+ color: 'text.primary',
30
+ },
31
+ ...(props.sx || {}),
32
+ }}>
33
+ {children}
34
+ {required && (
35
+ <Typography component="span" color="error">
36
+ *
37
+ </Typography>
38
+ )}
39
+ {tooltip &&
40
+ (typeof tooltip === 'string' ? (
41
+ <Tooltip title={tooltip}>
42
+ <InfoOutlined fontSize="small" sx={{ opacity: 0.7, fontSize: '1rem' }} />
43
+ </Tooltip>
44
+ ) : (
45
+ tooltip
46
+ ))}
47
+ </FormLabel>
48
+ {description && (
49
+ <Typography
50
+ variant="caption"
51
+ sx={{
52
+ color: 'text.secondary',
53
+ }}>
54
+ {description}
55
+ </Typography>
56
+ )}
57
+ </Box>
58
+ );
59
+ }
@@ -28,7 +28,7 @@ export default function LoadingButton({
28
28
  sx,
29
29
  ...props
30
30
  }: LoadingButtonProps & {
31
- ref?: React.RefObject<HTMLButtonElement>;
31
+ ref?: React.RefObject<HTMLButtonElement | null>;
32
32
  }) {
33
33
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
34
34
  if (loading) {
@@ -0,0 +1,276 @@
1
+ /* eslint-disable @typescript-eslint/indent */
2
+ /* eslint-disable react/require-default-props */
3
+ /* eslint-disable react/no-unused-prop-types */
4
+ /* eslint-disable @typescript-eslint/naming-convention */
5
+ /* eslint-disable no-nested-ternary */
6
+ /* eslint-disable react/no-unstable-nested-components */
7
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
8
+ import type { Paginated, TCreditGrantExpanded } from '@blocklet/payment-types';
9
+ import { Box, Typography, Chip } from '@mui/material';
10
+ import { useRequest } from 'ahooks';
11
+ import React, { useEffect, useRef, useState } from 'react';
12
+ // eslint-disable-next-line import/no-extraneous-dependencies
13
+ import { useNavigate } from 'react-router-dom';
14
+
15
+ import { styled } from '@mui/system';
16
+ import { formatBNStr, formatToDate } from '../../libs/util';
17
+ import { usePaymentContext } from '../../contexts/payment';
18
+ import api from '../../libs/api';
19
+ import Table from '../../components/table';
20
+ import { createLink, handleNavigation } from '../../libs/navigation';
21
+
22
+ type Result = Paginated<TCreditGrantExpanded>;
23
+
24
+ const fetchData = (params: Record<string, any> = {}): Promise<Result> => {
25
+ const search = new URLSearchParams();
26
+ Object.keys(params).forEach((key) => {
27
+ if (params[key]) {
28
+ search.set(key, String(params[key]));
29
+ }
30
+ });
31
+
32
+ return api.get(`/api/credit-grants?${search.toString()}`).then((res: any) => res.data);
33
+ };
34
+
35
+ type Props = {
36
+ customer_id?: string;
37
+ subscription_id?: string;
38
+ status?: string;
39
+ pageSize?: number;
40
+ onTableDataChange?: Function;
41
+ mode?: 'dashboard' | 'portal';
42
+ };
43
+
44
+ export function StatusChip({ status, label }: { status: string; label?: string }) {
45
+ const getStatusColor = (statusValue: string) => {
46
+ switch (statusValue) {
47
+ case 'granted':
48
+ return 'success';
49
+ case 'pending':
50
+ return 'warning';
51
+ case 'expired':
52
+ return 'default';
53
+ case 'depleted':
54
+ return 'default';
55
+ case 'voided':
56
+ return 'default';
57
+ default:
58
+ return 'default';
59
+ }
60
+ };
61
+
62
+ return <Chip label={label || status} size="small" color={getStatusColor(status) as any} />;
63
+ }
64
+
65
+ const getLink = (grant: TCreditGrantExpanded, inDashboard: boolean) => {
66
+ let path = `/customer/credit-grant/${grant.id}`;
67
+ if (inDashboard) {
68
+ path = `/admin/customers/${grant.id}`;
69
+ }
70
+
71
+ return {
72
+ link: createLink(path),
73
+ connect: false,
74
+ };
75
+ };
76
+
77
+ const GrantsTable = React.memo((props: Props) => {
78
+ const { pageSize, status = '', customer_id, subscription_id, onTableDataChange } = props;
79
+ const listKey = 'credit-grants-table';
80
+ const { t, locale } = useLocaleContext();
81
+ const { session } = usePaymentContext();
82
+ const navigate = useNavigate();
83
+
84
+ const isAdmin = ['owner', 'admin'].includes(session?.user?.role || '');
85
+
86
+ const inDashboard = props.mode === 'dashboard' && isAdmin;
87
+
88
+ // 如果没有传入 customer_id,使用当前登录用户的 DID
89
+ const effectiveCustomerId = customer_id || session?.user?.did;
90
+
91
+ const [search, setSearch] = useState<{ pageSize: number; page: number }>({
92
+ pageSize: pageSize || 10,
93
+ page: 1,
94
+ });
95
+
96
+ const { loading, data = { list: [], count: 0 } } = useRequest(
97
+ () =>
98
+ fetchData({
99
+ ...search,
100
+ status,
101
+ customer_id: effectiveCustomerId,
102
+ subscription_id,
103
+ }),
104
+ {
105
+ refreshDeps: [search, status, effectiveCustomerId, subscription_id],
106
+ }
107
+ );
108
+
109
+ const prevData = useRef(data);
110
+
111
+ const handleLinkClick = (e: React.MouseEvent, grant: TCreditGrantExpanded) => {
112
+ const { link } = getLink(grant, inDashboard);
113
+ handleNavigation(e, link, navigate, { target: link.external ? '_blank' : '_self' });
114
+ };
115
+
116
+ useEffect(() => {
117
+ if (onTableDataChange) {
118
+ onTableDataChange(data, prevData.current);
119
+ prevData.current = data;
120
+ }
121
+ // eslint-disable-next-line react-hooks/exhaustive-deps
122
+ }, [data]);
123
+
124
+ const columns = [
125
+ {
126
+ label: t('common.name'),
127
+ name: 'name',
128
+ options: {
129
+ customBodyRenderLite: (_: string, index: number) => {
130
+ const grant = data?.list[index] as TCreditGrantExpanded;
131
+ return <Box onClick={(e) => handleLinkClick(e, grant)}>{grant.name || grant.id}</Box>;
132
+ },
133
+ },
134
+ },
135
+ {
136
+ label: t('common.status'),
137
+ name: 'status',
138
+ options: {
139
+ customBodyRenderLite: (_: string, index: number) => {
140
+ const grant = data?.list[index] as TCreditGrantExpanded;
141
+ return (
142
+ <Box onClick={(e) => handleLinkClick(e, grant)}>
143
+ <StatusChip status={grant.status} label={t(`admin.customer.creditGrants.status.${grant.status}`)} />
144
+ </Box>
145
+ );
146
+ },
147
+ },
148
+ },
149
+ {
150
+ label: t('common.remainingCredit'),
151
+ name: 'remaining_amount',
152
+ align: 'right',
153
+ options: {
154
+ customBodyRenderLite: (_: string, index: number) => {
155
+ const grant = data?.list[index] as TCreditGrantExpanded;
156
+ return (
157
+ <Box onClick={(e) => handleLinkClick(e, grant)}>
158
+ <Typography variant="body2">
159
+ {formatBNStr(grant.remaining_amount, grant.paymentCurrency.decimal)} {grant.paymentCurrency.symbol}
160
+ </Typography>
161
+ </Box>
162
+ );
163
+ },
164
+ },
165
+ },
166
+ {
167
+ label: t('common.scope'),
168
+ name: 'scope',
169
+ options: {
170
+ customBodyRenderLite: (_: string, index: number) => {
171
+ const grant = data?.list[index] as TCreditGrantExpanded;
172
+ let scope = 'general';
173
+ if (grant.applicability_config?.scope?.prices) {
174
+ scope = 'specific';
175
+ }
176
+ return (
177
+ <Box onClick={(e) => handleLinkClick(e, grant)}>
178
+ {scope === 'specific' ? t('common.specific') : t('common.general')}
179
+ </Box>
180
+ );
181
+ },
182
+ },
183
+ },
184
+ {
185
+ label: t('common.effectiveDate'),
186
+ name: 'effective_at',
187
+ options: {
188
+ customBodyRenderLite: (_: string, index: number) => {
189
+ const grant = data?.list[index] as TCreditGrantExpanded;
190
+ const effectiveAt = grant.effective_at ? grant.effective_at * 1000 : grant.created_at;
191
+ return (
192
+ <Box onClick={(e) => handleLinkClick(e, grant)}>
193
+ {formatToDate(effectiveAt, locale, 'YYYY-MM-DD HH:mm')}
194
+ </Box>
195
+ );
196
+ },
197
+ },
198
+ },
199
+ {
200
+ label: t('common.expirationDate'),
201
+ name: 'expires_at',
202
+ options: {
203
+ customBodyRenderLite: (_: string, index: number) => {
204
+ const grant = data?.list[index] as TCreditGrantExpanded;
205
+ return (
206
+ <Box onClick={(e) => handleLinkClick(e, grant)}>
207
+ <Typography variant="body2">
208
+ {grant.expires_at ? formatToDate(grant.expires_at * 1000, locale, 'YYYY-MM-DD HH:mm') : '-'}
209
+ </Typography>
210
+ </Box>
211
+ );
212
+ },
213
+ },
214
+ },
215
+ ];
216
+
217
+ const onTableChange = ({ page, rowsPerPage }: any) => {
218
+ if (search.pageSize !== rowsPerPage) {
219
+ setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
220
+ } else if (search.page !== page + 1) {
221
+ setSearch((x) => ({ ...x, page: page + 1 }));
222
+ }
223
+ };
224
+
225
+ return (
226
+ <TableRoot>
227
+ <Table
228
+ hasRowLink
229
+ durable={`__${listKey}__`}
230
+ durableKeys={['page', 'rowsPerPage', 'searchText']}
231
+ data={data.list}
232
+ columns={columns}
233
+ options={{
234
+ count: data.count,
235
+ page: search.page - 1,
236
+ rowsPerPage: search.pageSize,
237
+ }}
238
+ loading={loading}
239
+ onChange={onTableChange}
240
+ toolbar={false}
241
+ sx={{ mt: 2 }}
242
+ showMobile={false}
243
+ mobileTDFlexDirection="row"
244
+ emptyNodeText={t('admin.creditGrants.noGrants')}
245
+ />
246
+ </TableRoot>
247
+ );
248
+ });
249
+
250
+ const TableRoot = styled(Box)`
251
+ @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
252
+ .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
253
+ > div {
254
+ width: fit-content;
255
+ flex: inherit;
256
+ font-size: 14px;
257
+ }
258
+ }
259
+ .invoice-summary {
260
+ padding-right: 20px;
261
+ }
262
+ }
263
+ `;
264
+ export default function CreditGrantsList(rawProps: Props) {
265
+ const props = Object.assign(
266
+ {
267
+ customer_id: '',
268
+ subscription_id: '',
269
+ status: 'granted,pending,depleted,expired',
270
+ pageSize: 10,
271
+ onTableDataChange: () => {},
272
+ },
273
+ rawProps
274
+ );
275
+ return <GrantsTable {...props} />;
276
+ }
@@ -0,0 +1,316 @@
1
+ /* eslint-disable @typescript-eslint/indent */
2
+ /* eslint-disable react/require-default-props */
3
+ /* eslint-disable react/no-unused-prop-types */
4
+ /* eslint-disable @typescript-eslint/naming-convention */
5
+ /* eslint-disable no-nested-ternary */
6
+ /* eslint-disable react/no-unstable-nested-components */
7
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
8
+ import type { Paginated, TCreditTransactionExpanded } from '@blocklet/payment-types';
9
+ import { Box, Typography, Stack, Link, Grid } from '@mui/material';
10
+ import { useRequest } from 'ahooks';
11
+ // eslint-disable-next-line import/no-extraneous-dependencies
12
+ import { useNavigate } from 'react-router-dom';
13
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
14
+ import { joinURL } from 'ufo';
15
+ import { styled } from '@mui/system';
16
+ import DateRangePicker, { type DateRangeValue } from '../../components/date-range-picker';
17
+ // eslint-disable-next-line import/no-extraneous-dependencies
18
+
19
+ import { formatBNStr, formatToDate, getPrefix } from '../../libs/util';
20
+ import { usePaymentContext } from '../../contexts/payment';
21
+ import api from '../../libs/api';
22
+ import Table from '../../components/table';
23
+ import { createLink, handleNavigation } from '../../libs/navigation';
24
+
25
+ type Result = Paginated<TCreditTransactionExpanded>;
26
+
27
+ const fetchData = (params: Record<string, any> = {}): Promise<Result> => {
28
+ const search = new URLSearchParams();
29
+ Object.keys(params).forEach((key) => {
30
+ if (params[key]) {
31
+ search.set(key, String(params[key]));
32
+ }
33
+ });
34
+
35
+ return api.get(`/api/credit-transactions?${search.toString()}`).then((res: any) => res.data);
36
+ };
37
+
38
+ type Props = {
39
+ customer_id?: string;
40
+ subscription_id?: string;
41
+ credit_grant_id?: string;
42
+ pageSize?: number;
43
+ onTableDataChange?: Function;
44
+ showAdminColumns?: boolean;
45
+ showTimeFilter?: boolean;
46
+ source?: string;
47
+ mode?: 'dashboard' | 'portal';
48
+ };
49
+
50
+ const getGrantDetailLink = (grantId: string, inDashboard: boolean) => {
51
+ let path = `/customer/credit-grant/${grantId}`;
52
+ if (inDashboard) {
53
+ path = `/admin/customers/${grantId}`;
54
+ }
55
+
56
+ return {
57
+ link: createLink(path),
58
+ connect: false,
59
+ };
60
+ };
61
+
62
+ const TransactionsTable = React.memo((props: Props) => {
63
+ const {
64
+ pageSize,
65
+ customer_id,
66
+ subscription_id,
67
+ credit_grant_id,
68
+ onTableDataChange,
69
+ showAdminColumns = false,
70
+ showTimeFilter = false,
71
+ source,
72
+ mode = 'portal',
73
+ } = props;
74
+ const listKey = 'credit-transactions-table';
75
+ const { t, locale } = useLocaleContext();
76
+ const { session } = usePaymentContext();
77
+ const isAdmin = ['owner', 'admin'].includes(session?.user?.role || '');
78
+ const navigate = useNavigate();
79
+
80
+ // 如果没有传入 customer_id,使用当前登录用户的 DID
81
+ const effectiveCustomerId = customer_id || session?.user?.did;
82
+
83
+ const [search, setSearch] = useState<{
84
+ pageSize: number;
85
+ page: number;
86
+ start?: number;
87
+ end?: number;
88
+ }>({
89
+ pageSize: pageSize || 10,
90
+ page: 1,
91
+ });
92
+
93
+ const [filters, setFilters] = useState<DateRangeValue>({
94
+ start: undefined,
95
+ end: undefined,
96
+ });
97
+
98
+ const handleDateRangeChange = useCallback((newValue: DateRangeValue) => {
99
+ setFilters(newValue);
100
+ setSearch((prev) => ({
101
+ ...prev,
102
+ page: 1,
103
+ start: newValue.start || undefined,
104
+ end: newValue.end || undefined,
105
+ }));
106
+ }, []);
107
+
108
+ const { loading, data = { list: [], count: 0 } } = useRequest(
109
+ () =>
110
+ fetchData({
111
+ ...search,
112
+ customer_id: effectiveCustomerId,
113
+ subscription_id,
114
+ credit_grant_id,
115
+ source,
116
+ }),
117
+ {
118
+ refreshDeps: [search, effectiveCustomerId, subscription_id, credit_grant_id, source],
119
+ }
120
+ );
121
+
122
+ // 初始化时应用默认日期筛选
123
+ useEffect(() => {
124
+ if (showTimeFilter && !search.start && !search.end) {
125
+ handleDateRangeChange(filters);
126
+ }
127
+ }, [showTimeFilter, handleDateRangeChange, search.start, search.end, filters]);
128
+
129
+ const prevData = useRef(data);
130
+
131
+ useEffect(() => {
132
+ if (onTableDataChange) {
133
+ onTableDataChange(data, prevData.current);
134
+ prevData.current = data;
135
+ }
136
+ // eslint-disable-next-line react-hooks/exhaustive-deps
137
+ }, [data]);
138
+
139
+ const columns = [
140
+ {
141
+ label: t('common.creditAmount'),
142
+ name: 'credit_amount',
143
+ align: 'right',
144
+ options: {
145
+ customBodyRenderLite: (_: string, index: number) => {
146
+ const transaction = data?.list[index] as TCreditTransactionExpanded;
147
+ const unit = transaction.meter?.unit || transaction.paymentCurrency.symbol;
148
+ return (
149
+ <Typography>
150
+ {formatBNStr(transaction.credit_amount, transaction.paymentCurrency.decimal)} {unit}
151
+ </Typography>
152
+ );
153
+ },
154
+ },
155
+ },
156
+ !credit_grant_id && {
157
+ label: t('common.creditGrant'),
158
+ name: 'credit_grant',
159
+ options: {
160
+ customBodyRenderLite: (_: string, index: number) => {
161
+ const transaction = data?.list[index] as TCreditTransactionExpanded;
162
+ return (
163
+ <Stack
164
+ direction="row"
165
+ spacing={1}
166
+ onClick={(e) => {
167
+ const link = getGrantDetailLink(transaction.credit_grant_id, isAdmin && mode === 'dashboard');
168
+ handleNavigation(e, link.link, navigate);
169
+ }}
170
+ sx={{
171
+ alignItems: 'center',
172
+ }}>
173
+ <Typography variant="body2" sx={{ color: 'text.link', cursor: 'pointer' }}>
174
+ {transaction.creditGrant.name || `Grant ${transaction.credit_grant_id.slice(-6)}`}
175
+ </Typography>
176
+ </Stack>
177
+ );
178
+ },
179
+ },
180
+ },
181
+ {
182
+ label: t('common.description'),
183
+ name: 'subscription',
184
+ options: {
185
+ customBodyRenderLite: (_: string, index: number) => {
186
+ const transaction = data?.list[index] as TCreditTransactionExpanded;
187
+ return (
188
+ <Typography variant="body2">{transaction.subscription?.description || transaction.description}</Typography>
189
+ );
190
+ },
191
+ },
192
+ },
193
+ ...(showAdminColumns && isAdmin
194
+ ? [
195
+ {
196
+ label: t('common.meterEvent'),
197
+ name: 'meter_event',
198
+ options: {
199
+ customBodyRenderLite: (_: string, index: number) => {
200
+ const transaction = data?.list[index] as TCreditTransactionExpanded;
201
+ if (!transaction.meter) {
202
+ return <Typography variant="body2">-</Typography>;
203
+ }
204
+ return (
205
+ <Link href={joinURL(getPrefix(), `/admin/billing/${transaction.meter.id}`)}>
206
+ <Typography variant="body2" sx={{ color: 'text.link' }}>
207
+ {transaction.meter.event_name}
208
+ </Typography>
209
+ </Link>
210
+ );
211
+ },
212
+ },
213
+ },
214
+ ]
215
+ : []),
216
+ {
217
+ label: t('admin.creditTransactions.transactionDate'),
218
+ name: 'created_at',
219
+ options: {
220
+ customBodyRenderLite: (_: string, index: number) => {
221
+ const transaction = data?.list[index] as TCreditTransactionExpanded;
222
+ return (
223
+ <Typography variant="body2">
224
+ {formatToDate(transaction.created_at, locale, 'YYYY-MM-DD HH:mm:ss')}
225
+ </Typography>
226
+ );
227
+ },
228
+ },
229
+ },
230
+ ].filter(Boolean);
231
+
232
+ const onTableChange = ({ page, rowsPerPage }: any) => {
233
+ if (search.pageSize !== rowsPerPage) {
234
+ setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
235
+ } else if (search.page !== page + 1) {
236
+ setSearch((x) => ({ ...x, page: page + 1 }));
237
+ }
238
+ };
239
+
240
+ return (
241
+ <TableRoot>
242
+ {showTimeFilter && (
243
+ <Box sx={{ my: 2 }}>
244
+ <Box sx={{ mt: 2 }}>
245
+ <Grid
246
+ container
247
+ spacing={2}
248
+ sx={{
249
+ alignItems: 'center',
250
+ }}>
251
+ <Grid
252
+ size={{
253
+ xs: 12,
254
+ sm: 6,
255
+ md: 4,
256
+ }}>
257
+ <DateRangePicker value={filters} onChange={handleDateRangeChange} size="small" fullWidth />
258
+ </Grid>
259
+ </Grid>
260
+ </Box>
261
+ </Box>
262
+ )}
263
+ <Table
264
+ hasRowLink
265
+ durable={`__${listKey}__`}
266
+ durableKeys={['page', 'rowsPerPage']}
267
+ data={data.list}
268
+ columns={columns}
269
+ options={{
270
+ count: data.count,
271
+ page: search.page - 1,
272
+ rowsPerPage: search.pageSize,
273
+ }}
274
+ loading={loading}
275
+ onChange={onTableChange}
276
+ toolbar={false}
277
+ sx={{ mt: 2 }}
278
+ showMobile={false}
279
+ mobileTDFlexDirection="row"
280
+ emptyNodeText={t('admin.creditTransactions.noTransactions')}
281
+ />
282
+ </TableRoot>
283
+ );
284
+ });
285
+
286
+ const TableRoot = styled(Box)`
287
+ @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
288
+ .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
289
+ > div {
290
+ width: fit-content;
291
+ flex: inherit;
292
+ font-size: 14px;
293
+ }
294
+ }
295
+ .invoice-summary {
296
+ padding-right: 20px;
297
+ }
298
+ }
299
+ `;
300
+ export default function CreditTransactionsList(rawProps: Props) {
301
+ const props = Object.assign(
302
+ {
303
+ customer_id: '',
304
+ subscription_id: '',
305
+ credit_grant_id: '',
306
+ source: '',
307
+ pageSize: 10,
308
+ onTableDataChange: () => {},
309
+ showAdminColumns: false,
310
+ showTimeFilter: false,
311
+ mode: 'portal',
312
+ },
313
+ rawProps
314
+ );
315
+ return <TransactionsTable {...props} />;
316
+ }