diff --git a/android/app/build.gradle b/android/app/build.gradle index 31ab904e..dba9b19b 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 33 + compileSdkVersion 34 ndkVersion flutter.ndkVersion compileOptions { diff --git a/lib/custom_widgets/transactions_list.dart b/lib/custom_widgets/transactions_list.dart index d248e944..26d58447 100644 --- a/lib/custom_widgets/transactions_list.dart +++ b/lib/custom_widgets/transactions_list.dart @@ -187,7 +187,10 @@ class TransactionRow extends ConsumerWidget with Functions { .read(transactionsProvider.notifier) .transactionUpdateState(transaction) .whenComplete( - () => Navigator.of(context).pushNamed("/add-page")); + () => Navigator.of(context).pushNamed( + "/add-page", + arguments: {'recurrencyEditingPermitted': !transaction.recurring} + )); }, borderRadius: BorderRadius.vertical( top: first ? const Radius.circular(8) : Radius.zero, @@ -224,6 +227,8 @@ class TransactionRow extends ConsumerWidget with Functions { const SizedBox(height: 11), Row( children: [ + if (transaction.recurring) // Check if the transaction is recurring + const Icon(Icons.repeat, color: Colors.blueAccent), // Add an icon for recurring transactions if (transaction.note != null) Text( transaction.note!, diff --git a/lib/database/sossoldi_database.dart b/lib/database/sossoldi_database.dart index 4b81ace6..740f8d3d 100644 --- a/lib/database/sossoldi_database.dart +++ b/lib/database/sossoldi_database.dart @@ -8,7 +8,7 @@ import '../model/bank_account.dart'; import '../model/budget.dart'; import '../model/category_transaction.dart'; import '../model/currency.dart'; -import '../model/recurring_transaction_amount.dart'; +import '../model/recurring_transaction.dart'; import '../model/transaction.dart'; class SossoldiDatabase { @@ -75,10 +75,7 @@ class SossoldiDatabase { `${TransactionFields.idBankAccount}` $integerNotNull, `${TransactionFields.idBankAccountTransfer}` $integer, `${TransactionFields.recurring}` $integerNotNull CHECK (${TransactionFields.recurring} IN (0, 1)), - `${TransactionFields.recurrencyType}` $text, - `${TransactionFields.recurrencyPayDay}` $integer, - `${TransactionFields.recurrencyFrom}` $text, - `${TransactionFields.recurrencyTo}` $text, + `${TransactionFields.idRecurringTransaction}` $integer, `${TransactionFields.createdAt}` $textNotNull, `${TransactionFields.updatedAt}` $textNotNull ) @@ -86,14 +83,18 @@ class SossoldiDatabase { // Recurring Transactions Amount Table await database.execute(''' - CREATE TABLE `$recurringTransactionAmountTable`( - `${RecurringTransactionAmountFields.id}` $integerPrimaryKeyAutoincrement, - `${RecurringTransactionAmountFields.from}` $textNotNull, - `${RecurringTransactionAmountFields.to}` $textNotNull, - `${RecurringTransactionAmountFields.amount}` $realNotNull, - `${RecurringTransactionAmountFields.idTransaction}` $integerNotNull, - `${RecurringTransactionAmountFields.createdAt}` $textNotNull, - `${RecurringTransactionAmountFields.updatedAt}` $textNotNull + 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 ) '''); @@ -154,7 +155,8 @@ class SossoldiDatabase { (12, "Furniture", "home", 2, '', 11, '${DateTime.now()}', '${DateTime.now()}'), (13, "Shopping", "shopping_cart", 3, '', null, '${DateTime.now()}', '${DateTime.now()}'), (14, "Leisure", "subscriptions", 4, '', null, '${DateTime.now()}', '${DateTime.now()}'), - (15, "Salary", "work", 5, '', null, '${DateTime.now()}', '${DateTime.now()}'); + (15, "Salary", "work", 5, '', null, '${DateTime.now()}', '${DateTime.now()}'), + (16, "Transports", "directions_car_rounded", 6, '', null, '${DateTime.now()}', '${DateTime.now()}'); '''); // Add currencies @@ -169,15 +171,23 @@ class SossoldiDatabase { // Add fake budgets await _database?.execute(''' INSERT INTO budget(idCategory, name, amountLimit, active, createdAt, updatedAt) VALUES - (13, "Grocery", 400.00, 1, '${DateTime.now()}', '${DateTime.now()}'), + (13, "Grocery", 900.00, 1, '${DateTime.now()}', '${DateTime.now()}'), (11, "Home", 123.45, 0, '${DateTime.now()}', '${DateTime.now()}'); '''); + // Add fake recurring transactions + await _database?.execute(''' + INSERT INTO recurringTransaction(fromDate, toDate, amount, note, recurrency, idCategory, idBankAccount, createdAt, updatedAt) VALUES + ("2024-02-23", null, 10.99, "404 Books", "MONTHLY", 14, 70, '${DateTime.now()}', '${DateTime.now()}'), + ("2023-12-13", null, 4.97, "ETF Consultant Parcel", "DAILY", 14, 70, '${DateTime.now()}', '${DateTime.now()}'), + ("2023-02-11", "2028-02-11", 1193.40, "Car Loan", "QUARTERLY", 16, 72, '${DateTime.now()}', '${DateTime.now()}'); + '''); + // Add fake transactions // First initialize some config stuff final rnd = Random(); var accounts = [70,71,72]; - var outNotes = ['Grocery', 'Tolls', 'Toys', 'ETF Consultant Parcel', 'Concert', 'Clothing', 'Pizza', 'Drugs', 'Laundry', 'Taxes', 'Health insurance', 'Furniture', 'Car Fuel', 'Train', 'Amazon', 'Delivery', 'CHEK dividends', 'Babysitter', 'sono.pove.ro Fees', 'Quingentole trip']; + var outNotes = ['Grocery', 'Tolls', 'Toys', 'Boardgames', '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]; double maxAmountOfSingleTransaction = 250.00; int dateInPastMaxRange = (countOfGeneratedTransaction / 90 ).round() * 30; // we want simulate about 90 transactions per month @@ -185,7 +195,7 @@ class SossoldiDatabase { DateTime now = DateTime.now(); // start building mega-query - const insertDemoTransactionsQuery = '''INSERT INTO `transaction` (date, amount, type, note, idCategory, idBankAccount, idBankAccountTransfer, recurring, recurrencyType, recurrencyPayDay, recurrencyFrom, recurrencyTo, createdAt, updatedAt) VALUES '''; + const insertDemoTransactionsQuery = '''INSERT INTO `transaction` (date, amount, type, note, idCategory, idBankAccount, idBankAccountTransfer, recurring, idRecurringTransaction, createdAt, updatedAt) VALUES '''; // init a List with transaction values final List demoTransactions = []; @@ -224,7 +234,7 @@ class SossoldiDatabase { } // put generated transaction in our list - demoTransactions.add('''('$randomDate', ${randomAmount.toStringAsFixed(2)}, '$randomType', '$randomNote', $randomCategory, $randomAccount, $idBankAccountTransfer, 0, null, null, null, null, '$randomDate', '$randomDate')'''); + demoTransactions.add('''('$randomDate', ${randomAmount.toStringAsFixed(2)}, '$randomType', '$randomNote', $randomCategory, $randomAccount, $idBankAccountTransfer, 0, null, '$randomDate', '$randomDate')'''); } // add salary every month @@ -232,24 +242,40 @@ class SossoldiDatabase { DateTime randomDate = now.subtract(Duration(days: 30*i)); 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')'''); + demoTransactions.add('''('$salaryDateTime', $fakeSalary, 'IN', 'Salary', 15, 70, null, 0, null, '$salaryDateTime', '$salaryDateTime')'''); } - // add some recurring payment too - demoTransactions.add('''(null, 7.99, 'OUT', 'Netflix', 14, 71, null, 1, 'monthly', 19, '2022-11-14', null, '2022-11-14 03:33:36.048611', '2022-11-14 03:33:36.048611')'''); - demoTransactions.add('''(null, 292.39, 'OUT', 'Car Loan', 13, 70, null, 1, 'monthly', 27, '2019-10-03', '2024-10-02', '2022-10-04 03:33:36.048611', '2022-10-04 03:33:36.048611')'''); - // finalize query and write! await _database?.execute("$insertDemoTransactionsQuery ${demoTransactions.join(",")};"); } + Future resetDatabase() async { + // delete database + try{ + await _database?.transaction((txn) async { + var batch = txn.batch(); + // drop tables + batch.execute('DROP TABLE IF EXISTS $bankAccountTable'); + batch.execute('DROP TABLE IF EXISTS `$transactionTable`'); + batch.execute('DROP TABLE IF EXISTS $recurringTransactionTable'); + batch.execute('DROP TABLE IF EXISTS $categoryTransactionTable'); + batch.execute('DROP TABLE IF EXISTS $budgetTable'); + batch.execute('DROP TABLE IF EXISTS $currencyTable'); + await batch.commit(); + }); + } catch(error){ + throw Exception('DbBase.resetDatabase: $error'); + } + await _createDB(_database!, 1); + } + Future clearDatabase() async { try{ await _database?.transaction((txn) async { var batch = txn.batch(); batch.delete(bankAccountTable); batch.delete(transactionTable); - batch.delete(recurringTransactionAmountTable); + batch.delete(recurringTransactionTable); batch.delete(categoryTransactionTable); batch.delete(budgetTable); batch.delete(currencyTable); @@ -268,7 +294,7 @@ class SossoldiDatabase { // WARNING: FOR DEV/TEST PURPOSES ONLY!! Future deleteDatabase() async { final databasePath = await getDatabasesPath(); - final path = join(databasePath, 'sossoldi.db'); + final path = join(databasePath, dbName); databaseFactory.deleteDatabase(path); } } diff --git a/lib/main.dart b/lib/main.dart index 6a05b6db..049ea769 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,8 +5,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:workmanager/workmanager.dart'; -import 'package:sossoldi/utils/worker_manager.dart'; +import 'model/recurring_transaction.dart'; +import 'utils/worker_manager.dart'; import 'pages/notifications/notifications_service.dart'; import 'providers/theme_provider.dart'; import 'routes.dart'; @@ -16,17 +17,29 @@ bool? _isFirstLogin = true; void main() async { WidgetsFlutterBinding.ensureInitialized(); + if(Platform.isAndroid){ requestNotificationPermissions(); initializeNotifications(); Workmanager().initialize(callbackDispatcher); } + SharedPreferences preferences = await SharedPreferences.getInstance(); bool? getPref = preferences.getBool('is_first_login'); getPref == null ? await preferences.setBool('is_first_login', false) : null; _isFirstLogin = getPref; + // perform recurring transactions checks + DateTime? lastCheckGetPref = preferences.getString('last_recurring_transactions_check') != null ? DateTime.parse(preferences.getString('last_recurring_transactions_check')!) : null; + DateTime? lastRecurringTransactionsCheck = lastCheckGetPref; + + if(lastRecurringTransactionsCheck == null || DateTime.now().difference(lastRecurringTransactionsCheck).inDays >= 1){ + RecurringTransactionMethods().checkRecurringTransactions(); + // update last recurring transactions runtime + await preferences.setString('last_recurring_transactions_check', DateTime.now().toIso8601String()); + } + initializeDateFormatting('it_IT', null).then((_) => runApp(const ProviderScope(child: Launcher()))); } diff --git a/lib/model/recurring_transaction.dart b/lib/model/recurring_transaction.dart new file mode 100644 index 00000000..39cb862c --- /dev/null +++ b/lib/model/recurring_transaction.dart @@ -0,0 +1,364 @@ +import '../database/sossoldi_database.dart'; +import 'transaction.dart'; +import 'base_entity.dart'; + +const String recurringTransactionTable = 'recurringTransaction'; + +class RecurringTransactionFields extends BaseEntityFields { + static String id = BaseEntityFields.getId; + static String fromDate = 'fromDate'; + static String toDate = 'toDate'; + static String amount = 'amount'; + static String note = 'note'; + static String recurrency = 'recurrency'; + static String idCategory = 'idCategory'; + static String idBankAccount = 'idBankAccount'; + static String lastInsertion = 'lastInsertion'; + static String createdAt = BaseEntityFields.getCreatedAt; + static String updatedAt = BaseEntityFields.getUpdatedAt; + + static final List allFields = [ + BaseEntityFields.id, + fromDate, + toDate, + amount, + note, + recurrency, + idCategory, + idBankAccount, + lastInsertion, + BaseEntityFields.createdAt, + BaseEntityFields.updatedAt + ]; +} + +Map recurrenciesMap = { + 'DAILY': { + 'label': 'Daily', + 'entity': 'days', + 'amount': 1 + }, + 'WEEKLY': { + 'label': 'Weekly', + 'entity': 'days', + 'amount': 7 + }, + 'MONTHLY': { + 'label': 'Monthly', + 'entity': 'months', + 'amount': 1 + }, + 'BIMONTHLY': { + 'label': 'Bimonthly', + 'entity': 'months', + 'amount': 2 + }, + 'QUARTERLY': { + 'label': 'Quarterly', + 'entity': 'months', + 'amount': 3 + }, + 'SEMESTER': { + 'label': 'Half Yearly', + 'entity': 'months', + 'amount': 6 + }, + 'YEARLY': { + 'label': 'Yearly', + 'entity': 'months', + 'amount': 12 + }, +}; + +class RecurringTransaction extends BaseEntity { + final DateTime fromDate; + final DateTime? toDate; + final num amount; + final String note; + final String recurrency; + final int idCategory; + final int idBankAccount; + final DateTime? lastInsertion; + + const RecurringTransaction( + {super.id, + required this.fromDate, + this.toDate, + required this.amount, + required this.note, + required this.recurrency, + required this.idCategory, + required this.idBankAccount, + this.lastInsertion, + super.createdAt, + super.updatedAt}); + + RecurringTransaction copy( + {int? id, + DateTime? fromDate, + DateTime? toDate, + num? amount, + String? note, + String? recurrency, + int? idCategory, + int? idBankAccount, + DateTime? lastInsertion, + DateTime? createdAt, + DateTime? updatedAt}) => + RecurringTransaction( + id: id ?? this.id, + fromDate: fromDate ?? this.fromDate, + toDate: toDate ?? this.toDate, + amount: amount ?? this.amount, + note: note ?? this.note, + recurrency: recurrency ?? this.recurrency, + idCategory: idCategory ?? this.idCategory, + idBankAccount: idBankAccount ?? this.idBankAccount, + lastInsertion: lastInsertion ?? this.lastInsertion, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt); + + static RecurringTransaction fromJson(Map json) => + RecurringTransaction( + id: json[BaseEntityFields.id] as int?, + fromDate: DateTime.parse( + json[RecurringTransactionFields.fromDate] as String), + toDate: json[RecurringTransactionFields.toDate] != null + ? DateTime.parse(json[RecurringTransactionFields.toDate] as String) + : null, + amount: json[RecurringTransactionFields.amount] as num, + note: json[RecurringTransactionFields.note] as String, + recurrency: json[RecurringTransactionFields.recurrency] as String, + idCategory: json[RecurringTransactionFields.idCategory] as int, + idBankAccount: json[RecurringTransactionFields.idBankAccount] as int, + lastInsertion: json[RecurringTransactionFields.lastInsertion] != null + ? DateTime.parse(json[RecurringTransactionFields.lastInsertion] as String) + : null, + createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), + updatedAt: + DateTime.parse(json[BaseEntityFields.updatedAt] as String)); + + Map toJson() => { + BaseEntityFields.id: id, + RecurringTransactionFields.fromDate: fromDate.toIso8601String(), + RecurringTransactionFields.toDate: toDate?.toIso8601String(), + RecurringTransactionFields.amount: amount, + RecurringTransactionFields.note: note, + RecurringTransactionFields.recurrency: recurrency, + RecurringTransactionFields.idCategory: idCategory, + RecurringTransactionFields.idBankAccount: idBankAccount, + RecurringTransactionFields.lastInsertion: lastInsertion?.toIso8601String(), + BaseEntityFields.createdAt: createdAt?.toIso8601String(), + BaseEntityFields.updatedAt: updatedAt?.toIso8601String(), + }; +} + +class RecurringTransactionMethods extends SossoldiDatabase { + Future insert(RecurringTransaction item) async { + try{ + final db = await database; + final id = await db.insert(recurringTransactionTable, item.toJson()); + return item.copy(id: id); + } catch (e){ + print(e); + } + return null; + } + + Future selectById(int id) async { + final db = await database; + + final maps = await db.query( + recurringTransactionTable, + columns: RecurringTransactionFields.allFields, + where: '${RecurringTransactionFields.id} = ?', + whereArgs: [id], + ); + + if (maps.isNotEmpty) { + return RecurringTransaction.fromJson(maps.first); + } else { + throw Exception('ID $id not found'); + } + } + + Future> selectAll() async { + final db = await database; + + final orderBy = '${RecurringTransactionFields.createdAt} ASC'; + + final result = await db.query(recurringTransactionTable, orderBy: orderBy); + + return result.map((json) => RecurringTransaction.fromJson(json)).toList(); + } + + Future> selectAllActive() async { + final db = await database; + + final orderBy = '${RecurringTransactionFields.createdAt} ASC'; + + final result = await db.query( + recurringTransactionTable, + orderBy: orderBy, + where: '${RecurringTransactionFields.toDate} IS NULL OR ${RecurringTransactionFields.toDate} > ?', + whereArgs: [DateTime.now().toIso8601String()], + ); + + return result.map((json) => RecurringTransaction.fromJson(json)).toList(); + } + + Future updateItem(RecurringTransaction item) async { + final db = await database; + + // You can use `rawUpdate` to write the query in SQL + return db.update( + recurringTransactionTable, + item.toJson(), + where: '${RecurringTransactionFields.id} = ?', + whereArgs: [item.id], + ); + } + + Future deleteById(int id) async { + final db = await database; + + return await db.delete(recurringTransactionTable, + where: '${RecurringTransactionFields.id} = ?', + whereArgs: [id]); + } + + Future checkRecurringTransactions() async { + // get all recurring transactions active + final transactions = await selectAllActive(); + + if (transactions.isEmpty) { + return; + } + + for (var transaction in transactions) { + DateTime lastTransactionDate; + + try { + lastTransactionDate = await _getLastRecurringTransactionInsertion(transaction.id ?? 0); + } catch (e) { + lastTransactionDate = transaction.fromDate; + } + + String entity = recurrenciesMap[transaction.recurrency]?['entity'] ?? 'UNMAPPED'; + int entityAmt = recurrenciesMap[transaction.recurrency]?['amount'] ?? 0; + + try { + if (entityAmt == 0) { + throw Exception('No amount provided for entity "$entity"'); + } + + populateRecurringTransaction(entity, lastTransactionDate, transaction, entityAmt); + + } catch (e) { + // TODO show an error to the user? + } + } + } + + Future _getLastRecurringTransactionInsertion(int tid) async { + if (tid == 0) { + throw Exception('No transaction ID provided'); + } + + final db = await database; + + final result = await db.query( + transactionTable, + orderBy: '${TransactionFields.date} DESC', + where: '${TransactionFields.idRecurringTransaction} = ?', + whereArgs: [tid], + limit: 1, + ); + + if (result.isEmpty) { + throw Exception('No transaction found for ID $tid'); + } + + return Transaction.fromJson(result.first).date; + } + + void populateRecurringTransaction(String scope, DateTime lastTransactionDate, RecurringTransaction transaction, int amount) { + + if (amount == 0) { + throw Exception('No amount provided for entity "$scope"'); + } + + DateTime now = DateTime.now(); + + // create a list to store the months + List transactions2Add = []; + + // calculate the number of periods between the current date and the last transaction insertion + int periods; + + switch (scope) { + case 'days': + periods = (now.difference(lastTransactionDate).inDays/amount).floor(); + break; + case 'months': + periods = (((now.year - lastTransactionDate.year) * 12 + now.month - lastTransactionDate.month)/amount).floor(); + break; + default: + throw Exception('No scope provided'); + } + + // for each period passed, insert a new transaction + for (int i = 0; i < periods; i++) { + switch (scope) { + case 'days': + lastTransactionDate = DateTime(lastTransactionDate.year, lastTransactionDate.month, lastTransactionDate.day + amount); + break; + case 'months': + // get the last day of the next period + int lastDayOfNextPeriod = DateTime(lastTransactionDate.year, (lastTransactionDate.month + amount + 1), 0).day; + int dayOfInsertion = transaction.fromDate.day; + + // if the next period's month has fewer days than the day of the last transaction insertion, adjust the day + if (transaction.fromDate.day > lastDayOfNextPeriod) { + dayOfInsertion = lastDayOfNextPeriod; + } + + lastTransactionDate = DateTime(lastTransactionDate.year, lastTransactionDate.month + amount, dayOfInsertion); + + break; + default: + //nothing to do + return; + } + + if ((transaction.toDate?.isAfter(lastTransactionDate) ?? true) && lastTransactionDate.isBefore(DateTime.now())) { + transactions2Add.add(lastTransactionDate); + } + + } + + for (var tr in transactions2Add) { + // insert a new transaction + + Transaction addTr = Transaction( + date: tr, + amount: transaction.amount, + type: TransactionType.expense, + note: transaction.note, + idCategory: transaction.idCategory, + idBankAccount: transaction.idBankAccount, + recurring: true, + idRecurringTransaction: transaction.id, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); + + TransactionMethods().insert(addTr); + } + + // update the last insertion date + updateItem(transaction.copy(lastInsertion: now)); + + } + +} \ No newline at end of file diff --git a/lib/model/recurring_transaction_amount.dart b/lib/model/recurring_transaction_amount.dart deleted file mode 100644 index 8d4859f4..00000000 --- a/lib/model/recurring_transaction_amount.dart +++ /dev/null @@ -1,144 +0,0 @@ -import '../database/sossoldi_database.dart'; -import 'base_entity.dart'; - -const String recurringTransactionAmountTable = 'recurringTransactionAmount'; - -class RecurringTransactionAmountFields extends BaseEntityFields { - static String id = BaseEntityFields.getId; - static String from = 'from'; - static String to = 'to'; - static String amount = 'amount'; - static String idTransaction = 'idTransaction'; // FK - static String createdAt = BaseEntityFields.getCreatedAt; - static String updatedAt = BaseEntityFields.getUpdatedAt; - - static final List allFields = [ - BaseEntityFields.id, - from, - to, - amount, - idTransaction, - BaseEntityFields.createdAt, - BaseEntityFields.updatedAt - ]; -} - -class RecurringTransactionAmount extends BaseEntity { - final DateTime from; - final DateTime to; - final num amount; - final int? idTransaction; - - const RecurringTransactionAmount( - {int? id, - required this.from, - required this.to, - required this.amount, - required this.idTransaction, - DateTime? createdAt, - DateTime? updatedAt}) - : super(id: id, createdAt: createdAt, updatedAt: updatedAt); - - RecurringTransactionAmount copy( - {int? id, - DateTime? from, - DateTime? to, - num? amount, - int? idTransaction, - DateTime? createdAt, - DateTime? updatedAt}) => - RecurringTransactionAmount( - id: id ?? this.id, - from: from ?? this.from, - to: to ?? this.to, - amount: amount ?? this.amount, - idTransaction: - idTransaction ?? this.idTransaction, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt); - - static RecurringTransactionAmount fromJson(Map json) => - RecurringTransactionAmount( - id: json[BaseEntityFields.id] as int?, - from: DateTime.parse( - json[RecurringTransactionAmountFields.from] as String), - to: DateTime.parse( - json[RecurringTransactionAmountFields.to] as String), - amount: json[RecurringTransactionAmountFields.amount] as num, - idTransaction: - json[RecurringTransactionAmountFields.idTransaction] - as int, - createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), - updatedAt: - DateTime.parse(json[BaseEntityFields.updatedAt] as String)); - - Map toJson() => { - BaseEntityFields.id: id, - RecurringTransactionAmountFields.from: from.toIso8601String(), - RecurringTransactionAmountFields.to: to.toIso8601String(), - RecurringTransactionAmountFields.amount: amount, - RecurringTransactionAmountFields.idTransaction: - idTransaction, - BaseEntityFields.createdAt: createdAt?.toIso8601String(), - BaseEntityFields.updatedAt: updatedAt?.toIso8601String(), - }; -} - -class RecurringTransactionMethods extends SossoldiDatabase { - Future insert(RecurringTransactionAmount item) async { - final db = await database; - final id = await db.insert(recurringTransactionAmountTable, item.toJson()); - return item.copy(id: id); - } - - - Future selectById(int id) async { - final db = await database; - - final maps = await db.query( - recurringTransactionAmountTable, - columns: RecurringTransactionAmountFields.allFields, - where: '${RecurringTransactionAmountFields.id} = ?', - whereArgs: [id], - ); - - if (maps.isNotEmpty) { - return RecurringTransactionAmount.fromJson(maps.first); - } else { - throw Exception('ID $id not found'); - } - } - - Future> selectAll() async { - final db = await database; - - final orderByASC = '${RecurringTransactionAmountFields.createdAt} ASC'; - - final result = await db.query(recurringTransactionAmountTable, orderBy: orderByASC); - - return result.map((json) => RecurringTransactionAmount.fromJson(json)).toList(); - } - - Future updateItem(RecurringTransactionAmount item) async { - final db = await database; - - // You can use `rawUpdate` to write the query in SQL - return db.update( - recurringTransactionAmountTable, - item.toJson(), - where: - '${RecurringTransactionAmountFields.id} = ?', - whereArgs: [item.id], - ); - } - - Future deleteById(int id) async { - final db = await database; - - return await db.delete(recurringTransactionAmountTable, - where: - '${RecurringTransactionAmountFields.id} = ?', - whereArgs: [id]); - } - -} \ No newline at end of file diff --git a/lib/model/transaction.dart b/lib/model/transaction.dart index 73492e37..a5436c36 100644 --- a/lib/model/transaction.dart +++ b/lib/model/transaction.dart @@ -20,10 +20,7 @@ class TransactionFields extends BaseEntityFields { static String idBankAccountTransfer = 'idBankAccountTransfer'; static String bankAccountTransferName = 'bankAccountTransferName'; static String recurring = 'recurring'; - static String recurrencyType = 'recurrencyType'; - static String recurrencyPayDay = 'recurrencyPayDay'; - static String recurrencyFrom = 'recurrencyFrom'; - static String recurrencyTo = 'recurrencyTo'; + static String idRecurringTransaction = 'idRecurringTransaction'; static String createdAt = BaseEntityFields.getCreatedAt; static String updatedAt = BaseEntityFields.getUpdatedAt; @@ -37,10 +34,7 @@ class TransactionFields extends BaseEntityFields { idBankAccount, idBankAccountTransfer, recurring, - recurrencyType, - recurrencyPayDay, - recurrencyFrom, - recurrencyTo, + idRecurringTransaction, BaseEntityFields.createdAt, BaseEntityFields.updatedAt ]; @@ -48,23 +42,40 @@ class TransactionFields extends BaseEntityFields { enum TransactionType { income, expense, transfer } -enum Recurrence { daily, weekly, monthly, bimonthly, quarterly, semester, annual } - Map typeMap = { "IN": TransactionType.income, "OUT": TransactionType.expense, "TRSF": TransactionType.transfer, }; -Map recurrenceMap = { - Recurrence.daily: "Daily", - Recurrence.weekly: "Weekly", - Recurrence.monthly: "Monthly", - Recurrence.bimonthly: "Bimonthly", - Recurrence.quarterly: "Quarterly", - Recurrence.semester: "Semester", - Recurrence.annual: "Annual", + +enum Recurrence { daily, weekly, monthly, bimonthly, quarterly, semester, annual } + +class RecurrenceData { + final Recurrence recurrence; + final String label; + final int days; + + RecurrenceData({ + required this.recurrence, + required this.label, + required this.days, + }); +} + +Map recurrenceMap = { + Recurrence.daily: RecurrenceData(recurrence: Recurrence.daily, label: "Daily", days: 1), + Recurrence.weekly: RecurrenceData(recurrence: Recurrence.weekly, label: "Weekly", days: 7), + Recurrence.monthly: RecurrenceData(recurrence: Recurrence.monthly, label: "Monthly", days: 30), + Recurrence.bimonthly: RecurrenceData(recurrence: Recurrence.bimonthly, label: "Bimonthly", days: 60), + Recurrence.quarterly: RecurrenceData(recurrence: Recurrence.quarterly, label: "Quarterly", days: 90), + Recurrence.semester: RecurrenceData(recurrence: Recurrence.semester, label: "Semester", days: 180), + Recurrence.annual: RecurrenceData(recurrence: Recurrence.annual, label: "Annual", days: 365), }; +Recurrence parseRecurrence(String s) { + return recurrenceMap.entries.firstWhere((entry) => entry.value.label.toLowerCase() == s.toLowerCase()).key; +} + class Transaction extends BaseEntity { final DateTime date; final num amount; @@ -79,10 +90,7 @@ class Transaction extends BaseEntity { final int? idBankAccountTransfer; final String? bankAccountTransferName; final bool recurring; - final String? recurrencyType; - final int? recurrencyPayDay; - final DateTime? recurrencyFrom; - final DateTime? recurrencyTo; + final int? idRecurringTransaction; const Transaction( {super.id, @@ -99,10 +107,7 @@ class Transaction extends BaseEntity { this.idBankAccountTransfer, this.bankAccountTransferName, required this.recurring, - this.recurrencyType, - this.recurrencyPayDay, - this.recurrencyFrom, - this.recurrencyTo, + this.idRecurringTransaction, super.createdAt, super.updatedAt}); @@ -116,10 +121,7 @@ class Transaction extends BaseEntity { int? idBankAccount, int? idBankAccountTransfer, bool? recurring, - String? recurrencyType, - int? recurrencyPayDay, - DateTime? recurrencyFrom, - DateTime? recurrencyTo, + int? idRecurringTransaction, DateTime? createdAt, DateTime? updatedAt}) => Transaction( @@ -132,10 +134,7 @@ class Transaction extends BaseEntity { idBankAccount: idBankAccount ?? this.idBankAccount, idBankAccountTransfer: idBankAccountTransfer ?? this.idBankAccountTransfer, recurring: recurring ?? this.recurring, - recurrencyType: recurrencyType ?? this.recurrencyType, - recurrencyPayDay: recurrencyPayDay ?? this.recurrencyPayDay, - recurrencyFrom: recurrencyFrom ?? this.recurrencyFrom, - recurrencyTo: recurrencyTo ?? this.recurrencyTo, + idRecurringTransaction: idRecurringTransaction ?? this.idRecurringTransaction, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt); @@ -154,14 +153,7 @@ class Transaction extends BaseEntity { idBankAccountTransfer: json[TransactionFields.idBankAccountTransfer] as int?, bankAccountTransferName: json[TransactionFields.bankAccountTransferName] as String?, recurring: json[TransactionFields.recurring] == 1 ? true : false, - recurrencyType: json[TransactionFields.recurrencyType] as String?, - recurrencyPayDay: json[TransactionFields.recurrencyPayDay] as int?, - recurrencyFrom: json[TransactionFields.recurrencyFrom] != null - ? DateTime.parse(TransactionFields.recurrencyFrom) - : null, - recurrencyTo: json[TransactionFields.recurrencyTo] != null - ? DateTime.parse(TransactionFields.recurrencyTo) - : null, + idRecurringTransaction: json[TransactionFields.idRecurringTransaction] as int?, createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), updatedAt: DateTime.parse(json[BaseEntityFields.updatedAt] as String)); @@ -175,10 +167,7 @@ class Transaction extends BaseEntity { TransactionFields.idBankAccount: idBankAccount, TransactionFields.idBankAccountTransfer: idBankAccountTransfer, TransactionFields.recurring: recurring ? 1 : 0, - TransactionFields.recurrencyType: recurrencyType, - TransactionFields.recurrencyPayDay: recurrencyPayDay, - TransactionFields.recurrencyFrom: recurrencyFrom, - TransactionFields.recurrencyTo: recurrencyTo, + TransactionFields.idRecurringTransaction: idRecurringTransaction, BaseEntityFields.createdAt: update ? createdAt?.toIso8601String() : DateTime.now().toIso8601String(), BaseEntityFields.updatedAt: DateTime.now().toIso8601String(), @@ -265,6 +254,14 @@ class TransactionMethods extends SossoldiDatabase { return result.map((json) => Transaction.fromJson(json)).toList(); } + Future> getRecurrenceTransactionsById({int? id}) async { + final db = await database; + + final result = await db.rawQuery('SELECT * FROM "$transactionTable" as t WHERE t.${TransactionFields.idRecurringTransaction} = $id ORDER BY ${TransactionFields.date} DESC'); + + return result.map((json) => Transaction.fromJson(json)).toList(); + } + Future> getAllLabels({String? label}) async { final db = await database; diff --git a/lib/pages/add_page/add_page.dart b/lib/pages/add_page/add_page.dart index def95d1b..ca885cb1 100644 --- a/lib/pages/add_page/add_page.dart +++ b/lib/pages/add_page/add_page.dart @@ -16,7 +16,9 @@ import 'widgets/label_list_tile.dart'; import 'widgets/recurrence_list_tile.dart'; class AddPage extends ConsumerStatefulWidget { - const AddPage({super.key}); + final bool recurrencyEditingPermitted; + + const AddPage({super.key, this.recurrencyEditingPermitted = true}); @override ConsumerState createState() => _AddPageState(); @@ -25,6 +27,7 @@ class AddPage extends ConsumerStatefulWidget { class _AddPageState extends ConsumerState with Functions { final TextEditingController amountController = TextEditingController(); final TextEditingController noteController = TextEditingController(); + bool? recurrencyEditingPermittedFromRoute; @override void initState() { @@ -38,6 +41,18 @@ class _AddPageState extends ConsumerState with Functions { super.initState(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Check if arguments are being passed + final args = ModalRoute.of(context)?.settings.arguments; + + if (recurrencyEditingPermittedFromRoute == null) { + final argsMap = args as Map?; + recurrencyEditingPermittedFromRoute = argsMap?['recurrencyEditingPermitted'] ?? widget.recurrencyEditingPermitted; + } + } + @override void dispose() { amountController.dispose(); @@ -81,13 +96,31 @@ class _AddPageState extends ConsumerState with Functions { final cleanAmount = getCleanAmountString(); - // Check that an amount it's inserted + // Check that an amount has been provided if (cleanAmount != '') { if (selectedTransaction != null) { - ref - .read(transactionsProvider.notifier) - .updateTransaction(currencyToNum(cleanAmount), noteController.text) - .whenComplete(() => Navigator.of(context).pop()); + // if the original transaction is not recurrent, but user sets a recurrency, add the corrispondent record + // and edit the original transaction + if(ref.read(selectedRecurringPayProvider) && !selectedTransaction.recurring) { + ref + .read(transactionsProvider.notifier) + .addRecurringTransaction(currencyToNum(cleanAmount), noteController.text) + .then((value) { + if (value != null) { + ref + .read(transactionsProvider.notifier) + .updateTransaction(currencyToNum(cleanAmount), noteController.text, value.id) + .whenComplete(() => Navigator.of(context).pop()); + } + }); + } else { + ref + .read(transactionsProvider.notifier) + .updateTransaction(currencyToNum(cleanAmount), noteController.text, selectedTransaction.idRecurringTransaction) + .whenComplete(() => Navigator.of(context).pop()); + } + + } else { if (selectedType == TransactionType.transfer) { if (ref.read(bankAccountTransferProvider) != null) { @@ -99,10 +132,17 @@ class _AddPageState extends ConsumerState with Functions { } else { // It's an income or an expense if (ref.read(categoryProvider) != null) { - ref + if(ref.read(selectedRecurringPayProvider)) { + ref + .read(transactionsProvider.notifier) + .addRecurringTransaction(currencyToNum(cleanAmount), noteController.text) + .whenComplete(() => Navigator.of(context).pop()); + } else { + ref .read(transactionsProvider.notifier) .addTransaction(currencyToNum(cleanAmount), noteController.text) .whenComplete(() => Navigator.of(context).pop()); + } } } } @@ -272,7 +312,10 @@ class _AddPageState extends ConsumerState with Functions { }, ), if (selectedType == TransactionType.expense) ...[ - const RecurrenceListTile(), + RecurrenceListTile( + recurrencyEditingPermitted: widget.recurrencyEditingPermitted, + selectedTransaction: ref.read(selectedTransactionUpdateProvider) + ) ], ], ), diff --git a/lib/pages/add_page/widgets/amount_section.dart b/lib/pages/add_page/widgets/amount_section.dart index 273d4b33..1a8fa194 100644 --- a/lib/pages/add_page/widgets/amount_section.dart +++ b/lib/pages/add_page/widgets/amount_section.dart @@ -7,6 +7,7 @@ import "../../../constants/style.dart"; import '../../../model/transaction.dart'; import '../../../providers/currency_provider.dart'; import '../../../providers/transactions_provider.dart'; +import '../../../pages/add_page/widgets/amount_widget.dart'; import 'account_selector.dart'; import 'type_tab.dart'; @@ -269,41 +270,7 @@ class _AmountSectionState extends ConsumerState with Functions { ), ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - child: TextField( - controller: widget.amountController, - decoration: InputDecoration( - hintText: "0", - border: InputBorder.none, - prefixText: ' ', // set to center the amount - suffixText: currencyState.selectedCurrency.symbol, - suffixStyle: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(color: typeToColor(selectedType)), - ), - keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}')), - ], - // inputFormatters: [DecimalTextInputFormatter(decimalDigits: 2)], - autofocus: false, - textAlign: TextAlign.center, - cursorColor: grey1, - style: TextStyle( - color: typeToColor(selectedType), - fontSize: 58, - fontWeight: FontWeight.bold, - ), - onTapOutside: (_){ - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } - }, - ), - ), + AmountWidget(widget.amountController), ], ), ); diff --git a/lib/pages/add_page/widgets/amount_widget.dart b/lib/pages/add_page/widgets/amount_widget.dart new file mode 100644 index 00000000..a7d2d6f5 --- /dev/null +++ b/lib/pages/add_page/widgets/amount_widget.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import '../../../constants/functions.dart'; +import "../../../constants/style.dart"; +import '../../../providers/currency_provider.dart'; +import '../../../providers/transactions_provider.dart'; + +class AmountWidget extends ConsumerStatefulWidget { + const AmountWidget( + this.amountController, { + super.key, + }); + + final TextEditingController amountController; + + @override + ConsumerState createState() => _AmountWidgetState(); +} + +class _AmountWidgetState extends ConsumerState with Functions { + @override + Widget build(BuildContext context) { + final selectedType = ref.watch(transactionTypeProvider); + final currencyState = ref.watch(currencyStateNotifier); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: TextField( + controller: widget.amountController, + decoration: InputDecoration( + hintText: "0", + border: InputBorder.none, + prefixText: ' ', + suffixText: currencyState.selectedCurrency.symbol, + suffixStyle: Theme.of(context) + .textTheme + .headlineMedium! + .copyWith(color: typeToColor(selectedType)), + ), + keyboardType: + const TextInputType.numberWithOptions(decimal: true, signed: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'\d*\.?\d{0,2}')), + ], + autofocus: false, + textAlign: TextAlign.center, + cursorColor: grey1, + style: TextStyle( + color: typeToColor(selectedType), + fontSize: 58, + fontWeight: FontWeight.bold, + ), + onTapOutside: (_) { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } + }, + ), + ); + } +} diff --git a/lib/pages/add_page/widgets/details_list_disabled_tile.dart b/lib/pages/add_page/widgets/details_list_disabled_tile.dart new file mode 100644 index 00000000..ed419555 --- /dev/null +++ b/lib/pages/add_page/widgets/details_list_disabled_tile.dart @@ -0,0 +1,61 @@ +import "package:flutter/material.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import "../../../constants/style.dart"; +import "details_list_tile.dart"; + +class NonEditableDetailsListTile extends DetailsListTile { + NonEditableDetailsListTile({ + required String title, + required IconData icon, + required String? value, + Key? key, + }) : super( + title: title, + icon: icon, + value: value, + callback: () {}, // Override the callback to make it non-editable + key: key, + ); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListTile( + contentPadding: const EdgeInsets.all(16.0), + tileColor: Theme.of(context).colorScheme.surface, + onTap: callback, + leading: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondary, + ), + padding: const EdgeInsets.all(10.0), + child: Icon( + icon, + size: 24.0, + color: Theme.of(context).colorScheme.background, + ), + ), + title: Text( + title, + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + value ?? '', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Colors.grey), // Make the text gray + ), + const SizedBox(width: 6.0) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/add_page/widgets/recurrence_list_tile.dart b/lib/pages/add_page/widgets/recurrence_list_tile.dart index 7a702f81..1d0f5934 100644 --- a/lib/pages/add_page/widgets/recurrence_list_tile.dart +++ b/lib/pages/add_page/widgets/recurrence_list_tile.dart @@ -1,19 +1,30 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import "package:flutter_riverpod/flutter_riverpod.dart"; +import '../../../constants/functions.dart'; import "../../../constants/style.dart"; import '../../../providers/transactions_provider.dart'; import '../../../model/transaction.dart'; import 'recurrence_selector.dart'; -class RecurrenceListTile extends ConsumerWidget { +class RecurrenceListTile extends ConsumerWidget with Functions { + final bool recurrencyEditingPermitted; + final Transaction? selectedTransaction; + const RecurrenceListTile({ - Key? key, - }) : super(key: key); + super.key, + required this.recurrencyEditingPermitted, + required this.selectedTransaction, // Add this line + }); @override Widget build(BuildContext context, WidgetRef ref) { final isRecurring = ref.watch(selectedRecurringPayProvider); + final endDate = ref.watch(endDateProvider); + bool isSnackBarVisible = false; return Column( children: [ @@ -41,91 +52,229 @@ class RecurrenceListTile extends ConsumerWidget { .titleLarge! .copyWith(color: Theme.of(context).colorScheme.primary), ), - trailing: Switch.adaptive( - value: isRecurring, - onChanged: (select) => - ref.read(selectedRecurringPayProvider.notifier).state = select, - ), + trailing: recurrencyEditingPermitted + ? Switch.adaptive( + value: isRecurring, + onChanged: (select) => + ref.read(selectedRecurringPayProvider.notifier).state = select, + ) + : GestureDetector( + onTap: () { + if (!isSnackBarVisible) { + isSnackBarVisible = true; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Switch is disabled'), + duration: Duration(milliseconds: 800), + ), + ).closed.then((_) { + isSnackBarVisible = false; + }); + } + }, + child: Tooltip( + message: 'Switch is disabled', + child: Switch( + value: isRecurring, + onChanged: null, // This makes the switch read-only + ), + ), + ) ), if (isRecurring) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.background, - padding: const EdgeInsets.all(16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4)), - ), - onPressed: () { - FocusManager.instance.primaryFocus?.unfocus(); - showModalBottomSheet( - context: context, - builder: (_) => const RecurrenceSelector(), - ); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "Interval", - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).colorScheme.primary), - ), - const Spacer(), - Text( - recurrenceMap[ref.watch(intervalProvider)]!, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.secondary), - ), - const SizedBox(width: 6), - Icon( - Icons.chevron_right, - color: Theme.of(context).colorScheme.secondary, - ), - ], + child: Opacity( + opacity: selectedTransaction == null || recurrencyEditingPermitted ? 1.0 : 0.5, + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.background, + padding: const EdgeInsets.all(16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4)), + ), + onPressed: selectedTransaction == null || recurrencyEditingPermitted + ? () { + FocusManager.instance.primaryFocus?.unfocus(); + showModalBottomSheet( + context: context, + builder: (_) => const RecurrenceSelector(), + ); + } + : null, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Interval", + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + const Spacer(), + Text( + recurrenceMap[ref.watch(intervalProvider)]!.label, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.secondary), + ), + const SizedBox(width: 6), + Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.secondary, + ), + ], + ), ), ), ), Padding( padding: const EdgeInsets.all(16), - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.background, - padding: const EdgeInsets.all(16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4)), - ), - onPressed: () {}, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - "End repetition", - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).colorScheme.primary), - ), - const Spacer(), - Text( - "Never", - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.secondary), - ), - const SizedBox(width: 6), - Icon( - Icons.chevron_right, - color: Theme.of(context).colorScheme.secondary, - ), - ], + child: Opacity( + opacity: selectedTransaction == null || recurrencyEditingPermitted ? 1.0 : 0.5, + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.background, + padding: const EdgeInsets.all(16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4)), + ), + onPressed: selectedTransaction == null || recurrencyEditingPermitted + ? () => showModalBottomSheet( + context: context, + elevation: 10, + builder: (BuildContext context) { + return const EndDateSelector(); + }, + ) + : null, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "End repetition", + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + const Spacer(), + Text( + endDate != null ? dateToString(endDate) : "Never", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.secondary), + ), + const SizedBox(width: 6), + Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.secondary, + ), + ], + ), ), ), ), + if (selectedTransaction != null && !recurrencyEditingPermitted) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextButton( + onPressed: () { + Navigator.of(context).pushNamed( + "/edit-recurring-transaction", + arguments: selectedTransaction, + ).then((value) => Navigator.of(context).pop()); + }, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.warning, + color: Colors.orange, + ), + SizedBox(width: 8), + Flexible( + child: Text( + 'This is a transaction generated by a recurring one: any change will affect this unique transaction.\nTo change all future transactions options, or recurrency options, TAP HERE', + style: TextStyle( + color: darkBlue5, + fontSize: 13, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ], ] ], ); } } + +class EndDateSelector extends ConsumerWidget with Functions { + const EndDateSelector({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(), + body: ListView( + scrollDirection: Axis.vertical, + shrinkWrap: true, + children: [ + ListTile( + visualDensity: const VisualDensity(vertical: -3), + trailing: ref.watch(endDateProvider) != null + ? null + : const Icon(Icons.check), + title: const Text( + "Never", + ), + onTap: () => { + ref.read(endDateProvider.notifier).state = null, + Navigator.pop(context) + }, + ), + ListTile( + visualDensity: const VisualDensity(vertical: -3), + title: const Text("On a date"), + trailing: ref.watch(endDateProvider) != null + ? const Icon(Icons.check) + : null, + subtitle: Text(ref.read(endDateProvider) != null ? dateToString(ref.read(endDateProvider.notifier).state!) : ''), + onTap: () async { + FocusManager.instance.primaryFocus?.unfocus(); + if (Platform.isIOS) { + showCupertinoModalPopup( + context: context, + builder: (_) => Container( + height: 300, + color: white, + child: CupertinoDatePicker( + initialDateTime: ref.watch(endDateProvider), + minimumYear: 2015, + maximumYear: 2050, + mode: CupertinoDatePickerMode.date, + onDateTimeChanged: (date) => + ref.read(endDateProvider.notifier).state = date, + ), + ), + ); + } else if (Platform.isAndroid) { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: ref.watch(endDateProvider), + firstDate: DateTime(2015), + lastDate: DateTime(2050), + ); + if (pickedDate != null) { + ref.read(endDateProvider.notifier).state = pickedDate; + } + } + }) + ], + )); + } +} diff --git a/lib/pages/add_page/widgets/recurrence_list_tile_edit.dart b/lib/pages/add_page/widgets/recurrence_list_tile_edit.dart new file mode 100644 index 00000000..8032603a --- /dev/null +++ b/lib/pages/add_page/widgets/recurrence_list_tile_edit.dart @@ -0,0 +1,203 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import "package:flutter_riverpod/flutter_riverpod.dart"; + +import '../../../constants/functions.dart'; +import "../../../constants/style.dart"; +import '../../../providers/transactions_provider.dart'; +import '../../../model/transaction.dart'; +import 'recurrence_selector.dart'; + +class RecurrenceListTileEdit extends ConsumerWidget with Functions { + const RecurrenceListTileEdit({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isRecurring = ref.watch(selectedRecurringPayProvider); + final endDate = ref.watch(endDateProvider); + + return Column( + children: [ + const Divider(height: 1, color: grey1), + ListTile( + contentPadding: const EdgeInsets.all(16), + leading: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondary, + ), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Icon( + Icons.autorenew, + size: 24.0, + color: Theme.of(context).colorScheme.background, + ), + ), + ), + title: Text( + "Recurring payment", + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + ), + if (isRecurring) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.background, + padding: const EdgeInsets.all(16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4)), + ), + onPressed: () { + FocusManager.instance.primaryFocus?.unfocus(); + showModalBottomSheet( + context: context, + builder: (_) => const RecurrenceSelector(), + ); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Interval", + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + const Spacer(), + Text( + recurrenceMap[ref.watch(intervalProvider)]!.label, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.secondary), + ), + const SizedBox(width: 6), + Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.secondary, + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: TextButton( + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.background, + padding: const EdgeInsets.all(16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4)), + ), + onPressed: () => showModalBottomSheet( + context: context, + elevation: 10, + builder: (BuildContext context) { + return const EndDateSelector(); + }, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "End repetition", + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + const Spacer(), + Text( + endDate != null ? dateToString(endDate) : "Never", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.secondary), + ), + const SizedBox(width: 6), + Icon( + Icons.chevron_right, + color: Theme.of(context).colorScheme.secondary, + ), + ], + ), + ), + ), + ] + ], + ); + } +} + +class EndDateSelector extends ConsumerWidget with Functions { + const EndDateSelector({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(), + body: ListView( + scrollDirection: Axis.vertical, + shrinkWrap: true, + children: [ + ListTile( + visualDensity: const VisualDensity(vertical: -3), + trailing: ref.watch(endDateProvider) != null + ? null + : const Icon(Icons.check), + title: const Text( + "Never", + ), + onTap: () => { + ref.read(endDateProvider.notifier).state = null, + Navigator.pop(context) + }, + ), + ListTile( + visualDensity: const VisualDensity(vertical: -3), + title: const Text("On a date"), + trailing: ref.watch(endDateProvider) != null + ? const Icon(Icons.check) + : null, + subtitle: Text(ref.read(endDateProvider) != null ? dateToString(ref.read(endDateProvider.notifier).state!) : ''), + onTap: () async { + FocusManager.instance.primaryFocus?.unfocus(); + if (Platform.isIOS) { + showCupertinoModalPopup( + context: context, + builder: (_) => Container( + height: 300, + color: white, + child: CupertinoDatePicker( + initialDateTime: ref.watch(endDateProvider), + minimumYear: 2015, + maximumYear: 2050, + mode: CupertinoDatePickerMode.date, + onDateTimeChanged: (date) => + ref.read(endDateProvider.notifier).state = date, + ), + ), + ); + } else if (Platform.isAndroid) { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: ref.watch(endDateProvider), + firstDate: DateTime(2015), + lastDate: DateTime(2050), + ); + if (pickedDate != null) { + ref.read(endDateProvider.notifier).state = pickedDate; + } + } + }) + ], + )); + } +} diff --git a/lib/pages/add_page/widgets/recurrence_selector.dart b/lib/pages/add_page/widgets/recurrence_selector.dart index eaa44e67..9ab84042 100644 --- a/lib/pages/add_page/widgets/recurrence_selector.dart +++ b/lib/pages/add_page/widgets/recurrence_selector.dart @@ -5,7 +5,7 @@ import '../../../constants/functions.dart'; import '../../../providers/transactions_provider.dart'; class RecurrenceSelector extends ConsumerStatefulWidget { - const RecurrenceSelector({Key? key}) : super(key: key); + const RecurrenceSelector({super.key}); @override ConsumerState createState() => _RecurrenceSelectorState(); @@ -27,7 +27,7 @@ class _RecurrenceSelectorState extends ConsumerState with Fu late Map recurrence; recurrenceMap.forEach((key, value) { if (j == i) { - recurrence = {key: value}; + recurrence = {key: value.label}; } j++; }); diff --git a/lib/pages/more_info_page/collaborators_page.dart b/lib/pages/more_info_page/collaborators_page.dart index 0e4c43d4..b70ce09e 100644 --- a/lib/pages/more_info_page/collaborators_page.dart +++ b/lib/pages/more_info_page/collaborators_page.dart @@ -122,19 +122,19 @@ class _CollaboratorsPageState extends ConsumerState { children: [ Text( option[0].toString(), - style: Theme.of(context).textTheme.headline6!.copyWith(color: Theme.of(context).colorScheme.primary), + style: Theme.of(context).textTheme.titleLarge!.copyWith(color: Theme.of(context).colorScheme.primary), textAlign: TextAlign.left, ), const SizedBox(height: 4), Text( option[1].toString(), - style: Theme.of(context).textTheme.bodyText2!.copyWith(color: Theme.of(context).colorScheme.primary), + style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: Theme.of(context).colorScheme.primary), textAlign: TextAlign.left, ), const SizedBox(height: 4), Text( option[2].toString(), - style: Theme.of(context).textTheme.caption!.copyWith(color: Theme.of(context).colorScheme.primary), + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.primary), textAlign: TextAlign.left, ), ], diff --git a/lib/pages/planning_page/planning_page.dart b/lib/pages/planning_page/planning_page.dart index 1bdd3926..e8f6611a 100644 --- a/lib/pages/planning_page/planning_page.dart +++ b/lib/pages/planning_page/planning_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'manage_budget_page.dart'; import 'widget/budget_card.dart'; -import 'widget/recurring_payments_card.dart'; +import 'widget/recurring_payments_list.dart'; class PlanningPage extends StatefulWidget { const PlanningPage({super.key}); @@ -27,7 +27,6 @@ class _PlanningPageState extends State { Widget build(BuildContext context) { return Container( key: _key, - color: Colors.white, padding: const EdgeInsetsDirectional.symmetric(horizontal: 10), child: ListView( children: [ @@ -70,7 +69,7 @@ class _PlanningPageState extends State { Text("Recurring payments", style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 10), - RecurringPaymentCard() + const RecurringPaymentSection() ])); } } diff --git a/lib/pages/planning_page/widget/edit_recurring_transaction.dart b/lib/pages/planning_page/widget/edit_recurring_transaction.dart new file mode 100644 index 00000000..79fd13a6 --- /dev/null +++ b/lib/pages/planning_page/widget/edit_recurring_transaction.dart @@ -0,0 +1,199 @@ +import 'dart:io' show Platform; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../constants/functions.dart'; +import '../../../constants/style.dart'; +import '../../../providers/transactions_provider.dart'; +import '../../add_page/widgets/account_selector.dart'; +import '../../add_page/widgets/amount_widget.dart'; +import '../../add_page/widgets/details_list_tile.dart'; +import '../../add_page/widgets/details_list_disabled_tile.dart'; +import '../../add_page/widgets/label_list_tile.dart'; +import '../../add_page/widgets/recurrence_list_tile_edit.dart'; + +class EditRecurringTransaction extends ConsumerStatefulWidget { + const EditRecurringTransaction({super.key}); + + @override + ConsumerState createState() => + _EditRecurringTransactionState(); +} + +class _EditRecurringTransactionState + extends ConsumerState with Functions { + final TextEditingController amountController = TextEditingController(); + final TextEditingController noteController = TextEditingController(); + + @override + void initState() { + amountController.text = numToCurrency(ref.read(selectedRecurringTransactionUpdateProvider)?.amount); + noteController.text =ref.read(selectedRecurringTransactionUpdateProvider)?.note ?? ''; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final selectedRecurringTransaction = ref.watch(selectedRecurringTransactionUpdateProvider); + + return Scaffold( + appBar: AppBar( + title: const Text( + "Edit recurring transaction", + style: TextStyle(fontSize: 20.0), + ), + leadingWidth: 75, + leading: TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancel', + style: + Theme.of(context).textTheme.titleSmall!.copyWith(color: blue5), + ), + ), + actions: [ + selectedRecurringTransaction != null + ? Container( + alignment: Alignment.centerRight, + child: IconButton( + icon: Icon( + Icons.delete_outline, + color: Theme.of(context).colorScheme.error, + ), + onPressed: () async { + ref + .read(transactionsProvider.notifier) + .deleteRecurringTransaction(selectedRecurringTransaction.id!) + .whenComplete(() => Navigator.pop(context)); + }, + ), + ) + : const SizedBox(), + ], + ), + body: Stack( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 72), + child: Column( + children: [ + AmountWidget(amountController), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 16, top: 32, bottom: 8), + child: Text( + "DETAILS (any change will affect only future transactions)", + style: Theme.of(context) + .textTheme + .labelLarge! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + ), + Container( + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + LabelListTile(noteController), + const Divider(height: 1, color: grey1), + DetailsListTile( + title: "Account", + icon: Icons.account_balance_wallet, + value: ref.watch(bankAccountProvider)?.name, + callback: () { + FocusManager.instance.primaryFocus?.unfocus(); + showModalBottomSheet( + context: context, + clipBehavior: Clip.antiAliasWithSaveLayer, + isScrollControlled: true, + useSafeArea: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + ), + builder: (_) => DraggableScrollableSheet( + expand: false, + minChildSize: 0.5, + initialChildSize: 0.7, + maxChildSize: 0.9, + builder: (_, controller) => AccountSelector( + provider: bankAccountProvider, + scrollController: controller, + ), + ), + ); + }, + ), + const Divider(height: 1, color: grey1), + NonEditableDetailsListTile( + title: "Category", + icon: Icons.list_alt, + value: ref.watch(categoryProvider)?.name + ), + const Divider(height: 1, color: grey1), + NonEditableDetailsListTile( + title: "Date Start", + icon: Icons.calendar_month, + value: dateToString(ref.watch(dateProvider)) + ), + const RecurrenceListTileEdit(), + ], + ), + ), + ], + ), + ), + Container( + alignment: Alignment.bottomCenter, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + boxShadow: [ + BoxShadow( + color: blue1.withOpacity(0.15), + blurRadius: 5.0, + offset: const Offset(0, -1.0), + ) + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + child: Container( + height: 48, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + boxShadow: [defaultShadow], + borderRadius: BorderRadius.circular(8), + ), + child: TextButton( + onPressed: () => { + ref + .read(transactionsProvider.notifier) + .updateRecurringTransaction( + currencyToNum(amountController.text), + noteController.text) + .whenComplete(() => Navigator.of(context).pop()) + }, + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), + ), + child: Text( + "UPDATE TRANSACTION", + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.background), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/planning_page/widget/older_recurring_payments.dart b/lib/pages/planning_page/widget/older_recurring_payments.dart new file mode 100644 index 00000000..d98fa84e --- /dev/null +++ b/lib/pages/planning_page/widget/older_recurring_payments.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sossoldi/model/transaction.dart'; +import 'package:intl/intl.dart'; + +import '../../../model/recurring_transaction.dart'; +import '../../../providers/currency_provider.dart'; + +class OlderRecurringPayments extends ConsumerStatefulWidget { + final RecurringTransaction transaction; + const OlderRecurringPayments({super.key, required this.transaction}); + + @override + ConsumerState createState() => _BudgetCardState(); +} + +class _BudgetCardState extends ConsumerState { + List? transactions; + num sum = 0.0; + + @override + void initState() { + super.initState(); + TransactionMethods() + .getRecurrenceTransactionsById(id: widget.transaction.id) + .then((value) { + setState(() { + transactions = value; + sum = value.fold( + 0.0, (previousValue, element) => previousValue + element.amount); + }); + }); + } + + @override + Widget build(BuildContext context) { + final currencyState = ref.watch(currencyStateNotifier); + Map> transactionsByYear = {}; + if (transactions != null) { + for (var transaction in transactions!) { + int year = transaction.date.year; + if (!transactionsByYear.containsKey(year)) { + transactionsByYear[year] = []; + } + transactionsByYear[year]!.add(transaction); + } + } + + return Column(children: [ + Row(children: [ + Expanded( + flex: 5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(widget.transaction.note, + style: const TextStyle(fontSize: 25)), + Text(widget.transaction.recurrency, + style: const TextStyle(fontSize: 15)), + ], + )), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "-${sum.toStringAsFixed(2)}${currencyState.selectedCurrency.symbol}", + style: const TextStyle(fontSize: 25, color: Colors.red)), + ], + )) + ]), + if (transactions == null) + const CircularProgressIndicator() + else if (transactions!.isEmpty) + const Text( + "All recurring payments will be displayed here", + style: TextStyle(fontWeight: FontWeight.normal, fontSize: 13), + ) + else + SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: transactionsByYear.entries.map((entry) { + int year = entry.key; + List transactionsOfYear = entry.value; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Text( + year.toString(), + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.bold), + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: transactionsOfYear.map((transaction) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15), + child: Row( + children: [ + Text( + DateFormat('d MMMM').format(transaction.date), + ), + const Expanded(child: SizedBox.shrink()), + Text( + "-${transaction.amount.toString()}${currencyState.selectedCurrency.symbol}", + style: const TextStyle(fontSize: 14, color: Colors.red), + ), + ], + ), + ); + }).toList(), + ), + ], + ); + }).toList(), + ), + ) + ]); + } +} diff --git a/lib/pages/planning_page/widget/recurring_payment_card.dart b/lib/pages/planning_page/widget/recurring_payment_card.dart new file mode 100644 index 00000000..48b4019a --- /dev/null +++ b/lib/pages/planning_page/widget/recurring_payment_card.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sossoldi/constants/constants.dart'; +import 'package:sossoldi/model/recurring_transaction.dart'; +import 'package:sossoldi/pages/planning_page/widget/older_recurring_payments.dart'; +import 'package:sossoldi/providers/accounts_provider.dart'; +import 'package:sossoldi/providers/currency_provider.dart'; + +import '../../../constants/functions.dart'; +import '../../../constants/style.dart'; +import '../../../model/transaction.dart'; +import '../../../providers/categories_provider.dart'; + +/// This class shows account summaries in dashboard +class RecurringPaymentCard extends ConsumerWidget with Functions { + final RecurringTransaction transaction; + + const RecurringPaymentCard({ + super.key, + required this.transaction, + }); + + String getNextText() { + final now = DateTime.now(); + final daysPassed = now.difference(transaction.lastInsertion ?? transaction.fromDate).inDays; + final daysInterval = recurrenceMap[parseRecurrence(transaction.recurrency)]!.days; + final daysUntilNextTransaction = daysInterval - (daysPassed % daysInterval); + return daysUntilNextTransaction.toString(); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final categories = ref.watch(categoriesProvider).value; + final accounts = ref.watch(accountsProvider).value; + final currencyState = ref.watch(currencyStateNotifier); + + var cat = categories + ?.firstWhere((element) => element.id == transaction.idCategory); + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: categoryColorList[cat!.color].withOpacity(0.1), + boxShadow: [defaultShadow], + ), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 16.0, + ), + child: Column(children: [ + Row(children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.secondary, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + iconList[cat.symbol], + size: 25.0, + color: const Color(0xFFFFFFFF), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + flex: 4, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text(transaction.recurrency, + style: const TextStyle( + fontWeight: FontWeight.w200, fontSize: 10)), + const SizedBox(height: 10), + Text(transaction.note, + style: const TextStyle( + fontWeight: FontWeight.w900, fontSize: 16)), + const SizedBox(height: 10), + Text(cat.name), + ], + )), + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text("In ${getNextText()} days"), + const SizedBox(height: 10), + Text( + "-${transaction.amount}${currencyState.selectedCurrency.symbol}", + style: const TextStyle(color: Colors.red)), + const SizedBox(height: 10), + Text(accounts! + .firstWhere((element) => + element.id == transaction.idBankAccount) + .name) + ], + )) + ]), + Row( + children: [ + Expanded( + flex: 4, + child: Container( + alignment: Alignment.centerLeft, + child: ElevatedButton( + onPressed: () => { + showModalBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.0), + topRight: Radius.circular(20.0), + ), + ), + elevation: 10, + builder: (BuildContext context) { + return ListView( + scrollDirection: Axis.vertical, + padding: const EdgeInsets.symmetric( + vertical: 20, horizontal: 10), + children: [ + OlderRecurringPayments( + transaction: transaction) + ]); + }, + ) + }, + style: ElevatedButton.styleFrom( + elevation: 0, + padding: const EdgeInsets.all(8), + backgroundColor: Colors.white, + ), + child: const Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Icon(Icons.checklist_rtl_outlined, color: blue4), + SizedBox(width: 10), + Text( + "See older payments", + style: TextStyle(color: blue4, fontSize: 14), + ), + ])))), + transaction.toDate != null + ? Expanded( + flex: 2, + child: Container( + alignment: Alignment.centerRight, + child: Text( + "Until ${dateToString(transaction.toDate!)}", + style: const TextStyle(fontSize: 8), + ))) + : Container(), + ], + ) + ])); + } +} diff --git a/lib/pages/planning_page/widget/recurring_payments_card.dart b/lib/pages/planning_page/widget/recurring_payments_card.dart deleted file mode 100644 index 0b8d76c4..00000000 --- a/lib/pages/planning_page/widget/recurring_payments_card.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/material.dart'; - -class RecurringPaymentCard extends StatefulWidget { - const RecurringPaymentCard({super.key}); - - @override - State createState() => _RecurringPaymentCardState(); -} - -class _RecurringPaymentCardState extends State { - void addRecurringPayment() { - print("addRecurringPayment"); - } - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), - child: Column( - children: [ - const Text( - "All recurring payments will be displayed here", - style: TextStyle(fontWeight: FontWeight.normal, fontSize: 13), - ), - const SizedBox(height: 10), - ElevatedButton.icon( - icon: Icon( - Icons.add_circle, - color: Theme.of(context).colorScheme.secondary, - ), - onPressed: addRecurringPayment, - label: Text( - "Add recurring payment", - style: Theme.of(context) - .textTheme - .titleSmall! - .apply(color: Theme.of(context).colorScheme.secondary), - ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - fixedSize: const Size(330, 50), - ), - ) - ], - ), - ), - ); - } -} diff --git a/lib/pages/planning_page/widget/recurring_payments_list.dart b/lib/pages/planning_page/widget/recurring_payments_list.dart new file mode 100644 index 00000000..49eafa44 --- /dev/null +++ b/lib/pages/planning_page/widget/recurring_payments_list.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sossoldi/pages/planning_page/widget/recurring_payment_card.dart'; +import 'package:sossoldi/providers/transactions_provider.dart'; + +import '../../../model/recurring_transaction.dart'; +import '../../../model/transaction.dart'; + +class RecurringPaymentSection extends ConsumerStatefulWidget { + const RecurringPaymentSection({super.key}); + + @override + ConsumerState createState() => + _RecurringPaymentSectionState(); +} + +class _RecurringPaymentSectionState extends ConsumerState { + Future> recurringTransactions = RecurringTransactionMethods().selectAllActive(); + + _refreshData() { + recurringTransactions = RecurringTransactionMethods().selectAllActive(); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(10)), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 16), + child: Column( + children: [ + FutureBuilder>( + future: recurringTransactions, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else if (!snapshot.hasData) { + return const Text( + "All recurring payments will be displayed here", + style: + TextStyle(fontWeight: FontWeight.normal, fontSize: 13), + ); + } else { + final transactions = snapshot.data; + return ListView.separated( + itemCount: transactions!.length, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) => InkWell( + onTap: () { + ref + .read(transactionsProvider.notifier) + .transactionUpdateState(transactions[index]) + .whenComplete(() { + Navigator.of(context).pushNamed("/edit-recurring-transaction").then((value) => setState(() { _refreshData(); })); + }); + }, + child: RecurringPaymentCard( + transaction: transactions[index])), + separatorBuilder: (context, index) => const Divider(), + ); + } + }, + ), + const SizedBox(height: 30), + ElevatedButton.icon( + icon: Icon( + Icons.add_circle, + color: Theme.of(context).colorScheme.secondary, + ), + onPressed: () => { + ref.read(selectedTransactionUpdateProvider.notifier).state = null, + ref.read(selectedRecurringPayProvider.notifier).state = true, + ref.read(bankAccountProvider.notifier).state = null, + ref.read(categoryProvider.notifier).state = null, + ref.read(intervalProvider.notifier).state = Recurrence.monthly, + ref.read(endDateProvider.notifier).state = null, + Navigator.of(context).pushNamed( + "/add-page", + arguments: {'recurrencyEditingPermitted': false}, + ).then((value) => setState(() { _refreshData(); })) + }, + label: Text( + "Add recurring payment", + style: Theme.of(context) + .textTheme + .titleSmall! + .apply(color: Theme.of(context).colorScheme.secondary), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + fixedSize: const Size(330, 50), + ), + ), + const SizedBox(height: 30), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index fecef047..aac4db7b 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -194,9 +194,9 @@ class _SettingsPageState extends ConsumerState { textAlign: TextAlign.center, ), ElevatedButton( - child: const Text('CLEAR DB'), + child: const Text('RESET DB'), onPressed: () async { - await SossoldiDatabase.instance.clearDatabase().then((v) { + await SossoldiDatabase.instance.resetDatabase().then((v) { ref.refresh(accountsProvider); ref.refresh(categoriesProvider); ref.refresh(transactionsProvider); diff --git a/lib/providers/transactions_provider.dart b/lib/providers/transactions_provider.dart index 4c56c4ff..21ae2670 100644 --- a/lib/providers/transactions_provider.dart +++ b/lib/providers/transactions_provider.dart @@ -1,7 +1,10 @@ +import 'dart:convert'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../model/bank_account.dart'; import '../model/category_transaction.dart'; +import '../model/recurring_transaction.dart'; import '../model/transaction.dart'; import 'accounts_provider.dart'; import 'dashboard_provider.dart'; @@ -25,10 +28,11 @@ final categoryProvider = StateProvider((ref) => null); // Recurring Payment final selectedRecurringPayProvider = StateProvider((ref) => false); final intervalProvider = StateProvider((ref) => Recurrence.monthly); -final repetitionProvider = StateProvider((ref) => null); +final endDateProvider = StateProvider((ref) => null); // Set when a transaction is selected for update final selectedTransactionUpdateProvider = StateProvider((ref) => null); +final selectedRecurringTransactionUpdateProvider = StateProvider((ref) => null); // Amount total for the transactions filtered final totalAmountProvider = StateProvider((ref) => 0); @@ -117,13 +121,62 @@ class AsyncTransactionsNotifier extends AutoDisposeAsyncNotifier updateTransaction(num amount, String label) async { + Future addRecurringTransaction(num amount, String label) async { + state = const AsyncValue.loading(); + + final date = ref.read(dateProvider); + final toDate = ref.read(endDateProvider); + final bankAccount = ref.read(bankAccountProvider)!; + final category = ref.read(categoryProvider); + final recurrency = ref.read(intervalProvider.notifier).state; + + RecurringTransaction transaction = RecurringTransaction( + amount: amount, + fromDate: date, + toDate: toDate, + note: label, + idBankAccount: bankAccount.id!, + idCategory: category!.id!, + recurrency: recurrency.name.toUpperCase(), + createdAt: date, + updatedAt: date, + lastInsertion: date + ); + + // Here we need the recurringTransaction just inserted, to get and return a model with also his ID + RecurringTransaction? insertedTransaction; + + state = await AsyncValue.guard(() async { + insertedTransaction = await RecurringTransactionMethods().insert(transaction); + return _getTransactions(update: true); + }); + + // check if fromDate is today, and add the first recurrence of the transaction + DateTime now = DateTime.now(); + + if (date.year == now.year && date.month == now.month && date.day == now.day) { + final transaction = Transaction( + date: date, + amount: amount, + type: TransactionType.expense, + note: label, + idBankAccount: bankAccount.id!, + idCategory: category.id!, + idRecurringTransaction: insertedTransaction!.id, + recurring: true, + ); + await TransactionMethods().insert(transaction); + } + + return insertedTransaction; + } + + Future updateTransaction(num amount, String label, [int? recurringTransactionId]) async { final type = ref.read(transactionTypeProvider); final date = ref.read(dateProvider); final bankAccount = ref.read(bankAccountProvider)!; final bankAccountTransfer = ref.read(bankAccountTransferProvider); final category = ref.read(categoryProvider); - final recurring = ref.read(selectedRecurringPayProvider); Transaction transaction = ref.read(selectedTransactionUpdateProvider)!.copy( date: date, @@ -133,7 +186,8 @@ class AsyncTransactionsNotifier extends AutoDisposeAsyncNotifier transactionUpdateState(Transaction transaction) async { - ref.read(selectedTransactionUpdateProvider.notifier).state = transaction; - final accountList = ref.watch(accountsProvider); - if (transaction.type != TransactionType.transfer) { - if (transaction.idCategory != null) { - ref.read(categoryProvider.notifier).state = - await CategoryTransactionMethods().selectById(transaction.idCategory!); + Future updateRecurringTransaction(num amount, String label) async { + final bankAccount = ref.read(bankAccountProvider)!; + final recurrency = ref.read(intervalProvider.notifier).state; + final category = ref.read(categoryProvider); + + RecurringTransaction transaction = + ref.read(selectedRecurringTransactionUpdateProvider)!.copy( + fromDate: ref.read(dateProvider), + toDate: ref.read(endDateProvider), + recurrency: recurrency.name.toUpperCase(), + amount: amount, + note: label, + idBankAccount: bankAccount.id!, + idCategory: category?.id, + updatedAt: DateTime.now() + ); + + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + await RecurringTransactionMethods().updateItem(transaction); + return _getTransactions(update: true); + }); + } + Future addRecurringDataToTransaction(num idTransaction, num idRecurringTransaction) async { + + } + + Future transactionUpdateState(dynamic transaction) async { + if(transaction is Transaction) { + ref.read(selectedTransactionUpdateProvider.notifier).state = transaction; + ref.read(selectedRecurringPayProvider.notifier).state = transaction.recurring; + + final accountList = ref.watch(accountsProvider); + if (transaction.type != TransactionType.transfer) { + if (transaction.idCategory != null) { + ref.read(categoryProvider.notifier).state = + await CategoryTransactionMethods().selectById(transaction.idCategory!); + } } + ref.read(bankAccountProvider.notifier).state = + accountList.value!.firstWhere((element) => element.id == transaction.idBankAccount); + ref.read(bankAccountTransferProvider.notifier).state = + transaction.type == TransactionType.transfer + ? accountList.value! + .firstWhere((element) => element.id == transaction.idBankAccountTransfer) + : null; + ref.read(transactionTypeProvider.notifier).state = transaction.type; + ref.read(dateProvider.notifier).state = transaction.date; + } else if(transaction is RecurringTransaction) { + ref.read(selectedRecurringTransactionUpdateProvider.notifier).state = transaction; + ref.read(selectedRecurringPayProvider.notifier).state = true; + ref.read(categoryProvider.notifier).state = await CategoryTransactionMethods().selectById(transaction.idCategory); + ref.read(bankAccountProvider.notifier).state = ref.watch(accountsProvider).value!.firstWhere((element) => element.id == transaction.idBankAccount); + ref.read(intervalProvider.notifier).state = parseRecurrence(transaction.recurrency); + ref.read(endDateProvider.notifier).state = transaction.toDate; } - ref.read(bankAccountProvider.notifier).state = - accountList.value!.firstWhere((element) => element.id == transaction.idBankAccount); - ref.read(bankAccountTransferProvider.notifier).state = - transaction.type == TransactionType.transfer - ? accountList.value! - .firstWhere((element) => element.id == transaction.idBankAccountTransfer) - : null; - ref.read(transactionTypeProvider.notifier).state = transaction.type; - ref.read(dateProvider.notifier).state = transaction.date; - ref.read(selectedRecurringPayProvider.notifier).state = transaction.recurring; } Future deleteTransaction(int transactionId) async { @@ -172,6 +263,14 @@ class AsyncTransactionsNotifier extends AutoDisposeAsyncNotifier deleteRecurringTransaction(int transactionId) async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + await RecurringTransactionMethods().deleteById(transactionId); + return _getTransactions(update: true); + }); + } + void switchAccount() { final fromAccount = ref.read(bankAccountProvider); final toAccount = ref.read(bankAccountTransferProvider); @@ -189,7 +288,6 @@ class AsyncTransactionsNotifier extends AutoDisposeAsyncNotifier makeRoute(RouteSettings settings) { switch (settings.name) { @@ -30,7 +31,13 @@ Route makeRoute(RouteSettings settings) { case '/dashboard': return _materialPageRoute(settings.name, const HomePage()); case '/add-page': - return _materialPageRoute(settings.name, const AddPage()); + final args = settings.arguments as Map?; + return _materialPageRoute( + settings.name, + AddPage(recurrencyEditingPermitted: args?['recurrencyEditingPermitted'] ?? true), + ); + case '/edit-recurring-transaction': + return _materialPageRoute(settings.name, const EditRecurringTransaction()); case '/transactions': return _materialPageRoute(settings.name, const TransactionsPage()); case '/category-list': diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 036b9901..bda234e8 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,7 +7,7 @@ import Foundation import flutter_local_notifications import shared_preferences_foundation -import sqflite +import sqflite_darwin import sqlite3_flutter_libs import url_launcher_macos diff --git a/pubspec.lock b/pubspec.lock index e2c052e0..944b5045 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,34 +5,47 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + url: "https://pub.dev" + source: hosted + version: "6.7.0" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "2.0.3" archive: dependency: transitive description: name: archive - sha256: "7e0d52067d05f2e0324268097ba723b71cb41ac8a6a2b24d1edf9c536b987b03" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.6" + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -77,10 +90,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -109,18 +122,18 @@ packages: dependency: transitive description: name: coverage - sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 url: "https://pub.dev" source: hosted - version: "1.7.2" + version: "1.9.2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" csslib: dependency: transitive description: @@ -133,10 +146,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dbus: dependency: transitive description: @@ -165,18 +178,18 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" fl_chart: dependency: "direct main" description: @@ -202,50 +215,50 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3 + sha256: "49eeef364fddb71515bc78d5a8c51435a68bccd6e4d68e25a942c5e47761ae71" url: "https://pub.dev" source: hosted - version: "16.3.2" + version: "17.2.3" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af url: "https://pub.dev" source: hosted - version: "4.0.0+1" + version: "4.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" url: "https://pub.dev" source: hosted - version: "7.0.0+1" + version: "7.2.0" flutter_native_splash: dependency: "direct main" description: name: flutter_native_splash - sha256: d93394f22f73e810bda59e11ebe83329c5511d6460b6b7509c4e1f3c92d6d625 + sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.4.1" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: "305203d1578f6857675f9730568548b03900ce53afd319f4aa9d2fa943334dbe" + sha256: "711d916456563f715bde1e139d7cfdca009f8264befab3ac9f8ded8b6ec26405" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.5.3" flutter_staggered_grid_view: dependency: "direct main" description: @@ -268,10 +281,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -308,10 +321,10 @@ packages: dependency: transitive description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.2.0" intl: dependency: "direct main" description: @@ -332,42 +345,42 @@ packages: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" json_annotation: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -384,6 +397,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -396,26 +417,26 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" node_preamble: dependency: transitive description: @@ -452,18 +473,18 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" percent_indicator: dependency: "direct main" description: @@ -484,34 +505,34 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" + sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa" url: "https://pub.dev" source: hosted - version: "12.0.5" + version: "12.0.12" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 + sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 url: "https://pub.dev" source: hosted - version: "9.4.4" + version: "9.4.5" permission_handler_html: dependency: transitive description: name: permission_handler_html - sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.3+2" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.3" permission_handler_windows: dependency: transitive description: @@ -524,34 +545,26 @@ packages: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" platform: dependency: transitive description: name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d - url: "https://pub.dev" - source: hosted - version: "2.1.6" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "2.1.8" pool: dependency: transitive description: @@ -572,66 +585,66 @@ packages: dependency: transitive description: name: riverpod - sha256: "2e84315036e64c59affaff7443dea51247bc2fe704461a32f26a27986fb63d55" + sha256: c86fedfb45dd1da98ee6493dd9374325cdf494e7d523ebfb0c387eecc5f7b5c9 url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.5.3" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shelf: dependency: transitive description: @@ -652,18 +665,18 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -673,10 +686,10 @@ packages: dependency: transitive description: name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" source_maps: dependency: transitive description: @@ -697,42 +710,66 @@ packages: dependency: "direct main" description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: "79a297dc3cc137e758c6a4baf83342b039e5a6d2436fcdf3f96a00adaaf2ad62" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.4+5" sqflite_common_ffi: dependency: "direct main" description: name: sqflite_common_ffi - sha256: "0d5cc1be2eb18400ac6701c31211d44164393aa75886093002ecdd947be04f93" + sha256: a6057d4c87e9260ba1ec436ebac24760a110589b9c0a859e128842eb69a7ef04 + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "769733dddf94622d5541c73e4ddc6aa7b252d865285914b6fcd54a63c4b4f027" + url: "https://pub.dev" + source: hosted + version: "2.4.1-1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" url: "https://pub.dev" source: hosted - version: "2.3.0+2" + version: "2.4.0" sqlite3: dependency: transitive description: name: sqlite3 - sha256: db65233e6b99e99b2548932f55a987961bc06d82a31a0665451fa0b4fff4c3fb + sha256: "45f168ae2213201b54e09429ed0c593dc2c88c924a1488d6f9c523a255d567cb" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.6" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "3e3583b77cf888a68eae2e49ee4f025f66b86623ef0d83c297c8d903daa14871" + sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" url: "https://pub.dev" source: hosted - version: "0.5.18" + version: "0.5.24" stack_trace: dependency: transitive description: @@ -769,10 +806,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -785,34 +822,34 @@ packages: dependency: "direct dev" description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.25.7" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.6.4" timezone: dependency: transitive description: name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.4" typed_data: dependency: transitive description: @@ -833,66 +870,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: b1c9e98774adf8820c96fbc7ae3601231d324a7d5ebd8babe27b6dfac91357ba + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def" + sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.12" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd" + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc" + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" vector_math: dependency: transitive description: @@ -905,10 +942,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" watcher: dependency: transitive description: @@ -921,18 +958,26 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" webkit_inspection_protocol: dependency: transitive description: @@ -941,14 +986,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - win32: - dependency: transitive - description: - name: win32 - sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 - url: "https://pub.dev" - source: hosted - version: "5.1.1" workmanager: dependency: "direct main" description: @@ -961,18 +998,18 @@ packages: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.1.0" xml: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" yaml: dependency: transitive description: @@ -982,5 +1019,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.5.0 <3.24.3" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 159073e4..eb3c7c1c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=3.1.5 <4.0.0' + sdk: '>=3.1.5 <3.24.3' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -61,7 +61,7 @@ dependencies: circular_menu: ^2.0.1 flutter_native_splash: ^2.2.18 shared_preferences: ^2.2.2 - flutter_local_notifications: 16.3.2 + flutter_local_notifications: 17.2.3 permission_handler: 11.2.0 workmanager: 0.5.2 diff --git a/test/model/bank_account_test.dart b/test/model/bank_account_test.dart index 5b6cdcaf..cd78c4db 100644 --- a/test/model/bank_account_test.dart +++ b/test/model/bank_account_test.dart @@ -94,7 +94,7 @@ void main() { sossoldiDatabase = SossoldiDatabase(dbName: 'test.db'); db = await sossoldiDatabase.database; - await sossoldiDatabase.clearDatabase(); + await sossoldiDatabase.resetDatabase(); }); tearDown(() async => { @@ -121,7 +121,7 @@ void main() { 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, recurrencyType, recurrencyPayDay, recurrencyFrom, recurrencyTo, 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(); @@ -187,7 +187,7 @@ void main() { 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, recurrencyType, recurrencyPayDay, recurrencyFrom, recurrencyTo, 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(); diff --git a/test/model/recurring_transaction_amount_test.dart b/test/model/recurring_transaction_amount_test.dart deleted file mode 100644 index f4277f95..00000000 --- a/test/model/recurring_transaction_amount_test.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:sossoldi/model/recurring_transaction_amount.dart'; -import 'package:sossoldi/model/base_entity.dart'; - -void main() { - test('Test Copy Recurring Transaction Amount', () { - RecurringTransactionAmount t = RecurringTransactionAmount( - id: 2, - from: DateTime.utc(2022), - to: DateTime.utc(2023), - amount: 0, - idTransaction: 0, - createdAt: DateTime.utc(2022), - updatedAt: DateTime.utc(2022)); - - RecurringTransactionAmount tCopy = t.copy(id: 10); - - assert(tCopy.id == 10); - assert(tCopy.from == t.from); - assert(tCopy.to == t.to); - assert(tCopy.amount == t.amount); - assert(tCopy.idTransaction == t.idTransaction); - assert(tCopy.createdAt == t.createdAt); - assert(tCopy.updatedAt == t.updatedAt); - }); - - test("Test fromJson Recurring Transaction Amount", () { - Map json = { - BaseEntityFields.id: 0, - RecurringTransactionAmountFields.from: - DateTime.utc(2022).toIso8601String(), - RecurringTransactionAmountFields.to: DateTime.utc(2023).toIso8601String(), - RecurringTransactionAmountFields.amount: 0, - RecurringTransactionAmountFields.idTransaction: 0, - BaseEntityFields.createdAt: DateTime.utc(2022).toIso8601String(), - BaseEntityFields.updatedAt: DateTime.utc(2022).toIso8601String(), - }; - - RecurringTransactionAmount t = RecurringTransactionAmount.fromJson(json); - - assert(t.id == json[BaseEntityFields.id]); - assert(t.from.toUtc().toIso8601String() == - json[RecurringTransactionAmountFields.from]); - assert(t.to.toUtc().toIso8601String() == - json[RecurringTransactionAmountFields.to]); - assert(t.amount == json[RecurringTransactionAmountFields.amount]); - assert(t.idTransaction == - json[RecurringTransactionAmountFields.idTransaction]); - assert(t.createdAt?.toUtc().toIso8601String() == - json[BaseEntityFields.createdAt]); - assert(t.updatedAt?.toUtc().toIso8601String() == - json[BaseEntityFields.updatedAt]); - }); - - test("Test toJson Recurring Transaction Amount", () { - RecurringTransactionAmount t = RecurringTransactionAmount( - id: 2, - from: DateTime.utc(2022), - to: DateTime.utc(2023), - amount: 0, - idTransaction: 0, - createdAt: DateTime.utc(2022), - updatedAt: DateTime.utc(2022)); - - Map json = t.toJson(); - - assert(t.id == json[BaseEntityFields.id]); - assert(t.from.toUtc().toIso8601String() == - json[RecurringTransactionAmountFields.from]); - assert(t.to.toUtc().toIso8601String() == - json[RecurringTransactionAmountFields.to]); - assert(t.amount == json[RecurringTransactionAmountFields.amount]); - assert(t.idTransaction == - json[RecurringTransactionAmountFields.idTransaction]); - assert(t.createdAt?.toUtc().toIso8601String() == - json[BaseEntityFields.createdAt]); - assert(t.updatedAt?.toUtc().toIso8601String() == - json[BaseEntityFields.updatedAt]); - }); -} diff --git a/test/model/recurring_transaction_test.dart b/test/model/recurring_transaction_test.dart new file mode 100644 index 00000000..0f6d28b6 --- /dev/null +++ b/test/model/recurring_transaction_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:sossoldi/model/recurring_transaction.dart'; +import 'package:sossoldi/model/base_entity.dart'; + +void main() { + test('Test Copy Recurring Transaction Amount', () { + DateTime toDateValue = DateTime.utc(2023); + RecurringTransaction t = RecurringTransaction( + id: 2, + fromDate: DateTime.utc(2022), + toDate: toDateValue, + amount: 14, + note: 'Test Transaction', + recurrency: 'MONTHLY', + idBankAccount: 34, + idCategory: 24, + createdAt: DateTime.utc(2022), + updatedAt: DateTime.utc(2022) + ); + + RecurringTransaction tCopy = t.copy(id: 10, toDate: toDateValue); + + assert(tCopy.id == 10); + assert(tCopy.fromDate == t.fromDate); + assert(tCopy.toDate == toDateValue); + assert(tCopy.amount == t.amount); + assert(tCopy.note == t.note); + assert(tCopy.recurrency == t.recurrency); + assert(tCopy.idBankAccount == t.idBankAccount); + assert(tCopy.idCategory == t.idCategory); + assert(tCopy.createdAt == t.createdAt); + assert(tCopy.updatedAt == t.updatedAt); + }); + + test("Test fromJson Recurring Transaction Amount", () { + Map json = { + BaseEntityFields.id: 0, + RecurringTransactionFields.fromDate: DateTime.utc(2022).toIso8601String(), + RecurringTransactionFields.toDate: DateTime.utc(2023).toIso8601String(), + RecurringTransactionFields.amount: 50, + RecurringTransactionFields.note: "Test Transaction", + RecurringTransactionFields.recurrency: "WEEKLY", + RecurringTransactionFields.idBankAccount: 44, + RecurringTransactionFields.idCategory: 4, + BaseEntityFields.createdAt: DateTime.utc(2022).toIso8601String(), + BaseEntityFields.updatedAt: DateTime.utc(2022).toIso8601String(), + }; + + RecurringTransaction t = RecurringTransaction.fromJson(json); + + assert(t.id == json[BaseEntityFields.id]); + assert(t.fromDate.toUtc().toIso8601String() == json[RecurringTransactionFields.fromDate]); + assert(t.toDate?.toUtc().toIso8601String() == json[RecurringTransactionFields.toDate]); + assert(t.amount == json[RecurringTransactionFields.amount]); + assert(t.note == json[RecurringTransactionFields.note]); + assert(t.recurrency == json[RecurringTransactionFields.recurrency]); + assert(t.idBankAccount == json[RecurringTransactionFields.idBankAccount]); + assert(t.idCategory == json[RecurringTransactionFields.idCategory]); + assert(t.createdAt?.toUtc().toIso8601String() == json[BaseEntityFields.createdAt]); + assert(t.updatedAt?.toUtc().toIso8601String() == json[BaseEntityFields.updatedAt]); + }); + + test("Test toJson Recurring Transaction Amount", () { + RecurringTransaction t = RecurringTransaction( + id: 2, + fromDate: DateTime.utc(2022), + toDate: DateTime.utc(2023), + amount: 0, + note: "Test transaction", + recurrency: "MONTHLY", + idBankAccount: 4, + idCategory: 45, + createdAt: DateTime.utc(2022), + updatedAt: DateTime.utc(2022)); + + Map json = t.toJson(); + + assert(t.id == json[BaseEntityFields.id]); + assert(t.fromDate.toUtc().toIso8601String() == json[RecurringTransactionFields.fromDate]); + assert(t.toDate?.toUtc().toIso8601String() == json[RecurringTransactionFields.toDate]); + assert(t.amount == json[RecurringTransactionFields.amount]); + assert(t.note == json[RecurringTransactionFields.note]); + assert(t.recurrency == json[RecurringTransactionFields.recurrency]); + assert(t.idBankAccount == json[RecurringTransactionFields.idBankAccount]); + assert(t.idCategory == json[RecurringTransactionFields.idCategory]); + assert(t.createdAt?.toUtc().toIso8601String() == json[BaseEntityFields.createdAt]); + assert(t.updatedAt?.toUtc().toIso8601String() == json[BaseEntityFields.updatedAt]); + }); +} diff --git a/test/model/transaction_test.dart b/test/model/transaction_test.dart index d3ac4518..d7ea30bc 100644 --- a/test/model/transaction_test.dart +++ b/test/model/transaction_test.dart @@ -22,13 +22,11 @@ void main() { idBankAccount: 0, idBankAccountTransfer: null, recurring: false, - recurrencyType: null, - recurrencyPayDay: null, - recurrencyFrom: null, - recurrencyTo: null, + idRecurringTransaction: null, idCategory: 1, createdAt: DateTime.utc(2022), - updatedAt: DateTime.utc(2022)); + updatedAt: DateTime.utc(2022) + ); Transaction tCopy = t.copy(id: 10); @@ -36,16 +34,13 @@ void main() { assert(tCopy.date == t.date); assert(tCopy.amount == t.amount); assert(tCopy.date == t.date); - assert(tCopy.note == t.note); assert(tCopy.type == t.type); + assert(tCopy.note == t.note); assert(tCopy.idBankAccount == t.idBankAccount); assert(tCopy.idCategory == t.idCategory); assert(tCopy.idBankAccountTransfer == t.idBankAccountTransfer); assert(tCopy.recurring == t.recurring); - assert(tCopy.recurrencyType == t.recurrencyType); - assert(tCopy.recurrencyPayDay == t.recurrencyPayDay); - assert(tCopy.recurrencyFrom == t.recurrencyFrom); - assert(tCopy.recurrencyTo == t.recurrencyTo); + assert(tCopy.idRecurringTransaction == t.idRecurringTransaction); assert(tCopy.createdAt == t.createdAt); assert(tCopy.updatedAt == t.updatedAt); }); @@ -61,10 +56,7 @@ void main() { TransactionFields.idCategory: 0, TransactionFields.idBankAccountTransfer: null, TransactionFields.recurring: false, - TransactionFields.recurrencyType: null, - TransactionFields.recurrencyPayDay: null, - TransactionFields.recurrencyFrom: null, - TransactionFields.recurrencyTo: null, + TransactionFields.idRecurringTransaction: null, BaseEntityFields.createdAt: DateTime.utc(2022).toIso8601String(), BaseEntityFields.updatedAt: DateTime.utc(2022).toIso8601String(), }; @@ -79,10 +71,7 @@ void main() { assert(t.idBankAccount == json[TransactionFields.idBankAccount]); assert(t.idBankAccountTransfer == json[TransactionFields.idBankAccountTransfer]); assert(t.recurring == json[TransactionFields.recurring]); - assert(t.recurrencyType == json[TransactionFields.recurrencyType]); - assert(t.recurrencyPayDay == json[TransactionFields.recurrencyPayDay]); - assert(t.recurrencyFrom == json[TransactionFields.recurrencyFrom]); - assert(t.recurrencyTo == json[TransactionFields.recurrencyTo]); + assert(t.idRecurringTransaction == json[TransactionFields.idRecurringTransaction]); assert(t.idCategory == json[TransactionFields.idCategory]); assert(t.createdAt?.toUtc().toIso8601String() == json[BaseEntityFields.createdAt]); @@ -101,10 +90,7 @@ void main() { idBankAccount: 0, idBankAccountTransfer: null, recurring: false, - recurrencyType: null, - recurrencyPayDay: null, - recurrencyFrom: null, - recurrencyTo: null + idRecurringTransaction: null ); Map json = t.toJson(); @@ -118,10 +104,7 @@ void main() { assert(t.idBankAccount == json[TransactionFields.idBankAccount]); assert(t.idBankAccountTransfer == json[TransactionFields.idBankAccountTransfer]); assert((t.recurring ? 1 : 0) == json[TransactionFields.recurring]); - assert(t.recurrencyType == json[TransactionFields.recurrencyType]); - assert(t.recurrencyPayDay == json[TransactionFields.recurrencyPayDay]); - assert(t.recurrencyFrom == json[TransactionFields.recurrencyFrom]); - assert(t.recurrencyTo == json[TransactionFields.recurrencyTo]); + assert(t.idRecurringTransaction == json[TransactionFields.idRecurringTransaction]); }); group("Transaction Methods", () { @@ -158,7 +141,7 @@ void main() { throw Exception('DbBase.cleanDatabase: $error'); } - const insertDemoTransactionsQuery = '''INSERT INTO `transaction` (date, amount, type, note, idCategory, idBankAccount, idBankAccountTransfer, recurring, recurrencyType, recurrencyPayDay, recurrencyFrom, recurrencyTo, 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(); @@ -211,7 +194,7 @@ void main() { throw Exception('DbBase.cleanDatabase: $error'); } - const insertDemoTransactionsQuery = '''INSERT INTO `transaction` (date, amount, type, note, idCategory, idBankAccount, idBankAccountTransfer, recurring, recurrencyType, recurrencyPayDay, recurrencyFrom, recurrencyTo, 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(); @@ -264,7 +247,7 @@ void main() { throw Exception('DbBase.cleanDatabase: $error'); } - const insertDemoTransactionsQuery = '''INSERT INTO `transaction` (date, amount, type, note, idCategory, idBankAccount, idBankAccountTransfer, recurring, recurrencyType, recurrencyPayDay, recurrencyFrom, recurrencyTo, 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(); @@ -322,7 +305,7 @@ void main() { throw Exception('DbBase.cleanDatabase: $error'); } - const insertDemoTransactionsQuery = '''INSERT INTO `transaction` (date, amount, type, note, idCategory, idBankAccount, idBankAccountTransfer, recurring, recurrencyType, recurrencyPayDay, recurrencyFrom, recurrencyTo, 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(); diff --git a/test/test_utils/sql_utils.dart b/test/test_utils/sql_utils.dart index 48b7a176..098f9214 100644 --- a/test/test_utils/sql_utils.dart +++ b/test/test_utils/sql_utils.dart @@ -10,7 +10,8 @@ String createInsertSqlTransaction({ int idBankAccount = 70, // Revolut int? idBankTransfert, bool recurring = false, - Recurrence? recurrencyType, + int? idRecurringTransaction, + Recurrence? recurrencyType, int? recurrencyPayDay, DateTime? recurrencyFrom, DateTime? recurrencyTo, @@ -20,5 +21,5 @@ String createInsertSqlTransaction({ createdAt = date; updatedAt = date; int recurringInt = recurring ? 1 : 0; - return '''('$date', $amount, '$type', '$note', $idCategory, $idBankAccount, $idBankTransfert, $recurringInt, $recurrencyType, $recurrencyPayDay, $recurrencyFrom, $recurrencyTo, '$createdAt', '$updatedAt')'''; + return '''('$date', $amount, '$type', '$note', $idCategory, $idBankAccount, $idBankTransfert, $recurringInt, $idRecurringTransaction, '$createdAt', '$updatedAt')'''; } \ No newline at end of file