'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([]); const [employes, setEmployes] = useState([]); const [selectedEquipes, setSelectedEquipes] = useState([]); const [selectedEmployes, setSelectedEmployes] = useState([]); 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>({}); const [employe, setEmploye] = useState>({}); const [selectedEquipe, setSelectedEquipe] = useState(null); const [selectedEmploye, setSelectedEmploye] = useState(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(null); const dt = useRef>(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, 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 ; }; 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 (
{rowData.membres?.slice(0, 3).map((membre, index) => ( ))} {nombreMembres > 3 && ( )} {nombreMembres} membre{nombreMembres > 1 ? 's' : ''}
); }; const equipSpecialitesBodyTemplate = (rowData: Equipe) => { return (
{rowData.specialites?.slice(0, 2).map(specialite => ( ))} {(rowData.specialites?.length || 0) > 2 && ( )}
); }; const equipActionBodyTemplate = (rowData: Equipe) => { return (
); }; // 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 ; }; 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 (
{competencesExpert > 0 && `(${competencesExpert} expert${competencesExpert > 1 ? 's' : ''})`}
); }; const employeActionBodyTemplate = (rowData: Employe) => { return (
); }; const leftToolbarTemplate = () => { return (
{activeTab === 0 ? ( <>
); }; const rightToolbarTemplate = () => { return (
setGlobalFilter(e.currentTarget.value)} />
); }; const renderStatsCards = () => { return (
{stats.totalEquipes}
Équipes
{stats.totalEmployes}
Employés
{stats.tauxOccupation}%
Taux occupation
{stats.competencesMoyennes}
Compétences moy.
); }; return (
{renderStatsCards()} setActiveTab(e.index)}> 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} > 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} > formatCurrency(rowData.tauxHoraire)} sortable headerStyle={{ minWidth: '8rem' }} /> {/* Dialog Équipe */}
} onHide={() => setEquipeDialog(false)} >
onInputChange(e, 'nom', 'equipe')} required className={submitted && !equipe.nom ? 'p-invalid' : ''} /> {submitted && !equipe.nom && Le nom est requis.}
onInputChange(e, 'description', 'equipe')} rows={3} />
({ 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" />
onDropdownChange(e, 'statut', 'equipe')} />
onMultiSelectChange(e, 'specialites', 'equipe')} placeholder="Sélectionnez les spécialités" display="chip" />
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" />
{/* Dialog Employé */}
} onHide={() => setEmployeDialog(false)} >
onInputChange(e, 'prenom', 'employe')} required className={submitted && !employe.prenom ? 'p-invalid' : ''} /> {submitted && !employe.prenom && Le prénom est requis.}
onInputChange(e, 'nom', 'employe')} required className={submitted && !employe.nom ? 'p-invalid' : ''} /> {submitted && !employe.nom && Le nom est requis.}
onInputChange(e, 'email', 'employe')} type="email" />
onInputChange(e, 'telephone', 'employe')} />
onDropdownChange(e, 'poste', 'employe')} placeholder="Sélectionnez un poste" />
onDropdownChange(e, 'statut', 'employe')} />
onNumberChange(e, 'tauxHoraire')} mode="currency" currency="EUR" locale="fr-FR" />
setEmploye(prev => ({ ...prev, dateEmbauche: e.value ? e.value.toISOString().split('T')[0] : '' }))} dateFormat="dd/mm/yy" showIcon />
onMultiSelectChange(e, 'specialites', 'employe')} placeholder="Sélectionnez les spécialités" display="chip" />
{/* Dialog Planning Équipe */} setPlanningDialog(false)} > {selectedEquipe && (
Chef: {equipChefBodyTemplate(selectedEquipe)}
Membres: {selectedEquipe.membres?.length || 0}
Statut: {equipStatutBodyTemplate(selectedEquipe)}
Spécialités: {selectedEquipe.specialites?.join(', ')}
(
{item.date}
{item.status}
)} />
{selectedEquipe.membres?.map(membre => (
{membre.prenom} {membre.nom}
{membre.poste}
{employeStatutBodyTemplate(membre)}
))}
)}
{/* Dialog Disponibilité Employé */} setDisponibiliteDialog(false)} > {selectedEmploye && (
Poste: {selectedEmploye.poste}
Équipe: {employeEquipeBodyTemplate(selectedEmploye)}
Statut: {employeStatutBodyTemplate(selectedEmploye)}
Taux horaire: {formatCurrency(selectedEmploye.tauxHoraire)}
Embauche: {formatDate(selectedEmploye.dateEmbauche)}
Compétences: {selectedEmploye.competences?.length || 0}
{selectedEmploye.competences?.map(competence => ( ))}
{selectedEmploye.disponibilites?.length ? ( ({ 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) => (
{item.date}
{item.status}
{item.description &&
{item.description}
}
)} /> ) : (

Aucune indisponibilité

)}
)}
{/* Dialog Suppression Équipe */} {/* Dialog Suppression Employé */} ); }; export default EquipesPage;