diff --git a/eme-frontend/lib/models/materiel_item.dart b/eme-frontend/lib/models/materiel_item.dart new file mode 100644 index 0000000..7b09c80 --- /dev/null +++ b/eme-frontend/lib/models/materiel_item.dart @@ -0,0 +1,21 @@ +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). +class MaterielItem { + const MaterielItem({ + required this.nom, + required this.categorie, + required this.reference, + required this.campus, + required this.disponible, + required this.icon, + }); + + final String nom; + final String categorie; + final String reference; + final String campus; + final bool disponible; + final IconData icon; +} diff --git a/eme-frontend/lib/screens/catalogue_screen.dart b/eme-frontend/lib/screens/catalogue_screen.dart new file mode 100644 index 0000000..4182d44 --- /dev/null +++ b/eme-frontend/lib/screens/catalogue_screen.dart @@ -0,0 +1,248 @@ +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/materiel_card.dart"; + +/// Catalogue matériel : recherche, filtres par catégorie et liste de matériels. +class CatalogueScreen extends StatefulWidget { + const CatalogueScreen({super.key}); + + @override + State createState() => _CatalogueScreenState(); +} + +const _materiels = [ + MaterielItem( + nom: "PC Dell Latitude 5420", + categorie: "Ordinateur", + reference: "ENS-PC-001", + campus: "Paris", + disponible: true, + icon: Icons.laptop_mac, + ), + MaterielItem( + nom: "Vidéoprojecteur Epson EB-X51", + categorie: "Projection", + reference: "ENS-VP-003", + campus: "Paris", + disponible: true, + icon: Icons.videocam_outlined, + ), + MaterielItem( + nom: "Chargeur USB-C 65W", + categorie: "Accessoire", + reference: "ENS-CH-012", + campus: "Paris", + disponible: false, + icon: Icons.power_outlined, + ), + MaterielItem( + nom: "PC Lenovo ThinkPad E14", + categorie: "Ordinateur", + reference: "ENS-PC-004", + campus: "Paris", + disponible: true, + icon: Icons.laptop_mac, + ), +]; + +const _categories = ["Tous", "Ordinateurs", "Projection", "Câbles", "Accessoires"]; + +class _CatalogueScreenState extends State { + String _recherche = ""; + String _categorie = "Tous"; + + List get _resultats { + final terme = _recherche.trim().toLowerCase(); + return _materiels.where((materiel) { + final matchTexte = terme.isEmpty || materiel.nom.toLowerCase().contains(terme); + final matchCategorie = _categorie == "Tous" || _categorie.startsWith(materiel.categorie); + return matchTexte && matchCategorie; + }).toList(); + } + + void _selectionner(BuildContext context, MaterielItem item) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("${item.nom} — détail à venir")), + ); + } + + @override + Widget build(BuildContext context) { + final resultats = _resultats; + + 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: "Catalogue matériel", + campusTag: "Paris · Ensitech", + userName: "Lucas Martin", + userInitials: "LM", + onBack: () => Navigator.of(context).pop(), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(32, 24, 32, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SearchField( + onChanged: (value) => setState(() => _recherche = value), + ), + const SizedBox(height: 14), + Wrap( + spacing: 8, + runSpacing: 8, + children: _categories.map((categorie) { + return _CategoryChip( + label: categorie, + selected: categorie == _categorie, + onTap: () => setState(() => _categorie = categorie), + ); + }).toList(), + ), + const SizedBox(height: 16), + const Divider(color: EnsupColors.line, height: 1), + const SizedBox(height: 16), + Expanded( + child: resultats.isEmpty + ? _EmptyState() + : ListView.separated( + itemCount: resultats.length, + separatorBuilder: (_, _) => const SizedBox(height: 10), + itemBuilder: (context, index) { + final item = resultats[index]; + return MaterielCard( + item: item, + onSelect: () => _selectionner(context, item), + ); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + const Positioned.fill(child: BrandCorners()), + ], + ), + ); + } +} + +class _SearchField extends StatelessWidget { + const _SearchField({required this.onChanged}); + + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: EnsupColors.line, width: 1.5), + ), + child: Row( + children: [ + const Icon(Icons.search, size: 18, color: EnsupColors.muted), + const SizedBox(width: 10), + Expanded( + child: TextField( + onChanged: onChanged, + style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.text), + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + hintText: "Rechercher un matériel...", + hintStyle: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.muted), + ), + ), + ), + ], + ), + ); + } +} + +class _CategoryChip extends StatelessWidget { + const _CategoryChip({required this.label, required this.selected, required this.onTap}); + + final String label; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(999), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + decoration: BoxDecoration( + color: selected ? EnsupColors.cyan : Colors.white, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: selected ? EnsupColors.cyan : EnsupColors.line, width: 1.5), + ), + child: Text( + label, + style: GoogleFonts.darkerGrotesque( + fontSize: 14, + fontWeight: FontWeight.w600, + color: selected ? Colors.white : EnsupColors.muted, + ), + ), + ), + ); + } +} + +class _EmptyState extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.search_off, size: 40, color: EnsupColors.muted), + const SizedBox(height: 12), + Text( + "Aucun matériel ne correspond", + style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.muted), + ), + ], + ), + ); + } +} diff --git a/eme-frontend/lib/screens/home_screen.dart b/eme-frontend/lib/screens/home_screen.dart index 2d276bd..d3ab7dd 100644 --- a/eme-frontend/lib/screens/home_screen.dart +++ b/eme-frontend/lib/screens/home_screen.dart @@ -5,6 +5,7 @@ import "../widgets/ensup_top_bar.dart"; import "../widgets/profile_card.dart"; import "../widgets/action_card.dart"; import "../widgets/brand_corners.dart"; +import "catalogue_screen.dart"; /// Écran d'accueil étudiant : profil + choix Emprunter / Restituer. class HomeScreen extends StatelessWidget { @@ -79,7 +80,9 @@ class HomeScreen extends StatelessWidget { description: "Accéder au catalogue et emprunter du matériel éducatif", color: EnsupColors.cyan, - onTap: () => _bientot(context, "Emprunter"), + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const CatalogueScreen()), + ), ), ), const SizedBox(width: 16), diff --git a/eme-frontend/lib/widgets/materiel_card.dart b/eme-frontend/lib/widgets/materiel_card.dart new file mode 100644 index 0000000..ef773cd --- /dev/null +++ b/eme-frontend/lib/widgets/materiel_card.dart @@ -0,0 +1,90 @@ +import "package:flutter/material.dart"; +import "package:google_fonts/google_fonts.dart"; +import "../theme/ensup_colors.dart"; +import "../models/materiel_item.dart"; +import "status_pill.dart"; + +/// Ligne du catalogue matériel (fidèle au `.mat-card` de la maquette). +class MaterielCard extends StatelessWidget { + const MaterielCard({super.key, required this.item, this.onSelect}); + + final MaterielItem item; + final VoidCallback? onSelect; + + @override + Widget build(BuildContext context) { + final disponible = item.disponible; + final accent = disponible ? EnsupColors.cyan : EnsupColors.muted; + + return Opacity( + opacity: disponible ? 1 : 0.55, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: EnsupColors.line, width: 1.5), + ), + child: Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: accent.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(item.icon, color: accent, size: 20), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.nom, + style: GoogleFonts.darkerGrotesque( + fontWeight: FontWeight.w700, + fontSize: 16, + color: EnsupColors.text, + ), + ), + const SizedBox(height: 2), + Text( + "${item.categorie} · Réf. ${item.reference} · ${item.campus}", + style: GoogleFonts.titilliumWeb(fontSize: 12, color: EnsupColors.muted), + ), + ], + ), + ), + const SizedBox(width: 10), + StatusPill(disponible: disponible), + const SizedBox(width: 12), + if (disponible) + FilledButton( + onPressed: onSelect, + style: FilledButton.styleFrom( + backgroundColor: EnsupColors.cyan, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: Text( + "Sélectionner", + style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 14), + ), + ) + else + Text( + "Indisponible", + style: GoogleFonts.darkerGrotesque( + fontWeight: FontWeight.w700, + fontSize: 14, + color: EnsupColors.muted, + ), + ), + ], + ), + ), + ); + } +} diff --git a/eme-frontend/lib/widgets/status_pill.dart b/eme-frontend/lib/widgets/status_pill.dart new file mode 100644 index 0000000..c8df5c4 --- /dev/null +++ b/eme-frontend/lib/widgets/status_pill.dart @@ -0,0 +1,29 @@ +import "package:flutter/material.dart"; +import "package:google_fonts/google_fonts.dart"; + +/// Pastille de statut d'un matériel (disponible / emprunté). +class StatusPill extends StatelessWidget { + const StatusPill({super.key, required this.disponible}); + + final bool disponible; + + @override + Widget build(BuildContext context) { + final label = disponible ? "Disponible" : "Emprunté"; + final background = disponible ? const Color(0xFFDCFCE7) : const Color(0xFFFEF9C3); + final foreground = disponible ? const Color(0xFF15803D) : const Color(0xFFA16207); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration(color: background, borderRadius: BorderRadius.circular(999)), + child: Text( + label, + style: GoogleFonts.darkerGrotesque( + fontSize: 14, + fontWeight: FontWeight.w700, + color: foreground, + ), + ), + ); + } +} diff --git a/review.md b/review.md index 3097b24..66b35e9 100644 --- a/review.md +++ b/review.md @@ -182,6 +182,11 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y - `EnsupTopBar` rendue réutilisable (utilisateur optionnel, marque paramétrable) ; nouveau widget `IdentificationOption` ; coins ENSUP aussi sur cet écran. - Navigation identification -> accueil étudiant (`HomeScreen`). +### Étape 24 — Frontend : écran catalogue matériel +- Écran catalogue (`s-catalogue`) : recherche **fonctionnelle** + filtres par catégorie (chips) + liste de matériels avec statut (Disponible/Emprunté) et état vide. +- 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). + --- ## Dette technique en attente @@ -197,4 +202,4 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y --- -*Dernière mise à jour : 2026-07-06 — Frontend : écran d'identification (démarrage) + accueil étudiant.* +*Dernière mise à jour : 2026-07-06 — Frontend : identification + accueil + catalogue matériel.*