Files
btpxpress-frontend/app/(main)/equipes/stats/page.tsx
dahoud a8825a058b Fix: Corriger toutes les erreurs de build du frontend
- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts
- Ajout des propriétés manquantes aux objets User mockés
- Conversion des dates de string vers objets Date
- Correction des appels asynchrones et des types incompatibles
- Ajout de dynamic rendering pour résoudre les erreurs useSearchParams
- Enveloppement de useSearchParams dans Suspense boundary
- Configuration de force-dynamic au niveau du layout principal

Build réussi: 126 pages générées avec succès

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 13:23:08 +00:00

484 lines
18 KiB
TypeScript

'use client';
export const dynamic = 'force-dynamic';
import React, { useState, useEffect } from 'react';
import { Card } from 'primereact/card';
import { Chart } from 'primereact/chart';
import { Button } from 'primereact/button';
import { Toolbar } from 'primereact/toolbar';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Tag } from 'primereact/tag';
import { Badge } from 'primereact/badge';
import { ProgressBar } from 'primereact/progressbar';
import { useRouter } from 'next/navigation';
import { apiClient } from '../../../../services/api-client';
interface StatistiquesEquipes {
totalEquipes: number;
equipesActives: number;
equipesInactives: number;
equipesDisponibles: number;
equipesEnMission: number;
repartitionParSpecialite: { [key: string]: number };
tauxOccupationMoyen: number;
tauxOccupationParSpecialite: { [key: string]: number };
performanceMoyenne: number;
performanceParSpecialite: { [key: string]: number };
coutMoyenJournalier: number;
coutParSpecialite: { [key: string]: number };
nombreEmployesMoyen: number;
repartitionTailleEquipes: { [key: string]: number };
topEquipesPerformantes: Array<{
id: number;
nom: string;
specialite: string;
evaluationPerformance: number;
nombreMissions: number;
tauxReussite: number;
}>;
equipesProblematiques: Array<{
id: number;
nom: string;
specialite: string;
problemes: string[];
tauxOccupation: number;
}>;
evolutionMensuelle: Array<{
mois: string;
nombreEquipes: number;
tauxOccupation: number;
performanceMoyenne: number;
}>;
}
const StatistiquesEquipesPage = () => {
const [stats, setStats] = useState<StatistiquesEquipes | null>(null);
const [loading, setLoading] = useState(true);
const [chartOptions] = useState({
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
});
const router = useRouter();
useEffect(() => {
loadStatistiques();
}, []);
const loadStatistiques = async () => {
try {
setLoading(true);
console.log('🔄 Chargement des statistiques équipes...');
const response = await apiClient.get('/api/equipes/statistiques');
console.log('✅ Statistiques équipes chargées:', response.data);
setStats(response.data);
} catch (error) {
console.error('❌ Erreur lors du chargement des statistiques:', error);
} finally {
setLoading(false);
}
};
const getStatutChartData = () => {
if (!stats) return {};
return {
labels: ['Actives', 'Disponibles', 'En mission', 'Inactives'],
datasets: [{
data: [
stats.equipesActives,
stats.equipesDisponibles,
stats.equipesEnMission,
stats.equipesInactives
],
backgroundColor: [
'#4CAF50',
'#2196F3',
'#FF9800',
'#F44336'
],
borderWidth: 2
}]
};
};
const getSpecialiteChartData = () => {
if (!stats) return {};
return {
labels: Object.keys(stats.repartitionParSpecialite),
datasets: [{
data: Object.values(stats.repartitionParSpecialite),
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF',
'#FF9F40',
'#FF6384',
'#C9CBCF'
],
borderWidth: 2
}]
};
};
const getOccupationChartData = () => {
if (!stats) return {};
return {
labels: Object.keys(stats.tauxOccupationParSpecialite),
datasets: [{
label: 'Taux d\'occupation (%)',
data: Object.values(stats.tauxOccupationParSpecialite),
backgroundColor: '#36A2EB',
borderColor: '#36A2EB',
borderWidth: 1
}]
};
};
const getEvolutionChartData = () => {
if (!stats) return {};
return {
labels: stats.evolutionMensuelle.map(e => e.mois),
datasets: [
{
label: 'Nombre d\'équipes',
data: stats.evolutionMensuelle.map(e => e.nombreEquipes),
borderColor: '#4CAF50',
backgroundColor: 'rgba(76, 175, 80, 0.1)',
yAxisID: 'y'
},
{
label: 'Taux d\'occupation (%)',
data: stats.evolutionMensuelle.map(e => e.tauxOccupation),
borderColor: '#FF9800',
backgroundColor: 'rgba(255, 152, 0, 0.1)',
yAxisID: 'y1'
}
]
};
};
const getEvolutionChartOptions = () => {
return {
...chartOptions,
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
},
y1: {
type: 'linear',
display: true,
position: 'right',
grid: {
drawOnChartArea: false,
},
},
}
};
};
const performanceBodyTemplate = (rowData: any) => {
return (
<div className="flex align-items-center gap-2">
<Tag value={`${rowData.evaluationPerformance}/5`} severity="success" />
<div className="flex">
{[1, 2, 3, 4, 5].map((star) => (
<i
key={star}
className={`pi pi-star${star <= rowData.evaluationPerformance ? '-fill' : ''} text-yellow-500`}
style={{ fontSize: '0.8rem' }}
/>
))}
</div>
</div>
);
};
const tauxReussiteBodyTemplate = (rowData: any) => {
const getColor = (taux: number) => {
if (taux >= 90) return 'success';
if (taux >= 75) return 'info';
if (taux >= 60) return 'warning';
return 'danger';
};
return (
<div className="flex align-items-center gap-2">
<Tag value={`${rowData.tauxReussite}%`} severity={getColor(rowData.tauxReussite)} />
<ProgressBar value={rowData.tauxReussite} className="w-6rem" />
</div>
);
};
const problemesBodyTemplate = (rowData: any) => {
return (
<div className="flex flex-wrap gap-1">
{rowData.problemes?.slice(0, 2).map((probleme: string, index: number) => (
<Tag key={index} value={probleme} severity="danger" className="p-tag-sm" />
))}
{rowData.problemes?.length > 2 && (
<Badge value={`+${rowData.problemes.length - 2}`} severity="danger" />
)}
</div>
);
};
const actionBodyTemplate = (rowData: any) => {
return (
<Button
icon="pi pi-eye"
className="p-button-rounded p-button-info p-button-sm"
onClick={() => router.push(`/equipes/${rowData.id}`)}
tooltip="Voir détails"
/>
);
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<Button
label="Retour aux équipes"
icon="pi pi-arrow-left"
className="p-button-outlined"
onClick={() => router.push('/equipes')}
/>
<Button
label="Export PDF"
icon="pi pi-file-pdf"
className="p-button-danger"
onClick={() => console.log('Export PDF')}
/>
<Button
label="Export Excel"
icon="pi pi-file-excel"
className="p-button-success"
onClick={() => console.log('Export Excel')}
/>
</div>
);
};
const rightToolbarTemplate = () => {
return (
<Button
icon="pi pi-refresh"
className="p-button-outlined"
onClick={loadStatistiques}
tooltip="Actualiser"
/>
);
};
if (loading) {
return (
<div className="grid">
<div className="col-12">
<Card>
<div className="flex justify-content-center">
<i className="pi pi-spin pi-spinner" style={{ fontSize: '2rem' }} />
</div>
</Card>
</div>
</div>
);
}
if (!stats) {
return (
<div className="grid">
<div className="col-12">
<Card>
<div className="text-center">
<p>Aucune donnée disponible</p>
</div>
</Card>
</div>
</div>
);
}
return (
<div className="grid">
<div className="col-12">
<Toolbar
className="mb-4"
left={leftToolbarTemplate}
right={rightToolbarTemplate}
/>
</div>
{/* Métriques principales */}
<div className="col-12 lg:col-3 md:col-6">
<Card className="bg-blue-100">
<div className="flex justify-content-between align-items-center">
<div>
<span className="block text-500 font-medium mb-3">Total Équipes</span>
<div className="text-900 font-medium text-xl">{stats.totalEquipes}</div>
</div>
<div className="flex align-items-center justify-content-center bg-blue-500 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
<i className="pi pi-users text-white text-xl" />
</div>
</div>
</Card>
</div>
<div className="col-12 lg:col-3 md:col-6">
<Card className="bg-green-100">
<div className="flex justify-content-between align-items-center">
<div>
<span className="block text-500 font-medium mb-3">Équipes Actives</span>
<div className="text-900 font-medium text-xl">{stats.equipesActives}</div>
</div>
<div className="flex align-items-center justify-content-center bg-green-500 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
<i className="pi pi-check-circle text-white text-xl" />
</div>
</div>
</Card>
</div>
<div className="col-12 lg:col-3 md:col-6">
<Card className="bg-orange-100">
<div className="flex justify-content-between align-items-center">
<div>
<span className="block text-500 font-medium mb-3">Taux Occupation</span>
<div className="text-900 font-medium text-xl">{stats.tauxOccupationMoyen}%</div>
</div>
<div className="flex align-items-center justify-content-center bg-orange-500 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
<i className="pi pi-chart-pie text-white text-xl" />
</div>
</div>
</Card>
</div>
<div className="col-12 lg:col-3 md:col-6">
<Card className="bg-purple-100">
<div className="flex justify-content-between align-items-center">
<div>
<span className="block text-500 font-medium mb-3">Performance Moyenne</span>
<div className="text-900 font-medium text-xl">{stats.performanceMoyenne}/5</div>
</div>
<div className="flex align-items-center justify-content-center bg-purple-500 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
<i className="pi pi-star text-white text-xl" />
</div>
</div>
</Card>
</div>
{/* Graphiques */}
<div className="col-12 lg:col-6">
<Card title="Répartition par Statut">
<Chart type="doughnut" data={getStatutChartData()} options={chartOptions} style={{ height: '300px' }} />
</Card>
</div>
<div className="col-12 lg:col-6">
<Card title="Répartition par Spécialité">
<Chart type="pie" data={getSpecialiteChartData()} options={chartOptions} style={{ height: '300px' }} />
</Card>
</div>
<div className="col-12">
<Card title="Taux d'Occupation par Spécialité">
<Chart type="bar" data={getOccupationChartData()} options={chartOptions} style={{ height: '300px' }} />
</Card>
</div>
<div className="col-12">
<Card title="Évolution Mensuelle">
<Chart type="line" data={getEvolutionChartData()} options={getEvolutionChartOptions()} style={{ height: '300px' }} />
</Card>
</div>
{/* Tableaux de données */}
<div className="col-12 lg:col-6">
<Card title="Top Équipes Performantes">
<DataTable value={stats.topEquipesPerformantes} responsiveLayout="scroll">
<Column field="nom" header="Nom" />
<Column field="specialite" header="Spécialité" />
<Column field="evaluationPerformance" header="Performance" body={performanceBodyTemplate} />
<Column field="nombreMissions" header="Missions" />
<Column field="tauxReussite" header="Taux réussite" body={tauxReussiteBodyTemplate} />
<Column body={actionBodyTemplate} header="Actions" />
</DataTable>
</Card>
</div>
<div className="col-12 lg:col-6">
<Card title="Équipes Problématiques">
<DataTable value={stats.equipesProblematiques} responsiveLayout="scroll">
<Column field="nom" header="Nom" />
<Column field="specialite" header="Spécialité" />
<Column field="problemes" header="Problèmes" body={problemesBodyTemplate} />
<Column
field="tauxOccupation"
header="Occupation"
body={(rowData) => <Tag value={`${rowData.tauxOccupation}%`} severity="warning" />}
/>
<Column body={actionBodyTemplate} header="Actions" />
</DataTable>
</Card>
</div>
{/* Informations supplémentaires */}
<div className="col-12">
<Card title="Informations Complémentaires">
<div className="grid">
<div className="col-12 md:col-3">
<div className="text-center">
<i className="pi pi-users text-4xl text-blue-500 mb-3" />
<h4>Taille Moyenne</h4>
<p className="text-600">
{stats.nombreEmployesMoyen} employés par équipe
</p>
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center">
<i className="pi pi-euro text-4xl text-green-500 mb-3" />
<h4>Coût Moyen</h4>
<p className="text-600">
{stats.coutMoyenJournalier.toLocaleString('fr-FR')} /jour
</p>
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center">
<i className="pi pi-check-circle text-4xl text-orange-500 mb-3" />
<h4>Disponibles</h4>
<p className="text-600">
{stats.equipesDisponibles} équipes disponibles
</p>
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center">
<i className="pi pi-clock text-4xl text-purple-500 mb-3" />
<h4>En Mission</h4>
<p className="text-600">
{stats.equipesEnMission} équipes en mission
</p>
</div>
</div>
</div>
</Card>
</div>
</div>
);
};
export default StatistiquesEquipesPage;