diff --git a/lib/data/repositories/bal_repository.dart b/lib/data/repositories/bal_repository.dart index 69da9b7..ccce2e4 100644 --- a/lib/data/repositories/bal_repository.dart +++ b/lib/data/repositories/bal_repository.dart @@ -1,5 +1,8 @@ import 'package:seshat/data/services/api_client.dart'; +import 'package:seshat/domain/models/accounting.dart'; import 'package:seshat/domain/models/bal.dart'; +import 'package:seshat/domain/models/bal_stats.dart'; +import 'package:seshat/domain/models/enums.dart'; import 'package:seshat/utils/result.dart'; class BalRepository { @@ -7,6 +10,7 @@ class BalRepository { final ApiClient _apiClient; List? _bals; + Accounting? accounting; Future>> getBals() async { if (_bals != null) { @@ -95,4 +99,71 @@ class BalRepository { await _getBalsNoCache(); return result; } + + Future> getBalStats(int id) async { + return _apiClient.getBalStats(id); + } + + Future> getAccountingNoCache(int balId) async { + final result = await _apiClient.getAccounting(balId); + switch (result) { + case Ok(): + accounting = result.value; + break; + default: + } + return result; + } + + Future> getAccounting(int balId) async { + if (accounting != null) { + return Result.ok(accounting!); + } + final result = await _apiClient.getAccounting(balId); + switch (result) { + case Ok(): + accounting = result.value; + break; + default: + } + return result; + } + + Future> returnToId( + int balId, + int ownerId, + ReturnType type, + ) async { + final result = await _apiClient.returnToId(balId, ownerId, type.name); + switch (result) { + case Ok(): + switch (type) { + case ReturnType.books: + final owner = accounting?.owners + .where((el) => el.ownerId == ownerId) + .firstOrNull; + if (owner?.owedMoney == 0) { + accounting?.owners.removeWhere((el) => el.ownerId == ownerId); + } + owner?.owed = []; + owner?.owedInstances = []; + break; + case ReturnType.money: + final owner = accounting?.owners + .where((el) => el.ownerId == ownerId) + .firstOrNull; + if (owner?.owed == null || owner!.owed.isEmpty) { + accounting?.owners.removeWhere((el) => el.ownerId == ownerId); + } + owner?.owedMoney = 0; + break; + case ReturnType.all: + accounting?.owners.removeWhere((el) => el.ownerId == ownerId); + break; + } + break; + default: + } + return result; + } } diff --git a/lib/data/services/api_client.dart b/lib/data/services/api_client.dart index d565c8f..383f269 100644 --- a/lib/data/services/api_client.dart +++ b/lib/data/services/api_client.dart @@ -1,10 +1,14 @@ import 'dart:convert'; +import 'dart:math'; +import 'package:flutter/rendering.dart'; 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'; +import 'package:seshat/domain/models/accounting.dart'; import 'package:seshat/domain/models/bal.dart'; +import 'package:seshat/domain/models/bal_stats.dart'; import 'package:seshat/domain/models/book.dart'; import 'package:seshat/domain/models/book_instance.dart'; import 'package:seshat/domain/models/owner.dart'; @@ -12,6 +16,12 @@ import 'package:seshat/domain/models/search_result.dart'; import 'package:seshat/utils/command.dart'; import 'package:seshat/utils/result.dart'; +extension StringExtension on String { + String capitalize() { + return "${this[0].toUpperCase()}${this.substring(1).toLowerCase()}"; + } +} + typedef AuthHeaderProvider = String? Function(); class ApiClient { @@ -37,12 +47,86 @@ class ApiClient { return headers; } + /* + * ======================== + * =====< Accounting >===== + * ======================== +*/ + + Future> getAccounting(int balId) async { + final client = Client(); + try { + final headers = await _getHeaders(); + final response = await client.get( + Uri.parse("https://$apiBasePath/bal/$balId/accounting"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + return Result.ok(Accounting.fromJSON(json)); + } else { + throw "Unknown error"; + } + } catch (e) { + return Result.error(Exception(e)); + } finally { + client.close(); + } + } + + Future> returnToId(int balId, int ownerId, String type) async { + final client = Client(); + try { + final headers = await _getHeaders({"Content-Type": "application/json"}); + final body = jsonEncode({"return_type": type.capitalize()}); + debugPrint(body); + final response = await client.post( + Uri.parse( + "https://$apiBasePath/bal/${balId.toString()}/accounting/return/${ownerId.toString()}", + ), + headers: headers, + body: body, + ); + if (response.statusCode == 200) { + return Result.ok(response); + } else { + throw "Unknown error ${response.statusCode.toString()}"; + } + } catch (e) { + debugPrint(e.toString()); + return Result.error(Exception(e)); + } finally { + client.close(); + } + } + /* * ================= * =====[ BAL ]===== * ================= */ + Future> getBalStats(int id) async { + final client = Client(); + try { + final headers = await _getHeaders(); + final response = await client.get( + Uri.parse("https://$apiBasePath/bal/${id.toString()}/stats"), + headers: headers, + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + return Result.ok(BalStats.fromJSON(json)); + } else { + throw "Unknown error"; + } + } catch (e) { + return Result.error(Exception(e)); + } finally { + client.close(); + } + } + Future> stopBal(int id) async { final client = Client(); try { diff --git a/lib/domain/models/accounting.dart b/lib/domain/models/accounting.dart new file mode 100644 index 0000000..c491783 --- /dev/null +++ b/lib/domain/models/accounting.dart @@ -0,0 +1,20 @@ +import 'package:seshat/domain/models/book.dart'; +import 'package:seshat/domain/models/return_owner.dart'; + +class Accounting { + Accounting(this.owners, this.books); + List owners; + Map books; + + factory Accounting.fromJSON(Map json) { + final ownersJson = json["owners"] as List; + List owners = ownersJson + .map((el) => ReturnOwner.fromJSON(el)) + .toList(); + + final booksJson = json["book_map"] as Map; + Map books = {}; + booksJson.forEach((k, v) => books[k] = Book.fromJSON(v)); + return Accounting(owners, books); + } +} diff --git a/lib/domain/models/bal_stats.dart b/lib/domain/models/bal_stats.dart new file mode 100644 index 0000000..cbc6d08 --- /dev/null +++ b/lib/domain/models/bal_stats.dart @@ -0,0 +1,28 @@ +class BalStats { + BalStats( + this.totalOwnedCollectedMoney, + this.totalDifferentOwners, + this.totalCollectedMoney, + this.balId, + this.totalSoldBooks, + this.totalOwnedSoldBooks, + ); + + int balId; + double totalCollectedMoney; + double totalOwnedCollectedMoney; + int totalDifferentOwners; + int totalSoldBooks; + int totalOwnedSoldBooks; + + factory BalStats.fromJSON(Map json) { + return BalStats( + json["total_owned_collected_money"], + json["total_different_owners"], + json["total_collected_money"], + json["bal_id"], + json["total_sold_books"], + json["total_owned_sold_books"], + ); + } +} diff --git a/lib/domain/models/enums.dart b/lib/domain/models/enums.dart new file mode 100644 index 0000000..359b36b --- /dev/null +++ b/lib/domain/models/enums.dart @@ -0,0 +1 @@ +enum ReturnType { books, money, all } diff --git a/lib/domain/models/return_owner.dart b/lib/domain/models/return_owner.dart index e7b7dc5..cdef6c4 100644 --- a/lib/domain/models/return_owner.dart +++ b/lib/domain/models/return_owner.dart @@ -1,11 +1,22 @@ import 'package:seshat/domain/models/book_instance.dart'; import 'package:seshat/domain/models/owner.dart'; +import 'package:seshat/domain/models/search_result.dart'; class ReturnOwner { - ReturnOwner(this.owner, this.owned, this.ownedMoney); - Owner owner; - List owned; - double ownedMoney; + ReturnOwner(this.ownerId, this.owedInstances, this.owedMoney); + int ownerId; + Owner? owner; + List owedInstances; + List owed = []; + double owedMoney; - // factory ReturnOwner.fromJSON(Map) {} + factory ReturnOwner.fromJSON(Map json) { + int owner = json["owner_id"]; + double owedMoney = json["owed_money"]; + final owedJson = json["owed_books"] as List; + List owed = owedJson + .map((el) => BookInstance.fromJSON(el)) + .toList(); + return ReturnOwner(owner, owed, owedMoney); + } } diff --git a/lib/routing/router.dart b/lib/routing/router.dart index 434c333..c330f51 100644 --- a/lib/routing/router.dart +++ b/lib/routing/router.dart @@ -55,7 +55,8 @@ GoRouter router(AuthRepository authRepository) => GoRouter( pageBuilder: (context, state) { final viewModel = BalViewModel( balRepository: context.read(), - id: int.tryParse(state.pathParameters["id"] ?? ""), + id: int.parse(state.pathParameters["id"] ?? ""), + ownerRepository: context.read(), ); return NoTransitionPage(child: BalPage(viewModel: viewModel)); }, diff --git a/lib/ui/bal_page/view_model/bal_view_model.dart b/lib/ui/bal_page/view_model/bal_view_model.dart index 0fa1857..f9947f1 100644 --- a/lib/ui/bal_page/view_model/bal_view_model.dart +++ b/lib/ui/bal_page/view_model/bal_view_model.dart @@ -1,19 +1,37 @@ import 'package:flutter/widgets.dart'; import 'package:seshat/data/repositories/bal_repository.dart'; +import 'package:seshat/data/repositories/owner_repository.dart'; import 'package:seshat/domain/models/bal.dart'; +import 'package:seshat/domain/models/bal_stats.dart'; +import 'package:seshat/domain/models/book.dart'; +import 'package:seshat/domain/models/book_instance.dart'; +import 'package:seshat/domain/models/enums.dart'; +import 'package:seshat/domain/models/return_owner.dart'; +import 'package:seshat/domain/models/search_result.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 { + BalViewModel({ + required BalRepository balRepository, + required this.id, + required OwnerRepository ownerRepository, + }) : _balRepository = balRepository, + _ownerRepository = ownerRepository { load = Command0(_load)..execute(); } final BalRepository _balRepository; + final OwnerRepository _ownerRepository; + + /* + * ===================== + * =====< General >===== + * ===================== +*/ Bal? _bal; - int? id; + int id; Bal? get bal => _bal; bool isABalOngoing = false; @@ -64,9 +82,55 @@ class BalViewModel extends ChangeNotifier { return result; } + /* + * ============================ + * =====< State Specific >===== + * ============================ +*/ + + // Specific to ended state + List? owedToOwners; + double? totalOwed; + BalStats? stats; + + Future applyAccountingOwners( + List owners, + Map books, + ) async { + owedToOwners = owners; + for (ReturnOwner owedToOwner in owedToOwners!) { + var res = await _ownerRepository.getOwnerById(owedToOwner.ownerId); + switch (res) { + case Ok(): + owedToOwner.owner = res.value; + break; + default: + } + owedToOwner.owed = []; + for (BookInstance instance in owedToOwner.owedInstances) { + final bookId = instance.bookId; + owedToOwner.owed.add(SearchResult(instance, books[bookId.toString()]!)); + } + } + } + + Future> returnById(ReturnType type, int ownerId) async { + final result = await _balRepository.returnToId(id, ownerId, type); + final result2 = await _balRepository.getAccounting(id); + switch (result2) { + case Ok(): + applyAccountingOwners(result2.value.owners, result2.value.books); + break; + case Error(): + debugPrint(result2.error.toString()); + } + notifyListeners(); + return result; + } + /* * ================================= - * =====[ COMMAND AND LOADING ]===== + * =====< COMMAND AND LOADING >===== * ================================= */ @@ -78,20 +142,32 @@ class BalViewModel extends ChangeNotifier { final result1 = await _loadBal(); switch (result1) { case Ok(): - isLoaded = true; + isLoaded = (_bal == null || _bal?.state != BalState.ended) + ? true + : false; break; default: break; } + debugPrint("$isLoaded"); + if (_bal?.state == BalState.ended) { + final result2 = await _loadEnded(); + debugPrint("Hello"); + switch (result2) { + case Ok(): + isLoaded = true; + break; + case Error(): + debugPrint("No ${result2.error}"); + break; + } + } notifyListeners(); return result1; } Future> _loadBal() async { - if (id == null) { - return Result.error(Exception("No id given")); - } - final result = await _balRepository.balById(id!); + final result = await _balRepository.balById(id); switch (result) { case Ok(): _bal = result.value; @@ -102,4 +178,22 @@ class BalViewModel extends ChangeNotifier { return result; } + + Future> _loadEnded() async { + final result = await _balRepository.getAccountingNoCache(id); + switch (result) { + case Ok(): + applyAccountingOwners(result.value.owners, result.value.books); + break; + default: + } + final result2 = await _balRepository.getBalStats(id); + switch (result2) { + case Ok(): + stats = result2.value; + break; + default: + } + return result; + } } diff --git a/lib/ui/bal_page/widget/ended/bal_ended_screen.dart b/lib/ui/bal_page/widget/ended/bal_ended_screen.dart index 71fc37a..694b85e 100644 --- a/lib/ui/bal_page/widget/ended/bal_ended_screen.dart +++ b/lib/ui/bal_page/widget/ended/bal_ended_screen.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:seshat/domain/models/enums.dart'; +import 'package:seshat/domain/models/return_owner.dart'; +import 'package:seshat/domain/models/search_result.dart'; import 'package:seshat/ui/bal_page/view_model/bal_view_model.dart'; import 'package:seshat/ui/core/ui/navigation_bar.dart'; @@ -31,17 +34,186 @@ class _BalEndedScreenState extends State controller: tabController, tabs: [ Tab(text: "Statistiques"), - Tab(text: "Livres à rendre"), + Tab(text: "À rendre"), ], ), ), - body: TabBarView( - controller: tabController, - children: [ - Center(child: Text("Coming soon")), - Center(child: Text("Coming soon.")), - ], + body: ListenableBuilder( + listenable: widget.viewModel, + builder: (context, child) { + return TabBarView( + controller: tabController, + children: [ + StatsTab(viewModel: widget.viewModel), + ReturnTab(viewModel: widget.viewModel), + ], + ); + }, ), ); } } + +class ReturnTab extends StatelessWidget { + const ReturnTab({super.key, required this.viewModel}); + final BalViewModel viewModel; + + @override + Widget build(BuildContext context) { + return ListView( + children: [ + (viewModel.owedToOwners?.isEmpty ?? true) + ? Center( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Text("Tout a été rendu"), + ), + ) + : SizedBox(), + for (ReturnOwner owedToOwner in viewModel.owedToOwners!) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Card( + child: ListTile( + title: Text( + "${owedToOwner.owner?.firstName ?? 'Erreur'} ${owedToOwner.owner?.lastName ?? 'Erreur'}", + ), + subtitle: Text( + "${owedToOwner.owed.length.toString()} livres · ${owedToOwner.owedMoney.toString()}€", + ), + trailing: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => ReturnPopup( + viewModel: viewModel, + currentOwedToOwner: owedToOwner, + ), + ); + }, + icon: Icon(Icons.visibility), + ), + ), + ), + ), + ], + ); + } +} + +class ReturnPopup extends StatelessWidget { + const ReturnPopup({ + super.key, + required this.viewModel, + required this.currentOwedToOwner, + }); + final BalViewModel viewModel; + final ReturnOwner currentOwedToOwner; + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: viewModel, + builder: (context, child) { + return AlertDialog( + title: Text( + "${currentOwedToOwner.owner?.firstName ?? 'Erreur'} ${currentOwedToOwner.owner?.lastName ?? 'Erreur'}", + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Argent à rembourser : ${currentOwedToOwner.owedMoney}€", + style: TextStyle(fontSize: 20), + ), + SizedBox( + height: 300, + width: 300, + child: ListView( + children: [ + (currentOwedToOwner.owed.isEmpty) + ? Text("Tout a été rendu") + : SizedBox(), + for (SearchResult book in currentOwedToOwner.owed) + Card( + child: ListTile( + title: Text(book.book.title), + subtitle: Text( + "${book.book.author} · ${currentOwedToOwner.owner!.firstName[0].toUpperCase()}${currentOwedToOwner.owner!.lastName[0].toUpperCase()}${book.instance.price}", + ), + ), + ), + ], + ), + ), + ], + ), + actions: [ + (currentOwedToOwner.owedMoney != 0) + ? TextButton( + onPressed: () async { + await viewModel.returnById( + ReturnType.money, + currentOwedToOwner.ownerId, + ); + }, + child: Text("Argent rendu"), + ) + : SizedBox(), + (currentOwedToOwner.owed.isNotEmpty) + ? TextButton( + onPressed: () async { + await viewModel.returnById( + ReturnType.books, + currentOwedToOwner.ownerId, + ); + }, + child: Text("Livres rendus"), + ) + : SizedBox(), + TextButton( + onPressed: () async { + await viewModel.returnById( + ReturnType.all, + currentOwedToOwner.ownerId, + ); + if (context.mounted) { + Navigator.of(context).pop(); + } + }, + child: Text("Tout rendu"), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text("Annuler"), + ), + ], + ); + }, + ); + } +} + +class StatsTab extends StatelessWidget { + const StatsTab({super.key, required this.viewModel}); + final BalViewModel viewModel; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Card( + child: ListTile( + title: Text("0€", style: TextStyle(fontSize: 30)), + subtitle: Text("Total d'argent collecté"), + ), + ), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 265984c..f140cfe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -105,6 +113,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "577aeac8ca414c25333334d7c4bb246775234c0e44b38b10a82b559dd4d764e7" + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index fe631f5..0ffed5e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: rxdart: ^0.28.0 intl: ^0.20.2 flutter_launcher_icons: ^0.14.4 + fl_chart: ^1.0.0 dev_dependencies: flutter_test: