'use client'; 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); // 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); 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 }); // Réinitialiser authProcessed si on a un nouveau code d'autorisation useEffect(() => { 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]); // Fonction pour nettoyer l'URL des paramètres d'authentification const cleanAuthParams = useCallback(() => { const url = new URL(window.location.href); if (url.searchParams.has('code') || url.searchParams.has('state')) { url.search = ''; window.history.replaceState({}, '', url.toString()); } }, []); // Hooks pour les données et actions du dashboard // Ne charger les données que si l'authentification est terminée ou qu'on a déjà des tokens const hasTokens = typeof window !== 'undefined' && !!localStorage.getItem('accessToken'); const shouldLoadData = authProcessed || hasTokens || !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]); // Nettoyer les paramètres d'authentification au montage useEffect(() => { cleanAuthParams(); }, [cleanAuthParams]); // Traiter l'authentification Keycloak si nécessaire useEffect(() => { // Si l'authentification est déjà terminée, ne rien faire if (authProcessed) { return; } const processAuth = async () => { // Protection absolue contre les boucles if (authInProgress || authProcessingRef.current) { console.log('🛑 Dashboard: Processus d\'authentification déjà en cours, arrêt'); return; } // Vérifier si on a déjà des tokens valides const hasTokens = localStorage.getItem('accessToken'); if (hasTokens) { console.log('✅ Tokens déjà présents, arrêt du processus d\'authentification'); setAuthProcessed(true); return; } 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 l'authentification comme en cours pour éviter les appels multiples authProcessingRef.current = true; processedCodeRef.current = code; setAuthInProgress(true); // Nettoyer les anciens tokens avant l'échange localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); localStorage.removeItem('idToken'); console.log('📡 Appel API /api/auth/token...'); const response = await fetch('/api/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ code, state }), }); console.log('📡 Réponse API /api/auth/token:', { status: response.status, ok: response.ok, statusText: response.statusText }); if (!response.ok) { const errorText = await response.text(); console.error('❌ Échec de l\'échange de token:', { status: response.status, error: errorText }); // Gestion spécifique des codes expirés, invalides ou code verifier manquant if (errorText.includes('invalid_grant') || errorText.includes('Code not valid') || errorText.includes('Code verifier manquant')) { console.log('🔄 Problème d\'authentification détecté:', errorText); // Vérifier si on n'est pas déjà en boucle const retryCount = parseInt(localStorage.getItem('auth_retry_count') || '0'); if (retryCount >= 3) { console.error('🚫 Trop de tentatives d\'authentification. Arrêt pour éviter la boucle infinie.'); localStorage.removeItem('auth_retry_count'); setAuthError('Erreur d\'authentification persistante. Veuillez rafraîchir la page.'); return; } localStorage.setItem('auth_retry_count', (retryCount + 1).toString()); console.log(`🔄 Tentative ${retryCount + 1}/3 - Redirection vers nouvelle authentification...`); // Nettoyer l'URL et rediriger vers une nouvelle authentification const url = new URL(window.location.href); url.search = ''; window.history.replaceState({}, '', url.toString()); // Attendre un peu pour éviter les boucles infinies setTimeout(() => { window.location.href = '/api/auth/login'; }, 2000); return; } throw new Error(`Échec de l'échange de token: ${errorText}`); } const tokens = await response.json(); console.log('✅ Tokens reçus dans le dashboard:', { hasAccessToken: !!tokens.access_token, hasRefreshToken: !!tokens.refresh_token, hasIdToken: !!tokens.id_token }); // Réinitialiser le compteur de tentatives d'authentification localStorage.removeItem('auth_retry_count'); // Stocker les tokens if (tokens.access_token) { localStorage.setItem('accessToken', tokens.access_token); localStorage.setItem('refreshToken', tokens.refresh_token); localStorage.setItem('idToken', tokens.id_token); // Stocker aussi dans un cookie pour le middleware document.cookie = `keycloak-token=${tokens.access_token}; path=/; max-age=3600; SameSite=Lax`; console.log('✅ Tokens stockés avec succès'); } setAuthProcessed(true); setAuthInProgress(false); authProcessingRef.current = false; // Vérifier s'il y a une URL de retour sauvegardée const returnUrl = localStorage.getItem('returnUrl'); if (returnUrl && returnUrl !== '/dashboard') { console.log('🔄 Dashboard: Redirection vers la page d\'origine:', returnUrl); localStorage.removeItem('returnUrl'); window.location.href = returnUrl; return; } // Nettoyer l'URL IMMÉDIATEMENT et arrêter tout traitement futur console.log('🧹 Dashboard: Nettoyage de l\'URL...'); window.history.replaceState({}, document.title, '/dashboard'); // Charger les données du dashboard console.log('🔄 Dashboard: Chargement des données...'); refresh(); // Arrêter définitivement le processus d'authentification return; } 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'); setAuthError(`Erreur lors de l'authentification: ${error.message}`); setAuthProcessed(true); setAuthInProgress(false); authProcessingRef.current = false; } } else { setAuthProcessed(true); setAuthInProgress(false); authProcessingRef.current = false; } }; processAuth(); }, [currentCode, currentState, authProcessed, authInProgress, refresh]); const actionBodyTemplate = useCallback((rowData: ChantierActif) => { const actions: ActionButtonType[] = ['VIEW', 'PHASES', 'PLANNING', 'STATS', 'MENU']; const handleActionClick = (action: ActionButtonType | string, chantier: ChantierActif) => { 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: break; } }; return (
); }, [handleQuickView, router, chantierActions]); // 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;