Files
btpxpress-frontend/app/(main)/chantiers/execution-granulaire/page.tsx
dahoud a8825a058b Fix: Corriger toutes les erreurs de build du frontend
- Correction des erreurs TypeScript dans userService.ts et workflowTester.ts
- Ajout des propriétés manquantes aux objets User mockés
- Conversion des dates de string vers objets Date
- Correction des appels asynchrones et des types incompatibles
- Ajout de dynamic rendering pour résoudre les erreurs useSearchParams
- Enveloppement de useSearchParams dans Suspense boundary
- Configuration de force-dynamic au niveau du layout principal

Build réussi: 126 pages générées avec succès

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 13:23:08 +00:00

382 lines
14 KiB
TypeScript

'use client';
export const dynamic = 'force-dynamic';
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;