Files
btpxpress-frontend/app/(main)/chantiers/workflow/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

590 lines
26 KiB
TypeScript

'use client';
export const dynamic = 'force-dynamic';
import React, { useState, useEffect, useRef } from 'react';
import { Card } from 'primereact/card';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { Tag } from 'primereact/tag';
import { ProgressBar } from 'primereact/progressbar';
import { Dialog } from 'primereact/dialog';
import { InputTextarea } from 'primereact/inputtextarea';
import { Dropdown } from 'primereact/dropdown';
import { Timeline } from 'primereact/timeline';
import { Toast } from 'primereact/toast';
import { Divider } from 'primereact/divider';
import { Badge } from 'primereact/badge';
import { Knob } from 'primereact/knob';
import chantierService from '../../../../services/chantierService';
/**
* Page Workflow Chantiers BTP Express
* Gestion complète du cycle de vie des chantiers avec transitions d'état
*/
const WorkflowChantiers = () => {
const [chantiers, setChantiers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [workflowDialog, setWorkflowDialog] = useState(false);
const [selectedChantier, setSelectedChantier] = useState<any>(null);
const [nouveauStatut, setNouveauStatut] = useState('');
const [commentaire, setCommentaire] = useState('');
const [historique, setHistorique] = useState<any[]>([]);
const [metriques, setMetriques] = useState<any>({});
const toast = useRef<Toast>(null);
// Définition des transitions workflow BTP
const workflowTransitions = {
'PLANIFIE': [
{ label: 'Démarrer le chantier', value: 'EN_COURS', icon: 'pi-play', color: 'success' },
{ label: 'Annuler', value: 'ANNULE', icon: 'pi-times', color: 'danger' }
],
'EN_COURS': [
{ label: 'Terminer le chantier', value: 'TERMINE', icon: 'pi-check', color: 'success' },
{ label: 'Suspendre', value: 'SUSPENDU', icon: 'pi-pause', color: 'warning' },
{ label: 'Annuler', value: 'ANNULE', icon: 'pi-times', color: 'danger' }
],
'SUSPENDU': [
{ label: 'Reprendre', value: 'EN_COURS', icon: 'pi-play', color: 'info' },
{ label: 'Annuler définitivement', value: 'ANNULE', icon: 'pi-times', color: 'danger' }
],
'TERMINE': [], // Statut final
'ANNULE': [] // Statut final
};
const statutsConfig = {
'PLANIFIE': { color: 'info', icon: 'pi-calendar', label: 'Planifié' },
'EN_COURS': { color: 'success', icon: 'pi-cog', label: 'En cours' },
'SUSPENDU': { color: 'warning', icon: 'pi-pause', label: 'Suspendu' },
'TERMINE': { color: 'success', icon: 'pi-check-circle', label: 'Terminé' },
'ANNULE': { color: 'danger', icon: 'pi-times-circle', label: 'Annulé' }
};
useEffect(() => {
loadChantiers();
loadMetriques();
}, []);
const loadChantiers = async () => {
try {
setLoading(true);
// Simulation données chantiers avec workflow
const mockChantiers = [
{
id: '1',
nom: 'Rénovation Villa Rousseau',
client: 'Claire Rousseau',
statut: 'EN_COURS' as any,
avancement: 65,
dateDebut: '2025-01-15',
dateFinPrevue: '2025-03-20',
montantPrevu: 85000,
montantReel: 72500,
equipe: 'Équipe Rénovation A',
phases: [
{ nom: 'Démolition', statut: 'TERMINE' as any, avancement: 100 },
{ nom: 'Gros œuvre', statut: 'EN_COURS' as any, avancement: 80 },
{ nom: 'Second œuvre', statut: 'PLANIFIE', avancement: 0 },
{ nom: 'Finitions', statut: 'PLANIFIE', avancement: 0 }
],
alertes: ['Retard de 3 jours sur livraison matériaux'],
derniereMiseAJour: new Date()
},
{
id: '2',
nom: 'Extension Maison Martin',
client: 'Sophie Martin',
statut: 'PLANIFIE',
avancement: 0,
dateDebut: '2025-02-10',
dateFinPrevue: '2025-05-15',
montantPrevu: 45000,
montantReel: 0,
equipe: 'Équipe Extension B',
phases: [
{ nom: 'Fondations', statut: 'PLANIFIE', avancement: 0 },
{ nom: 'Élévation', statut: 'PLANIFIE', avancement: 0 },
{ nom: 'Couverture', statut: 'PLANIFIE', avancement: 0 },
{ nom: 'Aménagement', statut: 'PLANIFIE', avancement: 0 }
],
alertes: [],
derniereMiseAJour: new Date()
},
{
id: '3',
nom: 'Réfection Toiture Dupont',
client: 'Jean Dupont',
statut: 'SUSPENDU' as any,
avancement: 30,
dateDebut: '2025-01-08',
dateFinPrevue: '2025-02-28',
montantPrevu: 28000,
montantReel: 12000,
equipe: 'Équipe Couverture C',
phases: [
{ nom: 'Dépose ancienne toiture', statut: 'TERMINE' as any, avancement: 100 },
{ nom: 'Charpente', statut: 'SUSPENDU' as any, avancement: 60 },
{ nom: 'Couverture neuve', statut: 'PLANIFIE', avancement: 0 },
{ nom: 'Isolation', statut: 'PLANIFIE', avancement: 0 }
],
alertes: ['Chantier suspendu - Problème météorologique', 'Attente validation assurance'],
derniereMiseAJour: new Date()
}
];
setChantiers(mockChantiers);
} catch (error) {
console.error('Erreur chargement chantiers:', error);
} finally {
setLoading(false);
}
};
const loadMetriques = () => {
setMetriques({
totalChantiers: 12,
enCours: 5,
planifies: 4,
terminesRecemment: 2,
suspendus: 1,
tauxReussite: 92,
delaiMoyen: -2 // Négatif = en avance
});
};
const ouvrirWorkflow = (chantier: any) => {
setSelectedChantier(chantier);
setNouveauStatut('');
setCommentaire('');
// Charger historique du chantier
const mockHistorique = [
{
date: new Date('2025-01-15T08:00:00'),
statut: 'EN_COURS' as any,
utilisateur: 'M. Laurent',
commentaire: 'Démarrage chantier - Équipe mobilisée',
automatique: false
},
{
date: new Date('2025-01-20T14:30:00'),
statut: 'EN_COURS' as any,
utilisateur: 'Système',
commentaire: 'Phase démolition terminée automatiquement',
automatique: true
},
{
date: new Date('2025-01-25T10:15:00'),
statut: 'EN_COURS' as any,
utilisateur: 'Mme Petit',
commentaire: 'Avancement gros œuvre - 50% réalisé',
automatique: false
}
];
setHistorique(mockHistorique);
setWorkflowDialog(true);
};
const executerTransition = async () => {
if (!selectedChantier || !nouveauStatut) return;
try {
// Simuler appel API pour changer le statut
const chantierMisAJour = {
...selectedChantier,
statut: nouveauStatut,
derniereMiseAJour: new Date()
};
// Logique métier spécifique selon le statut
if (nouveauStatut === 'EN_COURS') {
chantierMisAJour.dateDebutReel = new Date();
} else if (nouveauStatut === 'TERMINE') {
chantierMisAJour.dateFinReelle = new Date();
chantierMisAJour.avancement = 100;
}
// Mettre à jour la liste
const chantiersUpdated = chantiers.map(c =>
c.id === selectedChantier.id ? chantierMisAJour : c
);
setChantiers(chantiersUpdated);
// Ajouter à l'historique
const nouvelleEntree = {
date: new Date(),
statut: nouveauStatut,
utilisateur: 'Utilisateur actuel',
commentaire: commentaire || `Changement vers ${statutsConfig[nouveauStatut as keyof typeof statutsConfig]?.label}`,
automatique: false
};
setHistorique([nouvelleEntree, ...historique]);
toast.current?.show({
severity: 'success',
summary: 'Transition réussie',
detail: `Chantier "${selectedChantier.nom}" passé en statut ${statutsConfig[nouveauStatut as keyof typeof statutsConfig]?.label}`,
life: 4000
});
setWorkflowDialog(false);
loadMetriques(); // Recalculer les métriques
} catch (error) {
console.error('Erreur transition:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible d\'effectuer la transition',
life: 3000
});
}
};
const statutBodyTemplate = (rowData: any) => {
const config = statutsConfig[rowData.statut as keyof typeof statutsConfig];
return (
<div className="flex align-items-center gap-2">
<i className={`pi ${config.icon} text-${config.color}`} />
<Tag value={config.label} severity={config.color as any} />
</div>
);
};
const avancementBodyTemplate = (rowData: any) => {
return (
<div className="flex align-items-center gap-2">
<ProgressBar
value={rowData.avancement}
style={{ width: '100px', height: '8px' }}
color={rowData.avancement >= 100 ? '#10B981' : rowData.avancement >= 50 ? '#F59E0B' : '#3B82F6'}
/>
<span className="text-sm font-medium">{rowData.avancement}%</span>
</div>
);
};
const alertesBodyTemplate = (rowData: any) => {
if (!rowData.alertes || rowData.alertes.length === 0) {
return <span className="text-color-secondary">-</span>;
}
return (
<div className="flex align-items-center gap-2">
<Badge value={rowData.alertes.length} severity="danger" />
<Button
icon="pi pi-exclamation-triangle"
text
size="small"
severity="danger"
tooltip={rowData.alertes.join(', ')}
/>
</div>
);
};
const actionsBodyTemplate = (rowData: any) => {
const transitionsPossibles = workflowTransitions[rowData.statut as keyof typeof workflowTransitions] || [];
return (
<div className="flex gap-2">
<Button
icon="pi pi-cog"
size="small"
severity="info"
tooltip="Gérer workflow"
onClick={() => ouvrirWorkflow(rowData)}
/>
{transitionsPossibles.length > 0 && (
<Button
icon="pi pi-arrow-right"
size="small"
severity="success"
tooltip="Transitions disponibles"
onClick={() => ouvrirWorkflow(rowData)}
/>
)}
<Button
icon="pi pi-eye"
size="small"
severity="help"
tooltip="Voir détails"
/>
</div>
);
};
const phasesBodyTemplate = (rowData: any) => {
const phasesTerminees = rowData.phases?.filter((p: any) => p.statut === 'TERMINE').length || 0;
const totalPhases = rowData.phases?.length || 0;
return (
<div className="flex align-items-center gap-2">
<span className="text-sm">{phasesTerminees}/{totalPhases}</span>
<ProgressBar
value={totalPhases > 0 ? (phasesTerminees / totalPhases) * 100 : 0}
style={{ width: '60px', height: '6px' }}
/>
</div>
);
};
return (
<div className="grid">
<Toast ref={toast} />
{/* Métriques Workflow */}
<div className="col-12">
<div className="grid">
<div className="col-12 md:col-2">
<Card>
<div className="text-center">
<div className="text-2xl font-bold text-blue-500">{metriques.totalChantiers}</div>
<div className="text-color-secondary">Total chantiers</div>
</div>
</Card>
</div>
<div className="col-12 md:col-2">
<Card>
<div className="text-center">
<div className="text-2xl font-bold text-green-500">{metriques.enCours}</div>
<div className="text-color-secondary">En cours</div>
</div>
</Card>
</div>
<div className="col-12 md:col-2">
<Card>
<div className="text-center">
<div className="text-2xl font-bold text-cyan-500">{metriques.planifies}</div>
<div className="text-color-secondary">Planifiés</div>
</div>
</Card>
</div>
<div className="col-12 md:col-2">
<Card>
<div className="text-center">
<div className="text-2xl font-bold text-orange-500">{metriques.suspendus}</div>
<div className="text-color-secondary">Suspendus</div>
</div>
</Card>
</div>
<div className="col-12 md:col-2">
<Card>
<div className="text-center">
<Knob
value={metriques.tauxReussite}
size={60}
strokeWidth={8}
valueColor="#10B981"
/>
<div className="text-color-secondary mt-2">Taux réussite</div>
</div>
</Card>
</div>
<div className="col-12 md:col-2">
<Card>
<div className="text-center">
<div className={`text-2xl font-bold ${metriques.delaiMoyen < 0 ? 'text-green-500' : 'text-red-500'}`}>
{metriques.delaiMoyen > 0 ? `+${metriques.delaiMoyen}` : metriques.delaiMoyen}j
</div>
<div className="text-color-secondary">Délai moyen</div>
</div>
</Card>
</div>
</div>
</div>
<div className="col-12">
<Divider />
</div>
{/* Tableau chantiers avec workflow */}
<div className="col-12">
<Card title="Gestion Workflow Chantiers">
<DataTable
value={chantiers}
loading={loading}
paginator
rows={10}
dataKey="id"
emptyMessage="Aucun chantier trouvé"
responsiveLayout="scroll"
>
<Column field="nom" header="Chantier" sortable style={{ minWidth: '200px' }} />
<Column field="client" header="Client" sortable style={{ minWidth: '150px' }} />
<Column header="Statut" body={statutBodyTemplate} sortable style={{ minWidth: '120px' }} />
<Column header="Avancement" body={avancementBodyTemplate} style={{ minWidth: '150px' }} />
<Column header="Phases" body={phasesBodyTemplate} style={{ minWidth: '100px' }} />
<Column field="equipe" header="Équipe" style={{ minWidth: '150px' }} />
<Column header="Alertes" body={alertesBodyTemplate} style={{ minWidth: '80px' }} />
<Column
field="montantPrevu"
header="Budget"
body={(rowData) => new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
notation: 'compact'
}).format(rowData.montantPrevu)}
style={{ minWidth: '100px' }}
/>
<Column body={actionsBodyTemplate} style={{ minWidth: '150px' }} />
</DataTable>
</Card>
</div>
{/* Dialog Workflow */}
<Dialog
visible={workflowDialog}
style={{ width: '90vw', height: '90vh' }}
header={`Workflow - ${selectedChantier?.nom}`}
modal
onHide={() => setWorkflowDialog(false)}
maximizable
>
{selectedChantier && (
<div className="grid">
{/* Informations chantier */}
<div className="col-12 md:col-4">
<Card title="Informations Chantier">
<div className="flex flex-column gap-3">
<div>
<strong>Client:</strong> {selectedChantier.client}
</div>
<div>
<strong>Statut actuel:</strong>
<Tag
value={statutsConfig[selectedChantier.statut as keyof typeof statutsConfig]?.label}
severity={statutsConfig[selectedChantier.statut as keyof typeof statutsConfig]?.color as any}
className="ml-2"
/>
</div>
<div>
<strong>Avancement:</strong>
<ProgressBar value={selectedChantier.avancement} className="mt-2" />
</div>
<div>
<strong>Équipe:</strong> {selectedChantier.equipe}
</div>
<div>
<strong>Budget:</strong> {new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(selectedChantier.montantPrevu)}
</div>
</div>
</Card>
</div>
{/* Transitions disponibles */}
<div className="col-12 md:col-4">
<Card title="Actions Disponibles">
<div className="flex flex-column gap-3">
{workflowTransitions[selectedChantier.statut as keyof typeof workflowTransitions]?.map((transition) => (
<Button
key={transition.value}
label={transition.label}
icon={`pi ${transition.icon}`}
severity={transition.color}
className="justify-content-start"
onClick={() => setNouveauStatut(transition.value)}
outlined={nouveauStatut !== transition.value}
/>
))}
{workflowTransitions[selectedChantier.statut as keyof typeof workflowTransitions]?.length === 0 && (
<div className="text-center text-color-secondary p-4">
<i className="pi pi-lock text-3xl mb-3" />
<div>Aucune transition disponible</div>
<div className="text-sm">Statut final atteint</div>
</div>
)}
{nouveauStatut && (
<div className="mt-4">
<label className="block text-sm font-medium mb-2">
Commentaire (optionnel)
</label>
<InputTextarea
value={commentaire}
onChange={(e) => setCommentaire(e.target.value)}
rows={3}
placeholder="Ajoutez un commentaire sur cette transition..."
/>
<div className="flex gap-2 mt-3">
<Button
label="Confirmer la transition"
icon="pi pi-check"
severity="success"
onClick={executerTransition}
/>
<Button
label="Annuler"
icon="pi pi-times"
severity={"secondary" as any}
outlined
onClick={() => {
setNouveauStatut('');
setCommentaire('');
}}
/>
</div>
</div>
)}
</div>
</Card>
</div>
{/* Historique */}
<div className="col-12 md:col-4">
<Card title="Historique des Changements">
<Timeline
value={historique}
align="left"
content={(item) => (
<div className="p-2">
<div className="flex align-items-center gap-2 mb-1">
<Tag
value={statutsConfig[item.statut as keyof typeof statutsConfig]?.label}
severity={statutsConfig[item.statut as keyof typeof statutsConfig]?.color as any}
/>
{item.automatique && <Badge value="Auto" severity="info" />}
</div>
<div className="text-sm text-color-secondary mb-1">
{item.date.toLocaleString('fr-FR')}
</div>
<div className="text-sm font-medium mb-1">
{item.utilisateur}
</div>
<div className="text-sm">
{item.commentaire}
</div>
</div>
)}
/>
</Card>
</div>
</div>
)}
</Dialog>
</div>
);
};
export default WorkflowChantiers;