Compare commits
3 Commits
506225ac62
...
6d320bedc9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d320bedc9 | ||
|
|
cc7abba373 | ||
|
|
890449d5e3 |
@@ -18,7 +18,6 @@ canvaskit/canvaskit.js,1759914809082,bb9141a62dec1f0a41e311b845569915df9ebb5f074
|
|||||||
canvaskit/chromium/canvaskit.wasm,1759914809184,4a868d7961a9740ae6694f62fc15b2b0ed76df50598e8311d61e8ee814d78229
|
canvaskit/chromium/canvaskit.wasm,1759914809184,4a868d7961a9740ae6694f62fc15b2b0ed76df50598e8311d61e8ee814d78229
|
||||||
canvaskit/chromium/canvaskit.js.symbols,1759914809141,f395278c466a0eaed0201edd6b14a3aa8fee0a16bfedee2d239835cd7e865472
|
canvaskit/chromium/canvaskit.js.symbols,1759914809141,f395278c466a0eaed0201edd6b14a3aa8fee0a16bfedee2d239835cd7e865472
|
||||||
canvaskit/chromium/canvaskit.js,1759914809136,ce5184f74e2501d849490df34d0506167a0708b9120be088039b785343335664
|
canvaskit/chromium/canvaskit.js,1759914809136,ce5184f74e2501d849490df34d0506167a0708b9120be088039b785343335664
|
||||||
assets/packages/flutter_map/lib/assets/flutter_map_logo.png,1759916249804,26fe50c9203ccf93512b80d4ee1a7578184a910457b36a6a5b7d41b799efb966
|
|
||||||
assets/packages/flutter_dropzone_web/assets/flutter_dropzone.js,1748366257688,d640313cd6a02692249cd41e4643c2771b4202cc84e0f07f5f65cdc77a36826f
|
assets/packages/flutter_dropzone_web/assets/flutter_dropzone.js,1748366257688,d640313cd6a02692249cd41e4643c2771b4202cc84e0f07f5f65cdc77a36826f
|
||||||
assets/assets/Google__G__logo.svg,1741027482182,b365d560438f8f04caf08ffaa5d8995eff6c09b36b4483f44d6f5f82d8559d4f
|
assets/assets/Google__G__logo.svg,1741027482182,b365d560438f8f04caf08ffaa5d8995eff6c09b36b4483f44d6f5f82d8559d4f
|
||||||
assets/assets/google.png,1741029771653,537ca60ffa74c28eca4e62d249237403a7d47d2bc90bb90431b8d5aa923a92ee
|
assets/assets/google.png,1741029771653,537ca60ffa74c28eca4e62d249237403a7d47d2bc90bb90431b8d5aa923a92ee
|
||||||
@@ -32,16 +31,16 @@ assets/assets/images/tshirt-incrust.webp,1737393735487,af7cb34adfca19c0b41c8eb63
|
|||||||
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
||||||
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
||||||
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
||||||
version.json,1770478530807,2cbfdf7f34574c2f9d4f1af02acb86d8d230af93790c97a3c7e1674c4db42ef4
|
version.json,1771936797689,d107c3101fad9b2d43d0444e55cd784da08fa919db377f837d8b87a14d13d0be
|
||||||
index.html,1770478536326,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
index.html,1771936803931,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||||
flutter_service_worker.js,1770478628965,cb72807cfcb05b0a2e7b3f4f0cf618a0284a3d2476c93672bd86ea99670b0f5d
|
flutter_service_worker.js,1771936884025,fcbf7871d99f057b5b0d89f4d158bf26151769d47c42a538bba4fcd29c8503b9
|
||||||
assets/FontManifest.json,1770478624084,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
flutter_bootstrap.js,1771936803915,2e42b2a1c547488d5f71f985eecb263baf89d65f364a6522ab2bd3ec39b0a1b8
|
||||||
assets/AssetManifest.json,1770478624084,1e1501af5844823ef215cf650f4cef4002c0389d88770225ac07576d57dc1067
|
assets/FontManifest.json,1771936879989,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||||
flutter_bootstrap.js,1770478536318,bf4a3b4bf79eaed1ce24892f20cfb270bcc22fb392bc9f6a1d17aeed42ed4ed8
|
assets/AssetManifest.json,1771936879989,4bf36ad943b43d5ad6d8d214e15d84a5633d80d8e08158af25f55baf8980a4e9
|
||||||
assets/AssetManifest.bin.json,1770478624084,f446eb3de964f3a6f9e76fcc98d79a81b0429e076c9c7bf30cf8edd0263a0b0a
|
assets/AssetManifest.bin.json,1771936879989,c3fa09840d3272b5c70eb6303934d535526be377a9b64fd62ab4f7c48351bd9e
|
||||||
assets/AssetManifest.bin,1770478624084,72bbccb69d9a02d3885df0c5e58ebfed29e25a4919e10bf195b59542f4709ca3
|
assets/AssetManifest.bin,1771936879987,c9151561940e124b5fc93ff810ec49ddb3472e8ae438063c6c5c448444fa2b9e
|
||||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1770478628013,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
assets/shaders/ink_sparkle.frag,1771936880185,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||||
assets/shaders/ink_sparkle.frag,1770478624492,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1771936882976,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||||
assets/fonts/MaterialIcons-Regular.otf,1770478628013,50e06fd231edee237d875cddbae1e22b682d32bb1284e3c32ca409fa489f9c21
|
assets/fonts/MaterialIcons-Regular.otf,1771936882976,8d5c5d5749998c2d29a44f452643a016f3461a3d8cd40af1ef52c9be9c6fe021
|
||||||
assets/NOTICES,1770478624086,d02d64a466e62fdaeee2534a3f65541362ccf29beb495e2af0fdce41f4ae28d9
|
assets/NOTICES,1771936879989,538f39b8622001ef1c5520fab893e310b1f6f10bf65c6413c9412c9c14945bfa
|
||||||
main.dart.js,1770478620736,03d43aeaa96cfdbe5b7491f9610223ec95c29d47095570dd61cd6cddac863496
|
main.dart.js,1771936874652,2f1686906db6e8313b40447f700f72100c53797a775179e5193fe803e3446125
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
Toutes les modifications notables de ce projet seront documentées dans ce fichier.
|
Toutes les modifications notables de ce projet seront documentées dans ce fichier.
|
||||||
|
|
||||||
|
## 24/02/2026
|
||||||
|
Ajout de la gestion des maintenance et synthèse vocale
|
||||||
|
|
||||||
|
## 18/02/2026
|
||||||
|
Ajout de la fonctionnalité d'exportation des données au format CSV. Correction de bugs mineurs et amélioration des performances.
|
||||||
|
|
||||||
## 🚀 Nouveautés de la mise à jour
|
## 🚀 Nouveautés de la mise à jour
|
||||||
|
|
||||||
Cette version apporte des outils majeurs pour faciliter la gestion de votre parc et de vos événements :
|
Cette version apporte des outils majeurs pour faciliter la gestion de votre parc et de vos événements :
|
||||||
|
|||||||
BIN
em2rp/assets/sounds/error.mp3
Normal file
BIN
em2rp/assets/sounds/error.mp3
Normal file
Binary file not shown.
BIN
em2rp/assets/sounds/ok.mp3
Normal file
BIN
em2rp/assets/sounds/ok.mp3
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
/// Configuration de la version de l'application
|
/// Configuration de la version de l'application
|
||||||
class AppVersion {
|
class AppVersion {
|
||||||
static const String version = '1.1.4';
|
static const String version = '1.1.7';
|
||||||
|
|
||||||
/// Retourne la version complète de l'application
|
/// Retourne la version complète de l'application
|
||||||
static String get fullVersion => 'v$version';
|
static String get fullVersion => 'v$version';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:em2rp/views/calendar_page.dart';
|
|||||||
import 'package:em2rp/views/login_page.dart';
|
import 'package:em2rp/views/login_page.dart';
|
||||||
import 'package:em2rp/views/equipment_management_page.dart';
|
import 'package:em2rp/views/equipment_management_page.dart';
|
||||||
import 'package:em2rp/views/container_management_page.dart';
|
import 'package:em2rp/views/container_management_page.dart';
|
||||||
|
import 'package:em2rp/views/maintenance_management_page.dart';
|
||||||
import 'package:em2rp/views/container_form_page.dart';
|
import 'package:em2rp/views/container_form_page.dart';
|
||||||
import 'package:em2rp/views/container_detail_page.dart';
|
import 'package:em2rp/views/container_detail_page.dart';
|
||||||
import 'package:em2rp/views/event_preparation_page.dart';
|
import 'package:em2rp/views/event_preparation_page.dart';
|
||||||
@@ -159,6 +160,9 @@ class MyApp extends StatelessWidget {
|
|||||||
'/container_management': (context) => const AuthGuard(
|
'/container_management': (context) => const AuthGuard(
|
||||||
requiredPermission: "view_equipment",
|
requiredPermission: "view_equipment",
|
||||||
child: ContainerManagementPage()),
|
child: ContainerManagementPage()),
|
||||||
|
'/maintenance_management': (context) => const AuthGuard(
|
||||||
|
requiredPermission: "manage_maintenances",
|
||||||
|
child: MaintenanceManagementPage()),
|
||||||
'/container_form': (context) {
|
'/container_form': (context) {
|
||||||
final args = ModalRoute.of(context)?.settings.arguments;
|
final args = ModalRoute.of(context)?.settings.arguments;
|
||||||
return AuthGuard(
|
return AuthGuard(
|
||||||
|
|||||||
@@ -1,14 +1,39 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:em2rp/models/maintenance_model.dart';
|
import 'package:em2rp/models/maintenance_model.dart';
|
||||||
import 'package:em2rp/services/maintenance_service.dart';
|
import 'package:em2rp/services/maintenance_service.dart';
|
||||||
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
|
|
||||||
class MaintenanceProvider extends ChangeNotifier {
|
class MaintenanceProvider extends ChangeNotifier {
|
||||||
final MaintenanceService _service = MaintenanceService();
|
final MaintenanceService _service = MaintenanceService();
|
||||||
|
|
||||||
List<MaintenanceModel> _maintenances = [];
|
List<MaintenanceModel> _maintenances = [];
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
List<MaintenanceModel> get maintenances => _maintenances;
|
List<MaintenanceModel> get maintenances => _maintenances;
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
|
||||||
|
/// Charger toutes les maintenances
|
||||||
|
Future<void> loadMaintenances({String? equipmentId}) async {
|
||||||
|
_isLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (equipmentId != null) {
|
||||||
|
_maintenances = await _service.getMaintenancesByEquipment(equipmentId);
|
||||||
|
} else {
|
||||||
|
_maintenances = await _service.getAllMaintenances();
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[MaintenanceProvider] Error loading maintenances', e);
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Récupérer les maintenances pour un équipement spécifique
|
/// Récupérer les maintenances pour un équipement spécifique
|
||||||
Future<List<MaintenanceModel>> getMaintenances(String equipmentId) async {
|
Future<List<MaintenanceModel>> getMaintenances(String equipmentId) async {
|
||||||
@@ -24,9 +49,9 @@ class MaintenanceProvider extends ChangeNotifier {
|
|||||||
Future<void> createMaintenance(MaintenanceModel maintenance) async {
|
Future<void> createMaintenance(MaintenanceModel maintenance) async {
|
||||||
try {
|
try {
|
||||||
await _service.createMaintenance(maintenance);
|
await _service.createMaintenance(maintenance);
|
||||||
notifyListeners();
|
await loadMaintenances(); // Recharger après création
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error creating maintenance: $e');
|
DebugLog.error('[MaintenanceProvider] Error creating maintenance', e);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,9 +60,9 @@ class MaintenanceProvider extends ChangeNotifier {
|
|||||||
Future<void> updateMaintenance(String id, Map<String, dynamic> data) async {
|
Future<void> updateMaintenance(String id, Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
await _service.updateMaintenance(id, data);
|
await _service.updateMaintenance(id, data);
|
||||||
notifyListeners();
|
await loadMaintenances(); // Recharger après mise à jour
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error updating maintenance: $e');
|
DebugLog.error('[MaintenanceProvider] Error updating maintenance', e);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,9 +71,9 @@ class MaintenanceProvider extends ChangeNotifier {
|
|||||||
Future<void> deleteMaintenance(String id) async {
|
Future<void> deleteMaintenance(String id) async {
|
||||||
try {
|
try {
|
||||||
await _service.deleteMaintenance(id);
|
await _service.deleteMaintenance(id);
|
||||||
notifyListeners();
|
await loadMaintenances(); // Recharger après suppression
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error deleting maintenance: $e');
|
DebugLog.error('[MaintenanceProvider] Error deleting maintenance', e);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +83,7 @@ class MaintenanceProvider extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
return await _service.getMaintenanceById(id);
|
return await _service.getMaintenanceById(id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error getting maintenance: $e');
|
DebugLog.error('[MaintenanceProvider] Error getting maintenance', e);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,9 +96,9 @@ class MaintenanceProvider extends ChangeNotifier {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
await _service.completeMaintenance(id, performedBy: performedBy, cost: cost);
|
await _service.completeMaintenance(id, performedBy: performedBy, cost: cost);
|
||||||
notifyListeners();
|
await loadMaintenances(); // Recharger après complétion
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error completing maintenance: $e');
|
DebugLog.error('[MaintenanceProvider] Error completing maintenance', e);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,13 +108,13 @@ class MaintenanceProvider extends ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
await _service.checkUpcomingMaintenances();
|
await _service.checkUpcomingMaintenances();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error checking upcoming maintenances: $e');
|
DebugLog.error('[MaintenanceProvider] Error checking upcoming maintenances', e);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Récupérer les maintenances en retard
|
/// Récupérer les maintenances en retard
|
||||||
List<MaintenanceModel> get overdueMaintances {
|
List<MaintenanceModel> get overdueMaintenances {
|
||||||
return _maintenances.where((m) => m.isOverdue).toList();
|
return _maintenances.where((m) => m.isOverdue).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,5 +127,12 @@ class MaintenanceProvider extends ChangeNotifier {
|
|||||||
List<MaintenanceModel> get upcomingMaintenances {
|
List<MaintenanceModel> get upcomingMaintenances {
|
||||||
return _maintenances.where((m) => !m.isCompleted && !m.isOverdue).toList();
|
return _maintenances.where((m) => !m.isCompleted && !m.isOverdue).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Obtenir les maintenances pour un équipement spécifique (depuis le cache local)
|
||||||
|
List<MaintenanceModel> getForEquipment(String equipmentId) {
|
||||||
|
return _maintenances.where((m) =>
|
||||||
|
m.equipmentIds.contains(equipmentId)
|
||||||
|
).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,39 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:audioplayers/audioplayers.dart';
|
||||||
import 'package:em2rp/utils/debug_log.dart';
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
|
|
||||||
/// Service pour émettre des feedbacks sonores lors des interactions
|
/// Service pour émettre des feedbacks sonores lors des interactions
|
||||||
class AudioFeedbackService {
|
class AudioFeedbackService {
|
||||||
/// Jouer un son de succès (clic système)
|
static final AudioPlayer _player = AudioPlayer();
|
||||||
|
|
||||||
|
/// Jouer un son de succès
|
||||||
static Future<void> playSuccessBeep() async {
|
static Future<void> playSuccessBeep() async {
|
||||||
try {
|
try {
|
||||||
await SystemSound.play(SystemSoundType.click);
|
if (kIsWeb) {
|
||||||
|
// Sur Web, utiliser le chemin absolu
|
||||||
|
await _player.play(UrlSource('assets/sounds/ok.mp3'));
|
||||||
|
} else {
|
||||||
|
// Sur mobile/desktop, utiliser AssetSource
|
||||||
|
await _player.play(AssetSource('sounds/ok.mp3'));
|
||||||
|
}
|
||||||
|
await HapticFeedback.lightImpact();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLog.error('[AudioFeedbackService] Error playing success beep', e);
|
DebugLog.error('[AudioFeedbackService] Error playing success beep', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Jouer un son d'erreur (alerte système)
|
/// Jouer un son d'erreur
|
||||||
static Future<void> playErrorBeep() async {
|
static Future<void> playErrorBeep() async {
|
||||||
try {
|
try {
|
||||||
// Note: SystemSoundType.alert n'existe pas sur toutes les plateformes
|
if (kIsWeb) {
|
||||||
// On utilise click pour l'instant, peut être amélioré avec audioplayers
|
// Sur Web, utiliser le chemin absolu
|
||||||
await SystemSound.play(SystemSoundType.click);
|
await _player.play(UrlSource('assets/sounds/error.mp3'));
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
} else {
|
||||||
await SystemSound.play(SystemSoundType.click);
|
// Sur mobile/desktop, utiliser AssetSource
|
||||||
|
await _player.play(AssetSource('sounds/error.mp3'));
|
||||||
|
}
|
||||||
|
await HapticFeedback.heavyImpact();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLog.error('[AudioFeedbackService] Error playing error beep', e);
|
DebugLog.error('[AudioFeedbackService] Error playing error beep', e);
|
||||||
}
|
}
|
||||||
@@ -36,11 +50,15 @@ class AudioFeedbackService {
|
|||||||
|
|
||||||
/// Jouer un feedback complet (son + vibration)
|
/// Jouer un feedback complet (son + vibration)
|
||||||
static Future<void> playFullFeedback({bool isSuccess = true}) async {
|
static Future<void> playFullFeedback({bool isSuccess = true}) async {
|
||||||
await playHapticFeedback();
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
await playSuccessBeep();
|
await playSuccessBeep();
|
||||||
} else {
|
} else {
|
||||||
await playErrorBeep();
|
await playErrorBeep();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Nettoyer les ressources
|
||||||
|
static Future<void> dispose() async {
|
||||||
|
await _player.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,10 +299,20 @@ class PDFService {
|
|||||||
runSpacing: 0, // 0 espace entre les lignes
|
runSpacing: 0, // 0 espace entre les lignes
|
||||||
children: List.generate(pageItems.length, (i) {
|
children: List.generate(pageItems.length, (i) {
|
||||||
final item = pageItems[i];
|
final item = pageItems[i];
|
||||||
|
// Déterminer si c'est la première colonne (indices pairs)
|
||||||
|
final bool isFirstColumn = (i % 2) == 0;
|
||||||
|
// Décalage de 2mm pour la première colonne
|
||||||
|
final double leftPadding = isFirstColumn ? 8.0 : 6.0; // 6 + 2mm
|
||||||
|
|
||||||
return pw.Container(
|
return pw.Container(
|
||||||
width: labelWidth,
|
width: labelWidth,
|
||||||
height: labelHeight,
|
height: labelHeight,
|
||||||
padding: const pw.EdgeInsets.all(6),
|
padding: pw.EdgeInsets.only(
|
||||||
|
left: leftPadding,
|
||||||
|
right: 6,
|
||||||
|
top: 6,
|
||||||
|
bottom: 6,
|
||||||
|
),
|
||||||
// Suppression de la décoration (bordure)
|
// Suppression de la décoration (bordure)
|
||||||
child: pw.Row(
|
child: pw.Row(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||||
|
|||||||
104
em2rp/lib/services/text_to_speech_service.dart
Normal file
104
em2rp/lib/services/text_to_speech_service.dart
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:flutter_tts/flutter_tts.dart';
|
||||||
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
|
|
||||||
|
/// Service de synthèse vocale pour lire des textes à haute voix
|
||||||
|
class TextToSpeechService {
|
||||||
|
static final FlutterTts _tts = FlutterTts();
|
||||||
|
static bool _isInitialized = false;
|
||||||
|
|
||||||
|
/// Initialiser le service TTS
|
||||||
|
static Future<void> initialize() async {
|
||||||
|
if (_isInitialized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _tts.setLanguage('fr-FR');
|
||||||
|
await _tts.setSpeechRate(0.7); // Vitesse normale
|
||||||
|
await _tts.setVolume(1.0);
|
||||||
|
await _tts.setPitch(0.7); // Pitch plus bas pour une voix masculine
|
||||||
|
|
||||||
|
// Tenter de sélectionner une voix masculine si disponible
|
||||||
|
try {
|
||||||
|
final voices = await _tts.getVoices;
|
||||||
|
if (voices != null && voices is List) {
|
||||||
|
// Chercher une voix française masculine
|
||||||
|
final maleVoice = voices.firstWhere(
|
||||||
|
(voice) {
|
||||||
|
final voiceMap = voice as Map;
|
||||||
|
final name = voiceMap['name']?.toString().toLowerCase() ?? '';
|
||||||
|
final locale = voiceMap['locale']?.toString().toLowerCase() ?? '';
|
||||||
|
|
||||||
|
// Rechercher des voix françaises masculines
|
||||||
|
return locale.startsWith('fr') &&
|
||||||
|
(name.contains('male') || name.contains('homme') ||
|
||||||
|
name.contains('thomas') || name.contains('paul'));
|
||||||
|
},
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (maleVoice != null) {
|
||||||
|
final voiceMap = maleVoice as Map;
|
||||||
|
await _tts.setVoice({
|
||||||
|
'name': voiceMap['name'],
|
||||||
|
'locale': voiceMap['locale'],
|
||||||
|
});
|
||||||
|
DebugLog.info('[TextToSpeechService] Voix masculine sélectionnée: ${voiceMap['name']}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.info('[TextToSpeechService] Impossible de sélectionner une voix spécifique, utilisation de la voix par défaut');
|
||||||
|
}
|
||||||
|
|
||||||
|
_isInitialized = true;
|
||||||
|
DebugLog.info('[TextToSpeechService] Service initialisé avec voix masculine');
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[TextToSpeechService] Erreur lors de l\'initialisation', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lire un texte à haute voix
|
||||||
|
static Future<void> speak(String text) async {
|
||||||
|
if (!_isInitialized) {
|
||||||
|
await initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Arrêter toute lecture en cours
|
||||||
|
await _tts.stop();
|
||||||
|
|
||||||
|
// Lire le nouveau texte
|
||||||
|
await _tts.speak(text);
|
||||||
|
DebugLog.info('[TextToSpeechService] Lecture: $text');
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[TextToSpeechService] Erreur lors de la lecture', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Arrêter la lecture en cours
|
||||||
|
static Future<void> stop() async {
|
||||||
|
try {
|
||||||
|
await _tts.stop();
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[TextToSpeechService] Erreur lors de l\'arrêt', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifier si le service est en train de lire
|
||||||
|
static Future<bool> isSpeaking() async {
|
||||||
|
try {
|
||||||
|
// FlutterTts ne fournit pas directement cette info, on retourne false par défaut
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nettoyer les ressources
|
||||||
|
static Future<void> dispose() async {
|
||||||
|
try {
|
||||||
|
await _tts.stop();
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[TextToSpeechService] Erreur lors du nettoyage', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -35,10 +35,10 @@ enum AppPermission {
|
|||||||
|
|
||||||
// ============= MAINTENANCE =============
|
// ============= MAINTENANCE =============
|
||||||
/// Permet de voir les maintenances
|
/// Permet de voir les maintenances
|
||||||
viewMaintenance('view_maintenance'),
|
viewMaintenance('view_maintenances'),
|
||||||
|
|
||||||
/// Permet de créer, modifier et supprimer des maintenances
|
/// Permet de créer, modifier et supprimer des maintenances
|
||||||
manageMaintenance('manage_maintenance'),
|
manageMaintenance('manage_maintenances'),
|
||||||
|
|
||||||
// ============= UTILISATEURS =============
|
// ============= UTILISATEURS =============
|
||||||
/// Permet de voir la liste de tous les utilisateurs
|
/// Permet de voir la liste de tous les utilisateurs
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import 'package:em2rp/views/widgets/equipment/equipment_current_events_section.d
|
|||||||
import 'package:em2rp/views/widgets/equipment/equipment_price_section.dart';
|
import 'package:em2rp/views/widgets/equipment/equipment_price_section.dart';
|
||||||
import 'package:em2rp/views/widgets/equipment/equipment_maintenance_history_section.dart';
|
import 'package:em2rp/views/widgets/equipment/equipment_maintenance_history_section.dart';
|
||||||
import 'package:em2rp/views/widgets/equipment/equipment_dates_section.dart';
|
import 'package:em2rp/views/widgets/equipment/equipment_dates_section.dart';
|
||||||
|
import 'package:em2rp/views/maintenance_form_page.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
|
|
||||||
@@ -152,6 +153,7 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
|||||||
maintenances: _maintenances,
|
maintenances: _maintenances,
|
||||||
isLoading: _isLoadingMaintenances,
|
isLoading: _isLoadingMaintenances,
|
||||||
hasManagePermission: hasManagePermission,
|
hasManagePermission: hasManagePermission,
|
||||||
|
onAddMaintenance: hasManagePermission ? _planMaintenance : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -175,6 +177,7 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
|||||||
maintenances: _maintenances,
|
maintenances: _maintenances,
|
||||||
isLoading: _isLoadingMaintenances,
|
isLoading: _isLoadingMaintenances,
|
||||||
hasManagePermission: hasManagePermission,
|
hasManagePermission: hasManagePermission,
|
||||||
|
onAddMaintenance: hasManagePermission ? _planMaintenance : null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
EquipmentDatesSection(equipment: widget.equipment),
|
EquipmentDatesSection(equipment: widget.equipment),
|
||||||
@@ -378,6 +381,36 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Planifier une nouvelle maintenance pour cet équipment
|
||||||
|
Future<void> _planMaintenance() async {
|
||||||
|
final userProvider = Provider.of<LocalUserProvider>(context, listen: false);
|
||||||
|
final hasPermission = userProvider.hasPermission('manage_maintenances');
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Vous n\'avez pas la permission de gérer les maintenances'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await Navigator.push<bool>(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => MaintenanceFormPage(
|
||||||
|
initialEquipmentIds: [widget.equipment.id],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Recharger les maintenances si une maintenance a été créée
|
||||||
|
if (result == true && mounted) {
|
||||||
|
await _loadMaintenances();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _editEquipment() {
|
void _editEquipment() {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:em2rp/services/data_service.dart';
|
|||||||
import 'package:em2rp/services/api_service.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
import 'package:em2rp/services/qr_code_processing_service.dart';
|
import 'package:em2rp/services/qr_code_processing_service.dart';
|
||||||
import 'package:em2rp/services/audio_feedback_service.dart';
|
import 'package:em2rp/services/audio_feedback_service.dart';
|
||||||
|
import 'package:em2rp/services/text_to_speech_service.dart';
|
||||||
import 'package:em2rp/services/equipment_service.dart';
|
import 'package:em2rp/services/equipment_service.dart';
|
||||||
import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep;
|
import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep;
|
||||||
import 'package:em2rp/views/widgets/equipment/container_checklist_item.dart';
|
import 'package:em2rp/views/widgets/equipment/container_checklist_item.dart';
|
||||||
@@ -115,6 +116,9 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
duration: const Duration(milliseconds: 500),
|
duration: const Duration(milliseconds: 500),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Initialiser le service de synthèse vocale
|
||||||
|
TextToSpeechService.initialize();
|
||||||
|
|
||||||
// Vérification de sécurité et chargement après le premier frame
|
// Vérification de sécurité et chargement après le premier frame
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (_isCurrentStepCompleted()) {
|
if (_isCurrentStepCompleted()) {
|
||||||
@@ -152,6 +156,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
_animationController.dispose();
|
_animationController.dispose();
|
||||||
_manualCodeController.dispose();
|
_manualCodeController.dispose();
|
||||||
_manualCodeFocusNode.dispose();
|
_manualCodeFocusNode.dispose();
|
||||||
|
TextToSpeechService.stop();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,8 +656,15 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
// Feedback visuel
|
// Feedback visuel
|
||||||
_showSuccessFeedback(result.message ?? 'Code traité avec succès');
|
_showSuccessFeedback(result.message ?? 'Code traité avec succès');
|
||||||
|
|
||||||
|
// 🗣️ Annoncer le prochain item après un court délai
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
await _announceNextItem();
|
||||||
|
|
||||||
} else if (result.codeNotFoundInEvent) {
|
} else if (result.codeNotFoundInEvent) {
|
||||||
// 🔍 Code non trouvé dans l'événement → proposer de l'ajouter
|
// 🔍 Code non trouvé dans l'événement → proposer de l'ajouter
|
||||||
|
// 🔊 Son d'erreur
|
||||||
|
await AudioFeedbackService.playFullFeedback(isSuccess: false);
|
||||||
|
|
||||||
await _handleCodeNotFoundInEvent(code.trim());
|
await _handleCodeNotFoundInEvent(code.trim());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -1116,6 +1128,67 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trouve le prochain item non validé à scanner
|
||||||
|
String? _findNextItemToScan() {
|
||||||
|
// Parcourir les items dans l'ordre et trouver le premier non validé
|
||||||
|
|
||||||
|
// 1. Parcourir les containers et leurs équipements
|
||||||
|
for (final containerId in _currentEvent.assignedContainers) {
|
||||||
|
final container = _containerCache[containerId];
|
||||||
|
if (container == null) continue;
|
||||||
|
|
||||||
|
// Vérifier si le container a des équipements non validés
|
||||||
|
bool hasUnvalidatedChild = false;
|
||||||
|
for (final equipmentId in container.equipmentIds) {
|
||||||
|
|
||||||
|
if (_currentEvent.assignedEquipment.any((e) => e.equipmentId == equipmentId)) {
|
||||||
|
final isValidated = _localValidationState[equipmentId] ?? false;
|
||||||
|
if (!isValidated) {
|
||||||
|
hasUnvalidatedChild = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si le container a des items non validés, retourner le nom du container
|
||||||
|
if (hasUnvalidatedChild) {
|
||||||
|
return container.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parcourir les équipements standalone (pas dans un container)
|
||||||
|
final Set<String> equipmentIdsInContainers = {};
|
||||||
|
for (final containerId in _currentEvent.assignedContainers) {
|
||||||
|
final container = _containerCache[containerId];
|
||||||
|
if (container != null) {
|
||||||
|
equipmentIdsInContainers.addAll(container.equipmentIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final eventEquipment in _currentEvent.assignedEquipment) {
|
||||||
|
if (equipmentIdsInContainers.contains(eventEquipment.equipmentId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final isValidated = _localValidationState[eventEquipment.equipmentId] ?? false;
|
||||||
|
if (!isValidated) {
|
||||||
|
final equipment = _equipmentCache[eventEquipment.equipmentId];
|
||||||
|
return equipment?.name ?? 'Équipement ${eventEquipment.equipmentId}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // Tout est validé
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Annonce vocalement le prochain item à scanner
|
||||||
|
Future<void> _announceNextItem() async {
|
||||||
|
final nextItem = _findNextItemToScan();
|
||||||
|
if (nextItem != null) {
|
||||||
|
await TextToSpeechService.speak('Prochain item: $nextItem');
|
||||||
|
} else {
|
||||||
|
await TextToSpeechService.speak('Tous les items sont validés');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -1139,15 +1212,23 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
// Nom de l'événement et barre de progression sur la même ligne
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
_currentEvent.name,
|
_currentEvent.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Row(
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
@@ -1158,12 +1239,15 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'${_getValidatedCount()}/${_currentEvent.assignedEquipment.length}',
|
'${_getValidatedCount()}/${_currentEvent.assignedEquipment.length}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1193,14 +1277,17 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
|
|
||||||
// 🆕 Champ de saisie manuelle de code
|
// Champ de saisie manuelle avec bouton scanner
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 12),
|
||||||
TextField(
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
controller: _manualCodeController,
|
controller: _manualCodeController,
|
||||||
focusNode: _manualCodeFocusNode,
|
focusNode: _manualCodeFocusNode,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Saisie manuelle d\'un code',
|
labelText: 'Saisie manuelle d\'un code',
|
||||||
hintText: 'Entrez un ID d\'équipement ou container',
|
hintText: 'ID d\'équipement ou container',
|
||||||
prefixIcon: const Icon(Icons.keyboard, color: AppColors.bleuFonce),
|
prefixIcon: const Icon(Icons.keyboard, color: AppColors.bleuFonce),
|
||||||
suffixIcon: _manualCodeController.text.isNotEmpty
|
suffixIcon: _manualCodeController.text.isNotEmpty
|
||||||
? IconButton(
|
? IconButton(
|
||||||
@@ -1218,23 +1305,28 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: AppColors.bleuFonce, width: 2),
|
borderSide: const BorderSide(color: AppColors.bleuFonce, width: 2),
|
||||||
),
|
),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||||
),
|
),
|
||||||
onSubmitted: _handleManualCodeEntry,
|
onSubmitted: _handleManualCodeEntry,
|
||||||
onChanged: (value) => setState(() {}),
|
onChanged: (value) => setState(() {}),
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
),
|
),
|
||||||
|
|
||||||
// 🆕 Bouton Scanner QR Code
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
ElevatedButton.icon(
|
|
||||||
onPressed: _openQRScanner,
|
|
||||||
icon: const Icon(Icons.qr_code_scanner),
|
|
||||||
label: const Text('Scanner QR Code'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.blue[700],
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// IconButton pour scanner QR Code
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue[700],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: _openQRScanner,
|
||||||
|
icon: const Icon(Icons.qr_code_scanner, color: Colors.white),
|
||||||
|
iconSize: 28,
|
||||||
|
tooltip: 'Scanner QR Code',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -1254,10 +1346,45 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Expanded(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// Afficher 2 colonnes si la largeur le permet (> 600px)
|
||||||
|
final useColumns = constraints.maxWidth > 600;
|
||||||
|
final items = _buildChecklistItems();
|
||||||
|
|
||||||
|
if (useColumns && items.length > 1) {
|
||||||
|
// Diviser en 2 colonnes
|
||||||
|
final mid = (items.length / 2).ceil();
|
||||||
|
final leftItems = items.sublist(0, mid);
|
||||||
|
final rightItems = items.sublist(mid);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: _buildChecklistItems(),
|
children: leftItems,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const VerticalDivider(width: 1),
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: rightItems,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Une seule colonne
|
||||||
|
return ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
619
em2rp/lib/views/maintenance_form_page.dart
Normal file
619
em2rp/lib/views/maintenance_form_page.dart
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:em2rp/models/maintenance_model.dart';
|
||||||
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/providers/maintenance_provider.dart';
|
||||||
|
import 'package:em2rp/providers/equipment_provider.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
/// Page de formulaire pour créer ou modifier une maintenance
|
||||||
|
class MaintenanceFormPage extends StatefulWidget {
|
||||||
|
final MaintenanceModel? maintenance;
|
||||||
|
final List<String>? initialEquipmentIds;
|
||||||
|
|
||||||
|
const MaintenanceFormPage({
|
||||||
|
super.key,
|
||||||
|
this.maintenance,
|
||||||
|
this.initialEquipmentIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MaintenanceFormPage> createState() => _MaintenanceFormPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MaintenanceFormPageState extends State<MaintenanceFormPage> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _nameController = TextEditingController();
|
||||||
|
final _descriptionController = TextEditingController();
|
||||||
|
final _costController = TextEditingController();
|
||||||
|
final _notesController = TextEditingController();
|
||||||
|
|
||||||
|
MaintenanceType _selectedType = MaintenanceType.preventive;
|
||||||
|
DateTime _scheduledDate = DateTime.now();
|
||||||
|
final List<String> _selectedEquipmentIds = [];
|
||||||
|
bool _isLoading = false;
|
||||||
|
|
||||||
|
bool get _isEditing => widget.maintenance != null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
if (_isEditing) {
|
||||||
|
_nameController.text = widget.maintenance!.name;
|
||||||
|
_descriptionController.text = widget.maintenance!.description;
|
||||||
|
_selectedType = widget.maintenance!.type;
|
||||||
|
_scheduledDate = widget.maintenance!.scheduledDate;
|
||||||
|
_selectedEquipmentIds.addAll(widget.maintenance!.equipmentIds);
|
||||||
|
|
||||||
|
if (widget.maintenance!.cost != null) {
|
||||||
|
_costController.text = widget.maintenance!.cost!.toStringAsFixed(2);
|
||||||
|
}
|
||||||
|
if (widget.maintenance!.notes != null) {
|
||||||
|
_notesController.text = widget.maintenance!.notes!;
|
||||||
|
}
|
||||||
|
} else if (widget.initialEquipmentIds != null) {
|
||||||
|
// Pré-remplir avec les équipements fournis
|
||||||
|
_selectedEquipmentIds.addAll(widget.initialEquipmentIds!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charger les équipements
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.read<EquipmentProvider>().ensureLoaded();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_descriptionController.dispose();
|
||||||
|
_costController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(_isEditing ? 'Modifier la maintenance' : 'Nouvelle maintenance'),
|
||||||
|
backgroundColor: AppColors.bleuFonce,
|
||||||
|
),
|
||||||
|
body: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
// Nom
|
||||||
|
TextFormField(
|
||||||
|
controller: _nameController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Nom de la maintenance *',
|
||||||
|
hintText: 'Ex: Révision annuelle',
|
||||||
|
prefixIcon: Icon(Icons.title),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'Le nom est requis';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Type
|
||||||
|
DropdownButtonFormField<MaintenanceType>(
|
||||||
|
initialValue: _selectedType,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Type de maintenance *',
|
||||||
|
prefixIcon: Icon(Icons.category),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: MaintenanceType.values.map((type) {
|
||||||
|
final info = _getMaintenanceTypeInfo(type);
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: type,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(info.$2, size: 20, color: info.$3),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(info.$1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedType = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Date planifiée
|
||||||
|
InkWell(
|
||||||
|
onTap: _selectDate,
|
||||||
|
child: InputDecorator(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Date planifiée *',
|
||||||
|
prefixIcon: Icon(Icons.event),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(DateFormat('dd/MM/yyyy').format(_scheduledDate)),
|
||||||
|
const Icon(Icons.arrow_drop_down),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Équipements
|
||||||
|
_buildEquipmentSelector(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Description
|
||||||
|
TextFormField(
|
||||||
|
controller: _descriptionController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Description *',
|
||||||
|
hintText: 'Détails de l\'opération à effectuer',
|
||||||
|
prefixIcon: Icon(Icons.description),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
),
|
||||||
|
maxLines: 4,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.trim().isEmpty) {
|
||||||
|
return 'La description est requise';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Coût estimé
|
||||||
|
TextFormField(
|
||||||
|
controller: _costController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Coût estimé (€)',
|
||||||
|
hintText: 'Ex: 150.00',
|
||||||
|
prefixIcon: Icon(Icons.euro),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (value != null && value.isNotEmpty) {
|
||||||
|
if (double.tryParse(value) == null) {
|
||||||
|
return 'Coût invalide';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Notes
|
||||||
|
TextFormField(
|
||||||
|
controller: _notesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes',
|
||||||
|
hintText: 'Informations complémentaires',
|
||||||
|
prefixIcon: Icon(Icons.notes),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Bouton sauvegarder
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _isLoading ? null : _saveMaintenance,
|
||||||
|
icon: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.save),
|
||||||
|
label: Text(_isEditing ? 'Mettre à jour' : 'Créer la maintenance'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.bleuFonce,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEquipmentSelector() {
|
||||||
|
return Consumer<EquipmentProvider>(
|
||||||
|
builder: (context, equipmentProvider, _) {
|
||||||
|
// Filtrer uniquement les équipements
|
||||||
|
final availableEquipment = equipmentProvider.allEquipment
|
||||||
|
.cast<EquipmentModel>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
InputDecorator(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Équipements concernés *',
|
||||||
|
prefixIcon: const Icon(Icons.inventory),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
errorText: _selectedEquipmentIds.isEmpty ? 'Sélectionnez au moins un équipement' : null,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (_selectedEquipmentIds.isEmpty)
|
||||||
|
const Text(
|
||||||
|
'Aucun équipement sélectionné',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: _selectedEquipmentIds.map((id) {
|
||||||
|
final equipment = availableEquipment.firstWhere(
|
||||||
|
(eq) => eq.id == id,
|
||||||
|
orElse: () => EquipmentModel(
|
||||||
|
id: id,
|
||||||
|
name: 'Inconnu',
|
||||||
|
category: EquipmentCategory.other,
|
||||||
|
status: EquipmentStatus.available,
|
||||||
|
maintenanceIds: [],
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return Chip(
|
||||||
|
label: Text(equipment.name),
|
||||||
|
deleteIcon: const Icon(Icons.close, size: 18),
|
||||||
|
onDeleted: () {
|
||||||
|
setState(() {
|
||||||
|
_selectedEquipmentIds.remove(id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: () => _showEquipmentPicker(availableEquipment),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Ajouter des équipements'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showEquipmentPicker(List<EquipmentModel> availableEquipment) async {
|
||||||
|
final selectedIds = await showDialog<List<String>>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _EquipmentPickerDialog(
|
||||||
|
availableEquipment: availableEquipment,
|
||||||
|
initialSelectedIds: _selectedEquipmentIds,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedIds != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedEquipmentIds.clear();
|
||||||
|
_selectedEquipmentIds.addAll(selectedIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _selectDate() async {
|
||||||
|
final date = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: _scheduledDate,
|
||||||
|
firstDate: DateTime.now().subtract(const Duration(days: 365)),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365 * 5)),
|
||||||
|
locale: const Locale('fr', 'FR'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (date != null) {
|
||||||
|
setState(() {
|
||||||
|
_scheduledDate = date;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveMaintenance() async {
|
||||||
|
if (!_formKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_selectedEquipmentIds.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Veuillez sélectionner au moins un équipement'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final cost = _costController.text.trim().isNotEmpty
|
||||||
|
? double.tryParse(_costController.text.trim())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final notes = _notesController.text.trim().isNotEmpty
|
||||||
|
? _notesController.text.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (_isEditing) {
|
||||||
|
// Mise à jour
|
||||||
|
await context.read<MaintenanceProvider>().updateMaintenance(
|
||||||
|
widget.maintenance!.id,
|
||||||
|
{
|
||||||
|
'name': _nameController.text.trim(),
|
||||||
|
'description': _descriptionController.text.trim(),
|
||||||
|
'type': maintenanceTypeToString(_selectedType),
|
||||||
|
'scheduledDate': _scheduledDate,
|
||||||
|
'equipmentIds': _selectedEquipmentIds,
|
||||||
|
'cost': cost,
|
||||||
|
'notes': notes,
|
||||||
|
'updatedAt': DateTime.now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Maintenance mise à jour avec succès'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Création
|
||||||
|
final maintenance = MaintenanceModel(
|
||||||
|
id: const Uuid().v4(),
|
||||||
|
equipmentIds: _selectedEquipmentIds,
|
||||||
|
type: _selectedType,
|
||||||
|
scheduledDate: _scheduledDate,
|
||||||
|
name: _nameController.text.trim(),
|
||||||
|
description: _descriptionController.text.trim(),
|
||||||
|
cost: cost,
|
||||||
|
notes: notes,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
await context.read<MaintenanceProvider>().createMaintenance(maintenance);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Maintenance créée avec succès'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(String, IconData, Color) _getMaintenanceTypeInfo(MaintenanceType type) {
|
||||||
|
switch (type) {
|
||||||
|
case MaintenanceType.preventive:
|
||||||
|
return ('Préventive', Icons.schedule, Colors.blue);
|
||||||
|
case MaintenanceType.corrective:
|
||||||
|
return ('Corrective', Icons.build, Colors.orange);
|
||||||
|
case MaintenanceType.inspection:
|
||||||
|
return ('Inspection', Icons.search, Colors.purple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dialog pour sélectionner plusieurs équipements
|
||||||
|
class _EquipmentPickerDialog extends StatefulWidget {
|
||||||
|
final List<EquipmentModel> availableEquipment;
|
||||||
|
final List<String> initialSelectedIds;
|
||||||
|
|
||||||
|
const _EquipmentPickerDialog({
|
||||||
|
required this.availableEquipment,
|
||||||
|
required this.initialSelectedIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_EquipmentPickerDialog> createState() => _EquipmentPickerDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EquipmentPickerDialogState extends State<_EquipmentPickerDialog> {
|
||||||
|
late List<String> _selectedIds;
|
||||||
|
String _searchQuery = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_selectedIds = List.from(widget.initialSelectedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final filteredEquipment = widget.availableEquipment.where((eq) {
|
||||||
|
if (_searchQuery.isEmpty) return true;
|
||||||
|
return eq.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||||
|
eq.id.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Sélectionner des équipements'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Barre de recherche
|
||||||
|
TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Rechercher',
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_searchQuery = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Compteur
|
||||||
|
Text(
|
||||||
|
'${_selectedIds.length} équipement(s) sélectionné(s)',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Liste des équipements
|
||||||
|
Expanded(
|
||||||
|
child: filteredEquipment.isEmpty
|
||||||
|
? const Center(child: Text('Aucun équipement trouvé'))
|
||||||
|
: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: filteredEquipment.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final equipment = filteredEquipment[index];
|
||||||
|
final isSelected = _selectedIds.contains(equipment.id);
|
||||||
|
|
||||||
|
return CheckboxListTile(
|
||||||
|
value: isSelected,
|
||||||
|
onChanged: (selected) {
|
||||||
|
setState(() {
|
||||||
|
if (selected == true) {
|
||||||
|
_selectedIds.add(equipment.id);
|
||||||
|
} else {
|
||||||
|
_selectedIds.remove(equipment.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
title: Text(equipment.name),
|
||||||
|
subtitle: Text(
|
||||||
|
'${equipment.id} • ${_getCategoryLabel(equipment.category)}',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
secondary: Icon(
|
||||||
|
_getCategoryIcon(equipment.category),
|
||||||
|
color: AppColors.bleuFonce,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, _selectedIds),
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: AppColors.bleuFonce),
|
||||||
|
child: const Text('Valider'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getCategoryLabel(EquipmentCategory category) {
|
||||||
|
switch (category) {
|
||||||
|
case EquipmentCategory.sound:
|
||||||
|
return 'Son';
|
||||||
|
case EquipmentCategory.lighting:
|
||||||
|
return 'Lumière';
|
||||||
|
case EquipmentCategory.video:
|
||||||
|
return 'Vidéo';
|
||||||
|
case EquipmentCategory.structure:
|
||||||
|
return 'Structure';
|
||||||
|
case EquipmentCategory.effect:
|
||||||
|
return 'Effets';
|
||||||
|
case EquipmentCategory.cable:
|
||||||
|
return 'Câblage';
|
||||||
|
case EquipmentCategory.consumable:
|
||||||
|
return 'Consommable';
|
||||||
|
case EquipmentCategory.vehicle:
|
||||||
|
return 'Véhicule';
|
||||||
|
case EquipmentCategory.backline:
|
||||||
|
return 'Backline';
|
||||||
|
case EquipmentCategory.other:
|
||||||
|
return 'Autre';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getCategoryIcon(EquipmentCategory category) {
|
||||||
|
switch (category) {
|
||||||
|
case EquipmentCategory.sound:
|
||||||
|
return Icons.volume_up;
|
||||||
|
case EquipmentCategory.lighting:
|
||||||
|
return Icons.lightbulb;
|
||||||
|
case EquipmentCategory.video:
|
||||||
|
return Icons.videocam;
|
||||||
|
case EquipmentCategory.structure:
|
||||||
|
return Icons.construction;
|
||||||
|
case EquipmentCategory.effect:
|
||||||
|
return Icons.auto_awesome;
|
||||||
|
case EquipmentCategory.cable:
|
||||||
|
return Icons.cable;
|
||||||
|
case EquipmentCategory.consumable:
|
||||||
|
return Icons.inventory_2;
|
||||||
|
case EquipmentCategory.vehicle:
|
||||||
|
return Icons.local_shipping;
|
||||||
|
case EquipmentCategory.backline:
|
||||||
|
return Icons.queue_music;
|
||||||
|
case EquipmentCategory.other:
|
||||||
|
return Icons.category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
627
em2rp/lib/views/maintenance_management_page.dart
Normal file
627
em2rp/lib/views/maintenance_management_page.dart
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:em2rp/models/maintenance_model.dart';
|
||||||
|
import 'package:em2rp/providers/maintenance_provider.dart';
|
||||||
|
import 'package:em2rp/providers/equipment_provider.dart';
|
||||||
|
import 'package:em2rp/views/maintenance_form_page.dart';
|
||||||
|
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||||
|
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
||||||
|
import 'package:em2rp/utils/permission_gate.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
|
||||||
|
/// Page de gestion des maintenances
|
||||||
|
class MaintenanceManagementPage extends StatefulWidget {
|
||||||
|
const MaintenanceManagementPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MaintenanceManagementPage> createState() => _MaintenanceManagementPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MaintenanceManagementPageState extends State<MaintenanceManagementPage> {
|
||||||
|
String _filterType = 'all'; // all, upcoming, overdue, completed
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_loadMaintenances();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadMaintenances() async {
|
||||||
|
final maintenanceProvider = context.read<MaintenanceProvider>();
|
||||||
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
maintenanceProvider.loadMaintenances(),
|
||||||
|
equipmentProvider.ensureLoaded(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MaintenanceModel> _getFilteredMaintenances(List<MaintenanceModel> maintenances) {
|
||||||
|
switch (_filterType) {
|
||||||
|
case 'upcoming':
|
||||||
|
return maintenances.where((m) => !m.isCompleted && !m.isOverdue).toList();
|
||||||
|
case 'overdue':
|
||||||
|
return maintenances.where((m) => m.isOverdue).toList();
|
||||||
|
case 'completed':
|
||||||
|
return maintenances.where((m) => m.isCompleted).toList();
|
||||||
|
default:
|
||||||
|
return maintenances;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return PermissionGate(
|
||||||
|
requiredPermissions: const ['manage_maintenances'],
|
||||||
|
fallback: Scaffold(
|
||||||
|
appBar: const CustomAppBar(title: 'Accès refusé'),
|
||||||
|
drawer: const MainDrawer(currentPage: '/maintenance_management'),
|
||||||
|
body: const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(24.0),
|
||||||
|
child: Text(
|
||||||
|
'Vous n\'avez pas les permissions nécessaires pour accéder à la gestion des maintenances.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: const CustomAppBar(
|
||||||
|
title: 'Gestion des maintenances',
|
||||||
|
),
|
||||||
|
drawer: const MainDrawer(currentPage: '/maintenance_management'),
|
||||||
|
body: Consumer<MaintenanceProvider>(
|
||||||
|
builder: (context, maintenanceProvider, _) {
|
||||||
|
if (maintenanceProvider.isLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final filteredMaintenances = _getFilteredMaintenances(
|
||||||
|
maintenanceProvider.maintenances,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// Filtres
|
||||||
|
_buildFilterChips(),
|
||||||
|
|
||||||
|
// Statistiques
|
||||||
|
_buildStatsCards(maintenanceProvider),
|
||||||
|
|
||||||
|
// Liste des maintenances
|
||||||
|
Expanded(
|
||||||
|
child: filteredMaintenances.isEmpty
|
||||||
|
? _buildEmptyState()
|
||||||
|
: _buildMaintenanceList(filteredMaintenances),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
|
onPressed: () => _navigateToForm(null),
|
||||||
|
backgroundColor: AppColors.bleuFonce,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Nouvelle maintenance'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilterChips() {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildFilterChip('Toutes', 'all'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFilterChip('À venir', 'upcoming'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFilterChip('En retard', 'overdue'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFilterChip('Complétées', 'completed'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFilterChip(String label, String filterValue) {
|
||||||
|
final isSelected = _filterType == filterValue;
|
||||||
|
return FilterChip(
|
||||||
|
label: Text(label),
|
||||||
|
selected: isSelected,
|
||||||
|
onSelected: (selected) {
|
||||||
|
setState(() {
|
||||||
|
_filterType = filterValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
selectedColor: AppColors.bleuFonce.withValues(alpha: 0.2),
|
||||||
|
checkmarkColor: AppColors.bleuFonce,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatsCards(MaintenanceProvider provider) {
|
||||||
|
final upcoming = provider.maintenances.where((m) => !m.isCompleted && !m.isOverdue).length;
|
||||||
|
final overdue = provider.maintenances.where((m) => m.isOverdue).length;
|
||||||
|
final completed = provider.maintenances.where((m) => m.isCompleted).length;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
'À venir',
|
||||||
|
upcoming.toString(),
|
||||||
|
Icons.schedule,
|
||||||
|
Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
'En retard',
|
||||||
|
overdue.toString(),
|
||||||
|
Icons.warning,
|
||||||
|
Colors.orange,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: _buildStatCard(
|
||||||
|
'Complétées',
|
||||||
|
completed.toString(),
|
||||||
|
Icons.check_circle,
|
||||||
|
Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatCard(String label, String value, IconData icon, Color color) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: color, size: 28),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyState() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.build_outlined, size: 64, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Aucune maintenance',
|
||||||
|
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Créez votre première maintenance',
|
||||||
|
style: TextStyle(color: Colors.grey[500]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMaintenanceList(List<MaintenanceModel> maintenances) {
|
||||||
|
// Trier par date (les plus récentes/urgentes en premier)
|
||||||
|
final sortedMaintenances = List<MaintenanceModel>.from(maintenances)
|
||||||
|
..sort((a, b) {
|
||||||
|
if (a.isCompleted && !b.isCompleted) return 1;
|
||||||
|
if (!a.isCompleted && b.isCompleted) return -1;
|
||||||
|
return a.scheduledDate.compareTo(b.scheduledDate);
|
||||||
|
});
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: sortedMaintenances.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return _buildMaintenanceCard(sortedMaintenances[index]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMaintenanceCard(MaintenanceModel maintenance) {
|
||||||
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
|
final equipmentNames = maintenance.equipmentIds
|
||||||
|
.map((id) => equipmentProvider.allEquipment
|
||||||
|
.cast<dynamic>()
|
||||||
|
.firstWhere((e) => e.id == id, orElse: () => null)
|
||||||
|
?.name ?? 'Inconnu')
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final typeInfo = _getMaintenanceTypeInfo(maintenance.type);
|
||||||
|
final statusInfo = _getStatusInfo(maintenance);
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _navigateToForm(maintenance),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// En-tête
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: typeInfo.$3.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(typeInfo.$2, size: 16, color: typeInfo.$3),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
typeInfo.$1,
|
||||||
|
style: TextStyle(
|
||||||
|
color: typeInfo.$3,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusInfo.$2.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
statusInfo.$1,
|
||||||
|
style: TextStyle(
|
||||||
|
color: statusInfo.$2,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.more_vert),
|
||||||
|
onPressed: () => _showMaintenanceMenu(maintenance),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Nom
|
||||||
|
Text(
|
||||||
|
maintenance.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Description
|
||||||
|
if (maintenance.description.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
maintenance.description,
|
||||||
|
style: TextStyle(color: Colors.grey[700]),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Équipements
|
||||||
|
Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: equipmentNames.map((name) {
|
||||||
|
return Chip(
|
||||||
|
label: Text(name, style: const TextStyle(fontSize: 12)),
|
||||||
|
backgroundColor: Colors.grey[200],
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// Dates
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.event, size: 16, color: Colors.grey[600]),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
maintenance.isCompleted
|
||||||
|
? 'Complétée le ${DateFormat('dd/MM/yyyy').format(maintenance.completedDate!)}'
|
||||||
|
: 'Planifiée le ${DateFormat('dd/MM/yyyy').format(maintenance.scheduledDate)}',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Coût
|
||||||
|
if (maintenance.cost != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.euro, size: 16, color: Colors.grey[600]),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'${maintenance.cost!.toStringAsFixed(2)} €',
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
(String, IconData, Color) _getMaintenanceTypeInfo(MaintenanceType type) {
|
||||||
|
switch (type) {
|
||||||
|
case MaintenanceType.preventive:
|
||||||
|
return ('Préventive', Icons.schedule, Colors.blue);
|
||||||
|
case MaintenanceType.corrective:
|
||||||
|
return ('Corrective', Icons.build, Colors.orange);
|
||||||
|
case MaintenanceType.inspection:
|
||||||
|
return ('Inspection', Icons.search, Colors.purple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(String, Color) _getStatusInfo(MaintenanceModel maintenance) {
|
||||||
|
if (maintenance.isCompleted) {
|
||||||
|
return ('Complétée', Colors.green);
|
||||||
|
} else if (maintenance.isOverdue) {
|
||||||
|
return ('En retard', Colors.red);
|
||||||
|
} else {
|
||||||
|
return ('À venir', Colors.blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showMaintenanceMenu(MaintenanceModel maintenance) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (!maintenance.isCompleted)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.check_circle, color: Colors.green),
|
||||||
|
title: const Text('Marquer comme complétée'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_completeMaintenance(maintenance);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.edit, color: AppColors.bleuFonce),
|
||||||
|
title: const Text('Modifier'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_navigateToForm(maintenance);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.delete, color: Colors.red),
|
||||||
|
title: const Text('Supprimer'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
_deleteMaintenance(maintenance);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _completeMaintenance(MaintenanceModel maintenance) async {
|
||||||
|
final result = await showDialog<Map<String, dynamic>>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _CompleteMaintenanceDialog(maintenance: maintenance),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null && mounted) {
|
||||||
|
try {
|
||||||
|
await context.read<MaintenanceProvider>().completeMaintenance(
|
||||||
|
maintenance.id,
|
||||||
|
performedBy: result['performedBy'],
|
||||||
|
cost: result['cost'],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Maintenance marquée comme complétée'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_loadMaintenances();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteMaintenance(MaintenanceModel maintenance) async {
|
||||||
|
final confirm = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Supprimer la maintenance'),
|
||||||
|
content: Text('Êtes-vous sûr de vouloir supprimer "${maintenance.name}" ?'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
||||||
|
child: const Text('Supprimer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirm == true && mounted) {
|
||||||
|
try {
|
||||||
|
await context.read<MaintenanceProvider>().deleteMaintenance(maintenance.id);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Maintenance supprimée'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_loadMaintenances();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur: $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _navigateToForm(MaintenanceModel? maintenance) async {
|
||||||
|
final result = await Navigator.push<bool>(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => MaintenanceFormPage(maintenance: maintenance),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == true && mounted) {
|
||||||
|
_loadMaintenances();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dialog pour compléter une maintenance
|
||||||
|
class _CompleteMaintenanceDialog extends StatefulWidget {
|
||||||
|
final MaintenanceModel maintenance;
|
||||||
|
|
||||||
|
const _CompleteMaintenanceDialog({required this.maintenance});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_CompleteMaintenanceDialog> createState() => _CompleteMaintenanceDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CompleteMaintenanceDialogState extends State<_CompleteMaintenanceDialog> {
|
||||||
|
final _costController = TextEditingController();
|
||||||
|
final _notesController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_costController.dispose();
|
||||||
|
_notesController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Compléter la maintenance'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _costController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Coût (€)',
|
||||||
|
hintText: 'Ex: 150.00',
|
||||||
|
prefixIcon: Icon(Icons.euro),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _notesController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Notes (optionnel)',
|
||||||
|
hintText: 'Commentaires sur l\'intervention',
|
||||||
|
prefixIcon: Icon(Icons.notes),
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
final cost = double.tryParse(_costController.text);
|
||||||
|
Navigator.pop(context, {
|
||||||
|
'cost': cost,
|
||||||
|
'notes': _notesController.text.trim(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
|
||||||
|
child: const Text('Valider'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,12 +8,14 @@ class EquipmentMaintenanceHistorySection extends StatelessWidget {
|
|||||||
final List<MaintenanceModel> maintenances;
|
final List<MaintenanceModel> maintenances;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final bool hasManagePermission;
|
final bool hasManagePermission;
|
||||||
|
final VoidCallback? onAddMaintenance;
|
||||||
|
|
||||||
const EquipmentMaintenanceHistorySection({
|
const EquipmentMaintenanceHistorySection({
|
||||||
super.key,
|
super.key,
|
||||||
required this.maintenances,
|
required this.maintenances,
|
||||||
required this.isLoading,
|
required this.isLoading,
|
||||||
required this.hasManagePermission,
|
required this.hasManagePermission,
|
||||||
|
this.onAddMaintenance,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -37,20 +39,43 @@ class EquipmentMaintenanceHistorySection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (hasManagePermission && onAddMaintenance != null)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add_circle, color: AppColors.bleuFonce),
|
||||||
|
tooltip: 'Planifier une maintenance',
|
||||||
|
onPressed: onAddMaintenance,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
const Center(child: CircularProgressIndicator())
|
const Center(child: CircularProgressIndicator())
|
||||||
else if (maintenances.isEmpty)
|
else if (maintenances.isEmpty)
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Center(
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Aucune maintenance enregistrée',
|
'Aucune maintenance enregistrée',
|
||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (hasManagePermission && onAddMaintenance != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: onAddMaintenance,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Planifier une maintenance'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.bleuFonce,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
ListView.separated(
|
ListView.separated(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:em2rp/views/my_account_page.dart';
|
|||||||
import 'package:em2rp/views/user_management_page.dart';
|
import 'package:em2rp/views/user_management_page.dart';
|
||||||
import 'package:em2rp/views/data_management_page.dart';
|
import 'package:em2rp/views/data_management_page.dart';
|
||||||
import 'package:em2rp/views/equipment_management_page.dart';
|
import 'package:em2rp/views/equipment_management_page.dart';
|
||||||
|
import 'package:em2rp/views/maintenance_management_page.dart';
|
||||||
import 'package:em2rp/config/app_version.dart';
|
import 'package:em2rp/config/app_version.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
||||||
@@ -113,6 +114,24 @@ class MainDrawer extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
PermissionGate(
|
||||||
|
requiredPermissions: const ['manage_maintenances'],
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(Icons.build_circle),
|
||||||
|
title: const Text('Maintenances'),
|
||||||
|
selected: currentPage == '/maintenance_management',
|
||||||
|
selectedColor: AppColors.rouge,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
const MaintenanceManagementPage()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
ExpansionTileTheme(
|
ExpansionTileTheme(
|
||||||
data: const ExpansionTileThemeData(
|
data: const ExpansionTileThemeData(
|
||||||
iconColor: AppColors.noir,
|
iconColor: AppColors.noir,
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ dependencies:
|
|||||||
# Notifications
|
# Notifications
|
||||||
flutter_local_notifications: ^19.2.1
|
flutter_local_notifications: ^19.2.1
|
||||||
|
|
||||||
|
# Audio & TTS
|
||||||
|
audioplayers: ^6.1.0
|
||||||
|
flutter_tts: ^4.2.0
|
||||||
|
|
||||||
# Export/Import
|
# Export/Import
|
||||||
csv: ^6.0.0
|
csv: ^6.0.0
|
||||||
web: ^1.1.1
|
web: ^1.1.1
|
||||||
@@ -81,3 +85,4 @@ flutter:
|
|||||||
- assets/images/
|
- assets/images/
|
||||||
- assets/logos/
|
- assets/logos/
|
||||||
- assets/icons/
|
- assets/icons/
|
||||||
|
- assets/sounds/
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.1.5",
|
"version": "1.1.7",
|
||||||
"updateUrl": "https://app.em2events.fr",
|
"updateUrl": "https://app.em2events.fr",
|
||||||
"forceUpdate": true,
|
"forceUpdate": true,
|
||||||
"releaseNotes": "Cette version apporte des outils majeurs pour faciliter la gestion de votre parc et de vos événements :\r\n\r\n* **Scanner QR Code :** Retrouvez instantanément la fiche d'un équipement ou d'un conteneur en scannant son code directement depuis l'application. La génération des codes a également été rendue plus fluide.\r\n* **Centre de Notifications & Alertes :** Ne ratez plus rien ! Un nouveau système d'alertes (dans l'app et par email) vous prévient des maintenances, équipements manquants ou conflits. Vous pouvez configurer vos préférences d'envoi.",
|
"releaseNotes": "Ajout de la gestion des maintenance et synthèse vocale",
|
||||||
"timestamp": "2026-02-18T12:43:19.791Z"
|
"timestamp": "2026-02-24T12:39:57.675Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user