@blocklet/payment-react 1.24.2 → 1.24.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/subscriptions/${subscriptionId}`;
79
79
  }
80
80
 
81
81
  return {
@@ -84,6 +84,101 @@ const getTransactionDetailLink = (transactionId: string, inDashboard: boolean) =
84
84
  };
85
85
  };
86
86
 
87
+ const getMeterEventDetailLink = (meterEventId: string) => {
88
+ // Meter event detail page is only available in admin dashboard
89
+ const path = `/admin/billing/${meterEventId}`;
90
+
91
+ return {
92
+ link: createLink(path),
93
+ connect: false,
94
+ };
95
+ };
96
+
97
+ const getSubscriptionId = (item: any) =>
98
+ item.metadata?.subscription_id || item.subscription_id || item.invoice?.subscription_id;
99
+
100
+ const getInvoiceId = (item: any) => item.metadata?.invoice_id || item.invoice?.id;
101
+ const getMeterEventId = (item: any) => item.source || item.metadata?.meter_event_id;
102
+
103
+ // Extract credit activity classification logic for table columns reuse
104
+ const getCreditActivityFlags = (item: any) => {
105
+ const isGrant = item.activity_type === 'grant';
106
+ const isScheduled = isGrant && item.metadata?.delivery_mode === 'schedule';
107
+ const isDepleted = isGrant && item.status === 'depleted';
108
+ const isExpired = isGrant && (item.status === 'expired' || item.status === 'voided');
109
+ const isInactive = isDepleted || isExpired;
110
+
111
+ return { isGrant, isScheduled, isDepleted, isExpired, isInactive };
112
+ };
113
+
114
+ const getTransactionDetailLink = (item: any, inDashboard: boolean) => {
115
+ if (item.activity_type === 'grant') {
116
+ const invoiceId = getInvoiceId(item);
117
+ if (invoiceId) {
118
+ return getInvoiceDetailLink(invoiceId, inDashboard);
119
+ }
120
+
121
+ return getGrantDetailLink(item.id, inDashboard);
122
+ }
123
+
124
+ const meterEventId = getMeterEventId(item);
125
+ if (!meterEventId) {
126
+ return null;
127
+ }
128
+
129
+ return getMeterEventDetailLink(meterEventId);
130
+ };
131
+
132
+ const getTransactionDescription = (item: any, t: Function) => {
133
+ const { isGrant, isScheduled, isInactive } = getCreditActivityFlags(item);
134
+ const isPaid = isGrant && item.category === 'paid' && (!isScheduled || item.metadata?.schedule_seq === 1);
135
+
136
+ if (!isGrant) {
137
+ const secondLine = item.metadata?.is_repayment ? t('common.creditActivity.repayment') : item.description || '';
138
+ return {
139
+ isGrant,
140
+ isInactive,
141
+ activityType: t('common.creditActivity.consumption'),
142
+ secondLine,
143
+ };
144
+ }
145
+
146
+ if (isPaid) {
147
+ let secondLine = item.description || '';
148
+ if (item.invoice?.total && item.invoice?.paymentCurrency) {
149
+ const invoiceCurrency = item.invoice.paymentCurrency;
150
+ const paidAmount = formatCreditAmount(
151
+ formatBNStr(item.invoice.total, invoiceCurrency.decimal || 0),
152
+ invoiceCurrency.symbol || ''
153
+ );
154
+ secondLine = t('common.creditActivity.paidAmount', { amount: paidAmount });
155
+ }
156
+
157
+ return {
158
+ isGrant,
159
+ isInactive,
160
+ activityType: t('common.creditActivity.paidGrant'),
161
+ secondLine,
162
+ };
163
+ }
164
+
165
+ if (isScheduled) {
166
+ return {
167
+ isGrant,
168
+ isInactive,
169
+ activityType: t('common.creditActivity.resetGrant'),
170
+ secondLine: item.description || '',
171
+ };
172
+ }
173
+
174
+ return {
175
+ isGrant,
176
+ isInactive,
177
+ activityType: t('common.creditActivity.promotionalGrant'),
178
+ secondLine: item.description || '',
179
+ };
180
+ };
181
+
87
182
  const TransactionsTable = React.memo((props: Props) => {
88
183
  const {
89
184
  pageSize,
@@ -101,6 +196,7 @@ const TransactionsTable = React.memo((props: Props) => {
101
196
  const { t, locale } = useLocaleContext();
102
197
  const { session } = usePaymentContext();
103
198
  const isAdmin = ['owner', 'admin'].includes(session?.user?.role || '');
199
+ const isDashboard = isAdmin && mode === 'dashboard';
104
200
  const navigate = useNavigate();
105
201
 
106
202
  // 如果没有传入 customer_id,使用当前登录用户的 DID
@@ -121,7 +217,7 @@ const TransactionsTable = React.memo((props: Props) => {
121
217
  end: undefined,
122
218
  });
123
219
 
124
- const handleDateRangeChange = useCallback((newValue: DateRangeValue) => {
220
+ const handleDateRangeChange = (newValue: DateRangeValue) => {
125
221
  setFilters(newValue);
126
222
  setSearch((prev) => ({
127
223
  ...prev,
@@ -129,7 +225,7 @@ const TransactionsTable = React.memo((props: Props) => {
129
225
  start: newValue.start || undefined,
130
226
  end: newValue.end || undefined,
131
227
  }));
132
- }, []);
228
+ };
133
229
 
134
230
  const { loading, data = { list: [], count: 0 } } = useRequest(
135
231
  () =>
@@ -149,9 +245,14 @@ const TransactionsTable = React.memo((props: Props) => {
149
245
  // 初始化时应用默认日期筛选
150
246
  useEffect(() => {
151
247
  if (showTimeFilter && !search.start && !search.end) {
152
- handleDateRangeChange(filters);
248
+ setSearch((prev) => ({
249
+ ...prev,
250
+ page: 1,
251
+ start: filters.start || undefined,
252
+ end: filters.end || undefined,
253
+ }));
153
254
  }
154
- }, [showTimeFilter, handleDateRangeChange, search.start, search.end, filters]);
255
+ }, [showTimeFilter, search.start, search.end, filters.start, filters.end]);
155
256
 
156
257
  const prevData = useRef(data);
157
258
 
@@ -164,122 +265,125 @@ const TransactionsTable = React.memo((props: Props) => {
164
265
  }, [data]);
165
266
 
166
267
  const handleTransactionClick = (e: React.MouseEvent, item: any) => {
167
- 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' });
268
+ const detail = getTransactionDetailLink(item, isDashboard);
269
+ if (!detail) {
270
+ return;
173
271
  }
272
+
273
+ handleNavigation(e, detail.link, navigate, { target: detail.link.external ? '_blank' : '_self' });
274
+ };
275
+ const openSubscription = (e: React.MouseEvent, subscriptionId: string) => {
276
+ e.preventDefault();
277
+ const link = getSubscriptionDetailLink(subscriptionId, isDashboard);
278
+ handleNavigation(e, link.link, navigate);
279
+ };
280
+
281
+ const openInvoice = (e: React.MouseEvent, invoiceId: string) => {
282
+ e.preventDefault();
283
+ const link = getInvoiceDetailLink(invoiceId, isDashboard);
284
+ handleNavigation(e, link.link, navigate);
174
285
  };
286
+
287
+ const renderActionButton = (label: string, onClick: (e: React.MouseEvent) => void) => (
288
+ <Button variant="text" size="small" color="primary" sx={{ whiteSpace: 'nowrap' }} onClick={onClick}>
289
+ {label}
290
+ </Button>
291
+ );
292
+
175
293
  const columns = [
176
294
  {
177
- label: t('common.amount'),
178
- name: 'credit_amount',
179
- align: 'right',
295
+ label: t('common.date'),
296
+ name: 'created_at',
180
297
  options: {
298
+ setCellProps: () => ({ style: { width: '25%' } }),
181
299
  customBodyRenderLite: (_: string, index: number) => {
182
300
  const item = data?.list[index] as any;
183
- const isGrant = item.activity_type === 'grant';
184
- const isExpiredGrant = isGrant && item.status === 'expired';
185
- const amount = isGrant ? item.amount : item.credit_amount;
186
- const currency = item.paymentCurrency || item.currency;
187
- const unit = !isGrant && item.meter?.unit ? item.meter.unit : currency?.symbol;
188
- const displayAmount = formatCreditAmount(formatBNStr(amount, currency?.decimal || 0), unit);
189
-
190
- if (!includeGrants) {
191
- return (
192
- <Box onClick={(e) => handleTransactionClick(e, item)}>
193
- <Typography>{displayAmount}</Typography>
194
- </Box>
195
- );
196
- }
197
-
198
- const amountNode = (
199
- <Typography
200
- sx={{
201
- color: isGrant ? (isExpiredGrant ? 'text.disabled' : 'success.main') : 'error.main',
202
- }}>
203
- {isGrant ? '+' : '-'} {displayAmount}
204
- </Typography>
205
- );
206
-
207
301
  return (
208
302
  <Box onClick={(e) => handleTransactionClick(e, item)}>
209
- <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>
303
+ <Typography variant="body2" sx={{ fontSize: '0.875rem' }}>
304
+ {formatToDate(item.created_at, locale, 'YYYY-MM-DD HH:mm')}
305
+ </Typography>
226
306
  </Box>
227
307
  );
228
308
  },
229
309
  },
230
310
  },
231
311
  {
232
- label: t('common.creditGrant'),
233
- name: 'credit_grant',
312
+ label: t('common.description'),
313
+ name: 'description',
234
314
  options: {
315
+ setCellProps: () => ({ style: { width: '25%' } }),
235
316
  customBodyRenderLite: (_: string, index: number) => {
236
317
  const item = data?.list[index] as any;
237
- const isGrant = item.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
- );
318
+ const { activityType, secondLine, isInactive, isGrant } = getTransactionDescription(item, t);
319
+
247
320
  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>
321
+ <Box onClick={(e) => handleTransactionClick(e, item)} sx={{ cursor: 'pointer' }}>
322
+ <Stack direction="column" spacing={0.25}>
323
+ <Typography
324
+ variant="body2"
325
+ sx={{
326
+ color: isInactive ? 'text.secondary' : isGrant ? 'success.main' : 'error.main',
327
+ }}>
328
+ {activityType}
329
+ </Typography>
330
+ {secondLine && (
331
+ <Typography variant="caption" sx={{ color: 'text.secondary' }}>
332
+ {secondLine}
333
+ </Typography>
334
+ )}
335
+ </Stack>
336
+ </Box>
260
337
  );
261
338
  },
262
339
  },
263
340
  },
264
341
  {
265
- label: t('common.description'),
266
- name: 'description',
342
+ label: t('common.amount'),
343
+ name: 'credit_amount',
344
+ align: 'right',
267
345
  options: {
346
+ setCellProps: () => ({ style: { width: '20%' } }),
347
+ customHeadLabelRender: () => <Box sx={{ pr: 5 }}>{t('common.amount')}</Box>,
268
348
  customBodyRenderLite: (_: string, index: number) => {
269
349
  const item = data?.list[index] as any;
270
- const isGrant = item.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
- );
350
+ const { isGrant, isDepleted, isExpired, isInactive } = getCreditActivityFlags(item);
351
+ const amount = isGrant ? item.amount : item.credit_amount;
352
+ const currency = item.paymentCurrency || item.currency;
353
+ const unit = !isGrant && item.meter?.unit ? item.meter.unit : currency?.symbol;
354
+ const displayAmount = formatCreditAmount(formatBNStr(amount, currency?.decimal || 0), unit);
355
+
356
+ if (!includeGrants) {
357
+ return (
358
+ <Box onClick={(e) => handleTransactionClick(e, item)} sx={{ pr: 5 }}>
359
+ <Typography>{displayAmount}</Typography>
360
+ </Box>
361
+ );
362
+ }
281
363
 
282
- return <Box onClick={(e) => handleTransactionClick(e, item)}>{descriptionNode}</Box>;
364
+ return (
365
+ <Box onClick={(e) => handleTransactionClick(e, item)} sx={{ pr: 5 }}>
366
+ <Stack direction="column" spacing={0.25} alignItems="flex-end">
367
+ <Typography
368
+ sx={{
369
+ fontWeight: 500,
370
+ color: isInactive ? 'text.secondary' : isGrant ? 'success.main' : 'error.main',
371
+ whiteSpace: 'nowrap',
372
+ }}>
373
+ {isGrant ? '+' : '-'} {displayAmount}
374
+ </Typography>
375
+ {isDepleted ? (
376
+ <Typography variant="caption" sx={{ color: 'text.secondary' }}>
377
+ {t('common.consumed')}
378
+ </Typography>
379
+ ) : isExpired ? (
380
+ <Typography variant="caption" sx={{ color: 'text.secondary' }}>
381
+ {t('common.expired')}
382
+ </Typography>
383
+ ) : null}
384
+ </Stack>
385
+ </Box>
386
+ );
283
387
  },
284
388
  },
285
389
  },
@@ -306,64 +410,27 @@ const TransactionsTable = React.memo((props: Props) => {
306
410
  },
307
411
  ]
308
412
  : []),
309
- {
310
- label: t('common.date'),
311
- name: 'created_at',
312
- options: {
313
- customBodyRenderLite: (_: string, index: number) => {
314
- const item = data?.list[index] as any;
315
- return (
316
- <Box onClick={(e) => handleTransactionClick(e, item)}>
317
- <Typography
318
- variant="body2"
319
- sx={{
320
- color: 'text.secondary',
321
- fontSize: '0.875rem',
322
- }}>
323
- {formatToDate(item.created_at, locale, 'YYYY-MM-DD HH:mm')}
324
- </Typography>
325
- </Box>
326
- );
327
- },
328
- },
329
- },
330
413
  {
331
414
  label: t('common.actions'),
332
415
  name: 'actions',
333
416
  options: {
417
+ setCellProps: () => ({ style: { width: '25%' } }),
334
418
  customBodyRenderLite: (_: string, index: number) => {
335
419
  const item = data?.list[index] as any;
336
- const isGrant = item.activity_type === 'grant';
337
- const invoiceId = isGrant ? item.metadata?.invoice_id : null;
420
+ const { isGrant, isScheduled } = getCreditActivityFlags(item);
421
+ const isPaid = isGrant && item.category === 'paid' && !isScheduled;
422
+ const subscriptionId = getSubscriptionId(item);
423
+ const invoiceId = isGrant ? getInvoiceId(item) : null;
424
+ const shouldShowSubscription = Boolean(subscriptionId) && (!isGrant || isScheduled || isPaid);
338
425
 
339
426
  return (
340
- <Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
341
- {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
- )}
427
+ <Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'nowrap' }}>
428
+ {shouldShowSubscription && subscriptionId
429
+ ? renderActionButton(t('common.viewSubscription'), (e) => openSubscription(e, subscriptionId))
430
+ : null}
431
+ {isPaid && invoiceId
432
+ ? renderActionButton(t('common.viewInvoice'), (e) => openInvoice(e, invoiceId))
433
+ : null}
367
434
  </Box>
368
435
  );
369
436
  },
@@ -23,6 +23,7 @@ import api from '../../libs/api';
23
23
  import StripePaymentAction from '../../components/stripe-payment-action';
24
24
  import {
25
25
  formatBNStr,
26
+ formatCreditAmount,
26
27
  formatError,
27
28
  formatToDate,
28
29
  formatToDatetime,
@@ -112,6 +113,7 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
112
113
  const listKey = 'invoice-table';
113
114
  const { t, locale } = useLocaleContext();
114
115
  const navigate = useNavigate();
116
+ const { getCurrency } = usePaymentContext();
115
117
 
116
118
  const [search, setSearch] = useState<{ pageSize: number; page: number }>({
117
119
  pageSize: pageSize || 10,
@@ -256,25 +258,126 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
256
258
  ...(relatedSubscription
257
259
  ? [
258
260
  {
259
- label: t('common.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',