feat(loans): confirm declared material state changes

This commit is contained in:
SaidSoighiri94
2026-07-24 13:41:40 +02:00
parent 3739e9b0a0
commit a562b3248f
26 changed files with 1832 additions and 282 deletions
@@ -4,14 +4,13 @@ import "../demo_identity.dart";
import "../theme/ensup_colors.dart";
import "../models/emprunt_item.dart";
import "../models/anomalie_retour.dart";
import "../models/checklist_element_item.dart";
import "../services/emprunt_service.dart";
import "../widgets/ensup_top_bar.dart";
import "../widgets/brand_corners.dart";
import "../widgets/etat_checklist.dart";
import "resultat_retour_screen.dart";
/// Checklist de retour (RG16) : l'étudiant indique l'état de chaque accessoire
/// au retour, en vue de la comparaison avec la checklist de départ.
class ChecklistRetourScreen extends StatefulWidget {
const ChecklistRetourScreen({super.key, required this.emprunt});
@@ -22,16 +21,41 @@ class ChecklistRetourScreen extends StatefulWidget {
}
class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
late final List<String> _elements =
widget.emprunt.materiel.accessoires.isEmpty
? <String>[widget.emprunt.materiel.nom]
: widget.emprunt.materiel.accessoires;
late final List<EtatChecklist> _etats = List<EtatChecklist>.filled(
_elements.length,
EtatChecklist.present,
late final List<ChecklistElementItem> _checklistDepart =
widget.emprunt.checklistDepart.isNotEmpty
? widget.emprunt.checklistDepart
: (widget.emprunt.materiel.accessoires.isEmpty
? [
ChecklistElementItem(
accessoireId: null,
nom: widget.emprunt.materiel.nom,
etat: "PRESENT",
quantite: 1,
),
]
: widget.emprunt.materiel.accessoires
.map(
(nom) => ChecklistElementItem(
accessoireId: null,
nom: nom,
etat: "PRESENT",
quantite: 1,
),
)
.toList());
late final List<String> _elements = _checklistDepart
.map((element) => element.nom)
.toList();
late final List<EtatChecklist> _etatsInitiales = _checklistDepart
.map((element) => _etatDepuisApi(element.etat))
.toList();
late final List<EtatChecklist> _etats = List<EtatChecklist>.from(
_etatsInitiales,
);
final TextEditingController _commentaireController = TextEditingController();
bool _envoiEnCours = false;
bool _alerteEnCours = false;
bool _alerteEnvoyee = false;
@override
void dispose() {
@@ -44,15 +68,12 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
final anomalies = <AnomalieRetour>[];
for (var i = 0; i < _etats.length; i++) {
if (_etats[i] == EtatChecklist.absent) {
anomalies.add(
AnomalieRetour(element: _elements[i], etatRetour: "Absent au retour"),
);
} else if (_etats[i] == EtatChecklist.deteriore) {
if (_etats[i] != _etatsInitiales[i]) {
anomalies.add(
AnomalieRetour(
element: _elements[i],
etatRetour: "Détérioré au retour",
etatRetour:
"${_etatLabel(_etatsInitiales[i])}${_etatLabel(_etats[i])}",
),
);
}
@@ -63,12 +84,13 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
empruntId: widget.emprunt.id,
commentaireRetour: _commentaireController.text,
elements: _elements.asMap().entries.map((entry) {
final elementDepart = _checklistDepart[entry.key];
return {
if (elementDepart.accessoireId != null)
"accessoireId": elementDepart.accessoireId,
"nomElement": entry.value,
"etat": _etatApi(_etats[entry.key]),
"quantiteConstatee": _etats[entry.key] == EtatChecklist.absent
? 0
: 1,
"quantiteConstatee": _quantiteConstatee(entry.key),
};
}).toList(),
);
@@ -102,6 +124,60 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
}
}
Future<void> _changerEtat(int index, EtatChecklist etat) async {
setState(() => _etats[index] = etat);
if (_alerteEnvoyee || etat == _etatsInitiales[index]) {
return;
}
setState(() {
_alerteEnvoyee = true;
_alerteEnCours = true;
});
try {
await EmpruntService.signalerTentativeRetour(
empruntId: widget.emprunt.id,
nomElement: _elements[index],
etatInitial: _etatApi(_etatsInitiales[index]),
etatDemande: _etatApi(etat),
);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
"Le responsable a été alerté de cette tentative de modification.",
),
),
);
} catch (error) {
if (!mounted) return;
setState(() => _alerteEnvoyee = false);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(error.toString())));
} finally {
if (mounted) {
setState(() => _alerteEnCours = false);
}
}
}
int _quantiteConstatee(int index) {
if (_etats[index] == EtatChecklist.absent) {
return 0;
}
final quantiteDepart = _checklistDepart[index].quantite;
return quantiteDepart > 0 ? quantiteDepart : 1;
}
static EtatChecklist _etatDepuisApi(String etat) {
return switch (etat) {
"ABSENT" => EtatChecklist.absent,
"DETERIORE" => EtatChecklist.deteriore,
_ => EtatChecklist.present,
};
}
String _etatApi(EtatChecklist etat) {
switch (etat) {
case EtatChecklist.present:
@@ -113,6 +189,14 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
}
}
String _etatLabel(EtatChecklist etat) {
return switch (etat) {
EtatChecklist.present => "Présent",
EtatChecklist.absent => "Absent",
EtatChecklist.deteriore => "Détérioré",
};
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -242,7 +326,7 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
ChecklistTableau(
accessoires: _elements,
etats: _etats,
onChanged: (index, etat) => setState(() => _etats[index] = etat),
onChanged: _changerEtat,
),
],
);
@@ -252,11 +336,12 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ChecklistInstructions(
titre: "Rappel checklist de départ",
ChecklistInstructions(
titre: "État validé au départ",
texte:
"Au départ, tous les accessoires du kit étaient présents. "
"Indiquez leur état au retour ; toute différence sera signalée.",
"La restitution reprend automatiquement l'état validé au départ : "
"${_checklistDepart.map((element) => "${element.nom} ${_etatLabel(_etatDepuisApi(element.etat)).toLowerCase()}").join(" · ")}. "
"Toute modification alerte immédiatement un responsable.",
),
const SizedBox(height: 14),
ChecklistCommentaire(
@@ -267,8 +352,8 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _envoiEnCours ? null : _valider,
icon: _envoiEnCours
onPressed: _envoiEnCours || _alerteEnCours ? null : _valider,
icon: _envoiEnCours || _alerteEnCours
? const SizedBox(
width: 18,
height: 18,
@@ -279,7 +364,11 @@ class _ChecklistRetourScreenState extends State<ChecklistRetourScreen> {
)
: const Icon(Icons.check, size: 18),
label: Text(
_envoiEnCours ? "Enregistrement..." : "Valider le retour",
_alerteEnCours
? "Alerte en cours..."
: _envoiEnCours
? "Enregistrement..."
: "Valider le retour",
style: GoogleFonts.darkerGrotesque(
fontWeight: FontWeight.w700,
fontSize: 16,