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:
289
em2rp/lib/views/widgets/common/qr_code_scanner_dialog.dart
Normal file
289
em2rp/lib/views/widgets/common/qr_code_scanner_dialog.dart
Normal 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user