Files
btpxpress-frontend/components/phases/PhaseGenerationWizard.tsx

570 lines
21 KiB
TypeScript
Executable File

'use client';
/**
* Assistant de génération automatique de phases pour chantiers BTP
* Wizard en 3 étapes: Sélection template -> Personnalisation -> Prévisualisation & Génération
*/
import React, { useState, useRef, useEffect } from 'react';
import { Dialog } from 'primereact/dialog';
import { Steps } from 'primereact/steps';
import { Button } from 'primereact/button';
import { Card } from 'primereact/card';
import { Toast } from 'primereact/toast';
import { ProgressBar } from 'primereact/progressbar';
import { Divider } from 'primereact/divider';
import { Badge } from 'primereact/badge';
import { Tag } from 'primereact/tag';
import TemplateSelectionStep from './wizard/TemplateSelectionStep';
import CustomizationStep from './wizard/CustomizationStep';
import PreviewGenerationStep from './wizard/PreviewGenerationStep';
import typeChantierService from '../../services/typeChantierService';
import phaseService from '../../services/phaseService';
export interface PhaseTemplate {
id: string;
nom: string;
description: string;
ordre: number;
dureeEstimee: number;
budgetEstime: number;
competencesRequises: string[];
prerequis: string[];
sousPhases: SousPhaseTemplate[];
categorieMetier: 'GROS_OEUVRE' | 'SECOND_OEUVRE' | 'FINITIONS' | 'EQUIPEMENTS' | 'AMENAGEMENTS';
obligatoire: boolean;
personnalisable: boolean;
}
export interface SousPhaseTemplate {
id: string;
nom: string;
description: string;
ordre: number;
dureeEstimee: number;
budgetEstime: number;
competencesRequises: string[];
obligatoire: boolean;
}
export interface TypeChantierTemplate {
id: string;
nom: string;
description: string;
categorie: string;
phases: PhaseTemplate[];
dureeGlobaleEstimee: number;
budgetGlobalEstime: number;
nombreTotalPhases: number;
complexiteMetier: 'SIMPLE' | 'MOYENNE' | 'COMPLEXE' | 'EXPERT';
tags: string[];
}
export interface WizardConfiguration {
typeChantier: TypeChantierTemplate | null;
phasesSelectionnees: PhaseTemplate[];
configurationsPersonnalisees: Record<string, any>;
budgetGlobal: number;
dureeGlobale: number;
dateDebutSouhaitee: Date | null;
optionsAvancees: {
integrerPlanning: boolean;
calculerBudgetAuto: boolean;
appliquerMarges: boolean;
taux: {
margeCommerciale: number;
alea: number;
tva: number;
};
};
}
interface PhaseGenerationWizardProps {
visible: boolean;
onHide: () => void;
chantier: Chantier;
onGenerated: (phases: any[]) => void;
}
const PhaseGenerationWizard: React.FC<PhaseGenerationWizardProps> = ({
visible,
onHide,
chantier,
onGenerated
}) => {
const toast = useRef<Toast>(null);
// États du wizard
const [activeIndex, setActiveIndex] = useState(0);
const [isGenerating, setIsGenerating] = useState(false);
const [generationProgress, setGenerationProgress] = useState(0);
// Configuration du wizard initialisée avec les données du chantier
const [configuration, setConfiguration] = useState<WizardConfiguration>(() => ({
typeChantier: null,
phasesSelectionnees: [],
configurationsPersonnalisees: {},
budgetGlobal: chantier?.montantPrevu || 0,
dureeGlobale: chantier?.dateDebut && chantier?.dateFinPrevue
? Math.ceil((new Date(chantier.dateFinPrevue).getTime() - new Date(chantier.dateDebut).getTime()) / (1000 * 60 * 60 * 24))
: 0,
dateDebutSouhaitee: chantier?.dateDebut ? new Date(chantier.dateDebut) : null,
optionsAvancees: {
integrerPlanning: true,
calculerBudgetAuto: true,
appliquerMarges: true,
taux: {
margeCommerciale: 15,
alea: 10,
tva: 20
}
}
}));
// Données chargées
const [templatesTypes, setTemplatesTypes] = useState<TypeChantierTemplate[]>([]);
const [loading, setLoading] = useState(false);
// Étapes du wizard
const wizardSteps = [
{
label: 'Sélection Template',
icon: 'pi pi-th-large',
description: 'Choisir le type de chantier et template de phases'
},
{
label: 'Personnalisation',
icon: 'pi pi-cog',
description: 'Adapter les phases et paramètres à vos besoins'
},
{
label: 'Génération',
icon: 'pi pi-check-circle',
description: 'Prévisualiser et générer les phases'
}
];
// Charger les templates au montage
useEffect(() => {
if (visible) {
loadTemplatesTypes();
}
}, [visible]);
// Mettre à jour la configuration quand les données du chantier changent
useEffect(() => {
if (chantier && visible) {
setConfiguration(prev => ({
...prev,
budgetGlobal: chantier.montantPrevu || 0,
dureeGlobale: chantier.dateDebut && chantier.dateFinPrevue
? Math.ceil((new Date(chantier.dateFinPrevue).getTime() - new Date(chantier.dateDebut).getTime()) / (1000 * 60 * 60 * 24))
: 0,
dateDebutSouhaitee: chantier.dateDebut ? new Date(chantier.dateDebut) : null
}));
}
}, [chantier, visible]);
const loadTemplatesTypes = async () => {
try {
setLoading(true);
const templates = await typeChantierService.getAllTemplates();
setTemplatesTypes(templates);
} catch (error) {
console.error('Erreur lors du chargement des templates:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur',
detail: 'Impossible de charger les templates de chantiers',
life: 3000
});
} finally {
setLoading(false);
}
};
// Navigation du wizard
const nextStep = () => {
if (activeIndex < wizardSteps.length - 1) {
setActiveIndex(activeIndex + 1);
}
};
const prevStep = () => {
if (activeIndex > 0) {
setActiveIndex(activeIndex - 1);
}
};
const canProceedToNext = (): boolean => {
switch (activeIndex) {
case 0:
return configuration.typeChantier !== null;
case 1:
return configuration.phasesSelectionnees.length > 0;
case 2:
return true;
default:
return false;
}
};
// Génération des phases
const generatePhases = async () => {
if (!configuration.typeChantier) {
toast.current?.show({
severity: 'warn',
summary: 'Configuration incomplète',
detail: 'Veuillez sélectionner un type de chantier',
life: 3000
});
return;
}
try {
setIsGenerating(true);
setGenerationProgress(0);
// Simulation du processus de génération avec étapes
const etapes = [
{ label: 'Validation de la configuration', delay: 500 },
{ label: 'Génération des phases principales', delay: 800 },
{ label: 'Création des sous-phases', delay: 600 },
{ label: 'Calcul des budgets automatiques', delay: 700 },
{ label: 'Intégration au planning', delay: 400 },
{ label: 'Sauvegarde en base', delay: 500 }
];
for (let i = 0; i < etapes.length; i++) {
const etape = etapes[i];
toast.current?.show({
severity: 'info',
summary: `Étape ${i + 1}/${etapes.length}`,
detail: etape.label,
life: 2000
});
await new Promise(resolve => setTimeout(resolve, etape.delay));
setGenerationProgress(((i + 1) / etapes.length) * 100);
}
// Appel au service pour générer les phases avec données du chantier
try {
const phasesGenerees = await phaseService.generateFromTemplate(
parseInt(chantier.id.toString()),
configuration.typeChantier.id,
{
phasesSelectionnees: configuration.phasesSelectionnees,
configurationsPersonnalisees: configuration.configurationsPersonnalisees,
optionsAvancees: configuration.optionsAvancees,
dateDebutSouhaitee: configuration.dateDebutSouhaitee || chantier.dateDebut,
dureeGlobale: configuration.dureeGlobale,
// Données du chantier pour cohérence
chantierData: {
budgetTotal: chantier.montantPrevu,
typeChantier: chantier.typeChantier,
dateDebut: chantier.dateDebut,
dateFinPrevue: chantier.dateFinPrevue,
surface: chantier.surface,
adresse: chantier.adresse
}
}
);
toast.current?.show({
severity: 'success',
summary: 'Génération réussie',
detail: `${phasesGenerees.length} phases ont été générées avec succès`,
life: 5000
});
onGenerated(phasesGenerees);
onHide();
} catch (serviceError) {
console.warn('Service non disponible, génération simulée:', serviceError);
// Génération simulée de phases avec données du chantier
const baseDate = chantier.dateDebut ? new Date(chantier.dateDebut) : new Date();
let currentDate = new Date(baseDate);
const phasesSimulees = configuration.phasesSelectionnees.map((phase, index) => {
const dateDebut = new Date(currentDate);
const dateFin = new Date(dateDebut.getTime() + phase.dureeEstimee * 24 * 60 * 60 * 1000);
currentDate = new Date(dateFin.getTime() + 24 * 60 * 60 * 1000); // Jour suivant pour la phase suivante
// Calculer budget proportionnel si budget total défini
let budgetProportionnel = phase.budgetEstime;
if (chantier.montantPrevu && configuration.budgetGlobal) {
const ratioPhase = phase.budgetEstime / configuration.budgetGlobal;
budgetProportionnel = chantier.montantPrevu * ratioPhase;
}
return {
id: `sim_${Date.now()}_${index}`,
nom: phase.nom,
description: phase.description,
chantierId: chantier.id.toString(),
dateDebutPrevue: dateDebut.toISOString(),
dateFinPrevue: dateFin.toISOString(),
dureeEstimeeHeures: phase.dureeEstimee * 8,
budgetPrevu: budgetProportionnel,
coutReel: 0,
statut: 'PLANIFIEE',
priorite: phase.categorieMetier === 'GROS_OEUVRE' ? 'CRITIQUE' : 'MOYENNE',
critique: phase.obligatoire,
ordreExecution: phase.ordre,
phaseParent: null,
prerequisPhases: [],
competencesRequises: phase.competencesRequises,
materielsNecessaires: [],
fournisseursRecommandes: [],
dateCreation: new Date().toISOString(),
dateModification: new Date().toISOString(),
creePar: 'wizard',
modifiePar: 'wizard'
};
});
toast.current?.show({
severity: 'success',
summary: 'Génération simulée réussie',
detail: `${phasesSimulees.length} phases ont été générées (mode simulation)`,
life: 5000
});
onGenerated(phasesSimulees);
onHide();
}
} catch (error) {
console.error('Erreur lors de la génération:', error);
toast.current?.show({
severity: 'error',
summary: 'Erreur de génération',
detail: 'Impossible de générer les phases automatiquement',
life: 3000
});
} finally {
setIsGenerating(false);
setGenerationProgress(0);
}
};
// Reset du wizard avec données du chantier
const resetWizard = () => {
setActiveIndex(0);
setConfiguration({
typeChantier: null,
phasesSelectionnees: [],
configurationsPersonnalisees: {},
budgetGlobal: chantier?.montantPrevu || 0,
dureeGlobale: chantier?.dateDebut && chantier?.dateFinPrevue
? Math.ceil((new Date(chantier.dateFinPrevue).getTime() - new Date(chantier.dateDebut).getTime()) / (1000 * 60 * 60 * 24))
: 0,
dateDebutSouhaitee: chantier?.dateDebut ? new Date(chantier.dateDebut) : null,
optionsAvancees: {
integrerPlanning: true,
calculerBudgetAuto: true,
appliquerMarges: true,
taux: {
margeCommerciale: 15,
alea: 10,
tva: 20
}
}
});
setGenerationProgress(0);
setIsGenerating(false);
};
// Template du header
const headerTemplate = () => (
<div className="flex align-items-center gap-3">
<i className="pi pi-magic text-2xl text-primary"></i>
<div>
<h4 className="m-0">Assistant de Génération de Phases</h4>
<p className="m-0 text-color-secondary text-sm">
Chantier: <strong>{chantier?.nom}</strong>
{chantier?.typeChantier && (
<span className="ml-2 text-xs">({chantier.typeChantier})</span>
)}
</p>
{chantier && (
<p className="m-0 text-xs text-500">
Budget: {chantier.montantPrevu?.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR' }) || 'Non défini'} |
Période: {chantier.dateDebut ? new Date(chantier.dateDebut).toLocaleDateString('fr-FR') : 'Non défini'}
{chantier.dateFinPrevue ? new Date(chantier.dateFinPrevue).toLocaleDateString('fr-FR') : 'Non défini'}
</p>
)}
</div>
{configuration.typeChantier && (
<div className="ml-auto">
<Tag
value={configuration.typeChantier.nom}
severity="info"
icon="pi pi-building"
/>
</div>
)}
</div>
);
// Template du footer
const footerTemplate = () => (
<div className="flex justify-content-between align-items-center">
<div className="flex align-items-center gap-2">
<Button
label="Précédent"
icon="pi pi-arrow-left"
className="p-button-text p-button-rounded"
onClick={prevStep}
disabled={activeIndex === 0 || isGenerating}
/>
{activeIndex < wizardSteps.length - 1 ? (
<Button
label="Suivant"
icon="pi pi-arrow-right"
iconPos="right"
className="p-button-text p-button-rounded p-button-info"
onClick={nextStep}
disabled={!canProceedToNext() || isGenerating}
/>
) : (
<Button
label={isGenerating ? "Génération..." : "Générer les phases"}
icon={isGenerating ? "pi pi-spin pi-spinner" : "pi pi-check"}
className="p-button-text p-button-rounded p-button-success"
onClick={generatePhases}
disabled={!canProceedToNext() || isGenerating}
loading={isGenerating}
/>
)}
</div>
<div className="flex align-items-center gap-2">
{configuration.phasesSelectionnees.length > 0 && (
<Badge
value={`${configuration.phasesSelectionnees.length} phases`}
severity="info"
/>
)}
<Button
label="Fermer"
icon="pi pi-times"
className="p-button-text p-button-rounded"
onClick={() => {
resetWizard();
onHide();
}}
disabled={isGenerating}
/>
</div>
</div>
);
// Rendu conditionnel des étapes
const renderCurrentStep = () => {
switch (activeIndex) {
case 0:
return (
<TemplateSelectionStep
templates={templatesTypes}
loading={loading}
configuration={configuration}
onConfigurationChange={setConfiguration}
chantier={chantier}
/>
);
case 1:
return (
<CustomizationStep
configuration={configuration}
onConfigurationChange={setConfiguration}
/>
);
case 2:
return (
<PreviewGenerationStep
configuration={configuration}
onConfigurationChange={setConfiguration}
chantier={chantier}
/>
);
default:
return null;
}
};
return (
<>
<Toast ref={toast} />
<Dialog
visible={visible}
onHide={() => {
if (!isGenerating) {
resetWizard();
onHide();
}
}}
header={headerTemplate}
footer={footerTemplate}
style={{ width: '90vw', maxWidth: '1200px' }}
modal
closable={!isGenerating}
className="p-dialog-maximized-responsive"
>
<div className="grid">
{/* Barre de progression de génération */}
{isGenerating && (
<div className="col-12 mb-4">
<Card className="bg-blue-50 border-blue-200">
<div className="flex align-items-center gap-3">
<i className="pi pi-spin pi-cog text-blue-600 text-2xl"></i>
<div className="flex-1">
<h6 className="m-0 text-blue-800">Génération en cours...</h6>
<ProgressBar
value={generationProgress}
className="mt-2"
style={{ height: '8px' }}
/>
<small className="text-blue-600 mt-1 block">
{generationProgress.toFixed(0)}% terminé
</small>
</div>
</div>
</Card>
</div>
)}
{/* Steps navigation */}
<div className="col-12 mb-4">
<Steps
model={wizardSteps}
activeIndex={activeIndex}
onSelect={(e) => {
if (!isGenerating && e.index <= activeIndex) {
setActiveIndex(e.index);
}
}}
readOnly={isGenerating}
/>
</div>
<Divider />
{/* Contenu de l'étape courante */}
<div className="col-12">
<div style={{ minHeight: '500px' }}>
{renderCurrentStep()}
</div>
</div>
</div>
</Dialog>
</>
);
};
export default PhaseGenerationWizard;