reafctor: pages for management and split components

This commit is contained in:
Julien Froidefond
2025-08-23 08:16:09 +02:00
parent 97d274190d
commit 2e195ca5cf
29 changed files with 1968 additions and 1607 deletions

View File

@@ -0,0 +1,3 @@
export { TeamsManagementPage } from "./teams-management-page";
export { TeamFormDialog } from "./team-form-dialog";
export { TeamsList } from "./teams-list";

View File

@@ -0,0 +1,96 @@
"use client";
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,
} from "@/components/ui/dialog";
interface TeamFormData {
name: string;
direction: string;
}
interface TeamFormDialogProps {
isOpen: boolean;
onClose: () => void;
onSubmit: () => void;
title: string;
formData: TeamFormData;
onFormDataChange: (data: TeamFormData) => void;
directions: string[];
isSubmitting?: boolean;
}
export function TeamFormDialog({
isOpen,
onClose,
onSubmit,
title,
formData,
onFormDataChange,
directions,
isSubmitting = false,
}: TeamFormDialogProps) {
const handleInputChange = (field: keyof TeamFormData, value: string) => {
onFormDataChange({ ...formData, [field]: value });
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="team-name">Nom de l'équipe *</Label>
<Input
id="team-name"
value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)}
placeholder="Ex: Équipe Frontend, Équipe Backend"
/>
</div>
<div>
<Label htmlFor="team-direction">Direction *</Label>
<Select
value={formData.direction}
onValueChange={(value) => handleInputChange("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={onClose} disabled={isSubmitting}>
Annuler
</Button>
<Button onClick={onSubmit} disabled={isSubmitting}>
{isSubmitting ? "En cours..." : title.includes("Créer") ? "Créer" : "Mettre à jour"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,97 @@
"use client";
import { Users, Building2 } from "lucide-react";
import { TreeCategoryHeader, TreeItemRow, TeamMetrics } from "@/components/admin";
import { Team as TeamType } from "@/lib/types";
import { TeamStats } from "@/services/admin-service";
interface TeamsListProps {
filteredTeamsByDirection: Record<string, TeamType[]>;
expandedDirections: Set<string>;
onToggleDirection: (direction: string) => void;
onEditTeam: (team: TeamType) => void;
onDeleteTeam: (teamId: string) => void;
onDeleteDirection: (direction: string) => void;
onViewMembers: (team: TeamType) => void;
getTeamStats: (teamId: string) => TeamStats | undefined;
}
export function TeamsList({
filteredTeamsByDirection,
expandedDirections,
onToggleDirection,
onEditTeam,
onDeleteTeam,
onDeleteDirection,
onViewMembers,
getTeamStats,
}: TeamsListProps) {
return (
<>
{Object.entries(filteredTeamsByDirection).map(
([direction, directionTeams], index) => (
<div key={direction}>
<TreeCategoryHeader
category={direction}
isExpanded={expandedDirections.has(direction)}
onToggle={() => onToggleDirection(direction)}
icon={<Building2 className="w-5 h-5 text-blue-400" />}
itemCount={directionTeams.length}
itemLabel="équipe"
showSeparator={index > 0}
onDelete={() => onDeleteDirection(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={() => onEditTeam(team)}
onDelete={() => onDeleteTeam(team.id)}
onViewMembers={() => onViewMembers(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>
)
)}
</>
);
}

View File

@@ -0,0 +1,189 @@
"use client";
import { useState } from "react";
import { Plus, Building2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { SkillCategory, Team as TeamType } from "@/lib/types";
import { TeamStats } from "@/services/admin-service";
import { TreeViewPage } from "../management/tree-view-page";
import { useTreeView } from "@/hooks/use-tree-view";
import { useFormDialog } from "@/hooks/use-form-dialog";
import { useTeamsManagement } from "@/hooks/use-teams-management";
import { TeamFormDialog } from "./team-form-dialog";
import { TeamsList } from "./teams-list";
import { TeamMembersModal } from "../management/team-members-modal";
interface TeamsManagementPageProps {
teams: TeamType[];
teamStats: TeamStats[];
skillCategories: SkillCategory[];
}
export function TeamsManagementPage({
teams,
teamStats,
skillCategories,
}: TeamsManagementPageProps) {
const [searchTerm, setSearchTerm] = useState("");
const [isMembersModalOpen, setIsMembersModalOpen] = useState(false);
const [selectedTeam, setSelectedTeam] = useState<TeamType | null>(null);
const { isCreateDialogOpen, isEditDialogOpen, openCreateDialog, closeCreateDialog, openEditDialog, closeEditDialog } = useFormDialog();
const {
teams: localTeams,
teamStats: localTeamStats,
editingTeam,
teamFormData,
isSubmitting,
directions,
setTeamFormData,
resetForm,
getTeamStats,
handleCreateTeam,
handleEditTeam,
handleUpdateTeam,
handleDeleteTeam,
handleDeleteDirection,
} = useTeamsManagement(teams, teamStats);
// Utilisation du hook factorisé pour la vue arborescente
const {
filteredDataByCategory: filteredTeamsByDirection,
expandedCategories: expandedDirections,
toggleCategory: toggleDirection,
expandAll,
collapseAll,
} = useTreeView({
data: localTeams,
searchFields: ["name"],
groupBy: (team) => team.direction,
searchTerm,
onSearchChange: setSearchTerm,
});
const handleCreateSubmit = async () => {
const success = await handleCreateTeam();
if (success) {
closeCreateDialog();
}
};
const handleEditSubmit = async () => {
const success = await handleUpdateTeam();
if (success) {
closeEditDialog();
}
};
const handleOpenCreateDialog = () => {
resetForm();
openCreateDialog();
};
const handleOpenEditDialog = (team: TeamType) => {
handleEditTeam(team);
openEditDialog();
};
const handleViewMembers = (team: TeamType) => {
setSelectedTeam(team);
setIsMembersModalOpen(true);
};
// Fonction pour mettre à jour les stats d'une équipe après suppression d'un membre
const updateTeamStatsAfterMemberRemoval = (teamId: string) => {
// Cette logique sera gérée par le hook useTeamsManagement
};
const headerActions = (
<Button onClick={handleOpenCreateDialog}>
<Plus className="w-4 h-4 mr-2" />
Nouvelle Équipe
</Button>
);
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}
>
<TeamsList
filteredTeamsByDirection={filteredTeamsByDirection}
expandedDirections={expandedDirections}
onToggleDirection={toggleDirection}
onEditTeam={handleOpenEditDialog}
onDeleteTeam={handleDeleteTeam}
onDeleteDirection={handleDeleteDirection}
onViewMembers={handleViewMembers}
getTeamStats={getTeamStats}
/>
</TreeViewPage>
{/* Dialog de création */}
<TeamFormDialog
isOpen={isCreateDialogOpen}
onClose={closeCreateDialog}
onSubmit={handleCreateSubmit}
title="Créer une nouvelle équipe"
formData={teamFormData}
onFormDataChange={setTeamFormData}
directions={directions}
isSubmitting={isSubmitting}
/>
{/* Dialog d'édition */}
<TeamFormDialog
isOpen={isEditDialogOpen}
onClose={closeEditDialog}
onSubmit={handleEditSubmit}
title="Modifier l'équipe"
formData={teamFormData}
onFormDataChange={setTeamFormData}
directions={directions}
isSubmitting={isSubmitting}
/>
{/* Modal des membres d'équipe */}
{selectedTeam && (
<TeamMembersModal
teamId={selectedTeam.id}
teamName={selectedTeam.name}
isOpen={isMembersModalOpen}
onClose={() => {
setIsMembersModalOpen(false);
setSelectedTeam(null);
}}
onMemberRemoved={() => {
if (selectedTeam) {
updateTeamStatsAfterMemberRemoval(selectedTeam.id);
}
}}
/>
)}
</>
);
}