feat: Ajout du scan de QR Code pour retrouver équipements et conteneurs

Cette mise à jour introduit une fonctionnalité de scan de QR codes directement depuis l'application, permettant aux utilisateurs de retrouver et d'accéder rapidement à la page de détail d'un équipement ou d'un conteneur.

**Features :**
- **Scan de QR Code :**
    - Un nouveau bouton "Scanner QR Code" est ajouté sur les pages de gestion des équipements et des conteneurs.
    - L'appui sur ce bouton ouvre une nouvelle boîte de dialogue (`QRCodeScannerDialog`) utilisant la caméra de l'appareil pour scanner un QR code.
    - Le scanner affiche un overlay visuel clair avec un cadre de détection et fournit un retour visuel (icône de validation) lorsqu'un code est détecté avec succès.
- **Recherche et Redirection Intelligente :**
    - Une fois un QR code scanné, l'application recherche l'ID correspondant d'abord dans les équipements, puis dans les conteneurs.
    - Si une correspondance est trouvée, l'utilisateur est automatiquement redirigé vers la page de détail de l'élément correspondant (`EquipmentDetailPage` ou `ContainerDetailPage`).
    - Un message informe l'utilisateur si aucun élément ne correspond à l'ID scanné.

**Changements Techniques :**
- **Dépendance :** Ajout de la bibliothèque `mobile_scanner` pour gérer la fonctionnalité de scan.
- **Nouveau Widget :** Création du widget `QRCodeScannerDialog`, un dialogue réutilisable et stylisé pour le scan, incluant un overlay personnalisé (`_ScannerOverlayPainter`).
- **Intégration UI :**
    - Le `ManagementSearchBar` accepte désormais une liste de `additionalActions` pour permettre l'ajout de boutons personnalisés comme celui du scanner.
    - Ajout du bouton de scan sur les écrans `EquipmentManagementPage` et `ContainerManagementPage`, à la fois en version bureau (icône) et mobile (bouton plein).
- **Logique de Recherche :** Implémentation de la fonction `_scanQRCode` dans les deux pages de gestion pour orchestrer l'ouverture du scanner, la recherche dans les `EquipmentProvider` et `ContainerProvider`, et la navigation.
This commit is contained in:
ElPoyo
2026-01-16 00:35:10 +01:00
parent 67b85d323c
commit 06f394b728
5 changed files with 581 additions and 1 deletions

View File

@@ -0,0 +1,289 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:em2rp/utils/colors.dart';
/// Dialog pour scanner un QR code et récupérer l'ID
class QRCodeScannerDialog extends StatefulWidget {
const QRCodeScannerDialog({super.key});
@override
State<QRCodeScannerDialog> createState() => _QRCodeScannerDialogState();
}
class _QRCodeScannerDialogState extends State<QRCodeScannerDialog> {
MobileScannerController? _controller;
bool _isProcessing = false;
String? _scannedCode;
@override
void initState() {
super.initState();
_controller = MobileScannerController(
detectionSpeed: DetectionSpeed.normal,
facing: CameraFacing.back,
);
}
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
void _onDetect(BarcodeCapture capture) {
if (_isProcessing) return;
final List<Barcode> barcodes = capture.barcodes;
if (barcodes.isEmpty) return;
final barcode = barcodes.first;
final code = barcode.rawValue;
if (code != null && code.isNotEmpty) {
setState(() {
_isProcessing = true;
_scannedCode = code;
});
// Retourner le code après un court délai pour montrer le feedback visuel
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
Navigator.of(context).pop(code);
}
});
}
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.black,
insetPadding: const EdgeInsets.all(20),
child: Container(
constraints: const BoxConstraints(maxWidth: 600, maxHeight: 700),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// En-tête
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.rouge,
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Row(
children: [
const Icon(Icons.qr_code_scanner, color: Colors.white, size: 28),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Scanner un QR Code',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
// Zone de scan
Expanded(
child: Stack(
children: [
// Scanner
if (_controller != null)
ClipRRect(
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)),
child: MobileScanner(
controller: _controller,
onDetect: _onDetect,
),
),
// Overlay avec cadre de scan
Positioned.fill(
child: CustomPaint(
painter: _ScannerOverlayPainter(),
),
),
// Feedback visuel quand un code est détecté
if (_isProcessing && _scannedCode != null)
Positioned.fill(
child: Container(
color: Colors.black.withValues(alpha: 0.7),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.check_circle,
color: Colors.green,
size: 64,
),
const SizedBox(height: 16),
const Text(
'QR Code détecté !',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_scannedCode!,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontFamily: 'monospace',
),
),
),
],
),
),
),
),
],
),
),
// Instructions
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[900],
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)),
),
child: Row(
children: [
Icon(Icons.info_outline, color: Colors.grey[400], size: 20),
const SizedBox(width: 12),
Expanded(
child: Text(
'Positionnez le QR code dans le cadre',
style: TextStyle(
color: Colors.grey[300],
fontSize: 14,
),
),
),
],
),
),
],
),
),
);
}
}
/// Painter pour dessiner l'overlay du scanner avec un cadre
class _ScannerOverlayPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
const double scanAreaSize = 250.0;
final double left = (size.width - scanAreaSize) / 2;
final double top = (size.height - scanAreaSize) / 2;
// Fond semi-transparent
final backgroundPath = Path()
..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
final holePath = Path()
..addRRect(RRect.fromRectAndRadius(
Rect.fromLTWH(left, top, scanAreaSize, scanAreaSize),
const Radius.circular(16),
));
final backgroundPaint = Paint()
..color = Colors.black.withValues(alpha: 0.5)
..style = PaintingStyle.fill;
canvas.drawPath(
Path.combine(PathOperation.difference, backgroundPath, holePath),
backgroundPaint,
);
// Cadre de scan (coins)
final cornerPaint = Paint()
..color = AppColors.rouge
..style = PaintingStyle.stroke
..strokeWidth = 4
..strokeCap = StrokeCap.round;
const double cornerLength = 30.0;
// Coin haut-gauche
canvas.drawLine(
Offset(left, top + cornerLength),
Offset(left, top),
cornerPaint,
);
canvas.drawLine(
Offset(left, top),
Offset(left + cornerLength, top),
cornerPaint,
);
// Coin haut-droit
canvas.drawLine(
Offset(left + scanAreaSize - cornerLength, top),
Offset(left + scanAreaSize, top),
cornerPaint,
);
canvas.drawLine(
Offset(left + scanAreaSize, top),
Offset(left + scanAreaSize, top + cornerLength),
cornerPaint,
);
// Coin bas-gauche
canvas.drawLine(
Offset(left, top + scanAreaSize - cornerLength),
Offset(left, top + scanAreaSize),
cornerPaint,
);
canvas.drawLine(
Offset(left, top + scanAreaSize),
Offset(left + cornerLength, top + scanAreaSize),
cornerPaint,
);
// Coin bas-droit
canvas.drawLine(
Offset(left + scanAreaSize - cornerLength, top + scanAreaSize),
Offset(left + scanAreaSize, top + scanAreaSize),
cornerPaint,
);
canvas.drawLine(
Offset(left + scanAreaSize, top + scanAreaSize - cornerLength),
Offset(left + scanAreaSize, top + scanAreaSize),
cornerPaint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@@ -8,6 +8,7 @@ class ManagementSearchBar extends StatelessWidget {
final ValueChanged<String> onChanged;
final VoidCallback? onSelectionModeToggle;
final bool showSelectionModeButton;
final List<Widget>? additionalActions;
const ManagementSearchBar({
super.key,
@@ -16,6 +17,7 @@ class ManagementSearchBar extends StatelessWidget {
required this.onChanged,
this.onSelectionModeToggle,
this.showSelectionModeButton = true,
this.additionalActions,
});
@override
@@ -53,6 +55,7 @@ class ManagementSearchBar extends StatelessWidget {
onChanged: onChanged,
),
),
if (additionalActions != null) ...additionalActions!,
if (showSelectionModeButton && onSelectionModeToggle != null) ...[
const SizedBox(width: 12),
IconButton(