reafctor: pages for management and split components
This commit is contained in:
3
components/admin/teams/index.ts
Normal file
3
components/admin/teams/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { TeamsManagementPage } from "./teams-management-page";
|
||||
export { TeamFormDialog } from "./team-form-dialog";
|
||||
export { TeamsList } from "./teams-list";
|
||||
96
components/admin/teams/team-form-dialog.tsx
Normal file
96
components/admin/teams/team-form-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
97
components/admin/teams/teams-list.tsx
Normal file
97
components/admin/teams/teams-list.tsx
Normal 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>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
189
components/admin/teams/teams-management-page.tsx
Normal file
189
components/admin/teams/teams-management-page.tsx
Normal 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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user