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

View File

@@ -0,0 +1,364 @@
import { renderHook, waitFor } from '@testing-library/react'
import { useDashboard } from '../useDashboard'
import { clientService, chantierService, devisService, factureService } from '../../services/api'
// Mock des services
jest.mock('../../services/api')
const mockClientService = clientService as jest.Mocked<typeof clientService>
const mockChantierService = chantierService as jest.Mocked<typeof chantierService>
const mockDevisService = devisService as jest.Mocked<typeof devisService>
const mockFactureService = factureService as jest.Mocked<typeof factureService>
const mockClients = [
{ id: '1', nom: 'Client', prenom: 'Un', email: 'client1@test.com' },
{ id: '2', nom: 'Client', prenom: 'Deux', email: 'client2@test.com' },
]
const mockChantiers = [
{
id: '1',
nom: 'Chantier 1',
client: mockClients[0],
statut: 'EN_COURS',
dateDebut: '2024-01-01',
montantPrevu: 10000,
},
{
id: '2',
nom: 'Chantier 2',
client: mockClients[1],
statut: 'PLANIFIE',
dateDebut: '2024-01-15',
montantPrevu: 15000,
},
{
id: '3',
nom: 'Chantier 3',
client: mockClients[0],
statut: 'TERMINE',
dateDebut: '2024-01-10',
montantPrevu: 8000,
},
]
const mockDevis = [
{
id: '1',
numero: 'D2024-001',
client: mockClients[0],
statut: 'ENVOYE',
dateEmission: '2024-01-01',
dateValidite: '2024-12-31',
montantTTC: 5000,
},
{
id: '2',
numero: 'D2024-002',
client: mockClients[1],
statut: 'ACCEPTE',
dateEmission: '2024-01-05',
dateValidite: '2024-12-31',
montantTTC: 7000,
},
]
const mockFactures = [
{
id: '1',
numero: 'F2024-001',
client: mockClients[0],
statut: 'PAYEE',
dateEmission: '2024-01-01',
dateEcheance: '2024-01-31',
montantTTC: 10000,
},
{
id: '2',
numero: 'F2024-002',
client: mockClients[1],
statut: 'ENVOYEE',
dateEmission: '2024-01-10',
dateEcheance: '2024-01-01', // En retard
montantTTC: 5000,
},
]
describe('Hook useDashboard', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.spyOn(console, 'log').mockImplementation(() => {})
jest.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
jest.restoreAllMocks()
})
it('devrait initialiser avec les valeurs par défaut', () => {
mockClientService.getAll.mockImplementation(() => new Promise(() => {}))
mockChantierService.getAll.mockImplementation(() => new Promise(() => {}))
mockDevisService.getAll.mockImplementation(() => new Promise(() => {}))
mockFactureService.getAll.mockImplementation(() => new Promise(() => {}))
const { result } = renderHook(() => useDashboard())
expect(result.current.stats).toBeNull()
expect(result.current.chantiersRecents).toEqual([])
expect(result.current.facturesEnRetard).toEqual([])
expect(result.current.devisEnAttente).toEqual([])
expect(result.current.loading).toBe(true)
expect(result.current.error).toBeNull()
})
it('devrait charger les données avec succès', async () => {
mockClientService.getAll.mockResolvedValue(mockClients)
mockChantierService.getAll.mockResolvedValue(mockChantiers)
mockDevisService.getAll.mockResolvedValue(mockDevis)
mockFactureService.getAll.mockResolvedValue(mockFactures)
const { result } = renderHook(() => useDashboard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.stats).toEqual({
totalClients: 2,
totalChantiers: 3,
chantiersEnCours: 1,
chantiersPlanifies: 1,
chantiersTermines: 1,
totalDevis: 2,
devisAcceptes: 1,
devisEnAttente: 1,
totalFactures: 2,
facturesPayees: 1,
facturesEnRetard: 1,
chiffreAffaires: 10000,
chiffreAffairesMois: expect.any(Number),
chiffreAffairesAnnee: expect.any(Number),
})
})
it('devrait calculer les chantiers récents', async () => {
mockClientService.getAll.mockResolvedValue(mockClients)
mockChantierService.getAll.mockResolvedValue(mockChantiers)
mockDevisService.getAll.mockResolvedValue(mockDevis)
mockFactureService.getAll.mockResolvedValue(mockFactures)
const { result } = renderHook(() => useDashboard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.chantiersRecents).toHaveLength(3)
expect(result.current.chantiersRecents[0]).toEqual({
id: '2',
nom: 'Chantier 2',
client: 'Client Deux',
statut: 'PLANIFIE',
dateDebut: '2024-01-15',
montantPrevu: 15000,
})
})
it('devrait calculer les factures en retard', async () => {
mockClientService.getAll.mockResolvedValue(mockClients)
mockChantierService.getAll.mockResolvedValue(mockChantiers)
mockDevisService.getAll.mockResolvedValue(mockDevis)
mockFactureService.getAll.mockResolvedValue(mockFactures)
const { result } = renderHook(() => useDashboard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.facturesEnRetard).toHaveLength(1)
expect(result.current.facturesEnRetard[0]).toEqual({
id: '2',
numero: 'F2024-002',
client: 'Client Deux',
montantTTC: 5000,
dateEcheance: '2024-01-01',
joursRetard: expect.any(Number),
})
})
it('devrait calculer les devis en attente', async () => {
mockClientService.getAll.mockResolvedValue(mockClients)
mockChantierService.getAll.mockResolvedValue(mockChantiers)
mockDevisService.getAll.mockResolvedValue(mockDevis)
mockFactureService.getAll.mockResolvedValue(mockFactures)
const { result } = renderHook(() => useDashboard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.devisEnAttente).toHaveLength(1)
expect(result.current.devisEnAttente[0]).toEqual({
id: '1',
numero: 'D2024-001',
client: 'Client Un',
montantTTC: 5000,
dateEmission: '2024-01-01',
dateValidite: '2024-12-31',
joursRestants: expect.any(Number),
})
})
it('devrait gérer les erreurs partielles', async () => {
mockClientService.getAll.mockResolvedValue(mockClients)
mockChantierService.getAll.mockRejectedValue(new Error('Erreur chantiers'))
mockDevisService.getAll.mockResolvedValue(mockDevis)
mockFactureService.getAll.mockResolvedValue(mockFactures)
const { result } = renderHook(() => useDashboard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.error).toBeNull()
expect(result.current.stats?.totalClients).toBe(2)
expect(result.current.stats?.totalChantiers).toBe(0)
expect(result.current.chantiersRecents).toEqual([])
})
it('devrait gérer les erreurs complètes', async () => {
mockClientService.getAll.mockRejectedValue(new Error('Erreur clients'))
mockChantierService.getAll.mockRejectedValue(new Error('Erreur chantiers'))
mockDevisService.getAll.mockRejectedValue(new Error('Erreur devis'))
mockFactureService.getAll.mockRejectedValue(new Error('Erreur factures'))
const { result } = renderHook(() => useDashboard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.error).toBeNull()
expect(result.current.stats?.totalClients).toBe(0)
expect(result.current.stats?.totalChantiers).toBe(0)
expect(result.current.chantiersRecents).toEqual([])
})
it('devrait permettre de rafraîchir les données', async () => {
mockClientService.getAll.mockResolvedValue(mockClients)
mockChantierService.getAll.mockResolvedValue(mockChantiers)
mockDevisService.getAll.mockResolvedValue(mockDevis)
mockFactureService.getAll.mockResolvedValue(mockFactures)
const { result } = renderHook(() => useDashboard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(mockClientService.getAll).toHaveBeenCalledTimes(1)
// Rafraîchir
result.current.refresh()
await waitFor(() => {
expect(mockClientService.getAll).toHaveBeenCalledTimes(2)
})
})
it('devrait gérer les données non-array', async () => {
mockClientService.getAll.mockResolvedValue(null as any)
mockChantierService.getAll.mockResolvedValue(undefined as any)
mockDevisService.getAll.mockResolvedValue({} as any)
mockFactureService.getAll.mockResolvedValue('invalid' as any)
const { result } = renderHook(() => useDashboard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.stats?.totalClients).toBe(0)
expect(result.current.stats?.totalChantiers).toBe(0)
expect(result.current.stats?.totalDevis).toBe(0)
expect(result.current.stats?.totalFactures).toBe(0)
})
it('devrait calculer correctement le chiffre d\'affaires mensuel', async () => {
const today = new Date()
const thisMonth = today.getMonth()
const thisYear = today.getFullYear()
const facturesThisMonth = [
{
id: '1',
numero: 'F2024-001',
client: mockClients[0],
statut: 'PAYEE',
dateEmission: new Date(thisYear, thisMonth, 1).toISOString(),
dateEcheance: new Date(thisYear, thisMonth, 31).toISOString(),
montantTTC: 10000,
},
{
id: '2',
numero: 'F2024-002',
client: mockClients[1],
statut: 'PAYEE',
dateEmission: new Date(thisYear, thisMonth - 1, 1).toISOString(), // Mois précédent
dateEcheance: new Date(thisYear, thisMonth - 1, 31).toISOString(),
montantTTC: 5000,
},
]
mockClientService.getAll.mockResolvedValue(mockClients)
mockChantierService.getAll.mockResolvedValue([])
mockDevisService.getAll.mockResolvedValue([])
mockFactureService.getAll.mockResolvedValue(facturesThisMonth)
const { result } = renderHook(() => useDashboard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.stats?.chiffreAffairesMois).toBe(10000)
})
it('devrait limiter les chantiers récents à 5', async () => {
const manyChantiers = Array.from({ length: 10 }, (_, i) => ({
id: `${i + 1}`,
nom: `Chantier ${i + 1}`,
client: mockClients[0],
statut: 'EN_COURS',
dateDebut: new Date(2024, 0, i + 1).toISOString(),
montantPrevu: 10000,
}))
mockClientService.getAll.mockResolvedValue(mockClients)
mockChantierService.getAll.mockResolvedValue(manyChantiers)
mockDevisService.getAll.mockResolvedValue([])
mockFactureService.getAll.mockResolvedValue([])
const { result } = renderHook(() => useDashboard())
await waitFor(() => {
expect(result.current.loading).toBe(false)
})
expect(result.current.chantiersRecents).toHaveLength(5)
})
it('devrait fournir un alias isLoading', async () => {
mockClientService.getAll.mockResolvedValue(mockClients)
mockChantierService.getAll.mockResolvedValue(mockChantiers)
mockDevisService.getAll.mockResolvedValue(mockDevis)
mockFactureService.getAll.mockResolvedValue(mockFactures)
const { result } = renderHook(() => useDashboard())
expect(result.current.isLoading).toBe(result.current.loading)
})
})

173
hooks/useApiCall.tsx Normal file
View File

@@ -0,0 +1,173 @@
'use client';
import { useState, useCallback, useRef } from 'react';
import { Toast } from 'primereact/toast';
import { apiService } from '../services/api';
interface UseApiCallOptions {
onSuccess?: (data: any) => void;
onError?: (error: any) => void;
showSuccessToast?: boolean;
showErrorToast?: boolean;
successMessage?: string;
retryAttempts?: number;
retryDelay?: number;
}
interface UseApiCallReturn<T> {
data: T | null;
loading: boolean;
error: any;
execute: (...args: any[]) => Promise<T | null>;
retry: () => Promise<T | null>;
reset: () => void;
toast: React.RefObject<Toast>;
}
export function useApiCall<T = any>(
apiFunction: (...args: any[]) => Promise<T>,
options: UseApiCallOptions = {}
): UseApiCallReturn<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<any>(null);
const toast = useRef<Toast>(null);
const lastArgsRef = useRef<any[]>([]);
const retryCountRef = useRef(0);
const {
onSuccess,
onError,
showSuccessToast = false,
showErrorToast = true,
successMessage = 'Opération réussie',
retryAttempts = 0,
retryDelay = 1000
} = options;
const execute = useCallback(async (...args: any[]): Promise<T | null> => {
lastArgsRef.current = args;
setLoading(true);
setError(null);
retryCountRef.current = 0;
try {
const result = await apiFunction(...args);
setData(result);
// Notifier que le serveur répond (pour une détection immédiate)
// Cette ligne se déclenchera dès qu'un appel API réussit
if (showSuccessToast && toast.current) {
toast.current.show({
severity: 'success',
summary: 'Succès',
detail: successMessage,
life: 3000
});
}
if (onSuccess) {
onSuccess(result);
}
return result;
} catch (err: any) {
setError(err);
// Déclenchement d'un health check urgent uniquement pour les erreurs critiques
if (shouldRetry(err) && retryCountRef.current === 0) {
// Health check urgent seulement au premier échec pour éviter le spam
apiService.checkServerHealth(true).catch(() => {});
}
// Gestion des erreurs réseau avec retry automatique
if (shouldRetry(err) && retryCountRef.current < retryAttempts) {
retryCountRef.current++;
if (showErrorToast && toast.current) {
toast.current.show({
severity: 'warn',
summary: 'Tentative de reconnexion',
detail: `Tentative ${retryCountRef.current}/${retryAttempts}...`,
life: 2000
});
}
// Attendre avant de retry
await new Promise(resolve => setTimeout(resolve, retryDelay));
return execute(...args);
}
// Afficher l'erreur finale
if (showErrorToast && toast.current) {
const message = getErrorMessage(err);
toast.current.show({
severity: 'error',
summary: 'Erreur',
detail: message,
life: err.statusCode === 'SERVER_UNAVAILABLE' ? 0 : 5000 // Sticky pour erreurs serveur
});
}
if (onError) {
onError(err);
}
return null;
} finally {
setLoading(false);
}
}, [apiFunction, onSuccess, onError, showSuccessToast, showErrorToast, successMessage, retryAttempts, retryDelay]);
const retry = useCallback(async (): Promise<T | null> => {
if (lastArgsRef.current.length > 0) {
return execute(...lastArgsRef.current);
}
return execute();
}, [execute]);
const reset = useCallback(() => {
setData(null);
setError(null);
setLoading(false);
retryCountRef.current = 0;
}, []);
return {
data,
loading,
error,
execute,
retry,
reset,
toast
};
}
// Fonctions utilitaires
function shouldRetry(error: any): boolean {
return error?.statusCode === 'NETWORK_ERROR' ||
error?.statusCode === 'SERVER_UNAVAILABLE' ||
error?.statusCode === 'TIMEOUT' ||
error?.code === 'ECONNABORTED' ||
error?.code === 'ERR_NETWORK';
}
function getErrorMessage(error: any): string {
if (error?.statusCode === 'SERVER_UNAVAILABLE') {
return 'Serveur indisponible. Vérifiez que le serveur backend est démarré.';
}
if (error?.statusCode === 'NETWORK_ERROR') {
return 'Erreur réseau. Vérifiez votre connexion internet.';
}
if (error?.statusCode === 'TIMEOUT') {
return 'Délai d\'attente dépassé. Le serveur met trop de temps à répondre.';
}
return error?.userMessage || error?.message || 'Une erreur inattendue s\'est produite';
}
export default useApiCall;

491
hooks/useBTPServices.ts Normal file
View File

@@ -0,0 +1,491 @@
import { useState, useEffect, useCallback } from 'react';
import {
MaterielBTPService,
MaterielBTP,
CategorieMateriel,
RechercheMaterielParams
} from '../services/materielBTPService';
import {
ZoneClimatiqueService,
ZoneClimatique,
CriteresRecherche
} from '../services/zoneClimatiqueService';
import {
CalculsTechniquesService,
ParametresCalculBriques,
ResultatCalculBriques,
ParametresCalculBetonArme,
ResultatCalculBetonArme
} from '../services/calculsTechniquesService';
import {
ExportBTPService,
FormatExport,
OptionsExport,
ResultatExport
} from '../services/exportBTPService';
/**
* Hook personnalisé pour l'utilisation des services BTP ultra-détaillés
* Système le plus ambitieux d'Afrique pour la gestion BTP
*/
export interface UseBTPServicesReturn {
// États de chargement
loading: {
materiaux: boolean;
zones: boolean;
calculs: boolean;
export: boolean;
};
// États d'erreur
errors: {
materiaux: string | null;
zones: string | null;
calculs: string | null;
export: string | null;
};
// Données
data: {
materiaux: MaterielBTP[];
zones: ZoneClimatique[];
dernierCalcul: any;
dernierExport: ResultatExport | null;
};
// Actions matériaux
materiaux: {
charger: (params?: RechercheMaterielParams) => Promise<void>;
rechercher: (criteres: RechercheMaterielParams) => Promise<MaterielBTP[]>;
obtenirParCode: (code: string) => Promise<MaterielBTP>;
obtenirParCategorie: (categorie: CategorieMateriel) => Promise<MaterielBTP[]>;
validerPourZone: (codeMateriel: string, zoneCode: string) => Promise<any>;
obtenirAlternatives: (codeMateriel: string, zoneCode?: string) => Promise<MaterielBTP[]>;
};
// Actions zones climatiques
zones: {
charger: () => Promise<void>;
rechercher: (criteres: CriteresRecherche) => Promise<ZoneClimatique[]>;
obtenirParCode: (code: string) => Promise<ZoneClimatique>;
obtenirMeilleureAdaptation: (temp: number, humidite: number, vents: number) => Promise<ZoneClimatique | null>;
obtenirRecommandations: (zoneCode: string) => Promise<any>;
simulerImpact: (zoneCode: string, parametres: any) => Promise<any>;
};
// Actions calculs techniques
calculs: {
calculerBriques: (params: ParametresCalculBriques) => Promise<ResultatCalculBriques>;
calculerBetonArme: (params: ParametresCalculBetonArme) => Promise<ResultatCalculBetonArme>;
obtenirDosagesBeton: () => Promise<any>;
estimationRapideBriques: (surface: number, typeBrique?: string) => Promise<any>;
estimationRapideBeton: (volume: number, classeBeton?: string) => Promise<any>;
genererDevis: (calculs: any, options?: any) => Promise<any>;
};
// Actions export
export: {
exporterMateriaux: (options: OptionsExport & { filtres?: RechercheMaterielParams }) => Promise<void>;
exporterZones: (options: OptionsExport) => Promise<void>;
exporterCalculs: (calculs: any[], options: OptionsExport) => Promise<void>;
genererDevis: (devisData: any, format?: FormatExport) => Promise<void>;
telechargerDernier: () => void;
};
// Utilitaires
utils: {
reinitialiser: () => void;
obtenirStatistiques: () => any;
validerParametres: (type: string, params: any) => any;
};
}
export const useBTPServices = (): UseBTPServicesReturn => {
// États de chargement
const [loading, setLoading] = useState({
materiaux: false,
zones: false,
calculs: false,
export: false
});
// États d'erreur
const [errors, setErrors] = useState({
materiaux: null as string | null,
zones: null as string | null,
calculs: null as string | null,
export: null as string | null
});
// Données
const [data, setData] = useState({
materiaux: [] as MaterielBTP[],
zones: [] as ZoneClimatique[],
dernierCalcul: null as any,
dernierExport: null as ResultatExport | null
});
// =================== FONCTIONS UTILITAIRES ===================
const setLoadingState = useCallback((key: keyof typeof loading, value: boolean) => {
setLoading(prev => ({ ...prev, [key]: value }));
}, []);
const setErrorState = useCallback((key: keyof typeof errors, error: string | null) => {
setErrors(prev => ({ ...prev, [key]: error }));
}, []);
const handleError = useCallback((key: keyof typeof errors, error: any) => {
const message = error?.message || error?.toString() || 'Une erreur est survenue';
setErrorState(key, message);
console.error(`Erreur ${key}:`, error);
}, [setErrorState]);
// =================== ACTIONS MATÉRIAUX ===================
const chargerMateriaux = useCallback(async (params?: RechercheMaterielParams) => {
setLoadingState('materiaux', true);
setErrorState('materiaux', null);
try {
const response = await MaterielBTPService.getMateriaux(params);
setData(prev => ({ ...prev, materiaux: response.materiaux }));
} catch (error) {
handleError('materiaux', error);
} finally {
setLoadingState('materiaux', false);
}
}, [setLoadingState, setErrorState, handleError]);
const rechercherMateriaux = useCallback(async (criteres: RechercheMaterielParams): Promise<MaterielBTP[]> => {
try {
return await MaterielBTPService.rechercherMateriaux(criteres);
} catch (error) {
handleError('materiaux', error);
return [];
}
}, [handleError]);
const obtenirMaterielParCode = useCallback(async (code: string): Promise<MaterielBTP> => {
try {
return await MaterielBTPService.getMaterielByCode(code);
} catch (error) {
handleError('materiaux', error);
throw error;
}
}, [handleError]);
const obtenirMateriauxParCategorie = useCallback(async (categorie: CategorieMateriel): Promise<MaterielBTP[]> => {
try {
return await MaterielBTPService.getMateriauxByCategorie(categorie);
} catch (error) {
handleError('materiaux', error);
return [];
}
}, [handleError]);
const validerMaterielPourZone = useCallback(async (codeMateriel: string, zoneCode: string) => {
try {
return await MaterielBTPService.validerMaterielPourZone(codeMateriel, zoneCode);
} catch (error) {
handleError('materiaux', error);
return { adapte: false, warnings: ['Validation impossible'], recommendations: [] };
}
}, [handleError]);
const obtenirAlternativesMateriel = useCallback(async (codeMateriel: string, zoneCode?: string): Promise<MaterielBTP[]> => {
try {
return await MaterielBTPService.getAlternativesMateriel(codeMateriel, zoneCode);
} catch (error) {
handleError('materiaux', error);
return [];
}
}, [handleError]);
// =================== ACTIONS ZONES CLIMATIQUES ===================
const chargerZones = useCallback(async () => {
setLoadingState('zones', true);
setErrorState('zones', null);
try {
const response = await ZoneClimatiqueService.getZonesClimatiques();
setData(prev => ({ ...prev, zones: response.zones }));
} catch (error) {
handleError('zones', error);
} finally {
setLoadingState('zones', false);
}
}, [setLoadingState, setErrorState, handleError]);
const rechercherZones = useCallback(async (criteres: CriteresRecherche): Promise<ZoneClimatique[]> => {
try {
return await ZoneClimatiqueService.rechercherZones(criteres);
} catch (error) {
handleError('zones', error);
return [];
}
}, [handleError]);
const obtenirZoneParCode = useCallback(async (code: string): Promise<ZoneClimatique> => {
try {
return await ZoneClimatiqueService.getZoneByCode(code);
} catch (error) {
handleError('zones', error);
throw error;
}
}, [handleError]);
const obtenirMeilleureAdaptation = useCallback(async (temp: number, humidite: number, vents: number) => {
try {
return await ZoneClimatiqueService.getMeilleureAdaptation(temp, humidite, vents);
} catch (error) {
handleError('zones', error);
return null;
}
}, [handleError]);
const obtenirRecommandationsZone = useCallback(async (zoneCode: string) => {
try {
return await ZoneClimatiqueService.getRecommandationsConstruction(zoneCode);
} catch (error) {
handleError('zones', error);
return { fondations: [], structure: [], enveloppe: [], finitions: [], equipements: [] };
}
}, [handleError]);
const simulerImpactClimatique = useCallback(async (zoneCode: string, parametres: any) => {
try {
return await ZoneClimatiqueService.simulerImpactClimatique(zoneCode, parametres);
} catch (error) {
handleError('zones', error);
return null;
}
}, [handleError]);
// =================== ACTIONS CALCULS TECHNIQUES ===================
const calculerBriquesMur = useCallback(async (params: ParametresCalculBriques): Promise<ResultatCalculBriques> => {
setLoadingState('calculs', true);
setErrorState('calculs', null);
try {
const resultat = await CalculsTechniquesService.calculerBriquesMur(params);
setData(prev => ({ ...prev, dernierCalcul: { type: 'BRIQUES', resultat, params } }));
return resultat;
} catch (error) {
handleError('calculs', error);
throw error;
} finally {
setLoadingState('calculs', false);
}
}, [setLoadingState, setErrorState, handleError]);
const calculerBetonArme = useCallback(async (params: ParametresCalculBetonArme): Promise<ResultatCalculBetonArme> => {
setLoadingState('calculs', true);
setErrorState('calculs', null);
try {
const resultat = await CalculsTechniquesService.calculerBetonArme(params);
setData(prev => ({ ...prev, dernierCalcul: { type: 'BETON', resultat, params } }));
return resultat;
} catch (error) {
handleError('calculs', error);
throw error;
} finally {
setLoadingState('calculs', false);
}
}, [setLoadingState, setErrorState, handleError]);
const obtenirDosagesBeton = useCallback(async () => {
try {
return await CalculsTechniquesService.getDosagesBeton();
} catch (error) {
handleError('calculs', error);
return { dosages: {}, notes: [] };
}
}, [handleError]);
const estimationRapideBriques = useCallback(async (surface: number, typeBrique?: string) => {
try {
return await CalculsTechniquesService.estimationRapideBriques(surface, typeBrique);
} catch (error) {
handleError('calculs', error);
return { estimationBasse: 0, estimationHaute: 0, estimationMoyenne: 0, baseCalcul: '' };
}
}, [handleError]);
const estimationRapideBeton = useCallback(async (volume: number, classeBeton?: string) => {
try {
return await CalculsTechniquesService.estimationRapideBeton(volume, classeBeton);
} catch (error) {
handleError('calculs', error);
return { cimentSacs: 0, sableM3: 0, graviersM3: 0, eauLitres: 0, coutEstime: 0 };
}
}, [handleError]);
const genererDevis = useCallback(async (calculs: any, options?: any) => {
try {
return await CalculsTechniquesService.genererDevis(calculs, options);
} catch (error) {
handleError('calculs', error);
return { lignesDevis: [], totalHT: 0, totalTTC: 0, delaiExecution: 0 };
}
}, [handleError]);
// =================== ACTIONS EXPORT ===================
const exporterMateriaux = useCallback(async (options: OptionsExport & { filtres?: RechercheMaterielParams }) => {
setLoadingState('export', true);
setErrorState('export', null);
try {
const resultat = await ExportBTPService.exporterMateriaux(options);
setData(prev => ({ ...prev, dernierExport: resultat }));
ExportBTPService.telechargerFichier(resultat);
} catch (error) {
handleError('export', error);
} finally {
setLoadingState('export', false);
}
}, [setLoadingState, setErrorState, handleError]);
const exporterZones = useCallback(async (options: OptionsExport) => {
setLoadingState('export', true);
setErrorState('export', null);
try {
const resultat = await ExportBTPService.exporterZonesClimatiques(options);
setData(prev => ({ ...prev, dernierExport: resultat }));
ExportBTPService.telechargerFichier(resultat);
} catch (error) {
handleError('export', error);
} finally {
setLoadingState('export', false);
}
}, [setLoadingState, setErrorState, handleError]);
const exporterCalculs = useCallback(async (calculs: any[], options: OptionsExport) => {
setLoadingState('export', true);
setErrorState('export', null);
try {
const resultat = await ExportBTPService.exporterCalculs(calculs, options);
setData(prev => ({ ...prev, dernierExport: resultat }));
ExportBTPService.telechargerFichier(resultat);
} catch (error) {
handleError('export', error);
} finally {
setLoadingState('export', false);
}
}, [setLoadingState, setErrorState, handleError]);
const genererDevisBTP = useCallback(async (devisData: any, format: FormatExport = 'PDF') => {
setLoadingState('export', true);
setErrorState('export', null);
try {
const resultat = await ExportBTPService.genererDevisBTP(devisData, format);
setData(prev => ({ ...prev, dernierExport: resultat }));
ExportBTPService.telechargerFichier(resultat);
} catch (error) {
handleError('export', error);
} finally {
setLoadingState('export', false);
}
}, [setLoadingState, setErrorState, handleError]);
const telechargerDernierExport = useCallback(() => {
if (data.dernierExport) {
ExportBTPService.telechargerFichier(data.dernierExport);
}
}, [data.dernierExport]);
// =================== UTILITAIRES ===================
const reinitialiser = useCallback(() => {
setData({
materiaux: [],
zones: [],
dernierCalcul: null,
dernierExport: null
});
setErrors({
materiaux: null,
zones: null,
calculs: null,
export: null
});
}, []);
const obtenirStatistiques = useCallback(() => {
return {
nbMateriaux: data.materiaux.length,
nbZones: data.zones.length,
categoriesMateriaux: [...new Set(data.materiaux.map(m => m.categorie))],
derniereActivite: data.dernierCalcul ? new Date(data.dernierCalcul.date || Date.now()) : null,
tailleDernierExport: data.dernierExport?.size || 0
};
}, [data]);
const validerParametres = useCallback((type: string, params: any) => {
return CalculsTechniquesService.validerParametres(type as any, params);
}, []);
// =================== CHARGEMENT INITIAL ===================
useEffect(() => {
// Chargement automatique des données de base
chargerZones();
}, [chargerZones]);
// =================== RETURN ===================
return {
loading,
errors,
data,
materiaux: {
charger: chargerMateriaux,
rechercher: rechercherMateriaux,
obtenirParCode: obtenirMaterielParCode,
obtenirParCategorie: obtenirMateriauxParCategorie,
validerPourZone: validerMaterielPourZone,
obtenirAlternatives: obtenirAlternativesMateriel
},
zones: {
charger: chargerZones,
rechercher: rechercherZones,
obtenirParCode: obtenirZoneParCode,
obtenirMeilleureAdaptation,
obtenirRecommandations: obtenirRecommandationsZone,
simulerImpact: simulerImpactClimatique
},
calculs: {
calculerBriques: calculerBriquesMur,
calculerBetonArme,
obtenirDosagesBeton,
estimationRapideBriques,
estimationRapideBeton,
genererDevis
},
export: {
exporterMateriaux,
exporterZones,
exporterCalculs,
genererDevis: genererDevisBTP,
telechargerDernier: telechargerDernierExport
},
utils: {
reinitialiser,
obtenirStatistiques,
validerParametres
}
};
};

299
hooks/useChantierActions.ts Normal file
View File

@@ -0,0 +1,299 @@
/**
* Hook personnalisé pour gérer les actions sur les chantiers
* Centralise la logique métier et facilite la réutilisation
*/
import { useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { Toast } from 'primereact/toast';
import { confirmDialog } from 'primereact/confirmdialog';
import { chantierActionsService } from '../services/chantierActionsService';
import { ChantierActif } from './useDashboard';
interface UseChantierActionsProps {
toast?: React.RefObject<Toast>;
onRefresh?: () => void;
}
interface UseChantierActionsReturn {
handleQuickView: (chantier: ChantierActif) => void;
handleViewStats: (chantier: ChantierActif) => Promise<void>;
handleGenerateReport: (chantier: ChantierActif) => Promise<void>;
handleExport: (chantier: ChantierActif, format: 'pdf' | 'excel') => Promise<void>;
handleToggleSuspend: (chantier: ChantierActif) => void;
handleClose: (chantier: ChantierActif) => void;
handleArchive: (chantier: ChantierActif) => void;
handleMenuAction: (action: string, chantier: ChantierActif) => Promise<void>;
// Nouvelles actions prioritaires BTP
handleSuspendChantier: (chantier: ChantierActif) => void;
handleCloseChantier: (chantier: ChantierActif) => void;
handleNotifyClient: (chantier: ChantierActif) => Promise<void>;
handleGenerateInvoice: (chantier: ChantierActif) => Promise<void>;
handleCreateAmendment: (chantier: ChantierActif) => Promise<void>;
}
export const useChantierActions = ({
toast,
onRefresh
}: UseChantierActionsProps = {}): UseChantierActionsReturn => {
const router = useRouter();
const showToast = useCallback((severity: 'success' | 'info' | 'warn' | 'error', summary: string, detail: string, life = 3000) => {
toast?.current?.show({ severity, summary, detail, life });
}, [toast]);
const handleQuickView = useCallback(async (chantier: ChantierActif) => {
try {
// Pour la vue rapide, on utilise les données déjà disponibles
// Si nécessaire, on peut récupérer des détails depuis l'endpoint /chantiers/{id}
// Mais pour l'instant, on utilise ce qu'on a déjà
console.log('Vue rapide du chantier:', chantier);
// Calculer des stats basiques depuis les données existantes
const joursEcoules = chantier.dateDebut ?
Math.floor((new Date().getTime() - new Date(chantier.dateDebut).getTime()) / (1000 * 60 * 60 * 24)) : 0;
const joursRestants = chantier.dateFinPrevue ?
Math.floor((new Date(chantier.dateFinPrevue).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)) : 0;
const tauxDepense = chantier.budget > 0 ?
Math.round((chantier.coutReel / chantier.budget) * 100) : 0;
// Les données sont déjà passées au composant Dialog, pas besoin d'appel API
showToast('info', 'Vue rapide', `Affichage des détails de ${chantier.nom}`);
} catch (error) {
console.error('Erreur lors de l\'affichage de la vue rapide:', error);
showToast('warn', 'Avertissement', 'Détails limités disponibles');
}
}, [showToast]);
const handleViewStats = useCallback(async (chantier: ChantierActif) => {
showToast('info', 'Chargement', 'Chargement des statistiques...', 2000);
try {
const stats = await chantierActionsService.getChantierStats(chantier.id);
router.push(`/chantiers/${chantier.id}#stats`);
} catch (error) {
showToast('error', 'Erreur', 'Impossible de charger les statistiques');
}
}, [router, showToast]);
const handleGenerateReport = useCallback(async (chantier: ChantierActif) => {
showToast('info', 'Génération en cours', `Génération du rapport pour ${chantier.nom}...`);
const result = await chantierActionsService.generateReport(chantier.id);
if (result.success) {
showToast('success', 'Rapport généré', 'Le rapport a été téléchargé avec succès');
} else {
showToast('error', 'Erreur', result.message, 5000);
}
}, [showToast]);
const handleExport = useCallback(async (chantier: ChantierActif, format: 'pdf' | 'excel') => {
showToast('info', 'Export en cours', `Export ${format.toUpperCase()} pour ${chantier.nom}...`);
const result = await chantierActionsService.exportChantier(chantier.id, format);
if (result.success) {
showToast('success', 'Export réussi', `Le fichier ${format.toUpperCase()} a été téléchargé`);
} else {
showToast('error', 'Erreur d\'export', result.message, 5000);
}
}, [showToast]);
const handleToggleSuspend = useCallback((chantier: ChantierActif) => {
const isSuspended = chantier.statut === 'SUSPENDU';
const action = isSuspended ? 'reprendre' : 'suspendre';
confirmDialog({
message: `Êtes-vous sûr de vouloir ${action} le chantier "${chantier.nom}" ?`,
header: 'Confirmation',
icon: 'pi pi-exclamation-triangle',
acceptLabel: 'Oui',
rejectLabel: 'Non',
accept: async () => {
const result = await chantierActionsService.toggleSuspend(chantier.id, !isSuspended);
if (result.success) {
showToast('success', 'Succès', result.message);
onRefresh?.();
} else {
showToast('error', 'Erreur', result.message, 5000);
}
}
});
}, [showToast, onRefresh]);
const handleClose = useCallback((chantier: ChantierActif) => {
confirmDialog({
message: `Êtes-vous sûr de vouloir clôturer le chantier "${chantier.nom}" ?\nCette action est irréversible.`,
header: 'Clôturer le chantier',
icon: 'pi pi-info-circle',
acceptClassName: 'p-button-success',
acceptLabel: 'Clôturer',
rejectLabel: 'Annuler',
accept: async () => {
const result = await chantierActionsService.closeChantier(chantier.id);
if (result.success) {
showToast('success', 'Chantier clôturé', result.message);
onRefresh?.();
} else {
showToast('error', 'Erreur', result.message, 5000);
}
}
});
}, [showToast, onRefresh]);
const handleArchive = useCallback((chantier: ChantierActif) => {
confirmDialog({
message: `Voulez-vous archiver le chantier "${chantier.nom}" ?\nIl sera déplacé dans les archives.`,
header: 'Archiver le chantier',
icon: 'pi pi-inbox',
acceptLabel: 'Archiver',
rejectLabel: 'Annuler',
accept: async () => {
const result = await chantierActionsService.archiveChantier(chantier.id);
if (result.success) {
showToast('info', 'Chantier archivé', result.message);
onRefresh?.();
} else {
showToast('error', 'Erreur', result.message, 5000);
}
}
});
}, [showToast, onRefresh]);
const handleMenuAction = useCallback(async (action: string, chantier: ChantierActif) => {
switch (action) {
case 'details':
router.push(`/chantiers/${chantier.id}`);
break;
case 'documents':
router.push(`/documents?chantier=${chantier.id}`);
break;
case 'photos':
router.push(`/photos/par-chantier?id=${chantier.id}`);
break;
case 'team':
router.push(`/equipes?chantier=${chantier.id}`);
break;
case 'equipment':
router.push(`/materiels?chantier=${chantier.id}`);
break;
case 'report':
await handleGenerateReport(chantier);
break;
case 'export-pdf':
await handleExport(chantier, 'pdf');
break;
case 'export-excel':
await handleExport(chantier, 'excel');
break;
case 'toggle-suspend':
handleToggleSuspend(chantier);
break;
case 'close':
handleClose(chantier);
break;
case 'archive':
handleArchive(chantier);
break;
default:
console.warn('Action inconnue:', action);
}
}, [router, handleGenerateReport, handleExport, handleToggleSuspend, handleClose, handleArchive]);
// Actions prioritaires BTP
const handleSuspendChantier = useCallback((chantier: ChantierActif) => {
confirmDialog({
message: `Suspendre temporairement le chantier "${chantier.nom}" ?\nLes équipes seront notifiées.`,
header: 'Suspendre le chantier',
icon: 'pi pi-pause-circle',
acceptClassName: 'p-button-warning p-button-text p-button-rounded',
acceptLabel: 'Suspendre',
rejectLabel: 'Annuler',
accept: async () => {
try {
const result = await chantierActionsService.suspendChantier(chantier.id);
showToast('warn', 'Chantier suspendu', `${chantier.nom} a été suspendu temporairement`);
onRefresh?.();
} catch (error) {
showToast('error', 'Erreur', 'Impossible de suspendre le chantier');
}
}
});
}, [showToast, onRefresh]);
const handleCloseChantier = useCallback((chantier: ChantierActif) => {
confirmDialog({
message: `Clôturer définitivement le chantier "${chantier.nom}" ?\nUn rapport final sera généré.`,
header: 'Clôturer le chantier',
icon: 'pi pi-check-circle',
acceptClassName: 'p-button-success p-button-text p-button-rounded',
acceptLabel: 'Clôturer',
rejectLabel: 'Annuler',
accept: async () => {
try {
const result = await chantierActionsService.closeChantierDefinitively(chantier.id);
showToast('success', 'Chantier clôturé', `${chantier.nom} a été clôturé avec succès`);
onRefresh?.();
} catch (error) {
showToast('error', 'Erreur', 'Impossible de clôturer le chantier');
}
}
});
}, [showToast, onRefresh]);
const handleNotifyClient = useCallback(async (chantier: ChantierActif) => {
try {
showToast('info', 'Envoi en cours...', 'Préparation de la notification client');
const result = await chantierActionsService.notifyClient(chantier.id);
showToast('success', 'Notification envoyée', `Le client a été informé de l'avancement du chantier ${chantier.nom}`);
} catch (error) {
showToast('error', 'Erreur', 'Impossible d\'envoyer la notification au client');
}
}, [showToast]);
const handleGenerateInvoice = useCallback(async (chantier: ChantierActif) => {
try {
showToast('info', 'Génération...', 'Préparation de la facture intermédiaire');
const result = await chantierActionsService.generateIntermediateInvoice(chantier.id);
showToast('success', 'Facture générée', `Facture intermédiaire créée pour ${chantier.nom}`);
// Ouvrir la facture dans un nouvel onglet si URL fournie
if (result.pdfUrl) {
window.open(result.pdfUrl, '_blank');
}
} catch (error) {
showToast('error', 'Erreur', 'Impossible de générer la facture intermédiaire');
}
}, [showToast]);
const handleCreateAmendment = useCallback(async (chantier: ChantierActif) => {
try {
showToast('info', 'Création avenant...', 'Préparation du document d\'avenant');
const result = await chantierActionsService.createAmendment(chantier.id);
showToast('success', 'Avenant créé', `Avenant budgétaire créé pour ${chantier.nom}`);
// Rediriger vers la page d'édition de l'avenant
router.push(`/chantiers/${chantier.id}/avenant/${result.amendmentId}`);
} catch (error) {
showToast('error', 'Erreur', 'Impossible de créer l\'avenant');
}
}, [showToast, router]);
return {
handleQuickView,
handleViewStats,
handleGenerateReport,
handleExport,
handleToggleSuspend,
handleClose,
handleArchive,
handleMenuAction,
// Nouvelles actions prioritaires
handleSuspendChantier,
handleCloseChantier,
handleNotifyClient,
handleGenerateInvoice,
handleCreateAmendment
};
};

80
hooks/useChantiers.ts Normal file
View File

@@ -0,0 +1,80 @@
import { useState, useEffect } from 'react';
import { apiClient } from '../services/api-client';
import { Chantier } from '../types/btp';
export const useChantiers = () => {
const [chantiers, setChantiers] = useState<Chantier[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchChantiers = async () => {
try {
setLoading(true);
const response = await apiClient.get('/chantiers');
setChantiers(response.data);
setError(null);
} catch (err) {
console.error('Erreur lors du chargement des chantiers:', err);
setError('Erreur lors du chargement des chantiers');
} finally {
setLoading(false);
}
};
const getChantierById = async (id: string) => {
try {
const response = await apiClient.get(`/chantiers/${id}`);
return response.data;
} catch (err) {
console.error('Erreur lors du chargement du chantier:', err);
throw err;
}
};
const createChantier = async (chantier: Partial<Chantier>) => {
try {
const response = await apiClient.post('/chantiers', chantier);
await fetchChantiers();
return response.data;
} catch (err) {
console.error('Erreur lors de la création du chantier:', err);
throw err;
}
};
const updateChantier = async (id: string, chantier: Partial<Chantier>) => {
try {
const response = await apiClient.put(`/chantiers/${id}`, chantier);
await fetchChantiers();
return response.data;
} catch (err) {
console.error('Erreur lors de la mise à jour du chantier:', err);
throw err;
}
};
const deleteChantier = async (id: string) => {
try {
await apiClient.delete(`/chantiers/${id}`);
await fetchChantiers();
} catch (err) {
console.error('Erreur lors de la suppression du chantier:', err);
throw err;
}
};
useEffect(() => {
fetchChantiers();
}, []);
return {
chantiers,
loading,
error,
refresh: fetchChantiers,
getChantierById,
createChantier,
updateChantier,
deleteChantier
};
};

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;

173
hooks/usePhases.ts Normal file
View File

@@ -0,0 +1,173 @@
import { useState, useEffect } from 'react';
import { apiClient } from '../services/api-client';
import { PhaseChantier, StatutPhaseChantier } from '../types/phases';
export const usePhases = () => {
const [phases, setPhases] = useState<PhaseChantier[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchPhases = async () => {
try {
setLoading(true);
const response = await apiClient.get('/phases');
setPhases(response.data);
setError(null);
} catch (err) {
console.error('Erreur lors du chargement des phases:', err);
setError('Erreur lors du chargement des phases');
} finally {
setLoading(false);
}
};
const getPhasesByChantier = async (chantierId: string) => {
try {
const response = await apiClient.get(`/phases/chantier/${chantierId}`);
return response.data;
} catch (err) {
console.error('Erreur lors du chargement des phases du chantier:', err);
throw err;
}
};
const getPhasesByStatut = async (statut: StatutPhaseChantier) => {
try {
const response = await apiClient.get(`/phases/statut/${statut}`);
return response.data;
} catch (err) {
console.error('Erreur lors du chargement des phases par statut:', err);
throw err;
}
};
const getPhasesEnRetard = async () => {
try {
const response = await apiClient.get('/phases/en-retard');
return response.data;
} catch (err) {
console.error('Erreur lors du chargement des phases en retard:', err);
throw err;
}
};
const getPhasesCritiques = async () => {
try {
const response = await apiClient.get('/phases/critiques');
return response.data;
} catch (err) {
console.error('Erreur lors du chargement des phases critiques:', err);
throw err;
}
};
const createPhase = async (phase: Partial<PhaseChantier>) => {
try {
const response = await apiClient.post('/phases', phase);
await fetchPhases();
return response.data;
} catch (err) {
console.error('Erreur lors de la création de la phase:', err);
throw err;
}
};
const updatePhase = async (id: string, phase: Partial<PhaseChantier>) => {
try {
const response = await apiClient.put(`/phases/${id}`, phase);
await fetchPhases();
return response.data;
} catch (err) {
console.error('Erreur lors de la mise à jour de la phase:', err);
throw err;
}
};
const demarrerPhase = async (id: string) => {
try {
const response = await apiClient.post(`/phases/${id}/demarrer`);
await fetchPhases();
return response.data;
} catch (err) {
console.error('Erreur lors du démarrage de la phase:', err);
throw err;
}
};
const terminerPhase = async (id: string) => {
try {
const response = await apiClient.post(`/phases/${id}/terminer`);
await fetchPhases();
return response.data;
} catch (err) {
console.error('Erreur lors de la terminaison de la phase:', err);
throw err;
}
};
const suspendrePhase = async (id: string, motif: string) => {
try {
const response = await apiClient.post(`/phases/${id}/suspendre`, { motif });
await fetchPhases();
return response.data;
} catch (err) {
console.error('Erreur lors de la suspension de la phase:', err);
throw err;
}
};
const reprendrePhase = async (id: string) => {
try {
const response = await apiClient.post(`/phases/${id}/reprendre`);
await fetchPhases();
return response.data;
} catch (err) {
console.error('Erreur lors de la reprise de la phase:', err);
throw err;
}
};
const updateAvancement = async (id: string, pourcentage: number) => {
try {
const response = await apiClient.post(`/phases/${id}/avancement`, { pourcentage });
await fetchPhases();
return response.data;
} catch (err) {
console.error('Erreur lors de la mise à jour de l\'avancement:', err);
throw err;
}
};
const deletePhase = async (id: string) => {
try {
await apiClient.delete(`/phases/${id}`);
await fetchPhases();
} catch (err) {
console.error('Erreur lors de la suppression de la phase:', err);
throw err;
}
};
useEffect(() => {
fetchPhases();
}, []);
return {
phases,
loading,
error,
refresh: fetchPhases,
getPhasesByChantier,
getPhasesByStatut,
getPhasesEnRetard,
getPhasesCritiques,
createPhase,
updatePhase,
demarrerPhase,
terminerPhase,
suspendrePhase,
reprendrePhase,
updateAvancement,
deletePhase
};
};

298
hooks/usePhasesManager.ts Normal file
View File

@@ -0,0 +1,298 @@
import { useState, useCallback, useRef } from 'react';
import { PhaseChantier, PhaseFormData } from '../types/btp-extended';
import phaseService from '../services/phaseService';
import { Toast } from 'primereact/toast';
interface UsePhasesManagerProps {
chantierId?: string;
onPhasesChange?: () => void;
}
export const usePhasesManager = ({ chantierId, onPhasesChange }: UsePhasesManagerProps = {}) => {
const [phases, setPhases] = useState<PhaseChantier[]>([]);
const [loading, setLoading] = useState(false);
const [selectedPhase, setSelectedPhase] = useState<PhaseChantier | null>(null);
const toast = useRef<Toast>(null);
// Chargement des phases
const loadPhases = useCallback(async () => {
setLoading(true);
try {
let phasesData: PhaseChantier[];
if (chantierId) {
console.log('Chargement des phases pour le chantier:', chantierId);
phasesData = await phaseService.getByChantier(chantierId);
} else {
console.log('Chargement de toutes les phases');
phasesData = await phaseService.getAll();
}
console.log('Phases récupérées:', phasesData);
setPhases(phasesData);
if (onPhasesChange) {
onPhasesChange();
}
} catch (error) {
console.error('Erreur lors du chargement des phases:', error);
showError('Impossible de charger les phases');
} finally {
setLoading(false);
}
}, [chantierId, onPhasesChange]);
// Création d'une phase
const createPhase = useCallback(async (phaseData: PhaseFormData) => {
try {
setLoading(true);
const newPhase = await phaseService.create({
...phaseData,
chantierId: chantierId || phaseData.chantierId,
statut: 'PLANIFIEE'
});
await loadPhases();
showSuccess(`La phase "${phaseData.nom}" a été créée`);
return newPhase;
} catch (error) {
console.error('Erreur lors de la création de la phase:', error);
showError('Impossible de créer la phase');
throw error;
} finally {
setLoading(false);
}
}, [chantierId, loadPhases]);
// Mise à jour d'une phase
const updatePhase = useCallback(async (phaseId: string, phaseData: Partial<PhaseChantier>) => {
try {
setLoading(true);
const updatedPhase = await phaseService.update(phaseId, phaseData);
await loadPhases();
showSuccess(`La phase a été mise à jour`);
return updatedPhase;
} catch (error) {
console.error('Erreur lors de la mise à jour de la phase:', error);
showError('Impossible de mettre à jour la phase');
throw error;
} finally {
setLoading(false);
}
}, [loadPhases]);
// Suppression d'une phase
const deletePhase = useCallback(async (phaseId: string) => {
try {
setLoading(true);
await phaseService.delete(phaseId);
await loadPhases();
showSuccess('La phase a été supprimée');
} catch (error) {
console.error('Erreur lors de la suppression de la phase:', error);
showError('Impossible de supprimer la phase');
throw error;
} finally {
setLoading(false);
}
}, [loadPhases]);
// Démarrage d'une phase
const startPhase = useCallback(async (phaseId: string) => {
try {
setLoading(true);
await phaseService.demarrer(phaseId);
await loadPhases();
showSuccess('La phase a été démarrée');
} catch (error) {
console.error('Erreur lors du démarrage de la phase:', error);
showError('Impossible de démarrer la phase');
throw error;
} finally {
setLoading(false);
}
}, [loadPhases]);
// Mise à jour de l'avancement
const updateProgress = useCallback(async (phaseId: string, pourcentage: number) => {
try {
setLoading(true);
await phaseService.updateAvancement(phaseId, pourcentage);
await loadPhases();
showSuccess('L\'avancement a été mis à jour');
} catch (error) {
console.error('Erreur lors de la mise à jour de l\'avancement:', error);
showError('Impossible de mettre à jour l\'avancement');
throw error;
} finally {
setLoading(false);
}
}, [loadPhases]);
// Terminer une phase
const completePhase = useCallback(async (phaseId: string) => {
try {
setLoading(true);
await phaseService.terminer(phaseId);
await loadPhases();
showSuccess('La phase a été terminée');
} catch (error) {
console.error('Erreur lors de la terminaison de la phase:', error);
showError('Impossible de terminer la phase');
throw error;
} finally {
setLoading(false);
}
}, [loadPhases]);
// Suspendre une phase
const suspendPhase = useCallback(async (phaseId: string, motif: string) => {
try {
setLoading(true);
await phaseService.suspendre(phaseId, motif);
await loadPhases();
showSuccess('La phase a été suspendue');
} catch (error) {
console.error('Erreur lors de la suspension de la phase:', error);
showError('Impossible de suspendre la phase');
throw error;
} finally {
setLoading(false);
}
}, [loadPhases]);
// Reprendre une phase
const resumePhase = useCallback(async (phaseId: string) => {
try {
setLoading(true);
await phaseService.reprendre(phaseId);
await loadPhases();
showSuccess('La phase a été reprise');
} catch (error) {
console.error('Erreur lors de la reprise de la phase:', error);
showError('Impossible de reprendre la phase');
throw error;
} finally {
setLoading(false);
}
}, [loadPhases]);
// Créer une sous-phase
const createSubPhase = useCallback(async (parentPhaseId: string, subPhaseData: PhaseFormData) => {
try {
setLoading(true);
const parentPhase = phases.find(p => p.id === parentPhaseId);
if (!parentPhase) {
throw new Error('Phase parente non trouvée');
}
const newSubPhase = await phaseService.create({
...subPhaseData,
chantierId: parentPhase.chantierId || chantierId,
phaseParent: parentPhaseId,
statut: 'PLANIFIEE'
});
await loadPhases();
showSuccess(`La sous-phase "${subPhaseData.nom}" a été créée`);
return newSubPhase;
} catch (error) {
console.error('Erreur lors de la création de la sous-phase:', error);
showError('Impossible de créer la sous-phase');
throw error;
} finally {
setLoading(false);
}
}, [phases, chantierId, loadPhases]);
// Statistiques
const getStatistics = useCallback(() => {
const stats = {
total: phases.length,
planifiees: phases.filter(p => p.statut === 'PLANIFIEE').length,
enCours: phases.filter(p => p.statut === 'EN_COURS').length,
terminees: phases.filter(p => p.statut === 'TERMINEE').length,
enRetard: phases.filter(p => {
if (!p.dateFinPrevue || p.statut === 'TERMINEE') return false;
return new Date(p.dateFinPrevue) < new Date();
}).length,
critiques: phases.filter(p => p.priorite === 'CRITIQUE' || p.priorite === 'HAUTE').length,
avancementMoyen: phases.length > 0
? Math.round(phases.reduce((sum, p) => sum + (p.pourcentageAvancement || 0), 0) / phases.length)
: 0
};
return stats;
}, [phases]);
// Helpers pour les notifications
const showSuccess = (message: string) => {
if (toast.current) {
toast.current.show({
severity: 'success',
summary: 'Succès',
detail: message,
life: 3000
});
}
};
const showError = (message: string) => {
if (toast.current) {
toast.current.show({
severity: 'error',
summary: 'Erreur',
detail: message,
life: 5000
});
}
};
// Attacher la ref toast
const setToastRef = (ref: any) => {
toast.current = ref;
};
return {
// État
phases,
loading,
selectedPhase,
setSelectedPhase,
// Actions CRUD
loadPhases,
createPhase,
updatePhase,
deletePhase,
createSubPhase,
// Actions métier
startPhase,
completePhase,
suspendPhase,
resumePhase,
updateProgress,
// Statistiques
getStatistics,
// Helpers
setToastRef,
showSuccess,
showError
};
};
export default usePhasesManager;

View File

@@ -0,0 +1,29 @@
/**
* Hook pour initialiser le service de monitoring serveur SSE
* À utiliser dans le layout principal pour démarrer le service une seule fois
*/
import { useEffect } from 'react';
import { getServerStatusService } from '../services/serverStatusService';
export const useServerStatusInit = () => {
useEffect(() => {
// Vérifier si on est côté client
if (typeof window === 'undefined') return;
console.log('🌐 Initialisation globale du monitoring serveur SSE');
// Obtenir l'instance du service côté client
const service = getServerStatusService();
if (!service) return;
// Démarrer le service SSE
service.start();
// Nettoyer à la fermeture de l'application
return () => {
console.log('🛑 Arrêt du monitoring serveur SSE');
service.stop();
};
}, []); // Dépendances vides = exécution une seule fois
};

224
hooks/useStocks.ts Normal file
View File

@@ -0,0 +1,224 @@
import { useState, useEffect } from 'react';
import { apiClient } from '../services/api-client';
import { Stock, CategorieStock, StatutStock, MouvementStock } from '../types/stocks';
export const useStocks = () => {
const [stocks, setStocks] = useState<Stock[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchStocks = async () => {
try {
setLoading(true);
const response = await apiClient.get('/stocks');
setStocks(response.data);
setError(null);
} catch (err) {
console.error('Erreur lors du chargement des stocks:', err);
setError('Erreur lors du chargement des stocks');
} finally {
setLoading(false);
}
};
const getStockByReference = async (reference: string) => {
try {
const response = await apiClient.get(`/stocks/reference/${reference}`);
return response.data;
} catch (err) {
console.error('Erreur lors du chargement du stock:', err);
throw err;
}
};
const searchByDesignation = async (designation: string) => {
try {
const response = await apiClient.get('/stocks/search', {
params: { designation }
});
return response.data;
} catch (err) {
console.error('Erreur lors de la recherche:', err);
throw err;
}
};
const getStocksByCategorie = async (categorie: CategorieStock) => {
try {
const response = await apiClient.get(`/stocks/categorie/${categorie}`);
return response.data;
} catch (err) {
console.error('Erreur lors du chargement des stocks par catégorie:', err);
throw err;
}
};
const getStocksEnRupture = async () => {
try {
const response = await apiClient.get('/stocks/rupture');
return response.data;
} catch (err) {
console.error('Erreur lors du chargement des stocks en rupture:', err);
throw err;
}
};
const getStocksSousMinimum = async () => {
try {
const response = await apiClient.get('/stocks/sous-minimum');
return response.data;
} catch (err) {
console.error('Erreur lors du chargement des stocks sous minimum:', err);
throw err;
}
};
const createStock = async (stock: Partial<Stock>) => {
try {
const response = await apiClient.post('/stocks', stock);
await fetchStocks();
return response.data;
} catch (err) {
console.error('Erreur lors de la création du stock:', err);
throw err;
}
};
const updateStock = async (id: string, stock: Partial<Stock>) => {
try {
const response = await apiClient.put(`/stocks/${id}`, stock);
await fetchStocks();
return response.data;
} catch (err) {
console.error('Erreur lors de la mise à jour du stock:', err);
throw err;
}
};
const entreeStock = async (mouvement: MouvementStock) => {
try {
const response = await apiClient.post('/stocks/entree', mouvement);
await fetchStocks();
return response.data;
} catch (err) {
console.error('Erreur lors de l\'entrée de stock:', err);
throw err;
}
};
const sortieStock = async (mouvement: MouvementStock) => {
try {
const response = await apiClient.post('/stocks/sortie', mouvement);
await fetchStocks();
return response.data;
} catch (err) {
console.error('Erreur lors de la sortie de stock:', err);
throw err;
}
};
const reserverStock = async (stockId: string, quantite: number, chantierId?: string) => {
try {
const response = await apiClient.post('/stocks/reserver', {
stockId,
quantite,
chantierId
});
await fetchStocks();
return response.data;
} catch (err) {
console.error('Erreur lors de la réservation:', err);
throw err;
}
};
const libererReservation = async (stockId: string, quantite: number) => {
try {
const response = await apiClient.post('/stocks/liberer-reservation', {
stockId,
quantite
});
await fetchStocks();
return response.data;
} catch (err) {
console.error('Erreur lors de la libération de réservation:', err);
throw err;
}
};
const inventaire = async (stockId: string, quantiteReelle: number, observations?: string) => {
try {
const response = await apiClient.post('/stocks/inventaire', {
stockId,
quantiteReelle,
observations
});
await fetchStocks();
return response.data;
} catch (err) {
console.error('Erreur lors de l\'inventaire:', err);
throw err;
}
};
const changerStatut = async (stockId: string, nouveauStatut: StatutStock, motif?: string) => {
try {
const response = await apiClient.post('/stocks/statut', {
stockId,
nouveauStatut,
motif
});
await fetchStocks();
return response.data;
} catch (err) {
console.error('Erreur lors du changement de statut:', err);
throw err;
}
};
const getStatistiques = async () => {
try {
const response = await apiClient.get('/stocks/statistiques');
return response.data;
} catch (err) {
console.error('Erreur lors du chargement des statistiques:', err);
throw err;
}
};
const deleteStock = async (id: string) => {
try {
await apiClient.delete(`/stocks/${id}`);
await fetchStocks();
} catch (err) {
console.error('Erreur lors de la suppression du stock:', err);
throw err;
}
};
useEffect(() => {
fetchStocks();
}, []);
return {
stocks,
loading,
error,
refresh: fetchStocks,
getStockByReference,
searchByDesignation,
getStocksByCategorie,
getStocksEnRupture,
getStocksSousMinimum,
createStock,
updateStock,
entreeStock,
sortieStock,
reserverStock,
libererReservation,
inventaire,
changerStatut,
getStatistiques,
deleteStock
};
};

85
hooks/useUserRoles.ts Normal file
View File

@@ -0,0 +1,85 @@
'use client';
import { useState, useEffect } from 'react';
interface UserRoles {
roles: string[];
hasRole: (role: string) => boolean;
hasAnyRole: (roles: string[]) => boolean;
canAccess: (requiredRoles: string[]) => boolean;
isLoading: boolean;
}
export const useUserRoles = (): UserRoles => {
const [roles, setRoles] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadUserRoles = () => {
try {
// Récupérer les rôles depuis le token stocké
const accessToken = localStorage.getItem('accessToken');
if (accessToken) {
// Décoder le token JWT pour extraire les rôles
const payload = JSON.parse(atob(accessToken.split('.')[1]));
// Extraire les rôles du realm et du client
const realmRoles = payload.realm_access?.roles || [];
const clientRoles = payload.resource_access?.['btpxpress-frontend']?.roles || [];
const allRoles = [...realmRoles, ...clientRoles];
setRoles(allRoles);
console.log('🔐 Rôles utilisateur chargés:', allRoles);
} else {
console.warn('⚠️ Aucun token d\'accès trouvé');
setRoles([]);
}
} catch (error) {
console.error('❌ Erreur lors du chargement des rôles:', error);
setRoles([]);
} finally {
setIsLoading(false);
}
};
loadUserRoles();
}, []);
const hasRole = (role: string): boolean => {
return roles.includes(role);
};
const hasAnyRole = (requiredRoles: string[]): boolean => {
return requiredRoles.some(role => roles.includes(role));
};
const canAccess = (requiredRoles: string[]): boolean => {
if (requiredRoles.length === 0) return true;
return hasAnyRole(requiredRoles);
};
return {
roles,
hasRole,
hasAnyRole,
canAccess,
isLoading
};
};
// Configuration des rôles par page/fonctionnalité
export const PAGE_ROLES = {
DASHBOARD: [], // Accessible à tous les utilisateurs authentifiés
PLANNING: ['super_admin', 'admin', 'directeur', 'manager', 'chef_chantier'],
CHANTIERS: ['super_admin', 'admin', 'directeur', 'manager', 'chef_chantier', 'ouvrier'],
CLIENTS: ['super_admin', 'admin', 'directeur', 'commercial'],
DEVIS: ['super_admin', 'admin', 'directeur', 'commercial'],
FACTURES: ['super_admin', 'admin', 'directeur', 'comptable'],
MATERIELS: ['super_admin', 'admin', 'directeur', 'manager', 'logisticien'],
EMPLOYES: ['super_admin', 'admin', 'directeur', 'manager'],
EQUIPES: ['super_admin', 'admin', 'directeur', 'manager', 'chef_chantier'],
REPORTS: ['super_admin', 'admin', 'directeur', 'manager'],
ADMIN: ['super_admin', 'admin']
} as const;
export type PageRole = keyof typeof PAGE_ROLES;