From da2c3ac4f092cc54fb69ae34463f675167247d02 Mon Sep 17 00:00:00 2001 From: your-name Date: Wed, 30 Jul 2025 19:35:43 +0300 Subject: [PATCH] enhance: Enhances scanner UI and integrates toast notifications 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. --- lib/app.dart | 42 ++-- lib/components/components.dart | 1 + lib/components/flash_button_component.dart | 39 ++++ lib/components/product_scanned_component.dart | 2 +- .../reception_details_page_model.dart | 15 +- .../reception/reception_scan_page.dart | 217 ++++++++---------- lib/utils/toastification.dart | 24 ++ lib/utils/utils.dart | 2 + pubspec.lock | 52 ++++- pubspec.yaml | 3 +- 10 files changed, 237 insertions(+), 160 deletions(-) create mode 100644 lib/components/flash_button_component.dart create mode 100644 lib/utils/toastification.dart diff --git a/lib/app.dart b/lib/app.dart index 8ee3da4..e574543 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:responsive_framework/responsive_framework.dart'; +import 'package:toastification/toastification.dart'; class App extends ConsumerStatefulWidget { const App({super.key}); @@ -36,27 +37,30 @@ class _InnerApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return MaterialApp.router( - debugShowCheckedModeBanner: false, - title: "BarcodeScan", - locale: Locale('fr'), - supportedLocales: [Locale('fr', 'FR'), Locale('en', 'US')], - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, // Support for Material widgets - GlobalWidgetsLocalizations.delegate, // Localization for widgets - GlobalCupertinoLocalizations.delegate, // Support for Cupertino widgets - ], - routerConfig: ref.watch(routerProvider), - builder: (context, widget) => ResponsiveBreakpoints.builder( - child: _ResponsiveWrapper(child: widget ?? const SizedBox.shrink()), - breakpoints: [ - const Breakpoint(start: 0, end: 450, name: MOBILE), - const Breakpoint(start: 451, end: 800, name: TABLET), + return ToastificationWrapper( + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + title: "BarcodeScan", + locale: Locale('fr'), + supportedLocales: [Locale('fr', 'FR'), Locale('en', 'US')], + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, // Support for Material widgets + GlobalWidgetsLocalizations.delegate, // Localization for widgets + GlobalCupertinoLocalizations + .delegate, // Support for Cupertino widgets ], + routerConfig: ref.watch(routerProvider), + builder: (context, widget) => ResponsiveBreakpoints.builder( + child: _ResponsiveWrapper(child: widget ?? const SizedBox.shrink()), + breakpoints: [ + const Breakpoint(start: 0, end: 450, name: MOBILE), + const Breakpoint(start: 451, end: 800, name: TABLET), + ], + ), + theme: ThemeData(brightness: Brightness.light), + darkTheme: ThemeData(brightness: Brightness.dark), + themeMode: themeMode, ), - theme: ThemeData(brightness: Brightness.light), - darkTheme: ThemeData(brightness: Brightness.dark), - themeMode: themeMode, ); } } diff --git a/lib/components/components.dart b/lib/components/components.dart index 8bc32ca..2af3e5e 100644 --- a/lib/components/components.dart +++ b/lib/components/components.dart @@ -6,3 +6,4 @@ export 'outline_button_component.dart'; export 'quick_action_component.dart'; export 'main_appbar_component.dart'; export 'stock_picking_card_component.dart'; +export 'flash_button_component.dart'; diff --git a/lib/components/flash_button_component.dart b/lib/components/flash_button_component.dart new file mode 100644 index 0000000..96d652d --- /dev/null +++ b/lib/components/flash_button_component.dart @@ -0,0 +1,39 @@ +import 'package:e_scan/themes/app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +class FlashButtonComponent extends StatelessWidget { + const FlashButtonComponent({ + super.key, + required this.mobileScannerController, + }); + + final MobileScannerController mobileScannerController; + + @override + Widget build(BuildContext context) { + return 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/components/product_scanned_component.dart b/lib/components/product_scanned_component.dart index 984967a..80e41bd 100644 --- a/lib/components/product_scanned_component.dart +++ b/lib/components/product_scanned_component.dart @@ -80,7 +80,7 @@ class _ProductScannedComponentState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Code scanné:'), - Text(widget.productStruct.id.toString()), + Text(widget.productStruct.barcode.toString()), ], ), Divider( diff --git a/lib/pages/operation/reception/reception_details_page_model.dart b/lib/pages/operation/reception/reception_details_page_model.dart index 9b349f9..d7c63ca 100644 --- a/lib/pages/operation/reception/reception_details_page_model.dart +++ b/lib/pages/operation/reception/reception_details_page_model.dart @@ -1,5 +1,5 @@ -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:e_scan/backend/objectbox/objectbox_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -20,16 +20,11 @@ class ReceptionDetailsPageModel Future getReceptionById({required int id}) async { try { + final stockPickingRecords = objectboxManager.store + .box(); 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); - }, - ); + final entity = stockPickingRecords.get(id); + state = state.copyWith(loading: false, reception: entity); } catch (e) { state = state.copyWith(loading: false); } diff --git a/lib/pages/operation/reception/reception_scan_page.dart b/lib/pages/operation/reception/reception_scan_page.dart index 9a38b3b..4b52eaf 100644 --- a/lib/pages/operation/reception/reception_scan_page.dart +++ b/lib/pages/operation/reception/reception_scan_page.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'package:e_scan/components/loading_progress_component.dart'; -import 'package:e_scan/components/product_scanned_component.dart'; +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'; @@ -26,7 +26,6 @@ class _ReceptionScanPageState extends ConsumerState StreamSubscription? _subscription; bool qrcodeFound = false; - bool loading = false; @override void initState() { @@ -102,17 +101,10 @@ class _ReceptionScanPageState extends ConsumerState debugPrint("Qrcode non valide"); return; } - setState(() { - loading = true; - }); // find product in local database final isProductExist = model.isProductExist(barcode: qrcodeValue); if (isProductExist) { final product = model.getProduct(barcode: qrcodeValue); - setState(() { - loading = false; - }); - //show dialog await showDialog( barrierDismissible: false, @@ -154,29 +146,21 @@ class _ReceptionScanPageState extends ConsumerState }, ); } else { - setState(() { - loading = false; - }); debugPrint('Aucun produit trouvé.'); } } } Future _fakeBarcode() async { - final qrcodeValue = "0737628064502"; + final qrcodeValue = "AIRF0001"; qrcodeFound = true; final model = ref.read(receptionScanPageModelProvider.notifier); mobileScannerController.stop(); - setState(() { - loading = true; - }); // find product in local database final isProductExist = model.isProductExist(barcode: qrcodeValue); if (isProductExist) { final product = model.getProduct(barcode: qrcodeValue); - setState(() { - loading = false; - }); + model.incrementMoveLineQuantity(barcode: qrcodeValue); //show dialog await showDialog( barrierDismissible: false, @@ -218,9 +202,7 @@ class _ReceptionScanPageState extends ConsumerState }, ); } else { - setState(() { - loading = false; - }); + Toast.showError('Aucun produit trouvé.'); debugPrint('Aucun produit trouvé.'); } } @@ -262,7 +244,7 @@ class _ReceptionScanPageState extends ConsumerState centerTitle: true, elevation: 0.0, ), - body: loading || state.loading + body: state.loading ? Center(child: LoadingProgressComponent()) : SizedBox( width: double.infinity, @@ -303,110 +285,16 @@ class _ReceptionScanPageState extends ConsumerState 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, - ), - ), - ), - ], - ), + _InfoTextScan(), + _ZoneScanBox( + onTap: () { + _fakeBarcode(); + qrcodeFound = false; + mobileScannerController.start(); + }, ), - 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(); - }, - ), - ], - ), + FlashButtonComponent( + mobileScannerController: mobileScannerController, ), ], ), @@ -418,3 +306,78 @@ class _ReceptionScanPageState extends ConsumerState ); } } + +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), + ), + ), + ], + ), + ); + } +} diff --git a/lib/utils/toastification.dart b/lib/utils/toastification.dart new file mode 100644 index 0000000..0642369 --- /dev/null +++ b/lib/utils/toastification.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:toastification/toastification.dart'; + +class Toast { + static void showError(String message) { + toastification.show( + type: ToastificationType.error, + description: Text(message), + style: ToastificationStyle.flatColored, + alignment: Alignment.bottomCenter, + autoCloseDuration: Duration(seconds: 5), + ); + } + + static void showSuccess(String message) { + toastification.show( + type: ToastificationType.success, + description: Text(message), + style: ToastificationStyle.flatColored, + alignment: Alignment.bottomCenter, + autoCloseDuration: Duration(seconds: 5), + ); + } +} diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 51ace5c..e7d2379 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,6 +1,8 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; +export 'toastification.dart'; + extension StatefulWidgetExtensions on State { /// Check if the widget exist before safely setting state. void safeSetState(VoidCallback fn) { diff --git a/pubspec.lock b/pubspec.lock index 2515c3e..3d2ffb8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -233,6 +233,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -401,10 +409,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "02ff498f6279470ff7f60c998a69b872f26696ceec237c8402e63a2133868ddf" + sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431 url: "https://pub.dev" source: hosted - version: "15.2.3" + version: "16.0.0" go_router_builder: dependency: "direct dev" description: @@ -453,6 +461,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + iconsax_flutter: + dependency: transitive + description: + name: iconsax_flutter + sha256: d14b4cec8586025ac15276bdd40f6eea308cb85748135965bb6255f14beb2564 + url: "https://pub.dev" + source: hosted + version: "1.0.1" image: dependency: transitive description: @@ -685,6 +701,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pausable_timer: + dependency: transitive + description: + name: pausable_timer + sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" + url: "https://pub.dev" + source: hosted + version: "3.1.0+3" petitparser: dependency: transitive description: @@ -866,6 +890,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -930,6 +962,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + toastification: + dependency: "direct main" + description: + name: toastification + sha256: "69db2bff425b484007409650d8bcd5ed1ce2e9666293ece74dcd917dacf23112" + url: "https://pub.dev" + source: hosted + version: "3.0.3" typed_data: dependency: transitive description: @@ -938,6 +978,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 627dc1a..80cc374 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 mobile_scanner: ^7.0.1 - go_router: ^15.2.3 + go_router: ^16.0.0 flutter_riverpod: ^2.6.1 flutter_launcher_icons: ^0.14.4 freezed_annotation: ^3.0.0 @@ -55,6 +55,7 @@ dependencies: path_provider: ^2.1.5 path: ^1.9.1 multiple_result: ^5.1.0 + toastification: ^3.0.3 dev_dependencies: flutter_test: