Compare commits
10 Commits
1677015254
...
f7ce98343e
| Author | SHA1 | Date | |
|---|---|---|---|
| f7ce98343e | |||
| 669ed85924 | |||
| 695b0d3965 | |||
| 1c8ed77ebf | |||
| 1f0a120ecb | |||
| cb3597874d | |||
| c85fab8ca3 | |||
| 95c9080777 | |||
| 6ac94becfb | |||
| ef74fd49bc |
@@ -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 l’emprunt.",
|
||||||
|
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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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 l’opération à réaliser sur ce poste.",
|
||||||
|
style: GoogleFonts.titilliumWeb(
|
||||||
|
fontSize: 14,
|
||||||
|
color: EnsupColors.muted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final emprunter = ActionCard(
|
||||||
icon: Icons.inventory_2_outlined,
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
|
import "dart:typed_data";
|
||||||
import "package:http/http.dart" as http;
|
import "package:http/http.dart" as http;
|
||||||
|
|
||||||
/// Erreur levée par le client API (serveur injoignable ou réponse en échec).
|
/// Erreur levée par le client API (serveur injoignable ou réponse en échec).
|
||||||
@@ -11,6 +12,18 @@ class ApiException implements Exception {
|
|||||||
String toString() => message;
|
String toString() => message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ApiFile {
|
||||||
|
const ApiFile({
|
||||||
|
required this.bytes,
|
||||||
|
required this.fileName,
|
||||||
|
required this.contentType,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Uint8List bytes;
|
||||||
|
final String fileName;
|
||||||
|
final String contentType;
|
||||||
|
}
|
||||||
|
|
||||||
/// Point d'accès unique au backend : centralise l'URL de base, l'en-tête
|
/// Point d'accès unique au backend : centralise l'URL de base, l'en-tête
|
||||||
/// d'authentification (simulée) et la gestion des erreurs.
|
/// d'authentification (simulée) et la gestion des erreurs.
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
@@ -44,6 +57,37 @@ class ApiClient {
|
|||||||
throw ApiException(_messageErreur(reponse));
|
throw ApiException(_messageErreur(reponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<ApiFile> getFile(
|
||||||
|
String chemin, {
|
||||||
|
required String fallbackFileName,
|
||||||
|
String utilisateurEmail = _utilisateurEmail,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse("$_baseUrl$chemin");
|
||||||
|
|
||||||
|
late final http.Response reponse;
|
||||||
|
try {
|
||||||
|
reponse = await http.get(
|
||||||
|
uri,
|
||||||
|
headers: {"x-user-email": utilisateurEmail},
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
throw const ApiException(
|
||||||
|
"Impossible de joindre le serveur. Vérifiez que le backend est démarré.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reponse.statusCode < 200 || reponse.statusCode >= 300) {
|
||||||
|
throw ApiException(_messageErreur(reponse));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiFile(
|
||||||
|
bytes: reponse.bodyBytes,
|
||||||
|
fileName: _fileName(reponse, fallbackFileName),
|
||||||
|
contentType:
|
||||||
|
reponse.headers["content-type"] ?? "application/octet-stream",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<dynamic> post(
|
static Future<dynamic> post(
|
||||||
String chemin,
|
String chemin,
|
||||||
Map<String, dynamic> body, {
|
Map<String, dynamic> body, {
|
||||||
@@ -116,4 +160,15 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
return "Erreur ${reponse.statusCode}";
|
return "Erreur ${reponse.statusCode}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String _fileName(http.Response reponse, String fallback) {
|
||||||
|
final disposition = reponse.headers["content-disposition"];
|
||||||
|
if (disposition == null) return fallback;
|
||||||
|
|
||||||
|
final match = RegExp(
|
||||||
|
"filename=\"?([^\";]+)\"?",
|
||||||
|
caseSensitive: false,
|
||||||
|
).firstMatch(disposition);
|
||||||
|
return match?.group(1) ?? fallback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ class ResponsableService {
|
|||||||
static Future<List<Map<String, dynamic>>> historique() =>
|
static Future<List<Map<String, dynamic>>> historique() =>
|
||||||
_liste("/responsable/historique");
|
_liste("/responsable/historique");
|
||||||
|
|
||||||
|
static Future<ApiFile> exporterHistorique() => ApiClient.getFile(
|
||||||
|
"/responsable/historique/export.csv",
|
||||||
|
fallbackFileName: "historique-responsable.csv",
|
||||||
|
utilisateurEmail: _email,
|
||||||
|
);
|
||||||
|
|
||||||
static Future<int> marquerNotificationsLues() async {
|
static Future<int> marquerNotificationsLues() async {
|
||||||
final reponse = await ApiClient.patch(
|
final reponse = await ApiClient.patch(
|
||||||
"/responsable/notifications/lu-toutes",
|
"/responsable/notifications/lu-toutes",
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
|
||||||
|
|
||||||
|
import "dart:html" as html;
|
||||||
|
import "dart:typed_data";
|
||||||
|
|
||||||
|
void downloadFile({
|
||||||
|
required Uint8List bytes,
|
||||||
|
required String fileName,
|
||||||
|
required String contentType,
|
||||||
|
}) {
|
||||||
|
final blob = html.Blob([bytes], contentType);
|
||||||
|
final url = html.Url.createObjectUrlFromBlob(blob);
|
||||||
|
final anchor = html.AnchorElement(href: url)
|
||||||
|
..download = fileName
|
||||||
|
..style.display = "none";
|
||||||
|
|
||||||
|
html.document.body?.children.add(anchor);
|
||||||
|
anchor.click();
|
||||||
|
anchor.remove();
|
||||||
|
html.Url.revokeObjectUrl(url);
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ 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 | Dashboard et vues métier branchés sur l'API ; téléchargement CSV dans le navigateur à finaliser. |
|
| 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. |
|
||||||
| Tests automatisés | Non démarré | Tests backend/frontend/E2E à ajouter ; tests runtime manuels effectués. |
|
| Tests automatisés | Non démarré | Tests backend/frontend/E2E à ajouter ; tests runtime manuels effectués. |
|
||||||
| Documentation | Partielle | README principal et documents de conception présents ; OpenAPI, guides utilisateur et captures restent à produire. |
|
| Documentation | Partielle | README principal et documents de conception présents ; OpenAPI, guides utilisateur et captures restent à produire. |
|
||||||
|
|
||||||
@@ -415,6 +415,33 @@ En terminal interactif (VS Code), `migrate dev` fonctionne normalement (taper `y
|
|||||||
- Limites restantes explicitées : authentification Azure AD, téléchargement CSV frontend, tests automatisés, OpenAPI et guides utilisateur.
|
- Limites restantes explicitées : authentification Azure AD, téléchargement CSV frontend, tests automatisés, OpenAPI et guides utilisateur.
|
||||||
- Commandes de vérification actualisées : build et lint backend verts, analyse Dart directe sans erreur.
|
- Commandes de vérification actualisées : build et lint backend verts, analyse Dart directe sans erreur.
|
||||||
|
|
||||||
|
### Étape 44 — Refonte premium de l'espace responsable
|
||||||
|
- Branche dédiée `style/responsable-premium-dashboard` créée depuis `develop`.
|
||||||
|
- L'espace responsable adopte une structure de supervision distincte du parcours étudiant : barre supérieure métier, navigation latérale sur ordinateur et navigation compacte sur petit écran.
|
||||||
|
- Dashboard enrichi avec disponibilités, emprunts en cours, retards, anomalies actives, notifications non lues, activité récente et taux de disponibilité du parc.
|
||||||
|
- Accès directs ajoutés depuis les points d'attention vers les vues emprunts, stock, anomalies et notifications.
|
||||||
|
- Les listes métier partagent désormais une hiérarchie visuelle, une recherche et des lignes responsives cohérentes avec la charte ENSUP.
|
||||||
|
- Aucun changement backend ni métier ; rendu validé manuellement dans le navigateur et `dart analyze` sans erreur.
|
||||||
|
|
||||||
|
### Étape 45 — Téléchargement CSV de l'historique responsable
|
||||||
|
- Branche dédiée `feat/responsable-historique-export` créée depuis `develop`.
|
||||||
|
- Client HTTP étendu pour récupérer un fichier binaire avec son type de contenu et son nom.
|
||||||
|
- Le bouton `Export CSV` appelle désormais l'endpoint responsable avec l'identité simulée de Karim et déclenche le téléchargement navigateur.
|
||||||
|
- Un état de chargement bloque les doubles clics et les erreurs API sont affichées dans l'interface.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### É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-22 — Synthèse synchronisée avec l'état réel du projet après finalisation du premier parcours responsable.*
|
*Dernière mise à jour : 2026-07-23 — Espace étudiant premium et responsive, fidèle à la maquette ENSUP.*
|
||||||
|
|||||||
Reference in New Issue
Block a user