From f7ade5df64969ee278e614105e761e08f0b98d23 Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 16 Mar 2025 17:09:29 +0100 Subject: [PATCH 01/28] New "Uncategorized" Category, markAsDelete/Delete Dialog --- lib/constants/constants.dart | 3 + lib/constants/style.dart | 4 +- lib/database/sossoldi_database.dart | 36 +++++--- lib/model/category_transaction.dart | 91 +++++++++++++++++++ lib/pages/categories/add_category.dart | 19 ++-- lib/pages/categories/category_list.dart | 2 +- .../widgets/delete_category_dialog.dart | 55 +++++++++++ .../onboarding_page/widgets/budget_setup.dart | 3 +- .../planning_page/manage_budget_page.dart | 19 ++-- .../widget/recurring_payment_card.dart | 7 +- lib/pages/settings_page.dart | 31 +++++-- .../widgets/categories_tab.dart | 77 +++++++++++----- lib/providers/categories_provider.dart | 41 +++++---- test/model/category_transaction_test.dart | 24 +++-- 14 files changed, 321 insertions(+), 91 deletions(-) create mode 100644 lib/pages/categories/widgets/delete_category_dialog.dart diff --git a/lib/constants/constants.dart b/lib/constants/constants.dart index 6509f922..6b7310d1 100644 --- a/lib/constants/constants.dart +++ b/lib/constants/constants.dart @@ -20,6 +20,7 @@ const Map iconList = { 'device_thermostat': Icons.device_thermostat, 'dry_cleaning': Icons.dry_cleaning, 'work': Icons.work, + 'question_mark': Icons.question_mark, }; const Map accountIconList = { @@ -31,6 +32,7 @@ const Map accountIconList = { // colors const categoryColorList = [ + category0, category1, category2, category3, @@ -43,6 +45,7 @@ const categoryColorList = [ ]; const darkCategoryColorList = [ + darkCategory0, darkCategory1, darkCategory2, darkCategory3, diff --git a/lib/constants/style.dart b/lib/constants/style.dart index 75fa0263..8f9e2a44 100644 --- a/lib/constants/style.dart +++ b/lib/constants/style.dart @@ -35,6 +35,7 @@ const grey1 = Color(0xFF666666); const grey2 = Color(0xFFB9BABC); const grey3 = Color(0xFFF4F4F4); +const category0 = Color(0xFFB9BABC); const category1 = Color(0xFFEDC31C); const category2 = Color(0xFFF68428); const category3 = Color(0xFFFF4754); @@ -71,6 +72,7 @@ const darkGrey2 = Color(0xFFC6C7C8); const darkGrey3 = Color(0xFF181E25); const darkGrey4 = Color(0xFF2E3338); +const darkCategory0 = Color(0xFFB9BABC); const darkCategory1 = Color(0xFFE3B912); const darkCategory2 = Color(0xFFF6740C); const darkCategory3 = Color(0xFFFA3240); @@ -85,4 +87,4 @@ const darkAccount1 = Color(0xFFE0A30C); const darkAccount2 = Color(0xFFDC7807); const darkAccount3 = Color(0xFF33679B); const darkAccount4 = Color(0xFF61B4DB); -const darkAccount5 = Color(0xFF398F10); \ No newline at end of file +const darkAccount5 = Color(0xFF398F10); diff --git a/lib/database/sossoldi_database.dart b/lib/database/sossoldi_database.dart index 7357a87b..26747e4e 100644 --- a/lib/database/sossoldi_database.dart +++ b/lib/database/sossoldi_database.dart @@ -106,11 +106,19 @@ class SossoldiDatabase { `${CategoryTransactionFields.color}` $integerNotNull, `${CategoryTransactionFields.note}` $text, `${CategoryTransactionFields.parent}` $integer, + `${CategoryTransactionFields.markedAsDeleted}` $integerNotNull CHECK (${CategoryTransactionFields.markedAsDeleted} IN (0, 1)), `${CategoryTransactionFields.createdAt}` $textNotNull, `${CategoryTransactionFields.updatedAt}` $textNotNull ) '''); + // Default "Uncategorized" Category + await database.execute(''' + INSERT INTO `$categoryTransactionTable`(`${CategoryTransactionFields.id}`, `${CategoryTransactionFields.name}`, `${CategoryTransactionFields.type}`, `${CategoryTransactionFields.symbol}`, `${CategoryTransactionFields.color}`, `${CategoryTransactionFields.note}`, `${CategoryTransactionFields.parent}`, `${CategoryTransactionFields.markedAsDeleted}`, `${CategoryTransactionFields.createdAt}`, `${CategoryTransactionFields.updatedAt}`) VALUES + (0, "Uncategorized", "IN", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'), + (1, "Uncategorized", "OUT", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'); + '''); + // Budget Table await database.execute(''' CREATE TABLE `$budgetTable`( @@ -142,7 +150,6 @@ class SossoldiDatabase { ("CHF", "CHF", "Switzerland Franc", 0), ("£", "GBP", "United Kingdom Pound", 0); '''); - } Future exportToCSV() async { @@ -180,7 +187,8 @@ class SossoldiDatabase { final List> rows = await db.query(tableName); for (var row in rows) { - List csvRow = List.filled(headers.length, ''); // Initialize with empty strings + List csvRow = + List.filled(headers.length, ''); // Initialize with empty strings csvRow[0] = tableName; // Set table name // Fill in values for existing columns @@ -215,7 +223,8 @@ class SossoldiDatabase { } final String csvData = await file.readAsString(); - final List> rows = const CsvToListConverter().convert(csvData); + final List> rows = + const CsvToListConverter().convert(csvData); if (rows.isEmpty) { throw Exception('CSV file is empty'); @@ -251,7 +260,8 @@ class SossoldiDatabase { for (int i = 1; i < tableRows.length; i++) { final Map row = {}; for (int j = 0; j < headers.length; j++) { - if (j != tableNameIndex) { // Skip the table_name column + if (j != tableNameIndex) { + // Skip the table_name column final String header = headers[j]; final dynamic value = tableRows[i][j]; @@ -289,14 +299,16 @@ class SossoldiDatabase { // Add fake categories await _database?.execute(''' - INSERT INTO categoryTransaction(id, name, type, symbol, color, note, parent, createdAt, updatedAt) VALUES - (10, "Out", "OUT", "restaurant", 0, '', null, '${DateTime.now()}', '${DateTime.now()}'), - (11, "Home", "OUT", "home", 1, '', null, '${DateTime.now()}', '${DateTime.now()}'), - (12, "Furniture","OUT", "home", 2, '', 11, '${DateTime.now()}', '${DateTime.now()}'), - (13, "Shopping", "OUT", "shopping_cart", 3, '', null, '${DateTime.now()}', '${DateTime.now()}'), - (14, "Leisure", "OUT", "subscriptions", 4, '', null, '${DateTime.now()}', '${DateTime.now()}'), - (15, "Transports", "OUT", "directions_car_rounded", 6, '', null, '${DateTime.now()}', '${DateTime.now()}'), - (16, "Salary", "IN", "work", 5, '', null, '${DateTime.now()}', '${DateTime.now()}'); + INSERT INTO categoryTransaction(id, name, type, symbol, color, note, parent, markedAsDeleted, createdAt, updatedAt) VALUES + (0, "Uncategorized", "IN", "question_mark", 0, 'This is a default category for no categorized transactions', null, 0, '${DateTime.now()}', '${DateTime.now()}'), + (1, "Uncategorized", "OUT", "question_mark", 0, 'This is a default category for no categorized transactions', null, 0, '${DateTime.now()}', '${DateTime.now()}'), + (10, "Out", "OUT", "restaurant", 1, '', null, 0, '${DateTime.now()}', '${DateTime.now()}'), + (11, "Home", "OUT", "home", 2, '', null, 0, '${DateTime.now()}', '${DateTime.now()}'), + (12, "Furniture","OUT", "home", 3, '', 11, 0, '${DateTime.now()}', '${DateTime.now()}'), + (13, "Shopping", "OUT", "shopping_cart", 4, '', null, 0, '${DateTime.now()}', '${DateTime.now()}'), + (14, "Leisure", "OUT", "subscriptions", 5, '', null, 0, '${DateTime.now()}', '${DateTime.now()}'), + (15, "Transports", "OUT", "directions_car_rounded", 6, '', null, 0, '${DateTime.now()}', '${DateTime.now()}'), + (16, "Salary", "IN", "work", 5, '', null, 0, '${DateTime.now()}', '${DateTime.now()}'); '''); // Add currencies diff --git a/lib/model/category_transaction.dart b/lib/model/category_transaction.dart index 1244a26b..c71ce836 100644 --- a/lib/model/category_transaction.dart +++ b/lib/model/category_transaction.dart @@ -12,6 +12,7 @@ class CategoryTransactionFields extends BaseEntityFields { static String color = 'color'; static String note = 'note'; static String parent = 'parent'; + static String markedAsDeleted = 'markedAsDeleted'; static String createdAt = BaseEntityFields.getCreatedAt; static String updatedAt = BaseEntityFields.getUpdatedAt; @@ -23,11 +24,49 @@ class CategoryTransactionFields extends BaseEntityFields { color, note, parent, + markedAsDeleted, BaseEntityFields.createdAt, BaseEntityFields.updatedAt ]; } +class CategoryFilter { + final bool showSystemCategories; + final bool showDeletedCategories; + + const CategoryFilter({ + this.showSystemCategories = false, + this.showDeletedCategories = false, + }); + + //Avoid useless Riverpod recostructions + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is CategoryFilter && + other.showSystemCategories == showSystemCategories && + other.showDeletedCategories == showDeletedCategories; + } + + @override + int get hashCode => + showSystemCategories.hashCode ^ showDeletedCategories.hashCode; +} + +const userCategoriesFilter = CategoryFilter( + showSystemCategories: false, + showDeletedCategories: false, +); + +const onlyActiveCategoriesFilter = CategoryFilter( + showSystemCategories: true, + showDeletedCategories: false, +); +const allCategoriesFilter = CategoryFilter( + showSystemCategories: true, + showDeletedCategories: true, +); + enum CategoryTransactionType { income, expense } Map categoryTypeMap = { @@ -42,6 +81,7 @@ class CategoryTransaction extends BaseEntity { final int color; final String? note; final int? parent; + final bool markedAsDeleted; const CategoryTransaction({ super.id, @@ -51,6 +91,7 @@ class CategoryTransaction extends BaseEntity { required this.color, this.note, this.parent, + required this.markedAsDeleted, super.createdAt, super.updatedAt, }); @@ -63,6 +104,7 @@ class CategoryTransaction extends BaseEntity { int? color, String? note, int? parent, + bool? markedAsDeleted, DateTime? createdAt, DateTime? updatedAt}) => CategoryTransaction( @@ -73,6 +115,7 @@ class CategoryTransaction extends BaseEntity { color: color ?? this.color, note: note ?? this.note, parent: parent ?? this.parent, + markedAsDeleted: markedAsDeleted ?? this.markedAsDeleted, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt); @@ -86,6 +129,9 @@ class CategoryTransaction extends BaseEntity { color: json[CategoryTransactionFields.color] as int, note: json[CategoryTransactionFields.note] as String?, parent: json[CategoryTransactionFields.parent] as int?, + markedAsDeleted: json[CategoryTransactionFields.markedAsDeleted] == 1 + ? true + : false, createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), updatedAt: DateTime.parse(json[BaseEntityFields.updatedAt] as String)); @@ -99,6 +145,7 @@ class CategoryTransaction extends BaseEntity { CategoryTransactionFields.color: color, CategoryTransactionFields.note: note, CategoryTransactionFields.parent: parent, + CategoryTransactionFields.markedAsDeleted: markedAsDeleted ? 1 : 0, BaseEntityFields.createdAt: update ? createdAt?.toIso8601String() : DateTime.now().toIso8601String(), @@ -141,6 +188,39 @@ class CategoryTransactionMethods extends SossoldiDatabase { return result.map((json) => CategoryTransaction.fromJson(json)).toList(); } + Future> selectCategories( + CategoryFilter filter) async { + final db = await database; + + String whereClause = ''; + List whereArgs = []; + + // showSystemCategories == false => no uncategorized + if (!filter.showSystemCategories) { + whereClause = + '${CategoryTransactionFields.id} != ? AND ${CategoryTransactionFields.id} != ?'; + whereArgs = [0, 1]; + } + + // showDeletedCategories == false => no markedAsDeleted + if (!filter.showDeletedCategories) { + if (whereClause.isNotEmpty) { + whereClause += ' AND '; + } + whereClause += '${CategoryTransactionFields.markedAsDeleted} = ?'; + whereArgs.add(0); + } + + final result = await db.query( + categoryTransactionTable, + where: whereClause.isNotEmpty ? whereClause : null, + whereArgs: whereArgs.isNotEmpty ? whereArgs : null, + orderBy: orderByASC, + ); + + return result.map((json) => CategoryTransaction.fromJson(json)).toList(); + } + Future> selectCategoriesByType( CategoryTransactionType type) async { final db = await database; @@ -173,6 +253,17 @@ class CategoryTransactionMethods extends SossoldiDatabase { ); } + Future markAsDeleted(int id) async { + final db = await database; + + return await db.update( + categoryTransactionTable, + {CategoryTransactionFields.markedAsDeleted: 1}, + where: '${CategoryTransactionFields.id} = ?', + whereArgs: [id], + ); + } + Future deleteById(int id) async { final db = await database; diff --git a/lib/pages/categories/add_category.dart b/lib/pages/categories/add_category.dart index a93272d4..12e97495 100644 --- a/lib/pages/categories/add_category.dart +++ b/lib/pages/categories/add_category.dart @@ -5,6 +5,7 @@ import '../../constants/functions.dart'; import '../../constants/style.dart'; import '../../model/category_transaction.dart'; import '../../providers/categories_provider.dart'; +import 'widgets/delete_category_dialog.dart'; class AddCategory extends ConsumerStatefulWidget { final bool hideIncome; @@ -374,14 +375,8 @@ class _AddCategoryState extends ConsumerState with Functions { width: double.infinity, padding: const EdgeInsets.all(16), child: TextButton.icon( - onPressed: () => ref - .read(categoriesProvider.notifier) - .removeCategory(selectedCategory.id!) - .whenComplete(() { - if (context.mounted) { - Navigator.of(context).pop(); - } - }), + onPressed: () => showDeleteCategoryDialog( + context, ref, selectedCategory), style: TextButton.styleFrom( side: const BorderSide(color: red, width: 1), ), @@ -427,7 +422,8 @@ class _AddCategoryState extends ConsumerState with Functions { if (nameController.text.isNotEmpty) { if (selectedCategory != null) { await ref - .read(categoriesProvider.notifier) + .read( + categoriesProvider(userCategoriesFilter).notifier) .updateCategory( name: nameController.text, type: categoryType, @@ -435,7 +431,10 @@ class _AddCategoryState extends ConsumerState with Functions { color: categoryColor, ); } else { - await ref.read(categoriesProvider.notifier).addCategory( + await ref + .read( + categoriesProvider(userCategoriesFilter).notifier) + .addCategory( name: nameController.text, type: categoryType, icon: categoryIcon, diff --git a/lib/pages/categories/category_list.dart b/lib/pages/categories/category_list.dart index 4372b5bd..25811712 100644 --- a/lib/pages/categories/category_list.dart +++ b/lib/pages/categories/category_list.dart @@ -18,7 +18,7 @@ class CategoryList extends ConsumerStatefulWidget { class _CategoryListState extends ConsumerState with Functions { @override Widget build(BuildContext context) { - final categorysList = ref.watch(categoriesProvider); + final categorysList = ref.watch(categoriesProvider(userCategoriesFilter)); ref.listen(selectedCategoryProvider, (_, __) {}); return Scaffold( appBar: AppBar( diff --git a/lib/pages/categories/widgets/delete_category_dialog.dart b/lib/pages/categories/widgets/delete_category_dialog.dart new file mode 100644 index 00000000..abab4fee --- /dev/null +++ b/lib/pages/categories/widgets/delete_category_dialog.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../model/category_transaction.dart'; +import '../../../providers/categories_provider.dart'; + +Future showDeleteCategoryDialog( + BuildContext context, WidgetRef ref, selectedCategory) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + content: const SingleChildScrollView( + child: ListBody( + children: [ + Text( + 'With “Mark as deleted,” transitions with the category will be available, but new ones cannot be created\n'), + Text( + 'With “Delete” all transitions with that category will automatically be “Uncategorized”'), + ], + ), + ), + actions: [ + TextButton( + child: Text( + "Mark as deleted", + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + onPressed: () => ref + .read(categoriesProvider(userCategoriesFilter).notifier) + .markAsDeleted(selectedCategory.id) + .whenComplete(() { + if (context.mounted) { + Navigator.of(context).pop(); + } + }), + ), + TextButton( + child: Text( + "Delete", + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + onPressed: () => ref + .read(categoriesProvider(userCategoriesFilter).notifier) + .removeCategory(selectedCategory.id!) + .whenComplete(() { + if (context.mounted) { + Navigator.of(context).pop(); + } + }), + ), + ], + ); + }, + ); +} diff --git a/lib/pages/onboarding_page/widgets/budget_setup.dart b/lib/pages/onboarding_page/widgets/budget_setup.dart index 289ba328..c28efbfb 100644 --- a/lib/pages/onboarding_page/widgets/budget_setup.dart +++ b/lib/pages/onboarding_page/widgets/budget_setup.dart @@ -2,6 +2,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../model/category_transaction.dart'; import '../../categories/add_category.dart'; import '/constants/constants.dart'; import '/constants/style.dart'; @@ -32,7 +33,7 @@ class _BudgetSetupState extends ConsumerState { totalBudget = budgetsList?.fold( 0, (total, budget) => total + budget.amountLimit) ?? 0; - final categoriesGrid = ref.watch(categoriesProvider); + final categoriesGrid = ref.watch(categoriesProvider(userCategoriesFilter)); return Scaffold( backgroundColor: blue7, body: SafeArea( diff --git a/lib/pages/planning_page/manage_budget_page.dart b/lib/pages/planning_page/manage_budget_page.dart index 326f4253..3cd960e2 100644 --- a/lib/pages/planning_page/manage_budget_page.dart +++ b/lib/pages/planning_page/manage_budget_page.dart @@ -21,8 +21,11 @@ class _ManageBudgetPageState extends ConsumerState { List deletedBudgets = []; void _loadCategories() async { - categories = await ref.read(categoriesProvider.notifier).getCategories(); - categories.removeWhere((element) => element.type == CategoryTransactionType.income); + categories = await ref + .read(categoriesProvider(userCategoriesFilter).notifier) + .getCategories(); + categories.removeWhere( + (element) => element.type == CategoryTransactionType.income); budgets = await ref.read(budgetsProvider.notifier).getBudgets(); setState(() {}); } @@ -101,16 +104,19 @@ class _ManageBudgetPageState extends ConsumerState { child: BudgetCategorySelector( categories: categories, categoriesAlreadyUsed: categories - .where((element) => budgets.map((e) => e.name).contains(element.name)) + .where((element) => + budgets.map((e) => e.name).contains(element.name)) .map((e) => e.name) .toList(), budget: budgets[index], initSelectedCategory: categories - .where((element) => element.id == budgets[index].idCategory) + .where((element) => + element.id == budgets[index].idCategory) .isEmpty ? categories[0] : categories - .where((element) => element.id == budgets[index].idCategory) + .where((element) => + element.id == budgets[index].idCategory) .first, onBudgetChanged: (updatedBudget) { updateBudget(updatedBudget, index); @@ -120,7 +126,8 @@ class _ManageBudgetPageState extends ConsumerState { }, ), SizedBox(height: 8), - Text("Swipe left to delete", style: Theme.of(context).textTheme.bodySmall), + Text("Swipe left to delete", + style: Theme.of(context).textTheme.bodySmall), SizedBox(height: 12), TextButton.icon( icon: Icon(Icons.add_circle, size: 32), diff --git a/lib/pages/planning_page/widget/recurring_payment_card.dart b/lib/pages/planning_page/widget/recurring_payment_card.dart index 7be6798c..c01b224c 100644 --- a/lib/pages/planning_page/widget/recurring_payment_card.dart +++ b/lib/pages/planning_page/widget/recurring_payment_card.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../constants/constants.dart'; import '../../../custom_widgets/rounded_icon.dart'; +import '../../../model/category_transaction.dart'; import '../../../model/recurring_transaction.dart'; import '../../../providers/theme_provider.dart'; import 'older_recurring_payments.dart'; @@ -35,7 +36,8 @@ class RecurringPaymentCard extends ConsumerWidget with Functions { @override Widget build(BuildContext context, WidgetRef ref) { - final categories = ref.watch(categoriesProvider).value; + final categories = + ref.watch(categoriesProvider(userCategoriesFilter)).value; final accounts = ref.watch(accountsProvider).value; final isDarkMode = ref.watch(appThemeStateNotifier).isDarkModeEnabled; final currencyState = ref.watch(currencyStateNotifier); @@ -51,8 +53,7 @@ class RecurringPaymentCard extends ConsumerWidget with Functions { boxShadow: [defaultShadow], ), child: Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), decoration: BoxDecoration( color: categoryColorList[cat.color].withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 0aacc75b..b1bb2d14 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -10,6 +10,7 @@ import '../constants/style.dart'; import '../custom_widgets/alert_dialog.dart'; import '../custom_widgets/default_card.dart'; import '../database/sossoldi_database.dart'; +import '../model/category_transaction.dart'; import '../providers/accounts_provider.dart'; import '../providers/budgets_provider.dart'; import '../providers/categories_provider.dart'; @@ -100,7 +101,8 @@ class _SettingsPageState extends ConsumerState { onPressed: () => Navigator.pop(context), child: Text( "OK", - style: TextStyle(color: Theme.of(context).colorScheme.primary), + style: + TextStyle(color: Theme.of(context).colorScheme.primary), ), ), ], @@ -129,7 +131,8 @@ class _SettingsPageState extends ConsumerState { child: Column( children: [ Padding( - padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0), + padding: + const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0), child: GestureDetector( onTap: _onSettingsTap, child: Row( @@ -152,7 +155,8 @@ class _SettingsPageState extends ConsumerState { style: Theme.of(context) .textTheme .headlineLarge! - .copyWith(color: Theme.of(context).colorScheme.primary), + .copyWith( + color: Theme.of(context).colorScheme.primary), ), ], ), @@ -204,14 +208,20 @@ class _SettingsPageState extends ConsumerState { style: Theme.of(context) .textTheme .titleLarge! - .copyWith(color: Theme.of(context).colorScheme.primary), + .copyWith( + color: Theme.of(context) + .colorScheme + .primary), ), Text( setting[2].toString(), style: Theme.of(context) .textTheme .bodySmall! - .copyWith(color: Theme.of(context).colorScheme.primary), + .copyWith( + color: Theme.of(context) + .colorScheme + .primary), overflow: TextOverflow.ellipsis, maxLines: 2, ), @@ -252,7 +262,7 @@ class _SettingsPageState extends ConsumerState { onPressed: () async { await SossoldiDatabase.instance.resetDatabase().then((v) { ref.refresh(accountsProvider); - ref.refresh(categoriesProvider); + ref.refresh(categoriesProvider(userCategoriesFilter)); ref.refresh(transactionsProvider); ref.refresh(budgetsProvider); showSuccessDialog(context, "DB Cleared"); @@ -263,15 +273,18 @@ class _SettingsPageState extends ConsumerState { child: const Text('CLEAR AND FILL DEMO DATA'), onPressed: () async { await SossoldiDatabase.instance.clearDatabase(); - await SossoldiDatabase.instance.fillDemoData().then((value) { + await SossoldiDatabase.instance + .fillDemoData() + .then((value) { ref.refresh(accountsProvider); - ref.refresh(categoriesProvider); + ref.refresh(categoriesProvider(userCategoriesFilter)); ref.refresh(transactionsProvider); ref.refresh(budgetsProvider); ref.refresh(dashboardProvider); ref.refresh(lastTransactionsProvider); ref.refresh(statisticsProvider); - showSuccessDialog(context, "DB Cleared, and DEMO data added"); + showSuccessDialog( + context, "DB Cleared, and DEMO data added"); }); }, ), diff --git a/lib/pages/transactions_page/widgets/categories_tab.dart b/lib/pages/transactions_page/widgets/categories_tab.dart index 01006011..a8eba40e 100644 --- a/lib/pages/transactions_page/widgets/categories_tab.dart +++ b/lib/pages/transactions_page/widgets/categories_tab.dart @@ -23,13 +23,14 @@ class CategoriesTab extends ConsumerStatefulWidget { class _CategoriesTabState extends ConsumerState with Functions { @override Widget build(BuildContext context) { - final categories = ref.watch(categoriesProvider); + final categories = ref.watch(categoriesProvider(userCategoriesFilter)); final transactions = ref.watch(transactionsProvider); final transactionType = ref.watch(selectedTransactionTypeProvider); // create a map to link each categories with a list of its transactions // stored as Transaction to be passed to CategoryListTile - Map> categoryToTransactionsIncome = {}, categoryToTransactionsExpense = {}; + Map> categoryToTransactionsIncome = {}, + categoryToTransactionsExpense = {}; Map categoryToAmountIncome = {}, categoryToAmountExpense = {}; double totalIncome = 0, totalExpense = 0; @@ -40,29 +41,37 @@ class _CategoriesTabState extends ConsumerState with Functions { if (categoryToTransactionsIncome.containsKey(categoryId)) { categoryToTransactionsIncome[categoryId]?.add(transaction); } else { - categoryToTransactionsIncome.putIfAbsent(categoryId, () => [transaction]); + categoryToTransactionsIncome.putIfAbsent( + categoryId, () => [transaction]); } // update total amount for the category totalIncome += transaction.amount; if (categoryToAmountIncome.containsKey(categoryId)) { - categoryToAmountIncome[categoryId] = categoryToAmountIncome[categoryId]! + transaction.amount.toDouble(); + categoryToAmountIncome[categoryId] = + categoryToAmountIncome[categoryId]! + + transaction.amount.toDouble(); } else { - categoryToAmountIncome.putIfAbsent(categoryId, () => transaction.amount.toDouble()); + categoryToAmountIncome.putIfAbsent( + categoryId, () => transaction.amount.toDouble()); } } else if (transaction.type == TransactionType.expense) { if (categoryToTransactionsExpense.containsKey(categoryId)) { categoryToTransactionsExpense[categoryId]?.add(transaction); } else { - categoryToTransactionsExpense.putIfAbsent(categoryId, () => [transaction]); + categoryToTransactionsExpense.putIfAbsent( + categoryId, () => [transaction]); } // update total amount for the category totalExpense -= transaction.amount; if (categoryToAmountExpense.containsKey(categoryId)) { - categoryToAmountExpense[categoryId] = categoryToAmountExpense[categoryId]! - transaction.amount.toDouble(); + categoryToAmountExpense[categoryId] = + categoryToAmountExpense[categoryId]! - + transaction.amount.toDouble(); } else { - categoryToAmountExpense.putIfAbsent(categoryId, () => -transaction.amount.toDouble()); + categoryToAmountExpense.putIfAbsent( + categoryId, () => -transaction.amount.toDouble()); } } } @@ -77,10 +86,14 @@ class _CategoriesTabState extends ConsumerState with Functions { const SizedBox(height: 16), categories.when( data: (data) { - List categoryIncomeList = - data.where((category) => categoryToAmountIncome.containsKey(category.id)).toList(); - List categoryExpenseList = - data.where((category) => categoryToAmountExpense.containsKey(category.id)).toList(); + List categoryIncomeList = data + .where((category) => + categoryToAmountIncome.containsKey(category.id)) + .toList(); + List categoryExpenseList = data + .where((category) => + categoryToAmountExpense.containsKey(category.id)) + .toList(); return transactionType == TransactionType.income ? categoryIncomeList.isEmpty ? const SizedBox( @@ -101,14 +114,24 @@ class _CategoriesTabState extends ConsumerState with Functions { shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: categoryIncomeList.length, - separatorBuilder: (context, index) => const SizedBox(height: 10), + separatorBuilder: (context, index) => + const SizedBox(height: 10), itemBuilder: (context, index) { - CategoryTransaction category = categoryIncomeList[index]; + CategoryTransaction category = + categoryIncomeList[index]; return CategoryListTile( category: category, - transactions: categoryToTransactionsIncome[category.id] ?? [], - amount: categoryToAmountIncome[category.id] ?? 0, - percent: (categoryToAmountIncome[category.id] ?? 0) / totalIncome * 100, + transactions: categoryToTransactionsIncome[ + category.id] ?? + [], + amount: + categoryToAmountIncome[category.id] ?? + 0, + percent: + (categoryToAmountIncome[category.id] ?? + 0) / + totalIncome * + 100, index: index, ); }, @@ -134,14 +157,24 @@ class _CategoriesTabState extends ConsumerState with Functions { shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: categoryExpenseList.length, - separatorBuilder: (context, index) => const SizedBox(height: 10), + separatorBuilder: (context, index) => + const SizedBox(height: 10), itemBuilder: (context, index) { - CategoryTransaction category = categoryExpenseList[index]; + CategoryTransaction category = + categoryExpenseList[index]; return CategoryListTile( category: category, - transactions: categoryToTransactionsExpense[category.id] ?? [], - amount: categoryToAmountExpense[category.id] ?? 0, - percent: (categoryToAmountExpense[category.id] ?? 0) / totalExpense * 100, + transactions: categoryToTransactionsExpense[ + category.id] ?? + [], + amount: + categoryToAmountExpense[category.id] ?? + 0, + percent: + (categoryToAmountExpense[category.id] ?? + 0) / + totalExpense * + 100, index: index, ); }, diff --git a/lib/providers/categories_provider.dart b/lib/providers/categories_provider.dart index 08073628..95b51237 100644 --- a/lib/providers/categories_provider.dart +++ b/lib/providers/categories_provider.dart @@ -16,14 +16,17 @@ final categoryTypeProvider = StateProvider( final selectedCategoryIndexProvider = StateProvider.autoDispose((ref) => -1); -class AsyncCategoriesNotifier extends AsyncNotifier> { +class AsyncCategoriesNotifier + extends FamilyAsyncNotifier, CategoryFilter> { @override - Future> build() async { - return _getCategories(); + Future> build(CategoryFilter filter) async { + return _getCategories(filter); } - Future> _getCategories() async { - final categories = await CategoryTransactionMethods().selectAll(); + Future> _getCategories( + CategoryFilter filter) async { + final categories = + await CategoryTransactionMethods().selectCategories(filter); return categories; } @@ -38,13 +41,14 @@ class AsyncCategoriesNotifier extends AsyncNotifier> { symbol: icon, type: type, color: color, + markedAsDeleted: false, ); state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { await CategoryTransactionMethods().insert(category); ref.invalidate(categoriesByTypeProvider(category.type)); - return _getCategories(); + return _getCategories(arg); }); } @@ -64,7 +68,15 @@ class AsyncCategoriesNotifier extends AsyncNotifier> { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { await CategoryTransactionMethods().updateItem(category); - return _getCategories(); + return _getCategories(arg); + }); + } + + Future markAsDeleted(int categoryId) async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + await CategoryTransactionMethods().markAsDeleted(categoryId); + return _getCategories(arg); }); } @@ -72,20 +84,17 @@ class AsyncCategoriesNotifier extends AsyncNotifier> { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { await CategoryTransactionMethods().deleteById(categoryId); - return _getCategories(); + return _getCategories(arg); }); } Future> getCategories() async { - return _getCategories(); + return _getCategories(arg); } } -final categoriesProvider = - AsyncNotifierProvider>( - () { - return AsyncCategoriesNotifier(); -}); +final categoriesProvider = AsyncNotifierProviderFamily, CategoryFilter>(() => AsyncCategoriesNotifier()); final categoriesByTypeProvider = FutureProvider.family, CategoryTransactionType?>( @@ -178,9 +187,7 @@ final categoryToTransactionProvider = return CategoryTransactionMethods().categoryToTransactionType(type); }); -final monthlyTotalsProvider = - FutureProvider>( - (ref) async { +final monthlyTotalsProvider = FutureProvider>((ref) async { final categoryType = ref.watch(categoryTypeProvider); final dateStart = ref.watch(filterDateStartProvider); //final dateEnd = ref.watch(filterDateEndProvider); diff --git a/test/model/category_transaction_test.dart b/test/model/category_transaction_test.dart index aff43491..efa0c805 100644 --- a/test/model/category_transaction_test.dart +++ b/test/model/category_transaction_test.dart @@ -11,6 +11,7 @@ void main() { type: CategoryTransactionType.expense, symbol: "symbol", color: 0, + markedAsDeleted: false, createdAt: DateTime.utc(2022), updatedAt: DateTime.utc(2022)); @@ -21,6 +22,7 @@ void main() { assert(cCopy.type == c.type); assert(cCopy.symbol == c.symbol); assert(cCopy.color == c.color); + assert(cCopy.markedAsDeleted == c.markedAsDeleted); assert(cCopy.createdAt == c.createdAt); assert(cCopy.updatedAt == c.updatedAt); }); @@ -45,19 +47,21 @@ void main() { assert(c.symbol == json[CategoryTransactionFields.symbol]); assert(c.color == json[CategoryTransactionFields.color]); assert(c.note == json[CategoryTransactionFields.note]); - assert(c.createdAt?.toUtc().toIso8601String() == json[BaseEntityFields.createdAt]); - assert(c.updatedAt?.toUtc().toIso8601String() == json[BaseEntityFields.updatedAt]); + assert(c.createdAt?.toUtc().toIso8601String() == + json[BaseEntityFields.createdAt]); + assert(c.updatedAt?.toUtc().toIso8601String() == + json[BaseEntityFields.updatedAt]); }); test("Test toJson Category Transaction", () { CategoryTransaction c = const CategoryTransaction( - id: 2, - name: "name", - type: CategoryTransactionType.expense, - symbol: "symbol", - color: 0, - note: "note", - ); + id: 2, + name: "name", + type: CategoryTransactionType.expense, + symbol: "symbol", + color: 0, + note: "note", + markedAsDeleted: false); Map json = c.toJson(); @@ -67,5 +71,7 @@ void main() { assert(c.symbol == json[CategoryTransactionFields.symbol]); assert(c.color == json[CategoryTransactionFields.color]); assert(c.note == json[CategoryTransactionFields.note]); + assert((c.markedAsDeleted ? 1 : 0) == + json[CategoryTransactionFields.markedAsDeleted]); }); } From bbe4a76b36340970f5170f5756b187a0eaead095 Mon Sep 17 00:00:00 2001 From: napitek Date: Wed, 19 Mar 2025 23:37:19 +0100 Subject: [PATCH 02/28] markedAsDelete - delete CategoryTransaction workflow --- lib/model/category_transaction.dart | 2 +- .../add_page/widgets/category_selector.dart | 93 ++++++++++--------- .../widgets/delete_category_dialog.dart | 25 ++--- .../planning_page/widget/budget_card.dart | 57 +++++++----- .../widget/budget_pie_chart.dart | 6 +- .../widget/recurring_payment_card.dart | 3 +- .../widgets/categories_tab.dart | 2 +- lib/providers/categories_provider.dart | 30 ++++++ 8 files changed, 135 insertions(+), 83 deletions(-) diff --git a/lib/model/category_transaction.dart b/lib/model/category_transaction.dart index c71ce836..638e5ff5 100644 --- a/lib/model/category_transaction.dart +++ b/lib/model/category_transaction.dart @@ -58,7 +58,7 @@ const userCategoriesFilter = CategoryFilter( showDeletedCategories: false, ); -const onlyActiveCategoriesFilter = CategoryFilter( +const availableCategoriesFilter = CategoryFilter( showSystemCategories: true, showDeletedCategories: false, ); diff --git a/lib/pages/add_page/widgets/category_selector.dart b/lib/pages/add_page/widgets/category_selector.dart index 4d74c684..23fffbda 100644 --- a/lib/pages/add_page/widgets/category_selector.dart +++ b/lib/pages/add_page/widgets/category_selector.dart @@ -66,45 +66,54 @@ class _CategorySelectorState extends ConsumerState height: 74, width: double.infinity, child: categoriesList.when( - data: (categories) => ListView.builder( - itemCount: categories.length, // to prevent range error - scrollDirection: Axis.horizontal, - shrinkWrap: true, - itemBuilder: (context, i) { - CategoryTransaction category = categories[i]; - return GestureDetector( - onTap: () => { - ref.read(categoryProvider.notifier).state = - category, - Navigator.of(context).pop(), - }, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - RoundedIcon( - icon: iconList[category.symbol], - backgroundColor: - categoryColorListTheme[category.color], - ), - Text( - category.name, - style: Theme.of(context) - .textTheme - .labelLarge! - .copyWith( - color: Theme.of(context) - .colorScheme - .primary), - ), - ], + data: (categories) { + //availableCategories without markedAsDeleted + final availableCategories = categories + .where( + (category) => category.markedAsDeleted == false) + .toList(); + + return ListView.builder( + itemCount: availableCategories.length, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + itemBuilder: (context, i) { + CategoryTransaction category = + availableCategories[i]; + return GestureDetector( + onTap: () => { + ref.read(categoryProvider.notifier).state = + category, + Navigator.of(context).pop(), + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RoundedIcon( + icon: iconList[category.symbol], + backgroundColor: categoryColorListTheme[ + category.color], + ), + Text( + category.name, + style: Theme.of(context) + .textTheme + .labelLarge! + .copyWith( + color: Theme.of(context) + .colorScheme + .primary), + ), + ], + ), ), - ), - ); - }, - ), + ); + }, + ); + }, loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Text('Error: $err'), @@ -142,10 +151,10 @@ class _CategorySelectorState extends ConsumerState categoryColorListTheme[category.color], ), title: Text(category.name), - trailing: ref.watch(categoryProvider)?.id == - category.id - ? Icon(Icons.check) - : null, + trailing: + ref.watch(categoryProvider)?.id == category.id + ? Icon(Icons.check) + : null, ); }, ), diff --git a/lib/pages/categories/widgets/delete_category_dialog.dart b/lib/pages/categories/widgets/delete_category_dialog.dart index abab4fee..539e3ca9 100644 --- a/lib/pages/categories/widgets/delete_category_dialog.dart +++ b/lib/pages/categories/widgets/delete_category_dialog.dart @@ -5,6 +5,13 @@ import '../../../providers/categories_provider.dart'; Future showDeleteCategoryDialog( BuildContext context, WidgetRef ref, selectedCategory) async { + void backToCategoryList() { + if (context.mounted) { + Navigator.of(context) + .popUntil((route) => route.settings.name == '/category-list'); + } + } + return showDialog( context: context, builder: (BuildContext context) { @@ -13,9 +20,11 @@ Future showDeleteCategoryDialog( child: ListBody( children: [ Text( - 'With “Mark as deleted,” transitions with the category will be available, but new ones cannot be created\n'), + 'With "Mark as deleted," transitions with the category will be available, but new ones cannot be created\n', + ), Text( - 'With “Delete” all transitions with that category will automatically be “Uncategorized”'), + 'With "Delete" all transitions with that category will automatically be "Uncategorized"', + ), ], ), ), @@ -28,11 +37,7 @@ Future showDeleteCategoryDialog( onPressed: () => ref .read(categoriesProvider(userCategoriesFilter).notifier) .markAsDeleted(selectedCategory.id) - .whenComplete(() { - if (context.mounted) { - Navigator.of(context).pop(); - } - }), + .whenComplete(backToCategoryList), ), TextButton( child: Text( @@ -42,11 +47,7 @@ Future showDeleteCategoryDialog( onPressed: () => ref .read(categoriesProvider(userCategoriesFilter).notifier) .removeCategory(selectedCategory.id!) - .whenComplete(() { - if (context.mounted) { - Navigator.of(context).pop(); - } - }), + .whenComplete(backToCategoryList), ), ], ); diff --git a/lib/pages/planning_page/widget/budget_card.dart b/lib/pages/planning_page/widget/budget_card.dart index 78dda331..ce6b50da 100644 --- a/lib/pages/planning_page/widget/budget_card.dart +++ b/lib/pages/planning_page/widget/budget_card.dart @@ -5,6 +5,7 @@ import '../../../custom_widgets/default_container.dart'; import '../../../model/budget.dart'; import '../../../model/transaction.dart'; import '../../../providers/budgets_provider.dart'; +import '../../../providers/categories_provider.dart'; import '../../../providers/currency_provider.dart'; import '../../../providers/transactions_provider.dart'; import '../../graphs_page/widgets/linear_progress_bar.dart'; @@ -23,7 +24,8 @@ class _BudgetCardState extends ConsumerState { @override Widget build(BuildContext context) { final budgets = ref.watch(budgetsProvider.notifier).getBudgets(); - final transactions = ref.watch(transactionsProvider.notifier).getMonthlyTransactions(); + final transactions = + ref.watch(transactionsProvider.notifier).getMonthlyTransactions(); final currencyState = ref.watch(currencyStateNotifier); return DefaultContainer( @@ -44,47 +46,57 @@ class _BudgetCardState extends ConsumerState { ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("Composition", style: Theme.of(context).textTheme.titleLarge), + Text("Composition", + style: Theme.of(context).textTheme.titleLarge), BudgetPieChart(budgets: budgets as List), - Text("Progress", style: Theme.of(context).textTheme.titleLarge), + Text("Progress", + style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 10), ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: budgets.length, itemBuilder: (BuildContext context, int index) { - num spent = num.parse((transactions as List) - .where((t) => t.idCategory == budgets[index].idCategory) - .fold(0.0, (sum, t) => sum + t.amount) - .toStringAsFixed(2)); + num spent = num.parse( + (transactions as List) + .where((t) => + t.idCategory == budgets[index].idCategory) + .fold(0.0, (sum, t) => sum + t.amount) + .toStringAsFixed(2)); Budget budget = budgets.elementAt(index); + final budgetCategory = ref + .watch(categoryByIdProvider(budget.idCategory)) + .value; return Column( children: [ Row( children: [ Text( budget.name!, - style: const TextStyle(fontWeight: FontWeight.normal), + style: const TextStyle( + fontWeight: FontWeight.normal), ), const Spacer(), spent >= (budget.amountLimit * 0.9) - ? const Icon(Icons.error_outline, color: Colors.red) + ? const Icon(Icons.error_outline, + color: Colors.red) : Container(), Text( "$spent${currencyState.selectedCurrency.symbol}/${budget.amountLimit}${currencyState.selectedCurrency.symbol}", - style: const TextStyle(fontWeight: FontWeight.normal), + style: const TextStyle( + fontWeight: FontWeight.normal), ), ], ), const SizedBox(height: 4), LinearProgressBar( - type: BarType.category, - colorIndex: index, - amount: (spent == 0 || budget.amountLimit == 0) - ? 0 - : spent, - total: budget.amountLimit - ), + type: BarType.category, + colorIndex: budgetCategory?.color ?? 1, + amount: + (spent == 0 || budget.amountLimit == 0) + ? 0 + : spent, + total: budget.amountLimit), ], ); }, @@ -131,17 +143,16 @@ class _BudgetCardState extends ConsumerState { builder: (BuildContext context) { return FractionallySizedBox( heightFactor: 0.9, - child: - ManageBudgetPage(onRefreshBudgets: widget.onRefreshBudgets)); + child: ManageBudgetPage( + onRefreshBudgets: + widget.onRefreshBudgets)); }, ); }, label: Text( "Create budget", - style: Theme.of(context) - .textTheme - .titleSmall! - .apply(color: Theme.of(context).colorScheme.secondary), + style: Theme.of(context).textTheme.titleSmall!.apply( + color: Theme.of(context).colorScheme.secondary), ), style: TextButton.styleFrom( backgroundColor: Colors.white, diff --git a/lib/pages/planning_page/widget/budget_pie_chart.dart b/lib/pages/planning_page/widget/budget_pie_chart.dart index 71668c14..6fd4deaf 100644 --- a/lib/pages/planning_page/widget/budget_pie_chart.dart +++ b/lib/pages/planning_page/widget/budget_pie_chart.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../constants/constants.dart'; import '../../../model/budget.dart'; +import '../../../providers/categories_provider.dart'; import '../../../providers/currency_provider.dart'; class BudgetPieChart extends ConsumerStatefulWidget { @@ -50,11 +51,12 @@ class BudgetPieChartState extends ConsumerState { List showingSections() { return List.generate(widget.budgets.length, (i) { final Budget budget = widget.budgets.elementAt(i); - + final budgetCategory = + ref.watch(categoryByIdProvider(budget.idCategory)).value; double value = (budget.amountLimit / totalBudget) * 100; return PieChartSectionData( - color: categoryColorList[i], + color: categoryColorList[budgetCategory?.color ?? 1], value: value, title: "", radius: 20, diff --git a/lib/pages/planning_page/widget/recurring_payment_card.dart b/lib/pages/planning_page/widget/recurring_payment_card.dart index c01b224c..227cd75f 100644 --- a/lib/pages/planning_page/widget/recurring_payment_card.dart +++ b/lib/pages/planning_page/widget/recurring_payment_card.dart @@ -36,8 +36,7 @@ class RecurringPaymentCard extends ConsumerWidget with Functions { @override Widget build(BuildContext context, WidgetRef ref) { - final categories = - ref.watch(categoriesProvider(userCategoriesFilter)).value; + final categories = ref.watch(categoriesProvider(allCategoriesFilter)).value; final accounts = ref.watch(accountsProvider).value; final isDarkMode = ref.watch(appThemeStateNotifier).isDarkModeEnabled; final currencyState = ref.watch(currencyStateNotifier); diff --git a/lib/pages/transactions_page/widgets/categories_tab.dart b/lib/pages/transactions_page/widgets/categories_tab.dart index a8eba40e..0c72493c 100644 --- a/lib/pages/transactions_page/widgets/categories_tab.dart +++ b/lib/pages/transactions_page/widgets/categories_tab.dart @@ -23,7 +23,7 @@ class CategoriesTab extends ConsumerStatefulWidget { class _CategoriesTabState extends ConsumerState with Functions { @override Widget build(BuildContext context) { - final categories = ref.watch(categoriesProvider(userCategoriesFilter)); + final categories = ref.watch(categoriesProvider(allCategoriesFilter)); final transactions = ref.watch(transactionsProvider); final transactionType = ref.watch(selectedTransactionTypeProvider); diff --git a/lib/providers/categories_provider.dart b/lib/providers/categories_provider.dart index 95b51237..be4f0200 100644 --- a/lib/providers/categories_provider.dart +++ b/lib/providers/categories_provider.dart @@ -80,9 +80,33 @@ class AsyncCategoriesNotifier }); } + final reassignTransactionsProvider = + Provider Function(int, CategoryTransactionType)>((ref) { + return (int categoryId, CategoryTransactionType categoryType) async { + final defaultCategoryId = + categoryType == CategoryTransactionType.income ? 0 : 1; + + final transactionMethods = TransactionMethods(); + final transactions = await transactionMethods.selectAll(); + final affectedTransactions = + transactions.where((t) => t.idCategory == categoryId).toList(); + + for (var transaction in affectedTransactions) { + final updatedTransaction = + transaction.copy(idCategory: defaultCategoryId); + await transactionMethods.updateItem(updatedTransaction); + } + + ref.invalidate(transactionsProvider); + }; + }); + Future removeCategory(int categoryId) async { + final category = await CategoryTransactionMethods().selectById(categoryId); + state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { + await ref.read(reassignTransactionsProvider)(categoryId, category.type); await CategoryTransactionMethods().deleteById(categoryId); return _getCategories(arg); }); @@ -107,6 +131,12 @@ final categoriesByTypeProvider = return categories; }); +final categoryByIdProvider = + FutureProvider.family((ref, id) async { + final category = await CategoryTransactionMethods().selectById(id); + return category; +}); + final categoryMapProvider = FutureProvider>((ref) async { final categoryType = ref.watch(categoryTypeProvider); From 221f9e92a34ca86e8216757fe9c59705403c8576 Mon Sep 17 00:00:00 2001 From: napitek Date: Wed, 19 Mar 2025 23:50:54 +0100 Subject: [PATCH 03/28] Reassign CategoryTransaction to recurring transactions --- lib/providers/categories_provider.dart | 30 +++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/providers/categories_provider.dart b/lib/providers/categories_provider.dart index be4f0200..3eb4d79c 100644 --- a/lib/providers/categories_provider.dart +++ b/lib/providers/categories_provider.dart @@ -1,6 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../model/category_transaction.dart'; +import '../model/recurring_transaction.dart'; import '../model/transaction.dart'; import 'transactions_provider.dart'; @@ -86,15 +87,36 @@ class AsyncCategoriesNotifier final defaultCategoryId = categoryType == CategoryTransactionType.income ? 0 : 1; - final transactionMethods = TransactionMethods(); - final transactions = await transactionMethods.selectAll(); + final transactions = await TransactionMethods().selectAll(); final affectedTransactions = transactions.where((t) => t.idCategory == categoryId).toList(); for (var transaction in affectedTransactions) { final updatedTransaction = transaction.copy(idCategory: defaultCategoryId); - await transactionMethods.updateItem(updatedTransaction); + await TransactionMethods().updateItem(updatedTransaction); + } + + ref.invalidate(transactionsProvider); + }; + }); + + final reassignRecurringTransactionsProvider = + Provider Function(int, CategoryTransactionType)>((ref) { + return (int categoryId, CategoryTransactionType categoryType) async { + final defaultCategoryId = + categoryType == CategoryTransactionType.income ? 0 : 1; + + final recurringTransactions = + await RecurringTransactionMethods().selectAll(); + final affectedRecurringTransactions = recurringTransactions + .where((t) => t.idCategory == categoryId) + .toList(); + + for (var recurringTransaction in affectedRecurringTransactions) { + final updatedTransaction = + recurringTransaction.copy(idCategory: defaultCategoryId); + await RecurringTransactionMethods().updateItem(updatedTransaction); } ref.invalidate(transactionsProvider); @@ -107,6 +129,8 @@ class AsyncCategoriesNotifier state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { await ref.read(reassignTransactionsProvider)(categoryId, category.type); + await ref.read(reassignRecurringTransactionsProvider)( + categoryId, category.type); await CategoryTransactionMethods().deleteById(categoryId); return _getCategories(arg); }); From 5351b3e077a9a8aa20e691859d371d0ead8152b0 Mon Sep 17 00:00:00 2001 From: napitek Date: Thu, 20 Mar 2025 00:27:26 +0100 Subject: [PATCH 04/28] UNIQUE constraint failed: categoryTransaction.id, constraint failed (code 1555) --- lib/database/sossoldi_database.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/database/sossoldi_database.dart b/lib/database/sossoldi_database.dart index 26747e4e..d2af49f7 100644 --- a/lib/database/sossoldi_database.dart +++ b/lib/database/sossoldi_database.dart @@ -299,7 +299,7 @@ class SossoldiDatabase { // Add fake categories await _database?.execute(''' - INSERT INTO categoryTransaction(id, name, type, symbol, color, note, parent, markedAsDeleted, createdAt, updatedAt) VALUES + INSERT OR IGNORE INTO categoryTransaction(id, name, type, symbol, color, note, parent, markedAsDeleted, createdAt, updatedAt) VALUES (0, "Uncategorized", "IN", "question_mark", 0, 'This is a default category for no categorized transactions', null, 0, '${DateTime.now()}', '${DateTime.now()}'), (1, "Uncategorized", "OUT", "question_mark", 0, 'This is a default category for no categorized transactions', null, 0, '${DateTime.now()}', '${DateTime.now()}'), (10, "Out", "OUT", "restaurant", 1, '', null, 0, '${DateTime.now()}', '${DateTime.now()}'), From 725722d64203a41368b965f5fcb8078db9bbab9e Mon Sep 17 00:00:00 2001 From: napitek Date: Fri, 21 Mar 2025 00:26:35 +0100 Subject: [PATCH 05/28] Fix ALL CATEGORIES section filter --- .../add_page/widgets/category_selector.dart | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/lib/pages/add_page/widgets/category_selector.dart b/lib/pages/add_page/widgets/category_selector.dart index 23fffbda..d8469e4f 100644 --- a/lib/pages/add_page/widgets/category_selector.dart +++ b/lib/pages/add_page/widgets/category_selector.dart @@ -130,35 +130,46 @@ class _CategorySelectorState extends ConsumerState ), ), categoriesList.when( - data: (categories) => Container( - color: Theme.of(context).colorScheme.surface, - child: ListView.separated( - itemCount: categories.length, - scrollDirection: Axis.vertical, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (context, index) => - const Divider(height: 1, color: grey1), - itemBuilder: (context, i) { - CategoryTransaction category = categories[i]; - return ListTile( - onTap: () => ref - .read(categoryProvider.notifier) - .state = category, - leading: RoundedIcon( - icon: iconList[category.symbol], - backgroundColor: - categoryColorListTheme[category.color], - ), - title: Text(category.name), - trailing: - ref.watch(categoryProvider)?.id == category.id - ? Icon(Icons.check) - : null, - ); - }, - ), - ), + data: (categories) { + //availableCategories without markedAsDeleted + final availableCategories = categories + .where( + (category) => category.markedAsDeleted == false) + .toList(); + + return Container( + color: Theme.of(context).colorScheme.surface, + child: ListView.separated( + itemCount: availableCategories.length, + scrollDirection: Axis.vertical, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => + const Divider(height: 1, color: grey1), + itemBuilder: (context, i) { + CategoryTransaction category = + availableCategories[i]; + return ListTile( + onTap: () => { + ref.read(categoryProvider.notifier).state = + category, + Navigator.of(context).pop(), + }, + leading: RoundedIcon( + icon: iconList[category.symbol], + backgroundColor: + categoryColorListTheme[category.color], + ), + title: Text(category.name), + trailing: + ref.watch(categoryProvider)?.id == category.id + ? Icon(Icons.check) + : null, + ); + }, + ), + ); + }, loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Text('Error: $err'), From b357142698e75c705ce6ab4459ad2e6dd370b41d Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 30 Mar 2025 18:55:02 +0200 Subject: [PATCH 06/28] fix Uncategorized CategoryTransaction unselectable --- .../add_page/widgets/category_selector.dart | 14 +++---- .../widgets/delete_category_dialog.dart | 40 ++++++++++--------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/lib/pages/add_page/widgets/category_selector.dart b/lib/pages/add_page/widgets/category_selector.dart index d8469e4f..03432254 100644 --- a/lib/pages/add_page/widgets/category_selector.dart +++ b/lib/pages/add_page/widgets/category_selector.dart @@ -28,7 +28,7 @@ class _CategorySelectorState extends ConsumerState final transactionType = ref.watch(transactionTypeProvider); final categoryType = ref.watch(transactionToCategoryProvider(transactionType)); - final categoriesList = ref.watch(categoriesByTypeProvider(categoryType)); + final categoriesList = ref.watch(categoriesProvider(userCategoriesFilter)); return Container( color: Theme.of(context).colorScheme.primaryContainer, @@ -67,10 +67,10 @@ class _CategorySelectorState extends ConsumerState width: double.infinity, child: categoriesList.when( data: (categories) { - //availableCategories without markedAsDeleted final availableCategories = categories - .where( - (category) => category.markedAsDeleted == false) + .where((category) => + category.type == categoryType && + !category.markedAsDeleted) .toList(); return ListView.builder( @@ -131,10 +131,10 @@ class _CategorySelectorState extends ConsumerState ), categoriesList.when( data: (categories) { - //availableCategories without markedAsDeleted final availableCategories = categories - .where( - (category) => category.markedAsDeleted == false) + .where((category) => + category.type == categoryType && + !category.markedAsDeleted) .toList(); return Container( diff --git a/lib/pages/categories/widgets/delete_category_dialog.dart b/lib/pages/categories/widgets/delete_category_dialog.dart index 539e3ca9..f073221b 100644 --- a/lib/pages/categories/widgets/delete_category_dialog.dart +++ b/lib/pages/categories/widgets/delete_category_dialog.dart @@ -30,25 +30,29 @@ Future showDeleteCategoryDialog( ), actions: [ TextButton( - child: Text( - "Mark as deleted", - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - onPressed: () => ref - .read(categoriesProvider(userCategoriesFilter).notifier) - .markAsDeleted(selectedCategory.id) - .whenComplete(backToCategoryList), - ), + child: Text( + "Mark as deleted", + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + onPressed: () async { + ref + .read(categoriesProvider(userCategoriesFilter).notifier) + .markAsDeleted(selectedCategory.id) + .whenComplete(backToCategoryList); + final _ = ref.refresh(categoriesProvider(userCategoriesFilter)); + }), TextButton( - child: Text( - "Delete", - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - onPressed: () => ref - .read(categoriesProvider(userCategoriesFilter).notifier) - .removeCategory(selectedCategory.id!) - .whenComplete(backToCategoryList), - ), + child: Text( + "Delete", + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + onPressed: () async { + ref + .read(categoriesProvider(userCategoriesFilter).notifier) + .removeCategory(selectedCategory.id!) + .whenComplete(backToCategoryList); + final _ = ref.refresh(categoriesProvider(userCategoriesFilter)); + }), ], ); }, From 7abd401f5d61c2f4d642fca7d6c35ae9c3038f74 Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 30 Mar 2025 19:14:26 +0200 Subject: [PATCH 07/28] adapt the changes to the MigrationManager --- lib/database/migration_base.dart | 47 +++++++ lib/database/migration_manager.dart | 28 ++++ .../migrations/0001_initial_schema.dart | 123 +++++++++++++++++ .../migrations/0002_account_net_worth.dart | 20 +++ .../migrations/migration_registry.dart | 44 ++++++ lib/database/sossoldi_database.dart | 126 ++---------------- lib/model/bank_account.dart | 104 +++++++++------ lib/pages/accounts/add_account.dart | 35 ++--- lib/providers/accounts_provider.dart | 99 +++++++++----- 9 files changed, 417 insertions(+), 209 deletions(-) create mode 100644 lib/database/migration_base.dart create mode 100644 lib/database/migration_manager.dart create mode 100644 lib/database/migrations/0001_initial_schema.dart create mode 100644 lib/database/migrations/0002_account_net_worth.dart create mode 100644 lib/database/migrations/migration_registry.dart diff --git a/lib/database/migration_base.dart b/lib/database/migration_base.dart new file mode 100644 index 00000000..54275ddd --- /dev/null +++ b/lib/database/migration_base.dart @@ -0,0 +1,47 @@ +import 'package:sqflite/sqflite.dart'; + +/// Represents a database migration that can be applied to evolve the schema. +/// +/// Each migration has a database [version] number and a [description] that +/// explains what the migration does. +/// Migrations are collected in the migration registry and executed in order based +/// on their version numbers when the database needs to be created or upgraded. +/// +/// To create a new migration: +/// 1. Extend this class with a concrete implementation +/// 2. Implement the [up] method with the schema changes +/// 3. (optional) Implement the [down] method to revert those changes +/// 4. Add your migration to the registry in `migration_registry.dart` +/// +/// Example: +/// ```dart +/// class AddUserAvatarMigration extends Migration { +/// AddUserAvatarMigration() +/// : super( +/// version: 3, +/// description: 'Add avatar column to user table' +/// ); +/// +/// @override +/// Future up(Database db) async { +/// await db.execute('ALTER TABLE users ADD COLUMN avatar TEXT'); +/// } +/// +/// @override +/// Future down(Database db) async { +/// // reserved for future use +/// } +/// } +/// ``` +abstract class Migration { + /// The database version number of this migration. + final int version; + + /// A description of what this migration does. + final String description; + + Migration({required this.version, required this.description}); + + /// Applies this migration to upgrade the database schema. + Future up(Database db); +} diff --git a/lib/database/migration_manager.dart b/lib/database/migration_manager.dart new file mode 100644 index 00000000..79b4d286 --- /dev/null +++ b/lib/database/migration_manager.dart @@ -0,0 +1,28 @@ +import 'package:flutter/foundation.dart'; +import 'package:sqflite/sqflite.dart'; +import 'migration_base.dart'; +import 'migrations/migration_registry.dart'; + +class MigrationManager { + final List _migrations = getMigrations(); + + /// Get highest migration version + int get latestVersion => getLatestVersion(); + + Future migrate(Database db, int oldVersion, int newVersion) async { + if (kDebugMode) { + print( + '[MigrationManager] Migrating database from $oldVersion to $newVersion'); + } + + for (var migration in _migrations) { + if (migration.version > oldVersion && migration.version <= newVersion) { + if (kDebugMode) { + print( + '[MigrationManager] Running migration ${migration.version}: ${migration.description}'); + } + await migration.up(db); + } + } + } +} diff --git a/lib/database/migrations/0001_initial_schema.dart b/lib/database/migrations/0001_initial_schema.dart new file mode 100644 index 00000000..c8fa6f73 --- /dev/null +++ b/lib/database/migrations/0001_initial_schema.dart @@ -0,0 +1,123 @@ +import 'package:sqflite/sqflite.dart'; +import '../migration_base.dart'; + +// Models +import '/model/bank_account.dart'; +import '/model/budget.dart'; +import '/model/category_transaction.dart'; +import '/model/currency.dart'; +import '/model/recurring_transaction.dart'; +import '/model/transaction.dart'; + +class InitialSchema extends Migration { + InitialSchema() + : super(version: 1, description: 'Initial database schema creation'); + + @override + Future up(Database db) async { + const integerPrimaryKeyAutoincrement = 'INTEGER PRIMARY KEY AUTOINCREMENT'; + const integerNotNull = 'INTEGER NOT NULL'; + const integer = 'INTEGER'; + const realNotNull = 'REAL NOT NULL'; + const textNotNull = 'TEXT NOT NULL'; + const text = 'TEXT'; + + // Bank accounts Table + await db.execute(''' + CREATE TABLE `$bankAccountTable`( + `${BankAccountFields.id}` $integerPrimaryKeyAutoincrement, + `${BankAccountFields.name}` $textNotNull, + `${BankAccountFields.symbol}` $textNotNull, + `${BankAccountFields.color}` $integerNotNull, + `${BankAccountFields.startingValue}` $realNotNull, + `${BankAccountFields.active}` $integerNotNull CHECK (${BankAccountFields.active} IN (0, 1)), + `${BankAccountFields.mainAccount}` $integerNotNull CHECK (${BankAccountFields.mainAccount} IN (0, 1)), + `${BankAccountFields.createdAt}` $textNotNull, + `${BankAccountFields.updatedAt}` $textNotNull + ) + '''); + + // Transactions Table + await db.execute(''' + CREATE TABLE `$transactionTable`( + `${TransactionFields.id}` $integerPrimaryKeyAutoincrement, + `${TransactionFields.date}` $text, + `${TransactionFields.amount}` $realNotNull, + `${TransactionFields.type}` $integerNotNull, + `${TransactionFields.note}` $text, + `${TransactionFields.idCategory}` $integer, + `${TransactionFields.idBankAccount}` $integerNotNull, + `${TransactionFields.idBankAccountTransfer}` $integer, + `${TransactionFields.recurring}` $integerNotNull CHECK (${TransactionFields.recurring} IN (0, 1)), + `${TransactionFields.idRecurringTransaction}` $integer, + `${TransactionFields.createdAt}` $textNotNull, + `${TransactionFields.updatedAt}` $textNotNull + ) + '''); + + // Recurring Transactions Amount Table + await db.execute(''' + CREATE TABLE `$recurringTransactionTable`( + `${RecurringTransactionFields.id}` $integerPrimaryKeyAutoincrement, + `${RecurringTransactionFields.fromDate}` $textNotNull, + `${RecurringTransactionFields.toDate}` $text, + `${RecurringTransactionFields.amount}` $realNotNull, + `${RecurringTransactionFields.note}` $textNotNull, + `${RecurringTransactionFields.recurrency}` $textNotNull, + `${RecurringTransactionFields.idCategory}` $integerNotNull, + `${RecurringTransactionFields.idBankAccount}` $integerNotNull, + `${RecurringTransactionFields.lastInsertion}` $text, + `${RecurringTransactionFields.createdAt}` $textNotNull, + `${RecurringTransactionFields.updatedAt}` $textNotNull + ) + '''); + + // Category Transaction Table + await db.execute(''' + CREATE TABLE `$categoryTransactionTable`( + `${CategoryTransactionFields.id}` $integerPrimaryKeyAutoincrement, + `${CategoryTransactionFields.name}` $textNotNull, + `${CategoryTransactionFields.type}` $textNotNull, + `${CategoryTransactionFields.symbol}` $textNotNull, + `${CategoryTransactionFields.color}` $integerNotNull, + `${CategoryTransactionFields.note}` $text, + `${CategoryTransactionFields.parent}` $integer, + `${CategoryTransactionFields.markedAsDeleted}` $integerNotNull CHECK (${CategoryTransactionFields.markedAsDeleted} IN (0, 1)), + `${CategoryTransactionFields.createdAt}` $textNotNull, + `${CategoryTransactionFields.updatedAt}` $textNotNull + ) + '''); + + // Budget Table + await db.execute(''' + CREATE TABLE `$budgetTable`( + `${BudgetFields.id}` $integerPrimaryKeyAutoincrement, + `${BudgetFields.idCategory}` $integerNotNull, + `${BudgetFields.name}` $textNotNull, + `${BudgetFields.amountLimit}` $realNotNull, + `${BudgetFields.active}` $integerNotNull CHECK (${BudgetFields.active} IN (0, 1)), + `${BudgetFields.createdAt}` $textNotNull, + `${BudgetFields.updatedAt}` $textNotNull + ) + '''); + + // Currencies Table + await db.execute(''' + CREATE TABLE `$currencyTable`( + `${CurrencyFields.id}` $integerPrimaryKeyAutoincrement, + `${CurrencyFields.symbol}` $textNotNull, + `${CurrencyFields.code}` $textNotNull, + `${CurrencyFields.name}` $textNotNull, + `${CurrencyFields.mainCurrency}` $integerNotNull CHECK (${CurrencyFields.mainCurrency} IN (0, 1)) + ) + '''); + + await db.execute(''' + INSERT INTO `$currencyTable`(`${CurrencyFields.symbol}`, `${CurrencyFields.code}`, `${CurrencyFields.name}`, `${CurrencyFields.mainCurrency}`) VALUES + ("€", "EUR", "Euro", 1), + ("\$", "USD", "United States Dollar", 0), + ("CHF", "CHF", "Switzerland Franc", 0), + ("£", "GBP", "United Kingdom Pound", 0); + '''); + } +} diff --git a/lib/database/migrations/0002_account_net_worth.dart b/lib/database/migrations/0002_account_net_worth.dart new file mode 100644 index 00000000..4ada0992 --- /dev/null +++ b/lib/database/migrations/0002_account_net_worth.dart @@ -0,0 +1,20 @@ +import 'package:sqflite/sqflite.dart'; +import '../migration_base.dart'; + +// Models +import '/model/bank_account.dart'; + +class AccountNetWorth extends Migration { + AccountNetWorth() + : super(version: 2, description: 'Add account net worth column'); + + @override + Future up(Database db) async { + const integerNotNull = 'INTEGER NOT NULL'; + + // Bank accounts Table + await db.execute(''' + ALTER TABLE `$bankAccountTable` ADD COLUMN `${BankAccountFields.countNetWorth}` $integerNotNull CHECK (${BankAccountFields.countNetWorth} IN (0, 1)) DEFAULT 1; + '''); + } +} diff --git a/lib/database/migrations/migration_registry.dart b/lib/database/migrations/migration_registry.dart new file mode 100644 index 00000000..d386f0e7 --- /dev/null +++ b/lib/database/migrations/migration_registry.dart @@ -0,0 +1,44 @@ +/// Manages database migrations for the application. +/// +/// This registry maintains the list of all database migrations in the order they should be executed. +/// When adding new migrations to this list, please follow these guidelines: +/// +/// 1. Add migrations in ascending version order +/// 2. When two migrations share the same version number, their order in this list +/// determines execution order (first in the list = executed first) +/// 3. Use descriptive file names that include the version number (e.g., 0002_add_transaction_indexes.dart) +/// +/// The MigrationManager will execute migrations in the exact order defined here. +library; + +import '0001_initial_schema.dart'; +import '0002_account_net_worth.dart'; +import '../migration_base.dart'; + +/// Returns all available migrations in execution order. +/// +/// IMPORTANT: The order of migrations in this list is critical! +/// Migrations are executed in the order they appear here, not necessarily by their version number. +/// When multiple migrations share the same version number, their position in this list +/// determines which runs first. +List getMigrations() { + return [ + InitialSchema(), + AccountNetWorth(), + // Add future migrations here + ]; +} + +/// Returns the highest migration version number across all migrations. +/// Used to determine the current database schema version. +/// +/// NOTE: This should return the maximum version number found in any migration, +/// not just the version of the last migration in the list. If migrations aren't +/// added in strict version order, make sure this function still returns the highest version. +int getLatestVersion() { + final migrations = getMigrations(); + if (migrations.isEmpty) return 1; + + return migrations.fold( + 1, (max, migration) => migration.version > max ? migration.version : max); +} diff --git a/lib/database/sossoldi_database.dart b/lib/database/sossoldi_database.dart index d2af49f7..94a0620f 100644 --- a/lib/database/sossoldi_database.dart +++ b/lib/database/sossoldi_database.dart @@ -12,9 +12,11 @@ import '../model/category_transaction.dart'; import '../model/currency.dart'; import '../model/recurring_transaction.dart'; import '../model/transaction.dart'; +import 'migration_manager.dart'; class SossoldiDatabase { static final SossoldiDatabase instance = SossoldiDatabase._init(); + final MigrationManager _migrationManager = MigrationManager(); static Database? _database; static String dbName = 'sossoldi.db'; @@ -35,121 +37,21 @@ class SossoldiDatabase { Future _initDB(String filePath) async { final databasePath = await getDatabasesPath(); final path = join(databasePath, filePath); - return await openDatabase(path, version: 1, onCreate: _createDB); + return await openDatabase(path, + version: _migrationManager.latestVersion, + onCreate: _createDB, + onUpgrade: _upgradeDB); } static Future _createDB(Database database, int version) async { - const integerPrimaryKeyAutoincrement = 'INTEGER PRIMARY KEY AUTOINCREMENT'; - const integerNotNull = 'INTEGER NOT NULL'; - const integer = 'INTEGER'; - const realNotNull = 'REAL NOT NULL'; - const textNotNull = 'TEXT NOT NULL'; - const text = 'TEXT'; - - // Bank accounts Table - await database.execute(''' - CREATE TABLE `$bankAccountTable`( - `${BankAccountFields.id}` $integerPrimaryKeyAutoincrement, - `${BankAccountFields.name}` $textNotNull, - `${BankAccountFields.symbol}` $textNotNull, - `${BankAccountFields.color}` $integerNotNull, - `${BankAccountFields.startingValue}` $realNotNull, - `${BankAccountFields.active}` $integerNotNull CHECK (${BankAccountFields.active} IN (0, 1)), - `${BankAccountFields.mainAccount}` $integerNotNull CHECK (${BankAccountFields.mainAccount} IN (0, 1)), - `${BankAccountFields.createdAt}` $textNotNull, - `${BankAccountFields.updatedAt}` $textNotNull - ) - '''); - - // Transactions Table - await database.execute(''' - CREATE TABLE `$transactionTable`( - `${TransactionFields.id}` $integerPrimaryKeyAutoincrement, - `${TransactionFields.date}` $text, - `${TransactionFields.amount}` $realNotNull, - `${TransactionFields.type}` $integerNotNull, - `${TransactionFields.note}` $text, - `${TransactionFields.idCategory}` $integer, - `${TransactionFields.idBankAccount}` $integerNotNull, - `${TransactionFields.idBankAccountTransfer}` $integer, - `${TransactionFields.recurring}` $integerNotNull CHECK (${TransactionFields.recurring} IN (0, 1)), - `${TransactionFields.idRecurringTransaction}` $integer, - `${TransactionFields.createdAt}` $textNotNull, - `${TransactionFields.updatedAt}` $textNotNull - ) - '''); - - // Recurring Transactions Amount Table - await database.execute(''' - CREATE TABLE `$recurringTransactionTable`( - `${RecurringTransactionFields.id}` $integerPrimaryKeyAutoincrement, - `${RecurringTransactionFields.fromDate}` $textNotNull, - `${RecurringTransactionFields.toDate}` $text, - `${RecurringTransactionFields.amount}` $realNotNull, - `${RecurringTransactionFields.note}` $textNotNull, - `${RecurringTransactionFields.recurrency}` $textNotNull, - `${RecurringTransactionFields.idCategory}` $integerNotNull, - `${RecurringTransactionFields.idBankAccount}` $integerNotNull, - `${RecurringTransactionFields.lastInsertion}` $text, - `${RecurringTransactionFields.createdAt}` $textNotNull, - `${RecurringTransactionFields.updatedAt}` $textNotNull - ) - '''); - - // Category Transaction Table - await database.execute(''' - CREATE TABLE `$categoryTransactionTable`( - `${CategoryTransactionFields.id}` $integerPrimaryKeyAutoincrement, - `${CategoryTransactionFields.name}` $textNotNull, - `${CategoryTransactionFields.type}` $textNotNull, - `${CategoryTransactionFields.symbol}` $textNotNull, - `${CategoryTransactionFields.color}` $integerNotNull, - `${CategoryTransactionFields.note}` $text, - `${CategoryTransactionFields.parent}` $integer, - `${CategoryTransactionFields.markedAsDeleted}` $integerNotNull CHECK (${CategoryTransactionFields.markedAsDeleted} IN (0, 1)), - `${CategoryTransactionFields.createdAt}` $textNotNull, - `${CategoryTransactionFields.updatedAt}` $textNotNull - ) - '''); - - // Default "Uncategorized" Category - await database.execute(''' - INSERT INTO `$categoryTransactionTable`(`${CategoryTransactionFields.id}`, `${CategoryTransactionFields.name}`, `${CategoryTransactionFields.type}`, `${CategoryTransactionFields.symbol}`, `${CategoryTransactionFields.color}`, `${CategoryTransactionFields.note}`, `${CategoryTransactionFields.parent}`, `${CategoryTransactionFields.markedAsDeleted}`, `${CategoryTransactionFields.createdAt}`, `${CategoryTransactionFields.updatedAt}`) VALUES - (0, "Uncategorized", "IN", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'), - (1, "Uncategorized", "OUT", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'); - '''); - - // Budget Table - await database.execute(''' - CREATE TABLE `$budgetTable`( - `${BudgetFields.id}` $integerPrimaryKeyAutoincrement, - `${BudgetFields.idCategory}` $integerNotNull, - `${BudgetFields.name}` $textNotNull, - `${BudgetFields.amountLimit}` $realNotNull, - `${BudgetFields.active}` $integerNotNull CHECK (${BudgetFields.active} IN (0, 1)), - `${BudgetFields.createdAt}` $textNotNull, - `${BudgetFields.updatedAt}` $textNotNull - ) - '''); + // Use the migration manager to apply all migrations from version 0 + // This will run the InitialSchema migration (version 1) first + await instance._migrationManager.migrate(database, 0, version); + } - // Currencies Table - await database.execute(''' - CREATE TABLE `$currencyTable`( - `${CurrencyFields.id}` $integerPrimaryKeyAutoincrement, - `${CurrencyFields.symbol}` $textNotNull, - `${CurrencyFields.code}` $textNotNull, - `${CurrencyFields.name}` $textNotNull, - `${CurrencyFields.mainCurrency}` $integerNotNull CHECK (${CurrencyFields.mainCurrency} IN (0, 1)) - ) - '''); - - await database.execute(''' - INSERT INTO `$currencyTable`(`${CurrencyFields.symbol}`, `${CurrencyFields.code}`, `${CurrencyFields.name}`, `${CurrencyFields.mainCurrency}`) VALUES - ("€", "EUR", "Euro", 1), - ("\$", "USD", "United States Dollar", 0), - ("CHF", "CHF", "Switzerland Franc", 0), - ("£", "GBP", "United Kingdom Pound", 0); - '''); + static Future _upgradeDB( + Database database, int oldVersion, int newVersion) async { + await instance._migrationManager.migrate(database, oldVersion, newVersion); } Future exportToCSV() async { @@ -450,7 +352,7 @@ class SossoldiDatabase { } catch (error) { throw Exception('DbBase.resetDatabase: $error'); } - await _createDB(_database!, 1); + await _createDB(_database!, _migrationManager.latestVersion); } Future clearDatabase() async { diff --git a/lib/model/bank_account.dart b/lib/model/bank_account.dart index 2148a3e3..8461e6d4 100644 --- a/lib/model/bank_account.dart +++ b/lib/model/bank_account.dart @@ -14,6 +14,7 @@ class BankAccountFields extends BaseEntityFields { static String color = 'color'; static String startingValue = 'startingValue'; static String active = 'active'; + static String countNetWorth = 'countNetWorth'; static String mainAccount = 'mainAccount'; static String total = 'total'; static String createdAt = BaseEntityFields.getCreatedAt; @@ -26,6 +27,7 @@ class BankAccountFields extends BaseEntityFields { color, startingValue, active, + countNetWorth, mainAccount, BaseEntityFields.createdAt, BaseEntityFields.updatedAt @@ -38,6 +40,7 @@ class BankAccount extends BaseEntity { final int color; final num startingValue; final bool active; + final bool countNetWorth; final bool mainAccount; final num? total; @@ -48,46 +51,53 @@ class BankAccount extends BaseEntity { required this.color, required this.startingValue, required this.active, + required this.countNetWorth, required this.mainAccount, this.total, super.createdAt, super.updatedAt, }); - BankAccount copy( - {int? id, - String? name, - String? symbol, - int? color, - num? startingValue, - bool? active, - bool? mainAccount, - DateTime? createdAt, - DateTime? updatedAt,}) => + BankAccount copy({ + int? id, + String? name, + String? symbol, + int? color, + num? startingValue, + bool? active, + bool? countNetWorth, + bool? mainAccount, + DateTime? createdAt, + DateTime? updatedAt, + }) => BankAccount( - id: id ?? this.id, - name: name ?? this.name, - symbol: symbol ?? this.symbol, - color: color ?? this.color, - startingValue: startingValue ?? this.startingValue, - active: active ?? this.active, - mainAccount: mainAccount ?? this.mainAccount, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - total: total - ); + id: id ?? this.id, + name: name ?? this.name, + symbol: symbol ?? this.symbol, + color: color ?? this.color, + startingValue: startingValue ?? this.startingValue, + active: active ?? this.active, + countNetWorth: countNetWorth ?? this.countNetWorth, + mainAccount: mainAccount ?? this.mainAccount, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + total: total, + ); static BankAccount fromJson(Map json) => BankAccount( - id: json[BaseEntityFields.id] as int, - name: json[BankAccountFields.name] as String, - symbol: json[BankAccountFields.symbol] as String, - color: json[BankAccountFields.color] as int, - startingValue: json[BankAccountFields.startingValue] as num, - active: json[BankAccountFields.active] == 1 ? true : false, - mainAccount: json[BankAccountFields.mainAccount] == 1 ? true : false, - total: json[BankAccountFields.total] as num?, - createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), - updatedAt: DateTime.parse(json[BaseEntityFields.updatedAt] as String)); + id: json[BaseEntityFields.id] as int, + name: json[BankAccountFields.name] as String, + symbol: json[BankAccountFields.symbol] as String, + color: json[BankAccountFields.color] as int, + startingValue: json[BankAccountFields.startingValue] as num, + active: json[BankAccountFields.active] == 1 ? true : false, + countNetWorth: + json[BankAccountFields.countNetWorth] == 1 ? true : false, + mainAccount: json[BankAccountFields.mainAccount] == 1 ? true : false, + total: json[BankAccountFields.total] as num?, + createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), + updatedAt: DateTime.parse(json[BaseEntityFields.updatedAt] as String), + ); Map toJson({bool update = false}) => { BaseEntityFields.id: id, @@ -96,9 +106,11 @@ class BankAccount extends BaseEntity { BankAccountFields.color: color, BankAccountFields.startingValue: startingValue, BankAccountFields.active: active ? 1 : 0, + BankAccountFields.countNetWorth: countNetWorth ? 1 : 0, BankAccountFields.mainAccount: mainAccount ? 1 : 0, - BaseEntityFields.createdAt: - update ? createdAt?.toIso8601String() : DateTime.now().toIso8601String(), + BaseEntityFields.createdAt: update + ? createdAt?.toIso8601String() + : DateTime.now().toIso8601String(), BaseEntityFields.updatedAt: DateTime.now().toIso8601String(), }; } @@ -152,7 +164,8 @@ class BankAccountMethods extends SossoldiDatabase { final orderByASC = '${BankAccountFields.createdAt} ASC'; final where = '${BankAccountFields.active} = 1 '; - final recurringFilter = '(t.${TransactionFields.recurring} = 0 OR t.${TransactionFields.recurring} IS NULL)'; + final recurringFilter = + '(t.${TransactionFields.recurring} = 0 OR t.${TransactionFields.recurring} IS NULL)'; final result = await db.rawQuery(''' SELECT b.*, (b.${BankAccountFields.startingValue} + @@ -207,7 +220,11 @@ class BankAccountMethods extends SossoldiDatabase { Future deleteById(int id) async { final db = await database; - return await db.delete(bankAccountTable, where: '${BankAccountFields.id} = ?', whereArgs: [id]); + return await db.delete( + bankAccountTable, + where: '${BankAccountFields.id} = ?', + whereArgs: [id], + ); } Future deactivateById(int id) async { @@ -215,7 +232,7 @@ class BankAccountMethods extends SossoldiDatabase { return await db.update( bankAccountTable, - {'active': 0}, + {BankAccountFields.active: 0, BankAccountFields.mainAccount: 0}, where: '${BankAccountFields.id} = ?', whereArgs: [id], ); @@ -225,8 +242,11 @@ class BankAccountMethods extends SossoldiDatabase { final db = await database; //get account infos first - final result = - await db.query(bankAccountTable, where: '${BankAccountFields.id} = $id', limit: 1); + final result = await db.query( + bankAccountTable, + where: '${BankAccountFields.id} = $id', + limit: 1, + ); final singleObject = result.isNotEmpty ? result[0] : null; if (singleObject != null) { @@ -323,14 +343,16 @@ class BankAccountMethods extends SossoldiDatabase { double runningTotal = statritngValue[0]['Value'] as double; var result = resultQuery.map((e) { - runningTotal += double.parse(e['income'].toString()) - double.parse(e['expense'].toString()); + runningTotal += double.parse(e['income'].toString()) - + double.parse(e['expense'].toString()); return {"day": e["day"], "balance": runningTotal}; }).toList(); if (dateRangeStart != null) { return result - .where((element) => dateRangeStart - .isBefore(DateTime.parse(element["day"].toString()).add(const Duration(days: 1)))) + .where((element) => dateRangeStart.isBefore( + DateTime.parse(element["day"].toString()) + .add(const Duration(days: 1)))) .toList(); } diff --git a/lib/pages/accounts/add_account.dart b/lib/pages/accounts/add_account.dart index ad010e71..9e934abf 100644 --- a/lib/pages/accounts/add_account.dart +++ b/lib/pages/accounts/add_account.dart @@ -1,12 +1,11 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../providers/accounts_provider.dart'; import '../../constants/constants.dart'; import '../../constants/functions.dart'; import '../../constants/style.dart'; import '../../providers/currency_provider.dart'; +import '../../utils/decimal_text_input_formatter.dart'; import 'widgets/confirm_account_deletion_dialog.dart'; class AddAccount extends ConsumerStatefulWidget { @@ -34,7 +33,7 @@ class _AddAccountState extends ConsumerState with Functions { balanceController.text = numToCurrency(selectedAccount.total); accountIcon = selectedAccount.symbol; accountColor = selectedAccount.color; - countNetWorth = selectedAccount.active; + countNetWorth = selectedAccount.countNetWorth; mainAccount = selectedAccount.mainAccount; } super.initState(); @@ -293,14 +292,10 @@ class _AddAccountState extends ConsumerState with Functions { ), keyboardType: TextInputType.numberWithOptions(decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp(r'^\d*\.?\d{0,2}'), - ), + inputFormatters: [ + DecimalTextInputFormatter(decimalDigits: 2), ], - style: Theme.of(context) - .textTheme - .titleLarge, + style: Theme.of(context).textTheme.titleLarge, ), ], ), @@ -327,7 +322,7 @@ class _AddAccountState extends ConsumerState with Functions { "Set as main account", style: Theme.of(context).textTheme.bodyLarge, ), - CupertinoSwitch( + Switch.adaptive( value: mainAccount, onChanged: (value) => setState(() => mainAccount = value), @@ -345,7 +340,7 @@ class _AddAccountState extends ConsumerState with Functions { "Counts for the net worth", style: Theme.of(context).textTheme.bodyLarge, ), - CupertinoSwitch( + Switch.adaptive( value: countNetWorth, onChanged: (value) => setState(() => countNetWorth = value), @@ -369,7 +364,7 @@ class _AddAccountState extends ConsumerState with Functions { account: selectedAccount, onPressed: () => ref .read(accountsProvider.notifier) - .removeAccount(selectedAccount.id!) + .removeAccount(selectedAccount) .whenComplete( () { if (context.mounted) { @@ -432,24 +427,16 @@ class _AddAccountState extends ConsumerState with Functions { name: nameController.text, icon: accountIcon, color: accountColor, - active: countNetWorth, + balance: currencyToNum(balanceController.text), + countNetWorth: countNetWorth, mainAccount: mainAccount, ); - if (currencyToNum(balanceController.text) != - selectedAccount.total) { - await ref - .read(accountsProvider.notifier) - .reconcileAccount( - newBalance: currencyToNum(balanceController.text), - account: selectedAccount, - ); - } } else { await ref.read(accountsProvider.notifier).addAccount( name: nameController.text, icon: accountIcon, color: accountColor, - active: countNetWorth, + countNetWorth: countNetWorth, mainAccount: mainAccount, startingValue: currencyToNum(balanceController.text), ); diff --git a/lib/providers/accounts_provider.dart b/lib/providers/accounts_provider.dart index 0c506095..ef9958ec 100644 --- a/lib/providers/accounts_provider.dart +++ b/lib/providers/accounts_provider.dart @@ -3,12 +3,15 @@ import 'package:fl_chart/fl_chart.dart'; import '../model/bank_account.dart'; import '../model/transaction.dart'; +import 'dashboard_provider.dart'; import 'transactions_provider.dart'; final mainAccountProvider = StateProvider((ref) => null); -final selectedAccountProvider = StateProvider.autoDispose((ref) => null); -final selectedAccountCurrentMonthDailyBalanceProvider = StateProvider>((ref) => const []); +final selectedAccountProvider = + StateProvider.autoDispose((ref) => null); +final selectedAccountCurrentMonthDailyBalanceProvider = + StateProvider>((ref) => const []); final selectedAccountLastTransactions = StateProvider((ref) => const []); final filterAccountProvider = StateProvider>((ref) => {}); @@ -40,6 +43,7 @@ class AsyncAccountsNotifier extends AsyncNotifier> { required String icon, required int color, bool active = true, + bool countNetWorth = true, bool mainAccount = false, num startingValue = 0, }) async { @@ -49,6 +53,7 @@ class AsyncAccountsNotifier extends AsyncNotifier> { color: color, startingValue: startingValue, active: active, + countNetWorth: countNetWorth, mainAccount: mainAccount, ); @@ -60,72 +65,101 @@ class AsyncAccountsNotifier extends AsyncNotifier> { } Future updateAccount({ - required String name, - required String icon, - required int color, + String? name, + String? icon, + int? color, + num? balance, + bool? mainAccount, + bool? countNetWorth, bool active = true, - bool mainAccount = false, }) async { BankAccount account = ref.read(selectedAccountProvider)!.copy( name: name, symbol: icon, color: color, active: active, + countNetWorth: countNetWorth, mainAccount: mainAccount, ); state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { + if (balance != null && balance != account.total) { + await _reconcileAccount(account: account, newBalance: balance); + } await BankAccountMethods().updateItem(account); + if (account.mainAccount) { ref.read(mainAccountProvider.notifier).state = account; } + ref.invalidate(dashboardProvider); + return _getAccounts(); }); } - Future reconcileAccount( - {required num newBalance, required BankAccount account}) async { - final num difference = newBalance - (account.total ?? 0); - if (difference != 0) { - final transactionsNotifier = ref.read(transactionsProvider.notifier); - await transactionsNotifier.addTransaction( - difference.abs(), - 'Reconciliation', - account: account, - type: difference > 0 ? TransactionType.income : TransactionType.expense, - date: DateTime.now(), - ); - } + Future reconcileAccount({ + required BankAccount account, + required num newBalance, + }) async { + _reconcileAccount(account: account, newBalance: newBalance); state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { - await BankAccountMethods().updateItem(account); - if (account.mainAccount) { - ref.read(mainAccountProvider.notifier).state = account; - } return _getAccounts(); }); } + Future _reconcileAccount({ + required BankAccount account, + required num newBalance, + }) async { + final num difference = newBalance - (account.total ?? 0); + if (difference != 0) { + final transactionsNotifier = ref.read(transactionsProvider.notifier); + await transactionsNotifier.addTransaction( + difference.abs(), + 'Reconciliation', + account: account, + type: difference > 0 ? TransactionType.income : TransactionType.expense, + date: DateTime.now(), + ); + } + } + Future refreshAccount(BankAccount account) async { ref.read(selectedAccountProvider.notifier).state = account; - final currentMonthDailyBalance = await BankAccountMethods().accountDailyBalance(account.id!, - dateRangeStart: DateTime(DateTime.now().year, DateTime.now().month, 1), // beginnig of current month - dateRangeEnd: DateTime(DateTime.now().year, DateTime.now().month + 1, 1) // beginnig of next month - ); + final currentMonthDailyBalance = + await BankAccountMethods().accountDailyBalance( + account.id!, + dateRangeStart: DateTime( + DateTime.now().year, + DateTime.now().month, + 1, + ), // beginnig of current month + dateRangeEnd: DateTime( + DateTime.now().year, + DateTime.now().month + 1, + 1, + ), // beginnig of next month + ); - ref.read(selectedAccountCurrentMonthDailyBalanceProvider.notifier).state = currentMonthDailyBalance.map((e) { - return FlSpot(double.parse(e['day'].substring(8)) - 1, double.parse(e['balance'].toStringAsFixed(2))); + ref.read(selectedAccountCurrentMonthDailyBalanceProvider.notifier).state = + currentMonthDailyBalance.map((e) { + return FlSpot( + double.parse(e['day'].substring(8)) - 1, + double.parse(e['balance'].toStringAsFixed(2)), + ); }).toList(); ref.read(selectedAccountLastTransactions.notifier).state = await BankAccountMethods().getTransactions(account.id!, 50); } - Future removeAccount(int accountId) async { + Future removeAccount(BankAccount account) async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { - await BankAccountMethods().deactivateById(accountId); + await BankAccountMethods().deactivateById(account.id!); + if (account.mainAccount) ref.invalidate(mainAccountProvider); return _getAccounts(); }); } @@ -136,6 +170,7 @@ class AsyncAccountsNotifier extends AsyncNotifier> { } } -final accountsProvider = AsyncNotifierProvider>(() { +final accountsProvider = + AsyncNotifierProvider>(() { return AsyncAccountsNotifier(); }); From a67df8d49d4dae097c92258ec98b77e459794825 Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 30 Mar 2025 19:44:33 +0200 Subject: [PATCH 08/28] resolve dart format conflict --- lib/database/migration_base.dart | 1 + lib/database/migration_manager.dart | 8 +++----- lib/database/migrations/0001_initial_schema.dart | 7 ++++--- lib/database/migrations/0002_account_net_worth.dart | 6 ++++-- lib/database/migrations/migration_registry.dart | 5 +++-- lib/database/sossoldi_database.dart | 6 +++--- lib/model/bank_account.dart | 3 +-- lib/providers/accounts_provider.dart | 2 +- 8 files changed, 20 insertions(+), 18 deletions(-) diff --git a/lib/database/migration_base.dart b/lib/database/migration_base.dart index 54275ddd..4cb4d165 100644 --- a/lib/database/migration_base.dart +++ b/lib/database/migration_base.dart @@ -44,4 +44,5 @@ abstract class Migration { /// Applies this migration to upgrade the database schema. Future up(Database db); + } diff --git a/lib/database/migration_manager.dart b/lib/database/migration_manager.dart index 79b4d286..af92c59c 100644 --- a/lib/database/migration_manager.dart +++ b/lib/database/migration_manager.dart @@ -11,18 +11,16 @@ class MigrationManager { Future migrate(Database db, int oldVersion, int newVersion) async { if (kDebugMode) { - print( - '[MigrationManager] Migrating database from $oldVersion to $newVersion'); + print('[MigrationManager] Migrating database from $oldVersion to $newVersion'); } for (var migration in _migrations) { if (migration.version > oldVersion && migration.version <= newVersion) { if (kDebugMode) { - print( - '[MigrationManager] Running migration ${migration.version}: ${migration.description}'); + print('[MigrationManager] Running migration ${migration.version}: ${migration.description}'); } await migration.up(db); } } } -} +} \ No newline at end of file diff --git a/lib/database/migrations/0001_initial_schema.dart b/lib/database/migrations/0001_initial_schema.dart index c8fa6f73..0cd51e45 100644 --- a/lib/database/migrations/0001_initial_schema.dart +++ b/lib/database/migrations/0001_initial_schema.dart @@ -10,8 +10,10 @@ import '/model/recurring_transaction.dart'; import '/model/transaction.dart'; class InitialSchema extends Migration { - InitialSchema() - : super(version: 1, description: 'Initial database schema creation'); + InitialSchema() : super( + version: 1, + description: 'Initial database schema creation' + ); @override Future up(Database db) async { @@ -82,7 +84,6 @@ class InitialSchema extends Migration { `${CategoryTransactionFields.color}` $integerNotNull, `${CategoryTransactionFields.note}` $text, `${CategoryTransactionFields.parent}` $integer, - `${CategoryTransactionFields.markedAsDeleted}` $integerNotNull CHECK (${CategoryTransactionFields.markedAsDeleted} IN (0, 1)), `${CategoryTransactionFields.createdAt}` $textNotNull, `${CategoryTransactionFields.updatedAt}` $textNotNull ) diff --git a/lib/database/migrations/0002_account_net_worth.dart b/lib/database/migrations/0002_account_net_worth.dart index 4ada0992..65f51edc 100644 --- a/lib/database/migrations/0002_account_net_worth.dart +++ b/lib/database/migrations/0002_account_net_worth.dart @@ -5,8 +5,10 @@ import '../migration_base.dart'; import '/model/bank_account.dart'; class AccountNetWorth extends Migration { - AccountNetWorth() - : super(version: 2, description: 'Add account net worth column'); + AccountNetWorth() : super( + version: 2, + description: 'Add account net worth column' + ); @override Future up(Database db) async { diff --git a/lib/database/migrations/migration_registry.dart b/lib/database/migrations/migration_registry.dart index d386f0e7..75d39f40 100644 --- a/lib/database/migrations/migration_registry.dart +++ b/lib/database/migrations/migration_registry.dart @@ -11,6 +11,7 @@ /// The MigrationManager will execute migrations in the exact order defined here. library; + import '0001_initial_schema.dart'; import '0002_account_net_worth.dart'; import '../migration_base.dart'; @@ -39,6 +40,6 @@ int getLatestVersion() { final migrations = getMigrations(); if (migrations.isEmpty) return 1; - return migrations.fold( - 1, (max, migration) => migration.version > max ? migration.version : max); + return migrations.fold(1, (max, migration) => + migration.version > max ? migration.version : max); } diff --git a/lib/database/sossoldi_database.dart b/lib/database/sossoldi_database.dart index 94a0620f..f5a82969 100644 --- a/lib/database/sossoldi_database.dart +++ b/lib/database/sossoldi_database.dart @@ -37,7 +37,8 @@ class SossoldiDatabase { Future _initDB(String filePath) async { final databasePath = await getDatabasesPath(); final path = join(databasePath, filePath); - return await openDatabase(path, + return await openDatabase( + path, version: _migrationManager.latestVersion, onCreate: _createDB, onUpgrade: _upgradeDB); @@ -49,8 +50,7 @@ class SossoldiDatabase { await instance._migrationManager.migrate(database, 0, version); } - static Future _upgradeDB( - Database database, int oldVersion, int newVersion) async { + static Future _upgradeDB(Database database, int oldVersion, int newVersion) async { await instance._migrationManager.migrate(database, oldVersion, newVersion); } diff --git a/lib/model/bank_account.dart b/lib/model/bank_account.dart index 8461e6d4..07573d87 100644 --- a/lib/model/bank_account.dart +++ b/lib/model/bank_account.dart @@ -91,8 +91,7 @@ class BankAccount extends BaseEntity { color: json[BankAccountFields.color] as int, startingValue: json[BankAccountFields.startingValue] as num, active: json[BankAccountFields.active] == 1 ? true : false, - countNetWorth: - json[BankAccountFields.countNetWorth] == 1 ? true : false, + countNetWorth: json[BankAccountFields.countNetWorth] == 1 ? true : false, mainAccount: json[BankAccountFields.mainAccount] == 1 ? true : false, total: json[BankAccountFields.total] as num?, createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), diff --git a/lib/providers/accounts_provider.dart b/lib/providers/accounts_provider.dart index ef9958ec..4e74c4fd 100644 --- a/lib/providers/accounts_provider.dart +++ b/lib/providers/accounts_provider.dart @@ -92,7 +92,7 @@ class AsyncAccountsNotifier extends AsyncNotifier> { ref.read(mainAccountProvider.notifier).state = account; } ref.invalidate(dashboardProvider); - + return _getAccounts(); }); } From 6584732691e1d3c5250ed30662e47540f7cbcd33 Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 30 Mar 2025 19:46:41 +0200 Subject: [PATCH 09/28] test conflict --- lib/database/migrations/0001_initial_schema.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/database/migrations/0001_initial_schema.dart b/lib/database/migrations/0001_initial_schema.dart index 0cd51e45..7086bf2c 100644 --- a/lib/database/migrations/0001_initial_schema.dart +++ b/lib/database/migrations/0001_initial_schema.dart @@ -12,7 +12,7 @@ import '/model/transaction.dart'; class InitialSchema extends Migration { InitialSchema() : super( version: 1, - description: 'Initial database schema creation' + description: 'Initial database schema creation' ); @override From cb22393b7485d3b4dff6c4358d4417c35054ee72 Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 30 Mar 2025 19:58:37 +0200 Subject: [PATCH 10/28] conflicts --- lib/database/migration_base.dart | 4 ++-- lib/database/migrations/0001_initial_schema.dart | 6 +++--- lib/database/migrations/0002_account_net_worth.dart | 6 +++--- lib/database/migrations/migration_registry.dart | 3 ++- lib/providers/accounts_provider.dart | 4 ++-- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/database/migration_base.dart b/lib/database/migration_base.dart index 4cb4d165..145c70f1 100644 --- a/lib/database/migration_base.dart +++ b/lib/database/migration_base.dart @@ -44,5 +44,5 @@ abstract class Migration { /// Applies this migration to upgrade the database schema. Future up(Database db); - -} + +} \ No newline at end of file diff --git a/lib/database/migrations/0001_initial_schema.dart b/lib/database/migrations/0001_initial_schema.dart index 7086bf2c..ca55a824 100644 --- a/lib/database/migrations/0001_initial_schema.dart +++ b/lib/database/migrations/0001_initial_schema.dart @@ -11,8 +11,8 @@ import '/model/transaction.dart'; class InitialSchema extends Migration { InitialSchema() : super( - version: 1, - description: 'Initial database schema creation' + version: 1, + description: 'Initial database schema creation' ); @override @@ -121,4 +121,4 @@ class InitialSchema extends Migration { ("£", "GBP", "United Kingdom Pound", 0); '''); } -} +} \ No newline at end of file diff --git a/lib/database/migrations/0002_account_net_worth.dart b/lib/database/migrations/0002_account_net_worth.dart index 65f51edc..38831b92 100644 --- a/lib/database/migrations/0002_account_net_worth.dart +++ b/lib/database/migrations/0002_account_net_worth.dart @@ -6,8 +6,8 @@ import '/model/bank_account.dart'; class AccountNetWorth extends Migration { AccountNetWorth() : super( - version: 2, - description: 'Add account net worth column' + version: 2, + description: 'Add account net worth column' ); @override @@ -19,4 +19,4 @@ class AccountNetWorth extends Migration { ALTER TABLE `$bankAccountTable` ADD COLUMN `${BankAccountFields.countNetWorth}` $integerNotNull CHECK (${BankAccountFields.countNetWorth} IN (0, 1)) DEFAULT 1; '''); } -} +} \ No newline at end of file diff --git a/lib/database/migrations/migration_registry.dart b/lib/database/migrations/migration_registry.dart index 75d39f40..5f5fa8cc 100644 --- a/lib/database/migrations/migration_registry.dart +++ b/lib/database/migrations/migration_registry.dart @@ -16,6 +16,7 @@ import '0001_initial_schema.dart'; import '0002_account_net_worth.dart'; import '../migration_base.dart'; + /// Returns all available migrations in execution order. /// /// IMPORTANT: The order of migrations in this list is critical! @@ -42,4 +43,4 @@ int getLatestVersion() { return migrations.fold(1, (max, migration) => migration.version > max ? migration.version : max); -} +} \ No newline at end of file diff --git a/lib/providers/accounts_provider.dart b/lib/providers/accounts_provider.dart index 4e74c4fd..3bc09225 100644 --- a/lib/providers/accounts_provider.dart +++ b/lib/providers/accounts_provider.dart @@ -92,7 +92,7 @@ class AsyncAccountsNotifier extends AsyncNotifier> { ref.read(mainAccountProvider.notifier).state = account; } ref.invalidate(dashboardProvider); - + return _getAccounts(); }); } @@ -173,4 +173,4 @@ class AsyncAccountsNotifier extends AsyncNotifier> { final accountsProvider = AsyncNotifierProvider>(() { return AsyncAccountsNotifier(); -}); +}); \ No newline at end of file From e550eed82665c571507c813a786e996d196e801e Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 30 Mar 2025 20:06:37 +0200 Subject: [PATCH 11/28] empty line conflict test --- lib/database/migration_base.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/database/migration_base.dart b/lib/database/migration_base.dart index 145c70f1..08c4209e 100644 --- a/lib/database/migration_base.dart +++ b/lib/database/migration_base.dart @@ -45,4 +45,4 @@ abstract class Migration { /// Applies this migration to upgrade the database schema. Future up(Database db); -} \ No newline at end of file +} From a6b717c69a4357c163092ad63bbcace7afb7887f Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 30 Mar 2025 20:07:37 +0200 Subject: [PATCH 12/28] empty line conflict test 2 --- lib/database/migrations/0001_initial_schema.dart | 2 +- lib/database/migrations/0002_account_net_worth.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/database/migrations/0001_initial_schema.dart b/lib/database/migrations/0001_initial_schema.dart index ca55a824..74e71b97 100644 --- a/lib/database/migrations/0001_initial_schema.dart +++ b/lib/database/migrations/0001_initial_schema.dart @@ -121,4 +121,4 @@ class InitialSchema extends Migration { ("£", "GBP", "United Kingdom Pound", 0); '''); } -} \ No newline at end of file +} diff --git a/lib/database/migrations/0002_account_net_worth.dart b/lib/database/migrations/0002_account_net_worth.dart index 38831b92..731f90af 100644 --- a/lib/database/migrations/0002_account_net_worth.dart +++ b/lib/database/migrations/0002_account_net_worth.dart @@ -19,4 +19,4 @@ class AccountNetWorth extends Migration { ALTER TABLE `$bankAccountTable` ADD COLUMN `${BankAccountFields.countNetWorth}` $integerNotNull CHECK (${BankAccountFields.countNetWorth} IN (0, 1)) DEFAULT 1; '''); } -} \ No newline at end of file +} From 13e1a2d383a9b8452a2008f2a06140a22b349319 Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 30 Mar 2025 20:26:29 +0200 Subject: [PATCH 13/28] fix missing countNetWorth in bank_account_test --- test/model/bank_account_test.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/model/bank_account_test.dart b/test/model/bank_account_test.dart index 5ad5a46a..d61719ae 100644 --- a/test/model/bank_account_test.dart +++ b/test/model/bank_account_test.dart @@ -19,6 +19,7 @@ void main() { color: 0, startingValue: 100, active: true, + countNetWorth: true, mainAccount: true, createdAt: DateTime.utc(2022), updatedAt: DateTime.utc(2022)); @@ -31,6 +32,7 @@ void main() { assert(bCopy.color == b.color); assert(bCopy.startingValue == bCopy.startingValue); assert(bCopy.active == bCopy.active); + assert(bCopy.countNetWorth == bCopy.countNetWorth); assert(bCopy.mainAccount == bCopy.mainAccount); assert(bCopy.createdAt == b.createdAt); assert(bCopy.updatedAt == b.updatedAt); @@ -68,6 +70,7 @@ void main() { color: 0, startingValue: 100, active: true, + countNetWorth: true, mainAccount: false); Map json = b.toJson(); @@ -78,6 +81,7 @@ void main() { assert(b.color == json[BankAccountFields.color]); assert(b.startingValue == json[BankAccountFields.startingValue]); assert((b.active ? 1 : 0) == json[BankAccountFields.active]); + assert((b.countNetWorth ? 1 : 0) == json[BankAccountFields.countNetWorth]); assert((b.mainAccount ? 1 : 0) == json[BankAccountFields.mainAccount]); }); @@ -252,4 +256,4 @@ void main() { -} +} \ No newline at end of file From f1549f28061cf8d2f4c29b2a6c2e4660288ef7af Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 30 Mar 2025 20:29:04 +0200 Subject: [PATCH 14/28] another conflict --- test/model/bank_account_test.dart | 217 +++++++++++++++++++++--------- 1 file changed, 157 insertions(+), 60 deletions(-) diff --git a/test/model/bank_account_test.dart b/test/model/bank_account_test.dart index d61719ae..c6ccab31 100644 --- a/test/model/bank_account_test.dart +++ b/test/model/bank_account_test.dart @@ -64,14 +64,15 @@ void main() { test("Test toJson BankAccount", () { BankAccount b = const BankAccount( - id: 2, - name: "name", - symbol: "symbol", - color: 0, - startingValue: 100, - active: true, - countNetWorth: true, - mainAccount: false); + id: 2, + name: "name", + symbol: "symbol", + color: 0, + startingValue: 100, + active: true, + countNetWorth: true, + mainAccount: false, + ); Map json = b.toJson(); @@ -86,7 +87,6 @@ void main() { }); group("Bank Account Methods", () { - late SossoldiDatabase sossoldiDatabase; late sqflite.Database db; @@ -99,9 +99,7 @@ void main() { await sossoldiDatabase.resetDatabase(); }); - tearDown(() async => { - await sossoldiDatabase.clearDatabase() - }); + tearDown(() async => {await sossoldiDatabase.clearDatabase()}); tearDownAll(() { sossoldiDatabase.close(); @@ -110,49 +108,96 @@ void main() { test("selectAll", () async { await sossoldiDatabase.fillDemoData(countOfGeneratedTransaction: 2000); - try{ + try { await db.transaction((txn) async { var batch = txn.batch(); batch.delete(transactionTable); await batch.commit(); }); - } catch(error){ + } catch (error) { throw Exception('DbBase.cleanDatabase: $error'); } var transactions = await db.rawQuery("SELECT * FROM `transaction`"); expect(0, transactions.length); - const insertDemoTransactionsQuery = '''INSERT INTO `transaction` (date, amount, type, note, idCategory, idBankAccount, idBankAccountTransfer, recurring, idRecurringTransaction, createdAt, updatedAt) VALUES '''; + const insertDemoTransactionsQuery = + '''INSERT INTO `transaction` (date, amount, type, note, idCategory, idBankAccount, idBankAccountTransfer, recurring, idRecurringTransaction, createdAt, updatedAt) VALUES '''; final List demoTransactions = []; final today = DateTime.now(); final fistOfCurrentMonth = DateTime(today.year, today.month, 1); // Add a transaction of last month - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.subtract(const Duration(days: 10)))); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.subtract(const Duration(days: 10)), idBankAccount: 71)); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.subtract(const Duration(days: 10)), idBankAccount: 71, type: 'TRSF', idBankTransfert: 70)); + demoTransactions.add( + createInsertSqlTransaction( + date: fistOfCurrentMonth.subtract(const Duration(days: 10)), + ), + ); + demoTransactions.add( + createInsertSqlTransaction( + date: fistOfCurrentMonth.subtract(const Duration(days: 10)), + idBankAccount: 71, + ), + ); + demoTransactions.add( + createInsertSqlTransaction( + date: fistOfCurrentMonth.subtract(const Duration(days: 10)), + idBankAccount: 71, + type: 'TRSF', + idBankTransfert: 70, + ), + ); // Add transactions of current month // 1 - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth)); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth)); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth, idBankAccount: 71)); + demoTransactions + .add(createInsertSqlTransaction(date: fistOfCurrentMonth)); + demoTransactions + .add(createInsertSqlTransaction(date: fistOfCurrentMonth)); + demoTransactions.add( + createInsertSqlTransaction(date: fistOfCurrentMonth, idBankAccount: 71), + ); // 2 - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 1)))); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 1)), amount: 200, type: 'IN')); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 1)), idBankAccount: 71)); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 1)), amount: 50.5, idBankAccount: 70, type: 'TRSF', idBankTransfert: 71)); + demoTransactions.add( + createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 1)), + ), + ); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 1)), + amount: 200, + type: 'IN', + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 1)), + idBankAccount: 71, + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 1)), + amount: 50.5, + idBankAccount: 70, + type: 'TRSF', + idBankTransfert: 71, + )); // 3 - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 2)), type: 'IN')); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 2)), type: 'IN')); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 2)), idBankAccount: 71)); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 2)), + type: 'IN', + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 2)), + type: 'IN', + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 2)), + idBankAccount: 71, + )); // Add recurring transactions. These must be count as number of time they occout * amount - - await db.execute("$insertDemoTransactionsQuery ${demoTransactions.join(",")};"); + await db.execute( + "$insertDemoTransactionsQuery ${demoTransactions.join(",")};"); transactions = await db.rawQuery("SELECT * FROM `transaction`"); expect(13, transactions.length); @@ -176,55 +221,104 @@ void main() { test("accountDailyBalance", () async { await sossoldiDatabase.fillDemoData(countOfGeneratedTransaction: 2000); - try{ + try { await db.transaction((txn) async { var batch = txn.batch(); batch.delete(transactionTable); await batch.commit(); }); - } catch(error){ + } catch (error) { throw Exception('DbBase.cleanDatabase: $error'); } var transactions = await db.rawQuery("SELECT * FROM `transaction`"); expect(0, transactions.length); - const insertDemoTransactionsQuery = '''INSERT INTO `transaction` (date, amount, type, note, idCategory, idBankAccount, idBankAccountTransfer, recurring, idRecurringTransaction, createdAt, updatedAt) VALUES '''; + const insertDemoTransactionsQuery = + '''INSERT INTO `transaction` (date, amount, type, note, idCategory, idBankAccount, idBankAccountTransfer, recurring, idRecurringTransaction, createdAt, updatedAt) VALUES '''; final List demoTransactions = []; final today = DateTime.now(); final fistOfCurrentMonth = DateTime(today.year, today.month, 1); // Add a transaction of last month - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.subtract(const Duration(days: 10)))); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.subtract(const Duration(days: 10)), idBankAccount: 71)); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.subtract(const Duration(days: 10)), idBankAccount: 71, type: 'TRSF', idBankTransfert: 70)); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.subtract(const Duration(days: 10)), + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.subtract(const Duration(days: 10)), + idBankAccount: 71, + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.subtract(const Duration(days: 10)), + idBankAccount: 71, + type: 'TRSF', + idBankTransfert: 70, + )); // Add transactions of current month - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth)); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth)); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth, idBankAccount: 71)); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 1)))); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 1)), amount: 200, type: 'IN')); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 1)), idBankAccount: 71)); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 1)), amount: 50.5, idBankAccount: 70, type: 'TRSF', idBankTransfert: 71)); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 2)), type: 'IN')); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 2)), type: 'IN')); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 2)), idBankAccount: 71)); + demoTransactions + .add(createInsertSqlTransaction(date: fistOfCurrentMonth)); + demoTransactions + .add(createInsertSqlTransaction(date: fistOfCurrentMonth)); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth, + idBankAccount: 71, + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 1)), + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 1)), + amount: 200, + type: 'IN', + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 1)), + idBankAccount: 71, + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 1)), + amount: 50.5, + idBankAccount: 70, + type: 'TRSF', + idBankTransfert: 71, + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 2)), + type: 'IN', + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 2)), + type: 'IN', + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 2)), + idBankAccount: 71, + )); // Add a transaction of next month - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 32)))); - demoTransactions.add(createInsertSqlTransaction(date: fistOfCurrentMonth.add(const Duration(days: 32)), idBankAccount: 71)); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 32)), + )); + demoTransactions.add(createInsertSqlTransaction( + date: fistOfCurrentMonth.add(const Duration(days: 32)), + idBankAccount: 71, + )); - await db.execute("$insertDemoTransactionsQuery ${demoTransactions.join(",")};"); + await db.execute( + "$insertDemoTransactionsQuery ${demoTransactions.join(",")};"); transactions = await db.rawQuery("SELECT * FROM `transaction`"); expect(15, transactions.length); var result = await BankAccountMethods().accountDailyBalance( 70, - dateRangeStart: DateTime(DateTime.now().year, DateTime.now().month, 1), // beginnig of current month - dateRangeEnd: DateTime(DateTime.now().year, DateTime.now().month + 1, 1)); // beginnig of next month + dateRangeStart: DateTime(DateTime.now().year, DateTime.now().month, + 1), // beginnig of current month + dateRangeEnd: + DateTime(DateTime.now().year, DateTime.now().month + 1, 1), + ); // beginnig of next month expect(result.length, 3); final DateFormat formatter = DateFormat('yyyy-MM-dd'); @@ -232,28 +326,31 @@ void main() { expect(result[0]['day'], formatter.format(fistOfCurrentMonth)); expect(result[0]['balance'] - initialAccountAmount, -200); - expect(result[1]['day'], formatter.format(fistOfCurrentMonth.add(const Duration(days: 1)))); + expect(result[1]['day'], + formatter.format(fistOfCurrentMonth.add(const Duration(days: 1)))); expect(result[1]['balance'] - initialAccountAmount, -150.5); - expect(result[2]['day'], formatter.format(fistOfCurrentMonth.add(const Duration(days: 2)))); + expect(result[2]['day'], + formatter.format(fistOfCurrentMonth.add(const Duration(days: 2)))); expect(result[2]['balance'] - initialAccountAmount, 49.5); result = await BankAccountMethods().accountDailyBalance( 71, - dateRangeStart: DateTime(DateTime.now().year, DateTime.now().month, 1), // beginnig of current month - dateRangeEnd: DateTime(DateTime.now().year, DateTime.now().month + 1, 1)); // beginnig of next month; + dateRangeStart: DateTime(DateTime.now().year, DateTime.now().month, 1), + dateRangeEnd: + DateTime(DateTime.now().year, DateTime.now().month + 1, 1), + ); // beginnig of next month; expect(result.length, 3); initialAccountAmount = 3823.56; // taken from fillDemoData expect(result[0]['day'], formatter.format(fistOfCurrentMonth)); expect(result[0]['balance'] - initialAccountAmount, -300); - expect(result[1]['day'], formatter.format(fistOfCurrentMonth.add(const Duration(days: 1)))); + expect(result[1]['day'], + formatter.format(fistOfCurrentMonth.add(const Duration(days: 1)))); expect(result[1]['balance'] - initialAccountAmount, -349.5); - expect(result[2]['day'], formatter.format(fistOfCurrentMonth.add(const Duration(days: 2)))); + expect(result[2]['day'], + formatter.format(fistOfCurrentMonth.add(const Duration(days: 2)))); expect(result[2]['balance'] - initialAccountAmount, -449.5); }); }); - - - } \ No newline at end of file From 9881b3c3f49050130361dbfe3756910b7f661d69 Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 30 Mar 2025 20:30:15 +0200 Subject: [PATCH 15/28] missing empty line --- test/model/bank_account_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/model/bank_account_test.dart b/test/model/bank_account_test.dart index c6ccab31..ba95d469 100644 --- a/test/model/bank_account_test.dart +++ b/test/model/bank_account_test.dart @@ -353,4 +353,4 @@ void main() { expect(result[2]['balance'] - initialAccountAmount, -449.5); }); }); -} \ No newline at end of file +} From c88a5e195630021572aae3ea0188e642245598ae Mon Sep 17 00:00:00 2001 From: napitek Date: Sun, 30 Mar 2025 20:36:21 +0200 Subject: [PATCH 16/28] adapt inital_schema with markedAsDeleted --- lib/database/migrations/0001_initial_schema.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/database/migrations/0001_initial_schema.dart b/lib/database/migrations/0001_initial_schema.dart index 74e71b97..4f370f3e 100644 --- a/lib/database/migrations/0001_initial_schema.dart +++ b/lib/database/migrations/0001_initial_schema.dart @@ -84,11 +84,19 @@ class InitialSchema extends Migration { `${CategoryTransactionFields.color}` $integerNotNull, `${CategoryTransactionFields.note}` $text, `${CategoryTransactionFields.parent}` $integer, + `${CategoryTransactionFields.markedAsDeleted}` $integerNotNull CHECK (${CategoryTransactionFields.markedAsDeleted} IN (0, 1)), `${CategoryTransactionFields.createdAt}` $textNotNull, `${CategoryTransactionFields.updatedAt}` $textNotNull ) '''); + // Default "Uncategorized" Category + await db.execute(''' + INSERT INTO `$categoryTransactionTable`(`${CategoryTransactionFields.id}`, `${CategoryTransactionFields.name}`, `${CategoryTransactionFields.type}`, `${CategoryTransactionFields.symbol}`, `${CategoryTransactionFields.color}`, `${CategoryTransactionFields.note}`, `${CategoryTransactionFields.parent}`, `${CategoryTransactionFields.markedAsDeleted}`, `${CategoryTransactionFields.createdAt}`, `${CategoryTransactionFields.updatedAt}`) VALUES + (0, "Uncategorized", "IN", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'), + (1, "Uncategorized", "OUT", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'); + '''); + // Budget Table await db.execute(''' CREATE TABLE `$budgetTable`( From 7680807843eca3e2add46c0460e676da3f19b105 Mon Sep 17 00:00:00 2001 From: napitek Date: Mon, 31 Mar 2025 01:25:03 +0200 Subject: [PATCH 17/28] RoundedIcon with "markedAsDeleted" subIcon --- lib/custom_widgets/rounded_icon.dart | 52 ++++++++++++++----- lib/custom_widgets/transactions_list.dart | 5 ++ .../widgets/delete_category_dialog.dart | 6 ++- .../widget/recurring_payment_card.dart | 1 + 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/lib/custom_widgets/rounded_icon.dart b/lib/custom_widgets/rounded_icon.dart index 936c4fae..c515efa9 100644 --- a/lib/custom_widgets/rounded_icon.dart +++ b/lib/custom_widgets/rounded_icon.dart @@ -8,6 +8,8 @@ class RoundedIcon extends StatelessWidget { this.backgroundColor, this.size = 24, this.padding = const EdgeInsets.all(10.0), + this.markedAsDeleted = false, + this.onDelete, super.key, }); @@ -15,22 +17,46 @@ class RoundedIcon extends StatelessWidget { final Color? backgroundColor; final double? size; final EdgeInsets? padding; + final bool markedAsDeleted; + final VoidCallback? onDelete; @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: backgroundColor, - ), - padding: padding, - child: icon != null - ? Icon( - icon, - size: size, - color: white, - ) - : const SizedBox(), + return Stack( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: backgroundColor, + ), + padding: padding, + child: icon != null + ? Icon( + icon, + size: size, + color: white, + ) + : const SizedBox(), + ), + if (markedAsDeleted) + Positioned( + right: 0, + bottom: 0, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: category0, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 1.5), + ), + child: const Icon( + Icons.dangerous, + size: 12, + color: Colors.white, + ), + ), + ), + ], ); } } diff --git a/lib/custom_widgets/transactions_list.dart b/lib/custom_widgets/transactions_list.dart index 1c17996f..a4552fb5 100644 --- a/lib/custom_widgets/transactions_list.dart +++ b/lib/custom_widgets/transactions_list.dart @@ -6,6 +6,7 @@ import '../constants/constants.dart'; import '../constants/functions.dart'; import '../constants/style.dart'; import '../model/transaction.dart'; +import '../providers/categories_provider.dart'; import '../providers/currency_provider.dart'; import '../providers/transactions_provider.dart'; import '../utils/date_helper.dart'; @@ -128,6 +129,9 @@ class TransactionTile extends ConsumerWidget with Functions { @override Widget build(BuildContext context, WidgetRef ref) { final currencyState = ref.watch(currencyStateNotifier); + + final category = ref.watch(categoryByIdProvider(transaction.idCategory!)).value; + return Material( child: ListTile( visualDensity: VisualDensity.compact, @@ -154,6 +158,7 @@ class TransactionTile extends ConsumerWidget with Functions { : Theme.of(context).colorScheme.secondary, size: 25, padding: const EdgeInsets.all(8.0), + markedAsDeleted: category?.markedAsDeleted ?? false, ), title: Text( (transaction.note?.isEmpty ?? true) diff --git a/lib/pages/categories/widgets/delete_category_dialog.dart b/lib/pages/categories/widgets/delete_category_dialog.dart index f073221b..13dbff4f 100644 --- a/lib/pages/categories/widgets/delete_category_dialog.dart +++ b/lib/pages/categories/widgets/delete_category_dialog.dart @@ -39,7 +39,8 @@ Future showDeleteCategoryDialog( .read(categoriesProvider(userCategoriesFilter).notifier) .markAsDeleted(selectedCategory.id) .whenComplete(backToCategoryList); - final _ = ref.refresh(categoriesProvider(userCategoriesFilter)); + ref.invalidate(categoriesProvider(userCategoriesFilter)); + ref.invalidate(categoryByIdProvider); }), TextButton( child: Text( @@ -51,7 +52,8 @@ Future showDeleteCategoryDialog( .read(categoriesProvider(userCategoriesFilter).notifier) .removeCategory(selectedCategory.id!) .whenComplete(backToCategoryList); - final _ = ref.refresh(categoriesProvider(userCategoriesFilter)); + ref.invalidate(categoriesProvider(userCategoriesFilter)); + ref.invalidate(categoryByIdProvider); }), ], ); diff --git a/lib/pages/planning_page/widget/recurring_payment_card.dart b/lib/pages/planning_page/widget/recurring_payment_card.dart index 227cd75f..eed91859 100644 --- a/lib/pages/planning_page/widget/recurring_payment_card.dart +++ b/lib/pages/planning_page/widget/recurring_payment_card.dart @@ -67,6 +67,7 @@ class RecurringPaymentCard extends ConsumerWidget with Functions { backgroundColor: categoryColorList[cat.color], padding: const EdgeInsets.all(8.0), size: 25, + markedAsDeleted: cat.markedAsDeleted, ), const SizedBox(width: 10), Expanded( From 31d777c4a9b3db73eeac045b60027c8a41539389 Mon Sep 17 00:00:00 2001 From: napitek Date: Thu, 3 Apr 2025 07:20:41 +0200 Subject: [PATCH 18/28] delete budgets when category is markedAsDeleted/deleted --- .../widgets/delete_category_dialog.dart | 7 ++++-- lib/providers/budgets_provider.dart | 12 ++++++++- lib/providers/categories_provider.dart | 25 +++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/lib/pages/categories/widgets/delete_category_dialog.dart b/lib/pages/categories/widgets/delete_category_dialog.dart index 13dbff4f..cc59bed9 100644 --- a/lib/pages/categories/widgets/delete_category_dialog.dart +++ b/lib/pages/categories/widgets/delete_category_dialog.dart @@ -20,10 +20,13 @@ Future showDeleteCategoryDialog( child: ListBody( children: [ Text( - 'With "Mark as deleted," transitions with the category will be available, but new ones cannot be created\n', + "Mark as deleted: Category remains available for existing transitions but cannot be used for new ones.\n", ), Text( - 'With "Delete" all transitions with that category will automatically be "Uncategorized"', + "Delete: All transitions using this category will be automatically changed to 'Uncategorized'.\n", + ), + Text( + "Both options will delete budgets with the specified category.", ), ], ), diff --git a/lib/providers/budgets_provider.dart b/lib/providers/budgets_provider.dart index 756d0ce4..e5882ff5 100644 --- a/lib/providers/budgets_provider.dart +++ b/lib/providers/budgets_provider.dart @@ -2,7 +2,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../model/budget.dart'; -final monthlyBudgetsStatsProvider = FutureProvider>((ref) async { +final monthlyBudgetsStatsProvider = + FutureProvider>((ref) async { final budgets = await BudgetMethods().selectMonthlyBudgetsStats(); return budgets; }); @@ -56,3 +57,12 @@ final budgetsProvider = AsyncNotifierProvider>(() { return AsyncBudgetsNotifier(); }); + +final deleteBudgetsByCategoryProvider = + Provider Function(int)>((ref) { + return (int categoryId) async { + await BudgetMethods().deleteByCategory(categoryId); + + await ref.read(budgetsProvider.notifier).refreshBudgets(); + }; +}); diff --git a/lib/providers/categories_provider.dart b/lib/providers/categories_provider.dart index 3eb4d79c..ee80c6f7 100644 --- a/lib/providers/categories_provider.dart +++ b/lib/providers/categories_provider.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../model/category_transaction.dart'; import '../model/recurring_transaction.dart'; import '../model/transaction.dart'; +import 'budgets_provider.dart'; import 'transactions_provider.dart'; final categoryTransactionTypeList = Provider>( @@ -76,6 +77,7 @@ class AsyncCategoriesNotifier Future markAsDeleted(int categoryId) async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { + await ref.read(deleteBudgetsByCategoryProvider)(categoryId); await CategoryTransactionMethods().markAsDeleted(categoryId); return _getCategories(arg); }); @@ -123,6 +125,27 @@ class AsyncCategoriesNotifier }; }); + //final reassignBudgetsProvider = + // Provider Function(int, CategoryTransactionType)>((ref) { + // return (int categoryId, CategoryTransactionType categoryType) async { + // final defaultCategoryId = 1; +// + // final budgets = + // await BudgetMethods().selectAll(); + // final affectedBudgets = budgets + // .where((t) => t.idCategory == categoryId) + // .toList(); +// + // for (var budget in affectedBudgets) { + // final updatedBudget = + // budget.copy(idCategory: defaultCategoryId); + // await BudgetMethods().updateItem(updatedBudget); + // } +// + // ref.invalidate(budgetsProvider); + // }; + //}); + Future removeCategory(int categoryId) async { final category = await CategoryTransactionMethods().selectById(categoryId); @@ -131,6 +154,8 @@ class AsyncCategoriesNotifier await ref.read(reassignTransactionsProvider)(categoryId, category.type); await ref.read(reassignRecurringTransactionsProvider)( categoryId, category.type); + await ref.read(deleteBudgetsByCategoryProvider)(categoryId); + await CategoryTransactionMethods().deleteById(categoryId); return _getCategories(arg); }); From abcfb7057e1f67b0b412abb488bc45c756c5b877 Mon Sep 17 00:00:00 2001 From: napitek Date: Sat, 19 Apr 2025 15:25:12 +0200 Subject: [PATCH 19/28] conflict settings --- lib/pages/settings_page.dart | 71 +----------------------------------- 1 file changed, 1 insertion(+), 70 deletions(-) diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index e1c1ddd9..8a5e32e0 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -104,8 +104,6 @@ class _SettingsPageState extends ConsumerState { "OK", style: TextStyle(color: Theme.of(context).colorScheme.primary), - style: - TextStyle(color: Theme.of(context).colorScheme.primary), ), ), ], @@ -134,8 +132,7 @@ class _SettingsPageState extends ConsumerState { child: Column( children: [ Padding( - padding: - const EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( vertical: Sizes.xl, horizontal: Sizes.lg), child: GestureDetector( onTap: _onSettingsTap, @@ -236,67 +233,6 @@ class _SettingsPageState extends ConsumerState { ), ), ); - padding: const EdgeInsets.only(bottom: 16), - child: DefaultCard( - onTap: () { - if (setting[3] != null) { - final link = setting[3] as String; - if (link.startsWith("http")) { - Uri url = Uri.parse(link); - launchUrl(url); - } else { - Navigator.of(context).pushNamed(link); - } - } - }, - child: Row( - children: [ - Container( - decoration: const BoxDecoration( - color: blue5, - shape: BoxShape.circle, - ), - padding: const EdgeInsets.all(10.0), - child: Icon( - setting[0] as IconData, - size: 30.0, - color: white, - ), - ), - const SizedBox(width: 12.0), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - setting[1].toString(), - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith( - color: Theme.of(context) - .colorScheme - .primary), - ), - Text( - setting[2].toString(), - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( - color: Theme.of(context) - .colorScheme - .primary), - overflow: TextOverflow.ellipsis, - maxLines: 2, - ), - ], - ), - ), - ], - ), - )); }, ), ], @@ -339,9 +275,6 @@ class _SettingsPageState extends ConsumerState { child: const Text('CLEAR AND FILL DEMO DATA'), onPressed: () async { await SossoldiDatabase.instance.clearDatabase(); - await SossoldiDatabase.instance - .fillDemoData() - .then((value) { await SossoldiDatabase.instance .fillDemoData() .then((value) { @@ -354,8 +287,6 @@ class _SettingsPageState extends ConsumerState { ref.refresh(statisticsProvider); showSuccessDialog( context, "DB Cleared, and DEMO data added"); - showSuccessDialog( - context, "DB Cleared, and DEMO data added"); }); }, ), From e6af840339fa1780bc4368991be5426918299884 Mon Sep 17 00:00:00 2001 From: napitek Date: Sat, 19 Apr 2025 15:27:53 +0200 Subject: [PATCH 20/28] dart format --- lib/providers/accounts_provider.dart | 2 +- lib/ui/widgets/transactions_list.dart | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/providers/accounts_provider.dart b/lib/providers/accounts_provider.dart index 5b70c7fc..ef9958ec 100644 --- a/lib/providers/accounts_provider.dart +++ b/lib/providers/accounts_provider.dart @@ -173,4 +173,4 @@ class AsyncAccountsNotifier extends AsyncNotifier> { final accountsProvider = AsyncNotifierProvider>(() { return AsyncAccountsNotifier(); -}); \ No newline at end of file +}); diff --git a/lib/ui/widgets/transactions_list.dart b/lib/ui/widgets/transactions_list.dart index b9450717..43d25517 100644 --- a/lib/ui/widgets/transactions_list.dart +++ b/lib/ui/widgets/transactions_list.dart @@ -129,8 +129,9 @@ class TransactionTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final currencyState = ref.watch(currencyStateNotifier); - - final category = ref.watch(categoryByIdProvider(transaction.idCategory!)).value; + + final category = + ref.watch(categoryByIdProvider(transaction.idCategory!)).value; return Material( child: ListTile( From 15698282e9d4ec2d76658a4b592a752d04da3dc6 Mon Sep 17 00:00:00 2001 From: napitek Date: Sat, 19 Apr 2025 15:50:09 +0200 Subject: [PATCH 21/28] Delete category dialog review with ref.invalidates --- .../widgets/delete_category_dialog.dart | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/pages/categories/widgets/delete_category_dialog.dart b/lib/pages/categories/widgets/delete_category_dialog.dart index cc59bed9..17d9b23f 100644 --- a/lib/pages/categories/widgets/delete_category_dialog.dart +++ b/lib/pages/categories/widgets/delete_category_dialog.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../model/category_transaction.dart'; +import '../../../providers/budgets_provider.dart'; import '../../../providers/categories_provider.dart'; +import '../../../providers/dashboard_provider.dart'; +import '../../../providers/statistics_provider.dart'; +import '../../../providers/transactions_provider.dart'; Future showDeleteCategoryDialog( BuildContext context, WidgetRef ref, selectedCategory) async { + void backToCategoryList() { if (context.mounted) { Navigator.of(context) @@ -12,6 +17,16 @@ Future showDeleteCategoryDialog( } } + void invalidateProviders() { + ref.invalidate(categoriesProvider(userCategoriesFilter)); + ref.invalidate(categoryByIdProvider); + ref.invalidate(transactionsProvider); + ref.invalidate(budgetsProvider); + ref.invalidate(dashboardProvider); + ref.invalidate(lastTransactionsProvider); + ref.invalidate(statisticsProvider); + } + return showDialog( context: context, builder: (BuildContext context) { @@ -38,12 +53,11 @@ Future showDeleteCategoryDialog( style: TextStyle(color: Theme.of(context).colorScheme.primary), ), onPressed: () async { - ref + await ref .read(categoriesProvider(userCategoriesFilter).notifier) - .markAsDeleted(selectedCategory.id) - .whenComplete(backToCategoryList); - ref.invalidate(categoriesProvider(userCategoriesFilter)); - ref.invalidate(categoryByIdProvider); + .markAsDeleted(selectedCategory.id); + invalidateProviders(); + backToCategoryList(); }), TextButton( child: Text( @@ -51,12 +65,11 @@ Future showDeleteCategoryDialog( style: TextStyle(color: Theme.of(context).colorScheme.primary), ), onPressed: () async { - ref + await ref .read(categoriesProvider(userCategoriesFilter).notifier) - .removeCategory(selectedCategory.id!) - .whenComplete(backToCategoryList); - ref.invalidate(categoriesProvider(userCategoriesFilter)); - ref.invalidate(categoryByIdProvider); + .removeCategory(selectedCategory.id!); + invalidateProviders(); + backToCategoryList(); }), ], ); From d9f501c8328e3ba6c5290f4c71700a2f076d7bde Mon Sep 17 00:00:00 2001 From: napitek Date: Sat, 19 Apr 2025 16:15:18 +0200 Subject: [PATCH 22/28] Alert by duplicated category and empty category name --- lib/pages/categories/add_category.dart | 117 +++++++++--------- .../widgets/delete_category_dialog.dart | 3 +- 2 files changed, 57 insertions(+), 63 deletions(-) diff --git a/lib/pages/categories/add_category.dart b/lib/pages/categories/add_category.dart index cc650af7..7a117ed8 100644 --- a/lib/pages/categories/add_category.dart +++ b/lib/pages/categories/add_category.dart @@ -6,6 +6,7 @@ import '../../model/category_transaction.dart'; import '../../providers/categories_provider.dart'; import '../../ui/device.dart'; import '../../ui/extensions.dart'; +import '../../ui/widgets/alert_dialog.dart'; import 'widgets/delete_category_dialog.dart'; class AddCategory extends ConsumerStatefulWidget { @@ -46,6 +47,27 @@ class _AddCategoryState extends ConsumerState { super.dispose(); } + bool _isDuplicateCategory() { + final existingCategoriesAsync = + ref.read(categoriesProvider(userCategoriesFilter)); + final selectedCategory = ref.read(selectedCategoryProvider); + + if (existingCategoriesAsync is AsyncData) { + final existingCategories = existingCategoriesAsync.value; + + return existingCategories!.any((category) { + if (selectedCategory != null && category.id == selectedCategory.id) { + return false; + } + return category.name == nameController.text && + category.type == categoryType && + category.symbol == categoryIcon && + category.color == categoryColor; + }); + } + return false; + } + @override Widget build(BuildContext context) { final selectedCategory = ref.watch(selectedCategoryProvider); @@ -126,11 +148,8 @@ class _AddCategoryState extends ConsumerState { underline: const SizedBox(), isExpanded: true, items: (widget.hideIncome - ? [ - CategoryTransactionType.expense - ] // Only show 'expense' if true - : CategoryTransactionType - .values) // Otherwise, show all values + ? [CategoryTransactionType.expense] + : CategoryTransactionType.values) .map((CategoryTransactionType type) { return DropdownMenuItem( value: type, @@ -340,37 +359,6 @@ class _AddCategoryState extends ConsumerState { ], ), ), - /* temporary hided, see #178 - Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(left: 16, top: 32, bottom: 8), - child: Text( - "SUBCATEGORY", - style: - Theme.of(context).textTheme.labelLarge!.copyWith(color: Theme.of(context).colorScheme.primary), - ), - ), - Material( - child: InkWell( - onTap: () => print("click"), - child: Ink( - width: double.infinity, - color: Theme.of(context).colorScheme.surface, - padding: const EdgeInsets.all(16), - child: Row( - children: [ - const Icon(Icons.add_circle_outline_rounded, size: 30, color: grey1), - const SizedBox(width: 12), - Text( - "Add subcategory", - style: Theme.of(context).textTheme.titleSmall!.copyWith(color: grey1), - ), - ], - ), - ), - ), - ), - */ if (selectedCategory != null) Container( width: double.infinity, @@ -421,32 +409,39 @@ class _AddCategoryState extends ConsumerState { ), child: ElevatedButton( onPressed: () async { - if (nameController.text.isNotEmpty) { - if (selectedCategory != null) { - await ref - .read( - categoriesProvider(userCategoriesFilter).notifier) - .updateCategory( - name: nameController.text, - type: categoryType, - icon: categoryIcon, - color: categoryColor, - ); - } else { - await ref - .read( - categoriesProvider(userCategoriesFilter).notifier) - .addCategory( - name: nameController.text, - type: categoryType, - icon: categoryIcon, - color: categoryColor, - ); - } - ref.invalidate(selectedCategoryProvider); - ref.invalidate(categoryMapProvider); - if (context.mounted) Navigator.of(context).pop(); + if (nameController.text.isEmpty) { + showWarningDialog(context, "Category name cannot be empty"); + return; + } + + if (_isDuplicateCategory()) { + showErrorDialog( + context, "An identical category already exists"); + return; + } + + if (selectedCategory != null) { + await ref + .read(categoriesProvider(userCategoriesFilter).notifier) + .updateCategory( + name: nameController.text, + type: categoryType, + icon: categoryIcon, + color: categoryColor, + ); + } else { + await ref + .read(categoriesProvider(userCategoriesFilter).notifier) + .addCategory( + name: nameController.text, + type: categoryType, + icon: categoryIcon, + color: categoryColor, + ); } + ref.invalidate(selectedCategoryProvider); + ref.invalidate(categoryMapProvider); + if (context.mounted) Navigator.of(context).pop(); }, child: Text( "${selectedCategory == null ? "CREATE" : "UPDATE"} CATEGORY", diff --git a/lib/pages/categories/widgets/delete_category_dialog.dart b/lib/pages/categories/widgets/delete_category_dialog.dart index 17d9b23f..274775c7 100644 --- a/lib/pages/categories/widgets/delete_category_dialog.dart +++ b/lib/pages/categories/widgets/delete_category_dialog.dart @@ -9,7 +9,6 @@ import '../../../providers/transactions_provider.dart'; Future showDeleteCategoryDialog( BuildContext context, WidgetRef ref, selectedCategory) async { - void backToCategoryList() { if (context.mounted) { Navigator.of(context) @@ -65,7 +64,7 @@ Future showDeleteCategoryDialog( style: TextStyle(color: Theme.of(context).colorScheme.primary), ), onPressed: () async { - await ref + await ref .read(categoriesProvider(userCategoriesFilter).notifier) .removeCategory(selectedCategory.id!); invalidateProviders(); From 8a0f6e1e421093c9d2bdce5ad079d4e01f5807bd Mon Sep 17 00:00:00 2001 From: napitek Date: Sat, 19 Apr 2025 16:18:09 +0200 Subject: [PATCH 23/28] restore temporary hided #178 --- lib/pages/categories/add_category.dart | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/pages/categories/add_category.dart b/lib/pages/categories/add_category.dart index 7a117ed8..c987780e 100644 --- a/lib/pages/categories/add_category.dart +++ b/lib/pages/categories/add_category.dart @@ -359,6 +359,37 @@ class _AddCategoryState extends ConsumerState { ], ), ), + /* temporary hided, see #178 + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 16, top: 32, bottom: 8), + child: Text( + "SUBCATEGORY", + style: + Theme.of(context).textTheme.labelLarge!.copyWith(color: Theme.of(context).colorScheme.primary), + ), + ), + Material( + child: InkWell( + onTap: () => print("click"), + child: Ink( + width: double.infinity, + color: Theme.of(context).colorScheme.surface, + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const Icon(Icons.add_circle_outline_rounded, size: 30, color: grey1), + const SizedBox(width: 12), + Text( + "Add subcategory", + style: Theme.of(context).textTheme.titleSmall!.copyWith(color: grey1), + ), + ], + ), + ), + ), + ), + */ if (selectedCategory != null) Container( width: double.infinity, From 4fb8c7bfc33210daa75a0eb32b1a3d12349f4108 Mon Sep 17 00:00:00 2001 From: napitek Date: Sat, 14 Jun 2025 11:40:01 +0200 Subject: [PATCH 24/28] Patching for migrations workflow, RoundedIcon with "markedAsDeleted" Icon --- .../migrations/0001_initial_schema.dart | 10 +------ .../0003_category_marked_as_deleted.dart | 25 ++++++++++++++++++ .../0004_uncategorized_default_category.dart | 26 +++++++++++++++++++ .../migrations/migration_registry.dart | 4 +++ lib/database/sossoldi_database.dart | 6 ++--- lib/model/category_transaction.dart | 22 ++++++++-------- .../add_page/widgets/category_selector.dart | 5 ++-- .../widget/recurring_payment_card.dart | 2 +- lib/pages/settings_page.dart | 8 +++--- lib/providers/categories_provider.dart | 2 +- lib/ui/widgets/rounded_icon.dart | 6 ++--- lib/ui/widgets/transactions_list.dart | 1 + test/model/category_transaction_test.dart | 10 +++---- 13 files changed, 87 insertions(+), 40 deletions(-) create mode 100644 lib/database/migrations/0003_category_marked_as_deleted.dart create mode 100644 lib/database/migrations/0004_uncategorized_default_category.dart diff --git a/lib/database/migrations/0001_initial_schema.dart b/lib/database/migrations/0001_initial_schema.dart index 030c77ab..f51c4dc4 100644 --- a/lib/database/migrations/0001_initial_schema.dart +++ b/lib/database/migrations/0001_initial_schema.dart @@ -87,19 +87,11 @@ class InitialSchema extends Migration { `${CategoryTransactionFields.color}` $integerNotNull, `${CategoryTransactionFields.note}` $text, `${CategoryTransactionFields.parent}` $integer, - `${CategoryTransactionFields.markedAsDeleted}` $integerNotNull CHECK (${CategoryTransactionFields.markedAsDeleted} IN (0, 1)), `${CategoryTransactionFields.createdAt}` $textNotNull, `${CategoryTransactionFields.updatedAt}` $textNotNull ) '''); - // Default "Uncategorized" Category - await db.execute(''' - INSERT INTO `$categoryTransactionTable`(`${CategoryTransactionFields.id}`, `${CategoryTransactionFields.name}`, `${CategoryTransactionFields.type}`, `${CategoryTransactionFields.symbol}`, `${CategoryTransactionFields.color}`, `${CategoryTransactionFields.note}`, `${CategoryTransactionFields.parent}`, `${CategoryTransactionFields.markedAsDeleted}`, `${CategoryTransactionFields.createdAt}`, `${CategoryTransactionFields.updatedAt}`) VALUES - (0, "Uncategorized", "IN", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'), - (1, "Uncategorized", "OUT", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'); - '''); - // Budget Table await db.execute(''' CREATE TABLE `$budgetTable`( @@ -132,4 +124,4 @@ class InitialSchema extends Migration { ("£", "GBP", "United Kingdom Pound", 0); '''); } -} +} \ No newline at end of file diff --git a/lib/database/migrations/0003_category_marked_as_deleted.dart b/lib/database/migrations/0003_category_marked_as_deleted.dart new file mode 100644 index 00000000..bdc22d7b --- /dev/null +++ b/lib/database/migrations/0003_category_marked_as_deleted.dart @@ -0,0 +1,25 @@ +// ignore_for_file: file_names + +import 'package:sqflite/sqflite.dart'; +import '../migration_base.dart'; + +// Models +import '/model/category_transaction.dart'; + +class CategoryMarkedAsDeleted extends Migration { + CategoryMarkedAsDeleted() + : super( + version: 3, + description: 'Add deleted column to CategoryTransaction model', + ); + + @override + Future up(Database db) async { + const integerNotNull = 'INTEGER NOT NULL'; + + // CategoryTransactionTable + await db.execute(''' + ALTER TABLE `$categoryTransactionTable` ADD COLUMN `${CategoryTransactionFields.deleted}` $integerNotNull DEFAULT 0; + '''); + } +} \ No newline at end of file diff --git a/lib/database/migrations/0004_uncategorized_default_category.dart b/lib/database/migrations/0004_uncategorized_default_category.dart new file mode 100644 index 00000000..6e1ba32e --- /dev/null +++ b/lib/database/migrations/0004_uncategorized_default_category.dart @@ -0,0 +1,26 @@ +// ignore_for_file: file_names + +import 'package:sqflite/sqflite.dart'; +import '../migration_base.dart'; + +// Models +import '/model/category_transaction.dart'; + +class UncategorizedDefaultCategory extends Migration { + UncategorizedDefaultCategory() + : super( + version: 4, + description: 'Create default "Uncategorized" category', + ); + + @override + Future up(Database db) async { + + // Default "Uncategorized" Category + await db.execute(''' + INSERT INTO `$categoryTransactionTable`(`${CategoryTransactionFields.id}`, `${CategoryTransactionFields.name}`, `${CategoryTransactionFields.type}`, `${CategoryTransactionFields.symbol}`, `${CategoryTransactionFields.color}`, `${CategoryTransactionFields.note}`, `${CategoryTransactionFields.parent}`, `${CategoryTransactionFields.deleted}`, `${CategoryTransactionFields.createdAt}`, `${CategoryTransactionFields.updatedAt}`) VALUES + (0, "Uncategorized", "IN", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'), + (1, "Uncategorized", "OUT", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'); + '''); + } +} \ No newline at end of file diff --git a/lib/database/migrations/migration_registry.dart b/lib/database/migrations/migration_registry.dart index d386f0e7..eb415f79 100644 --- a/lib/database/migrations/migration_registry.dart +++ b/lib/database/migrations/migration_registry.dart @@ -13,6 +13,8 @@ library; import '0001_initial_schema.dart'; import '0002_account_net_worth.dart'; +import '0003_category_marked_as_deleted.dart'; +import '0004_uncategorized_default_category.dart'; import '../migration_base.dart'; /// Returns all available migrations in execution order. @@ -25,6 +27,8 @@ List getMigrations() { return [ InitialSchema(), AccountNetWorth(), + CategoryMarkedAsDeleted(), + UncategorizedDefaultCategory(), // Add future migrations here ]; } diff --git a/lib/database/sossoldi_database.dart b/lib/database/sossoldi_database.dart index 6d5fceb2..d1fdf66b 100644 --- a/lib/database/sossoldi_database.dart +++ b/lib/database/sossoldi_database.dart @@ -205,9 +205,9 @@ class SossoldiDatabase { // Add fake categories await _database?.execute(''' - INSERT OR IGNORE INTO categoryTransaction(id, name, type, symbol, color, note, parent, markedAsDeleted, createdAt, updatedAt) VALUES - (0, "Uncategorized", "IN", "question_mark", 0, 'This is a default category for no categorized transactions', null, 0, '${DateTime.now()}', '${DateTime.now()}'), - (1, "Uncategorized", "OUT", "question_mark", 0, 'This is a default category for no categorized transactions', null, 0, '${DateTime.now()}', '${DateTime.now()}'), + INSERT OR IGNORE INTO categoryTransaction(id, name, type, symbol, color, note, parent, deleted, createdAt, updatedAt) VALUES + (0, "Uncategorized", "IN", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'), + (1, "Uncategorized", "OUT", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'), (10, "Out", "OUT", "restaurant", 1, '', null, 0, '${DateTime.now()}', '${DateTime.now()}'), (11, "Home", "OUT", "home", 2, '', null, 0, '${DateTime.now()}', '${DateTime.now()}'), (12, "Furniture","OUT", "home", 3, '', 11, 0, '${DateTime.now()}', '${DateTime.now()}'), diff --git a/lib/model/category_transaction.dart b/lib/model/category_transaction.dart index 638e5ff5..4cec2c98 100644 --- a/lib/model/category_transaction.dart +++ b/lib/model/category_transaction.dart @@ -12,7 +12,7 @@ class CategoryTransactionFields extends BaseEntityFields { static String color = 'color'; static String note = 'note'; static String parent = 'parent'; - static String markedAsDeleted = 'markedAsDeleted'; + static String deleted = 'deleted'; static String createdAt = BaseEntityFields.getCreatedAt; static String updatedAt = BaseEntityFields.getUpdatedAt; @@ -24,7 +24,7 @@ class CategoryTransactionFields extends BaseEntityFields { color, note, parent, - markedAsDeleted, + deleted, BaseEntityFields.createdAt, BaseEntityFields.updatedAt ]; @@ -81,7 +81,7 @@ class CategoryTransaction extends BaseEntity { final int color; final String? note; final int? parent; - final bool markedAsDeleted; + final bool deleted; const CategoryTransaction({ super.id, @@ -91,7 +91,7 @@ class CategoryTransaction extends BaseEntity { required this.color, this.note, this.parent, - required this.markedAsDeleted, + required this.deleted, super.createdAt, super.updatedAt, }); @@ -104,7 +104,7 @@ class CategoryTransaction extends BaseEntity { int? color, String? note, int? parent, - bool? markedAsDeleted, + bool? deleted, DateTime? createdAt, DateTime? updatedAt}) => CategoryTransaction( @@ -115,7 +115,7 @@ class CategoryTransaction extends BaseEntity { color: color ?? this.color, note: note ?? this.note, parent: parent ?? this.parent, - markedAsDeleted: markedAsDeleted ?? this.markedAsDeleted, + deleted: deleted ?? this.deleted, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt); @@ -129,7 +129,7 @@ class CategoryTransaction extends BaseEntity { color: json[CategoryTransactionFields.color] as int, note: json[CategoryTransactionFields.note] as String?, parent: json[CategoryTransactionFields.parent] as int?, - markedAsDeleted: json[CategoryTransactionFields.markedAsDeleted] == 1 + deleted: json[CategoryTransactionFields.deleted] == 1 ? true : false, createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), @@ -145,7 +145,7 @@ class CategoryTransaction extends BaseEntity { CategoryTransactionFields.color: color, CategoryTransactionFields.note: note, CategoryTransactionFields.parent: parent, - CategoryTransactionFields.markedAsDeleted: markedAsDeleted ? 1 : 0, + CategoryTransactionFields.deleted: deleted ? 1 : 0, BaseEntityFields.createdAt: update ? createdAt?.toIso8601String() : DateTime.now().toIso8601String(), @@ -202,12 +202,12 @@ class CategoryTransactionMethods extends SossoldiDatabase { whereArgs = [0, 1]; } - // showDeletedCategories == false => no markedAsDeleted + // showDeletedCategories == false => no deleted if (!filter.showDeletedCategories) { if (whereClause.isNotEmpty) { whereClause += ' AND '; } - whereClause += '${CategoryTransactionFields.markedAsDeleted} = ?'; + whereClause += '${CategoryTransactionFields.deleted} = ?'; whereArgs.add(0); } @@ -258,7 +258,7 @@ class CategoryTransactionMethods extends SossoldiDatabase { return await db.update( categoryTransactionTable, - {CategoryTransactionFields.markedAsDeleted: 1}, + {CategoryTransactionFields.deleted: 1}, where: '${CategoryTransactionFields.id} = ?', whereArgs: [id], ); diff --git a/lib/pages/add_page/widgets/category_selector.dart b/lib/pages/add_page/widgets/category_selector.dart index 5060c3a8..a3933aa7 100644 --- a/lib/pages/add_page/widgets/category_selector.dart +++ b/lib/pages/add_page/widgets/category_selector.dart @@ -74,9 +74,8 @@ class _CategorySelectorState extends ConsumerState { final availableCategories = categories .where((category) => category.type == categoryType && - !category.markedAsDeleted) + !category.deleted) .toList(); - return ListView.builder( itemCount: availableCategories.length, scrollDirection: Axis.horizontal, @@ -138,7 +137,7 @@ class _CategorySelectorState extends ConsumerState { final availableCategories = categories .where((category) => category.type == categoryType && - !category.markedAsDeleted) + !category.deleted) .toList(); return Container( diff --git a/lib/pages/planning_page/widget/recurring_payment_card.dart b/lib/pages/planning_page/widget/recurring_payment_card.dart index dfbe0147..589d6fcc 100644 --- a/lib/pages/planning_page/widget/recurring_payment_card.dart +++ b/lib/pages/planning_page/widget/recurring_payment_card.dart @@ -70,7 +70,7 @@ class RecurringPaymentCard extends ConsumerWidget { backgroundColor: categoryColorList[cat.color], padding: const EdgeInsets.all(Sizes.sm), size: 25, - markedAsDeleted: cat.markedAsDeleted, + deleted: cat.deleted, ), const SizedBox(width: Sizes.sm), Expanded( diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index c31ac310..61d8ef1f 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -7,10 +7,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:url_launcher/url_launcher.dart'; import '../constants/style.dart'; +import '../model/category_transaction.dart'; import '../ui/widgets/alert_dialog.dart'; import '../ui/widgets/default_card.dart'; import '../database/sossoldi_database.dart'; -import '../model/category_transaction.dart'; import '../providers/accounts_provider.dart'; import '../providers/budgets_provider.dart'; import '../providers/categories_provider.dart'; @@ -264,7 +264,7 @@ class _SettingsPageState extends ConsumerState { onPressed: () async { await SossoldiDatabase.instance.resetDatabase(); ref.refresh(accountsProvider); - ref.refresh(categoriesProvider); + ref.refresh(categoriesProvider(userCategoriesFilter)); ref.refresh(transactionsProvider); ref.refresh(budgetsProvider); @@ -278,8 +278,8 @@ class _SettingsPageState extends ConsumerState { onPressed: () async { await SossoldiDatabase.instance.clearDatabase(); await SossoldiDatabase.instance.fillDemoData(); - ref.refresh(accountsProvider); - ref.refresh(categoriesProvider); + ref.refresh(accountsProvider); + ref.refresh(categoriesProvider(userCategoriesFilter)); ref.refresh(transactionsProvider); ref.refresh(budgetsProvider); ref.refresh(dashboardProvider); diff --git a/lib/providers/categories_provider.dart b/lib/providers/categories_provider.dart index ee80c6f7..f7428b23 100644 --- a/lib/providers/categories_provider.dart +++ b/lib/providers/categories_provider.dart @@ -43,7 +43,7 @@ class AsyncCategoriesNotifier symbol: icon, type: type, color: color, - markedAsDeleted: false, + deleted: false, ); state = const AsyncValue.loading(); diff --git a/lib/ui/widgets/rounded_icon.dart b/lib/ui/widgets/rounded_icon.dart index 5b7a0279..61aef146 100644 --- a/lib/ui/widgets/rounded_icon.dart +++ b/lib/ui/widgets/rounded_icon.dart @@ -8,7 +8,7 @@ class RoundedIcon extends StatelessWidget { this.backgroundColor, this.size = 24, this.padding = const EdgeInsets.all(10.0), - this.markedAsDeleted = false, + this.deleted = false, this.onDelete, super.key, }); @@ -17,7 +17,7 @@ class RoundedIcon extends StatelessWidget { final Color? backgroundColor; final double? size; final EdgeInsets? padding; - final bool markedAsDeleted; + final bool deleted; final VoidCallback? onDelete; @override @@ -38,7 +38,7 @@ class RoundedIcon extends StatelessWidget { ) : const SizedBox(), ), - if (markedAsDeleted) + if (deleted) Positioned( right: 0, bottom: 0, diff --git a/lib/ui/widgets/transactions_list.dart b/lib/ui/widgets/transactions_list.dart index 5f39f899..ee9af343 100644 --- a/lib/ui/widgets/transactions_list.dart +++ b/lib/ui/widgets/transactions_list.dart @@ -160,6 +160,7 @@ class TransactionTile extends ConsumerWidget { : Theme.of(context).colorScheme.secondary, size: 25, padding: const EdgeInsets.all(Sizes.sm), + deleted: category != null ? category.deleted : false, ), title: Text( (transaction.note?.isEmpty ?? true) diff --git a/test/model/category_transaction_test.dart b/test/model/category_transaction_test.dart index efa0c805..a685813f 100644 --- a/test/model/category_transaction_test.dart +++ b/test/model/category_transaction_test.dart @@ -11,7 +11,7 @@ void main() { type: CategoryTransactionType.expense, symbol: "symbol", color: 0, - markedAsDeleted: false, + deleted: false, createdAt: DateTime.utc(2022), updatedAt: DateTime.utc(2022)); @@ -22,7 +22,7 @@ void main() { assert(cCopy.type == c.type); assert(cCopy.symbol == c.symbol); assert(cCopy.color == c.color); - assert(cCopy.markedAsDeleted == c.markedAsDeleted); + assert(cCopy.deleted == c.deleted); assert(cCopy.createdAt == c.createdAt); assert(cCopy.updatedAt == c.updatedAt); }); @@ -61,7 +61,7 @@ void main() { symbol: "symbol", color: 0, note: "note", - markedAsDeleted: false); + deleted: false); Map json = c.toJson(); @@ -71,7 +71,7 @@ void main() { assert(c.symbol == json[CategoryTransactionFields.symbol]); assert(c.color == json[CategoryTransactionFields.color]); assert(c.note == json[CategoryTransactionFields.note]); - assert((c.markedAsDeleted ? 1 : 0) == - json[CategoryTransactionFields.markedAsDeleted]); + assert((c.deleted ? 1 : 0) == + json[CategoryTransactionFields.deleted]); }); } From 8cca904c8d3edfde922233af689cc1f2676c9ae3 Mon Sep 17 00:00:00 2001 From: napitek Date: Sat, 14 Jun 2025 11:42:48 +0200 Subject: [PATCH 25/28] dart format bruh --- lib/database/migrations/0001_initial_schema.dart | 2 +- lib/database/migrations/0003_category_marked_as_deleted.dart | 2 +- .../migrations/0004_uncategorized_default_category.dart | 3 +-- lib/model/category_transaction.dart | 4 +--- lib/pages/settings_page.dart | 2 +- test/model/category_transaction_test.dart | 3 +-- 6 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/database/migrations/0001_initial_schema.dart b/lib/database/migrations/0001_initial_schema.dart index f51c4dc4..2472d32e 100644 --- a/lib/database/migrations/0001_initial_schema.dart +++ b/lib/database/migrations/0001_initial_schema.dart @@ -124,4 +124,4 @@ class InitialSchema extends Migration { ("£", "GBP", "United Kingdom Pound", 0); '''); } -} \ No newline at end of file +} diff --git a/lib/database/migrations/0003_category_marked_as_deleted.dart b/lib/database/migrations/0003_category_marked_as_deleted.dart index bdc22d7b..6a286afc 100644 --- a/lib/database/migrations/0003_category_marked_as_deleted.dart +++ b/lib/database/migrations/0003_category_marked_as_deleted.dart @@ -22,4 +22,4 @@ class CategoryMarkedAsDeleted extends Migration { ALTER TABLE `$categoryTransactionTable` ADD COLUMN `${CategoryTransactionFields.deleted}` $integerNotNull DEFAULT 0; '''); } -} \ No newline at end of file +} diff --git a/lib/database/migrations/0004_uncategorized_default_category.dart b/lib/database/migrations/0004_uncategorized_default_category.dart index 6e1ba32e..1a50ece4 100644 --- a/lib/database/migrations/0004_uncategorized_default_category.dart +++ b/lib/database/migrations/0004_uncategorized_default_category.dart @@ -15,7 +15,6 @@ class UncategorizedDefaultCategory extends Migration { @override Future up(Database db) async { - // Default "Uncategorized" Category await db.execute(''' INSERT INTO `$categoryTransactionTable`(`${CategoryTransactionFields.id}`, `${CategoryTransactionFields.name}`, `${CategoryTransactionFields.type}`, `${CategoryTransactionFields.symbol}`, `${CategoryTransactionFields.color}`, `${CategoryTransactionFields.note}`, `${CategoryTransactionFields.parent}`, `${CategoryTransactionFields.deleted}`, `${CategoryTransactionFields.createdAt}`, `${CategoryTransactionFields.updatedAt}`) VALUES @@ -23,4 +22,4 @@ class UncategorizedDefaultCategory extends Migration { (1, "Uncategorized", "OUT", "question_mark", 0, 'This is a default category for no categorized transactions', null, '0', '${DateTime.now()}', '${DateTime.now()}'); '''); } -} \ No newline at end of file +} diff --git a/lib/model/category_transaction.dart b/lib/model/category_transaction.dart index 4cec2c98..ab1bcba3 100644 --- a/lib/model/category_transaction.dart +++ b/lib/model/category_transaction.dart @@ -129,9 +129,7 @@ class CategoryTransaction extends BaseEntity { color: json[CategoryTransactionFields.color] as int, note: json[CategoryTransactionFields.note] as String?, parent: json[CategoryTransactionFields.parent] as int?, - deleted: json[CategoryTransactionFields.deleted] == 1 - ? true - : false, + deleted: json[CategoryTransactionFields.deleted] == 1 ? true : false, createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), updatedAt: DateTime.parse(json[BaseEntityFields.updatedAt] as String)); diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 61d8ef1f..5a095018 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -278,7 +278,7 @@ class _SettingsPageState extends ConsumerState { onPressed: () async { await SossoldiDatabase.instance.clearDatabase(); await SossoldiDatabase.instance.fillDemoData(); - ref.refresh(accountsProvider); + ref.refresh(accountsProvider); ref.refresh(categoriesProvider(userCategoriesFilter)); ref.refresh(transactionsProvider); ref.refresh(budgetsProvider); diff --git a/test/model/category_transaction_test.dart b/test/model/category_transaction_test.dart index a685813f..78022ca3 100644 --- a/test/model/category_transaction_test.dart +++ b/test/model/category_transaction_test.dart @@ -71,7 +71,6 @@ void main() { assert(c.symbol == json[CategoryTransactionFields.symbol]); assert(c.color == json[CategoryTransactionFields.color]); assert(c.note == json[CategoryTransactionFields.note]); - assert((c.deleted ? 1 : 0) == - json[CategoryTransactionFields.deleted]); + assert((c.deleted ? 1 : 0) == json[CategoryTransactionFields.deleted]); }); } From 7342c6e1b1195b2b56707a8c790013c3280618ed Mon Sep 17 00:00:00 2001 From: napitek Date: Tue, 17 Jun 2025 17:49:52 +0200 Subject: [PATCH 26/28] fix migration names --- ...d_as_deleted.dart => 0004_category_marked_as_deleted.dart} | 2 +- ...category.dart => 0005_uncategorized_default_category.dart} | 2 +- lib/database/migrations/migration_registry.dart | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename lib/database/migrations/{0003_category_marked_as_deleted.dart => 0004_category_marked_as_deleted.dart} (96%) rename lib/database/migrations/{0004_uncategorized_default_category.dart => 0005_uncategorized_default_category.dart} (98%) diff --git a/lib/database/migrations/0003_category_marked_as_deleted.dart b/lib/database/migrations/0004_category_marked_as_deleted.dart similarity index 96% rename from lib/database/migrations/0003_category_marked_as_deleted.dart rename to lib/database/migrations/0004_category_marked_as_deleted.dart index 6a286afc..6b6793aa 100644 --- a/lib/database/migrations/0003_category_marked_as_deleted.dart +++ b/lib/database/migrations/0004_category_marked_as_deleted.dart @@ -9,7 +9,7 @@ import '/model/category_transaction.dart'; class CategoryMarkedAsDeleted extends Migration { CategoryMarkedAsDeleted() : super( - version: 3, + version: 4, description: 'Add deleted column to CategoryTransaction model', ); diff --git a/lib/database/migrations/0004_uncategorized_default_category.dart b/lib/database/migrations/0005_uncategorized_default_category.dart similarity index 98% rename from lib/database/migrations/0004_uncategorized_default_category.dart rename to lib/database/migrations/0005_uncategorized_default_category.dart index 1a50ece4..3cbb4b0d 100644 --- a/lib/database/migrations/0004_uncategorized_default_category.dart +++ b/lib/database/migrations/0005_uncategorized_default_category.dart @@ -9,7 +9,7 @@ import '/model/category_transaction.dart'; class UncategorizedDefaultCategory extends Migration { UncategorizedDefaultCategory() : super( - version: 4, + version: 5, description: 'Create default "Uncategorized" category', ); diff --git a/lib/database/migrations/migration_registry.dart b/lib/database/migrations/migration_registry.dart index eb415f79..ed31a2ae 100644 --- a/lib/database/migrations/migration_registry.dart +++ b/lib/database/migrations/migration_registry.dart @@ -13,8 +13,8 @@ library; import '0001_initial_schema.dart'; import '0002_account_net_worth.dart'; -import '0003_category_marked_as_deleted.dart'; -import '0004_uncategorized_default_category.dart'; +import '0004_category_marked_as_deleted.dart'; +import '0005_uncategorized_default_category.dart'; import '../migration_base.dart'; /// Returns all available migrations in execution order. From ab22234b4f1b4cac30f89008ea572c012158c5d8 Mon Sep 17 00:00:00 2001 From: napitek Date: Tue, 17 Jun 2025 17:53:15 +0200 Subject: [PATCH 27/28] dart format bruh 2 --- lib/model/bank_account.dart | 12 ++++++------ lib/pages/account_page/account_page.dart | 2 +- lib/providers/accounts_provider.dart | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/model/bank_account.dart b/lib/model/bank_account.dart index 85e3d07f..e6c24e5a 100644 --- a/lib/model/bank_account.dart +++ b/lib/model/bank_account.dart @@ -360,10 +360,10 @@ class BankAccountMethods extends SossoldiDatabase { } Future accountMonthlyBalance( - int accountId, { - DateTime? dateRangeStart, - DateTime? dateRangeEnd, - }) async { + int accountId, { + DateTime? dateRangeStart, + DateTime? dateRangeEnd, + }) async { final db = await database; final accountFilter = @@ -402,8 +402,8 @@ class BankAccountMethods extends SossoldiDatabase { if (dateRangeStart != null) { return result .where((element) => dateRangeStart.isBefore( - DateTime.parse(("${element["month"]}-01").toString()) - .add(const Duration(days: 1)))) + DateTime.parse(("${element["month"]}-01").toString()) + .add(const Duration(days: 1)))) .toList(); } diff --git a/lib/pages/account_page/account_page.dart b/lib/pages/account_page/account_page.dart index 531c884a..96cf78b6 100644 --- a/lib/pages/account_page/account_page.dart +++ b/lib/pages/account_page/account_page.dart @@ -37,7 +37,7 @@ class _AccountPage extends ConsumerState { Widget build(BuildContext context) { final account = ref.read(selectedAccountProvider); final accountTransactions = - ref.watch(selectedAccountCurrentYearMonthlyBalanceProvider); + ref.watch(selectedAccountCurrentYearMonthlyBalanceProvider); final transactions = ref.watch(selectedAccountLastTransactions); final currencyState = ref.watch(currencyStateNotifier); diff --git a/lib/providers/accounts_provider.dart b/lib/providers/accounts_provider.dart index 76e1a155..509c459d 100644 --- a/lib/providers/accounts_provider.dart +++ b/lib/providers/accounts_provider.dart @@ -97,7 +97,7 @@ class AsyncAccountsNotifier extends AsyncNotifier> { }); } -Future reconcileAccount({ + Future reconcileAccount({ required BankAccount account, required num newBalance, }) async { @@ -128,7 +128,7 @@ Future reconcileAccount({ Future refreshAccount(BankAccount account) async { ref.read(selectedAccountProvider.notifier).state = account; -final currentMonthDailyBalance = await BankAccountMethods() + final currentMonthDailyBalance = await BankAccountMethods() .accountMonthlyBalance(account.id!, dateRangeStart: DateTime(DateTime.now().year, 1, 1), // beginnig of current year From e622740dcc949322b9945c2b9df217679dcc002a Mon Sep 17 00:00:00 2001 From: napitek Date: Tue, 17 Jun 2025 17:55:49 +0200 Subject: [PATCH 28/28] cat to category --- lib/pages/planning_page/widget/recurring_payment_card.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/planning_page/widget/recurring_payment_card.dart b/lib/pages/planning_page/widget/recurring_payment_card.dart index 1f3c287e..955ac640 100644 --- a/lib/pages/planning_page/widget/recurring_payment_card.dart +++ b/lib/pages/planning_page/widget/recurring_payment_card.dart @@ -70,7 +70,7 @@ class RecurringPaymentCard extends ConsumerWidget { backgroundColor: categoryColorList[category.color], padding: const EdgeInsets.all(Sizes.sm), size: 25, - deleted: cat.deleted, + deleted: category.deleted, ), const SizedBox(width: Sizes.sm), Expanded(