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;
|
||||
Reference in New Issue
Block a user