Initial commit

This commit is contained in:
dahoud
2025-10-01 01:39:07 +00:00
commit b430bf3b96
826 changed files with 255287 additions and 0 deletions

335
hooks/useDashboard.ts Normal file
View 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;