diff --git a/mobile/lib/controllers/cart.dart b/mobile/lib/controllers/cart.dart index d5c8011..ac5f6a0 100644 --- a/mobile/lib/controllers/cart.dart +++ b/mobile/lib/controllers/cart.dart @@ -1,16 +1,38 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:mobile/models/cart_item.dart'; import 'package:mobile/models/product.dart'; import 'package:mobile/results.dart'; +import 'package:path_provider/path_provider.dart'; class ProductIdException implements Exception {} -class CartController extends ChangeNotifier { +abstract class CartController extends ChangeNotifier { + List allCartItems(); + CartItem? withProductId(int productId); + void incrementAmount(int productId); + void decrementAmount(int productId); + bool willRemoveOnNextDecrement(int productId); + void removeCartItem(int productId); + void addToCart(Product product); + int totalItemsInCart(); + int totalPrice(); + void clearCart(); +} + +class CartControllerMemory extends CartController { final List cart = []; + CartControllerMemory(); + + @override List allCartItems() { return cart; } + @override CartItem? withProductId(int productId) { for (var i = 0; i < cart.length; i++) { if (cart[i].product.id == productId) { @@ -20,6 +42,7 @@ class CartController extends ChangeNotifier { return null; } + @override void incrementAmount(int productId) { final cartItem = withProductId(productId); if (cartItem == null) { @@ -29,6 +52,7 @@ class CartController extends ChangeNotifier { notifyListeners(); } + @override void decrementAmount(int productId) { final cartItem = withProductId(productId); if (cartItem == null) { @@ -41,6 +65,7 @@ class CartController extends ChangeNotifier { notifyListeners(); } + @override bool willRemoveOnNextDecrement(int productId) { final cartItem = withProductId(productId); if (cartItem == null) { @@ -49,6 +74,7 @@ class CartController extends ChangeNotifier { return cartItem.amount <= 1; } + @override void removeCartItem(int productId) { final cartItem = withProductId(productId); if (cartItem == null) { @@ -58,6 +84,7 @@ class CartController extends ChangeNotifier { notifyListeners(); } + @override void addToCart(Product product) { final cartItem = withProductId(product.id); if (cartItem == null) { @@ -68,17 +95,20 @@ class CartController extends ChangeNotifier { notifyListeners(); } + @override int totalItemsInCart() { return cart.fold(0, (prev, cartItem) => prev + cartItem.amount); } + @override int totalPrice() { return cart.fold( 0, (prev, cartItem) => - prev + cartItem.amount * cartItem.product.priceInDkkCents); + prev + cartItem.amount * cartItem.product.priceDkkCent); } + @override void clearCart() { cart.clear(); notifyListeners(); @@ -89,9 +119,63 @@ class CartController extends ChangeNotifier { } } -class CartItem { - final Product product; - int amount; +class CartControllerCache extends CartControllerMemory { + static Future get _cacheFile async { + final directory = await getApplicationCacheDirectory(); + return File("${directory.path}/cart.json").create(); + } - CartItem({required this.product, required this.amount}); + CartControllerCache() { + load(); + } + + void save() async { + final json = + jsonEncode(cart.map((cartItem) => CartItem.toJson(cartItem)).toList()); + (await _cacheFile).writeAsString(json); + } + + void load() async { + final json = await (await _cacheFile).readAsString(); + print("Loading cache: $json"); + if (json.isEmpty) { + return; + } + final res = jsonDecode(json); + final cartItems = (res as List) + .map(((cartItems) => CartItem.fromJson(cartItems))) + .toList(); + cart.insertAll(0, cartItems); + notifyListeners(); + } + + @override + void incrementAmount(int productId) { + super.incrementAmount(productId); + save(); + } + + @override + void decrementAmount(int productId) { + super.decrementAmount(productId); + save(); + } + + @override + void removeCartItem(int productId) { + super.removeCartItem(productId); + save(); + } + + @override + void addToCart(Product product) { + super.addToCart(product); + save(); + } + + @override + void clearCart() { + super.clearCart(); + save(); + } } diff --git a/mobile/lib/controllers/product.dart b/mobile/lib/controllers/product.dart index a6103fb..a555c94 100644 --- a/mobile/lib/controllers/product.dart +++ b/mobile/lib/controllers/product.dart @@ -23,7 +23,7 @@ class ProductController extends ChangeNotifier { } } - get filteredProducts { + List get filteredProducts { if (query.trim().isEmpty) { return products; } diff --git a/mobile/lib/controllers/receipt.dart b/mobile/lib/controllers/receipt.dart index 54a60cf..b599e02 100644 --- a/mobile/lib/controllers/receipt.dart +++ b/mobile/lib/controllers/receipt.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:mobile/models/cart_item.dart'; import 'package:mobile/models/product.dart'; -import 'package:mobile/controllers/cart.dart'; class ReceiptController extends ChangeNotifier { int nextId = 0; @@ -80,6 +80,6 @@ class ReceiptItem { ReceiptItem({required this.product, required this.amount}); int totalPrice() { - return product.priceInDkkCents * amount; + return product.priceDkkCent * amount; } } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 22e60b9..39c91c9 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -29,7 +29,7 @@ class MyApp extends StatelessWidget { ChangeNotifierProvider(create: (_) => RoutingController()), ChangeNotifierProvider( create: (_) => ProductController(server: server)), - ChangeNotifierProvider(create: (_) => CartController()), + ChangeNotifierProvider(create: (_) => CartControllerCache()), ChangeNotifierProvider(create: (_) => ReceiptController()), ChangeNotifierProvider(create: (_) => PayingStateController()), ChangeNotifierProvider(create: (_) => AddToCartStateController()), diff --git a/mobile/lib/models/cart_item.dart b/mobile/lib/models/cart_item.dart new file mode 100644 index 0000000..ee167f9 --- /dev/null +++ b/mobile/lib/models/cart_item.dart @@ -0,0 +1,19 @@ +import 'package:mobile/models/product.dart'; + +class CartItem { + final Product product; + int amount; + + CartItem({required this.product, required this.amount}); + + static Map toJson(CartItem cartItem) { + return { + "product": Product.toJson(cartItem.product), + "amount": cartItem.amount + }; + } + + CartItem.fromJson(Map json) + : product = Product.fromJson(json["product"]), + amount = json["amount"]; +} diff --git a/mobile/lib/models/product.dart b/mobile/lib/models/product.dart index 1b4b9e2..e5bd6e3 100644 --- a/mobile/lib/models/product.dart +++ b/mobile/lib/models/product.dart @@ -4,24 +4,35 @@ class Product { final int id; final String name; final String description; - final int priceInDkkCents; + final int priceDkkCent; final Coordinate? location; final String? barcode; Product({ required this.id, required this.name, - required this.priceInDkkCents, + required this.priceDkkCent, required this.description, this.location, this.barcode, }); + static Map toJson(Product product) { + return { + "id": product.id, + "name": product.name, + "description": product.description, + "price_dkk_cent": product.priceDkkCent, + "location": product.location, + "barcode": product.barcode, + }; + } + Product.fromJson(Map json) : id = json["id"], name = json["name"], description = json["description"], - priceInDkkCents = json["price_dkk_cent"], + priceDkkCent = json["price_dkk_cent"], location = null, barcode = json["barcode"]; } diff --git a/mobile/lib/models/user.dart b/mobile/lib/models/user.dart index f8e2cb4..923251e 100644 --- a/mobile/lib/models/user.dart +++ b/mobile/lib/models/user.dart @@ -6,30 +6,30 @@ class User { final String name; // balance is in øre - int balanceInDkkCents; + int balanceDkkCents; User({ required this.id, required this.email, required this.name, - required this.balanceInDkkCents, + required this.balanceDkkCents, }); User.fromJson(Map json) : email = json["email"], id = json["id"], name = json["name"], - balanceInDkkCents = json["balance_dkk_cent"]; + balanceDkkCents = json["balance_dkk_cent"]; void addBalanceFounds(int amount) { - balanceInDkkCents += amount; + balanceDkkCents += amount; } Result pay(int amount) { - if (balanceInDkkCents < amount) { + if (balanceDkkCents < amount) { return Err("User can not afford paying amount $amount"); } - balanceInDkkCents -= amount; - return Ok(balanceInDkkCents); + balanceDkkCents -= amount; + return Ok(balanceDkkCents); } } diff --git a/mobile/lib/pages/all_products_page.dart b/mobile/lib/pages/all_products_page.dart index 0ed3782..18bd4b0 100644 --- a/mobile/lib/pages/all_products_page.dart +++ b/mobile/lib/pages/all_products_page.dart @@ -129,7 +129,7 @@ class AllProductsPage extends StatelessWidget { itemBuilder: (_, idx) => ProductListItem( productId: products[idx].id, name: products[idx].name, - price: products[idx].priceInDkkCents, + price: products[idx].priceDkkCent, productPage: ProductPage(product: products[idx]), product: products[idx], ), diff --git a/mobile/lib/pages/cart_page.dart b/mobile/lib/pages/cart_page.dart index 0a3af96..3a997c9 100644 --- a/mobile/lib/pages/cart_page.dart +++ b/mobile/lib/pages/cart_page.dart @@ -13,7 +13,7 @@ import 'package:mobile/widgets/sized_card.dart'; import 'package:provider/provider.dart'; class CartItemView extends StatelessWidget { - final CartController cartRepo; + final CartControllerCache cartRepo; final int productId; final String name; final int price; @@ -150,7 +150,7 @@ class CartPage extends StatelessWidget { return Column( children: [ Expanded( - child: Consumer( + child: Consumer( builder: (_, cartRepo, __) { final cart = cartRepo.allCartItems(); return ListView.builder( @@ -159,7 +159,7 @@ class CartPage extends StatelessWidget { cartRepo: cartRepo, productId: cart[idx].product.id, name: cart[idx].product.name, - price: cart[idx].product.priceInDkkCents, + price: cart[idx].product.priceDkkCent, amount: cart[idx].amount), itemCount: cart.length, ); @@ -202,9 +202,9 @@ class CartPage extends StatelessWidget { onPressed: () { final productRepo = context .read(); - final CartController cartRepo = - context - .read(); + final CartControllerCache + cartRepo = context.read< + CartControllerCache>(); final productResult = productRepo .productWithBarcode( inputController.text); @@ -260,8 +260,8 @@ class CartPage extends StatelessWidget { if (!context.mounted) { return; } - final CartController cartRepo = - context.read(); + final CartControllerCache cartRepo = + context.read(); final productRepo = context.read(); final productResult = productRepo diff --git a/mobile/lib/pages/dashboard.dart b/mobile/lib/pages/dashboard.dart index 9ff2a2f..9e94be3 100644 --- a/mobile/lib/pages/dashboard.dart +++ b/mobile/lib/pages/dashboard.dart @@ -23,7 +23,7 @@ class Dashboard extends StatelessWidget { Widget build(BuildContext context) { final pageIndexProvider = Provider.of(context); int currentIndex = pageIndexProvider.currentIndex; - final CartController cartRepo = context.watch(); + final CartControllerCache cartRepo = context.watch(); return Scaffold( bottomNavigationBar: BottomNavigationBar( diff --git a/mobile/lib/pages/finish_shopping_page.dart b/mobile/lib/pages/finish_shopping_page.dart index 7449e60..6169311 100644 --- a/mobile/lib/pages/finish_shopping_page.dart +++ b/mobile/lib/pages/finish_shopping_page.dart @@ -14,7 +14,8 @@ class FinishShoppingPage extends StatelessWidget { @override Widget build(BuildContext context) { - final CartController cartController = context.read(); + final CartControllerCache cartController = + context.read(); final ReceiptController receiptRepo = context.read(); final PayingStateController payingStateRepo = context.watch(); @@ -33,7 +34,7 @@ class FinishShoppingPage extends StatelessWidget { child: ListView.builder( shrinkWrap: true, itemBuilder: (_, idx) => ReceiptItemView( - pricePerAmount: cart[idx].product.priceInDkkCents, + pricePerAmount: cart[idx].product.priceDkkCent, name: cart[idx].product.name, amount: cart[idx].amount), itemCount: cart.length), diff --git a/mobile/lib/pages/home_page.dart b/mobile/lib/pages/home_page.dart index 5d686b0..bcc1c66 100644 --- a/mobile/lib/pages/home_page.dart +++ b/mobile/lib/pages/home_page.dart @@ -38,7 +38,7 @@ class HomePage extends StatelessWidget { sessionController: sessionController, placeholder: const CircularProgressIndicator(), builder: (context, user) => Text( - "Saldo: ${formatDkkCents(user.balanceInDkkCents)}", + "Saldo: ${formatDkkCents(user.balanceDkkCents)}", style: Theme.of(context).textTheme.headlineSmall))), ), Expanded( diff --git a/mobile/lib/pages/product_page.dart b/mobile/lib/pages/product_page.dart index c162b12..da090a6 100644 --- a/mobile/lib/pages/product_page.dart +++ b/mobile/lib/pages/product_page.dart @@ -38,7 +38,7 @@ class ProductPage extends StatelessWidget { ), ), Text( - formatDkkCents(product.priceInDkkCents), + formatDkkCents(product.priceDkkCent), style: const TextStyle( fontSize: 16, ), @@ -66,7 +66,7 @@ class ProductPage extends StatelessWidget { style: Theme.of(context).textTheme.bodyLarge, ), Text( - formatDkkCents(product.priceInDkkCents), + formatDkkCents(product.priceDkkCent), style: Theme.of(context).textTheme.bodyLarge, ), Padding( @@ -90,7 +90,7 @@ class ProductPage extends StatelessWidget { duration: const Duration(seconds: 2), ); ScaffoldMessenger.of(context).removeCurrentSnackBar(); - final cartRepo = context.read(); + final cartRepo = context.read(); cartRepo.addToCart(product); ScaffoldMessenger.of(context).showSnackBar(snackBar); }, diff --git a/mobile/lib/pages/receipt_page.dart b/mobile/lib/pages/receipt_page.dart index 0225ce5..9adcedd 100644 --- a/mobile/lib/pages/receipt_page.dart +++ b/mobile/lib/pages/receipt_page.dart @@ -29,7 +29,7 @@ class ReceiptView extends StatelessWidget { shrinkWrap: true, itemBuilder: (_, idx) => ReceiptItemView( pricePerAmount: - receiptItems[idx].product.priceInDkkCents, + receiptItems[idx].product.priceDkkCent, name: receiptItems[idx].product.name, amount: receiptItems[idx].amount), itemCount: receiptItems.length), diff --git a/mobile/lib/pages/settings_pages/saldo.dart b/mobile/lib/pages/settings_pages/saldo.dart index 26d09d7..600192a 100644 --- a/mobile/lib/pages/settings_pages/saldo.dart +++ b/mobile/lib/pages/settings_pages/saldo.dart @@ -29,7 +29,7 @@ class SaldoSettingsPage extends StatelessWidget { sessionController: sessionController, placeholder: const CircularProgressIndicator(), builder: (context, user) => Text( - "Nuværende saldo: ${formatDkkCents(user.balanceInDkkCents)}", + "Nuværende saldo: ${formatDkkCents(user.balanceDkkCents)}", style: Theme.of(context).textTheme.bodyLarge), ), ElevatedButton.icon( diff --git a/mobile/lib/server/mock_server.dart b/mobile/lib/server/mock_server.dart index 127b7e8..dd4f183 100644 --- a/mobile/lib/server/mock_server.dart +++ b/mobile/lib/server/mock_server.dart @@ -11,90 +11,80 @@ class MockServer implements Server { Product( id: nextId++, name: "Minimælk", - priceInDkkCents: 1200, + priceDkkCent: 1200, description: "Konventionel minimælk med fedtprocent på 0,4%"), Product( id: nextId++, name: "Letmælk", - priceInDkkCents: 1300, + priceDkkCent: 1300, description: "Konventionel letmælk med fedtprocent på 1,5%", location: Coordinate(x: 1800, y: 100)), Product( id: nextId++, name: "Frilands Øko Supermælk", - priceInDkkCents: 2000, + priceDkkCent: 2000, description: "Økologisk mælk af frilandskøer med fedtprocent på 3,5%. Ikke homogeniseret eller pasteuriseret. Skaber store muskler og styrker knoglerne 💪"), Product( id: nextId++, name: "Øko Gulerødder 1 kg", - priceInDkkCents: 1000, + priceDkkCent: 1000, description: ""), Product( - id: nextId++, - name: "Øko Agurk", - priceInDkkCents: 1000, - description: ""), + id: nextId++, name: "Øko Agurk", priceDkkCent: 1000, description: ""), Product( id: nextId++, name: "Æbler 1 kg", - priceInDkkCents: 1000, + priceDkkCent: 1000, description: ""), Product( id: nextId++, name: "Basmati Ris", - priceInDkkCents: 2000, + priceDkkCent: 2000, description: ""), Product( id: nextId++, name: "Haribo Mix", - priceInDkkCents: 3000, + priceDkkCent: 3000, description: ""), - Product( - id: nextId++, name: "Smør", priceInDkkCents: 3000, description: ""), + Product(id: nextId++, name: "Smør", priceDkkCent: 3000, description: ""), Product( id: nextId++, name: "Harboe Cola", - priceInDkkCents: 500, + priceDkkCent: 500, description: ""), Product( id: nextId++, barcode: "5060337502900", name: "Monster Energi Drik", - priceInDkkCents: 1500, + priceDkkCent: 1500, description: ""), Product( id: nextId++, barcode: "5712870659220", name: "Amper Energi Drik", - priceInDkkCents: 750, + priceDkkCent: 750, description: ""), Product( id: nextId++, barcode: "5710326001937", name: "Danskvand Med Brus", - priceInDkkCents: 500, + priceDkkCent: 500, description: "Med smag a blåbær"), Product( - id: nextId++, - name: "Spaghetti", - priceInDkkCents: 1000, - description: ""), + id: nextId++, name: "Spaghetti", priceDkkCent: 1000, description: ""), Product( - id: nextId++, - name: "Rød Cecil", - priceInDkkCents: 6000, - description: ""), + id: nextId++, name: "Rød Cecil", priceDkkCent: 6000, description: ""), Product( id: nextId++, name: "Jägermeister 750 ml", - priceInDkkCents: 12000, + priceDkkCent: 12000, description: ""), Product( id: nextId++, barcode: "5711953068881", name: "Protein Chokoladedrik", - priceInDkkCents: 1500, + priceDkkCent: 1500, description: "Arla's protein chokolade drik der giver store muskler"), ]); } @@ -128,7 +118,7 @@ class MockServer implements Server { id: 0, email: "test@test.com", name: "testuser", - balanceInDkkCents: 10000)); + balanceDkkCents: 10000)); } @override diff --git a/mobile/lib/utils/price.dart b/mobile/lib/utils/price.dart index b606beb..6621e0a 100644 --- a/mobile/lib/utils/price.dart +++ b/mobile/lib/utils/price.dart @@ -1,6 +1,6 @@ import 'package:intl/intl.dart'; -String formatDkkCents(int priceInDkkCents) { +String formatDkkCents(int priceDkkCents) { final formatter = NumberFormat("###,##0.00", "da_DK"); - return "${formatter.format(priceInDkkCents / 100.0)} kr"; + return "${formatter.format(priceDkkCents / 100.0)} kr"; } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 580c03e..00c8bfe 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -268,7 +268,7 @@ packages: source: hosted version: "1.9.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 9b52646..1339ace 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: google_fonts: ^6.2.1 intl: ^0.20.2 http: ^1.3.0 + path_provider: ^2.1.5 dev_dependencies: flutter_test: