Files
peakskills/components/admin/management/pages/teams-management.tsx
2025-08-22 08:56:02 +02:00

548 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useMemo } from "react";
import { Plus, Edit, Trash2, Users, Search, Building2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent } 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 { useToast } from "@/hooks/use-toast";
import { SkillCategory, Team as TeamType } from "@/lib/types";
import { TeamStats } from "@/services/admin-service";
import {
AdminManagementService,
Team,
} from "@/services/admin-management-service";
import {
TreeViewContainer,
TreeCategoryHeader,
TreeItemRow,
TreeSearchControls,
TeamMetrics,
} from "@/components/admin";
interface TeamsManagementProps {
teams: TeamType[];
teamStats: TeamStats[];
skillCategories: SkillCategory[];
}
interface TeamFormData {
name: string;
direction: string;
}
export function TeamsManagement({
teams,
teamStats,
skillCategories,
}: TeamsManagementProps) {
const [searchTerm, setSearchTerm] = useState("");
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [editingTeam, setEditingTeam] = useState<any>(null);
const [teamFormData, setTeamFormData] = useState<TeamFormData>({
name: "",
direction: "",
});
const { toast } = useToast();
// État pour les directions ouvertes/fermées
const [expandedDirections, setExpandedDirections] = useState<Set<string>>(
new Set()
);
// État pour gérer la liste des équipes
// Grouper les teams par direction et filtrer en fonction de la recherche
const filteredTeamsByDirection = useMemo(() => {
// Grouper les teams par direction
const teamsByDirection = teams.reduce((acc, team) => {
if (!acc[team.direction]) {
acc[team.direction] = [];
}
acc[team.direction].push(team);
return acc;
}, {} as Record<string, TeamType[]>);
// Filtrer les teams en fonction de la recherche
return Object.entries(teamsByDirection).reduce(
(acc, [direction, directionTeams]) => {
const filteredTeams = directionTeams.filter((team) => {
const matchesSearch = team.name
.toLowerCase()
.includes(searchTerm.toLowerCase());
return matchesSearch;
});
if (filteredTeams.length > 0) {
acc[direction] = filteredTeams;
}
return acc;
},
{} as Record<string, TeamType[]>
);
}, [teams, searchTerm]);
// Fonctions pour gérer l'expansion des directions
const toggleDirection = useMemo(
() => (direction: string) => {
setExpandedDirections((prev) => {
const newExpanded = new Set(prev);
if (newExpanded.has(direction)) {
newExpanded.delete(direction);
} else {
newExpanded.add(direction);
}
return newExpanded;
});
},
[]
);
const expandAll = useMemo(
() => () => {
setExpandedDirections(new Set(Object.keys(filteredTeamsByDirection)));
},
[filteredTeamsByDirection]
);
const collapseAll = useMemo(
() => () => {
setExpandedDirections(new Set());
},
[]
);
const getTeamStats = (teamId: string) => {
return teamStats.find((stats) => stats.teamId === teamId);
};
// Charger les teams depuis l'API
const fetchTeams = async () => {
try {
const teamsData = await AdminManagementService.getTeams();
// Note: on garde les teams existantes pour la compatibilité
// Les nouvelles teams créées via l'API seront visibles après rafraîchissement
} catch (error) {
console.error("Error fetching teams:", error);
toast({
title: "Erreur",
description: "Impossible de charger les teams",
variant: "destructive",
});
}
};
// Charger les teams au montage du composant
useEffect(() => {
fetchTeams();
}, []);
// Ouvrir automatiquement les directions qui contiennent des résultats lors de la recherche
useEffect(() => {
if (searchTerm.trim()) {
const directionsWithResults = Object.keys(filteredTeamsByDirection);
setExpandedDirections(new Set(directionsWithResults));
} else {
// Si pas de recherche, fermer toutes les directions
setExpandedDirections(new Set());
}
}, [searchTerm, filteredTeamsByDirection]);
const handleCreateTeam = async () => {
if (!teamFormData.name || !teamFormData.direction) {
toast({
title: "Erreur",
description: "Veuillez remplir tous les champs obligatoires",
variant: "destructive",
});
return;
}
try {
const newTeam = await AdminManagementService.createTeam(teamFormData);
toast({
title: "Succès",
description: "Équipe créée avec succès",
});
setTeamFormData({ name: "", direction: "" });
setIsCreateDialogOpen(false);
// Rafraîchir la page pour voir les changements
window.location.reload();
} catch (error: any) {
toast({
title: "Erreur",
description: error.message || "Erreur lors de la création de l'équipe",
variant: "destructive",
});
}
};
const handleEditTeam = (team: any) => {
setEditingTeam(team);
setTeamFormData({
name: team.name,
direction: team.direction,
});
setIsEditDialogOpen(true);
};
const handleUpdateTeam = async () => {
if (!editingTeam || !teamFormData.name || !teamFormData.direction) {
toast({
title: "Erreur",
description: "Veuillez remplir tous les champs obligatoires",
variant: "destructive",
});
return;
}
try {
await AdminManagementService.updateTeam({
id: editingTeam.id,
...teamFormData,
memberCount: editingTeam.memberCount || 0,
});
toast({
title: "Succès",
description: "Équipe mise à jour avec succès",
});
setIsEditDialogOpen(false);
setEditingTeam(null);
setTeamFormData({ name: "", direction: "" });
// Rafraîchir la page pour voir les changements
window.location.reload();
} catch (error: any) {
toast({
title: "Erreur",
description:
error.message || "Erreur lors de la mise à jour de l'équipe",
variant: "destructive",
});
}
};
const handleDeleteTeam = async (teamId: string) => {
const team = teams.find((t) => t.id === teamId);
const stats = getTeamStats(teamId);
if (stats && stats.totalMembers > 0) {
toast({
title: "Erreur",
description:
"Impossible de supprimer une équipe qui contient des membres",
variant: "destructive",
});
return;
}
if (
confirm(
`Êtes-vous sûr de vouloir supprimer l'équipe "${team?.name}" ? Cette action est irréversible.`
)
) {
try {
await AdminManagementService.deleteTeam(teamId);
toast({
title: "Succès",
description: "Équipe supprimée avec succès",
});
// Rafraîchir la page pour voir les changements
window.location.reload();
} catch (error: any) {
toast({
title: "Erreur",
description:
error.message || "Erreur lors de la suppression de l'équipe",
variant: "destructive",
});
}
}
};
const handleDeleteDirection = async (direction: string) => {
// Vérifier si des équipes de cette direction ont des membres
const teamsInDirection = teams.filter(
(team) => team.direction === direction
);
const hasMembers = teamsInDirection.some((team) => {
const stats = getTeamStats(team.id);
return stats && stats.totalMembers > 0;
});
if (hasMembers) {
toast({
title: "Erreur",
description: `Impossible de supprimer la direction "${direction}" car certaines équipes ont des membres`,
variant: "destructive",
});
return;
}
if (
confirm(
`Êtes-vous sûr de vouloir supprimer la direction "${direction}" et TOUTES ses équipes ?\n\n⚠ Cette action est irréversible !\n\nÉquipes qui seront supprimées :\n${teamsInDirection
.map((t) => `${t.name}`)
.join("\n")}`
)
) {
try {
await AdminManagementService.deleteDirection(direction);
toast({
title: "Succès",
description: `Direction "${direction}" et toutes ses équipes supprimées avec succès`,
variant: "default",
});
// Rafraîchir la page pour voir les changements
window.location.reload();
} catch (error: any) {
toast({
title: "Erreur",
description:
error.message || "Erreur lors de la suppression de la direction",
variant: "destructive",
});
}
}
};
const resetForm = () => {
setTeamFormData({ name: "", direction: "" });
};
// Extraire les directions uniques pour les formulaires
const directions = Array.from(new Set(teams.map((team) => team.direction)));
return (
<div className="space-y-4">
{/* Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
<div>
<h2 className="text-2xl font-bold text-white">Gestion des Teams</h2>
<p className="text-slate-400">
Créez, modifiez et supprimez les équipes de votre organisation
</p>
</div>
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
<DialogTrigger asChild>
<Button onClick={resetForm}>
<Plus className="w-4 h-4 mr-2" />
Nouvelle Équipe
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Créer une nouvelle équipe</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="team-name">Nom de l'équipe *</Label>
<Input
id="team-name"
value={teamFormData.name}
onChange={(e) =>
setTeamFormData({ ...teamFormData, name: e.target.value })
}
placeholder="Ex: Équipe Frontend, Équipe Backend"
/>
</div>
<div>
<Label htmlFor="team-direction">Direction *</Label>
<Select
value={teamFormData.direction}
onValueChange={(value) =>
setTeamFormData({ ...teamFormData, direction: value })
}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionner une direction" />
</SelectTrigger>
<SelectContent>
{directions.map((direction) => (
<SelectItem key={direction} value={direction}>
{direction}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex justify-end gap-2 pt-4">
<Button
variant="outline"
onClick={() => setIsCreateDialogOpen(false)}
>
Annuler
</Button>
<Button onClick={handleCreateTeam}>Créer</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
{/* Filtres et contrôles */}
<TreeSearchControls
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
onExpandAll={expandAll}
onCollapseAll={collapseAll}
placeholder="Rechercher une équipe..."
/>
{/* Vue arborescente des Teams */}
<TreeViewContainer
hasContent={Object.keys(filteredTeamsByDirection).length > 0}
emptyState={
<div className="text-center py-8">
<Building2 className="w-10 h-10 text-slate-500 mx-auto mb-3" />
<h3 className="text-base font-medium text-slate-400 mb-1">
{searchTerm ? "Aucune équipe trouvée" : "Aucune équipe"}
</h3>
<p className="text-sm text-slate-500">
{searchTerm
? "Essayez de modifier vos critères de recherche"
: "Commencez par créer votre première équipe"}
</p>
</div>
}
>
{Object.entries(filteredTeamsByDirection).map(
([direction, directionTeams], index) => (
<div key={direction}>
<TreeCategoryHeader
category={direction}
isExpanded={expandedDirections.has(direction)}
onToggle={() => toggleDirection(direction)}
icon={<Building2 className="w-5 h-5 text-blue-400" />}
itemCount={directionTeams.length}
itemLabel="équipe"
showSeparator={index > 0}
onDelete={() => handleDeleteDirection(direction)}
canDelete={true}
isDirection={true}
/>
{/* Liste des teams de la direction */}
{expandedDirections.has(direction) && (
<div className="bg-slate-950/30">
{directionTeams.map((team, teamIndex) => {
const stats = getTeamStats(team.id);
return (
<TreeItemRow
key={team.id}
icon={<Users className="w-5 h-5 text-green-400" />}
title={team.name}
badges={
stats
? [
{
text: `${stats.totalMembers} membres`,
variant: "outline",
},
]
: []
}
onEdit={() => handleEditTeam(team)}
onDelete={() => handleDeleteTeam(team.id)}
canDelete={!stats || stats.totalMembers === 0}
showSeparator={teamIndex > 0}
additionalInfo={
stats ? (
<TeamMetrics
averageSkillLevel={stats.averageSkillLevel}
skillCoverage={stats.skillCoverage}
topSkillsCount={stats.topSkills.length}
totalMembers={stats.totalMembers}
/>
) : (
<p className="text-slate-500 text-xs">
Aucune donnée disponible
</p>
)
}
/>
);
})}
</div>
)}
</div>
)
)}
</TreeViewContainer>
{/* Dialog d'édition */}
<Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Modifier l'équipe</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="edit-team-name">Nom de l'équipe *</Label>
<Input
id="edit-team-name"
value={teamFormData.name}
onChange={(e) =>
setTeamFormData({ ...teamFormData, name: e.target.value })
}
placeholder="Ex: Équipe Frontend, Équipe Backend"
/>
</div>
<div>
<Label htmlFor="edit-team-direction">Direction *</Label>
<Select
value={teamFormData.direction}
onValueChange={(value) =>
setTeamFormData({ ...teamFormData, direction: value })
}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionner une direction" />
</SelectTrigger>
<SelectContent>
{directions.map((direction) => (
<SelectItem key={direction} value={direction}>
{direction}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex justify-end gap-2 pt-4">
<Button
variant="outline"
onClick={() => setIsEditDialogOpen(false)}
>
Annuler
</Button>
<Button onClick={handleUpdateTeam}>Mettre à jour</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
}