feat: scan book to add to sell

This commit is contained in:
alzalia1 2025-08-15 12:51:36 +02:00
parent 3a013c829f
commit 07c7c98edb
11 changed files with 238 additions and 31 deletions

View file

@ -55,6 +55,13 @@ class BalRepository {
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 {
final result = await _apiClient.stopBal(id);
_getBalsNoCache();

View file

@ -11,6 +11,10 @@ class BookInstanceRepository {
final ApiClient _apiClient;
Future<Result<List<BookInstance>>> getByEan(int balId, int ean) async {
return await _apiClient.getBookInstanceByEAN(balId, ean);
}
Future<Result<BookInstance>> sendBook(
Book book,
Owner owner,

View file

@ -8,6 +8,10 @@ class BookRepository {
final ApiClient _apiClient;
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);
}
}

View file

@ -33,6 +33,18 @@ class OwnerRepository {
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].
Future<Result<Owner>> addOwner(
String firstName,

View file

@ -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 {
final client = Client();
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(
Book book,
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 {
final client = Client();
try {

View 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}";
}
}

View file

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

View file

@ -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<BookInstance> _scannedBooks = [];
List<BookInstance> 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<BookStack> _soldBooks = [];
List<BookStack> get soldBooks => _soldBooks;
final List<BookStack> _scannedBooks = [];
List<BookStack> 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():

View file

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

View 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()})",
),
),
),
),
),
],
),
),
},
);
},
);
}
}

View file

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