This commit is contained in:
nBiqoz
2025-10-06 19:16:20 +02:00
parent 96dd721fcb
commit 0f2adca44a
23 changed files with 1569 additions and 248 deletions

View File

@@ -96,7 +96,7 @@ export function UsersTable() {
{users.map((user) => {
const userCredits = creditsMap.get(user._id) || 0;
const isActive = new Date(user.updatedAt || user.createdAt) >
new Date(Date.now() - 5 * 60 * 1000); // 5 minutes en millisecondes
new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 jours en millisecondes
return (
<TableRow key={user._id}>
@@ -122,13 +122,13 @@ export function UsersTable() {
</span>
</TableCell>
<TableCell>
<Badge variant={isActive ? 'default' : 'destructive'}>
<Badge variant={isActive ? 'default' : 'secondary'}>
{isActive ? 'Actif' : 'Inactif'}
</Badge>
</TableCell>
<TableCell>
<span className="text-sm text-muted-foreground">
{formatDate(new Date(user.createdAt))}
{formatDate(user.createdAt)}
</span>
</TableCell>
</TableRow>

View File

@@ -0,0 +1,188 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Plus, DollarSign, Users, TrendingUp } from "lucide-react";
interface AddCreditsStats {
totalUsers: number;
totalBalances: number;
totalCredits: number;
averageCredits: number;
usersWithoutBalance: number;
}
interface AddCreditsResult {
totalUsers: number;
updatedBalances: number;
createdBalances: number;
creditsPerUser: number;
totalCreditsAdded: number;
}
export default function AddCredits() {
const [stats, setStats] = useState<AddCreditsStats | null>(null);
const [result, setResult] = useState<AddCreditsResult | null>(null);
const [loading, setLoading] = useState(false);
const [analyzing, setAnalyzing] = useState(false);
const analyzeCurrentCredits = async () => {
setAnalyzing(true);
try {
const response = await fetch("/api/add-credits");
const data = await response.json();
if (data.statistics) {
setStats(data.statistics);
}
} catch (error) {
console.error("Erreur lors de l'analyse:", error);
} finally {
setAnalyzing(false);
}
};
const addCreditsToAllUsers = async () => {
if (!confirm("Êtes-vous sûr de vouloir ajouter 5 millions de crédits à TOUS les utilisateurs ? Cette action est irréversible.")) {
return;
}
setLoading(true);
try {
const response = await fetch("/api/add-credits", {
method: "POST"
});
const data = await response.json();
if (data.success) {
setResult(data.statistics);
// Rafraîchir les stats
await analyzeCurrentCredits();
} else {
alert("Erreur: " + (data.error || data.message));
}
} catch (error) {
console.error("Erreur lors de l'ajout des crédits:", error);
alert("Erreur lors de l'ajout des crédits");
} finally {
setLoading(false);
}
};
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Plus className="h-5 w-5" />
Ajouter des Crédits
</CardTitle>
<CardDescription>
Ajouter 5 millions de tokens à tous les utilisateurs existants
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Bouton d'analyse */}
<div className="flex gap-2">
<Button
onClick={analyzeCurrentCredits}
disabled={analyzing}
variant="outline"
>
{analyzing ? "Analyse..." : "Analyser les crédits actuels"}
</Button>
</div>
{/* Statistiques actuelles */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-blue-50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium">Utilisateurs</span>
</div>
<p className="text-2xl font-bold text-blue-600">{stats.totalUsers}</p>
</div>
<div className="bg-green-50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<DollarSign className="h-4 w-4 text-green-600" />
<span className="text-sm font-medium">Total Crédits</span>
</div>
<p className="text-2xl font-bold text-green-600">
{stats.totalCredits.toLocaleString()}
</p>
</div>
<div className="bg-purple-50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-purple-600" />
<span className="text-sm font-medium">Moyenne</span>
</div>
<p className="text-2xl font-bold text-purple-600">
{stats.averageCredits.toLocaleString()}
</p>
</div>
<div className="bg-orange-50 p-3 rounded-lg">
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-orange-600" />
<span className="text-sm font-medium">Sans Balance</span>
</div>
<p className="text-2xl font-bold text-orange-600">{stats.usersWithoutBalance}</p>
</div>
</div>
)}
{/* Bouton d'ajout de crédits */}
{stats && (
<div className="border-t pt-4">
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
<h4 className="font-semibold text-yellow-800 mb-2"> Action Importante</h4>
<p className="text-yellow-700 text-sm">
Cette action va ajouter <strong>5,000,000 crédits</strong> à chacun des {stats.totalUsers} utilisateurs.
<br />
Total de crédits qui seront ajoutés: <strong>{(stats.totalUsers * 5000000).toLocaleString()}</strong>
</p>
</div>
<Button
onClick={addCreditsToAllUsers}
disabled={loading}
className="w-full bg-green-600 hover:bg-green-700"
>
{loading ? "Ajout en cours..." : `Ajouter 5M crédits à ${stats.totalUsers} utilisateurs`}
</Button>
</div>
)}
{/* Résultats */}
{result && (
<div className="border-t pt-4">
<h4 className="font-semibold text-green-600 mb-3"> Crédits ajoutés avec succès !</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-600">Balances mises à jour:</span>
<Badge variant="secondary" className="ml-2">{result.updatedBalances}</Badge>
</div>
<div>
<span className="text-gray-600">Nouvelles balances:</span>
<Badge variant="secondary" className="ml-2">{result.createdBalances}</Badge>
</div>
<div>
<span className="text-gray-600">Crédits par utilisateur:</span>
<Badge variant="secondary" className="ml-2">{result.creditsPerUser.toLocaleString()}</Badge>
</div>
<div>
<span className="text-gray-600">Total ajouté:</span>
<Badge variant="secondary" className="ml-2">{result.totalCreditsAdded.toLocaleString()}</Badge>
</div>
</div>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -9,14 +9,23 @@ import {
CartesianGrid,
Tooltip,
ResponsiveContainer,
Cell,
} from "recharts";
interface ModelDistributionChartProps {
title: string;
subtitle?: string;
data: Array<{
name: string;
value: number;
color?: string;
models?: Array<{
name: string;
value: number;
}>;
}>;
showLegend?: boolean;
totalTokens?: number;
}
interface TooltipPayload {
@@ -24,6 +33,11 @@ interface TooltipPayload {
payload: {
name: string;
value: number;
color?: string;
models?: Array<{
name: string;
value: number;
}>;
};
}
@@ -32,63 +46,211 @@ interface CustomTooltipProps {
payload?: TooltipPayload[];
}
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
if (active && payload && payload.length) {
return (
<div style={{
backgroundColor: "hsl(var(--background))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
padding: "8px",
fontSize: "12px"
}}>
<p style={{ margin: 0, color: "#ff0000" }}>
{`${payload[0].value.toLocaleString()} tokens`}
</p>
<p style={{ margin: 0, color: "#ff0000" }}>
{payload[0].payload.name}
</p>
</div>
);
}
// Couleurs par fournisseur selon l'image
const providerColors: { [key: string]: string } = {
Anthropic: "#7C3AED", // Violet vif
OpenAI: "#059669", // Vert turquoise vif
"Mistral AI": "#D97706", // Orange vif
Meta: "#DB2777", // Rose/Magenta vif
Google: "#2563EB", // Bleu vif
Cohere: "#0891B2", // Cyan vif
};
// Fonction pour regrouper les modèles par fournisseur
const groupByProvider = (modelData: Array<{ name: string; value: number }>) => {
const providerMap: {
[key: string]: {
value: number;
models: Array<{ name: string; value: number }>;
};
} = {};
modelData.forEach((model) => {
let provider = "";
// Déterminer le fournisseur basé sur le nom du modèle
if (
model.name.toLowerCase().includes("claude") ||
model.name.toLowerCase().includes("anthropic")
) {
provider = "Anthropic";
} else if (
model.name.toLowerCase().includes("gpt") ||
model.name.toLowerCase().includes("openai")
) {
provider = "OpenAI";
} else if (model.name.toLowerCase().includes("mistral")) {
provider = "Mistral AI";
} else if (
model.name.toLowerCase().includes("llama") ||
model.name.toLowerCase().includes("meta")
) {
provider = "Meta";
} else if (
model.name.toLowerCase().includes("palm") ||
model.name.toLowerCase().includes("gemini") ||
model.name.toLowerCase().includes("google")
) {
provider = "Google";
} else if (model.name.toLowerCase().includes("cohere")) {
provider = "Cohere";
} else {
provider = "Autres";
}
if (!providerMap[provider]) {
providerMap[provider] = { value: 0, models: [] };
}
providerMap[provider].value += model.value;
providerMap[provider].models.push(model);
});
return Object.entries(providerMap).map(([name, data]) => ({
name,
value: data.value,
models: data.models,
color: providerColors[name] || "#6B7280",
}));
};
const CustomTooltip = () => {
return null;
};
export function ModelDistributionChart({
title,
subtitle,
data,
totalTokens,
}: ModelDistributionChartProps) {
// Si les données sont déjà groupées par fournisseur, les utiliser directement
// Sinon, les regrouper automatiquement
const groupedData = data[0]?.models ? data : groupByProvider(data);
// Créer une liste de tous les modèles avec leurs couleurs
const allModels = groupedData.flatMap((provider) =>
provider.models?.map((model) => ({
name: model.name,
color: provider.color,
value: model.value
})) || []
);
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
{subtitle && (
<p className="text-xs text-muted-foreground mt-1">{subtitle}</p>
)}
</CardHeader>
<CardContent className="pt-0">
<ResponsiveContainer width="100%" height={200}>
<BarChart data={data}>
<BarChart
data={groupedData}
margin={{ top: 10, right: 10, left: 10, bottom: 10 }}
>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted/20" />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
className="text-xs fill-muted-foreground"
tick={false}
angle={-45}
textAnchor="end"
height={60}
interval={0}
/>
<YAxis
axisLine={false}
tickLine={false}
className="text-xs fill-muted-foreground"
tickFormatter={(value) => {
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
return value.toString();
}}
/>
<Tooltip content={<CustomTooltip />} />
<Bar
dataKey="value"
fill="#000000"
radius={[4, 4, 0, 0]}
/>
<Bar dataKey="value" radius={[2, 2, 0, 0]}>
{groupedData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
{/* Petites cartes légères pour chaque provider */}
<div className="mt-4 grid grid-cols-2 gap-3">
{groupedData.map((item, index) => (
<div
key={index}
className="p-3 rounded-lg border border-muted/20 bg-muted/5 hover:bg-muted/10 transition-colors"
style={{
borderLeftColor: item.color,
borderLeftWidth: "3px",
borderLeftStyle: "solid",
}}
>
<div className="flex items-center gap-2 mb-1">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: item.color }}
></div>
<h3
className="text-sm font-medium"
style={{ color: item.color }}
>
{item.name}
</h3>
</div>
<p className="text-lg font-semibold text-foreground">
{item.value.toLocaleString()}
</p>
<p className="text-xs text-muted-foreground">tokens</p>
</div>
))}
</div>
{/* Total général */}
{totalTokens && (
<div className="mt-4 pt-3 border-t border-muted/20 text-center">
<p className="text-sm text-muted-foreground">
Total général:{" "}
<span className="font-semibold text-foreground">
{totalTokens.toLocaleString()}
</span>{" "}
tokens
</p>
</div>
)}
{/* Légende dynamique des modèles */}
{allModels.length > 0 && (
<div className="mt-4 pt-3 border-t border-muted/20">
<h4 className="text-sm font-medium text-muted-foreground mb-3 text-center">
Modèles utilisés
</h4>
<div className="flex flex-wrap justify-center gap-x-4 gap-y-2">
{allModels
.sort((a, b) => b.value - a.value) // Trier par usage décroissant
.map((model, index) => (
<div key={index} className="flex items-center gap-2">
<div
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: model.color }}
></div>
<span className="text-xs text-muted-foreground">
{model.name}
</span>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
);

View File

@@ -44,12 +44,12 @@ export function RealUserActivityChart() {
{
name: "Utilisateurs actifs",
value: activity.activeUsers,
color: "#22c55e", // Vert clair pour actifs
color: "#000000", // Noir pour actifs
},
{
name: "Utilisateurs inactifs",
value: activity.inactiveUsers,
color: "#ef4444", // Rouge pour inactifs
color: "#666666", // Gris pour inactifs
},
];
@@ -62,7 +62,7 @@ export function RealUserActivityChart() {
Activité des utilisateurs
</CardTitle>
<p className="text-sm text-muted-foreground">
Actifs = connectés dans les 7 derniers jours
Actifs = connectés dans les 30 derniers jours
</p>
</CardHeader>
<CardContent>
@@ -74,8 +74,10 @@ export function RealUserActivityChart() {
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={5}
paddingAngle={2}
dataKey="value"
stroke="#ffffff"
strokeWidth={2}
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
@@ -86,17 +88,20 @@ export function RealUserActivityChart() {
backgroundColor: "hsl(var(--background))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
fontSize: "12px"
}}
formatter={(value: number) => [
`${value} utilisateurs (${((value / total) * 100).toFixed(
1
)}%)`,
`${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`,
"",
]}
/>
<Legend
wrapperStyle={{
paddingTop: "20px",
fontSize: "12px"
}}
formatter={(value, entry) => (
<span style={{ color: entry.color }}>
<span style={{ color: entry.color, fontWeight: 500 }}>
{value}: {entry.payload?.value} (
{((entry.payload?.value / total) * 100).toFixed(1)}%)
</span>

View File

@@ -31,8 +31,8 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
<AreaChart data={data}>
<defs>
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.3}/>
<stop offset="95%" stopColor={color} stopOpacity={0}/>
<stop offset="5%" stopColor={color} stopOpacity={0.8}/>
<stop offset="95%" stopColor={color} stopOpacity={0.2}/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted/20" />
@@ -46,6 +46,11 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
axisLine={false}
tickLine={false}
className="text-xs fill-muted-foreground"
tickFormatter={(value) => {
if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
return value.toString();
}}
/>
<Tooltip
contentStyle={{
@@ -54,12 +59,16 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
borderRadius: '8px',
fontSize: '12px'
}}
formatter={(value: number) => [
value >= 1000 ? `${(value / 1000).toFixed(1)}K tokens` : `${value} tokens`,
'Tokens consommés'
]}
/>
<Area
type="monotone"
dataKey="value"
stroke={color}
strokeWidth={2}
strokeWidth={3}
fill="url(#colorGradient)"
/>
</AreaChart>

View File

@@ -20,12 +20,12 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
{
name: 'Utilisateurs actifs',
value: activeUsers,
color: '#22c55e' // Vert clair pour actifs
color: '#000000' // Noir pour actifs
},
{
name: 'Utilisateurs inactifs',
value: inactiveUsers,
color: '#ef4444' // Rouge pour inactifs
color: '#666666' // Gris pour inactifs
},
];
@@ -36,7 +36,7 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
<CardHeader>
<CardTitle className="text-base font-medium">Activité des utilisateurs</CardTitle>
<p className="text-sm text-muted-foreground">
Actifs = connectés dans les 7 derniers jours
Actifs = connectés dans les 30 derniers jours
</p>
</CardHeader>
<CardContent>
@@ -48,8 +48,10 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
cy="50%"
innerRadius={60}
outerRadius={100}
paddingAngle={5}
paddingAngle={2}
dataKey="value"
stroke="#ffffff"
strokeWidth={2}
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
@@ -60,16 +62,22 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
backgroundColor: 'hsl(var(--background))',
border: '1px solid hsl(var(--border))',
borderRadius: '8px',
fontSize: '12px'
}}
formatter={(value: number) => [
`${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`,
''
'',
]}
/>
<Legend
<Legend
wrapperStyle={{
paddingTop: "20px",
fontSize: "12px"
}}
formatter={(value, entry) => (
<span style={{ color: entry.color }}>
{value}: {entry.payload?.value} ({((entry.payload?.value / total) * 100).toFixed(1)}%)
<span style={{ color: entry.color, fontWeight: 500 }}>
{value}: {entry.payload?.value} (
{((entry.payload?.value / total) * 100).toFixed(1)}%)
</span>
)}
/>

View File

@@ -11,7 +11,7 @@ export function RealTimeStats() {
if (loading) {
return (
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-6">
<div className="h-64 bg-muted animate-pulse rounded-lg" />
<div className="h-64 bg-muted animate-pulse rounded-lg" />
</div>
@@ -20,7 +20,7 @@ export function RealTimeStats() {
if (error) {
return (
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-6">
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="text-center">
@@ -47,7 +47,7 @@ export function RealTimeStats() {
if (!stats) {
return (
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-6">
<Card>
<CardContent className="flex items-center justify-center h-64">
<p className="text-sm text-muted-foreground">
@@ -67,7 +67,7 @@ export function RealTimeStats() {
}
return (
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-6">
<SimpleStatsChart
title="Tokens consommés par jour"
data={stats.dailyTokens}

View File

@@ -46,24 +46,20 @@ export function UsageAnalytics() {
setLoading(true);
// Console log pour débugger les données balances
console.log("=== DONNÉES BALANCES RÉCUPÉRÉES ===");
console.log("Nombre total d'entrées balances:", balances.length);
console.log("Toutes les entrées balances:", balances);
// NOUVEAU : Console log pour débugger les utilisateurs
console.log("=== DONNÉES UTILISATEURS ===");
console.log("Nombre total d'utilisateurs:", users.length);
console.log("Premiers 5 utilisateurs:", users.slice(0, 5));
// Analyser les doublons
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<string, number>();
balances.forEach(balance => {
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);
const duplicateUsers = Array.from(userCounts.entries()).filter(([, count]) => count > 1);
console.log("Utilisateurs avec plusieurs entrées:", duplicateUsers);
// Afficher quelques exemples d'entrées
@@ -73,6 +69,29 @@ export function UsageAnalytics() {
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));
@@ -90,41 +109,58 @@ export function UsageAnalytics() {
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 (5 dernières minutes)
const fiveMinutesAgo = new Date();
fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5);
// 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 >= fiveMinutesAgo;
return lastActivity >= thirtyDaysAgo;
}).length;
// CORRECTION : Créer une map des crédits par utilisateur en évitant les doublons
// CORRECTION AMÉLIORÉE : Créer une map des crédits par utilisateur
const creditsMap = new Map<string, number>();
// Grouper les balances par utilisateur
const balancesByUser = new Map<string, LibreChatBalance[]>();
balances.forEach((balance) => {
const userId = balance.user;
if (!balancesByUser.has(userId)) {
balancesByUser.set(userId, []);
// 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);
}
balancesByUser.get(userId)!.push(balance);
});
// Pour chaque utilisateur, prendre seulement la dernière entrée
// Pour chaque utilisateur, calculer les crédits selon votre logique métier
balancesByUser.forEach((userBalances, userId) => {
if (userBalances.length > 0) {
// Trier par date de mise à jour (plus récent en premier)
// 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);
// Prendre la plus récente
const latestBalance = sortedBalances[0];
creditsMap.set(userId, latestBalance.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);
}
});
@@ -167,12 +203,16 @@ export function UsageAnalytics() {
}
});
// CORRECTION : Calculer le total des crédits depuis la map corrigée
// 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]) => ({
@@ -242,7 +282,7 @@ export function UsageAnalytics() {
<CardContent>
<div className="text-2xl font-bold">{stats.totalUsers}</div>
<p className="text-xs text-muted-foreground">
{stats.activeUsers} actifs cette semaine
{stats.activeUsers} actifs ce mois
</p>
</CardContent>
</Card>

View File

@@ -1,12 +1,11 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { createClient } from "@/lib/supabase/client";
import Image from "next/image";
import {
LayoutDashboard,
@@ -21,135 +20,164 @@ import {
ChevronLeft,
ChevronRight,
BarChart3,
Activity,
LogOut,
User,
Mail,
} from "lucide-react";
import type { User as SupabaseUser } from "@supabase/supabase-js";
interface NavigationItem {
name: string;
href: string;
icon: React.ElementType;
badge?: string | null;
}
const navigation: NavigationItem[] = [
const topLevelNavigation = [
{
name: "Vue d'ensemble",
name: "Dashboard",
href: "/",
icon: LayoutDashboard,
badge: null,
},
{
name: "Analytics",
href: "/analytics",
icon: BarChart3,
badge: "Nouveau",
},
];
const dataNavigation: NavigationItem[] = [
{ name: "Utilisateurs", href: "/users", icon: Users, badge: null },
const navigationGroups = [
{
name: "Conversations",
href: "/conversations",
icon: MessageSquare,
badge: null,
name: "Données",
items: [
{
name: "Utilisateurs",
href: "/users",
icon: Users,
},
{
name: "Conversations",
href: "/conversations",
icon: MessageSquare,
},
{
name: "Messages",
href: "/messages",
icon: FileText,
},
{
name: "Transactions",
href: "/transactions",
icon: CreditCard,
},
],
},
{ name: "Messages", href: "/messages", icon: FileText, badge: null },
{
name: "Transactions",
href: "/transactions",
icon: CreditCard,
badge: null,
name: "Système",
items: [
{
name: "Collections",
href: "/collections",
icon: Database,
},
{
name: "Agents",
href: "/agents",
icon: Bot,
},
{
name: "Rôles",
href: "/roles",
icon: Shield,
},
{
name: "Paramètres",
href: "/settings",
icon: Settings,
},
],
},
];
const systemNavigation: NavigationItem[] = [
{ name: "Agents", href: "/agents", icon: Bot, badge: null },
{ name: "Rôles", href: "/roles", icon: Shield, badge: null },
{ name: "Collections", href: "/collections", icon: Database, badge: null },
{ name: "Paramètres", href: "/settings", icon: Settings, badge: null },
];
export function Sidebar() {
const [collapsed, setCollapsed] = useState(false);
const pathname = usePathname();
const router = useRouter();
const [collapsed, setCollapsed] = useState(false);
const [user, setUser] = useState<SupabaseUser | null>(null);
const [loading, setLoading] = useState(true);
const supabase = createClient();
const NavSection = ({
title,
items,
showTitle = true,
}: {
title: string;
items: NavigationItem[];
showTitle?: boolean;
}) => (
<div className="space-y-2">
{!collapsed && showTitle && (
<h3 className="px-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
{title}
</h3>
)}
{items.map((item) => {
const isActive = pathname === item.href;
return (
<Link key={item.name} href={item.href}>
<Button
variant={isActive ? "secondary" : "ghost"}
className={cn(
"w-full justify-start h-9 px-3",
collapsed && "px-2 justify-center",
isActive && "bg-secondary font-medium"
)}
>
<item.icon className={cn("h-4 w-4", collapsed ? "" : "mr-3")} />
{!collapsed && (
<div className="flex items-center justify-between w-full">
<span>{item.name}</span>
{item.badge && (
<Badge variant="secondary" className="text-xs">
{item.badge}
</Badge>
)}
</div>
)}
</Button>
</Link>
useEffect(() => {
const getUser = async () => {
try {
const {
data: { user },
} = await supabase.auth.getUser();
setUser(user);
} catch (error) {
console.error(
"Erreur lors de la récupération de l'utilisateur:",
error
);
})}
</div>
);
setUser(null);
} finally {
setLoading(false);
}
};
getUser();
// Écouter les changements d'authentification
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((event, session) => {
setUser(session?.user || null);
setLoading(false);
});
return () => subscription.unsubscribe();
}, [supabase.auth]);
const handleLogout = async () => {
try {
await supabase.auth.signOut();
router.push("/login");
router.refresh();
} catch (error) {
console.error("Erreur lors de la déconnexion:", error);
}
};
// Ne pas afficher la sidebar si l'utilisateur n'est pas connecté ou en cours de chargement
if (loading || !user) {
return null;
}
return (
<div
className={cn(
"flex flex-col h-screen bg-background border-r border-border transition-all duration-300 ease-in-out",
"flex flex-col h-screen bg-white border-r border-gray-200 transition-all duration-300",
collapsed ? "w-16" : "w-64"
)}
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border">
<div className="flex items-center justify-between p-4 border-b border-gray-200 flex-shrink-0">
{!collapsed && (
<div className="flex items-center space-x-2">
<div className="w-8 h-8 rounded-lg flex items-center justify-center">
<div className="flex items-center space-x-3">
<div className="relative w-8 h-8">
<Image
src="/img/logo.png"
alt="Cercle GPT Logo"
width={32}
height={32}
className="rounded-lg"
alt="Logo"
fill
className="object-contain"
/>
</div>
<div>
<h1 className="text-sm font-semibold">Cercle GPT</h1>
<p className="text-xs text-muted-foreground">Admin Dashboard</p>
<h1 className="text-lg font-semibold text-gray-900">
Cercle GPT
</h1>
<p className="text-xs text-gray-500">Admin Dashboard</p>
</div>
</div>
)}
<Button
variant="ghost"
size="icon"
size="sm"
onClick={() => setCollapsed(!collapsed)}
className="h-8 w-8"
className="p-1.5 hover:bg-gray-100"
>
{collapsed ? (
<ChevronRight className="h-4 w-4" />
@@ -160,32 +188,112 @@ export function Sidebar() {
</div>
{/* Navigation */}
<nav className="flex-1 p-3 space-y-6 overflow-y-auto">
<NavSection title="Dashboard" items={navigation} showTitle={false} />
<nav className="flex-1 p-4 space-y-6 overflow-y-auto">
{/* Navigation de niveau supérieur */}
<div className="space-y-1">
{topLevelNavigation.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors",
isActive
? "bg-gray-900 text-white"
: "text-gray-700 hover:bg-gray-100 hover:text-gray-900"
)}
>
<item.icon className="h-5 w-5 flex-shrink-0" />
{!collapsed && <span>{item.name}</span>}
</Link>
);
})}
</div>
{!collapsed && <Separator />}
{/* Séparateur */}
<div className="border-t border-gray-200"></div>
<NavSection title="Données" items={dataNavigation} />
{/* Groupes de navigation */}
{navigationGroups.map((group) => (
<div key={group.name}>
{/* Titre du groupe */}
{!collapsed && (
<h3 className="px-3 mb-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">
{group.name}
</h3>
)}
{!collapsed && <Separator />}
{/* Séparateur visuel quand collapsed */}
{collapsed && (
<div className="mb-2 mx-auto w-8 h-px bg-gray-300"></div>
)}
<NavSection title="Système" items={systemNavigation} />
</nav>
{/* Footer */}
{!collapsed && (
<div className="p-3 border-t border-border">
<div className="flex items-center space-x-3 p-2 rounded-lg bg-muted/50">
<div className="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
<Activity className="h-4 w-4 text-primary" />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-medium">Système en ligne</p>
<p className="text-xs text-muted-foreground">Tout fonctionne</p>
{/* Items du groupe */}
<div className="space-y-1">
{group.items.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors",
isActive
? "bg-gray-900 text-white"
: "text-gray-700 hover:bg-gray-100 hover:text-gray-900"
)}
>
<item.icon className="h-5 w-5 flex-shrink-0" />
{!collapsed && <span>{item.name}</span>}
</Link>
);
})}
</div>
</div>
))}
</nav>
{/* Section utilisateur connecté */}
<div className="p-4 border-t border-gray-200 space-y-3 flex-shrink-0 bg-white">
{/* Informations utilisateur */}
<div
className={cn(
"flex items-center space-x-3 px-3 py-2 bg-gray-50 rounded-md",
collapsed && "justify-center"
)}
>
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-gray-200 rounded-full flex items-center justify-center">
<User className="h-4 w-4 text-gray-600" />
</div>
</div>
{!collapsed && (
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
Administrateur
</p>
<div className="flex items-center space-x-1">
<Mail className="h-3 w-3 text-gray-400" />
<p className="text-xs text-gray-500 truncate">{user.email}</p>
</div>
</div>
)}
</div>
)}
{/* Bouton de déconnexion */}
<Button
variant="outline"
onClick={handleLogout}
className={cn(
"w-full flex items-center space-x-2 text-sm font-medium border-gray-200 hover:bg-gray-50",
collapsed && "justify-center px-2"
)}
>
<LogOut className="h-4 w-4" />
{!collapsed && <span>Déconnexion</span>}
</Button>
</div>
</div>
);
}

46
components/ui/alert.tsx Normal file
View File

@@ -0,0 +1,46 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertDescription }