diff --git a/lib/custom_widgets/accounts_sum.dart b/lib/custom_widgets/accounts_sum.dart index b8d1acf..b5bb177 100644 --- a/lib/custom_widgets/accounts_sum.dart +++ b/lib/custom_widgets/accounts_sum.dart @@ -6,14 +6,14 @@ import '../constants/functions.dart'; import '../constants/style.dart'; import 'account_modal.dart'; -/// This class shows account summaries in dashboard +/// This class shows account summaries in the dashboard class AccountsSum extends StatelessWidget with Functions { final BankAccount account; const AccountsSum({ - super.key, + Key? key, required this.account, - }); + }) : super(key: key); @override Widget build(BuildContext context) { @@ -26,10 +26,10 @@ class AccountsSum extends StatelessWidget with Functions { boxShadow: [defaultShadow], ), child: Container( - decoration: BoxDecoration( - color: accountColorList[account.color].withOpacity(0.2), - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration( + color: accountColorList[account.color].withOpacity(0.2), + borderRadius: BorderRadius.circular(8), + ), child: Material( color: Colors.transparent, child: InkWell( @@ -52,7 +52,7 @@ class AccountsSum extends StatelessWidget with Functions { children: [ AccountDialog( accountName: account.name, - amount: account.value, + amount: account.startingValue, ) ], ), @@ -84,22 +84,40 @@ class AccountsSum extends StatelessWidget with Functions { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(account.name, style: Theme.of(context).textTheme.bodyLarge), - RichText( - textScaleFactor: MediaQuery.of(context).textScaleFactor, - text: TextSpan( - children: [ - TextSpan( - text: numToCurrency(account.value), - style: Theme.of(context).textTheme.titleSmall, - ), - TextSpan( - text: "€", - style: Theme.of(context).textTheme.bodyMedium?.apply( - fontFeatures: [const FontFeature.subscripts()], + FutureBuilder( + future: BankAccountMethods().getAccountSum(account.id), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + // Show a loading indicator while waiting for the future to complete + return Transform.scale( + scale: 0.5, + child: CircularProgressIndicator(), + ); + } else if (snapshot.hasError) { + // Show an error message if the future encounters an error + return Text('Error: ${snapshot.error}'); + } else { + // Display the result once the future completes successfully + final accountSum = snapshot.data ?? 0; + return RichText( + textScaleFactor: MediaQuery.of(context).textScaleFactor, + text: TextSpan( + children: [ + TextSpan( + text: numToCurrency(accountSum), + style: Theme.of(context).textTheme.titleSmall, + ), + TextSpan( + text: "€", + style: Theme.of(context).textTheme.bodyMedium?.apply( + fontFeatures: [const FontFeature.subscripts()], + ), + ), + ], ), - ), - ], - ), + ); + } + }, ), ], ), diff --git a/lib/custom_widgets/transactions_list.dart b/lib/custom_widgets/transactions_list.dart index e25519c..c45a1b2 100644 --- a/lib/custom_widgets/transactions_list.dart +++ b/lib/custom_widgets/transactions_list.dart @@ -13,7 +13,6 @@ import '../providers/accounts_provider.dart'; import '../providers/categories_provider.dart'; import '../constants/style.dart'; -/// This class shows account summaries in dashboard class TransactionsList extends StatefulWidget { final List transactions; diff --git a/lib/database/sossoldi_database.dart b/lib/database/sossoldi_database.dart index 7de5ba3..a795c98 100644 --- a/lib/database/sossoldi_database.dart +++ b/lib/database/sossoldi_database.dart @@ -52,7 +52,7 @@ class SossoldiDatabase { `${BankAccountFields.name}` $textNotNull, `${BankAccountFields.symbol}` $textNotNull, `${BankAccountFields.color}` $integerNotNull, - `${BankAccountFields.value}` $realNotNull, + `${BankAccountFields.startingValue}` $realNotNull, `${BankAccountFields.active}` $integerNotNull CHECK (${BankAccountFields.active} IN (0, 1)), `${BankAccountFields.mainAccount}` $integerNotNull CHECK (${BankAccountFields.mainAccount} IN (0, 1)), `${BankAccountFields.createdAt}` $textNotNull, @@ -137,10 +137,10 @@ class SossoldiDatabase { Future fillDemoData() async { // Add some fake accounts await _database?.execute(''' - INSERT INTO bankAccount(id, name, symbol, color, value, active, mainAccount, createdAt, updatedAt) VALUES + INSERT INTO bankAccount(id, name, symbol, color, startingValue, active, mainAccount, createdAt, updatedAt) VALUES (70, "Revolut", 'payments', 1, 1235.10, 1, 1, '${DateTime.now()}', '${DateTime.now()}'), (71, "N26", 'credit_card', 2, 3823.56, 1, 0, '${DateTime.now()}', '${DateTime.now()}'), - (72, "Fineco", 'account_balance', 3, 0.07, 1, 0, '${DateTime.now()}', '${DateTime.now()}'); + (72, "Fineco", 'account_balance', 3, 0.00, 1, 0, '${DateTime.now()}', '${DateTime.now()}'); '''); // Add fake categories @@ -174,11 +174,12 @@ class SossoldiDatabase { // First initialize some config stuff final rnd = Random(); var accounts = [70,71,72]; - var outNotes = ['Grocery', 'Tolls', 'Toys', 'Tobacco', 'Concert', 'Clothing', 'Pizza', 'Drugs', 'Laundry', 'Taxes', 'Health insurance', 'Furniture', 'Car Fuel', 'Train', 'Amazon', 'Delivery', 'Hotel', 'Babysitter', 'Paypal Fees', 'Quingentole trip']; + var outNotes = ['Grocery', 'Tolls', 'Toys', 'Tobacco', 'Concert', 'Clothing', 'Pizza', 'Drugs', 'Laundry', 'Taxes', 'Health insurance', 'Furniture', 'Car Fuel', 'Train', 'Amazon', 'Delivery', 'CHEK dividends', 'Babysitter', 'sono.pove.ro Fees', 'Quingentole trip']; var categories = [10,11,12,13,14]; - int countOfGeneratedTransaction = 5000; + int countOfGeneratedTransaction = 10000; double maxAmountOfSingleTransaction = 250.00; - int dateInPastMaxRange = 2*365; // we want simulate past 2 years + int dateInPastMaxRange = (countOfGeneratedTransaction / 90 ).round() * 30; // we want simulate about 90 transactions per month + num fakeSalary = 5000; DateTime now = DateTime.now(); // start building mega-query @@ -189,7 +190,15 @@ class SossoldiDatabase { // Start a loop for (int i = 0; i < countOfGeneratedTransaction; i++) { - var randomAmount = rnd.nextDouble() * maxAmountOfSingleTransaction; + num randomAmount = 0; + + // we are more likely to give low amounts + if (rnd.nextInt(10) < 8) { + randomAmount = rnd.nextDouble() * (19.99 - 1) + 1; + } else { + randomAmount = rnd.nextDouble() * (maxAmountOfSingleTransaction - 100) + 100; + } + var randomType = 'OUT'; var randomAccount = accounts[rnd.nextInt(accounts.length)]; var randomNote = outNotes[rnd.nextInt(outNotes.length)]; @@ -197,11 +206,13 @@ class SossoldiDatabase { var idBankAccountTransfer; DateTime randomDate = now.subtract(Duration(days: rnd.nextInt(dateInPastMaxRange), hours: rnd.nextInt(20), minutes: rnd.nextInt(50))); - if (i % 70 == 0) { - // simulating a transfer every 70 iterations + if (i % (countOfGeneratedTransaction/100) == 0) { + // simulating a transfer every 1% of total iterations randomType = 'TRSF'; - randomNote = ''; + randomNote = 'Transfer'; + randomAccount = 70; // sender account is hardcoded with the one that receives our fake salary idBankAccountTransfer = accounts[rnd.nextInt(accounts.length)]; + randomAmount = (fakeSalary/100)*70; // be sure our FROM/TO accounts are not the same while (idBankAccountTransfer == randomAccount) { @@ -214,9 +225,11 @@ class SossoldiDatabase { } // add salary every month - for (int i = 0; i < dateInPastMaxRange/30; i++) { + for (int i = 1; i < dateInPastMaxRange/30; i++) { DateTime randomDate = now.subtract(Duration(days: 30*i)); - demoTransactions.add('''('$randomDate', 1550.00, 'IN', 'Salary', 15, 70, null, 0, null, null, null, null, '$randomDate', '$randomDate')'''); + var time = randomDate.toLocal(); + DateTime salaryDateTime = DateTime(time.year, time.month, 27, time.hour, time.minute, time.second, time.millisecond, time.microsecond); + demoTransactions.add('''('$salaryDateTime', $fakeSalary, 'IN', 'Salary', 15, 70, null, 0, null, null, null, null, '$salaryDateTime', '$salaryDateTime')'''); } // add some recurring payment too diff --git a/lib/model/bank_account.dart b/lib/model/bank_account.dart index f18e6ff..c7e997e 100644 --- a/lib/model/bank_account.dart +++ b/lib/model/bank_account.dart @@ -1,3 +1,4 @@ +import 'package:sossoldi/model/transaction.dart'; import 'package:sqflite/sqflite.dart'; import '../database/sossoldi_database.dart'; @@ -10,7 +11,7 @@ class BankAccountFields extends BaseEntityFields { static String name = 'name'; static String symbol = 'symbol'; static String color = 'color'; - static String value = 'value'; + static String startingValue = 'startingValue'; static String active = 'active'; static String mainAccount = 'mainAccount'; static String createdAt = BaseEntityFields.getCreatedAt; @@ -21,7 +22,7 @@ class BankAccountFields extends BaseEntityFields { name, symbol, color, - value, + startingValue, active, mainAccount, BaseEntityFields.createdAt, @@ -33,7 +34,7 @@ class BankAccount extends BaseEntity { final String name; final String symbol; final int color; - final num value; + final num startingValue; final bool active; final bool mainAccount; @@ -42,7 +43,7 @@ class BankAccount extends BaseEntity { required this.name, required this.symbol, required this.color, - required this.value, + required this.startingValue, required this.mainAccount, required this.active, DateTime? createdAt, @@ -54,7 +55,7 @@ class BankAccount extends BaseEntity { String? name, String? symbol, int? color, - num? value, + num? startingValue, bool? active, bool? mainAccount, DateTime? createdAt, @@ -64,7 +65,7 @@ class BankAccount extends BaseEntity { name: name ?? this.name, symbol: symbol ?? this.symbol, color: color ?? this.color, - value: value ?? this.value, + startingValue: startingValue ?? this.startingValue, active: active ?? this.active, mainAccount: mainAccount ?? this.mainAccount, createdAt: createdAt ?? this.createdAt, @@ -75,7 +76,7 @@ class BankAccount extends BaseEntity { name: json[BankAccountFields.name] as String, symbol: json[BankAccountFields.symbol] as String, color: json[BankAccountFields.color] as int, - value: json[BankAccountFields.value] as num, + startingValue: json[BankAccountFields.startingValue] as num, active: json[BankAccountFields.active] == 1 ? true : false, mainAccount: json[BankAccountFields.mainAccount] == 1 ? true : false, createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), @@ -86,7 +87,7 @@ class BankAccount extends BaseEntity { BankAccountFields.name: name, BankAccountFields.symbol: symbol, BankAccountFields.color: color, - BankAccountFields.value: value, + BankAccountFields.startingValue: startingValue, BankAccountFields.active: active ? 1 : 0, BankAccountFields.mainAccount: mainAccount ? 1 : 0, BaseEntityFields.createdAt: @@ -185,6 +186,7 @@ class BankAccountMethods extends SossoldiDatabase { return await db.delete(bankAccountTable, where: '${BankAccountFields.id} = ?', whereArgs: [id]); } + Future deactivateById(int id) async { final db = await database; @@ -195,4 +197,43 @@ class BankAccountMethods extends SossoldiDatabase { whereArgs: [id], ); } + + Future getAccountSum(int? id) async { + final db = await database; + + //get account infos first + final result = await db.query(bankAccountTable, where:'${BankAccountFields.id} = $id', limit: 1); + final singleObject = result.isNotEmpty ? result[0] : null; + + if (singleObject != null) { + num balance = singleObject[BankAccountFields.startingValue] as num; + + // get all transactions of that account + final transactionsResult = await db.query(transactionTable, where:'${TransactionFields.idBankAccount} = $id OR ${TransactionFields.idBankAccountTransfer} = $id'); + + for (var transaction in transactionsResult) { + num amount = transaction[TransactionFields.amount] as num; + + switch (transaction[TransactionFields.type]) { + case ('IN'): + balance += amount; + break; + case ('OUT'): + balance -= amount; + break; + case ('TRSF'): + if (transaction[TransactionFields.idBankAccount] == id) { + balance -= amount; + } else { + balance += amount; + } + break; + } + } + + return balance; + } else { + return 0; + } + } } diff --git a/lib/pages/statistics_page.dart b/lib/pages/statistics_page.dart index 66e19c4..96e7ee4 100644 --- a/lib/pages/statistics_page.dart +++ b/lib/pages/statistics_page.dart @@ -148,7 +148,7 @@ class _StatsPageState extends ConsumerState with Functions { double max = 0; for(var i = 0; i < accounts.length; i++) { - if (max <= accounts[i].value){max = accounts[i].value.toDouble();} + if (max <= accounts[i].startingValue){max = accounts[i].startingValue.toDouble();} } return SizedBox( height: 50.0, @@ -170,7 +170,7 @@ class _StatsPageState extends ConsumerState with Functions { ), Expanded( child: Text( - "${account.value}€", + "${account.startingValue}€", textAlign: TextAlign.right, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: blue1), ), @@ -184,7 +184,7 @@ class _StatsPageState extends ConsumerState with Functions { child: Row( children: [ Container( - width: MediaQuery.of(context).size.width * 0.9 * (account.value.toDouble()/max), + width: MediaQuery.of(context).size.width * 0.9 * (account.startingValue.toDouble()/max), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(4.0), @@ -194,7 +194,7 @@ class _StatsPageState extends ConsumerState with Functions { ), ), Container( - width: MediaQuery.of(context).size.width * 0.9 *(1 - (account.value.toDouble()/max)), + width: MediaQuery.of(context).size.width * 0.9 *(1 - (account.startingValue.toDouble()/max)), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topRight: Radius.circular(4.0), diff --git a/lib/providers/accounts_provider.dart b/lib/providers/accounts_provider.dart index ff9cd12..75e33a5 100644 --- a/lib/providers/accounts_provider.dart +++ b/lib/providers/accounts_provider.dart @@ -35,7 +35,7 @@ class AsyncAccountsNotifier extends AsyncNotifier> { name: ref.read(accountNameProvider)!, symbol: ref.read(accountIconProvider), color: ref.read(accountColorProvider), - value: 0, + startingValue: 0, active: true, mainAccount: ref.read(accountMainSwitchProvider), ); diff --git a/test/model/bank_account_test.dart b/test/model/bank_account_test.dart index 3a5b799..454413c 100644 --- a/test/model/bank_account_test.dart +++ b/test/model/bank_account_test.dart @@ -10,7 +10,7 @@ void main() { name: "name", symbol: 'symbol', color: 0, - value: 100, + startingValue: 100, active: true, mainAccount: true, createdAt: DateTime.utc(2022), @@ -22,7 +22,7 @@ void main() { assert(bCopy.name == b.name); assert(bCopy.symbol == b.symbol); assert(bCopy.color == b.color); - assert(bCopy.value == bCopy.value); + assert(bCopy.startingValue == bCopy.startingValue); assert(bCopy.active == bCopy.active); assert(bCopy.mainAccount == bCopy.mainAccount); assert(bCopy.createdAt == b.createdAt); @@ -35,7 +35,7 @@ void main() { BankAccountFields.name: "name", BankAccountFields.symbol: "symbol", BankAccountFields.color: 0, - BankAccountFields.value: 100, + BankAccountFields.startingValue: 100, BaseEntityFields.createdAt: DateTime.utc(2022).toIso8601String(), BaseEntityFields.updatedAt: DateTime.utc(2022).toIso8601String(), }; @@ -46,7 +46,7 @@ void main() { assert(b.name == json[BankAccountFields.name]); assert(b.symbol == json[BankAccountFields.symbol]); assert(b.color == json[BankAccountFields.color]); - assert(b.value == json[BankAccountFields.value]); + assert(b.startingValue == json[BankAccountFields.startingValue]); assert(b.createdAt?.toUtc().toIso8601String() == json[BaseEntityFields.createdAt]); assert(b.updatedAt?.toUtc().toIso8601String() == @@ -59,7 +59,7 @@ void main() { name: "name", symbol: "symbol", color: 0, - value: 100, + startingValue: 100, active: true, mainAccount: false); @@ -69,7 +69,7 @@ void main() { assert(b.name == json[BankAccountFields.name]); assert(b.symbol == json[BankAccountFields.symbol]); assert(b.color == json[BankAccountFields.color]); - assert(b.value == json[BankAccountFields.value]); + assert(b.startingValue == json[BankAccountFields.startingValue]); assert((b.active ? 1 : 0) == json[BankAccountFields.active]); assert((b.mainAccount ? 1 : 0) == json[BankAccountFields.mainAccount]); }); diff --git a/test/widget/accounts_sum_test.dart b/test/widget/accounts_sum_test.dart index a60c2ac..5c5819d 100644 --- a/test/widget/accounts_sum_test.dart +++ b/test/widget/accounts_sum_test.dart @@ -1,10 +1,14 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:flutter/material.dart'; import "dart:math"; import 'package:sossoldi/custom_widgets/accounts_sum.dart'; import 'package:sossoldi/model/bank_account.dart'; void main() { + // Initialize the database factory with sqflite_common_ffi + databaseFactory = databaseFactoryFfi; + testWidgets('Properly Render Accounts Widget', (WidgetTester tester) async { var accountsList = ['N26','Fineco','Crypto.com','Mediolanum']; var amountsList = [3823.56,0.07,574.22,14549.01]; @@ -15,11 +19,11 @@ void main() { var randomValue = amountsList[random.nextInt(amountsList.length)]; BankAccount randomBankAccount = BankAccount( - id: 0, + id: 99, name: randomAccount, symbol: "account_balance", color: 0, - value: randomValue, + startingValue: randomValue, active: true, mainAccount: false, ); @@ -30,7 +34,21 @@ void main() { ), )); + FutureBuilder( + future: BankAccountMethods().getAccountSum(99), + builder: (context, snapshot) { + if (snapshot.hasError) { + // Show an error message if the future encounters an error + return Text('Error: ${snapshot.error}'); + } else { + final accountSum = snapshot.data ?? 0; + // TODO need to test total amount with some transactions too + expect(find.text("${accountSum.toStringAsFixed(2).replaceAll('.', ',')}€", findRichText: true), findsOneWidget); + return const Text('Ok!'); + } + } + ); + expect(find.text(randomAccount), findsOneWidget); - expect(find.text("${randomValue.toStringAsFixed(2).replaceAll('.', ',')}€", findRichText: true), findsOneWidget); }); } \ No newline at end of file