From b63fc46182c415ab5bf66ec63f652ff722d11494 Mon Sep 17 00:00:00 2001 From: dahoud <41957584+DahoudG@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:06:21 +0000 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20am=C3=A9lioration=20UX=20NotImp?= =?UTF-8?q?lementedFailure=20+=20SnackbarHelper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NotImplementedFailure: ajout userFriendlyMessage et icon construction (blue) - ErrorDisplayWidget: support spécial pour NotImplementedFailure (bientôt disponible) - SnackbarHelper: classe centralisée pour messages cohérents (success, error, warning, info, notImplemented) - budgets_list_page: remplace generic snackbar par SnackbarHelper.showNotImplemented - conversations_page: remplace 2 TODOs par SnackbarHelper.showNotImplemented - export_members: met à jour TODO obsolète (endpoint PDF maintenant disponible) - cache_service: fix AppLogger.error calls (error: named param) - cached_datasource_decorator: fix AppLogger.error call Task #64 - Fix Snackbar Placeholders + NotImplementedFailure UX --- lib/core/cache/cache_service.dart | 12 +- .../cache/cached_datasource_decorator.dart | 2 +- lib/core/error/failures.dart | 11 +- .../pages/conversations_page.dart | 15 +- .../presentation/pages/budgets_list_page.dart | 8 +- .../domain/usecases/export_members.dart | 2 +- lib/shared/utils/snackbar_helper.dart | 213 ++++++++++++++++++ lib/shared/widgets/error_display_widget.dart | 12 + 8 files changed, 249 insertions(+), 26 deletions(-) create mode 100644 lib/shared/utils/snackbar_helper.dart diff --git a/lib/core/cache/cache_service.dart b/lib/core/cache/cache_service.dart index 1244849..25e2813 100644 --- a/lib/core/cache/cache_service.dart +++ b/lib/core/cache/cache_service.dart @@ -35,7 +35,7 @@ class CacheService { return success; } catch (e) { - AppLogger.error('Erreur lors de la mise en cache de $key', e); + AppLogger.error('Erreur lors de la mise en cache de $key', error: e); return false; } } @@ -65,7 +65,7 @@ class CacheService { AppLogger.debug('Cache hit: $key'); return value as T; } catch (e) { - AppLogger.error('Erreur lors de la lecture du cache $key', e); + AppLogger.error('Erreur lors de la lecture du cache $key', error: e); return null; } } @@ -92,7 +92,7 @@ class CacheService { try { return await _prefs.remove(key); } catch (e) { - AppLogger.error('Erreur lors de la suppression du cache $key', e); + AppLogger.error('Erreur lors de la suppression du cache $key', error: e); return false; } } @@ -109,7 +109,7 @@ class CacheService { AppLogger.info('Cache nettoyé pour préfixe: $prefix'); } catch (e) { - AppLogger.error('Erreur lors du nettoyage du cache $prefix', e); + AppLogger.error('Erreur lors du nettoyage du cache $prefix', error: e); } } @@ -118,7 +118,7 @@ class CacheService { try { return await _prefs.clear(); } catch (e) { - AppLogger.error('Erreur lors du nettoyage complet du cache', e); + AppLogger.error('Erreur lors du nettoyage complet du cache', error: e); return false; } } @@ -154,7 +154,7 @@ class CacheService { AppLogger.info('$cleaned entrées de cache expirées nettoyées'); } } catch (e) { - AppLogger.error('Erreur lors du nettoyage des caches expirés', e); + AppLogger.error('Erreur lors du nettoyage des caches expirés', error: e); } } diff --git a/lib/core/cache/cached_datasource_decorator.dart b/lib/core/cache/cached_datasource_decorator.dart index 8f2a5f0..2fa2e03 100644 --- a/lib/core/cache/cached_datasource_decorator.dart +++ b/lib/core/cache/cached_datasource_decorator.dart @@ -37,7 +37,7 @@ class CachedDatasourceDecorator { return result; } catch (e) { - AppLogger.error('Erreur dans withCache pour $cacheKey', e); + AppLogger.error('Erreur dans withCache pour $cacheKey', error: e); rethrow; } } diff --git a/lib/core/error/failures.dart b/lib/core/error/failures.dart index f78d7b1..7172320 100644 --- a/lib/core/error/failures.dart +++ b/lib/core/error/failures.dart @@ -136,8 +136,15 @@ class UnexpectedFailure extends Failure { /// Fonctionnalité non implémentée class NotImplementedFailure extends Failure { - const NotImplementedFailure(super.message, [super.code]); + const NotImplementedFailure( + super.message, [ + super.code, + super.isRetryable = false, // Not implemented features are not retryable + super.userFriendlyMessage = + 'Cette fonctionnalité sera bientôt disponible. Nous travaillons dessus !', + ]); @override - String toString() => 'NotImplementedFailure: $message${code != null ? ' (Code: $code)' : ''}'; + String toString() => + 'NotImplementedFailure: $message${code != null ? ' (Code: $code)' : ''}'; } diff --git a/lib/features/communication/presentation/pages/conversations_page.dart b/lib/features/communication/presentation/pages/conversations_page.dart index 5c599fb..d36c408 100644 --- a/lib/features/communication/presentation/pages/conversations_page.dart +++ b/lib/features/communication/presentation/pages/conversations_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/di/injection_container.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; +import '../../../../shared/utils/snackbar_helper.dart'; import '../bloc/messaging_bloc.dart'; import '../bloc/messaging_event.dart'; import '../bloc/messaging_state.dart'; @@ -117,12 +118,9 @@ class ConversationsPage extends StatelessWidget { return ConversationTile( conversation: conversation, onTap: () { - // Navigation vers la page de chat - // TODO: Implémenter navigation - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Ouvrir conversation: ${conversation.name}'), - ), + SnackbarHelper.showNotImplemented( + context, + 'Affichage des messages', ); }, ); @@ -137,10 +135,7 @@ class ConversationsPage extends StatelessWidget { floatingActionButton: FloatingActionButton( backgroundColor: AppColors.primaryGreen, onPressed: () { - // TODO: Ouvrir dialogue nouvelle conversation - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Nouvelle conversation (à implémenter)')), - ); + SnackbarHelper.showNotImplemented(context, 'Nouvelle conversation'); }, child: const Icon(Icons.add, color: Colors.white), ), diff --git a/lib/features/finance_workflow/presentation/pages/budgets_list_page.dart b/lib/features/finance_workflow/presentation/pages/budgets_list_page.dart index 0fbed61..b623875 100644 --- a/lib/features/finance_workflow/presentation/pages/budgets_list_page.dart +++ b/lib/features/finance_workflow/presentation/pages/budgets_list_page.dart @@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import '../../../../core/di/injection_container.dart'; import '../../../../shared/design_system/unionflow_design_system.dart'; +import '../../../../shared/utils/snackbar_helper.dart'; import '../../domain/entities/budget.dart'; import '../bloc/budget_bloc.dart'; import '../bloc/budget_event.dart'; @@ -99,12 +100,7 @@ class _BudgetsListView extends StatelessWidget { ), floatingActionButton: FloatingActionButton.extended( onPressed: () { - // TODO: Navigate to create budget page - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Fonctionnalité en cours de développement'), - ), - ); + SnackbarHelper.showNotImplemented(context, 'Création de budget'); }, icon: const Icon(Icons.add), label: const Text('Nouveau budget'), diff --git a/lib/features/members/domain/usecases/export_members.dart b/lib/features/members/domain/usecases/export_members.dart index 44b920f..038a9de 100644 --- a/lib/features/members/domain/usecases/export_members.dart +++ b/lib/features/members/domain/usecases/export_members.dart @@ -19,7 +19,7 @@ class ExportMembers { /// [format] - Format d'export ('csv' ou 'pdf') /// /// Retourne les données exportées (liste complète des membres selon critères) - /// TODO: Ajouter endpoint backend GET /api/membres/export?format=csv|pdf + /// Note: Backend endpoint GET /api/membres/export?format=csv|excel|pdf disponible /// Le use case actuel récupère toutes les données, l'export final se fait côté UI Future>> call({ MembreSearchCriteria? criteria, diff --git a/lib/shared/utils/snackbar_helper.dart b/lib/shared/utils/snackbar_helper.dart new file mode 100644 index 0000000..7b3ba03 --- /dev/null +++ b/lib/shared/utils/snackbar_helper.dart @@ -0,0 +1,213 @@ +/// Centralized helper for showing consistent Snackbar messages +library snackbar_helper; + +import 'package:flutter/material.dart'; +import '../../core/error/failures.dart'; + +/// Helper class for showing consistent Snackbar messages throughout the app +class SnackbarHelper { + /// Show success message + static void showSuccess( + BuildContext context, + String message, { + Duration duration = const Duration(seconds: 3), + SnackBarAction? action, + }) { + final snackBar = SnackBar( + content: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message)), + ], + ), + backgroundColor: Colors.green[700], + behavior: SnackBarBehavior.floating, + duration: duration, + action: action, + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + /// Show error message + static void showError( + BuildContext context, + String message, { + Duration duration = const Duration(seconds: 4), + VoidCallback? onRetry, + }) { + final snackBar = SnackBar( + content: Row( + children: [ + const Icon(Icons.error_outline, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message)), + ], + ), + backgroundColor: Colors.red[700], + behavior: SnackBarBehavior.floating, + duration: duration, + action: onRetry != null + ? SnackBarAction( + label: 'Réessayer', + textColor: Colors.white, + onPressed: onRetry, + ) + : null, + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + /// Show warning message + static void showWarning( + BuildContext context, + String message, { + Duration duration = const Duration(seconds: 4), + }) { + final snackBar = SnackBar( + content: Row( + children: [ + const Icon(Icons.warning_amber, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message)), + ], + ), + backgroundColor: Colors.orange[700], + behavior: SnackBarBehavior.floating, + duration: duration, + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + /// Show info message + static void showInfo( + BuildContext context, + String message, { + Duration duration = const Duration(seconds: 3), + }) { + final snackBar = SnackBar( + content: Row( + children: [ + const Icon(Icons.info_outline, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message)), + ], + ), + backgroundColor: Colors.blue[700], + behavior: SnackBarBehavior.floating, + duration: duration, + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + /// Show "not implemented" message with appropriate styling + static void showNotImplemented( + BuildContext context, + String? featureName, + ) { + final message = featureName != null + ? '$featureName sera bientôt disponible !' + : 'Cette fonctionnalité sera bientôt disponible !'; + + final snackBar = SnackBar( + content: Row( + children: [ + const Icon(Icons.construction, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(message)), + ], + ), + backgroundColor: Colors.blue[700], + behavior: SnackBarBehavior.floating, + duration: const Duration(seconds: 4), + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + /// Show failure with appropriate styling based on failure type + static void showFailure( + BuildContext context, + Failure failure, { + VoidCallback? onRetry, + }) { + // Handle NotImplementedFailure specially + if (failure is NotImplementedFailure) { + showNotImplemented(context, null); + return; + } + + Color backgroundColor; + IconData icon; + + if (failure is NetworkFailure) { + backgroundColor = Colors.orange[700]!; + icon = Icons.wifi_off; + } else if (failure is UnauthorizedFailure) { + backgroundColor = Colors.red[700]!; + icon = Icons.lock_outline; + } else if (failure is ForbiddenFailure) { + backgroundColor = Colors.deepOrange[700]!; + icon = Icons.block; + } else if (failure is ValidationFailure) { + backgroundColor = Colors.amber[700]!; + icon = Icons.error_outline; + } else { + backgroundColor = Colors.red[700]!; + icon = Icons.error_outline; + } + + final snackBar = SnackBar( + content: Row( + children: [ + Icon(icon, color: Colors.white), + const SizedBox(width: 12), + Expanded(child: Text(failure.getUserMessage())), + ], + ), + backgroundColor: backgroundColor, + behavior: SnackBarBehavior.floating, + duration: Duration(seconds: failure.isRetryable ? 6 : 4), + action: failure.isRetryable && onRetry != null + ? SnackBarAction( + label: 'Réessayer', + textColor: Colors.white, + onPressed: onRetry, + ) + : null, + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + /// Show loading message (use with caution - prefer progress indicators) + static void showLoading( + BuildContext context, + String message, { + Duration duration = const Duration(seconds: 2), + }) { + final snackBar = SnackBar( + content: Row( + children: [ + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + const SizedBox(width: 12), + Expanded(child: Text(message)), + ], + ), + backgroundColor: Colors.grey[800], + behavior: SnackBarBehavior.floating, + duration: duration, + ); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } + + /// Dismiss current Snackbar + static void dismiss(BuildContext context) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + } +} diff --git a/lib/shared/widgets/error_display_widget.dart b/lib/shared/widgets/error_display_widget.dart index ada8470..4b5e532 100644 --- a/lib/shared/widgets/error_display_widget.dart +++ b/lib/shared/widgets/error_display_widget.dart @@ -88,6 +88,8 @@ class ErrorDisplayWidget extends StatelessWidget { return Icons.error_outline; } else if (failure is ServerFailure) { return Icons.cloud_off; + } else if (failure is NotImplementedFailure) { + return Icons.construction; } else { return Icons.warning_amber; } @@ -103,6 +105,8 @@ class ErrorDisplayWidget extends StatelessWidget { return Colors.deepOrange; } else if (failure is ValidationFailure) { return Colors.amber; + } else if (failure is NotImplementedFailure) { + return Colors.blue[700]!; } else { return Theme.of(context).colorScheme.error; } @@ -122,6 +126,8 @@ class ErrorDisplayWidget extends StatelessWidget { return 'Données invalides'; } else if (failure is ServerFailure) { return 'Erreur serveur'; + } else if (failure is NotImplementedFailure) { + return 'Bientôt disponible'; } else { return 'Une erreur est survenue'; } @@ -213,6 +219,8 @@ class ErrorBanner extends StatelessWidget { return Icons.error_outline; } else if (failure is ServerFailure) { return Icons.cloud_off; + } else if (failure is NotImplementedFailure) { + return Icons.construction; } else { return Icons.warning_amber; } @@ -227,6 +235,8 @@ class ErrorBanner extends StatelessWidget { return Colors.deepOrange; } else if (failure is ValidationFailure) { return Colors.amber; + } else if (failure is NotImplementedFailure) { + return Colors.blue[700]!; } else { return Theme.of(context).colorScheme.error; } @@ -245,6 +255,8 @@ class ErrorBanner extends StatelessWidget { return 'Données invalides'; } else if (failure is ServerFailure) { return 'Erreur serveur'; + } else if (failure is NotImplementedFailure) { + return 'Bientôt disponible'; } else { return 'Erreur'; }