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.
This commit is contained in:
your-name 2025-07-30 19:35:43 +03:00
parent 68a2803b6e
commit da2c3ac4f0
10 changed files with 237 additions and 160 deletions

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:toastification/toastification.dart';
class App extends ConsumerStatefulWidget { class App extends ConsumerStatefulWidget {
const App({super.key}); const App({super.key});
@ -36,27 +37,30 @@ class _InnerApp extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp.router( return ToastificationWrapper(
debugShowCheckedModeBanner: false, child: MaterialApp.router(
title: "BarcodeScan", debugShowCheckedModeBanner: false,
locale: Locale('fr'), title: "BarcodeScan",
supportedLocales: [Locale('fr', 'FR'), Locale('en', 'US')], locale: Locale('fr'),
localizationsDelegates: [ supportedLocales: [Locale('fr', 'FR'), Locale('en', 'US')],
GlobalMaterialLocalizations.delegate, // Support for Material widgets localizationsDelegates: [
GlobalWidgetsLocalizations.delegate, // Localization for widgets GlobalMaterialLocalizations.delegate, // Support for Material widgets
GlobalCupertinoLocalizations.delegate, // Support for Cupertino widgets GlobalWidgetsLocalizations.delegate, // Localization for widgets
], GlobalCupertinoLocalizations
routerConfig: ref.watch(routerProvider), .delegate, // Support for Cupertino widgets
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),
], ],
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,
); );
} }
} }

View File

@ -6,3 +6,4 @@ export 'outline_button_component.dart';
export 'quick_action_component.dart'; export 'quick_action_component.dart';
export 'main_appbar_component.dart'; export 'main_appbar_component.dart';
export 'stock_picking_card_component.dart'; export 'stock_picking_card_component.dart';
export 'flash_button_component.dart';

View File

@ -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();
},
),
],
),
);
}
}

View File

@ -80,7 +80,7 @@ class _ProductScannedComponentState extends State<ProductScannedComponent> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('Code scanné:'), Text('Code scanné:'),
Text(widget.productStruct.id.toString()), Text(widget.productStruct.barcode.toString()),
], ],
), ),
Divider( Divider(

View File

@ -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/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/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -20,16 +20,11 @@ class ReceptionDetailsPageModel
Future getReceptionById({required int id}) async { Future getReceptionById({required int id}) async {
try { try {
final stockPickingRecords = objectboxManager.store
.box<StockPickingRecordEntity>();
state = state.copyWith(loading: true); state = state.copyWith(loading: true);
final res = await ApiCalls.getStockPikingById(id: id); final entity = stockPickingRecords.get(id);
res.when( state = state.copyWith(loading: false, reception: entity);
(data) {
state = state.copyWith(loading: false, reception: data);
},
(error) {
state = state.copyWith(loading: false);
},
);
} catch (e) { } catch (e) {
state = state.copyWith(loading: false); state = state.copyWith(loading: false);
} }

View File

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:e_scan/components/loading_progress_component.dart'; import 'package:e_scan/components/components.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/pages/operation/reception/reception_scan_page_model.dart';
import 'package:e_scan/themes/app_theme.dart'; import 'package:e_scan/themes/app_theme.dart';
import 'package:e_scan/utils/utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -26,7 +26,6 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
StreamSubscription<Object?>? _subscription; StreamSubscription<Object?>? _subscription;
bool qrcodeFound = false; bool qrcodeFound = false;
bool loading = false;
@override @override
void initState() { void initState() {
@ -102,17 +101,10 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
debugPrint("Qrcode non valide"); debugPrint("Qrcode non valide");
return; return;
} }
setState(() {
loading = true;
});
// find product in local database // find product in local database
final isProductExist = model.isProductExist(barcode: qrcodeValue); final isProductExist = model.isProductExist(barcode: qrcodeValue);
if (isProductExist) { if (isProductExist) {
final product = model.getProduct(barcode: qrcodeValue); final product = model.getProduct(barcode: qrcodeValue);
setState(() {
loading = false;
});
//show dialog //show dialog
await showDialog( await showDialog(
barrierDismissible: false, barrierDismissible: false,
@ -154,29 +146,21 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
}, },
); );
} else { } else {
setState(() {
loading = false;
});
debugPrint('Aucun produit trouvé.'); debugPrint('Aucun produit trouvé.');
} }
} }
} }
Future<void> _fakeBarcode() async { Future<void> _fakeBarcode() async {
final qrcodeValue = "0737628064502"; final qrcodeValue = "AIRF0001";
qrcodeFound = true; qrcodeFound = true;
final model = ref.read(receptionScanPageModelProvider.notifier); final model = ref.read(receptionScanPageModelProvider.notifier);
mobileScannerController.stop(); mobileScannerController.stop();
setState(() {
loading = true;
});
// find product in local database // find product in local database
final isProductExist = model.isProductExist(barcode: qrcodeValue); final isProductExist = model.isProductExist(barcode: qrcodeValue);
if (isProductExist) { if (isProductExist) {
final product = model.getProduct(barcode: qrcodeValue); final product = model.getProduct(barcode: qrcodeValue);
setState(() { model.incrementMoveLineQuantity(barcode: qrcodeValue);
loading = false;
});
//show dialog //show dialog
await showDialog( await showDialog(
barrierDismissible: false, barrierDismissible: false,
@ -218,9 +202,7 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
}, },
); );
} else { } else {
setState(() { Toast.showError('Aucun produit trouvé.');
loading = false;
});
debugPrint('Aucun produit trouvé.'); debugPrint('Aucun produit trouvé.');
} }
} }
@ -262,7 +244,7 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
centerTitle: true, centerTitle: true,
elevation: 0.0, elevation: 0.0,
), ),
body: loading || state.loading body: state.loading
? Center(child: LoadingProgressComponent()) ? Center(child: LoadingProgressComponent())
: SizedBox( : SizedBox(
width: double.infinity, width: double.infinity,
@ -303,110 +285,16 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Padding( _InfoTextScan(),
padding: EdgeInsetsDirectional.fromSTEB( _ZoneScanBox(
24.0, onTap: () {
40.0, _fakeBarcode();
24.0, qrcodeFound = false;
0.0, mobileScannerController.start();
), },
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( FlashButtonComponent(
alignment: AlignmentDirectional(0.0, 0.0), mobileScannerController: mobileScannerController,
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();
},
),
],
),
), ),
], ],
), ),
@ -418,3 +306,78 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
); );
} }
} }
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),
),
),
],
),
);
}
}

View File

@ -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),
);
}
}

View File

@ -1,6 +1,8 @@
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
export 'toastification.dart';
extension StatefulWidgetExtensions on State<StatefulWidget> { extension StatefulWidgetExtensions on State<StatefulWidget> {
/// Check if the widget exist before safely setting state. /// Check if the widget exist before safely setting state.
void safeSetState(VoidCallback fn) { void safeSetState(VoidCallback fn) {

View File

@ -233,6 +233,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
equatable:
dependency: transitive
description:
name: equatable
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
url: "https://pub.dev"
source: hosted
version: "2.0.7"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@ -401,10 +409,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: "02ff498f6279470ff7f60c998a69b872f26696ceec237c8402e63a2133868ddf" sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.2.3" version: "16.0.0"
go_router_builder: go_router_builder:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -453,6 +461,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" 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: image:
dependency: transitive dependency: transitive
description: description:
@ -685,6 +701,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" 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: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -866,6 +890,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -930,6 +962,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.2" 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: typed_data:
dependency: transitive dependency: transitive
description: description:
@ -938,6 +978,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View File

@ -37,7 +37,7 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
mobile_scanner: ^7.0.1 mobile_scanner: ^7.0.1
go_router: ^15.2.3 go_router: ^16.0.0
flutter_riverpod: ^2.6.1 flutter_riverpod: ^2.6.1
flutter_launcher_icons: ^0.14.4 flutter_launcher_icons: ^0.14.4
freezed_annotation: ^3.0.0 freezed_annotation: ^3.0.0
@ -55,6 +55,7 @@ dependencies:
path_provider: ^2.1.5 path_provider: ^2.1.5
path: ^1.9.1 path: ^1.9.1
multiple_result: ^5.1.0 multiple_result: ^5.1.0
toastification: ^3.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: