payment-kit 1.20.13 → 1.20.14

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