Initial commit
This commit is contained in:
386
components/phases/PhasesTimelinePreview.tsx
Normal file
386
components/phases/PhasesTimelinePreview.tsx
Normal file
@@ -0,0 +1,386 @@
|
||||
'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;
|
||||
Reference in New Issue
Block a user