feat: add CreditDetailViewer component and enhance credit transaction details in English and Chinese
This commit is contained in:
parent
b75e9eb282
commit
263440742a
@ -637,6 +637,10 @@
|
||||
"SUBSCRIPTION_RENEWAL": "Subscription Renewal",
|
||||
"LIFETIME_MONTHLY": "Lifetime Monthly"
|
||||
},
|
||||
"detailViewer": {
|
||||
"title": "Credit Transaction Detail",
|
||||
"close": "Close"
|
||||
},
|
||||
"expired": "Expired",
|
||||
"never": "Never"
|
||||
}
|
||||
|
@ -638,6 +638,10 @@
|
||||
"SUBSCRIPTION_RENEWAL": "订阅续费",
|
||||
"LIFETIME_MONTHLY": "终身月度"
|
||||
},
|
||||
"detailViewer": {
|
||||
"title": "积分交易详情",
|
||||
"close": "关闭"
|
||||
},
|
||||
"expired": "已过期",
|
||||
"never": "永不"
|
||||
}
|
||||
|
@ -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',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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 (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
<Badge variant={getTransactionTypeBadgeVariant(transaction.type)}>
|
||||
<Badge
|
||||
variant={getTransactionTypeBadgeVariant(transaction.type)}
|
||||
className="cursor-pointer hover:bg-accent transition-colors"
|
||||
>
|
||||
{getTransactionTypeIcon(transaction.type)}
|
||||
{getTransactionTypeDisplayName(transaction.type)}
|
||||
</Badge>
|
||||
@ -238,18 +242,7 @@ export function CreditTransactionsTable({
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const transaction = row.original;
|
||||
return (
|
||||
<div className="flex items-center gap-2 pl-3">
|
||||
<span
|
||||
className={`font-medium ${
|
||||
transaction.amount > 0 ? 'text-green-600' : 'text-red-600'
|
||||
}`}
|
||||
>
|
||||
{transaction.amount > 0 ? '+' : ''}
|
||||
{transaction.amount.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
return <CreditDetailViewer transaction={transaction} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -543,7 +536,7 @@ export function CreditTransactionsTable({
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
|
||||
{total > 0 && <span>{tTable('totalRecords', { count: total })}</span>}
|
||||
</div>
|
||||
|
250
src/credits/credit-detail-viewer.tsx
Normal file
250
src/credits/credit-detail-viewer.tsx
Normal file
@ -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 <RefreshCwIcon className="h-5 w-5" />;
|
||||
case CREDIT_TRANSACTION_TYPE.REGISTER_GIFT:
|
||||
return <GiftIcon className="h-5 w-5" />;
|
||||
case CREDIT_TRANSACTION_TYPE.PURCHASE:
|
||||
return <ShoppingCartIcon className="h-5 w-5" />;
|
||||
case CREDIT_TRANSACTION_TYPE.USAGE:
|
||||
return <MinusCircleIcon className="h-5 w-5" />;
|
||||
case CREDIT_TRANSACTION_TYPE.EXPIRE:
|
||||
return <ClockIcon className="h-5 w-5" />;
|
||||
case CREDIT_TRANSACTION_TYPE.SUBSCRIPTION_RENEWAL:
|
||||
return <RefreshCwIcon className="h-5 w-5" />;
|
||||
case CREDIT_TRANSACTION_TYPE.LIFETIME_MONTHLY:
|
||||
return <GiftIcon className="h-5 w-5" />;
|
||||
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 (
|
||||
<Drawer direction={isMobile ? 'bottom' : 'right'}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button
|
||||
variant="link"
|
||||
className="cursor-pointer text-foreground w-fit px-3 text-left h-auto"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`font-medium ${
|
||||
transaction.amount > 0 ? 'text-green-600' : 'text-red-600'
|
||||
}`}
|
||||
>
|
||||
{transaction.amount > 0 ? '+' : ''}
|
||||
{transaction.amount.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{t('detailViewer.title')}</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div className="flex flex-col gap-4 overflow-y-auto px-4 text-sm">
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Transaction Type Badge */}
|
||||
<Badge
|
||||
variant={getTransactionTypeBadgeVariant(transaction.type)}
|
||||
className="px-2 py-1"
|
||||
>
|
||||
{getTransactionTypeIcon(transaction.type)}
|
||||
{getTransactionTypeDisplayName(transaction.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Basic Information */}
|
||||
<div className="grid gap-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">
|
||||
{t('columns.amount')}:
|
||||
</span>
|
||||
<span
|
||||
className={`font-medium ${
|
||||
transaction.amount > 0 ? 'text-green-600' : 'text-red-600'
|
||||
}`}
|
||||
>
|
||||
{transaction.amount > 0 ? '+' : ''}
|
||||
{transaction.amount.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{transaction.remainingAmount !== null && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">
|
||||
{t('columns.remainingAmount')}:
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{transaction.remainingAmount.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{transaction.description && (
|
||||
<div className="grid gap-3">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t('columns.description')}:
|
||||
</span>
|
||||
<span className="break-words">{transaction.description}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{transaction.paymentId && (
|
||||
<div className="grid gap-3">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t('columns.paymentId')}:
|
||||
</span>
|
||||
<span
|
||||
className="font-mono text-sm cursor-pointer hover:bg-accent px-2 py-1 rounded border break-all"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(transaction.paymentId!);
|
||||
toast.success(t('paymentIdCopied'));
|
||||
}}
|
||||
>
|
||||
{transaction.paymentId}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{transaction.expirationDate && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">
|
||||
{t('columns.expirationDate')}:
|
||||
</span>
|
||||
<span>{formatDate(transaction.expirationDate)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{transaction.expirationDateProcessedAt && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">
|
||||
{t('columns.expirationDateProcessedAt')}:
|
||||
</span>
|
||||
<span>
|
||||
{formatDate(transaction.expirationDateProcessedAt)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Timestamps */}
|
||||
<div className="grid gap-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">
|
||||
{t('columns.createdAt')}:
|
||||
</span>
|
||||
<span>{formatDate(transaction.createdAt)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">
|
||||
{t('columns.updatedAt')}:
|
||||
</span>
|
||||
<span>{formatDate(transaction.updatedAt)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
<DrawerClose asChild>
|
||||
<Button variant="outline" className="cursor-pointer">
|
||||
{t('detailViewer.close')}
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
@ -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,
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user