Files
btpxpress-frontend/services/errorHandler.ts

229 lines
6.4 KiB
TypeScript
Executable File

/**
* 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<Toast> | null = null;
static setToast(toastRef: React.RefObject<Toast>) {
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, any>): 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;
}
}