user nouveau onglet

This commit is contained in:
Biqoz
2025-11-16 01:34:01 +01:00
parent 0d95eca1ee
commit ad575641a1
16 changed files with 1033 additions and 33 deletions

View File

@@ -7,7 +7,8 @@
"Bash(npm run build:*)", "Bash(npm run build:*)",
"Bash(git add:*)", "Bash(git add:*)",
"Bash(git commit:*)", "Bash(git commit:*)",
"Bash(git push)" "Bash(git push)",
"Bash(npm install:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -0,0 +1,29 @@
import { NextResponse } from "next/server";
import { getDatabase } from "@/lib/db/mongodb";
export async function GET() {
try {
const db = await getDatabase();
// Récupérer 5 users pour debug
const users = await db.collection("users")
.find({ referent: { $exists: true } })
.limit(5)
.toArray();
return NextResponse.json({
success: true,
count: users.length,
users: users.map(u => ({
_id: u._id,
name: u.name,
email: u.email,
referent: u.referent,
cours: u.cours,
})),
});
} catch (error) {
console.error("Debug error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
}

View File

@@ -6,7 +6,7 @@ export default function ConversationsPage() {
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Conversations</h1> <h1 className="text-3xl font-bold tracking-tight">Conversations</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Gestion des conversations Cercle GPTT Gestion des conversations Cercle GPT
</p> </p>
</div> </div>

View File

@@ -0,0 +1,194 @@
"use client";
import { useState, useEffect, useMemo } from "react";
import { useParams, useRouter } from "next/navigation";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ArrowLeft, Mail } from "lucide-react";
import { formatDate } from "@/lib/utils";
import { LibreChatUser, LibreChatBalance } from "@/lib/types";
import { useCollection } from "@/hooks/useCollection";
// Couleurs prédéfinies pour les référents
const REFERENT_COLORS: Record<string, string> = {
"Emmanuel WATHELE": "#3B82F6",
"IHECS": "#10B981",
};
export default function CoursDetailPage() {
const params = useParams();
const router = useRouter();
const referent = decodeURIComponent(params.referent as string);
const cours = decodeURIComponent(params.cours as string);
const [students, setStudents] = useState<LibreChatUser[]>([]);
const [loading, setLoading] = useState(true);
// Charger tous les balances
const { data: balances = [] } = useCollection<LibreChatBalance>("balances", {
limit: 1000,
});
// Créer une map des crédits par utilisateur
const creditsMap = useMemo(() => {
const map = new Map<string, number>();
balances.forEach((balance) => {
map.set(balance.user, balance.tokenCredits || 0);
});
return map;
}, [balances]);
useEffect(() => {
const fetchStudents = async () => {
try {
const response = await fetch(
"/api/collections/users?page=1&limit=1000&filter={}"
);
const result = await response.json();
if (result.data) {
// Filtrer les étudiants pour ce référent et ce cours
const filtered = result.data.filter(
(user: any) =>
user.referent === referent && user.cours === cours
);
setStudents(filtered);
}
} catch (error) {
console.error("Erreur lors du chargement des étudiants:", error);
} finally {
setLoading(false);
}
};
fetchStudents();
}, [referent, cours]);
const couleur = REFERENT_COLORS[referent] || "#6B7280";
if (loading) {
return (
<div className="space-y-6">
<Button
variant="ghost"
onClick={() => router.push("/referents")}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Retour aux référents
</Button>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-16 bg-muted animate-pulse rounded" />
))}
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-6">
<Button
variant="ghost"
onClick={() => router.push("/referents")}
className="mb-4"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Retour aux référents
</Button>
<div className="flex items-center gap-3">
<div
className="w-6 h-6 rounded-full flex-shrink-0"
style={{ backgroundColor: couleur }}
/>
<div>
<h1 className="text-3xl font-bold tracking-tight">{referent}</h1>
<p className="text-muted-foreground">{cours}</p>
</div>
</div>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>
Étudiants ({students.length})
</CardTitle>
</div>
</CardHeader>
<CardContent>
{students.length === 0 ? (
<div className="text-center py-12">
<p className="text-sm text-gray-500">
Aucun étudiant dans ce cours
</p>
</div>
) : (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Nom</TableHead>
<TableHead>Email</TableHead>
<TableHead>Rôle</TableHead>
<TableHead>Crédits</TableHead>
<TableHead>Créé le</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{students.map((student) => (
<TableRow key={student._id}>
<TableCell>
<div className="flex items-center gap-2">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: couleur }}
/>
<span className="font-medium">
{student.prenom} {student.nom}
</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Mail className="h-4 w-4 text-gray-400" />
<span className="text-sm">{student.email}</span>
</div>
</TableCell>
<TableCell>
<Badge variant={student.role === "ADMIN" ? "default" : "secondary"}>
{student.role}
</Badge>
</TableCell>
<TableCell>
<span className="text-sm">
{(creditsMap.get(student._id) || 0).toLocaleString()} tokens
</span>
</TableCell>
<TableCell className="text-sm text-gray-500">
{formatDate(student.createdAt)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
);
}

183
app/referents/page.tsx Normal file
View File

@@ -0,0 +1,183 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { GraduationCap, Users, ChevronRight } from "lucide-react";
import Link from "next/link";
// Couleurs prédéfinies pour les référents
const REFERENT_COLORS: Record<string, string> = {
"Emmanuel WATHELE": "#3B82F6", // Bleu
"IHECS": "#10B981", // Vert
};
interface ReferentData {
nom: string;
couleur: string;
cours: string[];
nombreEtudiants: number;
}
export default function ReferentsPage() {
const [referents, setReferents] = useState<ReferentData[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchReferents = async () => {
try {
// Récupérer tous les users pour compter par référent
const response = await fetch(
"/api/collections/users?page=1&limit=1000&filter={}"
);
const result = await response.json();
if (result.data) {
// Grouper par référent
const referentsMap = new Map<string, ReferentData>();
result.data.forEach((user: any) => {
if (user.referent) {
if (!referentsMap.has(user.referent)) {
referentsMap.set(user.referent, {
nom: user.referent,
couleur: REFERENT_COLORS[user.referent] || "#6B7280", // Gris par défaut
cours: [],
nombreEtudiants: 0,
});
}
const ref = referentsMap.get(user.referent)!;
ref.nombreEtudiants++;
// Ajouter le cours s'il n'existe pas déjà
if (user.cours && !ref.cours.includes(user.cours)) {
ref.cours.push(user.cours);
}
}
});
setReferents(Array.from(referentsMap.values()));
}
} catch (error) {
console.error("Erreur lors du chargement des référents:", error);
} finally {
setLoading(false);
}
};
fetchReferents();
}, []);
if (loading) {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Référents</h1>
<p className="text-muted-foreground">
Gestion des référents et leurs cours
</p>
</div>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-24 bg-muted animate-pulse rounded" />
))}
</div>
</CardContent>
</Card>
</div>
);
}
if (referents.length === 0) {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Référents</h1>
<p className="text-muted-foreground">
Gestion des référents et leurs cours
</p>
</div>
<Card>
<CardContent className="p-6">
<div className="text-center py-12">
<GraduationCap className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-semibold text-gray-900">
Aucun référent
</h3>
<p className="mt-1 text-sm text-gray-500">
Importez des utilisateurs avec référents pour commencer.
</p>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Référents</h1>
<p className="text-muted-foreground">
Gestion des référents et leurs cours
</p>
</div>
<div className="grid gap-4">
{referents.map((referent) => (
<Card key={referent.nom} className="hover:shadow-md transition-shadow">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{/* Point coloré */}
<div
className="w-4 h-4 rounded-full flex-shrink-0"
style={{ backgroundColor: referent.couleur }}
/>
<div>
<CardTitle className="text-xl">{referent.nom}</CardTitle>
<div className="flex items-center gap-2 mt-1">
<Users className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-500">
{referent.nombreEtudiants} étudiant
{referent.nombreEtudiants > 1 ? "s" : ""}
</span>
</div>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3">
<h4 className="text-sm font-semibold text-gray-700">Cours :</h4>
<div className="space-y-2">
{referent.cours.map((cours) => (
<Link
key={cours}
href={`/referents/${encodeURIComponent(referent.nom)}/${encodeURIComponent(cours)}`}
>
<div className="flex items-center justify-between p-3 bg-gray-50 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer">
<div className="flex items-center gap-2">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: referent.couleur }}
/>
<span className="text-sm font-medium">{cours}</span>
</div>
<ChevronRight className="h-4 w-4 text-gray-400" />
</div>
</Link>
))}
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}

View File

@@ -6,7 +6,7 @@ export default function UsersPage() {
<div> <div>
<h1 className="text-3xl font-bold tracking-tight">Utilisateurs</h1> <h1 className="text-3xl font-bold tracking-tight">Utilisateurs</h1>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Gestion des utilisateurs Cercle GPTT Gestion des utilisateurs Cercle GPT
</p> </p>
</div> </div>

View File

@@ -18,6 +18,12 @@ import { ChevronLeft, ChevronRight, Search } from "lucide-react";
import { formatDate } from "@/lib/utils"; import { formatDate } from "@/lib/utils";
import { LibreChatUser, LibreChatBalance } from "@/lib/types"; import { LibreChatUser, LibreChatBalance } from "@/lib/types";
// Couleurs prédéfinies pour les référents
const REFERENT_COLORS: Record<string, string> = {
"Emmanuel WATHELE": "#3B82F6", // Bleu
"IHECS": "#10B981", // Vert
};
export function UsersTable() { export function UsersTable() {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [searchInput, setSearchInput] = useState(""); // Ce que l'utilisateur tape const [searchInput, setSearchInput] = useState(""); // Ce que l'utilisateur tape
@@ -121,22 +127,20 @@ export function UsersTable() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>ID</TableHead> <TableHead className="w-20">ID</TableHead>
<TableHead>Nom</TableHead> <TableHead>Nom</TableHead>
<TableHead>Email</TableHead> <TableHead>Email</TableHead>
<TableHead>Rôle</TableHead> <TableHead className="w-32">Référent</TableHead>
<TableHead>Crédits</TableHead> <TableHead className="w-20">Rôle</TableHead>
<TableHead>Statut</TableHead> <TableHead className="w-32">Crédits</TableHead>
<TableHead>Créé le</TableHead> <TableHead className="w-28">Créé le</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{users.length > 0 ? ( {users.length > 0 ? (
users.map((user) => { users.map((user) => {
const userCredits = creditsMap.get(user._id) || 0; const userCredits = creditsMap.get(user._id) || 0;
const isActive = const referentColor = user.referent ? (REFERENT_COLORS[user.referent] || "#6B7280") : null;
new Date(user.updatedAt || user.createdAt) >
new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
return ( return (
<TableRow key={user._id}> <TableRow key={user._id}>
@@ -146,11 +150,29 @@ export function UsersTable() {
</span> </span>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2">
{referentColor && (
<div
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: referentColor }}
title={user.referent}
/>
)}
<span className="font-medium">{user.name}</span> <span className="font-medium">{user.name}</span>
</div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-sm">{user.email}</span> <span className="text-sm">{user.email}</span>
</TableCell> </TableCell>
<TableCell>
{user.referent ? (
<span className="text-sm truncate block max-w-[120px]" title={user.referent}>
{user.referent}
</span>
) : (
<span className="text-sm text-gray-400">-</span>
)}
</TableCell>
<TableCell> <TableCell>
<Badge <Badge
variant={ variant={
@@ -165,13 +187,6 @@ export function UsersTable() {
{userCredits.toLocaleString()} crédits {userCredits.toLocaleString()} crédits
</span> </span>
</TableCell> </TableCell>
<TableCell>
<span className="text-sm text-muted-foreground">
<Badge variant={isActive ? "default" : "secondary"}>
{isActive ? "Actif" : "Inactif"}
</Badge>
</span>
</TableCell>
<TableCell> <TableCell>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{formatDate(user.createdAt)} {formatDate(user.createdAt)}

View File

@@ -118,7 +118,8 @@ export default function CreateUser() {
} else { } else {
setResult({ setResult({
success: false, success: false,
message: data.error || "Erreur lors de la création de l&apos;utilisateur", message:
data.error || "Erreur lors de la création de l&apos;utilisateur",
}); });
} }
} catch (error) { } catch (error) {
@@ -185,7 +186,9 @@ export default function CreateUser() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="confirmPassword">Confirmer le mot de passe *</Label> <Label htmlFor="confirmPassword">
Confirmer le mot de passe *
</Label>
<Input <Input
id="confirmPassword" id="confirmPassword"
type="password" type="password"
@@ -204,7 +207,9 @@ export default function CreateUser() {
<Label htmlFor="role">Rôle</Label> <Label htmlFor="role">Rôle</Label>
<Select <Select
value={formData.role} value={formData.role}
onValueChange={(value: string) => handleInputChange("role", value)} onValueChange={(value: string) =>
handleInputChange("role", value)
}
disabled={isLoading} disabled={isLoading}
> >
<SelectTrigger> <SelectTrigger>
@@ -229,13 +234,10 @@ export default function CreateUser() {
{result.success && result.user && ( {result.success && result.user && (
<div className="mt-2 text-sm"> <div className="mt-2 text-sm">
<strong>Détails:</strong> <strong>Détails:</strong>
<br /> <br /> ID: {result.user.id}
ID: {result.user.id} <br /> Email: {result.user.email}
<br /> <br /> Rôle: {result.user.role}
Email: {result.user.email} <br /> Crédits initiaux: 3,000,000 tokens
<br />
Rôle: {result.user.role}
<br /> Crédits initiaux: 5,000,000 tokens
</div> </div>
)} )}
</AlertDescription> </AlertDescription>
@@ -262,7 +264,9 @@ export default function CreateUser() {
<div className="mt-6 p-4 bg-muted rounded-lg"> <div className="mt-6 p-4 bg-muted rounded-lg">
<h4 className="font-medium mb-2">Informations importantes :</h4> <h4 className="font-medium mb-2">Informations importantes :</h4>
<ul className="text-sm text-muted-foreground space-y-1"> <ul className="text-sm text-muted-foreground space-y-1">
<li> L&apos;utilisateur recevra automatiquement 5,000,000 tokens</li> <li>
L&apos;utilisateur recevra automatiquement 5,000,000 tokens
</li>
<li> Le mot de passe sera hashé de manière sécurisée</li> <li> Le mot de passe sera hashé de manière sécurisée</li>
<li> L&apos;email doit être unique dans le système</li> <li> L&apos;email doit être unique dans le système</li>
<li> L&apos;utilisateur pourra se connecter immédiatement</li> <li> L&apos;utilisateur pourra se connecter immédiatement</li>

View File

@@ -22,6 +22,7 @@ import {
LogOut, LogOut,
User, User,
Mail, Mail,
GraduationCap,
} from "lucide-react"; } from "lucide-react";
import type { User as SupabaseUser } from "@supabase/supabase-js"; import type { User as SupabaseUser } from "@supabase/supabase-js";
@@ -37,6 +38,11 @@ const navigationGroups = [
{ {
name: "Données", name: "Données",
items: [ items: [
{
name: "Référents",
href: "/referents",
icon: GraduationCap,
},
{ {
name: "Utilisateurs", name: "Utilisateurs",
href: "/users", href: "/users",

119
package-lock.json generated
View File

@@ -25,13 +25,15 @@
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^17.2.3",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"mongodb": "^6.20.0", "mongodb": "^6.20.0",
"next": "15.5.4", "next": "15.5.4",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"recharts": "^3.2.1", "recharts": "^3.2.1",
"tailwind-merge": "^3.3.1" "tailwind-merge": "^3.3.1",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
@@ -3054,6 +3056,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
} }
}, },
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -3458,6 +3469,19 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3512,6 +3536,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3548,6 +3581,18 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"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",
@@ -3848,6 +3893,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -4659,6 +4716,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -7062,6 +7128,18 @@
"memory-pager": "^1.0.2" "memory-pager": "^1.0.2"
} }
}, },
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/stable-hash": { "node_modules/stable-hash": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@@ -7815,6 +7893,24 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -7846,6 +7942,27 @@
} }
} }
}, },
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"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",

View File

@@ -26,13 +26,15 @@
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^17.2.3",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"mongodb": "^6.20.0", "mongodb": "^6.20.0",
"next": "15.5.4", "next": "15.5.4",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"recharts": "^3.2.1", "recharts": "^3.2.1",
"tailwind-merge": "^3.3.1" "tailwind-merge": "^3.3.1",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",

Binary file not shown.

Binary file not shown.

114
scripts/README.md Normal file
View File

@@ -0,0 +1,114 @@
# 📥 Import Automatique d'Utilisateurs depuis Excel
Ce script permet d'importer automatiquement des utilisateurs dans MongoDB depuis un fichier Excel.
## 🚀 Utilisation
### 1. Préparer le fichier Excel
Placer votre fichier `.xlsx` dans le dossier `public/list_users/`
**Format attendu :**
```
Nom Etudiant | Prénom Etudiant | Matricule HE | EMail Etudiant 2
```
**Exemple :**
```
Albarran Perez | Tamara | 1240935 | tamara.albarran@student.ihecs.be
Amjahed | Kawtar | 1241004 | kawtar.amjahed@student.ihecs.be
```
**Nom du fichier :** Le nom doit être le nom du référent avec underscores.
-`Emmanuel_WATHELE.xlsx`
-`Sophie_MARTIN.xlsx`
-`liste.xlsx`
### 2. Configurer le script
Ouvrir `scripts/import-users.js` et modifier ces lignes :
```javascript
const XLSX_FILENAME = "Emmanuel_WATHELE.xlsx"; // ⚠️ Nom du fichier
const DEFAULT_PASSWORD = "IHECS-2025"; // Mot de passe par défaut
const COURS = "Ouverture à l'esprit critique"; // Nom du cours
```
### 3. Lancer l'import
```bash
node scripts/import-users.js
```
## 📊 Ce que fait le script
1. ✅ Lit le fichier Excel
2. ✅ Extrait le référent depuis le nom du fichier (`Emmanuel_WATHELE``Emmanuel WATHELE`)
3. ✅ Hash le mot de passe une seule fois (optimisation)
4. ✅ Pour chaque étudiant :
- Vérifie que l'email est valide
- Vérifie que l'email n'existe pas déjà
- Crée l'utilisateur avec :
- `nom`, `prenom`, `email`
- `referent` (extrait du nom du fichier)
- `cours` (défini dans le script)
- `password` hashé
- `role: "USER"`
- Crée une balance initiale (3 000 000 tokens)
5. ✅ Affiche un résumé détaillé
## 📝 Exemple de résultat
```
🚀 Démarrage de l'import...
📋 Référent: Emmanuel WATHELE
📚 Cours: Ouverture à l'esprit critique
🔑 Password par défaut: IHECS-2025
📁 Lecture du fichier: C:\...\public\list_users\Emmanuel_WATHELE.xlsx
✅ 50 lignes détectées dans le fichier
🔐 Hash du mot de passe...
🔌 Connexion à MongoDB...
✅ Connecté à MongoDB
📥 Import en cours...
[1/50] ✅ Créé: Tamara Albarran Perez (tamara.albarran@student.ihecs.be)
[2/50] ✅ Créé: Kawtar Amjahed (kawtar.amjahed@student.ihecs.be)
[3/50] ⏭️ Ignoré (existe déjà): nezaket.arslan@student.ihecs.be
...
============================================================
📊 RÉSUMÉ DE L'IMPORT
============================================================
✅ Utilisateurs créés: 48
⏭️ Utilisateurs ignorés (déjà existants): 2
❌ Erreurs: 0
============================================================
✨ Import terminé !
```
## ⚠️ Important
- Le script **ignore** les utilisateurs déjà existants (même email)
- Le script **ignore** la colonne "Matricule HE" (non sauvegardée)
- Tous les utilisateurs auront le **même mot de passe** (ils pourront le changer après)
- Chaque utilisateur reçoit **3 000 000 tokens** par défaut
## 🔧 Dépannage
**Erreur : "Cannot find module 'xlsx'"**
```bash
npm install xlsx
```
**Erreur : "connect ECONNREFUSED"**
- Vérifier que MongoDB est démarré
- Vérifier la connexion dans `.env.local`
**Erreur : "File not found"**
- Vérifier que le fichier est bien dans `public/list_users/`
- Vérifier le nom du fichier dans le script

167
scripts/import-ihecs.js Normal file
View File

@@ -0,0 +1,167 @@
import XLSX from 'xlsx';
import { MongoClient, ObjectId } from 'mongodb';
import bcrypt from 'bcryptjs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Charger les variables d'environnement depuis .env.local
dotenv.config({ path: path.join(__dirname, '..', '.env.local') });
// Configuration
const MONGODB_URI = process.env.MONGODB_URI;
const XLSX_FILENAME = "IHECS.xlsx"; // ⚠️ Fichier pour IHECS / M2RP
const DEFAULT_PASSWORD = "IHECS-2025";
const REFERENT = "IHECS";
const COURS = "M2RP";
async function importUsers() {
console.log("🚀 Démarrage de l'import IHECS / M2RP...\n");
try {
// Vérifier que MONGODB_URI est défini
if (!MONGODB_URI) {
throw new Error("MONGODB_URI non défini dans .env.local");
}
console.log(`📋 Référent: ${REFERENT}`);
console.log(`📚 Cours: ${COURS}`);
console.log(`🔑 Password par défaut: ${DEFAULT_PASSWORD}\n`);
// Lire le fichier Excel
const filePath = path.join(__dirname, '..', 'public', 'list_users', XLSX_FILENAME);
console.log(`📁 Lecture du fichier: ${filePath}`);
const workbook = XLSX.readFile(filePath);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(sheet);
console.log(`${data.length} lignes détectées dans le fichier\n`);
// Hash le password une seule fois (optimisation)
console.log("🔐 Hash du mot de passe...");
const hashedPassword = await bcrypt.hash(DEFAULT_PASSWORD, 12);
// Préparer les users
const users = data.map(row => ({
nom: row["Nom de famille"],
prenom: row["Prénom"],
name: `${row["Prénom"]} ${row["Nom de famille"]}`,
email: row["Adresse de courriel"],
username: row["Nom d'utilisateur"] || row["Adresse de courriel"]?.split("@")[0],
password: hashedPassword,
emailVerified: false,
avatar: null,
provider: "local",
role: "USER",
// Nouveaux champs pour le cours et le référent
referent: REFERENT,
cours: COURS,
plugins: [],
twoFactorEnabled: false,
termsAccepted: true,
personalization: {
memories: false,
_id: new ObjectId(),
},
backupCodes: [],
refreshToken: [],
createdAt: new Date(),
updatedAt: new Date(),
__v: 0,
}));
// Connexion à MongoDB
console.log("🔌 Connexion à MongoDB...");
const client = new MongoClient(MONGODB_URI);
await client.connect();
const db = client.db('librechat');
console.log("✅ Connecté à MongoDB\n");
// Import des users
const results = { created: [], errors: [], skipped: [] };
console.log("📥 Import en cours...\n");
for (let i = 0; i < users.length; i++) {
const user = users[i];
const progress = `[${i + 1}/${users.length}]`;
try {
// Validation email
if (!user.email || !user.email.includes('@')) {
console.log(`${progress} ⚠️ Email invalide: ${user.name}`);
results.errors.push({ name: user.name, error: "Email invalide" });
continue;
}
// Vérifier si email existe déjà
const existing = await db.collection("users").findOne({ email: user.email });
if (existing) {
console.log(`${progress} ⏭️ Ignoré (existe déjà): ${user.email}`);
results.skipped.push(user.email);
continue;
}
// Insérer le user
const result = await db.collection("users").insertOne(user);
// Créer la balance initiale
await db.collection("balances").insertOne({
user: result.insertedId,
tokenCredits: 3000000,
autoRefillEnabled: false,
lastRefill: new Date(),
refillAmount: 0,
refillIntervalUnit: "month",
refillIntervalValue: 1,
__v: 0,
});
console.log(`${progress} ✅ Créé: ${user.prenom} ${user.nom} (${user.email})`);
results.created.push(user.email);
} catch (error) {
console.error(`${progress} ❌ Erreur: ${user.email} - ${error.message}`);
results.errors.push({ email: user.email, error: error.message });
}
}
// Fermer la connexion
await client.close();
// Résumé final
console.log("\n" + "=".repeat(60));
console.log("📊 RÉSUMÉ DE L'IMPORT - IHECS / M2RP");
console.log("=".repeat(60));
console.log(`✅ Utilisateurs créés: ${results.created.length}`);
console.log(`⏭️ Utilisateurs ignorés (déjà existants): ${results.skipped.length}`);
console.log(`❌ Erreurs: ${results.errors.length}`);
console.log("=".repeat(60));
if (results.errors.length > 0) {
console.log("\n⚠ DÉTAIL DES ERREURS:");
results.errors.forEach(e => {
console.log(` - ${e.email || e.name}: ${e.error}`);
});
}
console.log("\n✨ Import terminé !\n");
} catch (error) {
console.error("\n❌ ERREUR FATALE:", error.message);
console.error(error);
process.exit(1);
}
process.exit(0);
}
// Lancer l'import
importUsers();

168
scripts/import-users.js Normal file
View File

@@ -0,0 +1,168 @@
import XLSX from 'xlsx';
import { MongoClient, ObjectId } from 'mongodb';
import bcrypt from 'bcryptjs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Charger les variables d'environnement depuis .env.local
dotenv.config({ path: path.join(__dirname, '..', '.env.local') });
// Configuration
const MONGODB_URI = process.env.MONGODB_URI;
const XLSX_FILENAME = "Emmanuel_WATHELE.xlsx"; // ⚠️ À modifier selon le fichier
const DEFAULT_PASSWORD = "IHECS-2025";
const COURS = "Ouverture à l'esprit critique";
async function importUsers() {
console.log("🚀 Démarrage de l'import...\n");
try {
// Vérifier que MONGODB_URI est défini
if (!MONGODB_URI) {
throw new Error("MONGODB_URI non défini dans .env.local");
}
// 1. Extraire le référent depuis le nom du fichier
const referent = XLSX_FILENAME.replace('.xlsx', '').replace(/_/g, ' ');
console.log(`📋 Référent: ${referent}`);
console.log(`📚 Cours: ${COURS}`);
console.log(`🔑 Password par défaut: ${DEFAULT_PASSWORD}\n`);
// 2. Lire le fichier Excel
const filePath = path.join(__dirname, '..', 'public', 'list_users', XLSX_FILENAME);
console.log(`📁 Lecture du fichier: ${filePath}`);
const workbook = XLSX.readFile(filePath);
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(sheet);
console.log(`${data.length} lignes détectées dans le fichier\n`);
// 3. Hash le password une seule fois (optimisation)
console.log("🔐 Hash du mot de passe...");
const hashedPassword = await bcrypt.hash(DEFAULT_PASSWORD, 12);
// 4. Préparer les users
const users = data.map(row => ({
nom: row["Nom Etudiant"],
prenom: row["Prénom Etudiant"],
name: `${row["Prénom Etudiant"]} ${row["Nom Etudiant"]}`,
email: row["EMail Etudiant 2"],
username: row["EMail Etudiant 2"]?.split("@")[0],
password: hashedPassword,
emailVerified: false,
avatar: null,
provider: "local",
role: "USER",
// Nouveaux champs pour le cours et le référent
referent: referent,
cours: COURS,
plugins: [],
twoFactorEnabled: false,
termsAccepted: true,
personalization: {
memories: false,
_id: new ObjectId(),
},
backupCodes: [],
refreshToken: [],
createdAt: new Date(),
updatedAt: new Date(),
__v: 0,
}));
// 5. Connexion à MongoDB
console.log("🔌 Connexion à MongoDB...");
const client = new MongoClient(MONGODB_URI);
await client.connect();
const db = client.db('librechat');
console.log("✅ Connecté à MongoDB\n");
// 6. Import des users
const results = { created: [], errors: [], skipped: [] };
console.log("📥 Import en cours...\n");
for (let i = 0; i < users.length; i++) {
const user = users[i];
const progress = `[${i + 1}/${users.length}]`;
try {
// Validation email
if (!user.email || !user.email.includes('@')) {
console.log(`${progress} ⚠️ Email invalide: ${user.name}`);
results.errors.push({ name: user.name, error: "Email invalide" });
continue;
}
// Vérifier si email existe déjà
const existing = await db.collection("users").findOne({ email: user.email });
if (existing) {
console.log(`${progress} ⏭️ Ignoré (existe déjà): ${user.email}`);
results.skipped.push(user.email);
continue;
}
// Insérer le user
const result = await db.collection("users").insertOne(user);
// Créer la balance initiale
await db.collection("balances").insertOne({
user: result.insertedId,
tokenCredits: 3000000,
autoRefillEnabled: false,
lastRefill: new Date(),
refillAmount: 0,
refillIntervalUnit: "month",
refillIntervalValue: 1,
__v: 0,
});
console.log(`${progress} ✅ Créé: ${user.prenom} ${user.nom} (${user.email})`);
results.created.push(user.email);
} catch (error) {
console.error(`${progress} ❌ Erreur: ${user.email} - ${error.message}`);
results.errors.push({ email: user.email, error: error.message });
}
}
// 7. Fermer la connexion
await client.close();
// 8. Résumé final
console.log("\n" + "=".repeat(60));
console.log("📊 RÉSUMÉ DE L'IMPORT");
console.log("=".repeat(60));
console.log(`✅ Utilisateurs créés: ${results.created.length}`);
console.log(`⏭️ Utilisateurs ignorés (déjà existants): ${results.skipped.length}`);
console.log(`❌ Erreurs: ${results.errors.length}`);
console.log("=".repeat(60));
if (results.errors.length > 0) {
console.log("\n⚠ DÉTAIL DES ERREURS:");
results.errors.forEach(e => {
console.log(` - ${e.email || e.name}: ${e.error}`);
});
}
console.log("\n✨ Import terminé !\n");
} catch (error) {
console.error("\n❌ ERREUR FATALE:", error.message);
console.error(error);
process.exit(1);
}
process.exit(0);
}
// Lancer l'import
importUsers();