Skip to content
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f7ade5d
New "Uncategorized" Category, markAsDelete/Delete Dialog
napitek Mar 16, 2025
733ed54
Merge branch 'RIP-Comm:main' into main
napitek Mar 19, 2025
bbe4a76
markedAsDelete - delete CategoryTransaction workflow
napitek Mar 19, 2025
221f9e9
Reassign CategoryTransaction to recurring transactions
napitek Mar 19, 2025
5351b3e
UNIQUE constraint failed: categoryTransaction.id, constraint failed (…
napitek Mar 19, 2025
725722d
Fix ALL CATEGORIES section filter
napitek Mar 20, 2025
b357142
fix Uncategorized CategoryTransaction unselectable
napitek Mar 30, 2025
7abd401
adapt the changes to the MigrationManager
napitek Mar 30, 2025
a67df8d
resolve dart format conflict
napitek Mar 30, 2025
6584732
test conflict
napitek Mar 30, 2025
cb22393
conflicts
napitek Mar 30, 2025
e550eed
empty line conflict test
napitek Mar 30, 2025
a6b717c
empty line conflict test 2
napitek Mar 30, 2025
13e1a2d
fix missing countNetWorth in bank_account_test
napitek Mar 30, 2025
f1549f2
another conflict
napitek Mar 30, 2025
9881b3c
missing empty line
napitek Mar 30, 2025
c88a5e1
adapt inital_schema with markedAsDeleted
napitek Mar 30, 2025
efc30ab
Merge branch 'main' into main
napitek Mar 30, 2025
7680807
RoundedIcon with "markedAsDeleted" subIcon
napitek Mar 30, 2025
31d777c
delete budgets when category is markedAsDeleted/deleted
napitek Apr 3, 2025
be6ec53
Merge remote-tracking branch 'upstream/main'
napitek Apr 19, 2025
abcfb70
conflict settings
napitek Apr 19, 2025
e6af840
dart format
napitek Apr 19, 2025
1569828
Delete category dialog review with ref.invalidates
napitek Apr 19, 2025
d9f501c
Alert by duplicated category and empty category name
napitek Apr 19, 2025
8a0f6e1
restore temporary hided #178
napitek Apr 19, 2025
08586b7
Merge branch 'main' into main
napitek May 4, 2025
8673070
Merge branch 'RIP-Comm:main' into main
napitek Jun 9, 2025
4fb8c7b
Patching for migrations workflow, RoundedIcon with "markedAsDeleted" …
napitek Jun 14, 2025
8cca904
dart format bruh
napitek Jun 14, 2025
7342c6e
fix migration names
napitek Jun 17, 2025
7ee0b91
Merge branch 'main' into main
napitek Jun 17, 2025
ab22234
dart format bruh 2
napitek Jun 17, 2025
e622740
cat to category
napitek Jun 17, 2025
e556a0c
Merge branch 'RIP-Comm:main' into main
napitek Jul 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/constants/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const Map<String, IconData> iconList = {
'device_thermostat': Icons.device_thermostat,
'dry_cleaning': Icons.dry_cleaning,
'work': Icons.work,
'question_mark': Icons.question_mark,
};

const Map<String, IconData> accountIconList = {
Expand All @@ -31,6 +32,7 @@ const Map<String, IconData> accountIconList = {

// colors
const categoryColorList = [
category0,
category1,
category2,
category3,
Expand All @@ -43,6 +45,7 @@ const categoryColorList = [
];

const darkCategoryColorList = [
darkCategory0,
darkCategory1,
darkCategory2,
darkCategory3,
Expand Down
2 changes: 2 additions & 0 deletions lib/constants/style.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions lib/database/migrations/0004_category_marked_as_deleted.dart
Original file line number Diff line number Diff line change
@@ -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: 4,
description: 'Add deleted column to CategoryTransaction model',
);

@override
Future<void> up(Database db) async {
const integerNotNull = 'INTEGER NOT NULL';

// CategoryTransactionTable
await db.execute('''
ALTER TABLE `$categoryTransactionTable` ADD COLUMN `${CategoryTransactionFields.deleted}` $integerNotNull DEFAULT 0;
''');
}
}
25 changes: 25 additions & 0 deletions lib/database/migrations/0005_uncategorized_default_category.dart
Original file line number Diff line number Diff line change
@@ -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 UncategorizedDefaultCategory extends Migration {
UncategorizedDefaultCategory()
: super(
version: 5,
description: 'Create default "Uncategorized" category',
);

@override
Future<void> 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()}');
Comment on lines +20 to +22
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a problem in migration 5:
Users who don't have the demo data (i.e. they went through onboarding by adding a budget, or started from scratch and added a category afterwards) already have a categoryTransaction with ID 1, so this query fails and the app doesn't open.

There are two ways to fix it:

  1. We allow those categories to have any ID (I don't remember if they must be 0 and 1), and let the auto-increment assign them.

  2. Before running the query, we check if category ID 1 already exists, and if so, we assign it a different ID and update all associated transactions. Only then we insert the new categories with IDs 0 and 1.

Any other ideas?

''');
}
}
4 changes: 4 additions & 0 deletions lib/database/migrations/migration_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ library;
import '0001_initial_schema.dart';
import '0002_account_net_worth.dart';
import '0003_recurring_transaction_type.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.
Expand All @@ -27,6 +29,8 @@ List<Migration> getMigrations() {
InitialSchema(),
AccountNetWorth(),
RecurringTransactionType(),
CategoryMarkedAsDeleted(),
UncategorizedDefaultCategory(),
// Add future migrations here
];
}
Expand Down
18 changes: 10 additions & 8 deletions lib/database/sossoldi_database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,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 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()}'),
(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
Expand Down
12 changes: 6 additions & 6 deletions lib/model/bank_account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -360,10 +360,10 @@ class BankAccountMethods extends SossoldiDatabase {
}

Future<List> accountMonthlyBalance(
int accountId, {
DateTime? dateRangeStart,
DateTime? dateRangeEnd,
}) async {
int accountId, {
DateTime? dateRangeStart,
DateTime? dateRangeEnd,
}) async {
final db = await database;

final accountFilter =
Expand Down Expand Up @@ -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();
}

Expand Down
89 changes: 89 additions & 0 deletions lib/model/category_transaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class CategoryTransactionFields extends BaseEntityFields {
static String color = 'color';
static String note = 'note';
static String parent = 'parent';
static String deleted = 'deleted';
static String createdAt = BaseEntityFields.getCreatedAt;
static String updatedAt = BaseEntityFields.getUpdatedAt;

Expand All @@ -23,11 +24,49 @@ class CategoryTransactionFields extends BaseEntityFields {
color,
note,
parent,
deleted,
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 availableCategoriesFilter = CategoryFilter(
showSystemCategories: true,
showDeletedCategories: false,
);
const allCategoriesFilter = CategoryFilter(
showSystemCategories: true,
showDeletedCategories: true,
);

enum CategoryTransactionType { income, expense }

Map<String, CategoryTransactionType> categoryTypeMap = {
Expand All @@ -42,6 +81,7 @@ class CategoryTransaction extends BaseEntity {
final int color;
final String? note;
final int? parent;
final bool deleted;

const CategoryTransaction({
super.id,
Expand All @@ -51,6 +91,7 @@ class CategoryTransaction extends BaseEntity {
required this.color,
this.note,
this.parent,
required this.deleted,
super.createdAt,
super.updatedAt,
});
Expand All @@ -63,6 +104,7 @@ class CategoryTransaction extends BaseEntity {
int? color,
String? note,
int? parent,
bool? deleted,
DateTime? createdAt,
DateTime? updatedAt}) =>
CategoryTransaction(
Expand All @@ -73,6 +115,7 @@ class CategoryTransaction extends BaseEntity {
color: color ?? this.color,
note: note ?? this.note,
parent: parent ?? this.parent,
deleted: deleted ?? this.deleted,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt);

Expand All @@ -86,6 +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,
createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String),
updatedAt:
DateTime.parse(json[BaseEntityFields.updatedAt] as String));
Expand All @@ -99,6 +143,7 @@ class CategoryTransaction extends BaseEntity {
CategoryTransactionFields.color: color,
CategoryTransactionFields.note: note,
CategoryTransactionFields.parent: parent,
CategoryTransactionFields.deleted: deleted ? 1 : 0,
BaseEntityFields.createdAt: update
? createdAt?.toIso8601String()
: DateTime.now().toIso8601String(),
Expand Down Expand Up @@ -141,6 +186,39 @@ class CategoryTransactionMethods extends SossoldiDatabase {
return result.map((json) => CategoryTransaction.fromJson(json)).toList();
}

Future<List<CategoryTransaction>> selectCategories(
CategoryFilter filter) async {
final db = await database;

String whereClause = '';
List<dynamic> whereArgs = [];

// showSystemCategories == false => no uncategorized
if (!filter.showSystemCategories) {
whereClause =
'${CategoryTransactionFields.id} != ? AND ${CategoryTransactionFields.id} != ?';
whereArgs = [0, 1];
}

// showDeletedCategories == false => no deleted
if (!filter.showDeletedCategories) {
if (whereClause.isNotEmpty) {
whereClause += ' AND ';
}
whereClause += '${CategoryTransactionFields.deleted} = ?';
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<List<CategoryTransaction>> selectCategoriesByType(
CategoryTransactionType type) async {
final db = await database;
Expand Down Expand Up @@ -173,6 +251,17 @@ class CategoryTransactionMethods extends SossoldiDatabase {
);
}

Future<int> markAsDeleted(int id) async {
final db = await database;

return await db.update(
categoryTransactionTable,
{CategoryTransactionFields.deleted: 1},
where: '${CategoryTransactionFields.id} = ?',
whereArgs: [id],
);
}

Future<int> deleteById(int id) async {
final db = await database;

Expand Down
2 changes: 1 addition & 1 deletion lib/pages/account_page/account_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class _AccountPage extends ConsumerState<AccountPage> {
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);

Expand Down
Loading