new
This commit is contained in:
@@ -2,6 +2,7 @@ import { Suspense } from "react";
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { UsageAnalytics } from "@/components/dashboard/usage-analytics";
|
import { UsageAnalytics } from "@/components/dashboard/usage-analytics";
|
||||||
import { RecentTransactions } from "@/components/dashboard/recent-transactions";
|
import { RecentTransactions } from "@/components/dashboard/recent-transactions";
|
||||||
|
|
||||||
import { BarChart3 } from "lucide-react";
|
import { BarChart3 } from "lucide-react";
|
||||||
|
|
||||||
function AnalyticsSkeleton() {
|
function AnalyticsSkeleton() {
|
||||||
|
|||||||
124
app/api/add-credits/route.ts
Normal file
124
app/api/add-credits/route.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getDatabase } from "@/lib/db/mongodb";
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
try {
|
||||||
|
const db = await getDatabase();
|
||||||
|
const CREDITS_TO_ADD = 5000000; // 5 millions de tokens
|
||||||
|
|
||||||
|
console.log(`🚀 DÉBUT: Ajout de ${CREDITS_TO_ADD.toLocaleString()} crédits à tous les utilisateurs`);
|
||||||
|
|
||||||
|
// Récupérer tous les utilisateurs existants
|
||||||
|
const users = await db.collection("users").find({}).toArray();
|
||||||
|
console.log(`👥 Utilisateurs trouvés: ${users.length}`);
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: "Aucun utilisateur trouvé"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer toutes les balances existantes
|
||||||
|
const existingBalances = await db.collection("balances").find({}).toArray();
|
||||||
|
const existingBalanceUserIds = new Set(
|
||||||
|
existingBalances.map(balance => balance.user.toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`💰 Balances existantes: ${existingBalances.length}`);
|
||||||
|
|
||||||
|
let updatedCount = 0;
|
||||||
|
let createdCount = 0;
|
||||||
|
let totalCreditsAdded = 0;
|
||||||
|
|
||||||
|
// Pour chaque utilisateur
|
||||||
|
for (const user of users) {
|
||||||
|
const userId = user._id;
|
||||||
|
|
||||||
|
if (existingBalanceUserIds.has(userId.toString())) {
|
||||||
|
// Utilisateur a déjà une balance - ajouter les crédits
|
||||||
|
const updateResult = await db.collection("balances").updateOne(
|
||||||
|
{ user: userId },
|
||||||
|
{
|
||||||
|
$inc: { tokenCredits: CREDITS_TO_ADD },
|
||||||
|
$set: { lastRefill: new Date() }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updateResult.modifiedCount > 0) {
|
||||||
|
updatedCount++;
|
||||||
|
totalCreditsAdded += CREDITS_TO_ADD;
|
||||||
|
console.log(`✅ Crédits ajoutés pour l'utilisateur: ${user.email || user.name}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Utilisateur n'a pas de balance - créer une nouvelle balance
|
||||||
|
const newBalance = {
|
||||||
|
user: userId,
|
||||||
|
tokenCredits: CREDITS_TO_ADD,
|
||||||
|
autoRefillEnabled: false,
|
||||||
|
lastRefill: new Date(),
|
||||||
|
refillAmount: 0,
|
||||||
|
refillIntervalUnit: "month",
|
||||||
|
refillIntervalValue: 1,
|
||||||
|
__v: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
await db.collection("balances").insertOne(newBalance);
|
||||||
|
createdCount++;
|
||||||
|
totalCreditsAdded += CREDITS_TO_ADD;
|
||||||
|
console.log(`🆕 Nouvelle balance créée pour: ${user.email || user.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ TERMINÉ:`);
|
||||||
|
console.log(`- Balances mises à jour: ${updatedCount}`);
|
||||||
|
console.log(`- Nouvelles balances créées: ${createdCount}`);
|
||||||
|
console.log(`- Total crédits ajoutés: ${totalCreditsAdded.toLocaleString()}`);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
statistics: {
|
||||||
|
totalUsers: users.length,
|
||||||
|
updatedBalances: updatedCount,
|
||||||
|
createdBalances: createdCount,
|
||||||
|
creditsPerUser: CREDITS_TO_ADD,
|
||||||
|
totalCreditsAdded
|
||||||
|
},
|
||||||
|
message: `${CREDITS_TO_ADD.toLocaleString()} crédits ajoutés à ${users.length} utilisateurs (${updatedCount} mis à jour, ${createdCount} créés)`
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de l'ajout des crédits:", error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: "Erreur serveur lors de l'ajout des crédits"
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint pour vérifier les crédits actuels
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const db = await getDatabase();
|
||||||
|
|
||||||
|
const users = await db.collection("users").find({}).toArray();
|
||||||
|
const balances = await db.collection("balances").find({}).toArray();
|
||||||
|
|
||||||
|
const totalCredits = balances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0);
|
||||||
|
const averageCredits = balances.length > 0 ? totalCredits / balances.length : 0;
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
statistics: {
|
||||||
|
totalUsers: users.length,
|
||||||
|
totalBalances: balances.length,
|
||||||
|
totalCredits,
|
||||||
|
averageCredits: Math.round(averageCredits),
|
||||||
|
usersWithoutBalance: users.length - balances.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des statistiques:", error);
|
||||||
|
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
104
app/api/cleanup/balances/route.ts
Normal file
104
app/api/cleanup/balances/route.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { getDatabase } from "@/lib/db/mongodb";
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
try {
|
||||||
|
const db = await getDatabase();
|
||||||
|
|
||||||
|
// Récupérer tous les utilisateurs existants
|
||||||
|
const users = await db.collection("users").find({}).toArray();
|
||||||
|
const userIds = new Set(users.map(user => user._id.toString()));
|
||||||
|
|
||||||
|
// Récupérer toutes les balances
|
||||||
|
const balances = await db.collection("balances").find({}).toArray();
|
||||||
|
|
||||||
|
// Identifier les balances fantômes
|
||||||
|
const phantomBalances = balances.filter(balance =>
|
||||||
|
!userIds.has(balance.user.toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculer les statistiques avant nettoyage
|
||||||
|
const totalBalances = balances.length;
|
||||||
|
const phantomCount = phantomBalances.length;
|
||||||
|
const phantomCredits = phantomBalances.reduce(
|
||||||
|
(sum, balance) => sum + (balance.tokenCredits || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`🗑️ SUPPRESSION: ${phantomCount} balances fantômes détectées`);
|
||||||
|
console.log(`💰 CRÉDITS FANTÔMES: ${phantomCredits}`);
|
||||||
|
|
||||||
|
// SUPPRESSION DÉFINITIVE des balances fantômes
|
||||||
|
const deleteResult = await db.collection("balances").deleteMany({
|
||||||
|
user: { $nin: Array.from(userIds) }
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ SUPPRIMÉES: ${deleteResult.deletedCount} balances`);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
statistics: {
|
||||||
|
totalBalances,
|
||||||
|
phantomCount,
|
||||||
|
phantomCredits,
|
||||||
|
cleanedCount: deleteResult.deletedCount
|
||||||
|
},
|
||||||
|
message: `${deleteResult.deletedCount} balances fantômes supprimées définitivement`
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors du nettoyage des balances:", error);
|
||||||
|
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Endpoint pour analyser sans nettoyer
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const db = await getDatabase();
|
||||||
|
|
||||||
|
const users = await db.collection("users").find({}).toArray();
|
||||||
|
const userIds = new Set(users.map(user => user._id.toString()));
|
||||||
|
|
||||||
|
const balances = await db.collection("balances").find({}).toArray();
|
||||||
|
|
||||||
|
const phantomBalances = balances.filter(balance =>
|
||||||
|
!userIds.has(balance.user.toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
const phantomCredits = phantomBalances.reduce(
|
||||||
|
(sum, balance) => sum + (balance.tokenCredits || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Analyser les doublons aussi
|
||||||
|
const userCounts = new Map<string, number>();
|
||||||
|
balances.forEach(balance => {
|
||||||
|
const userId = balance.user.toString();
|
||||||
|
userCounts.set(userId, (userCounts.get(userId) || 0) + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const duplicates = Array.from(userCounts.entries())
|
||||||
|
.filter(([, count]) => count > 1)
|
||||||
|
.map(([userId, count]) => ({
|
||||||
|
userId,
|
||||||
|
count,
|
||||||
|
isPhantom: !userIds.has(userId)
|
||||||
|
}));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
analysis: {
|
||||||
|
totalBalances: balances.length,
|
||||||
|
totalUsers: users.length,
|
||||||
|
phantomBalances: phantomBalances.length,
|
||||||
|
phantomCredits,
|
||||||
|
duplicateUsers: duplicates.length,
|
||||||
|
duplicates: duplicates.slice(0, 10) // Premiers 10 exemples
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de l'analyse des balances:", error);
|
||||||
|
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,21 +13,21 @@ export async function GET() {
|
|||||||
db.collection("balances").find({}).toArray(),
|
db.collection("balances").find({}).toArray(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Calculer les utilisateurs actifs (dernière semaine)
|
// Calculer les utilisateurs actifs (30 derniers jours)
|
||||||
const oneWeekAgo = new Date();
|
const thirtyDaysAgo = new Date();
|
||||||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
const activeUsers = users.filter((user) => {
|
const activeUsers = users.filter((user) => {
|
||||||
const lastActivity = new Date(user.updatedAt || user.createdAt);
|
const lastActivity = new Date(user.updatedAt || user.createdAt);
|
||||||
return lastActivity >= oneWeekAgo;
|
return lastActivity >= thirtyDaysAgo;
|
||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
// Calculer les administrateurs
|
// Calculer les administrateurs
|
||||||
const totalAdmins = users.filter(user => user.role === 'ADMIN').length;
|
const totalAdmins = users.filter(user => user.role === 'ADMIN').length;
|
||||||
|
|
||||||
// Calculer les conversations actives (dernière semaine)
|
// Calculer les conversations actives (30 derniers jours)
|
||||||
const activeConversations = conversations.filter((conv) => {
|
const activeConversations = conversations.filter((conv) => {
|
||||||
const lastActivity = new Date(conv.updatedAt || conv.createdAt);
|
const lastActivity = new Date(conv.updatedAt || conv.createdAt);
|
||||||
return lastActivity >= oneWeekAgo;
|
return lastActivity >= thirtyDaysAgo;
|
||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
// Calculer le total des messages
|
// Calculer le total des messages
|
||||||
@@ -41,32 +41,27 @@ export async function GET() {
|
|||||||
return sum + Math.abs(Number(transaction.rawAmount) || 0);
|
return sum + Math.abs(Number(transaction.rawAmount) || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
// Calculer le total des crédits depuis balances
|
// Calculer le total des crédits
|
||||||
const totalCredits = balances.reduce((sum, balance) => {
|
const totalCreditsUsed = balances.reduce(
|
||||||
return sum + (Number(balance.tokenCredits) || 0);
|
(sum, balance) => sum + (balance.tokenCredits || 0),
|
||||||
}, 0);
|
0
|
||||||
|
);
|
||||||
|
|
||||||
// Récupérer les transactions récentes (dernières 10)
|
// Récupérer les transactions récentes (dernières 10)
|
||||||
const recentTransactions = transactions
|
const recentTransactions = transactions
|
||||||
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
.slice(0, 10)
|
.slice(0, 10);
|
||||||
.map(transaction => ({
|
|
||||||
_id: transaction._id,
|
|
||||||
description: `Transaction ${transaction.tokenType} - ${transaction.model}`,
|
|
||||||
amount: transaction.rawAmount,
|
|
||||||
type: transaction.rawAmount > 0 ? 'credit' : 'debit',
|
|
||||||
createdAt: transaction.createdAt
|
|
||||||
}));
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
totalUsers: users.length,
|
totalUsers: users.length,
|
||||||
activeUsers,
|
activeUsers,
|
||||||
totalAdmins,
|
totalAdmins,
|
||||||
totalCredits,
|
totalCredits: totalCreditsUsed,
|
||||||
activeConversations,
|
activeConversations,
|
||||||
totalMessages: totalMessages,
|
totalMessages,
|
||||||
totalTokensConsumed,
|
totalTokensConsumed,
|
||||||
recentTransactions
|
totalCreditsUsed,
|
||||||
|
recentTransactions,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors du calcul des métriques:", error);
|
console.error("Erreur lors du calcul des métriques:", error);
|
||||||
|
|||||||
@@ -8,6 +8,22 @@ export async function GET() {
|
|||||||
// Récupérer toutes les transactions
|
// Récupérer toutes les transactions
|
||||||
const transactions = await db.collection("transactions").find({}).toArray();
|
const transactions = await db.collection("transactions").find({}).toArray();
|
||||||
|
|
||||||
|
console.log(`Total transactions trouvées: ${transactions.length}`);
|
||||||
|
|
||||||
|
// Vérifier les champs de date disponibles dans les transactions
|
||||||
|
if (transactions.length > 0) {
|
||||||
|
const sampleTransaction = transactions[0];
|
||||||
|
console.log("Exemple de transaction:", {
|
||||||
|
_id: sampleTransaction._id,
|
||||||
|
createdAt: sampleTransaction.createdAt,
|
||||||
|
updatedAt: sampleTransaction.updatedAt,
|
||||||
|
date: sampleTransaction.date,
|
||||||
|
timestamp: sampleTransaction.timestamp,
|
||||||
|
rawAmount: sampleTransaction.rawAmount,
|
||||||
|
model: sampleTransaction.model
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Calculer les tokens par jour (7 derniers jours)
|
// Calculer les tokens par jour (7 derniers jours)
|
||||||
const dailyStats = [];
|
const dailyStats = [];
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@@ -22,14 +38,44 @@ export async function GET() {
|
|||||||
nextDate.setDate(nextDate.getDate() + 1);
|
nextDate.setDate(nextDate.getDate() + 1);
|
||||||
|
|
||||||
const dayTransactions = transactions.filter(transaction => {
|
const dayTransactions = transactions.filter(transaction => {
|
||||||
const transactionDate = new Date(transaction.createdAt);
|
// Essayer différents champs de date
|
||||||
|
let transactionDate = null;
|
||||||
|
|
||||||
|
if (transaction.createdAt) {
|
||||||
|
transactionDate = new Date(transaction.createdAt);
|
||||||
|
} else if (transaction.updatedAt) {
|
||||||
|
transactionDate = new Date(transaction.updatedAt);
|
||||||
|
} else if (transaction.date) {
|
||||||
|
transactionDate = new Date(transaction.date);
|
||||||
|
} else if (transaction.timestamp) {
|
||||||
|
transactionDate = new Date(transaction.timestamp);
|
||||||
|
} else if (transaction._id && transaction._id.getTimestamp) {
|
||||||
|
// Utiliser le timestamp de l'ObjectId MongoDB
|
||||||
|
transactionDate = transaction._id.getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transactionDate || isNaN(transactionDate.getTime())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return transactionDate >= date && transactionDate < nextDate;
|
return transactionDate >= date && transactionDate < nextDate;
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalTokens = dayTransactions.reduce((sum, transaction) => {
|
const totalTokens = dayTransactions.reduce((sum, transaction) => {
|
||||||
return sum + Math.abs(Number(transaction.rawAmount) || 0);
|
// Essayer différents champs pour les tokens
|
||||||
|
let tokens = 0;
|
||||||
|
if (transaction.rawAmount) {
|
||||||
|
tokens = Math.abs(Number(transaction.rawAmount) || 0);
|
||||||
|
} else if (transaction.amount) {
|
||||||
|
tokens = Math.abs(Number(transaction.amount) || 0);
|
||||||
|
} else if (transaction.tokens) {
|
||||||
|
tokens = Math.abs(Number(transaction.tokens) || 0);
|
||||||
|
}
|
||||||
|
return sum + tokens;
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
|
console.log(`${dayNames[date.getDay()]} (${date.toISOString().split('T')[0]}): ${dayTransactions.length} transactions, ${totalTokens} tokens`);
|
||||||
|
|
||||||
dailyStats.push({
|
dailyStats.push({
|
||||||
name: dayNames[date.getDay()],
|
name: dayNames[date.getDay()],
|
||||||
value: totalTokens
|
value: totalTokens
|
||||||
@@ -40,9 +86,19 @@ export async function GET() {
|
|||||||
const modelStats = new Map<string, number>();
|
const modelStats = new Map<string, number>();
|
||||||
|
|
||||||
transactions.forEach(transaction => {
|
transactions.forEach(transaction => {
|
||||||
const model = transaction.model || "Inconnu";
|
const model = transaction.model || transaction.modelName || "Inconnu";
|
||||||
const tokens = Math.abs(Number(transaction.rawAmount) || 0);
|
let tokens = 0;
|
||||||
modelStats.set(model, (modelStats.get(model) || 0) + tokens);
|
if (transaction.rawAmount) {
|
||||||
|
tokens = Math.abs(Number(transaction.rawAmount) || 0);
|
||||||
|
} else if (transaction.amount) {
|
||||||
|
tokens = Math.abs(Number(transaction.amount) || 0);
|
||||||
|
} else if (transaction.tokens) {
|
||||||
|
tokens = Math.abs(Number(transaction.tokens) || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokens > 0) {
|
||||||
|
modelStats.set(model, (modelStats.get(model) || 0) + tokens);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convertir en array et trier par usage
|
// Convertir en array et trier par usage
|
||||||
@@ -50,6 +106,12 @@ export async function GET() {
|
|||||||
.map(([name, value]) => ({ name, value }))
|
.map(([name, value]) => ({ name, value }))
|
||||||
.sort((a, b) => b.value - a.value);
|
.sort((a, b) => b.value - a.value);
|
||||||
|
|
||||||
|
console.log("Statistiques calculées:", {
|
||||||
|
dailyStats,
|
||||||
|
totalModels: modelData.length,
|
||||||
|
topModel: modelData[0]
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
dailyTokens: dailyStats,
|
dailyTokens: dailyStats,
|
||||||
modelDistribution: modelData
|
modelDistribution: modelData
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ export async function GET() {
|
|||||||
// Récupérer tous les utilisateurs
|
// Récupérer tous les utilisateurs
|
||||||
const users = await db.collection("users").find({}).toArray();
|
const users = await db.collection("users").find({}).toArray();
|
||||||
|
|
||||||
// Calculer les utilisateurs actifs (dernière semaine)
|
// Calculer les utilisateurs actifs (30 derniers jours)
|
||||||
const oneWeekAgo = new Date();
|
const thirtyDaysAgo = new Date();
|
||||||
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
|
|
||||||
let activeUsers = 0;
|
let activeUsers = 0;
|
||||||
let inactiveUsers = 0;
|
let inactiveUsers = 0;
|
||||||
|
|
||||||
users.forEach(user => {
|
users.forEach(user => {
|
||||||
const lastActivity = new Date(user.updatedAt || user.createdAt);
|
const lastActivity = new Date(user.updatedAt || user.createdAt);
|
||||||
if (lastActivity >= oneWeekAgo) {
|
if (lastActivity >= thirtyDaysAgo) {
|
||||||
activeUsers++;
|
activeUsers++;
|
||||||
} else {
|
} else {
|
||||||
inactiveUsers++;
|
inactiveUsers++;
|
||||||
|
|||||||
194
app/login/page.tsx
Normal file
194
app/login/page.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { createClient } from "@/lib/supabase/client";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { Loader2, Mail, Lock, Shield } from "lucide-react";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Fonction pour traduire les erreurs Supabase en français
|
||||||
|
const getErrorMessage = (error: string) => {
|
||||||
|
const errorMessages: { [key: string]: string } = {
|
||||||
|
"Invalid login credentials": "Identifiants de connexion invalides",
|
||||||
|
"Email not confirmed": "Email non confirmé",
|
||||||
|
"Too many requests": "Trop de tentatives de connexion",
|
||||||
|
"User not found": "Utilisateur non trouvé",
|
||||||
|
"Invalid email": "Adresse email invalide",
|
||||||
|
"Password should be at least 6 characters": "Le mot de passe doit contenir au moins 6 caractères",
|
||||||
|
"Email rate limit exceeded": "Limite de tentatives dépassée",
|
||||||
|
"Invalid email or password": "Email ou mot de passe incorrect",
|
||||||
|
"Account not found": "Compte non trouvé",
|
||||||
|
"Invalid credentials": "Identifiants incorrects",
|
||||||
|
"Authentication failed": "Échec de l'authentification",
|
||||||
|
"Access denied": "Accès refusé",
|
||||||
|
"Unauthorized": "Non autorisé",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Chercher une correspondance exacte
|
||||||
|
if (errorMessages[error]) {
|
||||||
|
return errorMessages[error];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chercher une correspondance partielle
|
||||||
|
for (const [englishError, frenchError] of Object.entries(errorMessages)) {
|
||||||
|
if (error.toLowerCase().includes(englishError.toLowerCase())) {
|
||||||
|
return frenchError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message par défaut si aucune correspondance
|
||||||
|
return "Erreur de connexion. Vérifiez vos identifiants.";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
// Validation côté client
|
||||||
|
if (!email || !password) {
|
||||||
|
setError("Veuillez remplir tous les champs");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.includes("@")) {
|
||||||
|
setError("Veuillez entrer une adresse email valide");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const supabase = createClient();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data, error: authError } = await supabase.auth.signInWithPassword({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (authError) {
|
||||||
|
setError(getErrorMessage(authError.message));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.user) {
|
||||||
|
router.push("/");
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
setError("Une erreur inattendue est survenue. Veuillez réessayer.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||||
|
<Card className="w-full max-w-md shadow-lg border border-gray-200">
|
||||||
|
<CardHeader className="space-y-6 pb-8">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="relative w-16 h-16 rounded-full bg-gray-100 p-2">
|
||||||
|
<Image
|
||||||
|
src="/img/logo.png"
|
||||||
|
alt="Logo"
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
className="rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center space-y-2">
|
||||||
|
<CardTitle className="text-2xl font-bold text-gray-900 flex items-center justify-center gap-2">
|
||||||
|
<Shield className="w-6 h-6 text-gray-700" />
|
||||||
|
Admin Dashboard
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
Connectez-vous pour accéder au tableau de bord
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{error && (
|
||||||
|
<Alert className="border-red-200 bg-red-50">
|
||||||
|
<AlertDescription className="text-red-700">
|
||||||
|
{error}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="email" className="text-sm font-medium text-gray-700">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="admin@example.com"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="pl-10 border-gray-300 focus:border-gray-900 focus:ring-gray-900"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="password" className="text-sm font-medium text-gray-700">
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="pl-10 border-gray-300 focus:border-gray-900 focus:ring-gray-900"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-gray-900 hover:bg-gray-800 text-white font-medium py-2.5 transition-colors"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Connexion en cours...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Se connecter"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Accès réservé aux administrateurs autorisés
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Database, Server, Settings } from "lucide-react";
|
||||||
|
|
||||||
|
import AddCredits from "@/components/dashboard/add-credits";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
return (
|
return (
|
||||||
@@ -7,65 +16,73 @@ export default function SettingsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Paramètres</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Paramètres</h1>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Configuration du dashboard Cercle GPT
|
Configuration et maintenance du système
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Informations système */}
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle>Connexion MongoDB</CardTitle>
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
Base de données
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-sm text-muted-foreground">Statut:</span>
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Statut MongoDB
|
||||||
|
</span>
|
||||||
<Badge variant="default">Connecté</Badge>
|
<Badge variant="default">Connecté</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Base de données:
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-mono">Cercle GPT</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Collections:
|
|
||||||
</span>
|
|
||||||
<span className="text-sm">29 collections</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle>Informations système</CardTitle>
|
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||||||
|
<Server className="h-4 w-4" />
|
||||||
|
Informations système
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Version Next.js:
|
Version Next.js
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm">15.5.4</span>
|
<Badge variant="secondary">14.x</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Version Node.js:
|
Version Node.js
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm">{process.version}</span>
|
<Badge variant="secondary">18.x</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Environnement:
|
Environnement
|
||||||
</span>
|
</span>
|
||||||
<Badge variant="outline">{process.env.NODE_ENV}</Badge>
|
<Badge variant="outline">Development</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Gestion des crédits */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight mb-4 flex items-center gap-2">
|
||||||
|
<Settings className="h-6 w-6" />
|
||||||
|
Gestion des Crédits
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<AddCredits />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ export function UsersTable() {
|
|||||||
{users.map((user) => {
|
{users.map((user) => {
|
||||||
const userCredits = creditsMap.get(user._id) || 0;
|
const userCredits = creditsMap.get(user._id) || 0;
|
||||||
const isActive = new Date(user.updatedAt || user.createdAt) >
|
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 (
|
return (
|
||||||
<TableRow key={user._id}>
|
<TableRow key={user._id}>
|
||||||
@@ -122,13 +122,13 @@ export function UsersTable() {
|
|||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={isActive ? 'default' : 'destructive'}>
|
<Badge variant={isActive ? 'default' : 'secondary'}>
|
||||||
{isActive ? 'Actif' : 'Inactif'}
|
{isActive ? 'Actif' : 'Inactif'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{formatDate(new Date(user.createdAt))}
|
{formatDate(user.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
188
components/dashboard/add-credits.tsx
Normal file
188
components/dashboard/add-credits.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,14 +9,23 @@ import {
|
|||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
|
Cell,
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
|
|
||||||
interface ModelDistributionChartProps {
|
interface ModelDistributionChartProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
data: Array<{
|
data: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
value: number;
|
value: number;
|
||||||
|
color?: string;
|
||||||
|
models?: Array<{
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}>;
|
||||||
}>;
|
}>;
|
||||||
|
showLegend?: boolean;
|
||||||
|
totalTokens?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TooltipPayload {
|
interface TooltipPayload {
|
||||||
@@ -24,6 +33,11 @@ interface TooltipPayload {
|
|||||||
payload: {
|
payload: {
|
||||||
name: string;
|
name: string;
|
||||||
value: number;
|
value: number;
|
||||||
|
color?: string;
|
||||||
|
models?: Array<{
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,63 +46,211 @@ interface CustomTooltipProps {
|
|||||||
payload?: TooltipPayload[];
|
payload?: TooltipPayload[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomTooltip = ({ active, payload }: CustomTooltipProps) => {
|
// Couleurs par fournisseur selon l'image
|
||||||
if (active && payload && payload.length) {
|
const providerColors: { [key: string]: string } = {
|
||||||
return (
|
Anthropic: "#7C3AED", // Violet vif
|
||||||
<div style={{
|
OpenAI: "#059669", // Vert turquoise vif
|
||||||
backgroundColor: "hsl(var(--background))",
|
"Mistral AI": "#D97706", // Orange vif
|
||||||
border: "1px solid hsl(var(--border))",
|
Meta: "#DB2777", // Rose/Magenta vif
|
||||||
borderRadius: "8px",
|
Google: "#2563EB", // Bleu vif
|
||||||
padding: "8px",
|
Cohere: "#0891B2", // Cyan vif
|
||||||
fontSize: "12px"
|
};
|
||||||
}}>
|
|
||||||
<p style={{ margin: 0, color: "#ff0000" }}>
|
// Fonction pour regrouper les modèles par fournisseur
|
||||||
{`${payload[0].value.toLocaleString()} tokens`}
|
const groupByProvider = (modelData: Array<{ name: string; value: number }>) => {
|
||||||
</p>
|
const providerMap: {
|
||||||
<p style={{ margin: 0, color: "#ff0000" }}>
|
[key: string]: {
|
||||||
{payload[0].payload.name}
|
value: number;
|
||||||
</p>
|
models: Array<{ name: string; value: number }>;
|
||||||
</div>
|
};
|
||||||
);
|
} = {};
|
||||||
}
|
|
||||||
|
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;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ModelDistributionChart({
|
export function ModelDistributionChart({
|
||||||
title,
|
title,
|
||||||
|
subtitle,
|
||||||
data,
|
data,
|
||||||
|
totalTokens,
|
||||||
}: ModelDistributionChartProps) {
|
}: 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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="pb-2">
|
<CardHeader className="pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
{title}
|
{title}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">{subtitle}</p>
|
||||||
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0">
|
||||||
<ResponsiveContainer width="100%" height={200}>
|
<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" />
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted/20" />
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey="name"
|
dataKey="name"
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
className="text-xs fill-muted-foreground"
|
className="text-xs fill-muted-foreground"
|
||||||
tick={false}
|
angle={-45}
|
||||||
|
textAnchor="end"
|
||||||
|
height={60}
|
||||||
|
interval={0}
|
||||||
/>
|
/>
|
||||||
<YAxis
|
<YAxis
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
className="text-xs fill-muted-foreground"
|
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 />} />
|
<Tooltip content={<CustomTooltip />} />
|
||||||
<Bar
|
<Bar dataKey="value" radius={[2, 2, 0, 0]}>
|
||||||
dataKey="value"
|
{groupedData.map((entry, index) => (
|
||||||
fill="#000000"
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
radius={[4, 4, 0, 0]}
|
))}
|
||||||
/>
|
</Bar>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ export function RealUserActivityChart() {
|
|||||||
{
|
{
|
||||||
name: "Utilisateurs actifs",
|
name: "Utilisateurs actifs",
|
||||||
value: activity.activeUsers,
|
value: activity.activeUsers,
|
||||||
color: "#22c55e", // Vert clair pour actifs
|
color: "#000000", // Noir pour actifs
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Utilisateurs inactifs",
|
name: "Utilisateurs inactifs",
|
||||||
value: activity.inactiveUsers,
|
value: activity.inactiveUsers,
|
||||||
color: "#ef4444", // Rouge pour inactifs
|
color: "#666666", // Gris pour inactifs
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export function RealUserActivityChart() {
|
|||||||
Activité des utilisateurs
|
Activité des utilisateurs
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Actifs = connectés dans les 7 derniers jours
|
Actifs = connectés dans les 30 derniers jours
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -74,8 +74,10 @@ export function RealUserActivityChart() {
|
|||||||
cy="50%"
|
cy="50%"
|
||||||
innerRadius={60}
|
innerRadius={60}
|
||||||
outerRadius={100}
|
outerRadius={100}
|
||||||
paddingAngle={5}
|
paddingAngle={2}
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
|
stroke="#ffffff"
|
||||||
|
strokeWidth={2}
|
||||||
>
|
>
|
||||||
{data.map((entry, index) => (
|
{data.map((entry, index) => (
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
@@ -86,17 +88,20 @@ export function RealUserActivityChart() {
|
|||||||
backgroundColor: "hsl(var(--background))",
|
backgroundColor: "hsl(var(--background))",
|
||||||
border: "1px solid hsl(var(--border))",
|
border: "1px solid hsl(var(--border))",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
|
fontSize: "12px"
|
||||||
}}
|
}}
|
||||||
formatter={(value: number) => [
|
formatter={(value: number) => [
|
||||||
`${value} utilisateurs (${((value / total) * 100).toFixed(
|
`${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`,
|
||||||
1
|
|
||||||
)}%)`,
|
|
||||||
"",
|
"",
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Legend
|
<Legend
|
||||||
|
wrapperStyle={{
|
||||||
|
paddingTop: "20px",
|
||||||
|
fontSize: "12px"
|
||||||
|
}}
|
||||||
formatter={(value, entry) => (
|
formatter={(value, entry) => (
|
||||||
<span style={{ color: entry.color }}>
|
<span style={{ color: entry.color, fontWeight: 500 }}>
|
||||||
{value}: {entry.payload?.value} (
|
{value}: {entry.payload?.value} (
|
||||||
{((entry.payload?.value / total) * 100).toFixed(1)}%)
|
{((entry.payload?.value / total) * 100).toFixed(1)}%)
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
|
|||||||
<AreaChart data={data}>
|
<AreaChart data={data}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor={color} stopOpacity={0.3}/>
|
<stop offset="5%" stopColor={color} stopOpacity={0.8}/>
|
||||||
<stop offset="95%" stopColor={color} stopOpacity={0}/>
|
<stop offset="95%" stopColor={color} stopOpacity={0.2}/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<CartesianGrid strokeDasharray="3 3" className="stroke-muted/20" />
|
<CartesianGrid strokeDasharray="3 3" className="stroke-muted/20" />
|
||||||
@@ -46,6 +46,11 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
|
|||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
className="text-xs fill-muted-foreground"
|
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
|
<Tooltip
|
||||||
contentStyle={{
|
contentStyle={{
|
||||||
@@ -54,12 +59,16 @@ export function SimpleStatsChart({ title, data, color = "hsl(var(--primary))" }:
|
|||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
fontSize: '12px'
|
fontSize: '12px'
|
||||||
}}
|
}}
|
||||||
|
formatter={(value: number) => [
|
||||||
|
value >= 1000 ? `${(value / 1000).toFixed(1)}K tokens` : `${value} tokens`,
|
||||||
|
'Tokens consommés'
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<Area
|
<Area
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
stroke={color}
|
stroke={color}
|
||||||
strokeWidth={2}
|
strokeWidth={3}
|
||||||
fill="url(#colorGradient)"
|
fill="url(#colorGradient)"
|
||||||
/>
|
/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
|
|||||||
{
|
{
|
||||||
name: 'Utilisateurs actifs',
|
name: 'Utilisateurs actifs',
|
||||||
value: activeUsers,
|
value: activeUsers,
|
||||||
color: '#22c55e' // Vert clair pour actifs
|
color: '#000000' // Noir pour actifs
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Utilisateurs inactifs',
|
name: 'Utilisateurs inactifs',
|
||||||
value: inactiveUsers,
|
value: inactiveUsers,
|
||||||
color: '#ef4444' // Rouge pour inactifs
|
color: '#666666' // Gris pour inactifs
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base font-medium">Activité des utilisateurs</CardTitle>
|
<CardTitle className="text-base font-medium">Activité des utilisateurs</CardTitle>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Actifs = connectés dans les 7 derniers jours
|
Actifs = connectés dans les 30 derniers jours
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -48,8 +48,10 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
|
|||||||
cy="50%"
|
cy="50%"
|
||||||
innerRadius={60}
|
innerRadius={60}
|
||||||
outerRadius={100}
|
outerRadius={100}
|
||||||
paddingAngle={5}
|
paddingAngle={2}
|
||||||
dataKey="value"
|
dataKey="value"
|
||||||
|
stroke="#ffffff"
|
||||||
|
strokeWidth={2}
|
||||||
>
|
>
|
||||||
{data.map((entry, index) => (
|
{data.map((entry, index) => (
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
@@ -60,16 +62,22 @@ export function UserActivityChart({ activeUsers, inactiveUsers }: UserActivityCh
|
|||||||
backgroundColor: 'hsl(var(--background))',
|
backgroundColor: 'hsl(var(--background))',
|
||||||
border: '1px solid hsl(var(--border))',
|
border: '1px solid hsl(var(--border))',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
|
fontSize: '12px'
|
||||||
}}
|
}}
|
||||||
formatter={(value: number) => [
|
formatter={(value: number) => [
|
||||||
`${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`,
|
`${value} utilisateurs (${((value / total) * 100).toFixed(1)}%)`,
|
||||||
''
|
'',
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Legend
|
<Legend
|
||||||
|
wrapperStyle={{
|
||||||
|
paddingTop: "20px",
|
||||||
|
fontSize: "12px"
|
||||||
|
}}
|
||||||
formatter={(value, entry) => (
|
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)}%)
|
{value}: {entry.payload?.value} (
|
||||||
|
{((entry.payload?.value / total) * 100).toFixed(1)}%)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function RealTimeStats() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
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 className="h-64 bg-muted animate-pulse rounded-lg" />
|
<div className="h-64 bg-muted animate-pulse rounded-lg" />
|
||||||
</div>
|
</div>
|
||||||
@@ -20,7 +20,7 @@ export function RealTimeStats() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex items-center justify-center h-64">
|
<CardContent className="flex items-center justify-center h-64">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@@ -47,7 +47,7 @@ export function RealTimeStats() {
|
|||||||
|
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="flex items-center justify-center h-64">
|
<CardContent className="flex items-center justify-center h-64">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@@ -67,7 +67,7 @@ export function RealTimeStats() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="space-y-6">
|
||||||
<SimpleStatsChart
|
<SimpleStatsChart
|
||||||
title="Tokens consommés par jour"
|
title="Tokens consommés par jour"
|
||||||
data={stats.dailyTokens}
|
data={stats.dailyTokens}
|
||||||
|
|||||||
@@ -46,24 +46,20 @@ export function UsageAnalytics() {
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Console log pour débugger les données balances
|
console.log("=== CALCUL DES STATISTIQUES ===");
|
||||||
console.log("=== DONNÉES BALANCES RÉCUPÉRÉES ===");
|
console.log("Utilisateurs:", users.length);
|
||||||
console.log("Nombre total d'entrées balances:", balances.length);
|
console.log("Conversations:", conversations.length);
|
||||||
console.log("Toutes les entrées balances:", balances);
|
console.log("Transactions:", transactions.length);
|
||||||
|
console.log("Balances:", balances.length);
|
||||||
|
|
||||||
// NOUVEAU : Console log pour débugger les utilisateurs
|
// Analyser les doublons dans les balances
|
||||||
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
|
|
||||||
const userCounts = new Map<string, number>();
|
const userCounts = new Map<string, number>();
|
||||||
balances.forEach(balance => {
|
balances.forEach((balance) => {
|
||||||
const userId = balance.user;
|
const userId = balance.user;
|
||||||
userCounts.set(userId, (userCounts.get(userId) || 0) + 1);
|
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);
|
console.log("Utilisateurs avec plusieurs entrées:", duplicateUsers);
|
||||||
|
|
||||||
// Afficher quelques exemples d'entrées
|
// Afficher quelques exemples d'entrées
|
||||||
@@ -73,6 +69,29 @@ export function UsageAnalytics() {
|
|||||||
const totalBrut = balances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0);
|
const totalBrut = balances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0);
|
||||||
console.log("Total brut (avec doublons potentiels):", totalBrut);
|
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
|
// NOUVEAU : Identifier les utilisateurs fantômes
|
||||||
console.log("=== ANALYSE DES UTILISATEURS FANTÔMES ===");
|
console.log("=== ANALYSE DES UTILISATEURS FANTÔMES ===");
|
||||||
const userIds = new Set(users.map(user => user._id));
|
const userIds = new Set(users.map(user => user._id));
|
||||||
@@ -91,40 +110,57 @@ export function UsageAnalytics() {
|
|||||||
console.log("Crédits des utilisateurs fantômes:", phantomCredits);
|
console.log("Crédits des utilisateurs fantômes:", phantomCredits);
|
||||||
console.log("Crédits des vrais utilisateurs:", totalBrut - phantomCredits);
|
console.log("Crédits des vrais utilisateurs:", totalBrut - phantomCredits);
|
||||||
|
|
||||||
// Calculer les utilisateurs actifs (5 dernières minutes)
|
// Analyser les utilisateurs fantômes
|
||||||
const fiveMinutesAgo = new Date();
|
const phantomDetails = uniquePhantomUsers.map(userId => {
|
||||||
fiveMinutesAgo.setMinutes(fiveMinutesAgo.getMinutes() - 5);
|
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 activeUsers = users.filter((user) => {
|
||||||
const lastActivity = new Date(user.updatedAt || user.createdAt);
|
const lastActivity = new Date(user.updatedAt || user.createdAt);
|
||||||
return lastActivity >= fiveMinutesAgo;
|
return lastActivity >= thirtyDaysAgo;
|
||||||
}).length;
|
}).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>();
|
const creditsMap = new Map<string, number>();
|
||||||
|
|
||||||
// Grouper les balances par utilisateur
|
// Grouper les balances par utilisateur
|
||||||
const balancesByUser = new Map<string, LibreChatBalance[]>();
|
const balancesByUser = new Map<string, LibreChatBalance[]>();
|
||||||
balances.forEach((balance) => {
|
balances.forEach((balance) => {
|
||||||
const userId = balance.user;
|
const userId = balance.user;
|
||||||
if (!balancesByUser.has(userId)) {
|
// Ignorer les utilisateurs fantômes (qui n'existent plus)
|
||||||
balancesByUser.set(userId, []);
|
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) => {
|
balancesByUser.forEach((userBalances, userId) => {
|
||||||
if (userBalances.length > 0) {
|
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 sortedBalances = userBalances.sort((a, b) => {
|
||||||
const aDate = new Date((a.updatedAt as string) || (a.createdAt as string) || 0);
|
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);
|
const bDate = new Date((b.updatedAt as string) || (b.createdAt as string) || 0);
|
||||||
return bDate.getTime() - aDate.getTime();
|
return bDate.getTime() - aDate.getTime();
|
||||||
});
|
});
|
||||||
|
creditsMap.set(userId, sortedBalances[0].tokenCredits || 0);
|
||||||
|
|
||||||
// Prendre la plus récente
|
// OPTION B: Sommer toutes les balances (si c'est votre logique)
|
||||||
const latestBalance = sortedBalances[0];
|
// const totalCredits = userBalances.reduce((sum, balance) => sum + (balance.tokenCredits || 0), 0);
|
||||||
creditsMap.set(userId, latestBalance.tokenCredits || 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(
|
const totalCreditsUsed = Array.from(creditsMap.values()).reduce(
|
||||||
(sum, credits) => sum + credits,
|
(sum, credits) => sum + credits,
|
||||||
0
|
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
|
// Tous les utilisateurs triés par tokens puis conversations
|
||||||
const allUsers = Array.from(userStats.entries())
|
const allUsers = Array.from(userStats.entries())
|
||||||
.map(([userId, stats]) => ({
|
.map(([userId, stats]) => ({
|
||||||
@@ -242,7 +282,7 @@ export function UsageAnalytics() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">{stats.totalUsers}</div>
|
<div className="text-2xl font-bold">{stats.totalUsers}</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{stats.activeUsers} actifs cette semaine
|
{stats.activeUsers} actifs ce mois
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { createClient } from "@/lib/supabase/client";
|
||||||
import { Separator } from "@/components/ui/separator";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
@@ -21,135 +20,164 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Activity,
|
LogOut,
|
||||||
|
User,
|
||||||
|
Mail,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import type { User as SupabaseUser } from "@supabase/supabase-js";
|
||||||
|
|
||||||
interface NavigationItem {
|
const topLevelNavigation = [
|
||||||
name: string;
|
|
||||||
href: string;
|
|
||||||
icon: React.ElementType;
|
|
||||||
badge?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigation: NavigationItem[] = [
|
|
||||||
{
|
{
|
||||||
name: "Vue d'ensemble",
|
name: "Dashboard",
|
||||||
href: "/",
|
href: "/",
|
||||||
icon: LayoutDashboard,
|
icon: LayoutDashboard,
|
||||||
badge: null,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Analytics",
|
name: "Analytics",
|
||||||
href: "/analytics",
|
href: "/analytics",
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
badge: "Nouveau",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const dataNavigation: NavigationItem[] = [
|
const navigationGroups = [
|
||||||
{ name: "Utilisateurs", href: "/users", icon: Users, badge: null },
|
|
||||||
{
|
{
|
||||||
name: "Conversations",
|
name: "Données",
|
||||||
href: "/conversations",
|
items: [
|
||||||
icon: MessageSquare,
|
{
|
||||||
badge: null,
|
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",
|
name: "Système",
|
||||||
href: "/transactions",
|
items: [
|
||||||
icon: CreditCard,
|
{
|
||||||
badge: null,
|
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() {
|
export function Sidebar() {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
const pathname = usePathname();
|
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 = ({
|
useEffect(() => {
|
||||||
title,
|
const getUser = async () => {
|
||||||
items,
|
try {
|
||||||
showTitle = true,
|
const {
|
||||||
}: {
|
data: { user },
|
||||||
title: string;
|
} = await supabase.auth.getUser();
|
||||||
items: NavigationItem[];
|
setUser(user);
|
||||||
showTitle?: boolean;
|
} catch (error) {
|
||||||
}) => (
|
console.error(
|
||||||
<div className="space-y-2">
|
"Erreur lors de la récupération de l'utilisateur:",
|
||||||
{!collapsed && showTitle && (
|
error
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
})}
|
setUser(null);
|
||||||
</div>
|
} 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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"
|
collapsed ? "w-16" : "w-64"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* 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 && (
|
{!collapsed && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="w-8 h-8 rounded-lg flex items-center justify-center">
|
<div className="relative w-8 h-8">
|
||||||
<Image
|
<Image
|
||||||
src="/img/logo.png"
|
src="/img/logo.png"
|
||||||
alt="Cercle GPT Logo"
|
alt="Logo"
|
||||||
width={32}
|
fill
|
||||||
height={32}
|
className="object-contain"
|
||||||
className="rounded-lg"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-sm font-semibold">Cercle GPT</h1>
|
<h1 className="text-lg font-semibold text-gray-900">
|
||||||
<p className="text-xs text-muted-foreground">Admin Dashboard</p>
|
Cercle GPT
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-gray-500">Admin Dashboard</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="sm"
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
className="h-8 w-8"
|
className="p-1.5 hover:bg-gray-100"
|
||||||
>
|
>
|
||||||
{collapsed ? (
|
{collapsed ? (
|
||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
@@ -160,32 +188,112 @@ export function Sidebar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className="flex-1 p-3 space-y-6 overflow-y-auto">
|
<nav className="flex-1 p-4 space-y-6 overflow-y-auto">
|
||||||
<NavSection title="Dashboard" items={navigation} showTitle={false} />
|
{/* 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} />
|
{/* Items du groupe */}
|
||||||
</nav>
|
<div className="space-y-1">
|
||||||
|
{group.items.map((item) => {
|
||||||
{/* Footer */}
|
const isActive = pathname === item.href;
|
||||||
{!collapsed && (
|
return (
|
||||||
<div className="p-3 border-t border-border">
|
<Link
|
||||||
<div className="flex items-center space-x-3 p-2 rounded-lg bg-muted/50">
|
key={item.name}
|
||||||
<div className="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
|
href={item.href}
|
||||||
<Activity className="h-4 w-4 text-primary" />
|
className={cn(
|
||||||
</div>
|
"flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors",
|
||||||
<div className="flex-1 min-w-0">
|
isActive
|
||||||
<p className="text-xs font-medium">Système en ligne</p>
|
? "bg-gray-900 text-white"
|
||||||
<p className="text-xs text-muted-foreground">Tout fonctionne</p>
|
: "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>
|
||||||
</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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
46
components/ui/alert.tsx
Normal file
46
components/ui/alert.tsx
Normal 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 }
|
||||||
8
lib/supabase/client.ts
Normal file
8
lib/supabase/client.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createBrowserClient } from '@supabase/ssr'
|
||||||
|
|
||||||
|
export function createClient() {
|
||||||
|
return createBrowserClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||||
|
)
|
||||||
|
}
|
||||||
29
lib/supabase/server.ts
Normal file
29
lib/supabase/server.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createServerClient as createSupabaseServerClient } from '@supabase/ssr'
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
|
|
||||||
|
export async function createServerClient() {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
|
||||||
|
return createSupabaseServerClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
{
|
||||||
|
cookies: {
|
||||||
|
getAll() {
|
||||||
|
return cookieStore.getAll()
|
||||||
|
},
|
||||||
|
setAll(cookiesToSet: Array<{ name: string; value: string; options?: Record<string, unknown> }>) {
|
||||||
|
try {
|
||||||
|
cookiesToSet.forEach(({ name, value, options }) =>
|
||||||
|
cookieStore.set(name, value, options)
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// The `setAll` method was called from a Server Component.
|
||||||
|
// This can be ignored if you have middleware refreshing
|
||||||
|
// user sessions.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
65
middleware.ts
Normal file
65
middleware.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { createServerClient } from '@supabase/ssr'
|
||||||
|
import { NextResponse, type NextRequest } from 'next/server'
|
||||||
|
|
||||||
|
export async function middleware(request: NextRequest) {
|
||||||
|
let supabaseResponse = NextResponse.next({
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
|
||||||
|
const supabase = createServerClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
{
|
||||||
|
cookies: {
|
||||||
|
getAll() {
|
||||||
|
return request.cookies.getAll()
|
||||||
|
},
|
||||||
|
setAll(cookiesToSet: Array<{ name: string; value: string; options?: Record<string, unknown> }>) {
|
||||||
|
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
|
||||||
|
supabaseResponse = NextResponse.next({
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
cookiesToSet.forEach(({ name, value, options }) =>
|
||||||
|
supabaseResponse.cookies.set(name, value, options)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rafraîchir la session
|
||||||
|
const { data: { user } } = await supabase.auth.getUser()
|
||||||
|
|
||||||
|
// Si l'utilisateur est sur la page de login
|
||||||
|
if (request.nextUrl.pathname === '/login') {
|
||||||
|
if (user) {
|
||||||
|
// Utilisateur connecté, rediriger vers le dashboard
|
||||||
|
const redirectUrl = new URL('/', request.url)
|
||||||
|
return NextResponse.redirect(redirectUrl)
|
||||||
|
}
|
||||||
|
// Utilisateur non connecté, autoriser l'accès à la page de login
|
||||||
|
return supabaseResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour toutes les autres pages, vérifier l'authentification
|
||||||
|
if (!user) {
|
||||||
|
// Utilisateur non connecté, rediriger vers login
|
||||||
|
const redirectUrl = new URL('/login', request.url)
|
||||||
|
return NextResponse.redirect(redirectUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return supabaseResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all request paths except for the ones starting with:
|
||||||
|
* - _next/static (static files)
|
||||||
|
* - _next/image (image optimization files)
|
||||||
|
* - favicon.ico (favicon file)
|
||||||
|
* - public folder
|
||||||
|
*/
|
||||||
|
'/((?!_next/static|_next/image|favicon.ico|public|img).*)',
|
||||||
|
],
|
||||||
|
}
|
||||||
158
package-lock.json
generated
158
package-lock.json
generated
@@ -15,6 +15,8 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@supabase/ssr": "^0.7.0",
|
||||||
|
"@supabase/supabase-js": "^2.58.0",
|
||||||
"@types/mongodb": "^4.0.6",
|
"@types/mongodb": "^4.0.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -1761,6 +1763,115 @@
|
|||||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@supabase/auth-js": {
|
||||||
|
"version": "2.72.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.72.0.tgz",
|
||||||
|
"integrity": "sha512-4+bnUrtTDK1YD0/FCx2YtMiQH5FGu9Jlf4IQi5kcqRwRwqp2ey39V61nHNdH86jm3DIzz0aZKiWfTW8qXk1swQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/functions-js": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-SXBx6Jvp+MOBekeKFu+G11YLYPeVeGQl23eYyAG9+Ro0pQ1aIP0UZNIBxHKNHqxzR0L0n6gysNr2KT3841NATw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/node-fetch": {
|
||||||
|
"version": "2.6.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz",
|
||||||
|
"integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/node-fetch/node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/node-fetch/node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/node-fetch/node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/postgrest-js": {
|
||||||
|
"version": "1.21.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.21.4.tgz",
|
||||||
|
"integrity": "sha512-TxZCIjxk6/dP9abAi89VQbWWMBbybpGWyvmIzTd79OeravM13OjR/YEYeyUOPcM1C3QyvXkvPZhUfItvmhY1IQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/realtime-js": {
|
||||||
|
"version": "2.15.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.15.5.tgz",
|
||||||
|
"integrity": "sha512-/Rs5Vqu9jejRD8ZeuaWXebdkH+J7V6VySbCZ/zQM93Ta5y3mAmocjioa/nzlB6qvFmyylUgKVS1KpE212t30OA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.13",
|
||||||
|
"@types/phoenix": "^1.6.6",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"ws": "^8.18.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/ssr": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-G65t5EhLSJ5c8hTCcXifSL9Q/ZRXvqgXeNo+d3P56f4U1IxwTqjB64UfmfixvmMcjuxnq2yGqEWVJqUcO+AzAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@supabase/supabase-js": "^2.43.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/storage-js": {
|
||||||
|
"version": "2.12.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.12.2.tgz",
|
||||||
|
"integrity": "sha512-SiySHxi3q7gia7NBYpsYRu8gyI0NhFwSORMxbZIxJ/zAVkN6QpwDRan158CJ+UdzD4WB/rQMAGRqIJQP+7ccAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/node-fetch": "^2.6.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@supabase/supabase-js": {
|
||||||
|
"version": "2.58.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.58.0.tgz",
|
||||||
|
"integrity": "sha512-Tm1RmQpoAKdQr4/8wiayGti/no+If7RtveVZjHR8zbO7hhQjmPW2Ok5ZBPf1MGkt5c+9R85AVMsTfSaqAP1sUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/auth-js": "2.72.0",
|
||||||
|
"@supabase/functions-js": "2.5.0",
|
||||||
|
"@supabase/node-fetch": "2.6.15",
|
||||||
|
"@supabase/postgrest-js": "1.21.4",
|
||||||
|
"@supabase/realtime-js": "2.15.5",
|
||||||
|
"@supabase/storage-js": "2.12.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
@@ -2154,12 +2265,17 @@
|
|||||||
"version": "20.19.19",
|
"version": "20.19.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz",
|
||||||
"integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==",
|
"integrity": "sha512-pb1Uqj5WJP7wrcbLU7Ru4QtA0+3kAXrkutGiD26wUKzSMgNNaPARTUDQmElUXp64kh3cWdou3Q0C7qwwxqSFmg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/phoenix": {
|
||||||
|
"version": "1.6.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||||
|
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.0",
|
"version": "19.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz",
|
||||||
@@ -2203,6 +2319,15 @@
|
|||||||
"@types/webidl-conversions": "*"
|
"@types/webidl-conversions": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.45.0",
|
"version": "8.45.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz",
|
||||||
@@ -3261,6 +3386,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -7280,7 +7414,6 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unrs-resolver": {
|
"node_modules/unrs-resolver": {
|
||||||
@@ -7539,6 +7672,27 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||||
|
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@supabase/ssr": "^0.7.0",
|
||||||
|
"@supabase/supabase-js": "^2.58.0",
|
||||||
"@types/mongodb": "^4.0.6",
|
"@types/mongodb": "^4.0.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user