From 6c3f2b80b007e5c823b0fa6158e14e29841f87d0 Mon Sep 17 00:00:00 2001 From: mandreshope Date: Fri, 4 Jul 2025 10:06:13 +0300 Subject: [PATCH] feat: Adds product editing and deletion features Enables viewing and editing existing product details by passing the product ID to the form page. The form now loads and displays the product data based on the provided ID. Adds a delete button to each item in the product list, allowing users to remove products directly from the list view. Updates navigation from the product list and scanner pages to pass the product ID when navigating to the product form. Includes minor code style changes by applying the `sort_constructors_first` lint rule and updates dependencies (`flutter_lints`, `lints`). --- analysis_options.yaml | 7 + .../entities/product/product_entity.dart | 19 +-- lib/backend/objectbox/objectbox_manager.dart | 4 +- .../product_form_page/product_form_page.dart | 145 +++++++++------- .../product_form_page_model.dart | 39 +++++ .../product_form_page_model.freezed.dart | 157 ++++++++++++++++++ .../product_list_page/product_list_page.dart | 13 +- .../product_list_page_model.dart | 9 +- lib/pages/scanner_page/scanner_page.dart | 6 +- lib/router/go_secure_router_builder.dart | 6 +- lib/router/go_secure_router_builder.g.dart | 9 +- pubspec.lock | 8 +- pubspec.yaml | 2 +- 13 files changed, 341 insertions(+), 83 deletions(-) create mode 100644 lib/pages/product_form_page/product_form_page_model.dart create mode 100644 lib/pages/product_form_page/product_form_page_model.freezed.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 0d29021..2c06e87 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -23,6 +23,13 @@ linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + - sort_constructors_first +analyzer: + exclude: + - "**/*.g.dart" + - "**/*.freezed.dart" + errors: + invalid_annotation_target: ignore # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/lib/backend/objectbox/entities/product/product_entity.dart b/lib/backend/objectbox/entities/product/product_entity.dart index 3e85c5a..faf2de1 100644 --- a/lib/backend/objectbox/entities/product/product_entity.dart +++ b/lib/backend/objectbox/entities/product/product_entity.dart @@ -4,16 +4,6 @@ import 'package:objectbox/objectbox.dart'; /// Modèle de base de données ObjectBox @Entity() class ProductEntity { - @Id(assignable: true) - int id; - - String? code; - String? name; - String? description; - String? price; - String? quantity; - String? image; - ProductEntity({ this.id = 0, this.code, @@ -23,6 +13,15 @@ class ProductEntity { this.quantity, this.image, }); + @Id(assignable: true) + int id; + + String? code; + String? name; + String? description; + String? price; + String? quantity; + String? image; /// Convertir vers ProductStruct ProductStruct toStruct() { diff --git a/lib/backend/objectbox/objectbox_manager.dart b/lib/backend/objectbox/objectbox_manager.dart index c01c6d7..f3fa472 100644 --- a/lib/backend/objectbox/objectbox_manager.dart +++ b/lib/backend/objectbox/objectbox_manager.dart @@ -8,11 +8,11 @@ export 'package:barcode_scanner/backend/objectbox/objectbox.g.dart'; late ObjectboxManager objectboxManager; class ObjectboxManager { + ObjectboxManager._create(this.store); + /// The Store of this app. late final Store store; - ObjectboxManager._create(this.store); - /// Create an instance of ObjectBox to use throughout the app. static Future create() async { Directory directory; diff --git a/lib/pages/product_form_page/product_form_page.dart b/lib/pages/product_form_page/product_form_page.dart index 37f330b..5c83072 100644 --- a/lib/pages/product_form_page/product_form_page.dart +++ b/lib/pages/product_form_page/product_form_page.dart @@ -1,22 +1,47 @@ +import 'package:barcode_scanner/components/loading_progress_component.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:barcode_scanner/components/primary_button_component.dart'; +import 'package:barcode_scanner/pages/product_form_page/product_form_page_model.dart'; import 'package:barcode_scanner/router/go_secure_router_builder.dart'; import 'package:barcode_scanner/themes/app_theme.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; -class ProductFormPage extends StatefulWidget { - const ProductFormPage({super.key}); +class ProductFormPage extends ConsumerStatefulWidget { + const ProductFormPage({super.key, required this.id}); + final int id; @override - State createState() => _ProductFormPageState(); + ConsumerState createState() => _ProductFormPageState(); } -class _ProductFormPageState extends State { +class _ProductFormPageState extends ConsumerState { final _formKey = GlobalKey(); final TextEditingController name = TextEditingController(); final TextEditingController code = TextEditingController(); final TextEditingController description = TextEditingController(); final TextEditingController price = TextEditingController(); + final TextEditingController quantity = TextEditingController(); + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.addPostFrameCallback((_) { + ref + .read(productFormPageModelProvider.notifier) + .getProductLocal( + id: widget.id, + onSuccess: (value) { + name.text = value?.name ?? ''; + code.text = value?.id.toString() ?? ''; + description.text = value?.description ?? ''; + price.text = value?.price ?? ''; + quantity.text = value?.quantity ?? ''; + }, + ); + }); + } @override void dispose() { @@ -41,6 +66,7 @@ class _ProductFormPageState extends State { @override Widget build(BuildContext context) { + final state = ref.watch(productFormPageModelProvider); return Scaffold( backgroundColor: AppTheme.of(context).primaryBackground, appBar: AppBar( @@ -58,63 +84,70 @@ class _ProductFormPageState extends State { backgroundColor: AppTheme.of(context).primaryBackground, elevation: 0, ), - body: Padding( - padding: const EdgeInsets.all(24), - child: Form( - key: _formKey, - child: ListView( - children: [ - /// Nom du produit - TextFormField( - controller: name, - decoration: _inputStyle("Nom du produit"), - validator: (value) => - (value == null || value.isEmpty) ? 'Champ requis' : null, - ), - const SizedBox(height: 16), + body: state.loading + ? Center(child: LoadingProgressComponent()) + : Padding( + padding: const EdgeInsets.all(24), + child: Form( + key: _formKey, + child: ListView( + children: [ + /// Nom du produit + TextFormField( + controller: name, + decoration: _inputStyle("Nom du produit"), + validator: (value) => (value == null || value.isEmpty) + ? 'Champ requis' + : null, + ), + const SizedBox(height: 16), - /// Code barre / code produit - TextFormField( - controller: code, - decoration: _inputStyle("Code produit / Code barre"), - validator: (value) => - (value == null || value.isEmpty) ? 'Champ requis' : null, - ), - const SizedBox(height: 16), + /// Code barre / code produit + TextFormField( + controller: code, + decoration: _inputStyle("Code produit / Code barre"), + validator: (value) => (value == null || value.isEmpty) + ? 'Champ requis' + : null, + ), + const SizedBox(height: 16), - /// Description - TextFormField( - controller: description, - decoration: _inputStyle("Commentaires"), - maxLines: 3, - ), - const SizedBox(height: 16), + /// Description + TextFormField( + controller: description, + decoration: _inputStyle("Commentaires"), + maxLines: 3, + ), + const SizedBox(height: 16), - /// Prix - TextFormField( - controller: price, - decoration: _inputStyle("Quantités"), - keyboardType: TextInputType.number, - validator: (value) => - (value == null || value.isEmpty) ? 'Champ requis' : null, - ), - const SizedBox(height: 24), + /// Prix + TextFormField( + controller: quantity, + decoration: _inputStyle("Quantités"), + keyboardType: TextInputType.number, + validator: (value) => (value == null || value.isEmpty) + ? 'Champ requis' + : null, + ), + const SizedBox(height: 24), - /// Bouton sauvegarder - PrimaryButtonComponent( - onPressed: () { - if (_formKey.currentState!.validate()) { - // Traitement ici - debugPrint("Produit : ${name.text}, Code : ${code.text}"); - HomeRoute().go(context); - } - }, - text: 'Sauvegarder', + /// Bouton sauvegarder + PrimaryButtonComponent( + onPressed: () { + if (_formKey.currentState!.validate()) { + // Traitement ici + debugPrint( + "Produit : ${name.text}, Code : ${code.text}", + ); + HomeRoute().go(context); + } + }, + text: 'Sauvegarder', + ), + ], + ), ), - ], - ), - ), - ), + ), ); } } diff --git a/lib/pages/product_form_page/product_form_page_model.dart b/lib/pages/product_form_page/product_form_page_model.dart new file mode 100644 index 0000000..9d6b0ff --- /dev/null +++ b/lib/pages/product_form_page/product_form_page_model.dart @@ -0,0 +1,39 @@ +import 'package:barcode_scanner/backend/objectbox/entities/product/product_entity.dart'; +import 'package:barcode_scanner/backend/objectbox/objectbox_manager.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'product_form_page_model.freezed.dart'; + +/// The provider for the AuthViewModel, using Riverpod's StateNotifierProvider +/// with autoDispose to manage the lifecycle of the view model. +final productFormPageModelProvider = + StateNotifierProvider((ref) { + return ProductFormPageModel(); + }); + +class ProductFormPageModel extends StateNotifier { + /// Constructor initializes the TaskRepository using the provider reference. + ProductFormPageModel() : super(const ProductFormPageState()); + + final productStore = objectboxManager.store.box(); + + Future getProductLocal({ + required int id, + Function(ProductEntity? value)? onSuccess, + }) async { + state = state.copyWith(loading: true); + final product = await productStore.getAsync(id); + onSuccess?.call(product); + state = state.copyWith(product: product, loading: false); + } +} + +@freezed +abstract class ProductFormPageState with _$ProductFormPageState { + const factory ProductFormPageState({ + ProductEntity? product, + @Default(false) bool loading, + }) = _ProductFormPageState; +} diff --git a/lib/pages/product_form_page/product_form_page_model.freezed.dart b/lib/pages/product_form_page/product_form_page_model.freezed.dart new file mode 100644 index 0000000..780cf66 --- /dev/null +++ b/lib/pages/product_form_page/product_form_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 'product_form_page_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$ProductFormPageState implements DiagnosticableTreeMixin { + + ProductEntity? get product; bool get loading; +/// Create a copy of ProductFormPageState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ProductFormPageStateCopyWith get copyWith => _$ProductFormPageStateCopyWithImpl(this as ProductFormPageState, _$identity); + + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ProductFormPageState')) + ..add(DiagnosticsProperty('product', product))..add(DiagnosticsProperty('loading', loading)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ProductFormPageState&&(identical(other.product, product) || other.product == product)&&(identical(other.loading, loading) || other.loading == loading)); +} + + +@override +int get hashCode => Object.hash(runtimeType,product,loading); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ProductFormPageState(product: $product, loading: $loading)'; +} + + +} + +/// @nodoc +abstract mixin class $ProductFormPageStateCopyWith<$Res> { + factory $ProductFormPageStateCopyWith(ProductFormPageState value, $Res Function(ProductFormPageState) _then) = _$ProductFormPageStateCopyWithImpl; +@useResult +$Res call({ + ProductEntity? product, bool loading +}); + + + + +} +/// @nodoc +class _$ProductFormPageStateCopyWithImpl<$Res> + implements $ProductFormPageStateCopyWith<$Res> { + _$ProductFormPageStateCopyWithImpl(this._self, this._then); + + final ProductFormPageState _self; + final $Res Function(ProductFormPageState) _then; + +/// Create a copy of ProductFormPageState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? product = freezed,Object? loading = null,}) { + return _then(_self.copyWith( +product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable +as ProductEntity?,loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// @nodoc + + +class _ProductFormPageState with DiagnosticableTreeMixin implements ProductFormPageState { + const _ProductFormPageState({this.product, this.loading = false}); + + +@override final ProductEntity? product; +@override@JsonKey() final bool loading; + +/// Create a copy of ProductFormPageState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ProductFormPageStateCopyWith<_ProductFormPageState> get copyWith => __$ProductFormPageStateCopyWithImpl<_ProductFormPageState>(this, _$identity); + + +@override +void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('type', 'ProductFormPageState')) + ..add(DiagnosticsProperty('product', product))..add(DiagnosticsProperty('loading', loading)); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProductFormPageState&&(identical(other.product, product) || other.product == product)&&(identical(other.loading, loading) || other.loading == loading)); +} + + +@override +int get hashCode => Object.hash(runtimeType,product,loading); + +@override +String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { + return 'ProductFormPageState(product: $product, loading: $loading)'; +} + + +} + +/// @nodoc +abstract mixin class _$ProductFormPageStateCopyWith<$Res> implements $ProductFormPageStateCopyWith<$Res> { + factory _$ProductFormPageStateCopyWith(_ProductFormPageState value, $Res Function(_ProductFormPageState) _then) = __$ProductFormPageStateCopyWithImpl; +@override @useResult +$Res call({ + ProductEntity? product, bool loading +}); + + + + +} +/// @nodoc +class __$ProductFormPageStateCopyWithImpl<$Res> + implements _$ProductFormPageStateCopyWith<$Res> { + __$ProductFormPageStateCopyWithImpl(this._self, this._then); + + final _ProductFormPageState _self; + final $Res Function(_ProductFormPageState) _then; + +/// Create a copy of ProductFormPageState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? product = freezed,Object? loading = null,}) { + return _then(_ProductFormPageState( +product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable +as ProductEntity?,loading: null == loading ? _self.loading : loading // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/lib/pages/product_list_page/product_list_page.dart b/lib/pages/product_list_page/product_list_page.dart index ff32bb4..33a8214 100644 --- a/lib/pages/product_list_page/product_list_page.dart +++ b/lib/pages/product_list_page/product_list_page.dart @@ -60,7 +60,7 @@ class _ProductListPageState extends ConsumerState { final product = state.products[index]; return ListTile( onTap: () { - ProductFormRoute().push(context); + ProductFormRoute(id: product.id).push(context); }, leading: Container( height: 60, @@ -89,6 +89,17 @@ class _ProductListPageState extends ConsumerState { ), ], ), + trailing: IconButton( + onPressed: () { + ref + .read(productListPageModelProvider.notifier) + .deleteProductLocal(product.id); + }, + icon: Icon( + Icons.delete, + color: AppTheme.of(context).error, + ), + ), ); }, ); diff --git a/lib/pages/product_list_page/product_list_page_model.dart b/lib/pages/product_list_page/product_list_page_model.dart index 7099634..7b390db 100644 --- a/lib/pages/product_list_page/product_list_page_model.dart +++ b/lib/pages/product_list_page/product_list_page_model.dart @@ -17,13 +17,18 @@ class ProductListPageModel extends StateNotifier { /// Constructor initializes the TaskRepository using the provider reference. ProductListPageModel() : super(const ProductListPageState()); + final productStore = objectboxManager.store.box(); + Future getProductsLocal() async { - Box productStore = objectboxManager.store - .box(); state = state.copyWith(loading: true); final products = await productStore.getAllAsync(); state = state.copyWith(products: products, loading: false); } + + void deleteProductLocal(int id) { + productStore.remove(id); + getProductsLocal(); + } } @freezed diff --git a/lib/pages/scanner_page/scanner_page.dart b/lib/pages/scanner_page/scanner_page.dart index 171c521..af05cd4 100644 --- a/lib/pages/scanner_page/scanner_page.dart +++ b/lib/pages/scanner_page/scanner_page.dart @@ -150,7 +150,7 @@ class _ScannerPageState extends ConsumerState unawaited(_subscription?.cancel()); _subscription = null; unawaited(mobileScannerController.stop()); - ProductFormRoute().push(context); + ProductFormRoute(id: productStruct.id).push(context); }, ), ), @@ -224,7 +224,7 @@ class _ScannerPageState extends ConsumerState unawaited(_subscription?.cancel()); _subscription = null; unawaited(mobileScannerController.stop()); - ProductFormRoute().push(context); + ProductFormRoute(id: productStruct.id).push(context); }, ), ), @@ -285,7 +285,7 @@ class _ScannerPageState extends ConsumerState child: MobileScanner( controller: mobileScannerController, onDetectError: (error, stackTrace) { - print("===========> $error"); + debugPrint("===========> $error"); }, errorBuilder: (c, p2) { return Center(child: Icon(Icons.error)); diff --git a/lib/router/go_secure_router_builder.dart b/lib/router/go_secure_router_builder.dart index 898571d..1da9010 100644 --- a/lib/router/go_secure_router_builder.dart +++ b/lib/router/go_secure_router_builder.dart @@ -56,10 +56,12 @@ class ScannerRoute extends GoRouteData with _$ScannerRoute { } class ProductFormRoute extends GoRouteData with _$ProductFormRoute { - const ProductFormRoute(); + const ProductFormRoute({required this.id}); + final int id; @override - Widget build(BuildContext context, GoRouterState state) => ProductFormPage(); + Widget build(BuildContext context, GoRouterState state) => + ProductFormPage(id: id); } class ProductListRoute extends GoRouteData with _$ProductListRoute { diff --git a/lib/router/go_secure_router_builder.g.dart b/lib/router/go_secure_router_builder.g.dart index 78f141f..5161d06 100644 --- a/lib/router/go_secure_router_builder.g.dart +++ b/lib/router/go_secure_router_builder.g.dart @@ -90,10 +90,15 @@ mixin _$ScannerRoute on GoRouteData { mixin _$ProductFormRoute on GoRouteData { static ProductFormRoute _fromState(GoRouterState state) => - const ProductFormRoute(); + ProductFormRoute(id: int.parse(state.uri.queryParameters['id']!)!); + + ProductFormRoute get _self => this as ProductFormRoute; @override - String get location => GoRouteData.$location('/SecurePage/ProductFormPage'); + String get location => GoRouteData.$location( + '/SecurePage/ProductFormPage', + queryParams: {'id': _self.id.toString()}, + ); @override void go(BuildContext context) => context.go(location); diff --git a/pubspec.lock b/pubspec.lock index 40cfd72..b318d48 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -290,10 +290,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -529,10 +529,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.0" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 00681e2..e10728d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,7 +67,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec