From 4ee593c8d4849f5b48f335d9f25bcba30c357fe9 Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Fri, 24 Jul 2026 14:08:55 +0200 Subject: [PATCH] feat(frontend): improve application feedback messages --- .../lib/screens/catalogue_screen.dart | 5 +- .../lib/screens/checklist_depart_screen.dart | 5 +- .../lib/screens/checklist_retour_screen.dart | 19 +- eme-frontend/lib/screens/home_screen.dart | 5 +- eme-frontend/lib/screens/login_screen.dart | 23 +-- .../lib/screens/responsable_screen.dart | 47 ++--- .../screens/restitution_select_screen.dart | 5 +- eme-frontend/lib/widgets/app_message.dart | 189 ++++++++++++++++++ 8 files changed, 236 insertions(+), 62 deletions(-) create mode 100644 eme-frontend/lib/widgets/app_message.dart diff --git a/eme-frontend/lib/screens/catalogue_screen.dart b/eme-frontend/lib/screens/catalogue_screen.dart index c0afcad..ddfa693 100644 --- a/eme-frontend/lib/screens/catalogue_screen.dart +++ b/eme-frontend/lib/screens/catalogue_screen.dart @@ -6,6 +6,7 @@ import "../models/materiel_item.dart"; import "../services/materiel_service.dart"; import "../widgets/ensup_top_bar.dart"; import "../widgets/brand_corners.dart"; +import "../widgets/app_message.dart"; import "../widgets/materiel_card.dart"; import "detail_screen.dart"; @@ -65,9 +66,7 @@ class _CatalogueScreenState extends State { if (!mounted) { return; } - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error); } finally { if (mounted) { setState(() => _selectionEnCoursId = null); diff --git a/eme-frontend/lib/screens/checklist_depart_screen.dart b/eme-frontend/lib/screens/checklist_depart_screen.dart index f5e8955..d4b68a0 100644 --- a/eme-frontend/lib/screens/checklist_depart_screen.dart +++ b/eme-frontend/lib/screens/checklist_depart_screen.dart @@ -7,6 +7,7 @@ import "../services/emprunt_service.dart"; import "../widgets/ensup_top_bar.dart"; import "../widgets/brand_corners.dart"; import "../widgets/etat_checklist.dart"; +import "../widgets/app_message.dart"; import "confirmation_screen.dart"; class ChecklistDepartScreen extends StatefulWidget { @@ -77,9 +78,7 @@ class _ChecklistDepartScreenState extends State { if (!mounted) { return; } - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error); } finally { if (mounted) { setState(() => _envoiEnCours = false); diff --git a/eme-frontend/lib/screens/checklist_retour_screen.dart b/eme-frontend/lib/screens/checklist_retour_screen.dart index fbb31c4..aff5296 100644 --- a/eme-frontend/lib/screens/checklist_retour_screen.dart +++ b/eme-frontend/lib/screens/checklist_retour_screen.dart @@ -9,6 +9,7 @@ import "../services/emprunt_service.dart"; import "../widgets/ensup_top_bar.dart"; import "../widgets/brand_corners.dart"; import "../widgets/etat_checklist.dart"; +import "../widgets/app_message.dart"; import "resultat_retour_screen.dart"; class ChecklistRetourScreen extends StatefulWidget { @@ -114,9 +115,7 @@ class _ChecklistRetourScreenState extends State { if (!mounted) { return; } - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error); } finally { if (mounted) { setState(() => _envoiEnCours = false); @@ -142,19 +141,15 @@ class _ChecklistRetourScreenState extends State { etatDemande: _etatApi(etat), ); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - "Le responsable a été alerté de cette tentative de modification.", - ), - ), + AppMessage.alerte( + context, + "Le responsable a été alerté de cette tentative de modification.", + titre: "Changement signalé", ); } catch (error) { if (!mounted) return; setState(() => _alerteEnvoyee = false); - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error); } finally { if (mounted) { setState(() => _alerteEnCours = false); diff --git a/eme-frontend/lib/screens/home_screen.dart b/eme-frontend/lib/screens/home_screen.dart index 01bf392..994f052 100644 --- a/eme-frontend/lib/screens/home_screen.dart +++ b/eme-frontend/lib/screens/home_screen.dart @@ -3,6 +3,7 @@ import "package:google_fonts/google_fonts.dart"; import "../demo_identity.dart"; import "../services/auth_service.dart"; import "../theme/ensup_colors.dart"; +import "../widgets/app_message.dart"; import "../widgets/ensup_top_bar.dart"; import "../widgets/profile_card.dart"; import "../widgets/action_card.dart"; @@ -22,9 +23,7 @@ class HomeScreen extends StatelessWidget { } } catch (error) { if (!context.mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error); } } diff --git a/eme-frontend/lib/screens/login_screen.dart b/eme-frontend/lib/screens/login_screen.dart index d51d19e..6db7f6f 100644 --- a/eme-frontend/lib/screens/login_screen.dart +++ b/eme-frontend/lib/screens/login_screen.dart @@ -4,6 +4,7 @@ import "../demo_identity.dart"; import "../services/api_client.dart"; import "../services/auth_service.dart"; import "../theme/ensup_colors.dart"; +import "../widgets/app_message.dart"; import "../widgets/ensup_top_bar.dart"; import "../widgets/identification_option.dart"; import "../widgets/brand_corners.dart"; @@ -72,10 +73,10 @@ class _LoginScreenState extends State { } } catch (_) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("La session Microsoft doit etre renouvelee."), - ), + AppMessage.alerte( + context, + "La session Microsoft doit être renouvelée.", + titre: "Session expirée", ); } finally { if (mounted) { @@ -87,12 +88,10 @@ class _LoginScreenState extends State { Future _identifierParQr() async { if (_connexionEnCours) return; if (AuthService.mode == AuthenticationMode.azure) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - "L'identification QR securisee n'est pas encore configuree.", - ), - ), + AppMessage.information( + context, + "L'identification QR sécurisée n'est pas encore configurée.", + titre: "QR code indisponible", ); return; } @@ -121,9 +120,7 @@ class _LoginScreenState extends State { _ouvrirPourRole(profile.roleCode); } catch (error) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error, titre: "Connexion impossible"); } finally { if (mounted) { setState(() => _connexionEnCours = false); diff --git a/eme-frontend/lib/screens/responsable_screen.dart b/eme-frontend/lib/screens/responsable_screen.dart index b6b152b..546db40 100644 --- a/eme-frontend/lib/screens/responsable_screen.dart +++ b/eme-frontend/lib/screens/responsable_screen.dart @@ -5,6 +5,7 @@ import "../services/auth_service.dart"; import "../services/responsable_service.dart"; import "../theme/ensup_colors.dart"; import "../utils/file_download.dart"; +import "../widgets/app_message.dart"; const _workspaceBackground = Color(0xFFF3F6F9); const _orange = Color(0xFFD97706); @@ -55,9 +56,7 @@ class _ResponsableScreenState extends State { } } catch (error) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error); } } @@ -73,12 +72,16 @@ class _ResponsableScreenState extends State { anomalie["id"] as int, prochain, ); + if (!mounted) return; + AppMessage.succes( + context, + "L'anomalie est maintenant au statut ${_statusLabel(prochain)}.", + titre: "Statut mis à jour", + ); _rafraichir(); } catch (error) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error); } finally { if (mounted) { setState(() => _transitionEnCours = false); @@ -106,21 +109,17 @@ class _ResponsableScreenState extends State { statutMateriel: decision.statutMateriel, ); if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - decision.decision == "CONFIRMER" - ? "Le changement a été confirmé et l'anomalie a été créée." - : "Le changement a été refusé.", - ), - ), + AppMessage.succes( + context, + decision.decision == "CONFIRMER" + ? "Le changement a été confirmé et l'anomalie a été créée." + : "Le changement a été refusé.", + titre: "Décision enregistrée", ); _rafraichir(); } catch (error) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error); } finally { if (mounted) { setState(() => _decisionEnCours = false); @@ -134,9 +133,7 @@ class _ResponsableScreenState extends State { _rafraichir(); } catch (error) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error); } } @@ -152,14 +149,14 @@ class _ResponsableScreenState extends State { contentType: file.contentType, ); if (!mounted) return; - ScaffoldMessenger.of( + AppMessage.succes( context, - ).showSnackBar(const SnackBar(content: Text("Export CSV téléchargé."))); + "Le fichier d'historique est disponible dans vos téléchargements.", + titre: "Export CSV téléchargé", + ); } catch (error) { if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error, titre: "Export impossible"); } finally { if (mounted) { setState(() => _exportEnCours = false); diff --git a/eme-frontend/lib/screens/restitution_select_screen.dart b/eme-frontend/lib/screens/restitution_select_screen.dart index ed311a2..00dbb54 100644 --- a/eme-frontend/lib/screens/restitution_select_screen.dart +++ b/eme-frontend/lib/screens/restitution_select_screen.dart @@ -7,6 +7,7 @@ import "../services/emprunt_service.dart"; import "../services/materiel_service.dart"; import "../widgets/ensup_top_bar.dart"; import "../widgets/brand_corners.dart"; +import "../widgets/app_message.dart"; import "checklist_retour_screen.dart"; /// Sélection de l'emprunt en cours à restituer (RG15 : ses propres emprunts). @@ -50,9 +51,7 @@ class _RestitutionSelectScreenState extends State { if (!mounted) { return; } - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(error.toString()))); + AppMessage.erreur(context, error); } finally { if (mounted) { setState(() => _chargementId = null); diff --git a/eme-frontend/lib/widgets/app_message.dart b/eme-frontend/lib/widgets/app_message.dart new file mode 100644 index 0000000..dcc1c01 --- /dev/null +++ b/eme-frontend/lib/widgets/app_message.dart @@ -0,0 +1,189 @@ +import "package:flutter/material.dart"; +import "package:google_fonts/google_fonts.dart"; + +import "../theme/ensup_colors.dart"; + +enum AppMessageType { information, succes, alerte, erreur } + +class AppMessage { + AppMessage._(); + + static int _messageActif = 0; + + static void information( + BuildContext context, + String message, { + String titre = "Information", + }) { + _afficher( + context, + type: AppMessageType.information, + titre: titre, + message: message, + ); + } + + static void succes( + BuildContext context, + String message, { + String titre = "Action réussie", + }) { + _afficher( + context, + type: AppMessageType.succes, + titre: titre, + message: message, + ); + } + + static void alerte( + BuildContext context, + String message, { + String titre = "Attention", + }) { + _afficher( + context, + type: AppMessageType.alerte, + titre: titre, + message: message, + ); + } + + static void erreur( + BuildContext context, + Object erreur, { + String titre = "Une erreur est survenue", + }) { + _afficher( + context, + type: AppMessageType.erreur, + titre: titre, + message: _nettoyerErreur(erreur), + duree: const Duration(seconds: 8), + ); + } + + static void _afficher( + BuildContext context, { + required AppMessageType type, + required String titre, + required String message, + Duration duree = const Duration(seconds: 6), + }) { + final messenger = ScaffoldMessenger.of(context); + final identifiant = ++_messageActif; + final style = _style(type); + + messenger + ..clearSnackBars() + ..hideCurrentMaterialBanner() + ..showMaterialBanner( + MaterialBanner( + backgroundColor: style.fond, + surfaceTintColor: Colors.transparent, + elevation: 6, + shadowColor: EnsupColors.text.withValues(alpha: 0.18), + padding: const EdgeInsets.fromLTRB(20, 14, 8, 14), + leadingPadding: const EdgeInsets.only(right: 14), + leading: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: style.couleur, + borderRadius: BorderRadius.circular(8), + ), + child: Icon(style.icone, color: Colors.white, size: 22), + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + titre, + style: GoogleFonts.darkerGrotesque( + fontSize: 16, + fontWeight: FontWeight.w800, + color: style.texte, + ), + ), + const SizedBox(height: 2), + Text( + message, + style: GoogleFonts.titilliumWeb( + fontSize: 13, + color: EnsupColors.text2, + height: 1.35, + ), + ), + ], + ), + actions: [ + IconButton( + tooltip: "Fermer le message", + onPressed: () { + _messageActif++; + messenger.hideCurrentMaterialBanner(); + }, + icon: Icon(Icons.close, color: style.texte), + ), + ], + ), + ); + + Future.delayed(duree, () { + if (_messageActif == identifiant && messenger.mounted) { + messenger.hideCurrentMaterialBanner(); + } + }); + } + + static String _nettoyerErreur(Object erreur) { + return erreur + .toString() + .replaceFirst(RegExp(r"^(Exception|Error):\s*"), "") + .trim(); + } + + static _AppMessageStyle _style(AppMessageType type) { + return switch (type) { + AppMessageType.information => const _AppMessageStyle( + fond: Color(0xFFEFF8FC), + couleur: EnsupColors.cyan, + texte: EnsupColors.blue3, + icone: Icons.info_outline, + ), + AppMessageType.succes => const _AppMessageStyle( + fond: Color(0xFFF0FDF4), + couleur: EnsupColors.green, + texte: Color(0xFF166534), + icone: Icons.check_circle_outline, + ), + AppMessageType.alerte => const _AppMessageStyle( + fond: Color(0xFFFFF7ED), + couleur: Color(0xFFD97706), + texte: Color(0xFF9A3412), + icone: Icons.warning_amber_rounded, + ), + AppMessageType.erreur => const _AppMessageStyle( + fond: Color(0xFFFFF1F2), + couleur: EnsupColors.red, + texte: Color(0xFF9F1239), + icone: Icons.error_outline, + ), + }; + } +} + +class _AppMessageStyle { + const _AppMessageStyle({ + required this.fond, + required this.couleur, + required this.texte, + required this.icone, + }); + + final Color fond; + final Color couleur; + final Color texte; + final IconData icone; +}