feat: Ajout de la gestion des maintenances et intégration de la synthèse vocale
This commit is contained in:
@@ -11,6 +11,7 @@ import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/services/qr_code_processing_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/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep;
|
||||
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),
|
||||
);
|
||||
|
||||
// Initialiser le service de synthèse vocale
|
||||
TextToSpeechService.initialize();
|
||||
|
||||
// Vérification de sécurité et chargement après le premier frame
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_isCurrentStepCompleted()) {
|
||||
@@ -152,6 +156,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
_animationController.dispose();
|
||||
_manualCodeController.dispose();
|
||||
_manualCodeFocusNode.dispose();
|
||||
TextToSpeechService.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -651,8 +656,15 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
// Feedback visuel
|
||||
_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) {
|
||||
// 🔍 Code non trouvé dans l'événement → proposer de l'ajouter
|
||||
// 🔊 Son d'erreur
|
||||
await AudioFeedbackService.playFullFeedback(isSuccess: false);
|
||||
|
||||
await _handleCodeNotFoundInEvent(code.trim());
|
||||
|
||||
} 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
|
||||
Widget build(BuildContext context) {
|
||||
@@ -1139,31 +1212,42 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
_currentEvent.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// Nom de l'événement et barre de progression sur la même ligne
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(
|
||||
value: _getProgress(),
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
allValidated ? Colors.green : AppColors.bleuFonce,
|
||||
child: Text(
|
||||
_currentEvent.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'${_getValidatedCount()}/${_currentEvent.assignedEquipment.length}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(
|
||||
value: _getProgress(),
|
||||
backgroundColor: Colors.grey.shade300,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
allValidated ? Colors.green : AppColors.bleuFonce,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${_getValidatedCount()}/${_currentEvent.assignedEquipment.length}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1193,48 +1277,56 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
|
||||
// 🆕 Champ de saisie manuelle de code
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _manualCodeController,
|
||||
focusNode: _manualCodeFocusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Saisie manuelle d\'un code',
|
||||
hintText: 'Entrez un ID d\'équipement ou container',
|
||||
prefixIcon: const Icon(Icons.keyboard, color: AppColors.bleuFonce),
|
||||
suffixIcon: _manualCodeController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_manualCodeController.clear();
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.bleuFonce, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
onSubmitted: _handleManualCodeEntry,
|
||||
onChanged: (value) => setState(() {}),
|
||||
textInputAction: TextInputAction.done,
|
||||
),
|
||||
|
||||
// 🆕 Bouton Scanner QR Code
|
||||
// Champ de saisie manuelle avec bouton scanner
|
||||
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),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _manualCodeController,
|
||||
focusNode: _manualCodeFocusNode,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Saisie manuelle d\'un code',
|
||||
hintText: 'ID d\'équipement ou container',
|
||||
prefixIcon: const Icon(Icons.keyboard, color: AppColors.bleuFonce),
|
||||
suffixIcon: _manualCodeController.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_manualCodeController.clear();
|
||||
setState(() {});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.bleuFonce, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
),
|
||||
onSubmitted: _handleManualCodeEntry,
|
||||
onChanged: (value) => setState(() {}),
|
||||
textInputAction: TextInputAction.done,
|
||||
),
|
||||
),
|
||||
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),
|
||||
@@ -1255,9 +1347,44 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: _buildChecklistItems(),
|
||||
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(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user