
Refreshes the list of receptions on the main page after returning from the details view, ensuring the displayed data is up-to-date. Improves the product scanning logic by ensuring product lookups are specific to the current reception. This prevents misidentification of products across different receptions. Adds an error toast message when a scanned product is not found within the active reception's product list, providing immediate user feedback. Refactors product retrieval and existence checks for clarity and robustness.
408 lines
13 KiB
Dart
408 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 isProductExist = model.getProduct(
|
|
receptionId: widget.receptionId,
|
|
barcode: qrcodeValue,
|
|
);
|
|
if (isProductExist != null) {
|
|
final product = model.getProduct(
|
|
receptionId: widget.receptionId,
|
|
barcode: qrcodeValue,
|
|
);
|
|
model.incrementMoveLineQuantity(
|
|
barcode: qrcodeValue,
|
|
receptionId: widget.receptionId,
|
|
);
|
|
//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 isProductExist = model.getProduct(
|
|
receptionId: widget.receptionId,
|
|
barcode: qrcodeValue,
|
|
);
|
|
if (isProductExist != null) {
|
|
final product = model.getProduct(
|
|
receptionId: widget.receptionId,
|
|
barcode: qrcodeValue,
|
|
);
|
|
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),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|