feat(mobile): amélioration UX NotImplementedFailure + SnackbarHelper

- 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
This commit is contained in:
dahoud
2026-03-17 10:06:21 +00:00
parent f4bdd81141
commit b63fc46182
8 changed files with 249 additions and 26 deletions

View File

@@ -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<Color>(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();
}
}

View File

@@ -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';
}