Files
btpxpress-frontend/app/(main)/planning/equipes/page.tsx

1357 lines
61 KiB
TypeScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { InputText } from 'primereact/inputtext';
import { Card } from 'primereact/card';
import { Dialog } from 'primereact/dialog';
import { Toast } from 'primereact/toast';
import { Toolbar } from 'primereact/toolbar';
import { Tag } from 'primereact/tag';
import { Dropdown } from 'primereact/dropdown';
import { Calendar } from 'primereact/calendar';
import { InputTextarea } from 'primereact/inputtextarea';
import { MultiSelect } from 'primereact/multiselect';
import { InputNumber } from 'primereact/inputnumber';
import { Panel } from 'primereact/panel';
import { TabView, TabPanel } from 'primereact/tabview';
import { ProgressBar } from 'primereact/progressbar';
import { Avatar } from 'primereact/avatar';
import { AvatarGroup } from 'primereact/avatargroup';
import { Splitter, SplitterPanel } from 'primereact/splitter';
import { Timeline } from 'primereact/timeline';
import { Badge } from 'primereact/badge';
import { ConfirmDialog, confirmDialog } from 'primereact/confirmdialog';
import { formatDate, formatCurrency } from '../../../../utils/formatters';
import type { Equipe, Employe, StatutEquipe, StatutEmploye, NiveauCompetence, Disponibilite } from '../../../../types/btp';
const EquipesPage = () => {
const [equipes, setEquipes] = useState<Equipe[]>([]);
const [employes, setEmployes] = useState<Employe[]>([]);
const [selectedEquipes, setSelectedEquipes] = useState<Equipe[]>([]);
const [selectedEmployes, setSelectedEmployes] = useState<Employe[]>([]);
const [loading, setLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [activeTab, setActiveTab] = useState(0);
// Dialogs
const [equipeDialog, setEquipeDialog] = useState(false);
const [employeDialog, setEmployeDialog] = useState(false);
const [planningDialog, setPlanningDialog] = useState(false);
const [disponibiliteDialog, setDisponibiliteDialog] = useState(false);
const [deleteEquipeDialog, setDeleteEquipeDialog] = useState(false);
const [deleteEmployeDialog, setDeleteEmployeDialog] = useState(false);
// Current entities
const [equipe, setEquipe] = useState<Partial<Equipe>>({});
const [employe, setEmploye] = useState<Partial<Employe>>({});
const [selectedEquipe, setSelectedEquipe] = useState<Equipe | null>(null);
const [selectedEmploye, setSelectedEmploye] = useState<Employe | null>(null);
const [submitted, setSubmitted] = useState(false);
// Stats
const [stats, setStats] = useState({
totalEquipes: 0,
equipesActives: 0,
totalEmployes: 0,
employesActifs: 0,
tauxOccupation: 0,
competencesMoyennes: 0
});
const toast = useRef<Toast>(null);
const dt = useRef<DataTable<Equipe[]>>(null);
const statutEquipeOptions = [
{ label: 'Active', value: 'ACTIVE' },
{ label: 'Inactive', value: 'INACTIVE' },
{ label: 'En formation', value: 'EN_FORMATION' },
{ label: 'Disponible', value: 'DISPONIBLE' },
{ label: 'Occupée', value: 'OCCUPEE' }
];
const statutEmployeOptions = [
{ label: 'Actif', value: 'ACTIF' },
{ label: 'Inactif', value: 'INACTIF' },
{ label: 'Congé', value: 'CONGE' },
{ label: 'Arrêt maladie', value: 'ARRET_MALADIE' },
{ label: 'Formation', value: 'FORMATION' }
];
const posteOptions = [
{ label: 'Chef de chantier', value: 'CHEF_CHANTIER' },
{ label: 'Maçon', value: 'MACON' },
{ label: 'Électricien', value: 'ELECTRICIEN' },
{ label: 'Plombier', value: 'PLOMBIER' },
{ label: 'Menuisier', value: 'MENUISIER' },
{ label: 'Peintre', value: 'PEINTRE' },
{ label: 'Carreleur', value: 'CARRELEUR' },
{ label: 'Couvreur', value: 'COUVREUR' },
{ label: 'Manœuvre', value: 'MANOEUVRE' },
{ label: 'Conducteur d\'engins', value: 'CONDUCTEUR_ENGINS' }
];
const specialiteOptions = [
{ label: 'Gros œuvre', value: 'GROS_OEUVRE' },
{ label: 'Second œuvre', value: 'SECOND_OEUVRE' },
{ label: 'Finitions', value: 'FINITIONS' },
{ label: 'Électricité', value: 'ELECTRICITE' },
{ label: 'Plomberie', value: 'PLOMBERIE' },
{ label: 'Chauffage', value: 'CHAUFFAGE' },
{ label: 'Isolation', value: 'ISOLATION' },
{ label: 'Étanchéité', value: 'ETANCHEITE' },
{ label: 'Démolition', value: 'DEMOLITION' },
{ label: 'Terrassement', value: 'TERRASSEMENT' }
];
const competenceOptions = [
{ label: 'Lecture de plans', value: 'LECTURE_PLANS' },
{ label: 'Sécurité', value: 'SECURITE' },
{ label: 'Conduite d\'engins', value: 'CONDUITE_ENGINS' },
{ label: 'Soudure', value: 'SOUDURE' },
{ label: 'Coffrage', value: 'COFFRAGE' },
{ label: 'Ferraillage', value: 'FERRAILLAGE' },
{ label: 'Maçonnerie', value: 'MACONNERIE' },
{ label: 'Charpente', value: 'CHARPENTE' },
{ label: 'Couverture', value: 'COUVERTURE' },
{ label: 'Plâtrerie', value: 'PLATRERIE' }
];
const niveauCompetenceOptions = [
{ label: 'Débutant', value: 'DEBUTANT' },
{ label: 'Intermédiaire', value: 'INTERMEDIAIRE' },
{ label: 'Avancé', value: 'AVANCE' },
{ label: 'Expert', value: 'EXPERT' }
];
useEffect(() => {
loadData();
}, []);
useEffect(() => {
calculateStats();
}, [equipes, employes]);
const loadData = async () => {
try {
setLoading(true);
// TODO: Replace with actual API calls
generateMockData();
} catch (error) {
console.error('Erreur lors du chargement des données:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les données',
life: 3000
});
} finally {
setLoading(false);
}
};
const generateMockData = () => {
// Générer des employés
const mockEmployes: Employe[] = [
{
id: '1',
nom: 'Martin',
prenom: 'Jean',
email: 'jean.martin@btpxpress.com',
telephone: '0123456789',
poste: 'CHEF_CHANTIER',
specialites: ['GROS_OEUVRE', 'SECURITE'],
tauxHoraire: 35,
dateEmbauche: '2020-01-15',
statut: 'ACTIF' as StatutEmploye,
competences: [
{
id: '1',
nom: 'LECTURE_PLANS',
niveau: 'EXPERT' as NiveauCompetence,
certifiee: true,
dateObtention: '2020-03-01'
},
{
id: '2',
nom: 'SECURITE',
niveau: 'EXPERT' as NiveauCompetence,
certifiee: true,
dateObtention: '2020-02-15',
dateExpiration: '2025-02-15'
}
],
disponibilites: [],
dateCreation: '2020-01-15T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
},
{
id: '2',
nom: 'Dubois',
prenom: 'Pierre',
email: 'pierre.dubois@btpxpress.com',
telephone: '0123456790',
poste: 'MACON',
specialites: ['GROS_OEUVRE', 'MACONNERIE'],
tauxHoraire: 28,
dateEmbauche: '2021-03-10',
statut: 'ACTIF' as StatutEmploye,
competences: [
{
id: '3',
nom: 'MACONNERIE',
niveau: 'AVANCE' as NiveauCompetence,
certifiee: false
},
{
id: '4',
nom: 'COFFRAGE',
niveau: 'INTERMEDIAIRE' as NiveauCompetence,
certifiee: true,
dateObtention: '2021-06-01'
}
],
disponibilites: [],
dateCreation: '2021-03-10T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
},
{
id: '3',
nom: 'Leroy',
prenom: 'Michel',
email: 'michel.leroy@btpxpress.com',
telephone: '0123456791',
poste: 'ELECTRICIEN',
specialites: ['ELECTRICITE', 'SECOND_OEUVRE'],
tauxHoraire: 32,
dateEmbauche: '2019-09-01',
statut: 'ACTIF' as StatutEmploye,
competences: [
{
id: '5',
nom: 'ELECTRICITE',
niveau: 'EXPERT' as NiveauCompetence,
certifiee: true,
dateObtention: '2019-12-01'
}
],
disponibilites: [],
dateCreation: '2019-09-01T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
},
{
id: '4',
nom: 'Bernard',
prenom: 'Luc',
email: 'luc.bernard@btpxpress.com',
telephone: '0123456792',
poste: 'PLOMBIER',
specialites: ['PLOMBERIE', 'CHAUFFAGE'],
tauxHoraire: 30,
dateEmbauche: '2022-01-15',
statut: 'CONGE' as StatutEmploye,
competences: [
{
id: '6',
nom: 'PLOMBERIE',
niveau: 'AVANCE' as NiveauCompetence,
certifiee: true,
dateObtention: '2022-04-01'
}
],
disponibilites: [
{
id: '1',
employe: {} as Employe, // référence circulaire simplifiée
dateDebut: new Date().toISOString(),
dateFin: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
type: 'CONGE_PAYE' as any,
motif: 'Congés annuels',
approuvee: true,
dateCreation: new Date().toISOString(),
dateModification: new Date().toISOString()
}
],
dateCreation: '2022-01-15T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
},
{
id: '5',
nom: 'Moreau',
prenom: 'Antoine',
email: 'antoine.moreau@btpxpress.com',
telephone: '0123456793',
poste: 'MANOEUVRE',
specialites: ['GROS_OEUVRE'],
tauxHoraire: 22,
dateEmbauche: '2023-06-01',
statut: 'ACTIF' as StatutEmploye,
competences: [
{
id: '7',
nom: 'SECURITE',
niveau: 'INTERMEDIAIRE' as NiveauCompetence,
certifiee: true,
dateObtention: '2023-07-01',
dateExpiration: '2026-07-01'
}
],
disponibilites: [],
dateCreation: '2023-06-01T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
}
];
// Générer des équipes
const mockEquipes: Equipe[] = [
{
id: '1',
nom: 'Équipe Gros Œuvre',
description: 'Équipe spécialisée dans les travaux de gros œuvre',
chef: mockEmployes[0], // Jean Martin
membres: [mockEmployes[0], mockEmployes[1], mockEmployes[4]], // Jean, Pierre, Antoine
specialites: ['GROS_OEUVRE', 'MACONNERIE', 'COFFRAGE'],
statut: 'ACTIVE' as StatutEquipe,
dateCreation: '2020-01-15T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
},
{
id: '2',
nom: 'Équipe Second Œuvre',
description: 'Équipe spécialisée dans les finitions et second œuvre',
chef: mockEmployes[2], // Michel Leroy
membres: [mockEmployes[2], mockEmployes[3]], // Michel, Luc
specialites: ['ELECTRICITE', 'PLOMBERIE', 'SECOND_OEUVRE'],
statut: 'DISPONIBLE' as StatutEquipe,
dateCreation: '2021-01-15T09:00:00',
dateModification: '2024-01-15T09:00:00',
actif: true
}
];
setEmployes(mockEmployes);
setEquipes(mockEquipes);
};
const calculateStats = () => {
const totalEquipes = equipes.length;
const equipesActives = equipes.filter(e => e.statut === 'ACTIVE').length;
const totalEmployes = employes.length;
const employesActifs = employes.filter(e => e.statut === 'ACTIF').length;
// Calcul approximatif du taux d'occupation
const employesOccupes = employes.filter(e =>
e.statut === 'ACTIF' &&
(!e.disponibilites || e.disponibilites.length === 0)
).length;
const tauxOccupation = totalEmployes > 0 ? Math.round((employesOccupes / totalEmployes) * 100) : 0;
// Calcul du niveau moyen des compétences
const totalCompetences = employes.reduce((total, emp) => total + (emp.competences?.length || 0), 0);
const competencesMoyennes = totalEmployes > 0 ? Math.round(totalCompetences / totalEmployes) : 0;
setStats({
totalEquipes,
equipesActives,
totalEmployes,
employesActifs,
tauxOccupation,
competencesMoyennes
});
};
const openNewEquipe = () => {
setEquipe({
nom: '',
description: '',
specialites: [],
statut: 'ACTIVE' as StatutEquipe,
actif: true
});
setSubmitted(false);
setEquipeDialog(true);
};
const openNewEmploye = () => {
setEmploye({
nom: '',
prenom: '',
email: '',
telephone: '',
poste: '',
specialites: [],
tauxHoraire: 25,
dateEmbauche: new Date().toISOString().split('T')[0],
statut: 'ACTIF' as StatutEmploye,
competences: [],
disponibilites: [],
actif: true
});
setSubmitted(false);
setEmployeDialog(true);
};
const editEquipe = (equipe: Equipe) => {
setEquipe({ ...equipe });
setEquipeDialog(true);
};
const editEmploye = (employe: Employe) => {
setEmploye({ ...employe });
setEmployeDialog(true);
};
const confirmDeleteEquipe = (equipe: Equipe) => {
setEquipe(equipe);
setDeleteEquipeDialog(true);
};
const confirmDeleteEmploye = (employe: Employe) => {
setEmploye(employe);
setDeleteEmployeDialog(true);
};
const deleteEquipe = () => {
const updatedEquipes = equipes.filter(e => e.id !== equipe.id);
setEquipes(updatedEquipes);
setDeleteEquipeDialog(false);
setEquipe({});
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Équipe supprimée',
life: 3000
});
};
const deleteEmploye = () => {
const updatedEmployes = employes.filter(e => e.id !== employe.id);
setEmployes(updatedEmployes);
setDeleteEmployeDialog(false);
setEmploye({});
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Employé supprimé',
life: 3000
});
};
const saveEquipe = () => {
setSubmitted(true);
if (equipe.nom?.trim()) {
const updatedEquipes = [...equipes];
const equipeData = {
...equipe,
dateModification: new Date().toISOString()
} as Equipe;
if (equipe.id) {
const index = findIndexById(equipe.id, equipes);
updatedEquipes[index] = equipeData;
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Équipe mise à jour',
life: 3000
});
} else {
equipeData.id = Date.now().toString();
equipeData.dateCreation = new Date().toISOString();
updatedEquipes.push(equipeData);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Équipe créée',
life: 3000
});
}
setEquipes(updatedEquipes);
setEquipeDialog(false);
setEquipe({});
}
};
const saveEmploye = () => {
setSubmitted(true);
if (employe.nom?.trim() && employe.prenom?.trim()) {
const updatedEmployes = [...employes];
const employeData = {
...employe,
dateModification: new Date().toISOString()
} as Employe;
if (employe.id) {
const index = findIndexById(employe.id, employes);
updatedEmployes[index] = employeData;
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Employé mis à jour',
life: 3000
});
} else {
employeData.id = Date.now().toString();
employeData.dateCreation = new Date().toISOString();
updatedEmployes.push(employeData);
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Employé créé',
life: 3000
});
}
setEmployes(updatedEmployes);
setEmployeDialog(false);
setEmploye({});
}
};
const findIndexById = (id: string, array: any[]) => {
return array.findIndex(item => item.id === id);
};
const exportCSV = () => {
dt.current?.exportCSV();
};
const onInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, name: string, entity: 'equipe' | 'employe') => {
const val = (e.target && e.target.value) || '';
if (entity === 'equipe') {
setEquipe(prev => ({ ...prev, [name]: val }));
} else {
setEmploye(prev => ({ ...prev, [name]: val }));
}
};
const onDropdownChange = (e: any, name: string, entity: 'equipe' | 'employe') => {
if (entity === 'equipe') {
setEquipe(prev => ({ ...prev, [name]: e.value }));
} else {
setEmploye(prev => ({ ...prev, [name]: e.value }));
}
};
const onMultiSelectChange = (e: any, name: string, entity: 'equipe' | 'employe') => {
if (entity === 'equipe') {
setEquipe(prev => ({ ...prev, [name]: e.value }));
} else {
setEmploye(prev => ({ ...prev, [name]: e.value }));
}
};
const onNumberChange = (e: any, name: string) => {
setEmploye(prev => ({ ...prev, [name]: e.value }));
};
// Templates pour les équipes
const equipStatutBodyTemplate = (rowData: Equipe) => {
const getSeverity = (status: StatutEquipe) => {
switch (status) {
case 'ACTIVE': return 'success';
case 'DISPONIBLE': return 'info';
case 'OCCUPEE': return 'warning';
case 'EN_FORMATION': return 'secondary';
case 'INACTIVE': return 'danger';
default: return 'secondary';
}
};
const getLabel = (status: StatutEquipe) => {
switch (status) {
case 'ACTIVE': return 'Active';
case 'DISPONIBLE': return 'Disponible';
case 'OCCUPEE': return 'Occupée';
case 'EN_FORMATION': return 'En formation';
case 'INACTIVE': return 'Inactive';
default: return status;
}
};
return <Tag value={getLabel(rowData.statut)} severity={getSeverity(rowData.statut)} />;
};
const equipChefBodyTemplate = (rowData: Equipe) => {
return rowData.chef ? `${rowData.chef.prenom} ${rowData.chef.nom}` : 'Non assigné';
};
const equipMembresBodyTemplate = (rowData: Equipe) => {
const nombreMembres = rowData.membres?.length || 0;
return (
<div className="flex align-items-center gap-2">
<AvatarGroup>
{rowData.membres?.slice(0, 3).map((membre, index) => (
<Avatar
key={membre.id}
label={`${membre.prenom[0]}${membre.nom[0]}`}
size="normal"
style={{ backgroundColor: '#2196F3', color: '#ffffff' }}
/>
))}
{nombreMembres > 3 && (
<Avatar
label={`+${nombreMembres - 3}`}
size="normal"
style={{ backgroundColor: '#9C27B0', color: '#ffffff' }}
/>
)}
</AvatarGroup>
<span className="text-sm">{nombreMembres} membre{nombreMembres > 1 ? 's' : ''}</span>
</div>
);
};
const equipSpecialitesBodyTemplate = (rowData: Equipe) => {
return (
<div className="flex flex-wrap gap-1">
{rowData.specialites?.slice(0, 2).map(specialite => (
<Tag key={specialite} value={specialite} severity="info" />
))}
{(rowData.specialites?.length || 0) > 2 && (
<Tag value={`+${(rowData.specialites?.length || 0) - 2}`} severity={"secondary" as any} />
)}
</div>
);
};
const equipActionBodyTemplate = (rowData: Equipe) => {
return (
<div className="flex gap-1">
<Button
icon="pi pi-eye"
size="small"
severity="info"
outlined
onClick={() => {
setSelectedEquipe(rowData);
setPlanningDialog(true);
}}
/>
<Button
icon="pi pi-pencil"
size="small"
severity={"secondary" as any}
outlined
onClick={() => editEquipe(rowData)}
/>
<Button
icon="pi pi-trash"
size="small"
severity="danger"
outlined
onClick={() => confirmDeleteEquipe(rowData)}
/>
</div>
);
};
// Templates pour les employés
const employeStatutBodyTemplate = (rowData: Employe) => {
const getSeverity = (status: StatutEmploye) => {
switch (status) {
case 'ACTIF': return 'success';
case 'CONGE': return 'info';
case 'FORMATION': return 'secondary';
case 'ARRET_MALADIE': return 'warning';
case 'INACTIF': return 'danger';
default: return 'secondary';
}
};
const getLabel = (status: StatutEmploye) => {
switch (status) {
case 'ACTIF': return 'Actif';
case 'CONGE': return 'Congé';
case 'FORMATION': return 'Formation';
case 'ARRET_MALADIE': return 'Arrêt maladie';
case 'INACTIF': return 'Inactif';
default: return status;
}
};
return <Tag value={getLabel(rowData.statut)} severity={getSeverity(rowData.statut)} />;
};
const employeEquipeBodyTemplate = (rowData: Employe) => {
return rowData.equipe?.nom || 'Non assigné';
};
const employeCompetencesBodyTemplate = (rowData: Employe) => {
const competences = rowData.competences || [];
const competencesExpert = competences.filter(c => c.niveau === 'EXPERT').length;
return (
<div className="flex align-items-center gap-2">
<Badge value={competences.length} />
<span className="text-sm text-color-secondary">
{competencesExpert > 0 && `(${competencesExpert} expert${competencesExpert > 1 ? 's' : ''})`}
</span>
</div>
);
};
const employeActionBodyTemplate = (rowData: Employe) => {
return (
<div className="flex gap-1">
<Button
icon="pi pi-calendar"
size="small"
severity="info"
outlined
onClick={() => {
setSelectedEmploye(rowData);
setDisponibiliteDialog(true);
}}
/>
<Button
icon="pi pi-pencil"
size="small"
severity={"secondary" as any}
outlined
onClick={() => editEmploye(rowData)}
/>
<Button
icon="pi pi-trash"
size="small"
severity="danger"
outlined
onClick={() => confirmDeleteEmploye(rowData)}
/>
</div>
);
};
const leftToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
{activeTab === 0 ? (
<>
<Button
label="Nouvelle équipe"
icon="pi pi-plus"
severity="success"
onClick={openNewEquipe}
/>
<Button
label="Supprimer"
icon="pi pi-trash"
severity="danger"
onClick={() => {
confirmDialog({
message: 'Êtes-vous sûr de vouloir supprimer les équipes sélectionnées ?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
accept: () => {
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Équipes supprimées',
life: 3000
});
}
});
}}
disabled={!selectedEquipes || selectedEquipes.length === 0}
/>
</>
) : (
<>
<Button
label="Nouvel employé"
icon="pi pi-plus"
severity="success"
onClick={openNewEmploye}
/>
<Button
label="Supprimer"
icon="pi pi-trash"
severity="danger"
onClick={() => {
confirmDialog({
message: 'Êtes-vous sûr de vouloir supprimer les employés sélectionnés ?',
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
accept: () => {
toast.current?.show({
severity: 'success',
summary: 'Succès',
detail: 'Employés supprimés',
life: 3000
});
}
});
}}
disabled={!selectedEmployes || selectedEmployes.length === 0}
/>
</>
)}
</div>
);
};
const rightToolbarTemplate = () => {
return (
<div className="flex flex-wrap gap-2">
<span className="p-input-icon-left">
<i className="pi pi-search" />
<InputText
type="search"
placeholder="Rechercher..."
onInput={(e) => setGlobalFilter(e.currentTarget.value)}
/>
</span>
<Button
label="Exporter"
icon="pi pi-upload"
severity="help"
onClick={exportCSV}
/>
</div>
);
};
const renderStatsCards = () => {
return (
<div className="grid mb-4">
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="bg-blue-100 p-3 border-round mr-3">
<i className="pi pi-users text-blue-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{stats.totalEquipes}</div>
<div className="text-color-secondary">Équipes</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="bg-green-100 p-3 border-round mr-3">
<i className="pi pi-user text-green-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{stats.totalEmployes}</div>
<div className="text-color-secondary">Employés</div>
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="bg-orange-100 p-3 border-round mr-3">
<i className="pi pi-chart-pie text-orange-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{stats.tauxOccupation}%</div>
<div className="text-color-secondary">Taux occupation</div>
<ProgressBar value={stats.tauxOccupation} className="mt-1" style={{ height: '4px' }} />
</div>
</div>
</Card>
</div>
<div className="col-12 md:col-3">
<Card>
<div className="flex align-items-center">
<div className="bg-purple-100 p-3 border-round mr-3">
<i className="pi pi-star text-purple-500 text-2xl" />
</div>
<div>
<div className="text-2xl font-bold text-color">{stats.competencesMoyennes}</div>
<div className="text-color-secondary">Compétences moy.</div>
</div>
</div>
</Card>
</div>
</div>
);
};
return (
<div className="grid">
<div className="col-12">
<Card>
<Toast ref={toast} />
<ConfirmDialog />
<Toolbar className="mb-4" left={leftToolbarTemplate} right={rightToolbarTemplate} />
{renderStatsCards()}
<TabView activeIndex={activeTab} onTabChange={(e) => setActiveTab(e.index)}>
<TabPanel header="Équipes">
<DataTable
ref={dt}
value={equipes}
selection={selectedEquipes}
onSelectionChange={(e) => setSelectedEquipes(e.value)}
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} équipes"
globalFilter={globalFilter}
emptyMessage="Aucune équipe trouvée."
loading={loading}
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="nom" header="Nom" sortable headerStyle={{ minWidth: '12rem' }} />
<Column field="chef" header="Chef d'équipe" body={equipChefBodyTemplate} sortable headerStyle={{ minWidth: '12rem' }} />
<Column field="membres" header="Membres" body={equipMembresBodyTemplate} headerStyle={{ minWidth: '15rem' }} />
<Column field="specialites" header="Spécialités" body={equipSpecialitesBodyTemplate} headerStyle={{ minWidth: '15rem' }} />
<Column field="statut" header="Statut" body={equipStatutBodyTemplate} sortable headerStyle={{ minWidth: '8rem' }} />
<Column body={equipActionBodyTemplate} headerStyle={{ minWidth: '10rem' }} />
</DataTable>
</TabPanel>
<TabPanel header="Employés">
<DataTable
value={employes}
selection={selectedEmployes}
onSelectionChange={(e) => setSelectedEmployes(e.value)}
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é."
loading={loading}
>
<Column selectionMode="multiple" headerStyle={{ width: '4rem' }} />
<Column field="prenom" header="Prénom" sortable headerStyle={{ minWidth: '10rem' }} />
<Column field="nom" header="Nom" sortable headerStyle={{ minWidth: '10rem' }} />
<Column field="poste" header="Poste" sortable headerStyle={{ minWidth: '10rem' }} />
<Column field="equipe" header="Équipe" body={employeEquipeBodyTemplate} sortable headerStyle={{ minWidth: '10rem' }} />
<Column field="tauxHoraire" header="Taux/h" body={(rowData) => formatCurrency(rowData.tauxHoraire)} sortable headerStyle={{ minWidth: '8rem' }} />
<Column field="competences" header="Compétences" body={employeCompetencesBodyTemplate} headerStyle={{ minWidth: '10rem' }} />
<Column field="statut" header="Statut" body={employeStatutBodyTemplate} sortable headerStyle={{ minWidth: '8rem' }} />
<Column body={employeActionBodyTemplate} headerStyle={{ minWidth: '10rem' }} />
</DataTable>
</TabPanel>
</TabView>
{/* Dialog Équipe */}
<Dialog
visible={equipeDialog}
style={{ width: '600px' }}
header="Détails de l'équipe"
modal
className="p-fluid"
footer={
<div>
<Button label="Annuler" icon="pi pi-times" onClick={() => setEquipeDialog(false)} />
<Button label="Sauvegarder" icon="pi pi-check" onClick={saveEquipe} />
</div>
}
onHide={() => setEquipeDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12">
<label htmlFor="nom">Nom de l'équipe *</label>
<InputText
id="nom"
value={equipe.nom || ''}
onChange={(e) => onInputChange(e, 'nom', 'equipe')}
required
className={submitted && !equipe.nom ? 'p-invalid' : ''}
/>
{submitted && !equipe.nom && <small className="p-invalid">Le nom est requis.</small>}
</div>
<div className="field col-12">
<label htmlFor="description">Description</label>
<InputTextarea
id="description"
value={equipe.description || ''}
onChange={(e) => onInputChange(e, 'description', 'equipe')}
rows={3}
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="chef">Chef d'équipe</label>
<Dropdown
id="chef"
value={equipe.chef?.id}
options={employes.map(emp => ({ label: `${emp.prenom} ${emp.nom}`, value: emp.id }))}
onChange={(e) => {
const selectedEmploye = employes.find(emp => emp.id === e.value);
setEquipe(prev => ({ ...prev, chef: selectedEmploye }));
}}
placeholder="Sélectionnez un chef"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="statut">Statut</label>
<Dropdown
id="statut"
value={equipe.statut}
options={statutEquipeOptions}
onChange={(e) => onDropdownChange(e, 'statut', 'equipe')}
/>
</div>
<div className="field col-12">
<label htmlFor="specialites">Spécialités</label>
<MultiSelect
id="specialites"
value={equipe.specialites}
options={specialiteOptions}
onChange={(e) => onMultiSelectChange(e, 'specialites', 'equipe')}
placeholder="Sélectionnez les spécialités"
display="chip"
/>
</div>
<div className="field col-12">
<label htmlFor="membres">Membres</label>
<MultiSelect
id="membres"
value={equipe.membres?.map(m => m.id) || []}
options={employes.map(emp => ({ label: `${emp.prenom} ${emp.nom} (${emp.poste})`, value: emp.id }))}
onChange={(e) => {
const selectedEmployes = employes.filter(emp => e.value.includes(emp.id));
setEquipe(prev => ({ ...prev, membres: selectedEmployes }));
}}
placeholder="Sélectionnez les membres"
display="chip"
/>
</div>
</div>
</Dialog>
{/* Dialog Employé */}
<Dialog
visible={employeDialog}
style={{ width: '800px' }}
header="Détails de l'employé"
modal
className="p-fluid"
footer={
<div>
<Button label="Annuler" icon="pi pi-times" onClick={() => setEmployeDialog(false)} />
<Button label="Sauvegarder" icon="pi pi-check" onClick={saveEmploye} />
</div>
}
onHide={() => setEmployeDialog(false)}
>
<div className="formgrid grid">
<div className="field col-12 md:col-6">
<label htmlFor="prenom">Prénom *</label>
<InputText
id="prenom"
value={employe.prenom || ''}
onChange={(e) => onInputChange(e, 'prenom', 'employe')}
required
className={submitted && !employe.prenom ? 'p-invalid' : ''}
/>
{submitted && !employe.prenom && <small className="p-invalid">Le prénom est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="nom">Nom *</label>
<InputText
id="nom"
value={employe.nom || ''}
onChange={(e) => onInputChange(e, 'nom', 'employe')}
required
className={submitted && !employe.nom ? 'p-invalid' : ''}
/>
{submitted && !employe.nom && <small className="p-invalid">Le nom est requis.</small>}
</div>
<div className="field col-12 md:col-6">
<label htmlFor="email">Email</label>
<InputText
id="email"
value={employe.email || ''}
onChange={(e) => onInputChange(e, 'email', 'employe')}
type="email"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="telephone">Téléphone</label>
<InputText
id="telephone"
value={employe.telephone || ''}
onChange={(e) => onInputChange(e, 'telephone', 'employe')}
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="poste">Poste</label>
<Dropdown
id="poste"
value={employe.poste}
options={posteOptions}
onChange={(e) => onDropdownChange(e, 'poste', 'employe')}
placeholder="Sélectionnez un poste"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="statut">Statut</label>
<Dropdown
id="statut"
value={employe.statut}
options={statutEmployeOptions}
onChange={(e) => onDropdownChange(e, 'statut', 'employe')}
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="tauxHoraire">Taux horaire ()</label>
<InputNumber
id="tauxHoraire"
value={employe.tauxHoraire}
onValueChange={(e) => onNumberChange(e, 'tauxHoraire')}
mode="currency"
currency="EUR"
locale="fr-FR"
/>
</div>
<div className="field col-12 md:col-6">
<label htmlFor="dateEmbauche">Date d'embauche</label>
<Calendar
id="dateEmbauche"
value={employe.dateEmbauche ? new Date(employe.dateEmbauche) : null}
onChange={(e) => setEmploye(prev => ({
...prev,
dateEmbauche: e.value ? e.value.toISOString().split('T')[0] : ''
}))}
dateFormat="dd/mm/yy"
showIcon
/>
</div>
<div className="field col-12">
<label htmlFor="specialites">Spécialités</label>
<MultiSelect
id="specialites"
value={employe.specialites}
options={specialiteOptions}
onChange={(e) => onMultiSelectChange(e, 'specialites', 'employe')}
placeholder="Sélectionnez les spécialités"
display="chip"
/>
</div>
</div>
</Dialog>
{/* Dialog Planning Équipe */}
<Dialog
visible={planningDialog}
style={{ width: '900px' }}
header={`Planning - ${selectedEquipe?.nom}`}
modal
onHide={() => setPlanningDialog(false)}
>
{selectedEquipe && (
<div className="flex flex-column gap-4">
<div className="grid">
<div className="col-12 md:col-6">
<Panel header="Informations générales">
<div className="flex flex-column gap-2">
<div><strong>Chef:</strong> {equipChefBodyTemplate(selectedEquipe)}</div>
<div><strong>Membres:</strong> {selectedEquipe.membres?.length || 0}</div>
<div><strong>Statut:</strong> {equipStatutBodyTemplate(selectedEquipe)}</div>
<div><strong>Spécialités:</strong> {selectedEquipe.specialites?.join(', ')}</div>
</div>
</Panel>
</div>
<div className="col-12 md:col-6">
<Panel header="Planning de la semaine">
<Timeline
value={[
{ status: 'Chantier A', date: 'Lundi 08:00-17:00', icon: 'pi pi-building', color: '#9C27B0' },
{ status: 'Formation sécurité', date: 'Mardi 14:00-16:00', icon: 'pi pi-book', color: '#2196F3' },
{ status: 'Chantier B', date: 'Mercredi 08:00-17:00', icon: 'pi pi-building', color: '#9C27B0' },
{ status: 'Réunion équipe', date: 'Jeudi 09:00-10:00', icon: 'pi pi-users', color: '#FF9800' },
{ status: 'Chantier A', date: 'Vendredi 08:00-17:00', icon: 'pi pi-building', color: '#9C27B0' }
]}
content={(item) => (
<div className="flex flex-column">
<small className="text-color-secondary">{item.date}</small>
<div className="font-medium">{item.status}</div>
</div>
)}
/>
</Panel>
</div>
</div>
<Panel header="Membres de l'équipe">
<div className="grid">
{selectedEquipe.membres?.map(membre => (
<div key={membre.id} className="col-12 md:col-6">
<Card>
<div className="flex align-items-center gap-3">
<Avatar
label={`${membre.prenom[0]}${membre.nom[0]}`}
size="large"
style={{ backgroundColor: '#2196F3', color: '#ffffff' }}
/>
<div className="flex-1">
<div className="font-medium">{membre.prenom} {membre.nom}</div>
<div className="text-sm text-color-secondary">{membre.poste}</div>
<div className="mt-1">
{employeStatutBodyTemplate(membre)}
</div>
</div>
</div>
</Card>
</div>
))}
</div>
</Panel>
</div>
)}
</Dialog>
{/* Dialog Disponibilité Employé */}
<Dialog
visible={disponibiliteDialog}
style={{ width: '700px' }}
header={`Disponibilités - ${selectedEmploye?.prenom} ${selectedEmploye?.nom}`}
modal
onHide={() => setDisponibiliteDialog(false)}
>
{selectedEmploye && (
<div className="flex flex-column gap-4">
<Panel header="Informations employé">
<div className="grid">
<div className="col-12 md:col-6">
<div><strong>Poste:</strong> {selectedEmploye.poste}</div>
<div><strong>Équipe:</strong> {employeEquipeBodyTemplate(selectedEmploye)}</div>
<div><strong>Statut:</strong> {employeStatutBodyTemplate(selectedEmploye)}</div>
</div>
<div className="col-12 md:col-6">
<div><strong>Taux horaire:</strong> {formatCurrency(selectedEmploye.tauxHoraire)}</div>
<div><strong>Embauche:</strong> {formatDate(selectedEmploye.dateEmbauche)}</div>
<div><strong>Compétences:</strong> {selectedEmploye.competences?.length || 0}</div>
</div>
</div>
</Panel>
<Panel header="Compétences">
<div className="flex flex-wrap gap-2">
{selectedEmploye.competences?.map(competence => (
<Tag
key={competence.id}
value={`${competence.nom} (${competence.niveau})`}
severity={competence.certifiee ? 'success' : 'info'}
icon={competence.certifiee ? 'pi pi-check' : undefined}
/>
))}
</div>
</Panel>
<Panel header="Indisponibilités actuelles">
{selectedEmploye.disponibilites?.length ? (
<Timeline
value={selectedEmploye.disponibilites.map(dispo => ({
status: dispo.type,
date: `${formatDate(dispo.dateDebut)} - ${formatDate(dispo.dateFin)}`,
icon: 'pi pi-calendar-times',
color: dispo.approuvee ? '#4CAF50' : '#FF9800',
description: dispo.motif
}))}
content={(item) => (
<div className="flex flex-column">
<small className="text-color-secondary">{item.date}</small>
<div className="font-medium">{item.status}</div>
{item.description && <div className="text-sm mt-1">{item.description}</div>}
</div>
)}
/>
) : (
<p className="text-center text-color-secondary">Aucune indisponibilité</p>
)}
</Panel>
</div>
)}
</Dialog>
{/* Dialog Suppression Équipe */}
<Dialog
visible={deleteEquipeDialog}
style={{ width: '450px' }}
header="Confirmer"
modal
footer={
<div>
<Button label="Non" icon="pi pi-times" onClick={() => setDeleteEquipeDialog(false)} />
<Button label="Oui" icon="pi pi-check" onClick={deleteEquipe} />
</div>
}
onHide={() => setDeleteEquipeDialog(false)}
>
<div className="flex align-items-center justify-content-center">
<i className="pi pi-exclamation-triangle mr-3" style={{ fontSize: '2rem' }} />
{equipe && (
<span>
Êtes-vous sûr de vouloir supprimer l'équipe <b>{equipe.nom}</b> ?
</span>
)}
</div>
</Dialog>
{/* Dialog Suppression Employé */}
<Dialog
visible={deleteEmployeDialog}
style={{ width: '450px' }}
header="Confirmer"
modal
footer={
<div>
<Button label="Non" icon="pi pi-times" onClick={() => setDeleteEmployeDialog(false)} />
<Button label="Oui" icon="pi pi-check" onClick={deleteEmploye} />
</div>
}
onHide={() => setDeleteEmployeDialog(false)}
>
<div className="flex align-items-center justify-content-center">
<i className="pi pi-exclamation-triangle mr-3" style={{ fontSize: '2rem' }} />
{employe && (
<span>
Êtes-vous sûr de vouloir supprimer l'employé <b>{employe.prenom} {employe.nom}</b> ?
</span>
)}
</div>
</Dialog>
</Card>
</div>
</div>
);
};
export default EquipesPage;