barcode_scanner/lib/pages/scanner/scanner_page.dart
mandreshope 1d331acc54 feat: Streamlines product data and stock picking API
Refactors the product data model to primarily store `id`, `barcode`, and `displayName`. This simplifies the product structure, focusing on essential attributes for inventory and scanning operations.

Optimizes API calls for stock picking to fetch more relevant product details. Unnecessary fields are removed, and specific product attributes like `barcode` and `quantity` are now explicitly requested for stock moves.

Moves `StockPickingRecordModel` to its own dedicated file for improved code organization and maintainability. Updates all affected UI components and scanner logic to align with the revised product model.
2025-07-30 12:47:08 +03:00

425 lines
16 KiB
Dart

import 'dart:async';
import 'package:e_scan/backend/api/api_calls.dart';
import 'package:e_scan/backend/objectbox/entities/product/product_entity.dart';
import 'package:e_scan/backend/objectbox/objectbox_manager.dart';
import 'package:e_scan/backend/schema/product/product_model.dart';
import 'package:e_scan/components/loading_progress_component.dart';
import 'package:e_scan/components/product_scanned_component.dart';
import 'package:e_scan/router/go_secure_router_builder.dart';
import 'package:e_scan/themes/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
class ScannerPage extends ConsumerStatefulWidget {
const ScannerPage({super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _ScannerPageState();
}
class _ScannerPageState extends ConsumerState<ScannerPage>
with WidgetsBindingObserver {
final MobileScannerController mobileScannerController =
MobileScannerController(
// required options for the scanner
);
StreamSubscription<Object?>? _subscription;
bool qrcodeFound = false;
bool loading = false;
@override
void initState() {
super.initState();
// Start listening to lifecycle changes.
WidgetsBinding.instance.addObserver(this);
// 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}");
if (qrcodeValue == null) {
debugPrint("Qrcode non valide");
return;
}
setState(() {
loading = true;
});
final product = await ApiCalls.fetchProduct(code.displayValue!);
if (product != null) {
setState(() {
loading = false;
});
debugPrint('Nom du produit : ${product["product_name"]}');
debugPrint('Marque : ${product["brands"]}');
debugPrint('Image : ${product["image_url"]}');
final productStruct = ProductModel(
id: int.parse(product["id"]),
displayName: product["generic_name"],
);
Box<ProductEntity> productStore = objectboxManager.store
.box<ProductEntity>();
productStore.put(productStruct.toEntity);
//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: productStruct,
onRescan: () async {
Navigator.of(context).pop();
qrcodeFound = false;
mobileScannerController.start();
},
onDetails: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute(id: productStruct.id ?? 0).push(context);
},
),
),
),
);
},
);
} else {
setState(() {
loading = false;
});
debugPrint('Aucun produit trouvé.');
}
}
}
Future<void> _fakeBarcode() async {
final qrcodeValue = "0737628064502";
qrcodeFound = true;
mobileScannerController.stop();
setState(() {
loading = true;
});
final product = await ApiCalls.fetchProduct(qrcodeValue);
if (product != null) {
setState(() {
loading = false;
});
debugPrint('Nom du produit : ${product["product_name"]}');
debugPrint('Marque : ${product["brands"]}');
debugPrint('Image : ${product["image_url"]}');
final productStruct = ProductModel(
id: int.parse(product["id"]),
displayName: product["generic_name"],
);
Box<ProductEntity> productStore = objectboxManager.store
.box<ProductEntity>();
productStore.put(productStruct.toEntity);
//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: productStruct,
onRescan: () async {
Navigator.of(context).pop();
qrcodeFound = false;
mobileScannerController.start();
},
onDetails: () {
Navigator.of(context).pop();
unawaited(_subscription?.cancel());
_subscription = null;
unawaited(mobileScannerController.stop());
ProductFormRoute(id: productStruct.id ?? 0).push(context);
},
),
),
),
);
},
);
} else {
setState(() {
loading = false;
});
debugPrint('Aucun produit trouvé.');
}
}
@override
Widget build(BuildContext context) {
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('Scanner', 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: 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: [
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,
),
),
),
],
),
),
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: () {
_fakeBarcode();
qrcodeFound = false;
mobileScannerController.start();
},
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,
),
],
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(40),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
color: AppTheme.of(context).primaryText,
iconSize: 2,
icon: Icon(
mobileScannerController.torchEnabled
? Icons.flash_off
: Icons.flash_on,
color: AppTheme.of(context).primaryText,
size: 28.0,
),
onPressed: () {
mobileScannerController.toggleTorch();
},
),
],
),
),
],
),
),
),
],
),
),
);
}
}