Initial commit

This commit is contained in:
dahoud
2025-10-01 01:39:07 +00:00
commit b430bf3b96
826 changed files with 255287 additions and 0 deletions

View File

@@ -0,0 +1,582 @@
'use client';
/**
* Étape 2: Personnalisation avancée avec tableau interactif
* Interface de configuration détaillée des phases et budgets
*/
import React, { useState, useRef, useEffect } from 'react';
import { Card } from 'primereact/card';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { Button } from 'primereact/button';
import { InputNumber } from 'primereact/inputnumber';
import { InputText } from 'primereact/inputtext';
import { Checkbox } from 'primereact/checkbox';
import { Calendar } from 'primereact/calendar';
import { TabView, TabPanel } from 'primereact/tabview';
import { Accordion, AccordionTab } from 'primereact/accordion';
import { Tag } from 'primereact/tag';
import { Badge } from 'primereact/badge';
import { Toast } from 'primereact/toast';
import { Message } from 'primereact/message';
import { Divider } from 'primereact/divider';
import { Slider } from 'primereact/slider';
import { ToggleButton } from 'primereact/togglebutton';
import { PhaseTemplate, SousPhaseTemplate, WizardConfiguration } from '../PhaseGenerationWizard';
interface CustomizationStepProps {
configuration: WizardConfiguration;
onConfigurationChange: (config: WizardConfiguration) => void;
}
const CustomizationStep: React.FC<CustomizationStepProps> = ({
configuration,
onConfigurationChange
}) => {
const toast = useRef<Toast>(null);
const [activeTabIndex, setActiveTabIndex] = useState(0);
const [expandedPhases, setExpandedPhases] = useState<Record<string, boolean>>({});
const [editingCell, setEditingCell] = useState<string | null>(null);
// État local pour les modifications
const [localPhases, setLocalPhases] = useState<PhaseTemplate[]>(
configuration.phasesSelectionnees || []
);
// Synchroniser avec la configuration parent seulement si les localPhases sont vides
useEffect(() => {
if (configuration.phasesSelectionnees && localPhases.length === 0) {
console.log('🔄 Synchronisation initiale localPhases avec configuration:', configuration.phasesSelectionnees.length, 'phases');
setLocalPhases([...configuration.phasesSelectionnees]);
}
}, [configuration.phasesSelectionnees, localPhases.length]);
// Log les changements de localPhases pour debug
useEffect(() => {
console.log('📋 LocalPhases mis à jour:', localPhases.length, 'phases, budget total:',
localPhases.reduce((sum, p) => sum + (p.budgetEstime || 0), 0));
}, [localPhases]);
// Mettre à jour la configuration
const updateConfiguration = (updates: Partial<WizardConfiguration>) => {
onConfigurationChange({
...configuration,
...updates
});
};
// Mettre à jour une phase
const updatePhase = (phaseId: string, updates: Partial<PhaseTemplate>) => {
setLocalPhases(prevPhases => {
const updatedPhases = prevPhases.map(phase =>
phase.id === phaseId
? { ...phase, ...updates }
: phase
);
// Recalculer le budget et la durée globale
const budgetTotal = updatedPhases.reduce((sum, p) => sum + (p.budgetEstime || 0), 0);
const dureeTotal = updatedPhases.reduce((sum, p) => sum + (p.dureeEstimee || 0), 0);
console.log('💰 Budget mis à jour:', { phaseId, updates, budgetTotal, dureeTotal });
// Mettre à jour la configuration avec les nouvelles données
setTimeout(() => {
updateConfiguration({
phasesSelectionnees: updatedPhases,
budgetGlobal: budgetTotal,
dureeGlobale: dureeTotal
});
}, 0);
return updatedPhases;
});
};
// Mettre à jour une sous-phase
const updateSousPhase = (phaseId: string, sousPhaseId: string, updates: Partial<SousPhaseTemplate>) => {
setLocalPhases(prevPhases => {
const updatedPhases = prevPhases.map(phase => {
if (phase.id === phaseId) {
const updatedSousPhases = phase.sousPhases.map(sp =>
sp.id === sousPhaseId ? { ...sp, ...updates } : sp
);
return { ...phase, sousPhases: updatedSousPhases };
}
return phase;
});
setTimeout(() => {
updateConfiguration({ phasesSelectionnees: updatedPhases });
}, 0);
return updatedPhases;
});
};
// Templates pour le tableau des phases
const checkboxTemplate = (rowData: PhaseTemplate) => (
<Checkbox
checked={localPhases.some(p => p.id === rowData.id)}
onChange={(e) => {
setLocalPhases(prevPhases => {
const newPhases = e.checked
? [...prevPhases, rowData]
: prevPhases.filter(p => p.id !== rowData.id);
setTimeout(() => {
updateConfiguration({ phasesSelectionnees: newPhases });
}, 0);
return newPhases;
});
}}
/>
);
const nomTemplate = (rowData: PhaseTemplate) => (
<div className="flex align-items-center gap-2">
<Badge value={rowData.ordre} severity="info" />
<div>
<div className="font-semibold">{rowData.nom}</div>
<small className="text-color-secondary">{rowData.description}</small>
</div>
</div>
);
const categorieTemplate = (rowData: PhaseTemplate) => (
<Tag
value={rowData.categorieMetier}
severity={
rowData.categorieMetier === 'GROS_OEUVRE' ? 'warning' :
rowData.categorieMetier === 'SECOND_OEUVRE' ? 'info' :
rowData.categorieMetier === 'FINITIONS' ? 'success' :
rowData.categorieMetier === 'EQUIPEMENTS' ? 'danger' : 'secondary'
}
/>
);
const dureeTemplate = (rowData: PhaseTemplate) => {
const isEditing = editingCell === `duree_${rowData.id}`;
return isEditing ? (
<InputNumber
value={rowData.dureeEstimee}
onValueChange={(e) => updatePhase(rowData.id, { dureeEstimee: e.value || 0 })}
onBlur={() => setEditingCell(null)}
suffix=" j"
min={1}
max={365}
autoFocus
className="w-full"
/>
) : (
<div
className="cursor-pointer hover:surface-hover p-2 border-round"
onClick={() => setEditingCell(`duree_${rowData.id}`)}
>
<span className="font-semibold">{rowData.dureeEstimee}</span>
<small className="text-color-secondary ml-1">jours</small>
</div>
);
};
const budgetTemplate = (rowData: PhaseTemplate) => {
const isEditing = editingCell === `budget_${rowData.id}`;
return isEditing ? (
<InputNumber
value={rowData.budgetEstime}
onValueChange={(e) => {
const newValue = e.value || 0;
console.log('💰 Modification budget phase:', rowData.id, 'ancien:', rowData.budgetEstime, 'nouveau:', newValue);
updatePhase(rowData.id, { budgetEstime: newValue });
}}
onBlur={() => {
console.log('💰 Fin édition budget phase:', rowData.id);
setEditingCell(null);
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === 'Tab') {
setEditingCell(null);
}
}}
mode="decimal"
minFractionDigits={0}
maxFractionDigits={2}
min={0}
placeholder="0"
autoFocus
className="w-full"
style={{ textAlign: 'right' }}
/>
) : (
<div
className="cursor-pointer hover:surface-hover p-2 border-round flex align-items-center gap-2"
onClick={() => {
console.log('💰 Début édition budget phase:', rowData.id);
setEditingCell(`budget_${rowData.id}`);
}}
title="Cliquer pour modifier le budget"
>
<span>
{new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
}).format(rowData.budgetEstime)}
</span>
<i className="pi pi-pencil text-xs text-color-secondary"></i>
</div>
);
};
const competencesTemplate = (rowData: PhaseTemplate) => (
<div className="flex flex-wrap gap-1 max-w-15rem">
{rowData.competencesRequises.slice(0, 3).map((comp, index) => (
<Badge key={index} value={comp} severity="secondary" className="text-xs" />
))}
{rowData.competencesRequises.length > 3 && (
<Badge value={`+${rowData.competencesRequises.length - 3}`} severity="info" className="text-xs" />
)}
</div>
);
const sousPhaseTemplate = (rowData: PhaseTemplate) => (
<Button
icon="pi pi-list"
label={`${rowData.sousPhases.length} sous-phases`}
className="p-button-text p-button-rounded p-button-sm"
onClick={() => {
setExpandedPhases({
...expandedPhases,
[rowData.id]: !expandedPhases[rowData.id]
});
}}
/>
);
// Panel d'options avancées
const optionsAvanceesPanel = () => (
<Card title="Options Avancées" className="mt-4">
<div className="grid">
<div className="col-12 md:col-6">
<div className="field-checkbox mb-4">
<Checkbox
id="integrerPlanning"
checked={configuration.optionsAvancees.integrerPlanning}
onChange={(e) => updateConfiguration({
optionsAvancees: {
...configuration.optionsAvancees,
integrerPlanning: e.checked || false
}
})}
/>
<label htmlFor="integrerPlanning" className="ml-2">
<strong>Intégrer au module Planning & Organisation</strong>
<div className="text-color-secondary text-sm">
Créer automatiquement les événements de planning
</div>
</label>
</div>
<div className="field-checkbox mb-4">
<Checkbox
id="calculerBudgetAuto"
checked={configuration.optionsAvancees.calculerBudgetAuto}
onChange={(e) => updateConfiguration({
optionsAvancees: {
...configuration.optionsAvancees,
calculerBudgetAuto: e.checked || false
}
})}
/>
<label htmlFor="calculerBudgetAuto" className="ml-2">
<strong>Calcul automatique des budgets</strong>
<div className="text-color-secondary text-sm">
Appliquer les coefficients et marges automatiquement
</div>
</label>
</div>
<div className="field-checkbox mb-4">
<Checkbox
id="appliquerMarges"
checked={configuration.optionsAvancees.appliquerMarges}
onChange={(e) => updateConfiguration({
optionsAvancees: {
...configuration.optionsAvancees,
appliquerMarges: e.checked || false
}
})}
/>
<label htmlFor="appliquerMarges" className="ml-2">
<strong>Appliquer les marges commerciales</strong>
<div className="text-color-secondary text-sm">
Inclure les taux de marge et aléas dans les calculs
</div>
</label>
</div>
</div>
{configuration.optionsAvancees.appliquerMarges && (
<div className="col-12 md:col-6">
<h6>Configuration des Taux</h6>
<div className="field mb-3">
<label htmlFor="margeCommerciale">Marge commerciale: {configuration.optionsAvancees.taux.margeCommerciale}%</label>
<Slider
id="margeCommerciale"
value={configuration.optionsAvancees.taux.margeCommerciale}
onChange={(e) => updateConfiguration({
optionsAvancees: {
...configuration.optionsAvancees,
taux: {
...configuration.optionsAvancees.taux,
margeCommerciale: e.value as number
}
}
})}
min={0}
max={50}
className="w-full"
/>
</div>
<div className="field mb-3">
<label htmlFor="alea">Aléa/Imprévus: {configuration.optionsAvancees.taux.alea}%</label>
<Slider
id="alea"
value={configuration.optionsAvancees.taux.alea}
onChange={(e) => updateConfiguration({
optionsAvancees: {
...configuration.optionsAvancees,
taux: {
...configuration.optionsAvancees.taux,
alea: e.value as number
}
}
})}
min={0}
max={30}
className="w-full"
/>
</div>
<div className="field mb-3">
<label htmlFor="tva">TVA: {configuration.optionsAvancees.taux.tva}%</label>
<Slider
id="tva"
value={configuration.optionsAvancees.taux.tva}
onChange={(e) => updateConfiguration({
optionsAvancees: {
...configuration.optionsAvancees,
taux: {
...configuration.optionsAvancees.taux,
tva: e.value as number
}
}
})}
min={0}
max={25}
step={0.5}
className="w-full"
/>
</div>
</div>
)}
</div>
</Card>
);
// Panel de planning
const planningPanel = () => (
<Card title="Configuration du Planning" className="mt-4">
<div className="grid">
<div className="col-12 md:col-6">
<div className="field">
<label htmlFor="dateDebut">Date de début souhaitée</label>
<Calendar
id="dateDebut"
value={configuration.dateDebutSouhaitee}
onChange={(e) => updateConfiguration({ dateDebutSouhaitee: e.value as Date })}
dateFormat="dd/mm/yy"
showIcon
className="w-full"
placeholder="Sélectionner une date"
/>
</div>
</div>
<div className="col-12 md:col-6">
<div className="field">
<label htmlFor="dureeGlobale">Durée globale estimée</label>
<InputNumber
id="dureeGlobale"
value={configuration.dureeGlobale}
onValueChange={(e) => updateConfiguration({ dureeGlobale: e.value || 0 })}
suffix=" jours"
min={1}
className="w-full"
/>
<small className="text-color-secondary">
Calculé automatiquement: {localPhases.reduce((sum, p) => sum + (p.dureeEstimee || 0), 0)} jours
</small>
</div>
</div>
</div>
</Card>
);
// Panel récapitulatif
const recapitulatifPanel = () => {
const budgetTotal = localPhases.reduce((sum, p) => sum + (p.budgetEstime || 0), 0);
const dureeTotal = localPhases.reduce((sum, p) => sum + (p.dureeEstimee || 0), 0);
const nombreSousPhases = localPhases.reduce((sum, p) => sum + p.sousPhases.length, 0);
console.log('📊 Récapitulatif recalculé:', {
budgetTotal,
dureeTotal,
nombrePhases: localPhases.length,
phases: localPhases.map(p => ({ id: p.id, nom: p.nom, budget: p.budgetEstime }))
});
return (
<Card title="Récapitulatif" className="mt-4">
<div className="grid">
<div className="col-12 md:col-3">
<div className="text-center p-3 surface-100 border-round">
<i className="pi pi-sitemap text-primary text-2xl mb-2"></i>
<div className="text-color font-bold text-xl">{localPhases.length}</div>
<small className="text-color-secondary">phases sélectionnées</small>
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center p-3 surface-100 border-round">
<i className="pi pi-list text-primary text-2xl mb-2"></i>
<div className="text-color font-bold text-xl">{nombreSousPhases}</div>
<small className="text-color-secondary">sous-phases incluses</small>
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center p-3 surface-100 border-round">
<i className="pi pi-clock text-primary text-2xl mb-2"></i>
<div className="text-color font-bold text-xl">{dureeTotal}</div>
<small className="text-color-secondary">jours estimés</small>
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center p-3 surface-100 border-round">
<i className="pi pi-euro text-primary text-2xl mb-2"></i>
<div className="text-color font-bold text-lg">
{new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
}).format(budgetTotal)}
</div>
<small className="text-color-secondary">budget estimé</small>
</div>
</div>
</div>
</Card>
);
};
if (!configuration.typeChantier) {
return (
<Message
severity="warn"
text="Veuillez d'abord sélectionner un template de chantier dans l'étape précédente."
/>
);
}
return (
<div>
<div className="flex align-items-center gap-3 mb-4">
<i className="pi pi-cog text-primary text-2xl"></i>
<div>
<h4 className="m-0">Personnalisation Avancée</h4>
<p className="m-0 text-color-secondary">
Configurez les phases, budgets et options pour votre chantier
</p>
</div>
</div>
<TabView activeIndex={activeTabIndex} onTabChange={(e) => setActiveTabIndex(e.index)}>
<TabPanel header="Phases & Budgets" leftIcon="pi pi-table">
<Message
severity="info"
text="Cliquez sur les cellules Durée et Budget pour les modifier. Cochez/décochez les phases à inclure."
className="mb-4"
/>
<DataTable
value={configuration.typeChantier.phases}
responsiveLayout="scroll"
paginator={false}
emptyMessage="Aucune phase disponible"
className="p-datatable-sm"
>
<Column
header="Inclure"
body={checkboxTemplate}
style={{ width: '80px', textAlign: 'center' }}
/>
<Column
field="nom"
header="Phase"
body={nomTemplate}
style={{ minWidth: '250px' }}
/>
<Column
field="categorieMetier"
header="Catégorie"
body={categorieTemplate}
style={{ width: '150px' }}
/>
<Column
field="dureeEstimee"
header="Durée"
body={dureeTemplate}
style={{ width: '120px' }}
/>
<Column
field="budgetEstime"
header="Budget"
body={budgetTemplate}
style={{ width: '150px' }}
/>
<Column
field="competencesRequises"
header="Compétences"
body={competencesTemplate}
style={{ width: '200px' }}
/>
<Column
field="sousPhases"
header="Sous-phases"
body={sousPhaseTemplate}
style={{ width: '150px' }}
/>
</DataTable>
{recapitulatifPanel()}
</TabPanel>
<TabPanel header="Options Avancées" leftIcon="pi pi-cog">
{optionsAvanceesPanel()}
</TabPanel>
<TabPanel header="Planning" leftIcon="pi pi-calendar">
{planningPanel()}
</TabPanel>
</TabView>
<Toast ref={toast} />
</div>
);
};
export default CustomizationStep;

View File

@@ -0,0 +1,744 @@
'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<PreviewGenerationStepProps> = ({
configuration,
onConfigurationChange,
chantier
}) => {
const [activeTabIndex, setActiveTabIndex] = useState(0);
const [phasesPreview, setPhasesPreview] = useState<PhasePreview[]>([]);
const [chartData, setChartData] = useState<any>({});
const [validationBudget, setValidationBudget] = useState<any>(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 (
<Tag
value={labelMap[rowData.status]}
severity={severityMap[rowData.status]}
icon={`pi ${rowData.status === 'ready' ? 'pi-check' : rowData.status === 'blocked' ? 'pi-times' : 'pi-clock'}`}
/>
);
};
const dateTemplate = (rowData: PhasePreview, field: 'dateDebut' | 'dateFin') => (
<span>{rowData[field].toLocaleDateString('fr-FR')}</span>
);
const budgetTemplate = (rowData: PhasePreview) => (
<span className="font-semibold">
{new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
}).format(rowData.budget)}
</span>
);
const sousPhaseTemplate = (rowData: PhasePreview) => (
<Badge
value={rowData.sousPhases.length}
severity="info"
className="mr-2"
/>
);
const prerequisTemplate = (rowData: PhasePreview) => (
<div className="flex flex-wrap gap-1">
{rowData.prerequis.slice(0, 2).map((prereq, index) => (
<span
key={index}
className="inline-block bg-orange-50 text-orange-700 px-2 py-1 border-round text-xs"
>
{prereq}
</span>
))}
{rowData.prerequis.length > 2 && (
<span className="inline-block bg-gray-100 text-gray-600 px-2 py-1 border-round text-xs">
+{rowData.prerequis.length - 2}
</span>
)}
</div>
);
// 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 = () => (
<Card title="Récapitulatif Exécutif" className="mb-4">
<div className="grid">
<div className="col-12 md:col-3">
<div className="text-center p-3 surface-100 border-round">
<i className="pi pi-building text-primary text-3xl mb-3"></i>
<h6 className="text-color m-0 mb-2">Chantier</h6>
<div className="text-color font-bold">{chantier.nom}</div>
<small className="text-color-secondary">
{configuration.typeChantier?.nom || chantier.typeChantier}
</small>
{chantier.montantPrevu && (
<div className="text-xs text-color-secondary mt-1">
Budget: {chantier.montantPrevu.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR' })}
</div>
)}
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center p-3 surface-100 border-round">
<i className="pi pi-sitemap text-primary text-3xl mb-3"></i>
<h6 className="text-color m-0 mb-2">Phases</h6>
<div className="text-color font-bold text-xl">{stats.totalPhases}</div>
<small className="text-color-secondary">+ {stats.totalSousPhases} sous-phases</small>
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center p-3 surface-100 border-round">
<i className="pi pi-calendar text-primary text-3xl mb-3"></i>
<h6 className="text-color m-0 mb-2">Planning</h6>
<div className="text-color font-bold text-xl">{stats.dureeTotal}</div>
<small className="text-color-secondary">jours ouvrés</small>
{stats.dateDebut && stats.dateFin && (
<div className="mt-2 text-xs text-color-secondary">
{stats.dateDebut.toLocaleDateString('fr-FR')} {stats.dateFin.toLocaleDateString('fr-FR')}
</div>
)}
</div>
</div>
<div className="col-12 md:col-3">
<div className="text-center p-3 surface-100 border-round">
<i className="pi pi-euro text-primary text-3xl mb-3"></i>
<h6 className="text-color m-0 mb-2">Budget</h6>
<div className="text-color font-bold text-lg">
{new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
}).format(budgetAvecMarges)}
</div>
<small className="text-color-secondary">
{configuration.optionsAvancees.appliquerMarges ? 'avec marges' : 'hors marges'}
</small>
</div>
</div>
</div>
{/* Alertes et validations */}
<Divider />
<div className="grid">
<div className="col-12 md:col-4">
{stats.phasesBlocked > 0 ? (
<Message
severity="warn"
text={`${stats.phasesBlocked} phase(s) bloquée(s) par des prérequis manquants`}
/>
) : (
<Message
severity="success"
text="Toutes les phases sont prêtes à être générées"
/>
)}
</div>
<div className="col-12 md:col-4">
{chargementValidation ? (
<Message
severity="info"
text="Validation budgétaire en cours..."
/>
) : validationBudget ? (
<Message
severity={validationBudget.valide ? "success" : "warn"}
text={validationBudget.message}
/>
) : (
<Message
severity="info"
text="Validation budgétaire indisponible"
/>
)}
</div>
<div className="col-12 md:col-4">
{configuration.optionsAvancees.calculerBudgetAuto ? (
<Message
severity="info"
text="Budgets calculés automatiquement avec coefficients"
/>
) : (
<Message
severity="info"
text="Budgets basés sur la saisie utilisateur"
/>
)}
</div>
</div>
{/* Recommandation budgétaire si nécessaire */}
{validationBudget && !validationBudget.valide && validationBudget.recommandation && (
<div className="mt-3">
<Message
severity="warn"
text={
validationBudget.recommandation === 'METTRE_A_JOUR_CHANTIER'
? `💡 Recommandation : 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'
? '💡 Recommandation : Ajuster les budgets des phases pour correspondre au budget du chantier'
: 'Vérification budgétaire recommandée'
}
/>
</div>
)}
</Card>
);
return (
<div>
<div className="flex align-items-center gap-3 mb-4">
<i className="pi pi-eye text-primary text-2xl"></i>
<div>
<h4 className="m-0">Prévisualisation & Génération</h4>
<p className="m-0 text-color-secondary">
Vérifiez la configuration finale avant génération
</p>
</div>
</div>
{recapitulatifExecutif()}
<TabView activeIndex={activeTabIndex} onTabChange={(e) => setActiveTabIndex(e.index)}>
<TabPanel header="Planning Détaillé" leftIcon="pi pi-calendar">
<div className="grid">
<div className="col-12 lg:col-8">
<Card title="Tableau des Phases">
<DataTable
value={phasesPreview}
paginator={false}
emptyMessage="Aucune phase à afficher"
className="p-datatable-sm"
>
<Column
field="ordre"
header="#"
style={{ width: '60px' }}
body={(rowData) => <Badge value={rowData.ordre} />}
/>
<Column
field="nom"
header="Phase"
style={{ minWidth: '200px' }}
/>
<Column
field="dateDebut"
header="Début"
body={(rowData) => dateTemplate(rowData, 'dateDebut')}
style={{ width: '100px' }}
/>
<Column
field="dateFin"
header="Fin"
body={(rowData) => dateTemplate(rowData, 'dateFin')}
style={{ width: '100px' }}
/>
<Column
field="duree"
header="Durée"
body={(rowData) => `${rowData.duree}j`}
style={{ width: '80px' }}
/>
<Column
field="budget"
header="Budget"
body={budgetTemplate}
style={{ width: '120px' }}
/>
<Column
field="sousPhases"
header="S.-phases"
body={sousPhaseTemplate}
style={{ width: '90px' }}
/>
<Column
field="status"
header="Statut"
body={statusTemplate}
style={{ width: '100px' }}
/>
</DataTable>
</Card>
</div>
<div className="col-12 lg:col-4">
<Card title="Timeline">
<Timeline
value={timelineEvents}
content={(item) => (
<div>
<div className="font-semibold text-sm">{item.title}</div>
<small className="text-color-secondary">{item.subtitle}</small>
</div>
)}
className="w-full"
/>
</Card>
</div>
</div>
</TabPanel>
<TabPanel header="Analyse Budgétaire" leftIcon="pi pi-chart-pie">
<div className="grid">
<div className="col-12 lg:col-6">
<Card title="Répartition Budgétaire par Phase">
{chartData.budget && (
<Chart
type="doughnut"
data={chartData.budget}
style={{ height: '300px' }}
/>
)}
</Card>
</div>
<div className="col-12 lg:col-6">
<Card title="Durée par Phase">
{chartData.planning && (
<Chart
type="bar"
data={chartData.planning}
style={{ height: '300px' }}
/>
)}
</Card>
</div>
</div>
{configuration.optionsAvancees.appliquerMarges && (
<Card title="Détail des Marges Appliquées" className="mt-4">
<div className="grid">
<div className="col-12 md:col-6">
<div className="flex justify-content-between mb-2">
<span>Budget base (phases):</span>
<span className="font-semibold">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(stats.budgetTotal)}
</span>
</div>
<div className="flex justify-content-between mb-2">
<span>+ Marge commerciale ({configuration.optionsAvancees.taux.margeCommerciale}%):</span>
<span className="text-blue-600">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(
stats.budgetTotal * configuration.optionsAvancees.taux.margeCommerciale / 100
)}
</span>
</div>
<div className="flex justify-content-between mb-2">
<span>+ Aléa/Imprévus ({configuration.optionsAvancees.taux.alea}%):</span>
<span className="text-orange-600">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(
stats.budgetTotal * (1 + configuration.optionsAvancees.taux.margeCommerciale / 100) *
configuration.optionsAvancees.taux.alea / 100
)}
</span>
</div>
<div className="flex justify-content-between mb-2">
<span>+ TVA ({configuration.optionsAvancees.taux.tva}%):</span>
<span className="text-purple-600">
{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
)}
</span>
</div>
<Divider />
<div className="flex justify-content-between">
<span className="font-bold">Total TTC:</span>
<span className="font-bold text-green-600 text-lg">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(budgetAvecMarges)}
</span>
</div>
</div>
<div className="col-12 md:col-6">
<ProgressBar
value={85}
showValue={false}
className="mb-2"
style={{ height: '20px' }}
/>
<small className="text-color-secondary">
Marge totale appliquée: {(((budgetAvecMarges - stats.budgetTotal) / stats.budgetTotal) * 100).toFixed(1)}%
</small>
</div>
</div>
</Card>
)}
</TabPanel>
<TabPanel header="Cohérence Budgétaire" leftIcon="pi pi-calculator">
<Card title="Analyse Budgétaire Détaillée">
{chargementValidation ? (
<div className="text-center p-4">
<ProgressBar mode="indeterminate" style={{ height: '6px' }} />
<p className="mt-3">Vérification de la cohérence budgétaire...</p>
</div>
) : validationBudget ? (
<div>
<div className="grid">
<div className="col-12 md:col-6">
<h6>Résumé de la validation</h6>
<div className="p-3 border-round" style={{
backgroundColor: validationBudget.valide ? '#f0f9ff' : '#fefce8',
border: `1px solid ${validationBudget.valide ? '#0ea5e9' : '#eab308'}`
}}>
<div className="flex align-items-center gap-2 mb-2">
<i className={`pi ${validationBudget.valide ? 'pi-check-circle text-green-600' : 'pi-exclamation-triangle text-yellow-600'} text-lg`}></i>
<span className="font-semibold">
{validationBudget.valide ? 'Budget cohérent' : 'Attention budgétaire'}
</span>
</div>
<p className="m-0 text-sm">{validationBudget.message}</p>
{validationBudget.recommandation && (
<div className="mt-3 p-2 bg-white border-round">
<strong>Recommandation :</strong>
<br />
{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'
}
</div>
)}
</div>
</div>
<div className="col-12 md:col-6">
<h6>Répartition budgétaire</h6>
<div className="text-sm">
<div className="flex justify-content-between mb-2">
<span>Budget total des phases :</span>
<span className="font-semibold">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(stats.budgetTotal)}
</span>
</div>
<div className="flex justify-content-between mb-2">
<span>Nombre de phases :</span>
<span className="font-semibold">{stats.totalPhases}</span>
</div>
<div className="flex justify-content-between mb-2">
<span>Budget moyen par phase :</span>
<span className="font-semibold">
{new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(stats.totalPhases > 0 ? stats.budgetTotal / stats.totalPhases : 0)}
</span>
</div>
</div>
</div>
</div>
</div>
) : (
<Message severity="warn" text="Validation budgétaire non disponible" />
)}
</Card>
</TabPanel>
<TabPanel header="Détails Techniques" leftIcon="pi pi-cog">
<Accordion>
{phasesPreview.map((phase, index) => (
<AccordionTab
key={phase.id}
header={
<div className="flex align-items-center gap-3 w-full">
<Badge value={phase.ordre} />
<span className="font-semibold">{phase.nom}</span>
{statusTemplate(phase)}
<span className="ml-auto text-color-secondary">
{phase.duree}j {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(phase.budget)}
</span>
</div>
}
>
<div className="grid">
<div className="col-12 md:col-6">
<h6>Informations générales</h6>
<div className="flex flex-column gap-2">
<div>
<span className="font-semibold">Catégorie:</span>
<Tag value={phase.categorie} severity="info" className="ml-2" />
</div>
<div>
<span className="font-semibold">Période:</span>
<span className="ml-2">
{phase.dateDebut.toLocaleDateString('fr-FR')} {phase.dateFin.toLocaleDateString('fr-FR')}
</span>
</div>
{phase.prerequis.length > 0 && (
<div>
<span className="font-semibold">Prérequis:</span>
<div className="mt-1">
{prerequisTemplate(phase)}
</div>
</div>
)}
</div>
</div>
<div className="col-12 md:col-6">
<h6>Compétences requises</h6>
<div className="flex flex-wrap gap-1">
{phase.competences.map((comp, idx) => (
<Badge key={idx} value={comp} severity="info" />
))}
</div>
{phase.sousPhases.length > 0 && (
<>
<h6 className="mt-4">Sous-phases ({phase.sousPhases.length})</h6>
<div className="flex flex-column gap-2">
{phase.sousPhases.map((sp, idx) => (
<div key={sp.id} className="p-2 bg-gray-50 border-round">
<div className="flex justify-content-between">
<span className="font-semibold text-sm">{sp.nom}</span>
<span className="text-sm text-color-secondary">
{sp.duree}j {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(sp.budget)}
</span>
</div>
<small className="text-color-secondary">
{sp.dateDebut.toLocaleDateString('fr-FR')} {sp.dateFin.toLocaleDateString('fr-FR')}
</small>
</div>
))}
</div>
</>
)}
</div>
</div>
</AccordionTab>
))}
</Accordion>
</TabPanel>
</TabView>
</div>
);
};
export default PreviewGenerationStep;

View File

@@ -0,0 +1,497 @@
'use client';
/**
* Étape 1: Sélection et configuration du template de phases
* Interface de choix du type de chantier avec preview des phases incluses
*/
import React, { useState, useEffect } from 'react';
import { Card } from 'primereact/card';
import { Button } from 'primereact/button';
import { Tag } from 'primereact/tag';
import { Badge } from 'primereact/badge';
import { Skeleton } from 'primereact/skeleton';
import { ScrollPanel } from 'primereact/scrollpanel';
import { Divider } from 'primereact/divider';
import { ProgressBar } from 'primereact/progressbar';
import { InputText } from 'primereact/inputtext';
import { Dropdown } from 'primereact/dropdown';
import { Message } from 'primereact/message';
import { TypeChantierTemplate, WizardConfiguration } from '../PhaseGenerationWizard';
import { Chantier } from '../../../types/btp';
interface TemplateSelectionStepProps {
templates: TypeChantierTemplate[];
loading: boolean;
configuration: WizardConfiguration;
onConfigurationChange: (config: WizardConfiguration) => void;
chantier?: Chantier;
}
const TemplateSelectionStep: React.FC<TemplateSelectionStepProps> = ({
templates,
loading,
configuration,
onConfigurationChange,
chantier
}) => {
const [searchFilter, setSearchFilter] = useState('');
const [categorieFilter, setCategorieFilter] = useState('');
const [complexiteFilter, setComplexiteFilter] = useState('');
const [selectedTemplate, setSelectedTemplate] = useState<TypeChantierTemplate | null>(configuration.typeChantier);
// Options de filtrage
const categorieOptions = [
{ label: 'Toutes catégories', value: '' },
{ label: 'Résidentiel', value: 'RESIDENTIEL' },
{ label: 'Commercial', value: 'COMMERCIAL' },
{ label: 'Industriel', value: 'INDUSTRIEL' },
{ label: 'Infrastructure', value: 'INFRASTRUCTURE' },
{ label: 'Rénovation', value: 'RENOVATION' }
];
const complexiteOptions = [
{ label: 'Toutes complexités', value: '' },
{ label: 'Simple', value: 'SIMPLE' },
{ label: 'Moyenne', value: 'MOYENNE' },
{ label: 'Complexe', value: 'COMPLEXE' },
{ label: 'Expert', value: 'EXPERT' }
];
// Filtrer les templates
const filteredTemplates = templates.filter(template => {
let matches = true;
if (searchFilter) {
const search = searchFilter.toLowerCase();
matches = matches && (
template.nom.toLowerCase().includes(search) ||
template.description.toLowerCase().includes(search) ||
template.tags.some(tag => tag.toLowerCase().includes(search))
);
}
if (categorieFilter) {
matches = matches && template.categorie === categorieFilter;
}
if (complexiteFilter) {
matches = matches && template.complexiteMetier === complexiteFilter;
}
return matches;
});
// Sélectionner un template
const selectTemplate = (template: TypeChantierTemplate) => {
setSelectedTemplate(template);
const newConfig = {
...configuration,
typeChantier: template,
phasesSelectionnees: template.phases, // Par défaut, toutes les phases sont sélectionnées
// Préserver les valeurs du chantier si elles existent, sinon utiliser celles du template
budgetGlobal: configuration.budgetGlobal || template.budgetGlobalEstime,
dureeGlobale: configuration.dureeGlobale || template.dureeGlobaleEstimee
};
onConfigurationChange(newConfig);
};
// Template de carte de template
const templateCard = (template: TypeChantierTemplate) => {
const isSelected = selectedTemplate?.id === template.id;
const complexiteSeverityMap = {
'SIMPLE': 'success',
'MOYENNE': 'info',
'COMPLEXE': 'warning',
'EXPERT': 'danger'
} as const;
const cardHeader = (
<div className="flex justify-content-between align-items-start">
<div className="flex-1">
<h5 className="m-0 mb-2">{template.nom}</h5>
<p className="text-color-secondary text-sm m-0 mb-3 line-height-3">
{template.description}
</p>
</div>
{isSelected && (
<i className="pi pi-check-circle text-green-500 text-2xl ml-2"></i>
)}
</div>
);
const cardFooter = (
<div className="flex justify-content-between align-items-center">
<div className="flex align-items-center gap-2">
<Tag
value={template.categorie}
severity="info"
className="text-xs"
/>
<Tag
value={template.complexiteMetier}
severity={complexiteSeverityMap[template.complexiteMetier]}
className="text-xs"
/>
</div>
<Button
label={isSelected ? "Sélectionné" : "Sélectionner"}
icon={isSelected ? "pi pi-check" : "pi pi-arrow-right"}
className={isSelected ? "p-button-text p-button-rounded p-button-success" : "p-button-text p-button-rounded"}
size="small"
onClick={() => selectTemplate(template)}
/>
</div>
);
return (
<Card
key={template.id}
header={cardHeader}
footer={cardFooter}
className={`h-full cursor-pointer transition-all duration-200 ${
isSelected
? 'border-primary-500 shadow-4'
: 'hover:shadow-2 border-transparent'
}`}
onClick={() => selectTemplate(template)}
>
<div className="grid">
<div className="col-12 md:col-6">
<div className="flex align-items-center gap-2 mb-2">
<i className="pi pi-sitemap text-primary"></i>
<span className="font-semibold">{template.nombreTotalPhases} phases</span>
</div>
<div className="flex align-items-center gap-2 mb-2">
<i className="pi pi-clock text-orange-500"></i>
<span>{template.dureeGlobaleEstimee} jours</span>
</div>
<div className="flex align-items-center gap-2">
<i className="pi pi-euro text-green-500"></i>
<span>
{new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
}).format(template.budgetGlobalEstime)}
</span>
</div>
</div>
<div className="col-12 md:col-6">
<div className="flex align-items-center gap-1 mb-2">
<span className="text-sm text-color-secondary">Catégories:</span>
</div>
<div className="flex flex-wrap gap-1">
{template.phases.slice(0, 3).map((phase, index) => (
<Badge
key={index}
value={phase.categorieMetier}
severity="info"
className="text-xs"
/>
))}
{template.phases.length > 3 && (
<Badge
value={`+${template.phases.length - 3}`}
severity="secondary"
className="text-xs"
/>
)}
</div>
{template.tags.length > 0 && (
<div className="mt-2">
<div className="flex flex-wrap gap-1">
{template.tags.slice(0, 2).map((tag, index) => (
<span
key={index}
className="inline-block bg-blue-50 text-blue-700 px-2 py-1 border-round text-xs"
>
{tag}
</span>
))}
{template.tags.length > 2 && (
<span className="inline-block bg-gray-100 text-gray-600 px-2 py-1 border-round text-xs">
+{template.tags.length - 2}
</span>
)}
</div>
</div>
)}
</div>
</div>
</Card>
);
};
// Prévisualisation du template sélectionné
const previewPanel = () => {
if (!selectedTemplate) return null;
return (
<Card className="mt-4">
<div className="flex align-items-center gap-3 mb-4">
<i className="pi pi-eye text-primary text-2xl"></i>
<h5 className="m-0">Prévisualisation: {selectedTemplate.nom}</h5>
</div>
<div className="grid">
<div className="col-12 md:col-4">
<Card className="bg-blue-50 h-full">
<div className="text-center">
<i className="pi pi-sitemap text-blue-600 text-3xl mb-3"></i>
<h6 className="text-blue-800 m-0 mb-2">Phases incluses</h6>
<div className="text-blue-900 font-bold text-2xl">
{selectedTemplate.nombreTotalPhases}
</div>
<small className="text-blue-600">
phases + sous-phases
</small>
</div>
</Card>
</div>
<div className="col-12 md:col-4">
<Card className="bg-orange-50 h-full">
<div className="text-center">
<i className="pi pi-clock text-orange-600 text-3xl mb-3"></i>
<h6 className="text-orange-800 m-0 mb-2">Durée estimée</h6>
<div className="text-orange-900 font-bold text-2xl">
{selectedTemplate.dureeGlobaleEstimee}
</div>
<small className="text-orange-600">
jours ouvrés
</small>
</div>
</Card>
</div>
<div className="col-12 md:col-4">
<Card className="bg-green-50 h-full">
<div className="text-center">
<i className="pi pi-euro text-green-600 text-3xl mb-3"></i>
<h6 className="text-green-800 m-0 mb-2">Budget estimé</h6>
<div className="text-green-900 font-bold text-xl">
{new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 0
}).format(selectedTemplate.budgetGlobalEstime)}
</div>
<small className="text-green-600">
estimation globale
</small>
</div>
</Card>
</div>
</div>
<Divider />
<div className="grid">
<div className="col-12 md:col-6">
<h6 className="text-primary mb-3">
<i className="pi pi-list mr-2"></i>
Phases principales ({selectedTemplate.phases.length})
</h6>
<ScrollPanel style={{ width: '100%', height: '200px' }}>
{selectedTemplate.phases.map((phase, index) => (
<div key={phase.id} className="flex align-items-center gap-2 mb-2 p-2 bg-gray-50 border-round">
<Badge value={index + 1} className="bg-primary" />
<div className="flex-1">
<div className="font-semibold text-sm">{phase.nom}</div>
<small className="text-color-secondary">
{phase.dureeEstimee}j {phase.sousPhases.length} sous-phases
</small>
</div>
<Tag
value={phase.categorieMetier}
severity="info"
className="text-xs"
/>
</div>
))}
</ScrollPanel>
</div>
<div className="col-12 md:col-6">
<h6 className="text-primary mb-3">
<i className="pi pi-tags mr-2"></i>
Caractéristiques du template
</h6>
<div className="flex flex-column gap-3">
<div>
<span className="font-semibold">Catégorie:</span>
<Tag value={selectedTemplate.categorie} severity="info" className="ml-2" />
</div>
<div>
<span className="font-semibold">Complexité:</span>
<Tag
value={selectedTemplate.complexiteMetier}
severity={
selectedTemplate.complexiteMetier === 'SIMPLE' ? 'success' :
selectedTemplate.complexiteMetier === 'MOYENNE' ? 'info' :
selectedTemplate.complexiteMetier === 'COMPLEXE' ? 'warning' : 'danger'
}
className="ml-2"
/>
</div>
{selectedTemplate.tags.length > 0 && (
<div>
<span className="font-semibold mb-2 block">Tags:</span>
<div className="flex flex-wrap gap-1">
{selectedTemplate.tags.map((tag, index) => (
<span
key={index}
className="inline-block bg-blue-50 text-blue-700 px-2 py-1 border-round text-xs"
>
{tag}
</span>
))}
</div>
</div>
)}
</div>
</div>
</div>
</Card>
);
};
return (
<div>
<div className="flex align-items-center gap-3 mb-4">
<i className="pi pi-th-large text-primary text-2xl"></i>
<h4 className="m-0">Sélection du Template de Chantier</h4>
</div>
{/* Informations du chantier */}
{chantier && (
<Card className="mb-4" style={{ backgroundColor: 'var(--surface-50)', border: '1px solid var(--primary-200)' }}>
<div className="flex align-items-center gap-3 mb-3">
<i className="pi pi-info-circle text-primary"></i>
<h6 className="m-0 text-color">Informations du chantier</h6>
</div>
<div className="grid">
<div className="col-12 md:col-6">
<div className="flex flex-column gap-2">
<div className="text-color">
<span className="font-semibold">Nom :</span> {chantier.nom}
</div>
{chantier.typeChantier && (
<div className="text-color">
<span className="font-semibold">Type :</span> {chantier.typeChantier}
</div>
)}
{chantier.surface && (
<div className="text-color">
<span className="font-semibold">Surface :</span> {chantier.surface} m²
</div>
)}
</div>
</div>
<div className="col-12 md:col-6">
<div className="flex flex-column gap-2">
{chantier.montantPrevu && (
<div className="text-color">
<span className="font-semibold">Budget prévu :</span> {chantier.montantPrevu.toLocaleString('fr-FR', {
style: 'currency',
currency: 'EUR'
})}
</div>
)}
{chantier.dateDebut && (
<div className="text-color">
<span className="font-semibold">Date de début :</span> {new Date(chantier.dateDebut).toLocaleDateString('fr-FR')}
</div>
)}
{chantier.dateFinPrevue && (
<div className="text-color">
<span className="font-semibold">Date de fin prévue :</span> {new Date(chantier.dateFinPrevue).toLocaleDateString('fr-FR')}
</div>
)}
</div>
</div>
</div>
<div className="mt-3 p-2 surface-ground border-round">
<small className="text-primary">
<i className="pi pi-lightbulb mr-1"></i>
Ces informations seront utilisées pour personnaliser les phases générées
</small>
</div>
</Card>
)}
<Message
severity="info"
text="Choisissez le type de chantier qui correspond le mieux à votre projet. Le template sélectionné déterminera les phases qui seront générées automatiquement."
className="mb-4"
/>
{/* Filtres */}
<Card className="mb-4">
<div className="grid">
<div className="col-12 md:col-4">
<span className="p-input-icon-left w-full">
<i className="pi pi-search" />
<InputText
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
placeholder="Rechercher un template..."
className="w-full"
/>
</span>
</div>
<div className="col-12 md:col-4">
<Dropdown
value={categorieFilter}
options={categorieOptions}
onChange={(e) => setCategorieFilter(e.value)}
placeholder="Filtrer par catégorie"
className="w-full"
/>
</div>
<div className="col-12 md:col-4">
<Dropdown
value={complexiteFilter}
options={complexiteOptions}
onChange={(e) => setComplexiteFilter(e.value)}
placeholder="Filtrer par complexité"
className="w-full"
/>
</div>
</div>
</Card>
{/* Liste des templates */}
{loading ? (
<div className="grid">
{[1, 2, 3, 4].map((item) => (
<div key={item} className="col-12 md:col-6 xl:col-4">
<Card>
<Skeleton width="100%" height="200px" />
</Card>
</div>
))}
</div>
) : filteredTemplates.length === 0 ? (
<Message
severity="warn"
text="Aucun template trouvé avec les critères de recherche actuels."
/>
) : (
<div className="grid">
{filteredTemplates.map((template) => (
<div key={template.id} className="col-12 md:col-6 xl:col-4">
{templateCard(template)}
</div>
))}
</div>
)}
{/* Prévisualisation */}
{previewPanel()}
</div>
);
};
export default TemplateSelectionStep;