From 6403fccf680c0015d7a25fec47e7aae675fc9dba Mon Sep 17 00:00:00 2001 From: Tom Bursch Date: Tue, 13 Aug 2024 20:20:29 +0200 Subject: [PATCH] feat: List remaining items for shopping lists --- .../app/controller/shoppinglist/schemas.py | 5 + .../shoppinglist/shoppinglist_controller.py | 43 ++- kitchenowl/lib/cubits/item_edit_cubit.dart | 1 + kitchenowl/lib/cubits/planner_cubit.dart | 1 + kitchenowl/lib/cubits/recipe_cubit.dart | 11 +- kitchenowl/lib/cubits/shoppinglist_cubit.dart | 272 +++++++++--------- kitchenowl/lib/models/shoppinglist.dart | 40 ++- .../lib/pages/household_page/planner.dart | 7 +- .../pages/household_page/shoppinglist.dart | 87 +++--- kitchenowl/lib/services/api/shoppinglist.dart | 37 +-- .../lib/services/storage/mem_storage.dart | 28 -- .../lib/services/storage/temp_storage.dart | 46 --- kitchenowl/lib/services/transaction.dart | 26 +- .../services/transactions/shoppinglist.dart | 240 +++++++--------- .../shopping_list_choice_chip.dart | 40 +++ 15 files changed, 442 insertions(+), 442 deletions(-) create mode 100644 kitchenowl/lib/widgets/shopping_list/shopping_list_choice_chip.dart diff --git a/backend/app/controller/shoppinglist/schemas.py b/backend/app/controller/shoppinglist/schemas.py index 694861707..671c81e6d 100644 --- a/backend/app/controller/shoppinglist/schemas.py +++ b/backend/app/controller/shoppinglist/schemas.py @@ -1,6 +1,11 @@ from marshmallow import fields, Schema, EXCLUDE +class GetShoppingLists(Schema): + orderby = fields.Integer() + recent_limit = fields.Integer(load_default=9, validate=lambda x: x > 0 and x <= 60) + + class AddItemByName(Schema): name = fields.String(required=True) description = fields.String() diff --git a/backend/app/controller/shoppinglist/shoppinglist_controller.py b/backend/app/controller/shoppinglist/shoppinglist_controller.py index 056b17327..8c3b6e67b 100644 --- a/backend/app/controller/shoppinglist/shoppinglist_controller.py +++ b/backend/app/controller/shoppinglist/shoppinglist_controller.py @@ -11,6 +11,7 @@ ) from app.helpers import validate_args, authorize_household from .schemas import ( + GetShoppingLists, RemoveItem, UpdateDescription, AddItemByName, @@ -44,9 +45,41 @@ def createShoppinglist(args, household_id): @shoppinglistHousehold.route("", methods=["GET"]) @jwt_required() @authorize_household() -def getShoppinglists(household_id): +@validate_args(GetShoppingLists) +def getShoppinglists(args, household_id): shoppinglists = Shoppinglist.all_from_household(household_id) - return jsonify([e.obj_to_dict() for e in shoppinglists]) + recentItems = {} + for shoppinglist in shoppinglists: + recentItems[shoppinglist.id] = [ + e.item.obj_to_dict() | {"description": e.description} + for e in History.get_recent(shoppinglist.id, args["recent_limit"]) + ] + + orderby = [Item.name] + if "orderby" in args and args["orderby"] == 1: + orderby = [Item.ordering == 0, Item.ordering] + + items = {} + for shoppinglist in shoppinglists: + items[shoppinglist.id] = ( + ShoppinglistItems.query.filter( + ShoppinglistItems.shoppinglist_id == shoppinglist.id + ) + .join(ShoppinglistItems.item) + .order_by(*orderby, Item.name) + .all() + ) + + return jsonify( + [ + shoppinglist.obj_to_dict() + | { + "recentItems": recentItems[shoppinglist.id], + "items": [e.obj_to_item_dict() for e in items[shoppinglist.id]], + } + for shoppinglist in shoppinglists + ] + ) @shoppinglist.route("/", methods=["POST"]) @@ -114,6 +147,9 @@ def updateItemDescription(args, id, item_id): @jwt_required() @validate_args(GetItems) def getAllShoppingListItems(args, id): + ''' + Deprecated in favor of including it directly in the shopping list + ''' shoppinglist = Shoppinglist.find_by_id(id) if not shoppinglist: raise NotFoundRequest() @@ -139,6 +175,9 @@ def getAllShoppingListItems(args, id): @jwt_required() @validate_args(GetRecentItems) def getRecentItems(args, id): + ''' + Deprecated in favor of including it directly in the shopping list + ''' shoppinglist = Shoppinglist.find_by_id(id) if not shoppinglist: raise NotFoundRequest() diff --git a/kitchenowl/lib/cubits/item_edit_cubit.dart b/kitchenowl/lib/cubits/item_edit_cubit.dart index 1d5d4ceec..d52448a5e 100644 --- a/kitchenowl/lib/cubits/item_edit_cubit.dart +++ b/kitchenowl/lib/cubits/item_edit_cubit.dart @@ -68,6 +68,7 @@ class ItemEditCubit extends Cubit { if (shoppingList != null && state.hasChangedDescription(_item)) { await TransactionHandler.getInstance() .runTransaction(TransactionShoppingListUpdateItem( + household: household!, shoppinglist: shoppingList!, item: _item, description: state.description, diff --git a/kitchenowl/lib/cubits/planner_cubit.dart b/kitchenowl/lib/cubits/planner_cubit.dart index 5f853a360..253159ec7 100644 --- a/kitchenowl/lib/cubits/planner_cubit.dart +++ b/kitchenowl/lib/cubits/planner_cubit.dart @@ -77,6 +77,7 @@ class PlannerCubit extends Cubit { ) { return TransactionHandler.getInstance() .runTransaction(TransactionShoppingListAddRecipeItems( + household: household, shoppinglist: shoppingList, items: items, )); diff --git a/kitchenowl/lib/cubits/recipe_cubit.dart b/kitchenowl/lib/cubits/recipe_cubit.dart index 41048d983..6739920e2 100644 --- a/kitchenowl/lib/cubits/recipe_cubit.dart +++ b/kitchenowl/lib/cubits/recipe_cubit.dart @@ -16,13 +16,11 @@ class RecipeCubit extends Cubit { final TransactionHandler _transactionHandler; RecipeCubit(Household? household, Recipe recipe, int? selectedYields) - : this.forTesting(TransactionHandler.getInstance(), household, recipe, selectedYields); + : this.forTesting(TransactionHandler.getInstance(), household, recipe, + selectedYields); - RecipeCubit.forTesting( - TransactionHandler transactionHandler, - this.household, - Recipe recipe, - int? selectedYields) + RecipeCubit.forTesting(TransactionHandler transactionHandler, this.household, + Recipe recipe, int? selectedYields) : _transactionHandler = transactionHandler, super(RecipeState(recipe: recipe, selectedYields: selectedYields)) { refresh(); @@ -69,6 +67,7 @@ class RecipeCubit extends Cubit { if (shoppingList != null) { await _transactionHandler .runTransaction(TransactionShoppingListAddRecipeItems( + household: household!, shoppinglist: shoppingList, items: state.dynamicRecipe.items .where((item) => state.selectedItems.contains(item.name)) diff --git a/kitchenowl/lib/cubits/shoppinglist_cubit.dart b/kitchenowl/lib/cubits/shoppinglist_cubit.dart index 4acbcad38..3b2eaeda3 100644 --- a/kitchenowl/lib/cubits/shoppinglist_cubit.dart +++ b/kitchenowl/lib/cubits/shoppinglist_cubit.dart @@ -52,6 +52,7 @@ class ShoppinglistCubit extends Cubit { final item = ShoppinglistItem.fromJson(data["item"]); TransactionHandler.getInstance().runTransaction( TransactionShoppingListAddItem( + household: household, shoppinglist: ShoppingList.fromJson(data["shoppinglist"]), item: item, ), @@ -68,7 +69,8 @@ class ShoppinglistCubit extends Cubit { void onShoppinglistItemRemove(dynamic data) { final item = ShoppinglistItem.fromJson(data["item"]); TransactionHandler.getInstance().runTransaction( - TransactionShoppingListDeleteItem( + TransactionShoppingListRemoveItem( + household: household, shoppinglist: ShoppingList.fromJson(data["shoppinglist"]), item: item, ), @@ -77,7 +79,9 @@ class ShoppinglistCubit extends Cubit { ); if (state.selectedShoppinglist == null || data["shoppinglist"]["id"] != state.selectedShoppinglist?.id || - !state.listItems.map((e) => e.id).contains(data["item"]["id"])) return; + !state.selectedShoppinglist!.items + .map((e) => e.id) + .contains(data["item"]["id"])) return; removeLocally(item); } @@ -89,20 +93,25 @@ class ShoppinglistCubit extends Cubit { if (_state.selectedShoppinglist == null) return; await TransactionHandler.getInstance() .runTransaction(TransactionShoppingListAddItem( + household: household, shoppinglist: _state.selectedShoppinglist!, item: item, )); await refresh(query: ''); } - void addLocally(ShoppinglistItem item) { + void addLocally(ShoppinglistItem item, [int? shoppinglistId]) { final _state = state; - if (_state.selectedShoppinglist == null) return; - final l = List.of(_state.listItems); + shoppinglistId ??= _state.selectedShoppinglist?.id; + if (shoppinglistId == null) return; + final shoppinglist = _state.shoppinglists[shoppinglistId]; + if (shoppinglist == null) return; + + final l = List.of(shoppinglist.items); l.removeWhere((e) => e.id == item.id || e.name == item.name); l.add(item); ShoppinglistSorting.sortShoppinglistItems(l, state.sorting); - final recent = List.of(_state.recentItems); + final recent = List.of(shoppinglist.recentItems); recent.removeWhere((e) => e.id == item.id); if (_state is SearchShoppinglistCubitState) { final result = List.of(_state.result); @@ -114,9 +123,16 @@ class ShoppinglistCubit extends Cubit { item, ); } - emit(_state.copyWith(listItems: l, recentItems: recent, result: result)); + emit(_state.copyWith( + shoppinglists: _replaceAndUpdateShoppingLists(_state.shoppinglists, + shoppinglist.copyWith(items: l, recentItems: recent)), + result: result, + )); } else { - emit(state.copyWith(listItems: l, recentItems: recent)); + emit(state.copyWith( + shoppinglists: _replaceAndUpdateShoppingLists(_state.shoppinglists, + shoppinglist.copyWith(items: l, recentItems: recent)), + )); } } @@ -124,7 +140,8 @@ class ShoppinglistCubit extends Cubit { final _state = state; removeLocally(item); if (!await TransactionHandler.getInstance() - .runTransaction(TransactionShoppingListDeleteItem( + .runTransaction(TransactionShoppingListRemoveItem( + household: household, shoppinglist: _state.selectedShoppinglist!, item: item, ))) { @@ -132,12 +149,16 @@ class ShoppinglistCubit extends Cubit { } } - void removeLocally(ShoppinglistItem item) { + void removeLocally(ShoppinglistItem item, [int? shoppinglistId]) { final _state = state; - if (_state.selectedShoppinglist == null) return; - final l = List.of(_state.listItems); + shoppinglistId ??= _state.selectedShoppinglist?.id; + if (shoppinglistId == null) return; + final shoppinglist = _state.shoppinglists[shoppinglistId]; + if (shoppinglist == null) return; + + final l = List.of(shoppinglist.items); l.remove(item); - final recent = List.of(_state.recentItems); + final recent = List.of(shoppinglist.recentItems); recent.insert(0, ItemWithDescription.fromItem(item: item)); if (recent.length > recentItemCountProvider()) { recent.removeLast(); @@ -155,9 +176,15 @@ class ShoppinglistCubit extends Cubit { ), ); } - emit(_state.copyWith(listItems: l, recentItems: recent, result: result)); + emit(_state.copyWith( + shoppinglists: _replaceAndUpdateShoppingLists(_state.shoppinglists, + shoppinglist.copyWith(items: l, recentItems: recent)), + result: result)); } else { - emit(state.copyWith(listItems: l, recentItems: recent)); + emit(state.copyWith( + shoppinglists: _replaceAndUpdateShoppingLists(_state.shoppinglists, + shoppinglist.copyWith(items: l, recentItems: recent)), + )); } } @@ -179,9 +206,9 @@ class ShoppinglistCubit extends Cubit { } final selectedItems = _state.selectedListItems .sorted((a, b) => a.id?.compareTo(b.id ?? 0) ?? -1); - final l = List.of(_state.listItems); + final l = List.of(_state.selectedShoppinglist!.items); l.removeWhere(selectedItems.contains); - final recent = List.of(_state.recentItems); + final recent = List.of(_state.selectedShoppinglist!.recentItems); recent.insertAll( 0, selectedItems.map((e) => ItemWithDescription.fromItem(item: e)), @@ -203,17 +230,24 @@ class ShoppinglistCubit extends Cubit { } } emit(_state.copyWith( - listItems: l, - recentItems: recent, + shoppinglists: _replaceAndUpdateShoppingLists( + _state.shoppinglists, + _state.selectedShoppinglist! + .copyWith(items: l, recentItems: recent)), result: result, selectedListItems: [], )); } else { - emit(state - .copyWith(listItems: l, recentItems: recent, selectedListItems: [])); + emit(_state.copyWith( + shoppinglists: _replaceAndUpdateShoppingLists( + _state.shoppinglists, + _state.selectedShoppinglist! + .copyWith(items: l, recentItems: recent)), + selectedListItems: [])); } if (!await TransactionHandler.getInstance() - .runTransaction(TransactionShoppingListDeleteItems( + .runTransaction(TransactionShoppingListRemoveItems( + household: household, shoppinglist: _state.selectedShoppinglist!, items: selectedItems, ))) { @@ -227,8 +261,11 @@ class ShoppinglistCubit extends Cubit { } void setSorting(ShoppinglistSorting sorting, [bool savePreference = true]) { - if (state is! SearchShoppinglistCubitState && state.listItems != const []) { - ShoppinglistSorting.sortShoppinglistItems(state.listItems, sorting); + if (state is! SearchShoppinglistCubitState && + state.selectedShoppinglist != null && + state.selectedShoppinglist?.items != const []) { + ShoppinglistSorting.sortShoppinglistItems( + state.selectedShoppinglist!.items, sorting); } if (savePreference) { PreferenceStorage.getInstance() @@ -248,12 +285,8 @@ class ShoppinglistCubit extends Cubit { ); } emit(state.copyWith( - selectedShoppinglist: shoppingList, - recentItems: [], - listItems: [], + selectedShoppinglistId: shoppingList.id, )); - _initialLoad(); - refresh(); } Future refresh({String? query}) { @@ -274,49 +307,34 @@ class ShoppinglistCubit extends Cubit { } Future _initialLoad() async { - final shoppingLists = await TransactionHandler.getInstance().runTransaction( - TransactionShoppingListGet(household: household), - forceOffline: true, - ); + final shoppingLists = await TransactionHandler.getInstance() + .runTransaction( + TransactionShoppingListGet(household: household), + forceOffline: true, + ) + .then((lists) => Map.fromEntries(lists + .map((e) => e.id != null ? MapEntry(e.id!, e) : null) + .whereNotNull())); final shoppinglist = - state.selectedShoppinglist ?? shoppingLists.firstOrNull; + state.selectedShoppinglist ?? shoppingLists.values.firstOrNull; if (shoppinglist == null) return; - Future> items = - TransactionHandler.getInstance().runTransaction( - TransactionShoppingListGetItems( - shoppinglist: shoppinglist, - sorting: state.sorting, - ), - forceOffline: true, - ); - Future> categories = TransactionHandler.getInstance().runTransaction( TransactionCategoriesGet(household: household), forceOffline: true, ); - final recent = TransactionHandler.getInstance().runTransaction( - TransactionShoppingListGetRecentItems( - shoppinglist: shoppinglist, - itemsCount: recentItemCountProvider(), - ), - forceOffline: true, - ); - List loadedShoppinglistItems = await items; final resState = LoadingShoppinglistCubitState( shoppinglists: shoppingLists, - selectedShoppinglist: shoppinglist, - listItems: loadedShoppinglistItems, - recentItems: await recent, + selectedShoppinglistId: shoppinglist.id, categories: await categories, sorting: state.sorting, selectedListItems: state.selectedListItems - .map((e) => (loadedShoppinglistItems) - .firstWhereOrNull((item) => item.id == e.id)) + .map((e) => + shoppinglist.items.firstWhereOrNull((item) => item.id == e.id)) .whereNotNull() .toList(), ); @@ -329,11 +347,12 @@ class ShoppinglistCubit extends Cubit { Future _refresh([String? query]) async { // Get required information late ShoppinglistCubitState resState; - if (state.recentItems.isEmpty && - state.listItems.isEmpty && - (query == null || query.isEmpty)) { + if (state.selectedShoppinglistId == null || + (state.selectedShoppinglist?.items.isEmpty ?? true) && + (state.selectedShoppinglist?.recentItems.isEmpty ?? true) && + (query == null || query.isEmpty)) { emit(LoadingShoppinglistCubitState( - selectedShoppinglist: state.selectedShoppinglist, + selectedShoppinglistId: state.selectedShoppinglistId, shoppinglists: state.shoppinglists, sorting: state.sorting, categories: state.categories, @@ -342,20 +361,17 @@ class ShoppinglistCubit extends Cubit { } final shoppingLists = await TransactionHandler.getInstance() - .runTransaction(TransactionShoppingListGet(household: household)); + .runTransaction(TransactionShoppingListGet(household: household)) + .then((lists) => Map.fromEntries(lists + .map((e) => e.id != null ? MapEntry(e.id!, e) : null) + .whereNotNull())); - final shoppinglist = - state.selectedShoppinglist ?? shoppingLists.firstOrNull; + final selectedShoppinglistId = + state.selectedShoppinglistId ?? shoppingLists.values.firstOrNull?.id; - if (shoppinglist == null) return; + if (selectedShoppinglistId == null) return; - Future> items = - TransactionHandler.getInstance().runTransaction( - TransactionShoppingListGetItems( - shoppinglist: shoppinglist, - sorting: state.sorting, - ), - ); + final shoppinglist = shoppingLists[selectedShoppinglistId]; Future> categories = TransactionHandler.getInstance() .runTransaction(TransactionCategoriesGet(household: household)); @@ -385,9 +401,8 @@ class ShoppinglistCubit extends Cubit { .toList()); List loadedItems = await searchItems; - List loadedShoppinglistItems = await items; - _mergeShoppinglistItems(loadedItems, loadedShoppinglistItems); + _mergeShoppinglistItems(loadedItems, shoppinglist?.items); if (loadedItems.isEmpty || !loadedItems .any((e) => e.name.toLowerCase() == queryName.toLowerCase())) { @@ -398,36 +413,26 @@ class ShoppinglistCubit extends Cubit { } resState = SearchShoppinglistCubitState( shoppinglists: shoppingLists, - selectedShoppinglist: shoppinglist, + selectedShoppinglistId: selectedShoppinglistId, result: loadedItems, query: query, - listItems: loadedShoppinglistItems, categories: await categories, sorting: state.sorting, - recentItems: state.recentItems, selectedListItems: state.selectedListItems - .map((e) => loadedShoppinglistItems - .firstWhereOrNull((item) => item.id == e.id)) + .map((e) => + shoppinglist?.items.firstWhereOrNull((item) => item.id == e.id)) .whereNotNull() .toList(), ); } else { - final recent = TransactionHandler.getInstance() - .runTransaction(TransactionShoppingListGetRecentItems( - shoppinglist: shoppinglist, - itemsCount: recentItemCountProvider(), - )); - List loadedShoppinglistItems = await items; resState = ShoppinglistCubitState( shoppinglists: shoppingLists, - selectedShoppinglist: shoppinglist, - listItems: loadedShoppinglistItems, - recentItems: await recent, + selectedShoppinglistId: selectedShoppinglistId, categories: await categories, sorting: state.sorting, selectedListItems: state.selectedListItems - .map((e) => (loadedShoppinglistItems) - .firstWhereOrNull((item) => item.id == e.id)) + .map((e) => + shoppinglist?.items.firstWhereOrNull((item) => item.id == e.id)) .whereNotNull() .toList(), ); @@ -440,9 +445,9 @@ class ShoppinglistCubit extends Cubit { void _mergeShoppinglistItems( List items, - List shoppinglist, + List? shoppinglist, ) { - if (shoppinglist.isEmpty) return; + if (shoppinglist == null || shoppinglist.isEmpty) return; for (int i = 0; i < items.length; i++) { final shoppinglistItem = shoppinglist.firstWhereOrNull((e) => e.id == items[i].id); @@ -452,41 +457,54 @@ class ShoppinglistCubit extends Cubit { } } } + + Map _replaceAndUpdateShoppingLists( + Map shoppinglists, ShoppingList shoppingList) { + if (shoppingList.id == null) return shoppinglists; + + final res = Map.of(shoppinglists); + res[shoppingList.id!] = shoppingList; + return res; + } } class ShoppinglistCubitState extends Equatable { - final List shoppinglists; - final ShoppingList? selectedShoppinglist; - final List listItems; - final List recentItems; + final Map shoppinglists; + final int? selectedShoppinglistId; final List categories; final ShoppinglistSorting sorting; final List selectedListItems; + final ShoppingList? _selectedShoppinglist; - const ShoppinglistCubitState({ - this.shoppinglists = const [], - required this.selectedShoppinglist, - this.listItems = const [], - this.recentItems = const [], + const ShoppinglistCubitState._({ + this.shoppinglists = const {}, this.categories = const [], this.sorting = ShoppinglistSorting.alphabetical, this.selectedListItems = const [], - }); + this.selectedShoppinglistId = null, + }) : this._selectedShoppinglist = null; + + ShoppinglistCubitState({ + this.shoppinglists = const {}, + required this.selectedShoppinglistId, + this.categories = const [], + this.sorting = ShoppinglistSorting.alphabetical, + this.selectedListItems = const [], + }) : _selectedShoppinglist = shoppinglists[selectedShoppinglistId]; + + ShoppingList? get selectedShoppinglist => _selectedShoppinglist; ShoppinglistCubitState copyWith({ - List? shoppinglists, - ShoppingList? selectedShoppinglist, - List? listItems, - List? recentItems, + Map? shoppinglists, + int? selectedShoppinglistId, List? categories, ShoppinglistSorting? sorting, List? selectedListItems, }) => ShoppinglistCubitState( shoppinglists: shoppinglists ?? this.shoppinglists, - selectedShoppinglist: selectedShoppinglist ?? this.selectedShoppinglist, - listItems: listItems ?? this.listItems, - recentItems: recentItems ?? this.recentItems, + selectedShoppinglistId: + selectedShoppinglistId ?? this.selectedShoppinglistId, categories: categories ?? this.categories, sorting: sorting ?? this.sorting, selectedListItems: selectedListItems ?? this.selectedListItems, @@ -495,9 +513,7 @@ class ShoppinglistCubitState extends Equatable { @override List get props => [ shoppinglists, - selectedShoppinglist, - listItems, - recentItems, + selectedShoppinglistId, categories, sorting, selectedListItems, @@ -507,18 +523,16 @@ class ShoppinglistCubitState extends Equatable { class LoadingShoppinglistCubitState extends ShoppinglistCubitState { const LoadingShoppinglistCubitState({ super.sorting, - super.selectedShoppinglist, + super.selectedShoppinglistId, super.shoppinglists, super.categories, super.selectedListItems, - super.listItems, - super.recentItems, - }); + }) : super._(); @override ShoppinglistCubitState copyWith({ - List? shoppinglists, - ShoppingList? selectedShoppinglist, + Map? shoppinglists, + int? selectedShoppinglistId, List? listItems, List? recentItems, List? categories, @@ -528,11 +542,10 @@ class LoadingShoppinglistCubitState extends ShoppinglistCubitState { LoadingShoppinglistCubitState( sorting: sorting ?? this.sorting, shoppinglists: shoppinglists ?? this.shoppinglists, - selectedShoppinglist: selectedShoppinglist ?? this.selectedShoppinglist, + selectedShoppinglistId: + selectedShoppinglistId ?? this.selectedShoppinglistId, categories: categories ?? this.categories, selectedListItems: selectedListItems ?? this.selectedListItems, - listItems: listItems ?? this.listItems, - recentItems: recentItems ?? this.recentItems, ); } @@ -540,11 +553,9 @@ class SearchShoppinglistCubitState extends ShoppinglistCubitState { final String query; final List result; - const SearchShoppinglistCubitState({ - super.shoppinglists = const [], - required super.selectedShoppinglist, - super.listItems = const [], - super.recentItems = const [], + SearchShoppinglistCubitState({ + super.shoppinglists = const {}, + required super.selectedShoppinglistId, super.categories = const [], super.sorting = ShoppinglistSorting.alphabetical, this.query = "", @@ -554,10 +565,8 @@ class SearchShoppinglistCubitState extends ShoppinglistCubitState { @override ShoppinglistCubitState copyWith({ - List? shoppinglists, - ShoppingList? selectedShoppinglist, - List? listItems, - List? recentItems, + Map? shoppinglists, + int? selectedShoppinglistId, List? categories, ShoppinglistSorting? sorting, List? result, @@ -565,9 +574,8 @@ class SearchShoppinglistCubitState extends ShoppinglistCubitState { }) => SearchShoppinglistCubitState( shoppinglists: shoppinglists ?? this.shoppinglists, - selectedShoppinglist: selectedShoppinglist ?? this.selectedShoppinglist, - listItems: listItems ?? this.listItems, - recentItems: recentItems ?? this.recentItems, + selectedShoppinglistId: + selectedShoppinglistId ?? this.selectedShoppinglistId, sorting: sorting ?? this.sorting, categories: categories ?? this.categories, query: query, diff --git a/kitchenowl/lib/models/shoppinglist.dart b/kitchenowl/lib/models/shoppinglist.dart index 9a48040ed..75b8d2224 100644 --- a/kitchenowl/lib/models/shoppinglist.dart +++ b/kitchenowl/lib/models/shoppinglist.dart @@ -1,26 +1,52 @@ +import 'package:kitchenowl/models/item.dart'; import 'package:kitchenowl/models/model.dart'; class ShoppingList extends Model { final int? id; final String name; + final List items; + final List recentItems; - const ShoppingList({this.id, required this.name}); + const ShoppingList({ + this.id, + required this.name, + this.items = const [], + this.recentItems = const [], + }); - factory ShoppingList.fromJson(Map map) => ShoppingList( - id: map['id'], - name: map['name'], - ); + factory ShoppingList.fromJson(Map map) { + List items = const []; + if (map.containsKey('items')) { + items = List.from(map['items'].map((e) => ShoppinglistItem.fromJson(e))); + } + List recentItems = const []; + if (map.containsKey('recentItems')) { + recentItems = List.from( + map['recentItems'].map((e) => ItemWithDescription.fromJson(e))); + } + + return ShoppingList( + id: map['id'], + name: map['name'], + items: items, + recentItems: recentItems, + ); + } ShoppingList copyWith({ String? name, + List? items, + List? recentItems, }) => ShoppingList( id: id, name: name ?? this.name, + items: items ?? this.items, + recentItems: recentItems ?? this.recentItems, ); @override - List get props => [id, name]; + List get props => [id, name, items, recentItems]; @override Map toJson() => { @@ -31,5 +57,7 @@ class ShoppingList extends Model { Map toJsonWithId() => toJson() ..addAll({ "id": id, + "items": items.map((e) => e.toJsonWithId()).toList(), + "recentItems": recentItems.map((e) => e.toJsonWithId()).toList(), }); } diff --git a/kitchenowl/lib/pages/household_page/planner.dart b/kitchenowl/lib/pages/household_page/planner.dart index 0c97407c5..4bb20315e 100644 --- a/kitchenowl/lib/pages/household_page/planner.dart +++ b/kitchenowl/lib/pages/household_page/planner.dart @@ -368,8 +368,11 @@ class _PlannerPageState extends State { selectText: AppLocalizations.of(ctx)!.addNumberIngredients, plans: (cubit.state as LoadedPlannerCubitState).recipePlans, title: AppLocalizations.of(ctx)!.addItemTitle, - shoppingLists: - BlocProvider.of(context).state.shoppinglists, + shoppingLists: BlocProvider.of(context) + .state + .shoppinglists + .values + .toList(), handleResult: (list, res) async { list ??= BlocProvider.of(context) .state diff --git a/kitchenowl/lib/pages/household_page/shoppinglist.dart b/kitchenowl/lib/pages/household_page/shoppinglist.dart index 4ce178744..f8bffa88b 100644 --- a/kitchenowl/lib/pages/household_page/shoppinglist.dart +++ b/kitchenowl/lib/pages/household_page/shoppinglist.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:kitchenowl/app.dart'; @@ -8,6 +9,7 @@ import 'package:kitchenowl/models/item.dart'; import 'package:kitchenowl/kitchenowl.dart'; import 'package:kitchenowl/widgets/choice_scroll.dart'; import 'package:kitchenowl/widgets/home_page/sliver_category_item_grid_list.dart'; +import 'package:kitchenowl/widgets/shopping_list/shopping_list_choice_chip.dart'; class ShoppinglistPage extends StatefulWidget { const ShoppinglistPage({super.key}); @@ -108,9 +110,10 @@ class _ShoppinglistPageState extends State { if (state.sorting != ShoppinglistSorting.category || state is LoadingShoppinglistCubitState && - state.listItems.isEmpty) { - body = SliverItemGridList( - items: state.listItems, + (state.selectedShoppinglist?.items.isEmpty ?? + false)) { + body = SliverItemGridList( + items: state.selectedShoppinglist?.items ?? [], categories: state.categories, shoppingList: state.selectedShoppinglist, selected: (item) => @@ -140,9 +143,11 @@ class _ShoppinglistPageState extends State { Category? category = i < state.categories.length ? state.categories[i] : null; - final List items = state.listItems - .where((e) => e.category == category) - .toList(); + final List items = state + .selectedShoppinglist?.items + .where((e) => e.category == category) + .toList() ?? + []; if (items.isEmpty) continue; grids.add(SliverCategoryItemGridList( @@ -185,42 +190,25 @@ class _ShoppinglistPageState extends State { left: (state.shoppinglists.length < 2) ? const SizedBox() : ChoiceScroll( - children: - state.shoppinglists.map((shoppinglist) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4, - ), - child: ChoiceChip( - showCheckmark: false, - label: Text( - shoppinglist.name, - style: TextStyle( - color: shoppinglist.id == - state - .selectedShoppinglist! - .id - ? Theme.of(context) - .colorScheme - .onPrimary - : null, - ), + children: state.shoppinglists.values + .sorted((a, b) => b.items.length + .compareTo(a.items.length)) + .map( + (shoppinglist) => + ShoppingListChoiceChip( + shoppingList: shoppinglist, + selected: shoppinglist.id == + state.selectedShoppinglistId, + onSelected: (bool selected) { + if (selected) { + cubit.setShoppingList( + shoppinglist, + ); + } + }, ), - selected: shoppinglist.id == - state.selectedShoppinglist!.id, - selectedColor: Theme.of(context) - .colorScheme - .secondary, - onSelected: (bool selected) { - if (selected) { - cubit.setShoppingList( - shoppinglist, - ); - } - }, - ), - ); - }).toList(), + ) + .toList(), ), right: Padding( padding: @@ -244,12 +232,17 @@ class _ShoppinglistPageState extends State { ), if (body is List) ...body, if (body is! List) body, - if ((state.recentItems.isNotEmpty || - state is LoadingShoppinglistCubitState)) - SliverCategoryItemGridList( + if ((state.selectedShoppinglist?.recentItems + .isNotEmpty ?? + false) || + state is LoadingShoppinglistCubitState) + SliverCategoryItemGridList( name: '${AppLocalizations.of(context)!.itemsRecent}:', - items: state.recentItems, + items: state.selectedShoppinglist?.recentItems + .take(App.settings.recentItemsCount) + .toList() ?? + [], onPressed: Nullable(cubit.add), categories: state.categories, shoppingList: state.selectedShoppinglist, @@ -260,7 +253,9 @@ class _ShoppinglistPageState extends State { !(state.sorting != ShoppinglistSorting.category || state is LoadingShoppinglistCubitState && - state.listItems.isEmpty), + (state.selectedShoppinglist?.items + .isEmpty ?? + false)), ), ], ); diff --git a/kitchenowl/lib/services/api/shoppinglist.dart b/kitchenowl/lib/services/api/shoppinglist.dart index 0da3b5f11..97abf7a5f 100644 --- a/kitchenowl/lib/services/api/shoppinglist.dart +++ b/kitchenowl/lib/services/api/shoppinglist.dart @@ -11,8 +11,13 @@ extension ShoppinglistApi on ApiService { String route({Household? household, ShoppingList? shoppinglist}) => "${household != null ? householdPath(household) : ""}$baseRoute${shoppinglist?.id != null ? "/${shoppinglist!.id}" : ""}"; - Future?> getShoppingLists(Household household) async { - final res = await get(route(household: household)); + Future?> getShoppingLists( + Household household, { + ShoppinglistSorting sorting = ShoppinglistSorting.alphabetical, + int recentItemlimit = 9, + }) async { + final res = await get(route(household: household) + + "?orderby=${sorting.index}&recent_limit=${recentItemlimit}"); if (res.statusCode != 200) return null; final body = List.from(jsonDecode(res.body)); @@ -51,34 +56,6 @@ extension ShoppinglistApi on ApiService { return res.statusCode == 200; } - Future?> getItems( - ShoppingList shoppinglist, [ - ShoppinglistSorting sorting = ShoppinglistSorting.alphabetical, - ]) async { - final res = await get( - '${route(shoppinglist: shoppinglist)}/items?orderby=${sorting.index}', - ); - if (res.statusCode != 200) return null; - - final body = List.from(jsonDecode(res.body)); - - return body.map((e) => ShoppinglistItem.fromJson(e)).toList(); - } - - Future?> getRecentItems( - ShoppingList shoppinglist, [ - int limit = 9, - ]) async { - final res = await get( - '${route(shoppinglist: shoppinglist)}/recent-items?limit=$limit', - ); - if (res.statusCode != 200) return null; - - final body = List.from(jsonDecode(res.body)); - - return body.map((e) => ItemWithDescription.fromJson(e)).toList(); - } - Future putItem( ShoppingList shoppinglist, Item item, diff --git a/kitchenowl/lib/services/storage/mem_storage.dart b/kitchenowl/lib/services/storage/mem_storage.dart index 8640b3cc5..75fa4207a 100644 --- a/kitchenowl/lib/services/storage/mem_storage.dart +++ b/kitchenowl/lib/services/storage/mem_storage.dart @@ -1,6 +1,5 @@ import 'package:kitchenowl/models/category.dart'; import 'package:kitchenowl/models/household.dart'; -import 'package:kitchenowl/models/item.dart'; import 'package:kitchenowl/models/recipe.dart'; import 'package:kitchenowl/models/shoppinglist.dart'; import 'package:kitchenowl/models/tag.dart'; @@ -25,9 +24,6 @@ class MemStorage { await readHouseholds().then( (value) => Future.wait( value?.map((household) async { - await readShoppingLists(household).then((value) => - value?.map((shoppingList) => clearItems(shoppingList))); - await Future.wait([ clearShoppingLists(household), clearRecipes(household), @@ -86,30 +82,6 @@ class MemStorage { _shoppinglists[household.id] = shoppingLists; } - Map?> _shoppinglistItems = {}; - - Future?> readItems(ShoppingList shoppingList) async { - if (persistentStorage != null && - _shoppinglistItems[shoppingList.id] == null) { - _shoppinglistItems[shoppingList.id] = - await persistentStorage!.readItems(shoppingList); - } - if (_shoppinglistItems[shoppingList.id] == null) return null; - return List.of(_shoppinglistItems[shoppingList.id]!); - } - - Future writeItems( - ShoppingList shoppinglist, - List items, - ) async { - persistentStorage?.writeItems(shoppinglist, items); - _shoppinglistItems[shoppinglist.id] = items; - } - - Future clearItems(ShoppingList shoppinglist) async { - _shoppinglistItems = {}; - } - Map?> _recipes = {}; Future?> readRecipes(Household household) async { diff --git a/kitchenowl/lib/services/storage/temp_storage.dart b/kitchenowl/lib/services/storage/temp_storage.dart index 1365da9e3..c6c1bbed3 100644 --- a/kitchenowl/lib/services/storage/temp_storage.dart +++ b/kitchenowl/lib/services/storage/temp_storage.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart' as foundation; import 'package:kitchenowl/models/category.dart'; import 'package:kitchenowl/models/household.dart'; -import 'package:kitchenowl/models/item.dart'; import 'package:kitchenowl/models/recipe.dart'; import 'package:kitchenowl/models/shoppinglist.dart'; import 'package:kitchenowl/models/tag.dart'; @@ -47,12 +46,6 @@ class TempStorage { return File('$path/${household.id}-shoppinglists.json'); } - Future _localItemFile(ShoppingList shoppinglist) async { - final path = await _localPath; - - return File('$path/${shoppinglist.id}-items.json'); - } - Future _localRecipesFile(Household household) async { final path = await _localPath; @@ -75,9 +68,6 @@ class TempStorage { await readHouseholds().then( (value) => Future.wait( value?.map((household) async { - await readShoppingLists(household).then((value) => - value?.map((shoppingList) => clearItems(shoppingList))); - await Future.wait([ clearShoppingLists(household), clearRecipes(household), @@ -159,42 +149,6 @@ class TempStorage { } } - Future?> readItems(ShoppingList shoppingList) async { - if (!foundation.kIsWeb) { - try { - final file = await _localItemFile(shoppingList); - final String content = await file.readAsString(); - - return List.from( - json.decode(content).map((e) => ShoppinglistItem.fromJson(e)), - ); - } catch (_) {} - } - - return null; - } - - Future writeItems( - ShoppingList shoppinglist, - List items, - ) async { - if (!foundation.kIsWeb) { - final file = await _localItemFile(shoppinglist); - await file.writeAsString( - json.encode(items.map((e) => e.toJsonWithId()).toList()), - ); - } - } - - Future clearItems(ShoppingList shoppinglist) async { - if (!foundation.kIsWeb) { - try { - final file = await _localItemFile(shoppinglist); - if (await file.exists()) await file.delete(); - } catch (_) {} - } - } - Future?> readRecipes(Household household) async { if (!foundation.kIsWeb) { try { diff --git a/kitchenowl/lib/services/transaction.dart b/kitchenowl/lib/services/transaction.dart index 616485f9c..687b19253 100644 --- a/kitchenowl/lib/services/transaction.dart +++ b/kitchenowl/lib/services/transaction.dart @@ -8,9 +8,9 @@ abstract class Transaction extends Model { "TransactionShoppingListAddItem": (m, t) => TransactionShoppingListAddItem.fromJson(m, t), "TransactionShoppingListDeleteItem": (m, t) => - TransactionShoppingListDeleteItem.fromJson(m, t), + TransactionShoppingListRemoveItem.fromJson(m, t), "TransactionShoppingListDeleteItems": (m, t) => - TransactionShoppingListDeleteItems.fromJson(m, t), + TransactionShoppingListRemoveItems.fromJson(m, t), "TransactionShoppingListUpdateItem": (m, t) => TransactionShoppingListUpdateItem.fromJson(m, t), "TransactionShoppingListAddRecipeItems": (m, t) => @@ -36,14 +36,21 @@ abstract class Transaction extends Model { DateTime.tryParse(map['timestamp']) ?? DateTime.now(); if (map.containsKey('className') && _transactionTypes.containsKey(map['className'])) { - return _transactionTypes[map['className']]!(map, timestamp) - as Transaction; + try { + return _transactionTypes[map['className']]!(map, timestamp) + as Transaction; + } catch (e) { + return ErrorTransaction( + timestamp, + map.containsKey('className') ? map['className'] : "ERROR", + e.toString()); + } } return ErrorTransaction( - timestamp, - map.containsKey('className') ? map['className'] : "ERROR", - ); + timestamp, + map.containsKey('className') ? map['className'] : "ERROR", + "Could not find transaction class"); } @override @@ -57,7 +64,10 @@ abstract class Transaction extends Model { } class ErrorTransaction extends Transaction { - const ErrorTransaction(super.timestamp, super.className) : super.internal(); + final String message; + + const ErrorTransaction(super.timestamp, super.className, [this.message = ""]) + : super.internal(); @override Future runLocal() { diff --git a/kitchenowl/lib/services/transactions/shoppinglist.dart b/kitchenowl/lib/services/transactions/shoppinglist.dart index 2e9d7fc32..66b712f30 100644 --- a/kitchenowl/lib/services/transactions/shoppinglist.dart +++ b/kitchenowl/lib/services/transactions/shoppinglist.dart @@ -3,15 +3,20 @@ import 'package:kitchenowl/models/household.dart'; import 'package:kitchenowl/models/item.dart'; import 'package:kitchenowl/models/shoppinglist.dart'; import 'package:kitchenowl/services/storage/mem_storage.dart'; -import 'package:kitchenowl/services/storage/transaction_storage.dart'; import 'package:kitchenowl/services/transaction.dart'; import 'package:kitchenowl/services/api/api_service.dart'; class TransactionShoppingListGet extends Transaction> { final Household household; + final ShoppinglistSorting sorting; + final int recentItemlimit; - TransactionShoppingListGet({DateTime? timestamp, required this.household}) - : super.internal( + TransactionShoppingListGet({ + DateTime? timestamp, + required this.household, + this.sorting = ShoppinglistSorting.alphabetical, + this.recentItemlimit = 9, + }) : super.internal( timestamp ?? DateTime.now(), "TransactionShoppingListGet", ); @@ -23,7 +28,11 @@ class TransactionShoppingListGet extends Transaction> { @override Future?> runOnline() async { - final lists = await ApiService.getInstance().getShoppingLists(household); + final lists = await ApiService.getInstance().getShoppingLists( + household, + sorting: sorting, + recentItemlimit: recentItemlimit, + ); if (lists != null) { MemStorage.getInstance().writeShoppingLists(household, lists); } @@ -32,40 +41,6 @@ class TransactionShoppingListGet extends Transaction> { } } -class TransactionShoppingListGetItems - extends Transaction> { - final ShoppingList shoppinglist; - final ShoppinglistSorting sorting; - - TransactionShoppingListGetItems({ - DateTime? timestamp, - required this.shoppinglist, - required this.sorting, - }) : super.internal( - timestamp ?? DateTime.now(), - "TransactionShoppingListGetItems", - ); - - @override - Future> runLocal() async { - final l = await MemStorage.getInstance().readItems(shoppinglist) ?? []; - ShoppinglistSorting.sortShoppinglistItems(l, sorting); - - return l; - } - - @override - Future?> runOnline() async { - final items = - await ApiService.getInstance().getItems(shoppinglist, sorting); - if (items != null) { - MemStorage.getInstance().writeItems(shoppinglist, items); - } - - return items; - } -} - class TransactionShoppingListSearchItem extends Transaction> { final Household household; final String query; @@ -81,25 +56,17 @@ class TransactionShoppingListSearchItem extends Transaction> { @override Future> runLocal() async { - final shoppinglist = await MemStorage.getInstance() - .readShoppingLists(household) - .then( - (shoppingLists) => Future.wait?>( - shoppingLists - ?.map((shoppingList) => - MemStorage.getInstance().readItems(shoppingList)) - .toList() ?? - [], - ), - ) - .then>((e) => e.fold>( - [], - (p, e) => p + (e ?? []), - )); - shoppinglist - .retainWhere((e) => e.name.toLowerCase().contains(query.toLowerCase())); - - return shoppinglist; + final shoppingLists = + await MemStorage.getInstance().readShoppingLists(household); + return (shoppingLists + ?.map( + (shoppingList) => shoppingList.recentItems + shoppingList.items) + .fold>( + [], + (p, e) => p + e, + ) ?? + []) + ..retainWhere((e) => e.name.toLowerCase().contains(query.toLowerCase())); } @override @@ -108,56 +75,13 @@ class TransactionShoppingListSearchItem extends Transaction> { } } -class TransactionShoppingListGetRecentItems - extends Transaction> { - final ShoppingList shoppinglist; - final int itemsCount; - - TransactionShoppingListGetRecentItems({ - DateTime? timestamp, - required this.shoppinglist, - required this.itemsCount, - }) : super.internal( - timestamp ?? DateTime.now(), - "TransactionShoppingListGetRecentItems", - ); - - @override - Future> runLocal() async { - if (itemsCount <= 0) return []; - final items = - (await MemStorage.getInstance().readItems(shoppinglist) ?? const []) - .map((e) => e.name) - .toSet(); - - return (await TransactionStorage.getInstance().readTransactions()) - .whereType() - .where((e) => e.shoppinglist.id == shoppinglist.id) - .map((e) => e.item) - .where((e) { - if (items.contains(e.name)) { - return false; - } else { - items.add(e.name); - - return true; - } - }).toList(); - } - - @override - Future?> runOnline() async { - if (itemsCount <= 0) return []; - return await ApiService.getInstance() - .getRecentItems(shoppinglist, itemsCount); - } -} - class TransactionShoppingListAddItem extends Transaction { + final Household household; final ShoppingList shoppinglist; final Item item; TransactionShoppingListAddItem({ + required this.household, required this.shoppinglist, required this.item, DateTime? timestamp, @@ -171,6 +95,7 @@ class TransactionShoppingListAddItem extends Transaction { DateTime timestamp, ) => TransactionShoppingListAddItem( + household: Household.fromJson(map['household']), shoppinglist: ShoppingList.fromJson(map['shoppinglist']), item: ItemWithDescription.fromJson(map['item']), timestamp: timestamp, @@ -182,15 +107,22 @@ class TransactionShoppingListAddItem extends Transaction { @override Map toJson() => super.toJson() ..addAll({ + "household": household.toJsonWithId(), "shoppinglist": shoppinglist.toJsonWithId(), "item": item.toJsonWithId(), }); @override Future runLocal() async { - final list = await MemStorage.getInstance().readItems(shoppinglist) ?? []; - list.add(ShoppinglistItem.fromItem(item: item)); - MemStorage.getInstance().writeItems(shoppinglist, list); + final shoppingLists = + await MemStorage.getInstance().readShoppingLists(household) ?? []; + final latestShoppingList = + shoppingLists.where((e) => e.id == shoppinglist.id).firstOrNull; + if (latestShoppingList == null) return false; + latestShoppingList.items.add(ShoppinglistItem.fromItem(item: item)); + latestShoppingList.recentItems + .removeWhere((item) => item.name == this.item.name); + MemStorage.getInstance().writeShoppingLists(household, shoppingLists); return true; } @@ -214,12 +146,14 @@ class TransactionShoppingListAddItem extends Transaction { } } -class TransactionShoppingListDeleteItem extends Transaction { +class TransactionShoppingListRemoveItem extends Transaction { + final Household household; final ShoppingList shoppinglist; final ShoppinglistItem item; - TransactionShoppingListDeleteItem({ + TransactionShoppingListRemoveItem({ DateTime? timestamp, + required this.household, required this.item, required this.shoppinglist, }) : super.internal( @@ -227,11 +161,12 @@ class TransactionShoppingListDeleteItem extends Transaction { "TransactionShoppingListDeleteItem", ); - factory TransactionShoppingListDeleteItem.fromJson( + factory TransactionShoppingListRemoveItem.fromJson( Map map, DateTime timestamp, ) => - TransactionShoppingListDeleteItem( + TransactionShoppingListRemoveItem( + household: Household.fromJson(map['household']), shoppinglist: ShoppingList.fromJson(map['shoppinglist']), item: ShoppinglistItem.fromJson(map['item']), timestamp: timestamp, @@ -243,15 +178,21 @@ class TransactionShoppingListDeleteItem extends Transaction { @override Map toJson() => super.toJson() ..addAll({ + 'household': household.toJsonWithId(), "shoppinglist": shoppinglist.toJsonWithId(), "item": item.toJsonWithId(), }); @override Future runLocal() async { - final list = await MemStorage.getInstance().readItems(shoppinglist) ?? []; - list.removeWhere((e) => e.name == item.name); - MemStorage.getInstance().writeItems(shoppinglist, list); + final shoppingLists = + await MemStorage.getInstance().readShoppingLists(household) ?? []; + final latestShoppingList = + shoppingLists.where((e) => e.id == shoppinglist.id).firstOrNull; + if (latestShoppingList == null) return false; + latestShoppingList.items.removeWhere((e) => e.name == item.name); + latestShoppingList.recentItems.insert(0, item); + MemStorage.getInstance().writeShoppingLists(household, shoppingLists); return true; } @@ -264,12 +205,14 @@ class TransactionShoppingListDeleteItem extends Transaction { } } -class TransactionShoppingListDeleteItems extends Transaction { +class TransactionShoppingListRemoveItems extends Transaction { + final Household household; final ShoppingList shoppinglist; final List items; - TransactionShoppingListDeleteItems({ + TransactionShoppingListRemoveItems({ DateTime? timestamp, + required this.household, required this.items, required this.shoppinglist, }) : super.internal( @@ -277,11 +220,12 @@ class TransactionShoppingListDeleteItems extends Transaction { "TransactionShoppingListDeleteItems", ); - factory TransactionShoppingListDeleteItems.fromJson( + factory TransactionShoppingListRemoveItems.fromJson( Map map, DateTime timestamp, ) => - TransactionShoppingListDeleteItems( + TransactionShoppingListRemoveItems( + household: Household.fromJson(map['household']), shoppinglist: ShoppingList.fromJson(map['shoppinglist']), items: List.from(map['items']) .map((e) => ShoppinglistItem.fromJson(e)) @@ -295,15 +239,22 @@ class TransactionShoppingListDeleteItems extends Transaction { @override Map toJson() => super.toJson() ..addAll({ + 'household': household.toJsonWithId(), "shoppinglist": shoppinglist.toJsonWithId(), "items": items.map((e) => e.toJsonWithId()).toList(), }); @override Future runLocal() async { - final list = await MemStorage.getInstance().readItems(shoppinglist) ?? []; - list.removeWhere((e) => items.map((e) => e.name).contains(e.name)); - MemStorage.getInstance().writeItems(shoppinglist, list); + final shoppingLists = + await MemStorage.getInstance().readShoppingLists(household) ?? []; + final latestShoppingList = + shoppingLists.where((e) => e.id == shoppinglist.id).firstOrNull; + if (latestShoppingList == null) return false; + latestShoppingList.items + .removeWhere((e) => items.map((e) => e.name).contains(e.name)); + latestShoppingList.recentItems.insertAll(0, items); + MemStorage.getInstance().writeShoppingLists(household, shoppingLists); return true; } @@ -317,11 +268,13 @@ class TransactionShoppingListDeleteItems extends Transaction { } class TransactionShoppingListUpdateItem extends Transaction { + final Household household; final ShoppingList shoppinglist; final Item item; final String description; TransactionShoppingListUpdateItem({ + required this.household, required this.shoppinglist, required this.item, required this.description, @@ -336,6 +289,7 @@ class TransactionShoppingListUpdateItem extends Transaction { DateTime timestamp, ) => TransactionShoppingListUpdateItem( + household: Household.fromJson(map['household']), shoppinglist: ShoppingList.fromJson(map['shoppinglist']), item: Item.fromJson(map['item']), description: map['description'], @@ -348,6 +302,7 @@ class TransactionShoppingListUpdateItem extends Transaction { @override Map toJson() => super.toJson() ..addAll({ + 'household': household.toJsonWithId(), "shoppinglist": shoppinglist.toJsonWithId(), "item": item.toJsonWithId(), "description": description, @@ -355,21 +310,25 @@ class TransactionShoppingListUpdateItem extends Transaction { @override Future runLocal() async { + final shoppingLists = + await MemStorage.getInstance().readShoppingLists(household) ?? []; + final latestShoppingList = + shoppingLists.where((e) => e.id == shoppinglist.id).firstOrNull; + if (latestShoppingList == null) return false; + if (item is ShoppinglistItem) { - final list = await MemStorage.getInstance().readItems(shoppinglist) ?? []; - final int i = list.indexWhere((e) => e.id == item.id); - list.removeAt(i); - list.insert( - i, - (item as ShoppinglistItem).copyWith(description: description), - ); - MemStorage.getInstance().writeItems(shoppinglist, list); + final int i = latestShoppingList.items.indexWhere((e) => e.id == item.id); + latestShoppingList.items[i] = + (item as ShoppinglistItem).copyWith(description: description); + MemStorage.getInstance().writeShoppingLists(household, shoppingLists); return true; } else if (description.isNotEmpty) { - final list = await MemStorage.getInstance().readItems(shoppinglist) ?? []; - list.add(ShoppinglistItem(name: item.name, description: description)); - MemStorage.getInstance().writeItems(shoppinglist, list); + latestShoppingList.items + .add(ShoppinglistItem(name: item.name, description: description)); + latestShoppingList.recentItems + .removeWhere((item) => item.name == this.item.name); + MemStorage.getInstance().writeShoppingLists(household, shoppingLists); return true; } @@ -387,10 +346,12 @@ class TransactionShoppingListUpdateItem extends Transaction { } class TransactionShoppingListAddRecipeItems extends Transaction { + final Household household; final ShoppingList shoppinglist; final List items; TransactionShoppingListAddRecipeItems({ + required this.household, required this.shoppinglist, required this.items, DateTime? timestamp, @@ -407,6 +368,7 @@ class TransactionShoppingListAddRecipeItems extends Transaction { List.from(map['items'].map((e) => RecipeItem.fromJson(e))); return TransactionShoppingListAddRecipeItems( + household: Household.fromJson(map['household']), shoppinglist: ShoppingList.fromJson(map['shoppinglist']), items: items, timestamp: timestamp, @@ -419,23 +381,29 @@ class TransactionShoppingListAddRecipeItems extends Transaction { @override Map toJson() => super.toJson() ..addAll({ + "household": household.toJsonWithId(), "shoppinglist": shoppinglist.toJsonWithId(), "items": items.map((e) => e.toJsonWithId()).toList(), }); @override Future runLocal() async { - final list = await MemStorage.getInstance().readItems(shoppinglist) ?? []; + final shoppingLists = + await MemStorage.getInstance().readShoppingLists(household) ?? []; + final latestShoppingList = + shoppingLists.where((e) => e.id == shoppinglist.id).firstOrNull; + if (latestShoppingList == null) return false; + for (final item in items) { - final int i = list.indexWhere((e) => e.id == item.id); + final int i = latestShoppingList.items.indexWhere((e) => e.id == item.id); if (i >= 0) { - list.removeAt(i); - list.insert(i, item.toShoppingListItem()); + latestShoppingList.items[i] = item.toShoppingListItem(); } else { - list.add(item.toShoppingListItem()); + latestShoppingList.items.add(item.toShoppingListItem()); + latestShoppingList.recentItems.removeWhere((e) => e.name == item.name); } } - MemStorage.getInstance().writeItems(shoppinglist, list); + MemStorage.getInstance().writeShoppingLists(household, shoppingLists); return true; } diff --git a/kitchenowl/lib/widgets/shopping_list/shopping_list_choice_chip.dart b/kitchenowl/lib/widgets/shopping_list/shopping_list_choice_chip.dart new file mode 100644 index 000000000..e4673a66d --- /dev/null +++ b/kitchenowl/lib/widgets/shopping_list/shopping_list_choice_chip.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:kitchenowl/models/shoppinglist.dart'; + +class ShoppingListChoiceChip extends StatelessWidget { + final ShoppingList shoppingList; + final bool selected; + final void Function(bool)? onSelected; + + const ShoppingListChoiceChip({ + super.key, + required this.shoppingList, + this.selected = false, + this.onSelected, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, + ), + child: ChoiceChip( + showCheckmark: false, + label: Text( + shoppingList.name + + (shoppingList.items.isNotEmpty + ? " (${shoppingList.items.length})" + : ""), + style: TextStyle( + color: selected ? Theme.of(context).colorScheme.onPrimary : null, + ), + ), + selected: selected, + elevation: shoppingList.items.isNotEmpty ? 2 : 0, + selectedColor: Theme.of(context).colorScheme.secondary, + onSelected: onSelected, + ), + ); + } +}