@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.
@@ -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, Chip } from '@mui/material';
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, { useCallback, useEffect, useRef, useState } from '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 getTransactionDetailLink = (transactionId: string, inDashboard: boolean) => {
76
- let path = `/customer/credit-transaction/${transactionId}`;
75
+ const getSubscriptionDetailLink = (subscriptionId: string, inDashboard: boolean) => {
76
+ let path = `/customer/subscription/${subscriptionId}`;
77
77
  if (inDashboard) {
78
- path = `/admin/customers/${transactionId}`;
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 = useCallback((newValue: DateRangeValue) => {
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
- handleDateRangeChange(filters);
261
+ setSearch((prev) => ({
262
+ ...prev,
263
+ page: 1,
264
+ start: filters.start || undefined,
265
+ end: filters.end || undefined,
266
+ }));
153
267
  }
154
- }, [showTimeFilter, handleDateRangeChange, search.start, search.end, filters]);
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
- if (item.activity_type === 'grant') {
168
- const { link } = getGrantDetailLink(item.id, isAdmin && mode === 'dashboard');
169
- handleNavigation(e, link, navigate, { target: link.external ? '_blank' : '_self' });
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.amount'),
178
- name: 'credit_amount',
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
- <Stack direction="row" spacing={1} alignItems="center" justifyContent="flex-end" sx={{ width: '100%' }}>
210
- {isExpiredGrant && (
211
- <Chip
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.creditGrant'),
233
- name: 'credit_grant',
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.activity_type === 'grant';
238
- const isExpiredGrant = isGrant && item.status === 'expired';
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
- <Stack
249
- direction="row"
250
- spacing={1}
251
- onClick={(e) => {
252
- const link = getGrantDetailLink(grantId, isAdmin && mode === 'dashboard');
253
- handleNavigation(e, link.link, navigate);
254
- }}
255
- sx={{
256
- alignItems: 'center',
257
- }}>
258
- {nameNode}
259
- </Stack>
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.description'),
266
- name: 'description',
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.activity_type === 'grant';
271
- const isExpiredGrant = isGrant && item.status === 'expired';
272
- const description = isGrant
273
- ? item.name || item.description || 'Credit Granted'
274
- : item.subscription?.description || item.description || `${item.meter_event_name} usage`;
275
-
276
- const descriptionNode = (
277
- <Typography variant="body2" sx={{ fontWeight: 400, color: isExpiredGrant ? 'text.disabled' : undefined }}>
278
- {description}
279
- </Typography>
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 <Box onClick={(e) => handleTransactionClick(e, item)}>{descriptionNode}</Box>;
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.activity_type === 'grant';
337
- const invoiceId = isGrant ? item.metadata?.invoice_id : null;
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
- {isGrant && invoiceId && (
342
- <Button
343
- variant="text"
344
- size="small"
345
- color="primary"
346
- onClick={(e) => {
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.relatedSubscription'),
260
- name: 'subscription',
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
- return invoice.subscription_id ? (
265
- <Box
266
- onClick={(e) => handleRelatedSubscriptionClick(e, invoice)}
267
- sx={{ color: 'text.link', cursor: 'pointer' }}>
268
- {invoice.subscription?.description}
269
- </Box>
270
- ) : (
271
- <Box onClick={(e) => handleLinkClick(e, invoice)} sx={{ ...linkStyle, color: 'text.lighter' }}>
272
- {t('common.none')}
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',