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:
parent
68a2803b6e
commit
da2c3ac4f0
42
lib/app.dart
42
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
39
lib/components/flash_button_component.dart
Normal file
39
lib/components/flash_button_component.dart
Normal 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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
24
lib/utils/toastification.dart
Normal file
24
lib/utils/toastification.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
52
pubspec.lock
52
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:
|
||||
|
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user