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`).
This commit is contained in:
mandreshope 2025-07-04 10:06:13 +03:00
parent 09405b5da7
commit 6c3f2b80b0
13 changed files with 341 additions and 83 deletions

View File

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

View File

@ -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() {

View File

@ -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<ObjectboxManager> create() async {
Directory directory;

View File

@ -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<ProductFormPage> createState() => _ProductFormPageState();
ConsumerState<ProductFormPage> createState() => _ProductFormPageState();
}
class _ProductFormPageState extends State<ProductFormPage> {
class _ProductFormPageState extends ConsumerState<ProductFormPage> {
final _formKey = GlobalKey<FormState>();
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<ProductFormPage> {
@override
Widget build(BuildContext context) {
final state = ref.watch(productFormPageModelProvider);
return Scaffold(
backgroundColor: AppTheme.of(context).primaryBackground,
appBar: AppBar(
@ -58,7 +84,9 @@ class _ProductFormPageState extends State<ProductFormPage> {
backgroundColor: AppTheme.of(context).primaryBackground,
elevation: 0,
),
body: Padding(
body: state.loading
? Center(child: LoadingProgressComponent())
: Padding(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
@ -68,8 +96,9 @@ class _ProductFormPageState extends State<ProductFormPage> {
TextFormField(
controller: name,
decoration: _inputStyle("Nom du produit"),
validator: (value) =>
(value == null || value.isEmpty) ? 'Champ requis' : null,
validator: (value) => (value == null || value.isEmpty)
? 'Champ requis'
: null,
),
const SizedBox(height: 16),
@ -77,8 +106,9 @@ class _ProductFormPageState extends State<ProductFormPage> {
TextFormField(
controller: code,
decoration: _inputStyle("Code produit / Code barre"),
validator: (value) =>
(value == null || value.isEmpty) ? 'Champ requis' : null,
validator: (value) => (value == null || value.isEmpty)
? 'Champ requis'
: null,
),
const SizedBox(height: 16),
@ -92,11 +122,12 @@ class _ProductFormPageState extends State<ProductFormPage> {
/// Prix
TextFormField(
controller: price,
controller: quantity,
decoration: _inputStyle("Quantités"),
keyboardType: TextInputType.number,
validator: (value) =>
(value == null || value.isEmpty) ? 'Champ requis' : null,
validator: (value) => (value == null || value.isEmpty)
? 'Champ requis'
: null,
),
const SizedBox(height: 24),
@ -105,7 +136,9 @@ class _ProductFormPageState extends State<ProductFormPage> {
onPressed: () {
if (_formKey.currentState!.validate()) {
// Traitement ici
debugPrint("Produit : ${name.text}, Code : ${code.text}");
debugPrint(
"Produit : ${name.text}, Code : ${code.text}",
);
HomeRoute().go(context);
}
},

View File

@ -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<ProductFormPageModel, ProductFormPageState>((ref) {
return ProductFormPageModel();
});
class ProductFormPageModel extends StateNotifier<ProductFormPageState> {
/// Constructor initializes the TaskRepository using the provider reference.
ProductFormPageModel() : super(const ProductFormPageState());
final productStore = objectboxManager.store.box<ProductEntity>();
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;
}

View File

@ -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>(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<ProductFormPageState> get copyWith => _$ProductFormPageStateCopyWithImpl<ProductFormPageState>(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

View File

@ -60,7 +60,7 @@ class _ProductListPageState extends ConsumerState<ProductListPage> {
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<ProductListPage> {
),
],
),
trailing: IconButton(
onPressed: () {
ref
.read(productListPageModelProvider.notifier)
.deleteProductLocal(product.id);
},
icon: Icon(
Icons.delete,
color: AppTheme.of(context).error,
),
),
);
},
);

View File

@ -17,13 +17,18 @@ class ProductListPageModel extends StateNotifier<ProductListPageState> {
/// Constructor initializes the TaskRepository using the provider reference.
ProductListPageModel() : super(const ProductListPageState());
final productStore = objectboxManager.store.box<ProductEntity>();
Future getProductsLocal() async {
Box<ProductEntity> productStore = objectboxManager.store
.box<ProductEntity>();
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

View File

@ -150,7 +150,7 @@ class _ScannerPageState extends ConsumerState<ScannerPage>
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<ScannerPage>
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<ScannerPage>
child: MobileScanner(
controller: mobileScannerController,
onDetectError: (error, stackTrace) {
print("===========> $error");
debugPrint("===========> $error");
},
errorBuilder: (c, p2) {
return Center(child: Icon(Icons.error));

View File

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

View File

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

View File

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

View File

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