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/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<SingleChildWidget> get providers {
ChangeNotifierProvider(
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 '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<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 ]=====
* ====================
*/
/// Call on `/owners` to get a list of all [Owner]s
Future<Result<List<Owner>>> 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<Result<Owner>> 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,

View file

@ -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();

View file

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

View file

@ -12,4 +12,12 @@ class Book {
String ean;
int id;
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.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<String, dynamic> 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,
);
}

View file

@ -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: [

View file

@ -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<Result<Book>> 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<Result<BookInstance>> sendBook(
Book book,
Owner owner,
Bal bal,
double price,
) async {
return await _bookInstanceRepository.sendBook(book, owner, bal, price);
}
/// Sends an api request with

View file

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

View file

@ -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<ConfirmationPopup> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
num price = 0;
double price = 0;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -67,7 +71,7 @@ class _ConfirmationPopupState extends State<ConfirmationPopup> {
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<ConfirmationPopup> {
return null;
},
onSaved: (newValue) {
price = num.parse(newValue!);
price = double.parse(newValue!);
},
)
: SizedBox(),
@ -111,23 +115,34 @@ class _ConfirmationPopupState extends State<ConfirmationPopup> {
child: Text("Annuler"),
),
TextButton(
onPressed: () {
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é",
"\"${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(
@ -138,6 +153,20 @@ class _ConfirmationPopupState extends State<ConfirmationPopup> {
);
widget.exitPopup(context);
}
}
break;
case Error():
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
"Une erreur est survenue : ${result.error}",
),
),
);
}
}
},
child: Text("Valider"),
),

View file

@ -21,7 +21,7 @@ class SellViewModel extends ChangeNotifier {
id: _scannedBooks.length,
ownerId: 5,
price: 5,
status: true,
available: true,
);
_scannedBooks.add(addedBook);
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]);
}
}