diff --git a/eme-frontend/lib/screens/checklist_depart_screen.dart b/eme-frontend/lib/screens/checklist_depart_screen.dart index fb33d62..779cfb8 100644 --- a/eme-frontend/lib/screens/checklist_depart_screen.dart +++ b/eme-frontend/lib/screens/checklist_depart_screen.dart @@ -4,10 +4,9 @@ import "../theme/ensup_colors.dart"; import "../models/materiel_item.dart"; import "../widgets/ensup_top_bar.dart"; import "../widgets/brand_corners.dart"; +import "../widgets/etat_checklist.dart"; import "confirmation_screen.dart"; -enum _Etat { present, absent, deteriore } - /// Checklist de départ (RG11) : l'étudiant indique l'état de chaque accessoire /// attendu avant de confirmer l'emprunt. class ChecklistDepartScreen extends StatefulWidget { @@ -20,17 +19,17 @@ class ChecklistDepartScreen extends StatefulWidget { } class _ChecklistDepartScreenState extends State { - late final List<_Etat> _etats = List<_Etat>.filled( + late final List _etats = List.filled( widget.item.accessoires.length, - _Etat.present, + EtatChecklist.present, ); void _valider() { final anomalies = []; for (var i = 0; i < _etats.length; i++) { - if (_etats[i] == _Etat.absent) { + if (_etats[i] == EtatChecklist.absent) { anomalies.add("${widget.item.accessoires[i]} absent au départ"); - } else if (_etats[i] == _Etat.deteriore) { + } else if (_etats[i] == EtatChecklist.deteriore) { anomalies.add("${widget.item.accessoires[i]} détérioré au départ"); } } @@ -141,77 +140,12 @@ class _ChecklistDepartScreenState extends State { ), ), const SizedBox(height: 16), - _entete(), - for (var i = 0; i < widget.item.accessoires.length; i++) _ligne(i), - ], - ); - } - - Widget _entete() { - return Container( - padding: const EdgeInsets.symmetric(vertical: 10), - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: EnsupColors.line, width: 2)), - ), - child: Row( - children: [ - Expanded(child: _texteEntete("Élément")), - _colonneEntete("Présent"), - _colonneEntete("Absent"), - _colonneEntete("Détérioré"), - ], - ), - ); - } - - Widget _texteEntete(String label) { - return Text( - label.toUpperCase(), - style: GoogleFonts.darkerGrotesque( - fontSize: 12, - fontWeight: FontWeight.w700, - color: EnsupColors.muted, - letterSpacing: 0.6, - ), - ); - } - - Widget _colonneEntete(String label) { - return SizedBox(width: 72, child: Center(child: _texteEntete(label))); - } - - Widget _ligne(int index) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 10), - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: EnsupColors.line)), - ), - child: Row( - children: [ - Expanded( - child: Text( - widget.item.accessoires[index], - style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.text2), - ), - ), - _caseEtat(index, _Etat.present), - _caseEtat(index, _Etat.absent), - _caseEtat(index, _Etat.deteriore), - ], - ), - ); - } - - Widget _caseEtat(int index, _Etat type) { - return SizedBox( - width: 72, - child: Center( - child: _EtatBox( - type: type, - selected: _etats[index] == type, - onTap: () => setState(() => _etats[index] = type), + ChecklistTableau( + accessoires: widget.item.accessoires, + etats: _etats, + onChanged: (index, etat) => setState(() => _etats[index] = etat), ), - ), + ], ); } @@ -219,59 +153,15 @@ class _ChecklistDepartScreenState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: EnsupColors.blue2.withValues(alpha: 0.05), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: EnsupColors.blue2.withValues(alpha: 0.15), width: 1.5), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "INSTRUCTIONS", - style: GoogleFonts.darkerGrotesque( - fontSize: 12, - fontWeight: FontWeight.w700, - color: EnsupColors.muted, - letterSpacing: 0.6, - ), - ), - const SizedBox(height: 8), - Text( - "Vérifiez physiquement chaque élément du kit et indiquez son état : " - "Présent, Absent ou Détérioré. Signalez tout élément manquant ou " - "endommagé pour éviter tout litige au retour.", - style: GoogleFonts.titilliumWeb( - fontSize: 13, - color: EnsupColors.text2, - height: 1.6, - ), - ), - ], - ), + ChecklistInstructions( + titre: "Instructions", + texte: + "Vérifiez physiquement chaque élément du kit et indiquez son état : " + "Présent, Absent ou Détérioré. Signalez tout élément manquant ou " + "endommagé pour éviter tout litige au retour.", ), const SizedBox(height: 14), - TextField( - maxLines: 4, - style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.text), - decoration: InputDecoration( - hintText: "Commentaire ou observation optionnelle...", - hintStyle: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted), - filled: true, - fillColor: Colors.white, - contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(9), - borderSide: const BorderSide(color: EnsupColors.line, width: 1.5), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(9), - borderSide: const BorderSide(color: EnsupColors.cyan, width: 1.5), - ), - ), - ), + const ChecklistCommentaire(), const SizedBox(height: 14), SizedBox( width: double.infinity, @@ -294,50 +184,3 @@ class _ChecklistDepartScreenState extends State { ); } } - -class _EtatBox extends StatelessWidget { - const _EtatBox({required this.type, required this.selected, required this.onTap}); - - final _Etat type; - final bool selected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - var background = Colors.white; - var border = EnsupColors.line; - Widget? contenu; - - if (selected) { - switch (type) { - case _Etat.present: - background = const Color(0xFFF0FDF4); - border = const Color(0xFF86EFAC); - contenu = const Icon(Icons.check, size: 16, color: Color(0xFF16A34A)); - case _Etat.absent: - background = const Color(0xFFFEF2F2); - border = const Color(0xFFFCA5A5); - contenu = const Icon(Icons.close, size: 16, color: EnsupColors.red); - case _Etat.deteriore: - background = const Color(0xFFFFFBEB); - border = const Color(0xFFFDE68A); - contenu = const Icon(Icons.priority_high, size: 16, color: Color(0xFFD97706)); - } - } - - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(8), - child: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: background, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: border, width: 2), - ), - child: contenu, - ), - ); - } -} diff --git a/eme-frontend/lib/screens/checklist_retour_screen.dart b/eme-frontend/lib/screens/checklist_retour_screen.dart new file mode 100644 index 0000000..5944168 --- /dev/null +++ b/eme-frontend/lib/screens/checklist_retour_screen.dart @@ -0,0 +1,173 @@ +import "package:flutter/material.dart"; +import "package:google_fonts/google_fonts.dart"; +import "../theme/ensup_colors.dart"; +import "../models/emprunt_item.dart"; +import "../widgets/ensup_top_bar.dart"; +import "../widgets/brand_corners.dart"; +import "../widgets/etat_checklist.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. +class ChecklistRetourScreen extends StatefulWidget { + const ChecklistRetourScreen({super.key, required this.emprunt}); + + final EmpruntItem emprunt; + + @override + State createState() => _ChecklistRetourScreenState(); +} + +class _ChecklistRetourScreenState extends State { + late final List _etats = List.filled( + widget.emprunt.materiel.accessoires.length, + EtatChecklist.present, + ); + + void _valider() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Résultat de la restitution — écran à venir")), + ); + } + + @override + Widget build(BuildContext context) { + 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: [ + EnsupTopBar( + brandText: "Checklist de retour", + campusTag: "Paris · Ensitech", + userName: "Lucas Martin", + userInitials: "LM", + onBack: () => Navigator.of(context).pop(), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 28), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 3, child: _colonneGauche()), + const SizedBox(width: 20), + Expanded(flex: 2, child: _colonneDroite()), + ], + ), + ), + ), + ], + ), + ), + ), + ), + const Positioned.fill(child: BrandCorners()), + ], + ), + ); + } + + Widget _colonneGauche() { + final materiel = widget.emprunt.materiel; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: EnsupColors.soft, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: EnsupColors.teal.withValues(alpha: 0.15), width: 1.5), + ), + child: Row( + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: EnsupColors.teal.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(9), + ), + child: Icon(materiel.icon, color: EnsupColors.teal, size: 18), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + materiel.nom, + style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w800, fontSize: 16), + ), + Text( + "Emprunté le ${widget.emprunt.dateEmprunt}", + style: GoogleFonts.titilliumWeb(fontSize: 12, color: EnsupColors.muted), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ChecklistTableau( + accessoires: materiel.accessoires, + etats: _etats, + onChanged: (index, etat) => setState(() => _etats[index] = etat), + ), + ], + ); + } + + Widget _colonneDroite() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ChecklistInstructions( + titre: "Rappel checklist de départ", + texte: + "Au départ, tous les accessoires du kit étaient présents. " + "Indiquez leur état au retour ; toute différence sera signalée.", + ), + const SizedBox(height: 14), + const ChecklistCommentaire(hint: "Commentaire optionnel..."), + const SizedBox(height: 14), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: _valider, + icon: const Icon(Icons.check, size: 18), + label: Text( + "Valider le retour", + style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 16), + ), + style: FilledButton.styleFrom( + backgroundColor: EnsupColors.blue3, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ), + ], + ); + } +} diff --git a/eme-frontend/lib/screens/restitution_select_screen.dart b/eme-frontend/lib/screens/restitution_select_screen.dart index cf6d335..757e5b9 100644 --- a/eme-frontend/lib/screens/restitution_select_screen.dart +++ b/eme-frontend/lib/screens/restitution_select_screen.dart @@ -5,6 +5,7 @@ import "../models/materiel_item.dart"; import "../models/emprunt_item.dart"; import "../widgets/ensup_top_bar.dart"; import "../widgets/brand_corners.dart"; +import "checklist_retour_screen.dart"; const _pcDell = MaterielItem( nom: "PC Dell Latitude 5420", @@ -28,8 +29,8 @@ class RestitutionSelectScreen extends StatelessWidget { const RestitutionSelectScreen({super.key}); void _restituer(BuildContext context, EmpruntItem emprunt) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Checklist de retour de ${emprunt.materiel.nom} — écran à venir")), + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => ChecklistRetourScreen(emprunt: emprunt)), ); } diff --git a/eme-frontend/lib/widgets/etat_checklist.dart b/eme-frontend/lib/widgets/etat_checklist.dart new file mode 100644 index 0000000..74e280b --- /dev/null +++ b/eme-frontend/lib/widgets/etat_checklist.dart @@ -0,0 +1,220 @@ +import "package:flutter/material.dart"; +import "package:google_fonts/google_fonts.dart"; +import "../theme/ensup_colors.dart"; + +/// État possible d'un élément de checklist (départ ou retour). +enum EtatChecklist { present, absent, deteriore } + +/// Case cliquable colorée selon l'état sélectionné (vert / rouge / jaune). +class EtatBox extends StatelessWidget { + const EtatBox({ + super.key, + required this.type, + required this.selected, + required this.onTap, + }); + + final EtatChecklist type; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + var background = Colors.white; + var border = EnsupColors.line; + Widget? contenu; + + if (selected) { + switch (type) { + case EtatChecklist.present: + background = const Color(0xFFF0FDF4); + border = const Color(0xFF86EFAC); + contenu = const Icon(Icons.check, size: 16, color: Color(0xFF16A34A)); + case EtatChecklist.absent: + background = const Color(0xFFFEF2F2); + border = const Color(0xFFFCA5A5); + contenu = const Icon(Icons.close, size: 16, color: EnsupColors.red); + case EtatChecklist.deteriore: + background = const Color(0xFFFFFBEB); + border = const Color(0xFFFDE68A); + contenu = const Icon(Icons.priority_high, size: 16, color: Color(0xFFD97706)); + } + } + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: background, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: border, width: 2), + ), + child: contenu, + ), + ); + } +} + +/// Tableau de checklist : une ligne par accessoire avec les 3 états possibles. +class ChecklistTableau extends StatelessWidget { + const ChecklistTableau({ + super.key, + required this.accessoires, + required this.etats, + required this.onChanged, + }); + + final List accessoires; + final List etats; + final void Function(int index, EtatChecklist etat) onChanged; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _entete(), + for (var i = 0; i < accessoires.length; i++) _ligne(i), + ], + ); + } + + Widget _entete() { + return Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: EnsupColors.line, width: 2)), + ), + child: Row( + children: [ + Expanded(child: _texteEntete("ÉLÉMENT")), + _colonneEntete("PRÉSENT"), + _colonneEntete("ABSENT"), + _colonneEntete("DÉTÉRIORÉ"), + ], + ), + ); + } + + Widget _texteEntete(String label) { + return Text( + label, + style: GoogleFonts.darkerGrotesque( + fontSize: 12, + fontWeight: FontWeight.w700, + color: EnsupColors.muted, + letterSpacing: 0.6, + ), + ); + } + + Widget _colonneEntete(String label) { + return SizedBox(width: 72, child: Center(child: _texteEntete(label))); + } + + Widget _ligne(int index) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: EnsupColors.line)), + ), + child: Row( + children: [ + Expanded( + child: Text( + accessoires[index], + style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.text2), + ), + ), + _case(index, EtatChecklist.present), + _case(index, EtatChecklist.absent), + _case(index, EtatChecklist.deteriore), + ], + ), + ); + } + + Widget _case(int index, EtatChecklist type) { + return SizedBox( + width: 72, + child: Center( + child: EtatBox( + type: type, + selected: etats[index] == type, + onTap: () => onChanged(index, type), + ), + ), + ); + } +} + +/// Encadré d'instructions / rappel à côté d'une checklist. +class ChecklistInstructions extends StatelessWidget { + const ChecklistInstructions({super.key, required this.titre, required this.texte}); + + final String titre; + final String texte; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: EnsupColors.blue2.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: EnsupColors.blue2.withValues(alpha: 0.15), width: 1.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + titre.toUpperCase(), + style: GoogleFonts.darkerGrotesque( + fontSize: 12, + fontWeight: FontWeight.w700, + color: EnsupColors.muted, + letterSpacing: 0.6, + ), + ), + const SizedBox(height: 8), + Text( + texte, + style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.text2, height: 1.6), + ), + ], + ), + ); + } +} + +/// Zone de commentaire optionnel sous une checklist. +class ChecklistCommentaire extends StatelessWidget { + const ChecklistCommentaire({super.key, this.hint = "Commentaire ou observation optionnelle..."}); + + final String hint; + + @override + Widget build(BuildContext context) { + return TextField( + maxLines: 4, + style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.text), + decoration: InputDecoration( + hintText: hint, + hintStyle: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted), + filled: true, + fillColor: Colors.white, + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(9), + borderSide: const BorderSide(color: EnsupColors.line, width: 1.5), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(9), + borderSide: const BorderSide(color: EnsupColors.cyan, width: 1.5), + ), + ), + ); + } +} diff --git a/review.md b/review.md index f14f178..52f4856 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-06 — Frontend : parcours emprunt complet + début restitution (sélection).* +*Dernière mise à jour : 2026-07-07 — Frontend : restitution jusqu'à la checklist de retour (widgets checklist factorisés).*