feat: remove a skill category empty
This commit is contained in:
39
app/api/admin/skills/categories/[categoryId]/route.ts
Normal file
39
app/api/admin/skills/categories/[categoryId]/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,10 @@ export class AdminClient extends BaseHttpClient {
|
|||||||
await this.delete(`/admin/skills?id=${skillId}`);
|
await this.delete(`/admin/skills?id=${skillId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteSkillCategory(categoryId: string): Promise<void> {
|
||||||
|
await this.delete(`/admin/skills/categories/${categoryId}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Teams Management
|
// Teams Management
|
||||||
async getTeams(): Promise<Team[]> {
|
async getTeams(): Promise<Team[]> {
|
||||||
return await this.get<Team[]>(`/admin/teams`);
|
return await this.get<Team[]>(`/admin/teams`);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ interface TreeCategoryHeaderProps {
|
|||||||
totalMembers: number;
|
totalMembers: number;
|
||||||
hasMembers: boolean;
|
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({
|
export function TreeCategoryHeader({
|
||||||
@@ -34,6 +36,8 @@ export function TreeCategoryHeader({
|
|||||||
canDelete = true,
|
canDelete = true,
|
||||||
isDirection = false,
|
isDirection = false,
|
||||||
directionStats,
|
directionStats,
|
||||||
|
onDeleteCategory,
|
||||||
|
canDeleteCategory = false,
|
||||||
}: TreeCategoryHeaderProps) {
|
}: TreeCategoryHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -85,6 +89,26 @@ export function TreeCategoryHeader({
|
|||||||
<Trash2 className="w-3 h-3" />
|
<Trash2 className="w-3 h-3" />
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface SkillsListProps {
|
|||||||
onToggleCategory: (category: string) => void;
|
onToggleCategory: (category: string) => void;
|
||||||
onEditSkill: (skill: Skill) => void;
|
onEditSkill: (skill: Skill) => void;
|
||||||
onDeleteSkill: (skillId: string) => void;
|
onDeleteSkill: (skillId: string) => void;
|
||||||
|
onDeleteCategory?: (categoryName: string) => void; // Nouvelle prop
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SkillsList({
|
export function SkillsList({
|
||||||
@@ -27,6 +28,7 @@ export function SkillsList({
|
|||||||
onToggleCategory,
|
onToggleCategory,
|
||||||
onEditSkill,
|
onEditSkill,
|
||||||
onDeleteSkill,
|
onDeleteSkill,
|
||||||
|
onDeleteCategory,
|
||||||
}: SkillsListProps) {
|
}: SkillsListProps) {
|
||||||
// Fonction pour obtenir l'icône de la catégorie
|
// Fonction pour obtenir l'icône de la catégorie
|
||||||
const getCategoryIcon = (category: string) => {
|
const getCategoryIcon = (category: string) => {
|
||||||
@@ -72,6 +74,8 @@ export function SkillsList({
|
|||||||
itemCount={categorySkills.length}
|
itemCount={categorySkills.length}
|
||||||
itemLabel="skill"
|
itemLabel="skill"
|
||||||
showSeparator={index > 0}
|
showSeparator={index > 0}
|
||||||
|
onDeleteCategory={onDeleteCategory}
|
||||||
|
canDeleteCategory={categorySkills.length === 0} // Peut supprimer si vide
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Liste des skills de la catégorie */}
|
{/* Liste des skills de la catégorie */}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import { SkillCategory, Team } from "@/lib/types";
|
|||||||
import { TreeViewPage } from "../management/tree-view-page";
|
import { TreeViewPage } from "../management/tree-view-page";
|
||||||
import { useTreeView } from "@/hooks/use-tree-view";
|
import { useTreeView } from "@/hooks/use-tree-view";
|
||||||
import { useFormDialog } from "@/hooks/use-form-dialog";
|
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 { SkillFormDialog } from "./skill-form-dialog";
|
||||||
import { SkillsList } from "./skills-list";
|
import { SkillsList } from "./skills-list";
|
||||||
|
|
||||||
@@ -18,10 +21,15 @@ interface SkillsManagementPageProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SkillsManagementPage({
|
export function SkillsManagementPage({
|
||||||
skillCategories,
|
skillCategories: initialSkillCategories,
|
||||||
initialSkills,
|
initialSkills,
|
||||||
}: SkillsManagementPageProps) {
|
}: SkillsManagementPageProps) {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
|
// Utiliser le hook dédié pour la gestion des catégories
|
||||||
|
const { skillCategories, handleDeleteCategory } =
|
||||||
|
useSkillCategoriesManagement(initialSkillCategories);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isCreateDialogOpen,
|
isCreateDialogOpen,
|
||||||
isEditDialogOpen,
|
isEditDialogOpen,
|
||||||
@@ -58,6 +66,7 @@ export function SkillsManagementPage({
|
|||||||
groupBy: (skill) => skill.category,
|
groupBy: (skill) => skill.category,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
onSearchChange: setSearchTerm,
|
onSearchChange: setSearchTerm,
|
||||||
|
availableCategories: skillCategories.map((cat) => cat.name), // Ajouter les catégories disponibles
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCreateSubmit = async () => {
|
const handleCreateSubmit = async () => {
|
||||||
@@ -127,6 +136,7 @@ export function SkillsManagementPage({
|
|||||||
onToggleCategory={toggleCategory}
|
onToggleCategory={toggleCategory}
|
||||||
onEditSkill={handleOpenEditDialog}
|
onEditSkill={handleOpenEditDialog}
|
||||||
onDeleteSkill={handleDeleteSkill}
|
onDeleteSkill={handleDeleteSkill}
|
||||||
|
onDeleteCategory={handleDeleteCategory}
|
||||||
/>
|
/>
|
||||||
</TreeViewPage>
|
</TreeViewPage>
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,51 @@ interface SkillFormData {
|
|||||||
icon: string;
|
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(
|
export function useSkillsManagement(
|
||||||
skillCategories: SkillCategory[],
|
skillCategories: SkillCategory[],
|
||||||
initialSkills?: any[]
|
initialSkills?: any[]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface UseTreeViewOptions<T> {
|
|||||||
groupBy: (item: T) => string;
|
groupBy: (item: T) => string;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
onSearchChange: (term: string) => void;
|
onSearchChange: (term: string) => void;
|
||||||
|
availableCategories?: string[]; // Nouvelles catégories disponibles
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTreeView<T>({
|
export function useTreeView<T>({
|
||||||
@@ -14,6 +15,7 @@ export function useTreeView<T>({
|
|||||||
groupBy,
|
groupBy,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
|
availableCategories = [],
|
||||||
}: UseTreeViewOptions<T>) {
|
}: UseTreeViewOptions<T>) {
|
||||||
// État pour les catégories ouvertes/fermées
|
// État pour les catégories ouvertes/fermées
|
||||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
||||||
@@ -33,7 +35,7 @@ export function useTreeView<T>({
|
|||||||
}, {} as Record<string, T[]>);
|
}, {} as Record<string, T[]>);
|
||||||
|
|
||||||
// Filtrer les données en fonction de la recherche
|
// Filtrer les données en fonction de la recherche
|
||||||
return Object.entries(dataByCategory).reduce(
|
const filteredCategories = Object.entries(dataByCategory).reduce(
|
||||||
(acc, [category, categoryItems]) => {
|
(acc, [category, categoryItems]) => {
|
||||||
const filteredItems = categoryItems.filter((item) => {
|
const filteredItems = categoryItems.filter((item) => {
|
||||||
const matchesSearch = searchFields.some((field) => {
|
const matchesSearch = searchFields.some((field) => {
|
||||||
@@ -53,7 +55,16 @@ export function useTreeView<T>({
|
|||||||
},
|
},
|
||||||
{} as Record<string, 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
|
// Fonctions pour gérer l'expansion des catégories
|
||||||
const toggleCategory = useCallback((category: string) => {
|
const toggleCategory = useCallback((category: string) => {
|
||||||
|
|||||||
@@ -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
|
* Récupère les données nécessaires pour la page de gestion des utilisateurs
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user