From 90d5a65f291836a36b20684e8b45f0e8978a0a24 Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Tue, 7 Jul 2026 10:08:33 +0200 Subject: [PATCH] feat(frontend): add restitution result screen (completes student flow) --- eme-frontend/lib/models/anomalie_retour.dart | 7 + .../lib/screens/checklist_retour_screen.dart | 18 +- .../lib/screens/resultat_retour_screen.dart | 268 ++++++++++++++++++ review.md | 2 +- 4 files changed, 292 insertions(+), 3 deletions(-) create mode 100644 eme-frontend/lib/models/anomalie_retour.dart create mode 100644 eme-frontend/lib/screens/resultat_retour_screen.dart diff --git a/eme-frontend/lib/models/anomalie_retour.dart b/eme-frontend/lib/models/anomalie_retour.dart new file mode 100644 index 0000000..303b16c --- /dev/null +++ b/eme-frontend/lib/models/anomalie_retour.dart @@ -0,0 +1,7 @@ +/// Une différence constatée au retour par rapport au départ (RG20). +class AnomalieRetour { + const AnomalieRetour({required this.element, required this.etatRetour}); + + final String element; + final String etatRetour; +} diff --git a/eme-frontend/lib/screens/checklist_retour_screen.dart b/eme-frontend/lib/screens/checklist_retour_screen.dart index 5944168..dedbc93 100644 --- a/eme-frontend/lib/screens/checklist_retour_screen.dart +++ b/eme-frontend/lib/screens/checklist_retour_screen.dart @@ -2,9 +2,11 @@ import "package:flutter/material.dart"; import "package:google_fonts/google_fonts.dart"; import "../theme/ensup_colors.dart"; import "../models/emprunt_item.dart"; +import "../models/anomalie_retour.dart"; import "../widgets/ensup_top_bar.dart"; import "../widgets/brand_corners.dart"; import "../widgets/etat_checklist.dart"; +import "resultat_retour_screen.dart"; /// Checklist de retour (RG16) : l'étudiant indique l'état de chaque accessoire /// au retour, en vue de la comparaison avec la checklist de départ. @@ -24,8 +26,20 @@ class _ChecklistRetourScreenState extends State { ); void _valider() { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Résultat de la restitution — écran à venir")), + final accessoires = widget.emprunt.materiel.accessoires; + final anomalies = []; + for (var i = 0; i < _etats.length; i++) { + if (_etats[i] == EtatChecklist.absent) { + anomalies.add(AnomalieRetour(element: accessoires[i], etatRetour: "Absent au retour")); + } else if (_etats[i] == EtatChecklist.deteriore) { + anomalies.add(AnomalieRetour(element: accessoires[i], etatRetour: "Détérioré au retour")); + } + } + + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ResultatRetourScreen(emprunt: widget.emprunt, anomalies: anomalies), + ), ); } diff --git a/eme-frontend/lib/screens/resultat_retour_screen.dart b/eme-frontend/lib/screens/resultat_retour_screen.dart new file mode 100644 index 0000000..c6a8c2f --- /dev/null +++ b/eme-frontend/lib/screens/resultat_retour_screen.dart @@ -0,0 +1,268 @@ +import "package:flutter/material.dart"; +import "package:google_fonts/google_fonts.dart"; +import "../theme/ensup_colors.dart"; +import "../models/emprunt_item.dart"; +import "../models/anomalie_retour.dart"; +import "../widgets/ensup_top_bar.dart"; +import "../widgets/brand_corners.dart"; + +/// Résultat de la restitution (RG17-RG20) : conforme (succès) ou non conforme +/// (anomalie détectée + notification au responsable). +class ResultatRetourScreen extends StatelessWidget { + const ResultatRetourScreen({super.key, required this.emprunt, required this.anomalies}); + + final EmpruntItem emprunt; + final List anomalies; + + bool get _conforme => anomalies.isEmpty; + + static String _deux(int valeur) => valeur.toString().padLeft(2, "0"); + + @override + Widget build(BuildContext context) { + final fond = _conforme + ? const [Color(0xFFF0FDF4), Color(0xFFF8FAFC)] + : const [Color(0xFFFFF5F5), Color(0xFFF8FAFC)]; + + return Scaffold( + backgroundColor: EnsupColors.soft, + body: Stack( + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1280, maxHeight: 820), + child: Container( + margin: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: EnsupColors.text.withValues(alpha: 0.14), + blurRadius: 60, + offset: const Offset(0, 18), + ), + ], + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + const EnsupTopBar( + brandText: "EME · Résultat du contrôle", + campusTag: "Paris · Ensitech", + ), + Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: fond, + ), + ), + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 32), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 560), + child: Column( + children: [ + if (_conforme) _succes() else _anomalie(), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: () => Navigator.of(context).popUntil( + (route) => route.settings.name == "home", + ), + icon: const Icon(Icons.home_outlined, size: 18), + label: Text( + "Retour à l'accueil", + style: GoogleFonts.darkerGrotesque( + fontWeight: FontWeight.w700, + fontSize: 16, + ), + ), + style: FilledButton.styleFrom( + backgroundColor: EnsupColors.cyan, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + const Positioned.fill(child: BrandCorners()), + ], + ), + ); + } + + Widget _succes() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(36), + decoration: BoxDecoration( + gradient: const LinearGradient(colors: [Color(0xFFF0FDF4), Color(0xFFECFDF5)]), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFBBF7D0), width: 2), + ), + child: Column( + children: [ + Container( + width: 72, + height: 72, + decoration: const BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient(colors: [Color(0xFF16A34A), Color(0xFF22C55E)]), + ), + child: const Icon(Icons.check, color: Colors.white, size: 36), + ), + const SizedBox(height: 18), + Text( + "Retour conforme", + style: GoogleFonts.darkerGrotesque( + fontSize: 28, + fontWeight: FontWeight.w900, + color: const Color(0xFF15803D), + ), + ), + const SizedBox(height: 8), + Text( + "L'emprunt a été clôturé et le matériel est de nouveau disponible.", + textAlign: TextAlign.center, + style: GoogleFonts.titilliumWeb(fontSize: 14, color: const Color(0xFF166534)), + ), + ], + ), + ); + } + + Widget _anomalie() { + final now = DateTime.now(); + final date = + "${_deux(now.day)}/${_deux(now.month)}/${now.year} · ${_deux(now.hour)}:${_deux(now.minute)}"; + + return Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: EnsupColors.red.withValues(alpha: 0.06), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: EnsupColors.red.withValues(alpha: 0.2), width: 1.5), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.warning_amber_rounded, color: EnsupColors.red, size: 24), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Retour non conforme — Anomalie détectée", + style: GoogleFonts.darkerGrotesque( + fontSize: 17, + fontWeight: FontWeight.w800, + color: EnsupColors.red, + ), + ), + const SizedBox(height: 4), + Text( + "Une différence a été constatée avec la checklist de départ. Une anomalie " + "a été créée automatiquement et le responsable matériel a été notifié.", + style: GoogleFonts.titilliumWeb( + fontSize: 13, + color: EnsupColors.text2, + height: 1.5, + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: EnsupColors.line, width: 1.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "DÉTAIL DE L'ANOMALIE", + style: GoogleFonts.darkerGrotesque( + fontSize: 12, + fontWeight: FontWeight.w700, + color: EnsupColors.muted, + letterSpacing: 0.6, + ), + ), + const SizedBox(height: 10), + for (final anomalie in anomalies) + _drow( + anomalie.element, + anomalie.etatRetour, + valeurCouleur: EnsupColors.red, + ), + _drow("Matériel", "${emprunt.materiel.nom} · ${emprunt.materiel.reference}"), + _drow("Emprunteur", "Lucas Martin"), + _drow("Date détection", date), + ], + ), + ), + const SizedBox(height: 14), + Text( + "Le responsable matériel traitera cette anomalie. Vous pouvez retourner à l'accueil.", + textAlign: TextAlign.center, + style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted, height: 1.6), + ), + ], + ); + } + + Widget _drow(String cle, String valeur, {Color? valeurCouleur}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 7), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(cle, style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted)), + const SizedBox(width: 16), + Expanded( + child: Text( + valeur, + textAlign: TextAlign.right, + style: GoogleFonts.titilliumWeb( + fontSize: 13, + fontWeight: FontWeight.w600, + color: valeurCouleur ?? EnsupColors.text, + ), + ), + ), + ], + ), + ); + } +} diff --git a/review.md b/review.md index 52f4856..d0dac30 100644 --- a/review.md +++ b/review.md @@ -207,4 +207,4 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y --- -*Dernière mise à jour : 2026-07-07 — Frontend : restitution jusqu'à la checklist de retour (widgets checklist factorisés).* +*Dernière mise à jour : 2026-07-07 — Frontend : parcours étudiant complet (emprunt + restitution avec résultat conforme/anomalie).*