feat(loans): confirm declared material state changes
This commit is contained in:
@@ -9,8 +9,6 @@ import "../widgets/brand_corners.dart";
|
||||
import "../widgets/etat_checklist.dart";
|
||||
import "confirmation_screen.dart";
|
||||
|
||||
/// Checklist de départ (RG11) : l'étudiant indique l'état de chaque accessoire
|
||||
/// attendu avant de confirmer l'emprunt.
|
||||
class ChecklistDepartScreen extends StatefulWidget {
|
||||
const ChecklistDepartScreen({super.key, required this.item});
|
||||
|
||||
@@ -31,6 +29,8 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
|
||||
final TextEditingController _commentaireController = TextEditingController();
|
||||
bool _envoiEnCours = false;
|
||||
|
||||
bool get _ecartDeclare => _etats.any((etat) => etat != EtatChecklist.present);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_commentaireController.dispose();
|
||||
@@ -240,12 +240,45 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ChecklistInstructions(
|
||||
titre: "Instructions",
|
||||
titre: "État de référence",
|
||||
texte:
|
||||
"Vérifiez physiquement chaque élément du kit et indiquez son état : "
|
||||
"Présent, Absent ou Détérioré. Signalez tout élément manquant ou "
|
||||
"endommagé pour éviter tout litige au retour.",
|
||||
"Tous les éléments sont préremplis comme présents. Vérifiez-les "
|
||||
"physiquement et modifiez uniquement ceux qui sont absents ou détériorés.",
|
||||
),
|
||||
if (_ecartDeclare) ...[
|
||||
const SizedBox(height: 14),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF7ED),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFFED7AA)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Color(0xFFD97706),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Cet écart sera transmis au responsable. L'emprunt restera "
|
||||
"en attente jusqu'à sa confirmation.",
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 13,
|
||||
color: EnsupColors.text2,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 14),
|
||||
ChecklistCommentaire(controller: _commentaireController),
|
||||
const SizedBox(height: 14),
|
||||
@@ -266,7 +299,9 @@ class _ChecklistDepartScreenState extends State<ChecklistDepartScreen> {
|
||||
label: Text(
|
||||
_envoiEnCours
|
||||
? "Enregistrement..."
|
||||
: "Valider la checklist et confirmer l'emprunt",
|
||||
: _ecartDeclare
|
||||
? "Signaler l'écart"
|
||||
: "Confirmer l'emprunt",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.darkerGrotesque(
|
||||
fontWeight: FontWeight.w700,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -7,7 +7,6 @@ import "../models/materiel_item.dart";
|
||||
import "../widgets/ensup_top_bar.dart";
|
||||
import "../widgets/brand_corners.dart";
|
||||
|
||||
/// Écran de succès clôturant l'emprunt : confirmation + récapitulatif.
|
||||
class ConfirmationScreen extends StatelessWidget {
|
||||
const ConfirmationScreen({super.key, required this.emprunt, this.note});
|
||||
|
||||
@@ -15,6 +14,7 @@ class ConfirmationScreen extends StatelessWidget {
|
||||
final String? note;
|
||||
|
||||
MaterielItem get item => emprunt.materiel;
|
||||
bool get _enAttente => emprunt.statut == "EN_ATTENTE_VALIDATION_DEPART";
|
||||
|
||||
static String _deuxChiffres(int valeur) => valeur.toString().padLeft(2, "0");
|
||||
|
||||
@@ -127,15 +127,23 @@ class ConfirmationScreen extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _boiteSucces() {
|
||||
final couleur = _enAttente
|
||||
? const Color(0xFFD97706)
|
||||
: const Color(0xFF15803D);
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(36),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFF0FDF4), Color(0xFFECFDF5)],
|
||||
gradient: LinearGradient(
|
||||
colors: _enAttente
|
||||
? const [Color(0xFFFFF7ED), Color(0xFFFFFBEB)]
|
||||
: const [Color(0xFFF0FDF4), Color(0xFFECFDF5)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFBBF7D0), width: 2),
|
||||
border: Border.all(
|
||||
color: _enAttente ? const Color(0xFFFED7AA) : const Color(0xFFBBF7D0),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -144,12 +152,14 @@ class ConfirmationScreen extends StatelessWidget {
|
||||
height: 72,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF16A34A), Color(0xFF22C55E)],
|
||||
gradient: LinearGradient(
|
||||
colors: _enAttente
|
||||
? const [Color(0xFFD97706), Color(0xFFF59E0B)]
|
||||
: const [Color(0xFF16A34A), Color(0xFF22C55E)],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF16A34A).withValues(alpha: 0.3),
|
||||
color: couleur.withValues(alpha: 0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
@@ -159,20 +169,24 @@ class ConfirmationScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
"Emprunt enregistré !",
|
||||
_enAttente ? "Écart transmis" : "Emprunt enregistré !",
|
||||
style: GoogleFonts.darkerGrotesque(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: const Color(0xFF15803D),
|
||||
color: couleur,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Votre emprunt a bien été enregistré. Vous pouvez récupérer le matériel.",
|
||||
_enAttente
|
||||
? "Le matériel est réservé. Attendez la confirmation du responsable avant de le récupérer."
|
||||
: "Votre emprunt est actif. Vous pouvez récupérer le matériel.",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 14,
|
||||
color: const Color(0xFF166534),
|
||||
color: _enAttente
|
||||
? const Color(0xFF9A3412)
|
||||
: const Color(0xFF166534),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -211,6 +225,11 @@ class ConfirmationScreen extends StatelessWidget {
|
||||
_rrow("Date d'emprunt", date),
|
||||
_rrow("Heure", heure),
|
||||
_rrow("Campus", item.campus),
|
||||
_rrow(
|
||||
"Statut",
|
||||
_enAttente ? "En attente de confirmation" : "En cours",
|
||||
valueColor: _enAttente ? const Color(0xFFD97706) : null,
|
||||
),
|
||||
if (note != null)
|
||||
_rrow("Note", note, valueColor: const Color(0xFFD97706)),
|
||||
],
|
||||
|
||||
@@ -20,10 +20,12 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
late Future<_ResponsableData> _future = _charger();
|
||||
int _onglet = 0;
|
||||
bool _transitionEnCours = false;
|
||||
bool _decisionEnCours = false;
|
||||
bool _exportEnCours = false;
|
||||
|
||||
Future<_ResponsableData> _charger() async {
|
||||
final dashboard = await ResponsableService.dashboard();
|
||||
final ecarts = await ResponsableService.ecarts();
|
||||
final emprunts = await ResponsableService.emprunts();
|
||||
final materiels = await ResponsableService.materiels();
|
||||
final anomalies = await ResponsableService.anomalies();
|
||||
@@ -32,6 +34,7 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
|
||||
return _ResponsableData(
|
||||
dashboard: dashboard,
|
||||
ecarts: ecarts,
|
||||
emprunts: emprunts,
|
||||
materiels: materiels,
|
||||
anomalies: anomalies,
|
||||
@@ -83,6 +86,48 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _examinerEcart(Map<String, dynamic> ecart) async {
|
||||
if (_decisionEnCours) return;
|
||||
|
||||
final decision = await showDialog<_DecisionEcart>(
|
||||
context: context,
|
||||
builder: (context) => _EcartDecisionDialog(ecart: ecart),
|
||||
);
|
||||
if (decision == null || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _decisionEnCours = true);
|
||||
try {
|
||||
await ResponsableService.deciderEcart(
|
||||
empruntId: ecart["id"] as int,
|
||||
decision: decision.decision,
|
||||
observation: decision.observation,
|
||||
statutMateriel: decision.statutMateriel,
|
||||
);
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
decision.decision == "CONFIRMER"
|
||||
? "Le changement a été confirmé et l'anomalie a été créée."
|
||||
: "Le changement a été refusé.",
|
||||
),
|
||||
),
|
||||
);
|
||||
_rafraichir();
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(error.toString())));
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _decisionEnCours = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _marquerNotificationsLues() async {
|
||||
try {
|
||||
await ResponsableService.marquerNotificationsLues();
|
||||
@@ -169,6 +214,7 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
_statutCount(anomalies, "DETECTEE") +
|
||||
_statutCount(anomalies, "EN_COURS_TRAITEMENT");
|
||||
final notificationsNonLues = kpis["notificationsNonLues"] as int? ?? 0;
|
||||
final ecartsEnAttente = data.ecarts.length;
|
||||
|
||||
final vues = [
|
||||
_VueDashboard(
|
||||
@@ -176,6 +222,19 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
onNavigate: (index) => setState(() => _onglet = index),
|
||||
onRefresh: _rafraichir,
|
||||
),
|
||||
_VueListe(
|
||||
titre: "Écarts à confirmer",
|
||||
description:
|
||||
"Contrôlez les changements déclarés avant toute modification de l’état officiel.",
|
||||
icon: Icons.fact_check_outlined,
|
||||
items: data.ecarts,
|
||||
builder: _ecartTile,
|
||||
searchableText: (item) {
|
||||
final etudiant = Map<String, dynamic>.from(item["etudiant"] as Map);
|
||||
final materiel = Map<String, dynamic>.from(item["materiel"] as Map);
|
||||
return "${item["typeEcart"]} ${materiel["nom"]} ${materiel["reference"]} ${etudiant["prenom"]} ${etudiant["nom"]}";
|
||||
},
|
||||
),
|
||||
_VueListe(
|
||||
titre: "Emprunts",
|
||||
description:
|
||||
@@ -257,6 +316,7 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
if (!compact)
|
||||
_ResponsableSidebar(
|
||||
index: _onglet,
|
||||
ecartsEnAttente: ecartsEnAttente,
|
||||
anomaliesActives: anomaliesActives,
|
||||
notificationsNonLues: notificationsNonLues,
|
||||
onChanged: (index) => setState(() => _onglet = index),
|
||||
@@ -293,6 +353,28 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _ecartTile(Map<String, dynamic> item) {
|
||||
final etudiant = Map<String, dynamic>.from(item["etudiant"] as Map);
|
||||
final materiel = Map<String, dynamic>.from(item["materiel"] as Map);
|
||||
final type = item["typeEcart"] == "DEPART" ? "Départ" : "Retour";
|
||||
return _InfoTile(
|
||||
icon: Icons.fact_check_outlined,
|
||||
title: "$type · ${materiel["nom"]}",
|
||||
subtitle:
|
||||
"${etudiant["prenom"]} ${etudiant["nom"]} · ${materiel["reference"]}",
|
||||
meta: "Emprunt #${item["id"]}",
|
||||
trailing: FilledButton.icon(
|
||||
onPressed: _decisionEnCours ? null : () => _examinerEcart(item),
|
||||
icon: const Icon(Icons.visibility_outlined, size: 16),
|
||||
label: const Text("Examiner"),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: EnsupColors.blue3,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(7)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _materielTile(Map<String, dynamic> item) {
|
||||
final categorie = Map<String, dynamic>.from(item["categorie"] as Map);
|
||||
return _InfoTile(
|
||||
@@ -358,13 +440,16 @@ class _ResponsableScreenState extends State<ResponsableScreen> {
|
||||
String _statusLabel(String statut) {
|
||||
return switch (statut) {
|
||||
"DISPONIBLE" => "Disponible",
|
||||
"RESERVE" => "Réservé",
|
||||
"EMPRUNTE" => "Emprunté",
|
||||
"NON_CONFORME" => "Non conforme",
|
||||
"DETERIORE" => "Détérioré",
|
||||
"MAINTENANCE" => "Maintenance",
|
||||
"INDISPONIBLE" => "Indisponible",
|
||||
"EN_COURS" => "En cours",
|
||||
"EN_ATTENTE_VALIDATION_DEPART" => "Écart départ en attente",
|
||||
"EN_RETARD" => "En retard",
|
||||
"EN_ATTENTE_VALIDATION_RETOUR" => "Écart retour en attente",
|
||||
"CLOTURE" => "Clôturé",
|
||||
"RETOUR_NON_CONFORME" => "Retour non conforme",
|
||||
"ANNULE" => "Annulé",
|
||||
@@ -374,6 +459,8 @@ String _statusLabel(String statut) {
|
||||
"CLOTUREE" => "Clôturée",
|
||||
"LUE" => "Lue",
|
||||
"NON_LUE" => "Non lue",
|
||||
"PRESENT" => "Présent",
|
||||
"ABSENT" => "Absent",
|
||||
_ => statut.replaceAll("_", " ").toLowerCase(),
|
||||
};
|
||||
}
|
||||
@@ -383,6 +470,15 @@ String _actionLabel(String action) {
|
||||
"CREATION_EMPRUNT" => "Création d'emprunt",
|
||||
"CREATION_ANOMALIE" => "Création d'anomalie",
|
||||
"TRAITEMENT_ANOMALIE" => "Traitement d'anomalie",
|
||||
"TENTATIVE_MODIFICATION_RETOUR" => "Tentative de modification au retour",
|
||||
"ECART_DEPART_SIGNALE" => "Écart signalé au départ",
|
||||
"ECART_RETOUR_SIGNALE" => "Écart signalé au retour",
|
||||
"ECART_DEPART_CONFIRMER" => "Écart de départ confirmé",
|
||||
"ECART_DEPART_REFUSER" => "Écart de départ refusé",
|
||||
"ECART_RETOUR_CONFIRMER" => "Écart de retour confirmé",
|
||||
"ECART_RETOUR_REFUSER" => "Écart de retour refusé",
|
||||
"EMPRUNT_AUTOMATIQUE" => "Emprunt automatique",
|
||||
"RESTITUTION_AUTOMATIQUE" => "Restitution automatique",
|
||||
_ => action.replaceAll("_", " ").toLowerCase(),
|
||||
};
|
||||
}
|
||||
@@ -400,11 +496,369 @@ Color _statusColor(String statut) {
|
||||
"DETECTEE" ||
|
||||
"NON_LUE" => EnsupColors.red,
|
||||
"EN_COURS" || "EN_COURS_TRAITEMENT" || "EMPRUNTE" => EnsupColors.cyan,
|
||||
"RESERVE" ||
|
||||
"EN_ATTENTE_VALIDATION_DEPART" ||
|
||||
"EN_ATTENTE_VALIDATION_RETOUR" => _orange,
|
||||
"MAINTENANCE" || "INDISPONIBLE" || "DETERIORE" => EnsupColors.purple,
|
||||
_ => EnsupColors.blue3,
|
||||
};
|
||||
}
|
||||
|
||||
class _DecisionEcart {
|
||||
const _DecisionEcart({
|
||||
required this.decision,
|
||||
required this.observation,
|
||||
this.statutMateriel,
|
||||
});
|
||||
|
||||
final String decision;
|
||||
final String observation;
|
||||
final String? statutMateriel;
|
||||
}
|
||||
|
||||
class _EcartDecisionDialog extends StatefulWidget {
|
||||
const _EcartDecisionDialog({required this.ecart});
|
||||
|
||||
final Map<String, dynamic> ecart;
|
||||
|
||||
@override
|
||||
State<_EcartDecisionDialog> createState() => _EcartDecisionDialogState();
|
||||
}
|
||||
|
||||
class _EcartDecisionDialogState extends State<_EcartDecisionDialog> {
|
||||
final TextEditingController _observationController = TextEditingController();
|
||||
String _statutMateriel = "NON_CONFORME";
|
||||
|
||||
bool get _estRetour => widget.ecart["typeEcart"] == "RETOUR";
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_observationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _elements(String type) {
|
||||
final checklists = widget.ecart["checklists"] as List<dynamic>? ?? const [];
|
||||
for (final checklistValue in checklists) {
|
||||
final checklist = Map<String, dynamic>.from(checklistValue as Map);
|
||||
if (checklist["type"] == type) {
|
||||
return (checklist["elements"] as List<dynamic>? ?? const [])
|
||||
.map((element) => Map<String, dynamic>.from(element as Map))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
String _etat(Map<String, dynamic> element) {
|
||||
final quantite = element["quantiteConstatee"] as int? ?? 0;
|
||||
return "${_statusLabel("${element["etat"]}")} · quantité $quantite";
|
||||
}
|
||||
|
||||
List<Widget> _comparaisons() {
|
||||
final depart = _elements("DEPART");
|
||||
if (!_estRetour) {
|
||||
return depart
|
||||
.map(
|
||||
(element) => _EcartComparisonRow(
|
||||
nom: "${element["nomElement"]}",
|
||||
reference: "Présent · état de référence",
|
||||
declaration: _etat(element),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final retour = _elements("RETOUR");
|
||||
final lignes = <Widget>[];
|
||||
for (final elementDepart in depart) {
|
||||
Map<String, dynamic>? elementRetour;
|
||||
for (final candidat in retour) {
|
||||
if (candidat["nomElement"] == elementDepart["nomElement"]) {
|
||||
elementRetour = candidat;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (elementRetour == null ||
|
||||
elementRetour["etat"] != elementDepart["etat"] ||
|
||||
elementRetour["quantiteConstatee"] !=
|
||||
elementDepart["quantiteConstatee"]) {
|
||||
lignes.add(
|
||||
_EcartComparisonRow(
|
||||
nom: "${elementDepart["nomElement"]}",
|
||||
reference: _etat(elementDepart),
|
||||
declaration: elementRetour == null
|
||||
? "Élément non renseigné"
|
||||
: _etat(elementRetour),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return lignes;
|
||||
}
|
||||
|
||||
void _terminer(String decision) {
|
||||
Navigator.of(context).pop(
|
||||
_DecisionEcart(
|
||||
decision: decision,
|
||||
observation: _observationController.text,
|
||||
statutMateriel: _estRetour && decision == "CONFIRMER"
|
||||
? _statutMateriel
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final etudiant = Map<String, dynamic>.from(widget.ecart["etudiant"] as Map);
|
||||
final materiel = Map<String, dynamic>.from(widget.ecart["materiel"] as Map);
|
||||
final type = _estRetour ? "retour" : "départ";
|
||||
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(20),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 720, maxHeight: 760),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 22, 16, 18),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 42,
|
||||
height: 42,
|
||||
decoration: BoxDecoration(
|
||||
color: _orange.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.fact_check_outlined,
|
||||
color: _orange,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Contrôle de $type",
|
||||
style: GoogleFonts.darkerGrotesque(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: EnsupColors.text,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${materiel["nom"]} · ${etudiant["prenom"]} ${etudiant["nom"]}",
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 13,
|
||||
color: EnsupColors.muted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: "Fermer",
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, color: EnsupColors.line),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
_estRetour
|
||||
? "Différences avec l’état validé au départ"
|
||||
: "Différences avec l’état de référence",
|
||||
style: GoogleFonts.darkerGrotesque(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
..._comparaisons(),
|
||||
const SizedBox(height: 20),
|
||||
if (_estRetour) ...[
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: _statutMateriel,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "État final si le changement est confirmé",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: "DISPONIBLE",
|
||||
child: Text("Disponible"),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "NON_CONFORME",
|
||||
child: Text("Non conforme"),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "DETERIORE",
|
||||
child: Text("Détérioré"),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "MAINTENANCE",
|
||||
child: Text("Maintenance"),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: "INDISPONIBLE",
|
||||
child: Text("Indisponible"),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() => _statutMateriel = value);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
],
|
||||
TextField(
|
||||
controller: _observationController,
|
||||
minLines: 2,
|
||||
maxLines: 4,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Observation",
|
||||
hintText: "Constat effectué lors du contrôle physique",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, color: EnsupColors.line),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.end,
|
||||
spacing: 10,
|
||||
runSpacing: 10,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _terminer("REFUSER"),
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
label: const Text("Refuser le changement"),
|
||||
),
|
||||
FilledButton.icon(
|
||||
onPressed: () => _terminer("CONFIRMER"),
|
||||
icon: const Icon(Icons.check, size: 18),
|
||||
label: const Text("Confirmer le changement"),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: EnsupColors.blue3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EcartComparisonRow extends StatelessWidget {
|
||||
const _EcartComparisonRow({
|
||||
required this.nom,
|
||||
required this.reference,
|
||||
required this.declaration,
|
||||
});
|
||||
|
||||
final String nom;
|
||||
final String reference;
|
||||
final String declaration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(bottom: BorderSide(color: EnsupColors.line)),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final compact = constraints.maxWidth < 520;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
nom,
|
||||
style: GoogleFonts.darkerGrotesque(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
if (compact)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
reference,
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 13,
|
||||
color: EnsupColors.text2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
declaration,
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _orange,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
reference,
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 13,
|
||||
color: EnsupColors.text2,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_forward, size: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
declaration,
|
||||
textAlign: TextAlign.right,
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _orange,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _repartition(Object? value) {
|
||||
if (value is! Map) return <String, dynamic>{};
|
||||
return Map<String, dynamic>.from(value);
|
||||
@@ -622,6 +1076,7 @@ class _ResponsableTopBar extends StatelessWidget {
|
||||
class _ResponsableSidebar extends StatelessWidget {
|
||||
const _ResponsableSidebar({
|
||||
required this.index,
|
||||
required this.ecartsEnAttente,
|
||||
required this.anomaliesActives,
|
||||
required this.notificationsNonLues,
|
||||
required this.onChanged,
|
||||
@@ -629,6 +1084,7 @@ class _ResponsableSidebar extends StatelessWidget {
|
||||
});
|
||||
|
||||
final int index;
|
||||
final int ecartsEnAttente;
|
||||
final int anomaliesActives;
|
||||
final int notificationsNonLues;
|
||||
final ValueChanged<int> onChanged;
|
||||
@@ -636,6 +1092,7 @@ class _ResponsableSidebar extends StatelessWidget {
|
||||
|
||||
static const _destinations = [
|
||||
("Vue d’ensemble", Icons.space_dashboard_outlined),
|
||||
("Écarts à confirmer", Icons.fact_check_outlined),
|
||||
("Emprunts", Icons.assignment_outlined),
|
||||
("Stock matériel", Icons.inventory_2_outlined),
|
||||
("Anomalies", Icons.report_problem_outlined),
|
||||
@@ -665,8 +1122,9 @@ class _ResponsableSidebar extends StatelessWidget {
|
||||
...List.generate(_destinations.length, (itemIndex) {
|
||||
final destination = _destinations[itemIndex];
|
||||
final badge = switch (itemIndex) {
|
||||
3 => anomaliesActives,
|
||||
4 => notificationsNonLues,
|
||||
1 => ecartsEnAttente,
|
||||
4 => anomaliesActives,
|
||||
5 => notificationsNonLues,
|
||||
_ => 0,
|
||||
};
|
||||
return _SidebarItem(
|
||||
@@ -674,7 +1132,7 @@ class _ResponsableSidebar extends StatelessWidget {
|
||||
icon: destination.$2,
|
||||
selected: itemIndex == index,
|
||||
badge: badge,
|
||||
alert: itemIndex == 3,
|
||||
alert: itemIndex == 1 || itemIndex == 4,
|
||||
onTap: () => onChanged(itemIndex),
|
||||
);
|
||||
}),
|
||||
@@ -839,6 +1297,7 @@ class _NavigationCompacte extends StatelessWidget {
|
||||
|
||||
static const _destinations = [
|
||||
("Vue d’ensemble", Icons.space_dashboard_outlined),
|
||||
("Écarts", Icons.fact_check_outlined),
|
||||
("Emprunts", Icons.assignment_outlined),
|
||||
("Stock", Icons.inventory_2_outlined),
|
||||
("Anomalies", Icons.report_problem_outlined),
|
||||
@@ -917,6 +1376,7 @@ class _ChargementWorkspace extends StatelessWidget {
|
||||
class _ResponsableData {
|
||||
const _ResponsableData({
|
||||
required this.dashboard,
|
||||
required this.ecarts,
|
||||
required this.emprunts,
|
||||
required this.materiels,
|
||||
required this.anomalies,
|
||||
@@ -925,6 +1385,7 @@ class _ResponsableData {
|
||||
});
|
||||
|
||||
final Map<String, dynamic> dashboard;
|
||||
final List<Map<String, dynamic>> ecarts;
|
||||
final List<Map<String, dynamic>> emprunts;
|
||||
final List<Map<String, dynamic>> materiels;
|
||||
final List<Map<String, dynamic>> anomalies;
|
||||
@@ -962,6 +1423,7 @@ class _VueDashboard extends StatelessWidget {
|
||||
_statutCount(anomalies, "DETECTEE") +
|
||||
_statutCount(anomalies, "EN_COURS_TRAITEMENT");
|
||||
final notificationsNonLues = kpis["notificationsNonLues"] as int? ?? 0;
|
||||
final ecartsEnAttente = data.ecarts.length;
|
||||
final indisponibles = [
|
||||
"NON_CONFORME",
|
||||
"DETERIORE",
|
||||
@@ -1043,9 +1505,10 @@ class _VueDashboard extends StatelessWidget {
|
||||
builder: (context, constraints) {
|
||||
final activity = _ActivityPanel(
|
||||
items: activite,
|
||||
onSeeAll: () => onNavigate(5),
|
||||
onSeeAll: () => onNavigate(6),
|
||||
);
|
||||
final priorities = _PrioritiesPanel(
|
||||
ecarts: ecartsEnAttente,
|
||||
retards: retards,
|
||||
anomalies: anomaliesActives,
|
||||
notifications: notificationsNonLues,
|
||||
@@ -1334,6 +1797,7 @@ class _ActivityRow extends StatelessWidget {
|
||||
|
||||
class _PrioritiesPanel extends StatelessWidget {
|
||||
const _PrioritiesPanel({
|
||||
required this.ecarts,
|
||||
required this.retards,
|
||||
required this.anomalies,
|
||||
required this.notifications,
|
||||
@@ -1343,6 +1807,7 @@ class _PrioritiesPanel extends StatelessWidget {
|
||||
required this.onNavigate,
|
||||
});
|
||||
|
||||
final int ecarts;
|
||||
final int retards;
|
||||
final int anomalies;
|
||||
final int notifications;
|
||||
@@ -1361,12 +1826,20 @@ class _PrioritiesPanel extends StatelessWidget {
|
||||
subtitle: "Éléments qui nécessitent votre suivi",
|
||||
child: Column(
|
||||
children: [
|
||||
_PriorityRow(
|
||||
label: "Écarts à confirmer",
|
||||
value: ecarts,
|
||||
icon: Icons.fact_check_outlined,
|
||||
color: EnsupColors.red,
|
||||
onTap: () => onNavigate(1),
|
||||
),
|
||||
const Divider(height: 1, color: EnsupColors.line),
|
||||
_PriorityRow(
|
||||
label: "Emprunts en retard",
|
||||
value: retards,
|
||||
icon: Icons.schedule_outlined,
|
||||
color: _orange,
|
||||
onTap: () => onNavigate(1),
|
||||
onTap: () => onNavigate(2),
|
||||
),
|
||||
const Divider(height: 1, color: EnsupColors.line),
|
||||
_PriorityRow(
|
||||
@@ -1374,7 +1847,7 @@ class _PrioritiesPanel extends StatelessWidget {
|
||||
value: anomalies,
|
||||
icon: Icons.report_problem_outlined,
|
||||
color: EnsupColors.red,
|
||||
onTap: () => onNavigate(3),
|
||||
onTap: () => onNavigate(4),
|
||||
),
|
||||
const Divider(height: 1, color: EnsupColors.line),
|
||||
_PriorityRow(
|
||||
@@ -1382,7 +1855,7 @@ class _PrioritiesPanel extends StatelessWidget {
|
||||
value: notifications,
|
||||
icon: Icons.notifications_none_outlined,
|
||||
color: EnsupColors.purple,
|
||||
onTap: () => onNavigate(4),
|
||||
onTap: () => onNavigate(5),
|
||||
),
|
||||
const Divider(height: 1, color: EnsupColors.line),
|
||||
_PriorityRow(
|
||||
@@ -1390,7 +1863,7 @@ class _PrioritiesPanel extends StatelessWidget {
|
||||
value: indisponibles,
|
||||
icon: Icons.build_outlined,
|
||||
color: EnsupColors.teal,
|
||||
onTap: () => onNavigate(2),
|
||||
onTap: () => onNavigate(3),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
|
||||
@@ -7,8 +7,6 @@ import "../models/anomalie_retour.dart";
|
||||
import "../widgets/ensup_top_bar.dart";
|
||||
import "../widgets/brand_corners.dart";
|
||||
|
||||
/// Résultat de la restitution (RG17-RG20) : conforme (succès) ou non conforme
|
||||
/// (anomalie détectée + notification au responsable).
|
||||
class ResultatRetourScreen extends StatelessWidget {
|
||||
const ResultatRetourScreen({
|
||||
super.key,
|
||||
@@ -19,7 +17,8 @@ class ResultatRetourScreen extends StatelessWidget {
|
||||
final EmpruntItem emprunt;
|
||||
final List<AnomalieRetour> anomalies;
|
||||
|
||||
bool get _conforme => anomalies.isEmpty;
|
||||
bool get _conforme => emprunt.statut == "CLOTURE";
|
||||
bool get _enAttente => emprunt.statut == "EN_ATTENTE_VALIDATION_RETOUR";
|
||||
|
||||
static String _deux(int valeur) => valeur.toString().padLeft(2, "0");
|
||||
|
||||
@@ -27,6 +26,8 @@ class ResultatRetourScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final fond = _conforme
|
||||
? const [Color(0xFFF0FDF4), Color(0xFFF8FAFC)]
|
||||
: _enAttente
|
||||
? const [Color(0xFFFFF7ED), Color(0xFFF8FAFC)]
|
||||
: const [Color(0xFFFFF5F5), Color(0xFFF8FAFC)];
|
||||
|
||||
return Scaffold(
|
||||
@@ -75,7 +76,12 @@ class ResultatRetourScreen extends StatelessWidget {
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: Column(
|
||||
children: [
|
||||
if (_conforme) _succes() else _anomalie(),
|
||||
if (_conforme)
|
||||
_succes()
|
||||
else if (_enAttente)
|
||||
_attente()
|
||||
else
|
||||
_anomalie(),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -280,6 +286,81 @@ class ResultatRetourScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _attente() {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(22),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF7ED),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFFED7AA), width: 1.5),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.pending_actions_outlined,
|
||||
color: Color(0xFFD97706),
|
||||
size: 42,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
"Changement en attente",
|
||||
style: GoogleFonts.darkerGrotesque(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: const Color(0xFF9A3412),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Le responsable a été alerté. Le changement reste provisoire "
|
||||
"et aucune anomalie n'est créée avant sa confirmation.",
|
||||
textAlign: TextAlign.center,
|
||||
style: GoogleFonts.titilliumWeb(
|
||||
fontSize: 14,
|
||||
color: EnsupColors.text2,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: EnsupColors.line),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"MODIFICATIONS DÉCLARÉES",
|
||||
style: GoogleFonts.darkerGrotesque(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: EnsupColors.muted,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
for (final anomalie in anomalies)
|
||||
_drow(
|
||||
anomalie.element,
|
||||
anomalie.etatRetour,
|
||||
valeurCouleur: const Color(0xFFD97706),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _drow(String cle, String valeur, {Color? valeurCouleur}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||
|
||||
Reference in New Issue
Block a user