Initial commit
This commit is contained in:
481
app/(main)/equipes/stats/page.tsx
Normal file
481
app/(main)/equipes/stats/page.tsx
Normal file
@@ -0,0 +1,481 @@
|
||||
'use client';
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user