import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'dart:async'; /// Exception personnalisée pour les erreurs API avec méthodes d'affichage intégrées class ApiException implements Exception { final String message; final int? statusCode; final String? errorCode; final Map? details; final Object? originalError; const ApiException( this.message, { this.statusCode, this.errorCode, this.details, this.originalError, }); /// Créer une ApiException depuis une DioException factory ApiException.fromDioException(DioException dioException) { final response = dioException.response; final statusCode = response?.statusCode; // Essayer d'extraire le message de la réponse API String message = 'Erreur de communication avec le serveur'; String? errorCode; Map? details; if (response?.data != null) { try { final data = response!.data as Map; // Message spécifique de l'API if (data.containsKey('message')) { message = data['message'] as String; } // Code d'erreur spécifique if (data.containsKey('error_code')) { errorCode = data['error_code'] as String; } // Détails supplémentaires if (data.containsKey('errors')) { details = data['errors'] as Map?; } } catch (e) { // Si on ne peut pas parser la réponse, utiliser le message par défaut } } // Messages par défaut selon le code de statut if (response?.data == null || message == 'Erreur de communication avec le serveur') { switch (statusCode) { case 400: message = 'Données invalides'; break; case 401: message = 'Non autorisé : veuillez vous reconnecter'; break; case 403: message = 'Accès interdit'; break; case 404: message = 'Ressource non trouvée'; break; case 409: message = 'Conflit : données déjà existantes'; break; case 422: message = 'Données de validation incorrectes'; break; case 500: message = 'Erreur serveur interne'; break; case 502: case 503: case 504: message = 'Service temporairement indisponible'; break; default: switch (dioException.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: message = 'Délai d\'attente dépassé'; break; case DioExceptionType.connectionError: message = 'Problème de connexion réseau'; break; case DioExceptionType.cancel: message = 'Requête annulée'; break; default: message = 'Erreur de communication avec le serveur'; } } } return ApiException( message, statusCode: statusCode, errorCode: errorCode, details: details, originalError: dioException, ); } /// Créer une ApiException depuis n'importe quelle erreur factory ApiException.fromError(Object error) { if (error is ApiException) { return error; } final errorString = error.toString(); if (errorString.contains('SocketException') || errorString.contains('NetworkException')) { return const ApiException('Problème de connexion réseau'); } if (errorString.contains('TimeoutException')) { return const ApiException('Délai d\'attente dépassé'); } return ApiException('Erreur inattendue', originalError: error); } @override String toString() => message; /// Obtenir un message d'erreur formaté pour l'affichage String get displayMessage => message; /// Vérifier si c'est une erreur de validation bool get isValidationError => statusCode == 422 || statusCode == 400; /// Vérifier si c'est une erreur d'authentification bool get isAuthError => statusCode == 401 || statusCode == 403; /// Vérifier si c'est une erreur de conflit (données déjà existantes) bool get isConflictError => statusCode == 409; /// Vérifier si c'est une erreur réseau bool get isNetworkError => statusCode == null || statusCode == 502 || statusCode == 503 || statusCode == 504 || originalError is DioException && (originalError as DioException).type == DioExceptionType.connectionError; // === MÉTHODES D'AFFICHAGE INTÉGRÉES === /// Afficher cette erreur (méthode d'instance) void show(BuildContext context, {Duration? duration}) { if (context.mounted) { final isInDialog = _isInDialog(context); if (isInDialog) { _showOverlaySnackBar(context, displayMessage, Colors.red, duration); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(displayMessage), backgroundColor: Colors.red, duration: duration ?? const Duration(seconds: 5), action: SnackBarAction( label: 'Fermer', textColor: Colors.white, onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(), ), ), ); } } } /// Méthode statique pour afficher une erreur depuis n'importe quel objet static void showError(BuildContext context, Object error, {Duration? duration}) { final apiException = ApiException.fromError(error); apiException.show(context, duration: duration); } /// Méthode statique pour afficher un message de succès (compatible avec les Dialogs) static void showSuccess(BuildContext context, String message, {Duration? duration}) { if (context.mounted) { final isInDialog = _isInDialog(context); if (isInDialog) { _showOverlaySnackBar(context, message, Colors.green, duration); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.green, duration: duration ?? const Duration(seconds: 3), ), ); } } } /// Vérifier si le contexte est dans un Dialog static bool _isInDialog(BuildContext context) { return context.findAncestorWidgetOfExactType() != null; } /// Afficher un SnackBar en overlay au-dessus de tout static void _showOverlaySnackBar(BuildContext context, String message, Color backgroundColor, Duration? duration) { final overlay = Overlay.of(context); late OverlayEntry overlayEntry; overlayEntry = OverlayEntry( builder: (context) => Positioned( top: MediaQuery.of(context).padding.top + 20, // En haut de l'écran left: 16, right: 16, child: Material( color: Colors.transparent, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Row( children: [ Icon( backgroundColor == Colors.red ? Icons.error_outline : Icons.check_circle_outline, color: Colors.white, size: 20, ), const SizedBox(width: 12), Expanded( child: Text( message, style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500, ), ), ), IconButton( icon: const Icon(Icons.close, color: Colors.white, size: 18), onPressed: () => overlayEntry.remove(), padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 30, minHeight: 30), ), ], ), ), ), ), ); overlay.insert(overlayEntry); // Auto-suppression après la durée spécifiée Timer(duration ?? const Duration(seconds: 5), () { if (overlayEntry.mounted) { overlayEntry.remove(); } }); } }