feat: add a book by scanning
This commit is contained in:
parent
72fd0b66a9
commit
981dce5bfe
14 changed files with 264 additions and 59 deletions
|
|
@ -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()),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
5
lib/domain/models/bal.dart
Normal file
5
lib/domain/models/bal.dart
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
class Bal {
|
||||
Bal({required this.id});
|
||||
|
||||
int id;
|
||||
}
|
||||
|
|
@ -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"],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"),
|
||||
|
|
|
|||
|
|
@ -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,32 +115,57 @@ class _ConfirmationPopupState extends State<ConfirmationPopup> {
|
|||
child: Text("Annuler"),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
switch (widget.viewModel.askPrice) {
|
||||
case true:
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
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é",
|
||||
),
|
||||
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(
|
||||
SnackBar(
|
||||
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"),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class SellViewModel extends ChangeNotifier {
|
|||
id: _scannedBooks.length,
|
||||
ownerId: 5,
|
||||
price: 5,
|
||||
status: true,
|
||||
available: true,
|
||||
);
|
||||
_scannedBooks.add(addedBook);
|
||||
notifyListeners();
|
||||
|
|
|
|||
27
lib/utils/overlay_boundary.dart
Normal file
27
lib/utils/overlay_boundary.dart
Normal 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]);
|
||||
}
|
||||
}
|
||||
Reference in a new issue