feat(responsable): add material stock table
This commit is contained in:
@@ -20,12 +20,19 @@ class ResponsableScreen extends StatefulWidget {
|
||||
class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
final TextEditingController _rechercheEmpruntsController =
|
||||
TextEditingController();
|
||||
final TextEditingController _rechercheMaterielsController =
|
||||
TextEditingController();
|
||||
final Map<int, String> _categoriesMateriels = {};
|
||||
late Future<_ResponsableData> _future = _charger();
|
||||
int _onglet = 0;
|
||||
String? _statutEmprunts;
|
||||
int? _anneeEmprunts;
|
||||
String _triEmprunts = "dateEmprunt";
|
||||
String _ordreEmprunts = "desc";
|
||||
String? _statutMateriels;
|
||||
int? _categorieMateriels;
|
||||
String _triMateriels = "nom";
|
||||
String _ordreMateriels = "asc";
|
||||
bool _transitionEnCours = false;
|
||||
bool _decisionEnCours = false;
|
||||
bool _exportEnCours = false;
|
||||
@@ -40,7 +47,17 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
tri: _triEmprunts,
|
||||
ordre: _ordreEmprunts,
|
||||
);
|
||||
final materiels = await ResponsableService.materiels();
|
||||
final materiels = await ResponsableService.materiels(
|
||||
statut: _statutMateriels,
|
||||
categorieId: _categorieMateriels,
|
||||
recherche: _rechercheMaterielsController.text,
|
||||
tri: _triMateriels,
|
||||
ordre: _ordreMateriels,
|
||||
);
|
||||
for (final materiel in materiels) {
|
||||
final categorie = Map<String, dynamic>.from(materiel["categorie"] as Map);
|
||||
_categoriesMateriels[categorie["id"] as int] = categorie["nom"] as String;
|
||||
}
|
||||
final anomalies = await ResponsableService.anomalies();
|
||||
final notifications = await ResponsableService.notifications();
|
||||
final historique = await ResponsableService.historique();
|
||||
@@ -91,9 +108,41 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void _trierMateriels(String tri) {
|
||||
setState(() {
|
||||
if (_triMateriels == tri) {
|
||||
_ordreMateriels = _ordreMateriels == "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
_triMateriels = tri;
|
||||
_ordreMateriels = "asc";
|
||||
}
|
||||
_future = _charger();
|
||||
});
|
||||
}
|
||||
|
||||
void _filtrerMateriels({String? statut, int? categorieId}) {
|
||||
setState(() {
|
||||
_statutMateriels = statut;
|
||||
_categorieMateriels = categorieId;
|
||||
_future = _charger();
|
||||
});
|
||||
}
|
||||
|
||||
void _reinitialiserFiltresMateriels() {
|
||||
_rechercheMaterielsController.clear();
|
||||
setState(() {
|
||||
_statutMateriels = null;
|
||||
_categorieMateriels = null;
|
||||
_triMateriels = "nom";
|
||||
_ordreMateriels = "asc";
|
||||
_future = _charger();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_rechercheEmpruntsController.dispose();
|
||||
_rechercheMaterielsController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -298,17 +347,23 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
onTri: _trierEmprunts,
|
||||
onReset: _reinitialiserFiltresEmprunts,
|
||||
),
|
||||
_VueListe(
|
||||
titre: "Stock matériel",
|
||||
description:
|
||||
"Consultez la disponibilité et l’état du parc de votre campus.",
|
||||
icon: Icons.inventory_2_outlined,
|
||||
_VueMaterielsTableau(
|
||||
items: data.materiels,
|
||||
builder: _materielTile,
|
||||
searchableText: (item) {
|
||||
final categorie = Map<String, dynamic>.from(item["categorie"] as Map);
|
||||
return "${item["nom"]} ${item["reference"]} ${item["statut"]} ${categorie["nom"]}";
|
||||
categories: _categoriesMateriels.entries.toList(),
|
||||
rechercheController: _rechercheMaterielsController,
|
||||
statut: _statutMateriels,
|
||||
categorieId: _categorieMateriels,
|
||||
tri: _triMateriels,
|
||||
ordre: _ordreMateriels,
|
||||
onRecherche: _rafraichir,
|
||||
onStatutChanged: (statut) {
|
||||
_filtrerMateriels(statut: statut, categorieId: _categorieMateriels);
|
||||
},
|
||||
onCategorieChanged: (categorieId) {
|
||||
_filtrerMateriels(statut: _statutMateriels, categorieId: categorieId);
|
||||
},
|
||||
onTri: _trierMateriels,
|
||||
onReset: _reinitialiserFiltresMateriels,
|
||||
),
|
||||
_VueListe(
|
||||
titre: "Anomalies",
|
||||
@@ -412,18 +467,6 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _materielTile(Map<String, dynamic> item) {
|
||||
final categorie = Map<String, dynamic>.from(item["categorie"] as Map);
|
||||
return _InfoTile(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
title: "${item["nom"]}",
|
||||
subtitle:
|
||||
"${item["reference"]} · ${categorie["nom"]} · ${item["etatGeneral"]}",
|
||||
meta: "${item["marque"]} ${item["modele"]}",
|
||||
trailing: _StatusBadge("${item["statut"]}"),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _anomalieTile(Map<String, dynamic> item) {
|
||||
final materiel = Map<String, dynamic>.from(item["materiel"] as Map);
|
||||
final prochain = _prochainStatut(item["statut"] as String?);
|
||||
@@ -2612,6 +2655,531 @@ class _VueEmpruntsTableau extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _VueMaterielsTableau extends StatelessWidget {
|
||||
const _VueMaterielsTableau({
|
||||
required this.items,
|
||||
required this.categories,
|
||||
required this.rechercheController,
|
||||
required this.statut,
|
||||
required this.categorieId,
|
||||
required this.tri,
|
||||
required this.ordre,
|
||||
required this.onRecherche,
|
||||
required this.onStatutChanged,
|
||||
required this.onCategorieChanged,
|
||||
required this.onTri,
|
||||
required this.onReset,
|
||||
});
|
||||
|
||||
final List<Map<String, dynamic>> items;
|
||||
final List<MapEntry<int, String>> categories;
|
||||
final TextEditingController rechercheController;
|
||||
final String? statut;
|
||||
final int? categorieId;
|
||||
final String tri;
|
||||
final String ordre;
|
||||
final VoidCallback onRecherche;
|
||||
final ValueChanged<String?> onStatutChanged;
|
||||
final ValueChanged<int?> onCategorieChanged;
|
||||
final ValueChanged<String> onTri;
|
||||
final VoidCallback onReset;
|
||||
|
||||
static const _statuts = [
|
||||
"DISPONIBLE",
|
||||
"RESERVE",
|
||||
"EMPRUNTE",
|
||||
"NON_CONFORME",
|
||||
"DETERIORE",
|
||||
"MAINTENANCE",
|
||||
"INDISPONIBLE",
|
||||
];
|
||||
|
||||
static const _tris = {
|
||||
"nom": "Matériel",
|
||||
"reference": "Référence",
|
||||
"categorie": "Catégorie",
|
||||
"marque": "Marque",
|
||||
"etatGeneral": "État",
|
||||
"statut": "Statut",
|
||||
};
|
||||
|
||||
int? get _sortColumnIndex {
|
||||
return switch (tri) {
|
||||
"reference" => 0,
|
||||
"nom" => 1,
|
||||
"categorie" => 2,
|
||||
"marque" => 3,
|
||||
"etatGeneral" => 4,
|
||||
"statut" => 5,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: _workspaceBackground,
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1440),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(28),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_entete(),
|
||||
const SizedBox(height: 22),
|
||||
_filtres(),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
"${items.length} matériel${items.length > 1 ? "s" : ""}",
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: EnsupColors.text2,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
"Tri : ${_tris[tri]} · ${ordre == "asc" ? "croissant" : "décroissant"}",
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 11,
|
||||
color: EnsupColors.muted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: items.isEmpty
|
||||
? const _EmptyState()
|
||||
: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 820) {
|
||||
return _listeCompacte();
|
||||
}
|
||||
return _tableau(constraints.maxWidth);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _entete() {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: EnsupColors.cyan.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
color: EnsupColors.blue3,
|
||||
size: 21,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Stock matériel",
|
||||
style: GoogleFonts.darkerGrotesque(
|
||||
fontSize: 29,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: EnsupColors.text,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Comparez la disponibilité et l’état du parc de votre campus.",
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 13,
|
||||
color: EnsupColors.muted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _filtres() {
|
||||
final categoriesTriees = [...categories]
|
||||
..sort((a, b) => a.value.toLowerCase().compareTo(b.value.toLowerCase()));
|
||||
|
||||
return Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 310,
|
||||
height: 48,
|
||||
child: TextField(
|
||||
controller: rechercheController,
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (_) => onRecherche(),
|
||||
decoration: InputDecoration(
|
||||
hintText: "Matériel, référence, marque ou modèle",
|
||||
prefixIcon: const Icon(Icons.search, size: 20),
|
||||
suffixIcon: IconButton(
|
||||
tooltip: "Lancer la recherche",
|
||||
onPressed: onRecherche,
|
||||
icon: const Icon(Icons.arrow_forward, size: 18),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: EnsupColors.line),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: EnsupColors.line),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: DropdownButtonFormField<String>(
|
||||
key: ValueKey("statut-materiel-$statut"),
|
||||
initialValue: statut ?? "",
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Statut",
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<String>(
|
||||
value: "",
|
||||
child: Text(
|
||||
"Tous les statuts",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
..._statuts.map(
|
||||
(value) => DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(
|
||||
_statusLabel(value),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
onStatutChanged(value == null || value.isEmpty ? null : value);
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 210,
|
||||
child: DropdownButtonFormField<int>(
|
||||
key: ValueKey("categorie-materiel-$categorieId"),
|
||||
initialValue: categorieId ?? 0,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Catégorie",
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<int>(
|
||||
value: 0,
|
||||
child: Text(
|
||||
"Toutes les catégories",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
...categoriesTriees.map(
|
||||
(categorie) => DropdownMenuItem<int>(
|
||||
value: categorie.key,
|
||||
child: Text(
|
||||
categorie.value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
onCategorieChanged(value == null || value == 0 ? null : value);
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 180,
|
||||
child: DropdownButtonFormField<String>(
|
||||
key: ValueKey("tri-materiel-$tri"),
|
||||
initialValue: tri,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Trier par",
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: _tris.entries
|
||||
.map(
|
||||
(entry) => DropdownMenuItem<String>(
|
||||
value: entry.key,
|
||||
child: Text(
|
||||
entry.value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null && value != tri) {
|
||||
onTri(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: ordre == "asc"
|
||||
? "Passer en ordre décroissant"
|
||||
: "Passer en ordre croissant",
|
||||
child: IconButton.outlined(
|
||||
onPressed: () => onTri(tri),
|
||||
icon: Icon(
|
||||
ordre == "asc"
|
||||
? Icons.arrow_upward_rounded
|
||||
: Icons.arrow_downward_rounded,
|
||||
),
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: "Réinitialiser les filtres",
|
||||
child: IconButton.outlined(
|
||||
onPressed: onReset,
|
||||
icon: const Icon(Icons.filter_alt_off_outlined),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tableau(double largeurDisponible) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: EnsupColors.line),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: largeurDisponible),
|
||||
child: DataTable(
|
||||
sortColumnIndex: _sortColumnIndex,
|
||||
sortAscending: ordre == "asc",
|
||||
headingRowColor: WidgetStatePropertyAll(
|
||||
EnsupColors.soft.withValues(alpha: 0.8),
|
||||
),
|
||||
headingTextStyle: GoogleFonts.titilliumWeb(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: EnsupColors.text2,
|
||||
),
|
||||
dataTextStyle: GoogleFonts.titilliumWeb(
|
||||
fontSize: 12,
|
||||
color: EnsupColors.text,
|
||||
),
|
||||
columns: [
|
||||
DataColumn(
|
||||
label: const Text("Référence"),
|
||||
onSort: (_, _) => onTri("reference"),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text("Matériel"),
|
||||
onSort: (_, _) => onTri("nom"),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text("Catégorie"),
|
||||
onSort: (_, _) => onTri("categorie"),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text("Marque / modèle"),
|
||||
onSort: (_, _) => onTri("marque"),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text("État"),
|
||||
onSort: (_, _) => onTri("etatGeneral"),
|
||||
),
|
||||
DataColumn(
|
||||
label: const Text("Statut"),
|
||||
onSort: (_, _) => onTri("statut"),
|
||||
),
|
||||
],
|
||||
rows: items.map(_ligneTableau).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
DataRow _ligneTableau(Map<String, dynamic> item) {
|
||||
final categorie = Map<String, dynamic>.from(item["categorie"] as Map);
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(
|
||||
Text(
|
||||
"${item["reference"]}",
|
||||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
SizedBox(
|
||||
width: 190,
|
||||
child: Text(
|
||||
"${item["nom"]}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: Text(
|
||||
"${categorie["nom"]}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
SizedBox(
|
||||
width: 170,
|
||||
child: Text(
|
||||
"${item["marque"] ?? "-"} ${item["modele"] ?? ""}".trim(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(Text(_statusLabel("${item["etatGeneral"]}"))),
|
||||
DataCell(_StatusBadge("${item["statut"]}")),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _listeCompacte() {
|
||||
return ListView.separated(
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final categorie = Map<String, dynamic>.from(item["categorie"] as Map);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: EnsupColors.line),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
"${item["nom"]}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.darkerGrotesque(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_StatusBadge("${item["statut"]}"),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
"${item["reference"]}",
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 12,
|
||||
color: EnsupColors.muted,
|
||||
),
|
||||
),
|
||||
const Divider(height: 20, color: EnsupColors.line),
|
||||
_detailCompact(Icons.category_outlined, "${categorie["nom"]}"),
|
||||
_detailCompact(
|
||||
Icons.precision_manufacturing_outlined,
|
||||
"${item["marque"] ?? "-"} ${item["modele"] ?? ""}".trim(),
|
||||
),
|
||||
_detailCompact(
|
||||
Icons.fact_check_outlined,
|
||||
"État : ${_statusLabel("${item["etatGeneral"]}")}",
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _detailCompact(IconData icon, String texte) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 15, color: EnsupColors.muted),
|
||||
const SizedBox(width: 7),
|
||||
Expanded(
|
||||
child: Text(
|
||||
texte,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 12,
|
||||
color: EnsupColors.text2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _VueListe extends StatefulWidget {
|
||||
const _VueListe({
|
||||
required this.titre,
|
||||
|
||||
Reference in New Issue
Block a user