'use client'; import React, { useState, useRef, useEffect, useContext, useCallback } from 'react'; import { useParams, useRouter } from 'next/navigation'; import { LayoutContext } from '../../../../../layout/context/layoutcontext'; import { Card } from 'primereact/card'; import { Button } from 'primereact/button'; import { Dialog } from 'primereact/dialog'; import { InputText } from 'primereact/inputtext'; import { InputTextarea } from 'primereact/inputtextarea'; import { Calendar } from 'primereact/calendar'; import { InputNumber } from 'primereact/inputnumber'; import { Dropdown } from 'primereact/dropdown'; import { Checkbox } from 'primereact/checkbox'; import { Toast } from 'primereact/toast'; import { Badge } from 'primereact/badge'; import { Toolbar } from 'primereact/toolbar'; import PhasesTable from '../../../../../components/phases/PhasesTable'; import usePhasesManager from '../../../../../hooks/usePhasesManager'; import BudgetPlanningDialog from '../../../../../components/phases/BudgetPlanningDialog'; import BudgetExecutionDialog from '../../../../../components/phases/BudgetExecutionDialog'; import PhaseGenerationWizard from '../../../../../components/phases/PhaseGenerationWizard'; import PhaseValidationPanel from '../../../../../components/phases/PhaseValidationPanel'; import { PhaseChantier, PhaseFormData, PhaseChantierFormData, StatutPhase } from '../../../../../types/btp-extended'; import { Chantier } from '../../../../../types/btp'; import chantierService from '../../../../../services/chantierService'; import phaseService from '../../../../../services/phaseService'; import { ConfirmDialog } from 'primereact/confirmdialog'; const PhasesPage: React.FC = () => { const params = useParams(); const router = useRouter(); const chantierId = params?.id as string; const { setBreadcrumbs } = useContext(LayoutContext); const toast = useRef(null); // Hook de gestion des phases const { phases, loading, selectedPhase, setSelectedPhase, loadPhases, createPhase, updatePhase, deletePhase, startPhase, updateProgress, createSubPhase, setToastRef } = usePhasesManager({ chantierId }); // États locaux const [chantier, setChantier] = useState(null); const [globalFilter, setGlobalFilter] = useState(''); const [operationLoading, setOperationLoading] = useState(false); // États pour les dialogues const [showPhaseDialog, setShowPhaseDialog] = useState(false); const [showSousPhaseDialog, setShowSousPhaseDialog] = useState(false); const [showWizardDialog, setShowWizardDialog] = useState(false); const [showValidationDialog, setShowValidationDialog] = useState(false); const [showProgressDialog, setShowProgressDialog] = useState(false); const [showBudgetPlanningDialog, setShowBudgetPlanningDialog] = useState(false); const [showBudgetExecutionDialog, setShowBudgetExecutionDialog] = useState(false); const [editingPhase, setEditingPhase] = useState(false); const [parentPhaseForSubPhase, setParentPhaseForSubPhase] = useState(null); // États pour les formulaires const [phaseForm, setPhaseForm] = useState({ nom: '', description: '', dateDebutPrevue: '', dateFinPrevue: '', dureeEstimeeHeures: 8, priorite: 'MOYENNE', critique: false, statut: 'PLANIFIEE', ordreExecution: 1, budgetPrevu: 0, coutReel: 0, prerequisPhases: [], competencesRequises: [], materielsNecessaires: [], fournisseursRecommandes: [] }); const [sousPhaseForm, setSousPhaseForm] = useState({ nom: '', description: '', dateDebutPrevue: '', dateFinPrevue: '', dureeEstimeeHeures: 8, priorite: 'MOYENNE', critique: false, statut: 'PLANIFIEE', ordreExecution: 1, budgetPrevu: 0, coutReel: 0, prerequisPhases: [], competencesRequises: [], materielsNecessaires: [], fournisseursRecommandes: [] }); // État pour l'avancement const [avancement, setAvancement] = useState(0); // Breadcrumbs useEffect(() => { if (chantier) { setBreadcrumbs([ { labels: ['Chantiers', chantier.nom, 'Phases'], to: `/chantiers/${chantierId}/phases` } ]); } else { setBreadcrumbs([ { labels: ['Chantiers', 'Phases'], to: `/chantiers/${chantierId}/phases` } ]); } }, [chantier, chantierId, setBreadcrumbs]); const loadChantier = useCallback(async () => { if (!chantierId) return; try { const chantierData = await chantierService.getById(chantierId); setChantier(chantierData); } catch (error) { console.error('Erreur lors du chargement du chantier:', error); toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Impossible de charger les informations du chantier', life: 5000 }); } }, [chantierId]); // Initialisation du toast useEffect(() => { setToastRef(toast.current); }, [setToastRef]); // Chargement des données useEffect(() => { if (chantierId) { loadChantier(); loadPhases(); } }, [chantierId]); // Gestionnaire pour les phases générées par l'assistant const handlePhasesGenerated = async (phasesGenerees: any[]) => { try { console.log('Phases générées par l\'assistant:', phasesGenerees); toast.current?.show({ severity: 'success', summary: 'Phases générées', detail: `${phasesGenerees.length} phases ont été créées avec succès`, life: 5000 }); // Attendre un peu pour que les données soient persistées setTimeout(async () => { console.log('Rechargement des phases après génération...'); await loadPhases(); }, 1000); } catch (error) { console.error('Erreur lors de la gestion des phases générées:', error); toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Impossible de traiter les phases générées', life: 3000 }); } }; // Gestionnaires d'événements pour les callbacks du composant PhasesTable // Gestionnaires d'événements pour les callbacks du composant PhasesTable const handleViewPhase = (phase: PhaseChantier) => { setSelectedPhase(phase); setShowValidationDialog(true); }; const handleEditPhase = (phase: PhaseChantier) => { setSelectedPhase(phase); setEditingPhase(true); setPhaseForm({ nom: phase.nom, description: phase.description || '', dateDebutPrevue: phase.dateDebutPrevue || '', dateFinPrevue: phase.dateFinPrevue || '', dureeEstimeeHeures: phase.dureeEstimeeHeures || 8, priorite: phase.priorite || 'MOYENNE', critique: phase.critique || false, statut: phase.statut, ordreExecution: phase.ordreExecution || 1, budgetPrevu: phase.budgetPrevu || 0, coutReel: phase.coutReel || 0, prerequisPhases: phase.prerequisPhases || [], competencesRequises: phase.competencesRequises || [], materielsNecessaires: phase.materielsNecessaires || [], fournisseursRecommandes: phase.fournisseursRecommandes || [] }); setShowPhaseDialog(true); }; const handleProgressPhase = (phase: PhaseChantier) => { setSelectedPhase(phase); setAvancement(phase.pourcentageAvancement || 0); setShowProgressDialog(true); }; const handleBudgetPlanPhase = (phase: PhaseChantier) => { setSelectedPhase(phase); setShowBudgetPlanningDialog(true); }; const handleBudgetTrackPhase = (phase: PhaseChantier) => { setSelectedPhase(phase); setShowBudgetExecutionDialog(true); }; const handleSubPhaseAdd = (parentPhase: PhaseChantier) => { setParentPhaseForSubPhase(parentPhase); setSelectedPhase(parentPhase); setEditingPhase(false); const sousPhases = phases.filter(p => p.phaseParent === parentPhase.id); setSousPhaseForm({ nom: '', description: '', dateDebutPrevue: '', dateFinPrevue: '', dureeEstimeeHeures: 8, priorite: 'MOYENNE', critique: false, statut: 'PLANIFIEE', ordreExecution: sousPhases.length + 1, budgetPrevu: 0, coutReel: 0, prerequisPhases: [], competencesRequises: [], materielsNecessaires: [], fournisseursRecommandes: [] }); setShowSousPhaseDialog(true); }; // Fonction pour sauvegarder la planification budgétaire const handleSaveBudgetPlanning = async (budgetData: any) => { if (!selectedPhase) return; try { setOperationLoading(true); // Mettre à jour le budget prévu de la phase avec le prix calculé const updatedPhaseData = { ...selectedPhase, budgetPrevu: budgetData.prixVenteCalcule, // Stocker l'analyse détaillée en tant que métadonnées analyseBudgetaire: { totalMateriel: budgetData.totalMateriel, totalMainOeuvre: budgetData.totalMainOeuvre, totalSousTraitance: budgetData.totalSousTraitance, totalTransport: budgetData.totalTransport, totalAutres: budgetData.totalAutres, totalHT: budgetData.totalHT, margeObjectif: budgetData.margeObjectif, tauxMarge: budgetData.tauxMarge, dateAnalyse: new Date().toISOString() } }; // Appeler l'API pour mettre à jour la phase await phaseService.update(selectedPhase.id!, updatedPhaseData); await loadPhases(); toast.current?.show({ severity: 'success', summary: 'Budget planifié', detail: `Le budget de la phase "${selectedPhase.nom}" a été mis à jour`, life: 3000 }); } catch (error) { console.error('Erreur lors de la sauvegarde du budget:', error); toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Impossible de sauvegarder la planification budgétaire', life: 3000 }); } finally { setOperationLoading(false); } }; // Fonction pour sauvegarder l'exécution budgétaire const handleSaveBudgetExecution = async (executionData: any) => { if (!selectedPhase) return; try { setOperationLoading(true); // Mettre à jour le coût réel de la phase const updatedPhaseData = { ...selectedPhase, coutReel: executionData.coutTotal, // Stocker l'analyse d'exécution en tant que métadonnées analyseExecution: { depensesValidees: executionData.depenses, analyseEcarts: executionData.analyse, ecartTotal: executionData.ecartTotal, ecartPourcentage: executionData.ecartPourcentage, dateAnalyse: executionData.dateAnalyse, statut: executionData.ecartPourcentage > 10 ? 'DEPASSEMENT' : executionData.ecartPourcentage > 5 ? 'ALERTE' : 'CONFORME' } }; // Appeler l'API pour mettre à jour la phase await phaseService.update(selectedPhase.id!, updatedPhaseData); await loadPhases(); toast.current?.show({ severity: 'success', summary: 'Exécution budgétaire mise à jour', detail: `Les dépenses de la phase "${selectedPhase.nom}" ont été enregistrées`, life: 3000 }); } catch (error) { console.error('Erreur lors de la sauvegarde de l\'exécution:', error); toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Impossible de sauvegarder l\'exécution budgétaire', life: 3000 }); } finally { setOperationLoading(false); } }; // Fonction pour créer/modifier une phase principale const handleSavePhase = async () => { if (!phaseForm.nom.trim()) { toast.current?.show({ severity: 'warn', summary: 'Données manquantes', detail: 'Veuillez remplir le nom de la phase', life: 3000 }); return; } try { setOperationLoading(true); const phaseData = { nom: phaseForm.nom, description: phaseForm.description, statut: 'PLANIFIEE' as StatutPhase, dateDebutPrevue: phaseForm.dateDebutPrevue, dateFinPrevue: phaseForm.dateFinPrevue, dureeEstimeeHeures: phaseForm.dureeEstimeeHeures, priorite: phaseForm.priorite, critique: phaseForm.critique, ordreExecution: phaseForm.ordreExecution, budgetPrevu: phaseForm.budgetPrevu, coutReel: phaseForm.coutReel, chantierId: chantierId, prerequisPhases: phaseForm.prerequisPhases, competencesRequises: phaseForm.competencesRequises, materielsNecessaires: phaseForm.materielsNecessaires, fournisseursRecommandes: phaseForm.fournisseursRecommandes }; console.log('Données de la phase à créer:', phaseData); console.log('ID du chantier:', chantierId); if (editingPhase && selectedPhase) { // Modification // ID sera ajouté par le service await phaseService.update(selectedPhase.id!, { ...phaseData, id: selectedPhase.id } as any); toast.current?.show({ severity: 'success', summary: 'Phase modifiée', detail: `La phase "${phaseForm.nom}" a été modifiée`, life: 3000 }); } else { // Création await phaseService.create(phaseData); toast.current?.show({ severity: 'success', summary: 'Phase créée', detail: `La phase "${phaseForm.nom}" a été créée`, life: 3000 }); } // Ajouter un petit délai pour s'assurer que le backend a bien persisté les données await new Promise(resolve => setTimeout(resolve, 500)); await loadPhases(); setShowPhaseDialog(false); setEditingPhase(false); setSelectedPhase(null); } catch (error: any) { console.error('Erreur lors de la sauvegarde de la phase:', error); // Gérer le cas où le serveur backend n'est pas disponible if (error.code === 'ECONNREFUSED' || error.message?.includes('Network Error') || error.response?.status === undefined) { toast.current?.show({ severity: 'warn', summary: 'Serveur indisponible', detail: 'Le serveur backend n\'est pas démarré. La phase sera créée quand le serveur sera disponible.', life: 8000 }); // En mode développement, simuler la création pour ne pas bloquer l'UI if (process.env.NODE_ENV === 'development') { const simulatedPhase: PhaseChantier = { id: Math.random().toString(), // ID temporaire nom: phaseForm.nom, description: phaseForm.description, statut: 'PLANIFIEE', dateDebutPrevue: phaseForm.dateDebutPrevue, dateFinPrevue: phaseForm.dateFinPrevue, dureeEstimeeHeures: phaseForm.dureeEstimeeHeures, priorite: phaseForm.priorite, critique: phaseForm.critique, ordreExecution: phaseForm.ordreExecution, chantierId: chantierId, prerequisPhases: phaseForm.prerequisPhases, competencesRequises: phaseForm.competencesRequises, materielsNecessaires: phaseForm.materielsNecessaires, fournisseursRecommandes: phaseForm.fournisseursRecommandes, dateCreation: new Date().toISOString(), dateModification: new Date().toISOString() }; // Ajouter la phase simulée à la liste locale // setPhases(prev => [...prev, simulatedPhase]); // Phases gérées par le chantier setShowPhaseDialog(false); setEditingPhase(false); setSelectedPhase(null); return; } } else { let errorMessage = 'Erreur inconnue'; if (error.response?.status === 400) { errorMessage = error.response?.data?.message || 'Données invalides. Vérifiez les champs obligatoires.'; } else if (error.response?.status === 401) { errorMessage = 'Vous n\'\u00eates pas autorisé à effectuer cette action.'; } else if (error.response?.status === 404) { errorMessage = 'Ressource non trouvée sur le serveur.'; } else if (error.response?.status >= 500) { errorMessage = 'Erreur interne du serveur. Veuillez réessayer plus tard.'; } else { errorMessage = error.response?.data?.message || error.message || 'Erreur inconnue'; } console.error('Détails de l\'erreur:', { status: error.response?.status, data: error.response?.data, config: error.config }); toast.current?.show({ severity: 'error', summary: 'Erreur', detail: `${editingPhase ? 'Impossible de modifier la phase' : 'Impossible de créer la phase'}: ${errorMessage}`, life: 8000 }); } } finally { setOperationLoading(false); } }; // Fonction pour créer/modifier une sous-phase const createSousPhase = async () => { if (!selectedPhase || !sousPhaseForm.nom.trim()) { toast.current?.show({ severity: 'warn', summary: 'Données manquantes', detail: 'Veuillez remplir le nom de la sous-phase', life: 3000 }); return; } // Debug logs console.log('createSousPhase - selectedPhase:', selectedPhase); console.log('createSousPhase - sousPhaseForm:', sousPhaseForm); console.log('createSousPhase - chantierId:', chantierId); try { setOperationLoading(true); // Calculer l'ordre d'exécution pour éviter les doublons let maxOrder = 0; try { if (phases && phases.length > 0) { const ordres = phases .map(p => p && typeof p.ordreExecution === 'number' ? p.ordreExecution : 0) .filter(ordre => !isNaN(ordre)); maxOrder = ordres.length > 0 ? Math.max(...ordres) : 0; } } catch (e) { console.warn('Erreur lors du calcul de l\'ordre max:', e); maxOrder = 0; } const nextOrder = maxOrder + 1; console.log('Ordre d\'exécution calculé:', nextOrder, 'Max actuel:', maxOrder); const phaseData: PhaseChantierFormData = { nom: sousPhaseForm.nom, description: sousPhaseForm.description, statut: 'PLANIFIEE', dateDebutPrevue: sousPhaseForm.dateDebutPrevue, dateFinPrevue: sousPhaseForm.dateFinPrevue, dureeEstimeeHeures: sousPhaseForm.dureeEstimeeHeures, priorite: sousPhaseForm.priorite, critique: sousPhaseForm.critique, pourcentageAvancement: 0, phaseParentId: selectedPhase.id, chantierId: selectedPhase.chantierId || chantierId, ordreExecution: nextOrder, // Utiliser l'ordre calculé budgetPrevu: sousPhaseForm.budgetPrevu, coutReel: sousPhaseForm.coutReel, prerequisPhases: sousPhaseForm.prerequisPhases, competencesRequises: sousPhaseForm.competencesRequises, materielsNecessaires: sousPhaseForm.materielsNecessaires, fournisseursRecommandes: sousPhaseForm.fournisseursRecommandes, actif: true }; if (editingPhase && selectedPhase.id) { await phaseService.update(selectedPhase.id, phaseData); toast.current?.show({ severity: 'success', summary: 'Sous-phase modifiée', detail: 'La sous-phase a été mise à jour', life: 3000 }); } else { await phaseService.create(phaseData); toast.current?.show({ severity: 'success', summary: 'Sous-phase créée', detail: `La sous-phase "${phaseData.nom}" a été ajoutée`, life: 3000 }); } await loadPhases(); setShowSousPhaseDialog(false); setEditingPhase(false); } catch (error) { // Gestion d'erreur super simplifiée pour éviter les erreurs dans la gestion d'erreur console.log('=== ERREUR CREATESOUXPHASE ==='); console.log('Type:', typeof error); console.log('Error object:', error); let errorMessage = 'Erreur lors de la création de la sous-phase'; // Tentative d'extraction du message de façon très sécurisée if (error?.response?.data?.message) { errorMessage = error.response.data.message; } else if (error?.message) { errorMessage = error.message; } console.log('Message final:', errorMessage); // Affichage du toast de façon très sécurisée if (toast && toast.current && typeof toast.current.show === 'function') { toast.current.show({ severity: 'error', summary: 'Erreur', detail: errorMessage, life: 8000 }); } } finally { setOperationLoading(false); } }; // Toolbar const toolbarStartTemplate = (
{chantier ? `Phases - ${chantier.nom}` : 'Gestion des phases'}
!p.phaseParent).length} />
); const toolbarEndTemplate = (
setGlobalFilter(e.target.value)} placeholder="Rechercher..." />
); // Footer du dialog de sous-phase const sousPhaseDialogFooter = (
); return (
{/* Dialog pour créer/modifier une phase principale */} { setShowPhaseDialog(false); setEditingPhase(false); setPhaseForm({ nom: '', description: '', dateDebutPrevue: '', dateFinPrevue: '', dureeEstimeeHeures: 8, priorite: 'MOYENNE', critique: false, statut: 'PLANIFIEE', ordreExecution: 1, prerequisPhases: [], competencesRequises: [], materielsNecessaires: [], fournisseursRecommandes: [] }); }} footer={
} style={{ width: '70vw', maxWidth: '900px' }} modal >
setPhaseForm(prev => ({ ...prev, nom: e.target.value }))} className="w-full" placeholder="Ex: Fondations, Gros œuvre, Finitions..." />
setPhaseForm(prev => ({ ...prev, description: e.target.value }))} className="w-full" rows={3} placeholder="Description détaillée de la phase..." />
setPhaseForm(prev => ({ ...prev, dateDebutPrevue: e.value ? e.value.toISOString().split('T')[0] : '' }))} className="w-full" dateFormat="dd/mm/yy" placeholder="Sélectionner une date" />
setPhaseForm(prev => ({ ...prev, dateFinPrevue: e.value ? e.value.toISOString().split('T')[0] : '' }))} className="w-full" dateFormat="dd/mm/yy" placeholder="Sélectionner une date" />
setPhaseForm(prev => ({ ...prev, dureeEstimeeHeures: e.value || 0 }))} className="w-full" min={1} suffix=" h" />
setPhaseForm(prev => ({ ...prev, ordreExecution: e.value || 1 }))} className="w-full" min={1} />
setPhaseForm(prev => ({ ...prev, priorite: e.value }))} className="w-full" />
setPhaseForm(prev => ({ ...prev, budgetPrevu: e.value || 0 }))} className="w-full" min={0} mode="currency" currency="EUR" locale="fr-FR" placeholder="0,00 €" />
setPhaseForm(prev => ({ ...prev, coutReel: e.value || 0 }))} className="w-full" min={0} mode="currency" currency="EUR" locale="fr-FR" placeholder="0,00 €" />
setPhaseForm(prev => ({ ...prev, critique: e.checked }))} />
{/* Dialog pour créer/modifier une sous-phase */} { setShowSousPhaseDialog(false); setEditingPhase(false); }} footer={sousPhaseDialogFooter} style={{ width: '50vw' }} modal >
setSousPhaseForm(prev => ({ ...prev, nom: e.target.value }))} className="w-full" placeholder="Ex: Préparation du béton" />
setSousPhaseForm(prev => ({ ...prev, description: e.target.value }))} className="w-full" rows={3} placeholder="Description détaillée de la sous-phase" />
setSousPhaseForm(prev => ({ ...prev, dateDebutPrevue: e.value ? e.value.toISOString().split('T')[0] : '' }))} className="w-full" dateFormat="dd/mm/yy" placeholder="Sélectionner une date" />
setSousPhaseForm(prev => ({ ...prev, dateFinPrevue: e.value ? e.value.toISOString().split('T')[0] : '' }))} className="w-full" dateFormat="dd/mm/yy" placeholder="Sélectionner une date" />
setSousPhaseForm(prev => ({ ...prev, dureeEstimeeHeures: e.value || 8 }))} className="w-full" min={1} max={1000} />
setSousPhaseForm(prev => ({ ...prev, priorite: e.value }))} className="w-full" />
setSousPhaseForm(prev => ({ ...prev, budgetPrevu: e.value || 0 }))} className="w-full" min={0} mode="currency" currency="EUR" locale="fr-FR" placeholder="0,00 €" />
setSousPhaseForm(prev => ({ ...prev, coutReel: e.value || 0 }))} className="w-full" min={0} mode="currency" currency="EUR" locale="fr-FR" placeholder="0,00 €" />
setSousPhaseForm(prev => ({ ...prev, critique: e.checked }))} />
{/* Dialog de validation des prérequis */} setShowValidationDialog(false)} style={{ width: '90vw', maxWidth: '800px' }} modal > {selectedPhase && ( { const prereq = phases.find(p => p.id === prereqId); if (prereq) { setSelectedPhase(prereq); } }} /> )} {/* Dialog de planification budgétaire */} setShowBudgetPlanningDialog(false)} phase={selectedPhase} onSave={handleSaveBudgetPlanning} /> {/* Dialog d'exécution budgétaire */} setShowBudgetExecutionDialog(false)} phase={selectedPhase} onSave={handleSaveBudgetExecution} /> {/* Assistant de génération automatique de phases */} {chantier && ( setShowWizardDialog(false)} chantier={chantier} onGenerated={handlePhasesGenerated} /> )}
); }; export default PhasesPage;