feat: remove a skill category empty

This commit is contained in:
Julien Froidefond
2025-08-26 07:10:26 +02:00
parent e12816a9c2
commit d7fef0be9b
8 changed files with 176 additions and 4 deletions

View File

@@ -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 }
);
}
}

View File

@@ -55,6 +55,10 @@ export class AdminClient extends BaseHttpClient {
await this.delete(`/admin/skills?id=${skillId}`);
}
async deleteSkillCategory(categoryId: string): Promise<void> {
await this.delete(`/admin/skills/categories/${categoryId}`);
}
// Teams Management
async getTeams(): Promise<Team[]> {
return await this.get<Team[]>(`/admin/teams`);

View File

@@ -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 (
<div>
@@ -85,6 +89,26 @@ export function TreeCategoryHeader({
<Trash2 className="w-3 h-3" />
</Button>
)}
{/* Bouton de suppression pour les catégories de skills vides */}
{!isDirection && onDeleteCategory && itemCount === 0 && (
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation(); // Empêcher l'ouverture/fermeture
onDeleteCategory(category);
}}
className="text-red-400 hover:text-red-300 hover:bg-red-500/20 h-6 w-6 p-0"
disabled={!canDeleteCategory}
title={
canDeleteCategory
? `Supprimer la catégorie "${category}"`
: `Impossible de supprimer la catégorie "${category}"`
}
>
<Trash2 className="w-3 h-3" />
</Button>
)}
</div>
</div>
</div>

View File

@@ -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 */}

View File

@@ -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}
/>
</TreeViewPage>

View File

@@ -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[]

View File

@@ -6,6 +6,7 @@ interface UseTreeViewOptions<T> {
groupBy: (item: T) => string;
searchTerm: string;
onSearchChange: (term: string) => void;
availableCategories?: string[]; // Nouvelles catégories disponibles
}
export function useTreeView<T>({
@@ -14,6 +15,7 @@ export function useTreeView<T>({
groupBy,
searchTerm,
onSearchChange,
availableCategories = [],
}: UseTreeViewOptions<T>) {
// État pour les catégories ouvertes/fermées
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
@@ -33,7 +35,7 @@ export function useTreeView<T>({
}, {} as Record<string, T[]>);
// 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<T>({
},
{} as Record<string, T[]>
);
}, [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) => {

View File

@@ -221,6 +221,41 @@ export class AdminService {
}
}
/**
* Supprime une catégorie de skills vide
*/
static async deleteSkillCategory(categoryId: string): Promise<void> {
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
*/