Files
btpxpress-frontend/components/phases/wizard/CustomizationStep.tsx
2025-10-13 05:29:32 +02:00

582 lines
25 KiB
TypeScript

'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;