feat(materiel): connect catalogue and detail to api

This commit is contained in:
SaidSoighiri94
2026-07-07 13:07:47 +02:00
parent 90d5a65f29
commit 9564dd1ade
15 changed files with 368 additions and 101 deletions
+146 -93
View File
@@ -2,12 +2,14 @@ import "package:flutter/material.dart";
import "package:google_fonts/google_fonts.dart";
import "../theme/ensup_colors.dart";
import "../models/materiel_item.dart";
import "../services/materiel_service.dart";
import "../widgets/ensup_top_bar.dart";
import "../widgets/brand_corners.dart";
import "../widgets/materiel_card.dart";
import "detail_screen.dart";
/// Catalogue matériel : recherche, filtres par catégorie et liste de matériels.
/// Catalogue matériel : recherche, filtres par catégorie et liste chargée
/// depuis l'API.
class CatalogueScreen extends StatefulWidget {
const CatalogueScreen({super.key});
@@ -15,82 +17,62 @@ class CatalogueScreen extends StatefulWidget {
State<CatalogueScreen> createState() => _CatalogueScreenState();
}
const _materiels = <MaterielItem>[
MaterielItem(
nom: "PC Dell Latitude 5420",
categorie: "Ordinateur",
marque: "Dell",
modele: "Latitude 5420",
reference: "ENS-PC-001",
campus: "Paris · Ensitech",
etatGeneral: "Bon état",
disponible: true,
accessoires: ["Chargeur 65W", "Souris sans fil", "Housse de protection", "Câble HDMI"],
icon: Icons.laptop_mac,
),
MaterielItem(
nom: "Vidéoprojecteur Epson EB-X51",
categorie: "Projection",
marque: "Epson",
modele: "EB-X51",
reference: "ENS-VP-003",
campus: "Paris · Ensitech",
etatGeneral: "Bon état",
disponible: true,
accessoires: ["Télécommande", "Câble HDMI", "Câble VGA"],
icon: Icons.videocam_outlined,
),
MaterielItem(
nom: "Chargeur USB-C 65W",
categorie: "Accessoire",
marque: "Anker",
modele: "PowerPort III",
reference: "ENS-CH-012",
campus: "Paris · Ensitech",
etatGeneral: "Bon état",
disponible: false,
accessoires: [],
icon: Icons.power_outlined,
),
MaterielItem(
nom: "PC Lenovo ThinkPad E14",
categorie: "Ordinateur",
marque: "Lenovo",
modele: "ThinkPad E14",
reference: "ENS-PC-004",
campus: "Paris · Ensitech",
etatGeneral: "Bon état",
disponible: true,
accessoires: ["Chargeur 65W", "Souris sans fil", "Housse de protection"],
icon: Icons.laptop_mac,
),
];
const _categories = ["Tous", "Ordinateurs", "Projection", "Câbles", "Accessoires"];
class _CatalogueScreenState extends State<CatalogueScreen> {
late Future<List<MaterielItem>> _future;
String _recherche = "";
String _categorie = "Tous";
int? _selectionEnCoursId;
List<MaterielItem> get _resultats {
@override
void initState() {
super.initState();
_future = MaterielService.getCatalogue();
}
void _recharger() {
setState(() => _future = MaterielService.getCatalogue());
}
List<String> _categories(List<MaterielItem> materiels) {
final distinctes = materiels.map((m) => m.categorie).toSet().toList()..sort();
return ["Tous", ...distinctes];
}
List<MaterielItem> _filtrer(List<MaterielItem> materiels) {
final terme = _recherche.trim().toLowerCase();
return _materiels.where((materiel) {
return materiels.where((materiel) {
final matchTexte = terme.isEmpty || materiel.nom.toLowerCase().contains(terme);
final matchCategorie = _categorie == "Tous" || _categorie.startsWith(materiel.categorie);
final matchCategorie = _categorie == "Tous" || materiel.categorie == _categorie;
return matchTexte && matchCategorie;
}).toList();
}
void _selectionner(BuildContext context, MaterielItem item) {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => DetailScreen(item: item)),
);
Future<void> _selectionner(MaterielItem item) async {
setState(() => _selectionEnCoursId = item.id);
try {
final detail = await MaterielService.getDetail(item.id);
if (!mounted) {
return;
}
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => DetailScreen(item: detail)),
);
} catch (error) {
if (!mounted) {
return;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error.toString())),
);
} finally {
if (mounted) {
setState(() => _selectionEnCoursId = null);
}
}
}
@override
Widget build(BuildContext context) {
final resultats = _resultats;
return Scaffold(
backgroundColor: EnsupColors.soft,
body: Stack(
@@ -131,34 +113,23 @@ class _CatalogueScreenState extends State<CatalogueScreen> {
onChanged: (value) => setState(() => _recherche = value),
),
const SizedBox(height: 14),
Wrap(
spacing: 8,
runSpacing: 8,
children: _categories.map((categorie) {
return _CategoryChip(
label: categorie,
selected: categorie == _categorie,
onTap: () => setState(() => _categorie = categorie),
);
}).toList(),
),
const SizedBox(height: 16),
const Divider(color: EnsupColors.line, height: 1),
const SizedBox(height: 16),
Expanded(
child: resultats.isEmpty
? _EmptyState()
: ListView.separated(
itemCount: resultats.length,
separatorBuilder: (_, _) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final item = resultats[index];
return MaterielCard(
item: item,
onSelect: () => _selectionner(context, item),
);
},
),
child: FutureBuilder<List<MaterielItem>>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return _ErreurState(
message: snapshot.error.toString(),
onRetry: _recharger,
);
}
final materiels = snapshot.data ?? const [];
return _contenu(materiels);
},
),
),
],
),
@@ -174,6 +145,53 @@ class _CatalogueScreenState extends State<CatalogueScreen> {
),
);
}
Widget _contenu(List<MaterielItem> materiels) {
final resultats = _filtrer(materiels);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: _categories(materiels).map((categorie) {
return _CategoryChip(
label: categorie,
selected: categorie == _categorie,
onTap: () => setState(() => _categorie = categorie),
);
}).toList(),
),
const SizedBox(height: 16),
const Divider(color: EnsupColors.line, height: 1),
const SizedBox(height: 16),
Expanded(
child: resultats.isEmpty
? const _EmptyState()
: ListView.separated(
itemCount: resultats.length,
separatorBuilder: (_, _) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final item = resultats[index];
return Stack(
children: [
MaterielCard(item: item, onSelect: () => _selectionner(item)),
if (_selectionEnCoursId == item.id)
const Positioned.fill(
child: ColoredBox(
color: Color(0x66FFFFFF),
child: Center(child: CircularProgressIndicator()),
),
),
],
);
},
),
),
],
);
}
}
class _SearchField extends StatelessWidget {
@@ -235,7 +253,7 @@ class _CategoryChip extends StatelessWidget {
child: Text(
label,
style: GoogleFonts.darkerGrotesque(
fontSize: 14,
fontSize: 13,
fontWeight: FontWeight.w600,
color: selected ? Colors.white : EnsupColors.muted,
),
@@ -246,14 +264,16 @@ class _CategoryChip extends StatelessWidget {
}
class _EmptyState extends StatelessWidget {
const _EmptyState();
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.search_off, size: 40, color: EnsupColors.muted),
const SizedBox(height: 12),
const Icon(Icons.inventory_2_outlined, size: 40, color: EnsupColors.muted),
const SizedBox(height: 10),
Text(
"Aucun matériel ne correspond",
style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.muted),
@@ -263,3 +283,36 @@ class _EmptyState extends StatelessWidget {
);
}
}
class _ErreurState extends StatelessWidget {
const _ErreurState({required this.message, required this.onRetry});
final String message;
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_off_outlined, size: 40, color: EnsupColors.muted),
const SizedBox(height: 12),
Text(
message,
textAlign: TextAlign.center,
style: GoogleFonts.titilliumWeb(fontSize: 14, color: EnsupColors.text2),
),
const SizedBox(height: 14),
OutlinedButton(
onPressed: onRetry,
child: Text(
"Réessayer",
style: GoogleFonts.darkerGrotesque(fontWeight: FontWeight.w700),
),
),
],
),
);
}
}
@@ -8,6 +8,7 @@ import "../widgets/brand_corners.dart";
import "checklist_retour_screen.dart";
const _pcDell = MaterielItem(
id: 0,
nom: "PC Dell Latitude 5420",
categorie: "Ordinateur",
marque: "Dell",