"use client"; import { useState, useEffect, useCallback } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Users, MessageSquare, DollarSign, Activity, Euro } from "lucide-react"; import { useCollection } from "@/hooks/useCollection"; import { convertCreditsToEuros } from "@/lib/utils/currency"; import { LibreChatUser, LibreChatConversation, LibreChatTransaction, LibreChatBalance, } from "@/lib/types"; interface UsageStats { totalUsers: number; activeUsers: number; totalConversations: number; totalMessages: number; totalTokensConsumed: number; totalCreditsUsed: number; averageTokensPerUser: number; topUsers: Array<{ userId: string; userName: string; conversations: number; tokens: number; credits: number; }>; } export function UsageAnalytics() { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const { data: users = [] } = useCollection("users", { limit: 1000 }); const { data: conversations = [] } = useCollection("conversations", { limit: 1000 }); const { data: transactions = [] } = useCollection("transactions", { limit: 1000 }); const { data: balances = [] } = useCollection("balances", { limit: 1000 }); const calculateStats = useCallback(() => { if (!users.length) { return; } setLoading(true); console.log("=== CALCUL DES STATISTIQUES ==="); console.log("Utilisateurs:", users.length); console.log("Conversations:", conversations.length); console.log("Transactions:", transactions.length); console.log("Balances:", balances.length); // Analyser les doublons dans les balances const userCounts = new Map(); balances.forEach((balance) => { const userId = balance.user; userCounts.set(userId, (userCounts.get(userId) || 0) + 1); }); const duplicateUsers = Array.from(userCounts.entries()).filter(([, count]) => count > 1); console.log("Utilisateurs avec plusieurs entrées:", duplicateUsers); // Afficher quelques exemples d'entrées console.log("Premières 5 entrées:", balances.slice(0, 5)); // Calculer le total brut (avec doublons) const totalBrut = balances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0); console.log("Total brut (avec doublons potentiels):", totalBrut); // Ajouter des logs détaillés pour comprendre le problème console.log("=== DIAGNOSTIC DÉTAILLÉ ==="); // Analyser les doublons const duplicateDetails = Array.from(userCounts.entries()) .filter(([, count]) => count > 1) .map(([userId, count]) => { const userBalances = balances.filter(b => b.user === userId); const totalCredits = userBalances.reduce((sum, b) => sum + (b.tokenCredits || 0), 0); return { userId, count, totalCredits, balances: userBalances.map(b => ({ credits: b.tokenCredits, createdAt: b.createdAt, updatedAt: b.updatedAt })) }; }); console.log("Détails des doublons:", duplicateDetails); // NOUVEAU : Identifier les utilisateurs fantômes console.log("=== ANALYSE DES UTILISATEURS FANTÔMES ==="); const userIds = new Set(users.map(user => user._id)); const balanceUserIds = balances.map(balance => balance.user); const phantomUsers = balanceUserIds.filter(userId => !userIds.has(userId)); const uniquePhantomUsers = [...new Set(phantomUsers)]; console.log("Utilisateurs fantômes (ont des balances mais n'existent plus):", uniquePhantomUsers); console.log("Nombre d'utilisateurs fantômes:", uniquePhantomUsers.length); // Calculer les crédits des utilisateurs fantômes const phantomCredits = balances .filter(balance => uniquePhantomUsers.includes(balance.user)) .reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0); console.log("Crédits des utilisateurs fantômes:", phantomCredits); console.log("Crédits des vrais utilisateurs:", totalBrut - phantomCredits); // Analyser les utilisateurs fantômes const phantomDetails = uniquePhantomUsers.map(userId => { const userBalances = balances.filter(b => b.user === userId); const totalCredits = userBalances.reduce((sum, b) => sum + (b.tokenCredits || 0), 0); return { userId, totalCredits, count: userBalances.length }; }); console.log("Détails des utilisateurs fantômes:", phantomDetails); // Calculer les utilisateurs actifs (30 derniers jours) const thirtyDaysAgo = new Date(); thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); const activeUsers = users.filter((user) => { const lastActivity = new Date(user.updatedAt || user.createdAt); return lastActivity >= thirtyDaysAgo; }).length; // CORRECTION AMÉLIORÉE : Créer une map des crédits par utilisateur const creditsMap = new Map(); // Grouper les balances par utilisateur const balancesByUser = new Map(); balances.forEach((balance) => { const userId = balance.user; // Ignorer les utilisateurs fantômes (qui n'existent plus) if (users.some(user => user._id === userId)) { if (!balancesByUser.has(userId)) { balancesByUser.set(userId, []); } balancesByUser.get(userId)!.push(balance); } }); // Pour chaque utilisateur, calculer les crédits selon votre logique métier balancesByUser.forEach((userBalances, userId) => { if (userBalances.length > 0) { // OPTION A: Prendre la balance la plus récente const sortedBalances = userBalances.sort((a, b) => { const aDate = new Date((a.updatedAt as string) || (a.createdAt as string) || 0); const bDate = new Date((b.updatedAt as string) || (b.createdAt as string) || 0); return bDate.getTime() - aDate.getTime(); }); creditsMap.set(userId, sortedBalances[0].tokenCredits || 0); // OPTION B: Sommer toutes les balances (si c'est votre logique) // const totalCredits = userBalances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0); // creditsMap.set(userId, totalCredits); // OPTION C: Prendre la balance avec le plus de crédits // const maxCredits = Math.max(...userBalances.map(b => b.tokenCredits || 0)); // creditsMap.set(userId, maxCredits); } }); // Initialiser les stats par utilisateur const userStats = new Map< string, { userName: string; conversations: number; tokens: number; credits: number; } >(); users.forEach((user) => { userStats.set(user._id, { userName: user.name || user.email || "Utilisateur inconnu", conversations: 0, tokens: 0, credits: creditsMap.get(user._id) || 0, }); }); // Calculer les conversations par utilisateur conversations.forEach((conv) => { const userStat = userStats.get(conv.user); if (userStat) { userStat.conversations++; } }); // Calculer les tokens par utilisateur depuis les transactions let totalTokensConsumed = 0; transactions.forEach((transaction) => { const userStat = userStats.get(transaction.user); if (userStat && transaction.rawAmount) { const tokens = Math.abs(Number(transaction.rawAmount) || 0); userStat.tokens += tokens; totalTokensConsumed += tokens; } }); // Calculer le total des crédits depuis la map corrigée (sans doublons ni fantômes) const totalCreditsUsed = Array.from(creditsMap.values()).reduce( (sum, credits) => sum + credits, 0 ); console.log("=== RÉSULTATS CORRIGÉS ==="); console.log("Crédits totaux (sans doublons ni fantômes):", totalCreditsUsed); console.log("Utilisateurs avec crédits:", creditsMap.size); // Tous les utilisateurs triés par tokens puis conversations const allUsers = Array.from(userStats.entries()) .map(([userId, stats]) => ({ userId, ...stats, })) .sort((a, b) => { // Trier d'abord par tokens, puis par conversations si tokens égaux if (b.tokens !== a.tokens) { return b.tokens - a.tokens; } return b.conversations - a.conversations; }); const totalMessages = conversations.reduce( (sum, conv) => sum + (Array.isArray(conv.messages) ? conv.messages.length : 0), 0 ); setStats({ totalUsers: users.length, activeUsers, totalConversations: conversations.length, totalMessages, totalTokensConsumed, totalCreditsUsed, averageTokensPerUser: users.length > 0 ? totalTokensConsumed / users.length : 0, topUsers: allUsers, // Afficher tous les utilisateurs }); setLoading(false); }, [users, conversations, transactions, balances]); useEffect(() => { calculateStats(); }, [calculateStats]); if (loading || !stats) { return (
{Array.from({ length: 4 }).map((_, i) => (
))}
); } return (
Utilisateurs totaux
{stats.totalUsers}

{stats.activeUsers} actifs ce mois

Conversations
{stats.totalConversations}

{stats.totalMessages} messages au total

Tokens consommés
{stats.totalTokensConsumed.toLocaleString()}

{Math.round(stats.averageTokensPerUser)} par utilisateur

Crédits totaux
{stats.totalCreditsUsed.toLocaleString()}

crédits disponibles

{/* Conversion en euros */}
Valeur: {convertCreditsToEuros(stats.totalCreditsUsed).formatted.eur}
({convertCreditsToEuros(stats.totalCreditsUsed).formatted.usd} USD)
Tous les utilisateurs
{stats.topUsers.map((user, index) => (
#{index + 1}

{user.userName}

{user.conversations} conversations

{user.tokens.toLocaleString()} tokens

{user.credits.toLocaleString()} crédits

))}
); }