feat(loans): confirm declared material state changes
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user