style(frontend): polish student workspace

This commit is contained in:
SaidSoighiri94
2026-07-23 10:27:27 +02:00
parent 1c8ed77ebf
commit 695b0d3965
12 changed files with 1162 additions and 484 deletions
+96 -24
View File
@@ -35,15 +35,18 @@ class _CatalogueScreenState extends State<CatalogueScreen> {
}
List<String> _categories(List<MaterielItem> 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<MaterielItem> _filtrer(List<MaterielItem> 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<CatalogueScreen> {
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<CatalogueScreen> {
),
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 lemprunt.",
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<List<MaterielItem>>(
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<CatalogueScreen> {
}).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<CatalogueScreen> {
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(
@@ -58,7 +58,9 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
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<ChecklistDepartScreen> {
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<ChecklistDepartScreen> {
),
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<ChecklistDepartScreen> {
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<ChecklistDepartScreen> {
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<ChecklistDepartScreen> {
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<ChecklistDepartScreen> {
? 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<ChecklistDepartScreen> {
? "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),
),
),
),
),
@@ -22,7 +22,8 @@ class ChecklistRetourScreen extends StatefulWidget {
}
class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
late final List<String> _elements = widget.emprunt.materiel.accessoires.isEmpty
late final List<String> _elements =
widget.emprunt.materiel.accessoires.isEmpty
? <String>[widget.emprunt.materiel.nom]
: widget.emprunt.materiel.accessoires;
late final List<EtatChecklist> _etats = List<EtatChecklist>.filled(
@@ -44,9 +45,16 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
final anomalies = <AnomalieRetour>[];
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<ChecklistRetourScreen> {
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<ChecklistRetourScreen> {
return;
}
final anomaliesAffichees = resultat.statut == "CLOTURE" ? <AnomalieRetour>[] : anomalies;
final anomaliesAffichees = resultat.statut == "CLOTURE"
? <AnomalieRetour>[]
: anomalies;
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ResultatRetourScreen(
@@ -80,9 +92,9 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
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<ChecklistRetourScreen> {
),
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<ChecklistRetourScreen> {
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<ChecklistRetourScreen> {
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<ChecklistRetourScreen> {
? 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),
),
),
),
),
+109 -24
View File
@@ -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,
),
),
),
),
+76 -39
View File
@@ -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 lopé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),
],
),
);
},
),
],
),
],
),
),
),
),
@@ -14,7 +14,8 @@ class RestitutionSelectScreen extends StatefulWidget {
const RestitutionSelectScreen({super.key});
@override
State<RestitutionSelectScreen> createState() => _RestitutionSelectScreenState();
State<RestitutionSelectScreen> createState() =>
_RestitutionSelectScreenState();
}
class _RestitutionSelectScreenState extends State<RestitutionSelectScreen> {
@@ -40,16 +41,18 @@ class _RestitutionSelectScreenState extends State<RestitutionSelectScreen> {
}
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<RestitutionSelectScreen> {
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 28),
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 28,
),
child: FutureBuilder<List<EmpruntItem>>(
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<RestitutionSelectScreen> {
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(