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:
@@ -5,14 +5,19 @@ import 'package:em2rp/utils/permission_gate.dart';
|
|||||||
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
||||||
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||||
import 'package:em2rp/providers/container_provider.dart';
|
import 'package:em2rp/providers/container_provider.dart';
|
||||||
|
import 'package:em2rp/providers/equipment_provider.dart';
|
||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
import 'package:em2rp/models/container_model.dart';
|
import 'package:em2rp/models/container_model.dart';
|
||||||
import 'package:em2rp/models/equipment_model.dart';
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/views/equipment_detail_page.dart';
|
||||||
|
import 'package:em2rp/views/widgets/common/qr_code_dialog.dart';
|
||||||
|
import 'package:em2rp/views/widgets/common/qr_code_scanner_dialog.dart';
|
||||||
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
|
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
|
||||||
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
||||||
import 'package:em2rp/views/widgets/management/management_search_bar.dart';
|
import 'package:em2rp/views/widgets/management/management_search_bar.dart';
|
||||||
import 'package:em2rp/views/widgets/management/management_card.dart';
|
import 'package:em2rp/views/widgets/management/management_card.dart';
|
||||||
import 'package:em2rp/views/widgets/management/management_list.dart';
|
import 'package:em2rp/views/widgets/management/management_list.dart';
|
||||||
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
|
|
||||||
class ContainerManagementPage extends StatefulWidget {
|
class ContainerManagementPage extends StatefulWidget {
|
||||||
const ContainerManagementPage({super.key});
|
const ContainerManagementPage({super.key});
|
||||||
@@ -177,6 +182,14 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
|
|||||||
},
|
},
|
||||||
onSelectionModeToggle: isSelectionMode ? null : toggleSelectionMode,
|
onSelectionModeToggle: isSelectionMode ? null : toggleSelectionMode,
|
||||||
showSelectionModeButton: !isSelectionMode,
|
showSelectionModeButton: !isSelectionMode,
|
||||||
|
additionalActions: [
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner, color: AppColors.rouge),
|
||||||
|
tooltip: 'Scanner un QR Code',
|
||||||
|
onPressed: _scanQRCode,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,7 +430,7 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
|
|||||||
_editContainer(container);
|
_editContainer(container);
|
||||||
break;
|
break;
|
||||||
case 'qr':
|
case 'qr':
|
||||||
// Non utilisé - les QR codes multiples sont générés via _generateQRCodesForSelected
|
_showQRCode(container);
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
_deleteContainer(container);
|
_deleteContainer(container);
|
||||||
@@ -425,6 +438,14 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Afficher le QR code d'un conteneur
|
||||||
|
void _showQRCode(ContainerModel container) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => QRCodeDialog.forContainer(container),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _navigateToForm(BuildContext context) async {
|
void _navigateToForm(BuildContext context) async {
|
||||||
final result = await Navigator.pushNamed(context, '/container_form');
|
final result = await Navigator.pushNamed(context, '/container_form');
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
@@ -583,5 +604,120 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scanner un QR Code et ouvrir la vue de détail correspondante
|
||||||
|
Future<void> _scanQRCode() async {
|
||||||
|
try {
|
||||||
|
// Ouvrir le scanner
|
||||||
|
final scannedCode = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const QRCodeScannerDialog(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (scannedCode == null || scannedCode.isEmpty) {
|
||||||
|
return; // L'utilisateur a annulé
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// Afficher un indicateur de chargement
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => const Center(
|
||||||
|
child: CircularProgressIndicator(color: AppColors.rouge),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rechercher d'abord dans les conteneurs
|
||||||
|
final containerProvider = context.read<ContainerProvider>();
|
||||||
|
if (containerProvider.containers.isEmpty) {
|
||||||
|
await containerProvider.loadContainers();
|
||||||
|
}
|
||||||
|
|
||||||
|
final container = containerProvider.containers.firstWhere(
|
||||||
|
(c) => c.id == scannedCode,
|
||||||
|
orElse: () => ContainerModel(
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
type: ContainerType.flightCase,
|
||||||
|
status: EquipmentStatus.available,
|
||||||
|
equipmentIds: [],
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop(); // Fermer l'indicateur
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container.id.isNotEmpty) {
|
||||||
|
// Conteneur trouvé
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pushNamed(
|
||||||
|
context,
|
||||||
|
'/container_detail',
|
||||||
|
arguments: container,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si pas trouvé dans les conteneurs, chercher dans les équipements
|
||||||
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
|
await equipmentProvider.ensureLoaded();
|
||||||
|
|
||||||
|
final equipment = equipmentProvider.allEquipment.firstWhere(
|
||||||
|
(eq) => eq.id == scannedCode,
|
||||||
|
orElse: () => EquipmentModel(
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
category: EquipmentCategory.other,
|
||||||
|
status: EquipmentStatus.available,
|
||||||
|
parentBoxIds: [],
|
||||||
|
maintenanceIds: [],
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (equipment.id.isNotEmpty) {
|
||||||
|
// Équipement trouvé
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => EquipmentDetailPage(equipment: equipment),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rien trouvé
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Aucun conteneur ou équipement trouvé avec l\'ID : $scannedCode'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[ContainerManagementPage] Error scanning QR code', e);
|
||||||
|
if (mounted) {
|
||||||
|
// Fermer l'indicateur si ouvert
|
||||||
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur lors du scan : ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,14 @@ import 'package:em2rp/utils/permission_gate.dart';
|
|||||||
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
import 'package:em2rp/views/widgets/nav/main_drawer.dart';
|
||||||
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||||
import 'package:em2rp/providers/equipment_provider.dart';
|
import 'package:em2rp/providers/equipment_provider.dart';
|
||||||
|
import 'package:em2rp/providers/container_provider.dart';
|
||||||
import 'package:em2rp/models/equipment_model.dart';
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/models/container_model.dart';
|
||||||
import 'package:em2rp/views/equipment_form_page.dart';
|
import 'package:em2rp/views/equipment_form_page.dart';
|
||||||
import 'package:em2rp/views/equipment_detail_page.dart';
|
import 'package:em2rp/views/equipment_detail_page.dart';
|
||||||
|
import 'package:em2rp/views/container_detail_page.dart';
|
||||||
import 'package:em2rp/views/widgets/common/qr_code_dialog.dart';
|
import 'package:em2rp/views/widgets/common/qr_code_dialog.dart';
|
||||||
|
import 'package:em2rp/views/widgets/common/qr_code_scanner_dialog.dart';
|
||||||
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
|
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
|
||||||
import 'package:em2rp/views/widgets/equipment/equipment_status_badge.dart';
|
import 'package:em2rp/views/widgets/equipment/equipment_status_badge.dart';
|
||||||
import 'package:em2rp/utils/debug_log.dart';
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
@@ -156,6 +160,17 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
// Bouton Scanner QR
|
||||||
|
IconButton.filled(
|
||||||
|
onPressed: _scanQRCode,
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
tooltip: 'Scanner un QR Code',
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey[700],
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
// Bouton Gérer les boîtes
|
// Bouton Gérer les boîtes
|
||||||
IconButton.filled(
|
IconButton.filled(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -256,6 +271,26 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Bouton Scanner QR
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 16.0),
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: _scanQRCode,
|
||||||
|
icon: const Icon(Icons.qr_code_scanner, color: Colors.white),
|
||||||
|
label: const Text(
|
||||||
|
'Scanner QR Code',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey[700],
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 16,
|
||||||
|
),
|
||||||
|
minimumSize: const Size(double.infinity, 50),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
// En-tête filtres
|
// En-tête filtres
|
||||||
Padding(
|
Padding(
|
||||||
@@ -992,4 +1027,120 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scanner un QR Code et ouvrir la vue de détail correspondante
|
||||||
|
Future<void> _scanQRCode() async {
|
||||||
|
try {
|
||||||
|
// Ouvrir le scanner
|
||||||
|
final scannedCode = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const QRCodeScannerDialog(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (scannedCode == null || scannedCode.isEmpty) {
|
||||||
|
return; // L'utilisateur a annulé
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// Afficher un indicateur de chargement
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => const Center(
|
||||||
|
child: CircularProgressIndicator(color: AppColors.rouge),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rechercher d'abord dans les équipements
|
||||||
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
|
await equipmentProvider.ensureLoaded();
|
||||||
|
|
||||||
|
final equipment = equipmentProvider.allEquipment.firstWhere(
|
||||||
|
(eq) => eq.id == scannedCode,
|
||||||
|
orElse: () => EquipmentModel(
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
category: EquipmentCategory.other,
|
||||||
|
status: EquipmentStatus.available,
|
||||||
|
parentBoxIds: [],
|
||||||
|
maintenanceIds: [],
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop(); // Fermer l'indicateur
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equipment.id.isNotEmpty) {
|
||||||
|
// Équipement trouvé
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => EquipmentDetailPage(equipment: equipment),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si pas trouvé dans les équipements, chercher dans les conteneurs
|
||||||
|
final containerProvider = context.read<ContainerProvider>();
|
||||||
|
if (containerProvider.containers.isEmpty) {
|
||||||
|
await containerProvider.loadContainers();
|
||||||
|
}
|
||||||
|
|
||||||
|
final container = containerProvider.containers.firstWhere(
|
||||||
|
(c) => c.id == scannedCode,
|
||||||
|
orElse: () => ContainerModel(
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
type: ContainerType.flightCase,
|
||||||
|
status: EquipmentStatus.available,
|
||||||
|
equipmentIds: [],
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (container.id.isNotEmpty) {
|
||||||
|
// Conteneur trouvé
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ContainerDetailPage(container: container),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rien trouvé
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Aucun équipement ou conteneur trouvé avec l\'ID : $scannedCode'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[EquipmentManagementPage] Error scanning QR code', e);
|
||||||
|
if (mounted) {
|
||||||
|
// Fermer l'indicateur si ouvert
|
||||||
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur lors du scan : ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ class ManagementSearchBar extends StatelessWidget {
|
|||||||
final ValueChanged<String> onChanged;
|
final ValueChanged<String> onChanged;
|
||||||
final VoidCallback? onSelectionModeToggle;
|
final VoidCallback? onSelectionModeToggle;
|
||||||
final bool showSelectionModeButton;
|
final bool showSelectionModeButton;
|
||||||
|
final List<Widget>? additionalActions;
|
||||||
|
|
||||||
const ManagementSearchBar({
|
const ManagementSearchBar({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -16,6 +17,7 @@ class ManagementSearchBar extends StatelessWidget {
|
|||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
this.onSelectionModeToggle,
|
this.onSelectionModeToggle,
|
||||||
this.showSelectionModeButton = true,
|
this.showSelectionModeButton = true,
|
||||||
|
this.additionalActions,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -53,6 +55,7 @@ class ManagementSearchBar extends StatelessWidget {
|
|||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (additionalActions != null) ...additionalActions!,
|
||||||
if (showSelectionModeButton && onSelectionModeToggle != null) ...[
|
if (showSelectionModeButton && onSelectionModeToggle != null) ...[
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ dependencies:
|
|||||||
pdf: ^3.10.7
|
pdf: ^3.10.7
|
||||||
printing: ^5.11.1
|
printing: ^5.11.1
|
||||||
qr_flutter: ^4.1.0
|
qr_flutter: ^4.1.0
|
||||||
|
mobile_scanner: ^5.2.3
|
||||||
flutter_local_notifications: ^19.2.1
|
flutter_local_notifications: ^19.2.1
|
||||||
timezone: ^0.10.1
|
timezone: ^0.10.1
|
||||||
flutter_secure_storage: ^9.0.0
|
flutter_secure_storage: ^9.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user