diff --git a/app/api/admin/skills/categories/[categoryId]/route.ts b/app/api/admin/skills/categories/[categoryId]/route.ts new file mode 100644 index 0000000..e59d34d --- /dev/null +++ b/app/api/admin/skills/categories/[categoryId]/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from "next/server"; +import { AdminService } from "@/services/admin-service"; + +export async function DELETE( + request: NextRequest, + { params }: { params: { categoryId: string } } +) { + try { + const { categoryId } = params; + + if (!categoryId) { + return NextResponse.json( + { error: "ID de catégorie requis" }, + { status: 400 } + ); + } + + await AdminService.deleteSkillCategory(categoryId); + + return NextResponse.json( + { message: "Catégorie supprimée avec succès" }, + { status: 200 } + ); + } catch (error) { + console.error("Erreur lors de la suppression de la catégorie:", error); + + if (error instanceof Error) { + return NextResponse.json( + { error: error.message }, + { status: 400 } + ); + } + + return NextResponse.json( + { error: "Erreur interne du serveur" }, + { status: 500 } + ); + } +} diff --git a/clients/domains/admin-client.ts b/clients/domains/admin-client.ts index d16cbf1..1bfdc02 100644 --- a/clients/domains/admin-client.ts +++ b/clients/domains/admin-client.ts @@ -55,6 +55,10 @@ export class AdminClient extends BaseHttpClient { await this.delete(`/admin/skills?id=${skillId}`); } + async deleteSkillCategory(categoryId: string): Promise { + await this.delete(`/admin/skills/categories/${categoryId}`); + } + // Teams Management async getTeams(): Promise { return await this.get(`/admin/teams`); diff --git a/components/admin/management/tree-category-header.tsx b/components/admin/management/tree-category-header.tsx index 5db9097..02cc0ca 100644 --- a/components/admin/management/tree-category-header.tsx +++ b/components/admin/management/tree-category-header.tsx @@ -20,6 +20,8 @@ interface TreeCategoryHeaderProps { totalMembers: number; hasMembers: boolean; }; + onDeleteCategory?: (categoryName: string) => void; // Nouvelle prop pour supprimer les catégories de skills + canDeleteCategory?: boolean; // Si la catégorie peut être supprimée } export function TreeCategoryHeader({ @@ -34,6 +36,8 @@ export function TreeCategoryHeader({ canDelete = true, isDirection = false, directionStats, + onDeleteCategory, + canDeleteCategory = false, }: TreeCategoryHeaderProps) { return (
@@ -85,6 +89,26 @@ export function TreeCategoryHeader({ )} + {/* Bouton de suppression pour les catégories de skills vides */} + {!isDirection && onDeleteCategory && itemCount === 0 && ( + + )}
diff --git a/components/admin/skills/skills-list.tsx b/components/admin/skills/skills-list.tsx index 6b4a5b7..502650a 100644 --- a/components/admin/skills/skills-list.tsx +++ b/components/admin/skills/skills-list.tsx @@ -19,6 +19,7 @@ interface SkillsListProps { onToggleCategory: (category: string) => void; onEditSkill: (skill: Skill) => void; onDeleteSkill: (skillId: string) => void; + onDeleteCategory?: (categoryName: string) => void; // Nouvelle prop } export function SkillsList({ @@ -27,6 +28,7 @@ export function SkillsList({ onToggleCategory, onEditSkill, onDeleteSkill, + onDeleteCategory, }: SkillsListProps) { // Fonction pour obtenir l'icône de la catégorie const getCategoryIcon = (category: string) => { @@ -72,6 +74,8 @@ export function SkillsList({ itemCount={categorySkills.length} itemLabel="skill" showSeparator={index > 0} + onDeleteCategory={onDeleteCategory} + canDeleteCategory={categorySkills.length === 0} // Peut supprimer si vide /> {/* Liste des skills de la catégorie */} diff --git a/components/admin/skills/skills-management-page.tsx b/components/admin/skills/skills-management-page.tsx index 4fcf36b..e40f41f 100644 --- a/components/admin/skills/skills-management-page.tsx +++ b/components/admin/skills/skills-management-page.tsx @@ -8,7 +8,10 @@ import { SkillCategory, Team } from "@/lib/types"; import { TreeViewPage } from "../management/tree-view-page"; import { useTreeView } from "@/hooks/use-tree-view"; import { useFormDialog } from "@/hooks/use-form-dialog"; -import { useSkillsManagement } from "@/hooks/use-skills-management"; +import { + useSkillsManagement, + useSkillCategoriesManagement, +} from "@/hooks/use-skills-management"; import { SkillFormDialog } from "./skill-form-dialog"; import { SkillsList } from "./skills-list"; @@ -18,10 +21,15 @@ interface SkillsManagementPageProps { } export function SkillsManagementPage({ - skillCategories, + skillCategories: initialSkillCategories, initialSkills, }: SkillsManagementPageProps) { const [searchTerm, setSearchTerm] = useState(""); + + // Utiliser le hook dédié pour la gestion des catégories + const { skillCategories, handleDeleteCategory } = + useSkillCategoriesManagement(initialSkillCategories); + const { isCreateDialogOpen, isEditDialogOpen, @@ -58,6 +66,7 @@ export function SkillsManagementPage({ groupBy: (skill) => skill.category, searchTerm, onSearchChange: setSearchTerm, + availableCategories: skillCategories.map((cat) => cat.name), // Ajouter les catégories disponibles }); const handleCreateSubmit = async () => { @@ -127,6 +136,7 @@ export function SkillsManagementPage({ onToggleCategory={toggleCategory} onEditSkill={handleOpenEditDialog} onDeleteSkill={handleDeleteSkill} + onDeleteCategory={handleDeleteCategory} /> diff --git a/hooks/use-skills-management.ts b/hooks/use-skills-management.ts index 2769634..b6f433a 100644 --- a/hooks/use-skills-management.ts +++ b/hooks/use-skills-management.ts @@ -11,6 +11,51 @@ interface SkillFormData { icon: string; } +// Hook pour gérer les catégories de skills +export function useSkillCategoriesManagement( + initialCategories: SkillCategory[] +) { + const [skillCategories, setSkillCategories] = useState(initialCategories); + const { toast } = useToast(); + + const handleDeleteCategory = async (categoryName: string) => { + try { + // Trouver la catégorie par son nom + const category = skillCategories.find((cat) => cat.name === categoryName); + if (!category) { + throw new Error("Catégorie non trouvée"); + } + + await adminClient.deleteSkillCategory(category.id); + + // Mettre à jour l'état local + setSkillCategories((prevCategories) => + prevCategories.filter((cat) => cat.id !== category.id) + ); + + toast({ + title: "Succès", + description: `Catégorie "${categoryName}" supprimée avec succès`, + }); + return true; + } catch (error: any) { + toast({ + title: "Erreur", + description: + error.message || "Erreur lors de la suppression de la catégorie", + variant: "destructive", + }); + return false; + } + }; + + return { + skillCategories, + setSkillCategories, + handleDeleteCategory, + }; +} + export function useSkillsManagement( skillCategories: SkillCategory[], initialSkills?: any[] diff --git a/hooks/use-tree-view.ts b/hooks/use-tree-view.ts index 7b71213..9d3fc7f 100644 --- a/hooks/use-tree-view.ts +++ b/hooks/use-tree-view.ts @@ -6,6 +6,7 @@ interface UseTreeViewOptions { groupBy: (item: T) => string; searchTerm: string; onSearchChange: (term: string) => void; + availableCategories?: string[]; // Nouvelles catégories disponibles } export function useTreeView({ @@ -14,6 +15,7 @@ export function useTreeView({ groupBy, searchTerm, onSearchChange, + availableCategories = [], }: UseTreeViewOptions) { // État pour les catégories ouvertes/fermées const [expandedCategories, setExpandedCategories] = useState>( @@ -33,7 +35,7 @@ export function useTreeView({ }, {} as Record); // Filtrer les données en fonction de la recherche - return Object.entries(dataByCategory).reduce( + const filteredCategories = Object.entries(dataByCategory).reduce( (acc, [category, categoryItems]) => { const filteredItems = categoryItems.filter((item) => { const matchesSearch = searchFields.some((field) => { @@ -53,7 +55,16 @@ export function useTreeView({ }, {} as Record ); - }, [data, searchFields, groupBy, searchTerm]); + + // Ajouter les catégories vides qui sont dans availableCategories + availableCategories.forEach(category => { + if (!filteredCategories[category]) { + filteredCategories[category] = []; + } + }); + + return filteredCategories; + }, [data, searchFields, groupBy, searchTerm, availableCategories]); // Fonctions pour gérer l'expansion des catégories const toggleCategory = useCallback((category: string) => { diff --git a/services/admin-service.ts b/services/admin-service.ts index 3fe665b..b8c1e03 100644 --- a/services/admin-service.ts +++ b/services/admin-service.ts @@ -221,6 +221,41 @@ export class AdminService { } } + /** + * Supprime une catégorie de skills vide + */ + static async deleteSkillCategory(categoryId: string): Promise { + const pool = getPool(); + const client = await pool.connect(); + + try { + await client.query("BEGIN"); + + // Vérifier que la catégorie n'a pas de skills + const skillsCheck = await client.query( + "SELECT COUNT(*) FROM skills WHERE category_id = $1", + [categoryId] + ); + + if (parseInt(skillsCheck.rows[0].count) > 0) { + throw new Error("Impossible de supprimer une catégorie qui contient des skills"); + } + + // Supprimer la catégorie + await client.query( + "DELETE FROM skill_categories WHERE id = $1", + [categoryId] + ); + + await client.query("COMMIT"); + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } + } + /** * Récupère les données nécessaires pour la page de gestion des utilisateurs */