Initial commit
This commit is contained in:
720
app/(main)/dashboard/ressources/page.tsx
Normal file
720
app/(main)/dashboard/ressources/page.tsx
Normal file
@@ -0,0 +1,720 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Card } from 'primereact/card';
|
||||
import { Chart } from 'primereact/chart';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { Tag } from 'primereact/tag';
|
||||
import { ProgressBar } from 'primereact/progressbar';
|
||||
import { Button } from 'primereact/button';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { Dropdown } from 'primereact/dropdown';
|
||||
import { Calendar } from 'primereact/calendar';
|
||||
import { Badge } from 'primereact/badge';
|
||||
import { Knob } from 'primereact/knob';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
interface RessourceDashboard {
|
||||
id: string;
|
||||
nom: string;
|
||||
type: string;
|
||||
categorie: string;
|
||||
statut: string;
|
||||
disponibilite: number;
|
||||
utilisation: number;
|
||||
localisation: string;
|
||||
chantierActuel?: string;
|
||||
dateFinUtilisation?: Date;
|
||||
coutJournalier: number;
|
||||
revenus: number;
|
||||
maintenancesPrevues: number;
|
||||
heuresUtilisation: number;
|
||||
capaciteMax: number;
|
||||
etatSante: number;
|
||||
}
|
||||
|
||||
interface EmployeRessource {
|
||||
id: string;
|
||||
nom: string;
|
||||
prenom: string;
|
||||
metier: string;
|
||||
competences: string[];
|
||||
disponibilite: number;
|
||||
chantierActuel?: string;
|
||||
tauxHoraire: number;
|
||||
heuresTravaillees: number;
|
||||
certifications: string[];
|
||||
experience: number;
|
||||
}
|
||||
|
||||
const DashboardRessources = () => {
|
||||
const toast = useRef<Toast>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const [materiels, setMateriels] = useState<RessourceDashboard[]>([]);
|
||||
const [employes, setEmployes] = useState<EmployeRessource[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedType, setSelectedType] = useState('');
|
||||
const [selectedCategorie, setSelectedCategorie] = useState('');
|
||||
const [selectedView, setSelectedView] = useState('materiel');
|
||||
|
||||
const [chartData, setChartData] = useState({});
|
||||
const [chartOptions, setChartOptions] = useState({});
|
||||
|
||||
const typeOptions = [
|
||||
{ label: 'Tous les types', value: '' },
|
||||
{ label: 'Engins de terrassement', value: 'TERRASSEMENT' },
|
||||
{ label: 'Grues et levage', value: 'LEVAGE' },
|
||||
{ label: 'Transport', value: 'TRANSPORT' },
|
||||
{ label: 'Outillage', value: 'OUTILLAGE' },
|
||||
{ label: 'Sécurité', value: 'SECURITE' }
|
||||
];
|
||||
|
||||
const categorieOptions = [
|
||||
{ label: 'Toutes les catégories', value: '' },
|
||||
{ label: 'Lourd', value: 'LOURD' },
|
||||
{ label: 'Léger', value: 'LEGER' },
|
||||
{ label: 'Spécialisé', value: 'SPECIALISE' },
|
||||
{ label: 'Consommable', value: 'CONSOMMABLE' }
|
||||
];
|
||||
|
||||
const viewOptions = [
|
||||
{ label: 'Matériel', value: 'materiel' },
|
||||
{ label: 'Employés', value: 'employes' },
|
||||
{ label: 'Vue globale', value: 'global' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
loadRessources();
|
||||
initCharts();
|
||||
}, [selectedType, selectedCategorie, selectedView]);
|
||||
|
||||
const loadRessources = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// TODO: Remplacer par de vrais appels API
|
||||
// const [materielResponse, employeResponse] = await Promise.all([
|
||||
// materielService.getDashboardData(),
|
||||
// employeService.getDashboardData()
|
||||
// ]);
|
||||
|
||||
// Données simulées pour la démonstration
|
||||
const mockMateriels: RessourceDashboard[] = [
|
||||
{
|
||||
id: '1',
|
||||
nom: 'Grue mobile Liebherr LTM 1050',
|
||||
type: 'LEVAGE',
|
||||
categorie: 'LOURD',
|
||||
statut: 'EN_SERVICE',
|
||||
disponibilite: 85,
|
||||
utilisation: 75,
|
||||
localisation: 'Chantier Résidence Les Jardins',
|
||||
chantierActuel: 'Résidence Les Jardins',
|
||||
dateFinUtilisation: new Date('2025-02-15'),
|
||||
coutJournalier: 850,
|
||||
revenus: 25500,
|
||||
maintenancesPrevues: 2,
|
||||
heuresUtilisation: 1250,
|
||||
capaciteMax: 50,
|
||||
etatSante: 90
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
nom: 'Pelleteuse CAT 320D',
|
||||
type: 'TERRASSEMENT',
|
||||
categorie: 'LOURD',
|
||||
statut: 'MAINTENANCE',
|
||||
disponibilite: 0,
|
||||
utilisation: 0,
|
||||
localisation: 'Atelier Central',
|
||||
coutJournalier: 650,
|
||||
revenus: 19500,
|
||||
maintenancesPrevues: 1,
|
||||
heuresUtilisation: 980,
|
||||
capaciteMax: 32,
|
||||
etatSante: 65
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
nom: 'Camion benne Volvo FMX',
|
||||
type: 'TRANSPORT',
|
||||
categorie: 'LOURD',
|
||||
statut: 'DISPONIBLE',
|
||||
disponibilite: 100,
|
||||
utilisation: 45,
|
||||
localisation: 'Dépôt Central',
|
||||
coutJournalier: 450,
|
||||
revenus: 13500,
|
||||
maintenancesPrevues: 1,
|
||||
heuresUtilisation: 750,
|
||||
capaciteMax: 25,
|
||||
etatSante: 85
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
nom: 'Bétonnière Schwing S36X',
|
||||
type: 'TRANSPORT',
|
||||
categorie: 'SPECIALISE',
|
||||
statut: 'EN_SERVICE',
|
||||
disponibilite: 70,
|
||||
utilisation: 90,
|
||||
localisation: 'Chantier Centre Commercial',
|
||||
chantierActuel: 'Centre Commercial Atlantis',
|
||||
dateFinUtilisation: new Date('2025-01-30'),
|
||||
coutJournalier: 750,
|
||||
revenus: 22500,
|
||||
maintenancesPrevues: 3,
|
||||
heuresUtilisation: 1100,
|
||||
capaciteMax: 36,
|
||||
etatSante: 80
|
||||
}
|
||||
];
|
||||
|
||||
const mockEmployes: EmployeRessource[] = [
|
||||
{
|
||||
id: '1',
|
||||
nom: 'Dupont',
|
||||
prenom: 'Jean',
|
||||
metier: 'Chef de chantier',
|
||||
competences: ['Gestion équipe', 'Planning', 'Sécurité'],
|
||||
disponibilite: 100,
|
||||
chantierActuel: 'Résidence Les Jardins',
|
||||
tauxHoraire: 45,
|
||||
heuresTravaillees: 1680,
|
||||
certifications: ['CACES R482', 'SST'],
|
||||
experience: 15
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
nom: 'Martin',
|
||||
prenom: 'Marie',
|
||||
metier: 'Conducteur d\'engins',
|
||||
competences: ['Pelleteuse', 'Bulldozer', 'Compacteur'],
|
||||
disponibilite: 85,
|
||||
chantierActuel: 'Centre Commercial Atlantis',
|
||||
tauxHoraire: 35,
|
||||
heuresTravaillees: 1520,
|
||||
certifications: ['CACES R482', 'CACES R372'],
|
||||
experience: 8
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
nom: 'Leroy',
|
||||
prenom: 'Pierre',
|
||||
metier: 'Grutier',
|
||||
competences: ['Grue mobile', 'Grue tour', 'Levage'],
|
||||
disponibilite: 90,
|
||||
chantierActuel: 'Résidence Les Jardins',
|
||||
tauxHoraire: 40,
|
||||
heuresTravaillees: 1600,
|
||||
certifications: ['CACES R483', 'CACES R487'],
|
||||
experience: 12
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
nom: 'Bernard',
|
||||
prenom: 'Luc',
|
||||
metier: 'Maçon',
|
||||
competences: ['Béton', 'Coffrage', 'Ferraillage'],
|
||||
disponibilite: 100,
|
||||
tauxHoraire: 28,
|
||||
heuresTravaillees: 1750,
|
||||
certifications: ['CQP Maçon', 'SST'],
|
||||
experience: 10
|
||||
}
|
||||
];
|
||||
|
||||
// Filtrer selon les critères sélectionnés
|
||||
let filteredMateriels = mockMateriels;
|
||||
|
||||
if (selectedType) {
|
||||
filteredMateriels = filteredMateriels.filter(m => m.type === selectedType);
|
||||
}
|
||||
|
||||
if (selectedCategorie) {
|
||||
filteredMateriels = filteredMateriels.filter(m => m.categorie === selectedCategorie);
|
||||
}
|
||||
|
||||
setMateriels(filteredMateriels);
|
||||
setEmployes(mockEmployes);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des ressources:', error);
|
||||
toast.current?.show({
|
||||
severity: 'error',
|
||||
summary: 'Erreur',
|
||||
detail: 'Impossible de charger les données des ressources'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const initCharts = () => {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
|
||||
if (selectedView === 'materiel') {
|
||||
// Graphique d'utilisation du matériel
|
||||
const utilisationData = {
|
||||
labels: ['En service', 'Disponible', 'Maintenance', 'Hors service'],
|
||||
datasets: [
|
||||
{
|
||||
data: [
|
||||
materiels.filter(m => m.statut === 'EN_SERVICE').length,
|
||||
materiels.filter(m => m.statut === 'DISPONIBLE').length,
|
||||
materiels.filter(m => m.statut === 'MAINTENANCE').length,
|
||||
materiels.filter(m => m.statut === 'HORS_SERVICE').length
|
||||
],
|
||||
backgroundColor: [
|
||||
documentStyle.getPropertyValue('--green-500'),
|
||||
documentStyle.getPropertyValue('--blue-500'),
|
||||
documentStyle.getPropertyValue('--orange-500'),
|
||||
documentStyle.getPropertyValue('--red-500')
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
setChartData(utilisationData);
|
||||
} else if (selectedView === 'employes') {
|
||||
// Graphique de répartition par métier
|
||||
const metiers = [...new Set(employes.map(e => e.metier))];
|
||||
const metierData = {
|
||||
labels: metiers,
|
||||
datasets: [
|
||||
{
|
||||
data: metiers.map(metier => employes.filter(e => e.metier === metier).length),
|
||||
backgroundColor: [
|
||||
documentStyle.getPropertyValue('--blue-500'),
|
||||
documentStyle.getPropertyValue('--green-500'),
|
||||
documentStyle.getPropertyValue('--orange-500'),
|
||||
documentStyle.getPropertyValue('--purple-500')
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
setChartData(metierData);
|
||||
}
|
||||
|
||||
const options = {
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
color: documentStyle.getPropertyValue('--text-color')
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setChartOptions(options);
|
||||
};
|
||||
|
||||
const getStatutSeverity = (statut: string) => {
|
||||
switch (statut) {
|
||||
case 'EN_SERVICE': return 'success';
|
||||
case 'DISPONIBLE': return 'info';
|
||||
case 'MAINTENANCE': return 'warning';
|
||||
case 'HORS_SERVICE': return 'danger';
|
||||
default: return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
const statutBodyTemplate = (rowData: RessourceDashboard) => (
|
||||
<Tag value={rowData.statut} severity={getStatutSeverity(rowData.statut)} />
|
||||
);
|
||||
|
||||
const utilisationBodyTemplate = (rowData: RessourceDashboard) => (
|
||||
<div className="flex align-items-center">
|
||||
<ProgressBar
|
||||
value={rowData.utilisation}
|
||||
style={{ width: '100px', marginRight: '8px' }}
|
||||
showValue={false}
|
||||
/>
|
||||
<span className="text-sm font-semibold">{rowData.utilisation}%</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const disponibiliteBodyTemplate = (rowData: RessourceDashboard) => {
|
||||
const couleur = rowData.disponibilite > 80 ? 'text-green-500' :
|
||||
rowData.disponibilite > 50 ? 'text-orange-500' : 'text-red-500';
|
||||
return (
|
||||
<span className={`font-semibold ${couleur}`}>
|
||||
{rowData.disponibilite}%
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const revenusBodyTemplate = (rowData: RessourceDashboard) => (
|
||||
<div>
|
||||
<div className="font-semibold">
|
||||
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(rowData.revenus)}
|
||||
</div>
|
||||
<div className="text-sm text-500">
|
||||
{rowData.coutJournalier}€/jour
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const etatSanteBodyTemplate = (rowData: RessourceDashboard) => (
|
||||
<div className="flex align-items-center justify-content-center">
|
||||
<Knob
|
||||
value={rowData.etatSante}
|
||||
size={50}
|
||||
readOnly
|
||||
valueColor={rowData.etatSante > 80 ? '#10b981' : rowData.etatSante > 60 ? '#f59e0b' : '#ef4444'}
|
||||
rangeColor="#e5e7eb"
|
||||
textColor="#374151"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const competencesBodyTemplate = (rowData: EmployeRessource) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{rowData.competences.slice(0, 2).map((comp, index) => (
|
||||
<Tag key={index} value={comp} severity="info" className="text-xs" />
|
||||
))}
|
||||
{rowData.competences.length > 2 && (
|
||||
<Tag value={`+${rowData.competences.length - 2}`} severity="secondary" className="text-xs" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const experienceBodyTemplate = (rowData: EmployeRessource) => (
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold">{rowData.experience}</div>
|
||||
<div className="text-xs text-500">ans</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const actionBodyTemplate = (rowData: any) => (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
icon="pi pi-eye"
|
||||
className="p-button-text p-button-sm"
|
||||
tooltip="Voir détails"
|
||||
onClick={() => {
|
||||
if (selectedView === 'materiel') {
|
||||
router.push(`/materiels/${rowData.id}`);
|
||||
} else {
|
||||
router.push(`/employes/${rowData.id}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-calendar"
|
||||
className="p-button-text p-button-sm"
|
||||
tooltip="Planning"
|
||||
onClick={() => router.push(`/planning?ressource=${rowData.id}`)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Calculs des métriques
|
||||
const tauxUtilisationMoyen = materiels.length > 0 ?
|
||||
materiels.reduce((sum, m) => sum + m.utilisation, 0) / materiels.length : 0;
|
||||
|
||||
const revenusTotal = materiels.reduce((sum, m) => sum + m.revenus, 0);
|
||||
const materielDisponible = materiels.filter(m => m.statut === 'DISPONIBLE').length;
|
||||
const employesDisponibles = employes.filter(e => !e.chantierActuel).length;
|
||||
|
||||
return (
|
||||
<div className="grid">
|
||||
<Toast ref={toast} />
|
||||
|
||||
{/* En-tête avec filtres */}
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<div className="flex justify-content-between align-items-center mb-4">
|
||||
<h2 className="text-2xl font-bold m-0">Dashboard Ressources</h2>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
label="Nouveau matériel"
|
||||
icon="pi pi-plus"
|
||||
onClick={() => router.push('/materiels/nouveau')}
|
||||
className="p-button-outlined"
|
||||
/>
|
||||
<Button
|
||||
label="Nouvel employé"
|
||||
icon="pi pi-user-plus"
|
||||
onClick={() => router.push('/employes/nouveau')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 align-items-center">
|
||||
<div className="field">
|
||||
<label htmlFor="view" className="font-semibold">Vue</label>
|
||||
<Dropdown
|
||||
id="view"
|
||||
value={selectedView}
|
||||
options={viewOptions}
|
||||
onChange={(e) => setSelectedView(e.value)}
|
||||
className="w-full md:w-12rem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedView === 'materiel' && (
|
||||
<>
|
||||
<div className="field">
|
||||
<label htmlFor="type" className="font-semibold">Type</label>
|
||||
<Dropdown
|
||||
id="type"
|
||||
value={selectedType}
|
||||
options={typeOptions}
|
||||
onChange={(e) => setSelectedType(e.value)}
|
||||
className="w-full md:w-14rem"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<label htmlFor="categorie" className="font-semibold">Catégorie</label>
|
||||
<Dropdown
|
||||
id="categorie"
|
||||
value={selectedCategorie}
|
||||
options={categorieOptions}
|
||||
onChange={(e) => setSelectedCategorie(e.value)}
|
||||
className="w-full md:w-14rem"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
icon="pi pi-refresh"
|
||||
className="p-button-outlined"
|
||||
onClick={loadRessources}
|
||||
loading={loading}
|
||||
tooltip="Actualiser"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Métriques principales */}
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">
|
||||
{selectedView === 'materiel' ? 'Matériels' : 'Employés'}
|
||||
</span>
|
||||
<div className="text-900 font-medium text-xl">
|
||||
{selectedView === 'materiel' ? materiels.length : employes.length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-blue-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className={`pi ${selectedView === 'materiel' ? 'pi-cog' : 'pi-users'} text-blue-500 text-xl`}></i>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">Disponibles</span>
|
||||
<div className="text-900 font-medium text-xl">
|
||||
{selectedView === 'materiel' ? materielDisponible : employesDisponibles}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-green-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className="pi pi-check-circle text-green-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">
|
||||
{selectedView === 'materiel' ? 'Revenus' : 'Heures totales'}
|
||||
</span>
|
||||
<div className="text-900 font-medium text-xl">
|
||||
{selectedView === 'materiel' ?
|
||||
new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', notation: 'compact' }).format(revenusTotal) :
|
||||
employes.reduce((sum, e) => sum + e.heuresTravaillees, 0).toLocaleString()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-purple-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className={`pi ${selectedView === 'materiel' ? 'pi-euro' : 'pi-clock'} text-purple-500 text-xl`}></i>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-12 lg:col-3 md:col-6">
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-content-between mb-3">
|
||||
<div>
|
||||
<span className="block text-500 font-medium mb-3">Taux Utilisation</span>
|
||||
<div className="text-900 font-medium text-xl">
|
||||
{selectedView === 'materiel' ?
|
||||
`${tauxUtilisationMoyen.toFixed(1)}%` :
|
||||
`${((employes.filter(e => e.chantierActuel).length / employes.length) * 100).toFixed(1)}%`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex align-items-center justify-content-center bg-orange-100 border-round" style={{ width: '2.5rem', height: '2.5rem' }}>
|
||||
<i className="pi pi-chart-line text-orange-500 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Graphique */}
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card>
|
||||
<h6>
|
||||
{selectedView === 'materiel' ? 'Répartition par Statut' : 'Répartition par Métier'}
|
||||
</h6>
|
||||
<Chart type="doughnut" data={chartData} options={chartOptions} className="w-full md:w-30rem" />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Indicateurs de performance */}
|
||||
<div className="col-12 lg:col-6">
|
||||
<Card>
|
||||
<h6>Indicateurs de Performance</h6>
|
||||
<div className="grid">
|
||||
{selectedView === 'materiel' ? (
|
||||
<>
|
||||
<div className="col-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-500">
|
||||
{materiels.filter(m => m.etatSante > 80).length}
|
||||
</div>
|
||||
<div className="text-sm text-500">Bon état</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-orange-500">
|
||||
{materiels.reduce((sum, m) => sum + m.maintenancesPrevues, 0)}
|
||||
</div>
|
||||
<div className="text-sm text-500">Maintenances prévues</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-500">
|
||||
{(materiels.reduce((sum, m) => sum + m.heuresUtilisation, 0) / materiels.length).toFixed(0)}
|
||||
</div>
|
||||
<div className="text-sm text-500">Heures moy./matériel</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-purple-500">
|
||||
{(revenusTotal / materiels.length).toFixed(0)}€
|
||||
</div>
|
||||
<div className="text-sm text-500">Revenus moy./matériel</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="col-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-500">
|
||||
{employes.filter(e => e.certifications.length > 1).length}
|
||||
</div>
|
||||
<div className="text-sm text-500">Multi-certifiés</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-500">
|
||||
{(employes.reduce((sum, e) => sum + e.experience, 0) / employes.length).toFixed(1)}
|
||||
</div>
|
||||
<div className="text-sm text-500">Expérience moyenne</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-purple-500">
|
||||
{(employes.reduce((sum, e) => sum + e.tauxHoraire, 0) / employes.length).toFixed(0)}€
|
||||
</div>
|
||||
<div className="text-sm text-500">Taux horaire moyen</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-orange-500">
|
||||
{employes.reduce((sum, e) => sum + e.competences.length, 0)}
|
||||
</div>
|
||||
<div className="text-sm text-500">Compétences totales</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tableau des ressources */}
|
||||
<div className="col-12">
|
||||
<Card>
|
||||
<div className="flex justify-content-between align-items-center mb-4">
|
||||
<h6>
|
||||
{selectedView === 'materiel' ?
|
||||
`Liste du Matériel (${materiels.length})` :
|
||||
`Liste des Employés (${employes.length})`
|
||||
}
|
||||
</h6>
|
||||
<Badge
|
||||
value={selectedView === 'materiel' ?
|
||||
`${materiels.filter(m => m.statut === 'EN_SERVICE').length} en service` :
|
||||
`${employes.filter(e => e.chantierActuel).length} en mission`
|
||||
}
|
||||
severity="success"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
value={selectedView === 'materiel' ? materiels : employes}
|
||||
loading={loading}
|
||||
responsiveLayout="scroll"
|
||||
paginator
|
||||
rows={10}
|
||||
rowsPerPageOptions={[5, 10, 25]}
|
||||
emptyMessage={`Aucun${selectedView === 'materiel' ? ' matériel' : ' employé'} trouvé`}
|
||||
sortMode="multiple"
|
||||
>
|
||||
{selectedView === 'materiel' ? (
|
||||
<>
|
||||
<Column field="nom" header="Matériel" sortable />
|
||||
<Column field="type" header="Type" sortable />
|
||||
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
|
||||
<Column field="utilisation" header="Utilisation" body={utilisationBodyTemplate} sortable />
|
||||
<Column field="disponibilite" header="Disponibilité" body={disponibiliteBodyTemplate} sortable />
|
||||
<Column field="revenus" header="Revenus" body={revenusBodyTemplate} sortable />
|
||||
<Column field="etatSante" header="État" body={etatSanteBodyTemplate} style={{ width: '100px' }} />
|
||||
<Column field="localisation" header="Localisation" sortable />
|
||||
<Column header="Actions" body={actionBodyTemplate} style={{ width: '100px' }} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Column field="nom" header="Nom" body={(rowData) => `${rowData.prenom} ${rowData.nom}`} sortable />
|
||||
<Column field="metier" header="Métier" sortable />
|
||||
<Column field="competences" header="Compétences" body={competencesBodyTemplate} />
|
||||
<Column field="experience" header="Expérience" body={experienceBodyTemplate} sortable />
|
||||
<Column field="disponibilite" header="Disponibilité" body={disponibiliteBodyTemplate} sortable />
|
||||
<Column field="chantierActuel" header="Chantier actuel" sortable />
|
||||
<Column field="tauxHoraire" header="Taux horaire" body={(rowData) => `${rowData.tauxHoraire}€`} sortable />
|
||||
<Column header="Actions" body={actionBodyTemplate} style={{ width: '100px' }} />
|
||||
</>
|
||||
)}
|
||||
</DataTable>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardRessources;
|
||||
Reference in New Issue
Block a user