Initial commit
This commit is contained in:
377
app/(main)/chantiers/execution-granulaire/page.tsx
Normal file
377
app/(main)/chantiers/execution-granulaire/page.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
'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"
|
||||
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;
|
||||
Reference in New Issue
Block a user