335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
/**
|
|
* 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; |