Files
EM2_ERP/em2rp/lib/views/event_preparation_page.dart
ElPoyo 08f046c89c feat: Refactor event equipment management with advanced selection and conflict detection
This commit introduces a complete overhaul of how equipment is assigned to events, focusing on an enhanced user experience, advanced selection capabilities, and robust conflict detection.

**Key Features & Enhancements:**

-   **Advanced Equipment Selection UI (`EquipmentSelectionDialog`):**
    -   New full-screen dialog to select equipment and containers ("boîtes") for an event.
    -   Hierarchical view showing containers and a flat list of all individual equipment.
    -   Real-time search and filtering by equipment category.
    -   Side panel summarizing the current selection and providing recommendations for containers based on selected equipment.
    -   Supports quantity selection for consumables and cables.

-   **Conflict Detection & Management (`EventAvailabilityService`):**
    -   A new service (`EventAvailabilityService`) checks for equipment availability against other events based on the selected date range.
    -   The selection dialog visually highlights equipment and containers with scheduling conflicts (e.g., already used, partially unavailable).
    -   A dedicated conflict resolution dialog (`EquipmentConflictDialog`) appears if conflicting items are selected, allowing the user to either remove them or force the assignment.

-   **Integrated Event Form (`EventAssignedEquipmentSection`):**
    -   The event creation/editing form now includes a new section for managing assigned equipment.
    -   It clearly displays assigned containers and standalone equipment, showing the composition of each container.
    -   Integrates the new selection dialog, ensuring all assignments are checked for conflicts before being saved.

-   **Event Preparation & Return Workflow (`EventPreparationPage`):**
    -   New page (`EventPreparationPage`) for managing the check-out (preparation) and check-in (return) of equipment for an event.
    -   Provides a checklist of all assigned equipment.
    -   Users can validate each item, with options to "validate all" or finalize with missing items.
    -   Includes a dialog (`MissingEquipmentDialog`) to handle discrepancies.
    -   Supports tracking returned quantities for consumables.

**Data Model and Other Changes:**

-   The `EventModel` now includes `assignedContainers` to explicitly link containers to an event.
-   `EquipmentAssociatedEventsSection` on the equipment detail page is now functional, displaying current, upcoming, and past events for that item.
-   Added deployment and versioning scripts (`scripts/deploy.js`, `scripts/increment_version.js`, `scripts/toggle_env.js`) to automate the release process.
-   Introduced an application version display in the main drawer (`AppVersion`).
2025-11-30 20:33:03 +01:00

386 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/models/equipment_model.dart';
import 'package:em2rp/providers/event_provider.dart';
import 'package:em2rp/providers/equipment_provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/services/event_preparation_service.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
import 'package:em2rp/views/widgets/event/equipment_checklist_item.dart';
import 'package:em2rp/views/widgets/event/missing_equipment_dialog.dart';
import 'package:em2rp/views/widgets/event/preparation_success_dialog.dart';
class EventPreparationPage extends StatefulWidget {
final String eventId;
const EventPreparationPage({
super.key,
required this.eventId,
});
@override
State<EventPreparationPage> createState() => _EventPreparationPageState();
}
class _EventPreparationPageState extends State<EventPreparationPage> {
final EventPreparationService _preparationService = EventPreparationService();
EventModel? _event;
Map<String, EquipmentModel> _equipmentMap = {};
Map<String, int> _returnedQuantities = {}; // Pour les quantités retournées (consommables)
bool _isLoading = true;
bool _isSaving = false;
// Mode déterminé automatiquement
bool get _isReturnMode {
if (_event == null) return false;
// Mode retour si préparation complétée et retour pas encore complété
return _event!.preparationStatus == PreparationStatus.completed ||
_event!.preparationStatus == PreparationStatus.completedWithMissing;
}
String get _pageTitle => _isReturnMode ? 'Retour matériel' : 'Préparation matériel';
@override
void initState() {
super.initState();
_loadEventAndEquipment();
}
Future<void> _loadEventAndEquipment() async {
try {
// Charger l'événement
final eventProvider = context.read<EventProvider>();
final event = await eventProvider.getEvent(widget.eventId);
if (event == null) {
throw Exception('Événement non trouvé');
}
// Charger tous les équipements assignés
final equipmentProvider = context.read<EquipmentProvider>();
final Map<String, EquipmentModel> equipmentMap = {};
for (var assignedEq in event.assignedEquipment) {
final equipment = await equipmentProvider.getEquipmentById(assignedEq.equipmentId);
if (equipment != null) {
equipmentMap[assignedEq.equipmentId] = equipment;
// Initialiser les quantités retournées pour les consommables
if (_isReturnMode &&
(equipment.category == EquipmentCategory.consumable ||
equipment.category == EquipmentCategory.cable)) {
_returnedQuantities[assignedEq.equipmentId] = assignedEq.returnedQuantity ?? assignedEq.quantity;
}
}
}
setState(() {
_event = event;
_equipmentMap = equipmentMap;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _toggleEquipmentValidation(String equipmentId, bool isValidated) async {
try {
if (_isReturnMode) {
if (isValidated) {
final returnedQty = _returnedQuantities[equipmentId];
await _preparationService.validateEquipmentReturn(
widget.eventId,
equipmentId,
returnedQuantity: returnedQty,
);
}
} else {
await _preparationService.validateEquipmentPreparation(widget.eventId, equipmentId);
}
// Recharger l'événement
await _loadEventAndEquipment();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
);
}
}
}
Future<void> _validateAllQuickly() async {
setState(() => _isSaving = true);
try {
if (_isReturnMode) {
await _preparationService.validateAllReturn(widget.eventId, _returnedQuantities);
} else {
await _preparationService.validateAllPreparation(widget.eventId);
}
// Afficher le dialog de succès avec animation
if (mounted) {
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) => PreparationSuccessDialog(isReturnMode: _isReturnMode),
);
// Retour à la page précédente
Navigator.of(context).pop();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
);
}
} finally {
setState(() => _isSaving = false);
}
}
Future<void> _validatePreparation() async {
if (_event == null) return;
// Vérifier quels équipements ne sont pas validés
final missingEquipment = <EquipmentModel>[];
final missingIds = <String>[];
for (var assignedEq in _event!.assignedEquipment) {
final isValidated = _isReturnMode ? assignedEq.isReturned : assignedEq.isPrepared;
if (!isValidated) {
final equipment = _equipmentMap[assignedEq.equipmentId];
if (equipment != null) {
missingEquipment.add(equipment);
missingIds.add(assignedEq.equipmentId);
}
}
}
// Si tout est validé, on finalise directement
if (missingEquipment.isEmpty) {
await _validateAllQuickly();
return;
}
// Sinon, afficher le dialog des manquants
if (mounted) {
final result = await showDialog<String>(
context: context,
builder: (context) => MissingEquipmentDialog(
missingEquipments: missingEquipment,
eventId: widget.eventId,
isReturnMode: _isReturnMode,
),
);
if (result == 'confirm_with_missing') {
setState(() => _isSaving = true);
try {
if (_isReturnMode) {
await _preparationService.completeReturnWithMissing(widget.eventId, missingIds);
} else {
await _preparationService.completePreparationWithMissing(widget.eventId, missingIds);
}
if (mounted) {
await showDialog(
context: context,
barrierDismissible: false,
builder: (context) => PreparationSuccessDialog(isReturnMode: _isReturnMode),
);
Navigator.of(context).pop();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e'), backgroundColor: Colors.red),
);
}
} finally {
setState(() => _isSaving = false);
}
} else if (result == 'validate_missing') {
// Valider tous les manquants
setState(() => _isSaving = true);
try {
for (var equipmentId in missingIds) {
await _toggleEquipmentValidation(equipmentId, true);
}
await _validateAllQuickly();
} finally {
setState(() => _isSaving = false);
}
}
}
}
@override
Widget build(BuildContext context) {
final userProvider = context.watch<LocalUserProvider>();
final userId = userProvider.uid;
final hasManagePermission = userProvider.hasPermission('manage_events');
// Vérifier si l'utilisateur fait partie de l'équipe
final isInWorkforce = _event?.workforce.any((ref) => ref.id == userId) ?? false;
final hasPermission = hasManagePermission || isInWorkforce;
if (!hasPermission) {
return Scaffold(
appBar: const CustomAppBar(title: 'Accès refusé'),
body: const Center(
child: Text('Vous n\'avez pas les permissions pour accéder à cette page.'),
),
);
}
return Scaffold(
appBar: CustomAppBar(title: _pageTitle),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _event == null
? const Center(child: Text('Événement introuvable'))
: Column(
children: [
// En-tête avec info de l'événement
_buildEventHeader(),
// Bouton "Tout confirmer"
_buildQuickValidateButton(),
// Liste des équipements
Expanded(child: _buildEquipmentList()),
// Bouton de validation final
_buildValidateButton(),
],
),
);
}
Widget _buildEventHeader() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.rouge.withOpacity(0.1),
border: Border(
bottom: BorderSide(color: Colors.grey.shade300),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_event!.name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'${_event!.assignedEquipment.length} équipement(s) assigné(s)',
style: TextStyle(color: Colors.grey.shade700),
),
],
),
);
}
Widget _buildQuickValidateButton() {
return Container(
padding: const EdgeInsets.all(16),
child: ElevatedButton.icon(
onPressed: _isSaving ? null : _validateAllQuickly,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
minimumSize: const Size(double.infinity, 50),
),
icon: const Icon(Icons.check_circle, color: Colors.white),
label: Text(
_isReturnMode ? 'Tout confirmer comme retourné' : 'Tout confirmer comme préparé',
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
);
}
Widget _buildEquipmentList() {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _event!.assignedEquipment.length,
itemBuilder: (context, index) {
final assignedEq = _event!.assignedEquipment[index];
final equipment = _equipmentMap[assignedEq.equipmentId];
if (equipment == null) {
return const SizedBox.shrink();
}
final isValidated = _isReturnMode ? assignedEq.isReturned : assignedEq.isPrepared;
return EquipmentChecklistItem(
equipment: equipment,
isValidated: isValidated,
onValidate: (value) => _toggleEquipmentValidation(assignedEq.equipmentId, value),
isReturnMode: _isReturnMode,
quantity: assignedEq.quantity,
returnedQuantity: _returnedQuantities[assignedEq.equipmentId],
onReturnedQuantityChanged: _isReturnMode
? (value) {
setState(() {
_returnedQuantities[assignedEq.equipmentId] = value;
});
}
: null,
);
},
);
}
Widget _buildValidateButton() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.3),
spreadRadius: 1,
blurRadius: 5,
offset: const Offset(0, -2),
),
],
),
child: ElevatedButton(
onPressed: _isSaving ? null : _validatePreparation,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.rouge,
minimumSize: const Size(double.infinity, 50),
),
child: _isSaving
? const CircularProgressIndicator(color: Colors.white)
: Text(
_isReturnMode ? 'Finaliser le retour' : 'Finaliser la préparation',
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
);
}
}