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",
|
"SUBSCRIPTION_RENEWAL": "Subscription Renewal",
|
||||||
"LIFETIME_MONTHLY": "Lifetime Monthly"
|
"LIFETIME_MONTHLY": "Lifetime Monthly"
|
||||||
},
|
},
|
||||||
|
"detailViewer": {
|
||||||
|
"title": "Credit Transaction Detail",
|
||||||
|
"close": "Close"
|
||||||
|
},
|
||||||
"expired": "Expired",
|
"expired": "Expired",
|
||||||
"never": "Never"
|
"never": "Never"
|
||||||
}
|
}
|
||||||
|
@ -638,6 +638,10 @@
|
|||||||
"SUBSCRIPTION_RENEWAL": "订阅续费",
|
"SUBSCRIPTION_RENEWAL": "订阅续费",
|
||||||
"LIFETIME_MONTHLY": "终身月度"
|
"LIFETIME_MONTHLY": "终身月度"
|
||||||
},
|
},
|
||||||
|
"detailViewer": {
|
||||||
|
"title": "积分交易详情",
|
||||||
|
"close": "关闭"
|
||||||
|
},
|
||||||
"expired": "已过期",
|
"expired": "已过期",
|
||||||
"never": "永不"
|
"never": "永不"
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,8 @@ export const getCreditTransactionsAction = actionClient
|
|||||||
remainingAmount: creditTransaction.remainingAmount,
|
remainingAmount: creditTransaction.remainingAmount,
|
||||||
paymentId: creditTransaction.paymentId,
|
paymentId: creditTransaction.paymentId,
|
||||||
expirationDate: creditTransaction.expirationDate,
|
expirationDate: creditTransaction.expirationDate,
|
||||||
expirationDateProcessedAt: creditTransaction.expirationDateProcessedAt,
|
expirationDateProcessedAt:
|
||||||
|
creditTransaction.expirationDateProcessedAt,
|
||||||
createdAt: creditTransaction.createdAt,
|
createdAt: creditTransaction.createdAt,
|
||||||
updatedAt: creditTransaction.updatedAt,
|
updatedAt: creditTransaction.updatedAt,
|
||||||
})
|
})
|
||||||
@ -108,7 +109,10 @@ export const getCreditTransactionsAction = actionClient
|
|||||||
console.error('get credit transactions error:', error);
|
console.error('get credit transactions error:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
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,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip';
|
} from '@/components/ui/tooltip';
|
||||||
|
import { CreditDetailViewer } from '@/credits/credit-detail-viewer';
|
||||||
import { CREDIT_TRANSACTION_TYPE } from '@/credits/types';
|
import { CREDIT_TRANSACTION_TYPE } from '@/credits/types';
|
||||||
import { formatDate } from '@/lib/formatter';
|
import { formatDate } from '@/lib/formatter';
|
||||||
import {
|
import {
|
||||||
@ -223,7 +224,10 @@ export function CreditTransactionsTable({
|
|||||||
const transaction = row.original;
|
const transaction = row.original;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 pl-3">
|
<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)}
|
{getTransactionTypeIcon(transaction.type)}
|
||||||
{getTransactionTypeDisplayName(transaction.type)}
|
{getTransactionTypeDisplayName(transaction.type)}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -238,18 +242,7 @@ export function CreditTransactionsTable({
|
|||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const transaction = row.original;
|
const transaction = row.original;
|
||||||
return (
|
return <CreditDetailViewer transaction={transaction} />;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -543,7 +536,7 @@ export function CreditTransactionsTable({
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</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">
|
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
|
||||||
{total > 0 && <span>{tTable('totalRecords', { count: total })}</span>}
|
{total > 0 && <span>{tTable('totalRecords', { count: total })}</span>}
|
||||||
</div>
|
</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,
|
userId,
|
||||||
amount: Number.parseInt(credits),
|
amount: Number.parseInt(credits),
|
||||||
type: CREDIT_TRANSACTION_TYPE.PURCHASE,
|
type: CREDIT_TRANSACTION_TYPE.PURCHASE,
|
||||||
description: `Credit package purchase: ${packageId} - ${credits} credits for $${amount}`,
|
description: `+${credits} credits for package ${packageId} (${amount})`,
|
||||||
paymentId: session.id,
|
paymentId: session.id,
|
||||||
expireDays: creditPackage.expireDays,
|
expireDays: creditPackage.expireDays,
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user