Files
Dashboard/components/collections/transactions-table.tsx
2025-10-05 16:10:35 +02:00

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>
);
}