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

View File

@ -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';

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,
children: [
Text('Code scanné:'),
Text(widget.productStruct.id.toString()),
Text(widget.productStruct.barcode.toString()),
],
),
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/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<StockPickingRecordEntity>();
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);
}

View File

@ -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<ReceptionScanPage>
StreamSubscription<Object?>? _subscription;
bool qrcodeFound = false;
bool loading = false;
@override
void initState() {
@ -102,17 +101,10 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
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<ReceptionScanPage>
},
);
} else {
setState(() {
loading = false;
});
debugPrint('Aucun produit trouvé.');
}
}
}
Future<void> _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<ReceptionScanPage>
},
);
} else {
setState(() {
loading = false;
});
Toast.showError('Aucun produit trouvé.');
debugPrint('Aucun produit trouvé.');
}
}
@ -262,7 +244,7 @@ class _ReceptionScanPageState extends ConsumerState<ReceptionScanPage>
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<ReceptionScanPage>
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<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:flutter/material.dart';
export 'toastification.dart';
extension StatefulWidgetExtensions on State<StatefulWidget> {
/// Check if the widget exist before safely setting state.
void safeSetState(VoidCallback fn) {

View File

@ -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:

View File

@ -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: