/** * Hook pour les données du dashboard - Version 2025 BTP Xpress */ import { useState, useEffect, useCallback } from 'react'; import { apiClient } from '../services/api-client'; import { Chantier } from '../types/btp'; // Types pour les données du dashboard export interface DashboardMetrics { totalChantiers: number; chantiersActifs: number; chantiersEnRetard: number; chantiersTermines: number; totalEquipes: number; equipesDisponibles: number; totalMateriel: number; materielDisponible: number; materielEnMaintenance: number; totalDocuments: number; totalPhotos: number; budgetTotal: number; coutReel: number; chiffreAffaires: number; objectifCA: number; tauxReussite: number; satisfactionClient: number; } export interface ChantierActif { id: string; nom: string; client: string | { nom: string; prenom?: string }; avancement: number; dateDebut: string; dateFinPrevue: string; statut: 'EN_COURS' | 'EN_RETARD' | 'PLANIFIE' | 'TERMINE'; budget: number; coutReel: number; equipe?: { id: string; nom: string; nombreMembres: number; }; } export interface ActiviteRecente { id: string; type: 'CHANTIER' | 'MAINTENANCE' | 'DOCUMENT' | 'EQUIPE'; titre: string; description: string; date: string; utilisateur: string; statut: 'SUCCESS' | 'WARNING' | 'ERROR' | 'INFO'; } export interface TacheUrgente { id: string; titre: string; description: string; priorite: 'HAUTE' | 'MOYENNE' | 'BASSE'; echeance: string; assignee: string; statut: 'A_FAIRE' | 'EN_COURS' | 'TERMINEE'; chantier?: { id: string; nom: string; }; } interface DashboardData { metrics: DashboardMetrics | null; chantiersActifs: ChantierActif[]; activitesRecentes: ActiviteRecente[]; tachesUrgentes: TacheUrgente[]; loading: boolean; error: string | null; } export const useDashboard = (periode: 'semaine' | 'mois' | 'trimestre' | 'annee' = 'mois') => { const [data, setData] = useState({ metrics: null, chantiersActifs: [], activitesRecentes: [], tachesUrgentes: [], loading: true, error: null, }); const [currentPeriode, setCurrentPeriode] = useState(periode); const loadDashboardData = useCallback(async (abortController?: AbortController) => { try { setData(prev => ({ ...prev, loading: true, error: null })); console.log('📊 Dashboard: Démarrage du chargement des données...'); console.log('🔗 API Base URL:', process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080/api/v1'); // Vérifier si la requête a été annulée if (abortController?.signal.aborted) { console.log('📊 Dashboard: Requête annulée avant le début'); return; } // Test de connectivité simple d'abord try { const healthCheck = await apiClient.get('/health'); console.log('💚 Backend accessible:', healthCheck.status === 200 ? 'OK' : 'ERREUR'); } catch (healthError) { console.warn('⚠️ Backend health check échoué, tentative des endpoints dashboard...', healthError.message); } // Charger les données du dashboard depuis l'API - STRICTEMENT depuis le backend const [dashboardStatsResponse, chantiersActifsResponse] = await Promise.allSettled([ apiClient.get('/api/v1/dashboard/stats').catch(async (error) => { console.warn('⚠️ /api/v1/dashboard/stats non disponible, fallback vers /api/v1/chantiers'); // Si l'endpoint stats n'existe pas, essayer /api/v1/chantiers const chantiers = await apiClient.get('/api/v1/chantiers'); const chantiersActifs = chantiers.data.filter(c => c.actif && (c.statut === 'EN_COURS' || c.statut === 'PLANIFIE')); return { data: { totalChantiers: chantiers.data.length, chantiersActifs: chantiersActifs.length, chantiersEnRetard: 0, chantiersTermines: chantiers.data.filter(c => c.statut === 'TERMINE').length, totalEquipes: 0, equipesDisponibles: 0, totalMateriel: 0, materielDisponible: 0, materielEnMaintenance: 0, totalDocuments: 0, totalPhotos: 0, budgetTotal: chantiersActifs.reduce((sum, c) => sum + (c.montantPrevu || 0), 0), coutReel: chantiersActifs.reduce((sum, c) => sum + (c.montantReel || 0), 0), chiffreAffaires: 0, objectifCA: 0, tauxReussite: 0, satisfactionClient: 0 } }; }), apiClient.get('/api/v1/chantiers').then(response => { const allChantiers = response.data; const chantiersActifs = allChantiers.filter(c => c.actif && (c.statut === 'EN_COURS' || c.statut === 'PLANIFIE')); return { data: chantiersActifs }; }) ]); // Extraire les données avec gestion d'erreur const dashboardStats = dashboardStatsResponse.status === 'fulfilled' ? dashboardStatsResponse.value.data : null; const chantiersActifs = chantiersActifsResponse.status === 'fulfilled' ? chantiersActifsResponse.value.data : []; // Transformer les données pour correspondre à l'interface attendue - UNIQUEMENT données réelles const metrics = dashboardStats ? { totalChantiers: dashboardStats.totalChantiers || 0, chantiersActifs: chantiersActifs.length || 0, chantiersEnRetard: dashboardStats.chantiersEnRetard || 0, chantiersTermines: dashboardStats.chantiersTermines || 0, totalEquipes: dashboardStats.totalEquipes || 0, equipesDisponibles: dashboardStats.equipesDisponibles || 0, totalMateriel: dashboardStats.totalMateriel || 0, materielDisponible: dashboardStats.materielDisponible || 0, materielEnMaintenance: dashboardStats.materielEnMaintenance || 0, totalDocuments: dashboardStats.totalDocuments || 0, totalPhotos: dashboardStats.totalPhotos || 0, budgetTotal: dashboardStats.budgetTotal || 0, coutReel: dashboardStats.coutReel || 0, chiffreAffaires: dashboardStats.chiffreAffaires || 0, objectifCA: dashboardStats.objectifCA || 0, tauxReussite: dashboardStats.tauxReussite || 0, satisfactionClient: dashboardStats.satisfactionClient || 0 } : null; // Transformer les chantiers Chantier[] vers ChantierActif[] avec avancement asynchrone const transformedChantiersActifs: ChantierActif[] = await Promise.all( (chantiersActifs as Chantier[]).map(async (chantier) => ({ id: chantier.id, nom: chantier.nom, client: typeof chantier.client === 'string' ? chantier.client : chantier.client?.nom || 'Client inconnu', avancement: await calculateAvancement(chantier), dateDebut: chantier.dateDebut, dateFinPrevue: chantier.dateFinPrevue || '', statut: mapStatutChantier(chantier.statut), budget: chantier.montantPrevu || 0, coutReel: chantier.montantReel || 0 })) ); const activitesRecentes = []; // À implémenter avec un endpoint spécifique const tachesUrgentes = []; // À implémenter avec un endpoint spécifique // Log des erreurs pour debugging if (dashboardStatsResponse.status === 'rejected') { console.error('❌ Erreur dashboard stats:', dashboardStatsResponse.reason?.message || dashboardStatsResponse.reason); console.error('📡 URL tentée:', 'GET /api/v1/dashboard/stats'); } if (chantiersActifsResponse.status === 'rejected') { console.error('❌ Erreur chantiers actifs:', chantiersActifsResponse.reason?.message || chantiersActifsResponse.reason); console.error('📡 URL tentée:', 'GET /api/v1/chantiers/actifs'); } console.log('✅ Dashboard stats:', dashboardStats ? 'Données reçues' : 'Pas de données'); console.log('✅ Chantiers actifs:', chantiersActifs ? `${chantiersActifs.length} chantiers` : 'Pas de données'); setData({ metrics, chantiersActifs: transformedChantiersActifs, activitesRecentes, tachesUrgentes, loading: false, error: null, }); } catch (error) { console.error('💥 Erreur lors du chargement du dashboard:', error); setData(prev => ({ ...prev, loading: false, error: 'Erreur de communication avec le serveur. Vérifiez que le backend est démarré sur http://localhost:8080', })); } }, [currentPeriode]); useEffect(() => { const abortController = new AbortController(); // Vérifier si on a des tokens d'authentification stockés if (typeof window !== 'undefined') { const hasTokens = localStorage.getItem('accessToken'); const currentUrl = window.location.href; const hasAuthCode = currentUrl.includes('code=') && currentUrl.includes('/dashboard'); console.log('📊 Dashboard Hook: État actuel', { hasTokens: !!hasTokens, hasAuthCode, url: currentUrl }); // Si on a des tokens, charger immédiatement même avec un code dans l'URL if (hasTokens) { console.log('📊 Dashboard Hook: Tokens trouvés, chargement immédiat des données...'); loadDashboardData(abortController); return () => abortController.abort(); } if (hasAuthCode && !hasTokens) { console.log('📊 Dashboard Hook: Attente de la fin du traitement d\'authentification...'); // Attendre un peu puis réessayer (délai réduit) const timer = setTimeout(() => { if (!abortController.signal.aborted) { console.log('📊 Dashboard Hook: Retry après authentification...'); loadDashboardData(abortController); } }, 3000); // Attendre 3 secondes pour que l'auth se termine return () => { clearTimeout(timer); abortController.abort(); }; } } // Charger par défaut console.log('📊 Dashboard Hook: Chargement par défaut...'); loadDashboardData(abortController); return () => abortController.abort(); }, [loadDashboardData]); const refresh = useCallback(() => { const abortController = new AbortController(); loadDashboardData(abortController); return () => abortController.abort(); }, [loadDashboardData]); const changePeriode = useCallback((nouvellePeriode: 'semaine' | 'mois' | 'trimestre' | 'annee') => { setCurrentPeriode(nouvellePeriode); }, []); return { ...data, refresh, changePeriode, periode: currentPeriode, isLoading: data.loading, }; }; // Fonctions utilitaires pour transformer les données async function calculateAvancement(chantier: Chantier): Promise { if (chantier.statut === 'TERMINE') return 100; if (chantier.statut === 'ANNULE') return 0; try { // Essayer d'obtenir l'avancement granulaire basé sur les tâches const avancementGranulaire = await apiClient.get(`/chantiers/${chantier.id}/avancement-granulaire`); if (avancementGranulaire.data && typeof avancementGranulaire.data.pourcentage === 'number') { return Math.round(avancementGranulaire.data.pourcentage); } } catch (error) { console.warn('Avancement granulaire non disponible, utilisation du calcul temporel:', error); } // Fallback : calcul basé sur les dates (ancien système) if (!chantier.dateDebut || !chantier.dateFinPrevue) { return chantier.statut === 'EN_COURS' ? 25 : 0; } const now = new Date(); const start = new Date(chantier.dateDebut); const end = new Date(chantier.dateFinPrevue); if (now < start) return 0; if (now > end) return 100; const totalDays = (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24); const elapsedDays = (now.getTime() - start.getTime()) / (1000 * 60 * 60 * 24); const calculatedProgress = Math.min(Math.max((elapsedDays / totalDays) * 100, 0), 100); return Math.round(calculatedProgress); } function mapStatutChantier(statut: string): 'EN_COURS' | 'EN_RETARD' | 'PLANIFIE' | 'TERMINE' { switch (statut) { case 'EN_COURS': return 'EN_COURS'; case 'PLANIFIE': return 'PLANIFIE'; case 'TERMINE': return 'TERMINE'; case 'EN_RETARD': return 'EN_RETARD'; // Si le statut backend n'est pas reconnu, essayer de déterminer s'il est en retard default: return 'EN_COURS'; } } export default useDashboard;