380 lines
14 KiB
TypeScript
380 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { DataTable } from 'primereact/datatable';
|
|
import { Column } from 'primereact/column';
|
|
import { Button } from 'primereact/button';
|
|
import { Card } from 'primereact/card';
|
|
import { ProgressBar } from 'primereact/progressbar';
|
|
import { Badge } from 'primereact/badge';
|
|
import { Toast } from 'primereact/toast';
|
|
import { InputText } from 'primereact/inputtext';
|
|
import { FilterMatchMode } from 'primereact/api';
|
|
import { apiClient } from '../../../../services/api-client';
|
|
import { executionGranulaireService } from '../../../../services/executionGranulaireService';
|
|
|
|
/**
|
|
* Interface pour les chantiers avec avancement granulaire
|
|
*/
|
|
interface ChantierExecutionGranulaire {
|
|
id: string;
|
|
nom: string;
|
|
client: string | { nom: string; prenom?: string };
|
|
statut: 'EN_COURS' | 'PLANIFIE' | 'TERMINE' | 'EN_RETARD';
|
|
dateDebut: Date;
|
|
dateFinPrevue: Date;
|
|
avancementGranulaire?: number;
|
|
totalTaches: number;
|
|
tachesTerminees: number;
|
|
avancementCalcule: boolean; // Si l'avancement est basé sur les tâches ou les dates
|
|
derniereMAJ?: Date;
|
|
}
|
|
|
|
/**
|
|
* Page listant tous les chantiers avec possibilité d'accès à l'exécution granulaire
|
|
*/
|
|
const ChantiersExecutionGranulaire = () => {
|
|
const router = useRouter();
|
|
const toast = useRef<Toast>(null);
|
|
|
|
// États principaux
|
|
const [chantiers, setChantiers] = useState<ChantierExecutionGranulaire[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [globalFilterValue, setGlobalFilterValue] = useState('');
|
|
const [filters, setFilters] = useState({
|
|
global: { value: null, matchMode: FilterMatchMode.CONTAINS }
|
|
});
|
|
|
|
useEffect(() => {
|
|
loadChantiersWithAvancement();
|
|
}, []);
|
|
|
|
/**
|
|
* Charge tous les chantiers avec leur avancement granulaire
|
|
*/
|
|
const loadChantiersWithAvancement = async () => {
|
|
try {
|
|
setLoading(true);
|
|
|
|
// Charger tous les chantiers actifs
|
|
const chantiersResponse = await apiClient.get('/chantiers');
|
|
const allChantiers = chantiersResponse.data.filter((c: any) =>
|
|
c.actif && (c.statut === 'EN_COURS' || c.statut === 'PLANIFIE')
|
|
);
|
|
|
|
// Pour chaque chantier, essayer d'obtenir l'avancement granulaire
|
|
const chantiersWithAvancement = await Promise.all(
|
|
allChantiers.map(async (chantier: any) => {
|
|
let avancementData = null;
|
|
let avancementCalcule = false;
|
|
|
|
try {
|
|
// Essayer d'obtenir l'avancement granulaire
|
|
avancementData = await executionGranulaireService.getAvancementGranulaire(chantier.id);
|
|
avancementCalcule = true;
|
|
} catch (error) {
|
|
// Pas encore d'avancement granulaire, on utilisera un calcul basé sur les dates
|
|
console.log(`Avancement granulaire non disponible pour ${chantier.nom}`);
|
|
}
|
|
|
|
return {
|
|
id: chantier.id,
|
|
nom: chantier.nom,
|
|
client: chantier.client?.nom || chantier.client || 'Client non défini',
|
|
statut: chantier.statut,
|
|
dateDebut: new Date(chantier.dateDebut),
|
|
dateFinPrevue: new Date(chantier.dateFinPrevue || Date.now()),
|
|
avancementGranulaire: avancementData?.pourcentage || calculateDateBasedProgress(chantier),
|
|
totalTaches: avancementData?.totalTaches || 0,
|
|
tachesTerminees: avancementData?.tachesTerminees || 0,
|
|
avancementCalcule,
|
|
derniereMAJ: avancementData?.derniereMAJ
|
|
};
|
|
})
|
|
);
|
|
|
|
setChantiers(chantiersWithAvancement);
|
|
} catch (error) {
|
|
console.error('Erreur lors du chargement des chantiers:', error);
|
|
showToast('error', 'Erreur', 'Impossible de charger la liste des chantiers');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Calcule l'avancement basé sur les dates si pas d'avancement granulaire
|
|
*/
|
|
const calculateDateBasedProgress = (chantier: any): number => {
|
|
if (chantier.statut === 'TERMINE') return 100;
|
|
if (chantier.statut === 'PLANIFIE') return 0;
|
|
|
|
if (!chantier.dateDebut || !chantier.dateFinPrevue) return 10;
|
|
|
|
const now = new Date();
|
|
const start = new Date(chantier.dateDebut);
|
|
const end = new Date(chantier.dateFinPrevue);
|
|
|
|
if (now < start) return 0;
|
|
if (now > end) return 100;
|
|
|
|
const totalDays = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
|
|
const elapsedDays = (now.getTime() - start.getTime()) / (1000 * 60 * 60 * 24);
|
|
|
|
return Math.min(Math.max((elapsedDays / totalDays) * 100, 0), 100);
|
|
};
|
|
|
|
/**
|
|
* Affiche un toast message
|
|
*/
|
|
const showToast = (severity: 'success' | 'info' | 'warn' | 'error', summary: string, detail: string) => {
|
|
toast.current?.show({ severity, summary, detail, life: 3000 });
|
|
};
|
|
|
|
/**
|
|
* Navigue vers la page d'exécution granulaire d'un chantier
|
|
*/
|
|
const navigateToExecution = (chantier: ChantierExecutionGranulaire) => {
|
|
router.push(`/chantiers/${chantier.id}/execution-granulaire`);
|
|
};
|
|
|
|
/**
|
|
* Initialise l'exécution granulaire pour un chantier
|
|
*/
|
|
const initialiserExecution = async (chantier: ChantierExecutionGranulaire) => {
|
|
try {
|
|
await executionGranulaireService.initialiserExecutionGranulaire(chantier.id);
|
|
showToast('success', 'Succès', `Exécution granulaire initialisée pour ${chantier.nom}`);
|
|
await loadChantiersWithAvancement(); // Recharger pour mettre à jour l'état
|
|
} catch (error) {
|
|
console.error('Erreur lors de l\'initialisation:', error);
|
|
showToast('error', 'Erreur', 'Impossible d\'initialiser l\'exécution granulaire');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Gestion du filtre global
|
|
*/
|
|
const onGlobalFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
let _filters = { ...filters };
|
|
_filters['global'].value = value;
|
|
|
|
setFilters(_filters);
|
|
setGlobalFilterValue(value);
|
|
};
|
|
|
|
/**
|
|
* Template pour l'affichage du nom du client
|
|
*/
|
|
const clientBodyTemplate = (rowData: ChantierExecutionGranulaire) => {
|
|
if (typeof rowData.client === 'string') {
|
|
return rowData.client;
|
|
}
|
|
return rowData.client?.nom || 'Client inconnu';
|
|
};
|
|
|
|
/**
|
|
* Template pour l'affichage du statut
|
|
*/
|
|
const statutBodyTemplate = (rowData: ChantierExecutionGranulaire) => {
|
|
const severityMap: Record<string, "success" | "info" | "warning" | "danger"> = {
|
|
'EN_COURS': 'info',
|
|
'PLANIFIE': 'warning',
|
|
'TERMINE': 'success',
|
|
'EN_RETARD': 'danger'
|
|
};
|
|
return <Badge value={rowData.statut} severity={severityMap[rowData.statut]} />;
|
|
};
|
|
|
|
/**
|
|
* Template pour l'affichage de l'avancement
|
|
*/
|
|
const avancementBodyTemplate = (rowData: ChantierExecutionGranulaire) => {
|
|
const progress = Math.round(rowData.avancementGranulaire || 0);
|
|
|
|
return (
|
|
<div className="flex align-items-center gap-2">
|
|
<ProgressBar
|
|
value={progress}
|
|
style={{ width: '100px', height: '8px' }}
|
|
showValue={false}
|
|
/>
|
|
<span className="font-medium">{progress}%</span>
|
|
{!rowData.avancementCalcule && (
|
|
<i className="pi pi-calendar text-orange-500" title="Avancement basé sur les dates" />
|
|
)}
|
|
{rowData.avancementCalcule && (
|
|
<i className="pi pi-check-circle text-green-500" title="Avancement granulaire disponible" />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Template pour l'affichage des tâches
|
|
*/
|
|
const tachesBodyTemplate = (rowData: ChantierExecutionGranulaire) => {
|
|
if (!rowData.avancementCalcule) {
|
|
return <span className="text-500">-</span>;
|
|
}
|
|
|
|
return (
|
|
<div className="text-sm">
|
|
<div>{rowData.tachesTerminees} / {rowData.totalTaches}</div>
|
|
<div className="text-500">tâches terminées</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Template pour les actions
|
|
*/
|
|
const actionsBodyTemplate = (rowData: ChantierExecutionGranulaire) => {
|
|
return (
|
|
<div className="flex gap-2">
|
|
{rowData.avancementCalcule ? (
|
|
<Button
|
|
icon="pi pi-play"
|
|
label="Exécuter"
|
|
size="small"
|
|
onClick={() => navigateToExecution(rowData)}
|
|
tooltip="Accéder à l'exécution granulaire"
|
|
/>
|
|
) : (
|
|
<Button
|
|
icon="pi pi-cog"
|
|
label="Initialiser"
|
|
size="small"
|
|
severity={"secondary" as any}
|
|
onClick={() => initialiserExecution(rowData)}
|
|
tooltip="Initialiser l'exécution granulaire"
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* En-tête du tableau avec filtre de recherche
|
|
*/
|
|
const renderHeader = () => {
|
|
return (
|
|
<div className="flex justify-content-between align-items-center">
|
|
<h6 className="m-0">Chantiers - Exécution Granulaire</h6>
|
|
<span className="p-input-icon-left">
|
|
<i className="pi pi-search" />
|
|
<InputText
|
|
value={globalFilterValue}
|
|
onChange={onGlobalFilterChange}
|
|
placeholder="Rechercher..."
|
|
/>
|
|
</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="grid">
|
|
<Toast ref={toast} />
|
|
|
|
{/* En-tête de la page */}
|
|
<div className="col-12">
|
|
<Card>
|
|
<div className="flex justify-content-between align-items-center">
|
|
<div>
|
|
<h4 className="mt-0 mb-2">Exécution Granulaire des Chantiers</h4>
|
|
<p className="text-600 mt-0">
|
|
Gérez l'avancement détaillé de vos chantiers avec un suivi granulaire par tâche
|
|
</p>
|
|
</div>
|
|
<Button
|
|
icon="pi pi-refresh"
|
|
label="Actualiser"
|
|
onClick={loadChantiersWithAvancement}
|
|
loading={loading}
|
|
/>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Tableau des chantiers */}
|
|
<div className="col-12">
|
|
<Card>
|
|
<DataTable
|
|
value={chantiers}
|
|
loading={loading}
|
|
header={renderHeader()}
|
|
filters={filters}
|
|
globalFilterFields={['nom', 'client']}
|
|
emptyMessage="Aucun chantier trouvé"
|
|
paginator
|
|
rows={10}
|
|
className="p-datatable-gridlines"
|
|
responsiveLayout="scroll"
|
|
sortField="nom"
|
|
sortOrder={1}
|
|
>
|
|
<Column
|
|
field="nom"
|
|
header="Nom du chantier"
|
|
sortable
|
|
style={{ minWidth: '200px' }}
|
|
/>
|
|
<Column
|
|
field="client"
|
|
header="Client"
|
|
body={clientBodyTemplate}
|
|
sortable
|
|
style={{ width: '150px' }}
|
|
/>
|
|
<Column
|
|
field="statut"
|
|
header="Statut"
|
|
body={statutBodyTemplate}
|
|
sortable
|
|
style={{ width: '120px' }}
|
|
/>
|
|
<Column
|
|
field="dateDebut"
|
|
header="Date de début"
|
|
sortable
|
|
body={(rowData) => rowData.dateDebut.toLocaleDateString('fr-FR')}
|
|
style={{ width: '120px' }}
|
|
/>
|
|
<Column
|
|
field="dateFinPrevue"
|
|
header="Fin prévue"
|
|
sortable
|
|
body={(rowData) => rowData.dateFinPrevue.toLocaleDateString('fr-FR')}
|
|
style={{ width: '120px' }}
|
|
/>
|
|
<Column
|
|
field="avancementGranulaire"
|
|
header="Avancement"
|
|
body={avancementBodyTemplate}
|
|
sortable
|
|
style={{ width: '160px' }}
|
|
/>
|
|
<Column
|
|
header="Tâches"
|
|
body={tachesBodyTemplate}
|
|
style={{ width: '120px' }}
|
|
/>
|
|
<Column
|
|
header="Actions"
|
|
body={actionsBodyTemplate}
|
|
style={{ width: '140px' }}
|
|
/>
|
|
</DataTable>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ChantiersExecutionGranulaire;
|
|
|
|
|