feat: scan book to add to sell
This commit is contained in:
parent
3a013c829f
commit
07c7c98edb
11 changed files with 238 additions and 31 deletions
|
|
@ -55,6 +55,13 @@ class BalRepository {
|
||||||
false;
|
false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Bal?> ongoingBal() async {
|
||||||
|
if (_bals == null) {
|
||||||
|
await _getBalsNoCache();
|
||||||
|
}
|
||||||
|
return _bals!.where((bal) => bal.state == BalState.ongoing).firstOrNull;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Result<Bal>> stopBal(int id) async {
|
Future<Result<Bal>> stopBal(int id) async {
|
||||||
final result = await _apiClient.stopBal(id);
|
final result = await _apiClient.stopBal(id);
|
||||||
_getBalsNoCache();
|
_getBalsNoCache();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,10 @@ class BookInstanceRepository {
|
||||||
|
|
||||||
final ApiClient _apiClient;
|
final ApiClient _apiClient;
|
||||||
|
|
||||||
|
Future<Result<List<BookInstance>>> getByEan(int balId, int ean) async {
|
||||||
|
return await _apiClient.getBookInstanceByEAN(balId, ean);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Result<BookInstance>> sendBook(
|
Future<Result<BookInstance>> sendBook(
|
||||||
Book book,
|
Book book,
|
||||||
Owner owner,
|
Owner owner,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ class BookRepository {
|
||||||
final ApiClient _apiClient;
|
final ApiClient _apiClient;
|
||||||
|
|
||||||
Future<Result<Book>> getBookByEAN(String ean) async {
|
Future<Result<Book>> getBookByEAN(String ean) async {
|
||||||
return _apiClient.getBookByEAN(ean);
|
return await _apiClient.getBookByEAN(ean);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Result<Book>> getBookById(int id) async {
|
||||||
|
return await _apiClient.getBookById(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,18 @@ class OwnerRepository {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Result<Owner>> 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].
|
/// Adds an [Owner] to the database, and gets the resulting [Owner].
|
||||||
Future<Result<Owner>> addOwner(
|
Future<Result<Owner>> addOwner(
|
||||||
String firstName,
|
String firstName,
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,27 @@ class ApiClient {
|
||||||
* ===================
|
* ===================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
Future<Result<Book>> 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<Result<Book>> getBookByEAN(String ean) async {
|
Future<Result<Book>> getBookByEAN(String ean) async {
|
||||||
final client = Client();
|
final client = Client();
|
||||||
try {
|
try {
|
||||||
|
|
@ -263,6 +284,32 @@ class ApiClient {
|
||||||
* =============================
|
* =============================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
Future<Result<List<BookInstance>>> 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<dynamic>;
|
||||||
|
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<Result<BookInstance>> sendBook(
|
Future<Result<BookInstance>> sendBook(
|
||||||
Book book,
|
Book book,
|
||||||
Owner owner,
|
Owner owner,
|
||||||
|
|
@ -304,6 +351,27 @@ class ApiClient {
|
||||||
* ====================
|
* ====================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
Future<Result<Owner>> 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<Result<Owner>> getSectionOwner() async {
|
Future<Result<Owner>> getSectionOwner() async {
|
||||||
final client = Client();
|
final client = Client();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
15
lib/domain/models/book_stack.dart
Normal file
15
lib/domain/models/book_stack.dart
Normal file
|
|
@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -80,7 +80,12 @@ GoRouter router(AuthRepository authRepository) => GoRouter(
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: Routes.sell,
|
path: Routes.sell,
|
||||||
pageBuilder: (context, state) {
|
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));
|
return NoTransitionPage(child: SellPage(viewModel: viewModel));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,34 @@
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||||
import 'package:seshat/data/repositories/bal_repository.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/bal.dart';
|
||||||
|
import 'package:seshat/domain/models/book.dart';
|
||||||
import 'package:seshat/domain/models/book_instance.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/command.dart';
|
||||||
import 'package:seshat/utils/result.dart';
|
import 'package:seshat/utils/result.dart';
|
||||||
|
|
||||||
class SellViewModel extends ChangeNotifier {
|
class SellViewModel extends ChangeNotifier {
|
||||||
SellViewModel({required BalRepository balRepository})
|
SellViewModel({
|
||||||
: _balRepository = balRepository {
|
required BalRepository balRepository,
|
||||||
|
required BookInstanceRepository bookInstanceRepository,
|
||||||
|
required BookRepository bookRepository,
|
||||||
|
required OwnerRepository ownerRepository,
|
||||||
|
}) : _balRepository = balRepository,
|
||||||
|
_bookInstanceRepository = bookInstanceRepository,
|
||||||
|
_bookRepository = bookRepository,
|
||||||
|
_ownerRepository = ownerRepository {
|
||||||
load = Command0(_load)..execute();
|
load = Command0(_load)..execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
final BalRepository _balRepository;
|
final BalRepository _balRepository;
|
||||||
|
final BookInstanceRepository _bookInstanceRepository;
|
||||||
|
final BookRepository _bookRepository;
|
||||||
|
final OwnerRepository _ownerRepository;
|
||||||
|
|
||||||
bool _showScan = false;
|
bool _showScan = false;
|
||||||
bool get showScan => _showScan;
|
bool get showScan => _showScan;
|
||||||
|
|
@ -27,28 +43,26 @@ class SellViewModel extends ChangeNotifier {
|
||||||
* ===============================
|
* ===============================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
final List<BookInstance> _scannedBooks = [];
|
final List<BookStack> _soldBooks = [];
|
||||||
List<BookInstance> get scannedBooks => _scannedBooks;
|
List<BookStack> get soldBooks => _soldBooks;
|
||||||
void scanBook(BarcodeCapture barcode) {
|
|
||||||
final addedBook = BookInstance(
|
final List<BookStack> _scannedBooks = [];
|
||||||
balId: 5,
|
List<BookStack> get scannedBooks => _scannedBooks;
|
||||||
bookId: 5,
|
|
||||||
id: _scannedBooks.length,
|
bool isScanLoaded = false;
|
||||||
ownerId: 5,
|
|
||||||
price: 5,
|
void sellBook(BookStack addedBook) {
|
||||||
available: true,
|
_soldBooks.add(addedBook);
|
||||||
);
|
|
||||||
_scannedBooks.add(addedBook);
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendSell() {
|
void sendSell() {
|
||||||
_scannedBooks.clear();
|
_soldBooks.clear();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteBook(int id) {
|
void deleteBook(int id) {
|
||||||
_scannedBooks.removeWhere((book) => book.id == id);
|
_soldBooks.removeWhere((book) => book.instance.id == id);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,6 +75,11 @@ class SellViewModel extends ChangeNotifier {
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case Ok():
|
case Ok():
|
||||||
for (BookInstance instance in result.value) {
|
for (BookInstance instance in result.value) {
|
||||||
|
if (_soldBooks
|
||||||
|
.where((book) => book.instance.id == instance.id)
|
||||||
|
.isNotEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
Book book;
|
Book book;
|
||||||
final result2 = await _bookRepository.getBookById(instance.bookId);
|
final result2 = await _bookRepository.getBookById(instance.bookId);
|
||||||
switch (result2) {
|
switch (result2) {
|
||||||
|
|
@ -79,9 +98,7 @@ class SellViewModel extends ChangeNotifier {
|
||||||
case Error():
|
case Error():
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_scannedBooks.add(
|
_scannedBooks.add(BookStack(book, instance, owner));
|
||||||
BookStack(instance: instance, book: book, owner: owner),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Error():
|
case Error():
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:mobile_scanner/mobile_scanner.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/view_model/sell_view_model.dart';
|
||||||
import 'package:seshat/ui/sell_page/widgets/manual_scan_popup.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 {
|
class ScanScreen extends StatefulWidget {
|
||||||
const ScanScreen({super.key, required this.viewModel});
|
const ScanScreen({super.key, required this.viewModel});
|
||||||
|
|
@ -30,12 +31,23 @@ class _ScanScreenState extends State<ScanScreen> {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
ColoredBox(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.sizeOf(context).width,
|
||||||
|
height: MediaQuery.sizeOf(context).height,
|
||||||
|
),
|
||||||
|
),
|
||||||
MobileScanner(
|
MobileScanner(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onDetect: (barcodes) async {
|
onDetect: (barcodes) async {
|
||||||
widget.viewModel.showScan = false;
|
controller.stop();
|
||||||
widget.viewModel.scanBook(barcodes);
|
showDialog(
|
||||||
controller.dispose();
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
SellChoicePopup(viewModel: widget.viewModel),
|
||||||
|
);
|
||||||
|
await widget.viewModel.scanBook(barcodes);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SafeArea(
|
SafeArea(
|
||||||
|
|
|
||||||
64
lib/ui/sell_page/widgets/sell_choice_popup.dart
Normal file
64
lib/ui/sell_page/widgets/sell_choice_popup.dart
Normal file
|
|
@ -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()})",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.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/routing/routes.dart';
|
||||||
import 'package:seshat/ui/core/ui/navigation_bar.dart';
|
import 'package:seshat/ui/core/ui/navigation_bar.dart';
|
||||||
import 'package:seshat/ui/sell_page/view_model/sell_view_model.dart';
|
import 'package:seshat/ui/sell_page/view_model/sell_view_model.dart';
|
||||||
|
|
@ -75,8 +75,7 @@ class _SellPageState extends State<SellPage> {
|
||||||
(widget.viewModel.scannedBooks.isEmpty)
|
(widget.viewModel.scannedBooks.isEmpty)
|
||||||
? Center(child: Text("Aucun"))
|
? Center(child: Text("Aucun"))
|
||||||
: SizedBox(),
|
: SizedBox(),
|
||||||
for (BookInstance bookInstance
|
for (BookStack book in widget.viewModel.soldBooks)
|
||||||
in widget.viewModel.scannedBooks)
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 15,
|
horizontal: 15,
|
||||||
|
|
@ -84,19 +83,19 @@ class _SellPageState extends State<SellPage> {
|
||||||
child: Card(
|
child: Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Text(
|
leading: Text(
|
||||||
"${bookInstance.price.toString()}€",
|
"${book.instance.price.toString()}€",
|
||||||
style: TextStyle(fontSize: 30),
|
style: TextStyle(fontSize: 30),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
"Les chiens et la charrue · Patrick K. Dewdney ${bookInstance.id}",
|
"${book.book.title} · ${book.book.author}",
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
"Union Étudiante Auvergne",
|
"${book.owner.firstName} ${book.owner.lastName} (${book.shortId()})",
|
||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.viewModel.deleteBook(
|
widget.viewModel.deleteBook(
|
||||||
bookInstance.id,
|
book.instance.id,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.delete),
|
icon: Icon(Icons.delete),
|
||||||
|
|
|
||||||
Reference in a new issue