From aa5b53757762c804a54ac6cd0b4eb511e6815e9d Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 22 Aug 2025 09:07:08 +0200 Subject: [PATCH] feat: enhance ProfileForm with team search and dropdown functionality - Added search functionality to filter teams by name or direction in the ProfileForm component. - Implemented a custom dropdown for team selection, including dynamic positioning based on available space. - Integrated click outside detection to close the dropdown when interacting outside of it. - Updated navigation component to use a Link for user info display, improving accessibility and interaction. --- components/admin/skills-management.tsx | 596 +++++++++++++++++++++++++ components/admin/teams-management.tsx | 547 +++++++++++++++++++++++ components/layout/navigation.tsx | 7 +- components/profile-form.tsx | 175 ++++++-- 4 files changed, 1288 insertions(+), 37 deletions(-) create mode 100644 components/admin/skills-management.tsx create mode 100644 components/admin/teams-management.tsx diff --git a/components/admin/skills-management.tsx b/components/admin/skills-management.tsx new file mode 100644 index 0000000..9a160a2 --- /dev/null +++ b/components/admin/skills-management.tsx @@ -0,0 +1,596 @@ +"use client"; + +import { useState, useEffect, useMemo } from "react"; +import { + Plus, + Edit, + Trash2, + Code2, + Search, + Folder, + FolderOpen, + ChevronRight, + ChevronDown, + Palette, + Database, + Cloud, + Shield, + Smartphone, + Layers, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { useToast } from "@/hooks/use-toast"; +import { SkillCategory, Team } from "@/lib/types"; +import { + AdminManagementService, + Skill, +} from "@/services/admin-management-service"; +import { TechIcon } from "@/components/icons/tech-icon"; +import { + TreeViewContainer, + TreeCategoryHeader, + TreeItemRow, + TreeSearchControls, +} from "@/components/admin"; + +interface SkillsManagementProps { + skillCategories: SkillCategory[]; + teams: Team[]; +} + +interface SkillFormData { + name: string; + categoryId: string; + description: string; + icon: string; +} + +export function SkillsManagement({ + skillCategories, + teams, +}: SkillsManagementProps) { + const [searchTerm, setSearchTerm] = useState(""); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [editingSkill, setEditingSkill] = useState(null); + const [skillFormData, setSkillFormData] = useState({ + name: "", + categoryId: "", + description: "", + icon: "", + }); + const { toast } = useToast(); + + // État des skills + const [skills, setSkills] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + // État pour les catégories ouvertes/fermées + const [expandedCategories, setExpandedCategories] = useState>( + new Set() + ); + + // Grouper les skills par catégorie et filtrer en fonction de la recherche + const filteredSkillsByCategory = useMemo(() => { + // Grouper les skills par catégorie + const skillsByCategory = skills.reduce((acc, skill) => { + if (!acc[skill.category]) { + acc[skill.category] = []; + } + acc[skill.category].push(skill); + return acc; + }, {} as Record); + + // Filtrer les skills en fonction de la recherche + return Object.entries(skillsByCategory).reduce( + (acc, [category, categorySkills]) => { + const filteredSkills = categorySkills.filter((skill) => { + const matchesSearch = + skill.name.toLowerCase().includes(searchTerm.toLowerCase()) || + (skill.description && + skill.description + .toLowerCase() + .includes(searchTerm.toLowerCase())); + return matchesSearch; + }); + + if (filteredSkills.length > 0) { + acc[category] = filteredSkills; + } + return acc; + }, + {} as Record + ); + }, [skills, searchTerm]); + + // Fonctions pour gérer l'expansion des catégories + const toggleCategory = useMemo( + () => (category: string) => { + setExpandedCategories((prev) => { + const newExpanded = new Set(prev); + if (newExpanded.has(category)) { + newExpanded.delete(category); + } else { + newExpanded.add(category); + } + return newExpanded; + }); + }, + [] + ); + + const expandAll = useMemo( + () => () => { + setExpandedCategories(new Set(Object.keys(filteredSkillsByCategory))); + }, + [filteredSkillsByCategory] + ); + + const collapseAll = useMemo( + () => () => { + setExpandedCategories(new Set()); + }, + [] + ); + + // Charger les skills depuis l'API + const fetchSkills = async () => { + try { + setIsLoading(true); + const skillsData = await AdminManagementService.getSkills(); + setSkills(skillsData); + } catch (error) { + console.error("Error fetching skills:", error); + toast({ + title: "Erreur", + description: "Impossible de charger les skills", + variant: "destructive", + }); + } finally { + setIsLoading(false); + } + }; + + // Charger les skills au montage du composant + useEffect(() => { + fetchSkills(); + }, []); + + // Ouvrir automatiquement les catégories qui contiennent des résultats lors de la recherche + useEffect(() => { + if (searchTerm.trim()) { + const categoriesWithResults = Object.keys(filteredSkillsByCategory); + setExpandedCategories(new Set(categoriesWithResults)); + } else { + // Si pas de recherche, fermer toutes les catégories + setExpandedCategories(new Set()); + } + }, [searchTerm, filteredSkillsByCategory]); + + const handleCreateSkill = async () => { + if (!skillFormData.name || !skillFormData.categoryId) { + toast({ + title: "Erreur", + description: "Veuillez remplir tous les champs obligatoires", + variant: "destructive", + }); + return; + } + + try { + const categoryIndex = parseInt(skillFormData.categoryId); + const category = skillCategories[categoryIndex]; + + const skillData = { + ...skillFormData, + category: category.category, + }; + + const newSkill = await AdminManagementService.createSkill(skillData); + setSkills([...skills, newSkill]); + setSkillFormData({ name: "", categoryId: "", description: "", icon: "" }); + setIsCreateDialogOpen(false); + + toast({ + title: "Succès", + description: "Skill créée avec succès", + }); + } catch (error: any) { + toast({ + title: "Erreur", + description: error.message || "Erreur lors de la création de la skill", + variant: "destructive", + }); + } + }; + + const handleEditSkill = (skill: any) => { + setEditingSkill(skill); + const categoryIndex = skillCategories.findIndex( + (cat) => cat.category === skill.category + ); + setSkillFormData({ + name: skill.name, + categoryId: categoryIndex !== -1 ? categoryIndex.toString() : "", + description: skill.description, + icon: skill.icon, + }); + setIsEditDialogOpen(true); + }; + + const handleUpdateSkill = async () => { + if (!editingSkill || !skillFormData.name || !skillFormData.categoryId) { + toast({ + title: "Erreur", + description: "Veuillez remplir tous les champs obligatoires", + variant: "destructive", + }); + return; + } + + try { + const categoryIndex = parseInt(skillFormData.categoryId); + const category = skillCategories[categoryIndex]; + + const skillData = { + id: editingSkill.id, + ...skillFormData, + category: category.category, + usageCount: editingSkill.usageCount, + }; + + const updatedSkill = await AdminManagementService.updateSkill(skillData); + + const updatedSkills = skills.map((skill) => + skill.id === editingSkill.id ? updatedSkill : skill + ); + + setSkills(updatedSkills); + setIsEditDialogOpen(false); + setEditingSkill(null); + setSkillFormData({ name: "", categoryId: "", description: "", icon: "" }); + + toast({ + title: "Succès", + description: "Skill mise à jour avec succès", + }); + } catch (error: any) { + toast({ + title: "Erreur", + description: + error.message || "Erreur lors de la mise à jour de la skill", + variant: "destructive", + }); + } + }; + + const handleDeleteSkill = async (skillId: string) => { + if ( + confirm( + "Êtes-vous sûr de vouloir supprimer cette skill ? Cette action est irréversible." + ) + ) { + try { + await AdminManagementService.deleteSkill(skillId); + setSkills(skills.filter((s) => s.id !== skillId)); + toast({ + title: "Succès", + description: "Skill supprimée avec succès", + }); + } catch (error: any) { + toast({ + title: "Erreur", + description: + error.message || "Erreur lors de la suppression de la skill", + variant: "destructive", + }); + } + } + }; + + const resetForm = () => { + setSkillFormData({ name: "", categoryId: "", description: "", icon: "" }); + }; + + // Fonction pour obtenir l'icône de la catégorie + const getCategoryIcon = (category: string) => { + const categoryName = category.toLowerCase(); + if (categoryName.includes("frontend") || categoryName.includes("front")) + return Code2; + if (categoryName.includes("backend") || categoryName.includes("back")) + return Layers; + if ( + categoryName.includes("design") || + categoryName.includes("ui") || + categoryName.includes("ux") + ) + return Palette; + if (categoryName.includes("data") || categoryName.includes("database")) + return Database; + if (categoryName.includes("cloud") || categoryName.includes("devops")) + return Cloud; + if (categoryName.includes("security") || categoryName.includes("securité")) + return Shield; + if ( + categoryName.includes("mobile") || + categoryName.includes("android") || + categoryName.includes("ios") + ) + return Smartphone; + return Code2; // Par défaut + }; + + return ( +
+ {/* Header */} +
+
+

Gestion des Skills

+

+ Créez, modifiez et supprimez les skills de votre organisation +

+
+ + + + + + + Créer une nouvelle skill + +
+
+ + + setSkillFormData({ ...skillFormData, name: e.target.value }) + } + placeholder="Ex: React, Node.js, PostgreSQL" + /> +
+
+ + +
+
+ +