Initial commit
This commit is contained in:
335
hooks/useDashboard.ts
Normal file
335
hooks/useDashboard.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* 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<DashboardData>({
|
||||
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<number> {
|
||||
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;
|
||||
Reference in New Issue
Block a user