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