Initial commit
This commit is contained in:
582
components/phases/wizard/CustomizationStep.tsx
Normal file
582
components/phases/wizard/CustomizationStep.tsx
Normal 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;
|
||||
744
components/phases/wizard/PreviewGenerationStep.tsx
Normal file
744
components/phases/wizard/PreviewGenerationStep.tsx
Normal 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;
|
||||
497
components/phases/wizard/TemplateSelectionStep.tsx
Normal file
497
components/phases/wizard/TemplateSelectionStep.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user