From 07c7c98edba8d1fe1cf1933c91030b41e98e0a10 Mon Sep 17 00:00:00 2001 From: alzalia1 Date: Fri, 15 Aug 2025 12:51:36 +0200 Subject: [PATCH] feat: scan book to add to sell --- lib/data/repositories/bal_repository.dart | 7 ++ .../book_instance_repository.dart | 4 ++ lib/data/repositories/book_repository.dart | 6 +- lib/data/repositories/owner_repository.dart | 12 ++++ lib/data/services/api_client.dart | 68 +++++++++++++++++++ lib/domain/models/book_stack.dart | 15 ++++ lib/routing/router.dart | 7 +- .../sell_page/view_model/sell_view_model.dart | 55 +++++++++------ lib/ui/sell_page/widgets/scan_screen.dart | 18 ++++- .../sell_page/widgets/sell_choice_popup.dart | 64 +++++++++++++++++ lib/ui/sell_page/widgets/sell_page.dart | 13 ++-- 11 files changed, 238 insertions(+), 31 deletions(-) create mode 100644 lib/domain/models/book_stack.dart create mode 100644 lib/ui/sell_page/widgets/sell_choice_popup.dart diff --git a/lib/data/repositories/bal_repository.dart b/lib/data/repositories/bal_repository.dart index 4a1f2dc..69da9b7 100644 --- a/lib/data/repositories/bal_repository.dart +++ b/lib/data/repositories/bal_repository.dart @@ -55,6 +55,13 @@ class BalRepository { false; } + Future ongoingBal() async { + if (_bals == null) { + await _getBalsNoCache(); + } + return _bals!.where((bal) => bal.state == BalState.ongoing).firstOrNull; + } + Future> stopBal(int id) async { final result = await _apiClient.stopBal(id); _getBalsNoCache(); diff --git a/lib/data/repositories/book_instance_repository.dart b/lib/data/repositories/book_instance_repository.dart index 66304a5..d59bf32 100644 --- a/lib/data/repositories/book_instance_repository.dart +++ b/lib/data/repositories/book_instance_repository.dart @@ -11,6 +11,10 @@ class BookInstanceRepository { final ApiClient _apiClient; + Future>> getByEan(int balId, int ean) async { + return await _apiClient.getBookInstanceByEAN(balId, ean); + } + Future> sendBook( Book book, Owner owner, diff --git a/lib/data/repositories/book_repository.dart b/lib/data/repositories/book_repository.dart index 7358be0..984d85b 100644 --- a/lib/data/repositories/book_repository.dart +++ b/lib/data/repositories/book_repository.dart @@ -8,6 +8,10 @@ class BookRepository { final ApiClient _apiClient; Future> getBookByEAN(String ean) async { - return _apiClient.getBookByEAN(ean); + return await _apiClient.getBookByEAN(ean); + } + + Future> getBookById(int id) async { + return await _apiClient.getBookById(id); } } diff --git a/lib/data/repositories/owner_repository.dart b/lib/data/repositories/owner_repository.dart index 80a6dfc..5020123 100644 --- a/lib/data/repositories/owner_repository.dart +++ b/lib/data/repositories/owner_repository.dart @@ -33,6 +33,18 @@ class OwnerRepository { return result; } + Future> getOwnerById(int id) async { + if (_cachedOwners != null) { + final result1 = _cachedOwners! + .where((owner) => owner.id == id) + .firstOrNull; + if (result1 != null) { + return Result.ok(result1); + } + } + return await _apiClient.getOwnerById(id); + } + /// Adds an [Owner] to the database, and gets the resulting [Owner]. Future> addOwner( String firstName, diff --git a/lib/data/services/api_client.dart b/lib/data/services/api_client.dart index 668a64e..3ae3708 100644 --- a/lib/data/services/api_client.dart +++ b/lib/data/services/api_client.dart @@ -236,6 +236,27 @@ class ApiClient { * =================== */ + Future> getBookById(int id) async { + final client = Client(); + try { + final headers = await _getHeaders(); + final response = await client.get( + Uri.parse("https://$apiBasePath/book/id/${id.toString()}"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + return Result.ok(Book.fromJSON(json)); + } else { + throw Exception("The book was not found"); + } + } catch (e) { + return Result.error(Exception("API $e")); + } finally { + client.close(); + } + } + Future> getBookByEAN(String ean) async { final client = Client(); try { @@ -263,6 +284,32 @@ class ApiClient { * ============================= */ + Future>> getBookInstanceByEAN( + int balId, + int ean, + ) async { + final client = Client(); + try { + final headers = await _getHeaders(); + final response = await client.get( + Uri.parse( + "https://$apiBasePath/bal/${balId.toString()}/ean/${ean.toString()}/book_instances", + ), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body) as List; + return Result.ok(json.map((el) => BookInstance.fromJSON(el)).toList()); + } else { + throw "Unknown Error"; + } + } catch (e) { + return Result.error(Exception("API $e")); + } finally { + client.close(); + } + } + Future> sendBook( Book book, Owner owner, @@ -304,6 +351,27 @@ class ApiClient { * ==================== */ + Future> getOwnerById(int id) async { + final client = Client(); + try { + final headers = await _getHeaders(); + final response = await client.get( + Uri.parse("https://$apiBasePath/owner/${id.toString()}"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + return Result.ok(Owner.fromJSON(json)); + } else { + throw Exception("The owner was not found"); + } + } catch (e) { + return Result.error(Exception("API $e")); + } finally { + client.close(); + } + } + Future> getSectionOwner() async { final client = Client(); try { diff --git a/lib/domain/models/book_stack.dart b/lib/domain/models/book_stack.dart new file mode 100644 index 0000000..39b8050 --- /dev/null +++ b/lib/domain/models/book_stack.dart @@ -0,0 +1,15 @@ +import 'package:seshat/domain/models/book.dart'; +import 'package:seshat/domain/models/book_instance.dart'; +import 'package:seshat/domain/models/owner.dart'; + +class BookStack { + BookStack(this.book, this.instance, this.owner); + + Book book; + BookInstance instance; + Owner owner; + + String shortId() { + return "${owner.firstName[0].toUpperCase()}${owner.lastName[0].toUpperCase()}${instance.price}"; + } +} diff --git a/lib/routing/router.dart b/lib/routing/router.dart index 3fa53d3..434c333 100644 --- a/lib/routing/router.dart +++ b/lib/routing/router.dart @@ -80,7 +80,12 @@ GoRouter router(AuthRepository authRepository) => GoRouter( GoRoute( path: Routes.sell, pageBuilder: (context, state) { - final viewModel = SellViewModel(balRepository: context.read()); + final viewModel = SellViewModel( + balRepository: context.read(), + bookInstanceRepository: context.read(), + bookRepository: context.read(), + ownerRepository: context.read(), + ); return NoTransitionPage(child: SellPage(viewModel: viewModel)); }, ), 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 b07ba9a..717aacb 100644 --- a/lib/ui/sell_page/view_model/sell_view_model.dart +++ b/lib/ui/sell_page/view_model/sell_view_model.dart @@ -1,18 +1,34 @@ import 'package:flutter/widgets.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:seshat/data/repositories/bal_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/domain/models/bal.dart'; +import 'package:seshat/domain/models/book.dart'; import 'package:seshat/domain/models/book_instance.dart'; +import 'package:seshat/domain/models/book_stack.dart'; +import 'package:seshat/domain/models/owner.dart'; import 'package:seshat/utils/command.dart'; import 'package:seshat/utils/result.dart'; class SellViewModel extends ChangeNotifier { - SellViewModel({required BalRepository balRepository}) - : _balRepository = balRepository { + SellViewModel({ + required BalRepository balRepository, + required BookInstanceRepository bookInstanceRepository, + required BookRepository bookRepository, + required OwnerRepository ownerRepository, + }) : _balRepository = balRepository, + _bookInstanceRepository = bookInstanceRepository, + _bookRepository = bookRepository, + _ownerRepository = ownerRepository { load = Command0(_load)..execute(); } final BalRepository _balRepository; + final BookInstanceRepository _bookInstanceRepository; + final BookRepository _bookRepository; + final OwnerRepository _ownerRepository; bool _showScan = false; bool get showScan => _showScan; @@ -27,28 +43,26 @@ class SellViewModel extends ChangeNotifier { * =============================== */ - final List _scannedBooks = []; - List get scannedBooks => _scannedBooks; - void scanBook(BarcodeCapture barcode) { - final addedBook = BookInstance( - balId: 5, - bookId: 5, - id: _scannedBooks.length, - ownerId: 5, - price: 5, - available: true, - ); - _scannedBooks.add(addedBook); + final List _soldBooks = []; + List get soldBooks => _soldBooks; + + final List _scannedBooks = []; + List get scannedBooks => _scannedBooks; + + bool isScanLoaded = false; + + void sellBook(BookStack addedBook) { + _soldBooks.add(addedBook); notifyListeners(); } void sendSell() { - _scannedBooks.clear(); + _soldBooks.clear(); notifyListeners(); } void deleteBook(int id) { - _scannedBooks.removeWhere((book) => book.id == id); + _soldBooks.removeWhere((book) => book.instance.id == id); notifyListeners(); } @@ -61,6 +75,11 @@ class SellViewModel extends ChangeNotifier { switch (result) { case Ok(): for (BookInstance instance in result.value) { + if (_soldBooks + .where((book) => book.instance.id == instance.id) + .isNotEmpty) { + continue; + } Book book; final result2 = await _bookRepository.getBookById(instance.bookId); switch (result2) { @@ -79,9 +98,7 @@ class SellViewModel extends ChangeNotifier { case Error(): continue; } - _scannedBooks.add( - BookStack(instance: instance, book: book, owner: owner), - ); + _scannedBooks.add(BookStack(book, instance, owner)); } break; case Error(): diff --git a/lib/ui/sell_page/widgets/scan_screen.dart b/lib/ui/sell_page/widgets/scan_screen.dart index 93bbc49..b8dfada 100644 --- a/lib/ui/sell_page/widgets/scan_screen.dart +++ b/lib/ui/sell_page/widgets/scan_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:seshat/ui/sell_page/view_model/sell_view_model.dart'; import 'package:seshat/ui/sell_page/widgets/manual_scan_popup.dart'; +import 'package:seshat/ui/sell_page/widgets/sell_choice_popup.dart'; class ScanScreen extends StatefulWidget { const ScanScreen({super.key, required this.viewModel}); @@ -30,12 +31,23 @@ class _ScanScreenState extends State { final theme = Theme.of(context); return Stack( children: [ + ColoredBox( + color: Theme.of(context).scaffoldBackgroundColor, + child: SizedBox( + width: MediaQuery.sizeOf(context).width, + height: MediaQuery.sizeOf(context).height, + ), + ), MobileScanner( controller: controller, onDetect: (barcodes) async { - widget.viewModel.showScan = false; - widget.viewModel.scanBook(barcodes); - controller.dispose(); + controller.stop(); + showDialog( + context: context, + builder: (context) => + SellChoicePopup(viewModel: widget.viewModel), + ); + await widget.viewModel.scanBook(barcodes); }, ), SafeArea( diff --git a/lib/ui/sell_page/widgets/sell_choice_popup.dart b/lib/ui/sell_page/widgets/sell_choice_popup.dart new file mode 100644 index 0000000..0a353d3 --- /dev/null +++ b/lib/ui/sell_page/widgets/sell_choice_popup.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:seshat/domain/models/book_stack.dart'; +import 'package:seshat/ui/sell_page/view_model/sell_view_model.dart'; + +class SellChoicePopup extends StatelessWidget { + const SellChoicePopup({super.key, required this.viewModel}); + + final SellViewModel viewModel; + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: viewModel, + builder: (context, child) { + return AlertDialog( + title: Text("Choix du bon livre"), + content: switch (viewModel.isScanLoaded) { + false => SizedBox( + height: 300, + child: Center(child: CircularProgressIndicator()), + ), + true => SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + (viewModel.scannedBooks.isEmpty) + ? Text( + "Ce livre n'a jamais été rentré, ou vous l'avez déjà mis dans cette vente.", + ) + : SizedBox(), + for (BookStack book in viewModel.scannedBooks) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Card( + child: InkWell( + onTap: () { + viewModel.sellBook(book); + Navigator.of(context).pop(); + viewModel.showScan = false; + }, + child: ListTile( + leading: Text( + "${book.instance.price.toString()}€", + style: TextStyle(fontSize: 30), + ), + title: Text( + "${book.book.title} · ${book.book.author}", + ), + subtitle: Text( + "${book.owner.firstName} ${book.owner.lastName} (${book.shortId()})", + ), + ), + ), + ), + ), + ], + ), + ), + }, + ); + }, + ); + } +} diff --git a/lib/ui/sell_page/widgets/sell_page.dart b/lib/ui/sell_page/widgets/sell_page.dart index fdc5c07..1cef8bb 100644 --- a/lib/ui/sell_page/widgets/sell_page.dart +++ b/lib/ui/sell_page/widgets/sell_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:seshat/domain/models/book_instance.dart'; +import 'package:seshat/domain/models/book_stack.dart'; import 'package:seshat/routing/routes.dart'; import 'package:seshat/ui/core/ui/navigation_bar.dart'; import 'package:seshat/ui/sell_page/view_model/sell_view_model.dart'; @@ -75,8 +75,7 @@ class _SellPageState extends State { (widget.viewModel.scannedBooks.isEmpty) ? Center(child: Text("Aucun")) : SizedBox(), - for (BookInstance bookInstance - in widget.viewModel.scannedBooks) + for (BookStack book in widget.viewModel.soldBooks) Padding( padding: const EdgeInsets.symmetric( horizontal: 15, @@ -84,19 +83,19 @@ class _SellPageState extends State { child: Card( child: ListTile( leading: Text( - "${bookInstance.price.toString()}€", + "${book.instance.price.toString()}€", style: TextStyle(fontSize: 30), ), title: Text( - "Les chiens et la charrue · Patrick K. Dewdney ${bookInstance.id}", + "${book.book.title} · ${book.book.author}", ), subtitle: Text( - "Union Étudiante Auvergne", + "${book.owner.firstName} ${book.owner.lastName} (${book.shortId()})", ), trailing: IconButton( onPressed: () { widget.viewModel.deleteBook( - bookInstance.id, + book.instance.id, ); }, icon: Icon(Icons.delete),