diff --git a/messages/en.json b/messages/en.json index 6e1d5be..f7b9c34 100644 --- a/messages/en.json +++ b/messages/en.json @@ -637,6 +637,10 @@ "SUBSCRIPTION_RENEWAL": "Subscription Renewal", "LIFETIME_MONTHLY": "Lifetime Monthly" }, + "detailViewer": { + "title": "Credit Transaction Detail", + "close": "Close" + }, "expired": "Expired", "never": "Never" } diff --git a/messages/zh.json b/messages/zh.json index 6ad555f..b0d2cd9 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -638,6 +638,10 @@ "SUBSCRIPTION_RENEWAL": "订阅续费", "LIFETIME_MONTHLY": "终身月度" }, + "detailViewer": { + "title": "积分交易详情", + "close": "关闭" + }, "expired": "已过期", "never": "永不" } diff --git a/src/actions/get-credit-transactions.ts b/src/actions/get-credit-transactions.ts index be36f23..65dd504 100644 --- a/src/actions/get-credit-transactions.ts +++ b/src/actions/get-credit-transactions.ts @@ -74,7 +74,8 @@ export const getCreditTransactionsAction = actionClient remainingAmount: creditTransaction.remainingAmount, paymentId: creditTransaction.paymentId, expirationDate: creditTransaction.expirationDate, - expirationDateProcessedAt: creditTransaction.expirationDateProcessedAt, + expirationDateProcessedAt: + creditTransaction.expirationDateProcessedAt, createdAt: creditTransaction.createdAt, updatedAt: creditTransaction.updatedAt, }) @@ -108,7 +109,10 @@ export const getCreditTransactionsAction = actionClient console.error('get credit transactions error:', error); return { success: false, - error: error instanceof Error ? error.message : 'Failed to fetch credit transactions', + error: + error instanceof Error + ? error.message + : 'Failed to fetch credit transactions', }; } }); diff --git a/src/components/settings/credits/credit-transactions-table.tsx b/src/components/settings/credits/credit-transactions-table.tsx index 9a5fe0f..64e0b31 100644 --- a/src/components/settings/credits/credit-transactions-table.tsx +++ b/src/components/settings/credits/credit-transactions-table.tsx @@ -29,6 +29,7 @@ import { TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip'; +import { CreditDetailViewer } from '@/credits/credit-detail-viewer'; import { CREDIT_TRANSACTION_TYPE } from '@/credits/types'; import { formatDate } from '@/lib/formatter'; import { @@ -223,7 +224,10 @@ export function CreditTransactionsTable({ const transaction = row.original; return (
- + {getTransactionTypeIcon(transaction.type)} {getTransactionTypeDisplayName(transaction.type)} @@ -238,18 +242,7 @@ export function CreditTransactionsTable({ ), cell: ({ row }) => { const transaction = row.original; - return ( -
- 0 ? 'text-green-600' : 'text-red-600' - }`} - > - {transaction.amount > 0 ? '+' : ''} - {transaction.amount.toLocaleString()} - -
- ); + return ; }, }, { @@ -543,7 +536,7 @@ export function CreditTransactionsTable({
-
+
{total > 0 && {tTable('totalRecords', { count: total })}}
diff --git a/src/credits/credit-detail-viewer.tsx b/src/credits/credit-detail-viewer.tsx new file mode 100644 index 0000000..4105f9d --- /dev/null +++ b/src/credits/credit-detail-viewer.tsx @@ -0,0 +1,250 @@ +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '@/components/ui/drawer'; +import { Separator } from '@/components/ui/separator'; +import { useIsMobile } from '@/hooks/use-mobile'; +import { formatDate } from '@/lib/formatter'; +import { + ClockIcon, + GiftIcon, + MinusCircleIcon, + RefreshCwIcon, + ShoppingCartIcon, +} from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { toast } from 'sonner'; +import { CREDIT_TRANSACTION_TYPE } from './types'; + +// Define the credit transaction interface (matching the one in the table) +export interface CreditTransaction { + id: string; + userId: string; + type: string; + description: string | null; + amount: number; + remainingAmount: number | null; + paymentId: string | null; + expirationDate: Date | null; + expirationDateProcessedAt: Date | null; + createdAt: Date; + updatedAt: Date; +} + +interface CreditDetailViewerProps { + transaction: CreditTransaction; +} + +export function CreditDetailViewer({ transaction }: CreditDetailViewerProps) { + const t = useTranslations('Dashboard.settings.credits.transactions'); + const isMobile = useIsMobile(); + + // Get transaction type icon + const getTransactionTypeIcon = (type: string) => { + switch (type) { + case CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH: + return ; + case CREDIT_TRANSACTION_TYPE.REGISTER_GIFT: + return ; + case CREDIT_TRANSACTION_TYPE.PURCHASE: + return ; + case CREDIT_TRANSACTION_TYPE.USAGE: + return ; + case CREDIT_TRANSACTION_TYPE.EXPIRE: + return ; + case CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL: + return ; + case CREDIT_TRANSACTION_TYPE.LIFETIME_MONTHLY: + return ; + default: + return null; + } + }; + + // Get transaction type badge variant + const getTransactionTypeBadgeVariant = (type: string) => { + switch (type) { + case CREDIT_TRANSACTION_TYPE.REGISTER_GIFT: + case CREDIT_TRANSACTION_TYPE.PURCHASE: + case CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH: + case CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL: + case CREDIT_TRANSACTION_TYPE.LIFETIME_MONTHLY: + return 'outline' as const; + case CREDIT_TRANSACTION_TYPE.USAGE: + case CREDIT_TRANSACTION_TYPE.EXPIRE: + return 'destructive' as const; + default: + return 'outline' as const; + } + }; + + // Get transaction type display name + const getTransactionTypeDisplayName = (type: string) => { + switch (type) { + case CREDIT_TRANSACTION_TYPE.MONTHLY_REFRESH: + return t('types.MONTHLY_REFRESH'); + case CREDIT_TRANSACTION_TYPE.REGISTER_GIFT: + return t('types.REGISTER_GIFT'); + case CREDIT_TRANSACTION_TYPE.PURCHASE: + return t('types.PURCHASE'); + case CREDIT_TRANSACTION_TYPE.USAGE: + return t('types.USAGE'); + case CREDIT_TRANSACTION_TYPE.EXPIRE: + return t('types.EXPIRE'); + case CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL: + return t('types.SUBSCRIPTION_RENEWAL'); + case CREDIT_TRANSACTION_TYPE.LIFETIME_MONTHLY: + return t('types.LIFETIME_MONTHLY'); + default: + return type; + } + }; + + return ( + + + + + + + {t('detailViewer.title')} + +
+
+
+ {/* Transaction Type Badge */} + + {getTransactionTypeIcon(transaction.type)} + {getTransactionTypeDisplayName(transaction.type)} + +
+ + {/* Basic Information */} +
+
+ + {t('columns.amount')}: + + 0 ? 'text-green-600' : 'text-red-600' + }`} + > + {transaction.amount > 0 ? '+' : ''} + {transaction.amount.toLocaleString()} + +
+ + {transaction.remainingAmount !== null && ( +
+ + {t('columns.remainingAmount')}: + + + {transaction.remainingAmount.toLocaleString()} + +
+ )} + + {transaction.description && ( +
+ + {t('columns.description')}: + + {transaction.description} +
+ )} + + {transaction.paymentId && ( +
+ + {t('columns.paymentId')}: + + { + navigator.clipboard.writeText(transaction.paymentId!); + toast.success(t('paymentIdCopied')); + }} + > + {transaction.paymentId} + +
+ )} + + {transaction.expirationDate && ( +
+ + {t('columns.expirationDate')}: + + {formatDate(transaction.expirationDate)} +
+ )} + + {transaction.expirationDateProcessedAt && ( +
+ + {t('columns.expirationDateProcessedAt')}: + + + {formatDate(transaction.expirationDateProcessedAt)} + +
+ )} +
+
+ + + + {/* Timestamps */} +
+
+ + {t('columns.createdAt')}: + + {formatDate(transaction.createdAt)} +
+
+ + {t('columns.updatedAt')}: + + {formatDate(transaction.updatedAt)} +
+
+
+ + + + + +
+
+ ); +} diff --git a/src/payment/provider/stripe.ts b/src/payment/provider/stripe.ts index bc76455..d364ee7 100644 --- a/src/payment/provider/stripe.ts +++ b/src/payment/provider/stripe.ts @@ -828,7 +828,7 @@ export class StripeProvider implements PaymentProvider { userId, amount: Number.parseInt(credits), type: CREDIT_TRANSACTION_TYPE.PURCHASE, - description: `Credit package purchase: ${packageId} - ${credits} credits for $${amount}`, + description: `+${credits} credits for package ${packageId} (${amount})`, paymentId: session.id, expireDays: creditPackage.expireDays, });