/** * Service centralisé de gestion d'erreurs pour BTP Xpress */ import { Toast } from 'primereact/toast'; import { AxiosError } from 'axios'; export interface ErrorDetails { code?: string; message: string; field?: string; severity: 'error' | 'warn' | 'info'; } export interface ApiErrorResponse { error: string; message?: string; details?: ErrorDetails[]; timestamp?: string; path?: string; } export class ErrorHandler { private static toast: React.RefObject | null = null; static setToast(toastRef: React.RefObject) { this.toast = toastRef; } /** * Gère les erreurs API avec messages appropriés */ static handleApiError(error: unknown, context?: string): void { console.error(`Erreur API ${context ? `(${context})` : ''}:`, error); if (error instanceof AxiosError) { const response = error.response; if (response) { const status = response.status; const data = response.data as ApiErrorResponse; switch (status) { case 400: this.showError('Données invalides', data.message || data.error || 'Vérifiez les informations saisies'); break; case 401: this.showError('Non autorisé', 'Veuillez vous reconnecter'); // Ne pas rediriger si on est en train de traiter un code d'autorisation if (typeof window !== 'undefined') { const currentUrl = window.location.href; const hasAuthCode = currentUrl.includes('code=') && currentUrl.includes('/dashboard'); if (!hasAuthCode) { // Rediriger vers la page de connexion window.location.href = '/api/auth/login'; } else { console.log('🔄 ErrorHandler: Erreur 401 ignorée car authentification en cours...'); } } break; case 403: this.showError('Accès refusé', 'Vous n\'avez pas les permissions nécessaires'); break; case 404: this.showError('Ressource non trouvée', data.message || 'L\'élément demandé n\'existe pas'); break; case 409: this.showError('Conflit', data.message || 'Cette opération entre en conflit avec l\'état actuel'); break; case 422: this.handleValidationErrors(data); break; case 500: this.showError('Erreur serveur', 'Une erreur interne s\'est produite. Veuillez réessayer plus tard.'); break; case 503: this.showError('Service indisponible', 'Le service est temporairement indisponible'); break; default: this.showError('Erreur réseau', `Erreur ${status}: ${data.message || data.error || 'Erreur inconnue'}`); } } else if (error.request) { this.showError('Erreur de connexion', 'Impossible de contacter le serveur. Vérifiez votre connexion internet.'); } else { this.showError('Erreur', error.message || 'Une erreur inattendue s\'est produite'); } } else if (error instanceof Error) { this.showError('Erreur', error.message); } else { this.showError('Erreur', 'Une erreur inconnue s\'est produite'); } } /** * Gère les erreurs de validation (422) */ private static handleValidationErrors(data: ApiErrorResponse): void { if (data.details && data.details.length > 0) { const messages = data.details.map(detail => detail.field ? `${detail.field}: ${detail.message}` : detail.message ); this.showError('Erreurs de validation', messages.join('\n')); } else { this.showError('Erreur de validation', data.message || data.error || 'Données invalides'); } } /** * Affiche un message d'erreur */ static showError(summary: string, detail: string): void { if (this.toast?.current) { this.toast.current.show({ severity: 'error', summary, detail, life: 5000 }); } else { console.error(`${summary}: ${detail}`); } } /** * Affiche un message d'avertissement */ static showWarning(summary: string, detail: string): void { if (this.toast?.current) { this.toast.current.show({ severity: 'warn', summary, detail, life: 4000 }); } else { console.warn(`${summary}: ${detail}`); } } /** * Affiche un message de succès */ static showSuccess(summary: string, detail: string): void { if (this.toast?.current) { this.toast.current.show({ severity: 'success', summary, detail, life: 3000 }); } else { console.log(`${summary}: ${detail}`); } } /** * Affiche un message d'information */ static showInfo(summary: string, detail: string): void { if (this.toast?.current) { this.toast.current.show({ severity: 'info', summary, detail, life: 3000 }); } else { console.info(`${summary}: ${detail}`); } } /** * Valide les champs obligatoires */ static validateRequired(fields: Record): string[] { const errors: string[] = []; Object.entries(fields).forEach(([fieldName, value]) => { if (value === null || value === undefined || (typeof value === 'string' && value.trim() === '') || (Array.isArray(value) && value.length === 0)) { errors.push(`Le champ "${fieldName}" est obligatoire`); } }); return errors; } /** * Valide un email */ static validateEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } /** * Valide un numéro de téléphone français */ static validatePhoneNumber(phone: string): boolean { const phoneRegex = /^(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}$/; return phoneRegex.test(phone); } /** * Valide un SIRET */ static validateSiret(siret: string): boolean { if (!siret || siret.length !== 14) return false; const digits = siret.replace(/\s/g, '').split('').map(Number); if (digits.some(isNaN)) return false; // Algorithme de validation SIRET let sum = 0; for (let i = 0; i < 14; i++) { let digit = digits[i]; if (i % 2 === 1) { digit *= 2; if (digit > 9) digit -= 9; } sum += digit; } return sum % 10 === 0; } }