Initial commit
This commit is contained in:
364
app/(main)/employes/[id]/page.tsx
Normal file
364
app/(main)/employes/[id]/page.tsx
Normal file
@@ -0,0 +1,364 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Toolbar } from 'primereact/toolbar';
|
||||
import { TabView, TabPanel } from 'primereact/tabview';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { Badge } from 'primereact/badge';
|
||||
import { useRouter, useParams } from 'next/navigation';
|
||||
import { apiClient } from '../../../../services/api-client';
|
||||
|
||||
interface EmployeDetail {
|
||||
id: number;
|
||||
nom: string;
|
||||
prenom: string;
|
||||
email: string;
|
||||
telephone: string;
|
||||
metier: string;
|
||||
statut: string;
|
||||
dateEmbauche: string;
|
||||
salaire: number;
|
||||
adresse: string;
|
||||
numeroSecu: string;
|
||||
niveauExperience: string;
|
||||
competences: string[];
|
||||
certifications: string[];
|
||||
equipeId?: number;
|
||||
equipeNom?: string;
|
||||
chantierActuel?: string;
|
||||
disponible: boolean;
|
||||
planning: Array<{
|
||||
id: number;
|
||||
chantierNom: string;
|
||||
dateDebut: string;
|
||||
dateFin: string;
|
||||
statut: string;
|
||||
}>;
|
||||
historique: Array<{
|
||||
id: number;
|
||||
chantierNom: string;
|
||||
dateDebut: string;
|
||||
dateFin: string;
|
||||
role: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const EmployeDetailPage = () => {
|
||||
const [employe, setEmploye] = useState<EmployeDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const employeId = params.id;
|
||||
|
||||
useEffect(() => {
|
||||
if (employeId) {
|
||||
loadEmployeDetail();
|
||||
}
|
||||
}, [employeId]);
|
||||
|
||||
const loadEmployeDetail = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('🔄 Chargement du détail employé...', employeId);
|
||||
const response = await apiClient.get(`/api/employes/${employeId}`);
|
||||
console.log('✅ Détail employé chargé:', response.data);
|
||||
setEmploye(response.data);
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors du chargement du détail:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleStatutEmploye = async () => {
|
||||
if (!employe) return;
|
||||
|
||||
try {
|
||||
const newStatut = employe.statut === 'ACTIF' ? 'INACTIF' : 'ACTIF';
|
||||
const endpoint = newStatut === 'ACTIF' ? 'activer' : 'desactiver';
|
||||
|
||||
await apiClient.post(`/api/employes/${employe.id}/${endpoint}`);
|
||||
|
||||
setEmploye({ ...employe, statut: newStatut });
|
||||
console.log(`✅ Statut employé ${newStatut.toLowerCase()}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors du changement de statut:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getStatutSeverity = (statut: string) => {
|
||||
switch (statut) {
|
||||
case 'ACTIF': return 'success';
|
||||
case 'INACTIF': return 'danger';
|
||||
case 'CONGE': return 'warning';
|
||||
case 'FORMATION': return 'info';
|
||||
default: return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
const getMetierColor = (metier: string) => {
|
||||
const colors: { [key: string]: string } = {
|
||||
'MACON': 'info',
|
||||
'ELECTRICIEN': 'warning',
|
||||
'PLOMBIER': 'primary',
|
||||
'CHARPENTIER': 'success',
|
||||
'PEINTRE': 'secondary',
|
||||
'CHEF_EQUIPE': 'danger',
|
||||
'CONDUCTEUR': 'help'
|
||||
};
|
||||
return colors[metier] || 'secondary';
|
||||
};
|
||||
|
||||
const leftToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
label="Retour"
|
||||
icon="pi pi-arrow-left"
|
||||
className="p-button-outlined"
|
||||
onClick={() => router.push('/employes')}
|
||||
/>
|
||||
<Button
|
||||
label="Modifier"
|
||||
icon="pi pi-pencil"
|
||||
className="p-button-success"
|
||||
onClick={() => router.push(`/employes/${employeId}/edit`)}
|
||||
/>
|
||||
<Button
|
||||
label={employe?.statut === 'ACTIF' ? 'Désactiver' : 'Activer'}
|
||||
icon={employe?.statut === 'ACTIF' ? 'pi pi-pause' : 'pi pi-play'}
|
||||
className={employe?.statut === 'ACTIF' ? 'p-button-warning' : 'p-button-success'}
|
||||
onClick={toggleStatutEmploye}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const rightToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
label="Planning"
|
||||
icon="pi pi-calendar"
|
||||
className="p-button-info"
|
||||
onClick={() => router.push(`/planning?employeId=${employeId}`)}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
className="p-button-outlined"
|
||||
onClick={loadEmployeDetail}
|
||||
tooltip="Actualiser"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (!employe) {
|
||||
return (
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<div className="text-center">
|
||||
<p>Employé non trouvé</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<Toolbar
|
||||
className="mb-4"
|
||||
left={leftToolbarTemplate}
|
||||
right={rightToolbarTemplate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* En-tête employé */}
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<div className="flex flex-column lg:flex-row lg:align-items-center gap-4">
|
||||
<div className="flex align-items-center justify-content-center bg-blue-100 border-round" style={{ width: '4rem', height: '4rem' }}>
|
||||
<i className="pi pi-user text-blue-500 text-2xl" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="m-0 mb-2">{employe.prenom} {employe.nom}</h2>
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
<Tag value={employe.metier} severity={getMetierColor(employe.metier)} />
|
||||
<Tag value={employe.statut} severity={getStatutSeverity(employe.statut)} />
|
||||
<Tag value={employe.niveauExperience} severity="info" />
|
||||
{employe.disponible ? (
|
||||
<Tag value="Disponible" severity="success" icon="pi pi-check" />
|
||||
) : (
|
||||
<Tag value="Occupé" severity="warning" icon="pi pi-clock" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-600">
|
||||
<p className="m-0">📧 {employe.email}</p>
|
||||
<p className="m-0">📞 {employe.telephone}</p>
|
||||
{employe.equipeNom && <p className="m-0">👥 Équipe: {employe.equipeNom}</p>}
|
||||
{employe.chantierActuel && <p className="m-0">🏗️ Chantier actuel: {employe.chantierActuel}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Onglets de détail */}
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<TabView>
|
||||
<TabPanel header="Informations Générales">
|
||||
<div className="grid">
|
||||
<div className="col-12 md:col-6">
|
||||
<h4>Informations Personnelles</h4>
|
||||
<div className="field">
|
||||
<label className="font-medium">Nom complet:</label>
|
||||
<p>{employe.prenom} {employe.nom}</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="font-medium">Email:</label>
|
||||
<p>{employe.email}</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="font-medium">Téléphone:</label>
|
||||
<p>{employe.telephone}</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="font-medium">Adresse:</label>
|
||||
<p>{employe.adresse || 'Non renseignée'}</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="font-medium">N° Sécurité Sociale:</label>
|
||||
<p>{employe.numeroSecu || 'Non renseigné'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 md:col-6">
|
||||
<h4>Informations Professionnelles</h4>
|
||||
<div className="field">
|
||||
<label className="font-medium">Métier:</label>
|
||||
<p><Tag value={employe.metier} severity={getMetierColor(employe.metier)} /></p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="font-medium">Niveau d'expérience:</label>
|
||||
<p><Tag value={employe.niveauExperience} severity="info" /></p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="font-medium">Date d'embauche:</label>
|
||||
<p>{new Date(employe.dateEmbauche).toLocaleDateString('fr-FR')}</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="font-medium">Salaire:</label>
|
||||
<p>{employe.salaire ? `${employe.salaire.toLocaleString('fr-FR')} €` : 'Non renseigné'}</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="font-medium">Statut:</label>
|
||||
<p><Tag value={employe.statut} severity={getStatutSeverity(employe.statut)} /></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Compétences & Certifications">
|
||||
<div className="grid">
|
||||
<div className="col-12 md:col-6">
|
||||
<h4>Compétences</h4>
|
||||
{employe.competences && employe.competences.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{employe.competences.map((comp, index) => (
|
||||
<Tag key={index} value={comp} severity="info" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-500">Aucune compétence renseignée</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 md:col-6">
|
||||
<h4>Certifications</h4>
|
||||
{employe.certifications && employe.certifications.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{employe.certifications.map((cert, index) => (
|
||||
<Tag key={index} value={cert} severity="success" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-500">Aucune certification renseignée</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Planning Actuel">
|
||||
<DataTable
|
||||
value={employe.planning || []}
|
||||
emptyMessage="Aucun planning en cours"
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column field="chantierNom" header="Chantier" />
|
||||
<Column
|
||||
field="dateDebut"
|
||||
header="Date début"
|
||||
body={(rowData) => new Date(rowData.dateDebut).toLocaleDateString('fr-FR')}
|
||||
/>
|
||||
<Column
|
||||
field="dateFin"
|
||||
header="Date fin"
|
||||
body={(rowData) => new Date(rowData.dateFin).toLocaleDateString('fr-FR')}
|
||||
/>
|
||||
<Column
|
||||
field="statut"
|
||||
header="Statut"
|
||||
body={(rowData) => <Tag value={rowData.statut} severity="info" />}
|
||||
/>
|
||||
</DataTable>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel header="Historique">
|
||||
<DataTable
|
||||
value={employe.historique || []}
|
||||
emptyMessage="Aucun historique disponible"
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column field="chantierNom" header="Chantier" />
|
||||
<Column
|
||||
field="dateDebut"
|
||||
header="Date début"
|
||||
body={(rowData) => new Date(rowData.dateDebut).toLocaleDateString('fr-FR')}
|
||||
/>
|
||||
<Column
|
||||
field="dateFin"
|
||||
header="Date fin"
|
||||
body={(rowData) => new Date(rowData.dateFin).toLocaleDateString('fr-FR')}
|
||||
/>
|
||||
<Column field="role" header="Rôle" />
|
||||
</DataTable>
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployeDetailPage;
|
||||
264
app/(main)/employes/actifs/page.tsx
Normal file
264
app/(main)/employes/actifs/page.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { Button } from 'primereact/button';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Toolbar } from 'primereact/toolbar';
|
||||
import { Badge } from 'primereact/badge';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { apiClient } from '../../../../services/api-client';
|
||||
|
||||
interface EmployeActif {
|
||||
id: number;
|
||||
nom: string;
|
||||
prenom: string;
|
||||
email: string;
|
||||
telephone: string;
|
||||
metier: string;
|
||||
statut: string;
|
||||
dateEmbauche: string;
|
||||
equipeId?: number;
|
||||
equipeNom?: string;
|
||||
competences: string[];
|
||||
certifications: string[];
|
||||
niveauExperience: string;
|
||||
disponible: boolean;
|
||||
chantierActuel?: string;
|
||||
}
|
||||
|
||||
const EmployesActifsPage = () => {
|
||||
const [employes, setEmployes] = useState<EmployeActif[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
loadEmployesActifs();
|
||||
}, []);
|
||||
|
||||
const loadEmployesActifs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('🔄 Chargement des employés actifs...');
|
||||
const response = await apiClient.get('/api/employes/actifs');
|
||||
console.log('✅ Employés actifs chargés:', response.data);
|
||||
setEmployes(response.data || []);
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors du chargement des employés actifs:', error);
|
||||
setEmployes([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const metierBodyTemplate = (rowData: EmployeActif) => {
|
||||
const getMetierColor = (metier: string) => {
|
||||
const colors: { [key: string]: string } = {
|
||||
'MACON': 'info',
|
||||
'ELECTRICIEN': 'warning',
|
||||
'PLOMBIER': 'primary',
|
||||
'CHARPENTIER': 'success',
|
||||
'PEINTRE': 'secondary',
|
||||
'CHEF_EQUIPE': 'danger',
|
||||
'CONDUCTEUR': 'help'
|
||||
};
|
||||
return colors[metier] || 'secondary';
|
||||
};
|
||||
|
||||
return <Tag value={rowData.metier} severity={getMetierColor(rowData.metier)} />;
|
||||
};
|
||||
|
||||
const disponibiliteBodyTemplate = (rowData: EmployeActif) => {
|
||||
return (
|
||||
<div className="flex align-items-center gap-2">
|
||||
<i className={`pi ${rowData.disponible ? 'pi-check-circle text-green-500' : 'pi-times-circle text-red-500'}`} />
|
||||
<span>{rowData.disponible ? 'Disponible' : 'Occupé'}</span>
|
||||
{rowData.chantierActuel && (
|
||||
<Badge value={rowData.chantierActuel} severity="info" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const equipeBodyTemplate = (rowData: EmployeActif) => {
|
||||
if (rowData.equipeNom) {
|
||||
return <Tag value={rowData.equipeNom} severity="success" />;
|
||||
}
|
||||
return <span className="text-500">Non assigné</span>;
|
||||
};
|
||||
|
||||
const experienceBodyTemplate = (rowData: EmployeActif) => {
|
||||
const getExperienceColor = (niveau: string) => {
|
||||
switch (niveau) {
|
||||
case 'DEBUTANT': return 'info';
|
||||
case 'INTERMEDIAIRE': return 'warning';
|
||||
case 'EXPERIMENTE': return 'success';
|
||||
case 'EXPERT': return 'danger';
|
||||
default: return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
return <Tag value={rowData.niveauExperience} severity={getExperienceColor(rowData.niveauExperience)} />;
|
||||
};
|
||||
|
||||
const competencesBodyTemplate = (rowData: EmployeActif) => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{rowData.competences?.slice(0, 2).map((comp, index) => (
|
||||
<Tag key={index} value={comp} className="p-tag-sm" />
|
||||
))}
|
||||
{rowData.competences?.length > 2 && (
|
||||
<Badge value={`+${rowData.competences.length - 2}`} severity="info" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const certificationsBodyTemplate = (rowData: EmployeActif) => {
|
||||
if (!rowData.certifications || rowData.certifications.length === 0) {
|
||||
return <span className="text-500">Aucune</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{rowData.certifications.slice(0, 2).map((cert, index) => (
|
||||
<Tag key={index} value={cert} severity="help" className="p-tag-sm" />
|
||||
))}
|
||||
{rowData.certifications.length > 2 && (
|
||||
<Badge value={`+${rowData.certifications.length - 2}`} severity="help" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const actionBodyTemplate = (rowData: EmployeActif) => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
className="p-button-rounded p-button-info p-button-sm"
|
||||
onClick={() => router.push(`/employes/${rowData.id}`)}
|
||||
tooltip="Voir détails"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-calendar"
|
||||
className="p-button-rounded p-button-success p-button-sm"
|
||||
onClick={() => router.push(`/employes/${rowData.id}/planning`)}
|
||||
tooltip="Planning"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-users"
|
||||
className="p-button-rounded p-button-help p-button-sm"
|
||||
onClick={() => router.push(`/equipes?employeId=${rowData.id}`)}
|
||||
tooltip="Affecter à une équipe"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const leftToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
label="Tous les employés"
|
||||
icon="pi pi-arrow-left"
|
||||
className="p-button-outlined"
|
||||
onClick={() => router.push('/employes')}
|
||||
/>
|
||||
<Button
|
||||
label="Disponibles"
|
||||
icon="pi pi-calendar-check"
|
||||
className="p-button-success"
|
||||
onClick={() => router.push('/employes/disponibles')}
|
||||
/>
|
||||
<Button
|
||||
label="Nouvel employé"
|
||||
icon="pi pi-plus"
|
||||
className="p-button-primary"
|
||||
onClick={() => router.push('/employes/nouveau')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const rightToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<span className="p-input-icon-left">
|
||||
<i className="pi pi-search" />
|
||||
<InputText
|
||||
type="search"
|
||||
placeholder="Rechercher..."
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
className="p-button-outlined"
|
||||
onClick={loadEmployesActifs}
|
||||
tooltip="Actualiser"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const header = (
|
||||
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
|
||||
<h2 className="m-0">Employés Actifs ({employes.length})</h2>
|
||||
<div className="flex gap-2 mt-2 md:mt-0">
|
||||
<Badge value={employes.filter(e => e.disponible).length} severity="success" />
|
||||
<span>Disponibles</span>
|
||||
<Badge value={employes.filter(e => !e.disponible).length} severity="warning" />
|
||||
<span>Occupés</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<Toolbar
|
||||
className="mb-4"
|
||||
left={leftToolbarTemplate}
|
||||
right={rightToolbarTemplate}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
value={employes}
|
||||
loading={loading}
|
||||
dataKey="id"
|
||||
paginator
|
||||
rows={10}
|
||||
rowsPerPageOptions={[5, 10, 25, 50]}
|
||||
className="datatable-responsive"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} employés actifs"
|
||||
globalFilter={globalFilter}
|
||||
emptyMessage="Aucun employé actif trouvé."
|
||||
header={header}
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column field="nom" header="Nom" sortable />
|
||||
<Column field="prenom" header="Prénom" sortable />
|
||||
<Column field="metier" header="Métier" body={metierBodyTemplate} sortable />
|
||||
<Column field="niveauExperience" header="Expérience" body={experienceBodyTemplate} sortable />
|
||||
<Column field="equipeNom" header="Équipe" body={equipeBodyTemplate} sortable />
|
||||
<Column field="disponible" header="Disponibilité" body={disponibiliteBodyTemplate} sortable />
|
||||
<Column field="competences" header="Compétences" body={competencesBodyTemplate} />
|
||||
<Column field="certifications" header="Certifications" body={certificationsBodyTemplate} />
|
||||
<Column field="email" header="Email" sortable />
|
||||
<Column body={actionBodyTemplate} header="Actions" style={{ minWidth: '10rem' }} />
|
||||
</DataTable>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployesActifsPage;
|
||||
307
app/(main)/employes/disponibles/page.tsx
Normal file
307
app/(main)/employes/disponibles/page.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { Button } from 'primereact/button';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Toolbar } from 'primereact/toolbar';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Badge } from 'primereact/badge';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { apiClient } from '../../../../services/api-client';
|
||||
|
||||
interface EmployeDisponible {
|
||||
id: number;
|
||||
nom: string;
|
||||
prenom: string;
|
||||
email: string;
|
||||
telephone: string;
|
||||
metier: string;
|
||||
niveauExperience: string;
|
||||
competences: string[];
|
||||
certifications: string[];
|
||||
equipeId?: number;
|
||||
equipeNom?: string;
|
||||
prochainChantier?: string;
|
||||
dateProchainChantier?: string;
|
||||
disponibleJusquau?: string;
|
||||
}
|
||||
|
||||
const EmployesDisponiblesPage = () => {
|
||||
const [employes, setEmployes] = useState<EmployeDisponible[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
const [dateRecherche, setDateRecherche] = useState<Date | null>(null);
|
||||
const [metierFiltre, setMetierFiltre] = useState<string>('');
|
||||
const router = useRouter();
|
||||
|
||||
const metierOptions = [
|
||||
{ label: 'Tous les métiers', value: '' },
|
||||
{ label: 'Maçon', value: 'MACON' },
|
||||
{ label: 'Électricien', value: 'ELECTRICIEN' },
|
||||
{ label: 'Plombier', value: 'PLOMBIER' },
|
||||
{ label: 'Charpentier', value: 'CHARPENTIER' },
|
||||
{ label: 'Peintre', value: 'PEINTRE' },
|
||||
{ label: 'Chef d\'équipe', value: 'CHEF_EQUIPE' },
|
||||
{ label: 'Conducteur', value: 'CONDUCTEUR' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadEmployesDisponibles();
|
||||
}, [dateRecherche, metierFiltre]);
|
||||
|
||||
const loadEmployesDisponibles = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('🔄 Chargement des employés disponibles...');
|
||||
|
||||
let url = '/api/employes/disponibles';
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (dateRecherche) {
|
||||
params.append('date', dateRecherche.toISOString().split('T')[0]);
|
||||
}
|
||||
if (metierFiltre) {
|
||||
params.append('metier', metierFiltre);
|
||||
}
|
||||
|
||||
if (params.toString()) {
|
||||
url += `?${params.toString()}`;
|
||||
}
|
||||
|
||||
const response = await apiClient.get(url);
|
||||
console.log('✅ Employés disponibles chargés:', response.data);
|
||||
setEmployes(response.data || []);
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors du chargement des employés disponibles:', error);
|
||||
setEmployes([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const metierBodyTemplate = (rowData: EmployeDisponible) => {
|
||||
const getMetierColor = (metier: string) => {
|
||||
const colors: { [key: string]: string } = {
|
||||
'MACON': 'info',
|
||||
'ELECTRICIEN': 'warning',
|
||||
'PLOMBIER': 'primary',
|
||||
'CHARPENTIER': 'success',
|
||||
'PEINTRE': 'secondary',
|
||||
'CHEF_EQUIPE': 'danger',
|
||||
'CONDUCTEUR': 'help'
|
||||
};
|
||||
return colors[metier] || 'secondary';
|
||||
};
|
||||
|
||||
return <Tag value={rowData.metier} severity={getMetierColor(rowData.metier)} />;
|
||||
};
|
||||
|
||||
const experienceBodyTemplate = (rowData: EmployeDisponible) => {
|
||||
const getExperienceColor = (niveau: string) => {
|
||||
switch (niveau) {
|
||||
case 'DEBUTANT': return 'info';
|
||||
case 'INTERMEDIAIRE': return 'warning';
|
||||
case 'EXPERIMENTE': return 'success';
|
||||
case 'EXPERT': return 'danger';
|
||||
default: return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
return <Tag value={rowData.niveauExperience} severity={getExperienceColor(rowData.niveauExperience)} />;
|
||||
};
|
||||
|
||||
const disponibiliteBodyTemplate = (rowData: EmployeDisponible) => {
|
||||
return (
|
||||
<div className="flex flex-column gap-1">
|
||||
<div className="flex align-items-center gap-2">
|
||||
<i className="pi pi-check-circle text-green-500" />
|
||||
<span className="text-green-600 font-medium">Disponible</span>
|
||||
</div>
|
||||
{rowData.disponibleJusquau && (
|
||||
<small className="text-500">
|
||||
Jusqu'au {new Date(rowData.disponibleJusquau).toLocaleDateString('fr-FR')}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const prochainChantierBodyTemplate = (rowData: EmployeDisponible) => {
|
||||
if (rowData.prochainChantier) {
|
||||
return (
|
||||
<div className="flex flex-column gap-1">
|
||||
<Tag value={rowData.prochainChantier} severity="warning" />
|
||||
{rowData.dateProchainChantier && (
|
||||
<small className="text-500">
|
||||
Début: {new Date(rowData.dateProchainChantier).toLocaleDateString('fr-FR')}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <span className="text-500">Aucun</span>;
|
||||
};
|
||||
|
||||
const competencesBodyTemplate = (rowData: EmployeDisponible) => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{rowData.competences?.slice(0, 3).map((comp, index) => (
|
||||
<Tag key={index} value={comp} className="p-tag-sm" />
|
||||
))}
|
||||
{rowData.competences?.length > 3 && (
|
||||
<Badge value={`+${rowData.competences.length - 3}`} severity="info" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const actionBodyTemplate = (rowData: EmployeDisponible) => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
className="p-button-rounded p-button-info p-button-sm"
|
||||
onClick={() => router.push(`/employes/${rowData.id}`)}
|
||||
tooltip="Voir détails"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-calendar-plus"
|
||||
className="p-button-rounded p-button-success p-button-sm"
|
||||
onClick={() => router.push(`/planning?employeId=${rowData.id}`)}
|
||||
tooltip="Planifier"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-users"
|
||||
className="p-button-rounded p-button-help p-button-sm"
|
||||
onClick={() => router.push(`/equipes?assignEmploye=${rowData.id}`)}
|
||||
tooltip="Affecter à une équipe"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-send"
|
||||
className="p-button-rounded p-button-warning p-button-sm"
|
||||
onClick={() => router.push(`/chantiers?assignEmploye=${rowData.id}`)}
|
||||
tooltip="Affecter à un chantier"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const leftToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
label="Tous les employés"
|
||||
icon="pi pi-arrow-left"
|
||||
className="p-button-outlined"
|
||||
onClick={() => router.push('/employes')}
|
||||
/>
|
||||
<Button
|
||||
label="Employés actifs"
|
||||
icon="pi pi-check-circle"
|
||||
className="p-button-info"
|
||||
onClick={() => router.push('/employes/actifs')}
|
||||
/>
|
||||
<Button
|
||||
label="Planifier équipe"
|
||||
icon="pi pi-users"
|
||||
className="p-button-success"
|
||||
onClick={() => router.push('/equipes/optimal')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const rightToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Calendar
|
||||
value={dateRecherche}
|
||||
onChange={(e) => setDateRecherche(e.value as Date)}
|
||||
placeholder="Date de recherche"
|
||||
dateFormat="dd/mm/yy"
|
||||
showIcon
|
||||
/>
|
||||
<Dropdown
|
||||
value={metierFiltre}
|
||||
options={metierOptions}
|
||||
onChange={(e) => setMetierFiltre(e.value)}
|
||||
placeholder="Filtrer par métier"
|
||||
/>
|
||||
<span className="p-input-icon-left">
|
||||
<i className="pi pi-search" />
|
||||
<InputText
|
||||
type="search"
|
||||
placeholder="Rechercher..."
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
className="p-button-outlined"
|
||||
onClick={loadEmployesDisponibles}
|
||||
tooltip="Actualiser"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const header = (
|
||||
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
|
||||
<h2 className="m-0">Employés Disponibles ({employes.length})</h2>
|
||||
<div className="flex gap-2 mt-2 md:mt-0">
|
||||
<Badge value={employes.filter(e => !e.prochainChantier).length} severity="success" />
|
||||
<span>Libres</span>
|
||||
<Badge value={employes.filter(e => e.prochainChantier).length} severity="warning" />
|
||||
<span>Avec prochain chantier</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<Toolbar
|
||||
className="mb-4"
|
||||
left={leftToolbarTemplate}
|
||||
right={rightToolbarTemplate}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
value={employes}
|
||||
loading={loading}
|
||||
dataKey="id"
|
||||
paginator
|
||||
rows={10}
|
||||
rowsPerPageOptions={[5, 10, 25, 50]}
|
||||
className="datatable-responsive"
|
||||
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||
currentPageReportTemplate="Affichage de {first} à {last} sur {totalRecords} employés disponibles"
|
||||
globalFilter={globalFilter}
|
||||
emptyMessage="Aucun employé disponible trouvé."
|
||||
header={header}
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column field="nom" header="Nom" sortable />
|
||||
<Column field="prenom" header="Prénom" sortable />
|
||||
<Column field="metier" header="Métier" body={metierBodyTemplate} sortable />
|
||||
<Column field="niveauExperience" header="Expérience" body={experienceBodyTemplate} sortable />
|
||||
<Column field="disponible" header="Disponibilité" body={disponibiliteBodyTemplate} />
|
||||
<Column field="prochainChantier" header="Prochain chantier" body={prochainChantierBodyTemplate} />
|
||||
<Column field="competences" header="Compétences" body={competencesBodyTemplate} />
|
||||
<Column field="email" header="Email" sortable />
|
||||
<Column body={actionBodyTemplate} header="Actions" style={{ minWidth: '12rem' }} />
|
||||
</DataTable>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployesDisponiblesPage;
|
||||
353
app/(main)/employes/nouveau/page.tsx
Normal file
353
app/(main)/employes/nouveau/page.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Card } from 'primereact/card';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { InputNumber } from 'primereact/inputnumber';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { MultiSelect } from 'primereact/multiselect';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { apiClient } from '../../../../services/api-client';
|
||||
|
||||
interface NouvelEmploye {
|
||||
nom: string;
|
||||
prenom: string;
|
||||
email: string;
|
||||
telephone: string;
|
||||
metier: string;
|
||||
statut: string;
|
||||
dateEmbauche: Date | null;
|
||||
salaire: number | null;
|
||||
competences: string[];
|
||||
certifications: string[];
|
||||
adresse: string;
|
||||
numeroSecu: string;
|
||||
niveauExperience: string;
|
||||
}
|
||||
|
||||
const NouvelEmployePage = () => {
|
||||
const [employe, setEmploye] = useState<NouvelEmploye>({
|
||||
nom: '',
|
||||
prenom: '',
|
||||
email: '',
|
||||
telephone: '',
|
||||
metier: '',
|
||||
statut: 'ACTIF',
|
||||
dateEmbauche: null,
|
||||
salaire: null,
|
||||
competences: [],
|
||||
certifications: [],
|
||||
adresse: '',
|
||||
numeroSecu: '',
|
||||
niveauExperience: 'DEBUTANT'
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const statutOptions = [
|
||||
{ label: 'Actif', value: 'ACTIF' },
|
||||
{ label: 'Inactif', value: 'INACTIF' },
|
||||
{ label: 'Formation', value: 'FORMATION' }
|
||||
];
|
||||
|
||||
const metierOptions = [
|
||||
{ label: 'Maçon', value: 'MACON' },
|
||||
{ label: 'Électricien', value: 'ELECTRICIEN' },
|
||||
{ label: 'Plombier', value: 'PLOMBIER' },
|
||||
{ label: 'Charpentier', value: 'CHARPENTIER' },
|
||||
{ label: 'Peintre', value: 'PEINTRE' },
|
||||
{ label: 'Chef d\'équipe', value: 'CHEF_EQUIPE' },
|
||||
{ label: 'Conducteur', value: 'CONDUCTEUR' },
|
||||
{ label: 'Couvreur', value: 'COUVREUR' },
|
||||
{ label: 'Carreleur', value: 'CARRELEUR' },
|
||||
{ label: 'Menuisier', value: 'MENUISIER' }
|
||||
];
|
||||
|
||||
const niveauExperienceOptions = [
|
||||
{ label: 'Débutant', value: 'DEBUTANT' },
|
||||
{ label: 'Intermédiaire', value: 'INTERMEDIAIRE' },
|
||||
{ label: 'Expérimenté', value: 'EXPERIMENTE' },
|
||||
{ label: 'Expert', value: 'EXPERT' }
|
||||
];
|
||||
|
||||
const competencesOptions = [
|
||||
{ label: 'Maçonnerie', value: 'MACONNERIE' },
|
||||
{ label: 'Électricité', value: 'ELECTRICITE' },
|
||||
{ label: 'Plomberie', value: 'PLOMBERIE' },
|
||||
{ label: 'Charpente', value: 'CHARPENTE' },
|
||||
{ label: 'Peinture', value: 'PEINTURE' },
|
||||
{ label: 'Carrelage', value: 'CARRELAGE' },
|
||||
{ label: 'Menuiserie', value: 'MENUISERIE' },
|
||||
{ label: 'Couverture', value: 'COUVERTURE' },
|
||||
{ label: 'Isolation', value: 'ISOLATION' },
|
||||
{ label: 'Cloisons sèches', value: 'CLOISONS_SECHES' },
|
||||
{ label: 'Terrassement', value: 'TERRASSEMENT' },
|
||||
{ label: 'Conduite d\'engins', value: 'CONDUITE_ENGINS' }
|
||||
];
|
||||
|
||||
const certificationsOptions = [
|
||||
{ label: 'CACES R482', value: 'CACES_R482' },
|
||||
{ label: 'CACES R489', value: 'CACES_R489' },
|
||||
{ label: 'Habilitation électrique', value: 'HABILITATION_ELECTRIQUE' },
|
||||
{ label: 'Travail en hauteur', value: 'TRAVAIL_HAUTEUR' },
|
||||
{ label: 'Soudure', value: 'SOUDURE' },
|
||||
{ label: 'Échafaudage', value: 'ECHAFAUDAGE' },
|
||||
{ label: 'Amiante', value: 'AMIANTE' },
|
||||
{ label: 'Plomb', value: 'PLOMB' }
|
||||
];
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!employe.nom || !employe.prenom || !employe.email || !employe.metier) {
|
||||
console.error('❌ Champs obligatoires manquants');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('🔄 Création de l\'employé...', employe);
|
||||
|
||||
const employeData = {
|
||||
...employe,
|
||||
dateEmbauche: employe.dateEmbauche?.toISOString().split('T')[0]
|
||||
};
|
||||
|
||||
const response = await apiClient.post('/api/employes', employeData);
|
||||
console.log('✅ Employé créé avec succès:', response.data);
|
||||
|
||||
router.push('/employes');
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors de la création:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
router.push('/employes');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<Card title="Nouvel Employé" className="shadow-2">
|
||||
<form onSubmit={handleSubmit} className="p-fluid">
|
||||
<div className="grid">
|
||||
{/* Informations personnelles */}
|
||||
<div className="col-12">
|
||||
<h3>Informations personnelles</h3>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="nom" className="block text-900 font-medium mb-2">
|
||||
Nom *
|
||||
</label>
|
||||
<InputText
|
||||
id="nom"
|
||||
value={employe.nom}
|
||||
onChange={(e) => setEmploye({...employe, nom: e.target.value})}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="prenom" className="block text-900 font-medium mb-2">
|
||||
Prénom *
|
||||
</label>
|
||||
<InputText
|
||||
id="prenom"
|
||||
value={employe.prenom}
|
||||
onChange={(e) => setEmploye({...employe, prenom: e.target.value})}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="email" className="block text-900 font-medium mb-2">
|
||||
Email *
|
||||
</label>
|
||||
<InputText
|
||||
id="email"
|
||||
type="email"
|
||||
value={employe.email}
|
||||
onChange={(e) => setEmploye({...employe, email: e.target.value})}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="telephone" className="block text-900 font-medium mb-2">
|
||||
Téléphone
|
||||
</label>
|
||||
<InputText
|
||||
id="telephone"
|
||||
value={employe.telephone}
|
||||
onChange={(e) => setEmploye({...employe, telephone: e.target.value})}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<label htmlFor="adresse" className="block text-900 font-medium mb-2">
|
||||
Adresse
|
||||
</label>
|
||||
<InputText
|
||||
id="adresse"
|
||||
value={employe.adresse}
|
||||
onChange={(e) => setEmploye({...employe, adresse: e.target.value})}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="numeroSecu" className="block text-900 font-medium mb-2">
|
||||
Numéro de sécurité sociale
|
||||
</label>
|
||||
<InputText
|
||||
id="numeroSecu"
|
||||
value={employe.numeroSecu}
|
||||
onChange={(e) => setEmploye({...employe, numeroSecu: e.target.value})}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Informations professionnelles */}
|
||||
<div className="col-12">
|
||||
<h3>Informations professionnelles</h3>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="metier" className="block text-900 font-medium mb-2">
|
||||
Métier *
|
||||
</label>
|
||||
<Dropdown
|
||||
id="metier"
|
||||
value={employe.metier}
|
||||
options={metierOptions}
|
||||
onChange={(e) => setEmploye({...employe, metier: e.value})}
|
||||
placeholder="Sélectionner un métier"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="niveauExperience" className="block text-900 font-medium mb-2">
|
||||
Niveau d'expérience
|
||||
</label>
|
||||
<Dropdown
|
||||
id="niveauExperience"
|
||||
value={employe.niveauExperience}
|
||||
options={niveauExperienceOptions}
|
||||
onChange={(e) => setEmploye({...employe, niveauExperience: e.value})}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="statut" className="block text-900 font-medium mb-2">
|
||||
Statut
|
||||
</label>
|
||||
<Dropdown
|
||||
id="statut"
|
||||
value={employe.statut}
|
||||
options={statutOptions}
|
||||
onChange={(e) => setEmploye({...employe, statut: e.value})}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="dateEmbauche" className="block text-900 font-medium mb-2">
|
||||
Date d'embauche
|
||||
</label>
|
||||
<Calendar
|
||||
id="dateEmbauche"
|
||||
value={employe.dateEmbauche}
|
||||
onChange={(e) => setEmploye({...employe, dateEmbauche: e.value as Date})}
|
||||
dateFormat="dd/mm/yy"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="salaire" className="block text-900 font-medium mb-2">
|
||||
Salaire mensuel (€)
|
||||
</label>
|
||||
<InputNumber
|
||||
id="salaire"
|
||||
value={employe.salaire}
|
||||
onValueChange={(e) => setEmploye({...employe, salaire: e.value})}
|
||||
mode="currency"
|
||||
currency="EUR"
|
||||
locale="fr-FR"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="competences" className="block text-900 font-medium mb-2">
|
||||
Compétences
|
||||
</label>
|
||||
<MultiSelect
|
||||
id="competences"
|
||||
value={employe.competences}
|
||||
options={competencesOptions}
|
||||
onChange={(e) => setEmploye({...employe, competences: e.value})}
|
||||
placeholder="Sélectionner les compétences"
|
||||
className="w-full"
|
||||
display="chip"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12 md:col-6">
|
||||
<label htmlFor="certifications" className="block text-900 font-medium mb-2">
|
||||
Certifications
|
||||
</label>
|
||||
<MultiSelect
|
||||
id="certifications"
|
||||
value={employe.certifications}
|
||||
options={certificationsOptions}
|
||||
onChange={(e) => setEmploye({...employe, certifications: e.value})}
|
||||
placeholder="Sélectionner les certifications"
|
||||
className="w-full"
|
||||
display="chip"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Boutons d'action */}
|
||||
<div className="col-12">
|
||||
<div className="flex gap-2 justify-content-end">
|
||||
<Button
|
||||
label="Annuler"
|
||||
icon="pi pi-times"
|
||||
className="p-button-secondary"
|
||||
onClick={handleCancel}
|
||||
type="button"
|
||||
/>
|
||||
<Button
|
||||
label="Créer l'employé"
|
||||
icon="pi pi-check"
|
||||
className="p-button-success"
|
||||
loading={loading}
|
||||
type="submit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
<Toast />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NouvelEmployePage;
|
||||
314
app/(main)/employes/page.tsx
Normal file
314
app/(main)/employes/page.tsx
Normal file
@@ -0,0 +1,314 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import RoleProtectedPage from '@/components/RoleProtectedPage';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { Button } from 'primereact/button';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Toolbar } from 'primereact/toolbar';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { ConfirmDialog } from 'primereact/confirmdialog';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { apiClient } from '../../../services/api-client';
|
||||
|
||||
interface Employe {
|
||||
id: number;
|
||||
nom: string;
|
||||
prenom: string;
|
||||
email: string;
|
||||
telephone: string;
|
||||
metier: string;
|
||||
statut: 'ACTIF' | 'INACTIF' | 'CONGE' | 'FORMATION';
|
||||
dateEmbauche: string;
|
||||
salaire: number;
|
||||
equipeId?: number;
|
||||
competences: string[];
|
||||
certifications: string[];
|
||||
}
|
||||
|
||||
const EmployesPageContent = () => {
|
||||
const [employes, setEmployes] = useState<Employe[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
const [selectedEmployes, setSelectedEmployes] = useState<Employe[]>([]);
|
||||
const [deleteDialog, setDeleteDialog] = useState(false);
|
||||
const [employeToDelete, setEmployeToDelete] = useState<Employe | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const statutOptions = [
|
||||
{ label: 'Tous', value: null },
|
||||
{ label: 'Actif', value: 'ACTIF' },
|
||||
{ label: 'Inactif', value: 'INACTIF' },
|
||||
{ label: 'Congé', value: 'CONGE' },
|
||||
{ label: 'Formation', value: 'FORMATION' }
|
||||
];
|
||||
|
||||
const metierOptions = [
|
||||
{ label: 'Tous', value: null },
|
||||
{ label: 'Maçon', value: 'MACON' },
|
||||
{ label: 'Électricien', value: 'ELECTRICIEN' },
|
||||
{ label: 'Plombier', value: 'PLOMBIER' },
|
||||
{ label: 'Charpentier', value: 'CHARPENTIER' },
|
||||
{ label: 'Peintre', value: 'PEINTRE' },
|
||||
{ label: 'Chef d\'équipe', value: 'CHEF_EQUIPE' },
|
||||
{ label: 'Conducteur', value: 'CONDUCTEUR' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadEmployes();
|
||||
}, []);
|
||||
|
||||
const loadEmployes = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('🔄 Chargement des employés...');
|
||||
const response = await apiClient.get('/api/employes');
|
||||
console.log('✅ Employés chargés:', response.data);
|
||||
setEmployes(response.data || []);
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors du chargement des employés:', error);
|
||||
setEmployes([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeleteEmploye = (employe: Employe) => {
|
||||
setEmployeToDelete(employe);
|
||||
setDeleteDialog(true);
|
||||
};
|
||||
|
||||
const deleteEmploye = async () => {
|
||||
if (employeToDelete) {
|
||||
try {
|
||||
await apiClient.delete(`/api/employes/${employeToDelete.id}`);
|
||||
setEmployes(employes.filter(e => e.id !== employeToDelete.id));
|
||||
setDeleteDialog(false);
|
||||
setEmployeToDelete(null);
|
||||
console.log('✅ Employé supprimé avec succès');
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors de la suppression:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleStatutEmploye = async (employe: Employe) => {
|
||||
try {
|
||||
const newStatut = employe.statut === 'ACTIF' ? 'INACTIF' : 'ACTIF';
|
||||
const endpoint = newStatut === 'ACTIF' ? 'activer' : 'desactiver';
|
||||
|
||||
await apiClient.post(`/api/employes/${employe.id}/${endpoint}`);
|
||||
|
||||
setEmployes(employes.map(e =>
|
||||
e.id === employe.id ? { ...e, statut: newStatut } : e
|
||||
));
|
||||
|
||||
console.log(`✅ Statut employé ${newStatut.toLowerCase()}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lors du changement de statut:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const statutBodyTemplate = (rowData: Employe) => {
|
||||
const getSeverity = (statut: string) => {
|
||||
switch (statut) {
|
||||
case 'ACTIF': return 'success';
|
||||
case 'INACTIF': return 'danger';
|
||||
case 'CONGE': return 'warning';
|
||||
case 'FORMATION': return 'info';
|
||||
default: return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
return <Tag value={rowData.statut} severity={getSeverity(rowData.statut)} />;
|
||||
};
|
||||
|
||||
const actionBodyTemplate = (rowData: Employe) => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
className="p-button-rounded p-button-info p-button-sm"
|
||||
onClick={() => router.push(`/employes/${rowData.id}`)}
|
||||
tooltip="Voir détails"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
className="p-button-rounded p-button-success p-button-sm"
|
||||
onClick={() => router.push(`/employes/${rowData.id}/edit`)}
|
||||
tooltip="Modifier"
|
||||
/>
|
||||
<Button
|
||||
icon={rowData.statut === 'ACTIF' ? 'pi pi-pause' : 'pi pi-play'}
|
||||
className={`p-button-rounded p-button-sm ${
|
||||
rowData.statut === 'ACTIF' ? 'p-button-warning' : 'p-button-success'
|
||||
}`}
|
||||
onClick={() => toggleStatutEmploye(rowData)}
|
||||
tooltip={rowData.statut === 'ACTIF' ? 'Désactiver' : 'Activer'}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
className="p-button-rounded p-button-danger p-button-sm"
|
||||
onClick={() => confirmDeleteEmploye(rowData)}
|
||||
tooltip="Supprimer"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const competencesBodyTemplate = (rowData: Employe) => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{rowData.competences?.slice(0, 3).map((comp, index) => (
|
||||
<Tag key={index} value={comp} className="p-tag-sm" />
|
||||
))}
|
||||
{rowData.competences?.length > 3 && (
|
||||
<Tag value={`+${rowData.competences.length - 3}`} className="p-tag-sm" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const leftToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
label="Nouvel Employé"
|
||||
icon="pi pi-plus"
|
||||
className="p-button-success"
|
||||
onClick={() => router.push('/employes/nouveau')}
|
||||
/>
|
||||
<Button
|
||||
label="Employés Actifs"
|
||||
icon="pi pi-check-circle"
|
||||
className="p-button-info"
|
||||
onClick={() => router.push('/employes/actifs')}
|
||||
/>
|
||||
<Button
|
||||
label="Disponibles"
|
||||
icon="pi pi-calendar-check"
|
||||
className="p-button-help"
|
||||
onClick={() => router.push('/employes/disponibles')}
|
||||
/>
|
||||
<Button
|
||||
label="Statistiques"
|
||||
icon="pi pi-chart-bar"
|
||||
className="p-button-secondary"
|
||||
onClick={() => router.push('/employes/stats')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const rightToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="flex gap-2">
|
||||
<span className="p-input-icon-left">
|
||||
<i className="pi pi-search" />
|
||||
<InputText
|
||||
type="search"
|
||||
placeholder="Rechercher..."
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
className="p-button-outlined"
|
||||
onClick={loadEmployes}
|
||||
tooltip="Actualiser"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const header = (
|
||||
<div className="flex flex-column md:flex-row md:justify-content-between md:align-items-center">
|
||||
<h2 className="m-0">Gestion des Employés</h2>
|
||||
<span className="block mt-2 md:mt-0 p-input-icon-left">
|
||||
<i className="pi pi-search" />
|
||||
<InputText
|
||||
type="search"
|
||||
placeholder="Recherche globale..."
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<Toolbar
|
||||
className="mb-4"
|
||||
left={leftToolbarTemplate}
|
||||
right={rightToolbarTemplate}
|
||||
/>
|
||||
|
||||
<DataTable
|
||||
value={employes}
|
||||
loading={loading}
|
||||
dataKey="id"
|
||||
paginator
|
||||
rows={10}
|
||||
rowsPerPageOptions={[5, 10, 25, 50]}
|
||||
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}
|
||||
selection={selectedEmployes}
|
||||
onSelectionChange={(e) => setSelectedEmployes(e.value)}
|
||||
responsiveLayout="scroll"
|
||||
>
|
||||
<Column selectionMode="multiple" headerStyle={{ width: '3rem' }} />
|
||||
<Column field="nom" header="Nom" sortable />
|
||||
<Column field="prenom" header="Prénom" sortable />
|
||||
<Column field="email" header="Email" sortable />
|
||||
<Column field="metier" header="Métier" sortable />
|
||||
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
|
||||
<Column field="competences" header="Compétences" body={competencesBodyTemplate} />
|
||||
<Column field="dateEmbauche" header="Date d'embauche" sortable />
|
||||
<Column body={actionBodyTemplate} header="Actions" style={{ minWidth: '12rem' }} />
|
||||
</DataTable>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
visible={deleteDialog}
|
||||
onHide={() => setDeleteDialog(false)}
|
||||
message={`Êtes-vous sûr de vouloir supprimer l'employé ${employeToDelete?.prenom} ${employeToDelete?.nom} ?`}
|
||||
header="Confirmation de suppression"
|
||||
icon="pi pi-exclamation-triangle"
|
||||
accept={deleteEmploye}
|
||||
reject={() => setDeleteDialog(false)}
|
||||
acceptLabel="Oui"
|
||||
rejectLabel="Non"
|
||||
acceptClassName="p-button-danger"
|
||||
/>
|
||||
|
||||
<Toast />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EmployesPage = () => {
|
||||
return (
|
||||
<RoleProtectedPage
|
||||
requiredPage="EMPLOYES"
|
||||
fallbackMessage="Vous devez avoir un rôle de manager ou supérieur pour accéder aux employés."
|
||||
>
|
||||
<EmployesPageContent />
|
||||
</RoleProtectedPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployesPage;
|
||||
340
app/(main)/employes/stats/page.tsx
Normal file
340
app/(main)/employes/stats/page.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
'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 { useRouter } from 'next/navigation';
|
||||
import { apiClient } from '../../../../services/api-client';
|
||||
|
||||
interface StatistiquesEmployes {
|
||||
totalEmployes: number;
|
||||
employesActifs: number;
|
||||
employesInactifs: number;
|
||||
employesEnConge: number;
|
||||
employesEnFormation: number;
|
||||
repartitionParMetier: { [key: string]: number };
|
||||
repartitionParExperience: { [key: string]: number };
|
||||
repartitionParEquipe: { [key: string]: number };
|
||||
employesSansCertification: number;
|
||||
employesAvecCertifications: number;
|
||||
moyenneAgeEmployes: number;
|
||||
ancienneteMoyenne: number;
|
||||
tauxDisponibilite: number;
|
||||
topCompetences: Array<{ competence: string; nombre: number }>;
|
||||
topCertifications: Array<{ certification: string; nombre: number }>;
|
||||
}
|
||||
|
||||
const StatistiquesEmployesPage = () => {
|
||||
const [stats, setStats] = useState<StatistiquesEmployes | 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 employés...');
|
||||
const response = await apiClient.get('/api/employes/statistiques');
|
||||
console.log('✅ Statistiques employés 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: ['Actifs', 'Inactifs', 'En congé', 'En formation'],
|
||||
datasets: [{
|
||||
data: [
|
||||
stats.employesActifs,
|
||||
stats.employesInactifs,
|
||||
stats.employesEnConge,
|
||||
stats.employesEnFormation
|
||||
],
|
||||
backgroundColor: [
|
||||
'#4CAF50',
|
||||
'#F44336',
|
||||
'#FF9800',
|
||||
'#2196F3'
|
||||
],
|
||||
borderWidth: 2
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const getMetierChartData = () => {
|
||||
if (!stats) return {};
|
||||
|
||||
return {
|
||||
labels: Object.keys(stats.repartitionParMetier),
|
||||
datasets: [{
|
||||
data: Object.values(stats.repartitionParMetier),
|
||||
backgroundColor: [
|
||||
'#FF6384',
|
||||
'#36A2EB',
|
||||
'#FFCE56',
|
||||
'#4BC0C0',
|
||||
'#9966FF',
|
||||
'#FF9F40',
|
||||
'#FF6384',
|
||||
'#C9CBCF'
|
||||
],
|
||||
borderWidth: 2
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const getExperienceChartData = () => {
|
||||
if (!stats) return {};
|
||||
|
||||
return {
|
||||
labels: Object.keys(stats.repartitionParExperience),
|
||||
datasets: [{
|
||||
label: 'Nombre d\'employés',
|
||||
data: Object.values(stats.repartitionParExperience),
|
||||
backgroundColor: '#36A2EB',
|
||||
borderColor: '#36A2EB',
|
||||
borderWidth: 1
|
||||
}]
|
||||
};
|
||||
};
|
||||
|
||||
const leftToolbarTemplate = () => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
label="Retour aux employés"
|
||||
icon="pi pi-arrow-left"
|
||||
className="p-button-outlined"
|
||||
onClick={() => router.push('/employes')}
|
||||
/>
|
||||
<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 Employés</span>
|
||||
<div className="text-900 font-medium text-xl">{stats.totalEmployes}</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">Employés Actifs</span>
|
||||
<div className="text-900 font-medium text-xl">{stats.employesActifs}</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 Disponibilité</span>
|
||||
<div className="text-900 font-medium text-xl">{stats.tauxDisponibilite}%</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-calendar-check 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">Ancienneté Moyenne</span>
|
||||
<div className="text-900 font-medium text-xl">{stats.ancienneteMoyenne} ans</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-clock 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 Métier">
|
||||
<Chart type="pie" data={getMetierChartData()} options={chartOptions} style={{ height: '300px' }} />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<Card title="Répartition par Niveau d'Expérience">
|
||||
<Chart type="bar" data={getExperienceChartData()} options={chartOptions} style={{ height: '300px' }} />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tableaux de données */}
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card title="Top Compétences">
|
||||
<DataTable value={stats.topCompetences} responsiveLayout="scroll">
|
||||
<Column field="competence" header="Compétence" />
|
||||
<Column
|
||||
field="nombre"
|
||||
header="Nombre d'employés"
|
||||
body={(rowData) => <Tag value={rowData.nombre} severity="info" />}
|
||||
/>
|
||||
</DataTable>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card title="Top Certifications">
|
||||
<DataTable value={stats.topCertifications} responsiveLayout="scroll">
|
||||
<Column field="certification" header="Certification" />
|
||||
<Column
|
||||
field="nombre"
|
||||
header="Nombre d'employés"
|
||||
body={(rowData) => <Tag value={rowData.nombre} severity="success" />}
|
||||
/>
|
||||
</DataTable>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Informations supplémentaires */}
|
||||
<div className="col-12">
|
||||
<Card title="Informations Complémentaires">
|
||||
<div className="grid">
|
||||
<div className="col-12 md:col-4">
|
||||
<div className="text-center">
|
||||
<i className="pi pi-graduation-cap text-4xl text-blue-500 mb-3" />
|
||||
<h4>Certifications</h4>
|
||||
<p className="text-600">
|
||||
{stats.employesAvecCertifications} employés avec certifications<br />
|
||||
{stats.employesSansCertification} sans certification
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 md:col-4">
|
||||
<div className="text-center">
|
||||
<i className="pi pi-calendar text-4xl text-green-500 mb-3" />
|
||||
<h4>Âge Moyen</h4>
|
||||
<p className="text-600">
|
||||
{stats.moyenneAgeEmployes} ans
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 md:col-4">
|
||||
<div className="text-center">
|
||||
<i className="pi pi-users text-4xl text-orange-500 mb-3" />
|
||||
<h4>Équipes</h4>
|
||||
<p className="text-600">
|
||||
{Object.keys(stats.repartitionParEquipe).length} équipes actives
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatistiquesEmployesPage;
|
||||
Reference in New Issue
Block a user