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