diff --git a/lib/pages/operation/operation_page.dart b/lib/pages/operation/operation_page.dart index 65d9ba7..a4fa002 100644 --- a/lib/pages/operation/operation_page.dart +++ b/lib/pages/operation/operation_page.dart @@ -2,3 +2,4 @@ export 'reception/reception_page.dart'; export 'delivery/delivery_page.dart'; export 'inventory/inventory_page.dart'; export 'reception/reception_details_page.dart'; +export 'reception/reception_scan_page.dart'; diff --git a/lib/pages/operation/reception/reception_details_page.dart b/lib/pages/operation/reception/reception_details_page.dart index e7bbf25..6e975af 100644 --- a/lib/pages/operation/reception/reception_details_page.dart +++ b/lib/pages/operation/reception/reception_details_page.dart @@ -47,7 +47,9 @@ class _ReceptionDetailsPageState extends ConsumerState { if (reception?.isDone == false) ...[ QuickActionComponent( onTapScan: () { - ScannerRoute().push(context); + final id = reception?.id; + if (id == null) return; + ReceptionScanRoute(receptionId: id).push(context); }, ), const SizedBox(height: 16), @@ -138,7 +140,9 @@ class _ReceptionDetailsPageState extends ConsumerState { ? FloatingActionButton( backgroundColor: AppTheme.of(context).primary, onPressed: () { - ScannerRoute().push(context); + final id = reception?.id; + if (id == null) return; + ReceptionScanRoute(receptionId: id).push(context); }, child: Icon(Icons.qr_code, color: AppTheme.of(context).white), ) diff --git a/lib/pages/operation/reception/reception_scan_page.dart b/lib/pages/operation/reception/reception_scan_page.dart new file mode 100644 index 0000000..6d6e193 --- /dev/null +++ b/lib/pages/operation/reception/reception_scan_page.dart @@ -0,0 +1,439 @@ +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/pages/operation/reception/reception_scan_page_model.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/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 createState() => + _ReceptionScanPageState(); +} + +class _ReceptionScanPageState extends ConsumerState + with WidgetsBindingObserver { + final MobileScannerController mobileScannerController = + MobileScannerController( + // required options for the scanner + ); + + StreamSubscription? _subscription; + bool qrcodeFound = false; + bool loading = 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 _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 productStore = objectboxManager.store + .box(); + 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 _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 productStore = objectboxManager.store + .box(); + 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) { + 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: loading || 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: [ + 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(); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/operation/reception/reception_scan_page_model.dart b/lib/pages/operation/reception/reception_scan_page_model.dart new file mode 100644 index 0000000..61a34fe --- /dev/null +++ b/lib/pages/operation/reception/reception_scan_page_model.dart @@ -0,0 +1,45 @@ +import 'package:e_scan/backend/api/api_calls.dart'; +import 'package:e_scan/backend/objectbox/entities/stock_picking/stock_picking_record_entity.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'reception_scan_page_model.freezed.dart'; + +final receptionScanPageModelProvider = + StateNotifierProvider.autoDispose< + ReceptionScanPageModel, + ReceptionScanPageModelState + >((ref) { + return ReceptionScanPageModel(); + }); + +class ReceptionScanPageModel + extends StateNotifier { + ReceptionScanPageModel() : super(const ReceptionScanPageModelState()); + + Future getReceptionById({required int id}) async { + try { + state = state.copyWith(loading: true); + final res = await ApiCalls.getStockPikingById(id: id); + res.when( + (data) { + state = state.copyWith(loading: false, reception: data); + }, + (error) { + state = state.copyWith(loading: false); + }, + ); + } catch (e) { + state = state.copyWith(loading: false); + } + } +} + +@freezed +abstract class ReceptionScanPageModelState with _$ReceptionScanPageModelState { + const factory ReceptionScanPageModelState({ + StockPickingRecordEntity? reception, + @Default(false) bool loading, + }) = _ReceptionScanPageModelState; +} diff --git a/lib/pages/operation/reception/reception_scan_page_model.freezed.dart b/lib/pages/operation/reception/reception_scan_page_model.freezed.dart new file mode 100644 index 0000000..4eb590a --- /dev/null +++ b/lib/pages/operation/reception/reception_scan_page_model.freezed.dart @@ -0,0 +1,157 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'reception_scan_page_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ReceptionScanPageModelState implements DiagnosticableTreeMixin { + + StockPickingRecordEntity? get reception; bool get loading; +/// Create a copy of ReceptionScanPageModelState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ReceptionScanPageModelStateCopyWith get copyWith => _$ReceptionScanPageModelStateCopyWithImpl(this as ReceptionScanPageModelState, _$identity); + + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ReceptionScanPageModelState')) + ..add(DiagnosticsProperty('reception', reception))..add(DiagnosticsProperty('loading', loading)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ReceptionScanPageModelState&&(identical(other.reception, reception) || other.reception == reception)&&(identical(other.loading, loading) || other.loading == loading)); +} + + +@override +int get hashCode => Object.hash(runtimeType,reception,loading); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ReceptionScanPageModelState(reception: $reception, loading: $loading)'; +} + + +} + +/// @nodoc +abstract mixin class $ReceptionScanPageModelStateCopyWith<$Res> { + factory $ReceptionScanPageModelStateCopyWith(ReceptionScanPageModelState value, $Res Function(ReceptionScanPageModelState) _then) = _$ReceptionScanPageModelStateCopyWithImpl; +@useResult +$Res call({ + StockPickingRecordEntity? reception, bool loading +}); + + + + +} +/// @nodoc +class _$ReceptionScanPageModelStateCopyWithImpl<$Res> + implements $ReceptionScanPageModelStateCopyWith<$Res> { + _$ReceptionScanPageModelStateCopyWithImpl(this._self, this._then); + + final ReceptionScanPageModelState _self; + final $Res Function(ReceptionScanPageModelState) _then; + +/// Create a copy of ReceptionScanPageModelState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? reception = freezed,Object? loading = null,}) { + return _then(_self.copyWith( +reception: freezed == reception ? _self.reception : reception // ignore: cast_nullable_to_non_nullable +as StockPickingRecordEntity?,loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// @nodoc + + +class _ReceptionScanPageModelState with DiagnosticableTreeMixin implements ReceptionScanPageModelState { + const _ReceptionScanPageModelState({this.reception, this.loading = false}); + + +@override final StockPickingRecordEntity? reception; +@override@JsonKey() final bool loading; + +/// Create a copy of ReceptionScanPageModelState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ReceptionScanPageModelStateCopyWith<_ReceptionScanPageModelState> get copyWith => __$ReceptionScanPageModelStateCopyWithImpl<_ReceptionScanPageModelState>(this, _$identity); + + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ReceptionScanPageModelState')) + ..add(DiagnosticsProperty('reception', reception))..add(DiagnosticsProperty('loading', loading)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ReceptionScanPageModelState&&(identical(other.reception, reception) || other.reception == reception)&&(identical(other.loading, loading) || other.loading == loading)); +} + + +@override +int get hashCode => Object.hash(runtimeType,reception,loading); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ReceptionScanPageModelState(reception: $reception, loading: $loading)'; +} + + +} + +/// @nodoc +abstract mixin class _$ReceptionScanPageModelStateCopyWith<$Res> implements $ReceptionScanPageModelStateCopyWith<$Res> { + factory _$ReceptionScanPageModelStateCopyWith(_ReceptionScanPageModelState value, $Res Function(_ReceptionScanPageModelState) _then) = __$ReceptionScanPageModelStateCopyWithImpl; +@override @useResult +$Res call({ + StockPickingRecordEntity? reception, bool loading +}); + + + + +} +/// @nodoc +class __$ReceptionScanPageModelStateCopyWithImpl<$Res> + implements _$ReceptionScanPageModelStateCopyWith<$Res> { + __$ReceptionScanPageModelStateCopyWithImpl(this._self, this._then); + + final _ReceptionScanPageModelState _self; + final $Res Function(_ReceptionScanPageModelState) _then; + +/// Create a copy of ReceptionScanPageModelState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? reception = freezed,Object? loading = null,}) { + return _then(_ReceptionScanPageModelState( +reception: freezed == reception ? _self.reception : reception // ignore: cast_nullable_to_non_nullable +as StockPickingRecordEntity?,loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/lib/router/go_secure_router_builder.dart b/lib/router/go_secure_router_builder.dart index 7723d3b..f505c22 100644 --- a/lib/router/go_secure_router_builder.dart +++ b/lib/router/go_secure_router_builder.dart @@ -21,6 +21,7 @@ final appSecureRoutes = $appRoutes; TypedGoRoute(path: 'DeliveryPage'), TypedGoRoute(path: 'InventoryPage'), TypedGoRoute(path: 'ReceptionDetailsPage'), + TypedGoRoute(path: 'ReceptionScanPage'), ], ) class SecureRoute extends GoRouteData with _$SecureRoute { @@ -120,3 +121,12 @@ class ReceptionDetailsRoute extends GoRouteData with _$ReceptionDetailsRoute { Widget build(BuildContext context, GoRouterState state) => ReceptionDetailsPage(receptionId: receptionId); } + +class ReceptionScanRoute extends GoRouteData with _$ReceptionScanRoute { + const ReceptionScanRoute({required this.receptionId}); + final int receptionId; + + @override + Widget build(BuildContext context, GoRouterState state) => + ReceptionScanPage(receptionId: receptionId); +} diff --git a/lib/router/go_secure_router_builder.g.dart b/lib/router/go_secure_router_builder.g.dart index 42e8aac..dde76b9 100644 --- a/lib/router/go_secure_router_builder.g.dart +++ b/lib/router/go_secure_router_builder.g.dart @@ -45,6 +45,11 @@ RouteBase get $secureRoute => GoRouteData.$route( factory: _$ReceptionDetailsRoute._fromState, ), + GoRouteData.$route( + path: 'ReceptionScanPage', + + factory: _$ReceptionScanRoute._fromState, + ), ], ); @@ -244,3 +249,31 @@ mixin _$ReceptionDetailsRoute on GoRouteData { @override void replace(BuildContext context) => context.replace(location); } + +mixin _$ReceptionScanRoute on GoRouteData { + static ReceptionScanRoute _fromState(GoRouterState state) => + ReceptionScanRoute( + receptionId: int.parse(state.uri.queryParameters['reception-id']!)!, + ); + + ReceptionScanRoute get _self => this as ReceptionScanRoute; + + @override + String get location => GoRouteData.$location( + '/SecurePage/ReceptionScanPage', + queryParams: {'reception-id': _self.receptionId.toString()}, + ); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +}