feat(materiel): connect catalogue and detail to api
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user