Files
GTARH/src/pages/TeamDetail.tsx
2025-12-02 17:57:33 +01:00

298 lines
14 KiB
TypeScript

import { useState, useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Calendar, ArrowLeft, Users } from "lucide-react";
import { useNavigate, useParams } from "react-router-dom";
import { toast } from "sonner";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
interface Membre {
id: number;
nom: string;
email: string;
role: string;
soldeCP: number;
soldeRTT: number;
typeContrat?: string;
Actif?: number;
}
interface Equipe {
Id: number;
nomService: string;
nombreMembres: number;
demandesEnAttente: number;
campus: string;
}
const TeamDetail = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const [equipe, setEquipe] = useState<Equipe | null>(null);
const [membres, setMembres] = useState<Membre[]>([]);
const [loadingEquipe, setLoadingEquipe] = useState(true);
const [loadingMembres, setLoadingMembres] = useState(true);
useEffect(() => {
if (id) {
chargerEquipe(parseInt(id));
chargerMembres(parseInt(id));
}
}, [id]);
const chargerEquipe = async (equipeId: number) => {
setLoadingEquipe(true);
try {
const token = localStorage.getItem('token');
const response = await fetch(`/api/equipes/${equipeId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) throw new Error('Équipe non trouvée');
const data = await response.json();
setEquipe(data);
} catch (error) {
toast.error("Erreur lors du chargement de l'équipe");
setEquipe(null);
} finally {
setLoadingEquipe(false);
}
};
const chargerMembres = async (equipeId: number) => {
setLoadingMembres(true);
try {
const token = localStorage.getItem('token');
const response = await fetch(`/api/equipes/${equipeId}/membres`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) throw new Error('Impossible de récupérer les membres');
const data = await response.json();
console.log('📊 Données membres reçues en frontend:', data);
data.forEach((m: Membre) => {
console.log(` - ${m.nom}: CP=${m.soldeCP} (type: ${typeof m.soldeCP}), RTT=${m.soldeRTT} (type: ${typeof m.soldeRTT})`);
});
setMembres(data);
} catch (error) {
console.error('❌ Erreur chargement membres:', error);
toast.error("Erreur lors du chargement des membres");
setMembres([]);
} finally {
setLoadingMembres(false);
}
};
const getRoleBadge = (role: string) => {
switch (role) {
case 'Admin':
return <Badge variant="destructive">{role}</Badge>;
case 'RH':
return <Badge variant="default">{role}</Badge>;
case 'Validateur':
return <Badge variant="secondary">{role}</Badge>;
case 'Directeur de campus':
return <Badge className="bg-purple-600">{role}</Badge>;
default:
return <Badge variant="outline">{role}</Badge>;
}
};
// ✅ Fonction pour détecter si un membre est apprenti
const estApprenti = (membre: Membre): boolean => {
return membre.typeContrat === 'Apprentissage' ||
membre.role === 'Apprenti' ||
membre.role?.toLowerCase().includes('apprenti');
};
// ✅ CORRECTION: Utiliser useMemo pour recalculer les totaux à chaque changement de membres
const totaux = useMemo(() => {
if (!membres || membres.length === 0) {
console.log('⚠️ Aucun membre pour calculer les totaux');
return { totalCP: 0, totalRTT: 0 };
}
console.log('🧮 Calcul des totaux pour', membres.length, 'membres');
const totalCP = membres.reduce((sum, m) => {
const solde = parseFloat(String(m.soldeCP)) || 0;
console.log(` - ${m.nom}: CP=${m.soldeCP} (${typeof m.soldeCP}) → ajout de ${solde}`);
return sum + solde;
}, 0);
// ✅ Exclure les apprentis du calcul des RTT
const totalRTT = membres.reduce((sum, m) => {
if (estApprenti(m)) {
console.log(` - ${m.nom}: RTT=0 (Apprenti - pas de RTT)`);
return sum;
}
const solde = parseFloat(String(m.soldeRTT)) || 0;
console.log(` - ${m.nom}: RTT=${m.soldeRTT} (${typeof m.soldeRTT}) → ajout de ${solde}`);
return sum + solde;
}, 0);
console.log('✅ Totaux calculés: CP=', totalCP, 'RTT=', totalRTT);
return { totalCP, totalRTT };
}, [membres]); // ✅ Recalculer quand membres change
if (loadingEquipe) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-muted-foreground">Chargement de l'équipe...</p>
</div>
</div>
);
}
if (!equipe) {
return (
<div className="min-h-screen flex items-center justify-center">
<p className="text-red-500 text-lg">Équipe introuvable</p>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-subtle">
<header className="bg-card border-b border-border shadow-card">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 flex items-center justify-between">
<Button
variant="ghost"
size="sm"
onClick={() => navigate("/teams")}
className="transition-smooth"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Retour
</Button>
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ backgroundColor: "#7e5aa2" }}>
<Users className="w-5 h-5 text-primary-foreground" />
</div>
<div>
<h1 className="text-2xl font-bold text-foreground">{equipe.nomService}</h1>
<p className="text-sm text-muted-foreground">{equipe.campus}</p>
</div>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<Card className="shadow-card border-0">
<CardContent className="p-6 flex justify-between items-center">
<div>
<p className="text-sm text-muted-foreground">Membres</p>
<p className="text-2xl font-bold">{equipe.nombreMembres}</p>
</div>
<Users className="w-8 h-8 text-primary" />
</CardContent>
</Card>
<Card className="shadow-card border-0">
<CardContent className="p-6 flex justify-between items-center">
<div>
<p className="text-sm text-muted-foreground">Demandes en attente</p>
<p className="text-2xl font-bold text-warning">{equipe.demandesEnAttente}</p>
</div>
<Calendar className="w-8 h-8 text-warning" />
</CardContent>
</Card>
<Card className="shadow-card border-0">
<CardContent className="p-6">
<p className="text-sm text-muted-foreground mb-1">Total CP</p>
<p className="text-2xl font-bold">{totaux.totalCP.toFixed(1)}j</p>
</CardContent>
</Card>
<Card className="shadow-card border-0">
<CardContent className="p-6">
<p className="text-sm text-muted-foreground mb-1">Total RTT</p>
<p className="text-2xl font-bold">{totaux.totalRTT.toFixed(1)}j</p>
</CardContent>
</Card>
</div>
<Card className="shadow-card border-0">
<CardHeader>
<CardTitle>Membres de l'équipe</CardTitle>
<CardDescription>
{membres.length} membre{membres.length > 1 ? 's' : ''}
</CardDescription>
</CardHeader>
<CardContent>
{loadingMembres ? (
<div className="text-center py-12">
<div className="w-16 h-16 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-muted-foreground">Chargement des membres...</p>
</div>
) : membres.length > 0 ? (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Nom</TableHead>
<TableHead>Email</TableHead>
<TableHead>Rôle</TableHead>
<TableHead className="text-right">CP restants</TableHead>
<TableHead className="text-right">RTT restants</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{membres.map((membre) => {
const isApprenti = estApprenti(membre);
return (
<TableRow key={membre.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
{membre.nom}
{isApprenti && (
<Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-300 text-xs">
Apprenti
</Badge>
)}
</div>
</TableCell>
<TableCell className="text-sm text-muted-foreground">{membre.email}</TableCell>
<TableCell>{getRoleBadge(membre.role)}</TableCell>
<TableCell className="text-right">
<span className="font-semibold">
{parseFloat(String(membre.soldeCP || 0)).toFixed(1)}j
</span>
</TableCell>
<TableCell className="text-right">
{isApprenti ? (
<span className="text-muted-foreground italic">N/A</span>
) : (
<span className="font-semibold">
{parseFloat(String(membre.soldeRTT || 0)).toFixed(1)}j
</span>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-12">
<Users className="w-16 h-16 text-muted-foreground mx-auto mb-4 opacity-50" />
<p className="text-muted-foreground text-lg">Aucun membre dans cette équipe</p>
</div>
)}
</CardContent>
</Card>
</main>
</div>
);
};
export default TeamDetail;