
Integrates the `toastification` package to provide clear and non-intrusive user feedback for operations like product not found. Refactors the reception scan page UI by extracting reusable widgets for the scan information text, the scan box, and the flash button. This improves code organization and readability. Switches the data source for reception details from API calls to the local ObjectBox database, improving performance and enabling offline access for this specific data. Corrects the display of scanned product information to show the barcode instead of a generic ID. Updates `go_router` to version 16.0.0.
384 lines
13 KiB
Dart
384 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("Qrcode non valide");
|
|
return;
|
|
}
|
|
// find product in local database
|
|
final isProductExist = model.isProductExist(barcode: qrcodeValue);
|
|
if (isProductExist) {
|
|
final product = model.getProduct(barcode: qrcodeValue);
|
|
//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 {
|
|
debugPrint('Aucun produit trouvé.');
|
|
}
|
|
}
|
|
}
|
|
|
|
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.isProductExist(barcode: qrcodeValue);
|
|
if (isProductExist) {
|
|
final product = model.getProduct(barcode: qrcodeValue);
|
|
model.incrementMoveLineQuantity(barcode: qrcodeValue);
|
|
//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),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|