From 9941ae97dc52afb20dfb36150e9e257e136aef76 Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Mon, 6 Jul 2026 15:02:31 +0200 Subject: [PATCH] feat(frontend): add departure checklist screen --- .../lib/screens/checklist_depart_screen.dart | 330 ++++++++++++++++++ eme-frontend/lib/screens/detail_screen.dart | 5 +- review.md | 2 +- 3 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 eme-frontend/lib/screens/checklist_depart_screen.dart diff --git a/eme-frontend/lib/screens/checklist_depart_screen.dart b/eme-frontend/lib/screens/checklist_depart_screen.dart new file mode 100644 index 0000000..0b5074e --- /dev/null +++ b/eme-frontend/lib/screens/checklist_depart_screen.dart @@ -0,0 +1,330 @@ +import "package:flutter/material.dart"; +import "package:google_fonts/google_fonts.dart"; +import "../theme/ensup_colors.dart"; +import "../models/materiel_item.dart"; +import "../widgets/ensup_top_bar.dart"; +import "../widgets/brand_corners.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 { + const ChecklistDepartScreen({super.key, required this.item}); + + final MaterielItem item; + + @override + State createState() => _ChecklistDepartScreenState(); +} + +class _ChecklistDepartScreenState extends State { + late final List<_Etat> _etats = List<_Etat>.filled( + widget.item.accessoires.length, + _Etat.present, + ); + + void _valider() { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Confirmation de l'emprunt — é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 départ", + 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() { + 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.cyan.withValues(alpha: 0.15), width: 1.5), + ), + child: Row( + children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration( + color: EnsupColors.cyan.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(9), + ), + child: Icon(widget.item.icon, color: EnsupColors.cyan, size: 18), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.item.nom, + style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w800, fontSize: 16), + ), + Text( + "Vérifiez chaque élément physiquement avant de valider", + style: GoogleFonts.titilliumWeb(fontSize: 12, color: EnsupColors.muted), + ), + ], + ), + ), + ], + ), + ), + 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), + ), + ), + ); + } + + Widget _colonneDroite() { + 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, + ), + ), + ], + ), + ), + 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 SizedBox(height: 14), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: _valider, + icon: const Icon(Icons.check, size: 18), + label: Text( + "Valider la checklist et confirmer l'emprunt", + textAlign: TextAlign.center, + style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 15), + ), + style: FilledButton.styleFrom( + backgroundColor: EnsupColors.blue3, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + ), + ), + ], + ); + } +} + +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/detail_screen.dart b/eme-frontend/lib/screens/detail_screen.dart index 42d9319..21a6dd6 100644 --- a/eme-frontend/lib/screens/detail_screen.dart +++ b/eme-frontend/lib/screens/detail_screen.dart @@ -5,6 +5,7 @@ import "../models/materiel_item.dart"; import "../widgets/ensup_top_bar.dart"; import "../widgets/brand_corners.dart"; import "../widgets/status_pill.dart"; +import "checklist_depart_screen.dart"; /// Détail d'un matériel : caractéristiques, accessoires du kit et lancement de /// l'emprunt. @@ -14,8 +15,8 @@ class DetailScreen extends StatelessWidget { final MaterielItem item; void _demarrer(BuildContext context) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Checklist de départ — écran à venir")), + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => ChecklistDepartScreen(item: item)), ); } diff --git a/review.md b/review.md index 163837a..9efe2e9 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 : identification + accueil + catalogue + détail matériel.* +*Dernière mise à jour : 2026-07-06 — Frontend : parcours emprunt jusqu'à la checklist de départ.*