diff --git a/mobile/lib/controllers/receipt.dart b/mobile/lib/controllers/receipt.dart index b599e02..b039d50 100644 --- a/mobile/lib/controllers/receipt.dart +++ b/mobile/lib/controllers/receipt.dart @@ -1,85 +1,16 @@ -import 'package:flutter/material.dart'; -import 'package:mobile/models/cart_item.dart'; -import 'package:mobile/models/product.dart'; +import 'package:mobile/controllers/session.dart'; +import 'package:mobile/models/receipt.dart'; +import 'package:mobile/results.dart'; +import 'package:mobile/server/server.dart'; -class ReceiptController extends ChangeNotifier { - int nextId = 0; - final List receipts = []; +class ReceiptController { + final Server server; + final SessionController sessionController; - List allReceipts() { - return receipts; - } + ReceiptController({required this.server, required this.sessionController}); - List sortedReceiptsByDate() { - List clonedReceipts = []; - for (var i = 0; i < receipts.length; i++) { - clonedReceipts.add(receipts[i]); - } - clonedReceipts.sort((a, b) => b.date.compareTo(a.date)); - return clonedReceipts; - } - - Receipt? receiptWithId(int id) { - for (var i = 0; i < receipts.length; i++) { - if (receipts[i].id == id) { - return receipts[i]; - } - } - return null; - } - - void createReceipt(List cartItems) { - List receiptItems = []; - for (var i = 0; i < cartItems.length; i++) { - final ReceiptItem receiptItem = ReceiptItem( - amount: cartItems[i].amount, product: cartItems[i].product); - receiptItems.add(receiptItem); - } - receipts.add(Receipt( - date: DateTime.now(), receiptItems: receiptItems, id: nextId++)); - notifyListeners(); - } -} - -class Receipt { - final int id; - final DateTime date; - final List receiptItems; - - Receipt({required this.date, required this.receiptItems, required this.id}); - - List allReceiptItems() { - return receiptItems; - } - - ReceiptItem? withProductId(int productId) { - for (var i = 0; i < receiptItems.length; i++) { - if (receiptItems[i].product.id == productId) { - return receiptItems[i]; - } - } - return null; - } - - int totalPrice() { - var result = 0; - for (var i = 0; i < receiptItems.length; i++) { - result += receiptItems[i].totalPrice(); - } - return result; - } - - String dateFormatted() { - return "${date.day}-${date.month}-${date.year}"; - } -} - -class ReceiptItem { - final Product product; - final int amount; - ReceiptItem({required this.product, required this.amount}); - - int totalPrice() { - return product.priceDkkCent * amount; + Future> receiptWithId(int id) async { + return await sessionController.requestWithSession( + (Server server, String token) => server.oneReceipt(token, id)); } } diff --git a/mobile/lib/controllers/receipt_header.dart b/mobile/lib/controllers/receipt_header.dart new file mode 100644 index 0000000..b32524f --- /dev/null +++ b/mobile/lib/controllers/receipt_header.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:mobile/controllers/session.dart'; +import 'package:mobile/models/receipt.dart'; +import 'package:mobile/results.dart'; +import 'package:mobile/server/server.dart'; + +class ReceiptHeaderController extends ChangeNotifier { + List _receiptHeaders = []; + + final Server server; + + final SessionController sessionController; + + ReceiptHeaderController( + {required this.server, required this.sessionController}) { + fetchReceiptsFromServer(); + } + + Future fetchReceiptsFromServer() async { + final res = await sessionController.requestWithSession( + (Server server, String token) => server.allReceipts(token)); + + switch (res) { + case Ok, String>(value: final receiptHeaders): + _receiptHeaders = receiptHeaders; + case Err, String>(): + break; + } + notifyListeners(); + } + + List receiptHeadersSortedByDate() { + List clonedReceiptHeaders = []; + for (var i = 0; i < _receiptHeaders.length; i++) { + clonedReceiptHeaders.add(_receiptHeaders[i]); + } + clonedReceiptHeaders.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + return clonedReceiptHeaders; + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 4dd10c7..7012d69 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:mobile/controllers/receipt_header.dart'; import 'package:mobile/controllers/session.dart'; import 'package:mobile/pages/dashboard.dart'; import 'package:mobile/pages/log_in_page.dart'; @@ -56,7 +57,12 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider( create: (_) => CartControllerCache( server: server, sessionController: sessionController)), - ChangeNotifierProvider(create: (_) => ReceiptController()), + Provider( + create: (_) => ReceiptController( + server: server, sessionController: sessionController)), + ChangeNotifierProvider( + create: (_) => ReceiptHeaderController( + server: server, sessionController: sessionController)), ChangeNotifierProvider(create: (_) => PayingStateController()), ChangeNotifierProvider(create: (_) => AddToCartStateController()), ChangeNotifierProvider(create: (_) => LocationImageController()), diff --git a/mobile/lib/models/receipt.dart b/mobile/lib/models/receipt.dart new file mode 100644 index 0000000..d6a16c6 --- /dev/null +++ b/mobile/lib/models/receipt.dart @@ -0,0 +1,72 @@ +class Receipt { + final int id; + final DateTime timestamp; + final List receiptItems; + + Receipt( + {required this.timestamp, required this.receiptItems, required this.id}); + + List allReceiptItems() { + return receiptItems; + } + + ReceiptItem? withProductId(int productId) { + for (var i = 0; i < receiptItems.length; i++) { + if (receiptItems[i].productId == productId) { + return receiptItems[i]; + } + } + return null; + } + + int totalPrice() { + var result = 0; + for (var i = 0; i < receiptItems.length; i++) { + result += receiptItems[i].totalPrice(); + } + return result; + } + + Receipt.fromJson(Map json) + : id = json["receipt_id"], + timestamp = DateTime.parse(json["timestamp"]), + receiptItems = (json["products"] as List) + .map(((receiptItem) => ReceiptItem.fromJson(receiptItem))) + .toList(); +} + +class ReceiptItem { + final int productId; + final String name; + final int priceDkkCent; + final int amount; + ReceiptItem( + {required this.productId, + required this.name, + required this.priceDkkCent, + required this.amount}); + + int totalPrice() { + return priceDkkCent * amount; + } + + ReceiptItem.fromJson(Map json) + : productId = json["product_id"], + name = json["name"], + priceDkkCent = json["price_dkk_cent"], + amount = json["amount"]; +} + +class ReceiptHeader { + final int id; + final DateTime timestamp; + final int totalDkkCent; + + ReceiptHeader( + {required this.timestamp, required this.id, required this.totalDkkCent}); + + ReceiptHeader.fromJson(Map json) + : id = json["id"], + totalDkkCent = json["total_dkk_cent"], + timestamp = DateTime.parse(json["timestamp"]); +} diff --git a/mobile/lib/pages/all_receipts_page.dart b/mobile/lib/pages/all_receipts_page.dart index adf3b5c..ade4f42 100644 --- a/mobile/lib/pages/all_receipts_page.dart +++ b/mobile/lib/pages/all_receipts_page.dart @@ -1,18 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:mobile/controllers/receipt_header.dart'; +import 'package:mobile/models/receipt.dart'; import 'package:mobile/pages/receipt_page.dart'; -import 'package:mobile/controllers/receipt.dart'; +import 'package:mobile/utils/date.dart'; import 'package:mobile/utils/price.dart'; import 'package:provider/provider.dart'; class ReceiptsListItem extends StatelessWidget { - final String dateFormatted; - final int totalPrice; - final ReceiptPage receiptPage; - const ReceiptsListItem( - {super.key, - required this.dateFormatted, - required this.totalPrice, - required this.receiptPage}); + final ReceiptHeader receiptHeader; + const ReceiptsListItem({super.key, required this.receiptHeader}); @override Widget build(BuildContext context) { @@ -21,14 +17,19 @@ class ReceiptsListItem extends StatelessWidget { child: InkWell( borderRadius: const BorderRadius.all(Radius.circular(10)), onTap: () { - Navigator.of(context) - .push(MaterialPageRoute(builder: (context) => receiptPage)); + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => ReceiptPage( + receiptId: receiptHeader.id, + ))); }, child: Container( padding: const EdgeInsets.all(20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [Text(dateFormatted), Text(formatDkkCents(totalPrice))], + children: [ + Text(dateFormatted(receiptHeader.timestamp)), + Text(formatDkkCents(receiptHeader.totalDkkCent)) + ], ), ), ), @@ -41,20 +42,21 @@ class AllReceiptsPage extends StatelessWidget { @override Widget build(BuildContext context) { + context.read().fetchReceiptsFromServer(); return Column( children: [ - Expanded(child: Consumer( - builder: (_, receiptRepo, __) { - final allReceipts = receiptRepo.sortedReceiptsByDate(); + Expanded(child: Consumer( + builder: (_, receiptHeaderController, __) { + final allReceiptHeaders = + receiptHeaderController.receiptHeadersSortedByDate(); return ListView.builder( shrinkWrap: true, itemBuilder: (_, idx) { return ReceiptsListItem( - dateFormatted: allReceipts[idx].dateFormatted(), - totalPrice: allReceipts[idx].totalPrice(), - receiptPage: ReceiptPage(receipt: allReceipts[idx])); + receiptHeader: allReceiptHeaders[idx], + ); }, - itemCount: allReceipts.length, + itemCount: allReceiptHeaders.length, ); }, )), diff --git a/mobile/lib/pages/finish_shopping_page.dart b/mobile/lib/pages/finish_shopping_page.dart index 2b1a6c5..d375497 100644 --- a/mobile/lib/pages/finish_shopping_page.dart +++ b/mobile/lib/pages/finish_shopping_page.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:mobile/controllers/routing.dart'; import 'package:mobile/controllers/cart.dart'; import 'package:mobile/controllers/paying_state.dart'; -import 'package:mobile/controllers/receipt.dart'; import 'package:mobile/results.dart'; import 'package:mobile/utils/price.dart'; import 'package:mobile/widgets/primary_button.dart'; @@ -16,7 +15,6 @@ class FinishShoppingPage extends StatelessWidget { Widget build(BuildContext context) { final CartControllerCache cartController = context.read(); - final ReceiptController receiptRepo = context.read(); final PayingStateController payingStateRepo = context.watch(); final cart = cartController.allCartItems(); @@ -70,8 +68,8 @@ class FinishShoppingPage extends StatelessWidget { actions: [ TextButton( onPressed: () => - Navigator.pop(context, 'OK'), - child: const Text('OK'), + Navigator.pop(context, 'Ok'), + child: const Text('Ok'), ), ], ), @@ -80,7 +78,6 @@ class FinishShoppingPage extends StatelessWidget { payingStateRepo.reset(); return; } - receiptRepo.createReceipt(cart); payingStateRepo.next(); await Future.delayed(const Duration(seconds: 1)); cartController.clearCart(); diff --git a/mobile/lib/pages/receipt_page.dart b/mobile/lib/pages/receipt_page.dart index 9adcedd..7ccc0e6 100644 --- a/mobile/lib/pages/receipt_page.dart +++ b/mobile/lib/pages/receipt_page.dart @@ -1,15 +1,19 @@ import 'package:flutter/material.dart'; import 'package:mobile/controllers/receipt.dart'; +import 'package:mobile/models/receipt.dart'; +import 'package:mobile/results.dart'; +import 'package:mobile/utils/date.dart'; import 'package:mobile/utils/price.dart'; import 'package:mobile/widgets/receipt_item.dart'; +import 'package:provider/provider.dart'; class ReceiptView extends StatelessWidget { - final Receipt receipt; - const ReceiptView({super.key, required this.receipt}); + final int receiptId; + const ReceiptView({super.key, required this.receiptId}); @override Widget build(BuildContext context) { - final receiptItems = receipt.allReceiptItems(); + final receiptController = context.read(); return SafeArea( child: Column( @@ -19,34 +23,65 @@ class ReceiptView extends StatelessWidget { Expanded( child: Container( margin: const EdgeInsets.all(20), - child: Column( - children: [ - Text(receipt.dateFormatted()), - Expanded( - child: Column( - children: [ - ListView.builder( - shrinkWrap: true, - itemBuilder: (_, idx) => ReceiptItemView( - pricePerAmount: - receiptItems[idx].product.priceDkkCent, - name: receiptItems[idx].product.name, - amount: receiptItems[idx].amount), - itemCount: receiptItems.length), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Total:", - style: TextStyle(fontWeight: FontWeight.bold), + child: FutureBuilder( + future: receiptController.receiptWithId(receiptId), + builder: (context, snapshot) { + final error = snapshot.error; + if (error != null) { + throw error; + } + final receipt = snapshot.data; + switch (receipt) { + case null: + return const CircularProgressIndicator(); + case Err(value: final message): + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'Ok'), + child: const Text('Ok'), + ), + ], ), - Text(formatDkkCents(receipt.totalPrice())), - ], - ), - ], - )), - ], - ), + ); + return Container(); + case Ok(value: final receipt): + return Column( + children: [ + Text(dateFormatted(receipt.timestamp)), + Expanded( + child: Column( + children: [ + ListView.builder( + shrinkWrap: true, + itemBuilder: (_, idx) => ReceiptItemView( + pricePerAmount: receipt + .receiptItems[idx].priceDkkCent, + name: receipt.receiptItems[idx].name, + amount: + receipt.receiptItems[idx].amount), + itemCount: receipt.receiptItems.length), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Total:", + style: TextStyle( + fontWeight: FontWeight.bold), + ), + Text(formatDkkCents(receipt.totalPrice())), + ], + ), + ], + )), + ], + ); + } + }), ), ), ], @@ -56,12 +91,12 @@ class ReceiptView extends StatelessWidget { } class ReceiptPage extends StatelessWidget { - final Receipt receipt; + final int receiptId; - const ReceiptPage({super.key, required this.receipt}); + const ReceiptPage({super.key, required this.receiptId}); @override Widget build(BuildContext context) { - return Scaffold(body: ReceiptView(receipt: receipt)); + return Scaffold(body: ReceiptView(receiptId: receiptId)); } } diff --git a/mobile/lib/pages/settings_pages/saldo.dart b/mobile/lib/pages/settings_pages/saldo.dart index 5214825..7975451 100644 --- a/mobile/lib/pages/settings_pages/saldo.dart +++ b/mobile/lib/pages/settings_pages/saldo.dart @@ -42,8 +42,8 @@ class SaldoSettingsPage extends StatelessWidget { content: Text('Serverfejl: $message'), actions: [ TextButton( - onPressed: () => Navigator.pop(context, 'OK'), - child: const Text('OK'), + onPressed: () => Navigator.pop(context, 'Ok'), + child: const Text('Ok'), ), ], )); diff --git a/mobile/lib/server/backend_server.dart b/mobile/lib/server/backend_server.dart index 95eb651..5373776 100644 --- a/mobile/lib/server/backend_server.dart +++ b/mobile/lib/server/backend_server.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:mobile/models/cart_item.dart'; import 'package:mobile/models/product.dart'; +import 'package:mobile/models/receipt.dart'; import 'package:mobile/models/user.dart'; import 'package:mobile/results.dart'; import 'package:mobile/server/server.dart'; @@ -30,7 +31,7 @@ class BackendServer implements Server { .then((res) => json.decode(res.body)); if (res["ok"]) { return Ok((res["products"] as List) - .map(((product) => Product.fromJson(product))) + .map(((productJson) => Product.fromJson(productJson))) .toList()); } else { return Err(res["msg"]); @@ -149,4 +150,43 @@ class BackendServer implements Server { return Err(res["msg"]); } } + + @override + Future, String>> allReceipts(String token) async { + final res = await http.get( + Uri.parse("$_apiUrl/receipts/all"), + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "Session-Token": token + }, + ).then((res) => json.decode(res.body)); + + if (res["ok"]) { + return Ok((res["receipts"] as List) + .map(((receiptHeaderJson) => + ReceiptHeader.fromJson(receiptHeaderJson))) + .toList()); + } else { + return Err(res["msg"]); + } + } + + @override + Future> oneReceipt(String token, int id) async { + final res = await http.get( + Uri.parse("$_apiUrl/receipts/one?receipt_id=$id"), + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "Session-Token": token + }, + ).then((res) => json.decode(res.body)); + + if (res["ok"]) { + return Ok((Receipt.fromJson(res["receipt"] as Map))); + } else { + return Err(res["msg"]); + } + } } diff --git a/mobile/lib/server/mock_server.dart b/mobile/lib/server/mock_server.dart index e068640..1de4965 100644 --- a/mobile/lib/server/mock_server.dart +++ b/mobile/lib/server/mock_server.dart @@ -1,6 +1,7 @@ import 'package:mobile/models/cart_item.dart'; import 'package:mobile/models/coordinate.dart'; import 'package:mobile/models/product.dart'; +import 'package:mobile/models/receipt.dart'; import 'package:mobile/models/user.dart'; import 'package:mobile/results.dart'; import 'package:mobile/server/server.dart'; @@ -132,4 +133,17 @@ class MockServer implements Server { Future> addBalance(String token) async { return const Ok(null); } + + @override + Future, String>> allReceipts(String token) async { + return Ok([ + ReceiptHeader(timestamp: DateTime.now(), id: 0, totalDkkCent: 1242431) + ]); + } + + @override + Future> oneReceipt(String token, int id) async { + return Ok(Receipt( + timestamp: DateTime.now(), id: id, receiptItems: [])); + } } diff --git a/mobile/lib/server/server.dart b/mobile/lib/server/server.dart index cd3ba7d..9d75e23 100644 --- a/mobile/lib/server/server.dart +++ b/mobile/lib/server/server.dart @@ -1,5 +1,6 @@ import 'package:mobile/models/cart_item.dart'; import 'package:mobile/models/product.dart'; +import 'package:mobile/models/receipt.dart'; import 'package:mobile/models/user.dart'; import 'package:mobile/results.dart'; @@ -24,4 +25,7 @@ abstract class Server { String token, List cartItems); Future> addBalance(String token); + + Future, String>> allReceipts(String token); + Future> oneReceipt(String token, int id); } diff --git a/mobile/lib/utils/date.dart b/mobile/lib/utils/date.dart new file mode 100644 index 0000000..62b3627 --- /dev/null +++ b/mobile/lib/utils/date.dart @@ -0,0 +1,3 @@ +String dateFormatted(DateTime date) { + return "${date.day}-${date.month}-${date.year} ${date.hour}:${date.minute}"; +}