barcode_scanner/lib/pages/operation/reception/reception_scan_page.dart
your-name e6901eae8f refactor: Refactors reception scan and updates app name
Optimizes product lookup during reception scanning by removing redundant database calls.
Adds error feedback for quantity increment failures, enhancing user experience.
Updates the application name from 'BarcodeScan' to 'eScan'.
2025-07-31 04:31:55 +03:00

403 lines
13 KiB
Dart

import 'dart:async';
import 'package:e_scan/components/components.dart';
import 'package:e_scan/pages/operation/reception/reception_scan_page_model.dart';
import 'package:e_scan/themes/app_theme.dart';
import 'package:e_scan/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class ReceptionScanPage extends ConsumerStatefulWidget {
const ReceptionScanPage({super.key, required this.receptionId});
final int receptionId;
@override
ConsumerState<ConsumerStatefulWidget> createState() =>
_ReceptionScanPageState();
}
class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
with WidgetsBindingObserver {
final MobileScannerController mobileScannerController =
MobileScannerController(
// required options for the scanner
);
StreamSubscription<Object?>? _subscription;
bool qrcodeFound = false;
@override
void initState() {
super.initState();
// Start listening to lifecycle changes.
WidgetsBinding.instance.addObserver(this);
SchedulerBinding.instance.addPostFrameCallback((_) {
ref
.read(receptionScanPageModelProvider.notifier)
.getReceptionById(id: widget.receptionId);
});
// Start listening to the barcode events.
_subscription = mobileScannerController.barcodes.listen(_handleBarcode);
// Finally, start the scanner itself.
unawaited(mobileScannerController.start());
}
@override
void dispose() {
// Stop listening to lifecycle changes.
WidgetsBinding.instance.removeObserver(this);
// Stop listening to the barcode events.
unawaited(_subscription?.cancel());
_subscription = null;
// Dispose the widget itself.
// Finally, dispose of the controller.
mobileScannerController.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// If the controller is not ready, do not try to start or stop it.
// Permission dialogs can trigger lifecycle changes before the controller is ready.
if (!mobileScannerController.value.hasCameraPermission) {
return;
}
switch (state) {
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
case AppLifecycleState.paused:
return;
case AppLifecycleState.resumed:
// Restart the scanner when the app is resumed.
// Don't forget to resume listening to the barcode events.
_subscription = mobileScannerController.barcodes.listen(_handleBarcode);
unawaited(mobileScannerController.start());
case AppLifecycleState.inactive:
// Stop the scanner when the app is paused.
// Also stop the barcode events subscription.
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
}
}
Future<void> _handleBarcode(BarcodeCapture barcodeCapture) async {
if (qrcodeFound) return;
if (barcodeCapture.barcodes.isNotEmpty) {
qrcodeFound = true;
mobileScannerController.stop();
final code = barcodeCapture.barcodes.first;
final qrcodeValue = code.displayValue;
debugPrint("Code détecté : ${code.displayValue}");
final model = ref.read(receptionScanPageModelProvider.notifier);
if (qrcodeValue == null) {
debugPrint("Produit non attendu dans cette réception");
Toast.showError('Produit non attendu dans cette réception');
return;
}
// find product in local database
final product = model.getProduct(
receptionId: widget.receptionId,
barcode: qrcodeValue,
);
if (product != null) {
model.incrementMoveLineQuantity(
barcode: qrcodeValue,
receptionId: widget.receptionId,
onError: () {
Toast.showError('Aucun produit trouvé.');
},
);
//show dialog
await showDialog(
barrierDismissible: false,
context: context,
builder: (dialogContext) {
return Dialog(
elevation: 0,
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
alignment: AlignmentDirectional(
0.0,
0.0,
).resolve(Directionality.of(context)),
child: GestureDetector(
onTap: () {
FocusScope.of(dialogContext).unfocus();
FocusManager.instance.primaryFocus?.unfocus();
},
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 0.9,
child: ProductScannedComponent(
productStruct: product,
onRescan: () async {
Navigator.of(context).pop();
qrcodeFound = false;
mobileScannerController.start();
},
onDetails: () {
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
Navigator.of(context).pop();
Navigator.of(context).pop();
},
),
),
),
);
},
);
} else {
Toast.showError('Produit non attendu dans cette réception');
debugPrint('Produit non attendu dans cette réception');
}
}
}
Future<void> _fakeBarcode() async {
final qrcodeValue = "AIRF0001";
qrcodeFound = true;
final model = ref.read(receptionScanPageModelProvider.notifier);
mobileScannerController.stop();
// find product in local database
final product = model.getProduct(
receptionId: widget.receptionId,
barcode: qrcodeValue,
);
if (product != null) {
model.incrementMoveLineQuantity(
barcode: qrcodeValue,
receptionId: widget.receptionId,
onError: () {
Toast.showError('Aucun produit trouvé.');
},
);
//show dialog
await showDialog(
barrierDismissible: false,
context: context,
builder: (dialogContext) {
return Dialog(
elevation: 0,
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
alignment: AlignmentDirectional(
0.0,
0.0,
).resolve(Directionality.of(context)),
child: GestureDetector(
onTap: () {
FocusScope.of(dialogContext).unfocus();
FocusManager.instance.primaryFocus?.unfocus();
},
child: SizedBox(
width: MediaQuery.sizeOf(context).width * 0.9,
child: ProductScannedComponent(
productStruct: product,
onRescan: () async {
qrcodeFound = false;
mobileScannerController.start();
Navigator.of(context).pop();
},
onDetails: () {
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
Navigator.of(context).pop();
Navigator.of(context).pop();
},
),
),
),
);
},
);
} else {
Toast.showError('Aucun produit trouvé.');
debugPrint('Aucun produit trouvé.');
}
}
@override
Widget build(BuildContext context) {
final state = ref.watch(receptionScanPageModelProvider);
final reception = state.reception;
return Scaffold(
backgroundColor: AppTheme.of(context).primaryBackground,
appBar: AppBar(
backgroundColor: AppTheme.of(context).primaryBackground,
automaticallyImplyLeading: false,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: AppTheme.of(context).primaryText,
size: 24.0,
),
onPressed: () async {
Navigator.of(context).pop();
},
),
title: Text(
'Réception ${reception?.name}',
style: AppTheme.of(context).titleLarge,
),
actions: [
Padding(
padding: EdgeInsetsDirectional.fromSTEB(8.0, 0.0, 8.0, 0.0),
child: IconButton(
icon: Icon(Icons.help_outline, color: Colors.white, size: 24.0),
onPressed: () {
debugPrint('IconButton pressed ...');
},
),
),
],
centerTitle: true,
elevation: 0.0,
),
body: state.loading
? Center(child: LoadingProgressComponent())
: SizedBox(
width: double.infinity,
height: double.infinity,
child: Stack(
children: [
SizedBox(
width: double.infinity,
height: double.infinity,
child: MobileScanner(
controller: mobileScannerController,
onDetectError: (error, stackTrace) {
debugPrint("===========> $error");
},
errorBuilder: (c, p2) {
return Center(child: Icon(Icons.error));
},
),
),
Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.of(context).primaryBackground,
Colors.transparent,
AppTheme.of(context).primaryBackground,
],
stops: [0.0, 0.5, 1.0],
begin: AlignmentDirectional(0.0, -1.0),
end: AlignmentDirectional(0, 1.0),
),
),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_InfoTextScan(),
_ZoneScanBox(
onTap: () {
_fakeBarcode();
qrcodeFound = false;
mobileScannerController.start();
},
),
FlashButtonComponent(
mobileScannerController: mobileScannerController,
),
],
),
),
),
],
),
),
);
}
}
class _ZoneScanBox extends StatelessWidget {
const _ZoneScanBox({this.onTap});
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Align(
alignment: AlignmentDirectional(0.0, 0.0),
child: Container(
width: 280.0,
height: 280.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.0),
border: Border.all(color: Colors.white, width: 3.0),
),
child: GestureDetector(
onTap: onTap,
child: Container(
width: 260.0,
height: 260.0,
decoration: BoxDecoration(
color: Color(0x33FFFFFF),
borderRadius: BorderRadius.circular(12.0),
),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.qr_code_scanner,
color: Colors.white.withValues(alpha: .5),
size: 64.0,
),
],
),
),
),
),
),
);
}
}
class _InfoTextScan extends StatelessWidget {
const _InfoTextScan();
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsetsDirectional.fromSTEB(24.0, 40.0, 24.0, 0.0),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Scanner le code',
textAlign: TextAlign.center,
style: TextStyle(color: AppTheme.of(context).primaryText),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(0.0, 8.0, 0.0, 0.0),
child: Text(
'Positionnez le code-barres ou QR code dans le cadre',
textAlign: TextAlign.center,
style: TextStyle(color: AppTheme.of(context).primaryText),
),
),
],
),
);
}
}