Skip to content

Commit

Permalink
Merge branch 'feature/data-management' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
TheNordicOne committed May 19, 2024
2 parents b6cf9f0 + d4df24a commit 4e518dd
Show file tree
Hide file tree
Showing 51 changed files with 1,242 additions and 87 deletions.
9 changes: 9 additions & 0 deletions lib/abstracts/navigation_state_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:flutter/material.dart';

abstract class NavigationStateService {
void setCurrentPageIndex(int index);

int get currentPageIndexSync;

List<NavigationDestination> get destinations;
}
6 changes: 6 additions & 0 deletions lib/database/daos/basket_items_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ class BasketItemsDao extends DatabaseAccessor<AppDatabase> with _$BasketItemsDao
return _rowsToViewModels(rows);
}

Future<int> countItemsInBasket(int basketId) async {
final itemsInBasket = basketItems.basket.count(filter: basketItems.basket.equals(basketId));
final itemsInBasketQuery = selectOnly(basketItems)..addColumns([itemsInBasket]);
return await itemsInBasketQuery.map((row) => row.read(itemsInBasket)).getSingle() ?? 0;
}

List<OrderingTerm> _getOrderingTerms<T>(
SortMode sortMode,
) {
Expand Down
49 changes: 48 additions & 1 deletion lib/database/daos/item_categories_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import 'package:rabenkorb/database/database.dart';
import 'package:rabenkorb/database/tables/item_categories.dart';
import 'package:rabenkorb/mappers/to_view_model.dart';
import 'package:rabenkorb/models/item_category_view_model.dart';
import 'package:rabenkorb/shared/sort_direction.dart';
import 'package:rabenkorb/shared/sort_mode.dart';

part 'item_categories_dao.g.dart';

Expand Down Expand Up @@ -32,6 +34,51 @@ class ItemCategoriesDao extends DatabaseAccessor<AppDatabase> with _$ItemCategor
}

Stream<List<ItemCategoryViewModel>> watchItemCategories() {
return (select(itemCategories)).watch().map((categories) => categories.map((category) => toItemCategoryViewModel(category)!).toList());
return (select(itemCategories)..orderBy([(tbl) => OrderingTerm(expression: tbl.name)]))
.watch()
.map((categories) => categories.map((category) => toItemCategoryViewModel(category)!).toList());
}

Stream<List<ItemCategoryViewModel>> watchItemCategoriesInOrder(SortMode sortMode, SortDirection sortDirection, {int? sortRuleId}) {
final sourceQuery = select(itemCategories);

final query = sourceQuery.join(
[
leftOuterJoin(attachedDatabase.sortOrders,
itemCategories.id.equalsExp(attachedDatabase.sortOrders.categoryId) & attachedDatabase.sortOrders.ruleId.equalsNullable(sortRuleId)),
],
);
query.orderBy([
OrderingTerm(expression: itemCategories.id.isNull(), mode: toOrderingMode(sortDirection)),
OrderingTerm(expression: attachedDatabase.sortOrders.sortOrder.isNull(), mode: toOrderingMode(sortDirection)),
..._getOrderingTerms(sortMode),
]);

return query.watch().map((rows) => rows.map((row) => toItemCategoryViewModel(row.readTable(itemCategories))!).toList());
}

List<OrderingTerm> _getOrderingTerms<T>(
SortMode sortMode,
) {
switch (sortMode) {
case SortMode.databaseOrder:
return [_byId()];
case SortMode.name:
return [_byName()];
case SortMode.custom:
return [_bySortOrder(), _byName()];
}
}

OrderingTerm _byId() {
return OrderingTerm(expression: itemCategories.id);
}

OrderingTerm _byName() {
return OrderingTerm(expression: itemCategories.name);
}

OrderingTerm _bySortOrder() {
return OrderingTerm(expression: attachedDatabase.sortOrders.sortOrder);
}
}
4 changes: 3 additions & 1 deletion lib/database/daos/item_units_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class ItemUnitsDao extends DatabaseAccessor<AppDatabase> with _$ItemUnitsDaoMixi
}

Stream<List<ItemUnitViewModel>> watchItemUnits() {
return (select(itemUnits)).watch().map((units) => units.map((unit) => toItemUnitViewModel(unit)!).toList());
return (select(itemUnits)..orderBy([(tbl) => OrderingTerm(expression: tbl.name)]))
.watch()
.map((units) => units.map((unit) => toItemUnitViewModel(unit)!).toList());
}
}
72 changes: 43 additions & 29 deletions lib/database/daos/sort_orders_dao.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:drift/drift.dart';
import 'package:rabenkorb/database/database.dart';
import 'package:rabenkorb/database/tables/sort_orders.dart';
import 'package:rabenkorb/models/item_category_view_model.dart';
import 'package:rabenkorb/shared/default_sort_rules.dart';

part 'sort_orders_dao.g.dart';

Expand Down Expand Up @@ -28,51 +30,63 @@ class SortOrdersDao extends DatabaseAccessor<AppDatabase> with _$SortOrdersDaoMi
});
}

Future<void> updateOrderSingle(int sortRuleId, int targetId, {int? placeBeforeId, int? placeAfterId}) async {
Future<void> updateOrderSingle(int sortRuleId, List<ItemCategoryViewModel> visibleCategories, int oldIndex, int newIndex) async {
await transaction(() async {
// Fetch current sort orders for the given sort rule
final allOrders = await (select(sortOrders)
..where((tbl) => tbl.ruleId.equals(sortRuleId))
..orderBy([(tbl) => OrderingTerm(expression: tbl.sortOrder)]))
.get();

// Find target, placeBefore, and placeAfter indices
final targetIndex = allOrders.indexWhere((order) => order.categoryId == targetId);
final placeBeforeIndex = placeBeforeId != null ? allOrders.indexWhere((order) => order.categoryId == placeBeforeId) : -1;
final placeAfterIndex = placeAfterId != null ? allOrders.indexWhere((order) => order.categoryId == placeAfterId) : -1;
// Create a map of categoryId to its sort order
final orderMap = {for (var order in allOrders) order.categoryId: order};

if (targetIndex == -1) {
throw ArgumentError("Target ID $targetId not found.");
}
// Filter out the pseudo-category with id -1 from visibleCategories
visibleCategories = visibleCategories.where((category) => category.id != withoutCategoryId).toList();

if (placeBeforeId == null && placeAfterId == null) {
// No reordering needed
return;
// Validate indices
if (oldIndex < 0 || oldIndex >= visibleCategories.length || newIndex < 0 || newIndex >= visibleCategories.length) {
throw ArgumentError("Invalid indices provided.");
}

final targetOrder = allOrders.removeAt(targetIndex);
// Reorder the visible categories based on the indices provided
final targetCategory = visibleCategories.removeAt(oldIndex);
visibleCategories.insert(newIndex, targetCategory);

// Determine the new position for targetOrder
int newIndex;
if (placeBeforeIndex != -1) {
newIndex = placeBeforeIndex > targetIndex ? placeBeforeIndex - 1 : placeBeforeIndex;
} else if (placeAfterIndex != -1) {
newIndex = placeAfterIndex > targetIndex ? placeAfterIndex : placeAfterIndex + 1;
} else {
// If both are null, it means we shouldn't be here due to the previous check
return;
// Ensure all categories within the range have sort orders
for (int i = 0; i < visibleCategories.length; i++) {
final category = visibleCategories[i];
if (orderMap.containsKey(category.id)) {
// Update existing sort order
orderMap[category.id] = orderMap[category.id]!.copyWith(sortOrder: i + 1);
} else {
// Create a new sort order
orderMap[category.id] = SortOrder(
categoryId: category.id,
ruleId: sortRuleId,
sortOrder: i + 1,
);
}
}

// Insert the target order at the new position
allOrders.insert(newIndex, targetOrder);
// Handle unsorted categories not visible but should be ordered sequentially after visible ones
final sortedOrderCategories = orderMap.values.toList();
sortedOrderCategories.sort((a, b) => a.sortOrder.compareTo(b.sortOrder));
int nextSortOrder = sortedOrderCategories.length + 1;

final unsortedCategories = await (select(itemCategories)..where((tbl) => (tbl.id.isIn(orderMap.keys.toList()) & tbl.id.isNotValue(-1)).not())).get();

// Update sortOrder to ensure no gaps
for (int i = 0; i < allOrders.length; i++) {
allOrders[i] = allOrders[i].copyWith(sortOrder: i + 1);
for (var category in unsortedCategories) {
orderMap[category.id] = SortOrder(
categoryId: category.id,
ruleId: sortRuleId,
sortOrder: nextSortOrder++,
);
}

// Update the database
for (var order in allOrders) {
await (update(sortOrders)..where((tbl) => tbl.categoryId.equals(order.categoryId) & tbl.ruleId.equals(sortRuleId))).write(order);
// Update the database with new and updated sort orders
for (var order in orderMap.values) {
await (into(sortOrders).insertOnConflictUpdate(order));
}
});
}
Expand Down
4 changes: 3 additions & 1 deletion lib/database/daos/sort_rules_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class SortRulesDao extends DatabaseAccessor<AppDatabase> with _$SortRulesDaoMixi
}

Stream<List<SortRuleViewModel>> watchSortRules() {
return (select(sortRules)).watch().map((rules) => rules.map((rule) => toSortRuleViewModel(rule)!).toList());
return (select(sortRules)..orderBy([(tbl) => OrderingTerm(expression: tbl.name)]))
.watch()
.map((rules) => rules.map((rule) => toSortRuleViewModel(rule)!).toList());
}
}
10 changes: 8 additions & 2 deletions lib/di/di_setup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:rabenkorb/features/core/logging/core_logger.dart';
import 'package:rabenkorb/features/core/logging/sinks/mongo_db_sink.dart';
import 'package:rabenkorb/features/core/logging/sinks/void_sink.dart';
import 'package:rabenkorb/services/business/basket_service.dart';
import 'package:rabenkorb/services/business/data_management_service.dart';
import 'package:rabenkorb/services/business/library_service.dart';
import 'package:rabenkorb/services/business/metadata_service.dart';
import 'package:rabenkorb/services/business/sort_service.dart';
Expand All @@ -27,10 +28,12 @@ import 'package:rabenkorb/services/data_access/sort_rule_service.dart';
import 'package:rabenkorb/services/data_access/template_library_service.dart';
import 'package:rabenkorb/services/data_access/variant_key_service.dart';
import 'package:rabenkorb/services/state/basket_state_service.dart';
import 'package:rabenkorb/services/state/data_management_navigation_state_service.dart';
import 'package:rabenkorb/services/state/data_management_state_service.dart';
import 'package:rabenkorb/services/state/intl_state_service.dart';
import 'package:rabenkorb/services/state/library_state_service.dart';
import 'package:rabenkorb/services/state/loading_state.dart';
import 'package:rabenkorb/services/state/navigation_state_service.dart';
import 'package:rabenkorb/services/state/main_navigation_state_service.dart';
import 'package:rabenkorb/services/state/shared_preference_service.dart';
import 'package:rabenkorb/services/utility/backup_service.dart';
import 'package:rabenkorb/services/utility/image_service.dart';
Expand Down Expand Up @@ -111,6 +114,7 @@ void _registerBusinessServices() {

di.registerSingletonWithDependencies<LibraryService>(() => LibraryService(), dependsOn: [ItemTemplateService, MetadataService]);
di.registerSingletonWithDependencies<BasketService>(() => BasketService(), dependsOn: [BasketItemService, MetadataService]);
di.registerSingletonWithDependencies<DataManagementService>(() => DataManagementService(), dependsOn: [MetadataService]);
}

Future<void> _registerStateServices() async {
Expand All @@ -122,7 +126,9 @@ Future<void> _registerStateServices() async {
await intlService.init();
return intlService;
}, dependsOn: [PreferenceService]);
di.registerSingletonWithDependencies<NavigationStateService>(() => NavigationStateService(), dependsOn: [IntlStateService]);
di.registerSingletonWithDependencies<MainNavigationStateService>(() => MainNavigationStateService(), dependsOn: [IntlStateService]);
di.registerSingletonWithDependencies<DataManagementNavigationStateService>(() => DataManagementNavigationStateService(), dependsOn: [IntlStateService]);
di.registerLazySingleton<DataManagementStateService>(() => DataManagementStateService());
}

void _registerUtilityServices() {
Expand Down
2 changes: 1 addition & 1 deletion lib/features/basket/basket_main_action.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:rabenkorb/features/main/navigation/destination_details.dart';
import 'package:rabenkorb/routing/routes.dart';
import 'package:rabenkorb/shared/destination_details.dart';

final basketMainAction = MainAction(
onPressed: (BuildContext context) {
Expand Down
28 changes: 28 additions & 0 deletions lib/features/basket/overview/basket_list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
import 'package:rabenkorb/features/basket/overview/basket_tile.dart';
import 'package:rabenkorb/models/shopping_basket_view_model.dart';
import 'package:rabenkorb/services/business/basket_service.dart';
import 'package:watch_it/watch_it.dart';

class BasketList extends StatelessWidget with WatchItMixin {
const BasketList({super.key});

@override
Widget build(BuildContext context) {
final baskets = watchStream((BasketService p0) => p0.baskets, initialValue: []);
final basketList = baskets.data ?? [];
return Expanded(
child: ListView.builder(
prototypeItem: BasketTile(
basket: ShoppingBasketViewModel(-1, "Prototyp"),
),
itemCount: basketList.length,
itemBuilder: (BuildContext context, int index) {
return BasketTile(
basket: basketList[index],
);
},
),
);
}
}
43 changes: 43 additions & 0 deletions lib/features/basket/overview/basket_overview_page.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:rabenkorb/features/basket/overview/basket_list.dart';
import 'package:rabenkorb/features/core/structural/core_scaffold.dart';
import 'package:rabenkorb/features/core/structural/drawer/core_drawer.dart';
import 'package:rabenkorb/generated/l10n.dart';
import 'package:rabenkorb/services/business/basket_service.dart';
import 'package:rabenkorb/shared/helper_functions.dart';
import 'package:watch_it/watch_it.dart';

class BasketOverviewPage extends StatelessWidget {
const BasketOverviewPage({super.key});

@override
Widget build(BuildContext context) {
return CoreScaffold(
body: const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [BasketList()],
),
),
),
drawer: const CoreDrawer(),
appBar: AppBar(
title: Text(S.of(context).BasketOverview),
),
floatingActionButton: FloatingActionButton(
key: const Key("basket-overview-page-fab"),
onPressed: () async {
await showRenameDialog(
context,
onConfirm: (String? newName, bool _) async {
await di<BasketService>().createShoppingBasket(newName);
},
);
},
child: const Icon(Icons.add),
),
);
}
}
68 changes: 68 additions & 0 deletions lib/features/basket/overview/basket_tile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import 'package:flutter/material.dart';
import 'package:rabenkorb/generated/l10n.dart';
import 'package:rabenkorb/models/shopping_basket_view_model.dart';
import 'package:rabenkorb/services/business/basket_service.dart';
import 'package:rabenkorb/shared/helper_functions.dart';
import 'package:rabenkorb/shared/widgets/inputs/core_icon_button.dart';
import 'package:watch_it/watch_it.dart';

class BasketTile extends StatelessWidget {
final ShoppingBasketViewModel basket;

const BasketTile({super.key, required this.basket});

@override
Widget build(BuildContext context) {
return FutureBuilder<int>(
future: di<BasketService>().countItemsInBasket(basket.id),
initialData: 0,
builder: (BuildContext context, AsyncSnapshot<int> itemsInBasketSnapshot) {
final itemsInBasketCount = itemsInBasketSnapshot.data ?? 0;
return Card(
child: ListTile(
title: Text(basket.name),
subtitle: Text("${S.of(context).Items}: $itemsInBasketCount"),
trailing: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
CoreIconButton(
icon: const Icon(Icons.edit),
onPressed: () async {
await showRenameDialog(
context,
initialName: basket.name,
onConfirm: (String? newName, bool nameChanged) async {
if (!nameChanged || newName == null) {
return;
}
await di<BasketService>().updateShoppingBasket(basket.id, newName);
},
);
},
),
CoreIconButton(
icon: const Icon(
Icons.delete_forever,
color: Colors.red,
),
onPressed: () async {
await doWithConfirmation(
context,
text: S.of(context).ConfirmDeleteBasket,
title: S.of(context).Confirm,
onConfirm: () async {
await di<BasketService>().deleteShoppingBasketById(basket.id);
di<BasketService>().setFirstShoppingBasketActive();
},
);
},
)
],
),
),
);
},
);
}
}
Loading

0 comments on commit 4e518dd

Please sign in to comment.