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`).
This commit is contained in:
226
em2rp/lib/views/widgets/event/preparation_success_dialog.dart
Normal file
226
em2rp/lib/views/widgets/event/preparation_success_dialog.dart
Normal file
@@ -0,0 +1,226 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
|
||||
/// Dialog de succès avec animation de camion
|
||||
class PreparationSuccessDialog extends StatefulWidget {
|
||||
final bool isReturnMode;
|
||||
|
||||
const PreparationSuccessDialog({
|
||||
super.key,
|
||||
this.isReturnMode = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PreparationSuccessDialog> createState() => _PreparationSuccessDialogState();
|
||||
}
|
||||
|
||||
class _PreparationSuccessDialogState extends State<PreparationSuccessDialog>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _truckAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
late Animation<double> _scaleAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 2500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// Animation du camion qui part (translation)
|
||||
_truckAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.5,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.3, 1.0, curve: Curves.easeInBack),
|
||||
));
|
||||
|
||||
// Animation de fade out
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.7, 1.0, curve: Curves.easeOut),
|
||||
));
|
||||
|
||||
// Animation de scale pour le check
|
||||
_scaleAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: const Interval(0.0, 0.3, curve: Curves.elasticOut),
|
||||
));
|
||||
|
||||
_controller.forward();
|
||||
|
||||
// Auto-fermer après l'animation
|
||||
Future.delayed(const Duration(milliseconds: 2500), () {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Opacity(
|
||||
opacity: _fadeAnimation.value,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Animation du check qui pop
|
||||
Transform.scale(
|
||||
scale: _scaleAnimation.value,
|
||||
child: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.check,
|
||||
color: Colors.white,
|
||||
size: 50,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Texte
|
||||
Text(
|
||||
widget.isReturnMode
|
||||
? 'Retour validé !'
|
||||
: 'Préparation validée !',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Animation du camion avec pneus qui crissent
|
||||
SizedBox(
|
||||
height: 100,
|
||||
child: Stack(
|
||||
children: [
|
||||
// Traces de pneus (lignes qui apparaissent)
|
||||
if (_truckAnimation.value > 0.1)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: MediaQuery.of(context).size.width * 0.3,
|
||||
bottom: 30,
|
||||
child: CustomPaint(
|
||||
painter: TireMarksPainter(
|
||||
progress: (_truckAnimation.value - 0.1).clamp(0.0, 1.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Camion qui part
|
||||
Positioned(
|
||||
left: MediaQuery.of(context).size.width * _truckAnimation.value - 100,
|
||||
bottom: 20,
|
||||
child: Transform.rotate(
|
||||
angle: _truckAnimation.value > 0.5 ? -0.1 : 0,
|
||||
child: const Icon(
|
||||
Icons.local_shipping,
|
||||
size: 60,
|
||||
color: AppColors.rouge,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
widget.isReturnMode
|
||||
? 'Le matériel est de retour au dépôt'
|
||||
: 'Le matériel est prêt pour l\'événement',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom painter pour dessiner les traces de pneus
|
||||
class TireMarksPainter extends CustomPainter {
|
||||
final double progress;
|
||||
|
||||
TireMarksPainter({required this.progress});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = Colors.grey.shade400
|
||||
..strokeWidth = 2
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final dashWidth = 10.0;
|
||||
final dashSpace = 5.0;
|
||||
final maxWidth = size.width * progress;
|
||||
|
||||
// Dessiner deux lignes de traces (pour les deux roues)
|
||||
for (var i = 0; i < 2; i++) {
|
||||
final y = i * 15.0;
|
||||
var startX = 0.0;
|
||||
|
||||
while (startX < maxWidth) {
|
||||
final endX = (startX + dashWidth).clamp(0.0, maxWidth);
|
||||
canvas.drawLine(
|
||||
Offset(startX, y),
|
||||
Offset(endX, y),
|
||||
paint,
|
||||
);
|
||||
startX += dashWidth + dashSpace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(TireMarksPainter oldDelegate) {
|
||||
return oldDelegate.progress != progress;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user