diff --git a/frontend/app/admin/layout.tsx b/frontend/app/admin/layout.tsx
new file mode 100644
index 0000000..a35b054
--- /dev/null
+++ b/frontend/app/admin/layout.tsx
@@ -0,0 +1,10 @@
+import { AdminLayout } from "@/components/admin-layout";
+import { AuthLoading } from "@/components/auth-loading";
+
+export default function AdminRootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx
new file mode 100644
index 0000000..13d4335
--- /dev/null
+++ b/frontend/app/admin/page.tsx
@@ -0,0 +1,199 @@
+"use client";
+
+import { useState } from "react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { Users, Shield, Tags, Settings, BarChart4 } from "lucide-react";
+import Link from "next/link";
+
+export default function AdminDashboardPage() {
+ const [activeTab, setActiveTab] = useState("overview");
+
+ // Mock data for the admin dashboard
+ const stats = [
+ {
+ title: "Utilisateurs",
+ value: "24",
+ description: "Utilisateurs actifs",
+ icon: Users,
+ href: "/admin/users",
+ },
+ {
+ title: "Tags globaux",
+ value: "18",
+ description: "Tags disponibles",
+ icon: Tags,
+ href: "/admin/tags",
+ },
+ {
+ title: "Projets",
+ value: "32",
+ description: "Projets créés",
+ icon: BarChart4,
+ href: "/admin/stats",
+ },
+ {
+ title: "Paramètres",
+ value: "7",
+ description: "Paramètres système",
+ icon: Settings,
+ href: "/admin/settings",
+ },
+ ];
+
+ // Mock data for recent activities
+ const recentActivities = [
+ {
+ id: 1,
+ user: "Jean Dupont",
+ action: "a créé un nouveau projet",
+ target: "Formation Dev Web",
+ date: "2025-05-15T14:32:00",
+ },
+ {
+ id: 2,
+ user: "Marie Martin",
+ action: "a modifié un tag global",
+ target: "Frontend",
+ date: "2025-05-15T13:45:00",
+ },
+ {
+ id: 3,
+ user: "Admin",
+ action: "a ajouté un nouvel utilisateur",
+ target: "Pierre Durand",
+ date: "2025-05-15T11:20:00",
+ },
+ {
+ id: 4,
+ user: "Sophie Lefebvre",
+ action: "a créé un nouveau groupe",
+ target: "Groupe A",
+ date: "2025-05-15T10:15:00",
+ },
+ {
+ id: 5,
+ user: "Admin",
+ action: "a modifié les paramètres système",
+ target: "Paramètres de notification",
+ date: "2025-05-14T16:30:00",
+ },
+ ];
+
+ return (
+
+
+
Administration
+
+
+ Mode administrateur
+
+
+
+
+
+ Vue d'ensemble
+ Activité récente
+ Système
+
+
+
+
+ {stats.map((stat, index) => (
+
+
+ {stat.title}
+
+
+
+ {stat.value}
+ {stat.description}
+
+ Gérer
+
+
+
+ ))}
+
+
+
+
+
+
+ Activité récente
+
+ Les dernières actions effectuées sur la plateforme
+
+
+
+
+ {recentActivities.map((activity) => (
+
+
+
+ {activity.user} {activity.action}{" "}
+ {activity.target}
+
+
+ {new Date(activity.date).toLocaleString("fr-FR", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ })}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Informations système
+
+ Informations sur l'état du système
+
+
+
+
+
+
+
Version de l'application
+
v1.0.0
+
+
+
Dernière mise à jour
+
15 mai 2025
+
+
+
+
Utilisation de la base de données
+
42%
+
+
+
+
+
+
+
+ Paramètres système
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/admin/settings/page.tsx b/frontend/app/admin/settings/page.tsx
new file mode 100644
index 0000000..0eb9216
--- /dev/null
+++ b/frontend/app/admin/settings/page.tsx
@@ -0,0 +1,581 @@
+"use client";
+
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Separator } from "@/components/ui/separator";
+import { Switch } from "@/components/ui/switch";
+import { Textarea } from "@/components/ui/textarea";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { toast } from "sonner";
+import {
+ Save,
+ RefreshCw,
+ Shield,
+ Bell,
+ Mail,
+ Database,
+ Server,
+ FileJson,
+ Loader2
+} from "lucide-react";
+
+export default function AdminSettingsPage() {
+ const [activeTab, setActiveTab] = useState("general");
+ const [isLoading, setIsLoading] = useState(false);
+
+ // Mock system settings
+ const systemSettings = {
+ general: {
+ siteName: "Application de Création de Groupes",
+ siteDescription: "Une application web moderne dédiée à la création et à la gestion de groupes",
+ contactEmail: "admin@example.com",
+ maxProjectsPerUser: "10",
+ maxPersonsPerProject: "100",
+ },
+ authentication: {
+ enableGithubAuth: true,
+ requireEmailVerification: false,
+ sessionTimeout: "7",
+ maxLoginAttempts: "5",
+ passwordMinLength: "8",
+ },
+ notifications: {
+ enableEmailNotifications: true,
+ enableSystemNotifications: true,
+ notifyOnNewUser: true,
+ notifyOnNewProject: false,
+ adminEmailRecipients: "admin@example.com",
+ },
+ maintenance: {
+ maintenanceMode: false,
+ maintenanceMessage: "Le site est actuellement en maintenance. Veuillez réessayer plus tard.",
+ debugMode: false,
+ logLevel: "error",
+ },
+ };
+
+ const { register: registerGeneral, handleSubmit: handleSubmitGeneral, formState: { errors: errorsGeneral } } = useForm({
+ defaultValues: systemSettings.general,
+ });
+
+ const { register: registerAuth, handleSubmit: handleSubmitAuth, formState: { errors: errorsAuth } } = useForm({
+ defaultValues: systemSettings.authentication,
+ });
+
+ const { register: registerNotif, handleSubmit: handleSubmitNotif, formState: { errors: errorsNotif } } = useForm({
+ defaultValues: systemSettings.notifications,
+ });
+
+ const { register: registerMaint, handleSubmit: handleSubmitMaint, formState: { errors: errorsMaint } } = useForm({
+ defaultValues: systemSettings.maintenance,
+ });
+
+ const onSubmitGeneral = async (data: any) => {
+ setIsLoading(true);
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ setIsLoading(false);
+ toast.success("Paramètres généraux mis à jour avec succès");
+ };
+
+ const onSubmitAuth = async (data: any) => {
+ setIsLoading(true);
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ setIsLoading(false);
+ toast.success("Paramètres d'authentification mis à jour avec succès");
+ };
+
+ const onSubmitNotif = async (data: any) => {
+ setIsLoading(true);
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ setIsLoading(false);
+ toast.success("Paramètres de notification mis à jour avec succès");
+ };
+
+ const onSubmitMaint = async (data: any) => {
+ setIsLoading(true);
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ setIsLoading(false);
+ toast.success("Paramètres de maintenance mis à jour avec succès");
+ };
+
+ const handleExportConfig = async () => {
+ setIsLoading(true);
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ setIsLoading(false);
+ toast.success("Configuration exportée avec succès");
+ };
+
+ const handleClearCache = async () => {
+ setIsLoading(true);
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ setIsLoading(false);
+ toast.success("Cache vidé avec succès");
+ };
+
+ return (
+
+
+
Paramètres système
+
+
+ Configuration globale
+
+
+
+
+
+ Général
+ Authentification
+ Notifications
+ Maintenance
+
+
+
+
+
+
+
+
+
+
+
+
+ Paramètres d'authentification
+
+ Configurez les options d'authentification et de sécurité
+
+
+
+
+
+
+
Authentification GitHub
+
+ Activer l'authentification via GitHub OAuth
+
+
+
+
+
+
+
+
+
+
Vérification d'email
+
+ Exiger la vérification de l'email lors de l'inscription
+
+
+
+
+
+
+
+
+
+
Durée de session (jours)
+
+ {errorsAuth.sessionTimeout && (
+
{errorsAuth.sessionTimeout.message as string}
+ )}
+
+
+
Tentatives de connexion max.
+
+ {errorsAuth.maxLoginAttempts && (
+
{errorsAuth.maxLoginAttempts.message as string}
+ )}
+
+
+
Longueur min. du mot de passe
+
+ {errorsAuth.passwordMinLength && (
+
{errorsAuth.passwordMinLength.message as string}
+ )}
+
+
+
+
+
+
+ {isLoading ? (
+ <>
+
+ Enregistrement...
+ >
+ ) : (
+ <>
+
+ Enregistrer les modifications
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+ Paramètres de notification
+
+ Configurez les options de notification système et email
+
+
+
+
+
+
+
Notifications par email
+
+ Activer l'envoi de notifications par email
+
+
+
+
+
+
+
+
+
+
Notifications système
+
+ Activer les notifications dans l'application
+
+
+
+
+
+
+
+
+
+
Notification nouvel utilisateur
+
+ Notifier les administrateurs lors de l'inscription d'un nouvel utilisateur
+
+
+
+
+
+
+
+
+
+
Notification nouveau projet
+
+ Notifier les administrateurs lors de la création d'un nouveau projet
+
+
+
+
+
+
+
+
+
Destinataires des emails administratifs
+
+
+ Séparez les adresses email par des virgules pour plusieurs destinataires
+
+ {errorsNotif.adminEmailRecipients && (
+
{errorsNotif.adminEmailRecipients.message as string}
+ )}
+
+
+
+
+
+ {isLoading ? (
+ <>
+
+ Enregistrement...
+ >
+ ) : (
+ <>
+
+ Enregistrer les modifications
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+ Maintenance et débogage
+
+ Configurez les options de maintenance et de débogage
+
+
+
+
+
+
+
Mode maintenance
+
+ Activer le mode maintenance (le site sera inaccessible aux utilisateurs)
+
+
+
+
+
+
+ Message de maintenance
+
+
+
+
+
+
+
+
Mode débogage
+
+ Activer le mode débogage (affiche des informations supplémentaires)
+
+
+
+
+
+
+ Niveau de journalisation
+
+ Error
+ Warning
+ Info
+ Debug
+ Trace
+
+
+
+
+
+
+
+
+
+ Exporter la configuration
+
+
+
+
+
+ Vider le cache
+
+
+
+
+
+
+
+ {isLoading ? (
+ <>
+
+ Enregistrement...
+ >
+ ) : (
+ <>
+
+ Enregistrer les modifications
+ >
+ )}
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/admin/stats/page.tsx b/frontend/app/admin/stats/page.tsx
new file mode 100644
index 0000000..7ec36ba
--- /dev/null
+++ b/frontend/app/admin/stats/page.tsx
@@ -0,0 +1,319 @@
+"use client";
+
+import { useState } from "react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ Legend,
+ ResponsiveContainer,
+ PieChart,
+ Pie,
+ Cell,
+ LineChart,
+ Line
+} from "recharts";
+import {
+ BarChart4,
+ Users,
+ FolderKanban,
+ Tags,
+ Calendar,
+ Download
+} from "lucide-react";
+import { Button } from "@/components/ui/button";
+
+// Mock data for charts
+const userRegistrationData = [
+ { name: "Jan", count: 4 },
+ { name: "Fév", count: 3 },
+ { name: "Mar", count: 5 },
+ { name: "Avr", count: 7 },
+ { name: "Mai", count: 2 },
+ { name: "Juin", count: 6 },
+ { name: "Juil", count: 8 },
+ { name: "Août", count: 9 },
+ { name: "Sep", count: 11 },
+ { name: "Oct", count: 13 },
+ { name: "Nov", count: 7 },
+ { name: "Déc", count: 5 },
+];
+
+const projectCreationData = [
+ { name: "Jan", count: 2 },
+ { name: "Fév", count: 4 },
+ { name: "Mar", count: 3 },
+ { name: "Avr", count: 5 },
+ { name: "Mai", count: 1 },
+ { name: "Juin", count: 3 },
+ { name: "Juil", count: 6 },
+ { name: "Août", count: 4 },
+ { name: "Sep", count: 7 },
+ { name: "Oct", count: 8 },
+ { name: "Nov", count: 5 },
+ { name: "Déc", count: 3 },
+];
+
+const userRoleData = [
+ { name: "Administrateurs", value: 3 },
+ { name: "Utilisateurs standard", value: 21 },
+];
+
+const tagUsageData = [
+ { name: "Frontend", value: 12 },
+ { name: "Backend", value: 8 },
+ { name: "Fullstack", value: 5 },
+ { name: "UX/UI", value: 3 },
+ { name: "DevOps", value: 2 },
+];
+
+const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8'];
+
+const dailyActiveUsersData = [
+ { name: "Lun", users: 15 },
+ { name: "Mar", users: 18 },
+ { name: "Mer", users: 22 },
+ { name: "Jeu", users: 19 },
+ { name: "Ven", users: 23 },
+ { name: "Sam", users: 12 },
+ { name: "Dim", users: 10 },
+];
+
+export default function AdminStatsPage() {
+ const [activeTab, setActiveTab] = useState("overview");
+
+ // Mock statistics
+ const stats = [
+ {
+ title: "Utilisateurs",
+ value: "24",
+ change: "+12%",
+ trend: "up",
+ icon: Users,
+ },
+ {
+ title: "Projets",
+ value: "32",
+ change: "+8%",
+ trend: "up",
+ icon: FolderKanban,
+ },
+ {
+ title: "Groupes créés",
+ value: "128",
+ change: "+15%",
+ trend: "up",
+ icon: Users,
+ },
+ {
+ title: "Tags utilisés",
+ value: "18",
+ change: "+5%",
+ trend: "up",
+ icon: Tags,
+ },
+ ];
+
+ const handleExportStats = () => {
+ alert("Statistiques exportées en CSV");
+ };
+
+ return (
+
+
+
Statistiques
+
+
+ Exporter en CSV
+
+
+
+
+ {stats.map((stat, index) => (
+
+
+ {stat.title}
+
+
+
+ {stat.value}
+
+ {stat.change} depuis le mois dernier
+
+
+
+ ))}
+
+
+
+
+ Utilisateurs
+ Projets
+ Tags
+ Activité
+
+
+
+
+
+ Inscriptions d'utilisateurs par mois
+
+ Nombre de nouveaux utilisateurs inscrits par mois
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Répartition des rôles utilisateurs
+
+ Proportion d'administrateurs et d'utilisateurs standard
+
+
+
+
+
+ `${name}: ${(percent * 100).toFixed(0)}%`}
+ outerRadius={80}
+ fill="#8884d8"
+ dataKey="value"
+ >
+ {userRoleData.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+ Création de projets par mois
+
+ Nombre de nouveaux projets créés par mois
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Utilisation des tags
+
+ Nombre d'utilisations par tag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Utilisateurs actifs par jour
+
+ Nombre d'utilisateurs actifs par jour de la semaine
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/admin/tags/page.tsx b/frontend/app/admin/tags/page.tsx
new file mode 100644
index 0000000..cf4b857
--- /dev/null
+++ b/frontend/app/admin/tags/page.tsx
@@ -0,0 +1,278 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow
+} from "@/components/ui/table";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from "@/components/ui/dropdown-menu";
+import {
+ PlusCircle,
+ Search,
+ MoreHorizontal,
+ Pencil,
+ Trash2,
+ Users,
+ CircleDot
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+
+export default function AdminTagsPage() {
+ const [searchQuery, setSearchQuery] = useState("");
+
+ // Mock data for global tags
+ const tags = [
+ {
+ id: 1,
+ name: "Frontend",
+ description: "Développement frontend",
+ color: "blue",
+ usageCount: 12,
+ global: true,
+ createdBy: "Admin",
+ },
+ {
+ id: 2,
+ name: "Backend",
+ description: "Développement backend",
+ color: "green",
+ usageCount: 8,
+ global: true,
+ createdBy: "Admin",
+ },
+ {
+ id: 3,
+ name: "Fullstack",
+ description: "Développement fullstack",
+ color: "purple",
+ usageCount: 5,
+ global: true,
+ createdBy: "Admin",
+ },
+ {
+ id: 4,
+ name: "UX/UI",
+ description: "Design UX/UI",
+ color: "pink",
+ usageCount: 3,
+ global: true,
+ createdBy: "Marie Martin",
+ },
+ {
+ id: 5,
+ name: "DevOps",
+ description: "Infrastructure et déploiement",
+ color: "orange",
+ usageCount: 2,
+ global: true,
+ createdBy: "Thomas Bernard",
+ },
+ {
+ id: 6,
+ name: "Junior",
+ description: "Niveau junior",
+ color: "yellow",
+ usageCount: 7,
+ global: true,
+ createdBy: "Admin",
+ },
+ {
+ id: 7,
+ name: "Medior",
+ description: "Niveau intermédiaire",
+ color: "amber",
+ usageCount: 5,
+ global: true,
+ createdBy: "Admin",
+ },
+ {
+ id: 8,
+ name: "Senior",
+ description: "Niveau senior",
+ color: "red",
+ usageCount: 6,
+ global: true,
+ createdBy: "Admin",
+ },
+ ];
+
+ // Map color names to Tailwind classes
+ const colorMap: Record = {
+ blue: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
+ green: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
+ purple: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
+ pink: "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300",
+ orange: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300",
+ yellow: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
+ amber: "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300",
+ red: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
+ };
+
+ // Filter tags based on search query
+ const filteredTags = tags.filter(
+ (tag) =>
+ tag.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ tag.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ tag.createdBy.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+
+ const handleDeleteTag = (tagId: number) => {
+ toast.success(`Tag #${tagId} supprimé avec succès`);
+ };
+
+ return (
+
+
+
Tags globaux
+
+
+
+ Nouveau tag global
+
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+ {/* Mobile card view */}
+
+ {filteredTags.length === 0 ? (
+
+ Aucun tag trouvé.
+
+ ) : (
+ filteredTags.map((tag) => (
+
+
+
+ {tag.name}
+
+
+ {tag.usageCount} utilisations
+
+
+
+
+ Créé par: {tag.createdBy}
+
+
+
+
+
+ Modifier
+
+
+
handleDeleteTag(tag.id)}
+ className="text-destructive hover:text-destructive"
+ >
+
+
+
+
+ ))
+ )}
+
+
+ {/* Desktop table view */}
+
+
+
+
+ Tag
+ Description
+ Utilisations
+ Créé par
+ Actions
+
+
+
+ {filteredTags.length === 0 ? (
+
+
+ Aucun tag trouvé.
+
+
+ ) : (
+ filteredTags.map((tag) => (
+
+
+
+ {tag.name}
+
+
+ {tag.description}
+ {tag.usageCount}
+ {tag.createdBy}
+
+
+
+
+
+ Actions
+
+
+
+ Actions
+
+
+
+
+ Modifier
+
+
+
+
+
+ Voir les utilisations
+
+
+ handleDeleteTag(tag.id)}
+ className="text-destructive focus:text-destructive flex items-center"
+ >
+
+ Supprimer
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/admin/users/page.tsx b/frontend/app/admin/users/page.tsx
new file mode 100644
index 0000000..e696149
--- /dev/null
+++ b/frontend/app/admin/users/page.tsx
@@ -0,0 +1,301 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow
+} from "@/components/ui/table";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from "@/components/ui/dropdown-menu";
+import {
+ PlusCircle,
+ Search,
+ MoreHorizontal,
+ Pencil,
+ Trash2,
+ Shield,
+ UserCog
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+
+export default function AdminUsersPage() {
+ const [searchQuery, setSearchQuery] = useState("");
+
+ // Mock data for users
+ const users = [
+ {
+ id: 1,
+ name: "Jean Dupont",
+ email: "jean.dupont@example.com",
+ role: "user",
+ status: "active",
+ lastLogin: "2025-05-15T14:32:00",
+ projects: 3,
+ },
+ {
+ id: 2,
+ name: "Marie Martin",
+ email: "marie.martin@example.com",
+ role: "admin",
+ status: "active",
+ lastLogin: "2025-05-15T13:45:00",
+ projects: 5,
+ },
+ {
+ id: 3,
+ name: "Pierre Durand",
+ email: "pierre.durand@example.com",
+ role: "user",
+ status: "inactive",
+ lastLogin: "2025-05-10T11:20:00",
+ projects: 1,
+ },
+ {
+ id: 4,
+ name: "Sophie Lefebvre",
+ email: "sophie.lefebvre@example.com",
+ role: "user",
+ status: "active",
+ lastLogin: "2025-05-15T10:15:00",
+ projects: 2,
+ },
+ {
+ id: 5,
+ name: "Thomas Bernard",
+ email: "thomas.bernard@example.com",
+ role: "admin",
+ status: "active",
+ lastLogin: "2025-05-14T16:30:00",
+ projects: 0,
+ },
+ ];
+
+ // Filter users based on search query
+ const filteredUsers = users.filter(
+ (user) =>
+ user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ user.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ user.role.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+
+ const handleDeleteUser = (userId: number) => {
+ toast.success(`Utilisateur #${userId} supprimé avec succès`);
+ };
+
+ const handleChangeRole = (userId: number, newRole: string) => {
+ toast.success(`Rôle de l'utilisateur #${userId} changé en ${newRole}`);
+ };
+
+ return (
+
+
+
Gestion des utilisateurs
+
+
+
+ Nouvel utilisateur
+
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+ {/* Mobile card view */}
+
+ {filteredUsers.length === 0 ? (
+
+ Aucun utilisateur trouvé.
+
+ ) : (
+ filteredUsers.map((user) => (
+
+
+
+ {user.name}
+ {user.email}
+
+
+ {user.role === "admin" ? (
+
+ ) : null}
+ {user.role === "admin" ? "Admin" : "Utilisateur"}
+
+
+
+
+ Statut:
+
+ {user.status === "active" ? "Actif" : "Inactif"}
+
+
+
+ Projets:
+ {user.projects}
+
+
+ Dernière connexion:
+
+ {new Date(user.lastLogin).toLocaleString("fr-FR", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ })}
+
+
+
+
+
+
+
+ Gérer
+
+
+
+
+
+
+
+
+
+ Actions
+
+
+
+
+ Modifier
+
+
+ handleChangeRole(user.id, user.role === "admin" ? "user" : "admin")}
+ >
+
+ {user.role === "admin" ? "Retirer les droits admin" : "Promouvoir admin"}
+
+ handleDeleteUser(user.id)}
+ className="text-destructive focus:text-destructive"
+ >
+
+ Supprimer
+
+
+
+
+
+ ))
+ )}
+
+
+ {/* Desktop table view */}
+
+
+
+
+ Nom
+ Email
+ Rôle
+ Statut
+ Dernière connexion
+ Projets
+ Actions
+
+
+
+ {filteredUsers.length === 0 ? (
+
+
+ Aucun utilisateur trouvé.
+
+
+ ) : (
+ filteredUsers.map((user) => (
+
+ {user.name}
+ {user.email}
+
+
+ {user.role === "admin" ? (
+
+ ) : null}
+ {user.role === "admin" ? "Admin" : "Utilisateur"}
+
+
+
+
+ {user.status === "active" ? "Actif" : "Inactif"}
+
+
+
+ {new Date(user.lastLogin).toLocaleString("fr-FR", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ })}
+
+ {user.projects}
+
+
+
+
+
+ Actions
+
+
+
+ Actions
+
+
+
+
+ Modifier
+
+
+ handleChangeRole(user.id, user.role === "admin" ? "user" : "admin")}
+ className="flex items-center"
+ >
+
+ {user.role === "admin" ? "Retirer les droits admin" : "Promouvoir admin"}
+
+ handleDeleteUser(user.id)}
+ className="text-destructive focus:text-destructive flex items-center"
+ >
+
+ Supprimer
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/app/auth/callback/page.tsx b/frontend/app/auth/callback/page.tsx
new file mode 100644
index 0000000..29fe90e
--- /dev/null
+++ b/frontend/app/auth/callback/page.tsx
@@ -0,0 +1,79 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { Loader2 } from "lucide-react";
+import { useAuth } from "@/lib/auth-context";
+
+export default function CallbackPage() {
+ const router = useRouter();
+ const [error, setError] = useState(null);
+ const { login, user } = useAuth();
+
+ useEffect(() => {
+ async function handleCallback() {
+ try {
+ // Get the code from the URL query parameters
+ const urlParams = new URLSearchParams(window.location.search);
+ const code = urlParams.get('code');
+
+ if (!code) {
+ throw new Error('No authorization code found in the URL');
+ }
+
+ // Use the auth context to login
+ await login(code);
+
+ // Check if there's a stored callbackUrl
+ const callbackUrl = sessionStorage.getItem('callbackUrl');
+
+ // Clear the stored callbackUrl
+ sessionStorage.removeItem('callbackUrl');
+
+ // Redirect based on role and callbackUrl
+ if (callbackUrl) {
+ // For admin routes, check if user has admin role
+ if (callbackUrl.startsWith('/admin') && user?.role !== 'ADMIN') {
+ router.push('/dashboard');
+ } else {
+ router.push(callbackUrl);
+ }
+ } else {
+ // Default redirects if no callbackUrl
+ if (user && user.role === 'ADMIN') {
+ router.push('/admin');
+ } else {
+ router.push('/dashboard');
+ }
+ }
+ } catch (err) {
+ console.error("Authentication error:", err);
+ setError("Une erreur est survenue lors de l'authentification. Veuillez réessayer.");
+ }
+ }
+
+ handleCallback();
+ }, [router]);
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
Authentification en cours...
+
Vous allez être redirigé vers l'application.
+
+ );
+}
diff --git a/frontend/app/auth/login/page.tsx b/frontend/app/auth/login/page.tsx
new file mode 100644
index 0000000..a40211f
--- /dev/null
+++ b/frontend/app/auth/login/page.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import { useState } from "react";
+import { Github } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+
+export default function LoginPage() {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleGitHubLogin = async () => {
+ setIsLoading(true);
+ try {
+ // Get the callbackUrl from the URL if present
+ const urlParams = new URLSearchParams(window.location.search);
+ const callbackUrl = urlParams.get('callbackUrl');
+
+ // Use the API service to get the GitHub OAuth URL
+ const { url } = await import('@/lib/api').then(module =>
+ module.authAPI.getGitHubOAuthUrl()
+ );
+
+ // Store the callbackUrl in sessionStorage to use after authentication
+ if (callbackUrl) {
+ sessionStorage.setItem('callbackUrl', callbackUrl);
+ }
+
+ // Redirect to GitHub OAuth page
+ window.location.href = url;
+ } catch (error) {
+ console.error('Login error:', error);
+ setIsLoading(false);
+ // You could add error handling UI here
+ }
+ };
+
+ return (
+
+
+
+ Connexion
+
+ Connectez-vous pour accéder à l'application de création de groupes
+
+
+
+
+ {isLoading ? (
+
+
+ Connexion en cours...
+
+ ) : (
+
+
+ Se connecter avec GitHub
+
+ )}
+
+
+
+
+ En vous connectant, vous acceptez nos conditions d'utilisation et notre politique de confidentialité.
+
+
+
+
+ );
+}
diff --git a/frontend/app/auth/logout/page.tsx b/frontend/app/auth/logout/page.tsx
new file mode 100644
index 0000000..fc7c579
--- /dev/null
+++ b/frontend/app/auth/logout/page.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { Loader2 } from "lucide-react";
+import { useAuth } from "@/lib/auth-context";
+
+export default function LogoutPage() {
+ const router = useRouter();
+ const { logout } = useAuth();
+
+ useEffect(() => {
+ async function handleLogout() {
+ try {
+ // Use the auth context to logout
+ await logout();
+
+ // Note: The auth context handles clearing localStorage and redirecting
+ } catch (error) {
+ console.error('Logout error:', error);
+ // Even if there's an error, still redirect to login
+ router.push('/auth/login');
+ }
+ }
+
+ handleLogout();
+ }, [router]);
+
+ return (
+
+
+
Déconnexion en cours...
+
Vous allez être redirigé vers la page de connexion.
+
+ );
+}
diff --git a/frontend/app/dashboard/layout.tsx b/frontend/app/dashboard/layout.tsx
new file mode 100644
index 0000000..3909db2
--- /dev/null
+++ b/frontend/app/dashboard/layout.tsx
@@ -0,0 +1,10 @@
+import { DashboardLayout } from "@/components/dashboard-layout";
+import { AuthLoading } from "@/components/auth-loading";
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx
new file mode 100644
index 0000000..f92e0be
--- /dev/null
+++ b/frontend/app/dashboard/page.tsx
@@ -0,0 +1,176 @@
+"use client";
+
+import { useState } from "react";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Button } from "@/components/ui/button";
+import { PlusCircle, Users, FolderKanban, Tags } from "lucide-react";
+import Link from "next/link";
+
+export default function DashboardPage() {
+ const [activeTab, setActiveTab] = useState("overview");
+
+ // Mock data for the dashboard
+ const stats = [
+ {
+ title: "Projets",
+ value: "5",
+ description: "Projets actifs",
+ icon: FolderKanban,
+ href: "/projects",
+ },
+ {
+ title: "Personnes",
+ value: "42",
+ description: "Personnes enregistrées",
+ icon: Users,
+ href: "/persons",
+ },
+ {
+ title: "Tags",
+ value: "12",
+ description: "Tags disponibles",
+ icon: Tags,
+ href: "/tags",
+ },
+ ];
+
+ // Mock data for recent projects
+ const recentProjects = [
+ {
+ id: 1,
+ name: "Projet Formation Dev Web",
+ description: "Création de groupes pour la formation développement web",
+ date: "2025-05-15",
+ groups: 4,
+ persons: 16,
+ },
+ {
+ id: 2,
+ name: "Projet Hackathon",
+ description: "Équipes pour le hackathon annuel",
+ date: "2025-05-10",
+ groups: 8,
+ persons: 32,
+ },
+ {
+ id: 3,
+ name: "Projet Workshop UX/UI",
+ description: "Groupes pour l'atelier UX/UI",
+ date: "2025-05-05",
+ groups: 5,
+ persons: 20,
+ },
+ ];
+
+ return (
+
+
+
Tableau de bord
+
+
+
+ Nouveau projet
+
+
+
+
+
+
+ Vue d'ensemble
+ Analytiques
+ Rapports
+
+
+
+ {stats.map((stat, index) => (
+
+
+ {stat.title}
+
+
+
+
+
+ {stat.value}
+ {stat.description}
+
+ Voir tous
+
+
+
+ ))}
+
+
+
+
+ Projets récents
+
+ Vous avez {recentProjects.length} projets récents
+
+
+
+
+ {recentProjects.map((project) => (
+
+
+
+
{project.name}
+
{project.description}
+
+
+ {new Date(project.date).toLocaleDateString("fr-FR")}
+ {project.groups} groupes
+ {project.persons} personnes
+
+
+
+
+ Voir
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Analytiques
+
+ Visualisez les statistiques de vos projets et groupes
+
+
+
+
+
+ Les graphiques d'analytiques seront disponibles prochainement
+
+
+
+
+
+
+
+
+ Rapports
+
+ Générez des rapports sur vos projets et groupes
+
+
+
+
+
+ La génération de rapports sera disponible prochainement
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index f7fa87e..963f46c 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -1,6 +1,8 @@
-import type { Metadata } from "next";
+import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
+import { ThemeProvider } from "@/components/theme-provider";
+import { AuthProvider } from "@/lib/auth-context";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -13,8 +15,15 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Application de Création de Groupes",
+ description: "Une application web moderne dédiée à la création et à la gestion de groupes",
+};
+
+export const viewport: Viewport = {
+ width: "device-width",
+ initialScale: 1,
+ maximumScale: 1,
+ userScalable: true,
};
export default function RootLayout({
@@ -23,11 +32,20 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+
- {children}
+
+
+ {children}
+
+
);
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index 88f0cc9..1d5aed6 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -1,102 +1,113 @@
-import Image from "next/image";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import {
+ Users,
+ FolderKanban,
+ Tags,
+ ArrowRight
+} from "lucide-react";
export default function Home() {
return (
-
-
-
-
-
- Get started by editing{" "}
-
- app/page.tsx
-
- .
-
-
- Save and see your changes instantly.
-
-
-
-
-
-
- Deploy now
-
-
- Read our docs
-
+
+ {/* Header */}
+
+
+
+ Groupes
+
+
+
+ Connexion
+
+
+
+
+ Connexion
+
+
+
+
+
+ {/* Hero Section */}
+
+
+
+
+
+ Application de Création de Groupes
+
+
+ Une application web moderne dédiée à la création et à la gestion de groupes, permettant aux utilisateurs de créer des groupes selon différents critères.
+
+
+
+
+
+
+
+ {/* Features Section */}
+
+
+
+
+
+
+
+
+
Gestion de Projets
+
+ Créez et gérez des projets de groupe avec une liste de personnes et des critères personnalisés.
+
+
+
+
+
+
+
+
+
Création de Groupes
+
+ Utilisez notre assistant pour créer automatiquement des groupes équilibrés ou créez-les manuellement.
+
+
+
+
+
+
+
+
+
Gestion des Tags
+
+ Attribuez des tags aux personnes pour faciliter la création de groupes équilibrés.
+
+
+
+
+
+
+
+ {/* Footer */}
+
);
diff --git a/frontend/app/persons/[id]/edit/page.tsx b/frontend/app/persons/[id]/edit/page.tsx
new file mode 100644
index 0000000..516b2ed
--- /dev/null
+++ b/frontend/app/persons/[id]/edit/page.tsx
@@ -0,0 +1,346 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useParams, useRouter } from "next/navigation";
+import Link from "next/link";
+import { useForm, Controller } from "react-hook-form";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ ArrowLeft,
+ Loader2,
+ Save,
+ Plus,
+ X
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+// Type definitions
+interface PersonFormData {
+ name: string;
+ email: string;
+ level: string;
+}
+
+// Mock data for available tags
+const availableTags = [
+ "Frontend", "Backend", "Fullstack", "UX/UI", "DevOps",
+ "React", "Vue", "Angular", "Node.js", "Python", "Java", "PHP",
+ "JavaScript", "TypeScript", "CSS", "Docker", "Kubernetes", "Design",
+ "Figma", "MERN"
+];
+
+// Levels
+const levels = ["Junior", "Medior", "Senior"];
+
+// Mock person data
+const getPersonData = (id: string) => {
+ return {
+ id: parseInt(id),
+ name: "Jean Dupont",
+ email: "jean.dupont@example.com",
+ tags: ["Frontend", "React", "Junior"],
+ };
+};
+
+export default function EditPersonPage() {
+ const params = useParams();
+ const router = useRouter();
+ const personId = params.id as string;
+
+ const [person, setPerson] = useState
(null);
+ const [loading, setLoading] = useState(true);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const [selectedTags, setSelectedTags] = useState([]);
+ const [tagInput, setTagInput] = useState("");
+ const [filteredTags, setFilteredTags] = useState([]);
+
+ const { register, handleSubmit, control, formState: { errors }, reset } = useForm();
+
+ // Filter available tags based on input
+ useEffect(() => {
+ if (tagInput) {
+ const filtered = availableTags.filter(
+ tag =>
+ tag.toLowerCase().includes(tagInput.toLowerCase()) &&
+ !selectedTags.includes(tag)
+ );
+ setFilteredTags(filtered);
+ } else {
+ setFilteredTags([]);
+ }
+ }, [tagInput, selectedTags]);
+
+ useEffect(() => {
+ // Simulate API call to fetch person data
+ const fetchPerson = async () => {
+ setLoading(true);
+ try {
+ // In a real app, this would be an API call
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ const data = getPersonData(personId);
+ setPerson(data);
+
+ // Extract level from tags (assuming the last tag is the level)
+ const level = data.tags.find(tag => ["Junior", "Medior", "Senior"].includes(tag)) || "";
+
+ // Set selected tags (excluding the level)
+ const tags = data.tags.filter(tag => !["Junior", "Medior", "Senior"].includes(tag));
+ setSelectedTags(tags);
+
+ // Reset form with person data
+ reset({
+ name: data.name,
+ email: data.email,
+ level: level
+ });
+ } catch (error) {
+ console.error("Error fetching person:", error);
+ toast.error("Erreur lors du chargement de la personne");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchPerson();
+ }, [personId, reset]);
+
+ const handleAddTag = (tag: string) => {
+ if (!selectedTags.includes(tag)) {
+ setSelectedTags([...selectedTags, tag]);
+ }
+ setTagInput("");
+ setFilteredTags([]);
+ };
+
+ const handleRemoveTag = (tag: string) => {
+ setSelectedTags(selectedTags.filter(t => t !== tag));
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && tagInput) {
+ e.preventDefault();
+ if (filteredTags.length > 0) {
+ handleAddTag(filteredTags[0]);
+ } else if (!selectedTags.includes(tagInput)) {
+ // Add as a new tag if it doesn't exist
+ handleAddTag(tagInput);
+ }
+ }
+ };
+
+ const onSubmit = async (data: PersonFormData) => {
+ if (selectedTags.length === 0) {
+ toast.error("Veuillez sélectionner au moins un tag");
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ // In a real app, this would be an API call to update the person
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Combine form data with selected tags
+ const personData = {
+ ...data,
+ tags: [...selectedTags, data.level]
+ };
+
+ toast.success("Personne mise à jour avec succès");
+ router.push("/persons");
+ } catch (error) {
+ console.error("Error updating person:", error);
+ toast.error("Erreur lors de la mise à jour de la personne");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!person) {
+ return (
+
+
Personne non trouvée
+
+ Retour aux personnes
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
Modifier la personne
+
+
+
+
+
+ Informations de la personne
+
+ Modifiez les informations et les tags de la personne
+
+
+
+
+
Nom
+
+ {errors.name && (
+
{errors.name.message}
+ )}
+
+
+
+
Email
+
+ {errors.email && (
+
{errors.email.message}
+ )}
+
+
+
+
Niveau
+
(
+
+
+
+
+
+ {levels.map((level) => (
+
+ {level}
+
+ ))}
+
+
+ )}
+ />
+ {errors.level && (
+ {errors.level.message}
+ )}
+
+
+
+
Tags
+
+ {selectedTags.map((tag) => (
+
+ {tag}
+ handleRemoveTag(tag)}
+ >
+
+ Supprimer le tag {tag}
+
+
+ ))}
+
+
+
setTagInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ />
+ {filteredTags.length > 0 && (
+
+
+ {filteredTags.map((tag) => (
+
handleAddTag(tag)}
+ >
+
+ {tag}
+
+ ))}
+
+
+ )}
+
+
+ Appuyez sur Entrée pour ajouter un tag ou sélectionnez-en un dans la liste
+
+
+
+
+
+ Annuler
+
+
+ {isSubmitting ? (
+ <>
+
+ Enregistrement...
+ >
+ ) : (
+ <>
+
+ Enregistrer les modifications
+ >
+ )}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/persons/layout.tsx b/frontend/app/persons/layout.tsx
new file mode 100644
index 0000000..34e1a93
--- /dev/null
+++ b/frontend/app/persons/layout.tsx
@@ -0,0 +1,10 @@
+import { DashboardLayout } from "@/components/dashboard-layout";
+import { AuthLoading } from "@/components/auth-loading";
+
+export default function PersonsLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/app/persons/new/page.tsx b/frontend/app/persons/new/page.tsx
new file mode 100644
index 0000000..429e0aa
--- /dev/null
+++ b/frontend/app/persons/new/page.tsx
@@ -0,0 +1,286 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import { useForm, Controller } from "react-hook-form";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ ArrowLeft,
+ Loader2,
+ Save,
+ Plus,
+ X
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+// Type definitions
+interface PersonFormData {
+ name: string;
+ email: string;
+ level: string;
+}
+
+// Mock data for available tags
+const availableTags = [
+ "Frontend", "Backend", "Fullstack", "UX/UI", "DevOps",
+ "React", "Vue", "Angular", "Node.js", "Python", "Java", "PHP",
+ "JavaScript", "TypeScript", "CSS", "Docker", "Kubernetes", "Design",
+ "Figma", "MERN"
+];
+
+// Levels
+const levels = ["Junior", "Medior", "Senior"];
+
+export default function NewPersonPage() {
+ const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [selectedTags, setSelectedTags] = useState([]);
+ const [tagInput, setTagInput] = useState("");
+ const [filteredTags, setFilteredTags] = useState([]);
+
+ const { register, handleSubmit, control, formState: { errors } } = useForm({
+ defaultValues: {
+ name: "",
+ email: "",
+ level: ""
+ }
+ });
+
+ // Filter available tags based on input
+ useEffect(() => {
+ if (tagInput) {
+ const filtered = availableTags.filter(
+ tag =>
+ tag.toLowerCase().includes(tagInput.toLowerCase()) &&
+ !selectedTags.includes(tag)
+ );
+ setFilteredTags(filtered);
+ } else {
+ setFilteredTags([]);
+ }
+ }, [tagInput, selectedTags]);
+
+ const handleAddTag = (tag: string) => {
+ if (!selectedTags.includes(tag)) {
+ setSelectedTags([...selectedTags, tag]);
+ }
+ setTagInput("");
+ setFilteredTags([]);
+ };
+
+ const handleRemoveTag = (tag: string) => {
+ setSelectedTags(selectedTags.filter(t => t !== tag));
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && tagInput) {
+ e.preventDefault();
+ if (filteredTags.length > 0) {
+ handleAddTag(filteredTags[0]);
+ } else if (!selectedTags.includes(tagInput)) {
+ // Add as a new tag if it doesn't exist
+ handleAddTag(tagInput);
+ }
+ }
+ };
+
+ const onSubmit = async (data: PersonFormData) => {
+ if (selectedTags.length === 0) {
+ toast.error("Veuillez sélectionner au moins un tag");
+ return;
+ }
+
+ setIsSubmitting(true);
+ try {
+ // In a real app, this would be an API call to create a new person
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Combine form data with selected tags
+ const personData = {
+ ...data,
+ tags: [...selectedTags, data.level]
+ };
+
+ // Simulate a successful response with a person ID
+ const personId = Date.now();
+
+ toast.success("Personne créée avec succès");
+ router.push("/persons");
+ } catch (error) {
+ console.error("Error creating person:", error);
+ toast.error("Erreur lors de la création de la personne");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
Nouvelle personne
+
+
+
+
+
+ Informations de la personne
+
+ Ajoutez une nouvelle personne à votre projet
+
+
+
+
+
Nom
+
+ {errors.name && (
+
{errors.name.message}
+ )}
+
+
+
+
Email
+
+ {errors.email && (
+
{errors.email.message}
+ )}
+
+
+
+
Niveau
+
(
+
+
+
+
+
+ {levels.map((level) => (
+
+ {level}
+
+ ))}
+
+
+ )}
+ />
+ {errors.level && (
+ {errors.level.message}
+ )}
+
+
+
+
Tags
+
+ {selectedTags.map((tag) => (
+
+ {tag}
+ handleRemoveTag(tag)}
+ >
+
+ Supprimer le tag {tag}
+
+
+ ))}
+
+
+
setTagInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ />
+ {filteredTags.length > 0 && (
+
+
+ {filteredTags.map((tag) => (
+
handleAddTag(tag)}
+ >
+
+ {tag}
+
+ ))}
+
+
+ )}
+
+
+ Appuyez sur Entrée pour ajouter un tag ou sélectionnez-en un dans la liste
+
+
+
+
+
+ Annuler
+
+
+ {isSubmitting ? (
+ <>
+
+ Création en cours...
+ >
+ ) : (
+ <>
+
+ Créer la personne
+ >
+ )}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/persons/page.tsx b/frontend/app/persons/page.tsx
new file mode 100644
index 0000000..5e1416c
--- /dev/null
+++ b/frontend/app/persons/page.tsx
@@ -0,0 +1,193 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow
+} from "@/components/ui/table";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from "@/components/ui/dropdown-menu";
+import {
+ PlusCircle,
+ Search,
+ MoreHorizontal,
+ Pencil,
+ Trash2,
+ Tag
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+
+export default function PersonsPage() {
+ const [searchQuery, setSearchQuery] = useState("");
+
+ // Mock data for persons
+ const persons = [
+ {
+ id: 1,
+ name: "Jean Dupont",
+ email: "jean.dupont@example.com",
+ tags: ["Frontend", "React", "Junior"],
+ projects: 2,
+ },
+ {
+ id: 2,
+ name: "Marie Martin",
+ email: "marie.martin@example.com",
+ tags: ["Backend", "Node.js", "Senior"],
+ projects: 3,
+ },
+ {
+ id: 3,
+ name: "Pierre Durand",
+ email: "pierre.durand@example.com",
+ tags: ["Fullstack", "JavaScript", "Medior"],
+ projects: 1,
+ },
+ {
+ id: 4,
+ name: "Sophie Lefebvre",
+ email: "sophie.lefebvre@example.com",
+ tags: ["UX/UI", "Design", "Senior"],
+ projects: 2,
+ },
+ {
+ id: 5,
+ name: "Thomas Bernard",
+ email: "thomas.bernard@example.com",
+ tags: ["Backend", "Java", "Senior"],
+ projects: 1,
+ },
+ {
+ id: 6,
+ name: "Julie Petit",
+ email: "julie.petit@example.com",
+ tags: ["Frontend", "Vue", "Junior"],
+ projects: 2,
+ },
+ {
+ id: 7,
+ name: "Nicolas Moreau",
+ email: "nicolas.moreau@example.com",
+ tags: ["DevOps", "Docker", "Medior"],
+ projects: 3,
+ },
+ ];
+
+ // Filter persons based on search query
+ const filteredPersons = persons.filter(
+ (person) =>
+ person.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ person.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ person.tags.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))
+ );
+
+ return (
+
+
+
Personnes
+
+
+
+ Nouvelle personne
+
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+
+
+
+
+ Nom
+ Email
+ Tags
+ Projets
+ Actions
+
+
+
+ {filteredPersons.length === 0 ? (
+
+
+ Aucune personne trouvée.
+
+
+ ) : (
+ filteredPersons.map((person) => (
+
+ {person.name}
+ {person.email}
+
+
+ {person.tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+ {person.projects}
+
+
+
+
+
+ Actions
+
+
+
+ Actions
+
+
+
+
+ Modifier
+
+
+
+
+
+ Gérer les tags
+
+
+
+
+ Supprimer
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/projects/[id]/edit/page.tsx b/frontend/app/projects/[id]/edit/page.tsx
new file mode 100644
index 0000000..c47672c
--- /dev/null
+++ b/frontend/app/projects/[id]/edit/page.tsx
@@ -0,0 +1,186 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useParams, useRouter } from "next/navigation";
+import Link from "next/link";
+import { useForm } from "react-hook-form";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ ArrowLeft,
+ Loader2,
+ Save
+} from "lucide-react";
+import { toast } from "sonner";
+
+// Type definitions
+interface ProjectFormData {
+ name: string;
+ description: string;
+}
+
+// Mock project data
+const getProjectData = (id: string) => {
+ return {
+ id: parseInt(id),
+ name: "Projet Formation Dev Web",
+ description: "Création de groupes pour la formation développement web",
+ date: "2025-05-15",
+ };
+};
+
+export default function EditProjectPage() {
+ const params = useParams();
+ const router = useRouter();
+ const projectId = params.id as string;
+
+ const [project, setProject] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const { register, handleSubmit, formState: { errors }, reset } = useForm();
+
+ useEffect(() => {
+ // Simulate API call to fetch project data
+ const fetchProject = async () => {
+ setLoading(true);
+ try {
+ // In a real app, this would be an API call
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ const data = getProjectData(projectId);
+ setProject(data);
+
+ // Reset form with project data
+ reset({
+ name: data.name,
+ description: data.description
+ });
+ } catch (error) {
+ console.error("Error fetching project:", error);
+ toast.error("Erreur lors du chargement du projet");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchProject();
+ }, [projectId, reset]);
+
+ const onSubmit = async (data: ProjectFormData) => {
+ setIsSubmitting(true);
+ try {
+ // In a real app, this would be an API call to update the project
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ toast.success("Projet mis à jour avec succès");
+ router.push(`/projects/${projectId}`);
+ } catch (error) {
+ console.error("Error updating project:", error);
+ toast.error("Erreur lors de la mise à jour du projet");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!project) {
+ return (
+
+
Projet non trouvé
+
+ Retour aux projets
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
Modifier le projet
+
+
+
+
+
+ Informations du projet
+
+ Modifiez les informations de votre projet
+
+
+
+
+
Nom du projet
+
+ {errors.name && (
+
{errors.name.message}
+ )}
+
+
+
+
Description
+
+ {errors.description && (
+
{errors.description.message}
+ )}
+
+
+
+
+ Annuler
+
+
+ {isSubmitting ? (
+ <>
+
+ Enregistrement...
+ >
+ ) : (
+ <>
+
+ Enregistrer les modifications
+ >
+ )}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/projects/[id]/groups/auto-create/page.tsx b/frontend/app/projects/[id]/groups/auto-create/page.tsx
new file mode 100644
index 0000000..16addea
--- /dev/null
+++ b/frontend/app/projects/[id]/groups/auto-create/page.tsx
@@ -0,0 +1,432 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useParams, useRouter } from "next/navigation";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Slider } from "@/components/ui/slider";
+import { Switch } from "@/components/ui/switch";
+import {
+ ArrowLeft,
+ Loader2,
+ Wand2,
+ Save,
+ RefreshCw
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+
+// Mock project data (same as in the groups page)
+const getProjectData = (id: string) => {
+ return {
+ id: parseInt(id),
+ name: "Projet Formation Dev Web",
+ description: "Création de groupes pour la formation développement web",
+ date: "2025-05-15",
+ persons: [
+ { id: 1, name: "Jean Dupont", tags: ["Frontend", "React", "Junior"] },
+ { id: 2, name: "Marie Martin", tags: ["Backend", "Node.js", "Senior"] },
+ { id: 3, name: "Pierre Durand", tags: ["Fullstack", "JavaScript", "Medior"] },
+ { id: 4, name: "Sophie Lefebvre", tags: ["UX/UI", "Design", "Senior"] },
+ { id: 5, name: "Thomas Bernard", tags: ["Backend", "Java", "Senior"] },
+ { id: 6, name: "Julie Petit", tags: ["Frontend", "Vue", "Junior"] },
+ { id: 7, name: "Nicolas Moreau", tags: ["DevOps", "Docker", "Medior"] },
+ { id: 8, name: "Emma Dubois", tags: ["Frontend", "Angular", "Junior"] },
+ { id: 9, name: "Lucas Leroy", tags: ["Backend", "Python", "Medior"] },
+ { id: 10, name: "Camille Roux", tags: ["Fullstack", "TypeScript", "Senior"] },
+ { id: 11, name: "Hugo Fournier", tags: ["Frontend", "React", "Medior"] },
+ { id: 12, name: "Léa Girard", tags: ["UX/UI", "Figma", "Junior"] },
+ { id: 13, name: "Mathis Bonnet", tags: ["Backend", "PHP", "Junior"] },
+ { id: 14, name: "Chloé Lambert", tags: ["Frontend", "CSS", "Senior"] },
+ { id: 15, name: "Nathan Mercier", tags: ["DevOps", "Kubernetes", "Senior"] },
+ { id: 16, name: "Zoé Faure", tags: ["Fullstack", "MERN", "Medior"] },
+ ]
+ };
+};
+
+// Type definitions
+interface Person {
+ id: number;
+ name: string;
+ tags: string[];
+}
+
+interface Group {
+ id: number;
+ name: string;
+ persons: Person[];
+}
+
+export default function AutoCreateGroupsPage() {
+ const params = useParams();
+ const router = useRouter();
+ const projectId = params.id as string;
+
+ const [project, setProject] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [generating, setGenerating] = useState(false);
+ const [saving, setSaving] = useState(false);
+
+ // State for auto-generation parameters
+ const [numberOfGroups, setNumberOfGroups] = useState(4);
+ const [balanceTags, setBalanceTags] = useState(true);
+ const [balanceLevels, setBalanceLevels] = useState(true);
+ const [groups, setGroups] = useState([]);
+ const [availableTags, setAvailableTags] = useState([]);
+ const [availableLevels, setAvailableLevels] = useState([]);
+
+ useEffect(() => {
+ // Simulate API call to fetch project data
+ const fetchProject = async () => {
+ setLoading(true);
+ try {
+ // In a real app, this would be an API call
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ const data = getProjectData(projectId);
+ setProject(data);
+
+ // Extract unique tags and levels
+ const tags = new Set();
+ const levels = new Set();
+
+ data.persons.forEach(person => {
+ person.tags.forEach(tag => {
+ // Assuming the last tag is the level (Junior, Medior, Senior)
+ if (["Junior", "Medior", "Senior"].includes(tag)) {
+ levels.add(tag);
+ } else {
+ tags.add(tag);
+ }
+ });
+ });
+
+ setAvailableTags(Array.from(tags));
+ setAvailableLevels(Array.from(levels));
+
+ } catch (error) {
+ console.error("Error fetching project:", error);
+ toast.error("Erreur lors du chargement du projet");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchProject();
+ }, [projectId]);
+
+ const generateGroups = async () => {
+ if (!project) return;
+
+ setGenerating(true);
+ try {
+ // In a real app, this would be an API call to the backend
+ // which would run the algorithm to create balanced groups
+ await new Promise(resolve => setTimeout(resolve, 1500));
+
+ // Simple algorithm to create balanced groups
+ const persons = [...project.persons];
+ const newGroups: Group[] = [];
+
+ // Create empty groups
+ for (let i = 0; i < numberOfGroups; i++) {
+ newGroups.push({
+ id: i + 1,
+ name: `Groupe ${String.fromCharCode(65 + i)}`, // A, B, C, ...
+ persons: []
+ });
+ }
+
+ // Sort persons by level if balancing levels
+ if (balanceLevels) {
+ persons.sort((a, b) => {
+ const aLevel = a.tags.find((tag: string) => ["Junior", "Medior", "Senior"].includes(tag)) || "";
+ const bLevel = b.tags.find((tag: string) => ["Junior", "Medior", "Senior"].includes(tag)) || "";
+
+ // Order: Senior, Medior, Junior
+ const levelOrder: Record = { "Senior": 0, "Medior": 1, "Junior": 2 };
+ return levelOrder[aLevel] - levelOrder[bLevel];
+ });
+ }
+
+ // Sort persons by tags if balancing tags
+ if (balanceTags) {
+ // Group persons by their primary skill tag
+ const personsByTag: Record = {};
+
+ persons.forEach(person => {
+ // Get first tag that's not a level
+ const primaryTag = person.tags.find((tag: string) => !["Junior", "Medior", "Senior"].includes(tag));
+ if (primaryTag) {
+ if (!personsByTag[primaryTag]) {
+ personsByTag[primaryTag] = [];
+ }
+ personsByTag[primaryTag].push(person);
+ }
+ });
+
+ // Distribute persons from each tag group evenly
+ let currentGroupIndex = 0;
+
+ Object.values(personsByTag).forEach(tagPersons => {
+ tagPersons.forEach(person => {
+ newGroups[currentGroupIndex].persons.push(person);
+ currentGroupIndex = (currentGroupIndex + 1) % numberOfGroups;
+ });
+ });
+ } else {
+ // Simple distribution without balancing tags
+ persons.forEach((person, index) => {
+ const groupIndex = index % numberOfGroups;
+ newGroups[groupIndex].persons.push(person);
+ });
+ }
+
+ setGroups(newGroups);
+ toast.success("Groupes générés avec succès");
+ } catch (error) {
+ console.error("Error generating groups:", error);
+ toast.error("Erreur lors de la génération des groupes");
+ } finally {
+ setGenerating(false);
+ }
+ };
+
+ const handleSaveGroups = async () => {
+ if (groups.length === 0) {
+ toast.error("Veuillez d'abord générer des groupes");
+ return;
+ }
+
+ setSaving(true);
+ try {
+ // In a real app, this would be an API call to save the groups
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ toast.success("Groupes enregistrés avec succès");
+
+ // Navigate back to the groups page
+ router.push(`/projects/${projectId}/groups`);
+ } catch (error) {
+ console.error("Error saving groups:", error);
+ toast.error("Erreur lors de l'enregistrement des groupes");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!project) {
+ return (
+
+
Projet non trouvé
+
+ Retour aux projets
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
Assistant de création de groupes
+
+
+ {saving ? (
+ <>
+
+ Enregistrement...
+ >
+ ) : (
+ <>
+
+ Enregistrer les groupes
+ >
+ )}
+
+
+
+
+ {/* Parameters */}
+
+
+ Paramètres
+
+ Configurez les paramètres pour la génération automatique de groupes
+
+
+
+
+
Nombre de groupes: {numberOfGroups}
+
setNumberOfGroups(value[0])}
+ />
+
+ {Math.ceil(project.persons.length / numberOfGroups)} personnes par groupe en moyenne
+
+
+
+
+
+
+
Équilibrer les compétences
+
+ Répartir équitablement les compétences dans chaque groupe
+
+
+
+
+
+
+
+
Équilibrer les niveaux
+
+ Répartir équitablement les niveaux (Junior, Medior, Senior) dans chaque groupe
+
+
+
+
+
+
+
+
Compétences disponibles
+
+ {availableTags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+
+
+
Niveaux disponibles
+
+ {availableLevels.map((level, index) => (
+
+ {level}
+
+ ))}
+
+
+
+
+
+ {generating ? (
+ <>
+
+ Génération en cours...
+ >
+ ) : (
+ <>
+
+ Générer les groupes
+ >
+ )}
+
+
+
+
+ {/* Generated Groups */}
+
+
+
+ Groupes générés
+
+ {groups.length > 0
+ ? `${groups.length} groupes avec ${project.persons.length} personnes au total`
+ : "Utilisez les paramètres à gauche pour générer des groupes"}
+
+
+
+ {groups.length === 0 ? (
+
+
+
+ Aucun groupe généré. Cliquez sur "Générer les groupes" pour commencer.
+
+
+ ) : (
+
+ {groups.map((group) => (
+
+
+ {group.name}
+
+ {group.persons.length} personnes
+
+
+
+
+ {group.persons.map((person) => (
+
+
+
{person.name}
+
+ {person.tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+
+ ))}
+
+
+
+ ))}
+
+ )}
+
+ {groups.length > 0 && (
+
+
+
+ Régénérer les groupes
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/app/projects/[id]/groups/create/page.tsx b/frontend/app/projects/[id]/groups/create/page.tsx
new file mode 100644
index 0000000..d4cc592
--- /dev/null
+++ b/frontend/app/projects/[id]/groups/create/page.tsx
@@ -0,0 +1,379 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useParams, useRouter } from "next/navigation";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ ArrowLeft,
+ Loader2,
+ Save,
+ Plus,
+ X
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+
+// Mock project data (same as in the groups page)
+const getProjectData = (id: string) => {
+ return {
+ id: parseInt(id),
+ name: "Projet Formation Dev Web",
+ description: "Création de groupes pour la formation développement web",
+ date: "2025-05-15",
+ persons: [
+ { id: 1, name: "Jean Dupont", tags: ["Frontend", "React", "Junior"] },
+ { id: 2, name: "Marie Martin", tags: ["Backend", "Node.js", "Senior"] },
+ { id: 3, name: "Pierre Durand", tags: ["Fullstack", "JavaScript", "Medior"] },
+ { id: 4, name: "Sophie Lefebvre", tags: ["UX/UI", "Design", "Senior"] },
+ { id: 5, name: "Thomas Bernard", tags: ["Backend", "Java", "Senior"] },
+ { id: 6, name: "Julie Petit", tags: ["Frontend", "Vue", "Junior"] },
+ { id: 7, name: "Nicolas Moreau", tags: ["DevOps", "Docker", "Medior"] },
+ { id: 8, name: "Emma Dubois", tags: ["Frontend", "Angular", "Junior"] },
+ { id: 9, name: "Lucas Leroy", tags: ["Backend", "Python", "Medior"] },
+ { id: 10, name: "Camille Roux", tags: ["Fullstack", "TypeScript", "Senior"] },
+ { id: 11, name: "Hugo Fournier", tags: ["Frontend", "React", "Medior"] },
+ { id: 12, name: "Léa Girard", tags: ["UX/UI", "Figma", "Junior"] },
+ { id: 13, name: "Mathis Bonnet", tags: ["Backend", "PHP", "Junior"] },
+ { id: 14, name: "Chloé Lambert", tags: ["Frontend", "CSS", "Senior"] },
+ { id: 15, name: "Nathan Mercier", tags: ["DevOps", "Kubernetes", "Senior"] },
+ { id: 16, name: "Zoé Faure", tags: ["Fullstack", "MERN", "Medior"] },
+ ]
+ };
+};
+
+// Type definitions
+interface Person {
+ id: number;
+ name: string;
+ tags: string[];
+}
+
+interface Group {
+ id: number;
+ name: string;
+ persons: Person[];
+}
+
+export default function CreateGroupsPage() {
+ const params = useParams();
+ const router = useRouter();
+ const projectId = params.id as string;
+
+ const [project, setProject] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+
+ // State for groups and available persons
+ const [groups, setGroups] = useState([]);
+ const [availablePersons, setAvailablePersons] = useState([]);
+ const [newGroupName, setNewGroupName] = useState("");
+
+ // State for drag and drop
+ const [draggedPerson, setDraggedPerson] = useState(null);
+ const [draggedFromGroup, setDraggedFromGroup] = useState(null);
+ const [dragOverGroup, setDragOverGroup] = useState(null);
+
+ useEffect(() => {
+ // Simulate API call to fetch project data
+ const fetchProject = async () => {
+ setLoading(true);
+ try {
+ // In a real app, this would be an API call
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ const data = getProjectData(projectId);
+ setProject(data);
+ setAvailablePersons(data.persons);
+ } catch (error) {
+ console.error("Error fetching project:", error);
+ toast.error("Erreur lors du chargement du projet");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchProject();
+ }, [projectId]);
+
+ const handleAddGroup = () => {
+ if (!newGroupName.trim()) {
+ toast.error("Veuillez entrer un nom de groupe");
+ return;
+ }
+
+ const newGroup: Group = {
+ id: Date.now(), // Use timestamp as temporary ID
+ name: newGroupName,
+ persons: []
+ };
+
+ setGroups([...groups, newGroup]);
+ setNewGroupName("");
+ };
+
+ const handleRemoveGroup = (groupId: number) => {
+ const group = groups.find(g => g.id === groupId);
+ if (group) {
+ // Return persons from this group to available persons
+ setAvailablePersons([...availablePersons, ...group.persons]);
+ }
+ setGroups(groups.filter(g => g.id !== groupId));
+ };
+
+ const handleDragStart = (person: Person, fromGroup: number | null) => {
+ setDraggedPerson(person);
+ setDraggedFromGroup(fromGroup);
+ };
+
+ const handleDragOver = (e: React.DragEvent, toGroup: number | null) => {
+ e.preventDefault();
+ setDragOverGroup(toGroup);
+ };
+
+ const handleDrop = (e: React.DragEvent, toGroup: number | null) => {
+ e.preventDefault();
+
+ if (!draggedPerson) return;
+
+ // Remove person from source
+ if (draggedFromGroup === null) {
+ // From available persons
+ setAvailablePersons(availablePersons.filter(p => p.id !== draggedPerson.id));
+ } else {
+ // From another group
+ const sourceGroup = groups.find(g => g.id === draggedFromGroup);
+ if (sourceGroup) {
+ const updatedGroups = groups.map(g => {
+ if (g.id === draggedFromGroup) {
+ return {
+ ...g,
+ persons: g.persons.filter(p => p.id !== draggedPerson.id)
+ };
+ }
+ return g;
+ });
+ setGroups(updatedGroups);
+ }
+ }
+
+ // Add person to destination
+ if (toGroup === null) {
+ // To available persons
+ setAvailablePersons([...availablePersons, draggedPerson]);
+ } else {
+ // To a group
+ const updatedGroups = groups.map(g => {
+ if (g.id === toGroup) {
+ return {
+ ...g,
+ persons: [...g.persons, draggedPerson]
+ };
+ }
+ return g;
+ });
+ setGroups(updatedGroups);
+ }
+
+ // Reset drag state
+ setDraggedPerson(null);
+ setDraggedFromGroup(null);
+ setDragOverGroup(null);
+ };
+
+ const handleSaveGroups = async () => {
+ setSaving(true);
+ try {
+ // Validate that all groups have at least one person
+ const emptyGroups = groups.filter(g => g.persons.length === 0);
+ if (emptyGroups.length > 0) {
+ toast.error(`${emptyGroups.length} groupe(s) vide(s). Veuillez ajouter des personnes à tous les groupes.`);
+ setSaving(false);
+ return;
+ }
+
+ // In a real app, this would be an API call to save the groups
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ toast.success("Groupes enregistrés avec succès");
+
+ // Navigate back to the groups page
+ router.push(`/projects/${projectId}/groups`);
+ } catch (error) {
+ console.error("Error saving groups:", error);
+ toast.error("Erreur lors de l'enregistrement des groupes");
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!project) {
+ return (
+
+
Projet non trouvé
+
+ Retour aux projets
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
Créer des groupes
+
+
+ {saving ? (
+ <>
+
+ Enregistrement...
+ >
+ ) : (
+ <>
+
+ Enregistrer les groupes
+ >
+ )}
+
+
+
+
+ {/* Available persons */}
+
handleDragOver(e, null)}
+ onDrop={(e) => handleDrop(e, null)}
+ >
+
Personnes disponibles ({availablePersons.length})
+
+ {availablePersons.map(person => (
+
handleDragStart(person, null)}
+ >
+
{person.name}
+
+ {person.tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+ ))}
+ {availablePersons.length === 0 && (
+
+ Toutes les personnes ont été assignées à des groupes
+
+ )}
+
+
+
+ {/* Groups */}
+
+ {/* Add new group form */}
+
+
+ Ajouter un nouveau groupe
+
+
+
+
+ Nom du groupe
+ setNewGroupName(e.target.value)}
+ />
+
+
+
+ Ajouter
+
+
+
+
+
+ {/* Groups list */}
+ {groups.length === 0 ? (
+
+
+
+ Aucun groupe créé. Commencez par ajouter un groupe.
+
+
+
+ ) : (
+
+ {groups.map(group => (
+
handleDragOver(e, group.id)}
+ onDrop={(e) => handleDrop(e, group.id)}
+ >
+
+ {group.name}
+ handleRemoveGroup(group.id)}
+ className="h-8 w-8 text-destructive"
+ >
+
+
+
+
+
+ {group.persons.map(person => (
+
handleDragStart(person, group.id)}
+ >
+
{person.name}
+
+ {person.tags.map((tag, index) => (
+
+ {tag}
+
+ ))}
+
+
+ ))}
+ {group.persons.length === 0 && (
+
+ Glissez-déposez des personnes ici
+
+ )}
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/projects/[id]/groups/page.tsx b/frontend/app/projects/[id]/groups/page.tsx
new file mode 100644
index 0000000..5d87f22
--- /dev/null
+++ b/frontend/app/projects/[id]/groups/page.tsx
@@ -0,0 +1,256 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useParams } from "next/navigation";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ PlusCircle,
+ Users,
+ Wand2,
+ ArrowLeft,
+ Loader2
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+
+// Mock project data
+const getProjectData = (id: string) => {
+ return {
+ id: parseInt(id),
+ name: "Projet Formation Dev Web",
+ description: "Création de groupes pour la formation développement web",
+ date: "2025-05-15",
+ groups: [
+ {
+ id: 1,
+ name: "Groupe A",
+ persons: [
+ { id: 1, name: "Jean Dupont", tags: ["Frontend", "React", "Junior"] },
+ { id: 2, name: "Marie Martin", tags: ["Backend", "Node.js", "Senior"] },
+ { id: 3, name: "Pierre Durand", tags: ["Fullstack", "JavaScript", "Medior"] },
+ { id: 4, name: "Sophie Lefebvre", tags: ["UX/UI", "Design", "Senior"] },
+ ]
+ },
+ {
+ id: 2,
+ name: "Groupe B",
+ persons: [
+ { id: 5, name: "Thomas Bernard", tags: ["Backend", "Java", "Senior"] },
+ { id: 6, name: "Julie Petit", tags: ["Frontend", "Vue", "Junior"] },
+ { id: 7, name: "Nicolas Moreau", tags: ["DevOps", "Docker", "Medior"] },
+ { id: 8, name: "Emma Dubois", tags: ["Frontend", "Angular", "Junior"] },
+ ]
+ },
+ {
+ id: 3,
+ name: "Groupe C",
+ persons: [
+ { id: 9, name: "Lucas Leroy", tags: ["Backend", "Python", "Medior"] },
+ { id: 10, name: "Camille Roux", tags: ["Fullstack", "TypeScript", "Senior"] },
+ { id: 11, name: "Hugo Fournier", tags: ["Frontend", "React", "Medior"] },
+ { id: 12, name: "Léa Girard", tags: ["UX/UI", "Figma", "Junior"] },
+ ]
+ },
+ {
+ id: 4,
+ name: "Groupe D",
+ persons: [
+ { id: 13, name: "Mathis Bonnet", tags: ["Backend", "PHP", "Junior"] },
+ { id: 14, name: "Chloé Lambert", tags: ["Frontend", "CSS", "Senior"] },
+ { id: 15, name: "Nathan Mercier", tags: ["DevOps", "Kubernetes", "Senior"] },
+ { id: 16, name: "Zoé Faure", tags: ["Fullstack", "MERN", "Medior"] },
+ ]
+ }
+ ],
+ persons: [
+ { id: 1, name: "Jean Dupont", tags: ["Frontend", "React", "Junior"] },
+ { id: 2, name: "Marie Martin", tags: ["Backend", "Node.js", "Senior"] },
+ { id: 3, name: "Pierre Durand", tags: ["Fullstack", "JavaScript", "Medior"] },
+ { id: 4, name: "Sophie Lefebvre", tags: ["UX/UI", "Design", "Senior"] },
+ { id: 5, name: "Thomas Bernard", tags: ["Backend", "Java", "Senior"] },
+ { id: 6, name: "Julie Petit", tags: ["Frontend", "Vue", "Junior"] },
+ { id: 7, name: "Nicolas Moreau", tags: ["DevOps", "Docker", "Medior"] },
+ { id: 8, name: "Emma Dubois", tags: ["Frontend", "Angular", "Junior"] },
+ { id: 9, name: "Lucas Leroy", tags: ["Backend", "Python", "Medior"] },
+ { id: 10, name: "Camille Roux", tags: ["Fullstack", "TypeScript", "Senior"] },
+ { id: 11, name: "Hugo Fournier", tags: ["Frontend", "React", "Medior"] },
+ { id: 12, name: "Léa Girard", tags: ["UX/UI", "Figma", "Junior"] },
+ { id: 13, name: "Mathis Bonnet", tags: ["Backend", "PHP", "Junior"] },
+ { id: 14, name: "Chloé Lambert", tags: ["Frontend", "CSS", "Senior"] },
+ { id: 15, name: "Nathan Mercier", tags: ["DevOps", "Kubernetes", "Senior"] },
+ { id: 16, name: "Zoé Faure", tags: ["Fullstack", "MERN", "Medior"] },
+ ]
+ };
+};
+
+export default function ProjectGroupsPage() {
+ const params = useParams();
+ const projectId = params.id as string;
+ const [project, setProject] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState("existing");
+
+ useEffect(() => {
+ // Simulate API call to fetch project data
+ const fetchProject = async () => {
+ setLoading(true);
+ try {
+ // In a real app, this would be an API call
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ const data = getProjectData(projectId);
+ setProject(data);
+ } catch (error) {
+ console.error("Error fetching project:", error);
+ toast.error("Erreur lors du chargement du projet");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchProject();
+ }, [projectId]);
+
+ const handleCreateGroups = async () => {
+ toast.success("Redirection vers la page de création de groupes");
+ // In a real app, this would redirect to the group creation page
+ };
+
+ const handleAutoCreateGroups = async () => {
+ toast.success("Redirection vers l'assistant de création automatique de groupes");
+ // In a real app, this would redirect to the automatic group creation page
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!project) {
+ return (
+
+
Projet non trouvé
+
+ Retour aux projets
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
{project.name} - Groupes
+
+
+
+
+ Groupes existants
+ Créer des groupes
+
+
+
+ {project.groups.length === 0 ? (
+
+
+ Aucun groupe
+
+ Ce projet ne contient pas encore de groupes. Créez-en un maintenant.
+
+
+
+
+
+ Créer un groupe
+
+
+
+ ) : (
+
+ {project.groups.map((group: any) => (
+
+
+ {group.name}
+
+ {group.persons.length} personnes
+
+
+
+
+ {group.persons.map((person: any) => (
+
+
+
{person.name}
+
+ {person.tags.map((tag: string, index: number) => (
+
+ {tag}
+
+ ))}
+
+
+
+ ))}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ Création manuelle
+
+ Créez des groupes manuellement en glissant-déposant les personnes
+
+
+
+
+
+ Utilisez l'interface de glisser-déposer pour créer vos groupes selon vos critères
+
+
+
+ Créer manuellement
+
+
+
+
+
+
+ Création automatique
+
+ Laissez l'assistant créer des groupes équilibrés automatiquement
+
+
+
+
+
+ L'assistant prendra en compte les tags et niveaux pour créer des groupes équilibrés
+
+
+
+ Utiliser l'assistant
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/projects/layout.tsx b/frontend/app/projects/layout.tsx
new file mode 100644
index 0000000..f1872a7
--- /dev/null
+++ b/frontend/app/projects/layout.tsx
@@ -0,0 +1,10 @@
+import { DashboardLayout } from "@/components/dashboard-layout";
+import { AuthLoading } from "@/components/auth-loading";
+
+export default function ProjectsLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/app/projects/new/page.tsx b/frontend/app/projects/new/page.tsx
new file mode 100644
index 0000000..44c78d6
--- /dev/null
+++ b/frontend/app/projects/new/page.tsx
@@ -0,0 +1,134 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import { useForm } from "react-hook-form";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ ArrowLeft,
+ Loader2,
+ Save
+} from "lucide-react";
+import { toast } from "sonner";
+
+// Type definitions
+interface ProjectFormData {
+ name: string;
+ description: string;
+}
+
+export default function NewProjectPage() {
+ const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const { register, handleSubmit, formState: { errors } } = useForm({
+ defaultValues: {
+ name: "",
+ description: ""
+ }
+ });
+
+ const onSubmit = async (data: ProjectFormData) => {
+ setIsSubmitting(true);
+ try {
+ // In a real app, this would be an API call to create a new project
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Simulate a successful response with a project ID
+ const projectId = Date.now();
+
+ toast.success("Projet créé avec succès");
+ router.push(`/projects/${projectId}`);
+ } catch (error) {
+ console.error("Error creating project:", error);
+ toast.error("Erreur lors de la création du projet");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
Nouveau projet
+
+
+
+
+
+ Informations du projet
+
+ Créez un nouveau projet pour organiser vos groupes
+
+
+
+
+
Nom du projet
+
+ {errors.name && (
+
{errors.name.message}
+ )}
+
+
+
+
Description
+
+ {errors.description && (
+
{errors.description.message}
+ )}
+
+
+
+
+ Annuler
+
+
+ {isSubmitting ? (
+ <>
+
+ Création en cours...
+ >
+ ) : (
+ <>
+
+ Créer le projet
+ >
+ )}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/projects/page.tsx b/frontend/app/projects/page.tsx
new file mode 100644
index 0000000..1509957
--- /dev/null
+++ b/frontend/app/projects/page.tsx
@@ -0,0 +1,262 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow
+} from "@/components/ui/table";
+import {
+ Card,
+ CardHeader,
+ CardTitle,
+ CardDescription,
+ CardContent,
+ CardFooter
+} from "@/components/ui/card";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from "@/components/ui/dropdown-menu";
+import {
+ PlusCircle,
+ Search,
+ MoreHorizontal,
+ Pencil,
+ Trash2,
+ Users,
+ Eye
+} from "lucide-react";
+
+export default function ProjectsPage() {
+ const [searchQuery, setSearchQuery] = useState("");
+
+ // Mock data for projects
+ const projects = [
+ {
+ id: 1,
+ name: "Projet Formation Dev Web",
+ description: "Création de groupes pour la formation développement web",
+ date: "2025-05-15",
+ groups: 4,
+ persons: 16,
+ },
+ {
+ id: 2,
+ name: "Projet Hackathon",
+ description: "Équipes pour le hackathon annuel",
+ date: "2025-05-10",
+ groups: 8,
+ persons: 32,
+ },
+ {
+ id: 3,
+ name: "Projet Workshop UX/UI",
+ description: "Groupes pour l'atelier UX/UI",
+ date: "2025-05-05",
+ groups: 5,
+ persons: 20,
+ },
+ {
+ id: 4,
+ name: "Projet Conférence Tech",
+ description: "Groupes pour la conférence technologique",
+ date: "2025-04-28",
+ groups: 6,
+ persons: 24,
+ },
+ {
+ id: 5,
+ name: "Projet Formation Data Science",
+ description: "Création de groupes pour la formation data science",
+ date: "2025-04-20",
+ groups: 3,
+ persons: 12,
+ },
+ ];
+
+ // Filter projects based on search query
+ const filteredProjects = projects.filter(
+ (project) =>
+ project.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ project.description.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+
+ return (
+
+
+
Projets
+
+
+
+ Nouveau projet
+
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+ {/* Mobile card view */}
+
+ {filteredProjects.length === 0 ? (
+
+ Aucun projet trouvé.
+
+ ) : (
+ filteredProjects.map((project) => (
+
+
+ {project.name}
+ {project.description}
+
+
+
+
+ Date
+ {new Date(project.date).toLocaleDateString("fr-FR")}
+
+
+ Groupes
+ {project.groups}
+
+
+ Personnes
+ {project.persons}
+
+
+
+
+
+
+
+ Voir
+
+
+
+
+
+
+ Actions
+
+
+
+ Actions
+
+
+
+
+ Gérer les groupes
+
+
+
+
+
+ Modifier
+
+
+
+
+ Supprimer
+
+
+
+
+
+ ))
+ )}
+
+
+ {/* Desktop table view */}
+
+
+
+
+ Nom
+ Description
+ Date de création
+ Groupes
+ Personnes
+ Actions
+
+
+
+ {filteredProjects.length === 0 ? (
+
+
+ Aucun projet trouvé.
+
+
+ ) : (
+ filteredProjects.map((project) => (
+
+ {project.name}
+ {project.description}
+ {new Date(project.date).toLocaleDateString("fr-FR")}
+ {project.groups}
+ {project.persons}
+
+
+
+
+
+ Actions
+
+
+
+ Actions
+
+
+
+
+ Voir
+
+
+
+
+
+ Gérer les groupes
+
+
+
+
+
+ Modifier
+
+
+
+
+ Supprimer
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/app/settings/layout.tsx b/frontend/app/settings/layout.tsx
new file mode 100644
index 0000000..0442d4f
--- /dev/null
+++ b/frontend/app/settings/layout.tsx
@@ -0,0 +1,10 @@
+import { DashboardLayout } from "@/components/dashboard-layout";
+import { AuthLoading } from "@/components/auth-loading";
+
+export default function SettingsLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/app/settings/page.tsx b/frontend/app/settings/page.tsx
new file mode 100644
index 0000000..191a46b
--- /dev/null
+++ b/frontend/app/settings/page.tsx
@@ -0,0 +1,268 @@
+"use client";
+
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Separator } from "@/components/ui/separator";
+import { Switch } from "@/components/ui/switch";
+import { Textarea } from "@/components/ui/textarea";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { toast } from "sonner";
+
+export default function SettingsPage() {
+ const [activeTab, setActiveTab] = useState("profile");
+ const [isLoading, setIsLoading] = useState(false);
+
+ // Mock user data
+ const user = {
+ name: "Jean Dupont",
+ email: "jean.dupont@example.com",
+ avatar: "",
+ bio: "Développeur frontend passionné par les interfaces utilisateur et l'expérience utilisateur.",
+ notifications: {
+ email: true,
+ push: false,
+ projectUpdates: true,
+ groupChanges: true,
+ newMembers: false,
+ },
+ };
+
+ const { register, handleSubmit, formState: { errors } } = useForm({
+ defaultValues: {
+ name: user.name,
+ email: user.email,
+ bio: user.bio,
+ },
+ });
+
+ const onSubmitProfile = async (data: any) => {
+ setIsLoading(true);
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ setIsLoading(false);
+ toast.success("Profil mis à jour avec succès");
+ };
+
+ const onSubmitNotifications = async (data: any) => {
+ setIsLoading(true);
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ setIsLoading(false);
+ toast.success("Préférences de notification mises à jour avec succès");
+ };
+
+ const onExportData = async () => {
+ setIsLoading(true);
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ setIsLoading(false);
+ toast.success("Vos données ont été exportées. Vous recevrez un email avec le lien de téléchargement.");
+ };
+
+ const onDeleteAccount = async () => {
+ setIsLoading(true);
+ // Simulate API call
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ setIsLoading(false);
+ toast.success("Votre compte a été supprimé avec succès.");
+ };
+
+ return (
+
+
+
Paramètres
+
+
+
+
+ Profil
+ Notifications
+ Confidentialité
+
+
+
+
+
+ Profil
+
+ Gérez vos informations personnelles et votre profil.
+
+
+
+
+
+
+ {user.name.split(" ").map(n => n[0]).join("")}
+
+
+
+ Changer d'avatar
+
+
+
+
+
+
+
Nom
+
+ {errors.name && (
+
{errors.name.message}
+ )}
+
+
+
Email
+
+ {errors.email && (
+
{errors.email.message}
+ )}
+
+
+ Bio
+
+
+
+ {isLoading ? "Enregistrement..." : "Enregistrer les modifications"}
+
+
+
+
+
+
+
+
+
+ Notifications
+
+ Configurez vos préférences de notification.
+
+
+
+
+
+
+
Notifications par email
+
+ Recevez des notifications par email.
+
+
+
+
+
+
+
+
Notifications push
+
+ Recevez des notifications push dans votre navigateur.
+
+
+
+
+
+
+
+
Mises à jour de projets
+
+ Soyez notifié des mises à jour de vos projets.
+
+
+
+
+
+
+
+
Changements de groupes
+
+ Soyez notifié des changements dans vos groupes.
+
+
+
+
+
+
+
+
Nouveaux membres
+
+ Soyez notifié lorsque de nouveaux membres rejoignent vos projets.
+
+
+
+
+
+
+
+
+ {isLoading ? "Enregistrement..." : "Enregistrer les préférences"}
+
+
+
+
+
+
+
+
+ Confidentialité et données
+
+ Gérez vos données personnelles et vos paramètres de confidentialité.
+
+
+
+
+
+
Exporter vos données
+
+ Téléchargez une copie de vos données personnelles.
+
+
+ {isLoading ? "Exportation..." : "Exporter mes données"}
+
+
+
+
+
Supprimer votre compte
+
+ Supprimez définitivement votre compte et toutes vos données.
+
+
+ {isLoading ? "Suppression..." : "Supprimer mon compte"}
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/tags/[id]/edit/page.tsx b/frontend/app/tags/[id]/edit/page.tsx
new file mode 100644
index 0000000..d9d302e
--- /dev/null
+++ b/frontend/app/tags/[id]/edit/page.tsx
@@ -0,0 +1,277 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useParams, useRouter } from "next/navigation";
+import Link from "next/link";
+import { useForm, Controller } from "react-hook-form";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ ArrowLeft,
+ Loader2,
+ Save,
+ CircleDot
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+// Type definitions
+interface TagFormData {
+ name: string;
+ description: string;
+ color: string;
+}
+
+// Available colors
+const colors = [
+ { name: "Bleu", value: "blue" },
+ { name: "Vert", value: "green" },
+ { name: "Violet", value: "purple" },
+ { name: "Rose", value: "pink" },
+ { name: "Orange", value: "orange" },
+ { name: "Jaune", value: "yellow" },
+ { name: "Ambre", value: "amber" },
+ { name: "Rouge", value: "red" },
+];
+
+// Map color names to Tailwind classes
+const colorMap: Record = {
+ blue: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
+ green: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
+ purple: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
+ pink: "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300",
+ orange: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300",
+ yellow: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
+ amber: "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300",
+ red: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
+};
+
+// Mock tag data
+const getTagData = (id: string) => {
+ return {
+ id: parseInt(id),
+ name: "Frontend",
+ description: "Développement frontend",
+ color: "blue",
+ persons: 12,
+ };
+};
+
+export default function EditTagPage() {
+ const params = useParams();
+ const router = useRouter();
+ const tagId = params.id as string;
+
+ const [tag, setTag] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const { register, handleSubmit, control, watch, formState: { errors }, reset } = useForm();
+
+ const selectedColor = watch("color");
+
+ useEffect(() => {
+ // Simulate API call to fetch tag data
+ const fetchTag = async () => {
+ setLoading(true);
+ try {
+ // In a real app, this would be an API call
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ const data = getTagData(tagId);
+ setTag(data);
+
+ // Reset form with tag data
+ reset({
+ name: data.name,
+ description: data.description,
+ color: data.color
+ });
+ } catch (error) {
+ console.error("Error fetching tag:", error);
+ toast.error("Erreur lors du chargement du tag");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchTag();
+ }, [tagId, reset]);
+
+ const onSubmit = async (data: TagFormData) => {
+ setIsSubmitting(true);
+ try {
+ // In a real app, this would be an API call to update the tag
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ toast.success("Tag mis à jour avec succès");
+ router.push("/tags");
+ } catch (error) {
+ console.error("Error updating tag:", error);
+ toast.error("Erreur lors de la mise à jour du tag");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!tag) {
+ return (
+
+
Tag non trouvé
+
+ Retour aux tags
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
Modifier le tag
+
+
+
+
+
+ Informations du tag
+
+ Modifiez les informations du tag
+
+
+
+
+
Nom du tag
+
+ {errors.name && (
+
{errors.name.message}
+ )}
+
+
+
+
Description
+
+ {errors.description && (
+
{errors.description.message}
+ )}
+
+
+
+
Couleur
+
(
+
+
+
+
+
+ {colors.map((color) => (
+
+
+
+ {color.name}
+
+
+ ))}
+
+
+ )}
+ />
+ {errors.color && (
+ {errors.color.message}
+ )}
+
+
+
+
Aperçu
+
+
+ {watch("name") || tag.name}
+
+
+ {watch("description") || tag.description}
+
+
+
+
+ {tag.persons > 0 && (
+
+
+ Ce tag est utilisé par {tag.persons} personne{tag.persons > 1 ? 's' : ''}.
+ La modification du tag affectera toutes ces personnes.
+
+
+ )}
+
+
+
+ Annuler
+
+
+ {isSubmitting ? (
+ <>
+
+ Enregistrement...
+ >
+ ) : (
+ <>
+
+ Enregistrer les modifications
+ >
+ )}
+
+
+
+
+
+ );
+}
diff --git a/frontend/app/tags/demo/page.tsx b/frontend/app/tags/demo/page.tsx
new file mode 100644
index 0000000..044e945
--- /dev/null
+++ b/frontend/app/tags/demo/page.tsx
@@ -0,0 +1,172 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { ArrowLeft } from "lucide-react";
+import { TagSelector, Tag } from "@/components/tag-selector";
+import { Label } from "@/components/ui/label";
+
+export default function TagSelectorDemoPage() {
+ const [selectedTags, setSelectedTags] = useState([]);
+
+ return (
+
+
+
+
+
+
+
+
Démo du Sélecteur de Tags
+
+
+
+
+
+ Sélecteur de Tags
+
+ Un composant réutilisable pour sélectionner plusieurs tags
+
+
+
+
+ Tags
+
+
+
+
+
+ {selectedTags.length > 0
+ ? `${selectedTags.length} tag${selectedTags.length > 1 ? "s" : ""} sélectionné${selectedTags.length > 1 ? "s" : ""}`
+ : "Aucun tag sélectionné"}
+
+
+
+
+
+
+ Utilisation dans un formulaire
+
+ Comment utiliser le sélecteur de tags dans un formulaire
+
+
+
+
+
+{`// Importer le composant
+import { TagSelector, Tag } from "@/components/tag-selector";
+
+// Définir l'état pour les tags sélectionnés
+const [selectedTags, setSelectedTags] = useState([]);
+
+// Utiliser le composant dans le formulaire
+
+ Tags
+
+
+
+// Accéder aux tags sélectionnés
+console.log(selectedTags);
+`}
+
+
+
+
+
+
Tags sélectionnés (données)
+
+
+ {JSON.stringify(selectedTags, null, 2)}
+
+
+
+
+
+
+
+
+
+ Exemple d'intégration
+
+ Comment le sélecteur de tags peut être intégré dans différents formulaires
+
+
+
+
+
Formulaire de création de personne
+
+ Le sélecteur de tags peut être utilisé pour attribuer des compétences ou des caractéristiques à une personne.
+
+
+
+{`// Dans le formulaire de création de personne
+
+
+ Nom
+
+
+
+
+ Email
+
+
+
+
+ Compétences / Caractéristiques
+
+
+
`}
+
+
+
+
+
+
Formulaire de création de projet
+
+ Le sélecteur de tags peut être utilisé pour catégoriser un projet.
+
+
+
+{`// Dans le formulaire de création de projet
+
+
+ Nom du projet
+
+
+
+
+ Description
+
+
+
+
+ Catégories
+
+
+
`}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/tags/layout.tsx b/frontend/app/tags/layout.tsx
new file mode 100644
index 0000000..4d4ba89
--- /dev/null
+++ b/frontend/app/tags/layout.tsx
@@ -0,0 +1,10 @@
+import { DashboardLayout } from "@/components/dashboard-layout";
+import { AuthLoading } from "@/components/auth-loading";
+
+export default function TagsLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/app/tags/new/page.tsx b/frontend/app/tags/new/page.tsx
new file mode 100644
index 0000000..e4881e0
--- /dev/null
+++ b/frontend/app/tags/new/page.tsx
@@ -0,0 +1,215 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import { useForm, Controller } from "react-hook-form";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ ArrowLeft,
+ Loader2,
+ Save,
+ CircleDot
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { toast } from "sonner";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+// Type definitions
+interface TagFormData {
+ name: string;
+ description: string;
+ color: string;
+}
+
+// Available colors
+const colors = [
+ { name: "Bleu", value: "blue" },
+ { name: "Vert", value: "green" },
+ { name: "Violet", value: "purple" },
+ { name: "Rose", value: "pink" },
+ { name: "Orange", value: "orange" },
+ { name: "Jaune", value: "yellow" },
+ { name: "Ambre", value: "amber" },
+ { name: "Rouge", value: "red" },
+];
+
+// Map color names to Tailwind classes
+const colorMap: Record = {
+ blue: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
+ green: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
+ purple: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
+ pink: "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300",
+ orange: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300",
+ yellow: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
+ amber: "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300",
+ red: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
+};
+
+export default function NewTagPage() {
+ const router = useRouter();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const { register, handleSubmit, control, watch, formState: { errors } } = useForm({
+ defaultValues: {
+ name: "",
+ description: "",
+ color: "blue"
+ }
+ });
+
+ const selectedColor = watch("color");
+
+ const onSubmit = async (data: TagFormData) => {
+ setIsSubmitting(true);
+ try {
+ // In a real app, this would be an API call to create a new tag
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ // Simulate a successful response with a tag ID
+ const tagId = Date.now();
+
+ toast.success("Tag créé avec succès");
+ router.push("/tags");
+ } catch (error) {
+ console.error("Error creating tag:", error);
+ toast.error("Erreur lors de la création du tag");
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
Nouveau tag
+
+
+
+
+
+ Informations du tag
+
+ Créez un nouveau tag pour catégoriser les personnes
+
+
+
+
+
Nom du tag
+
+ {errors.name && (
+
{errors.name.message}
+ )}
+
+
+
+
Description
+
+ {errors.description && (
+
{errors.description.message}
+ )}
+
+
+
+
Couleur
+
(
+
+
+
+
+
+ {colors.map((color) => (
+
+
+
+ {color.name}
+
+
+ ))}
+
+
+ )}
+ />
+ {errors.color && (
+ {errors.color.message}
+ )}
+
+
+
+
Aperçu
+
+
+ {watch("name") || "Nom du tag"}
+
+
+ {watch("description") || "Description du tag"}
+
+
+
+
+
+
+ Annuler
+
+
+ {isSubmitting ? (
+ <>
+
+ Création en cours...
+ >
+ ) : (
+ <>
+
+ Créer le tag
+ >
+ )}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/app/tags/page.tsx b/frontend/app/tags/page.tsx
new file mode 100644
index 0000000..0232a3a
--- /dev/null
+++ b/frontend/app/tags/page.tsx
@@ -0,0 +1,212 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow
+} from "@/components/ui/table";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
+} from "@/components/ui/dropdown-menu";
+import {
+ PlusCircle,
+ Search,
+ MoreHorizontal,
+ Pencil,
+ Trash2,
+ Users
+} from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+
+export default function TagsPage() {
+ const [searchQuery, setSearchQuery] = useState("");
+
+ // Mock data for tags
+ const tags = [
+ {
+ id: 1,
+ name: "Frontend",
+ description: "Développement frontend",
+ color: "blue",
+ persons: 12,
+ },
+ {
+ id: 2,
+ name: "Backend",
+ description: "Développement backend",
+ color: "green",
+ persons: 8,
+ },
+ {
+ id: 3,
+ name: "Fullstack",
+ description: "Développement fullstack",
+ color: "purple",
+ persons: 5,
+ },
+ {
+ id: 4,
+ name: "UX/UI",
+ description: "Design UX/UI",
+ color: "pink",
+ persons: 3,
+ },
+ {
+ id: 5,
+ name: "DevOps",
+ description: "Infrastructure et déploiement",
+ color: "orange",
+ persons: 2,
+ },
+ {
+ id: 6,
+ name: "Junior",
+ description: "Niveau junior",
+ color: "yellow",
+ persons: 7,
+ },
+ {
+ id: 7,
+ name: "Medior",
+ description: "Niveau intermédiaire",
+ color: "amber",
+ persons: 5,
+ },
+ {
+ id: 8,
+ name: "Senior",
+ description: "Niveau senior",
+ color: "red",
+ persons: 6,
+ },
+ ];
+
+ // Map color names to Tailwind classes
+ const colorMap: Record = {
+ blue: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
+ green: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
+ purple: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
+ pink: "bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300",
+ orange: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300",
+ yellow: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
+ amber: "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300",
+ red: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
+ };
+
+ // Filter tags based on search query
+ const filteredTags = tags.filter(
+ (tag) =>
+ tag.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ tag.description.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+
+ return (
+
+
+
Tags
+
+
+
+ Démo sélecteur
+
+
+
+
+
+ Nouveau tag
+
+
+
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+
+
+
+
+ Nom
+ Description
+ Personnes
+ Actions
+
+
+
+ {filteredTags.length === 0 ? (
+
+
+ Aucun tag trouvé.
+
+
+ ) : (
+ filteredTags.map((tag) => (
+
+
+
+ {tag.name}
+
+
+ {tag.description}
+ {tag.persons}
+
+
+
+
+
+ Actions
+
+
+
+ Actions
+
+
+
+
+ Modifier
+
+
+
+
+
+ Voir les personnes
+
+
+
+
+ Supprimer
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+ );
+}