386 lines
16 KiB
TypeScript
386 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Card } from 'primereact/card';
|
|
import { Button } from 'primereact/button';
|
|
import { Tag } from 'primereact/tag';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Toolbar } from 'primereact/toolbar';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { TabView, TabPanel } from 'primereact/tabview';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { useRouter } from 'next/navigation';
|
|
import { apiClient } from '../../../../services/api-client';
|
|
|
|
interface EquipeParSpecialite {
|
|
specialite: string;
|
|
nombreEquipes: number;
|
|
equipesActives: number;
|
|
equipesDisponibles: number;
|
|
equipesEnMission: number;
|
|
tauxOccupationMoyen: number;
|
|
competencesPrincipales: string[];
|
|
equipes: Array<{
|
|
id: number;
|
|
nom: string;
|
|
statut: string;
|
|
nombreEmployes: number;
|
|
chefEquipeNom?: string;
|
|
chantierActuel?: string;
|
|
tauxOccupation: number;
|
|
evaluationPerformance: number;
|
|
}>;
|
|
}
|
|
|
|
const EquipesSpecialitesPage = () => {
|
|
const [specialites, setSpecialites] = useState<EquipeParSpecialite[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const router = useRouter();
|
|
|
|
const specialiteLabels: { [key: string]: string } = {
|
|
'GROS_OEUVRE': 'Gros Œuvre',
|
|
'SECOND_OEUVRE': 'Second Œuvre',
|
|
'FINITIONS': 'Finitions',
|
|
'ELECTRICITE': 'Électricité',
|
|
'PLOMBERIE': 'Plomberie',
|
|
'CHARPENTE': 'Charpente',
|
|
'COUVERTURE': 'Couverture',
|
|
'TERRASSEMENT': 'Terrassement'
|
|
};
|
|
|
|
const specialiteColors: { [key: string]: string } = {
|
|
'GROS_OEUVRE': 'danger',
|
|
'SECOND_OEUVRE': 'warning',
|
|
'FINITIONS': 'success',
|
|
'ELECTRICITE': 'info',
|
|
'PLOMBERIE': 'primary',
|
|
'CHARPENTE': 'help',
|
|
'COUVERTURE': 'secondary',
|
|
'TERRASSEMENT': 'contrast'
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadEquipesParSpecialite();
|
|
}, []);
|
|
|
|
const loadEquipesParSpecialite = async () => {
|
|
try {
|
|
setLoading(true);
|
|
console.log('🔄 Chargement des équipes par spécialité...');
|
|
const response = await apiClient.get('/api/equipes/par-specialite');
|
|
console.log('✅ Équipes par spécialité chargées:', response.data);
|
|
setSpecialites(response.data || []);
|
|
} catch (error) {
|
|
console.error('❌ Erreur lors du chargement:', error);
|
|
setSpecialites([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const getStatutSeverity = (statut: string) => {
|
|
switch (statut) {
|
|
case 'ACTIVE': return 'success';
|
|
case 'INACTIVE': return 'danger';
|
|
case 'EN_MISSION': return 'warning';
|
|
case 'DISPONIBLE': return 'info';
|
|
default: return 'secondary';
|
|
}
|
|
};
|
|
|
|
const getOccupationColor = (taux: number) => {
|
|
if (taux >= 90) return 'danger';
|
|
if (taux >= 70) return 'warning';
|
|
if (taux >= 40) return 'success';
|
|
return 'info';
|
|
};
|
|
|
|
const statutBodyTemplate = (rowData: any) => {
|
|
return <Tag value={rowData.statut} severity={getStatutSeverity(rowData.statut) as any} />;
|
|
};
|
|
|
|
const chefEquipeBodyTemplate = (rowData: any) => {
|
|
if (rowData.chefEquipeNom) {
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<i className="pi pi-user text-blue-500" />
|
|
<span>{rowData.chefEquipeNom}</span>
|
|
</div>
|
|
);
|
|
}
|
|
return <span className="text-500">Non assigné</span>;
|
|
};
|
|
|
|
const occupationBodyTemplate = (rowData: any) => {
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<Tag value={`${rowData.tauxOccupation}%`} severity={getOccupationColor(rowData.tauxOccupation)} />
|
|
{rowData.chantierActuel && (
|
|
<small className="text-500">{rowData.chantierActuel}</small>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const performanceBodyTemplate = (rowData: any) => {
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<Tag value={`${rowData.evaluationPerformance}/5`} severity="success" />
|
|
<div className="flex">
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
<i
|
|
key={star}
|
|
className={`pi pi-star${star <= rowData.evaluationPerformance ? '-fill' : ''} text-yellow-500`}
|
|
style={{ fontSize: '0.8rem' }}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const actionBodyTemplate = (rowData: any) => {
|
|
return (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
icon="pi pi-eye"
|
|
className="p-button-rounded p-button-info p-button-sm"
|
|
onClick={() => router.push(`/equipes/${rowData.id}`)}
|
|
tooltip="Voir détails"
|
|
/>
|
|
<Button
|
|
icon="pi pi-calendar"
|
|
className="p-button-rounded p-button-warning p-button-sm"
|
|
onClick={() => router.push(`/planning?equipeId=${rowData.id}`)}
|
|
tooltip="Planning"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderSpecialiteCard = (specialiteData: EquipeParSpecialite) => {
|
|
return (
|
|
<Card key={specialiteData.specialite} className="mb-4">
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<div className="flex align-items-center gap-3">
|
|
<Tag
|
|
value={specialiteLabels[specialiteData.specialite]}
|
|
severity={specialiteColors[specialiteData.specialite] as any}
|
|
className="text-lg"
|
|
/>
|
|
<Badge value={specialiteData.nombreEquipes} severity="info" />
|
|
<span>équipes</span>
|
|
</div>
|
|
<Button
|
|
label="Nouvelle équipe"
|
|
icon="pi pi-plus"
|
|
className="p-button-success p-button-sm"
|
|
onClick={() => router.push(`/equipes/nouvelle?specialite=${specialiteData.specialite}`)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Métriques */}
|
|
<div className="grid mb-4">
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-green-500">{specialiteData.equipesActives}</div>
|
|
<div className="text-500">Actives</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-blue-500">{specialiteData.equipesDisponibles}</div>
|
|
<div className="text-500">Disponibles</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-orange-500">{specialiteData.equipesEnMission}</div>
|
|
<div className="text-500">En mission</div>
|
|
</div>
|
|
</div>
|
|
<div className="col-12 md:col-3">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-purple-500">{specialiteData.tauxOccupationMoyen}%</div>
|
|
<div className="text-500">Occupation</div>
|
|
<ProgressBar
|
|
value={specialiteData.tauxOccupationMoyen}
|
|
className="mt-2"
|
|
color={getOccupationColor(specialiteData.tauxOccupationMoyen)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Compétences principales */}
|
|
<div className="mb-4">
|
|
<h4>Compétences principales</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
{specialiteData.competencesPrincipales?.map((comp, index) => (
|
|
<Tag key={index} value={comp} severity="info" />
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Liste des équipes */}
|
|
<DataTable
|
|
value={specialiteData.equipes}
|
|
responsiveLayout="scroll"
|
|
emptyMessage="Aucune équipe dans cette spécialité"
|
|
>
|
|
<Column field="nom" header="Nom" sortable />
|
|
<Column field="statut" header="Statut" body={statutBodyTemplate} sortable />
|
|
<Column field="nombreEmployes" header="Employés" sortable />
|
|
<Column field="chefEquipeNom" header="Chef d'équipe" body={chefEquipeBodyTemplate} />
|
|
<Column field="tauxOccupation" header="Occupation" body={occupationBodyTemplate} sortable />
|
|
<Column field="evaluationPerformance" header="Performance" body={performanceBodyTemplate} sortable />
|
|
<Column body={actionBodyTemplate} header="Actions" style={{ minWidth: '8rem' }} />
|
|
</DataTable>
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
const leftToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
label="Retour aux équipes"
|
|
icon="pi pi-arrow-left"
|
|
className="p-button-outlined"
|
|
onClick={() => router.push('/equipes')}
|
|
/>
|
|
<Button
|
|
label="Vue globale"
|
|
icon="pi pi-th-large"
|
|
className="p-button-info"
|
|
onClick={() => router.push('/equipes')}
|
|
/>
|
|
<Button
|
|
label="Équipes disponibles"
|
|
icon="pi pi-check-circle"
|
|
className="p-button-success"
|
|
onClick={() => router.push('/equipes/disponibles')}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const rightToolbarTemplate = () => {
|
|
return (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
label="Statistiques"
|
|
icon="pi pi-chart-bar"
|
|
className="p-button-secondary"
|
|
onClick={() => router.push('/equipes/stats')}
|
|
/>
|
|
<Button
|
|
icon="pi pi-refresh"
|
|
className="p-button-outlined"
|
|
onClick={loadEquipesParSpecialite}
|
|
tooltip="Actualiser"
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="flex justify-content-center">
|
|
<i className="pi pi-spin pi-spinner" style={{ fontSize: '2rem' }} />
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="grid">
|
|
<div className="col-12">
|
|
<Toolbar
|
|
className="mb-4"
|
|
left={leftToolbarTemplate}
|
|
right={rightToolbarTemplate}
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center mb-4">
|
|
<h2 className="m-0">Équipes par Spécialité</h2>
|
|
<div className="flex gap-2">
|
|
<Badge value={specialites.length} severity="info" />
|
|
<span>spécialités</span>
|
|
<Badge value={specialites.reduce((acc, s) => acc + s.nombreEquipes, 0)} severity="success" />
|
|
<span>équipes total</span>
|
|
</div>
|
|
</div>
|
|
|
|
<TabView activeIndex={activeIndex} onTabChange={(e) => setActiveIndex(e.index)}>
|
|
<TabPanel header="Vue par Spécialité">
|
|
<div className="grid">
|
|
{specialites.map((specialiteData) => (
|
|
<div key={specialiteData.specialite} className="col-12">
|
|
{renderSpecialiteCard(specialiteData)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TabPanel>
|
|
|
|
<TabPanel header="Vue Synthétique">
|
|
<div className="grid">
|
|
{specialites.map((specialiteData) => (
|
|
<div key={specialiteData.specialite} className="col-12 lg:col-6 xl:col-4">
|
|
<Card className="h-full">
|
|
<div className="text-center">
|
|
<Tag
|
|
value={specialiteLabels[specialiteData.specialite]}
|
|
severity={specialiteColors[specialiteData.specialite] as any}
|
|
className="mb-3"
|
|
/>
|
|
<div className="text-3xl font-bold mb-2">{specialiteData.nombreEquipes}</div>
|
|
<div className="text-500 mb-3">équipes</div>
|
|
|
|
<div className="grid text-sm">
|
|
<div className="col-6">
|
|
<div className="text-green-500 font-bold">{specialiteData.equipesActives}</div>
|
|
<div>Actives</div>
|
|
</div>
|
|
<div className="col-6">
|
|
<div className="text-blue-500 font-bold">{specialiteData.equipesDisponibles}</div>
|
|
<div>Disponibles</div>
|
|
</div>
|
|
</div>
|
|
|
|
<ProgressBar
|
|
value={specialiteData.tauxOccupationMoyen}
|
|
className="mt-3 mb-3"
|
|
/>
|
|
|
|
<Button
|
|
label="Voir détails"
|
|
icon="pi pi-eye"
|
|
className="p-button-sm w-full"
|
|
onClick={() => setActiveIndex(0)}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TabPanel>
|
|
</TabView>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default EquipesSpecialitesPage;
|