'use client'; /** * Étape 3 : Prévisualisation et génération avec planning intégré * Interface finale de validation et génération des phases */ import React, { useState, useEffect } from 'react'; import { Card } from 'primereact/card'; import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import { Timeline } from 'primereact/timeline'; import { TabView, TabPanel } from 'primereact/tabview'; import { Tag } from 'primereact/tag'; import { Badge } from 'primereact/badge'; import { Divider } from 'primereact/divider'; import { Message } from 'primereact/message'; import { Panel } from 'primereact/panel'; import { ProgressBar } from 'primereact/progressbar'; import { Calendar } from 'primereact/calendar'; import { Accordion, AccordionTab } from 'primereact/accordion'; import { Chart } from 'primereact/chart'; import { WizardConfiguration } from '../PhaseGenerationWizard'; import { Chantier } from '../../../types/btp'; import budgetCoherenceService from '../../../services/budgetCoherenceService'; interface PreviewGenerationStepProps { configuration: WizardConfiguration; onConfigurationChange: (config: WizardConfiguration) => void; chantier: Chantier; } interface PhasePreview { id: string; nom: string; ordre: number; dateDebut: Date; dateFin: Date; duree: number; budget: number; categorie: string; sousPhases: SousPhasePreview[]; prerequis: string[]; competences: string[]; status: 'pending' | 'ready' | 'blocked'; } interface SousPhasePreview { id: string; nom: string; dateDebut: Date; dateFin: Date; duree: number; budget: number; } const PreviewGenerationStep: React.FC = ({ configuration, onConfigurationChange, chantier }) => { const [activeTabIndex, setActiveTabIndex] = useState(0); const [phasesPreview, setPhasesPreview] = useState([]); const [chartData, setChartData] = useState({}); const [validationBudget, setValidationBudget] = useState(null); const [chargementValidation, setChargementValidation] = useState(false); // Calculer la prévisualisation des phases avec dates useEffect(() => { if (configuration.phasesSelectionnees.length > 0) { calculatePhasesPreview(); } }, [configuration.phasesSelectionnees, configuration.dateDebutSouhaitee]); // Validation budgétaire séparée quand la prévisualisation change useEffect(() => { if (phasesPreview.length > 0) { validerBudgetPhases(); } }, [phasesPreview]); const calculatePhasesPreview = () => { const dateDebut = configuration.dateDebutSouhaitee || new Date(); let currentDate = new Date(dateDebut); const previews: PhasePreview[] = configuration.phasesSelectionnees .sort((a, b) => a.ordre - b.ordre) .map((phase) => { const dateDebutPhase = new Date(currentDate); const dateFinPhase = new Date(currentDate); const dureePhaseDays = phase.dureeEstimee || 1; // Défaut 1 jour si non défini dateFinPhase.setDate(dateFinPhase.getDate() + dureePhaseDays); // Calculer les sous-phases let currentSousPhaseDate = new Date(dateDebutPhase); const sousPhases: SousPhasePreview[] = phase.sousPhases.map((sp) => { const debutSp = new Date(currentSousPhaseDate); const finSp = new Date(currentSousPhaseDate); const dureeSousPhasedays = sp.dureeEstimee || 1; // Défaut 1 jour si non défini finSp.setDate(finSp.getDate() + dureeSousPhasedays); currentSousPhaseDate = finSp; return { id: sp.id, nom: sp.nom, dateDebut: debutSp, dateFin: finSp, duree: sp.dureeEstimee || 0, budget: sp.budgetEstime || 0 }; }); // Déterminer le statut let status: 'pending' | 'ready' | 'blocked' = 'ready'; if (phase.prerequis.length > 0) { // Vérifier si tous les prérequis sont satisfaits const prerequisSatisfaits = phase.prerequis.every(prereq => configuration.phasesSelectionnees.some(p => p.nom.includes(prereq) && p.ordre < phase.ordre ) ); status = prerequisSatisfaits ? 'ready' : 'blocked'; } const preview: PhasePreview = { id: phase.id, nom: phase.nom, ordre: phase.ordre, dateDebut: dateDebutPhase, dateFin: dateFinPhase, duree: phase.dureeEstimee || 0, budget: phase.budgetEstime || 0, categorie: phase.categorieMetier, sousPhases, prerequis: phase.prerequis, competences: phase.competencesRequises, status }; // Passer à la phase suivante currentDate = new Date(dateFinPhase); currentDate.setDate(currentDate.getDate() + 1); // 1 jour de battement return preview; }); setPhasesPreview(previews); prepareChartData(previews); }; const prepareChartData = (previews: PhasePreview[]) => { // Données pour le graphique de répartition budgétaire const budgetData = { labels: previews.map(p => p.nom.substring(0, 15) + '...'), datasets: [{ data: previews.map(p => p.budget), backgroundColor: [ '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#FF6384', '#C9CBCF' ], borderWidth: 0 }] }; // Données pour le planning (Gantt simplifié) const planningData = { labels: previews.map(p => p.nom.substring(0, 10)), datasets: [{ label: 'Durée (jours)', data: previews.map(p => p.duree), backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }; setChartData({ budget: budgetData, planning: planningData }); }; // Validation budgétaire const validerBudgetPhases = async () => { if (phasesPreview.length === 0 || chargementValidation) return; setChargementValidation(true); try { const budgetTotal = phasesPreview.reduce((sum, p) => sum + (p.budget || 0), 0); if (budgetTotal > 0) { const validation = await budgetCoherenceService.validerBudgetPhases( chantier.id.toString(), budgetTotal ); setValidationBudget(validation); } } catch (error) { console.warn('Erreur lors de la validation budgétaire:', error); setValidationBudget(null); } finally { setChargementValidation(false); } }; // Templates pour les tableaux const statusTemplate = (rowData: PhasePreview) => { const severityMap = { 'ready': 'success', 'pending': 'warning', 'blocked': 'danger' } as const; const labelMap = { 'ready': 'Prête', 'pending': 'En attente', 'blocked': 'Bloquée' }; return ( ); }; const dateTemplate = (rowData: PhasePreview, field: 'dateDebut' | 'dateFin') => ( {rowData[field].toLocaleDateString('fr-FR')} ); const budgetTemplate = (rowData: PhasePreview) => ( {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0 }).format(rowData.budget)} ); const sousPhaseTemplate = (rowData: PhasePreview) => ( ); const prerequisTemplate = (rowData: PhasePreview) => (
{rowData.prerequis.slice(0, 2).map((prereq, index) => ( {prereq} ))} {rowData.prerequis.length > 2 && ( +{rowData.prerequis.length - 2} )}
); // Timeline des phases const timelineEvents = phasesPreview.map((phase, index) => ({ status: phase.status === 'ready' ? 'success' : phase.status === 'blocked' ? 'danger' : 'warning', date: phase.dateDebut.toLocaleDateString('fr-FR'), icon: phase.status === 'ready' ? 'pi-check' : phase.status === 'blocked' ? 'pi-times' : 'pi-clock', color: phase.status === 'ready' ? '#22c55e' : phase.status === 'blocked' ? '#ef4444' : '#f59e0b', title: phase.nom, subtitle: `${phase.duree} jours • ${new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0 }).format(phase.budget)}` })); // Statistiques récapitulatives const stats = { totalPhases: phasesPreview.length, totalSousPhases: phasesPreview.reduce((sum, p) => sum + p.sousPhases.length, 0), dureeTotal: phasesPreview.reduce((sum, p) => sum + p.duree, 0), budgetTotal: phasesPreview.reduce((sum, p) => sum + p.budget, 0), phasesReady: phasesPreview.filter(p => p.status === 'ready').length, phasesBlocked: phasesPreview.filter(p => p.status === 'blocked').length, dateDebut: phasesPreview.length > 0 ? phasesPreview[0].dateDebut : null, dateFin: phasesPreview.length > 0 ? phasesPreview[phasesPreview.length - 1].dateFin : null }; const budgetAvecMarges = configuration.optionsAvancees.appliquerMarges ? stats.budgetTotal * (1 + configuration.optionsAvancees.taux.margeCommerciale / 100) * (1 + configuration.optionsAvancees.taux.alea / 100) * (1 + configuration.optionsAvancees.taux.tva / 100) : stats.budgetTotal; // Panel de récapitulatif exécutif const recapitulatifExecutif = () => (
Chantier
{chantier.nom}
{configuration.typeChantier?.nom || chantier.typeChantier} {chantier.montantPrevu && (
Budget: {chantier.montantPrevu.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR' })}
)}
Phases
{stats.totalPhases}
+ {stats.totalSousPhases} sous-phases
Planning
{stats.dureeTotal}
jours ouvrés {stats.dateDebut && stats.dateFin && (
{stats.dateDebut.toLocaleDateString('fr-FR')} → {stats.dateFin.toLocaleDateString('fr-FR')}
)}
Budget
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 0 }).format(budgetAvecMarges)}
{configuration.optionsAvancees.appliquerMarges ? 'avec marges' : 'hors marges'}
{/* Alertes et validations */}
{stats.phasesBlocked > 0 ? ( ) : ( )}
{chargementValidation ? ( ) : validationBudget ? ( ) : ( )}
{configuration.optionsAvancees.calculerBudgetAuto ? ( ) : ( )}
{/* Recommandation budgétaire si nécessaire */} {validationBudget && !validationBudget.valide && validationBudget.recommandation && (
)}
); return (

Prévisualisation & Génération

Vérifiez la configuration finale avant génération

{recapitulatifExecutif()} setActiveTabIndex(e.index)}>
} /> dateTemplate(rowData, 'dateDebut')} style={{ width: '100px' }} /> dateTemplate(rowData, 'dateFin')} style={{ width: '100px' }} /> `${rowData.duree}j`} style={{ width: '80px' }} />
(
{item.title}
{item.subtitle}
)} className="w-full" />
{chartData.budget && ( )}
{chartData.planning && ( )}
{configuration.optionsAvancees.appliquerMarges && (
Budget base (phases): {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(stats.budgetTotal)}
+ Marge commerciale ({configuration.optionsAvancees.taux.margeCommerciale}%): {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format( stats.budgetTotal * configuration.optionsAvancees.taux.margeCommerciale / 100 )}
+ Aléa/Imprévus ({configuration.optionsAvancees.taux.alea}%): {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format( stats.budgetTotal * (1 + configuration.optionsAvancees.taux.margeCommerciale / 100) * configuration.optionsAvancees.taux.alea / 100 )}
+ TVA ({configuration.optionsAvancees.taux.tva}%): {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format( stats.budgetTotal * (1 + configuration.optionsAvancees.taux.margeCommerciale / 100) * (1 + configuration.optionsAvancees.taux.alea / 100) * configuration.optionsAvancees.taux.tva / 100 )}
Total TTC: {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(budgetAvecMarges)}
Marge totale appliquée: {(((budgetAvecMarges - stats.budgetTotal) / stats.budgetTotal) * 100).toFixed(1)}%
)}
{chargementValidation ? (

Vérification de la cohérence budgétaire...

) : validationBudget ? (
Résumé de la validation
{validationBudget.valide ? 'Budget cohérent' : 'Attention budgétaire'}

{validationBudget.message}

{validationBudget.recommandation && (
Recommandation :
{validationBudget.recommandation === 'METTRE_A_JOUR_CHANTIER' && `Mettre à jour le budget du chantier à ${validationBudget.nouveauBudgetSuggere ? new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(validationBudget.nouveauBudgetSuggere) : 'calculer'}` } {validationBudget.recommandation === 'AJUSTER_PHASES' && 'Ajuster les budgets des phases pour correspondre au budget du chantier' }
)}
Répartition budgétaire
Budget total des phases : {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(stats.budgetTotal)}
Nombre de phases : {stats.totalPhases}
Budget moyen par phase : {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(stats.totalPhases > 0 ? stats.budgetTotal / stats.totalPhases : 0)}
) : ( )}
{phasesPreview.map((phase, index) => ( {phase.nom} {statusTemplate(phase)} {phase.duree}j • {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(phase.budget)}
} >
Informations générales
Catégorie:
Période: {phase.dateDebut.toLocaleDateString('fr-FR')} → {phase.dateFin.toLocaleDateString('fr-FR')}
{phase.prerequis.length > 0 && (
Prérequis:
{prerequisTemplate(phase)}
)}
Compétences requises
{phase.competences.map((comp, idx) => ( ))}
{phase.sousPhases.length > 0 && ( <>
Sous-phases ({phase.sousPhases.length})
{phase.sousPhases.map((sp, idx) => (
{sp.nom} {sp.duree}j • {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(sp.budget)}
{sp.dateDebut.toLocaleDateString('fr-FR')} → {sp.dateFin.toLocaleDateString('fr-FR')}
))}
)}
))} ); }; export default PreviewGenerationStep;