diff --git a/< b/< new file mode 100644 index 0000000..2c8173c --- /dev/null +++ b/< @@ -0,0 +1,46 @@ +import 'package:provider/provider.dart'; + +enum State { pending, ongoing, ended } + +class Bal { + Bal({ + required this.id, + required this.name, + required this.state, + required this.startTime, + required this.endTime, + }); + + int id; + String name; + State state; + DateTime startTime; + DateTime endTime; + + factory Bal.fromJSON(Map json) => Bal( + id: json["id"], + name: json["name"], + state: switch (json["state"]) { + "Pending" => State.pending, + "Ongoing" => State.ongoing, + _ => State.ended, + }, + startTime: DateTime.fromMillisecondsSinceEpoch( + json["start_timestamp"] * 1000, + isUtc: true, + ), + endTime: DateTime.fromMillisecondsSinceEpoch( + json["end_timestamp"] * 1000, + isUtc: true, + ), + ); + + int compareTo(Bal other) { + if (ended == other.ended) { + return 0; + } else if (ended) { + return 1; + } + return -1; + } +} diff --git a/lib/config/dependencies.dart b/lib/config/dependencies.dart index 0f3d249..7ff7e4c 100644 --- a/lib/config/dependencies.dart +++ b/lib/config/dependencies.dart @@ -1,6 +1,7 @@ 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/bal_repository.dart"; import "package:seshat/data/repositories/book_instance_repository.dart"; import "package:seshat/data/repositories/book_repository.dart"; @@ -25,5 +26,6 @@ List get providers { Provider( create: (context) => BookInstanceRepository(apiClient: context.read()), ), + Provider(create: (context) => BalRepository(apiClient: context.read())), ]; } diff --git a/lib/data/repositories/bal_repository.dart b/lib/data/repositories/bal_repository.dart new file mode 100644 index 0000000..983fe55 --- /dev/null +++ b/lib/data/repositories/bal_repository.dart @@ -0,0 +1,51 @@ +import 'package:seshat/data/services/api_client.dart'; +import 'package:seshat/domain/models/bal.dart'; +import 'package:seshat/utils/result.dart'; + +class BalRepository { + BalRepository({required ApiClient apiClient}) : _apiClient = apiClient; + + final ApiClient _apiClient; + List? _bals; + + Future>> getBals() async { + if (_bals != null) { + return Result.ok(_bals!); + } + final result = await _apiClient.getBals(); + switch (result) { + case Ok(): + _bals = result.value; + return Result.ok(result.value); + case Error(): + return result; + } + } + + Future> balById(int id) async { + if (_bals == null) { + await getBals(); + } + Bal? bal = _bals!.where((bal) => bal.id == id).firstOrNull; + if (bal != null) { + return Result.ok(bal); + } + final result = await _apiClient.getBalById(id); + switch (result) { + case Ok(): + return Result.ok(result.value); + case Error(): + return result; + } + } + + Future> addBal(String name) async { + final result = await _apiClient.addBal(name); + switch (result) { + case Ok(): + return result; + case Error(): + return result; + } + } +} diff --git a/lib/data/services/api_client.dart b/lib/data/services/api_client.dart index a32a001..00270d3 100644 --- a/lib/data/services/api_client.dart +++ b/lib/data/services/api_client.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter/widgets.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart'; import 'package:seshat/config/constants.dart'; @@ -35,6 +36,106 @@ class ApiClient { return headers; } + /* + * ================= + * =====[ BAL ]===== + * ================= +*/ + + Future> getBalById(int id) async { + final client = Client(); + try { + final headers = await _getHeaders(); + final response = await client.get( + Uri.parse("https://$apiBasePath/bal/${id.toString()}"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + return Result.ok(Bal.fromJSON(json)); + } else if (response.statusCode == 403) { + return Result.error(Exception("You don't own the specified bal")); + } else { + return Result.error( + Exception("No bal wirth this id exists the database"), + ); + } + } catch (e) { + return Result.error(Exception(e)); + } finally { + client.close(); + } + } + + Future> addBal(String name) async { + final client = Client(); + try { + final headers = await _getHeaders({"Content-Type": "application/json"}); + final body = {"name": name}; + final response = await client.post( + Uri.parse("https://$apiBasePath/bal"), + headers: headers, + body: jsonEncode(body), + ); + if (response.statusCode == 201) { + final json = jsonDecode(response.body); + return Result.ok(Bal.fromJSON(json)); + } else { + return Result.error(Exception("Something went wrong")); + } + } catch (e) { + debugPrint("\n\n\n\n${e.toString()}\n\n\n\n"); + return Result.error(Exception(e)); + } finally { + client.close(); + } + } + + Future>> getBals() async { + final client = Client(); + try { + final headers = await _getHeaders(); + final response = await client.get( + Uri.parse("https://$apiBasePath/bals"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body) as List; + debugPrint("\n\n\n\nRECEIVED $json\n\n\n\n"); + return Result.ok(json.map((element) => Bal.fromJSON(element)).toList()); + } else { + return Result.error(Exception("Something wrong happened")); + } + } catch (e) { + return Result.error(Exception(e)); + } finally { + client.close(); + } + } + + Future> getCurrentBal() async { + final client = Client(); + try { + final headers = await _getHeaders(); + final response = await client.get( + Uri.parse("https://$apiBasePath/bal/current"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + return Result.ok(Bal.fromJSON(json)); + } else if (response.statusCode == 404) { + return Result.ok(null); + } else { + return Result.error(Exception("Something went wrong")); + } + } catch (e) { + return Result.error(Exception(e)); + } finally { + client.close(); + } + } + /* * =================== * =====[ BOOKS ]===== diff --git a/lib/domain/models/bal.dart b/lib/domain/models/bal.dart index 5b98811..0ec96ed 100644 --- a/lib/domain/models/bal.dart +++ b/lib/domain/models/bal.dart @@ -1,5 +1,44 @@ +enum BalState { pending, ongoing, ended } + class Bal { - Bal({required this.id}); + Bal({ + required this.id, + required this.name, + required this.state, + required this.startTime, + required this.endTime, + }); int id; + String name; + BalState state; + DateTime startTime; + DateTime endTime; + + factory Bal.fromJSON(Map json) => Bal( + id: json["id"], + name: json["name"], + state: switch (json["state"]) { + "Pending" => BalState.pending, + "Ongoing" => BalState.ongoing, + _ => BalState.ended, + }, + startTime: DateTime.fromMillisecondsSinceEpoch( + json["start_timestamp"] * 1000, + isUtc: true, + ), + endTime: DateTime.fromMillisecondsSinceEpoch( + json["end_timestamp"] * 1000, + isUtc: true, + ), + ); + + int compareTo(Bal other) { + if (state.index == other.state.index) { + return 0; + } else if (state.index > other.state.index) { + return state.index; + } + return -state.index; + } } diff --git a/lib/routing/router.dart b/lib/routing/router.dart index 00cb9a4..654f5a9 100644 --- a/lib/routing/router.dart +++ b/lib/routing/router.dart @@ -6,7 +6,10 @@ import 'package:seshat/ui/add_page/view_model/add_view_model.dart'; import 'package:seshat/ui/add_page/widgets/add_page.dart'; import 'package:seshat/ui/auth/viewmodel/login_view_model.dart'; import 'package:seshat/ui/auth/widgets/login_page.dart'; -import 'package:seshat/ui/home_page/home_page.dart'; +import 'package:seshat/ui/bal_page/view_model/bal_view_model.dart'; +import 'package:seshat/ui/bal_page/widget/bal_screen.dart'; +import 'package:seshat/ui/home_page/view_model/home_view_model.dart'; +import 'package:seshat/ui/home_page/widgets/home_page.dart'; import 'package:seshat/ui/sell_page/view_model/sell_view_model.dart'; import 'package:seshat/ui/sell_page/widgets/sell_page.dart'; @@ -30,8 +33,22 @@ GoRouter router(AuthRepository authRepository) => GoRouter( routes: [ GoRoute( path: Routes.home, - pageBuilder: (context, state) => NoTransitionPage(child: HomePage()), + pageBuilder: (context, state) { + final viewModel = HomeViewModel(balRepository: context.read()); + return NoTransitionPage(child: HomePage(viewModel: viewModel)); + }, routes: [ + GoRoute( + path: Routes.balPage, + name: "bal", + pageBuilder: (context, state) { + final viewModel = BalViewModel( + balRepository: context.read(), + id: int.tryParse(state.pathParameters["id"] ?? ""), + ); + return NoTransitionPage(child: BalScreen(viewModel: viewModel)); + }, + ), GoRoute( path: Routes.add, pageBuilder: (context, state) { @@ -39,6 +56,7 @@ GoRouter router(AuthRepository authRepository) => GoRouter( ownerRepository: context.read(), bookRepository: context.read(), bookInstanceRepository: context.read(), + balRepository: context.read(), ); return NoTransitionPage(child: AddPage(viewModel: viewModel)); }, @@ -51,7 +69,7 @@ GoRouter router(AuthRepository authRepository) => GoRouter( GoRoute( path: Routes.sell, pageBuilder: (context, state) { - final viewModel = SellViewModel(); + final viewModel = SellViewModel(balRepository: context.read()); return NoTransitionPage(child: SellPage(viewModel: viewModel)); }, ), diff --git a/lib/routing/routes.dart b/lib/routing/routes.dart index 39b52b0..b773ff8 100644 --- a/lib/routing/routes.dart +++ b/lib/routing/routes.dart @@ -1,6 +1,7 @@ abstract final class Routes { // ==[ HOME ]== static const home = '/'; + static const balPage = '/bal/:id'; // ==[ ADD ]== static const add = '/add'; diff --git a/lib/ui/add_page/view_model/add_view_model.dart b/lib/ui/add_page/view_model/add_view_model.dart index 939d88e..61c5b3d 100644 --- a/lib/ui/add_page/view_model/add_view_model.dart +++ b/lib/ui/add_page/view_model/add_view_model.dart @@ -3,6 +3,7 @@ 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/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'; @@ -18,16 +19,18 @@ class AddViewModel extends ChangeNotifier { required OwnerRepository ownerRepository, required BookRepository bookRepository, required BookInstanceRepository bookInstanceRepository, + required BalRepository balRepository, }) : _ownerRepository = ownerRepository, _bookRepository = bookRepository, - _bookInstanceRepository = bookInstanceRepository { + _bookInstanceRepository = bookInstanceRepository, + _balRepository = balRepository { load = Command0(_load)..execute(); } final OwnerRepository _ownerRepository; final BookRepository _bookRepository; final BookInstanceRepository _bookInstanceRepository; - late final StreamSubscription sub; + final BalRepository _balRepository; /* * ==================== @@ -73,6 +76,15 @@ class AddViewModel extends ChangeNotifier { } } + /* + * ================= + * =====[ BAL ]===== + * ================= +*/ + + Bal? _currentBal; + Bal? get currentBal => _currentBal; + /* * =================== * =====[ PRICE ]===== @@ -109,11 +121,6 @@ class AddViewModel extends ChangeNotifier { return await _bookInstanceRepository.sendBook(book, owner, bal, price); } - /// Sends an api request with - // Result newBookInstance() { - - // }; - /* * ================================= * =====[ COMMAND AND LOADING ]===== @@ -124,7 +131,32 @@ class AddViewModel extends ChangeNotifier { bool isLoaded = false; Future> _load() async { - return await _loadOwners(); + final result1 = await _loadOwners(); + switch (result1) { + case Error(): + return result1; + default: + break; + } + final result2 = await _loadBal(); + isLoaded = true; + notifyListeners(); + return result2; + } + + Future> _loadBal() async { + final result = await _balRepository.getBals(); + switch (result) { + case Ok(): + _currentBal = result.value + .where((bal) => bal.state == BalState.ongoing) + .firstOrNull; + break; + case Error(): + break; + } + + return result; } Future> _loadOwners() async { @@ -137,18 +169,10 @@ class AddViewModel extends ChangeNotifier { "${b.firstName} ${b.lastName}", ), ); - isLoaded = true; case Error(): break; } - notifyListeners(); return result; } - - @override - void dispose() { - sub.cancel(); - super.dispose(); - } } diff --git a/lib/ui/add_page/widgets/add_page.dart b/lib/ui/add_page/widgets/add_page.dart index b74ff70..f5ececa 100644 --- a/lib/ui/add_page/widgets/add_page.dart +++ b/lib/ui/add_page/widgets/add_page.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:go_router/go_router.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:seshat/domain/models/book.dart'; +import 'package:seshat/routing/routes.dart'; import 'package:seshat/ui/add_page/view_model/add_view_model.dart'; import 'package:seshat/ui/add_page/widgets/form_popup.dart'; import 'package:seshat/ui/add_page/widgets/owner_popup.dart'; @@ -36,133 +38,163 @@ class _AddPageState extends State { listenable: widget.viewModel, builder: (context, child) => switch (widget.viewModel.isLoaded) { false => Center(child: CircularProgressIndicator()), - true => Stack( - children: [ - ColoredBox(color: Colors.black), - MobileScanner( - controller: controller, - onDetect: (barcodes) async { - if (widget.viewModel.currentOwner == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Attention : vous devez choisir un·e propriétaire", - ), - behavior: SnackBarBehavior.floating, - ), - ); - return; - } - - void setPrice(num newPrice) async { - setState(() { - price = newPrice; - }); - } - - Result result = await widget.viewModel.scanBook( - barcodes, - ); - - switch (result) { - case Ok(): - await _confirmationDialogBuilder( - context, - setPrice, - controller, - widget.viewModel, - result.value, - ); - break; - case Error(): - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Erreur : ${result.error}"), - behavior: SnackBarBehavior.floating, - ), - ); - break; - } - }, - ), - SafeArea( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(height: 5), - Center( - child: Card( - margin: EdgeInsets.symmetric(horizontal: 50), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: Icon(Icons.person), - title: TextButton( - child: Text( - (widget.viewModel.currentOwner == null) - ? "Aucun" - : "${widget.viewModel.currentOwner!.firstName} ${widget.viewModel.currentOwner!.lastName}", - ), - onPressed: () => _ownerDialogBuilder( - context, - controller, - widget.viewModel, - ), - ), - ), - ListTile( - leading: Icon(Icons.attach_money), - title: TextButton( - child: Text( - (widget.viewModel.askPrice) - ? "Demander à chaque fois" - : "Prix libre toujours", - ), - onPressed: () { - setState(() { - widget.viewModel.askPrice = - !widget.viewModel.askPrice; - }); - }, - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - Center(child: SvgPicture.asset('assets/scan-overlay.svg')), - SafeArea( + true => switch (widget.viewModel.currentBal) { + null => Center( + child: SizedBox( + width: 300, child: Column( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Center( - child: TextButton( - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll( - theme.cardColor, - ), - ), - onPressed: () => _formDialogBuilder( - context, - controller, - widget.viewModel, - ), - child: Text("Enregistrer manuellement"), - ), + Text( + "Aucune bal n'est active.", + style: TextStyle(fontSize: 25), + textAlign: TextAlign.center, + ), + SizedBox(height: 15), + Text( + "Vous devez créer puis activer une BAL pour pouvoir scanner des livres.", + textAlign: TextAlign.center, + ), + SizedBox(height: 30), + ElevatedButton( + onPressed: () { + context.go(Routes.home); + }, + child: Text("Gérer les BALs"), ), - SizedBox(height: 5), ], ), ), - ], - ), + ), + _ => Stack( + children: [ + ColoredBox(color: Colors.black), + MobileScanner( + controller: controller, + onDetect: (barcodes) async { + if (widget.viewModel.currentOwner == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Attention : vous devez choisir un·e propriétaire", + ), + behavior: SnackBarBehavior.floating, + ), + ); + return; + } + + void setPrice(num newPrice) async { + setState(() { + price = newPrice; + }); + } + + Result result = await widget.viewModel.scanBook( + barcodes, + ); + + switch (result) { + case Ok(): + await _confirmationDialogBuilder( + context, + setPrice, + controller, + widget.viewModel, + result.value, + ); + break; + case Error(): + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Erreur : ${result.error}"), + behavior: SnackBarBehavior.floating, + ), + ); + break; + } + }, + ), + SafeArea( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 5), + Center( + child: Card( + margin: EdgeInsets.symmetric(horizontal: 50), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.person), + title: TextButton( + child: Text( + (widget.viewModel.currentOwner == null) + ? "Aucun" + : "${widget.viewModel.currentOwner!.firstName} ${widget.viewModel.currentOwner!.lastName}", + ), + onPressed: () => _ownerDialogBuilder( + context, + controller, + widget.viewModel, + ), + ), + ), + ListTile( + leading: Icon(Icons.attach_money), + title: TextButton( + child: Text( + (widget.viewModel.askPrice) + ? "Demander à chaque fois" + : "Prix libre toujours", + ), + onPressed: () { + setState(() { + widget.viewModel.askPrice = + !widget.viewModel.askPrice; + }); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + Center(child: SvgPicture.asset('assets/scan-overlay.svg')), + SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Center( + child: TextButton( + style: ButtonStyle( + backgroundColor: WidgetStatePropertyAll( + theme.cardColor, + ), + ), + onPressed: () => _formDialogBuilder( + context, + controller, + widget.viewModel, + ), + child: Text("Enregistrer manuellement"), + ), + ), + SizedBox(height: 5), + ], + ), + ), + ], + ), + }, }, ), ); diff --git a/lib/ui/add_page/widgets/confirmation_popup.dart b/lib/ui/add_page/widgets/confirmation_popup.dart index 55816a9..84041d2 100644 --- a/lib/ui/add_page/widgets/confirmation_popup.dart +++ b/lib/ui/add_page/widgets/confirmation_popup.dart @@ -1,5 +1,4 @@ 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'; @@ -117,7 +116,7 @@ class _ConfirmationPopupState extends State { var result = await widget.viewModel.sendBook( widget.book, widget.viewModel.currentOwner!, - Bal(id: 1), + widget.viewModel.currentBal!, price, ); switch (result) { diff --git a/lib/ui/bal_page/view_model/bal_view_model.dart b/lib/ui/bal_page/view_model/bal_view_model.dart new file mode 100644 index 0000000..e52fc02 --- /dev/null +++ b/lib/ui/bal_page/view_model/bal_view_model.dart @@ -0,0 +1,56 @@ +import 'package:flutter/widgets.dart'; +import 'package:seshat/data/repositories/bal_repository.dart'; +import 'package:seshat/domain/models/bal.dart'; +import 'package:seshat/utils/command.dart'; +import 'package:seshat/utils/result.dart'; + +class BalViewModel extends ChangeNotifier { + BalViewModel({required BalRepository balRepository, required this.id}) + : _balRepository = balRepository { + load = Command0(_load)..execute(); + } + + final BalRepository _balRepository; + + Bal? _bal; + int? id; + Bal? get bal => _bal; + + /* + * ================================= + * =====[ COMMAND AND LOADING ]===== + * ================================= +*/ + + late final Command0 load; + bool isLoaded = false; + + Future> _load() async { + final result1 = await _loadBal(); + switch (result1) { + case Error(): + return result1; + default: + break; + } + isLoaded = true; + notifyListeners(); + return result1; + } + + Future> _loadBal() async { + if (id == null) { + return Result.error(Exception("No id given")); + } + final result = await _balRepository.balById(id!); + switch (result) { + case Ok(): + _bal = result.value; + break; + case Error(): + break; + } + + return result; + } +} diff --git a/lib/ui/bal_page/widget/bal_screen.dart b/lib/ui/bal_page/widget/bal_screen.dart new file mode 100644 index 0000000..2d5dbb4 --- /dev/null +++ b/lib/ui/bal_page/widget/bal_screen.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:seshat/domain/models/bal.dart'; +import 'package:seshat/ui/bal_page/view_model/bal_view_model.dart'; +import 'package:seshat/ui/core/ui/navigation_bar.dart'; + +class BalScreen extends StatefulWidget { + const BalScreen({super.key, required this.viewModel}); + + final BalViewModel viewModel; + + @override + State createState() => _BalScreenState(); +} + +class _BalScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + bottomNavigationBar: AppNavigationBar(startIndex: 0), + body: ListenableBuilder( + listenable: widget.viewModel, + builder: (context, child) { + return switch (widget.viewModel.isLoaded) { + false => Center(child: CircularProgressIndicator()), + true => switch (widget.viewModel.bal == null) { + true => Center( + child: Text("La BAL référencée n'est pas accessible"), + ), + false => switch (widget.viewModel.bal!.state) { + BalState.pending => Center(child: Text("Pending")), + BalState.ongoing => Center(child: Text("Ongoing")), + BalState.ended => Center(child: Text("Ending")), + }, + }, + }; + }, + ), + ); + } +} diff --git a/lib/ui/home_page/home_page.dart b/lib/ui/home_page/home_page.dart deleted file mode 100644 index 64eaf4a..0000000 --- a/lib/ui/home_page/home_page.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:seshat/ui/core/ui/navigation_bar.dart'; - -class HomePage extends StatelessWidget { - const HomePage({super.key}); - - @override - Widget build(BuildContext context) { - return Scaffold( - bottomNavigationBar: AppNavigationBar(startIndex: 0), - body: Center(child: Text("Home page.")), - ); - // return Center(child: Text("Home page;")); - } -} diff --git a/lib/ui/home_page/view_model/home_view_model.dart b/lib/ui/home_page/view_model/home_view_model.dart new file mode 100644 index 0000000..22eae91 --- /dev/null +++ b/lib/ui/home_page/view_model/home_view_model.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:seshat/data/repositories/bal_repository.dart'; +import 'package:seshat/domain/models/bal.dart'; +import 'package:seshat/utils/command.dart'; +import 'package:seshat/utils/result.dart'; + +class HomeViewModel extends ChangeNotifier { + HomeViewModel({required BalRepository balRepository}) + : _balRepository = balRepository { + load = Command0(_load)..execute(); + } + + final BalRepository _balRepository; + + /* + * ================= + * =====[ BAL ]===== + * ================= +*/ + + List _bals = []; + List get bals => _bals; + + Bal? _currentBal; + Bal? get currentBal => _currentBal; + + Future> createBal(String name) async { + final result = await _balRepository.addBal(name); + switch (result) { + case Ok(): + final result2 = await _balRepository.getBals(); + switch (result2) { + case Ok(): + _bals = result2.value..sort((a, b) => a.compareTo(b)); + break; + case Error(): + debugPrint("\n\n\n\n${result2.error.toString()}\n\n\n\n"); + return result2; + } + break; + case Error(): + return result; + } + notifyListeners(); + return result; + } + + /* + * ================================= + * =====[ COMMAND AND LOADING ]===== + * ================================= +*/ + + late final Command0 load; + bool isLoaded = false; + + Future> _load() async { + final result2 = await _loadBal(); + isLoaded = true; + notifyListeners(); + return result2; + } + + Future> _loadBal() async { + final result = await _balRepository.getBals(); + switch (result) { + case Ok(): + _bals = result.value..sort((a, b) => a.compareTo(b)); + _currentBal = _bals + .where((bal) => bal.state == BalState.ongoing) + .firstOrNull; + break; + case Error(): + return result; + } + + return result; + } +} diff --git a/lib/ui/home_page/widgets/create_confirmation_popup.dart b/lib/ui/home_page/widgets/create_confirmation_popup.dart new file mode 100644 index 0000000..211f2ae --- /dev/null +++ b/lib/ui/home_page/widgets/create_confirmation_popup.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:seshat/ui/home_page/view_model/home_view_model.dart'; + +class CreateConfirmationPopup extends StatefulWidget { + const CreateConfirmationPopup({super.key, required this.viewModel}); + + final HomeViewModel viewModel; + + @override + State createState() => + _CreateConfirmationPopupState(); +} + +class _CreateConfirmationPopupState extends State { + final GlobalKey _formKey = GlobalKey(); + String? name; + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("Créer une BAL"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: "Nom de la BAL", + border: OutlineInputBorder(), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return "Veuillez entrer un nom"; + } + return null; + }, + onSaved: (newValue) { + name = newValue; + }, + ), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text("Annuler"), + ), + TextButton( + onPressed: () async { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + await widget.viewModel.createBal(name!); + } + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: Text("Valider"), + ), + ], + ); + } +} diff --git a/lib/ui/home_page/widgets/home_page.dart b/lib/ui/home_page/widgets/home_page.dart new file mode 100644 index 0000000..586db9d --- /dev/null +++ b/lib/ui/home_page/widgets/home_page.dart @@ -0,0 +1,213 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:seshat/domain/models/bal.dart'; +import 'package:seshat/ui/core/ui/navigation_bar.dart'; +import 'package:seshat/ui/home_page/view_model/home_view_model.dart'; +import 'package:seshat/ui/home_page/widgets/create_confirmation_popup.dart'; + +class HomePage extends StatefulWidget { + const HomePage({super.key, required this.viewModel}); + + final HomeViewModel viewModel; + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + bottomNavigationBar: AppNavigationBar(startIndex: 0), + body: ListenableBuilder( + listenable: widget.viewModel, + builder: (context, child) { + return switch (widget.viewModel.isLoaded) { + false => Center(child: CircularProgressIndicator()), + true => switch (widget.viewModel.currentBal == null) { + true => HomePageOnNoCurrent(widget: widget), + false => HomePageOnCurrent(widget: widget), + }, + }; + }, + ), + ); + // return Center(child: Text("Home page;")); + } +} + +class HomePageOnNoCurrent extends StatelessWidget { + const HomePageOnNoCurrent({super.key, required this.widget}); + + final HomePage widget; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: (widget.viewModel.bals.isEmpty) + ? Center(child: Text("Aucune BAL existante")) + : ListView( + children: [ + for (Bal bal in widget.viewModel.bals.where( + (el) => el.id != widget.viewModel.currentBal?.id, + )) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Card( + child: ListTile( + leading: switch (bal.state) { + BalState.pending => Icon(Icons.event), + BalState.ongoing => Icon(Icons.event_available), + BalState.ended => Icon(Icons.lock), + }, + title: Text(bal.name), + subtitle: switch (bal.state) { + BalState.pending => Text( + "À venir · Débute le ${bal.startTime.toString()}", + ), + BalState.ongoing => Text("En cours"), + BalState.ended => Text("Terminée"), + }, + trailing: switch (bal.state) { + BalState.pending => IconButton( + onPressed: () { + _moveToBal(context, bal.id); + }, + icon: Icon(Icons.edit), + ), + BalState.ongoing => IconButton( + onPressed: () { + _moveToBal(context, bal.id); + }, + icon: Icon(Icons.arrow_forward), + ), + BalState.ended => IconButton( + onPressed: () { + _moveToBal(context, bal.id); + }, + icon: Icon(Icons.analytics), + ), + }, + ), + ), + ), + ], + ), + ), + SizedBox(height: 10), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return CreateConfirmationPopup(viewModel: widget.viewModel); + }, + ); + }, + child: Text("Débuter une BAL"), + ), + SizedBox(height: 5), + ], + ); + } +} + +class HomePageOnCurrent extends StatelessWidget { + const HomePageOnCurrent({super.key, required this.widget}); + + final HomePage widget; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: (widget.viewModel.bals.isEmpty) + ? Center(child: Text("Aucune BAL existante")) + : ListView( + children: [ + for (Bal bal in widget.viewModel.bals.where( + (el) => el.id != widget.viewModel.currentBal?.id, + )) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Card( + child: ListTile( + leading: switch (bal.state) { + BalState.pending => Icon(Icons.event), + BalState.ongoing => Icon(Icons.event_available), + BalState.ended => Icon(Icons.lock), + }, + title: Text(bal.name), + subtitle: switch (bal.state) { + BalState.pending => Text( + "À venir · Débute le ${bal.startTime.toString()}", + ), + BalState.ongoing => Text("En cours"), + BalState.ended => Text("Terminée"), + }, + trailing: switch (bal.state) { + BalState.pending => IconButton( + onPressed: () { + _moveToBal(context, bal.id); + }, + icon: Icon(Icons.edit), + ), + BalState.ongoing => IconButton( + onPressed: () { + _moveToBal(context, bal.id); + }, + icon: Icon(Icons.arrow_forward), + ), + BalState.ended => IconButton( + onPressed: () { + _moveToBal(context, bal.id); + }, + icon: Icon(Icons.analytics), + ), + }, + ), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Card( + child: ListTile( + leading: Icon(Icons.event_available), + title: Text(widget.viewModel.currentBal!.name), + subtitle: Text("BAL en cours"), + trailing: IconButton( + onPressed: () { + _moveToBal(context, widget.viewModel.currentBal!.id); + }, + icon: Icon(Icons.arrow_forward), + ), + ), + ), + ), + SizedBox(height: 10), + ElevatedButton( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return CreateConfirmationPopup(viewModel: widget.viewModel); + }, + ); + }, + child: Text("Créer une BAL"), + ), + SizedBox(height: 5), + ], + ); + } +} + +void _moveToBal(BuildContext context, int id) { + context.goNamed("bal", pathParameters: {"id": id.toString()}); +} diff --git a/lib/ui/sell_page/view_model/sell_view_model.dart b/lib/ui/sell_page/view_model/sell_view_model.dart index b4dd5ae..85b1670 100644 --- a/lib/ui/sell_page/view_model/sell_view_model.dart +++ b/lib/ui/sell_page/view_model/sell_view_model.dart @@ -1,9 +1,18 @@ import 'package:flutter/widgets.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:seshat/data/repositories/bal_repository.dart'; +import 'package:seshat/domain/models/bal.dart'; import 'package:seshat/domain/models/book_instance.dart'; +import 'package:seshat/utils/command.dart'; +import 'package:seshat/utils/result.dart'; class SellViewModel extends ChangeNotifier { - SellViewModel(); + SellViewModel({required BalRepository balRepository}) + : _balRepository = balRepository { + load = Command0(_load)..execute(); + } + + final BalRepository _balRepository; bool _showScan = false; bool get showScan => _showScan; @@ -12,6 +21,12 @@ class SellViewModel extends ChangeNotifier { notifyListeners(); } + /* + * =============================== + * =====[ BOOKS & INSTANCES ]===== + * =============================== +*/ + final List _scannedBooks = []; get scannedBooks => _scannedBooks; void scanBook(BarcodeCapture barcode) { @@ -36,4 +51,44 @@ class SellViewModel extends ChangeNotifier { _scannedBooks.removeWhere((book) => book.id == id); notifyListeners(); } + + /* + * ================= + * =====[ BAL ]===== + * ================= +*/ + + Bal? _currentBal; + get currentBal => _currentBal; + + /* + * ================================= + * =====[ COMMAND AND LOADING ]===== + * ================================= +*/ + + late final Command0 load; + bool isLoaded = false; + + Future> _load() async { + final result2 = await _loadBal(); + isLoaded = true; + notifyListeners(); + return result2; + } + + Future> _loadBal() async { + final result = await _balRepository.getBals(); + switch (result) { + case Ok(): + _currentBal = result.value + .where((bal) => bal.state == BalState.ongoing) + .firstOrNull; + break; + case Error(): + break; + } + + return result; + } } diff --git a/lib/ui/sell_page/widgets/scan_screen.dart b/lib/ui/sell_page/widgets/scan_screen.dart index f083ab3..e7fb237 100644 --- a/lib/ui/sell_page/widgets/scan_screen.dart +++ b/lib/ui/sell_page/widgets/scan_screen.dart @@ -5,9 +5,9 @@ import 'package:seshat/ui/sell_page/view_model/sell_view_model.dart'; import 'package:seshat/ui/sell_page/widgets/manual_scan_popup.dart'; class ScanScreen extends StatefulWidget { - ScanScreen({super.key, required this.viewModel}); + const ScanScreen({super.key, required this.viewModel}); - SellViewModel viewModel; + final SellViewModel viewModel; @override State createState() => _ScanScreenState(); diff --git a/lib/ui/sell_page/widgets/sell_page.dart b/lib/ui/sell_page/widgets/sell_page.dart index 6fd69df..47da1e6 100644 --- a/lib/ui/sell_page/widgets/sell_page.dart +++ b/lib/ui/sell_page/widgets/sell_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:seshat/domain/models/book_instance.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'; import 'package:seshat/ui/sell_page/widgets/scan_screen.dart'; @@ -21,93 +23,133 @@ class _SellPageState extends State { body: ListenableBuilder( listenable: widget.viewModel, builder: (context, child) { - return Stack( - children: [ - SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(height: 6), - Expanded( - child: ListView( - children: [ - for (BookInstance bookInstance - in widget.viewModel.scannedBooks) - Card( - child: ListTile( - leading: Text( - "${bookInstance.price.toString()}€", - style: TextStyle(fontSize: 30), - ), - title: Text( - "Les chiens et la charrue · Patrick K. Dewdney ${bookInstance.id}", - ), - subtitle: Text("Union Étudiante Auvergne"), - trailing: IconButton( - onPressed: () { - widget.viewModel.deleteBook( - bookInstance.id, - ); - }, - icon: Icon(Icons.delete), - ), - ), - ), - ], + return switch (widget.viewModel.isLoaded) { + false => Center(child: CircularProgressIndicator()), + true => switch (widget.viewModel.currentBal) { + null => Center( + child: SizedBox( + width: 300, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Aucune bal n'est active.", + style: TextStyle(fontSize: 25), + textAlign: TextAlign.center, ), - ), - SizedBox(height: 40), - Text("Somme minimum requise : 20€"), - SizedBox( - width: 400, - child: TextField( - decoration: InputDecoration( - labelText: "Argent reçu", - helperText: - "L'argent reçu sera réparti automatiquement", - suffixText: "€", - border: OutlineInputBorder(), - ), - keyboardType: TextInputType.number, + SizedBox(height: 15), + Text( + "Vous devez créer puis activer une BAL pour pouvoir scanner des livres.", + textAlign: TextAlign.center, ), - ), - SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - onPressed: () { - widget.viewModel.sendSell(); - }, - icon: Icon(Icons.check), - style: ButtonStyle( - iconSize: WidgetStatePropertyAll(70), - ), - ), - SizedBox(width: 70), - IconButton( - onPressed: () { - widget.viewModel.showScan = true; - }, - icon: Icon(Icons.add), - style: ButtonStyle( - iconSize: WidgetStatePropertyAll(70), - elevation: WidgetStatePropertyAll(50), - ), - ), - ], - ), - SizedBox(height: 5), - ], + SizedBox(height: 30), + ElevatedButton( + onPressed: () { + context.go(Routes.home); + }, + child: Text("Gérer les BALs"), + ), + ], + ), ), ), + _ => Stack( + children: [ + SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 6), + Expanded( + child: ListView( + children: [ + for (BookInstance bookInstance + in widget.viewModel.scannedBooks) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + ), + child: Card( + child: ListTile( + leading: Text( + "${bookInstance.price.toString()}€", + style: TextStyle(fontSize: 30), + ), + title: Text( + "Les chiens et la charrue · Patrick K. Dewdney ${bookInstance.id}", + ), + subtitle: Text( + "Union Étudiante Auvergne", + ), + trailing: IconButton( + onPressed: () { + widget.viewModel.deleteBook( + bookInstance.id, + ); + }, + icon: Icon(Icons.delete), + ), + ), + ), + ), + ], + ), + ), + SizedBox(height: 40), + Text("Somme minimum requise : 20€"), + SizedBox( + width: 400, + child: TextField( + decoration: InputDecoration( + labelText: "Argent reçu", + helperText: + "L'argent reçu sera réparti automatiquement", + suffixText: "€", + border: OutlineInputBorder(), + ), + keyboardType: TextInputType.number, + ), + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + widget.viewModel.sendSell(); + }, + icon: Icon(Icons.check), + style: ButtonStyle( + iconSize: WidgetStatePropertyAll(70), + ), + ), + SizedBox(width: 70), + IconButton( + onPressed: () { + widget.viewModel.showScan = true; + }, + icon: Icon(Icons.add), + style: ButtonStyle( + iconSize: WidgetStatePropertyAll(70), + elevation: WidgetStatePropertyAll(50), + ), + ), + ], + ), + SizedBox(height: 5), + ], + ), + ), - (widget.viewModel.showScan) - ? ScanScreen(viewModel: widget.viewModel) - : SizedBox(), - ], - ); + (widget.viewModel.showScan) + ? ScanScreen(viewModel: widget.viewModel) + : SizedBox(), + ], + ), + }, + }; }, ), );