From b124d7a775a5498e6880eb4ad49851780eff1574 Mon Sep 17 00:00:00 2001 From: SaidSoighiri94 Date: Mon, 6 Jul 2026 12:44:43 +0200 Subject: [PATCH] feat(frontend): add material detail screen --- eme-frontend/lib/models/materiel_item.dart | 12 +- .../lib/screens/catalogue_screen.dart | 29 +- eme-frontend/lib/screens/detail_screen.dart | 247 ++++++++++++++++++ review.md | 7 +- 4 files changed, 286 insertions(+), 9 deletions(-) create mode 100644 eme-frontend/lib/screens/detail_screen.dart diff --git a/eme-frontend/lib/models/materiel_item.dart b/eme-frontend/lib/models/materiel_item.dart index 7b09c80..1fbcd20 100644 --- a/eme-frontend/lib/models/materiel_item.dart +++ b/eme-frontend/lib/models/materiel_item.dart @@ -1,21 +1,29 @@ import "package:flutter/material.dart"; -/// Représentation d'un matériel affiché dans le catalogue (données statiques -/// pour l'instant ; sera alimenté par l'API plus tard). +/// Représentation d'un matériel affiché dans le catalogue et son détail +/// (données statiques pour l'instant ; sera alimenté par l'API plus tard). class MaterielItem { const MaterielItem({ required this.nom, required this.categorie, + required this.marque, + required this.modele, required this.reference, required this.campus, + required this.etatGeneral, required this.disponible, + required this.accessoires, required this.icon, }); final String nom; final String categorie; + final String marque; + final String modele; final String reference; final String campus; + final String etatGeneral; final bool disponible; + final List accessoires; final IconData icon; } diff --git a/eme-frontend/lib/screens/catalogue_screen.dart b/eme-frontend/lib/screens/catalogue_screen.dart index 4182d44..9c88540 100644 --- a/eme-frontend/lib/screens/catalogue_screen.dart +++ b/eme-frontend/lib/screens/catalogue_screen.dart @@ -5,6 +5,7 @@ import "../models/materiel_item.dart"; import "../widgets/ensup_top_bar.dart"; import "../widgets/brand_corners.dart"; import "../widgets/materiel_card.dart"; +import "detail_screen.dart"; /// Catalogue matériel : recherche, filtres par catégorie et liste de matériels. class CatalogueScreen extends StatefulWidget { @@ -18,33 +19,49 @@ const _materiels = [ MaterielItem( nom: "PC Dell Latitude 5420", categorie: "Ordinateur", + marque: "Dell", + modele: "Latitude 5420", reference: "ENS-PC-001", - campus: "Paris", + campus: "Paris · Ensitech", + etatGeneral: "Bon état", disponible: true, + accessoires: ["Chargeur 65W", "Souris sans fil", "Housse de protection", "Câble HDMI"], icon: Icons.laptop_mac, ), MaterielItem( nom: "Vidéoprojecteur Epson EB-X51", categorie: "Projection", + marque: "Epson", + modele: "EB-X51", reference: "ENS-VP-003", - campus: "Paris", + campus: "Paris · Ensitech", + etatGeneral: "Bon état", disponible: true, + accessoires: ["Télécommande", "Câble HDMI", "Câble VGA"], icon: Icons.videocam_outlined, ), MaterielItem( nom: "Chargeur USB-C 65W", categorie: "Accessoire", + marque: "Anker", + modele: "PowerPort III", reference: "ENS-CH-012", - campus: "Paris", + campus: "Paris · Ensitech", + etatGeneral: "Bon état", disponible: false, + accessoires: [], icon: Icons.power_outlined, ), MaterielItem( nom: "PC Lenovo ThinkPad E14", categorie: "Ordinateur", + marque: "Lenovo", + modele: "ThinkPad E14", reference: "ENS-PC-004", - campus: "Paris", + campus: "Paris · Ensitech", + etatGeneral: "Bon état", disponible: true, + accessoires: ["Chargeur 65W", "Souris sans fil", "Housse de protection"], icon: Icons.laptop_mac, ), ]; @@ -65,8 +82,8 @@ class _CatalogueScreenState extends State { } void _selectionner(BuildContext context, MaterielItem item) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("${item.nom} — détail à venir")), + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => DetailScreen(item: item)), ); } diff --git a/eme-frontend/lib/screens/detail_screen.dart b/eme-frontend/lib/screens/detail_screen.dart new file mode 100644 index 0000000..42d9319 --- /dev/null +++ b/eme-frontend/lib/screens/detail_screen.dart @@ -0,0 +1,247 @@ +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"; +import "../widgets/status_pill.dart"; + +/// Détail d'un matériel : caractéristiques, accessoires du kit et lancement de +/// l'emprunt. +class DetailScreen extends StatelessWidget { + const DetailScreen({super.key, required this.item}); + + final MaterielItem item; + + void _demarrer(BuildContext context) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Checklist de départ — é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: "Détail matériel", + 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(context)), + ], + ), + ), + ), + ], + ), + ), + ), + ), + const Positioned.fill(child: BrandCorners()), + ], + ), + ); + } + + Widget _colonneGauche() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(18), + decoration: BoxDecoration( + color: EnsupColors.soft, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: EnsupColors.line, width: 1.5), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.nom, + style: GoogleFonts.darkerGrotesque( + fontSize: 20, + fontWeight: FontWeight.w900, + color: EnsupColors.text, + ), + ), + const SizedBox(height: 12), + _row("Référence", item.reference), + _row("Catégorie", item.categorie), + _row("Marque", item.marque), + _row("Modèle", item.modele), + _row("État", item.etatGeneral, valueColor: EnsupColors.green), + _row("Campus", item.campus), + _rowStatut(), + ], + ), + ), + if (item.accessoires.isNotEmpty) ...[ + const SizedBox(height: 18), + Text( + "ACCESSOIRES INCLUS DANS LE KIT", + style: GoogleFonts.darkerGrotesque( + fontSize: 13, + fontWeight: FontWeight.w700, + color: EnsupColors.muted, + letterSpacing: 0.8, + ), + ), + const SizedBox(height: 10), + Wrap( + spacing: 6, + runSpacing: 6, + children: item.accessoires.map(_tag).toList(), + ), + ], + ], + ); + } + + Widget _row(String cle, String valeur, {Color? valueColor}) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(color: EnsupColors.line)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(cle, style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted)), + Text( + valeur, + style: GoogleFonts.titilliumWeb( + fontSize: 13, + fontWeight: FontWeight.w600, + color: valueColor ?? EnsupColors.text, + ), + ), + ], + ), + ); + } + + Widget _rowStatut() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text("Statut", style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted)), + StatusPill(disponible: item.disponible), + ], + ), + ); + } + + Widget _tag(String label) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), + decoration: BoxDecoration( + color: EnsupColors.teal.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Text( + label, + style: GoogleFonts.darkerGrotesque( + fontSize: 13, + fontWeight: FontWeight.w600, + color: EnsupColors.teal, + ), + ), + ); + } + + Widget _colonneDroite(BuildContext context) { + return Column( + children: [ + Container( + height: 180, + width: double.infinity, + decoration: BoxDecoration( + color: EnsupColors.soft, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: EnsupColors.line, width: 1.5), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(item.icon, size: 48, color: EnsupColors.muted), + const SizedBox(height: 10), + Text( + "Aperçu du matériel", + style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted), + ), + ], + ), + ), + const SizedBox(height: 14), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: item.disponible ? () => _demarrer(context) : null, + icon: const Icon(Icons.arrow_forward, size: 18), + label: Text( + "Démarrer l'emprunt", + 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 SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + style: OutlinedButton.styleFrom( + foregroundColor: EnsupColors.text2, + side: const BorderSide(color: EnsupColors.line, width: 1.5), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: Text( + "Retour au catalogue", + style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 15), + ), + ), + ), + ], + ); + } +} diff --git a/review.md b/review.md index 66b35e9..163837a 100644 --- a/review.md +++ b/review.md @@ -187,6 +187,11 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - Nouveaux composants : `MaterielCard`, `StatusPill`, modèle `MaterielItem`. Données statiques (4 matériels de la maquette). - Navigation : Accueil (Emprunter) -> Catalogue ; bouton retour vers l'accueil. « Sélectionner » -> placeholder (détail à venir). +### Étape 25 — Frontend : écran détail matériel +- Écran détail (`s-detail`) : caractéristiques (référence, catégorie, marque, modèle, état, campus, statut) + accessoires du kit (tags) + aperçu + boutons « Démarrer l'emprunt » / « Retour au catalogue ». +- Modèle `MaterielItem` enrichi (marque, modèle, état, accessoires) ; données statiques mises à jour. +- Navigation : Catalogue (Sélectionner) -> Détail ; « Démarrer l'emprunt » -> placeholder (checklist à venir). + --- ## Dette technique en attente @@ -202,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 matériel.* +*Dernière mise à jour : 2026-07-06 — Frontend : identification + accueil + catalogue + détail matériel.*