diff --git a/lib/custom_widgets/transactions_list.dart b/lib/custom_widgets/transactions_list.dart index f8cd821..2626983 100644 --- a/lib/custom_widgets/transactions_list.dart +++ b/lib/custom_widgets/transactions_list.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import '../constants/constants.dart'; import '../constants/functions.dart'; @@ -41,289 +42,186 @@ class _TransactionsListState extends State with Functions { super.didUpdateWidget(oldWidget); } - updateTotal() { + void updateTotal() { totals = {}; - for (var transaction in transactions) { - String date = transaction.date.toYMD(); - if (totals.containsKey(date)) { - if (transaction.type == TransactionType.expense) { - totals[date] = totals[date]! - transaction.amount.toDouble(); - } else if (transaction.type == TransactionType.income) { - totals[date] = totals[date]! + transaction.amount.toDouble(); - } - } else { - if (transaction.type == TransactionType.expense) { - totals.putIfAbsent(date, () => -transaction.amount.toDouble()); - } else if (transaction.type == TransactionType.income) { - totals.putIfAbsent(date, () => transaction.amount.toDouble()); - } - } + for (final transaction in transactions) { + final date = transaction.date.toYMD(); + final currentTotal = totals[date] ?? 0.0; + + final amount = switch (transaction.type) { + TransactionType.expense => -transaction.amount.toDouble(), + TransactionType.income => transaction.amount.toDouble(), + TransactionType.transfer => 0.0, // Explicitly handle transfers + }; + + totals[date] = currentTotal + amount; } } @override Widget build(BuildContext context) { return transactions.isNotEmpty - ? SingleChildScrollView( - padding: widget.padding, - child: DefaultContainer( - child: Column( - children: transactions.map((transaction) { - int index = transactions.indexOf(transaction); - bool first = index == 0 || - !transaction.date - .isSameDate(transactions[index - 1].date); - bool last = index == transactions.length - 1 || - !transaction.date - .isSameDate(transactions[index + 1].date); - - return Column( - children: [ - if (first) - TransactionTitle( - date: transaction.date, - total: totals[transaction.date.toYMD()] ?? 0, - first: index == 0, - ), - TransactionRow(transaction, first: first, last: last), - ], - ); - }).toList(), - ), + ? DefaultContainer( + child: ListView.separated( + physics: const NeverScrollableScrollPhysics(), + padding: widget.padding, + shrinkWrap: true, + itemCount: totals.keys.length, + separatorBuilder: (_, __) => const SizedBox(height: 16), + itemBuilder: (context, monthIndex) { + // Group transactions by month + final dates = totals.keys.toList()..sort((a, b) => b.compareTo(a)); + final currentDate = dates[monthIndex]; + final dateTransactions = transactions.where((t) => t.date.toYMD() == currentDate).toList(); + + return Column( + children: [ + TransactionTitle(date: DateTime.parse(currentDate), total: totals[currentDate] ?? 0), + Container( + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(8), + ), + child: ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: dateTransactions.length, + separatorBuilder: (_, __) => Divider( + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.4), + ), + itemBuilder: (context, index) => TransactionTile(transaction: dateTransactions[index])), + ), + ], + ); + }, ), ) - : Container( - padding: const EdgeInsets.all(16), - margin: const EdgeInsets.all(16), - width: double.infinity, - child: const Center( - child: Text("No transactions available"), - ), + : const Center( + child: Text("No transactions available"), ); } } -class TransactionTitle extends ConsumerWidget with Functions { - final DateTime date; - final num total; - final bool first; +class TransactionTile extends ConsumerWidget with Functions { + const TransactionTile({required this.transaction, super.key}); - const TransactionTitle({ - super.key, - required this.date, - this.total = 0, - this.first = false, - }); + final Transaction transaction; @override Widget build(BuildContext context, WidgetRef ref) { final currencyState = ref.watch(currencyStateNotifier); - final color = total < 0 ? red : (total > 0 ? green : blue3); - return Padding( - padding: EdgeInsets.only(top: first ? 0 : 24), - child: Column( + return ListTile( + visualDensity: VisualDensity.compact, + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + onTap: () { + ref.read(transactionsProvider.notifier).transactionUpdateState(transaction).whenComplete(() { + if (context.mounted) { + Navigator.of(context) + .pushNamed("/add-page", arguments: {'recurrencyEditingPermitted': !transaction.recurring}); + } + }); + }, + leading: RoundedIcon( + icon: transaction.categorySymbol != null ? iconList[transaction.categorySymbol] : Icons.swap_horiz_rounded, + backgroundColor: transaction.categoryColor != null + ? categoryColorListTheme[transaction.categoryColor!] + : Theme.of(context).colorScheme.secondary, + size: 25, + padding: const EdgeInsets.all(8.0), + ), + title: Text( + (transaction.note?.isEmpty ?? true) + ? DateFormat("dd MMMM - HH:mm").format(transaction.date) + : transaction.note!, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + subtitle: Text( + transaction.categoryName ?? "Uncategorized", + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelMedium!.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, children: [ Row( + mainAxisSize: MainAxisSize.min, children: [ Text( - dateToString(date), - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Theme.of(context).colorScheme.primary), - ), - const Spacer(), - RichText( - text: TextSpan( - children: [ - TextSpan( - text: numToCurrency(total), - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(color: color), + '${transaction.type == TransactionType.expense ? "-" : ""}${numToCurrency(transaction.amount)}', + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: typeToColor( + transaction.type, + brightness: Theme.of(context).brightness, + ), ), - TextSpan( - text: currencyState.selectedCurrency.symbol, - style: Theme.of(context) - .textTheme - .labelMedium! - .copyWith(color: color), + ), + Text( + currencyState.selectedCurrency.symbol, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: typeToColor( + transaction.type, + brightness: Theme.of(context).brightness, + ), ), - ], - ), ), ], ), - const SizedBox(height: 8), + Text( + transaction.type == TransactionType.transfer + ? "${transaction.bankAccountName ?? ''}→${transaction.bankAccountTransferName ?? ''}" + : transaction.bankAccountName ?? '', + style: Theme.of(context).textTheme.labelMedium!.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), ], ), ); } } -class TransactionRow extends ConsumerWidget with Functions { - const TransactionRow(this.transaction, - {this.first = false, this.last = false, super.key}); +class TransactionTitle extends ConsumerWidget with Functions { + final DateTime date; + final num total; - final Transaction transaction; - final bool first; - final bool last; + const TransactionTitle({ + super.key, + required this.date, + required this.total, + }); @override Widget build(BuildContext context, WidgetRef ref) { final currencyState = ref.watch(currencyStateNotifier); - - return Column( - children: [ - Material( - borderRadius: BorderRadius.vertical( - top: first ? const Radius.circular(8) : Radius.zero, - bottom: last ? const Radius.circular(8) : Radius.zero, + final color = total < 0 ? red : (total > 0 ? green : blue3); + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + Text( + dateToString(date), + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: Theme.of(context).colorScheme.primary), ), - color: Theme.of(context).colorScheme.primaryContainer, - child: InkWell( - onTap: () { - ref - .read(transactionsProvider.notifier) - .transactionUpdateState(transaction) - .whenComplete(() { - if (context.mounted) { - Navigator.of(context).pushNamed("/add-page", arguments: { - 'recurrencyEditingPermitted': !transaction.recurring - }); - } - }); - }, - borderRadius: BorderRadius.vertical( - top: first ? const Radius.circular(8) : Radius.zero, - bottom: last ? const Radius.circular(8) : Radius.zero, - ), - child: Container( - padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - RoundedIcon( - icon: transaction.categorySymbol != null - ? iconList[transaction.categorySymbol] - : Icons.swap_horiz_rounded, - backgroundColor: transaction.categoryColor != null - ? categoryColorListTheme[transaction.categoryColor!] - : Theme.of(context).colorScheme.secondary, - size: 25, - padding: const EdgeInsets.all(8.0), - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - 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!, - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith( - color: - Theme.of(context).colorScheme.primary, - ), - ), - const Spacer(), - RichText( - text: TextSpan( - children: [ - TextSpan( - text: - '${transaction.type == TransactionType.expense ? "-" : ""}${numToCurrency(transaction.amount)}', - style: Theme.of(context) - .textTheme - .labelLarge! - .copyWith( - color: typeToColor( - transaction.type, - brightness: - Theme.of(context).brightness, - ), - ), - ), - TextSpan( - text: currencyState.selectedCurrency.symbol, - style: Theme.of(context) - .textTheme - .labelSmall! - .copyWith( - color: typeToColor( - transaction.type, - brightness: - Theme.of(context).brightness, - ), - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - if (transaction.categoryName != null) - Text( - transaction.categoryName!, - style: Theme.of(context) - .textTheme - .labelMedium! - .copyWith( - color: - Theme.of(context).colorScheme.primary, - ), - ), - const Spacer(), - Text( - transaction.type == TransactionType.transfer - ? "${transaction.bankAccountName ?? ''}→${transaction.bankAccountTransferName ?? ''}" - : transaction.bankAccountName ?? '', - style: Theme.of(context) - .textTheme - .labelMedium! - .copyWith( - color: - Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - const SizedBox(height: 11), - ], - ), - ), - ], - ), - ), + const Spacer(), + Text( + numToCurrency(total), + style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: color), ), - ), - if (!last) - Container( - color: Theme.of(context).colorScheme.primaryContainer, - child: Divider( - height: 1, - indent: 12, - endIndent: 12, - color: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.4), - ), + Text( + currencyState.selectedCurrency.symbol, + style: Theme.of(context).textTheme.labelMedium!.copyWith(color: color), ), - ], + ], + ), ); } } diff --git a/lib/model/bank_account.dart b/lib/model/bank_account.dart index 5d7f445..2148a3e 100644 --- a/lib/model/bank_account.dart +++ b/lib/model/bank_account.dart @@ -63,7 +63,7 @@ class BankAccount extends BaseEntity { bool? active, bool? mainAccount, DateTime? createdAt, - DateTime? updatedAt}) => + DateTime? updatedAt,}) => BankAccount( id: id ?? this.id, name: name ?? this.name, @@ -73,7 +73,9 @@ class BankAccount extends BaseEntity { active: active ?? this.active, mainAccount: mainAccount ?? this.mainAccount, createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt); + updatedAt: updatedAt ?? this.updatedAt, + total: total + ); static BankAccount fromJson(Map json) => BankAccount( id: json[BaseEntityFields.id] as int, diff --git a/lib/pages/account_page/account_page.dart b/lib/pages/account_page/account_page.dart index 02e1e78..3d5a2f9 100644 --- a/lib/pages/account_page/account_page.dart +++ b/lib/pages/account_page/account_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../constants/functions.dart'; @@ -7,6 +8,7 @@ import '../../custom_widgets/line_chart.dart'; import '../../custom_widgets/transactions_list.dart'; import '../../providers/accounts_provider.dart'; import '../../model/transaction.dart'; +import '../../providers/currency_provider.dart'; class AccountPage extends ConsumerStatefulWidget { const AccountPage({super.key}); @@ -16,12 +18,23 @@ class AccountPage extends ConsumerStatefulWidget { } class _AccountPage extends ConsumerState with Functions { + bool isRecoinciling = false; + final TextEditingController _newBalanceController = TextEditingController(); + + FocusNode focusNode = FocusNode(); + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final account = ref.read(selectedAccountProvider); - final accountTransactions = - ref.watch(selectedAccountCurrentMonthDailyBalanceProvider); + final accountTransactions = ref.watch(selectedAccountCurrentMonthDailyBalanceProvider); final transactions = ref.watch(selectedAccountLastTransactions); + final currencyState = ref.watch(currencyStateNotifier); return Scaffold( appBar: AppBar( @@ -35,6 +48,7 @@ class _AccountPage extends ConsumerState with Functions { ), body: SingleChildScrollView( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(vertical: 12.0), @@ -63,14 +77,116 @@ class _AccountPage extends ConsumerState with Functions { ], ), ), - Container( - padding: const EdgeInsets.only(top: 40.0), - child: TransactionsList( - transactions: transactions - .map((json) => Transaction.fromJson(json)) - .toList(), + Card( + margin: const EdgeInsets.all(16), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + spacing: 8, + children: [ + const Icon(Icons.info_outline), + Text("Balance Discrepancy?"), + ], + ), + const SizedBox(height: 8), + Text( + "Your recorder balance might differ from your bank's statement. Tap below to manually adjust your balance and keep your records accurate."), + const SizedBox(height: 16), + if (isRecoinciling) + Column( + children: [ + TextField( + focusNode: focusNode, + controller: _newBalanceController, + decoration: InputDecoration( + hintText: "New Balance", + border: OutlineInputBorder(), + prefixIcon: SizedBox( + width: 40, + child: Center( + child: Text( + currencyState.selectedCurrency.symbol, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + )), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'^\d*\.?\d{0,2}'),), + ], + ), + const SizedBox(height: 16), + Row( + spacing: 8, + children: [ + Expanded( + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + iconColor: Colors.white, + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + backgroundColor: Colors.green), + onPressed: () async { + if (account != null) { + await ref.read(accountsProvider.notifier).reconcileAccount( + newBalance: currencyToNum(_newBalanceController.text), account: account + ); + if (context.mounted) Navigator.of(context).pop(); + } + }, + label: const Text("Save"), + icon: const Icon(Icons.check), + ), + ), + Expanded( + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + iconColor: Colors.red, + side: const BorderSide(color: Colors.red), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + foregroundColor: Colors.red, + backgroundColor: Colors.transparent), + onPressed: () => setState(() => isRecoinciling = false), + label: const Text( + "Cancel", + style: TextStyle(fontSize: 14), + ), + icon: const Icon(Icons.cancel_outlined), + ), + ), + ], + ), + ], + ) + else + TextButton.icon( + onPressed: () { + setState(() => isRecoinciling = true); + focusNode.requestFocus(); + }, + icon: const Icon(Icons.sync), + label: Text( + "Start Reconciliation", + style: Theme.of(context).textTheme.bodyMedium, + )), + ], + ), ), ), + Padding( + padding: const EdgeInsets.only(left: 16, bottom: 8, top: 8), + child: Text("Your last transactions", style: Theme.of(context).textTheme.titleLarge), + ), + TransactionsList( + transactions: transactions.map((json) => Transaction.fromJson(json)).toList(), + ), ], ), ), diff --git a/lib/pages/accounts/add_account.dart b/lib/pages/accounts/add_account.dart index a1ba223..28a2401 100644 --- a/lib/pages/accounts/add_account.dart +++ b/lib/pages/accounts/add_account.dart @@ -17,7 +17,7 @@ class AddAccount extends ConsumerStatefulWidget { class _AddAccountState extends ConsumerState with Functions { final TextEditingController nameController = TextEditingController(); - final TextEditingController startingValueController = TextEditingController(); + final TextEditingController balanceController = TextEditingController(); String accountIcon = accountIconList.keys.first; int accountColor = 0; bool countNetWorth = true; @@ -30,7 +30,7 @@ class _AddAccountState extends ConsumerState with Functions { final selectedAccount = ref.read(selectedAccountProvider); if (selectedAccount != null) { nameController.text = selectedAccount.name; - startingValueController.text = selectedAccount.startingValue.toString(); + balanceController.text = selectedAccount.total.toString(); accountIcon = selectedAccount.symbol; accountColor = selectedAccount.color; countNetWorth = selectedAccount.active; @@ -42,7 +42,7 @@ class _AddAccountState extends ConsumerState with Functions { @override void dispose() { nameController.dispose(); - startingValueController.dispose(); + balanceController.dispose(); super.dispose(); } @@ -67,8 +67,7 @@ class _AddAccountState extends ConsumerState with Functions { children: [ Container( width: double.infinity, - margin: const EdgeInsets.symmetric( - horizontal: 16, vertical: 24), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, @@ -92,8 +91,7 @@ class _AddAccountState extends ConsumerState with Functions { Container( width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.symmetric( - horizontal: 16, vertical: 12), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(4), @@ -111,10 +109,8 @@ class _AddAccountState extends ConsumerState with Functions { Material( color: Colors.transparent, child: InkWell( - borderRadius: - const BorderRadius.all(Radius.circular(90)), - onTap: () => - setState(() => showAccountIcons = true), + borderRadius: const BorderRadius.all(Radius.circular(90)), + onTap: () => setState(() => showAccountIcons = true), child: Ink( decoration: BoxDecoration( shape: BoxShape.circle, @@ -148,17 +144,13 @@ class _AddAccountState extends ConsumerState with Functions { Align( alignment: Alignment.topRight, child: TextButton( - onPressed: () => setState( - () => showAccountIcons = false), + onPressed: () => setState(() => showAccountIcons = false), child: Text( "Done", style: Theme.of(context) .textTheme .bodyLarge! - .copyWith( - color: Theme.of(context) - .colorScheme - .secondary), + .copyWith(color: Theme.of(context).colorScheme.secondary), ), ), ), @@ -166,39 +158,27 @@ class _AddAccountState extends ConsumerState with Functions { itemCount: accountIconList.length, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 6, ), itemBuilder: (context, index) { - IconData accountIconData = - accountIconList.values.elementAt(index); - String accountIconName = - accountIconList.keys.elementAt(index); + IconData accountIconData = accountIconList.values.elementAt(index); + String accountIconName = accountIconList.keys.elementAt(index); return GestureDetector( - onTap: () => setState( - () => accountIcon = accountIconName), + onTap: () => setState(() => accountIcon = accountIconName), child: Container( margin: const EdgeInsets.all(4), decoration: BoxDecoration( - color: accountIconList[accountIcon] == - accountIconData - ? Theme.of(context) - .colorScheme - .secondary - : Theme.of(context) - .colorScheme - .surface, + color: accountIconList[accountIcon] == accountIconData + ? Theme.of(context).colorScheme.secondary + : Theme.of(context).colorScheme.surface, shape: BoxShape.circle, ), child: Icon( accountIconData, - color: accountIconList[accountIcon] == - accountIconData + color: accountIconList[accountIcon] == accountIconData ? white - : Theme.of(context) - .colorScheme - .primary, + : Theme.of(context).colorScheme.primary, size: 24, ), ), @@ -216,35 +196,23 @@ class _AddAccountState extends ConsumerState with Functions { scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 16), - separatorBuilder: (context, index) => - const SizedBox(width: 16), + separatorBuilder: (context, index) => const SizedBox(width: 16), itemBuilder: (context, index) { Color color = accountColorListTheme[index]; return GestureDetector( - onTap: () => - setState(() => accountColor = index), + onTap: () => setState(() => accountColor = index), child: Container( - height: accountColorListTheme[accountColor] == - color - ? 38 - : 32, - width: accountColorListTheme[accountColor] == - color - ? 38 - : 32, + height: accountColorListTheme[accountColor] == color ? 38 : 32, + width: accountColorListTheme[accountColor] == color ? 38 : 32, decoration: BoxDecoration( shape: BoxShape.circle, color: color, - border: - accountColorListTheme[accountColor] == - color - ? Border.all( - color: Theme.of(context) - .colorScheme - .primary, - width: 3, - ) - : null, + border: accountColorListTheme[accountColor] == color + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 3, + ) + : null, ), ), ); @@ -258,48 +226,44 @@ class _AddAccountState extends ConsumerState with Functions { style: Theme.of(context) .textTheme .labelMedium! - .copyWith( - color: Theme.of(context).colorScheme.primary), + .copyWith(color: Theme.of(context).colorScheme.primary), ), const SizedBox(height: 12), ], ), ), - if (selectedAccount == null) - Container( - width: double.infinity, - margin: const EdgeInsets.fromLTRB(16, 24, 16, 0), - padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(4), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "STARTING VALUE", - style: Theme.of(context).textTheme.labelLarge, + Container( + margin: const EdgeInsets.fromLTRB(16, 24, 16, 0), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(4), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${selectedAccount == null ? "INITIAL" : "CURRENT"} BALANCE", + style: Theme.of(context).textTheme.labelLarge, + ), + TextField( + controller: balanceController, + decoration: InputDecoration( + hintText: "${selectedAccount == null ? "Initial" : "Current"} Balance", + suffixText: currencyState.selectedCurrency.symbol, ), - TextField( - controller: startingValueController, - decoration: InputDecoration( - hintText: "Initial balance", - suffixText: currencyState.selectedCurrency.symbol, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow( + RegExp(r'^\d*\.?\d{0,2}'), ), - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp(r'^\d*\.?\d{0,2}'), - ), - ], - style: Theme.of(context).textTheme.titleLarge, - ), - ], - ), + ], + style: Theme.of(context).textTheme.titleLarge!.copyWith(color: grey1), + ), + ], ), + ), Container( - width: double.infinity, margin: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, @@ -323,8 +287,7 @@ class _AddAccountState extends ConsumerState with Functions { ), CupertinoSwitch( value: mainAccount, - onChanged: (value) => - setState(() => mainAccount = value), + onChanged: (value) => setState(() => mainAccount = value), ), ], ), @@ -341,8 +304,7 @@ class _AddAccountState extends ConsumerState with Functions { ), CupertinoSwitch( value: countNetWorth, - onChanged: (value) => - setState(() => countNetWorth = value), + onChanged: (value) => setState(() => countNetWorth = value), ), ], ), @@ -355,10 +317,8 @@ class _AddAccountState extends ConsumerState with Functions { width: double.infinity, padding: const EdgeInsets.all(16), child: TextButton.icon( - onPressed: () => ref - .read(accountsProvider.notifier) - .removeAccount(selectedAccount.id!) - .whenComplete(() { + onPressed: () => + ref.read(accountsProvider.notifier).removeAccount(selectedAccount.id!).whenComplete(() { if (context.mounted) { Navigator.of(context).pop(); } @@ -369,10 +329,7 @@ class _AddAccountState extends ConsumerState with Functions { icon: const Icon(Icons.delete_outlined, color: red), label: Text( "Delete account", - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(color: red), + style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: red), ), ), ), @@ -387,10 +344,7 @@ class _AddAccountState extends ConsumerState with Functions { color: Theme.of(context).colorScheme.surface, boxShadow: [ BoxShadow( - color: Theme.of(context) - .colorScheme - .primary - .withValues(alpha: 0.15), + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.15), blurRadius: 5.0, offset: const Offset(0, -1.0), ) @@ -413,6 +367,12 @@ class _AddAccountState extends ConsumerState with Functions { active: countNetWorth, mainAccount: mainAccount, ); + if (currencyToNum(balanceController.text) != selectedAccount.total) { + await ref.read(accountsProvider.notifier).reconcileAccount( + newBalance: currencyToNum(balanceController.text), + account: selectedAccount, + ); + } } else { await ref.read(accountsProvider.notifier).addAccount( name: nameController.text, @@ -420,8 +380,7 @@ class _AddAccountState extends ConsumerState with Functions { color: accountColor, active: countNetWorth, mainAccount: mainAccount, - startingValue: - currencyToNum(startingValueController.text), + startingValue: currencyToNum(balanceController.text), ); } if (context.mounted) Navigator.of(context).pop(); diff --git a/lib/pages/transactions_page/widgets/account_list_tile.dart b/lib/pages/transactions_page/widgets/account_list_tile.dart index 9f0c686..8ba9e00 100644 --- a/lib/pages/transactions_page/widgets/account_list_tile.dart +++ b/lib/pages/transactions_page/widgets/account_list_tile.dart @@ -75,10 +75,7 @@ class AccountListTile extends ConsumerWidget { ), Text( "${amount.toStringAsFixed(2)} ${currencyState.selectedCurrency.symbol}", - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: (amount > 0) ? green : red), + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: (amount > 0) ? green : red), ), ], ), @@ -100,9 +97,7 @@ class AccountListTile extends ConsumerWidget { ), const SizedBox(width: 8.0), Icon( - (selectedAccountIndex == index) - ? Icons.expand_more - : Icons.chevron_right, + (selectedAccountIndex == index) ? Icons.expand_more : Icons.chevron_right, ), ], ), @@ -166,8 +161,10 @@ class TransactionRow extends ConsumerWidget with Functions { ), Text( "${numToCurrency(transaction.amount)} ${currencyState.selectedCurrency.symbol}", - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: (transaction.amount > 0) ? green : red), + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(color: (transaction.amount > 0) ? green : red), ), ], ), @@ -175,7 +172,7 @@ class TransactionRow extends ConsumerWidget with Functions { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - transaction.categoryName?.toUpperCase() ?? "", + transaction.categoryName?.toUpperCase() ?? "Uncategorized", style: Theme.of(context).textTheme.labelLarge, ), Text( @@ -208,8 +205,7 @@ class ExpandedSection extends StatefulWidget { State createState() => _ExpandedSectionState(); } -class _ExpandedSectionState extends State - with SingleTickerProviderStateMixin { +class _ExpandedSectionState extends State with SingleTickerProviderStateMixin { late AnimationController expandController; late Animation animation; diff --git a/lib/providers/accounts_provider.dart b/lib/providers/accounts_provider.dart index da8c8a1..0c50609 100644 --- a/lib/providers/accounts_provider.dart +++ b/lib/providers/accounts_provider.dart @@ -2,12 +2,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fl_chart/fl_chart.dart'; import '../model/bank_account.dart'; +import '../model/transaction.dart'; +import 'transactions_provider.dart'; final mainAccountProvider = StateProvider((ref) => null); final selectedAccountProvider = StateProvider.autoDispose((ref) => null); -final selectedAccountCurrentMonthDailyBalanceProvider = - StateProvider>((ref) => const []); +final selectedAccountCurrentMonthDailyBalanceProvider = StateProvider>((ref) => const []); final selectedAccountLastTransactions = StateProvider((ref) => const []); final filterAccountProvider = StateProvider>((ref) => {}); @@ -72,7 +73,29 @@ class AsyncAccountsNotifier extends AsyncNotifier> { active: active, mainAccount: mainAccount, ); + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + await BankAccountMethods().updateItem(account); + if (account.mainAccount) { + ref.read(mainAccountProvider.notifier).state = account; + } + return _getAccounts(); + }); + } + Future reconcileAccount( + {required num newBalance, required BankAccount account}) async { + final num difference = newBalance - (account.total ?? 0); + if (difference != 0) { + final transactionsNotifier = ref.read(transactionsProvider.notifier); + await transactionsNotifier.addTransaction( + difference.abs(), + 'Reconciliation', + account: account, + type: difference > 0 ? TransactionType.income : TransactionType.expense, + date: DateTime.now(), + ); + } state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { await BankAccountMethods().updateItem(account); @@ -86,18 +109,13 @@ class AsyncAccountsNotifier extends AsyncNotifier> { Future refreshAccount(BankAccount account) async { ref.read(selectedAccountProvider.notifier).state = account; - final currentMonthDailyBalance = await BankAccountMethods().accountDailyBalance( - account.id!, - dateRangeStart: DateTime(DateTime.now().year, DateTime.now().month, - 1), // beginnig of current month - dateRangeEnd: DateTime(DateTime.now().year, DateTime.now().month + 1, - 1) // beginnig of next month + final currentMonthDailyBalance = await BankAccountMethods().accountDailyBalance(account.id!, + dateRangeStart: DateTime(DateTime.now().year, DateTime.now().month, 1), // beginnig of current month + dateRangeEnd: DateTime(DateTime.now().year, DateTime.now().month + 1, 1) // beginnig of next month ); - ref.read(selectedAccountCurrentMonthDailyBalanceProvider.notifier).state = - currentMonthDailyBalance.map((e) { - return FlSpot(double.parse(e['day'].substring(8)) - 1, - double.parse(e['balance'].toStringAsFixed(2))); + ref.read(selectedAccountCurrentMonthDailyBalanceProvider.notifier).state = currentMonthDailyBalance.map((e) { + return FlSpot(double.parse(e['day'].substring(8)) - 1, double.parse(e['balance'].toStringAsFixed(2))); }).toList(); ref.read(selectedAccountLastTransactions.notifier).state = @@ -118,7 +136,6 @@ class AsyncAccountsNotifier extends AsyncNotifier> { } } -final accountsProvider = - AsyncNotifierProvider>(() { +final accountsProvider = AsyncNotifierProvider>(() { return AsyncAccountsNotifier(); }); diff --git a/lib/providers/transactions_provider.dart b/lib/providers/transactions_provider.dart index 544cd71..df38a87 100644 --- a/lib/providers/transactions_provider.dart +++ b/lib/providers/transactions_provider.dart @@ -69,8 +69,8 @@ class AsyncTransactionsNotifier extends AutoDisposeAsyncNotifier( 0, @@ -97,25 +97,19 @@ class AsyncTransactionsNotifier extends AutoDisposeAsyncNotifier addTransaction(num amount, String label) async { + Future addTransaction(num amount, String label, + {BankAccount? account, DateTime? date, TransactionType? type}) async { state = const AsyncValue.loading(); - 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 = Transaction( - date: date, + date: date ?? ref.read(dateProvider), amount: amount, - type: type, + type: type ?? ref.read(transactionTypeProvider), note: label, - idBankAccount: bankAccount.id!, - idBankAccountTransfer: bankAccountTransfer?.id, - idCategory: category?.id, - recurring: recurring, + idBankAccount: (account ?? ref.read(bankAccountProvider))!.id!, + idBankAccountTransfer: account != null ? null : ref.read(bankAccountTransferProvider)?.id, + idCategory: account != null ? null : ref.read(categoryProvider)?.id, + recurring: account != null ? false : ref.read(selectedRecurringPayProvider), ); state = await AsyncValue.guard(() async { @@ -134,17 +128,16 @@ class AsyncTransactionsNotifier extends AutoDisposeAsyncNotifier addRecurringDataToTransaction(num idTransaction, num idRecurringTransaction) async { - } + Future addRecurringDataToTransaction(num idTransaction, num idRecurringTransaction) async {} Future transactionUpdateState(dynamic transaction) async { - if(transaction is Transaction) { + if (transaction is Transaction) { ref.read(selectedTransactionUpdateProvider.notifier).state = transaction; ref.read(selectedRecurringPayProvider.notifier).state = transaction.recurring; @@ -241,18 +230,17 @@ class AsyncTransactionsNotifier extends AutoDisposeAsyncNotifier element.id == transaction.idBankAccount); - ref.read(bankAccountTransferProvider.notifier).state = - transaction.type == TransactionType.transfer - ? accountList.value! - .firstWhere((element) => element.id == transaction.idBankAccountTransfer) - : null; + 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) { + } 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(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; } @@ -295,7 +283,6 @@ class AsyncTransactionsNotifier extends AutoDisposeAsyncNotifier>(() { +final transactionsProvider = AsyncNotifierProvider.autoDispose>(() { return AsyncTransactionsNotifier(); });