1357 lines
61 KiB
TypeScript
Executable File
1357 lines
61 KiB
TypeScript
Executable File
'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;
|
|
|
|
|