Files
peakskills/components/admin/management/pages/teams-management.tsx
Julien Froidefond 97d274190d refacto: dialogs
2025-08-23 07:52:38 +02:00

566 lines
18 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 } from "react";
import { Plus, Edit, Trash2, Users, Building2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
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 {
TreeCategoryHeader,
TreeItemRow,
TeamMetrics,
} from "@/components/admin";
import { TeamMembersModal } from "@/components/admin/management/team-members-modal";
import { TreeViewPage } from "../tree-view-page";
import { useTreeView } from "@/hooks/use-tree-view";
import { useFormDialog } from "@/hooks/use-form-dialog";
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 [isMembersModalOpen, setIsMembersModalOpen] = useState(false);
const [selectedTeam, setSelectedTeam] = useState<any>(null);
const [editingTeam, setEditingTeam] = useState<any>(null);
const [teamFormData, setTeamFormData] = useState<TeamFormData>({
name: "",
direction: "",
});
const { toast } = useToast();
const {
isCreateDialogOpen,
isEditDialogOpen,
openCreateDialog,
closeCreateDialog,
openEditDialog,
closeEditDialog,
} = useFormDialog();
// État local pour les équipes et leurs stats
const [localTeams, setLocalTeams] = useState<TeamType[]>(teams);
const [localTeamStats, setLocalTeamStats] = useState<TeamStats[]>(teamStats);
// Utilisation du hook factorisé
const {
filteredDataByCategory: filteredTeamsByDirection,
expandedCategories: expandedDirections,
toggleCategory: toggleDirection,
expandAll,
collapseAll,
} = useTreeView({
data: localTeams,
searchFields: ["name"],
groupBy: (team) => team.direction,
searchTerm,
onSearchChange: setSearchTerm,
});
const getTeamStats = (teamId: string): TeamStats | undefined => {
return localTeamStats.find((stats) => stats.teamId === teamId);
};
// Fonction pour obtenir une équipe depuis la liste locale
const getLocalTeam = (teamId: string): TeamType | undefined => {
return localTeams.find((team) => team.id === 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();
}, []);
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: "" });
closeCreateDialog();
// Mettre à jour l'état local avec la nouvelle équipe
const newLocalTeam: TeamType = {
id: newTeam.id,
name: newTeam.name,
direction: newTeam.direction,
};
setLocalTeams((prev) => [...prev, newLocalTeam]);
// Ajouter les stats de la nouvelle équipe (avec les propriétés minimales)
const newTeamStats = {
teamId: newTeam.id,
teamName: newTeam.name,
direction: newTeam.direction,
totalMembers: 0,
averageSkillLevel: 0,
skillCoverage: 0,
topSkills: [],
members: [],
} as TeamStats;
setLocalTeamStats((prev) => [...prev, newTeamStats]);
} 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,
});
openEditDialog();
};
const handleViewMembers = (team: any) => {
setSelectedTeam(team);
setIsMembersModalOpen(true);
};
// Fonction pour mettre à jour les stats d'une équipe après suppression d'un membre
const updateTeamStatsAfterMemberRemoval = (teamId: string) => {
setLocalTeamStats((prev) => {
const updated = prev.map((stats) =>
stats.teamId === teamId
? { ...stats, totalMembers: Math.max(0, stats.totalMembers - 1) }
: stats
);
return updated;
});
};
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",
});
closeEditDialog();
setEditingTeam(null);
setTeamFormData({ name: "", direction: "" });
// Mettre à jour l'état local au lieu de recharger la page
// On pourrait mettre à jour les données locales ici si nécessaire
// Pour l'instant, on laisse l'utilisateur voir le changement
} 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 = getLocalTeam(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",
});
// Mettre à jour l'état local au lieu de recharger la page
// Supprimer l'équipe de la liste locale
setLocalTeams((prev) => prev.filter((t) => t.id !== teamId));
// Mettre à jour les stats locales
setLocalTeamStats((prev) =>
prev.filter((stats) => stats.teamId !== teamId)
);
} 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",
});
// Mettre à jour l'état local au lieu de recharger la page
// Supprimer les équipes de la direction de la liste locale
setLocalTeams((prev) =>
prev.filter((team) => team.direction !== direction)
);
// Supprimer les équipes de la direction des stats locales
setLocalTeamStats((prev) =>
prev.filter((stats) => stats.direction !== direction)
);
} 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(localTeams.map((team) => team.direction))
);
const headerActions = (
<Dialog open={isCreateDialogOpen} onOpenChange={closeCreateDialog}>
<DialogTrigger asChild>
<Button
onClick={() => {
resetForm();
openCreateDialog();
}}
>
<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={closeCreateDialog}>
Annuler
</Button>
<Button onClick={handleCreateTeam}>Créer</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
const 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>
);
return (
<>
<TreeViewPage
title="Gestion des Teams"
description="Créez, modifiez et supprimez les équipes de votre organisation"
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
onExpandAll={expandAll}
onCollapseAll={collapseAll}
searchPlaceholder="Rechercher une équipe..."
hasContent={Object.keys(filteredTeamsByDirection).length > 0}
emptyState={emptyState}
headerActions={headerActions}
>
{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)}
onViewMembers={() => handleViewMembers(team)}
canDelete={!stats || stats.totalMembers === 0}
showSeparator={teamIndex > 0}
hasMembers={stats ? stats.totalMembers > 0 : false}
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>
)
)}
</TreeViewPage>
{/* Dialog d'édition */}
<Dialog open={isEditDialogOpen} onOpenChange={closeEditDialog}>
<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={closeEditDialog}>
Annuler
</Button>
<Button onClick={handleUpdateTeam}>Mettre à jour</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Modal des membres d'équipe */}
{selectedTeam && (
<TeamMembersModal
teamId={selectedTeam.id}
teamName={selectedTeam.name}
isOpen={isMembersModalOpen}
onClose={() => {
setIsMembersModalOpen(false);
setSelectedTeam(null);
// Pas besoin de rafraîchir ici, les stats locales sont déjà à jour
}}
onMemberRemoved={() => {
// Mettre à jour les stats localement
if (selectedTeam) {
updateTeamStatsAfterMemberRemoval(selectedTeam.id);
}
}}
/>
)}
</>
);
}