'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); // Nettoyer les paramètres OAuth de l'URL après le retour de Keycloak useEffect(() => { if (typeof window === 'undefined') return; const url = new URL(window.location.href); // Nettoyer query string if (url.searchParams.has('code') || url.searchParams.has('state') || url.searchParams.has('session_state')) { console.log('🧹 Dashboard: Nettoyage des paramètres OAuth de l\'URL (query)'); url.search = ''; window.history.replaceState({}, '', url.toString()); } // Nettoyer fragment (hash) si Keycloak utilise implicit flow par erreur if (url.hash && (url.hash.includes('code=') || url.hash.includes('state='))) { console.log('🧹 Dashboard: Nettoyage des paramètres OAuth de l\'URL (fragment)'); url.hash = ''; window.history.replaceState({}, '', url.toString()); } }, []); const { metrics, chantiersActifs, loading, error, refresh } = useDashboard(); const chantierActions = useChantierActions({ toast, onRefresh: refresh }); console.log('🏗️ Dashboard: État du chargement -', { loading, metricsLoaded: !!metrics, chantiersCount: chantiersActifs?.length || 0 }); // 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]); 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]); 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;