- 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>
987 lines
46 KiB
TypeScript
987 lines
46 KiB
TypeScript
'use client';
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { Button } from 'primereact/button';
|
|
import { Calendar } from 'primereact/calendar';
|
|
import { Chart } from 'primereact/chart';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Toast } from 'primereact/toast';
|
|
import { Toolbar } from 'primereact/toolbar';
|
|
import { Dropdown } from 'primereact/dropdown';
|
|
import { TabView, TabPanel } from 'primereact/tabview';
|
|
import { Rating } from 'primereact/rating';
|
|
import { Tag } from 'primereact/tag';
|
|
import { InputText } from 'primereact/inputtext';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { Knob } from 'primereact/knob';
|
|
|
|
interface Employe {
|
|
id: string;
|
|
nom: string;
|
|
prenom: string;
|
|
poste: string;
|
|
equipe: string;
|
|
dateEmbauche: Date;
|
|
statut: 'ACTIF' | 'CONGE' | 'ARRET' | 'FORMATION';
|
|
heuresTravaillees: number;
|
|
heuresObjectif: number;
|
|
chantiersAssignes: number;
|
|
chantiersTermines: number;
|
|
tauxReussite: number;
|
|
evaluationPerformance: number;
|
|
salaireBase: number;
|
|
primes: number;
|
|
specialites: string[];
|
|
certifications: string[];
|
|
formation: number; // heures de formation
|
|
securite: number; // score sécurité
|
|
satisfaction: number; // satisfaction client
|
|
}
|
|
|
|
interface EquipePerformance {
|
|
nom: string;
|
|
chef: string;
|
|
nbMembres: number;
|
|
heuresTravaillees: number;
|
|
productivite: number;
|
|
chantiersAssignes: number;
|
|
chantiersTermines: number;
|
|
tauxReussite: number;
|
|
budgetRespect: number;
|
|
delaiRespect: number;
|
|
securiteScore: number;
|
|
satisfactionClient: number;
|
|
coutTotal: number;
|
|
chiffreAffaires: number;
|
|
rentabilite: number;
|
|
}
|
|
|
|
interface IndicateurRH {
|
|
nom: string;
|
|
valeurActuelle: number;
|
|
objectif: number;
|
|
unite: string;
|
|
tendance: 'HAUSSE' | 'BAISSE' | 'STABLE';
|
|
couleur: string;
|
|
}
|
|
|
|
const PerformanceEquipesPage = () => {
|
|
const [loading, setLoading] = useState(true);
|
|
const [employes, setEmployes] = useState<Employe[]>([]);
|
|
const [equipes, setEquipes] = useState<EquipePerformance[]>([]);
|
|
const [indicateursRH, setIndicateursRH] = useState<IndicateurRH[]>([]);
|
|
const [selectedEmployes, setSelectedEmployes] = useState<Employe[]>([]);
|
|
const [dateDebut, setDateDebut] = useState<Date>(new Date(new Date().getFullYear(), 0, 1));
|
|
const [dateFin, setDateFin] = useState<Date>(new Date());
|
|
const [selectedPeriod, setSelectedPeriod] = useState('annee');
|
|
const [selectedEquipe, setSelectedEquipe] = useState('toutes');
|
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const toast = useRef<Toast>(null);
|
|
const dt = useRef<DataTable<Employe[]>>(null);
|
|
|
|
const periodOptions = [
|
|
{ label: 'Ce mois', value: 'mois' },
|
|
{ label: 'Ce trimestre', value: 'trimestre' },
|
|
{ label: 'Cette année', value: 'annee' },
|
|
{ label: 'Personnalisé', value: 'custom' }
|
|
];
|
|
|
|
const equipeOptions = [
|
|
{ label: 'Toutes les équipes', value: 'toutes' },
|
|
{ label: 'Équipe Gros Œuvre', value: 'gros-oeuvre' },
|
|
{ label: 'Équipe Second Œuvre', value: 'second-oeuvre' },
|
|
{ label: 'Équipe Finitions', value: 'finitions' },
|
|
{ label: 'Équipe Maintenance', value: 'maintenance' }
|
|
];
|
|
|
|
useEffect(() => {
|
|
loadPerformanceData();
|
|
}, [dateDebut, dateFin, selectedEquipe]);
|
|
|
|
const loadPerformanceData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Données mockées employés
|
|
const mockEmployes: Employe[] = [
|
|
{
|
|
id: '1',
|
|
nom: 'Martin',
|
|
prenom: 'Jean',
|
|
poste: 'Chef d\'équipe',
|
|
equipe: 'gros-oeuvre',
|
|
dateEmbauche: new Date('2022-01-15'),
|
|
statut: 'ACTIF',
|
|
heuresTravaillees: 1680,
|
|
heuresObjectif: 1800,
|
|
chantiersAssignes: 8,
|
|
chantiersTermines: 7,
|
|
tauxReussite: 87.5,
|
|
evaluationPerformance: 4.5,
|
|
salaireBase: 3200,
|
|
primes: 450,
|
|
specialites: ['Maçonnerie', 'Coffrage', 'Management'],
|
|
certifications: ['CACES R389', 'Sauveteur Secouriste'],
|
|
formation: 35,
|
|
securite: 95,
|
|
satisfaction: 4.3
|
|
},
|
|
{
|
|
id: '2',
|
|
nom: 'Dubois',
|
|
prenom: 'Marie',
|
|
poste: 'Maçon',
|
|
equipe: 'gros-oeuvre',
|
|
dateEmbauche: new Date('2023-03-20'),
|
|
statut: 'ACTIF',
|
|
heuresTravaillees: 1620,
|
|
heuresObjectif: 1750,
|
|
chantiersAssignes: 6,
|
|
chantiersTermines: 6,
|
|
tauxReussite: 100,
|
|
evaluationPerformance: 4.2,
|
|
salaireBase: 2800,
|
|
primes: 320,
|
|
specialites: ['Maçonnerie', 'Carrelage'],
|
|
certifications: ['Sauveteur Secouriste'],
|
|
formation: 28,
|
|
securite: 92,
|
|
satisfaction: 4.5
|
|
},
|
|
{
|
|
id: '3',
|
|
nom: 'Durand',
|
|
prenom: 'Pierre',
|
|
poste: 'Électricien',
|
|
equipe: 'second-oeuvre',
|
|
dateEmbauche: new Date('2021-09-10'),
|
|
statut: 'ACTIF',
|
|
heuresTravaillees: 1750,
|
|
heuresObjectif: 1800,
|
|
chantiersAssignes: 10,
|
|
chantiersTermines: 9,
|
|
tauxReussite: 90,
|
|
evaluationPerformance: 4.7,
|
|
salaireBase: 3000,
|
|
primes: 500,
|
|
specialites: ['Électricité', 'Domotique', 'Photovoltaïque'],
|
|
certifications: ['Habilitation B2V', 'QualiPV'],
|
|
formation: 42,
|
|
securite: 98,
|
|
satisfaction: 4.6
|
|
},
|
|
{
|
|
id: '4',
|
|
nom: 'Bernard',
|
|
prenom: 'Sophie',
|
|
poste: 'Plombier',
|
|
equipe: 'second-oeuvre',
|
|
dateEmbauche: new Date('2023-01-08'),
|
|
statut: 'FORMATION',
|
|
heuresTravaillees: 1580,
|
|
heuresObjectif: 1750,
|
|
chantiersAssignes: 5,
|
|
chantiersTermines: 4,
|
|
tauxReussite: 80,
|
|
evaluationPerformance: 3.8,
|
|
salaireBase: 2700,
|
|
primes: 180,
|
|
specialites: ['Plomberie', 'Chauffage'],
|
|
certifications: ['PGN'],
|
|
formation: 65,
|
|
securite: 88,
|
|
satisfaction: 4.1
|
|
},
|
|
{
|
|
id: '5',
|
|
nom: 'Moreau',
|
|
prenom: 'Luc',
|
|
poste: 'Peintre',
|
|
equipe: 'finitions',
|
|
dateEmbauche: new Date('2022-06-15'),
|
|
statut: 'ACTIF',
|
|
heuresTravaillees: 1650,
|
|
heuresObjectif: 1750,
|
|
chantiersAssignes: 12,
|
|
chantiersTermines: 11,
|
|
tauxReussite: 91.7,
|
|
evaluationPerformance: 4.1,
|
|
salaireBase: 2600,
|
|
primes: 275,
|
|
specialites: ['Peinture', 'Papier peint', 'Décoration'],
|
|
certifications: ['Peinture écologique'],
|
|
formation: 18,
|
|
securite: 90,
|
|
satisfaction: 4.4
|
|
}
|
|
];
|
|
|
|
const mockEquipes: EquipePerformance[] = [
|
|
{
|
|
nom: 'Équipe Gros Œuvre',
|
|
chef: 'Jean Martin',
|
|
nbMembres: 6,
|
|
heuresTravaillees: 9800,
|
|
productivite: 92,
|
|
chantiersAssignes: 15,
|
|
chantiersTermines: 14,
|
|
tauxReussite: 93.3,
|
|
budgetRespect: 88,
|
|
delaiRespect: 85,
|
|
securiteScore: 94,
|
|
satisfactionClient: 4.3,
|
|
coutTotal: 125000,
|
|
chiffreAffaires: 180000,
|
|
rentabilite: 30.6
|
|
},
|
|
{
|
|
nom: 'Équipe Second Œuvre',
|
|
chef: 'Pierre Durand',
|
|
nbMembres: 8,
|
|
heuresTravaillees: 13600,
|
|
productivite: 95,
|
|
chantiersAssignes: 20,
|
|
chantiersTermines: 18,
|
|
tauxReussite: 90,
|
|
budgetRespect: 92,
|
|
delaiRespect: 88,
|
|
securiteScore: 96,
|
|
satisfactionClient: 4.5,
|
|
coutTotal: 168000,
|
|
chiffreAffaires: 250000,
|
|
rentabilite: 32.8
|
|
},
|
|
{
|
|
nom: 'Équipe Finitions',
|
|
chef: 'Luc Moreau',
|
|
nbMembres: 4,
|
|
heuresTravaillees: 6800,
|
|
productivite: 89,
|
|
chantiersAssignes: 25,
|
|
chantiersTermines: 23,
|
|
tauxReussite: 92,
|
|
budgetRespect: 90,
|
|
delaiRespect: 95,
|
|
securiteScore: 91,
|
|
satisfactionClient: 4.6,
|
|
coutTotal: 88000,
|
|
chiffreAffaires: 135000,
|
|
rentabilite: 34.8
|
|
},
|
|
{
|
|
nom: 'Équipe Maintenance',
|
|
chef: 'Anne Petit',
|
|
nbMembres: 3,
|
|
heuresTravaillees: 5100,
|
|
productivite: 87,
|
|
chantiersAssignes: 18,
|
|
chantiersTermines: 17,
|
|
tauxReussite: 94.4,
|
|
budgetRespect: 95,
|
|
delaiRespect: 98,
|
|
securiteScore: 97,
|
|
satisfactionClient: 4.2,
|
|
coutTotal: 65000,
|
|
chiffreAffaires: 95000,
|
|
rentabilite: 31.6
|
|
}
|
|
];
|
|
|
|
const mockIndicateursRH: IndicateurRH[] = [
|
|
{ nom: 'Productivité Globale', valeurActuelle: 93.2, objectif: 95, unite: '%', tendance: 'HAUSSE', couleur: '#10B981' },
|
|
{ nom: 'Taux de Réussite', valeurActuelle: 92.4, objectif: 90, unite: '%', tendance: 'STABLE', couleur: '#3B82F6' },
|
|
{ nom: 'Respect Délais', valeurActuelle: 91.5, objectif: 95, unite: '%', tendance: 'HAUSSE', couleur: '#F59E0B' },
|
|
{ nom: 'Score Sécurité', valeurActuelle: 94.5, objectif: 95, unite: '%', tendance: 'HAUSSE', couleur: '#EF4444' },
|
|
{ nom: 'Satisfaction Client', valeurActuelle: 4.4, objectif: 4.5, unite: '/5', tendance: 'STABLE', couleur: '#8B5CF6' },
|
|
{ nom: 'Heures Formation', valeurActuelle: 37.6, objectif: 40, unite: 'h', tendance: 'HAUSSE', couleur: '#06B6D4' }
|
|
];
|
|
|
|
setEmployes(selectedEquipe === 'toutes' ? mockEmployes : mockEmployes.filter(e => e.equipe === selectedEquipe));
|
|
setEquipes(mockEquipes);
|
|
setIndicateursRH(mockIndicateursRH);
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement:', error);
|
|
toast.current?.show({
|
|
severity: 'error',
|
|
summary: 'Erreur',
|
|
detail: 'Impossible de charger les données de performance',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const onPeriodChange = (e: any) => {
|
|
setSelectedPeriod(e.value);
|
|
|
|
const now = new Date();
|
|
let debut = new Date();
|
|
|
|
switch (e.value) {
|
|
case 'mois':
|
|
debut = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
break;
|
|
case 'trimestre':
|
|
debut = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1);
|
|
break;
|
|
case 'annee':
|
|
debut = new Date(now.getFullYear(), 0, 1);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
setDateDebut(debut);
|
|
setDateFin(now);
|
|
};
|
|
|
|
const exportPDF = () => {
|
|
toast.current?.show({
|
|
severity: 'info',
|
|
summary: 'Export PDF',
|
|
detail: 'Génération du rapport PDF...',
|
|
life: 3000
|
|
});
|
|
};
|
|
|
|
const exportExcel = () => {
|
|
dt.current?.exportCSV();
|
|
};
|
|
|
|
const leftToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex flex-wrap gap-2 align-items-center">
|
|
<Dropdown
|
|
value={selectedPeriod}
|
|
options={periodOptions}
|
|
onChange={onPeriodChange}
|
|
placeholder="Période"
|
|
/>
|
|
<Dropdown
|
|
value={selectedEquipe}
|
|
options={equipeOptions}
|
|
onChange={(e) => setSelectedEquipe(e.value)}
|
|
placeholder="Équipe"
|
|
/>
|
|
{selectedPeriod === 'custom' && (
|
|
<>
|
|
<Calendar
|
|
value={dateDebut}
|
|
onChange={(e) => setDateDebut(e.value || new Date())}
|
|
dateFormat="dd/mm/yy"
|
|
placeholder="Date début"
|
|
/>
|
|
<Calendar
|
|
value={dateFin}
|
|
onChange={(e) => setDateFin(e.value || new Date())}
|
|
dateFormat="dd/mm/yy"
|
|
placeholder="Date fin"
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const rightToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
label="PDF"
|
|
icon="pi pi-file-pdf"
|
|
severity="danger"
|
|
onClick={exportPDF}
|
|
/>
|
|
<Button
|
|
label="Excel"
|
|
icon="pi pi-file-excel"
|
|
severity="success"
|
|
onClick={exportExcel}
|
|
/>
|
|
<Button
|
|
label="Actualiser"
|
|
icon="pi pi-refresh"
|
|
onClick={loadPerformanceData}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
return new Intl.NumberFormat('fr-FR', {
|
|
style: 'currency',
|
|
currency: 'EUR'
|
|
}).format(amount);
|
|
};
|
|
|
|
const statutBodyTemplate = (rowData: Employe) => {
|
|
let severity: "success" | "warning" | "danger" | "info" = 'success';
|
|
let label: string = rowData.statut;
|
|
|
|
switch (rowData.statut) {
|
|
case 'ACTIF':
|
|
severity = 'success';
|
|
label = 'Actif';
|
|
break;
|
|
case 'CONGE':
|
|
severity = 'info';
|
|
label = 'Congé';
|
|
break;
|
|
case 'ARRET':
|
|
severity = 'danger';
|
|
label = 'Arrêt';
|
|
break;
|
|
case 'FORMATION':
|
|
severity = 'warning';
|
|
label = 'Formation';
|
|
break;
|
|
}
|
|
|
|
return <Tag value={label} severity={severity} />;
|
|
};
|
|
|
|
const performanceBodyTemplate = (rowData: Employe) => {
|
|
return <Rating value={rowData.evaluationPerformance} readOnly cancel={false} />;
|
|
};
|
|
|
|
const productiviteBodyTemplate = (rowData: Employe) => {
|
|
const productivite = (rowData.heuresTravaillees / rowData.heuresObjectif) * 100;
|
|
const color = productivite >= 95 ? 'text-green-600' : productivite >= 80 ? 'text-orange-600' : 'text-red-600';
|
|
return (
|
|
<div>
|
|
<ProgressBar value={productivite} showValue={false} />
|
|
<small className={color}>{productivite.toFixed(1)}%</small>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const reussiteBodyTemplate = (rowData: Employe) => {
|
|
const color = rowData.tauxReussite >= 90 ? 'text-green-600' :
|
|
rowData.tauxReussite >= 75 ? 'text-orange-600' : 'text-red-600';
|
|
return <span className={color}>{rowData.tauxReussite.toFixed(1)}%</span>;
|
|
};
|
|
|
|
const securiteBodyTemplate = (rowData: Employe) => {
|
|
return (
|
|
<div>
|
|
<ProgressBar value={rowData.securite} showValue={false} />
|
|
<small>{rowData.securite}%</small>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const specialitesBodyTemplate = (rowData: Employe) => {
|
|
return rowData.specialites.slice(0, 2).map((spec, index) => (
|
|
<Tag key={index} value={spec} className="mr-1 mb-1" severity="info" />
|
|
));
|
|
};
|
|
|
|
const dateEmbaucheBodyTemplate = (rowData: Employe) => {
|
|
const anciennete = Math.floor((new Date().getTime() - rowData.dateEmbauche.getTime()) / (1000 * 3600 * 24 * 365));
|
|
return (
|
|
<div>
|
|
<div>{rowData.dateEmbauche.toLocaleDateString('fr-FR')}</div>
|
|
<small className="text-color-secondary">{anciennete} an{anciennete > 1 ? 's' : ''}</small>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const salaireBodyTemplate = (rowData: Employe) => {
|
|
return (
|
|
<div>
|
|
<div>{formatCurrency(rowData.salaireBase)}</div>
|
|
{rowData.primes > 0 && (
|
|
<small className="text-green-500">+{formatCurrency(rowData.primes)} primes</small>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const header = (
|
|
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
|
|
<h5 className="m-0">Performance des Équipes</h5>
|
|
<span className="block mt-2 md:mt-0 p-input-icon-left">
|
|
<i className="pi pi-search" />
|
|
<InputText
|
|
type="search"
|
|
placeholder="Rechercher..."
|
|
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
|
|
/>
|
|
</span>
|
|
</div>
|
|
);
|
|
|
|
// Calculs pour les indicateurs globaux
|
|
const totalEmployes = employes.length;
|
|
const employesActifs = employes.filter(e => e.statut === 'ACTIF').length;
|
|
const heuresTotal = employes.reduce((sum, e) => sum + e.heuresTravaillees, 0);
|
|
const heuresObjectifTotal = employes.reduce((sum, e) => sum + e.heuresObjectif, 0);
|
|
const productiviteGlobale = heuresObjectifTotal > 0 ? (heuresTotal / heuresObjectifTotal) * 100 : 0;
|
|
const performanceMoyenne = totalEmployes > 0 ? employes.reduce((sum, e) => sum + e.evaluationPerformance, 0) / totalEmployes : 0;
|
|
|
|
// Données pour les graphiques
|
|
const equipePerformanceData = {
|
|
labels: equipes.map(e => e.nom),
|
|
datasets: [
|
|
{
|
|
label: 'Productivité (%)',
|
|
data: equipes.map(e => e.productivite),
|
|
backgroundColor: '#3B82F6',
|
|
borderColor: '#1D4ED8',
|
|
borderWidth: 1
|
|
},
|
|
{
|
|
label: 'Taux Réussite (%)',
|
|
data: equipes.map(e => e.tauxReussite),
|
|
backgroundColor: '#10B981',
|
|
borderColor: '#047857',
|
|
borderWidth: 1
|
|
}
|
|
]
|
|
};
|
|
|
|
const rentabiliteEquipeData = {
|
|
labels: equipes.map(e => e.nom),
|
|
datasets: [
|
|
{
|
|
data: equipes.map(e => e.rentabilite),
|
|
backgroundColor: ['#10B981', '#3B82F6', '#F59E0B', '#EF4444'],
|
|
hoverBackgroundColor: ['#059669', '#2563EB', '#D97706', '#DC2626']
|
|
}
|
|
]
|
|
};
|
|
|
|
const tendanceFormationData = {
|
|
labels: ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Jun'],
|
|
datasets: [
|
|
{
|
|
label: 'Heures Formation',
|
|
data: [32, 28, 35, 42, 38, 45],
|
|
borderColor: '#8B5CF6',
|
|
backgroundColor: 'rgba(139, 92, 246, 0.1)',
|
|
tension: 0.4,
|
|
fill: true
|
|
}
|
|
]
|
|
};
|
|
|
|
const chartOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom' as const
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card>
|
|
<Toast ref={toast} />
|
|
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
|
|
|
|
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
|
|
<TabPanel header="Vue d'Ensemble" leftIcon="pi pi-chart-line mr-2">
|
|
<div className="grid">
|
|
{/* Indicateurs principaux */}
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-6xl text-primary mb-2">
|
|
<i className="pi pi-users"></i>
|
|
</div>
|
|
<div className="text-3xl font-bold text-primary mb-1">
|
|
{totalEmployes}
|
|
</div>
|
|
<div className="text-lg text-color-secondary">
|
|
Total Employés
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-6xl text-green-500 mb-2">
|
|
<i className="pi pi-check-circle"></i>
|
|
</div>
|
|
<div className="text-3xl font-bold text-green-500 mb-1">
|
|
{employesActifs}
|
|
</div>
|
|
<div className="text-lg text-color-secondary">
|
|
Employés Actifs
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-6xl text-orange-500 mb-2">
|
|
<i className="pi pi-chart-line"></i>
|
|
</div>
|
|
<div className="text-3xl font-bold text-orange-500 mb-1">
|
|
{productiviteGlobale.toFixed(1)}%
|
|
</div>
|
|
<div className="text-lg text-color-secondary">
|
|
Productivité Globale
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-3">
|
|
<Card className="text-center">
|
|
<div className="text-6xl text-purple-500 mb-2">
|
|
<i className="pi pi-star"></i>
|
|
</div>
|
|
<div className="text-3xl font-bold text-purple-500 mb-1">
|
|
{performanceMoyenne.toFixed(1)}/5
|
|
</div>
|
|
<div className="text-lg text-color-secondary">
|
|
Performance Moyenne
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Indicateurs RH */}
|
|
<div className="col-12">
|
|
<Card title="Indicateurs RH">
|
|
<div className="grid">
|
|
{indicateursRH.map((indicateur, index) => (
|
|
<div key={index} className="col-12 md:col-6 lg:col-4">
|
|
<div className="text-center p-3">
|
|
<div className="flex justify-content-between align-items-center mb-2">
|
|
<span className="font-semibold">{indicateur.nom}</span>
|
|
<i className={`pi ${
|
|
indicateur.tendance === 'HAUSSE' ? 'pi-trending-up text-green-500' :
|
|
indicateur.tendance === 'BAISSE' ? 'pi-trending-down text-red-500' :
|
|
'pi-minus text-orange-500'
|
|
}`}></i>
|
|
</div>
|
|
<Knob
|
|
value={indicateur.valeurActuelle}
|
|
max={indicateur.unite === '/5' ? 5 : 100}
|
|
size={100}
|
|
valueColor={indicateur.couleur}
|
|
rangeColor="#E5E7EB"
|
|
textColor="#374151"
|
|
/>
|
|
<div className="mt-2">
|
|
<div className="font-bold">{indicateur.valeurActuelle}{indicateur.unite}</div>
|
|
<div className="text-sm text-color-secondary">
|
|
Objectif: {indicateur.objectif}{indicateur.unite}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Performance par équipe */}
|
|
<div className="col-12">
|
|
<Card title="Performance par Équipe">
|
|
<DataTable
|
|
value={equipes}
|
|
loading={loading}
|
|
emptyMessage="Aucune équipe"
|
|
>
|
|
<Column field="nom" header="Équipe" />
|
|
<Column field="chef" header="Chef d'équipe" />
|
|
<Column field="nbMembres" header="Membres" />
|
|
<Column
|
|
field="productivite"
|
|
header="Productivité"
|
|
body={(rowData) => (
|
|
<div>
|
|
<ProgressBar value={rowData.productivite} showValue={false} />
|
|
<small>{rowData.productivite}%</small>
|
|
</div>
|
|
)}
|
|
/>
|
|
<Column
|
|
field="tauxReussite"
|
|
header="Taux Réussite"
|
|
body={(rowData) => `${rowData.tauxReussite.toFixed(1)}%`}
|
|
/>
|
|
<Column
|
|
field="delaiRespect"
|
|
header="Respect Délais"
|
|
body={(rowData) => `${rowData.delaiRespect}%`}
|
|
/>
|
|
<Column
|
|
field="securiteScore"
|
|
header="Sécurité"
|
|
body={(rowData) => `${rowData.securiteScore}%`}
|
|
/>
|
|
<Column
|
|
field="satisfactionClient"
|
|
header="Satisfaction"
|
|
body={(rowData) => (
|
|
<Rating value={rowData.satisfactionClient} readOnly cancel={false} />
|
|
)}
|
|
/>
|
|
<Column
|
|
field="rentabilite"
|
|
header="Rentabilité"
|
|
body={(rowData) => `${rowData.rentabilite.toFixed(1)}%`}
|
|
/>
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Graphiques */}
|
|
<div className="col-12 md:col-8">
|
|
<Card title="Comparaison Performance Équipes">
|
|
<Chart
|
|
type="bar"
|
|
data={equipePerformanceData}
|
|
options={chartOptions}
|
|
style={{ height: '400px' }}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-4">
|
|
<Card title="Rentabilité par Équipe">
|
|
<Chart
|
|
type="doughnut"
|
|
data={rentabiliteEquipeData}
|
|
options={chartOptions}
|
|
style={{ height: '400px' }}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Employés Détaillés" leftIcon="pi pi-list mr-2">
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card>
|
|
<DataTable
|
|
ref={dt}
|
|
value={employes}
|
|
selection={selectedEmployes}
|
|
onSelectionChange={(e) => setSelectedEmployes(e.value)}
|
|
selectionMode="checkbox"
|
|
dataKey="id"
|
|
paginator
|
|
rows={10}
|
|
rowsPerPageOptions={[5, 10, 25]}
|
|
className="datatable-responsive"
|
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
|
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} employés"
|
|
globalFilter={globalFilter}
|
|
emptyMessage="Aucun employé trouvé."
|
|
header={header}
|
|
loading={loading}
|
|
>
|
|
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
|
|
<Column field="prenom" header="Prénom" sortable />
|
|
<Column field="nom" header="Nom" sortable />
|
|
<Column field="poste" header="Poste" sortable />
|
|
<Column field="equipe" header="Équipe" sortable />
|
|
<Column field="dateEmbauche" header="Embauche" body={dateEmbaucheBodyTemplate} sortable />
|
|
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
|
|
<Column field="productivite" header="Productivité" body={productiviteBodyTemplate} />
|
|
<Column field="chantiersTermines" header="Chantiers" body={(rowData) => `${rowData.chantiersTermines}/${rowData.chantiersAssignes}`} sortable />
|
|
<Column field="tauxReussite" header="Réussite" body={reussiteBodyTemplate} sortable />
|
|
<Column field="evaluationPerformance" header="Performance" body={performanceBodyTemplate} sortable />
|
|
<Column field="securite" header="Sécurité" body={securiteBodyTemplate} sortable />
|
|
<Column field="formation" header="Formation (h)" sortable />
|
|
<Column field="specialites" header="Spécialités" body={specialitesBodyTemplate} />
|
|
<Column field="salaireBase" header="Salaire" body={salaireBodyTemplate} sortable />
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Formation & Développement" leftIcon="pi pi-graduation-cap mr-2">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Évolution Formation">
|
|
<Chart
|
|
type="line"
|
|
data={tendanceFormationData}
|
|
options={chartOptions}
|
|
style={{ height: '300px' }}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-6">
|
|
<Card title="Répartition Formations par Employé">
|
|
<Chart
|
|
type="bar"
|
|
data={{
|
|
labels: employes.map(e => `${e.prenom} ${e.nom}`),
|
|
datasets: [
|
|
{
|
|
label: 'Heures Formation',
|
|
data: employes.map(e => e.formation),
|
|
backgroundColor: '#8B5CF6',
|
|
borderColor: '#7C3AED',
|
|
borderWidth: 1
|
|
}
|
|
]
|
|
}}
|
|
options={chartOptions}
|
|
style={{ height: '300px' }}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12">
|
|
<Card title="Compétences et Certifications">
|
|
<DataTable
|
|
value={employes}
|
|
loading={loading}
|
|
>
|
|
<Column field="prenom" header="Prénom" />
|
|
<Column field="nom" header="Nom" />
|
|
<Column field="poste" header="Poste" />
|
|
<Column
|
|
field="specialites"
|
|
header="Spécialités"
|
|
body={(rowData) => rowData.specialites.join(', ')}
|
|
/>
|
|
<Column
|
|
field="certifications"
|
|
header="Certifications"
|
|
body={(rowData) => rowData.certifications.join(', ')}
|
|
/>
|
|
<Column
|
|
field="formation"
|
|
header="Formation (h)"
|
|
body={(rowData) => (
|
|
<div>
|
|
<ProgressBar value={(rowData.formation / 80) * 100} showValue={false} />
|
|
<small>{rowData.formation}h / 80h objectif</small>
|
|
</div>
|
|
)}
|
|
/>
|
|
<Column
|
|
field="evaluationPerformance"
|
|
header="Performance"
|
|
body={performanceBodyTemplate}
|
|
/>
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Analyse Coûts RH" leftIcon="pi pi-money-bill mr-2">
|
|
<div className="grid">
|
|
<div className="col-12 md:col-4">
|
|
<Card title="Coût Total Masse Salariale">
|
|
<div className="text-center">
|
|
<div className="text-3xl font-bold text-primary mb-2">
|
|
{formatCurrency(employes.reduce((sum, e) => sum + e.salaireBase + e.primes, 0))}
|
|
</div>
|
|
<div className="text-color-secondary">
|
|
Salaires + Primes mensuel
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-4">
|
|
<Card title="Coût par Heure">
|
|
<div className="text-center">
|
|
<div className="text-3xl font-bold text-green-500 mb-2">
|
|
{heuresTotal > 0 ?
|
|
(employes.reduce((sum, e) => sum + e.salaireBase + e.primes, 0) / heuresTotal).toFixed(2) :
|
|
'0.00'
|
|
}€
|
|
</div>
|
|
<div className="text-color-secondary">
|
|
Coût horaire moyen
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12 md:col-4">
|
|
<Card title="ROI Formation">
|
|
<div className="text-center">
|
|
<div className="text-3xl font-bold text-orange-500 mb-2">
|
|
+12.5%
|
|
</div>
|
|
<div className="text-color-secondary">
|
|
Amélioration productivité
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<div className="col-12">
|
|
<Card title="Analyse Coûts par Employé">
|
|
<DataTable
|
|
value={employes}
|
|
loading={loading}
|
|
>
|
|
<Column field="prenom" header="Prénom" />
|
|
<Column field="nom" header="Nom" />
|
|
<Column field="poste" header="Poste" />
|
|
<Column
|
|
field="salaireBase"
|
|
header="Salaire Base"
|
|
body={(rowData) => formatCurrency(rowData.salaireBase)}
|
|
sortable
|
|
/>
|
|
<Column
|
|
field="primes"
|
|
header="Primes"
|
|
body={(rowData) => formatCurrency(rowData.primes)}
|
|
sortable
|
|
/>
|
|
<Column
|
|
field="coutTotal"
|
|
header="Coût Total"
|
|
body={(rowData) => formatCurrency(rowData.salaireBase + rowData.primes)}
|
|
sortable
|
|
/>
|
|
<Column
|
|
field="coutHeure"
|
|
header="Coût/Heure"
|
|
body={(rowData) =>
|
|
`${((rowData.salaireBase + rowData.primes) / rowData.heuresTravaillees).toFixed(2)}€`
|
|
}
|
|
/>
|
|
<Column
|
|
field="productivite"
|
|
header="Productivité"
|
|
body={productiviteBodyTemplate}
|
|
/>
|
|
<Column
|
|
field="rentabilite"
|
|
header="Rentabilité"
|
|
body={(rowData) => {
|
|
const ca = rowData.chantiersTermines * 50000; // CA estimé
|
|
const cout = rowData.salaireBase + rowData.primes;
|
|
const rentabilite = ca > 0 ? ((ca - cout) / ca) * 100 : 0;
|
|
return `${rentabilite.toFixed(1)}%`;
|
|
}}
|
|
/>
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</TabPanel>
|
|
</TabView>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PerformanceEquipesPage; |