/* eslint-disable @typescript-eslint/indent */ /* eslint-disable react/require-default-props */ /* eslint-disable react/no-unused-prop-types */ /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable no-nested-ternary */ /* eslint-disable react/no-unstable-nested-components */ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context'; import type { Paginated, TCreditTransactionExpanded } from '@blocklet/payment-types'; import { Box, Typography, Grid, Stack, Link, Button } from '@mui/material'; import { useRequest } from 'ahooks'; // eslint-disable-next-line import/no-extraneous-dependencies import { useNavigate } from 'react-router-dom'; import React, { useEffect, useRef, useState } from 'react'; import { styled } from '@mui/system'; import { joinURL } from 'ufo'; import DateRangePicker, { type DateRangeValue } from '../../components/date-range-picker'; // eslint-disable-next-line import/no-extraneous-dependencies import { formatToDate, getPrefix, formatCreditAmount, formatBNStr } from '../../libs/util'; import { usePaymentContext } from '../../contexts/payment'; import api from '../../libs/api'; import Table from '../../components/table'; import { createLink, handleNavigation } from '../../libs/navigation'; type Result = Paginated; const fetchData = (params: Record = {}): Promise => { const search = new URLSearchParams(); Object.keys(params).forEach((key) => { if (params[key]) { search.set(key, String(params[key])); } }); return api.get(`/api/credit-transactions?${search.toString()}`).then((res: any) => res.data); }; type Props = { customer_id?: string; subscription_id?: string; credit_grant_id?: string; pageSize?: number; onTableDataChange?: Function; showAdminColumns?: boolean; showTimeFilter?: boolean; includeGrants?: boolean; // Enable unified cash flow view source?: string; mode?: 'dashboard' | 'portal'; }; const getGrantDetailLink = (grantId: string, inDashboard: boolean) => { let path = `/customer/credit-grant/${grantId}`; if (inDashboard) { path = `/admin/customers/${grantId}`; } return { link: createLink(path), connect: false, }; }; const getInvoiceDetailLink = (invoiceId: string, inDashboard: boolean) => { let path = `/customer/invoice/${invoiceId}`; if (inDashboard) { path = `/admin/billing/${invoiceId}`; } return { link: createLink(path), connect: false, }; }; const getSubscriptionDetailLink = (subscriptionId: string, inDashboard: boolean) => { let path = `/customer/subscription/${subscriptionId}`; if (inDashboard) { path = `/admin/billing/${subscriptionId}`; } return { link: createLink(path), connect: false, }; }; const getCreditTransactionDetailLink = (transactionId: string) => { const path = `/customer/credit-transaction/${transactionId}`; return { link: createLink(path), connect: false, }; }; const getMeterEventDetailLink = (meterEventId: string) => { // Meter event detail page is only available in admin dashboard const path = `/admin/billing/${meterEventId}`; return { link: createLink(path), connect: false, }; }; const getSubscriptionId = (item: any) => item.metadata?.subscription_id || item.subscription_id || item.invoice?.subscription_id; const getInvoiceId = (item: any) => item.metadata?.invoice_id || item.invoice?.id; const getMeterEventId = (item: any) => item.source || item.metadata?.meter_event_id; // Extract credit activity classification logic for table columns reuse const getCreditActivityFlags = (item: any) => { const isGrant = item.activity_type === 'grant'; const isScheduled = isGrant && item.metadata?.delivery_mode === 'schedule'; const isDepleted = isGrant && item.status === 'depleted'; const isExpired = isGrant && (item.status === 'expired' || item.status === 'voided'); const isInactive = isDepleted || isExpired; return { isGrant, isScheduled, isDepleted, isExpired, isInactive }; }; const getTransactionDetailLink = (item: any, inDashboard: boolean) => { if (item.activity_type === 'grant') { const invoiceId = getInvoiceId(item); if (invoiceId) { return getInvoiceDetailLink(invoiceId, inDashboard); } return getGrantDetailLink(item.id, inDashboard); } if (!inDashboard) { return getCreditTransactionDetailLink(item.id); } const meterEventId = getMeterEventId(item); if (!meterEventId) { return null; } return getMeterEventDetailLink(meterEventId); }; const getTransactionDescription = (item: any, t: Function) => { const { isGrant, isScheduled, isInactive } = getCreditActivityFlags(item); const isPaid = isGrant && item.category === 'paid' && (!isScheduled || item.metadata?.schedule_seq === 1); if (!isGrant) { const secondLine = item.metadata?.is_repayment ? t('common.creditActivity.repayment') : item.description || ''; return { isGrant, isInactive, activityType: t('common.creditActivity.consumption'), secondLine, }; } if (isPaid) { let secondLine = item.description || ''; if (item.invoice?.total && item.invoice?.paymentCurrency) { const invoiceCurrency = item.invoice.paymentCurrency; const paidAmount = formatCreditAmount( formatBNStr(item.invoice.total, invoiceCurrency.decimal || 0), invoiceCurrency.symbol || '' ); secondLine = t('common.creditActivity.paidAmount', { amount: paidAmount }); } return { isGrant, isInactive, activityType: t('common.creditActivity.paidGrant'), secondLine, }; } if (isScheduled) { return { isGrant, isInactive, activityType: t('common.creditActivity.resetGrant'), secondLine: item.description || '', }; } return { isGrant, isInactive, activityType: t('common.creditActivity.promotionalGrant'), secondLine: item.description || '', }; }; const TransactionsTable = React.memo((props: Props) => { const { pageSize, customer_id, subscription_id, credit_grant_id, onTableDataChange, showAdminColumns = false, showTimeFilter = false, includeGrants = false, source, mode = 'portal', } = props; const listKey = 'credit-transactions-table'; const { t, locale } = useLocaleContext(); const { session } = usePaymentContext(); const isAdmin = ['owner', 'admin'].includes(session?.user?.role || ''); const isDashboard = isAdmin && mode === 'dashboard'; const navigate = useNavigate(); // 如果没有传入 customer_id,使用当前登录用户的 DID const effectiveCustomerId = customer_id || session?.user?.did; const [search, setSearch] = useState<{ pageSize: number; page: number; start?: number; end?: number; }>({ pageSize: pageSize || 10, page: 1, }); const [filters, setFilters] = useState({ start: undefined, end: undefined, }); const handleDateRangeChange = (newValue: DateRangeValue) => { setFilters(newValue); setSearch((prev) => ({ ...prev, page: 1, start: newValue.start || undefined, end: newValue.end || undefined, })); }; const { loading, data = { list: [], count: 0 } } = useRequest( () => fetchData({ ...search, customer_id: effectiveCustomerId, subscription_id, credit_grant_id, source, include_grants: includeGrants, }), { refreshDeps: [search, effectiveCustomerId, subscription_id, credit_grant_id, source, includeGrants], } ); // 初始化时应用默认日期筛选 useEffect(() => { if (showTimeFilter && !search.start && !search.end) { setSearch((prev) => ({ ...prev, page: 1, start: filters.start || undefined, end: filters.end || undefined, })); } }, [showTimeFilter, search.start, search.end, filters.start, filters.end]); const prevData = useRef(data); useEffect(() => { if (onTableDataChange) { onTableDataChange(data, prevData.current); prevData.current = data; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); const handleTransactionClick = (e: React.MouseEvent, item: any) => { const detail = getTransactionDetailLink(item, isDashboard); if (!detail) { return; } handleNavigation(e, detail.link, navigate, { target: detail.link.external ? '_blank' : '_self' }); }; const openSubscription = (e: React.MouseEvent, subscriptionId: string) => { e.preventDefault(); const link = getSubscriptionDetailLink(subscriptionId, isDashboard); handleNavigation(e, link.link, navigate); }; const openInvoice = (e: React.MouseEvent, invoiceId: string) => { e.preventDefault(); const link = getInvoiceDetailLink(invoiceId, isDashboard); handleNavigation(e, link.link, navigate); }; const renderActionButton = (label: string, onClick: (e: React.MouseEvent) => void) => ( ); const columns = [ { label: t('common.date'), name: 'created_at', options: { setCellProps: () => ({ style: { width: '25%' } }), customBodyRenderLite: (_: string, index: number) => { const item = data?.list[index] as any; return ( handleTransactionClick(e, item)}> {formatToDate(item.created_at, locale, 'YYYY-MM-DD HH:mm')} ); }, }, }, { label: t('common.description'), name: 'description', options: { setCellProps: () => ({ style: { width: '25%' } }), customBodyRenderLite: (_: string, index: number) => { const item = data?.list[index] as any; const { activityType, secondLine, isInactive, isGrant } = getTransactionDescription(item, t); return ( handleTransactionClick(e, item)} sx={{ cursor: 'pointer' }}> {activityType} {secondLine && ( {secondLine} )} ); }, }, }, { label: t('common.amount'), name: 'credit_amount', align: 'right', options: { setCellProps: () => ({ style: { width: '20%' } }), customHeadLabelRender: () => {t('common.amount')}, customBodyRenderLite: (_: string, index: number) => { const item = data?.list[index] as any; const { isGrant, isDepleted, isExpired, isInactive } = getCreditActivityFlags(item); const amount = isGrant ? item.amount : item.credit_amount; const currency = item.paymentCurrency || item.currency; const unit = !isGrant && item.meter?.unit ? item.meter.unit : currency?.symbol; const displayAmount = formatCreditAmount(formatBNStr(amount, currency?.decimal || 0), unit); if (!includeGrants) { return ( handleTransactionClick(e, item)} sx={{ pr: 5 }}> {displayAmount} ); } return ( handleTransactionClick(e, item)} sx={{ pr: 5 }}> {isGrant ? '+' : '-'} {displayAmount} {isDepleted ? ( {t('common.consumed')} ) : isExpired ? ( {t('common.expired')} ) : null} ); }, }, }, ...(showAdminColumns && isAdmin ? [ { label: t('common.meterEvent'), name: 'meter_event', options: { customBodyRenderLite: (_: string, index: number) => { const transaction = data?.list[index] as TCreditTransactionExpanded; if (!transaction.meter) { return -; } return ( {transaction.meter.event_name} ); }, }, }, ] : []), { label: t('common.actions'), name: 'actions', options: { setCellProps: () => ({ style: { width: '25%' } }), customBodyRenderLite: (_: string, index: number) => { const item = data?.list[index] as any; const { isGrant, isScheduled } = getCreditActivityFlags(item); const isPaid = isGrant && item.category === 'paid' && !isScheduled; const subscriptionId = getSubscriptionId(item); const invoiceId = isGrant ? getInvoiceId(item) : null; const shouldShowSubscription = Boolean(subscriptionId) && (!isGrant || isScheduled || isPaid); return ( {shouldShowSubscription && subscriptionId ? renderActionButton(t('common.viewSubscription'), (e) => openSubscription(e, subscriptionId)) : null} {isPaid && invoiceId ? renderActionButton(t('common.viewInvoice'), (e) => openInvoice(e, invoiceId)) : null} ); }, }, }, ].filter(Boolean); const onTableChange = ({ page, rowsPerPage }: any) => { if (search.pageSize !== rowsPerPage) { setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 })); } else if (search.page !== page + 1) { setSearch((x) => ({ ...x, page: page + 1 })); } }; return ( {showTimeFilter && ( )} ); }); const TableRoot = styled(Box)` @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) { .MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root { > div { width: fit-content; flex: inherit; font-size: 14px; } } .invoice-summary { padding-right: 20px; } } `; export default function CreditTransactionsList(rawProps: Props) { const props = Object.assign( { customer_id: '', subscription_id: '', credit_grant_id: '', source: '', pageSize: 10, onTableDataChange: () => {}, showAdminColumns: false, showTimeFilter: false, includeGrants: false, mode: 'portal', }, rawProps ); return ; }