'use client'; export const dynamic = 'force-dynamic'; import React, { useContext, useRef, useState, useEffect, useCallback, useMemo } from 'react'; import { Button } from 'primereact/button'; import { Column } from 'primereact/column'; import { DataTable } from 'primereact/datatable'; import { Avatar } from 'primereact/avatar'; import { Toast } from 'primereact/toast'; import { Dialog } from 'primereact/dialog'; import { Tag } from 'primereact/tag'; import { ProgressBar } from 'primereact/progressbar'; import { confirmDialog, ConfirmDialog } from 'primereact/confirmdialog'; import { useRouter, useSearchParams } from 'next/navigation'; import { ProgressSpinner } from 'primereact/progressspinner'; import { Message } from 'primereact/message'; import { LayoutContext } from '../../../layout/context/layoutcontext'; import { useDashboard, ChantierActif } from '../../../hooks/useDashboard'; import { useChantierActions } from '../../../hooks/useChantierActions'; import { ChantierStatusBadge, ChantierProgressBar, ChantierUrgencyIndicator } from '../../../components/chantiers'; import ActionButtonGroup from '../../../components/chantiers/ActionButtonGroup'; import { ActionButtonType } from '../../../components/chantiers/ActionButtonStyles'; import CFASymbol from '../../../components/ui/CFASymbol'; const Dashboard = () => { console.log('đŸ—ïž Dashboard: Composant chargĂ©'); const { layoutConfig } = useContext(LayoutContext); const toast = useRef(null); const router = useRouter(); const searchParams = useSearchParams(); const [selectedChantier, setSelectedChantier] = useState(null); const [showQuickView, setShowQuickView] = useState(false); const [authProcessed, setAuthProcessed] = useState(false); const [authError, setAuthError] = useState(null); const [authInProgress, setAuthInProgress] = useState(false); const [isHydrated, setIsHydrated] = useState(false); // Flag global pour Ă©viter les appels multiples (React 18 StrictMode) const authProcessingRef = useRef(false); // MĂ©moriser le code traitĂ© pour Ă©viter les retraitements const processedCodeRef = useRef(null); // Flag pour Ă©viter les redirections multiples const redirectingRef = useRef(false); const currentCode = searchParams.get('code'); const currentState = searchParams.get('state'); console.log('đŸ—ïž Dashboard: SearchParams:', { code: currentCode?.substring(0, 20) + '...', state: currentState, authProcessed, processedCode: processedCodeRef.current?.substring(0, 20) + '...', authInProgress: authInProgress }); // GĂ©rer l'hydratation pour Ă©viter les erreurs SSR/CSR useEffect(() => { setIsHydrated(true); }, []); // RĂ©initialiser authProcessed si on a un nouveau code d'autorisation useEffect(() => { if (!isHydrated) return; // Attendre l'hydratation if (currentCode && authProcessed && !authInProgress && processedCodeRef.current !== currentCode) { console.log('🔄 Dashboard: Nouveau code dĂ©tectĂ©, rĂ©initialisation authProcessed'); setAuthProcessed(false); processedCodeRef.current = null; } }, [currentCode, authProcessed, authInProgress, isHydrated]); // Fonction pour nettoyer l'URL des paramĂštres d'authentification const cleanAuthParams = useCallback(() => { if (typeof window === 'undefined') return; // Protection SSR const url = new URL(window.location.href); if (url.searchParams.has('code') || url.searchParams.has('state')) { url.search = ''; window.history.replaceState({}, '', url.toString()); // Nettoyer sessionStorage aprĂšs succĂšs if (typeof window !== 'undefined') { sessionStorage.removeItem('oauth_code_processed'); } } }, []); // Hooks pour les donnĂ©es et actions du dashboard // Charger les donnĂ©es aprĂšs authentification ou si pas de code d'autorisation const shouldLoadData = authProcessed || !currentCode; const { metrics, chantiersActifs, loading, error, refresh } = useDashboard(); const chantierActions = useChantierActions({ toast, onRefresh: refresh }); // Optimisations avec useMemo pour les calculs coĂ»teux const formattedMetrics = useMemo(() => { if (!metrics) return null; return { chantiersActifs: metrics.chantiersActifs || 0, chiffreAffaires: new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', notation: 'compact', maximumFractionDigits: 1 }).format(metrics.chiffreAffaires || 0), chantiersEnRetard: metrics.chantiersEnRetard || 0, tauxReussite: `${metrics.tauxReussite || 0}%` }; }, [metrics]); const chantiersActifsCount = useMemo(() => { return chantiersActifs?.length || 0; }, [chantiersActifs]); // Templates DataTable optimisĂ©s avec useCallback const chantierBodyTemplate = useCallback((rowData: ChantierActif) => { return (
{rowData.nom}
ID: {rowData.id?.substring(0, 8)}
); }, []); const clientBodyTemplate = useCallback((rowData: ChantierActif) => { const clientName = typeof rowData.client === 'string' ? rowData.client : rowData.client?.nom || 'N/A'; return (
{clientName}
); }, []); const statutBodyTemplate = useCallback((rowData: ChantierActif) => { return ; }, []); const avancementBodyTemplate = useCallback((rowData: ChantierActif) => { return (
{rowData.avancement || 0}%
); }, []); const budgetBodyTemplate = useCallback((rowData: ChantierActif) => { if (!rowData.budget) return -; return ( {new Intl.NumberFormat('fr-FR', { style: 'decimal', notation: 'compact', maximumFractionDigits: 1 }).format(rowData.budget)} ); }, []); const handleQuickView = useCallback((chantier: ChantierActif) => { setSelectedChantier(chantier); setShowQuickView(true); // Le hook gĂšre dĂ©jĂ  les dĂ©tails supplĂ©mentaires chantierActions.handleQuickView(chantier); }, [chantierActions]); // Traiter l'authentification Keycloak si nĂ©cessaire useEffect(() => { // Attendre l'hydratation avant de traiter l'authentification if (!isHydrated) { return; } // Mode dĂ©veloppement : ignorer l'authentification Keycloak if (process.env.NEXT_PUBLIC_DEV_MODE === 'true' || process.env.NEXT_PUBLIC_SKIP_AUTH === 'true') { console.log('🔧 Dashboard: Mode dĂ©veloppement dĂ©tectĂ©, authentification ignorĂ©e'); setAuthProcessed(true); return; } // Si l'authentification est dĂ©jĂ  terminĂ©e, ne rien faire if (authProcessed) { return; } // Si on est en train de rediriger, ne rien faire if (redirectingRef.current) { return; } const processAuth = async () => { try { // Protection absolue contre les boucles if (authInProgress || authProcessingRef.current) { console.log('🛑 Dashboard: Processus d\'authentification dĂ©jĂ  en cours, arrĂȘt'); return; } // Les tokens sont maintenant stockĂ©s dans des cookies HttpOnly // Le middleware les vĂ©rifiera automatiquement // Pas besoin de vĂ©rifier localStorage const code = currentCode; const state = currentState; const error = searchParams.get('error'); if (error) { setAuthError(`Erreur d'authentification: ${error}`); setAuthProcessed(true); return; } // VĂ©rifier si ce code a dĂ©jĂ  Ă©tĂ© traitĂ© if (code && !authProcessed && !authInProgress && !authProcessingRef.current && processedCodeRef.current !== code) { try { console.log('🔐 Traitement du code d\'autorisation Keycloak...', { code: code.substring(0, 20) + '...', state }); // Marquer IMMÉDIATEMENT dans sessionStorage pour empĂȘcher double traitement if (typeof window !== 'undefined') { sessionStorage.setItem('oauth_code_processed', code); } // Marquer l'authentification comme en cours pour Ă©viter les appels multiples authProcessingRef.current = true; processedCodeRef.current = code; setAuthInProgress(true); console.log('📡 Appel de /api/auth/token...'); // Utiliser fetch au lieu d'un formulaire pour Ă©viter la boucle de redirection const response = await fetch('/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, state: state || '' }) }); if (response.ok) { console.log('✅ Authentification rĂ©ussie, tokens stockĂ©s dans les cookies'); // Nettoyer l'URL en enlevant les paramĂštres OAuth window.history.replaceState({}, '', '/dashboard'); // Nettoyer sessionStorage aprĂšs succĂšs if (typeof window !== 'undefined') { sessionStorage.removeItem('oauth_code_processed'); } // Marquer l'authentification comme terminĂ©e setAuthProcessed(true); setAuthInProgress(false); authProcessingRef.current = false; } else { console.error("❌ Erreur lors de l'authentification"); const errorData = await response.json(); setAuthError(`Erreur lors de l'authentification: ${errorData.error || 'Erreur inconnue'}`); setAuthProcessed(true); setAuthInProgress(false); authProcessingRef.current = false; } } catch (error) { console.error('❌ Erreur lors du traitement de l\'authentification:', error); // ARRÊTER LA BOUCLE : Ne pas rediriger automatiquement, juste marquer comme traitĂ© console.log('🛑 Dashboard: Erreur d\'authentification, arrĂȘt du processus pour Ă©viter la boucle'); const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue lors de l\'authentification'; setAuthError(`Erreur lors de l'authentification: ${errorMessage}`); setAuthProcessed(true); setAuthInProgress(false); authProcessingRef.current = false; } } else { setAuthProcessed(true); setAuthInProgress(false); authProcessingRef.current = false; } } catch (error) { console.error('❌ Erreur gĂ©nĂ©rale lors du traitement de l\'authentification:', error); const errorMessage = error instanceof Error ? error.message : 'Erreur inconnue lors de l\'authentification'; setAuthError(`Erreur lors de l'authentification: ${errorMessage}`); setAuthProcessed(true); setAuthInProgress(false); authProcessingRef.current = false; } }; processAuth(); }, [currentCode, currentState, authProcessed, authInProgress, isHydrated]); const actionBodyTemplate = useCallback((rowData: ChantierActif) => { const actions: ActionButtonType[] = ['VIEW', 'PHASES', 'PLANNING', 'STATS', 'MENU']; const handleActionClick = (action: ActionButtonType | string, chantier: ChantierActif) => { try { switch (action) { case 'VIEW': handleQuickView(chantier); break; case 'PHASES': router.push(`/chantiers/${chantier.id}/phases`); break; case 'PLANNING': router.push(`/planning?chantier=${chantier.id}`); break; case 'STATS': chantierActions.handleViewStats(chantier); break; case 'MENU': // Le menu sera gĂ©rĂ© directement par ChantierMenuActions break; // Actions du menu "Plus d'actions" case 'suspend': chantierActions.handleSuspendChantier(chantier); break; case 'close': chantierActions.handleCloseChantier(chantier); break; case 'notify-client': chantierActions.handleNotifyClient(chantier); break; case 'generate-report': chantierActions.handleGenerateReport(chantier); break; case 'generate-invoice': chantierActions.handleGenerateInvoice(chantier); break; case 'create-amendment': chantierActions.handleCreateAmendment(chantier); break; default: console.warn('Action non reconnue:', action); break; } } catch (error) { console.error('Erreur lors de l\'exĂ©cution de l\'action:', action, error); toast.current?.show({ severity: 'error', summary: 'Erreur', detail: 'Une erreur est survenue lors de l\'exĂ©cution de l\'action', life: 3000 }); } }; return (
); }, [handleQuickView, router, chantierActions]); // Attendre l'hydratation pour éviter les erreurs SSR/CSR if (!isHydrated) { return (
Chargement...

Initialisation de l'application

); } // Afficher le chargement pendant le traitement de l'authentification if (!authProcessed) { return (
Authentification en cours...

Traitement des informations de connexion

); } // Afficher l'erreur d'authentification if (authError) { return (
Erreur d'authentification

{authError}

); } if (loading) { return (
Chargement des données...

Récupération des informations du dashboard

); } if (error) { return (
Erreur de chargement

{error}

); } return (
{/* En-tĂȘte du dashboard */}
Dashboard BTPXpress
{/* Métriques KPI - Style Atlantis */}
Chantiers Actifs
{loading ? '...' : (formattedMetrics?.chantiersActifs || 0)}
CA Réalisé
{loading ? '...' : (formattedMetrics?.chiffreAffaires || '0')} {!loading && }
En Retard
{loading ? '...' : (formattedMetrics?.chantiersEnRetard || 0)}
Taux Réussite
{loading ? '...' : (formattedMetrics?.tauxReussite || '0%')}
{/* Tableau des chantiers actifs */}
Chantiers Actifs ({chantiersActifsCount})
{/* Dialog Vue Rapide */} setShowQuickView(false)} style={{ width: '60vw' }} breakpoints={{ '960px': '75vw', '641px': '100vw' }} > {selectedChantier && (() => { // Calculs statistiques depuis les données existantes const dateDebut = selectedChantier.dateDebut ? new Date(selectedChantier.dateDebut) : null; const dateFinPrevue = selectedChantier.dateFinPrevue ? new Date(selectedChantier.dateFinPrevue) : null; const aujourd_hui = new Date(); const joursEcoules = dateDebut ? Math.floor((aujourd_hui.getTime() - dateDebut.getTime()) / (1000 * 60 * 60 * 24)) : 0; const joursRestants = dateFinPrevue ? Math.max(0, Math.floor((dateFinPrevue.getTime() - aujourd_hui.getTime()) / (1000 * 60 * 60 * 24))) : 0; const dureeTotal = dateDebut && dateFinPrevue ? Math.floor((dateFinPrevue.getTime() - dateDebut.getTime()) / (1000 * 60 * 60 * 24)) : 0; const tauxDepense = selectedChantier.budget > 0 ? Math.round((selectedChantier.coutReel / selectedChantier.budget) * 100) : 0; const ecartBudget = selectedChantier.budget - selectedChantier.coutReel; const retard = dateFinPrevue && aujourd_hui > dateFinPrevue && selectedChantier.statut !== 'TERMINE'; const joursRetard = retard ? Math.floor((aujourd_hui.getTime() - dateFinPrevue.getTime()) / (1000 * 60 * 60 * 24)) : 0; return (
{/* Informations principales */}

{typeof selectedChantier.client === 'string' ? selectedChantier.client : selectedChantier.client?.nom}

{retard && ( )}
{selectedChantier.avancement === 100 && ( )}
Début: {dateDebut ? dateDebut.toLocaleDateString('fr-FR') : 'Non définie'}
Fin prévue: {dateFinPrevue ? dateFinPrevue.toLocaleDateString('fr-FR') : 'Non définie'}
{/* Statistiques financiĂšres */}
Prévu: {selectedChantier.budget ? ( {new Intl.NumberFormat('fr-FR', { style: 'decimal' }).format(selectedChantier.budget)} ) : 'Non défini'}
Dépensé: {new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(selectedChantier.coutReel)} ({tauxDepense}%)
Reste: = 0 ? 'text-green-600' : 'text-red-600'}`}> {new Intl.NumberFormat('fr-FR', { style: 'decimal' }).format(Math.abs(ecartBudget))}
Durée totale: {dureeTotal} jours
Jours écoulés: {joursEcoules} jours
Jours restants: {joursRestants > 0 ? `${joursRestants} jours` : 'Terminé'}
{/* Indicateurs de performance */}
{selectedChantier.avancement || 0}%
Avancement
{tauxDepense}%
Budget utilisé
{joursEcoules > 0 && (
{Math.round((selectedChantier.avancement || 0) / joursEcoules * 10) / 10}%
% par jour
)}
{/* Actions */}
); })()}
); }; export default Dashboard;