214 lines
7.3 KiB
TypeScript
214 lines
7.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useMemo } from "react";
|
|
import { useCollection } from "@/hooks/useCollection";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
import { formatDate, formatCurrency } from "@/lib/utils";
|
|
import { LibreChatTransaction, LibreChatUser } from "@/lib/types";
|
|
|
|
// Interface étendue pour les transactions avec description optionnelle
|
|
interface TransactionWithDescription extends LibreChatTransaction {
|
|
description?: string;
|
|
}
|
|
|
|
export function TransactionsTable() {
|
|
const { data: transactions, loading } =
|
|
useCollection<LibreChatTransaction>("transactions");
|
|
const { data: users } = useCollection<LibreChatUser>("users");
|
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const itemsPerPage = 10;
|
|
|
|
// Créer une map pour les lookups rapides des utilisateurs
|
|
const usersMap = useMemo(() => {
|
|
if (!users) return new Map();
|
|
return new Map(users.map((user) => [user._id, user]));
|
|
}, [users]);
|
|
|
|
const totalPages = Math.ceil((transactions?.length || 0) / itemsPerPage);
|
|
|
|
const handlePrevPage = () => {
|
|
setCurrentPage((prev) => Math.max(1, prev - 1));
|
|
};
|
|
|
|
const handleNextPage = () => {
|
|
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
|
|
};
|
|
|
|
// Fonction pour obtenir le nom d'utilisateur
|
|
const getUserName = (userId: string): string => {
|
|
if (!userId || userId === "undefined") return "Utilisateur inconnu";
|
|
const user = usersMap.get(userId);
|
|
return user?.name || user?.email || `Utilisateur ${userId.slice(-8)}`;
|
|
};
|
|
|
|
// Fonction pour formater le montant en euros
|
|
const formatAmount = (rawAmount: number): string => {
|
|
// Convertir les tokens en euros (exemple: 1000 tokens = 1 euro)
|
|
const euros = rawAmount / 1000;
|
|
return formatCurrency(euros);
|
|
};
|
|
|
|
// Fonction pour obtenir la description
|
|
const getDescription = (transaction: LibreChatTransaction): string => {
|
|
const transactionWithDesc = transaction as TransactionWithDescription;
|
|
|
|
if (transactionWithDesc.description &&
|
|
typeof transactionWithDesc.description === 'string' &&
|
|
transactionWithDesc.description !== "undefined") {
|
|
return transactionWithDesc.description;
|
|
}
|
|
|
|
// Générer une description basée sur le type et le montant
|
|
const amount = Math.abs(Number(transaction.rawAmount) || 0);
|
|
if (amount > 0) {
|
|
return `Consommation de ${amount.toLocaleString()} tokens`;
|
|
}
|
|
|
|
return "Transaction sans description";
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Transactions</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<div key={i} className="h-16 bg-muted animate-pulse rounded" />
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>
|
|
Transactions récentes ({transactions?.length || 0})
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>ID</TableHead>
|
|
<TableHead>Utilisateur</TableHead>
|
|
<TableHead>Type</TableHead>
|
|
<TableHead>Montant</TableHead>
|
|
<TableHead>Tokens</TableHead>
|
|
<TableHead>Description</TableHead>
|
|
<TableHead>Date</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{transactions
|
|
?.slice(
|
|
(currentPage - 1) * itemsPerPage,
|
|
currentPage * itemsPerPage
|
|
)
|
|
.map((transaction) => {
|
|
const userName = getUserName(transaction.user);
|
|
const description = getDescription(transaction);
|
|
const tokenAmount = Math.abs(
|
|
Number(transaction.rawAmount) || 0
|
|
);
|
|
const isCredit = Number(transaction.rawAmount) > 0;
|
|
|
|
return (
|
|
<TableRow key={transaction._id}>
|
|
<TableCell>
|
|
<span className="font-mono text-xs">
|
|
{transaction._id.slice(-8)}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="max-w-xs">
|
|
<span className="font-mono text-xs text-muted-foreground">
|
|
{transaction.user?.slice(-8) || "N/A"}
|
|
</span>
|
|
<div className="text-sm truncate">{userName}</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant={isCredit ? "default" : "destructive"}>
|
|
{isCredit ? "Crédit" : "Débit"}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<span className="font-semibold">
|
|
{formatAmount(transaction.rawAmount)}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell>
|
|
{tokenAmount > 0 && (
|
|
<Badge variant="outline" className="text-xs">
|
|
{tokenAmount.toLocaleString()} tokens
|
|
</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<span className="max-w-xs truncate block text-sm">
|
|
{description}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell>
|
|
<span className="text-sm text-muted-foreground">
|
|
{formatDate(new Date(transaction.createdAt))}
|
|
</span>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
{/* Pagination */}
|
|
<div className="flex items-center justify-between space-x-2 py-4">
|
|
<div className="text-sm text-muted-foreground">
|
|
Page {currentPage} sur {totalPages} ({transactions?.length || 0}{" "}
|
|
éléments au total)
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handlePrevPage}
|
|
disabled={currentPage <= 1}
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
Précédent
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleNextPage}
|
|
disabled={currentPage >= totalPages}
|
|
>
|
|
Suivant
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|