582 lines
25 KiB
TypeScript
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; |