user nouveau onglet
This commit is contained in:
@@ -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": []
|
||||||
|
|||||||
29
app/api/debug-users/route.ts
Normal file
29
app/api/debug-users/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
194
app/referents/[referent]/[cours]/page.tsx
Normal file
194
app/referents/[referent]/[cours]/page.tsx
Normal 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
183
app/referents/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
<span className="font-medium">{user.name}</span>
|
<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>
|
||||||
|
</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)}
|
||||||
|
|||||||
@@ -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'utilisateur",
|
message:
|
||||||
|
data.error || "Erreur lors de la création de l'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'utilisateur recevra automatiquement 5,000,000 tokens</li>
|
<li>
|
||||||
|
• L'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'email doit être unique dans le système</li>
|
<li>• L'email doit être unique dans le système</li>
|
||||||
<li>• L'utilisateur pourra se connecter immédiatement</li>
|
<li>• L'utilisateur pourra se connecter immédiatement</li>
|
||||||
|
|||||||
@@ -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
119
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
BIN
public/list_users/Emmanuel_WATHELE.xlsx
Normal file
BIN
public/list_users/Emmanuel_WATHELE.xlsx
Normal file
Binary file not shown.
BIN
public/list_users/IHECS.xlsx
Normal file
BIN
public/list_users/IHECS.xlsx
Normal file
Binary file not shown.
114
scripts/README.md
Normal file
114
scripts/README.md
Normal 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
167
scripts/import-ihecs.js
Normal 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
168
scripts/import-users.js
Normal 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();
|
||||||
Reference in New Issue
Block a user