Files
btpxpress-frontend/components/phases/PhasesTimelinePreview.tsx
2025-10-13 05:29:32 +02:00

386 lines
16 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import { Card } from 'primereact/card';
import { Timeline } from 'primereact/timeline';
import { Badge } from 'primereact/badge';
import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button';
import { Accordion, AccordionTab } from 'primereact/accordion';
import { ProgressBar } from 'primereact/progressbar';
import { Tooltip } from 'primereact/tooltip';
import chantierTemplateService from '../../services/chantierTemplateService';
import type { TypeChantier, PhaseTemplate } from '../../types/chantier-templates';
import type { ChantierPreview } from '../../types/chantier-form';
interface PhasesTimelinePreviewProps {
typeChantier: TypeChantier;
dateDebut: Date;
surface?: number;
nombreNiveaux?: number;
inclureSousPhases?: boolean;
ajusterDelais?: boolean;
margeSecurite?: number;
className?: string;
showDetails?: boolean;
compact?: boolean;
}
interface PhaseTimelineItem {
id: string;
nom: string;
description: string;
dateDebut: Date;
dateFin: Date;
duree: number;
ordreExecution: number;
critique: boolean;
prerequis: string[];
sousPhases?: PhaseTimelineItem[];
competences: string[];
status?: 'planned' | 'current' | 'completed' | 'late';
}
const PhasesTimelinePreview: React.FC<PhasesTimelinePreviewProps> = ({
typeChantier,
dateDebut,
surface,
nombreNiveaux,
inclureSousPhases = true,
ajusterDelais = true,
margeSecurite = 5,
className = '',
showDetails = true,
compact = false
}) => {
const [timelineItems, setTimelineItems] = useState<PhaseTimelineItem[]>([]);
const [preview, setPreview] = useState<ChantierPreview | null>(null);
const [loading, setLoading] = useState(true);
const [expandedPhases, setExpandedPhases] = useState<string[]>([]);
useEffect(() => {
generateTimeline();
}, [typeChantier, dateDebut, surface, nombreNiveaux, inclureSousPhases, ajusterDelais, margeSecurite]);
const generateTimeline = async () => {
setLoading(true);
try {
const template = chantierTemplateService.getTemplate(typeChantier);
const complexity = chantierTemplateService.analyzeComplexity(typeChantier);
const planning = chantierTemplateService.calculatePlanning(typeChantier, dateDebut);
// Générer la prévisualisation
const previewData: ChantierPreview = {
typeChantier,
nom: template.nom,
dureeEstimee: template.dureeMoyenneJours,
dateFinEstimee: planning.dateFin,
complexite: complexity,
phasesCount: template.phases.length,
sousePhasesCount: template.phases.reduce((total, phase) => total + (phase.sousPhases?.length || 0), 0),
specificites: template.specificites || [],
reglementations: template.reglementations || []
};
setPreview(previewData);
// Convertir les phases du template en éléments timeline
const items: PhaseTimelineItem[] = [];
let currentDate = new Date(dateDebut);
template.phases.forEach((phase, index) => {
const adjustedDuration = ajusterDelais ?
Math.ceil(phase.dureePrevueJours * (complexity.score / 100)) :
phase.dureePrevueJours;
const phaseItem: PhaseTimelineItem = {
id: phase.id,
nom: phase.nom,
description: phase.description,
dateDebut: new Date(currentDate),
dateFin: addDays(currentDate, adjustedDuration),
duree: adjustedDuration,
ordreExecution: phase.ordreExecution,
critique: phase.critique,
prerequis: phase.prerequis || [],
competences: phase.competencesRequises || [],
status: 'planned'
};
if (inclureSousPhases && phase.sousPhases) {
let sousPhaseDate = new Date(currentDate);
phaseItem.sousPhases = phase.sousPhases.map(sousPhase => {
const sousPhaseAdjustedDuration = ajusterDelais ?
Math.ceil(sousPhase.dureePrevueJours * (complexity.score / 100)) :
sousPhase.dureePrevueJours;
const item: PhaseTimelineItem = {
id: sousPhase.id,
nom: sousPhase.nom,
description: sousPhase.description,
dateDebut: new Date(sousPhaseDate),
dateFin: addDays(sousPhaseDate, sousPhaseAdjustedDuration),
duree: sousPhaseAdjustedDuration,
ordreExecution: sousPhase.ordreExecution,
critique: sousPhase.critique,
prerequis: sousPhase.prerequis || [],
competences: sousPhase.competencesRequises || [],
status: 'planned'
};
sousPhaseDate = addDays(sousPhaseDate, sousPhaseAdjustedDuration);
return item;
});
}
items.push(phaseItem);
currentDate = addDays(currentDate, adjustedDuration + (margeSecurite || 0));
});
setTimelineItems(items);
} catch (error) {
console.error('Erreur lors de la génération du timeline:', error);
} finally {
setLoading(false);
}
};
const addDays = (date: Date, days: number): Date => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
};
const formatDate = (date: Date): string => {
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const getSeverityByStatus = (status: string) => {
switch (status) {
case 'completed': return 'success';
case 'current': return 'info';
case 'late': return 'danger';
default: return 'secondary';
}
};
const getIconByPhase = (phase: PhaseTimelineItem): string => {
if (phase.critique) return 'pi pi-exclamation-triangle';
if (phase.competences.includes('ELECTRICITE')) return 'pi pi-bolt';
if (phase.competences.includes('PLOMBERIE')) return 'pi pi-home';
if (phase.competences.includes('MACONNERIE')) return 'pi pi-building';
if (phase.competences.includes('CHARPENTE')) return 'pi pi-sitemap';
return 'pi pi-circle';
};
const renderPhaseCard = (phase: PhaseTimelineItem, isSubPhase = false) => (
<div
key={phase.id}
className={`${isSubPhase ? 'ml-4 surface-100' : 'surface-card'} p-3 border-1 surface-border border-round mb-2`}
>
<div className="flex justify-content-between align-items-start mb-2">
<div className="flex-1">
<div className="flex align-items-center gap-2 mb-1">
<i className={getIconByPhase(phase)} />
<span className={`font-semibold ${isSubPhase ? 'text-sm' : ''}`}>
{phase.nom}
</span>
{phase.critique && (
<Tag value="Critique" severity="danger" className="text-xs" />
)}
</div>
<p className="text-600 text-sm m-0 mb-2">{phase.description}</p>
{/* Compétences requises */}
{phase.competences.length > 0 && (
<div className="flex gap-1 mb-2">
{phase.competences.map(comp => (
<Badge
key={comp}
value={comp}
severity="info"
className="text-xs"
/>
))}
</div>
)}
</div>
<div className="text-right">
<div className="text-sm font-semibold text-primary">
{phase.duree} jour{phase.duree > 1 ? 's' : ''}
</div>
<div className="text-xs text-600">
{formatDate(phase.dateDebut)} - {formatDate(phase.dateFin)}
</div>
</div>
</div>
{/* Prérequis */}
{showDetails && phase.prerequis.length > 0 && (
<div className="mt-2 p-2 surface-100 border-round">
<div className="text-xs font-semibold text-600 mb-1">Prérequis:</div>
<div className="text-xs text-700">
{phase.prerequis.join(', ')}
</div>
</div>
)}
{/* Sous-phases */}
{!compact && inclureSousPhases && phase.sousPhases && phase.sousPhases.length > 0 && (
<div className="mt-3">
<Button
label={`${expandedPhases.includes(phase.id) ? 'Masquer' : 'Voir'} les sous-phases (${phase.sousPhases.length})`}
icon={`pi pi-chevron-${expandedPhases.includes(phase.id) ? 'up' : 'down'}`}
className="p-button-text p-button-sm"
onClick={() => {
setExpandedPhases(prev =>
prev.includes(phase.id)
? prev.filter(id => id !== phase.id)
: [...prev, phase.id]
);
}}
/>
{expandedPhases.includes(phase.id) && (
<div className="mt-2">
{phase.sousPhases.map(sousPhase =>
renderPhaseCard(sousPhase, true)
)}
</div>
)}
</div>
)}
</div>
);
const renderCompactTimeline = () => {
const timelineData = timelineItems.map(phase => ({
status: phase.nom,
date: formatDate(phase.dateDebut),
icon: getIconByPhase(phase),
color: phase.critique ? '#ef4444' : '#3b82f6',
phase: phase
}));
return (
<Timeline
value={timelineData}
opposite={(item) => (
<div className="text-right">
<div className="font-semibold">{item.status}</div>
<div className="text-600 text-sm">{item.phase.duree} jour{item.phase.duree > 1 ? 's' : ''}</div>
</div>
)}
content={(item) => (
<div>
<div className="text-600 text-sm">{item.date}</div>
{item.phase.critique && (
<Tag value="Critique" severity="danger" className="text-xs mt-1" />
)}
</div>
)}
className="w-full"
/>
);
};
if (loading) {
return (
<Card className={className}>
<ProgressBar mode="indeterminate" style={{ height: '4px' }} />
<div className="text-center mt-3">
<span className="text-600">Génération du planning...</span>
</div>
</Card>
);
}
return (
<Card
title="Planning prévisionnel des phases"
className={className}
subTitle={preview ? `${preview.dureeEstimee} jours estimés • ${preview.phasesCount} phases • ${preview.sousePhasesCount} sous-phases` : undefined}
>
{/* Métriques rapides */}
{preview && (
<div className="grid mb-4">
<div className="col-3">
<div className="text-center p-2 border-1 surface-border border-round">
<div className="text-lg font-bold text-primary">{preview.phasesCount}</div>
<div className="text-600 text-sm">Phases</div>
</div>
</div>
<div className="col-3">
<div className="text-center p-2 border-1 surface-border border-round">
<div className="text-lg font-bold text-primary">{preview.sousePhasesCount}</div>
<div className="text-600 text-sm">Sous-phases</div>
</div>
</div>
<div className="col-3">
<div className="text-center p-2 border-1 surface-border border-round">
<div className="text-lg font-bold text-orange-500">{preview.dureeEstimee}</div>
<div className="text-600 text-sm">Jours</div>
</div>
</div>
<div className="col-3">
<div className="text-center p-2 border-1 surface-border border-round">
<Tag
value={preview.complexite.niveau}
severity={
preview.complexite.niveau === 'SIMPLE' ? 'success' :
preview.complexite.niveau === 'MOYEN' ? 'warning' :
'danger'
}
className="text-xs"
/>
<div className="text-600 text-sm mt-1">Complexité</div>
</div>
</div>
</div>
)}
{/* Affichage compact ou détaillé */}
{compact ? (
renderCompactTimeline()
) : (
<div className="max-h-30rem overflow-auto">
{timelineItems.map(phase => renderPhaseCard(phase))}
</div>
)}
{/* Légende */}
{showDetails && (
<div className="mt-4 p-3 surface-100 border-round">
<div className="text-sm font-semibold text-600 mb-2">Légende:</div>
<div className="flex flex-wrap gap-3 text-xs">
<div className="flex align-items-center gap-1">
<i className="pi pi-exclamation-triangle text-red-500"></i>
<span>Phase critique</span>
</div>
<div className="flex align-items-center gap-1">
<i className="pi pi-bolt text-yellow-500"></i>
<span>Électricité</span>
</div>
<div className="flex align-items-center gap-1">
<i className="pi pi-home text-blue-500"></i>
<span>Plomberie</span>
</div>
<div className="flex align-items-center gap-1">
<i className="pi pi-building text-gray-600"></i>
<span>Maçonnerie</span>
</div>
</div>
</div>
)}
<Tooltip target=".phase-tooltip" />
</Card>
);
};
export default PhasesTimelinePreview;