diff --git a/eme-frontend/lib/screens/catalogue_screen.dart b/eme-frontend/lib/screens/catalogue_screen.dart index 2086982..c0afcad 100644 --- a/eme-frontend/lib/screens/catalogue_screen.dart +++ b/eme-frontend/lib/screens/catalogue_screen.dart @@ -35,15 +35,18 @@ class _CatalogueScreenState extends State { } List _categories(List materiels) { - final distinctes = materiels.map((m) => m.categorie).toSet().toList()..sort(); + final distinctes = materiels.map((m) => m.categorie).toSet().toList() + ..sort(); return ["Tous", ...distinctes]; } List _filtrer(List materiels) { final terme = _recherche.trim().toLowerCase(); return materiels.where((materiel) { - final matchTexte = terme.isEmpty || materiel.nom.toLowerCase().contains(terme); - final matchCategorie = _categorie == "Tous" || materiel.categorie == _categorie; + final matchTexte = + terme.isEmpty || materiel.nom.toLowerCase().contains(terme); + final matchCategorie = + _categorie == "Tous" || materiel.categorie == _categorie; return matchTexte && matchCategorie; }).toList(); } @@ -55,16 +58,16 @@ class _CatalogueScreenState extends State { if (!mounted) { return; } - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => DetailScreen(item: detail)), - ); + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (_) => DetailScreen(item: detail))); } catch (error) { if (!mounted) { return; } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(error.toString())), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(error.toString()))); } finally { if (mounted) { setState(() => _selectionEnCoursId = null); @@ -106,20 +109,42 @@ class _CatalogueScreenState extends State { ), Expanded( child: Padding( - padding: const EdgeInsets.fromLTRB(32, 24, 32, 8), + padding: const EdgeInsets.fromLTRB(24, 24, 24, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + "Matériel disponible", + style: GoogleFonts.darkerGrotesque( + fontSize: 28, + height: 1, + fontWeight: FontWeight.w900, + color: EnsupColors.text, + ), + ), + const SizedBox(height: 5), + Text( + "Recherchez un équipement puis vérifiez son contenu avant l’emprunt.", + style: GoogleFonts.titilliumWeb( + fontSize: 13, + color: EnsupColors.muted, + ), + ), + const SizedBox(height: 18), _SearchField( - onChanged: (value) => setState(() => _recherche = value), + onChanged: (value) => + setState(() => _recherche = value), ), const SizedBox(height: 14), Expanded( child: FutureBuilder>( future: _future, builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); + if (snapshot.connectionState == + ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); } if (snapshot.hasError) { return _ErreurState( @@ -165,8 +190,25 @@ class _CatalogueScreenState extends State { }).toList(), ), const SizedBox(height: 16), - const Divider(color: EnsupColors.line, height: 1), - const SizedBox(height: 16), + Row( + children: [ + Text( + "${resultats.length} équipement${resultats.length > 1 ? "s" : ""}", + style: GoogleFonts.darkerGrotesque( + fontSize: 16, + fontWeight: FontWeight.w800, + color: EnsupColors.text2, + ), + ), + const Expanded( + child: Padding( + padding: EdgeInsets.only(left: 14), + child: Divider(color: EnsupColors.line, height: 1), + ), + ), + ], + ), + const SizedBox(height: 12), Expanded( child: resultats.isEmpty ? const _EmptyState() @@ -177,7 +219,10 @@ class _CatalogueScreenState extends State { final item = resultats[index]; return Stack( children: [ - MaterielCard(item: item, onSelect: () => _selectionner(item)), + MaterielCard( + item: item, + onSelect: () => _selectionner(item), + ), if (_selectionEnCoursId == item.id) const Positioned.fill( child: ColoredBox( @@ -217,12 +262,18 @@ class _SearchField extends StatelessWidget { Expanded( child: TextField( onChanged: onChanged, - style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.text), + 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), + hintStyle: GoogleFonts.titilliumWeb( + fontSize: 14, + color: EnsupColors.muted, + ), ), ), ), @@ -233,7 +284,11 @@ class _SearchField extends StatelessWidget { } class _CategoryChip extends StatelessWidget { - const _CategoryChip({required this.label, required this.selected, required this.onTap}); + const _CategoryChip({ + required this.label, + required this.selected, + required this.onTap, + }); final String label; final bool selected; @@ -249,7 +304,10 @@ class _CategoryChip extends StatelessWidget { decoration: BoxDecoration( color: selected ? EnsupColors.cyan : Colors.white, borderRadius: BorderRadius.circular(999), - border: Border.all(color: selected ? EnsupColors.cyan : EnsupColors.line, width: 1.5), + border: Border.all( + color: selected ? EnsupColors.cyan : EnsupColors.line, + width: 1.5, + ), ), child: Text( label, @@ -273,11 +331,18 @@ class _EmptyState extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.inventory_2_outlined, size: 40, color: EnsupColors.muted), + const Icon( + Icons.inventory_2_outlined, + size: 40, + color: EnsupColors.muted, + ), const SizedBox(height: 10), Text( "Aucun matériel ne correspond", - style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.muted), + style: GoogleFonts.titilliumWeb( + fontSize: 14, + color: EnsupColors.muted, + ), ), ], ), @@ -297,12 +362,19 @@ class _ErreurState extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.cloud_off_outlined, size: 40, color: EnsupColors.muted), + const Icon( + Icons.cloud_off_outlined, + size: 40, + color: EnsupColors.muted, + ), const SizedBox(height: 12), Text( message, textAlign: TextAlign.center, - style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.text2), + style: GoogleFonts.titilliumWeb( + fontSize: 14, + color: EnsupColors.text2, + ), ), const SizedBox(height: 14), OutlinedButton( diff --git a/eme-frontend/lib/screens/checklist_depart_screen.dart b/eme-frontend/lib/screens/checklist_depart_screen.dart index ca8086e..e71e149 100644 --- a/eme-frontend/lib/screens/checklist_depart_screen.dart +++ b/eme-frontend/lib/screens/checklist_depart_screen.dart @@ -58,7 +58,9 @@ class _ChecklistDepartScreenState extends State { return { "nomElement": entry.value, "etat": _etatApi(_etats[entry.key]), - "quantiteConstatee": _etats[entry.key] == EtatChecklist.absent ? 0 : 1, + "quantiteConstatee": _etats[entry.key] == EtatChecklist.absent + ? 0 + : 1, }; }).toList(), ); @@ -75,9 +77,9 @@ class _ChecklistDepartScreenState extends State { if (!mounted) { return; } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(error.toString())), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(error.toString()))); } finally { if (mounted) { setState(() => _envoiEnCours = false); @@ -130,14 +132,31 @@ class _ChecklistDepartScreenState extends State { ), 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()), - ], + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 26, + ), + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 780) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _colonneGauche(), + const SizedBox(height: 22), + _colonneDroite(), + ], + ); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 3, child: _colonneGauche()), + const SizedBox(width: 20), + Expanded(flex: 2, child: _colonneDroite()), + ], + ); + }, ), ), ), @@ -161,7 +180,10 @@ class _ChecklistDepartScreenState extends State { decoration: BoxDecoration( color: EnsupColors.soft, borderRadius: BorderRadius.circular(12), - border: Border.all(color: EnsupColors.cyan.withValues(alpha: 0.15), width: 1.5), + border: Border.all( + color: EnsupColors.cyan.withValues(alpha: 0.15), + width: 1.5, + ), ), child: Row( children: [ @@ -172,7 +194,11 @@ class _ChecklistDepartScreenState extends State { color: EnsupColors.cyan.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(9), ), - child: Icon(widget.item.icon, color: EnsupColors.cyan, size: 18), + child: Icon( + widget.item.icon, + color: EnsupColors.cyan, + size: 18, + ), ), const SizedBox(width: 12), Expanded( @@ -181,11 +207,17 @@ class _ChecklistDepartScreenState extends State { children: [ Text( widget.item.nom, - style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w800, fontSize: 16), + 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), + style: GoogleFonts.titilliumWeb( + fontSize: 12, + color: EnsupColors.muted, + ), ), ], ), @@ -225,7 +257,10 @@ class _ChecklistDepartScreenState extends State { ? const SizedBox( width: 18, height: 18, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), ) : const Icon(Icons.check, size: 18), label: Text( @@ -233,12 +268,17 @@ class _ChecklistDepartScreenState extends State { ? "Enregistrement..." : "Valider la checklist et confirmer l'emprunt", textAlign: TextAlign.center, - style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 15), + 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)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), ), ), diff --git a/eme-frontend/lib/screens/checklist_retour_screen.dart b/eme-frontend/lib/screens/checklist_retour_screen.dart index 1ab37a2..6c73e4b 100644 --- a/eme-frontend/lib/screens/checklist_retour_screen.dart +++ b/eme-frontend/lib/screens/checklist_retour_screen.dart @@ -22,7 +22,8 @@ class ChecklistRetourScreen extends StatefulWidget { } class _ChecklistRetourScreenState extends State { - late final List _elements = widget.emprunt.materiel.accessoires.isEmpty + late final List _elements = + widget.emprunt.materiel.accessoires.isEmpty ? [widget.emprunt.materiel.nom] : widget.emprunt.materiel.accessoires; late final List _etats = List.filled( @@ -44,9 +45,16 @@ class _ChecklistRetourScreenState extends State { final anomalies = []; for (var i = 0; i < _etats.length; i++) { if (_etats[i] == EtatChecklist.absent) { - anomalies.add(AnomalieRetour(element: _elements[i], etatRetour: "Absent au retour")); + anomalies.add( + AnomalieRetour(element: _elements[i], etatRetour: "Absent au retour"), + ); } else if (_etats[i] == EtatChecklist.deteriore) { - anomalies.add(AnomalieRetour(element: _elements[i], etatRetour: "Détérioré au retour")); + anomalies.add( + AnomalieRetour( + element: _elements[i], + etatRetour: "Détérioré au retour", + ), + ); } } @@ -58,7 +66,9 @@ class _ChecklistRetourScreenState extends State { return { "nomElement": entry.value, "etat": _etatApi(_etats[entry.key]), - "quantiteConstatee": _etats[entry.key] == EtatChecklist.absent ? 0 : 1, + "quantiteConstatee": _etats[entry.key] == EtatChecklist.absent + ? 0 + : 1, }; }).toList(), ); @@ -67,7 +77,9 @@ class _ChecklistRetourScreenState extends State { return; } - final anomaliesAffichees = resultat.statut == "CLOTURE" ? [] : anomalies; + final anomaliesAffichees = resultat.statut == "CLOTURE" + ? [] + : anomalies; Navigator.of(context).push( MaterialPageRoute( builder: (_) => ResultatRetourScreen( @@ -80,9 +92,9 @@ class _ChecklistRetourScreenState extends State { if (!mounted) { return; } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(error.toString())), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(error.toString()))); } finally { if (mounted) { setState(() => _envoiEnCours = false); @@ -135,14 +147,31 @@ class _ChecklistRetourScreenState extends State { ), 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()), - ], + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 26, + ), + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 780) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _colonneGauche(), + const SizedBox(height: 22), + _colonneDroite(), + ], + ); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 3, child: _colonneGauche()), + const SizedBox(width: 20), + Expanded(flex: 2, child: _colonneDroite()), + ], + ); + }, ), ), ), @@ -168,7 +197,10 @@ class _ChecklistRetourScreenState extends State { decoration: BoxDecoration( color: EnsupColors.soft, borderRadius: BorderRadius.circular(12), - border: Border.all(color: EnsupColors.teal.withValues(alpha: 0.15), width: 1.5), + border: Border.all( + color: EnsupColors.teal.withValues(alpha: 0.15), + width: 1.5, + ), ), child: Row( children: [ @@ -188,11 +220,17 @@ class _ChecklistRetourScreenState extends State { children: [ Text( materiel.nom, - style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w800, fontSize: 16), + style: GoogleFonts.darkerGrotesque( + fontWeight: FontWeight.w800, + fontSize: 16, + ), ), Text( "Emprunté le ${widget.emprunt.dateEmprunt}", - style: GoogleFonts.titilliumWeb(fontSize: 12, color: EnsupColors.muted), + style: GoogleFonts.titilliumWeb( + fontSize: 12, + color: EnsupColors.muted, + ), ), ], ), @@ -234,17 +272,25 @@ class _ChecklistRetourScreenState extends State { ? const SizedBox( width: 18, height: 18, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), ) : const Icon(Icons.check, size: 18), label: Text( _envoiEnCours ? "Enregistrement..." : "Valider le retour", - style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 16), + 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)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), ), ), diff --git a/eme-frontend/lib/screens/detail_screen.dart b/eme-frontend/lib/screens/detail_screen.dart index 5d68078..c7dc130 100644 --- a/eme-frontend/lib/screens/detail_screen.dart +++ b/eme-frontend/lib/screens/detail_screen.dart @@ -55,14 +55,34 @@ class DetailScreen extends StatelessWidget { ), 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)), - ], + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 26, + ), + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 760) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _colonneGauche(), + const SizedBox(height: 20), + _colonneDroite(context), + ], + ); + } + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(flex: 3, child: _colonneGauche()), + const SizedBox(width: 20), + Expanded( + flex: 2, + child: _colonneDroite(context), + ), + ], + ); + }, ), ), ), @@ -84,17 +104,35 @@ class DetailScreen extends StatelessWidget { Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( - color: EnsupColors.soft, + color: Colors.white, borderRadius: BorderRadius.circular(12), border: Border.all(color: EnsupColors.line, width: 1.5), + boxShadow: [ + BoxShadow( + color: EnsupColors.text.withValues(alpha: 0.04), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: EnsupColors.cyan.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(item.icon, color: EnsupColors.blue3, size: 21), + ), + const SizedBox(height: 12), Text( item.nom, style: GoogleFonts.darkerGrotesque( - fontSize: 20, + fontSize: 24, + height: 1, fontWeight: FontWeight.w900, color: EnsupColors.text, ), @@ -139,15 +177,25 @@ class DetailScreen extends StatelessWidget { border: Border(bottom: BorderSide(color: EnsupColors.line)), ), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(cle, style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted)), Text( - valeur, + cle, style: GoogleFonts.titilliumWeb( fontSize: 13, - fontWeight: FontWeight.w600, - color: valueColor ?? EnsupColors.text, + color: EnsupColors.muted, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Text( + valeur, + textAlign: TextAlign.right, + style: GoogleFonts.titilliumWeb( + fontSize: 13, + fontWeight: FontWeight.w600, + color: valueColor ?? EnsupColors.text, + ), ), ), ], @@ -161,7 +209,13 @@ class DetailScreen extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("Statut", style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted)), + Text( + "Statut", + style: GoogleFonts.titilliumWeb( + fontSize: 13, + color: EnsupColors.muted, + ), + ), StatusPill(disponible: item.disponible), ], ), @@ -193,18 +247,39 @@ class DetailScreen extends StatelessWidget { height: 180, width: double.infinity, decoration: BoxDecoration( - color: EnsupColors.soft, + color: EnsupColors.cyan.withValues(alpha: 0.045), borderRadius: BorderRadius.circular(12), - border: Border.all(color: EnsupColors.line, width: 1.5), + border: Border.all( + color: EnsupColors.cyan.withValues(alpha: 0.18), + width: 1.5, + ), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(item.icon, size: 48, color: EnsupColors.muted), + Container( + width: 72, + height: 72, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: EnsupColors.blue3.withValues(alpha: 0.1), + blurRadius: 14, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon(item.icon, size: 36, color: EnsupColors.blue3), + ), const SizedBox(height: 10), Text( "Aperçu du matériel", - style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted), + style: GoogleFonts.titilliumWeb( + fontSize: 13, + color: EnsupColors.muted, + ), ), ], ), @@ -217,12 +292,17 @@ class DetailScreen extends StatelessWidget { icon: const Icon(Icons.arrow_forward, size: 18), label: Text( "Démarrer l'emprunt", - style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 16), + 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)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), ), ), @@ -235,11 +315,16 @@ class DetailScreen extends StatelessWidget { foregroundColor: EnsupColors.text2, side: const BorderSide(color: EnsupColors.line, width: 1.5), padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), child: Text( "Retour au catalogue", - style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 15), + style: GoogleFonts.darkerGrotesque( + fontWeight: FontWeight.w700, + fontSize: 15, + ), ), ), ), diff --git a/eme-frontend/lib/screens/home_screen.dart b/eme-frontend/lib/screens/home_screen.dart index c9e8f5a..ae84be3 100644 --- a/eme-frontend/lib/screens/home_screen.dart +++ b/eme-frontend/lib/screens/home_screen.dart @@ -46,60 +46,97 @@ class HomeScreen extends StatelessWidget { ), Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 28), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const ProfileCard( - name: DemoIdentity.userName, - subtitle: DemoIdentity.userSubtitle, - initials: DemoIdentity.userInitials, - ), - const SizedBox(height: 24), - Text( - "Que souhaitez-vous faire ?", - style: GoogleFonts.darkerGrotesque( - fontSize: 28, - fontWeight: FontWeight.w900, - color: EnsupColors.text, - ), - ), - const SizedBox(height: 18), - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: ActionCard( + padding: const EdgeInsets.symmetric( + horizontal: 28, + vertical: 26, + ), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1040), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ProfileCard( + name: DemoIdentity.userName, + subtitle: DemoIdentity.userSubtitle, + initials: DemoIdentity.userInitials, + ), + const SizedBox(height: 28), + Text( + "Que souhaitez-vous faire ?", + style: GoogleFonts.darkerGrotesque( + fontSize: 30, + height: 1, + fontWeight: FontWeight.w900, + color: EnsupColors.text, + ), + ), + const SizedBox(height: 6), + Text( + "Choisissez l’opération à réaliser sur ce poste.", + style: GoogleFonts.titilliumWeb( + fontSize: 14, + color: EnsupColors.muted, + ), + ), + const SizedBox(height: 20), + LayoutBuilder( + builder: (context, constraints) { + final emprunter = ActionCard( icon: Icons.inventory_2_outlined, title: "EMPRUNTER", description: - "Accéder au catalogue et emprunter du matériel éducatif", + "Consulter les équipements disponibles et démarrer un nouvel emprunt.", color: EnsupColors.cyan, onTap: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const CatalogueScreen()), + MaterialPageRoute( + builder: (_) => + const CatalogueScreen(), + ), ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: ActionCard( + ); + final restituer = ActionCard( icon: Icons.assignment_return_outlined, title: "RESTITUER", description: - "Restituer un matériel que vous avez emprunté", + "Sélectionner un emprunt en cours et contrôler le matériel rendu.", color: EnsupColors.teal, onTap: () => Navigator.of(context).push( MaterialPageRoute( - builder: (_) => const RestitutionSelectScreen(), + builder: (_) => + const RestitutionSelectScreen(), ), ), - ), - ), - ], - ), + ); + + if (constraints.maxWidth < 720) { + return Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + emprunter, + const SizedBox(height: 14), + restituer, + ], + ); + } + + return IntrinsicHeight( + child: Row( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + Expanded(child: emprunter), + const SizedBox(width: 16), + Expanded(child: restituer), + ], + ), + ); + }, + ), + ], ), - ], + ), ), ), ), diff --git a/eme-frontend/lib/screens/restitution_select_screen.dart b/eme-frontend/lib/screens/restitution_select_screen.dart index f0c764f..ed311a2 100644 --- a/eme-frontend/lib/screens/restitution_select_screen.dart +++ b/eme-frontend/lib/screens/restitution_select_screen.dart @@ -14,7 +14,8 @@ class RestitutionSelectScreen extends StatefulWidget { const RestitutionSelectScreen({super.key}); @override - State createState() => _RestitutionSelectScreenState(); + State createState() => + _RestitutionSelectScreenState(); } class _RestitutionSelectScreenState extends State { @@ -40,16 +41,18 @@ class _RestitutionSelectScreenState extends State { } Navigator.of(context).push( MaterialPageRoute( - builder: (_) => ChecklistRetourScreen(emprunt: emprunt.copyWith(materiel: detail)), + builder: (_) => ChecklistRetourScreen( + emprunt: emprunt.copyWith(materiel: detail), + ), ), ); } catch (error) { if (!mounted) { return; } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(error.toString())), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(error.toString()))); } finally { if (mounted) { setState(() => _chargementId = null); @@ -91,12 +94,18 @@ class _RestitutionSelectScreenState extends State { ), Expanded( child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 28), + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 28, + ), child: FutureBuilder>( future: _future, builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); + if (snapshot.connectionState == + ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); } if (snapshot.hasError) { return _ErreurState( @@ -128,12 +137,21 @@ class _RestitutionSelectScreenState extends State { Text( "Sélectionner le matériel à restituer", style: GoogleFonts.darkerGrotesque( - fontSize: 24, + fontSize: 28, + height: 1, fontWeight: FontWeight.w900, color: EnsupColors.text, ), ), - const SizedBox(height: 16), + const SizedBox(height: 6), + Text( + "${emprunts.length} emprunt${emprunts.length > 1 ? "s" : ""} en cours associé${emprunts.length > 1 ? "s" : ""} à votre compte.", + style: GoogleFonts.titilliumWeb( + fontSize: 13, + color: EnsupColors.muted, + ), + ), + const SizedBox(height: 18), Expanded( child: emprunts.isEmpty ? const _EmptyState() @@ -174,70 +192,144 @@ class _EmpruntCard extends StatelessWidget { 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: EnsupColors.teal.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(10), - ), - child: Icon(emprunt.materiel.icon, color: EnsupColors.teal, size: 20), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - emprunt.materiel.nom, - style: GoogleFonts.darkerGrotesque( - fontWeight: FontWeight.w700, - fontSize: 16, - color: EnsupColors.text, - ), - ), - const SizedBox(height: 2), - Text( - "Emprunté le ${emprunt.dateEmprunt} · Retour prévu ${emprunt.retourPrevu}", - style: GoogleFonts.titilliumWeb(fontSize: 12, color: EnsupColors.muted), - ), - ], - ), - ), - const SizedBox(width: 10), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: const Color(0xFFFEF9C3), - borderRadius: BorderRadius.circular(999), - ), - child: Text( - emprunt.statut == "EN_RETARD" ? "En retard" : "En cours", - style: GoogleFonts.darkerGrotesque( - fontSize: 13, - fontWeight: FontWeight.w700, - color: const Color(0xFFA16207), - ), - ), - ), - const SizedBox(width: 12), - FilledButton( - onPressed: loading ? null : onRestituer, - style: FilledButton.styleFrom( - backgroundColor: EnsupColors.cyan, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - ), - child: Text( - loading ? "Chargement..." : "Restituer", - style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 14), - ), + boxShadow: [ + BoxShadow( + color: EnsupColors.text.withValues(alpha: 0.035), + blurRadius: 10, + offset: const Offset(0, 3), ), ], ), + child: LayoutBuilder( + builder: (context, constraints) { + final compact = constraints.maxWidth < 650; + final information = Row( + children: [ + Container( + width: 46, + height: 46, + decoration: BoxDecoration( + color: EnsupColors.teal.withValues(alpha: 0.09), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + emprunt.materiel.icon, + color: EnsupColors.teal, + size: 21, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + emprunt.materiel.nom, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.darkerGrotesque( + fontWeight: FontWeight.w800, + fontSize: 17, + height: 1.05, + color: EnsupColors.text, + ), + ), + const SizedBox(height: 4), + Text( + "Emprunté le ${emprunt.dateEmprunt} · Retour prévu ${emprunt.retourPrevu}", + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.titilliumWeb( + fontSize: 12, + color: EnsupColors.muted, + ), + ), + ], + ), + ), + ], + ); + final actions = Wrap( + spacing: 10, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.end, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 11, + vertical: 5, + ), + decoration: BoxDecoration( + color: emprunt.statut == "EN_RETARD" + ? EnsupColors.red.withValues(alpha: 0.09) + : const Color(0xFFFEF9C3), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + emprunt.statut == "EN_RETARD" ? "En retard" : "En cours", + style: GoogleFonts.darkerGrotesque( + fontSize: 13, + fontWeight: FontWeight.w700, + color: emprunt.statut == "EN_RETARD" + ? EnsupColors.red + : const Color(0xFFA16207), + ), + ), + ), + FilledButton.icon( + onPressed: loading ? null : onRestituer, + style: FilledButton.styleFrom( + backgroundColor: EnsupColors.cyan, + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 11, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(9), + ), + ), + icon: loading + ? const SizedBox( + width: 15, + height: 15, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.assignment_return_outlined, size: 16), + label: Text( + loading ? "Chargement" : "Restituer", + style: GoogleFonts.darkerGrotesque( + fontWeight: FontWeight.w700, + fontSize: 14, + ), + ), + ), + ], + ); + + if (compact) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + information, + const SizedBox(height: 14), + Align(alignment: Alignment.centerRight, child: actions), + ], + ); + } + + return Row( + children: [ + Expanded(child: information), + const SizedBox(width: 16), + actions, + ], + ); + }, + ), ); } } @@ -268,12 +360,19 @@ class _ErreurState extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.cloud_off_outlined, size: 40, color: EnsupColors.muted), + const Icon( + Icons.cloud_off_outlined, + size: 40, + color: EnsupColors.muted, + ), const SizedBox(height: 12), Text( message, textAlign: TextAlign.center, - style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.text2), + style: GoogleFonts.titilliumWeb( + fontSize: 14, + color: EnsupColors.text2, + ), ), const SizedBox(height: 14), OutlinedButton( diff --git a/eme-frontend/lib/widgets/action_card.dart b/eme-frontend/lib/widgets/action_card.dart index 0fa4565..82f2fa1 100644 --- a/eme-frontend/lib/widgets/action_card.dart +++ b/eme-frontend/lib/widgets/action_card.dart @@ -3,7 +3,7 @@ import "package:google_fonts/google_fonts.dart"; import "../theme/ensup_colors.dart"; /// Grande carte d'action de l'accueil (Emprunter / Restituer). -class ActionCard extends StatelessWidget { +class ActionCard extends StatefulWidget { const ActionCard({ super.key, required this.icon, @@ -19,56 +19,107 @@ class ActionCard extends StatelessWidget { final Color color; final VoidCallback? onTap; + @override + State createState() => _ActionCardState(); +} + +class _ActionCardState extends State { + bool _hovered = false; + @override Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(16), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), + final color = widget.color; + return MouseRegion( + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), + cursor: SystemMouseCursors.click, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOut, + constraints: const BoxConstraints(minHeight: 230), decoration: BoxDecoration( - color: color.withValues(alpha: 0.07), + color: _hovered + ? color.withValues(alpha: 0.1) + : color.withValues(alpha: 0.06), borderRadius: BorderRadius.circular(16), - border: Border.all(color: color.withValues(alpha: 0.2), width: 2), + border: Border.all( + color: color.withValues(alpha: _hovered ? 0.55 : 0.22), + width: _hovered ? 2 : 1.5, + ), + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: _hovered ? 0.16 : 0.06), + blurRadius: _hovered ? 22 : 10, + offset: Offset(0, _hovered ? 8 : 3), + ), + ], ), - child: Column( - children: [ - Container( - width: 60, - height: 60, - decoration: BoxDecoration( - gradient: LinearGradient(colors: [color, color.withValues(alpha: 0.75)]), - borderRadius: BorderRadius.circular(18), - boxShadow: [ - BoxShadow( - color: color.withValues(alpha: 0.35), - blurRadius: 16, - offset: const Offset(0, 4), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.onTap, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 180), + width: 60, + height: 60, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(18), + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: 0.3), + blurRadius: _hovered ? 18 : 12, + offset: const Offset(0, 5), + ), + ], + ), + child: Icon(widget.icon, color: Colors.white, size: 28), + ), + const SizedBox(height: 16), + Text( + widget.title, + textAlign: TextAlign.center, + style: GoogleFonts.darkerGrotesque( + fontSize: 25, + height: 1, + fontWeight: FontWeight.w900, + color: EnsupColors.text, + ), + ), + const SizedBox(height: 8), + Text( + widget.description, + textAlign: TextAlign.center, + style: GoogleFonts.titilliumWeb( + fontSize: 13, + color: EnsupColors.muted, + height: 1.45, + ), + ), + const SizedBox(height: 16), + Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + shape: BoxShape.circle, + ), + child: Icon( + Icons.arrow_forward_rounded, + size: 17, + color: color, + ), ), ], ), - child: Icon(icon, color: Colors.white, size: 28), ), - const SizedBox(height: 14), - Text( - title, - style: GoogleFonts.darkerGrotesque( - fontSize: 26, - fontWeight: FontWeight.w900, - color: EnsupColors.text, - ), - ), - const SizedBox(height: 6), - Text( - description, - textAlign: TextAlign.center, - style: GoogleFonts.titilliumWeb( - fontSize: 13, - color: EnsupColors.muted, - height: 1.5, - ), - ), - ], + ), ), ), ); diff --git a/eme-frontend/lib/widgets/ensup_top_bar.dart b/eme-frontend/lib/widgets/ensup_top_bar.dart index fd3ccf5..e94e654 100644 --- a/eme-frontend/lib/widgets/ensup_top_bar.dart +++ b/eme-frontend/lib/widgets/ensup_top_bar.dart @@ -27,96 +27,122 @@ class EnsupTopBar extends StatelessWidget { final onBack = this.onBack; return Container( - height: 56, - padding: const EdgeInsets.symmetric(horizontal: 20), + height: 60, + padding: const EdgeInsets.symmetric(horizontal: 18), color: EnsupColors.blue3, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( + child: LayoutBuilder( + builder: (context, constraints) { + final compact = constraints.maxWidth < 620; + final showCampus = constraints.maxWidth >= 520; + final showName = constraints.maxWidth >= 760; + + return Row( children: [ if (onBack != null) ...[ - _BackButton(onTap: onBack), - const SizedBox(width: 8), + _BackButton(onTap: onBack, compact: compact), + SizedBox(width: compact ? 5 : 8), ], const Icon(Icons.badge_outlined, color: Colors.white, size: 18), - const SizedBox(width: 10), - Text( - brandText, - style: GoogleFonts.darkerGrotesque( - color: Colors.white, - fontWeight: FontWeight.w900, - fontSize: 22, - letterSpacing: 0.3, - ), - ), - ], - ), - Row( - children: [ - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.15), - borderRadius: BorderRadius.circular(999), - border: Border.all(color: Colors.white.withValues(alpha: 0.2)), - ), + const SizedBox(width: 9), + Expanded( child: Text( - campusTag, - style: GoogleFonts.titilliumWeb( + brandText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.darkerGrotesque( color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w900, + fontSize: compact ? 18 : 21, ), ), ), - if (name != null && initials != null) ...[ - const SizedBox(width: 12), - _Avatar(initials: initials), - const SizedBox(width: 8), - Text( - name, - style: GoogleFonts.titilliumWeb( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.w600, + const SizedBox(width: 10), + if (showCampus) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 11, + vertical: 5, + ), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.14), + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + ), + ), + child: Text( + campusTag, + style: GoogleFonts.titilliumWeb( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w600, + ), ), ), + if (name != null && initials != null) ...[ + const SizedBox(width: 10), + _Avatar(initials: initials), + if (showName) ...[ + const SizedBox(width: 8), + Text( + name, + style: GoogleFonts.titilliumWeb( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], ], ], - ), - ], + ); + }, ), ); } } class _BackButton extends StatelessWidget { - const _BackButton({required this.onTap}); + const _BackButton({required this.onTap, required this.compact}); final VoidCallback onTap; + final bool compact; @override Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.arrow_back, size: 16, color: Colors.white.withValues(alpha: 0.85)), - const SizedBox(width: 6), - Text( - "Retour", - style: GoogleFonts.titilliumWeb( - color: Colors.white.withValues(alpha: 0.85), - fontSize: 13, - fontWeight: FontWeight.w600, + return Tooltip( + message: "Retour", + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(7), + child: Container( + height: 34, + padding: EdgeInsets.symmetric(horizontal: compact ? 8 : 10), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(7), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.arrow_back_rounded, + size: 17, + color: Colors.white.withValues(alpha: 0.9), ), - ), - ], + if (!compact) ...[ + const SizedBox(width: 6), + Text( + "Retour", + style: GoogleFonts.titilliumWeb( + color: Colors.white.withValues(alpha: 0.9), + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ], + ), ), ), ); @@ -135,8 +161,11 @@ class _Avatar extends StatelessWidget { height: 30, decoration: BoxDecoration( shape: BoxShape.circle, - gradient: const LinearGradient(colors: [EnsupColors.teal, EnsupColors.cyan]), - border: Border.all(color: Colors.white.withValues(alpha: 0.3), width: 2), + color: EnsupColors.purple, + border: Border.all( + color: Colors.white.withValues(alpha: 0.3), + width: 2, + ), ), alignment: Alignment.center, child: Text( diff --git a/eme-frontend/lib/widgets/etat_checklist.dart b/eme-frontend/lib/widgets/etat_checklist.dart index 3efd452..aa8d7e2 100644 --- a/eme-frontend/lib/widgets/etat_checklist.dart +++ b/eme-frontend/lib/widgets/etat_checklist.dart @@ -37,7 +37,11 @@ class EtatBox extends StatelessWidget { case EtatChecklist.deteriore: background = const Color(0xFFFFFBEB); border = const Color(0xFFFDE68A); - contenu = const Icon(Icons.priority_high, size: 16, color: Color(0xFFD97706)); + contenu = const Icon( + Icons.priority_high, + size: 16, + color: Color(0xFFD97706), + ); } } @@ -73,15 +77,36 @@ class ChecklistTableau extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - _entete(), - for (var i = 0; i < accessoires.length; i++) _ligne(i), - ], + return LayoutBuilder( + builder: (context, constraints) { + final columnWidth = constraints.maxWidth < 520 ? 58.0 : 72.0; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: EnsupColors.line, width: 1.5), + boxShadow: [ + BoxShadow( + color: EnsupColors.text.withValues(alpha: 0.035), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + _entete(columnWidth), + for (var i = 0; i < accessoires.length; i++) + _ligne(i, columnWidth), + ], + ), + ); + }, ); } - Widget _entete() { + Widget _entete(double columnWidth) { return Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: const BoxDecoration( @@ -90,9 +115,9 @@ class ChecklistTableau extends StatelessWidget { child: Row( children: [ Expanded(child: _texteEntete("ÉLÉMENT")), - _colonneEntete("PRÉSENT"), - _colonneEntete("ABSENT"), - _colonneEntete("DÉTÉRIORÉ"), + _colonneEntete("PRÉSENT", columnWidth), + _colonneEntete("ABSENT", columnWidth), + _colonneEntete("DÉTÉRIORÉ", columnWidth), ], ), ); @@ -110,11 +135,16 @@ class ChecklistTableau extends StatelessWidget { ); } - Widget _colonneEntete(String label) { - return SizedBox(width: 72, child: Center(child: _texteEntete(label))); + Widget _colonneEntete(String label, double width) { + return SizedBox( + width: width, + child: Center( + child: FittedBox(fit: BoxFit.scaleDown, child: _texteEntete(label)), + ), + ); } - Widget _ligne(int index) { + Widget _ligne(int index, double columnWidth) { return Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: const BoxDecoration( @@ -125,20 +155,23 @@ class ChecklistTableau extends StatelessWidget { Expanded( child: Text( accessoires[index], - style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.text2), + style: GoogleFonts.titilliumWeb( + fontSize: 14, + color: EnsupColors.text2, + ), ), ), - _case(index, EtatChecklist.present), - _case(index, EtatChecklist.absent), - _case(index, EtatChecklist.deteriore), + _case(index, EtatChecklist.present, columnWidth), + _case(index, EtatChecklist.absent, columnWidth), + _case(index, EtatChecklist.deteriore, columnWidth), ], ), ); } - Widget _case(int index, EtatChecklist type) { + Widget _case(int index, EtatChecklist type, double width) { return SizedBox( - width: 72, + width: width, child: Center( child: EtatBox( type: type, @@ -152,7 +185,11 @@ class ChecklistTableau extends StatelessWidget { /// Encadré d'instructions / rappel à côté d'une checklist. class ChecklistInstructions extends StatelessWidget { - const ChecklistInstructions({super.key, required this.titre, required this.texte}); + const ChecklistInstructions({ + super.key, + required this.titre, + required this.texte, + }); final String titre; final String texte; @@ -164,7 +201,10 @@ class ChecklistInstructions extends StatelessWidget { 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), + border: Border.all( + color: EnsupColors.blue2.withValues(alpha: 0.15), + width: 1.5, + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -181,7 +221,11 @@ class ChecklistInstructions extends StatelessWidget { const SizedBox(height: 8), Text( texte, - style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.text2, height: 1.6), + style: GoogleFonts.titilliumWeb( + fontSize: 13, + color: EnsupColors.text2, + height: 1.6, + ), ), ], ), @@ -208,10 +252,16 @@ class ChecklistCommentaire extends StatelessWidget { style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.text), decoration: InputDecoration( hintText: hint, - hintStyle: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted), + hintStyle: GoogleFonts.titilliumWeb( + fontSize: 13, + color: EnsupColors.muted, + ), filled: true, fillColor: Colors.white, - contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + contentPadding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(9), borderSide: const BorderSide(color: EnsupColors.line, width: 1.5), diff --git a/eme-frontend/lib/widgets/identification_option.dart b/eme-frontend/lib/widgets/identification_option.dart index 2b273d9..cc161c8 100644 --- a/eme-frontend/lib/widgets/identification_option.dart +++ b/eme-frontend/lib/widgets/identification_option.dart @@ -3,7 +3,7 @@ import "package:google_fonts/google_fonts.dart"; import "../theme/ensup_colors.dart"; /// Option d'identification de l'écran de connexion (QR ou mail ENSUP). -class IdentificationOption extends StatelessWidget { +class IdentificationOption extends StatefulWidget { const IdentificationOption({ super.key, required this.icon, @@ -19,56 +19,102 @@ class IdentificationOption extends StatelessWidget { final String description; final VoidCallback? onTap; + @override + State createState() => _IdentificationOptionState(); +} + +class _IdentificationOptionState extends State { + bool _hovered = false; + @override Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), + return MouseRegion( + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), + cursor: SystemMouseCursors.click, + child: AnimatedContainer( + duration: const Duration(milliseconds: 160), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), - border: Border.all(color: EnsupColors.line, width: 2), - ), - child: Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: iconColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _hovered ? widget.iconColor : EnsupColors.line, + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: widget.iconColor.withValues( + alpha: _hovered ? 0.12 : 0.035, ), - child: Icon(icon, color: iconColor, size: 24), + blurRadius: _hovered ? 16 : 8, + offset: const Offset(0, 4), ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 17), + child: Row( children: [ - Text( - label, - style: GoogleFonts.darkerGrotesque( - fontWeight: FontWeight.w700, - fontSize: 18, - color: EnsupColors.text, + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: widget.iconColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(widget.icon, color: widget.iconColor, size: 24), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.label, + style: GoogleFonts.darkerGrotesque( + fontWeight: FontWeight.w800, + fontSize: 18, + height: 1.05, + color: EnsupColors.text, + ), + ), + const SizedBox(height: 4), + Text( + widget.description, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.titilliumWeb( + fontSize: 12, + color: EnsupColors.muted, + height: 1.35, + ), + ), + ], ), ), - const SizedBox(height: 3), - Text( - description, - style: GoogleFonts.titilliumWeb( - fontSize: 12, - color: EnsupColors.muted, - height: 1.4, + const SizedBox(width: 8), + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: widget.iconColor.withValues(alpha: 0.08), + shape: BoxShape.circle, + ), + child: Icon( + Icons.chevron_right_rounded, + color: widget.iconColor, + size: 19, ), ), ], ), ), - const Icon(Icons.chevron_right, color: EnsupColors.muted, size: 20), - ], + ), ), ), ); diff --git a/eme-frontend/lib/widgets/materiel_card.dart b/eme-frontend/lib/widgets/materiel_card.dart index ef773cd..cb81b3f 100644 --- a/eme-frontend/lib/widgets/materiel_card.dart +++ b/eme-frontend/lib/widgets/materiel_card.dart @@ -24,65 +24,142 @@ class MaterielCard extends StatelessWidget { 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), + boxShadow: [ + BoxShadow( + color: EnsupColors.text.withValues(alpha: 0.035), + blurRadius: 10, + offset: const Offset(0, 3), ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + ], + ), + child: LayoutBuilder( + builder: (context, constraints) { + final compact = constraints.maxWidth < 620; + final information = Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 46, + height: 46, + decoration: BoxDecoration( + color: accent.withValues(alpha: 0.09), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(item.icon, color: accent, size: 21), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.nom, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.darkerGrotesque( + fontWeight: FontWeight.w800, + fontSize: 17, + height: 1.05, + color: EnsupColors.text, + ), + ), + const SizedBox(height: 4), + Text( + "${item.categorie} · Réf. ${item.reference}", + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.titilliumWeb( + fontSize: 12, + color: EnsupColors.muted, + ), + ), + const SizedBox(height: 3), + Row( + children: [ + const Icon( + Icons.location_on_outlined, + size: 13, + color: EnsupColors.muted, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + item.campus, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.titilliumWeb( + fontSize: 11, + color: EnsupColors.muted, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ); + final actions = Wrap( + spacing: 10, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.end, + children: [ + StatusPill(disponible: disponible), + if (disponible) + FilledButton.icon( + onPressed: onSelect, + style: FilledButton.styleFrom( + backgroundColor: EnsupColors.cyan, + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 11, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(9), + ), + ), + icon: const Icon(Icons.arrow_forward_rounded, size: 16), + label: Text( + "Sélectionner", + style: GoogleFonts.darkerGrotesque( + fontWeight: FontWeight.w700, + fontSize: 14, + ), + ), + ) + else Text( - item.nom, + "Indisponible", style: GoogleFonts.darkerGrotesque( fontWeight: FontWeight.w700, - fontSize: 16, - color: EnsupColors.text, + fontSize: 14, + color: EnsupColors.muted, ), ), - const SizedBox(height: 2), - Text( - "${item.categorie} · Réf. ${item.reference} · ${item.campus}", - style: GoogleFonts.titilliumWeb(fontSize: 12, color: EnsupColors.muted), - ), + ], + ); + + if (compact) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + information, + const SizedBox(height: 14), + Align(alignment: Alignment.centerRight, child: actions), ], - ), - ), - 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, - ), - ), - ], + ); + } + + return Row( + children: [ + Expanded(child: information), + const SizedBox(width: 16), + actions, + ], + ); + }, ), ), ); diff --git a/eme-frontend/lib/widgets/profile_card.dart b/eme-frontend/lib/widgets/profile_card.dart index 40d519d..41a9241 100644 --- a/eme-frontend/lib/widgets/profile_card.dart +++ b/eme-frontend/lib/widgets/profile_card.dart @@ -18,74 +18,120 @@ class ProfileCard extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - EnsupColors.cyan.withValues(alpha: 0.06), - EnsupColors.teal.withValues(alpha: 0.04), - ], - ), + color: EnsupColors.cyan.withValues(alpha: 0.055), borderRadius: BorderRadius.circular(12), - border: Border.all(color: EnsupColors.cyan.withValues(alpha: 0.15), width: 1.5), - ), - child: Row( - children: [ - Container( - width: 48, - height: 48, - decoration: const BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient(colors: [EnsupColors.teal, EnsupColors.cyan]), - ), - alignment: Alignment.center, - child: Text( - initials, - style: GoogleFonts.darkerGrotesque( - color: Colors.white, - fontWeight: FontWeight.w900, - fontSize: 18, - ), - ), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - name, - style: GoogleFonts.darkerGrotesque( - fontWeight: FontWeight.w800, - fontSize: 20, - color: EnsupColors.text, - ), - ), - const SizedBox(height: 2), - Text( - subtitle, - style: GoogleFonts.titilliumWeb(fontSize: 12, color: EnsupColors.muted), - ), - ], - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), - decoration: BoxDecoration( - color: EnsupColors.green.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(999), - ), - child: Text( - "Connecté", - style: GoogleFonts.titilliumWeb( - fontSize: 12, - fontWeight: FontWeight.w600, - color: EnsupColors.green, - ), - ), + border: Border.all( + color: EnsupColors.cyan.withValues(alpha: 0.15), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: EnsupColors.blue3.withValues(alpha: 0.045), + blurRadius: 12, + offset: const Offset(0, 4), ), ], ), + child: LayoutBuilder( + builder: (context, constraints) { + final showStatus = constraints.maxWidth >= 430; + return Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: EnsupColors.teal, + border: Border.all(color: Colors.white, width: 3), + boxShadow: [ + BoxShadow( + color: EnsupColors.teal.withValues(alpha: 0.22), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + alignment: Alignment.center, + child: Text( + initials, + style: GoogleFonts.darkerGrotesque( + color: Colors.white, + fontWeight: FontWeight.w900, + fontSize: 18, + ), + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.darkerGrotesque( + fontWeight: FontWeight.w800, + fontSize: 20, + height: 1, + color: EnsupColors.text, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.titilliumWeb( + fontSize: 12, + color: EnsupColors.muted, + ), + ), + ], + ), + ), + if (showStatus) ...[ + const SizedBox(width: 12), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 11, + vertical: 6, + ), + decoration: BoxDecoration( + color: EnsupColors.green.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: EnsupColors.green.withValues(alpha: 0.18), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.check_circle, + size: 13, + color: EnsupColors.green, + ), + const SizedBox(width: 5), + Text( + "Identifié", + style: GoogleFonts.titilliumWeb( + fontSize: 11, + fontWeight: FontWeight.w700, + color: EnsupColors.green, + ), + ), + ], + ), + ), + ], + ], + ); + }, + ), ); } }