merge: student premium workspace

This commit is contained in:
SaidSoighiri94
2026-07-23 10:30:03 +02:00
13 changed files with 1175 additions and 486 deletions
+96 -24
View File
@@ -35,15 +35,18 @@ class _CatalogueScreenState extends State<CatalogueScreen> {
} }
List<String> _categories(List<MaterielItem> materiels) { 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]; return ["Tous", ...distinctes];
} }
List<MaterielItem> _filtrer(List<MaterielItem> materiels) { List<MaterielItem> _filtrer(List<MaterielItem> materiels) {
final terme = _recherche.trim().toLowerCase(); final terme = _recherche.trim().toLowerCase();
return materiels.where((materiel) { return materiels.where((materiel) {
final matchTexte = terme.isEmpty || materiel.nom.toLowerCase().contains(terme); final matchTexte =
final matchCategorie = _categorie == "Tous" || materiel.categorie == _categorie; terme.isEmpty || materiel.nom.toLowerCase().contains(terme);
final matchCategorie =
_categorie == "Tous" || materiel.categorie == _categorie;
return matchTexte && matchCategorie; return matchTexte && matchCategorie;
}).toList(); }).toList();
} }
@@ -55,16 +58,16 @@ class _CatalogueScreenState extends State<CatalogueScreen> {
if (!mounted) { if (!mounted) {
return; return;
} }
Navigator.of(context).push( Navigator.of(
MaterialPageRoute(builder: (_) => DetailScreen(item: detail)), context,
); ).push(MaterialPageRoute(builder: (_) => DetailScreen(item: detail)));
} catch (error) { } catch (error) {
if (!mounted) { if (!mounted) {
return; return;
} }
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(error.toString())), context,
); ).showSnackBar(SnackBar(content: Text(error.toString())));
} finally { } finally {
if (mounted) { if (mounted) {
setState(() => _selectionEnCoursId = null); setState(() => _selectionEnCoursId = null);
@@ -106,20 +109,42 @@ class _CatalogueScreenState extends State<CatalogueScreen> {
), ),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(32, 24, 32, 8), padding: const EdgeInsets.fromLTRB(24, 24, 24, 8),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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( _SearchField(
onChanged: (value) => setState(() => _recherche = value), onChanged: (value) =>
setState(() => _recherche = value),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
Expanded( Expanded(
child: FutureBuilder<List<MaterielItem>>( child: FutureBuilder<List<MaterielItem>>(
future: _future, future: _future,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState ==
return const Center(child: CircularProgressIndicator()); ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
} }
if (snapshot.hasError) { if (snapshot.hasError) {
return _ErreurState( return _ErreurState(
@@ -165,8 +190,25 @@ class _CatalogueScreenState extends State<CatalogueScreen> {
}).toList(), }).toList(),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Divider(color: EnsupColors.line, height: 1), Row(
const SizedBox(height: 16), 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( Expanded(
child: resultats.isEmpty child: resultats.isEmpty
? const _EmptyState() ? const _EmptyState()
@@ -177,7 +219,10 @@ class _CatalogueScreenState extends State<CatalogueScreen> {
final item = resultats[index]; final item = resultats[index];
return Stack( return Stack(
children: [ children: [
MaterielCard(item: item, onSelect: () => _selectionner(item)), MaterielCard(
item: item,
onSelect: () => _selectionner(item),
),
if (_selectionEnCoursId == item.id) if (_selectionEnCoursId == item.id)
const Positioned.fill( const Positioned.fill(
child: ColoredBox( child: ColoredBox(
@@ -217,12 +262,18 @@ class _SearchField extends StatelessWidget {
Expanded( Expanded(
child: TextField( child: TextField(
onChanged: onChanged, onChanged: onChanged,
style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.text), style: GoogleFonts.titilliumWeb(
fontSize: 14,
color: EnsupColors.text,
),
decoration: InputDecoration( decoration: InputDecoration(
isCollapsed: true, isCollapsed: true,
border: InputBorder.none, border: InputBorder.none,
hintText: "Rechercher un matériel...", 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 { 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 String label;
final bool selected; final bool selected;
@@ -249,7 +304,10 @@ class _CategoryChip extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? EnsupColors.cyan : Colors.white, color: selected ? EnsupColors.cyan : Colors.white,
borderRadius: BorderRadius.circular(999), 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( child: Text(
label, label,
@@ -273,11 +331,18 @@ class _EmptyState extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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), const SizedBox(height: 10),
Text( Text(
"Aucun matériel ne correspond", "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( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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), const SizedBox(height: 12),
Text( Text(
message, message,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.text2), style: GoogleFonts.titilliumWeb(
fontSize: 14,
color: EnsupColors.text2,
),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
OutlinedButton( OutlinedButton(
@@ -58,7 +58,9 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
return { return {
"nomElement": entry.value, "nomElement": entry.value,
"etat": _etatApi(_etats[entry.key]), "etat": _etatApi(_etats[entry.key]),
"quantiteConstatee": _etats[entry.key] == EtatChecklist.absent ? 0 : 1, "quantiteConstatee": _etats[entry.key] == EtatChecklist.absent
? 0
: 1,
}; };
}).toList(), }).toList(),
); );
@@ -75,9 +77,9 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
if (!mounted) { if (!mounted) {
return; return;
} }
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(error.toString())), context,
); ).showSnackBar(SnackBar(content: Text(error.toString())));
} finally { } finally {
if (mounted) { if (mounted) {
setState(() => _envoiEnCours = false); setState(() => _envoiEnCours = false);
@@ -130,14 +132,31 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
), ),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 28), padding: const EdgeInsets.symmetric(
child: Row( horizontal: 24,
crossAxisAlignment: CrossAxisAlignment.start, vertical: 26,
children: [ ),
Expanded(flex: 3, child: _colonneGauche()), child: LayoutBuilder(
const SizedBox(width: 20), builder: (context, constraints) {
Expanded(flex: 2, child: _colonneDroite()), 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( decoration: BoxDecoration(
color: EnsupColors.soft, color: EnsupColors.soft,
borderRadius: BorderRadius.circular(12), 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( child: Row(
children: [ children: [
@@ -172,7 +194,11 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
color: EnsupColors.cyan.withValues(alpha: 0.1), color: EnsupColors.cyan.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(9), 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), const SizedBox(width: 12),
Expanded( Expanded(
@@ -181,11 +207,17 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
children: [ children: [
Text( Text(
widget.item.nom, widget.item.nom,
style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w800, fontSize: 16), style: GoogleFonts.darkerGrotesque(
fontWeight: FontWeight.w800,
fontSize: 16,
),
), ),
Text( Text(
"Vérifiez chaque élément physiquement avant de valider", "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( ? const SizedBox(
width: 18, width: 18,
height: 18, height: 18,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
) )
: const Icon(Icons.check, size: 18), : const Icon(Icons.check, size: 18),
label: Text( label: Text(
@@ -233,12 +268,17 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
? "Enregistrement..." ? "Enregistrement..."
: "Valider la checklist et confirmer l'emprunt", : "Valider la checklist et confirmer l'emprunt",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 15), style: GoogleFonts.darkerGrotesque(
fontWeight: FontWeight.w700,
fontSize: 15,
),
), ),
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: EnsupColors.blue3, backgroundColor: EnsupColors.blue3,
padding: const EdgeInsets.symmetric(vertical: 16), 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> { 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] ? <String>[widget.emprunt.materiel.nom]
: widget.emprunt.materiel.accessoires; : widget.emprunt.materiel.accessoires;
late final List<EtatChecklist> _etats = List<EtatChecklist>.filled( late final List<EtatChecklist> _etats = List<EtatChecklist>.filled(
@@ -44,9 +45,16 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
final anomalies = <AnomalieRetour>[]; final anomalies = <AnomalieRetour>[];
for (var i = 0; i < _etats.length; i++) { for (var i = 0; i < _etats.length; i++) {
if (_etats[i] == EtatChecklist.absent) { 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) { } 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 { return {
"nomElement": entry.value, "nomElement": entry.value,
"etat": _etatApi(_etats[entry.key]), "etat": _etatApi(_etats[entry.key]),
"quantiteConstatee": _etats[entry.key] == EtatChecklist.absent ? 0 : 1, "quantiteConstatee": _etats[entry.key] == EtatChecklist.absent
? 0
: 1,
}; };
}).toList(), }).toList(),
); );
@@ -67,7 +77,9 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
return; return;
} }
final anomaliesAffichees = resultat.statut == "CLOTURE" ? <AnomalieRetour>[] : anomalies; final anomaliesAffichees = resultat.statut == "CLOTURE"
? <AnomalieRetour>[]
: anomalies;
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => ResultatRetourScreen( builder: (_) => ResultatRetourScreen(
@@ -80,9 +92,9 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
if (!mounted) { if (!mounted) {
return; return;
} }
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(error.toString())), context,
); ).showSnackBar(SnackBar(content: Text(error.toString())));
} finally { } finally {
if (mounted) { if (mounted) {
setState(() => _envoiEnCours = false); setState(() => _envoiEnCours = false);
@@ -135,14 +147,31 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
), ),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 28), padding: const EdgeInsets.symmetric(
child: Row( horizontal: 24,
crossAxisAlignment: CrossAxisAlignment.start, vertical: 26,
children: [ ),
Expanded(flex: 3, child: _colonneGauche()), child: LayoutBuilder(
const SizedBox(width: 20), builder: (context, constraints) {
Expanded(flex: 2, child: _colonneDroite()), 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( decoration: BoxDecoration(
color: EnsupColors.soft, color: EnsupColors.soft,
borderRadius: BorderRadius.circular(12), 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( child: Row(
children: [ children: [
@@ -188,11 +220,17 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
children: [ children: [
Text( Text(
materiel.nom, materiel.nom,
style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w800, fontSize: 16), style: GoogleFonts.darkerGrotesque(
fontWeight: FontWeight.w800,
fontSize: 16,
),
), ),
Text( Text(
"Emprunté le ${widget.emprunt.dateEmprunt}", "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( ? const SizedBox(
width: 18, width: 18,
height: 18, height: 18,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
) )
: const Icon(Icons.check, size: 18), : const Icon(Icons.check, size: 18),
label: Text( label: Text(
_envoiEnCours ? "Enregistrement..." : "Valider le retour", _envoiEnCours ? "Enregistrement..." : "Valider le retour",
style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 16), style: GoogleFonts.darkerGrotesque(
fontWeight: FontWeight.w700,
fontSize: 16,
),
), ),
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: EnsupColors.blue3, backgroundColor: EnsupColors.blue3,
padding: const EdgeInsets.symmetric(vertical: 16), 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( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 28), padding: const EdgeInsets.symmetric(
child: Row( horizontal: 24,
crossAxisAlignment: CrossAxisAlignment.start, vertical: 26,
children: [ ),
Expanded(flex: 3, child: _colonneGauche()), child: LayoutBuilder(
const SizedBox(width: 20), builder: (context, constraints) {
Expanded(flex: 2, child: _colonneDroite(context)), 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( Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( decoration: BoxDecoration(
color: EnsupColors.soft, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: EnsupColors.line, width: 1.5), 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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( Text(
item.nom, item.nom,
style: GoogleFonts.darkerGrotesque( style: GoogleFonts.darkerGrotesque(
fontSize: 20, fontSize: 24,
height: 1,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: EnsupColors.text, color: EnsupColors.text,
), ),
@@ -139,15 +177,25 @@ class DetailScreen extends StatelessWidget {
border: Border(bottom: BorderSide(color: EnsupColors.line)), border: Border(bottom: BorderSide(color: EnsupColors.line)),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(cle, style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted)),
Text( Text(
valeur, cle,
style: GoogleFonts.titilliumWeb( style: GoogleFonts.titilliumWeb(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w600, color: EnsupColors.muted,
color: valueColor ?? EnsupColors.text, ),
),
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( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ 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), StatusPill(disponible: item.disponible),
], ],
), ),
@@ -193,18 +247,39 @@ class DetailScreen extends StatelessWidget {
height: 180, height: 180,
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: EnsupColors.soft, color: EnsupColors.cyan.withValues(alpha: 0.045),
borderRadius: BorderRadius.circular(12), 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( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ 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), const SizedBox(height: 10),
Text( Text(
"Aperçu du matériel", "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), icon: const Icon(Icons.arrow_forward, size: 18),
label: Text( label: Text(
"Démarrer l'emprunt", "Démarrer l'emprunt",
style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700, fontSize: 16), style: GoogleFonts.darkerGrotesque(
fontWeight: FontWeight.w700,
fontSize: 16,
),
), ),
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: EnsupColors.cyan, backgroundColor: EnsupColors.cyan,
padding: const EdgeInsets.symmetric(vertical: 16), 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, foregroundColor: EnsupColors.text2,
side: const BorderSide(color: EnsupColors.line, width: 1.5), side: const BorderSide(color: EnsupColors.line, width: 1.5),
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
), ),
child: Text( child: Text(
"Retour au catalogue", "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( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 28), padding: const EdgeInsets.symmetric(
child: Column( horizontal: 28,
crossAxisAlignment: CrossAxisAlignment.start, vertical: 26,
children: [ ),
const ProfileCard( child: Center(
name: DemoIdentity.userName, child: ConstrainedBox(
subtitle: DemoIdentity.userSubtitle, constraints: const BoxConstraints(maxWidth: 1040),
initials: DemoIdentity.userInitials, child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(height: 24), children: [
Text( const ProfileCard(
"Que souhaitez-vous faire ?", name: DemoIdentity.userName,
style: GoogleFonts.darkerGrotesque( subtitle: DemoIdentity.userSubtitle,
fontSize: 28, initials: DemoIdentity.userInitials,
fontWeight: FontWeight.w900, ),
color: EnsupColors.text, const SizedBox(height: 28),
), Text(
), "Que souhaitez-vous faire ?",
const SizedBox(height: 18), style: GoogleFonts.darkerGrotesque(
IntrinsicHeight( fontSize: 30,
child: Row( height: 1,
crossAxisAlignment: CrossAxisAlignment.stretch, fontWeight: FontWeight.w900,
children: [ color: EnsupColors.text,
Expanded( ),
child: ActionCard( ),
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, icon: Icons.inventory_2_outlined,
title: "EMPRUNTER", title: "EMPRUNTER",
description: description:
"Accéder au catalogue et emprunter du matériel éducatif", "Consulter les équipements disponibles et démarrer un nouvel emprunt.",
color: EnsupColors.cyan, color: EnsupColors.cyan,
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const CatalogueScreen()), MaterialPageRoute(
builder: (_) =>
const CatalogueScreen(),
),
), ),
), );
), final restituer = ActionCard(
const SizedBox(width: 16),
Expanded(
child: ActionCard(
icon: Icons.assignment_return_outlined, icon: Icons.assignment_return_outlined,
title: "RESTITUER", title: "RESTITUER",
description: 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, color: EnsupColors.teal,
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context).push(
MaterialPageRoute( 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}); const RestitutionSelectScreen({super.key});
@override @override
State<RestitutionSelectScreen> createState() => _RestitutionSelectScreenState(); State<RestitutionSelectScreen> createState() =>
_RestitutionSelectScreenState();
} }
class _RestitutionSelectScreenState extends State<RestitutionSelectScreen> { class _RestitutionSelectScreenState extends State<RestitutionSelectScreen> {
@@ -40,16 +41,18 @@ class _RestitutionSelectScreenState extends State<RestitutionSelectScreen> {
} }
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => ChecklistRetourScreen(emprunt: emprunt.copyWith(materiel: detail)), builder: (_) => ChecklistRetourScreen(
emprunt: emprunt.copyWith(materiel: detail),
),
), ),
); );
} catch (error) { } catch (error) {
if (!mounted) { if (!mounted) {
return; return;
} }
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(error.toString())), context,
); ).showSnackBar(SnackBar(content: Text(error.toString())));
} finally { } finally {
if (mounted) { if (mounted) {
setState(() => _chargementId = null); setState(() => _chargementId = null);
@@ -91,12 +94,18 @@ class _RestitutionSelectScreenState extends State<RestitutionSelectScreen> {
), ),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 28), padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 28,
),
child: FutureBuilder<List<EmpruntItem>>( child: FutureBuilder<List<EmpruntItem>>(
future: _future, future: _future,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState ==
return const Center(child: CircularProgressIndicator()); ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
} }
if (snapshot.hasError) { if (snapshot.hasError) {
return _ErreurState( return _ErreurState(
@@ -128,12 +137,21 @@ class _RestitutionSelectScreenState extends State<RestitutionSelectScreen> {
Text( Text(
"Sélectionner le matériel à restituer", "Sélectionner le matériel à restituer",
style: GoogleFonts.darkerGrotesque( style: GoogleFonts.darkerGrotesque(
fontSize: 24, fontSize: 28,
height: 1,
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
color: EnsupColors.text, 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( Expanded(
child: emprunts.isEmpty child: emprunts.isEmpty
? const _EmptyState() ? const _EmptyState()
@@ -174,70 +192,144 @@ class _EmpruntCard extends StatelessWidget {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: EnsupColors.line, width: 1.5), border: Border.all(color: EnsupColors.line, width: 1.5),
), boxShadow: [
child: Row( BoxShadow(
children: [ color: EnsupColors.text.withValues(alpha: 0.035),
Container( blurRadius: 10,
width: 44, offset: const Offset(0, 3),
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),
),
), ),
], ],
), ),
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( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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), const SizedBox(height: 12),
Text( Text(
message, message,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.text2), style: GoogleFonts.titilliumWeb(
fontSize: 14,
color: EnsupColors.text2,
),
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
OutlinedButton( OutlinedButton(
+93 -42
View File
@@ -3,7 +3,7 @@ import "package:google_fonts/google_fonts.dart";
import "../theme/ensup_colors.dart"; import "../theme/ensup_colors.dart";
/// Grande carte d'action de l'accueil (Emprunter / Restituer). /// Grande carte d'action de l'accueil (Emprunter / Restituer).
class ActionCard extends StatelessWidget { class ActionCard extends StatefulWidget {
const ActionCard({ const ActionCard({
super.key, super.key,
required this.icon, required this.icon,
@@ -19,56 +19,107 @@ class ActionCard extends StatelessWidget {
final Color color; final Color color;
final VoidCallback? onTap; final VoidCallback? onTap;
@override
State<ActionCard> createState() => _ActionCardState();
}
class _ActionCardState extends State<ActionCard> {
bool _hovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( final color = widget.color;
onTap: onTap, return MouseRegion(
borderRadius: BorderRadius.circular(16), onEnter: (_) => setState(() => _hovered = true),
child: Container( onExit: (_) => setState(() => _hovered = false),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32), cursor: SystemMouseCursors.click,
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeOut,
constraints: const BoxConstraints(minHeight: 230),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color.withValues(alpha: 0.07), color: _hovered
? color.withValues(alpha: 0.1)
: color.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(16), 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( child: Material(
children: [ color: Colors.transparent,
Container( child: InkWell(
width: 60, onTap: widget.onTap,
height: 60, borderRadius: BorderRadius.circular(16),
decoration: BoxDecoration( child: Padding(
gradient: LinearGradient(colors: [color, color.withValues(alpha: 0.75)]), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 28),
borderRadius: BorderRadius.circular(18), child: Column(
boxShadow: [ mainAxisAlignment: MainAxisAlignment.center,
BoxShadow( children: [
color: color.withValues(alpha: 0.35), AnimatedContainer(
blurRadius: 16, duration: const Duration(milliseconds: 180),
offset: const Offset(0, 4), 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,
),
),
],
), ),
), ),
); );
+95 -66
View File
@@ -27,96 +27,122 @@ class EnsupTopBar extends StatelessWidget {
final onBack = this.onBack; final onBack = this.onBack;
return Container( return Container(
height: 56, height: 60,
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 18),
color: EnsupColors.blue3, color: EnsupColors.blue3,
child: Row( child: LayoutBuilder(
mainAxisAlignment: MainAxisAlignment.spaceBetween, builder: (context, constraints) {
children: [ final compact = constraints.maxWidth < 620;
Row( final showCampus = constraints.maxWidth >= 520;
final showName = constraints.maxWidth >= 760;
return Row(
children: [ children: [
if (onBack != null) ...[ if (onBack != null) ...[
_BackButton(onTap: onBack), _BackButton(onTap: onBack, compact: compact),
const SizedBox(width: 8), SizedBox(width: compact ? 5 : 8),
], ],
const Icon(Icons.badge_outlined, color: Colors.white, size: 18), const Icon(Icons.badge_outlined, color: Colors.white, size: 18),
const SizedBox(width: 10), const SizedBox(width: 9),
Text( Expanded(
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)),
),
child: Text( child: Text(
campusTag, brandText,
style: GoogleFonts.titilliumWeb( maxLines: 1,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.darkerGrotesque(
color: Colors.white, color: Colors.white,
fontSize: 12, fontWeight: FontWeight.w900,
fontWeight: FontWeight.w600, fontSize: compact ? 18 : 21,
), ),
), ),
), ),
if (name != null && initials != null) ...[ const SizedBox(width: 10),
const SizedBox(width: 12), if (showCampus)
_Avatar(initials: initials), Container(
const SizedBox(width: 8), padding: const EdgeInsets.symmetric(
Text( horizontal: 11,
name, vertical: 5,
style: GoogleFonts.titilliumWeb( ),
color: Colors.white, decoration: BoxDecoration(
fontSize: 13, color: Colors.white.withValues(alpha: 0.14),
fontWeight: FontWeight.w600, 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 { class _BackButton extends StatelessWidget {
const _BackButton({required this.onTap}); const _BackButton({required this.onTap, required this.compact});
final VoidCallback onTap; final VoidCallback onTap;
final bool compact;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return Tooltip(
onTap: onTap, message: "Retour",
borderRadius: BorderRadius.circular(8), child: InkWell(
child: Padding( onTap: onTap,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), borderRadius: BorderRadius.circular(7),
child: Row( child: Container(
mainAxisSize: MainAxisSize.min, height: 34,
children: [ padding: EdgeInsets.symmetric(horizontal: compact ? 8 : 10),
Icon(Icons.arrow_back, size: 16, color: Colors.white.withValues(alpha: 0.85)), decoration: BoxDecoration(
const SizedBox(width: 6), color: Colors.white.withValues(alpha: 0.08),
Text( borderRadius: BorderRadius.circular(7),
"Retour", ),
style: GoogleFonts.titilliumWeb( child: Row(
color: Colors.white.withValues(alpha: 0.85), mainAxisSize: MainAxisSize.min,
fontSize: 13, children: [
fontWeight: FontWeight.w600, 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, height: 30,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
gradient: const LinearGradient(colors: [EnsupColors.teal, EnsupColors.cyan]), color: EnsupColors.purple,
border: Border.all(color: Colors.white.withValues(alpha: 0.3), width: 2), border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 2,
),
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
+74 -24
View File
@@ -37,7 +37,11 @@ class EtatBox extends StatelessWidget {
case EtatChecklist.deteriore: case EtatChecklist.deteriore:
background = const Color(0xFFFFFBEB); background = const Color(0xFFFFFBEB);
border = const Color(0xFFFDE68A); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return LayoutBuilder(
children: [ builder: (context, constraints) {
_entete(), final columnWidth = constraints.maxWidth < 520 ? 58.0 : 72.0;
for (var i = 0; i < accessoires.length; i++) _ligne(i), 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( return Container(
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
decoration: const BoxDecoration( decoration: const BoxDecoration(
@@ -90,9 +115,9 @@ class ChecklistTableau extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Expanded(child: _texteEntete("ÉLÉMENT")), Expanded(child: _texteEntete("ÉLÉMENT")),
_colonneEntete("PRÉSENT"), _colonneEntete("PRÉSENT", columnWidth),
_colonneEntete("ABSENT"), _colonneEntete("ABSENT", columnWidth),
_colonneEntete("DÉTÉRIORÉ"), _colonneEntete("DÉTÉRIORÉ", columnWidth),
], ],
), ),
); );
@@ -110,11 +135,16 @@ class ChecklistTableau extends StatelessWidget {
); );
} }
Widget _colonneEntete(String label) { Widget _colonneEntete(String label, double width) {
return SizedBox(width: 72, child: Center(child: _texteEntete(label))); 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( return Container(
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
decoration: const BoxDecoration( decoration: const BoxDecoration(
@@ -125,20 +155,23 @@ class ChecklistTableau extends StatelessWidget {
Expanded( Expanded(
child: Text( child: Text(
accessoires[index], 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.present, columnWidth),
_case(index, EtatChecklist.absent), _case(index, EtatChecklist.absent, columnWidth),
_case(index, EtatChecklist.deteriore), _case(index, EtatChecklist.deteriore, columnWidth),
], ],
), ),
); );
} }
Widget _case(int index, EtatChecklist type) { Widget _case(int index, EtatChecklist type, double width) {
return SizedBox( return SizedBox(
width: 72, width: width,
child: Center( child: Center(
child: EtatBox( child: EtatBox(
type: type, type: type,
@@ -152,7 +185,11 @@ class ChecklistTableau extends StatelessWidget {
/// Encadré d'instructions / rappel à côté d'une checklist. /// Encadré d'instructions / rappel à côté d'une checklist.
class ChecklistInstructions extends StatelessWidget { 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 titre;
final String texte; final String texte;
@@ -164,7 +201,10 @@ class ChecklistInstructions extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: EnsupColors.blue2.withValues(alpha: 0.05), color: EnsupColors.blue2.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10), 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -181,7 +221,11 @@ class ChecklistInstructions extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
texte, 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), style: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.text),
decoration: InputDecoration( decoration: InputDecoration(
hintText: hint, hintText: hint,
hintStyle: GoogleFonts.titilliumWeb(fontSize: 13, color: EnsupColors.muted), hintStyle: GoogleFonts.titilliumWeb(
fontSize: 13,
color: EnsupColors.muted,
),
filled: true, filled: true,
fillColor: Colors.white, fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 12,
),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(9), borderRadius: BorderRadius.circular(9),
borderSide: const BorderSide(color: EnsupColors.line, width: 1.5), borderSide: const BorderSide(color: EnsupColors.line, width: 1.5),
@@ -3,7 +3,7 @@ import "package:google_fonts/google_fonts.dart";
import "../theme/ensup_colors.dart"; import "../theme/ensup_colors.dart";
/// Option d'identification de l'écran de connexion (QR ou mail ENSUP). /// Option d'identification de l'écran de connexion (QR ou mail ENSUP).
class IdentificationOption extends StatelessWidget { class IdentificationOption extends StatefulWidget {
const IdentificationOption({ const IdentificationOption({
super.key, super.key,
required this.icon, required this.icon,
@@ -19,56 +19,102 @@ class IdentificationOption extends StatelessWidget {
final String description; final String description;
final VoidCallback? onTap; final VoidCallback? onTap;
@override
State<IdentificationOption> createState() => _IdentificationOptionState();
}
class _IdentificationOptionState extends State<IdentificationOption> {
bool _hovered = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InkWell( return MouseRegion(
onTap: onTap, onEnter: (_) => setState(() => _hovered = true),
borderRadius: BorderRadius.circular(12), onExit: (_) => setState(() => _hovered = false),
child: Container( cursor: SystemMouseCursors.click,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: EnsupColors.line, width: 2), border: Border.all(
), color: _hovered ? widget.iconColor : EnsupColors.line,
child: Row( width: 1.5,
children: [ ),
Container( boxShadow: [
width: 48, BoxShadow(
height: 48, color: widget.iconColor.withValues(
decoration: BoxDecoration( alpha: _hovered ? 0.12 : 0.035,
color: iconColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
), ),
child: Icon(icon, color: iconColor, size: 24), blurRadius: _hovered ? 16 : 8,
offset: const Offset(0, 4),
), ),
const SizedBox(width: 16), ],
Expanded( ),
child: Column( child: Material(
crossAxisAlignment: CrossAxisAlignment.start, 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: [ children: [
Text( Container(
label, width: 48,
style: GoogleFonts.darkerGrotesque( height: 48,
fontWeight: FontWeight.w700, decoration: BoxDecoration(
fontSize: 18, color: widget.iconColor.withValues(alpha: 0.1),
color: EnsupColors.text, 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), const SizedBox(width: 8),
Text( Container(
description, width: 28,
style: GoogleFonts.titilliumWeb( height: 28,
fontSize: 12, decoration: BoxDecoration(
color: EnsupColors.muted, color: widget.iconColor.withValues(alpha: 0.08),
height: 1.4, shape: BoxShape.circle,
),
child: Icon(
Icons.chevron_right_rounded,
color: widget.iconColor,
size: 19,
), ),
), ),
], ],
), ),
), ),
const Icon(Icons.chevron_right, color: EnsupColors.muted, size: 20), ),
],
), ),
), ),
); );
+129 -52
View File
@@ -24,65 +24,142 @@ class MaterielCard extends StatelessWidget {
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: EnsupColors.line, width: 1.5), border: Border.all(color: EnsupColors.line, width: 1.5),
), boxShadow: [
child: Row( BoxShadow(
children: [ color: EnsupColors.text.withValues(alpha: 0.035),
Container( blurRadius: 10,
width: 44, offset: const Offset(0, 3),
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( child: LayoutBuilder(
crossAxisAlignment: CrossAxisAlignment.start, builder: (context, constraints) {
children: [ 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( Text(
item.nom, "Indisponible",
style: GoogleFonts.darkerGrotesque( style: GoogleFonts.darkerGrotesque(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 16, fontSize: 14,
color: EnsupColors.text, 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), return Row(
const SizedBox(width: 12), children: [
if (disponible) Expanded(child: information),
FilledButton( const SizedBox(width: 16),
onPressed: onSelect, actions,
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,
),
),
],
), ),
), ),
); );
+109 -63
View File
@@ -18,74 +18,120 @@ class ProfileCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( color: EnsupColors.cyan.withValues(alpha: 0.055),
colors: [
EnsupColors.cyan.withValues(alpha: 0.06),
EnsupColors.teal.withValues(alpha: 0.04),
],
),
borderRadius: BorderRadius.circular(12), 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),
child: Row( width: 1.5,
children: [ ),
Container( boxShadow: [
width: 48, BoxShadow(
height: 48, color: EnsupColors.blue3.withValues(alpha: 0.045),
decoration: const BoxDecoration( blurRadius: 12,
shape: BoxShape.circle, offset: const Offset(0, 4),
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,
),
),
), ),
], ],
), ),
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,
),
),
],
),
),
],
],
);
},
),
); );
} }
} }
+13 -2
View File
@@ -36,7 +36,7 @@ l'historique complet, y compris des étapes devenues obsolètes après brancheme
| Infrastructure | Terminé | Docker Compose SQL Server + Adminer, monorepo, backend Node/TS, frontend Flutter Web. | | Infrastructure | Terminé | Docker Compose SQL Server + Adminer, monorepo, backend Node/TS, frontend Flutter Web. |
| Base de données | Terminé pour la V1 | Schéma Prisma 16 entités, migrations, seeds et fixtures de démonstration. | | Base de données | Terminé pour la V1 | Schéma Prisma 16 entités, migrations, seeds et fixtures de démonstration. |
| API étudiant | Terminé pour la V1 | Catalogue, détail matériel, création d'emprunt, mes emprunts, restitution, anomalie automatique. | | API étudiant | Terminé pour la V1 | Catalogue, détail matériel, création d'emprunt, mes emprunts, restitution, anomalie automatique. |
| Frontend étudiant | Fonctionnel pour la V1 | Parcours emprunt et restitution branchés sur l'API et testés en réel ; conservation de l'action avant identification à finaliser. | | Frontend étudiant | Fonctionnel pour la V1 | Parcours API complet, ordre identification puis choix validé, interface premium et responsive fidèle à la maquette. |
| Authentification | Simulée | `x-user-email` côté backend et identité démo côté frontend ; Azure AD reste à faire. | | Authentification | Simulée | `x-user-email` côté backend et identité démo côté frontend ; Azure AD reste à faire. |
| API responsable | Terminé pour la V1 | Dashboard, emprunts, stock, anomalies, notifications, historique et export CSV, avec contrôle du rôle et du campus. | | API responsable | Terminé pour la V1 | Dashboard, emprunts, stock, anomalies, notifications, historique et export CSV, avec contrôle du rôle et du campus. |
| Frontend responsable | Fonctionnel pour la V1 | Espace de supervision responsive avec navigation dédiée, dashboard métier, vues API et export CSV opérationnel. | | Frontend responsable | Fonctionnel pour la V1 | Espace de supervision responsive avec navigation dédiée, dashboard métier, vues API et export CSV opérationnel. |
@@ -431,6 +431,17 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
- Endpoint vérifié au runtime : réponse `200`, type `text/csv`, nom `historique-responsable.csv` et contenu non vide. - Endpoint vérifié au runtime : réponse `200`, type `text/csv`, nom `historique-responsable.csv` et contenu non vide.
- Vérification statique : `dart analyze` sans erreur. - Vérification statique : `dart analyze` sans erreur.
### Étape 46 — Refonte premium de l'espace étudiant
- L'ordre actuel du parcours est confirmé : identification, puis choix `Emprunter` ou `Restituer`.
- Branche dédiée `style/student-premium-workspace` créée depuis `develop`.
- Charte et maquette conservées : couleurs ENSUP, typographies, barre supérieure, coins de marque et structure des écrans.
- Barre supérieure rendue responsive, avec priorité donnée au titre et à l'identité sur les petites largeurs.
- Accueil rééquilibré avec profil identifié, hiérarchie renforcée et cartes d'action adaptatives avec états de survol.
- Catalogue, détail matériel et sélection de restitution enrichis sans modifier leurs données ni leurs actions.
- Détail et checklists passent automatiquement de deux colonnes à une colonne sur écran étroit ; tableau de contrôle et lignes de matériel évitent les débordements.
- Écrans de confirmation et de résultat conservés fonctionnellement et harmonisés via les composants partagés.
- Vérification statique : `dart analyze` sans erreur.
--- ---
*Dernière mise à jour : 2026-07-23 — Export CSV de l'historique responsable opérationnel depuis l'interface.* *Dernière mise à jour : 2026-07-23 — Espace étudiant premium et responsive, fidèle à la maquette ENSUP.*