feat: add a book by scanning

This commit is contained in:
Alzalia 2025-08-09 01:42:22 +02:00
parent 72fd0b66a9
commit 981dce5bfe
14 changed files with 264 additions and 59 deletions

View file

@ -1,6 +1,8 @@
import "package:provider/provider.dart"; import "package:provider/provider.dart";
import "package:provider/single_child_widget.dart"; import "package:provider/single_child_widget.dart";
import "package:seshat/data/repositories/auth_repository.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/repositories/owner_repository.dart";
import "package:seshat/data/services/api_client.dart"; import "package:seshat/data/services/api_client.dart";
@ -19,5 +21,9 @@ List<SingleChildWidget> get providers {
ChangeNotifierProvider( ChangeNotifierProvider(
create: (context) => AuthRepository(authClient: context.read()), create: (context) => AuthRepository(authClient: context.read()),
), ),
Provider(create: (context) => BookRepository(apiClient: context.read())),
Provider(
create: (context) => BookInstanceRepository(apiClient: context.read()),
),
]; ];
} }

View file

@ -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<Result<BookInstance>> sendBook(
Book book,
Owner owner,
Bal bal,
double price,
) async {
return await _apiClient.sendBook(book, owner, bal, price);
}
}

View file

@ -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<Result<Book>> getBookByEAN(String ean) async {
return _apiClient.getBookByEAN(ean);
}
}

View file

@ -1,8 +1,12 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:seshat/config/constants.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/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';
@ -23,18 +27,101 @@ class ApiClient {
); );
} }
Future<Map<String, String>> _getHeaders([
Map<String, String>? additionalHeaders,
]) async {
await _initStore();
final token = await _secureStorage!.read(key: "token");
final headers = {"Authorization": "Bearer $token", ...?additionalHeaders};
return headers;
}
/*
* ===================
* =====[ BOOKS ]=====
* ===================
*/
Future<Result<Book>> 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<Result<BookInstance>> 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 ]===== * =====[ OWNERS ]=====
* ==================== * ====================
*/ */
/// Call on `/owners` to get a list of all [Owner]s
Future<Result<List<Owner>>> getOwners() async { Future<Result<List<Owner>>> getOwners() async {
final client = Client(); final client = Client();
try { try {
await _initStore(); final headers = await _getHeaders();
final token = await _secureStorage!.read(key: "token");
final headers = {"Authorization": "Bearer $token"};
final response = await client.get( final response = await client.get(
Uri.parse("https://$apiBasePath/owners"), Uri.parse("https://$apiBasePath/owners"),
headers: headers, headers: headers,
@ -54,6 +141,7 @@ class ApiClient {
} }
} }
/// Adds an owner to the database
Future<Result<Owner>> addOwner( Future<Result<Owner>> addOwner(
String firstName, String firstName,
String lastName, String lastName,
@ -61,12 +149,7 @@ class ApiClient {
) async { ) async {
final client = Client(); final client = Client();
try { try {
await _initStore(); final headers = await _getHeaders({"Content-Type": "application/json"});
final token = await _secureStorage!.read(key: "token");
final headers = {
"Authorization": "Bearer $token",
"Content-Type": "application/json",
};
final body = { final body = {
"first_name": firstName, "first_name": firstName,
"last_name": lastName, "last_name": lastName,

View file

@ -20,7 +20,6 @@ class AuthClient {
try { try {
await _initStore(); await _initStore();
bool hasToken = await _secureStorage!.containsKey(key: "token"); bool hasToken = await _secureStorage!.containsKey(key: "token");
debugPrint("\n\n\n${hasToken == true} => HAS_TOKEN\n\n\n");
if (hasToken) { if (hasToken) {
var token = await _secureStorage!.read(key: "token"); var token = await _secureStorage!.read(key: "token");
var url = Uri.parse("https://$apiBasePath/token-check"); var url = Uri.parse("https://$apiBasePath/token-check");
@ -29,9 +28,6 @@ class AuthClient {
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: jsonEncode({"token": token}), body: jsonEncode({"token": token}),
); );
debugPrint(
"\n\n\n${response.body is String} => ${response.body}\n\n\n",
);
if (response.body == "true") { if (response.body == "true") {
return Result.ok(true); return Result.ok(true);
@ -39,7 +35,6 @@ class AuthClient {
} }
return Result.ok(false); return Result.ok(false);
} catch (e) { } catch (e) {
debugPrint(e.toString());
return Result.error(Exception(e)); return Result.error(Exception(e));
} }
} }
@ -67,8 +62,6 @@ class AuthClient {
return Result.error(Exception("Token creation error")); return Result.error(Exception("Token creation error"));
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
debugPrint(e.toString());
debugPrintStack(stackTrace: stackTrace);
return Result.error(Exception(e)); return Result.error(Exception(e));
} finally { } finally {
client.close(); client.close();

View file

@ -0,0 +1,5 @@
class Bal {
Bal({required this.id});
int id;
}

View file

@ -12,4 +12,12 @@ class Book {
String ean; String ean;
int id; int id;
String priceNew; String priceNew;
factory Book.fromJSON(Map<String, dynamic> json) => Book(
author: json["author"],
ean: json["ean"],
id: json["id"],
priceNew: json["price_new"],
title: json["title"],
);
} }

View file

@ -5,7 +5,7 @@ class BookInstance {
required this.id, required this.id,
required this.ownerId, required this.ownerId,
required this.price, required this.price,
required this.status, required this.available,
this.soldPrice, this.soldPrice,
}); });
@ -15,14 +15,15 @@ class BookInstance {
int ownerId; int ownerId;
double price; double price;
double? soldPrice; double? soldPrice;
bool status; bool available;
factory BookInstance.fromJSON(Map<String, dynamic> json) => BookInstance( factory BookInstance.fromJSON(Map<String, dynamic> json) => BookInstance(
balId: json["balId"], balId: json["bal_id"],
bookId: json["bookId"], bookId: json["book_id"],
id: json["id"], id: json["id"],
ownerId: json["ownerId"], ownerId: json["owner_id"],
price: json["price"], price: json["price"],
status: json["status"], available: json["available"],
soldPrice: json["sold_price"] ?? 0,
); );
} }

View file

@ -35,7 +35,11 @@ GoRouter router(AuthRepository authRepository) => GoRouter(
GoRoute( GoRoute(
path: Routes.add, path: Routes.add,
pageBuilder: (context, state) { 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)); return NoTransitionPage(child: AddPage(viewModel: viewModel));
}, },
// routes: [ // routes: [

View file

@ -3,19 +3,30 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.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/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.dart';
import 'package:seshat/domain/models/book_instance.dart';
import 'package:seshat/domain/models/owner.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 AddViewModel extends ChangeNotifier { class AddViewModel extends ChangeNotifier {
AddViewModel({required OwnerRepository ownerRepository}) AddViewModel({
: _ownerRepository = ownerRepository { required OwnerRepository ownerRepository,
required BookRepository bookRepository,
required BookInstanceRepository bookInstanceRepository,
}) : _ownerRepository = ownerRepository,
_bookRepository = bookRepository,
_bookInstanceRepository = bookInstanceRepository {
load = Command0(_load)..execute(); load = Command0(_load)..execute();
} }
final OwnerRepository _ownerRepository; final OwnerRepository _ownerRepository;
final BookRepository _bookRepository;
final BookInstanceRepository _bookInstanceRepository;
late final StreamSubscription sub; 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 /// 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]. /// either created or retrieved. Sens the [Book] back wrapped in a [Result].
Future<Result<Book>> scanBook(BarcodeCapture barcode) async { Future<Result<Book>> scanBook(BarcodeCapture barcode) async {
return Result.ok( var ean = barcode.barcodes.first.rawValue!;
Book( var result = await _bookRepository.getBookByEAN(ean);
author: "Patrick K. Dewdney", return result;
ean: barcode.barcodes.first.rawValue!, }
id: 56,
priceNew: "50 EUR", Future<Result<BookInstance>> sendBook(
title: "Les chiens et la charrue", Book book,
), Owner owner,
); Bal bal,
double price,
) async {
return await _bookInstanceRepository.sendBook(book, owner, bal, price);
} }
/// Sends an api request with /// Sends an api request with

View file

@ -75,6 +75,8 @@ class _AddPageState extends State<AddPage> {
); );
break; break;
case Error(): case Error():
debugPrintStack();
debugPrint(result.error.toString());
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text("Erreur : ${result.error}"), content: Text("Erreur : ${result.error}"),

View file

@ -1,6 +1,10 @@
import 'dart:ffi';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:seshat/domain/models/bal.dart';
import 'package:seshat/domain/models/book.dart'; import 'package:seshat/domain/models/book.dart';
import 'package:seshat/ui/add_page/view_model/add_view_model.dart'; import 'package:seshat/ui/add_page/view_model/add_view_model.dart';
import 'package:seshat/utils/result.dart';
class ConfirmationPopup extends StatefulWidget { class ConfirmationPopup extends StatefulWidget {
const ConfirmationPopup({ const ConfirmationPopup({
@ -22,7 +26,7 @@ class ConfirmationPopup extends StatefulWidget {
class _ConfirmationPopupState extends State<ConfirmationPopup> { class _ConfirmationPopupState extends State<ConfirmationPopup> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
num price = 0; double price = 0;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@ -67,7 +71,7 @@ class _ConfirmationPopupState extends State<ConfirmationPopup> {
text: "Prix à neuf : ", text: "Prix à neuf : ",
style: TextStyle(fontWeight: FontWeight.bold), 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<ConfirmationPopup> {
return null; return null;
}, },
onSaved: (newValue) { onSaved: (newValue) {
price = num.parse(newValue!); price = double.parse(newValue!);
}, },
) )
: SizedBox(), : SizedBox(),
@ -111,32 +115,57 @@ class _ConfirmationPopupState extends State<ConfirmationPopup> {
child: Text("Annuler"), child: Text("Annuler"),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () async {
switch (widget.viewModel.askPrice) { var result = await widget.viewModel.sendBook(
case true: widget.book,
if (_formKey.currentState!.validate()) { widget.viewModel.currentOwner!,
_formKey.currentState!.save(); 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( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( 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"), child: Text("Valider"),

View file

@ -21,7 +21,7 @@ class SellViewModel extends ChangeNotifier {
id: _scannedBooks.length, id: _scannedBooks.length,
ownerId: 5, ownerId: 5,
price: 5, price: 5,
status: true, available: true,
); );
_scannedBooks.add(addedBook); _scannedBooks.add(addedBook);
notifyListeners(); notifyListeners();

View file

@ -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<OverlayBoundary> createState() => _OverlayBoundaryState();
}
class _OverlayBoundaryState extends State<OverlayBoundary> {
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]);
}
}