From 981dce5bfea975c2d3532f520731ff238da244af Mon Sep 17 00:00:00 2001 From: Alzalia Date: Sat, 9 Aug 2025 01:42:22 +0200 Subject: [PATCH] feat: add a book by scanning --- lib/config/dependencies.dart | 6 ++ .../book_instance_repository.dart | 23 +++- lib/data/repositories/book_repository.dart | 14 ++- lib/data/services/api_client.dart | 101 ++++++++++++++++-- lib/data/services/auth_client.dart | 7 -- lib/domain/models/bal.dart | 5 + lib/domain/models/book.dart | 8 ++ lib/domain/models/book_instance.dart | 13 +-- lib/routing/router.dart | 6 +- .../add_page/view_model/add_view_model.dart | 36 +++++-- lib/ui/add_page/widgets/add_page.dart | 2 + .../add_page/widgets/confirmation_popup.dart | 73 +++++++++---- .../sell_page/view_model/sell_view_model.dart | 2 +- lib/utils/overlay_boundary.dart | 27 +++++ 14 files changed, 264 insertions(+), 59 deletions(-) create mode 100644 lib/domain/models/bal.dart create mode 100644 lib/utils/overlay_boundary.dart diff --git a/lib/config/dependencies.dart b/lib/config/dependencies.dart index 95a5787..0f3d249 100644 --- a/lib/config/dependencies.dart +++ b/lib/config/dependencies.dart @@ -1,6 +1,8 @@ import "package:provider/provider.dart"; import "package:provider/single_child_widget.dart"; import "package:seshat/data/repositories/auth_repository.dart"; +import "package:seshat/data/repositories/book_instance_repository.dart"; +import "package:seshat/data/repositories/book_repository.dart"; import "package:seshat/data/repositories/owner_repository.dart"; import "package:seshat/data/services/api_client.dart"; @@ -19,5 +21,9 @@ List get providers { ChangeNotifierProvider( create: (context) => AuthRepository(authClient: context.read()), ), + Provider(create: (context) => BookRepository(apiClient: context.read())), + Provider( + create: (context) => BookInstanceRepository(apiClient: context.read()), + ), ]; } diff --git a/lib/data/repositories/book_instance_repository.dart b/lib/data/repositories/book_instance_repository.dart index a4a8b47..66304a5 100644 --- a/lib/data/repositories/book_instance_repository.dart +++ b/lib/data/repositories/book_instance_repository.dart @@ -1 +1,22 @@ -class BookInstanceRepository {} +import 'package:seshat/data/services/api_client.dart'; +import 'package:seshat/domain/models/bal.dart'; +import 'package:seshat/domain/models/book.dart'; +import 'package:seshat/domain/models/book_instance.dart'; +import 'package:seshat/domain/models/owner.dart'; +import 'package:seshat/utils/result.dart'; + +class BookInstanceRepository { + BookInstanceRepository({required ApiClient apiClient}) + : _apiClient = apiClient; + + final ApiClient _apiClient; + + Future> sendBook( + Book book, + Owner owner, + Bal bal, + double price, + ) async { + return await _apiClient.sendBook(book, owner, bal, price); + } +} diff --git a/lib/data/repositories/book_repository.dart b/lib/data/repositories/book_repository.dart index 0c4efee..7358be0 100644 --- a/lib/data/repositories/book_repository.dart +++ b/lib/data/repositories/book_repository.dart @@ -1 +1,13 @@ -class BookRepository {} +import 'package:seshat/data/services/api_client.dart'; +import 'package:seshat/domain/models/book.dart'; +import 'package:seshat/utils/result.dart'; + +class BookRepository { + BookRepository({required ApiClient apiClient}) : _apiClient = apiClient; + + final ApiClient _apiClient; + + Future> getBookByEAN(String ean) async { + return _apiClient.getBookByEAN(ean); + } +} diff --git a/lib/data/services/api_client.dart b/lib/data/services/api_client.dart index fb6a496..c6ccf82 100644 --- a/lib/data/services/api_client.dart +++ b/lib/data/services/api_client.dart @@ -1,8 +1,12 @@ import 'dart:convert'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:seshat/config/constants.dart'; +import 'package:seshat/domain/models/bal.dart'; +import 'package:seshat/domain/models/book.dart'; +import 'package:seshat/domain/models/book_instance.dart'; import 'package:seshat/domain/models/owner.dart'; import 'package:seshat/utils/command.dart'; import 'package:seshat/utils/result.dart'; @@ -23,18 +27,101 @@ class ApiClient { ); } + Future> _getHeaders([ + Map? additionalHeaders, + ]) async { + await _initStore(); + final token = await _secureStorage!.read(key: "token"); + final headers = {"Authorization": "Bearer $token", ...?additionalHeaders}; + return headers; + } + + /* + * =================== + * =====[ BOOKS ]===== + * =================== +*/ + + Future> getBookByEAN(String ean) async { + final client = Client(); + try { + final headers = await _getHeaders(); + final response = await client.get( + Uri.parse("https://$apiBasePath/book/ean/$ean"), + headers: headers, + ); + debugPrint("\n\n\n\nGOT : ${response.statusCode}\n\n\n\n"); + if (response.statusCode == 200) { + debugPrint("\n\n\n\nWITH : ${response.body}\n\n\n\n"); + final json = jsonDecode(response.body); + return Result.ok(Book.fromJSON(json)); + } else { + debugPrintStack(); + return Result.error(Exception("The book was not found")); + } + } catch (e, stackTrace) { + debugPrintStack(stackTrace: stackTrace); + return Result.error(Exception("API $e")); + } finally { + client.close(); + } + } + + /* + * ============================= + * =====[ BOOKS INSTANCES ]===== + * ============================= +*/ + + Future> sendBook( + Book book, + Owner owner, + Bal bal, + double price, + ) async { + final client = Client(); + try { + final headers = await _getHeaders({"Content-Type": "application/json"}); + final body = jsonEncode({ + "bal_id": bal.id, + "book_id": book.id, + "owner_id": owner.id, + "price": price, + }); + debugPrint("\n\n\n\nSENDING : ${body}\n\n\n\n"); + final response = await client.post( + Uri.parse("https://$apiBasePath/book_instance"), + headers: headers, + body: body, + ); + if (response.statusCode == 201) { + final json = jsonDecode(response.body); + debugPrint("\n\n\n\nRECEIVED : ${json}\n\n\n\n"); + return Result.ok(BookInstance.fromJSON(json)); + } else if (response.statusCode == 403) { + return Result.error(Exception("You don't own that book instance")); + } else { + return Result.error(Exception("Something wrong happened")); + } + } catch (e, stack) { + debugPrintStack(stackTrace: stack); + return Result.error(Exception(e)); + } finally { + client.close(); + } + } + /* * ==================== * =====[ OWNERS ]===== * ==================== */ + /// Call on `/owners` to get a list of all [Owner]s Future>> getOwners() async { final client = Client(); try { - await _initStore(); - final token = await _secureStorage!.read(key: "token"); - final headers = {"Authorization": "Bearer $token"}; + final headers = await _getHeaders(); final response = await client.get( Uri.parse("https://$apiBasePath/owners"), headers: headers, @@ -54,6 +141,7 @@ class ApiClient { } } + /// Adds an owner to the database Future> addOwner( String firstName, String lastName, @@ -61,12 +149,7 @@ class ApiClient { ) async { final client = Client(); try { - await _initStore(); - final token = await _secureStorage!.read(key: "token"); - final headers = { - "Authorization": "Bearer $token", - "Content-Type": "application/json", - }; + final headers = await _getHeaders({"Content-Type": "application/json"}); final body = { "first_name": firstName, "last_name": lastName, diff --git a/lib/data/services/auth_client.dart b/lib/data/services/auth_client.dart index f451d6d..0d68094 100644 --- a/lib/data/services/auth_client.dart +++ b/lib/data/services/auth_client.dart @@ -20,7 +20,6 @@ class AuthClient { try { await _initStore(); bool hasToken = await _secureStorage!.containsKey(key: "token"); - debugPrint("\n\n\n${hasToken == true} => HAS_TOKEN\n\n\n"); if (hasToken) { var token = await _secureStorage!.read(key: "token"); var url = Uri.parse("https://$apiBasePath/token-check"); @@ -29,9 +28,6 @@ class AuthClient { headers: {"Content-Type": "application/json"}, body: jsonEncode({"token": token}), ); - debugPrint( - "\n\n\n${response.body is String} => ${response.body}\n\n\n", - ); if (response.body == "true") { return Result.ok(true); @@ -39,7 +35,6 @@ class AuthClient { } return Result.ok(false); } catch (e) { - debugPrint(e.toString()); return Result.error(Exception(e)); } } @@ -67,8 +62,6 @@ class AuthClient { return Result.error(Exception("Token creation error")); } } catch (e, stackTrace) { - debugPrint(e.toString()); - debugPrintStack(stackTrace: stackTrace); return Result.error(Exception(e)); } finally { client.close(); diff --git a/lib/domain/models/bal.dart b/lib/domain/models/bal.dart new file mode 100644 index 0000000..5b98811 --- /dev/null +++ b/lib/domain/models/bal.dart @@ -0,0 +1,5 @@ +class Bal { + Bal({required this.id}); + + int id; +} diff --git a/lib/domain/models/book.dart b/lib/domain/models/book.dart index 60a0a53..51b2e68 100644 --- a/lib/domain/models/book.dart +++ b/lib/domain/models/book.dart @@ -12,4 +12,12 @@ class Book { String ean; int id; String priceNew; + + factory Book.fromJSON(Map json) => Book( + author: json["author"], + ean: json["ean"], + id: json["id"], + priceNew: json["price_new"], + title: json["title"], + ); } diff --git a/lib/domain/models/book_instance.dart b/lib/domain/models/book_instance.dart index 3c3bde7..bba43ac 100644 --- a/lib/domain/models/book_instance.dart +++ b/lib/domain/models/book_instance.dart @@ -5,7 +5,7 @@ class BookInstance { required this.id, required this.ownerId, required this.price, - required this.status, + required this.available, this.soldPrice, }); @@ -15,14 +15,15 @@ class BookInstance { int ownerId; double price; double? soldPrice; - bool status; + bool available; factory BookInstance.fromJSON(Map json) => BookInstance( - balId: json["balId"], - bookId: json["bookId"], + balId: json["bal_id"], + bookId: json["book_id"], id: json["id"], - ownerId: json["ownerId"], + ownerId: json["owner_id"], price: json["price"], - status: json["status"], + available: json["available"], + soldPrice: json["sold_price"] ?? 0, ); } diff --git a/lib/routing/router.dart b/lib/routing/router.dart index c60e2a7..00cb9a4 100644 --- a/lib/routing/router.dart +++ b/lib/routing/router.dart @@ -35,7 +35,11 @@ GoRouter router(AuthRepository authRepository) => GoRouter( GoRoute( path: Routes.add, pageBuilder: (context, state) { - final viewModel = AddViewModel(ownerRepository: context.read()); + final viewModel = AddViewModel( + ownerRepository: context.read(), + bookRepository: context.read(), + bookInstanceRepository: context.read(), + ); return NoTransitionPage(child: AddPage(viewModel: viewModel)); }, // routes: [ diff --git a/lib/ui/add_page/view_model/add_view_model.dart b/lib/ui/add_page/view_model/add_view_model.dart index 783b27e..c332563 100644 --- a/lib/ui/add_page/view_model/add_view_model.dart +++ b/lib/ui/add_page/view_model/add_view_model.dart @@ -3,19 +3,30 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:seshat/data/repositories/book_instance_repository.dart'; +import 'package:seshat/data/repositories/book_repository.dart'; import 'package:seshat/data/repositories/owner_repository.dart'; +import 'package:seshat/domain/models/bal.dart'; import 'package:seshat/domain/models/book.dart'; +import 'package:seshat/domain/models/book_instance.dart'; import 'package:seshat/domain/models/owner.dart'; import 'package:seshat/utils/command.dart'; import 'package:seshat/utils/result.dart'; class AddViewModel extends ChangeNotifier { - AddViewModel({required OwnerRepository ownerRepository}) - : _ownerRepository = ownerRepository { + AddViewModel({ + required OwnerRepository ownerRepository, + required BookRepository bookRepository, + required BookInstanceRepository bookInstanceRepository, + }) : _ownerRepository = ownerRepository, + _bookRepository = bookRepository, + _bookInstanceRepository = bookInstanceRepository { load = Command0(_load)..execute(); } final OwnerRepository _ownerRepository; + final BookRepository _bookRepository; + final BookInstanceRepository _bookInstanceRepository; late final StreamSubscription sub; /* @@ -87,15 +98,18 @@ class AddViewModel extends ChangeNotifier { /// Sends an api request with a [bacorde], then gets the [Book] that was /// either created or retrieved. Sens the [Book] back wrapped in a [Result]. Future> scanBook(BarcodeCapture barcode) async { - return Result.ok( - Book( - author: "Patrick K. Dewdney", - ean: barcode.barcodes.first.rawValue!, - id: 56, - priceNew: "50 EUR", - title: "Les chiens et la charrue", - ), - ); + var ean = barcode.barcodes.first.rawValue!; + var result = await _bookRepository.getBookByEAN(ean); + return result; + } + + Future> sendBook( + Book book, + Owner owner, + Bal bal, + double price, + ) async { + return await _bookInstanceRepository.sendBook(book, owner, bal, price); } /// Sends an api request with diff --git a/lib/ui/add_page/widgets/add_page.dart b/lib/ui/add_page/widgets/add_page.dart index b74ff70..26789ec 100644 --- a/lib/ui/add_page/widgets/add_page.dart +++ b/lib/ui/add_page/widgets/add_page.dart @@ -75,6 +75,8 @@ class _AddPageState extends State { ); break; case Error(): + debugPrintStack(); + debugPrint(result.error.toString()); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text("Erreur : ${result.error}"), diff --git a/lib/ui/add_page/widgets/confirmation_popup.dart b/lib/ui/add_page/widgets/confirmation_popup.dart index ad8f757..a218f62 100644 --- a/lib/ui/add_page/widgets/confirmation_popup.dart +++ b/lib/ui/add_page/widgets/confirmation_popup.dart @@ -1,6 +1,10 @@ +import 'dart:ffi'; + import 'package:flutter/material.dart'; +import 'package:seshat/domain/models/bal.dart'; import 'package:seshat/domain/models/book.dart'; import 'package:seshat/ui/add_page/view_model/add_view_model.dart'; +import 'package:seshat/utils/result.dart'; class ConfirmationPopup extends StatefulWidget { const ConfirmationPopup({ @@ -22,7 +26,7 @@ class ConfirmationPopup extends StatefulWidget { class _ConfirmationPopupState extends State { final GlobalKey _formKey = GlobalKey(); - num price = 0; + double price = 0; @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -67,7 +71,7 @@ class _ConfirmationPopupState extends State { text: "Prix à neuf : ", style: TextStyle(fontWeight: FontWeight.bold), ), - TextSpan(text: widget.book.priceNew), + TextSpan(text: widget.book.priceNew.replaceAll("EUR", "€")), ], ), ), @@ -89,7 +93,7 @@ class _ConfirmationPopupState extends State { return null; }, onSaved: (newValue) { - price = num.parse(newValue!); + price = double.parse(newValue!); }, ) : SizedBox(), @@ -111,32 +115,57 @@ class _ConfirmationPopupState extends State { child: Text("Annuler"), ), TextButton( - onPressed: () { - switch (widget.viewModel.askPrice) { - case true: - if (_formKey.currentState!.validate()) { - _formKey.currentState!.save(); + onPressed: () async { + var result = await widget.viewModel.sendBook( + widget.book, + widget.viewModel.currentOwner!, + Bal(id: 1), + price, + ); + switch (result) { + case Ok(): + switch (widget.viewModel.askPrice) { + case true: + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "\"${widget.book.title}\" ($price€) a bien été enregistré", + ), + behavior: SnackBarBehavior.floating, + ), + ); + widget.exitPopup(context); + } + } + break; + case false: + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "\"${widget.book.title}\" (PL) a bien été enregistré", + ), + behavior: SnackBarBehavior.floating, + ), + ); + widget.exitPopup(context); + } + } + + break; + case Error(): + if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - "\"${widget.book.title}\" ($price) a bien été enregistré", + "Une erreur est survenue : ${result.error}", ), - behavior: SnackBarBehavior.floating, ), ); - widget.exitPopup(context); } - break; - case false: - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "\"${widget.book.title}\" (PL) a bien été enregistré", - ), - behavior: SnackBarBehavior.floating, - ), - ); - widget.exitPopup(context); } }, child: Text("Valider"), diff --git a/lib/ui/sell_page/view_model/sell_view_model.dart b/lib/ui/sell_page/view_model/sell_view_model.dart index de55e0d..b4dd5ae 100644 --- a/lib/ui/sell_page/view_model/sell_view_model.dart +++ b/lib/ui/sell_page/view_model/sell_view_model.dart @@ -21,7 +21,7 @@ class SellViewModel extends ChangeNotifier { id: _scannedBooks.length, ownerId: 5, price: 5, - status: true, + available: true, ); _scannedBooks.add(addedBook); notifyListeners(); diff --git a/lib/utils/overlay_boundary.dart b/lib/utils/overlay_boundary.dart new file mode 100644 index 0000000..e5005f0 --- /dev/null +++ b/lib/utils/overlay_boundary.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class OverlayBoundary extends StatefulWidget { + const OverlayBoundary({super.key, required this.child}); + + final Widget child; + + @override + State createState() => _OverlayBoundaryState(); +} + +class _OverlayBoundaryState extends State { + late final OverlayEntry _overlayEntry = OverlayEntry( + builder: (context) => widget.child, + ); + + @override + void didUpdateWidget(covariant OverlayBoundary oldWidget) { + super.didUpdateWidget(oldWidget); + _overlayEntry.markNeedsBuild(); + } + + @override + Widget build(BuildContext context) { + return Overlay(initialEntries: [_overlayEntry]); + } +}