barcode_scanner/lib/pages/operation/reception/reception_scan_page.dart
your-name ab4a56ed41 feat: Adds draft status for stock pickings
Introduces an `isDraft` property to `StockPickingRecordEntity` to mark receptions with local modifications.

Automatically sets a reception to draft status when a product's quantity is incremented via scanning. Displays a 'Brouillon' chip on reception cards to provide visual feedback for draft operations.

Ensures reception details are refreshed after scanning to reflect the updated draft status and provides immediate error feedback when no product is found during a scan.
2025-07-30 20:26:17 +03:00

393 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");
Toast.showError('Aucun produit trouvé.');
return;
}
// find product in local database
final isProductExist = model.isProductExist(barcode: qrcodeValue);
if (isProductExist) {
final product = model.getProduct(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('Aucun produit trouvé.');
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,
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 {
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),
),
),
],
),
);
}
}