diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 44961b0..a0192a8 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -17,12 +17,12 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v2 with: - flutter-version: '3.13.9' + flutter-version: '3.16.5' channel: stable cache: true cache-key: flutter cache-path: ${{ runner.tool_cache }}/flutter - + - run: | flutter pub get flutter test diff --git a/lib/constants/functions.dart b/lib/constants/functions.dart index d824795..932a649 100644 --- a/lib/constants/functions.dart +++ b/lib/constants/functions.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; + import '../constants/style.dart'; import '../model/transaction.dart'; mixin Functions { String numToCurrency(num? value) { if (value == null) return ''; - return value.toStringAsFixed(2).replaceAll(".", ","); + return value.toStringAsFixed(2); } num currencyToNum(String value) { @@ -21,13 +22,13 @@ mixin Functions { return format.format(date); } - Color typeToColor(Type type) { + Color typeToColor(TransactionType type) { switch (type) { - case Type.income: + case TransactionType.income: return green; - case Type.expense: + case TransactionType.expense: return red; - case Type.transfer: + case TransactionType.transfer: return blue3; default: return blue3; diff --git a/lib/custom_widgets/account_modal.dart b/lib/custom_widgets/account_modal.dart index 05efbef..b338181 100644 --- a/lib/custom_widgets/account_modal.dart +++ b/lib/custom_widgets/account_modal.dart @@ -80,8 +80,6 @@ class AccountDialog extends StatelessWidget with Functions { line2Data: [], colorLine2Data: Color(0xffffffff), colorBackground: Color(0xff356CA3), - maxY: 5.0, - minY: -5.0, maxDays: 30.0, ), const Padding( diff --git a/lib/custom_widgets/accounts_sum.dart b/lib/custom_widgets/accounts_sum.dart index d0112f2..819679f 100644 --- a/lib/custom_widgets/accounts_sum.dart +++ b/lib/custom_widgets/accounts_sum.dart @@ -13,9 +13,9 @@ class AccountsSum extends ConsumerWidget with Functions { final BankAccount account; const AccountsSum({ - Key? key, + super.key, required this.account, - }) : super(key: key); + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -40,8 +40,7 @@ class AccountsSum extends ConsumerWidget with Functions { await ref .read(accountsProvider.notifier) .selectedAccount(account) - .whenComplete( - () => Navigator.of(context).pushNamed('/account')); + .whenComplete(() => Navigator.of(context).pushNamed('/account')); }, child: Container( padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), @@ -68,53 +67,26 @@ class AccountsSum extends ConsumerWidget with Functions { children: [ Text( account.name, - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(color: darkBlue7), - ), // TODO: set dinamically instead of hardcoded - FutureBuilder( - future: BankAccountMethods().getAccountSum(account.id), - builder: (context, snapshot) { - if (snapshot.connectionState == - ConnectionState.waiting) { - // Show a loading indicator while waiting for the future to complete - return Transform.scale( - scale: 0.5, - child: const CircularProgressIndicator(), - ); - } else if (snapshot.hasError) { - // Show an error message if the future encounters an error - return Text('Error: ${snapshot.error}'); - } else { - // Display the result once the future completes successfully - final accountSum = snapshot.data ?? 0; - return RichText( - textScaleFactor: - MediaQuery.of(context).textScaleFactor, - text: TextSpan( - children: [ - TextSpan( - text: numToCurrency(accountSum), - style: - Theme.of(context).textTheme.titleSmall!.copyWith(color: darkBlue7), - ), - TextSpan( - text: "€", - style: Theme.of(context) - .textTheme - .bodyMedium - ?.apply( - fontFeatures: [ - const FontFeature.subscripts() - ], - ).copyWith(color: darkBlue7), - ), - ], - ), - ); - } - }, + style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: darkBlue7), + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: numToCurrency(account.total), + style: Theme.of(context) + .textTheme + .titleSmall! + .copyWith(color: darkBlue7), + ), + TextSpan( + text: "€", + style: Theme.of(context).textTheme.bodyMedium?.apply( + fontFeatures: [const FontFeature.subscripts()], + ).copyWith(color: darkBlue7), + ), + ], + ), ), ], ), diff --git a/lib/custom_widgets/default_card.dart b/lib/custom_widgets/default_card.dart new file mode 100644 index 0000000..ea953f4 --- /dev/null +++ b/lib/custom_widgets/default_card.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'default_container.dart'; + +class DefaultCard extends StatefulWidget { + const DefaultCard({required this.child, required this.onTap, super.key}); + + final Widget child; + final GestureTapCallback? onTap; + + @override + State createState() => _DefaultCardState(); +} + +class _DefaultCardState extends State { + @override + Widget build(BuildContext context) { + return DefaultContainer( + padding: const EdgeInsets.all(0), + child: Material( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: widget.onTap, + child: Ink( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(8), + ), + child: widget.child, + ), + ), + ), + ); + } +} diff --git a/lib/custom_widgets/default_container.dart b/lib/custom_widgets/default_container.dart index e498a59..913ba50 100644 --- a/lib/custom_widgets/default_container.dart +++ b/lib/custom_widgets/default_container.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; + import '../constants/style.dart'; class DefaultContainer extends StatefulWidget { - const DefaultContainer({required this.child, required this.onTap, super.key}); + const DefaultContainer({required this.child, this.padding = const EdgeInsets.all(16.0), super.key}); final Widget child; - final GestureTapCallback? onTap; + final EdgeInsetsGeometry? padding; @override State createState() => _DefaultContainerState(); @@ -15,28 +16,14 @@ class _DefaultContainerState extends State { @override Widget build(BuildContext context) { return Container( - margin: const EdgeInsets.symmetric(horizontal: 16.0), + padding: widget.padding, + margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, borderRadius: BorderRadius.circular(8), boxShadow: [defaultShadow], ), - child: Material( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: widget.onTap, - child: Ink( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), - ), - child: widget.child, - ), - ), - ), + child: widget.child, ); } } diff --git a/lib/custom_widgets/line_chart.dart b/lib/custom_widgets/line_chart.dart index 08d4408..fa56ef8 100644 --- a/lib/custom_widgets/line_chart.dart +++ b/lib/custom_widgets/line_chart.dart @@ -3,31 +3,25 @@ import 'package:flutter/material.dart'; //This class can be used when we need to draw a line chart with one or two lines class LineChartWidget extends StatefulWidget { - final line1Data; //this should be a list of Flspot(x,y) - final colorLine1Data; + final List line1Data; //this should be a list of Flspot(x,y) + final Color colorLine1Data; - final line2Data; //this should be a list of Flspot(x,y), if you only need one just put an empty list - final colorLine2Data; - - //These will be used to determine the max value of the chart in order to get the right visualization of the data - final maxY; - final minY; + final List line2Data; //this should be a list of Flspot(x,y), if you only need one just put an empty list + final Color colorLine2Data; //Contains the number of days of the month - final maxDays; + final double maxDays; - final colorBackground; + final Color colorBackground; const LineChartWidget({ - super.key, - required this.line1Data, - required this.colorLine1Data, + super.key, + required this.line1Data, + required this.colorLine1Data, required this.line2Data, - required this.colorLine2Data, + required this.colorLine2Data, required this.colorBackground, - required this.maxY, - required this.minY, - required this.maxDays, - }); + this.maxDays = 31, + }); @override State createState() => _LineChartSample2State(); @@ -39,7 +33,7 @@ class _LineChartSample2State extends State { @override Widget build(BuildContext context) { return Stack( - children: [ + children: [ AspectRatio( aspectRatio: 2, child: DecoratedBox( @@ -50,11 +44,7 @@ class _LineChartSample2State extends State { color: widget.colorBackground, ), child: Padding( - padding: const EdgeInsets.only( - right: 18, - left: 12, - top: 24, - ), + padding: const EdgeInsets.only(top: 24), child: LineChart( mainData(), ), @@ -72,7 +62,7 @@ class _LineChartSample2State extends State { fontSize: 8, ); Widget text; - switch(widget.maxDays) { + switch (widget.maxDays) { case 12: switch (value.toInt()) { case 0: @@ -89,7 +79,7 @@ class _LineChartSample2State extends State { break; case 8: text = Text('Sep', style: style); - break; + break; case 10: text = Text('Nov', style: style); break; @@ -99,30 +89,30 @@ class _LineChartSample2State extends State { } break; case 31: - switch (value.toInt()) { - case 3: - text = Text('4', style: style); - break; - case 10: - text = Text('11', style: style); - break; - case 17: - text = Text('18', style: style); - break; - case 24: - text = Text('25', style: style); - break; - case 30: - text = Text('31', style: style); - break; - default: - text = Text('', style: style); - break; - } + switch (value.toInt()) { + case 3: + text = Text('4', style: style); + break; + case 10: + text = Text('11', style: style); + break; + case 17: + text = Text('18', style: style); + break; + case 24: + text = Text('25', style: style); + break; + case 30: + text = Text('31', style: style); + break; + default: + text = Text('', style: style); + break; + } + break; + default: + text = Text('', style: style); break; - default: - text = Text('', style: style); - break; } return SideTitleWidget( @@ -135,15 +125,13 @@ class _LineChartSample2State extends State { return LineChartData( titlesData: FlTitlesData( show: true, - rightTitles: AxisTitles( + rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), - topTitles: AxisTitles( + topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), - leftTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false) - ), + leftTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, @@ -153,14 +141,14 @@ class _LineChartSample2State extends State { ), ), ), - gridData: FlGridData(show: false), + gridData: const FlGridData(show: false), borderData: FlBorderData( - border: const Border(bottom: BorderSide(color: Colors.grey, width: 1.0, style: BorderStyle.solid)) - ), + border: const Border( + bottom: BorderSide(color: Colors.grey, width: 1.0, style: BorderStyle.solid), + ), + ), minX: 0, - maxX: widget.maxDays-1, - minY: widget.minY, - maxY: widget.maxY, + maxX: widget.maxDays - 1, lineBarsData: [ LineChartBarData( spots: widget.line1Data, @@ -168,13 +156,10 @@ class _LineChartSample2State extends State { barWidth: 1.5, isStrokeCapRound: true, color: widget.colorLine1Data, - dotData: FlDotData( + dotData: const FlDotData( show: false, ), - belowBarData: BarAreaData( - show: true, - color: widget.colorLine1Data.withOpacity(0.3) - ), + belowBarData: BarAreaData(show: true, color: widget.colorLine1Data.withOpacity(0.3)), ), LineChartBarData( spots: widget.line2Data, @@ -182,11 +167,11 @@ class _LineChartSample2State extends State { barWidth: 1, isStrokeCapRound: true, color: widget.colorLine2Data, - dotData: FlDotData( + dotData: const FlDotData( show: false, ), ), ], ); } -} \ No newline at end of file +} diff --git a/lib/custom_widgets/transactions_list.dart b/lib/custom_widgets/transactions_list.dart index 7242b44..8f10004 100644 --- a/lib/custom_widgets/transactions_list.dart +++ b/lib/custom_widgets/transactions_list.dart @@ -1,82 +1,97 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../constants/constants.dart'; -import '../providers/transactions_provider.dart'; import '../constants/functions.dart'; -import '../model/bank_account.dart'; -import '../model/category_transaction.dart'; -import '../model/transaction.dart'; -import '../providers/accounts_provider.dart'; -import '../providers/categories_provider.dart'; import '../constants/style.dart'; +import '../model/transaction.dart'; +import '../providers/transactions_provider.dart'; +import '../utils/date_helper.dart'; +import 'default_container.dart'; class TransactionsList extends StatefulWidget { - final List transactions; - const TransactionsList({ super.key, required this.transactions, + this.padding, }); + final List transactions; + final EdgeInsetsGeometry? padding; + @override State createState() => _TransactionsListState(); } class _TransactionsListState extends State with Functions { - List list = []; + Map totals = {}; + List get transactions => widget.transactions; @override void initState() { - num sum = 0; - DateTime? date; - List transactionList = []; - for (var transaction in widget.transactions) { - if (transaction != widget.transactions.first && - dateToString(transaction.date) != dateToString(date!)) { - final title = TransactionTitle(date: date, sum: sum, first: list.isEmpty ? true : false); - final transactionRow = TransactionRow(transactions: transactionList); - list = [...list, title, transactionRow]; - transactionList = []; - sum = 0; - } - date = transaction.date; - transactionList = [...transactionList, transaction]; - if (transaction.type == Type.expense) { - sum -= transaction.amount; - } else if (transaction.type == Type.income) { - sum += transaction.amount; - } - if (transaction == widget.transactions.last) { - final title = TransactionTitle(date: date, sum: sum, first: list.isEmpty ? true : false); - final transactionRow = TransactionRow(transactions: transactionList); - list = [...list, title, transactionRow]; + updateTotal(); + super.initState(); + } + + @override + void didUpdateWidget(covariant TransactionsList oldWidget) { + updateTotal(); + super.didUpdateWidget(oldWidget); + } + + 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()); + } } } - super.initState(); } @override Widget build(BuildContext context) { - return list.isNotEmpty - ? Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - width: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8), - boxShadow: [defaultShadow], - ), - padding: const EdgeInsets.all(16), - child: Column( - children: list, + 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(), + ), ), - ) + ) : Container( + padding: const EdgeInsets.all(16), margin: const EdgeInsets.all(16), width: double.infinity, - padding: const EdgeInsets.all(16), child: const Center( child: Text("No transactions available"), ), @@ -86,19 +101,19 @@ class _TransactionsListState extends State with Functions { class TransactionTitle extends StatelessWidget with Functions { final DateTime date; - final num sum; + final num total; final bool first; const TransactionTitle({ super.key, required this.date, - required this.sum, - required this.first, + this.total = 0, + this.first = false, }); @override Widget build(BuildContext context) { - final color = sum >= 0 ? green : red; + final color = total < 0 ? red : (total > 0 ? green : blue3); return Padding( padding: EdgeInsets.only(top: first ? 0 : 24), child: Column( @@ -114,11 +129,10 @@ class TransactionTitle extends StatelessWidget with Functions { ), const Spacer(), RichText( - textScaleFactor: MediaQuery.of(context).textScaleFactor, text: TextSpan( children: [ TextSpan( - text: numToCurrency(sum), + text: numToCurrency(total), style: Theme.of(context).textTheme.bodyLarge!.copyWith(color: color), ), TextSpan( @@ -126,8 +140,7 @@ class TransactionTitle extends StatelessWidget with Functions { style: Theme.of(context) .textTheme .labelMedium! - .copyWith(color: color) - .apply(fontFeatures: [const FontFeature.subscripts()]), + .copyWith(color: color), ), ], ), @@ -142,187 +155,136 @@ class TransactionTitle extends StatelessWidget with Functions { } class TransactionRow extends ConsumerWidget with Functions { - final List transactions; + const TransactionRow(this.transaction, {this.first = false, this.last = false, super.key}); - const TransactionRow({ - super.key, - required this.transactions, - }); + final Transaction transaction; + final bool first; + final bool last; @override Widget build(BuildContext context, WidgetRef ref) { - final accountList = ref.watch(accountsProvider); - final categoriesList = ref.watch(categoriesProvider); - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(8), - ), - child: ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: transactions.length, - scrollDirection: Axis.vertical, - shrinkWrap: true, - itemBuilder: (context, i) { - Transaction transaction = transactions[i]; - Iterable? categories = - transaction.type != Type.transfer && categoriesList.value != null - ? categoriesList.value!.where((element) => element.id == transaction.idCategory) - : []; - CategoryTransaction? category = categories.isNotEmpty ? categories.first : null; - BankAccount account = - accountList.value!.firstWhere((element) => element.id == transaction.idBankAccount); - BankAccount? accountTransfer = transaction.type == Type.transfer - ? accountList.value! - .firstWhere((element) => element.id == transaction.idBankAccountTransfer) - : null; - return Column( - children: [ - Material( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context).colorScheme.primaryContainer, - child: InkWell( - onTap: () { - ref.read(selectedTransactionUpdateProvider.notifier).state = - transaction; - ref - .read(transactionsProvider.notifier) - .transactionUpdateState(); - ref - .read(transactionsProvider.notifier) - .transactionUpdateState() - .whenComplete( - () => Navigator.of(context).pushNamed("/add-page"), - ); - }, - borderRadius: BorderRadius.vertical( - top: i == 0 ? const Radius.circular(8) : Radius.zero, - bottom: transactions.length == i + 1 ? const Radius.circular(8) : Radius.zero, + return Column( + children: [ + Material( + borderRadius: BorderRadius.vertical( + top: first ? const Radius.circular(8) : Radius.zero, + bottom: last ? const Radius.circular(8) : Radius.zero, + ), + color: Theme.of(context).colorScheme.primaryContainer, + child: InkWell( + onTap: () { + ref + .read(transactionsProvider.notifier) + .transactionUpdateState(transaction) + .whenComplete(() => Navigator.of(context).pushNamed("/add-page")); + }, + 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: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: transaction.categoryColor != null + ? categoryColorListTheme[transaction.categoryColor!] + : Theme.of(context).colorScheme.secondary, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + transaction.categorySymbol != null + ? iconList[transaction.categorySymbol] + : Icons.swap_horiz_rounded, + size: 25.0, + color: white, + ), + ), ), - child: Container( - padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: category?.color != null - ? categoryColorListTheme[category!.color] - : Theme.of(context).colorScheme.secondary, - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - category?.symbol != null - ? iconList[category!.symbol] - : Icons.swap_horiz_rounded, - size: 25.0, - color: white, - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 11), - Row( - children: [ - if (transaction.note != null) - Text( - transaction.note!, - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - const Spacer(), - RichText( - textScaleFactor: MediaQuery.of(context).textScaleFactor, - text: TextSpan( - children: [ - TextSpan( - text: - '${transaction.type == Type.expense ? "-" : ""}${numToCurrency(transaction.amount)}', - style: Theme.of(context) - .textTheme - .labelLarge! - .copyWith(color: typeToColor(transaction.type)), - ), - TextSpan( - text: "€", - style: Theme.of(context) - .textTheme - .labelSmall! - .copyWith(color: typeToColor(transaction.type)) - .apply( - fontFeatures: [const FontFeature.subscripts()], - ), - ), - ], + const SizedBox(height: 11), + Row( + children: [ + 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)), + ), + TextSpan( + text: "€", + style: Theme.of(context) + .textTheme + .labelSmall! + .copyWith(color: typeToColor(transaction.type)), ), ], ), - const SizedBox(height: 12), - transaction.type == Type.transfer - ? Row( - children: [ - Text( - account.name, - style: Theme.of(context).textTheme.labelMedium!.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - const Spacer(), - Text( - accountTransfer!.name, - style: Theme.of(context).textTheme.labelMedium!.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ], - ) - : Row( - children: [ - if (category != null) - Text( - category.name, - style: - Theme.of(context).textTheme.labelMedium!.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - const Spacer(), - Text( - account.name, - style: Theme.of(context).textTheme.labelMedium!.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ], + ), + ], + ), + 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 SizedBox(height: 11), - ], - ), + ), + 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), ], ), ), - ), + ], ), - if (transactions.length != i + 1) - Divider( - height: 1, - indent: 12, - endIndent: 12, - color: Theme.of(context).colorScheme.primary.withOpacity(0.4), - ), - ], - ); - }, - ), + ), + ), + ), + if (!last) + Container( + color: Theme.of(context).colorScheme.background, + child: Divider( + height: 1, + indent: 12, + endIndent: 12, + color: Theme.of(context).colorScheme.primary.withOpacity(0.4), + ), + ), + ], ); } } diff --git a/lib/database/sossoldi_database.dart b/lib/database/sossoldi_database.dart index a795c98..3bc7ad5 100644 --- a/lib/database/sossoldi_database.dart +++ b/lib/database/sossoldi_database.dart @@ -1,15 +1,15 @@ +import 'dart:math'; // used for random number generation in demo data + import 'package:path/path.dart'; import 'package:sqflite/sqflite.dart'; -import 'dart:math'; // used for random number generation in demo data - // Models import '../model/bank_account.dart'; -import '../model/transaction.dart'; -import '../model/recurring_transaction_amount.dart'; -import '../model/category_transaction.dart'; import '../model/budget.dart'; +import '../model/category_transaction.dart'; import '../model/currency.dart'; +import '../model/recurring_transaction_amount.dart'; +import '../model/transaction.dart'; class SossoldiDatabase { static final SossoldiDatabase instance = SossoldiDatabase._init(); @@ -211,6 +211,7 @@ class SossoldiDatabase { randomType = 'TRSF'; randomNote = 'Transfer'; randomAccount = 70; // sender account is hardcoded with the one that receives our fake salary + randomCategory = 0; // no category for transfers idBankAccountTransfer = accounts[rnd.nextInt(accounts.length)]; randomAmount = (fakeSalary/100)*70; diff --git a/lib/main.dart b/lib/main.dart index 55fac8e..204bb67 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sossoldi/providers/theme_provider.dart'; -import 'package:sossoldi/utils/app_theme.dart'; -import 'routes.dart'; import 'package:intl/date_symbol_data_local.dart'; +import 'providers/theme_provider.dart'; +import 'routes.dart'; +import 'utils/app_theme.dart'; + void main() { initializeDateFormatting('it_IT', null) .then((_) => runApp(const ProviderScope(child: Launcher()))); diff --git a/lib/model/bank_account.dart b/lib/model/bank_account.dart index c7e997e..25e11e0 100644 --- a/lib/model/bank_account.dart +++ b/lib/model/bank_account.dart @@ -1,8 +1,8 @@ -import 'package:sossoldi/model/transaction.dart'; import 'package:sqflite/sqflite.dart'; import '../database/sossoldi_database.dart'; import 'base_entity.dart'; +import 'transaction.dart'; const String bankAccountTable = 'bankAccount'; @@ -14,6 +14,7 @@ class BankAccountFields extends BaseEntityFields { static String startingValue = 'startingValue'; static String active = 'active'; static String mainAccount = 'mainAccount'; + static String total = 'total'; static String createdAt = BaseEntityFields.getCreatedAt; static String updatedAt = BaseEntityFields.getUpdatedAt; @@ -37,18 +38,19 @@ class BankAccount extends BaseEntity { final num startingValue; final bool active; final bool mainAccount; + final num? total; const BankAccount( - {int? id, + {super.id, required this.name, required this.symbol, required this.color, required this.startingValue, - required this.mainAccount, required this.active, - DateTime? createdAt, - DateTime? updatedAt}) - : super(id: id, createdAt: createdAt, updatedAt: updatedAt); + required this.mainAccount, + this.total, + super.createdAt, + super.updatedAt}); BankAccount copy( {int? id, @@ -79,6 +81,7 @@ class BankAccount extends BaseEntity { startingValue: json[BankAccountFields.startingValue] as num, active: json[BankAccountFields.active] == 1 ? true : false, mainAccount: json[BankAccountFields.mainAccount] == 1 ? true : false, + total: json[BankAccountFields.total] as num?, createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), updatedAt: DateTime.parse(json[BaseEntityFields.updatedAt] as String)); @@ -101,7 +104,7 @@ class BankAccountMethods extends SossoldiDatabase { final db = await database; await changeMainAccount(db, item); - + final id = await db.insert(bankAccountTable, item.toJson()); return item.copy(id: id); } @@ -144,9 +147,21 @@ class BankAccountMethods extends SossoldiDatabase { final db = await database; final orderByASC = '${BankAccountFields.createdAt} ASC'; - final where = '${BankAccountFields.active} = 1'; - - final result = await db.query(bankAccountTable, where:where, orderBy: orderByASC); + final where = '${BankAccountFields.active} = 1'; + + final result = await db.rawQuery(''' + SELECT b.*, (b.${BankAccountFields.startingValue} + + SUM(CASE WHEN t.${TransactionFields.type} = 'IN' OR t.${TransactionFields.type} = 'TRSF' AND t.${TransactionFields.idBankAccountTransfer} = b.${BankAccountFields.id} THEN t.${TransactionFields.amount} + ELSE 0 END) - + SUM(CASE WHEN t.${TransactionFields.type} = 'OUT' OR t.${TransactionFields.type} = 'TRSF' AND t.${TransactionFields.idBankAccount} = b.${BankAccountFields.id} THEN t.${TransactionFields.amount} + ELSE 0 END) + ) as ${BankAccountFields.total} + FROM $bankAccountTable as b + LEFT JOIN "$transactionTable" as t ON t.${TransactionFields.idBankAccount} = b.${BankAccountFields.id} OR t.${TransactionFields.idBankAccountTransfer} = b.${BankAccountFields.id} + WHERE $where + GROUP BY b.${BankAccountFields.id} + ORDER BY $orderByASC + '''); return result.map((json) => BankAccount.fromJson(json)).toList(); } diff --git a/lib/model/budget.dart b/lib/model/budget.dart index 165a1a1..05cfb91 100644 --- a/lib/model/budget.dart +++ b/lib/model/budget.dart @@ -111,9 +111,9 @@ class BudgetMethods extends SossoldiDatabase { } Future> selectAllActive() async { - final database = await SossoldiDatabase.instance.database; + final db = await database; final orderByASC = '${BudgetFields.createdAt} ASC'; - final result = await database.rawQuery( + final result = await db.rawQuery( 'SELECT bt.*, ct.name FROM $budgetTable as bt LEFT JOIN $categoryTransactionTable as ct ON bt.${BudgetFields.idCategory} = ct.${CategoryTransactionFields.id} WHERE bt.active = 1 ORDER BY $orderByASC'); return result.map((json) => Budget.fromJson(json)).toList(); } diff --git a/lib/model/transaction.dart b/lib/model/transaction.dart index 3ba255d..8774938 100644 --- a/lib/model/transaction.dart +++ b/lib/model/transaction.dart @@ -1,5 +1,7 @@ import '../database/sossoldi_database.dart'; +import 'bank_account.dart'; import 'base_entity.dart'; +import 'category_transaction.dart'; const String transactionTable = 'transaction'; @@ -10,8 +12,13 @@ class TransactionFields extends BaseEntityFields { static String type = 'type'; static String note = 'note'; static String idCategory = 'idCategory'; // FK + static String categoryName = 'categoryName'; + static String categoryColor = 'categoryColor'; + static String categorySymbol = 'categorySymbol'; static String idBankAccount = 'idBankAccount'; // FK + static String bankAccountName = 'bankAccountName'; static String idBankAccountTransfer = 'idBankAccountTransfer'; + static String bankAccountTransferName = 'bankAccountTransferName'; static String recurring = 'recurring'; static String recurrencyType = 'recurrencyType'; static String recurrencyPayDay = 'recurrencyPayDay'; @@ -39,13 +46,14 @@ class TransactionFields extends BaseEntityFields { ]; } -enum Type { income, expense, transfer } +enum TransactionType { income, expense, transfer } + enum Recurrence { daily, weekly, monthly, bimonthly, quarterly, semester, annual } -Map typeMap = { - "IN": Type.income, - "OUT": Type.expense, - "TRSF": Type.transfer, +Map typeMap = { + "IN": TransactionType.income, + "OUT": TransactionType.expense, + "TRSF": TransactionType.transfer, }; Map recurrenceMap = { Recurrence.daily: "Daily", @@ -60,11 +68,16 @@ Map recurrenceMap = { class Transaction extends BaseEntity { final DateTime date; final num amount; - final Type type; + final TransactionType type; final String? note; final int? idCategory; + final String? categoryName; + final int? categoryColor; + final String? categorySymbol; final int idBankAccount; + final String? bankAccountName; final int? idBankAccountTransfer; + final String? bankAccountTransferName; final bool recurring; final String? recurrencyType; final int? recurrencyPayDay; @@ -72,56 +85,59 @@ class Transaction extends BaseEntity { final DateTime? recurrencyTo; const Transaction( - {int? id, + {super.id, required this.date, required this.amount, required this.type, this.note, this.idCategory, + this.categoryName, + this.categoryColor, + this.categorySymbol, required this.idBankAccount, + this.bankAccountName, this.idBankAccountTransfer, + this.bankAccountTransferName, required this.recurring, this.recurrencyType, this.recurrencyPayDay, this.recurrencyFrom, this.recurrencyTo, - DateTime? createdAt, - DateTime? updatedAt}) - : super(id: id, createdAt: createdAt, updatedAt: updatedAt); + super.createdAt, + super.updatedAt}); Transaction copy( - {int? id, - DateTime? date, - num? amount, - Type? type, - String? note, - int? idCategory, - int? idBankAccount, - int? idBankAccountTransfer, - bool? recurring, - String? recurrencyType, - int? recurrencyPayDay, - DateTime? recurrencyFrom, - DateTime? recurrencyTo, - DateTime? createdAt, - DateTime? updatedAt}) => - Transaction( - id: id ?? this.id, - date: date ?? this.date, - amount: amount ?? this.amount, - type: type ?? this.type, - note: note ?? this.note, - idCategory: idCategory ?? this.idCategory, - 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, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt - ); + {int? id, + DateTime? date, + num? amount, + TransactionType? type, + String? note, + int? idCategory, + int? idBankAccount, + int? idBankAccountTransfer, + bool? recurring, + String? recurrencyType, + int? recurrencyPayDay, + DateTime? recurrencyFrom, + DateTime? recurrencyTo, + DateTime? createdAt, + DateTime? updatedAt}) => + Transaction( + id: id ?? this.id, + date: date ?? this.date, + amount: amount ?? this.amount, + type: type ?? this.type, + note: note ?? this.note, + idCategory: idCategory ?? this.idCategory, + 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, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt); static Transaction fromJson(Map json) => Transaction( id: json[BaseEntityFields.id] as int?, @@ -130,16 +146,24 @@ class Transaction extends BaseEntity { type: typeMap[json[TransactionFields.type] as String]!, note: json[TransactionFields.note] as String?, idCategory: json[TransactionFields.idCategory] as int?, + categoryName: json[TransactionFields.categoryName] as String?, + categoryColor: json[TransactionFields.categoryColor] as int?, + categorySymbol: json[TransactionFields.categorySymbol] as String?, idBankAccount: json[TransactionFields.idBankAccount] as int, + bankAccountName: json[TransactionFields.bankAccountName] as String?, 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, + recurrencyFrom: json[TransactionFields.recurrencyFrom] != null + ? DateTime.parse(TransactionFields.recurrencyFrom) + : null, + recurrencyTo: json[TransactionFields.recurrencyTo] != null + ? DateTime.parse(TransactionFields.recurrencyTo) + : null, createdAt: DateTime.parse(json[BaseEntityFields.createdAt] as String), - updatedAt: DateTime.parse(json[BaseEntityFields.updatedAt] as String) - ); + updatedAt: DateTime.parse(json[BaseEntityFields.updatedAt] as String)); Map toJson({bool update = false}) => { TransactionFields.id: id, @@ -170,13 +194,7 @@ class TransactionMethods extends SossoldiDatabase { Future selectById(int id) async { final db = await database; - - final maps = await db.query( - transactionTable, - columns: TransactionFields.allFields, - where: '${TransactionFields.id} = ?', - whereArgs: [id], - ); + final maps = await db.rawQuery('SELECT t.*, c.${CategoryTransactionFields.name} as ${TransactionFields.categoryName}, c.${CategoryTransactionFields.color} as ${TransactionFields.categoryColor}, c.${CategoryTransactionFields.symbol} as ${TransactionFields.categorySymbol}, b1.${BankAccountFields.name} as ${TransactionFields.bankAccountName}, b2.${BankAccountFields.name} as ${TransactionFields.bankAccountTransferName} FROM $transactionTable as t LEFT JOIN $categoryTransactionTable as c ON t.${TransactionFields.idCategory} = c.${CategoryTransactionFields.id} LEFT JOIN $bankAccountTable as b1 ON t.${TransactionFields.idBankAccount} = b1.${BankAccountFields.id} LEFT JOIN $bankAccountTable as b2 ON t.${TransactionFields.idBankAccountTransfer} = b2.${BankAccountFields.id} WHERE t.${TransactionFields.id} = ?', [id]); if (maps.isNotEmpty) { return Transaction.fromJson(maps.first); @@ -185,21 +203,61 @@ class TransactionMethods extends SossoldiDatabase { } } - Future> selectAll({int? type, DateTime? date, int? limit}) async { + Future> selectAll( + {int? type, + DateTime? date, + DateTime? dateRangeStart, + DateTime? dateRangeEnd, + int? limit}) async { final db = await database; String? where = type != null ? '${TransactionFields.type} = $type' : null; // filter type - if(date != null) { - where = where != null ? "$where and ${TransactionFields.date} >= '2021-01-01' and ${TransactionFields.date} <= '2022-10-10'" : "${TransactionFields.date} >= '2021-01-01' and ${TransactionFields.date} <= '2022-10-10'"; // filter date + if (date != null) { + where = + "${where != null ? '$where and ' : ''}strftime('%Y-%m-%d', ${TransactionFields.date}) >= '${date.toString().substring(0, 10)}' and ${TransactionFields.date} <= '${date.toIso8601String().substring(0, 10)}'"; + } else if (dateRangeStart != null && dateRangeEnd != null) { + where = + "${where != null ? '$where and ' : ''}strftime('%Y-%m-%d', ${TransactionFields.date}) BETWEEN '${dateRangeStart.toString().substring(0, 10)}' and '${dateRangeEnd.toIso8601String().substring(0, 10)}'"; } final orderByDESC = '${TransactionFields.date} DESC'; - final result = await db.query(transactionTable, where: where, orderBy: orderByDESC, limit: limit); + final result = + await db.rawQuery('SELECT t.*, c.${CategoryTransactionFields.name} as ${TransactionFields.categoryName}, c.${CategoryTransactionFields.color} as ${TransactionFields.categoryColor}, c.${CategoryTransactionFields.symbol} as ${TransactionFields.categorySymbol}, b1.${BankAccountFields.name} as ${TransactionFields.bankAccountName}, b2.${BankAccountFields.name} as ${TransactionFields.bankAccountTransferName} FROM "$transactionTable" as t LEFT JOIN $categoryTransactionTable as c ON t.${TransactionFields.idCategory} = c.${CategoryTransactionFields.id} LEFT JOIN $bankAccountTable as b1 ON t.${TransactionFields.idBankAccount} = b1.${BankAccountFields.id} LEFT JOIN $bankAccountTable as b2 ON t.${TransactionFields.idBankAccountTransfer} = b2.${BankAccountFields.id} ${where != null ? "WHERE $where" : ""} ORDER BY $orderByDESC ${limit != null ? "LIMIT $limit" : ""}'); return result.map((json) => Transaction.fromJson(json)).toList(); } + Future currentMonthTransactions() async { + final db = await database; + final result = await db.rawQuery(''' + SELECT + strftime('%Y-%m-%d', ${TransactionFields.date}) as day, + SUM(CASE WHEN ${TransactionFields.type} = 'IN' THEN ${TransactionFields.amount} ELSE 0 END) as income, + SUM(CASE WHEN ${TransactionFields.type} = 'OUT' THEN ${TransactionFields.amount} ELSE 0 END) as expense + FROM "$transactionTable" + WHERE strftime('%Y-%m', ${TransactionFields.date}) = strftime('%Y-%m', date('now')) + GROUP BY day + '''); + + return result; + } + + Future lastMonthTransactions() async { + final db = await database; + final result = await db.rawQuery(''' + SELECT + strftime('%Y-%m-%d', ${TransactionFields.date}) as day, + SUM(CASE WHEN ${TransactionFields.type} = 'IN' THEN ${TransactionFields.amount} ELSE 0 END) as income, + SUM(CASE WHEN ${TransactionFields.type} = 'OUT' THEN ${TransactionFields.amount} ELSE 0 END) as expense + FROM "$transactionTable" + WHERE strftime('%Y-%m', ${TransactionFields.date}) = strftime('%Y-%m', date('now', '-1 month')) + GROUP BY day + '''); + + return result; + } + Future updateItem(Transaction item) async { final db = await database; diff --git a/lib/pages/account_page/account_page.dart b/lib/pages/account_page/account_page.dart index 4aca2a9..39ffb6a 100644 --- a/lib/pages/account_page/account_page.dart +++ b/lib/pages/account_page/account_page.dart @@ -1,14 +1,14 @@ +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fl_chart/fl_chart.dart'; -import '../../providers/accounts_provider.dart'; -import '../../custom_widgets/line_chart.dart'; import '../../constants/functions.dart'; import '../../constants/style.dart'; +import '../../custom_widgets/line_chart.dart'; +import '../../providers/accounts_provider.dart'; class AccountPage extends ConsumerStatefulWidget { - const AccountPage({Key? key}) : super(key: key); + const AccountPage({super.key}); @override ConsumerState createState() => _AccountPage(); @@ -17,12 +17,11 @@ class AccountPage extends ConsumerStatefulWidget { class _AccountPage extends ConsumerState with Functions { @override Widget build(BuildContext context) { - final accountName = ref.read(accountNameProvider); - final accountAmount = ref.read(accountStartingValueProvider); + final account = ref.read(selectedAccountProvider); return Scaffold( appBar: AppBar( - title: Text(accountName ?? "", style: const TextStyle(color: white)), + title: Text(account?.name ?? "", style: const TextStyle(color: white)), backgroundColor: blue5, elevation: 0, ), @@ -35,7 +34,7 @@ class _AccountPage extends ConsumerState with Functions { child: Column( children: [ Text( - numToCurrency(accountAmount), + numToCurrency(account?.total), style: const TextStyle( color: white, fontSize: 32.0, @@ -68,8 +67,6 @@ class _AccountPage extends ConsumerState with Functions { line2Data: [], colorLine2Data: Color(0xffffffff), colorBackground: blue5, - maxY: 5.0, - minY: -5.0, maxDays: 30.0, ), ], diff --git a/lib/pages/accounts/account_list.dart b/lib/pages/accounts/account_list.dart index 7c44460..e454a42 100644 --- a/lib/pages/accounts/account_list.dart +++ b/lib/pages/accounts/account_list.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../constants/constants.dart'; -import '../../../custom_widgets/default_container.dart'; -import '../../../constants/functions.dart'; -import '../../../model/bank_account.dart'; -import '../../../providers/accounts_provider.dart'; + +import '../../constants/constants.dart'; +import '../../constants/functions.dart'; +import '../../custom_widgets/default_card.dart'; +import '../../model/bank_account.dart'; +import '../../providers/accounts_provider.dart'; class AccountList extends ConsumerStatefulWidget { - const AccountList({Key? key}) : super(key: key); + const AccountList({super.key}); @override ConsumerState createState() => _AccountListState(); @@ -18,12 +19,17 @@ class _AccountListState extends ConsumerState with Functions { Widget build(BuildContext context) { final accountsList = ref.watch(accountsProvider); return Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, appBar: AppBar( - title: const Text("Account"), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => Navigator.pop(context), + ), actions: [ IconButton( - onPressed: () => Navigator.of(context).pushNamed('/add-account'), + onPressed: () { + ref.read(accountsProvider.notifier).reset(); + Navigator.of(context).pushNamed('/add-account'); + }, icon: const Icon(Icons.add_circle), splashRadius: 28, ), @@ -42,10 +48,10 @@ class _AccountListState extends ConsumerState with Functions { shape: BoxShape.circle, color: Theme.of(context).colorScheme.primary, ), - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.all(8.0), child: Icon( Icons.account_balance_wallet, - size: 16.0, + size: 24.0, color: Theme.of(context).colorScheme.background, ), ), @@ -70,9 +76,12 @@ class _AccountListState extends ConsumerState with Functions { BankAccount account = accounts[i]; IconData? icon = accountIconList[account.symbol]; Color? color = accountColorListTheme[account.color]; - return DefaultContainer( + return DefaultCard( onTap: () async { - await ref.read(accountsProvider.notifier).selectedAccount(account).whenComplete(() => Navigator.of(context).pushNamed('/add-account')); + await ref + .read(accountsProvider.notifier) + .selectedAccount(account) + .whenComplete(() => Navigator.of(context).pushNamed('/add-account')); }, child: Row( children: [ diff --git a/lib/pages/accounts/add_account.dart b/lib/pages/accounts/add_account.dart index ef09b5d..09da007 100644 --- a/lib/pages/accounts/add_account.dart +++ b/lib/pages/accounts/add_account.dart @@ -20,33 +20,28 @@ class _AddAccountState extends ConsumerState with Functions { final TextEditingController nameController = TextEditingController(); final TextEditingController startingValueController = TextEditingController(); + @override + void initState() { + nameController.text = ref.read(selectedAccountProvider)?.name ?? ''; + startingValueController.text = + ref.read(selectedAccountProvider)?.startingValue.toString() ?? ''; + super.initState(); + } + @override void dispose() { - ref.invalidate(selectedAccountProvider); - ref.invalidate(accountNameProvider); - ref.invalidate(accountIconProvider); - ref.invalidate(accountColorProvider); - ref.invalidate(accountStartingValueProvider); - ref.invalidate(accountMainSwitchProvider); - ref.invalidate(countNetWorthSwitchProvider); + nameController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final accountName = - ref.read(accountNameProvider); // Used only to retrieve the value when updating an account final selectedAccount = ref.watch(selectedAccountProvider); final accountIcon = ref.watch(accountIconProvider); final accountColor = ref.watch(accountColorProvider); final showAccountIcons = ref.watch(showAccountIconsProvider); final accountMainSwitch = ref.watch(accountMainSwitchProvider); final countNetWorth = ref.watch(countNetWorthSwitchProvider); - ref.listen(accountNameProvider, (_, __) {}); - - setState(() { - nameController.text = accountName ?? ''; - }); return Scaffold( backgroundColor: Theme.of(context).colorScheme.background, @@ -83,7 +78,6 @@ class _AddAccountState extends ConsumerState with Functions { contentPadding: const EdgeInsets.all(0), ), style: Theme.of(context).textTheme.titleLarge!.copyWith(color: grey1), - onChanged: (value) => ref.read(accountNameProvider.notifier).state = value, ) ], ), @@ -237,7 +231,6 @@ class _AddAccountState extends ConsumerState with Functions { ], ), ), - if (selectedAccount == null) Container( width: double.infinity, @@ -279,14 +272,10 @@ class _AddAccountState extends ConsumerState with Functions { .textTheme .titleLarge! .copyWith(color: grey1), - onChanged: (value) => ref - .read(accountStartingValueProvider.notifier) - .state = currencyToNum(value), ) ], ), ), - Container( width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), @@ -331,7 +320,6 @@ class _AddAccountState extends ConsumerState with Functions { .bodyLarge! .copyWith(color: Theme.of(context).colorScheme.primary), ), - // TODO: Need to add this feature in the db CupertinoSwitch( value: countNetWorth, onChanged: (value) => @@ -393,12 +381,12 @@ class _AddAccountState extends ConsumerState with Functions { if (selectedAccount != null) { ref .read(accountsProvider.notifier) - .updateAccount(selectedAccount) + .updateAccount(nameController.text) .whenComplete(() => Navigator.of(context).pop()); } else { ref .read(accountsProvider.notifier) - .addAccount() + .addAccount(nameController.text, startingValueController.text.isEmpty ? null : currencyToNum(startingValueController.text)) .whenComplete(() => Navigator.of(context).pop()); } }, diff --git a/lib/pages/add_page/add_page.dart b/lib/pages/add_page/add_page.dart index 3440645..c38eee8 100644 --- a/lib/pages/add_page/add_page.dart +++ b/lib/pages/add_page/add_page.dart @@ -1,10 +1,11 @@ import 'dart:io' show Platform; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../constants/style.dart'; import '../../constants/functions.dart'; +import '../../constants/style.dart'; import '../../model/transaction.dart'; import '../../providers/transactions_provider.dart'; import "widgets/account_selector.dart"; @@ -27,10 +28,8 @@ class _AddPageState extends ConsumerState with Functions { @override void initState() { - amountController.text = - numToCurrency(ref.read(selectedTransactionUpdateProvider)?.amount); - noteController.text = - ref.read(selectedTransactionUpdateProvider)?.note ?? ''; + amountController.text = numToCurrency(ref.read(selectedTransactionUpdateProvider)?.amount); + noteController.text = ref.read(selectedTransactionUpdateProvider)?.note ?? ''; super.initState(); } @@ -38,44 +37,25 @@ class _AddPageState extends ConsumerState with Functions { void dispose() { amountController.dispose(); noteController.dispose(); - ref.invalidate(selectedTransactionUpdateProvider); - ref.invalidate(transactionTypesProvider); - ref.invalidate(bankAccountProvider); - ref.invalidate(dateProvider); - ref.invalidate(categoryProvider); - ref.invalidate(amountProvider); - ref.invalidate(noteProvider); - ref.invalidate(selectedRecurringPayProvider); - ref.invalidate(intervalProvider); - ref.invalidate(repetitionProvider); - ref.invalidate(transactionTypesProvider); super.dispose(); } @override Widget build(BuildContext context) { - final trsncTypeList = ref.watch(transactionTypeList); - final trnscTypes = ref.watch(transactionTypesProvider); + final selectedType = ref.watch(transactionTypeProvider); final selectedTransaction = ref.watch(selectedTransactionUpdateProvider); - final selectedType = trsncTypeList[trnscTypes.indexOf(true)]; - // I listen servono a evitare che il provider faccia il dispose subito dopo essere stato aggiornato - ref.listen(amountProvider, (_, __) {}); - ref.listen(noteProvider, (_, __) {}); return Scaffold( appBar: AppBar( title: Text( - (selectedTransaction != null) - ? "Editing transaction" - : "New transaction", + (selectedTransaction != null) ? "Editing transaction" : "New transaction", ), leadingWidth: 100, leading: TextButton( onPressed: () => Navigator.pop(context), child: Text( 'Cancel', - style: - Theme.of(context).textTheme.titleMedium!.copyWith(color: blue5), + style: Theme.of(context).textTheme.titleMedium!.copyWith(color: blue5), ), ), actions: [ @@ -91,216 +71,211 @@ class _AddPageState extends ConsumerState with Functions { ref .read(transactionsProvider.notifier) .deleteTransaction(selectedTransaction.id!) - .whenComplete(() => Navigator.of(context).pop()); + .whenComplete(() => Navigator.pop(context)); }, ), ) : const SizedBox(), ], ), - body: SingleChildScrollView( - child: Column( - children: [ - AmountSection( - amountController: amountController, - ), - Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(left: 16, top: 32, bottom: 8), - child: Text( - "DETAILS", - style: Theme.of(context) - .textTheme - .labelLarge! - .copyWith(color: Theme.of(context).colorScheme.primary), - ), - ), - Container( - color: Theme.of(context).colorScheme.surface, - child: ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - LabelListTile( - labelController: noteController, - labelProvider: noteProvider, + body: Stack( + children: [ + SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 72), + child: Column( + children: [ + AmountSection(amountController), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 16, top: 32, bottom: 8), + child: Text( + "DETAILS", + style: Theme.of(context) + .textTheme + .labelLarge! + .copyWith(color: Theme.of(context).colorScheme.primary), ), - const Divider(height: 1, color: grey1), - if (selectedType != Type.transfer) ...[ - 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), - DetailsListTile( - title: "Category", - icon: Icons.list_alt, - value: ref.watch(categoryProvider)?.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) => CategorySelector( - scrollController: controller, - ), - ), - ); - }, - ), - ], - const Divider(height: 1, color: grey1), - DetailsListTile( - title: "Date", - icon: Icons.calendar_month, - value: dateToString(ref.watch(dateProvider)), - callback: () async { - FocusManager.instance.primaryFocus?.unfocus(); - if (Platform.isIOS) { - showCupertinoModalPopup( - context: context, - builder: (_) => Container( - height: 300, - color: white, - child: CupertinoDatePicker( - initialDateTime: ref.watch(dateProvider), - minimumYear: 2015, - maximumYear: 2050, - mode: CupertinoDatePickerMode.date, - onDateTimeChanged: (date) => - ref.read(dateProvider.notifier).state = date, - ), - ), - ); - } else if (Platform.isAndroid) { - final DateTime? pickedDate = await showDatePicker( - context: context, - initialDate: ref.watch(dateProvider), - firstDate: DateTime(2015), - lastDate: DateTime(2050), - ); - if (pickedDate != null) { - ref.read(dateProvider.notifier).state = pickedDate; - } - } - }, + ), + Container( + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + LabelListTile(noteController), + const Divider(height: 1, color: grey1), + if (selectedType != TransactionType.transfer) ...[ + 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), + DetailsListTile( + title: "Category", + icon: Icons.list_alt, + value: ref.watch(categoryProvider)?.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) => CategorySelector( + scrollController: controller, + ), + ), + ); + }, + ), + const Divider(height: 1, color: grey1), + ], + DetailsListTile( + title: "Date", + icon: Icons.calendar_month, + value: dateToString(ref.watch(dateProvider)), + callback: () async { + FocusManager.instance.primaryFocus?.unfocus(); + if (Platform.isIOS) { + showCupertinoModalPopup( + context: context, + builder: (_) => Container( + height: 300, + color: white, + child: CupertinoDatePicker( + initialDateTime: ref.watch(dateProvider), + minimumYear: 2015, + maximumYear: 2050, + mode: CupertinoDatePickerMode.date, + onDateTimeChanged: (date) => + ref.read(dateProvider.notifier).state = date, + ), + ), + ); + } else if (Platform.isAndroid) { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: ref.watch(dateProvider), + firstDate: DateTime(2015), + lastDate: DateTime(2050), + ); + if (pickedDate != null) { + ref.read(dateProvider.notifier).state = pickedDate; + } + } + }, + ), + if (selectedType == TransactionType.expense) ...[ + const RecurrenceListTile(), + ], + ], ), - if (selectedType == Type.expense) ...[ - const RecurrenceListTile(), - ], + ), + ], + ), + ), + 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), + ) ], ), - ), - Container( - alignment: Alignment.bottomCenter, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: Container( - width: double.infinity, + height: 48, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - boxShadow: [ - BoxShadow( - color: blue1.withOpacity(0.15), - blurRadius: 5.0, - offset: const Offset(0, -1.0), - ) - ], + color: Theme.of(context).colorScheme.secondary, + boxShadow: [defaultShadow], + borderRadius: BorderRadius.circular(8), ), - padding: const EdgeInsets.fromLTRB(24, 12, 24, 24), - child: Container( - height: 48, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondary, - boxShadow: [defaultShadow], - borderRadius: BorderRadius.circular(8), - ), - child: TextButton( - onPressed: () async { - // Check that an amount it's inserted - if (ref.read(amountProvider) != 0) { - if (selectedTransaction != null) { - ref - .read(transactionsProvider.notifier) - .updateTransaction() - .whenComplete(() => Navigator.of(context).pop()); + child: TextButton( + onPressed: () async { + // Check that an amount it's inserted + if (amountController.text != '') { + if (selectedTransaction != null) { + ref + .read(transactionsProvider.notifier) + .updateTransaction(currencyToNum(amountController.text), noteController.text) + .whenComplete(() => Navigator.of(context).pop()); + } else { + if (selectedType == TransactionType.transfer) { + if (ref.read(bankAccountTransferProvider) != null) { + ref + .read(transactionsProvider.notifier) + .addTransaction(currencyToNum(amountController.text), noteController.text) + .whenComplete(() => Navigator.of(context).pop()); + } } else { - if (selectedType == Type.transfer) { - if (ref.read(bankAccountTransferProvider) != null) { - ref - .read(transactionsProvider.notifier) - .addTransaction() - .whenComplete( - () => Navigator.of(context).pop()); - } - } else { - // It's an income or an expense - if (ref.read(categoryProvider) != null) { - ref - .read(transactionsProvider.notifier) - .addTransaction() - .whenComplete( - () => Navigator.of(context).pop()); - } + // It's an income or an expense + if (ref.read(categoryProvider) != null) { + ref + .read(transactionsProvider.notifier) + .addTransaction(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( - selectedTransaction != null - ? "UPDATE TRANSACTION" - : "ADD TRANSACTION", - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - color: Theme.of(context).colorScheme.background), - ), + } + }, + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.secondary, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text( + selectedTransaction != null ? "UPDATE TRANSACTION" : "ADD TRANSACTION", + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Theme.of(context).colorScheme.background), ), ), ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/add_page/widgets/account_selector.dart b/lib/pages/add_page/widgets/account_selector.dart index 258da6a..97b5ffa 100644 --- a/lib/pages/add_page/widgets/account_selector.dart +++ b/lib/pages/add_page/widgets/account_selector.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../../constants/constants.dart'; -import '../../../constants/style.dart'; import '../../../constants/functions.dart'; +import '../../../constants/style.dart'; import '../../../model/bank_account.dart'; import '../../../providers/accounts_provider.dart'; @@ -10,18 +11,19 @@ class AccountSelector extends ConsumerStatefulWidget { const AccountSelector({ required this.provider, required this.scrollController, - Key? key, - }) : super(key: key); + this.fromAccount, + super.key, + }); final StateProvider provider; final ScrollController scrollController; + final int? fromAccount; @override ConsumerState createState() => _AccountSelectorState(); } -class _AccountSelectorState extends ConsumerState - with Functions { +class _AccountSelectorState extends ConsumerState with Functions { @override Widget build(BuildContext context) { final accountsList = ref.watch(accountsProvider); @@ -84,9 +86,7 @@ class _AccountSelectorState extends ConsumerState ? Icon( icon, size: 24.0, - color: Theme.of(context) - .colorScheme - .background, + color: Theme.of(context).colorScheme.background, ) : const SizedBox(), ), @@ -102,8 +102,7 @@ class _AccountSelectorState extends ConsumerState ); }, ), - loading: () => - const Center(child: CircularProgressIndicator()), + loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Text('Error: $err'), ), ), @@ -124,17 +123,14 @@ class _AccountSelectorState extends ConsumerState scrollDirection: Axis.vertical, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (context, index) => - const Divider(height: 1, color: grey1), + separatorBuilder: (context, index) => const Divider(height: 1, color: grey1), itemBuilder: (context, i) { BankAccount account = accounts[i]; IconData? icon = accountIconList[account.symbol]; Color? color = accountColorListTheme[account.color]; return ListTile( - tileColor: Theme.of(context).colorScheme.surface, - onTap: () => - ref.read(widget.provider.notifier).state = account, - contentPadding: const EdgeInsets.all(12.0), + onTap: () => ref.read(widget.provider.notifier).state = account, + enabled: account.id != widget.fromAccount, leading: Container( decoration: BoxDecoration( shape: BoxShape.circle, @@ -145,20 +141,11 @@ class _AccountSelectorState extends ConsumerState ? Icon( icon, size: 24.0, - color: - Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.background, ) : const SizedBox(), ), - title: Text( - account.name, - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), + title: Text(account.name), trailing: (ref.watch(widget.provider)?.id == account.id) ? Icon( Icons.done, @@ -168,8 +155,7 @@ class _AccountSelectorState extends ConsumerState ); }, ), - loading: () => - const Center(child: CircularProgressIndicator()), + loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Text('Error: $err'), ), ], diff --git a/lib/pages/add_page/widgets/amount_section.dart b/lib/pages/add_page/widgets/amount_section.dart index 5da09e4..8bfe9b2 100644 --- a/lib/pages/add_page/widgets/amount_section.dart +++ b/lib/pages/add_page/widgets/amount_section.dart @@ -1,30 +1,47 @@ import 'package:flutter/material.dart'; import "package:flutter_riverpod/flutter_riverpod.dart"; -import 'package:flutter/services.dart'; +import '../../../constants/functions.dart'; import "../../../constants/style.dart"; -import "../../../constants/functions.dart"; import '../../../model/transaction.dart'; import '../../../providers/transactions_provider.dart'; -import '../../../utils/decimal_text_input_formatter.dart'; import 'account_selector.dart'; import 'type_tab.dart'; -class AmountSection extends ConsumerWidget with Functions { - const AmountSection({ - required this.amountController, - Key? key, - }) : super(key: key); +class AmountSection extends ConsumerStatefulWidget { + const AmountSection( + this.amountController, { + super.key, + }); final TextEditingController amountController; + @override + ConsumerState createState() => _AmountSectionState(); +} + +class _AmountSectionState extends ConsumerState with Functions { static const List _titleList = ['Income', 'Expense', 'Transfer']; + List _typeToggleState = [false, true, false]; + @override - Widget build(BuildContext context, WidgetRef ref) { + void initState() { + final selectedType = ref.read(transactionTypeProvider); + setState(() { + if (selectedType == TransactionType.income) { + _typeToggleState = [true, false, false]; + } else if (selectedType == TransactionType.transfer) { + _typeToggleState = [false, false, true]; + } + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { final trsncTypeList = ref.watch(transactionTypeList); - final trnscTypes = ref.watch(transactionTypesProvider); - final selectedType = trsncTypeList[trnscTypes.indexOf(true)]; + final selectedType = ref.watch(transactionTypeProvider); return Container( color: Theme.of(context).colorScheme.surface, @@ -41,11 +58,16 @@ class AmountSection extends ConsumerWidget with Functions { child: ToggleButtons( direction: Axis.horizontal, onPressed: (int index) { - List list = trnscTypes; - for (int i = 0; i < 3; i++) { - list[i] = i == index; + List newSelection = []; + for (TransactionType type in trsncTypeList) { + if (type == trsncTypeList[index]) { + newSelection.add(true); + ref.read(transactionTypeProvider.notifier).state = type; + } else { + newSelection.add(false); + } } - ref.read(transactionTypesProvider.notifier).state = [...list]; + setState(() => _typeToggleState = newSelection); }, borderRadius: const BorderRadius.all(Radius.circular(4)), renderBorder: false, @@ -57,18 +79,18 @@ class AmountSection extends ConsumerWidget with Functions { minWidth: (MediaQuery.of(context).size.width - 36) / 3, maxWidth: (MediaQuery.of(context).size.width - 36) / 3, ), - isSelected: trnscTypes, + isSelected: _typeToggleState, children: List.generate( - trnscTypes.length, + _typeToggleState.length, (index) => TypeTab( - trnscTypes[index], + _typeToggleState[index], _titleList[index], typeToColor(trsncTypeList[index]), ), ), ), ), - if (selectedType == Type.transfer) + if (selectedType == TransactionType.transfer) Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: SizedBox( @@ -82,10 +104,7 @@ class AmountSection extends ConsumerWidget with Functions { const SizedBox(height: 8), Text( "FROM:", - style: Theme.of(context) - .textTheme - .labelMedium! - .copyWith( + style: Theme.of(context).textTheme.labelMedium!.copyWith( color: grey1, ), ), @@ -130,9 +149,7 @@ class AmountSection extends ConsumerWidget with Functions { Container( decoration: BoxDecoration( shape: BoxShape.circle, - color: Theme.of(context) - .colorScheme - .secondary, + color: Theme.of(context).colorScheme.secondary, ), padding: const EdgeInsets.all(4.0), child: const Icon( @@ -144,10 +161,7 @@ class AmountSection extends ConsumerWidget with Functions { const Spacer(), Text( ref.watch(bankAccountProvider)!.name, - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( + style: Theme.of(context).textTheme.bodySmall!.copyWith( color: grey1, ), ), @@ -161,17 +175,13 @@ class AmountSection extends ConsumerWidget with Functions { ), ), GestureDetector( - onTap: () => ref - .read(transactionsProvider.notifier) - .switchAccount(), + onTap: () => ref.read(transactionsProvider.notifier).switchAccount(), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Expanded( - child: VerticalDivider(width: 1, color: grey2)), + Expanded(child: VerticalDivider(width: 1, color: grey2)), Padding( - padding: EdgeInsets.symmetric( - vertical: 2, horizontal: 20), + padding: EdgeInsets.symmetric(vertical: 2, horizontal: 20), child: Icon( Icons.change_circle, size: 32, @@ -191,10 +201,7 @@ class AmountSection extends ConsumerWidget with Functions { const SizedBox(height: 8), Text( "TO:", - style: Theme.of(context) - .textTheme - .labelMedium! - .copyWith( + style: Theme.of(context).textTheme.labelMedium!.copyWith( color: grey1, ), ), @@ -202,7 +209,6 @@ class AmountSection extends ConsumerWidget with Functions { Material( child: InkWell( onTap: () { - FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus(); showModalBottomSheet( context: context, @@ -224,6 +230,7 @@ class AmountSection extends ConsumerWidget with Functions { // to provider: bankAccountTransferProvider, scrollController: controller, + fromAccount: ref.watch(bankAccountProvider)?.id, ), ), ); @@ -240,15 +247,9 @@ class AmountSection extends ConsumerWidget with Functions { const Icon(Icons.sort, color: grey2), const Spacer(), Text( - ref - .watch( - bankAccountTransferProvider) - ?.name ?? + ref.watch(bankAccountTransferProvider)?.name ?? "Select account", - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith( + style: Theme.of(context).textTheme.bodySmall!.copyWith( color: grey1, ), ), @@ -268,7 +269,7 @@ class AmountSection extends ConsumerWidget with Functions { Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24), child: TextField( - controller: amountController, + controller: widget.amountController, decoration: InputDecoration( hintText: "0", border: InputBorder.none, @@ -279,11 +280,8 @@ class AmountSection extends ConsumerWidget with Functions { .headlineMedium! .copyWith(color: typeToColor(selectedType)), ), - keyboardType: - const TextInputType.numberWithOptions(decimal: true), - inputFormatters: [ - DecimalTextInputFormatter(decimalDigits: 2) - ], + keyboardType: const TextInputType.numberWithOptions(decimal: true), + // inputFormatters: [DecimalTextInputFormatter(decimalDigits: 2)], autofocus: false, textAlign: TextAlign.center, cursorColor: grey1, @@ -292,8 +290,6 @@ class AmountSection extends ConsumerWidget with Functions { fontSize: 58, fontWeight: FontWeight.bold, ), - onChanged: (value) => ref.read(amountProvider.notifier).state = - currencyToNum(value), ), ), ], diff --git a/lib/pages/add_page/widgets/category_selector.dart b/lib/pages/add_page/widgets/category_selector.dart index 9986ad7..4bbba57 100644 --- a/lib/pages/add_page/widgets/category_selector.dart +++ b/lib/pages/add_page/widgets/category_selector.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../../constants/constants.dart'; -import '../../../constants/style.dart'; import '../../../constants/functions.dart'; +import '../../../constants/style.dart'; import '../../../model/category_transaction.dart'; import '../../../providers/categories_provider.dart'; import '../../../providers/transactions_provider.dart'; @@ -10,8 +11,8 @@ import '../../../providers/transactions_provider.dart'; class CategorySelector extends ConsumerStatefulWidget { const CategorySelector({ required this.scrollController, - Key? key, - }) : super(key: key); + super.key, + }); final ScrollController scrollController; @@ -19,8 +20,7 @@ class CategorySelector extends ConsumerStatefulWidget { ConsumerState createState() => _CategorySelectorState(); } -class _CategorySelectorState extends ConsumerState - with Functions { +class _CategorySelectorState extends ConsumerState with Functions { @override Widget build(BuildContext context) { final categoriesList = ref.watch(categoriesProvider); @@ -82,9 +82,7 @@ class _CategorySelectorState extends ConsumerState ? Icon( icon, size: 24.0, - color: Theme.of(context) - .colorScheme - .background, + color: Theme.of(context).colorScheme.background, ) : const SizedBox(), ), @@ -93,18 +91,14 @@ class _CategorySelectorState extends ConsumerState style: Theme.of(context) .textTheme .labelLarge! - .copyWith( - color: Theme.of(context) - .colorScheme - .primary), + .copyWith(color: Theme.of(context).colorScheme.primary), ), ], ), ); }, ), - loading: () => - const Center(child: CircularProgressIndicator()), + loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Text('Error: $err'), ), ), @@ -125,17 +119,13 @@ class _CategorySelectorState extends ConsumerState scrollDirection: Axis.vertical, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (context, index) => - const Divider(height: 1, color: grey1), + separatorBuilder: (context, index) => const Divider(height: 1, color: grey1), itemBuilder: (context, i) { CategoryTransaction category = categories[i]; IconData? icon = iconList[category.symbol]; Color? color = categoryColorListTheme[category.color]; return ListTile( - tileColor: Theme.of(context).colorScheme.surface, - onTap: () => ref.read(categoryProvider.notifier).state = - category, - contentPadding: const EdgeInsets.all(16), + onTap: () => ref.read(categoryProvider.notifier).state = category, leading: Container( decoration: BoxDecoration( shape: BoxShape.circle, @@ -146,20 +136,11 @@ class _CategorySelectorState extends ConsumerState ? Icon( icon, size: 24.0, - color: - Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.background, ) : const SizedBox(), ), - title: Text( - category.name, - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), + title: Text(category.name), trailing: ref.watch(categoryProvider)?.id == category.id ? Icon( Icons.done, @@ -169,8 +150,7 @@ class _CategorySelectorState extends ConsumerState ); }, ), - loading: () => - const Center(child: CircularProgressIndicator()), + loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Text('Error: $err'), ), ], diff --git a/lib/pages/add_page/widgets/label_list_tile.dart b/lib/pages/add_page/widgets/label_list_tile.dart index 0967fbf..00116fc 100644 --- a/lib/pages/add_page/widgets/label_list_tile.dart +++ b/lib/pages/add_page/widgets/label_list_tile.dart @@ -1,20 +1,17 @@ import 'package:flutter/material.dart'; -import "package:flutter_riverpod/flutter_riverpod.dart"; import "../../../constants/style.dart"; -class LabelListTile extends ConsumerWidget { - const LabelListTile({ - required this.labelController, - required this.labelProvider, - Key? key, - }) : super(key: key); +class LabelListTile extends StatelessWidget { + const LabelListTile( + this.labelController, { + super.key, + }); final TextEditingController labelController; - final StateProvider labelProvider; @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 32, 16), child: Row( @@ -47,10 +44,7 @@ class LabelListTile extends ConsumerWidget { controller: labelController, decoration: const InputDecoration(border: InputBorder.none), textAlign: TextAlign.end, - style: - Theme.of(context).textTheme.bodySmall!.copyWith(color: grey1), - onChanged: (value) => - ref.read(labelProvider.notifier).state = value, + style: Theme.of(context).textTheme.bodySmall!.copyWith(color: grey1), ), ), ], diff --git a/lib/pages/categories/add_category.dart b/lib/pages/categories/add_category.dart index 58957dc..c74bcb9 100644 --- a/lib/pages/categories/add_category.dart +++ b/lib/pages/categories/add_category.dart @@ -17,28 +17,24 @@ class AddCategory extends ConsumerStatefulWidget { class _AddCategoryState extends ConsumerState with Functions { final TextEditingController nameController = TextEditingController(); + @override + void initState() { + nameController.text = ref.read(selectedCategoryProvider)?.name ?? ''; + super.initState(); + } + @override void dispose() { - ref.invalidate(selectedCategoryProvider); - ref.invalidate(categoryNameProvider); - ref.invalidate(categoryIconProvider); - ref.invalidate(categoryColorProvider); + nameController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final categoryName = - ref.read(categoryNameProvider); // Used only to retrieve the value when updating a category final selectedCategory = ref.watch(selectedCategoryProvider); final categoryIcon = ref.watch(categoryIconProvider); final categoryColor = ref.watch(categoryColorProvider); final showCategoryIcons = ref.watch(showCategoryIconsProvider); - ref.listen(categoryNameProvider, (_, __) {}); - - setState(() { - nameController.text = categoryName ?? ''; - }); return Scaffold( backgroundColor: Theme.of(context).colorScheme.background, appBar: AppBar(title: Text("${selectedCategory == null ? "New" : "Edit"} Category")), @@ -74,7 +70,6 @@ class _AddCategoryState extends ConsumerState with Functions { contentPadding: const EdgeInsets.all(0), ), style: Theme.of(context).textTheme.titleLarge!.copyWith(color: grey1), - onChanged: (value) => ref.read(categoryNameProvider.notifier).state = value, ) ], ), @@ -267,7 +262,7 @@ class _AddCategoryState extends ConsumerState with Functions { .removeCategory(selectedCategory.id!) .whenComplete(() => Navigator.of(context).pop()), style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: const EdgeInsets.all(16), side: const BorderSide(color: red, width: 1), ), icon: const Icon(Icons.delete_outlined, color: red), @@ -294,11 +289,9 @@ class _AddCategoryState extends ConsumerState with Functions { ) ], ), - padding: const EdgeInsets.fromLTRB(24, 12, 24, 24), + 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), ), @@ -307,17 +300,18 @@ class _AddCategoryState extends ConsumerState with Functions { if (selectedCategory != null) { ref .read(categoriesProvider.notifier) - .updateCategory(selectedCategory) + .updateCategory(nameController.text) .whenComplete(() => Navigator.of(context).pop()); } else { - ref - .read(categoriesProvider.notifier) - .addCategory() - .whenComplete(() => Navigator.of(context).pop()); + ref + .read(categoriesProvider.notifier) + .addCategory(nameController.text) + .whenComplete(() => Navigator.of(context).pop()); } }, style: TextButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.secondary, + padding: const EdgeInsets.all(16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), child: Text( diff --git a/lib/pages/categories/category_list.dart b/lib/pages/categories/category_list.dart index 58e347e..48cfe79 100644 --- a/lib/pages/categories/category_list.dart +++ b/lib/pages/categories/category_list.dart @@ -1,13 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../../constants/constants.dart'; -import '../../../custom_widgets/default_container.dart'; import '../../../constants/functions.dart'; import '../../../model/category_transaction.dart'; import '../../../providers/categories_provider.dart'; +import '../../custom_widgets/default_card.dart'; class CategoryList extends ConsumerStatefulWidget { - const CategoryList({Key? key}) : super(key: key); + const CategoryList({super.key}); @override ConsumerState createState() => _CategoryListState(); @@ -18,12 +19,17 @@ class _CategoryListState extends ConsumerState with Functions { Widget build(BuildContext context) { final categorysList = ref.watch(categoriesProvider); return Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, appBar: AppBar( - title: const Text("Category"), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => Navigator.pop(context), + ), actions: [ IconButton( - onPressed: () => Navigator.of(context).pushNamed('/add-category'), + onPressed: () { + ref.read(categoriesProvider.notifier).reset(); + Navigator.of(context).pushNamed('/add-category'); + }, icon: const Icon(Icons.add_circle), splashRadius: 28, ), @@ -42,10 +48,10 @@ class _CategoryListState extends ConsumerState with Functions { shape: BoxShape.circle, color: Theme.of(context).colorScheme.primary, ), - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.all(8.0), child: Icon( Icons.list_alt, - size: 16.0, + size: 24.0, color: Theme.of(context).colorScheme.background, ), ), @@ -70,10 +76,9 @@ class _CategoryListState extends ConsumerState with Functions { CategoryTransaction category = categorys[i]; IconData? icon = iconList[category.symbol]; Color? color = categoryColorListTheme[category.color]; - return DefaultContainer( - onTap: () async { - // TODO: fix this - await ref.read(categoriesProvider.notifier).selectedCategory(category); + return DefaultCard( + onTap: () { + ref.read(categoriesProvider.notifier).selectedCategory(category); Navigator.of(context).pushNamed('/add-category'); }, child: Row( diff --git a/lib/pages/general_options/general_settings.dart b/lib/pages/general_options/general_settings.dart index 404e697..cc18fec 100644 --- a/lib/pages/general_options/general_settings.dart +++ b/lib/pages/general_options/general_settings.dart @@ -1,16 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -import 'package:sossoldi/providers/theme_provider.dart'; import '../../constants/style.dart'; +import '../../providers/theme_provider.dart'; class GeneralSettingsPage extends ConsumerStatefulWidget { const GeneralSettingsPage({super.key}); @override - ConsumerState createState() => - _GeneralSettingsPageState(); + ConsumerState createState() => _GeneralSettingsPageState(); } class _GeneralSettingsPageState extends ConsumerState { @@ -39,18 +38,9 @@ class _GeneralSettingsPageState extends ConsumerState { final appThemeState = ref.watch(appThemeStateNotifier); return Scaffold( appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.background, - elevation: 0, - centerTitle: true, leading: IconButton( - icon: const Icon( - Icons.arrow_back_ios_new, - color: Color(0XFF7DA1C4), - ), - onPressed: () { - Navigator.pop(context); - // Return to previous page - }, + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => Navigator.pop(context), ), title: Text( 'General Settings', @@ -68,8 +58,10 @@ class _GeneralSettingsPageState extends ConsumerState { Row( children: [ Text("Appearance", - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.primary)), + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: Theme.of(context).colorScheme.primary)), const Spacer(), CircleAvatar( radius: 30.0, @@ -79,19 +71,13 @@ class _GeneralSettingsPageState extends ConsumerState { onPressed: () { // Toggle dark mode using the provider if (appThemeState.isDarkModeEnabled) { - ref - .read(appThemeStateNotifier.notifier) - .setLightTheme(); + ref.read(appThemeStateNotifier.notifier).setLightTheme(); } else { - ref - .read(appThemeStateNotifier.notifier) - .setDarkTheme(); + ref.read(appThemeStateNotifier.notifier).setDarkTheme(); } }, icon: Icon( - appThemeState.isDarkModeEnabled - ? Icons.dark_mode - : Icons.light_mode, + appThemeState.isDarkModeEnabled ? Icons.dark_mode : Icons.light_mode, size: 25.0, color: Theme.of(context).colorScheme.background, ), @@ -102,8 +88,10 @@ class _GeneralSettingsPageState extends ConsumerState { Row( children: [ Text("Currency", - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.primary)), + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: Theme.of(context).colorScheme.primary)), const Spacer(), GestureDetector( onTap: () { @@ -116,8 +104,7 @@ class _GeneralSettingsPageState extends ConsumerState { child: Text( NumberFormat().simpleCurrencySymbol(selectedCurrency), style: TextStyle( - color: Theme.of(context).colorScheme.background, - fontSize: 25), + color: Theme.of(context).colorScheme.background, fontSize: 25), )))), ], ), @@ -212,10 +199,7 @@ class _GeneralSettingsPageState extends ConsumerState { backgroundColor: blue5, child: Text(currencies.elementAt(index)[0], style: TextStyle( - color: Theme.of(context) - .colorScheme - .background, - fontSize: 20)), + color: Theme.of(context).colorScheme.background, fontSize: 20)), ), title: Text( currencies.elementAt(index)[1], diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index ed0c513..d89d539 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,15 +1,16 @@ -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../custom_widgets/transactions_list.dart'; + import '../constants/functions.dart'; -import '../providers/transactions_provider.dart'; -import '../custom_widgets/budget_circular_indicator.dart'; -import '../providers/accounts_provider.dart'; import '../constants/style.dart'; -import '../model/bank_account.dart'; import '../custom_widgets/accounts_sum.dart'; +import '../custom_widgets/budget_circular_indicator.dart'; import '../custom_widgets/line_chart.dart'; +import '../custom_widgets/transactions_list.dart'; +import '../model/bank_account.dart'; +import '../providers/accounts_provider.dart'; +import '../providers/dashboard_provider.dart'; +import '../providers/transactions_provider.dart'; class HomePage extends ConsumerStatefulWidget { const HomePage({super.key}); @@ -22,366 +23,315 @@ class _HomePageState extends ConsumerState with Functions { @override Widget build(BuildContext context) { final accountList = ref.watch(accountsProvider); - final transactionList = ref.watch(transactionsProvider); - return ListView( - children: [ - Column( - children: [ - const SizedBox(height: 24), - Row( - children: [ - const SizedBox(width: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "MONTHLY BALANCE", - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: Theme.of(context).colorScheme.primary), - ), - RichText( - textScaleFactor: MediaQuery.of(context).textScaleFactor, - text: TextSpan( + final lastTransactions = ref.watch(lastTransactionsProvider); + return Container( + color: Theme.of(context).colorScheme.tertiary, + child: ListView( + children: [ + ref.watch(dashboardProvider).when( + data: (value) { + final income = ref.watch(incomeProvider); + final expense = ref.watch(expenseProvider); + final total = income + expense; + final currentMonthList = ref.watch(currentMonthListProvider); + final lastMonthList = ref.watch(lastMonthListProvider); + return Column( + children: [ + const SizedBox(height: 24), + Row( children: [ - TextSpan( - text: numToCurrency(-1536.65), - style: Theme.of(context) - .textTheme - .headlineLarge - ?.copyWith( - color: - Theme.of(context).colorScheme.primary), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "MONTHLY BALANCE", + style: Theme.of(context) + .textTheme + .labelMedium + ?.copyWith(color: Theme.of(context).colorScheme.primary), + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: numToCurrency(total), + style: Theme.of(context) + .textTheme + .headlineLarge + ?.copyWith(color: Theme.of(context).colorScheme.primary), + ), + TextSpan( + text: "€", + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(color: Theme.of(context).colorScheme.primary), + ), + ], + ), + ), + ], ), - TextSpan( - text: "€", - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith( - color: - Theme.of(context).colorScheme.primary), + const SizedBox(width: 30), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "INCOME", + style: Theme.of(context).textTheme.labelMedium, + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: numToCurrency(income), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: green), + ), + TextSpan( + text: "€", + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(color: green), + ), + ], + ), + ), + ], + ), + const SizedBox(width: 30), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "EXPENSES", + style: Theme.of(context).textTheme.labelMedium, + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: numToCurrency(expense), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: red), + ), + TextSpan( + text: "€", + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith(color: red), + ), + ], + ), + ), + ], ), ], ), - ), - ], - ), - const SizedBox(width: 30), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "INCOME", - style: Theme.of(context).textTheme.labelMedium, - ), - RichText( - textScaleFactor: MediaQuery.of(context).textScaleFactor, - text: TextSpan( + const SizedBox(height: 16), + LineChartWidget( + line1Data: currentMonthList, + colorLine1Data: const Color(0xff00152D), + line2Data: lastMonthList, + colorLine2Data: const Color(0xffB9BABC), //da modificare in darkMode + colorBackground: Theme.of(context).colorScheme.tertiary, + ), + Row( children: [ - TextSpan( - text: numToCurrency(1050.65), - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: green), + const SizedBox(width: 16), + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.primary, + ), ), - TextSpan( - text: "€", + const SizedBox(width: 4), + Text( + "Current month", style: Theme.of(context) .textTheme - .labelLarge - ?.copyWith(color: green), + .labelMedium + ?.copyWith(color: Theme.of(context).colorScheme.primary), ), - ], - ), - ), - ], - ), - const SizedBox(width: 30), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "EXPENSES", - style: Theme.of(context).textTheme.labelMedium, - ), - RichText( - textScaleFactor: MediaQuery.of(context).textScaleFactor, - text: TextSpan( - children: [ - TextSpan( - text: numToCurrency(-1050.65), - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: red), + const SizedBox(width: 12), + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: grey2, + ), ), - TextSpan( - text: "€", + const SizedBox(width: 4), + Text( + "Last month", style: Theme.of(context) .textTheme - .labelLarge - ?.copyWith(color: red), + .labelMedium + ?.copyWith(color: Theme.of(context).colorScheme.primary), ), ], ), - ), - ], - ), - ], - ), - const SizedBox(height: 16), - const LineChartWidget( - line1Data: [ - FlSpot(0, 3), - FlSpot(1, 1.3), - FlSpot(2, -2), - FlSpot(3, -4.5), - FlSpot(4, -5), - FlSpot(5, -2.2), - FlSpot(6, -3.1), - FlSpot(7, -0.2), - FlSpot(8, -4), - FlSpot(9, -3), - FlSpot(10, -2), - FlSpot(11, -4), - FlSpot(12, 3), - FlSpot(13, 1.3), - FlSpot(14, -2), - FlSpot(15, -4.5), - FlSpot(16, 2.5), - ], - colorLine1Data: Color(0xff00152D), - line2Data: [ - FlSpot(0, -3), - FlSpot(1, -1.3), - FlSpot(2, 2), - FlSpot(3, 4.5), - FlSpot(4, 5), - FlSpot(5, 2.2), - FlSpot(6, 3.1), - FlSpot(7, 0.2), - FlSpot(8, 4), - FlSpot(9, 3), - FlSpot(10, 2), - FlSpot(11, 4), - FlSpot(12, -3), - FlSpot(13, -1.3), - FlSpot(14, 2), - FlSpot(15, 4.5), - FlSpot(16, 5), - FlSpot(17, 2.2), - FlSpot(18, 3.1), - FlSpot(19, 0.2), - FlSpot(20, 4), - FlSpot(21, 3), - FlSpot(22, 2), - FlSpot(23, 4), - FlSpot(24, -3), - FlSpot(25, -1.3), - FlSpot(26, 2), - FlSpot(27, 4.5), - FlSpot(28, 5), - FlSpot(29, 4.7), - FlSpot(30, 1), - ], - colorLine2Data: Color(0xffB9BABC), //da modificare in darkMode - colorBackground: Color(0xffF1F5F9), - maxY: 5.0, - minY: -5.0, - maxDays: 31.0, + const SizedBox(height: 22), + ], + ); + }, + loading: () => const SizedBox(height: 330), + error: (err, stack) => Text('Error: $err'), + ), + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, //da modificare in darkMode + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), ), - Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(width: 16), - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.primary, - ), - ), - const SizedBox(width: 4), - Text( - "Current month", - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith(color: Theme.of(context).colorScheme.primary), - ), - const SizedBox(width: 12), - Container( - width: 8, - height: 8, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: grey2, + Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), + child: Text( + "Your accounts", + style: Theme.of(context).textTheme.titleLarge, ), ), - const SizedBox(width: 4), - Text( - "Last month", - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith(color: Theme.of(context).colorScheme.primary), - ), - ], - ), - const SizedBox(height: 22), - ], - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .primaryContainer, //da modificare in darkMode - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), - child: Text( - "Your accounts", - style: Theme.of(context).textTheme.titleLarge, - ), - ), - SizedBox( - height: 86.0, - child: accountList.when( - data: (accounts) => ListView.builder( - itemCount: accounts.length + 1, - shrinkWrap: true, - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - physics: const BouncingScrollPhysics(), - scrollDirection: Axis.horizontal, - itemBuilder: (context, i) { - if (i == accounts.length) { - return Padding( - padding: const EdgeInsets.fromLTRB(0, 4, 0, 16), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - boxShadow: [defaultShadow], - ), - child: TextButton.icon( - style: ButtonStyle( - maximumSize: MaterialStateProperty.all( - const Size(130, 48)), - backgroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.surface), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8.0), - ), - ), + SizedBox( + height: 86.0, + child: accountList.when( + data: (accounts) => ListView.builder( + itemCount: accounts.length + 1, + shrinkWrap: true, + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemBuilder: (context, i) { + if (i == accounts.length) { + return Padding( + padding: const EdgeInsets.fromLTRB(0, 4, 0, 16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + boxShadow: [defaultShadow], ), - icon: Container( - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: blue5, - ), - child: const Padding( - padding: EdgeInsets.all(5.0), - child: Icon( - Icons.add_rounded, - size: 24.0, - color: white, + child: TextButton.icon( + style: ButtonStyle( + maximumSize: MaterialStateProperty.all(const Size(130, 48)), + backgroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.surface), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), ), ), - ), - label: Text( - "New Account", - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith( - color: Theme.of(context) - .colorScheme - .tertiary, + icon: Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: blue5, + ), + child: const Padding( + padding: EdgeInsets.all(5.0), + child: Icon( + Icons.add_rounded, + size: 24.0, + color: white, ), - maxLines: 2, + ), + ), + label: Text( + "New Account", + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + maxLines: 2, + ), + onPressed: () { + ref.read(accountsProvider.notifier).reset(); + Navigator.of(context).pushNamed('/add-account'); + }, ), - onPressed: () => Navigator.of(context) - .pushNamed('/add-account'), ), - ), - ); - } else { - BankAccount account = accounts[i]; - return AccountsSum(account: account); - } - }, + ); + } else { + BankAccount account = accounts[i]; + return AccountsSum(account: account); + } + }, + ), + loading: () => const SizedBox(), + error: (err, stack) => Text('Error: $err'), ), + ), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 32, 16, 8), + child: Text( + "Last transactions", + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + lastTransactions.when( + data: (transactions) => TransactionsList(transactions: transactions), loading: () => const SizedBox(), error: (err, stack) => Text('Error: $err'), ), - ), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 32, 16, 8), - child: Text( - "Last transactions", - style: Theme.of(context).textTheme.titleLarge, + const SizedBox(height: 28), + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + "Your budgets", + style: Theme.of(context).textTheme.titleLarge, + ), ), ), - ), - transactionList.when( - data: (transactions) => - TransactionsList(transactions: transactions), - loading: () => const SizedBox(), - error: (err, stack) => Text('Error: $err'), - ), - const SizedBox(height: 28), - Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - "Your budgets", - style: Theme.of(context).textTheme.titleLarge, + const SizedBox(height: 16), + Container( + margin: const EdgeInsets.only(left: 16.0, right: 16.0), + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BudgetCircularIndicator( + title: "TOTALE", + amount: 320, + perc: 0.25, + color: Color(0xFFEBC35F), + ), + BudgetCircularIndicator( + title: "SPESE", + amount: 500, + perc: 0.5, + color: Color(0xFFD336B6), + ), + BudgetCircularIndicator( + title: "SVAGO", + amount: 178.67, + perc: 0.88, + color: Color(0xFF8E5FEB), + ), + ], ), ), - ), - const SizedBox(height: 16), - Container( - margin: const EdgeInsets.only(left: 16.0, right: 16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ - BudgetCircularIndicator( - title: "TOTALE", - amount: 320, - perc: 0.25, - color: Color(0xFFEBC35F), - ), - BudgetCircularIndicator( - title: "SPESE", - amount: 500, - perc: 0.5, - color: Color(0xFFD336B6), - ), - BudgetCircularIndicator( - title: "SVAGO", - amount: 178.67, - perc: 0.88, - color: Color(0xFF8E5FEB), - ), - ], - ), - ), - const SizedBox(height: 50), - ], + const SizedBox(height: 50), + ], + ), ), - ), - ], + ], + ), ); } } diff --git a/lib/pages/more_info_page/more_info.dart b/lib/pages/more_info_page/more_info.dart index effdded..f17d119 100644 --- a/lib/pages/more_info_page/more_info.dart +++ b/lib/pages/more_info_page/more_info.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../custom_widgets/default_container.dart'; +import '../../custom_widgets/default_card.dart'; class MoreInfoPage extends ConsumerStatefulWidget { const MoreInfoPage({super.key}); @@ -36,21 +36,12 @@ class _MoreInfoPageState extends ConsumerState { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.background, - elevation: 0, - centerTitle: true, leading: IconButton( - icon: const Icon( - Icons.arrow_back_ios_new, - color: Color(0XFF7DA1C4), - ), - onPressed: () { - Navigator.pop(context); - // Return to previous page - }, + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => Navigator.pop(context), ), title: Text( - 'More Info', + 'App Info', style: Theme.of(context) .textTheme .headlineLarge! @@ -59,52 +50,49 @@ class _MoreInfoPageState extends ConsumerState { ), body: SingleChildScrollView( physics: const BouncingScrollPhysics(), - child: Column( - children: [ - ListView.separated( - itemCount: moreInfoOptions.length, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (context, index) => const SizedBox(height: 16), - itemBuilder: (context, i) { - List option = moreInfoOptions[i]; - return DefaultContainer( - onTap: () { - option[2] != null - ? Navigator.of(context).pushNamed(option[2] as String) - : print("click"); - }, - child: Row( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: ListView.separated( + itemCount: moreInfoOptions.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => const SizedBox(height: 16), + itemBuilder: (context, i) { + List option = moreInfoOptions[i]; + return DefaultCard( + onTap: () { + if(option[2] != null) { + Navigator.of(context).pushNamed(option[2] as String); + } + }, + child: Row( + children: [ + const SizedBox(width: 12.0), + Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(width: 12.0), - Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - option[0].toString(), - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: Theme.of(context).colorScheme.primary), - textAlign: TextAlign.left, - ), - Text( - option[1].toString(), - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Theme.of(context).colorScheme.primary), - textAlign: TextAlign.left, - ), - ], + Text( + option[0].toString(), + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: Theme.of(context).colorScheme.primary), + textAlign: TextAlign.left, + ), + Text( + option[1].toString(), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Theme.of(context).colorScheme.primary), + textAlign: TextAlign.left, ), ], ), - ); - }, - ), - ], + ], + ), + ); + }, ), ), ); diff --git a/lib/pages/notifications/notifications_settings.dart b/lib/pages/notifications/notifications_settings.dart new file mode 100644 index 0000000..e85b7dc --- /dev/null +++ b/lib/pages/notifications/notifications_settings.dart @@ -0,0 +1,162 @@ +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/settings_provider.dart'; + +class NotificationsSettings extends ConsumerStatefulWidget { + const NotificationsSettings({super.key}); + + @override + ConsumerState createState() => _NotificationsSettingsState(); +} + +class _NotificationsSettingsState extends ConsumerState with Functions { + @override + Widget build(BuildContext context) { + // Is used to init the state of the switches + ref.read(settingsProvider); + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => Navigator.pop(context), + ), + ), + body: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.primary, + ), + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.notifications_active, + size: 24.0, + color: Theme.of(context).colorScheme.background, + ), + ), + const SizedBox(width: 12.0), + Text( + "Notifications", + style: Theme.of(context) + .textTheme + .headlineLarge! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + ], + ), + ), + Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "Add transactions reminder", + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + CupertinoSwitch( + value: ref.watch(transactionReminderSwitchProvider), + onChanged: (value) { + ref.read(transactionReminderSwitchProvider.notifier).state = value; + ref.read(settingsProvider.notifier).updateNotifications(); + }, + ), + ], + ), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 28, top: 24, bottom: 8), + child: Text( + "RECURRING TRANSACTIONS", + style: Theme.of(context).textTheme.labelLarge!.copyWith(color: grey1), + ), + ), + Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(4), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "Recurring transaction added", + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + CupertinoSwitch( + value: ref.watch(transactionRecAddedSwitchProvider), + onChanged: (value) { + ref.read(transactionRecAddedSwitchProvider.notifier).state = value; + ref.read(settingsProvider.notifier).updateNotifications(); + }, + ), + ], + ), + const SizedBox(height: 12), + Divider( + height: 1, + color: Theme.of(context).colorScheme.primary.withOpacity(0.4), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + "Recurring transaction reminder", + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + CupertinoSwitch( + value: ref.watch(transactionRecReminderSwitchProvider), + onChanged: (value) { + ref.read(transactionRecReminderSwitchProvider.notifier).state = value; + ref.read(settingsProvider.notifier).updateNotifications(); + }, + ), + ], + ), + ], + ), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 28, top: 6), + child: Text( + "Remind me before a recurring transaction is added", + style: Theme.of(context).textTheme.labelMedium!.copyWith(color: grey1), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/planning_page/widget/budget_card.dart b/lib/pages/planning_page/widget/budget_card.dart index 9870a22..9c541f4 100644 --- a/lib/pages/planning_page/widget/budget_card.dart +++ b/lib/pages/planning_page/widget/budget_card.dart @@ -1,148 +1,144 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../constants/style.dart'; import '../../../model/budget.dart'; +import '../../../providers/budgets_provider.dart'; import 'budget_pie_chart.dart'; -class BudgetCard extends StatefulWidget { +class BudgetCard extends ConsumerStatefulWidget { const BudgetCard({super.key}); @override - State createState() => _BudgetCardState(); + ConsumerState createState() => _BudgetCardState(); } -class _BudgetCardState extends State { - int touchedIndex = -1; - - // temporary static data - List budgets = [ - const Budget(idCategory: 1, amountLimit: 100, active: true, name: "Travel"), - const Budget(idCategory: 2, amountLimit: 200, active: true, name: "Health"), - ]; - - createBudget() { - print("create budget"); - } - +class _BudgetCardState extends ConsumerState { @override Widget build(BuildContext context) { + final budgets = ref.watch(budgetsProvider); 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: budgets.isNotEmpty - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Composition", - style: Theme.of(context).textTheme.titleLarge), - BudgetPieChart(budgets: budgets), - Text("Progress", - style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: 10), - ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: budgets.length, - itemBuilder: (BuildContext context, int index) { - // Temporary static data of the amount spent on a budget. - int spent = 15; - Budget budget = budgets.elementAt(index); - return Column( - children: [ - Row( - children: [ - Text(budget.name!, - style: const TextStyle( - fontWeight: FontWeight.normal)), - const Spacer(), - Text("$spent/${budget.amountLimit}€", - style: const TextStyle( - fontWeight: FontWeight.normal)) - ], - ), - const SizedBox(height: 5), - SizedBox( - height: 10, - child: ClipRRect( - borderRadius: const BorderRadius.all( - Radius.circular(10)), - child: LinearProgressIndicator( - value: spent / budget.amountLimit, - backgroundColor: index == 0 - ? Colors.deepPurple.withOpacity(0.3) - : Colors.blue.withOpacity(0.3), - valueColor: AlwaysStoppedAnimation( - index == 0 - ? Colors.deepPurple - : Colors.blue), - color: Colors.black, - ))) - ], - ); - }, - separatorBuilder: (BuildContext context, int index) { - return const SizedBox(height: 15); - }, - ), - const SizedBox(height: 5), - const Divider(color: Colors.black), - const SizedBox(height: 5), - ElevatedButton.icon( - icon: Icon( - Icons.add_circle, - color: Theme.of(context).colorScheme.secondary, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: budgets.when( + data: (data) { + return data.isNotEmpty + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Composition", style: Theme.of(context).textTheme.titleLarge), + BudgetPieChart(budgets: data), + Text("Progress", style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 10), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: data.length, + itemBuilder: (BuildContext context, int index) { + // Temporary static data of the amount spent on a budget. + int spent = 50; + Budget budget = data.elementAt(index); + return Column( + children: [ + Row( + children: [ + Text( + budget.name!, + style: const TextStyle(fontWeight: FontWeight.normal), + ), + const Spacer(), + Text( + "$spent/${budget.amountLimit}€", + style: const TextStyle(fontWeight: FontWeight.normal), + ), + ], + ), + const SizedBox(height: 4), + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: LinearProgressIndicator( + value: spent / budget.amountLimit, + minHeight: 16, + backgroundColor: index == 0 + ? Colors.deepPurple.withOpacity(0.3) + : Colors.blue.withOpacity(0.3), + valueColor: AlwaysStoppedAnimation( + index == 0 ? Colors.deepPurple : Colors.blue), + borderRadius: const BorderRadius.all(Radius.circular(16)), + ), + ), + ], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const SizedBox(height: 15); + }, ), - onPressed: createBudget, - label: Text( - "Add category budget", - style: Theme.of(context).textTheme.titleSmall!.apply( - color: Theme.of(context).colorScheme.secondary), + const SizedBox(height: 5), + const Divider(color: grey2), + const SizedBox(height: 5), + TextButton.icon( + icon: Icon( + Icons.add_circle, + color: Theme.of(context).colorScheme.secondary, + ), + onPressed: null, // TODO + label: Text( + "Add category budget", + style: Theme.of(context) + .textTheme + .titleSmall! + .apply(color: Theme.of(context).colorScheme.secondary), + ), ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - fixedSize: const Size(330, 50), + ], + ) + : Column( + children: [ + Text( + "There are no budget set", + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, ), - ) - ], - ) - : Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text("There are no budget set", - style: TextStyle( - fontWeight: FontWeight.normal, fontSize: 13), - textAlign: TextAlign.center), - Image.asset( - 'assets/wallet.png', - width: 240, - height: 240, - ), - const Text( - "A monthly budget can help you keep track of your expenses and stay within the limits", - style: TextStyle( - fontWeight: FontWeight.normal, fontSize: 13), - textAlign: TextAlign.center), - const SizedBox(height: 15), - ElevatedButton.icon( - icon: Icon( - Icons.add_circle, - color: Theme.of(context).colorScheme.secondary, - ), - onPressed: createBudget, - label: Text( - "Create budget", - style: Theme.of(context).textTheme.titleSmall!.apply( - color: Theme.of(context).colorScheme.secondary), + Image.asset( + 'assets/wallet.png', + width: 240, + height: 240, ), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - fixedSize: const Size(330, 50), + Text( + "A monthly budget can help you keep track of your expenses and stay within the limits", + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, ), - ) - ], - ), - )); + const SizedBox(height: 15), + TextButton.icon( + icon: Icon( + Icons.add_circle, + color: Theme.of(context).colorScheme.secondary, + ), + onPressed: null, // TODO + label: Text( + "Create budget", + style: Theme.of(context) + .textTheme + .titleSmall! + .apply(color: Theme.of(context).colorScheme.secondary), + ), + style: TextButton.styleFrom( + backgroundColor: Colors.white, + ), + ) + ], + ); + }, + error: (error, stack) => const Text(""), + loading: () => const Center(child: CircularProgressIndicator()), + ), + ), + ); } } diff --git a/lib/pages/planning_page/widget/recurring_payments_card.dart b/lib/pages/planning_page/widget/recurring_payments_card.dart index eb72ae9..0b8d76c 100644 --- a/lib/pages/planning_page/widget/recurring_payments_card.dart +++ b/lib/pages/planning_page/widget/recurring_payments_card.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import '../../../model/recurring_transaction_amount.dart'; - class RecurringPaymentCard extends StatefulWidget { const RecurringPaymentCard({super.key}); @@ -10,7 +8,6 @@ class RecurringPaymentCard extends StatefulWidget { } class _RecurringPaymentCardState extends State { - void addRecurringPayment() { print("addRecurringPayment"); } @@ -18,36 +15,40 @@ class _RecurringPaymentCardState extends State { @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), - ), - ) - ], - ))); + 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/settings_page.dart b/lib/pages/settings_page.dart index d0aa426..620e344 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -1,21 +1,24 @@ // Settings page. +// ignore_for_file: unused_result + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../custom_widgets/default_container.dart'; + import '../constants/style.dart'; import '../custom_widgets/alert_dialog.dart'; +import '../custom_widgets/default_card.dart'; import '../database/sossoldi_database.dart'; -import '../providers/transactions_provider.dart'; import '../providers/accounts_provider.dart'; import '../providers/budgets_provider.dart'; import '../providers/categories_provider.dart'; +import '../providers/dashboard_provider.dart'; +import '../providers/transactions_provider.dart'; class SettingsPage extends ConsumerStatefulWidget { const SettingsPage({super.key}); @override - // ignore: library_private_types_in_public_api ConsumerState createState() => _SettingsPageState(); } @@ -54,7 +57,7 @@ var settingsOptions = const [ Icons.notifications_active, "Notifications", "Manage your notifications settings", - null, + "/notifications-settings", ], [ Icons.info, @@ -69,33 +72,41 @@ class _SettingsPageState extends ConsumerState { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.background, - elevation: 0, - centerTitle: true, leading: IconButton( - icon: const Icon( - Icons.arrow_back_ios_new, - color: Color(0XFF7DA1C4), - ), - onPressed: () { - Navigator.pop(context); - // Return to previous page - }, - ), - title: Text( - 'Settings', - style: Theme.of(context) - .textTheme - .headlineLarge! - .copyWith(color: Theme.of(context).colorScheme.primary), + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => Navigator.pop(context), ), ), body: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( children: [ - const Padding( - padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), + Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0), + child: Row( + children: [ + Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.primary, + ), + padding: const EdgeInsets.all(5.0), + child: Icon( + Icons.settings, + size: 28.0, + color: Theme.of(context).colorScheme.background, + ), + ), + const SizedBox(width: 12.0), + Text( + "Settings", + style: Theme.of(context) + .textTheme + .headlineLarge! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + ], + ), ), ListView.separated( itemCount: settingsOptions.length, @@ -104,11 +115,11 @@ class _SettingsPageState extends ConsumerState { separatorBuilder: (context, index) => const SizedBox(height: 16), itemBuilder: (context, i) { List setting = settingsOptions[i]; - return DefaultContainer( + return DefaultCard( onTap: () { - setting[3] != null - ? Navigator.of(context).pushNamed(setting[3] as String) - : print("click"); + if (setting[3] != null) { + Navigator.of(context).pushNamed(setting[3] as String); + } }, child: Row( children: [ @@ -125,27 +136,29 @@ class _SettingsPageState extends ConsumerState { ), ), const SizedBox(width: 12.0), - Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - setting[1].toString(), - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: Theme.of(context).colorScheme.primary), - textAlign: TextAlign.left, - ), - Text( - setting[2].toString(), - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Theme.of(context).colorScheme.primary), - textAlign: TextAlign.left, - ), - ], + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + setting[1].toString(), + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + Text( + setting[2].toString(), + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Theme.of(context).colorScheme.primary), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ], + ), ), ], ), @@ -156,52 +169,55 @@ class _SettingsPageState extends ConsumerState { ), ), bottomSheet: Container( - color: Colors.deepOrangeAccent, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const Text( - '[DEV ONLY]\nDANGEROUS\nZONE', - style: TextStyle( - fontSize: 11, - color: Colors.yellowAccent, - shadows: [ - Shadow( - offset: Offset(1.0, 1.0), - blurRadius: 3.0, - color: Color.fromARGB(255, 0, 0, 0), - ), - ], - ), - textAlign: TextAlign.center, - ), - ElevatedButton( - child: const Text('CLEAR DB'), - onPressed: () async { - await SossoldiDatabase.instance.clearDatabase().then((v) { - ref.refresh(accountsProvider); - ref.refresh(categoriesProvider); - ref.refresh(transactionsProvider); - ref.refresh(budgetsProvider); - showSuccessDialog(context, "DB Cleared"); - }); - }, - ), - ElevatedButton( - child: const Text('CLEAR AND FILL DEMO DATA'), - onPressed: () async { - await SossoldiDatabase.instance.clearDatabase(); - await SossoldiDatabase.instance.fillDemoData().then((value) { - ref.refresh(accountsProvider); - ref.refresh(categoriesProvider); - ref.refresh(transactionsProvider); - ref.refresh(budgetsProvider); - showSuccessDialog(context, "DB Cleared, and DEMO data added"); - }); - }, + color: Colors.deepOrangeAccent, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const Text( + '[DEV ONLY]\nDANGEROUS\nZONE', + style: TextStyle( + fontSize: 11, + color: Colors.yellowAccent, + shadows: [ + Shadow( + offset: Offset(1.0, 1.0), + blurRadius: 3.0, + color: Color.fromARGB(255, 0, 0, 0), + ), + ], ), - ], - )), + textAlign: TextAlign.center, + ), + ElevatedButton( + child: const Text('CLEAR DB'), + onPressed: () async { + await SossoldiDatabase.instance.clearDatabase().then((v) { + ref.refresh(accountsProvider); + ref.refresh(categoriesProvider); + ref.refresh(transactionsProvider); + ref.refresh(budgetsProvider); + showSuccessDialog(context, "DB Cleared"); + }); + }, + ), + ElevatedButton( + child: const Text('CLEAR AND FILL DEMO DATA'), + onPressed: () async { + await SossoldiDatabase.instance.clearDatabase(); + await SossoldiDatabase.instance.fillDemoData().then((value) { + ref.refresh(accountsProvider); + ref.refresh(categoriesProvider); + ref.refresh(transactionsProvider); + ref.refresh(budgetsProvider); + ref.refresh(dashboardProvider); + ref.refresh(lastTransactionsProvider); + showSuccessDialog(context, "DB Cleared, and DEMO data added"); + }); + }, + ), + ], + ), + ), ); } } diff --git a/lib/pages/statistics_page.dart b/lib/pages/statistics_page.dart index eee5e85..00d7cff 100644 --- a/lib/pages/statistics_page.dart +++ b/lib/pages/statistics_page.dart @@ -1,10 +1,11 @@ // Satistics page. +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../constants/functions.dart'; import '../constants/style.dart'; import '../custom_widgets/line_chart.dart'; -import 'package:fl_chart/fl_chart.dart'; import '../model/bank_account.dart'; import '../providers/accounts_provider.dart'; @@ -108,8 +109,6 @@ class _StatsPageState extends ConsumerState with Functions { line2Data: [], colorLine2Data: Color(0xffB9BABC), colorBackground: Color(0xffF1F5F9), - maxY: 5.0, - minY: -5.0, maxDays: 12.0, ), Align( diff --git a/lib/pages/structure.dart b/lib/pages/structure.dart index fada12d..2a0982f 100644 --- a/lib/pages/structure.dart +++ b/lib/pages/structure.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../constants/style.dart'; -import 'add_page/add_page.dart'; +import '../providers/transactions_provider.dart'; import 'home_page.dart'; import 'planning_page/planning_page.dart'; import 'statistics_page.dart'; @@ -40,17 +39,12 @@ class _StructureState extends ConsumerState { Widget build(BuildContext context) { final selectedIndex = ref.watch(selectedIndexProvider); return Scaffold( - // backgroundColor: blue7, - resizeToAvoidBottomInset: - false, // Prevent the fab moving up when the keyboard is opened + // Prevent the fab moving up when the keyboard is opened + resizeToAvoidBottomInset: false, appBar: AppBar( - // Sulla dashboard (0) setto il background blue - // backgroundColor: selectedIndex == 0 ? blue7 : Theme.of(context).colorScheme.background, - elevation: 0, - centerTitle: true, + backgroundColor: selectedIndex == 0 ? Theme.of(context).colorScheme.tertiary : Theme.of(context).colorScheme.background, title: Text( _pagesTitle.elementAt(selectedIndex), - style: Theme.of(context).textTheme.headlineLarge!, ), leading: Padding( padding: const EdgeInsets.only(left: 16), @@ -88,19 +82,15 @@ class _StructureState extends ConsumerState { ], ), body: Center( - child: _pages.elementAt(selectedIndex), + child: _pages[selectedIndex], ), bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, - selectedItemColor: Theme.of(context).colorScheme.primary, - // unselectedItemColor: grey1, selectedFontSize: 8, unselectedFontSize: 8, - // backgroundColor: const Color(0xFFF6F6F6), currentIndex: selectedIndex, - onTap: (index) => index != 2 - ? ref.read(selectedIndexProvider.notifier).state = index - : null, + onTap: (index) => + index != 2 ? ref.read(selectedIndexProvider.notifier).state = index : null, items: [ BottomNavigationBarItem( icon: Icon(selectedIndex == 0 ? Icons.home : Icons.home_outlined), @@ -114,15 +104,12 @@ class _StructureState extends ConsumerState { ), const BottomNavigationBarItem(icon: Text(""), label: ""), BottomNavigationBarItem( - icon: Icon(selectedIndex == 3 - ? Icons.calendar_today - : Icons.calendar_today_outlined), + icon: Icon(selectedIndex == 3 ? Icons.calendar_today : Icons.calendar_today_outlined), label: "PLANNING", ), BottomNavigationBarItem( - icon: Icon(selectedIndex == 4 - ? Icons.data_exploration - : Icons.data_exploration_outlined), + icon: + Icon(selectedIndex == 4 ? Icons.data_exploration : Icons.data_exploration_outlined), label: "GRAPHS", ), ], @@ -136,10 +123,12 @@ class _StructureState extends ConsumerState { size: 55, color: Theme.of(context).colorScheme.background, ), - onPressed: () => Navigator.of(context).pushNamed("/add-page"), + onPressed: () { + ref.read(transactionsProvider.notifier).reset(); + Navigator.of(context).pushNamed("/add-page"); + }, ), - floatingActionButtonLocation: - FloatingActionButtonLocation.miniCenterDocked, + floatingActionButtonLocation: FloatingActionButtonLocation.miniCenterDocked, ); } } diff --git a/lib/pages/transactions_page/transactions_page.dart b/lib/pages/transactions_page/transactions_page.dart index 2e85543..ceffd5c 100644 --- a/lib/pages/transactions_page/transactions_page.dart +++ b/lib/pages/transactions_page/transactions_page.dart @@ -1,18 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'widgets/custom_sliver_delegate.dart'; -import 'widgets/categories_tab.dart'; import 'widgets/accounts_tab.dart'; +import 'widgets/categories_tab.dart'; +import 'widgets/custom_sliver_delegate.dart'; import 'widgets/list_tab.dart'; -class TransactionsPage extends StatefulWidget { +class TransactionsPage extends ConsumerStatefulWidget { const TransactionsPage({super.key}); @override - State createState() => _TransactionsPageState(); + ConsumerState createState() => _TransactionsPageState(); } -class _TransactionsPageState extends State +class _TransactionsPageState extends ConsumerState with TickerProviderStateMixin { static const List myTabs = [ Tab(text: "List", height: 35), @@ -26,17 +27,6 @@ class _TransactionsPageState extends State final double headerMaxHeight = 140.0; final double headerMinHeight = 56.0; - final startDate = ValueNotifier(DateTime( - DateTime.now().year, - DateTime.now().month, - 1, - )); // last day of the month - final endDate = ValueNotifier(DateTime( - DateTime.now().year, - DateTime.now().month + 1, - 0, - )); - @override void initState() { super.initState(); @@ -85,9 +75,6 @@ class _TransactionsPageState extends State tabController: _tabController, expandedHeight: headerMaxHeight, minHeight: headerMinHeight, - amount: 290.89, // TODO: compute for current date range - startDate: startDate, - endDate: endDate, ), pinned: true, floating: true, diff --git a/lib/pages/transactions_page/widgets/account_list_tile.dart b/lib/pages/transactions_page/widgets/account_list_tile.dart index eb81a0a..fa453a4 100644 --- a/lib/pages/transactions_page/widgets/account_list_tile.dart +++ b/lib/pages/transactions_page/widgets/account_list_tile.dart @@ -1,8 +1,10 @@ import "package:flutter/material.dart"; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../constants/style.dart'; +import 'accounts_tab.dart'; -class AccountListTile extends StatelessWidget { +class AccountListTile extends ConsumerWidget { const AccountListTile({ super.key, required this.title, @@ -12,7 +14,6 @@ class AccountListTile extends StatelessWidget { required this.percent, required this.color, required this.icon, - required this.notifier, required this.index, }); @@ -23,134 +24,113 @@ class AccountListTile extends StatelessWidget { final double percent; final Color color; final IconData icon; - final ValueNotifier notifier; final int index; - /// Toogle the box to expand or collapse - void _toogleExpand() { - if (notifier.value == index) { - notifier.value = -1; - } else { - notifier.value = index; - } - } - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: notifier, - builder: (context, value, child) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: _toogleExpand, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: color.withAlpha(90), - ), - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 16.0, + Widget build(BuildContext context, WidgetRef ref) { + final selectedAccountIndex = ref.watch(selectedAccountIndexProvider); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () { + if (selectedAccountIndex == index) { + ref.invalidate(selectedAccountIndexProvider); + } else { + ref.read(selectedAccountIndexProvider.notifier).state = index; + } + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: color.withAlpha(90), + ), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 16.0, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + child: Icon( + icon, + color: Colors.white, + ), ), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color, - ), - child: Icon( - icon, - color: Colors.white, + const SizedBox(width: 8.0), + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + "${amount.toStringAsFixed(2)} €", + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(color: (amount > 0) ? green : red), + ), + ], ), - ), - const SizedBox(width: 8.0), - Expanded( - child: Column( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleMedium, - ), - Text( - "${amount.toStringAsFixed(2)} €", - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith( - color: (amount > 0) ? green : red), - ), - ], + Text( + "$nTransactions transactions", + style: Theme.of(context).textTheme.labelLarge, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "$nTransactions transactions", - style: Theme.of(context).textTheme.labelLarge, - ), - Text( - "${percent.toStringAsFixed(2)}%", - style: Theme.of(context).textTheme.labelLarge, - ), - ], + Text( + "${percent.toStringAsFixed(2)}%", + style: Theme.of(context).textTheme.labelLarge, ), ], ), - ), - const SizedBox(width: 8.0), - Icon( - (notifier.value == index) - ? Icons.expand_more - : Icons.chevron_right, - ), - ], + ], + ), ), - ), - ), - ExpandedSection( - expand: notifier.value == index, - child: Container( - color: white, - height: 70.0 * nTransactions, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: (nTransactions > 0) - ? List.generate( - 2 * nTransactions - 1, - (i) { - if (i % 2 == 0) { - return TransactionRow( - account: transactions[i ~/ 2]["account"], - amount: transactions[i ~/ 2]["amount"], - category: transactions[i ~/ 2]["category"], - title: transactions[i ~/ 2]["title"], - ); - } else { - return const Divider( - height: 1, - thickness: 1, - indent: 15, - endIndent: 15, - ); - } - }, - ) - : [], + const SizedBox(width: 8.0), + Icon( + (selectedAccountIndex == index) ? Icons.expand_more : Icons.chevron_right, ), + ], + ), + ), + ), + ExpandedSection( + expand: selectedAccountIndex == index, + child: Container( + color: white, + height: 70.0 * nTransactions, + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: nTransactions, + separatorBuilder: (context, index) => const Divider( + height: 1, + thickness: 1, + indent: 15, + endIndent: 15, ), - ) - ], - ); - }, + itemBuilder: (context, index) { + return TransactionRow( + transaction: transactions[index], + ); + }, + ), + ), + ) + ], ); } } @@ -158,16 +138,10 @@ class AccountListTile extends StatelessWidget { class TransactionRow extends StatelessWidget { const TransactionRow({ super.key, - required this.title, - required this.category, - required this.amount, - required this.account, + required this.transaction, }); - final String title; - final String category; - final double amount; - final String account; + final Map transaction; @override Widget build(BuildContext context) { @@ -187,15 +161,15 @@ class TransactionRow extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - title, + transaction['title'], style: Theme.of(context).textTheme.titleMedium, ), Text( - "${amount.toStringAsFixed(2)} €", + "${transaction['amount'].toStringAsFixed(2)} €", style: Theme.of(context) .textTheme .bodyLarge - ?.copyWith(color: (amount > 0) ? green : red), + ?.copyWith(color: (transaction['amount'] > 0) ? green : red), ), ], ), @@ -203,11 +177,11 @@ class TransactionRow extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - category.toUpperCase(), + transaction['category'].toUpperCase(), style: Theme.of(context).textTheme.labelLarge, ), Text( - account.toUpperCase(), + transaction['account'].toUpperCase(), style: Theme.of(context).textTheme.labelLarge, ), ], @@ -236,8 +210,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/pages/transactions_page/widgets/accounts_pie_chart.dart b/lib/pages/transactions_page/widgets/accounts_pie_chart.dart index fbd993b..eb5b0e2 100644 --- a/lib/pages/transactions_page/widgets/accounts_pie_chart.dart +++ b/lib/pages/transactions_page/widgets/accounts_pie_chart.dart @@ -1,101 +1,97 @@ -import "package:flutter/material.dart"; import 'package:fl_chart/fl_chart.dart'; +import "package:flutter/material.dart"; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../../constants/constants.dart'; import '../../../constants/functions.dart'; import '../../../constants/style.dart'; import '../../../model/bank_account.dart'; -import '../../../model/category_transaction.dart'; +import 'accounts_tab.dart'; -class AccountsPieChart extends StatelessWidget with Functions { +class AccountsPieChart extends ConsumerWidget with Functions { const AccountsPieChart({ - required this.notifier, required this.accounts, required this.amounts, required this.total, - Key? key, - }) : super(key: key); + super.key, + }); - final ValueNotifier notifier; final List accounts; final Map amounts; final double total; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final selectedAccountIndex = ref.watch(selectedAccountIndexProvider); return SizedBox( height: 200, - child: ValueListenableBuilder( - valueListenable: notifier, - builder: (context, value, child) { - return Stack( - alignment: Alignment.center, - children: [ - PieChart( - PieChartData( - startDegreeOffset: -90, - centerSpaceRadius: 70, - sectionsSpace: 0, - borderData: FlBorderData(show: false), - sections: showingSections(), - pieTouchData: PieTouchData( - touchCallback: (FlTouchEvent event, pieTouchResponse) { - // expand category when tapped - if (!event.isInterestedForInteractions || - pieTouchResponse == null || - pieTouchResponse.touchedSection == null) { - return; - } - notifier.value = - pieTouchResponse.touchedSection!.touchedSectionIndex; - }, - ), - ), + child: Stack( + alignment: Alignment.center, + children: [ + PieChart( + PieChartData( + startDegreeOffset: -90, + centerSpaceRadius: 70, + sectionsSpace: 0, + borderData: FlBorderData(show: false), + sections: showingSections(selectedAccountIndex), + pieTouchData: PieTouchData( + touchCallback: (FlTouchEvent event, pieTouchResponse) { + // expand category when tapped + if (!event.isInterestedForInteractions || + pieTouchResponse == null || + pieTouchResponse.touchedSection == null) { + return; + } + ref.read(selectedAccountIndexProvider.notifier).state = + pieTouchResponse.touchedSection!.touchedSectionIndex; + }, ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - (value != -1) - ? Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: accountColorList[accounts[value].color], - ), - child: Icon( - accountIconList[accounts[value].symbol] ?? - Icons.swap_horiz_rounded, - color: Colors.white, - ), - ) - : const SizedBox(), - Text( - (value != -1) - ? "${amounts[accounts[value].id]!.toStringAsFixed(2)} €" - : "${total.toStringAsFixed(2)} €", - style: Theme.of(context).textTheme.headlineLarge?.copyWith( - color: ((value != -1 && - amounts[accounts[value].id]! > 0) || - (value == -1 && total > 0)) - ? green - : red), - ), - (value != -1) - ? Text(accounts[value].name) - : const Text("Total"), - ], + ), + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + (selectedAccountIndex != -1) + ? Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: accountColorList[accounts[selectedAccountIndex].color], + ), + child: Icon( + accountIconList[accounts[selectedAccountIndex].symbol] ?? + Icons.swap_horiz_rounded, + color: Colors.white, + ), + ) + : const SizedBox(), + Text( + (selectedAccountIndex != -1) + ? "${amounts[accounts[selectedAccountIndex].id]!.toStringAsFixed(2)} €" + : "${total.toStringAsFixed(2)} €", + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + color: ((selectedAccountIndex != -1 && + amounts[accounts[selectedAccountIndex].id]! > 0) || + (selectedAccountIndex == -1 && total > 0)) + ? green + : red), ), + (selectedAccountIndex != -1) + ? Text(accounts[selectedAccountIndex].name) + : const Text("Total"), ], - ); - }, + ), + ], ), ); } - List showingSections() { + List showingSections(int index) { return List.generate( amounts.values.length, (i) { - final isTouched = (i == notifier.value); + final isTouched = (i == index); final radius = isTouched ? 30.0 : 25.0; return PieChartSectionData( diff --git a/lib/pages/transactions_page/widgets/accounts_tab.dart b/lib/pages/transactions_page/widgets/accounts_tab.dart index ddcd72e..4a6081f 100644 --- a/lib/pages/transactions_page/widgets/accounts_tab.dart +++ b/lib/pages/transactions_page/widgets/accounts_tab.dart @@ -1,243 +1,182 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sossoldi/pages/transactions_page/widgets/transaction_type_button.dart'; + import '../../../constants/constants.dart'; -import '../../../constants/style.dart'; import '../../../constants/functions.dart'; +import '../../../custom_widgets/default_container.dart'; import '../../../model/bank_account.dart'; -import '../../../model/category_transaction.dart'; +import '../../../model/transaction.dart'; import '../../../providers/accounts_provider.dart'; import '../../../providers/transactions_provider.dart'; -import 'accounts_pie_chart.dart'; import 'account_list_tile.dart'; +import 'accounts_pie_chart.dart'; -enum Type { income, expense } +final selectedAccountIndexProvider = StateProvider.autoDispose((ref) => -1); class AccountsTab extends ConsumerStatefulWidget { const AccountsTab({ - Key? key, - }) : super(key: key); + super.key, + }); @override ConsumerState createState() => _AccountsTabState(); } class _AccountsTabState extends ConsumerState with Functions { - final selectedCategory = ValueNotifier(-1); - - /// income or expenses - final transactionType = ValueNotifier(Type.income.index); - @override Widget build(BuildContext context) { - // TODO: query only categories with expenses/income during the selected month final accounts = ref.watch(accountsProvider); final transactions = ref.watch(transactionsProvider); + final transactionType = ref.watch(selectedTransactionTypeProvider); - // create a map to link each categories with a list of its transactions - // stored as Map to be passed to CategoryListTile - Map>> accountToTransactions = {}; - Map categoryToAmount = {}; - double total = 0; + // create a map to link each accounts with a list of its transactions + // stored as Map to be passed to AccountListTile + Map>> accountToTransactionsIncome = {}, + accountToTransactionsExpense = {}; + Map accountToAmountIncome = {}, accountToAmountExpense = {}; + double totalIncome = 0, totalExpense = 0; - for (var transaction in transactions.value ?? []) { + for (Transaction transaction in transactions.value ?? []) { final accountId = transaction.idBankAccount; - if (accountId != null) { - final updateValue = { - "account": transaction.idBankAccount.toString(), - "amount": transaction.amount, - "category": accountId.toString(), - "title": transaction.note, - }; - - if (accountToTransactions.containsKey(accountId)) { - accountToTransactions[accountId]?.add(updateValue); + final updateValue = { + "account": transaction.idBankAccount.toString(), + "amount": transaction.amount, + "category": accountId.toString(), + "title": transaction.note, + }; + if (transaction.type == TransactionType.income) { + if (accountToTransactionsIncome.containsKey(accountId)) { + accountToTransactionsIncome[accountId]?.add(updateValue); } else { - accountToTransactions.putIfAbsent(accountId, () => [updateValue]); + accountToTransactionsIncome.putIfAbsent(accountId, () => [updateValue]); } - // update total amount for the category - total += transaction.amount; - if (categoryToAmount.containsKey(accountId)) { - categoryToAmount[accountId] = - categoryToAmount[accountId]! + transaction.amount.toDouble(); + // update total amount for the account + totalIncome += transaction.amount; + if (accountToAmountIncome.containsKey(accountId)) { + accountToAmountIncome[accountId] = + accountToAmountIncome[accountId]! + transaction.amount.toDouble(); } else { - categoryToAmount.putIfAbsent(accountId, () => transaction.amount.toDouble()); + accountToAmountIncome.putIfAbsent(accountId, () => transaction.amount.toDouble()); + } + } else if (transaction.type == TransactionType.expense) { + if (accountToTransactionsExpense.containsKey(accountId)) { + accountToTransactionsExpense[accountId]?.add(updateValue); + } else { + accountToTransactionsExpense.putIfAbsent(accountId, () => [updateValue]); } - } - } - // Add missing catogories (with amount 0) - // This will be removed when only categories with transactions are queried - for (var account in accounts.value ?? []) { - if (account.id != null) { - categoryToAmount.putIfAbsent(account.id, () => 0); + // update total amount for the account + totalExpense -= transaction.amount; + if (accountToAmountExpense.containsKey(accountId)) { + accountToAmountExpense[accountId] = + accountToAmountExpense[accountId]! - transaction.amount.toDouble(); + } else { + accountToAmountExpense.putIfAbsent(accountId, () => -transaction.amount.toDouble()); + } } } - return Container( - margin: const EdgeInsets.symmetric(horizontal: 8.0), - padding: const EdgeInsets.symmetric(horizontal: 12.0), - color: grey3, - child: ListView( - children: [ - const SizedBox(height: 12.0), - TransactionTypeButton( - width: MediaQuery.of(context).size.width, - notifier: transactionType, - ), - const SizedBox(height: 16), - accounts.when( - data: (data) => AccountsPieChart( - notifier: selectedCategory, - accounts: accounts.value!, - amounts: categoryToAmount, - total: total, - ), - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), - loading: () => const Center( - child: CircularProgressIndicator(), - ), - ), - const SizedBox(height: 16), - accounts.when( - data: (data) { - return ValueListenableBuilder( - valueListenable: selectedCategory, - builder: (context, value, child) { - return ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: accounts.value!.length, - separatorBuilder: (context, index) => const SizedBox(height: 10), - itemBuilder: (context, index) { - BankAccount b = accounts.value![index]; - return AccountListTile( - title: b.name, - nTransactions: - accountToTransactions[b.id]?.length ?? 0, - transactions: accountToTransactions[b.id] ?? [], - amount: categoryToAmount[b.id] ?? 0, - percent: - (categoryToAmount[b.id] ?? 0) / total * 100, - color: accountColorList[b.color], - icon: - accountIconList[b.symbol] ?? Icons.swap_horiz_rounded, - notifier: selectedCategory, - index: index, + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 24), + child: DefaultContainer( + child: Column( + children: [ + const TransactionTypeButton(), + const SizedBox(height: 16), + accounts.when( + data: (data) { + List accountIncomeList = + data.where((account) => accountToAmountIncome.containsKey(account.id)).toList(); + List accountExpenseList = data + .where((account) => accountToAmountExpense.containsKey(account.id)) + .toList(); + return transactionType == TransactionType.income + ? accountIncomeList.isEmpty + ? const SizedBox( + height: 400, + child: Center( + child: Text("No incomes for selected month"), + ), + ) + : Column( + children: [ + AccountsPieChart( + accounts: accountIncomeList, + amounts: accountToAmountIncome, + total: totalIncome, + ), + const SizedBox(height: 16), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: accountIncomeList.length, + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (context, index) { + BankAccount b = accountIncomeList[index]; + return AccountListTile( + title: b.name, + nTransactions: accountToTransactionsIncome[b.id]?.length ?? 0, + transactions: accountToTransactionsIncome[b.id] ?? [], + amount: accountToAmountIncome[b.id] ?? 0, + percent: (accountToAmountIncome[b.id] ?? 0) / totalIncome * 100, + color: accountColorList[b.color], + icon: accountIconList[b.symbol] ?? Icons.swap_horiz_rounded, + index: index, + ); + }, + ) + ], + ) + : accountExpenseList.isEmpty + ? const SizedBox( + height: 400, + child: Center( + child: Text("No expenses for selected month"), + ), + ) + : Column( + children: [ + AccountsPieChart( + accounts: accountExpenseList, + amounts: accountToAmountExpense, + total: totalExpense, + ), + const SizedBox(height: 16), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: accountExpenseList.length, + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (context, index) { + BankAccount b = accountExpenseList[index]; + return AccountListTile( + title: b.name, + nTransactions: accountToTransactionsExpense[b.id]?.length ?? 0, + transactions: accountToTransactionsExpense[b.id] ?? [], + amount: accountToAmountExpense[b.id] ?? 0, + percent: + (accountToAmountExpense[b.id] ?? 0) / totalExpense * 100, + color: accountColorList[b.color], + icon: accountIconList[b.symbol] ?? Icons.swap_horiz_rounded, + index: index, + ); + }, + ) + ], ); - }, - ); - }, - ); - }, - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), - loading: () => const Center( - child: CircularProgressIndicator(), - ), - ), - const SizedBox(height: 12.0), - ], - ), - ); - } -} - -/// Switch between income and expenses -class TransactionTypeButton extends StatelessWidget { - const TransactionTypeButton({ - super.key, - required this.width, - required this.notifier, - }); - - final ValueNotifier notifier; - final double width; - final double height = 28.0; - - @override - Widget build(BuildContext context) { - return Container( - width: width, - height: height, - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all( - Radius.circular(5.0), - ), - ), - child: ValueListenableBuilder( - valueListenable: notifier, - builder: (context, value, child) { - return Stack( - children: [ - AnimatedAlign( - alignment: Alignment( - (notifier.value == Type.income.index) ? -1 : 1, - 0, - ), - curve: Curves.decelerate, - duration: const Duration(milliseconds: 180), - child: Container( - width: width * 0.5, - height: height, - decoration: const BoxDecoration( - color: blue5, - borderRadius: BorderRadius.all( - Radius.circular(5.0), - ), - ), - ), - ), - GestureDetector( - onTap: () { - notifier.value = Type.income.index; - }, - child: Align( - alignment: const Alignment(-1, 0), - child: Container( - width: width * 0.5, - color: Colors.transparent, - alignment: Alignment.center, - child: Text( - "Income", - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: (notifier.value == Type.income.index) - ? white - : blue2), - ), - ), - ), + }, + error: (error, stackTrace) => Center( + child: Text(error.toString()), ), - GestureDetector( - onTap: () { - notifier.value = Type.expense.index; - }, - child: Align( - alignment: const Alignment(1, 0), - child: Container( - width: width * 0.5, - color: Colors.transparent, - alignment: Alignment.center, - child: Text( - 'Expenses', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: (notifier.value == Type.expense.index) - ? white - : blue2), - ), - ), - ), + loading: () => const Center( + child: CircularProgressIndicator(), ), - ], - ); - }, + ), + ], + ), ), ); } } - - diff --git a/lib/pages/transactions_page/widgets/categories_pie_chart.dart b/lib/pages/transactions_page/widgets/categories_pie_chart.dart index 26ef961..8d17102 100644 --- a/lib/pages/transactions_page/widgets/categories_pie_chart.dart +++ b/lib/pages/transactions_page/widgets/categories_pie_chart.dart @@ -1,100 +1,95 @@ -import "package:flutter/material.dart"; import 'package:fl_chart/fl_chart.dart'; +import "package:flutter/material.dart"; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../../../constants/constants.dart'; import '../../../constants/functions.dart'; import '../../../constants/style.dart'; import '../../../model/category_transaction.dart'; +import 'categories_tab.dart'; -class CategoriesPieChart extends StatelessWidget with Functions { +class CategoriesPieChart extends ConsumerWidget with Functions { const CategoriesPieChart({ - required this.notifier, required this.categories, required this.amounts, required this.total, - Key? key, - }) : super(key: key); + super.key, + }); - final ValueNotifier notifier; final List categories; final Map amounts; final double total; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final selectedCategoryIndex = ref.watch(selectedCategoryIndexProvider); + final selectedCategory = + (selectedCategoryIndex >= 0) ? categories[selectedCategoryIndex] : null; return SizedBox( height: 200, - child: ValueListenableBuilder( - valueListenable: notifier, - builder: (context, value, child) { - return Stack( - alignment: Alignment.center, - children: [ - PieChart( - PieChartData( - startDegreeOffset: -90, - centerSpaceRadius: 70, - sectionsSpace: 0, - borderData: FlBorderData(show: false), - sections: showingSections(), - pieTouchData: PieTouchData( - touchCallback: (FlTouchEvent event, pieTouchResponse) { - // expand category when tapped - if (!event.isInterestedForInteractions || - pieTouchResponse == null || - pieTouchResponse.touchedSection == null) { - return; - } - notifier.value = - pieTouchResponse.touchedSection!.touchedSectionIndex; - }, - ), - ), + child: Stack( + alignment: Alignment.center, + children: [ + PieChart( + PieChartData( + startDegreeOffset: -90, + centerSpaceRadius: 70, + sectionsSpace: 0, + borderData: FlBorderData(show: false), + sections: showingSections(selectedCategoryIndex), + pieTouchData: PieTouchData( + touchCallback: (FlTouchEvent event, pieTouchResponse) { + // expand category when tapped + if (!event.isInterestedForInteractions || + pieTouchResponse == null || + pieTouchResponse.touchedSection == null) { + return; + } + ref.read(selectedCategoryIndexProvider.notifier).state = + pieTouchResponse.touchedSection!.touchedSectionIndex; + }, ), - Column( - mainAxisSize: MainAxisSize.min, - children: [ - (value != -1) - ? Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: categoryColorList[categories[value].color], - ), - child: Icon( - iconList[categories[value].symbol] ?? - Icons.swap_horiz_rounded, - color: Colors.white, - ), - ) - : const SizedBox(), - Text( - (value != -1) - ? "${amounts[categories[value].id]!.toStringAsFixed(2)} €" - : "${total.toStringAsFixed(2)} €", - style: Theme.of(context).textTheme.headlineLarge?.copyWith( - color: ((value != -1 && - amounts[categories[value].id]! > 0) || - (value == -1 && total > 0)) - ? green - : red), - ), - (value != -1) - ? Text(categories[value].name) - : const Text("Total"), - ], + ), + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + (selectedCategory != null) + ? Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: categoryColorList[selectedCategory.color], + ), + child: Icon( + iconList[selectedCategory.symbol] ?? Icons.swap_horiz_rounded, + color: Colors.white, + ), + ) + : const SizedBox(), + Text( + (selectedCategory != null) + ? "${amounts[selectedCategory.id]!.toStringAsFixed(2)} €" + : "${total.toStringAsFixed(2)} €", + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + color: ((selectedCategory != null && amounts[selectedCategory.id]! > 0) || + (selectedCategory == null && total > 0)) + ? green + : red), ), + (selectedCategory != null) ? Text(selectedCategory.name) : const Text("Total"), ], - ); - }, + ), + ], ), ); } - List showingSections() { + List? showingSections(int index) { return List.generate( amounts.values.length, (i) { - final isTouched = (i == notifier.value); + final isTouched = (i == index); final radius = isTouched ? 30.0 : 25.0; return PieChartSectionData( diff --git a/lib/pages/transactions_page/widgets/categories_tab.dart b/lib/pages/transactions_page/widgets/categories_tab.dart index dd0d73d..e83902a 100644 --- a/lib/pages/transactions_page/widgets/categories_tab.dart +++ b/lib/pages/transactions_page/widgets/categories_tab.dart @@ -1,45 +1,42 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../constants/constants.dart'; -import '../../../constants/style.dart'; +import 'package:sossoldi/pages/transactions_page/widgets/transaction_type_button.dart'; + import '../../../constants/functions.dart'; +import '../../../custom_widgets/default_container.dart'; import '../../../model/category_transaction.dart'; +import '../../../model/transaction.dart'; import '../../../providers/categories_provider.dart'; import '../../../providers/transactions_provider.dart'; import 'categories_pie_chart.dart'; import 'category_list_tile.dart'; -enum Type { income, expense } +final selectedCategoryIndexProvider = StateProvider.autoDispose((ref) => -1); class CategoriesTab extends ConsumerStatefulWidget { const CategoriesTab({ - Key? key, - }) : super(key: key); + super.key, + }); @override ConsumerState createState() => _CategoriesTabState(); } class _CategoriesTabState extends ConsumerState with Functions { - final selectedCategory = ValueNotifier(-1); - - /// income or expenses - final transactionType = ValueNotifier(Type.income.index); - @override Widget build(BuildContext context) { - // TODO: query only categories with expenses/income during the selected month final categories = ref.watch(categoriesProvider); final transactions = ref.watch(transactionsProvider); + final transactionType = ref.watch(selectedTransactionTypeProvider); // create a map to link each categories with a list of its transactions // stored as Map to be passed to CategoryListTile - Map>> categoryToTransactions = {}; - Map categoryToAmount = {}; - double total = 0; + Map>> categoryToTransactionsIncome = {}, + categoryToTransactionsExpense = {}; + Map categoryToAmountIncome = {}, categoryToAmountExpense = {}; + double totalIncome = 0, totalExpense = 0; - for (var transaction in transactions.value ?? []) { - print(transaction.idCategory); + for (Transaction transaction in transactions.value ?? []) { final categoryId = transaction.idCategory; if (categoryId != null) { final updateValue = { @@ -48,193 +45,136 @@ class _CategoriesTabState extends ConsumerState with Functions { "category": categoryId.toString(), "title": transaction.note, }; + if (transaction.type == TransactionType.income) { + if (categoryToTransactionsIncome.containsKey(categoryId)) { + categoryToTransactionsIncome[categoryId]?.add(updateValue); + } else { + categoryToTransactionsIncome.putIfAbsent(categoryId, () => [updateValue]); + } - if (categoryToTransactions.containsKey(categoryId)) { - categoryToTransactions[categoryId]?.add(updateValue); - } else { - categoryToTransactions.putIfAbsent(categoryId, () => [updateValue]); - } + // update total amount for the category + totalIncome += transaction.amount; + if (categoryToAmountIncome.containsKey(categoryId)) { + categoryToAmountIncome[categoryId] = + categoryToAmountIncome[categoryId]! + transaction.amount.toDouble(); + } else { + categoryToAmountIncome.putIfAbsent(categoryId, () => transaction.amount.toDouble()); + } + } else if (transaction.type == TransactionType.expense) { + if (categoryToTransactionsExpense.containsKey(categoryId)) { + categoryToTransactionsExpense[categoryId]?.add(updateValue); + } else { + categoryToTransactionsExpense.putIfAbsent(categoryId, () => [updateValue]); + } - // update total amount for the category - total += transaction.amount; - if (categoryToAmount.containsKey(categoryId)) { - categoryToAmount[categoryId] = - categoryToAmount[categoryId]! + transaction.amount.toDouble(); - } else { - categoryToAmount.putIfAbsent(categoryId, () => transaction.amount.toDouble()); + // update total amount for the category + totalExpense -= transaction.amount; + if (categoryToAmountExpense.containsKey(categoryId)) { + categoryToAmountExpense[categoryId] = + categoryToAmountExpense[categoryId]! - transaction.amount.toDouble(); + } else { + categoryToAmountExpense.putIfAbsent(categoryId, () => -transaction.amount.toDouble()); + } } } } - // Add missing catogories (with amount 0) - // This will be removed when only categories with transactions are queried - for (var category in categories.value ?? []) { - if (category.id != null) { - categoryToAmount.putIfAbsent(category.id, () => 0); - } - } - - return Container( - margin: const EdgeInsets.symmetric(horizontal: 8.0), - padding: const EdgeInsets.symmetric(horizontal: 12.0), - color: grey3, - child: ListView( - children: [ - const SizedBox(height: 12.0), - TransactionTypeButton( - width: MediaQuery.of(context).size.width, - notifier: transactionType, - ), - const SizedBox(height: 16), - categories.when( - data: (data) => CategoriesPieChart( - notifier: selectedCategory, - categories: categories.value!, - amounts: categoryToAmount, - total: total, - ), - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), - loading: () => const Center( - child: CircularProgressIndicator(), - ), - ), - const SizedBox(height: 16), - categories.when( - data: (data) { - return ValueListenableBuilder( - valueListenable: selectedCategory, - builder: (context, value, child) { - return ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: categories.value!.length, - separatorBuilder: (context, index) => const SizedBox(height: 10), - itemBuilder: (context, index) { - CategoryTransaction t = categories.value![index]; - return CategoryListTile( - title: t.name, - nTransactions: - categoryToTransactions[t.id]?.length ?? 0, - transactions: categoryToTransactions[t.id] ?? [], - amount: categoryToAmount[t.id] ?? 0, - percent: - (categoryToAmount[t.id] ?? 0) / total * 100, - color: categoryColorList[t.color], - icon: - iconList[t.symbol] ?? Icons.swap_horiz_rounded, - notifier: selectedCategory, - index: index, + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 24), + child: DefaultContainer( + child: Column( + children: [ + const TransactionTypeButton(), + const SizedBox(height: 16), + categories.when( + data: (data) { + List categoryIncomeList = data + .where((category) => categoryToAmountIncome.containsKey(category.id)) + .toList(); + List categoryExpenseList = data + .where((category) => categoryToAmountExpense.containsKey(category.id)) + .toList(); + return transactionType == TransactionType.income + ? categoryIncomeList.isEmpty + ? const SizedBox( + height: 400, + child: Center( + child: Text("No incomes for selected month"), + ), + ) + : Column( + children: [ + CategoriesPieChart( + categories: categoryIncomeList, + amounts: categoryToAmountIncome, + total: totalIncome, + ), + const SizedBox(height: 16), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: categoryIncomeList.length, + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (context, index) { + CategoryTransaction category = categoryIncomeList[index]; + return CategoryListTile( + category: category, + transactions: categoryToTransactionsIncome[category.id] ?? [], + amount: categoryToAmountIncome[category.id] ?? 0, + percent: (categoryToAmountIncome[category.id] ?? 0) / + totalIncome * + 100, + index: index, + ); + }, + ), + ], + ) + : categoryExpenseList.isEmpty + ? const SizedBox( + height: 400, + child: Center( + child: Text("No expenses for selected month"), + ), + ) + : Column( + children: [ + CategoriesPieChart( + categories: categoryExpenseList, + amounts: categoryToAmountExpense, + total: totalExpense, + ), + const SizedBox(height: 16), + ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: categoryExpenseList.length, + separatorBuilder: (context, index) => const SizedBox(height: 10), + itemBuilder: (context, index) { + CategoryTransaction category = categoryExpenseList[index]; + return CategoryListTile( + category: category, + transactions: categoryToTransactionsExpense[category.id] ?? [], + amount: categoryToAmountExpense[category.id] ?? 0, + percent: (categoryToAmountExpense[category.id] ?? 0) / + totalExpense * + 100, + index: index, + ); + }, + ), + ], ); - }, - ); - }, - ); - }, - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), - loading: () => const Center( - child: CircularProgressIndicator(), - ), - ), - const SizedBox(height: 12.0), - ], - ), - ); - } -} - -/// Switch between income and expenses -class TransactionTypeButton extends StatelessWidget { - const TransactionTypeButton({ - super.key, - required this.width, - required this.notifier, - }); - - final ValueNotifier notifier; - final double width; - final double height = 28.0; - - @override - Widget build(BuildContext context) { - return Container( - width: width, - height: height, - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all( - Radius.circular(5.0), - ), - ), - child: ValueListenableBuilder( - valueListenable: notifier, - builder: (context, value, child) { - return Stack( - children: [ - AnimatedAlign( - alignment: Alignment( - (notifier.value == Type.income.index) ? -1 : 1, - 0, - ), - curve: Curves.decelerate, - duration: const Duration(milliseconds: 180), - child: Container( - width: width * 0.5, - height: height, - decoration: const BoxDecoration( - color: blue5, - borderRadius: BorderRadius.all( - Radius.circular(5.0), - ), - ), - ), + }, + error: (error, stackTrace) => Center( + child: Text(error.toString()), ), - GestureDetector( - onTap: () { - notifier.value = Type.income.index; - }, - child: Align( - alignment: const Alignment(-1, 0), - child: Container( - width: width * 0.5, - color: Colors.transparent, - alignment: Alignment.center, - child: Text( - "Income", - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: (notifier.value == Type.income.index) - ? white - : blue2), - ), - ), - ), + loading: () => const Center( + child: CircularProgressIndicator(), ), - GestureDetector( - onTap: () { - notifier.value = Type.expense.index; - }, - child: Align( - alignment: const Alignment(1, 0), - child: Container( - width: width * 0.5, - color: Colors.transparent, - alignment: Alignment.center, - child: Text( - 'Expenses', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: (notifier.value == Type.expense.index) - ? white - : blue2), - ), - ), - ), - ), - ], - ); - }, + ), + ], + ), ), ); } diff --git a/lib/pages/transactions_page/widgets/category_list_tile.dart b/lib/pages/transactions_page/widgets/category_list_tile.dart index 362c2c7..85ab855 100644 --- a/lib/pages/transactions_page/widgets/category_list_tile.dart +++ b/lib/pages/transactions_page/widgets/category_list_tile.dart @@ -1,184 +1,147 @@ import "package:flutter/material.dart"; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../constants/constants.dart'; import '../../../constants/style.dart'; +import '../../../model/category_transaction.dart'; +import 'categories_tab.dart'; -class CategoryListTile extends StatelessWidget { +class CategoryListTile extends ConsumerWidget { const CategoryListTile({ super.key, - required this.title, + required this.category, required this.amount, - required this.nTransactions, required this.transactions, required this.percent, - required this.color, - required this.icon, - required this.notifier, required this.index, }); - final String title; + final CategoryTransaction category; final double amount; - final int nTransactions; final List> transactions; final double percent; - final Color color; - final IconData icon; - final ValueNotifier notifier; final int index; - /// Toogle the box to expand or collapse - void _toogleExpand() { - if (notifier.value == index) { - notifier.value = -1; - } else { - notifier.value = index; - } - } - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: notifier, - builder: (context, value, child) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: _toogleExpand, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: color.withAlpha(90), - ), - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 16.0, + Widget build(BuildContext context, WidgetRef ref) { + final selectedCategoryIndex = ref.watch(selectedCategoryIndexProvider); + final nTransactions = transactions.length; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () { + if (selectedCategoryIndex == index) { + ref.invalidate(selectedCategoryIndexProvider); + } else { + ref.read(selectedCategoryIndexProvider.notifier).state = index; + } + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: categoryColorList[category.color].withAlpha(90), + ), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 16.0, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: categoryColorList[category.color], + ), + child: Icon( + iconList[category.symbol], + color: Colors.white, + ), ), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color, - ), - child: Icon(icon, color: Colors.white, + const SizedBox(width: 8.0), + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + category.name, + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + "${amount.toStringAsFixed(2)} €", + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(color: (amount > 0) ? green : red), + ), + ], ), - ), - const SizedBox(width: 8.0), - Expanded( - child: Column( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: Theme - .of(context) - .textTheme - .titleMedium, - ), - Text( - "${amount.toStringAsFixed(2)} €", - style: Theme - .of(context) - .textTheme - .bodyLarge - ?.copyWith( - color: (amount > 0) ? green : red), - ), - ], + Text( + "$nTransactions transactions", + style: Theme.of(context).textTheme.labelLarge, ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "$nTransactions transactions", - style: Theme - .of(context) - .textTheme - .labelLarge, - ), - Text( - "${percent.toStringAsFixed(2)}%", - style: Theme - .of(context) - .textTheme - .labelLarge, - ), - ], + Text( + "${percent.toStringAsFixed(2)}%", + style: Theme.of(context).textTheme.labelLarge, ), ], ), - ), - const SizedBox(width: 8.0), - Icon( - (notifier.value == index) - ? Icons.expand_more - : Icons.chevron_right, - ), - ], + ], + ), ), - ), - ), - ExpandedSection( - expand: notifier.value == index, - child: Container( - color: white, - height: 70.0 * nTransactions, - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: (nTransactions > 0) - ? List.generate( - 2 * nTransactions - 1, - (i) { - if (i % 2 == 0) { - return TransactionRow( - account: transactions[i ~/ 2]["account"], - amount: transactions[i ~/ 2]["amount"], - category: transactions[i ~/ 2]["category"], - title: transactions[i ~/ 2]["title"], - ); - } else { - return const Divider( - height: 1, - thickness: 1, - indent: 15, - endIndent: 15, - ); - } - }, - ) - : [], + const SizedBox(width: 8.0), + Icon( + (selectedCategoryIndex == index) ? Icons.expand_more : Icons.chevron_right, ), + ], + ), + ), + ), + ExpandedSection( + expand: selectedCategoryIndex == index, + child: Container( + color: white, + height: 70.0 * nTransactions, + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: nTransactions, + separatorBuilder: (context, index) => const Divider( + height: 1, + thickness: 1, + indent: 15, + endIndent: 15, ), - ) - ], - ); - }, + itemBuilder: (context, index) { + return TransactionRow( + transaction: transactions[index], + ); + }, + ), + ), + ) + ], ); } } -class TransactionRow extends StatelessWidget { +class TransactionRow extends ConsumerWidget { const TransactionRow({ super.key, - required this.title, - required this.category, - required this.amount, - required this.account, + required this.transaction, }); - final String title; - final String category; - final double amount; - final String account; + final Map transaction; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Container( padding: const EdgeInsets.symmetric( horizontal: 8.0, @@ -195,19 +158,15 @@ class TransactionRow extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - title, - style: Theme - .of(context) - .textTheme - .titleMedium, + transaction['title'], + style: Theme.of(context).textTheme.titleMedium, ), Text( - "${amount.toStringAsFixed(2)} €", - style: Theme - .of(context) + "${transaction['amount'].toStringAsFixed(2)} €", + style: Theme.of(context) .textTheme .bodyLarge - ?.copyWith(color: (amount > 0) ? green : red), + ?.copyWith(color: (transaction['amount'] > 0) ? green : red), ), ], ), @@ -215,18 +174,12 @@ class TransactionRow extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - category.toUpperCase(), - style: Theme - .of(context) - .textTheme - .labelLarge, + transaction['category'].toUpperCase(), + style: Theme.of(context).textTheme.labelLarge, ), Text( - account.toUpperCase(), - style: Theme - .of(context) - .textTheme - .labelLarge, + transaction['account'].toUpperCase(), + style: Theme.of(context).textTheme.labelLarge, ), ], ), @@ -254,8 +207,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/pages/transactions_page/widgets/custom_sliver_delegate.dart b/lib/pages/transactions_page/widgets/custom_sliver_delegate.dart index 6bbd146..5d527f0 100644 --- a/lib/pages/transactions_page/widgets/custom_sliver_delegate.dart +++ b/lib/pages/transactions_page/widgets/custom_sliver_delegate.dart @@ -2,8 +2,11 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../constants/functions.dart'; import '../../../constants/style.dart'; +import '../../../providers/transactions_provider.dart'; import '../../../utils/formatted_date_range.dart'; import 'month_selector.dart'; @@ -14,19 +17,12 @@ class CustomSliverDelegate extends SliverPersistentHeaderDelegate { required this.tabController, required this.expandedHeight, required this.minHeight, - required this.startDate, - required this.endDate, - required this.amount, }); final TabController tabController; final List myTabs; final TickerProvider ticker; - final ValueNotifier startDate; - final ValueNotifier endDate; - - final double amount; final double minHeight; final double expandedHeight; @@ -51,21 +47,6 @@ class CustomSliverDelegate extends SliverPersistentHeaderDelegate { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ConstrainedBox( - constraints: BoxConstraints.tightFor( - height: max( - 18, - 25 * (1 - shrinkPercentage), - ), - ), - child: FittedBox( - alignment: Alignment.topLeft, - child: Text( - "View:", - style: Theme.of(context).textTheme.titleLarge, - ), - ), - ), Expanded( child: Stack( alignment: Alignment.bottomCenter, @@ -78,7 +59,7 @@ class CustomSliverDelegate extends SliverPersistentHeaderDelegate { if (shrinkPercentage > .5) Opacity( opacity: shrinkPercentage, - child: _buildCollapsedWidget(context), + child: CollapsedWidget(myTabs, tabController), ), ], ), @@ -102,13 +83,12 @@ class CustomSliverDelegate extends SliverPersistentHeaderDelegate { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox(height: 8), TabBar( controller: tabController, tabs: myTabs, labelColor: Colors.white, indicatorSize: TabBarIndicatorSize.tab, - unselectedLabelColor: grey2, + unselectedLabelColor: Theme.of(context).colorScheme.secondary, splashBorderRadius: BorderRadius.circular(50), indicator: BoxDecoration( borderRadius: BorderRadius.circular(50), @@ -117,17 +97,10 @@ class CustomSliverDelegate extends SliverPersistentHeaderDelegate { // TODO: capitalize text of the selected label // not possible from TextStyle https://github.com/flutter/flutter/issues/22695 labelStyle: Theme.of(context).textTheme.bodyLarge, - unselectedLabelStyle: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(fontWeight: FontWeight.w400), - ), - const SizedBox(height: 8), - MonthSelector( - amount: amount, - startDate: startDate, - endDate: endDate, + unselectedLabelStyle: Theme.of(context).textTheme.bodyLarge, ), + const SizedBox(height: 16), + const MonthSelector(), ], ), ), @@ -136,43 +109,6 @@ class CustomSliverDelegate extends SliverPersistentHeaderDelegate { ); } - _buildCollapsedWidget(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 8.0, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50), - color: Theme.of(context).colorScheme.secondary, - ), - child: Text( - myTabs[tabController.index].text!, - textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: Colors.white), - ), - ), - Text( - getFormattedDateRange(startDate.value, endDate.value), - style: Theme.of(context).textTheme.bodyLarge, - ), - Text( - "$amount €", - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: (amount > 0) ? green : red), - ), - ], - ); - } - @override double get maxExtent => expandedHeight; @@ -193,3 +129,63 @@ class CustomSliverDelegate extends SliverPersistentHeaderDelegate { ); } } + +class CollapsedWidget extends StatelessWidget with Functions { + const CollapsedWidget(this.myTabs, this.tabController, {super.key}); + + final List myTabs; + final TabController tabController; + + @override + Widget build(BuildContext context) { + return Consumer(builder: (context, ref, child) { + final totalAmount = ref.watch(totalAmountProvider); + final startDate = ref.watch(filterDateStartProvider); + final endDate = ref.watch(filterDateEndProvider); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8.0, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: Theme.of(context).colorScheme.secondary, + ), + child: Text( + myTabs[tabController.index].text!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge?.copyWith(color: Colors.white), + ), + ), + Text( + getFormattedDateRange(startDate, endDate), + style: Theme.of(context).textTheme.bodyLarge, + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: numToCurrency(totalAmount), + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: totalAmount >= 0 ? green : red), + ), + TextSpan( + text: "€", + style: Theme.of(context) + .textTheme + .labelLarge! + .copyWith(color: totalAmount >= 0 ? green : red), + ), + ], + ), + ), + ], + ); + }); + } +} diff --git a/lib/pages/transactions_page/widgets/list_tab.dart b/lib/pages/transactions_page/widgets/list_tab.dart index fd3f387..603b46e 100644 --- a/lib/pages/transactions_page/widgets/list_tab.dart +++ b/lib/pages/transactions_page/widgets/list_tab.dart @@ -1,21 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../constants/constants.dart'; -import '../../../constants/style.dart'; import '../../../constants/functions.dart'; -import '../../../model/category_transaction.dart'; -import '../../../model/transaction.dart'; -import '../../../providers/accounts_provider.dart'; +import '../../../custom_widgets/transactions_list.dart'; import '../../../providers/transactions_provider.dart'; -import '../../../providers/categories_provider.dart'; -import '../../../utils/date_helper.dart'; -import '../../transactions_page/widgets/transaction_list_tile.dart'; class ListTab extends ConsumerStatefulWidget { const ListTab({ - Key? key, - }) : super(key: key); + super.key, + }); @override ConsumerState createState() => _ListTabState(); @@ -24,157 +17,27 @@ class ListTab extends ConsumerStatefulWidget { class _ListTabState extends ConsumerState with Functions { @override Widget build(BuildContext context) { - final categories = ref.watch(categoriesProvider); - final transactions = ref.watch(transactionsProvider); - final accounts = ref.watch(accountsProvider); - - // calculate the total for each day - Map totals = {}; - if (transactions.value != null) { - for (var t in transactions.value!) { - String date = t.date.toYMD(); - if (totals.containsKey(date)) { - if (t.type == Type.expense) { - totals[date] = totals[date]! - t.amount.toDouble(); - } else if (t.type == Type.income) { - totals[date] = totals[date]! + t.amount.toDouble(); - } - } else { - if (t.type == Type.expense) { - totals.putIfAbsent(date, () => -t.amount.toDouble()); - } else if (t.type == Type.income) { - totals.putIfAbsent(date, () => t.amount.toDouble()); - } - } - } - } + final asyncTransactions = ref.watch(transactionsProvider); return Container( - margin: const EdgeInsets.symmetric(horizontal: 8.0), - padding: const EdgeInsets.symmetric(horizontal: 12.0), - color: Theme.of(context).colorScheme.primaryContainer, // da sistemare prima il layout della pagina - child: transactions.when( - data: (data) { - return ListView.separated( - itemCount: transactions.value!.length + 1, - itemBuilder: (context, i) { - if (i == 0) { - return DateSeparator( - transaction: transactions.value![i], - total: totals[transactions.value![i].date.toYMD()] ?? 0, - ); - } else { - Transaction transaction = transactions.value![i - 1]; - Iterable tCategories = - (transaction.type != Type.transfer && - categories.value != null) - ? categories.value! - .where((e) => e.id == transaction.idCategory) - : []; - - String account = accounts.value! - .firstWhere((e) => e.id == transaction.idBankAccount) - .name; - - // account the money is moved to in a trasfer - String targetAccount = (transaction.type == Type.transfer) - ? accounts.value! - .firstWhere( - (element) => - element.id == transaction.idBankAccountTransfer, - ) - .name - : ""; - - return TransactionListTile( - transaction: transaction, - title: transaction.note ?? "", - type: transaction.type, - amount: transaction.amount.toDouble(), - account: (transaction.type == Type.transfer) - ? "$account → $targetAccount" - : account, - category: (tCategories.isNotEmpty) - ? tCategories.first.name - : "no category", - color: (tCategories.isNotEmpty) - ? categoryColorList[tCategories.first.color] - : blue3, - icon: (tCategories.isNotEmpty) - ? iconList[tCategories.first.symbol] ?? - Icons.swap_horiz_rounded - : Icons.swap_horiz_rounded, - ); - } - }, - separatorBuilder: (context, i) { - if (i == 0) { - return const SizedBox(height: 0); - } else { - if (!transactions.value![i - 1].date - .isSameDate(transactions.value![i].date)) { - return DateSeparator( - transaction: transactions.value![i], - total: totals[transactions.value![i].date.toYMD()] ?? 0, - ); - } else { - return const Divider( - height: 0, - thickness: 1, - endIndent: 10, - indent: 10, - ); - } - } - }, + child: asyncTransactions.when( + data: (transactions) { + return TransactionsList( + padding: const EdgeInsets.symmetric(vertical: 16.0), + transactions: transactions, ); }, loading: () { - return const Center( - child: CircularProgressIndicator(), + return Container( + color: Colors.white, ); }, error: (error, stackTrace) { return Center( - child: Text(error.toString()), + child: Text(stackTrace.toString()), ); }, ), ); } } - -class DateSeparator extends StatelessWidget with Functions { - const DateSeparator({ - Key? key, - required this.transaction, - required this.total, - }) : super(key: key); - - final Transaction transaction; - final double total; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: 12.0, bottom: 6.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - transaction.date.formatDate(), - style: - Theme.of(context).textTheme.bodySmall?.copyWith(color: blue1), - ), - Text( - "${numToCurrency(total)} €", - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: (total > 0) ? green : red), - ) - ], - ), - ); - } -} diff --git a/lib/pages/transactions_page/widgets/month_selector.dart b/lib/pages/transactions_page/widgets/month_selector.dart index 1b1f902..1a440f1 100644 --- a/lib/pages/transactions_page/widgets/month_selector.dart +++ b/lib/pages/transactions_page/widgets/month_selector.dart @@ -1,49 +1,45 @@ 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 '../../../utils/formatted_date_range.dart'; -class MonthSelector extends StatelessWidget { +class MonthSelector extends ConsumerWidget with Functions { const MonthSelector({ - required this.amount, - required this.startDate, - required this.endDate, - Key? key, - }) : super(key: key); + super.key, + }); - final double amount; - final ValueNotifier startDate; - final ValueNotifier endDate; final double height = 60; - // last day of the month - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final totalAmount = ref.watch(totalAmountProvider); + final startDate = ref.watch(filterDateStartProvider); + final endDate = ref.watch(filterDateEndProvider); return GestureDetector( onTap: () async { // pick range of dates DateTimeRange? range = await showDateRangePicker( context: context, - firstDate: DateTime(2010, 1, 1), - lastDate: DateTime(2030, 12, 31), + firstDate: DateTime(1970, 1, 1), + lastDate: DateTime(2100, 12, 31), currentDate: DateTime.now(), initialDateRange: DateTimeRange( - start: startDate.value, - end: endDate.value, + start: startDate, + end: endDate, ), builder: (context, child) => Theme( data: Theme.of(context).copyWith( - appBarTheme: Theme.of(context) - .appBarTheme - .copyWith(backgroundColor: blue1), + appBarTheme: Theme.of(context).appBarTheme.copyWith(backgroundColor: blue1), ), child: child!, ), ); if (range != null) { - startDate.value = range.start; - endDate.value = range.end; + ref.read(filterDateStartProvider.notifier).state = range.start; + ref.read(filterDateEndProvider.notifier).state = range.end; } }, child: Container( @@ -59,10 +55,11 @@ class MonthSelector extends StatelessWidget { GestureDetector( onTap: () { // move to previous month - startDate.value = DateTime(startDate.value.year, - startDate.value.month - 1, startDate.value.day); - endDate.value = DateTime( - startDate.value.year, startDate.value.month + 1, 0); + ref.read(filterDateStartProvider.notifier).state = + DateTime(startDate.year, startDate.month - 1, startDate.day); + ref.read(filterDateEndProvider.notifier).state = + DateTime(startDate.year, startDate.month, 0); + ref.read(transactionsProvider.notifier).filterTransactions(); }, child: Container( height: height, @@ -74,34 +71,43 @@ class MonthSelector extends StatelessWidget { ), ), ), - ValueListenableBuilder( - valueListenable: startDate, - builder: (context, value, child) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Text( - getFormattedDateRange(startDate.value, endDate.value), - style: Theme.of(context).textTheme.titleLarge, - ), - Text( - "$amount €", - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: (amount > 0) ? green : red), - ), - ], - ); - }, + Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text( + getFormattedDateRange(startDate, endDate), + style: Theme.of(context).textTheme.titleLarge, + ), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: numToCurrency(totalAmount), + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: totalAmount >= 0 ? green : red), + ), + TextSpan( + text: "€", + style: Theme.of(context) + .textTheme + .labelLarge! + .copyWith(color: totalAmount >= 0 ? green : red) + ), + ], + ), + ), + ], ), GestureDetector( onTap: () { // move to next month - startDate.value = DateTime(startDate.value.year, - startDate.value.month + 1, startDate.value.day); - endDate.value = DateTime( - startDate.value.year, startDate.value.month + 1, 0); + ref.read(filterDateStartProvider.notifier).state = + DateTime(startDate.year, startDate.month + 1, startDate.day); + ref.read(filterDateEndProvider.notifier).state = + DateTime(startDate.year, startDate.month + 2, 0); + ref.read(transactionsProvider.notifier).filterTransactions(); }, child: Container( height: height, diff --git a/lib/pages/transactions_page/widgets/transaction_list_tile.dart b/lib/pages/transactions_page/widgets/transaction_list_tile.dart deleted file mode 100644 index f4f319e..0000000 --- a/lib/pages/transactions_page/widgets/transaction_list_tile.dart +++ /dev/null @@ -1,99 +0,0 @@ -import "package:flutter/material.dart"; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import '../../../constants/style.dart'; -import '../../../constants/functions.dart'; -import '../../../model/transaction.dart'; -import '../../../providers/transactions_provider.dart'; - -class TransactionListTile extends ConsumerWidget with Functions { - const TransactionListTile({ - Key? key, - required this.title, - required this.type, - required this.amount, - required this.color, - required this.category, - required this.icon, - required this.account, - required this.transaction, - }) : super(key: key); - - final String title; - final Type type; - final double amount; - final Color color; - final String category; - final IconData icon; - final String account; - final Transaction transaction; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return InkWell( - onTap: () { - ref.read(selectedTransactionUpdateProvider.notifier).state = - transaction; - ref.read(transactionsProvider.notifier).transactionUpdateState(); - - Navigator.of(context).pushNamed('/add-page'); - }, - child: Container( - color: white, - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 12.0, - ), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color, - ), - child: Icon(icon, color: white), - ), - const SizedBox(width: 8.0), - Expanded( - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: Theme.of(context).textTheme.titleMedium, - ), - Text( - "${type == Type.expense ? '-' : ''}${numToCurrency(amount)} €", - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(color: typeToColor(type)), - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - category.toUpperCase(), - style: Theme.of(context).textTheme.labelMedium, - ), - Text( - account.toUpperCase(), - style: Theme.of(context).textTheme.labelMedium, - ), - ], - ), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/pages/transactions_page/widgets/transaction_type_button.dart b/lib/pages/transactions_page/widgets/transaction_type_button.dart new file mode 100644 index 0000000..13c5b3e --- /dev/null +++ b/lib/pages/transactions_page/widgets/transaction_type_button.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../constants/style.dart'; +import '../../../model/transaction.dart'; +import 'accounts_tab.dart'; +import 'categories_tab.dart'; + +final selectedTransactionTypeProvider = + StateProvider.autoDispose((ref) => TransactionType.income); + +class TransactionTypeButton extends ConsumerWidget { + const TransactionTypeButton({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final transactionType = ref.watch(selectedTransactionTypeProvider); + // Reset index for categories and accounts when transactions type changes + ref.listen(selectedTransactionTypeProvider, (previous, next) { + ref.invalidate(selectedAccountIndexProvider); + ref.invalidate(selectedCategoryIndexProvider); + }); + final width = (MediaQuery.of(context).size.width - 64) * 0.5; + return Container( + height: 28, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all( + Radius.circular(5.0), + ), + ), + child: Stack( + children: [ + AnimatedAlign( + alignment: Alignment( + (transactionType == TransactionType.income) ? -1 : 1, + 0, + ), + curve: Curves.decelerate, + duration: const Duration(milliseconds: 180), + child: Container( + width: width, + height: 28, + decoration: const BoxDecoration( + color: blue5, + borderRadius: BorderRadius.all( + Radius.circular(5.0), + ), + ), + ), + ), + GestureDetector( + onTap: () { + ref.read(selectedTransactionTypeProvider.notifier).state = TransactionType.income; + }, + child: Align( + alignment: const Alignment(-1, 0), + child: Container( + width: width, + color: Colors.transparent, + alignment: Alignment.center, + child: Text( + "Income", + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: (transactionType == TransactionType.income) ? white : blue2), + ), + ), + ), + ), + GestureDetector( + onTap: () { + ref.read(selectedTransactionTypeProvider.notifier).state = TransactionType.expense; + }, + child: Align( + alignment: const Alignment(1, 0), + child: Container( + width: width, + color: Colors.transparent, + alignment: Alignment.center, + child: Text( + 'Expenses', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: (transactionType == TransactionType.expense) ? white : blue2), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/providers/accounts_provider.dart b/lib/providers/accounts_provider.dart index fba0076..bece3d4 100644 --- a/lib/providers/accounts_provider.dart +++ b/lib/providers/accounts_provider.dart @@ -5,10 +5,8 @@ import '../model/bank_account.dart'; final mainAccountProvider = StateProvider((ref) => null); final selectedAccountProvider = StateProvider((ref) => null); -final accountNameProvider = StateProvider((ref) => null); final accountIconProvider = StateProvider((ref) => accountIconList.keys.first); final accountColorProvider = StateProvider((ref) => 0); -final accountStartingValueProvider = StateProvider((ref) => null); final accountMainSwitchProvider = StateProvider((ref) => false); final countNetWorthSwitchProvider = StateProvider((ref) => true); @@ -20,8 +18,8 @@ class AsyncAccountsNotifier extends AsyncNotifier> { } Future> _getAccounts() async { - final account = await BankAccountMethods().selectAll(); - return account; + final accounts = await BankAccountMethods().selectAll(); + return accounts; } Future _getMainAccount() async { @@ -29,37 +27,37 @@ class AsyncAccountsNotifier extends AsyncNotifier> { return account; } - Future addAccount() async { - state = const AsyncValue.loading(); - + Future addAccount(String name, num? startingValue) async { BankAccount account = BankAccount( - name: ref.read(accountNameProvider)!, + name: name, symbol: ref.read(accountIconProvider), color: ref.read(accountColorProvider), - startingValue: ref.read(accountStartingValueProvider) ?? 0, - active: true, + startingValue: startingValue ?? 0, + active: ref.read(countNetWorthSwitchProvider), mainAccount: ref.read(accountMainSwitchProvider), ); + state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { await BankAccountMethods().insert(account); return _getAccounts(); }); } - Future updateAccount(BankAccount account) async { - BankAccount editAccount = account.copy( - name: ref.read(accountNameProvider)!, + Future updateAccount(String name) async { + BankAccount account = ref.read(selectedAccountProvider)!.copy( + name: name, symbol: ref.read(accountIconProvider), color: ref.read(accountColorProvider), + active: ref.read(countNetWorthSwitchProvider), mainAccount: ref.read(accountMainSwitchProvider), ); state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { - await BankAccountMethods().updateItem(editAccount); - if(editAccount.mainAccount) { - ref.read(mainAccountProvider.notifier).state = editAccount; + await BankAccountMethods().updateItem(account); + if(account.mainAccount) { + ref.read(mainAccountProvider.notifier).state = account; } return _getAccounts(); }); @@ -67,10 +65,8 @@ class AsyncAccountsNotifier extends AsyncNotifier> { Future selectedAccount(BankAccount account) async { ref.read(selectedAccountProvider.notifier).state = account; - ref.read(accountNameProvider.notifier).state = account.name; ref.read(accountIconProvider.notifier).state = account.symbol; ref.read(accountColorProvider.notifier).state = account.color; - ref.read(accountStartingValueProvider.notifier).state = account.startingValue; ref.read(accountMainSwitchProvider.notifier).state = account.mainAccount; } @@ -81,6 +77,14 @@ class AsyncAccountsNotifier extends AsyncNotifier> { return _getAccounts(); }); } + + void reset() { + ref.invalidate(selectedAccountProvider); + ref.invalidate(accountIconProvider); + ref.invalidate(accountColorProvider); + ref.invalidate(accountMainSwitchProvider); + ref.invalidate(countNetWorthSwitchProvider); + } } final accountsProvider = AsyncNotifierProvider>(() { diff --git a/lib/providers/categories_provider.dart b/lib/providers/categories_provider.dart index 2cd7869..0145d07 100644 --- a/lib/providers/categories_provider.dart +++ b/lib/providers/categories_provider.dart @@ -1,10 +1,9 @@ -import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import '../constants/constants.dart'; import '../model/category_transaction.dart'; final selectedCategoryProvider = StateProvider((ref) => null); -final categoryNameProvider = StateProvider((ref) => null); final categoryIconProvider = StateProvider((ref) => iconList.keys.first); final categoryColorProvider = StateProvider((ref) => 0); @@ -19,22 +18,27 @@ class AsyncCategoriesNotifier extends AsyncNotifier> { return categories; } - Future addCategory() async { - state = const AsyncValue.loading(); - + Future addCategory(String name) async { CategoryTransaction category = CategoryTransaction( - name: ref.read(categoryNameProvider)!, + name: name, symbol: ref.read(categoryIconProvider), color: ref.read(categoryColorProvider), ); + state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { await CategoryTransactionMethods().insert(category); return _getCategories(); }); } - Future updateCategory(CategoryTransaction category) async { + Future updateCategory(String name) async { + CategoryTransaction category = ref.read(selectedCategoryProvider)!.copy( + name: name, + symbol: ref.read(categoryIconProvider), + color: ref.read(categoryColorProvider), + ); + state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { await CategoryTransactionMethods().updateItem(category); @@ -42,9 +46,8 @@ class AsyncCategoriesNotifier extends AsyncNotifier> { }); } - Future selectedCategory(CategoryTransaction category) async { + void selectedCategory(CategoryTransaction category) { ref.read(selectedCategoryProvider.notifier).state = category; - ref.read(categoryNameProvider.notifier).state = category.name; ref.read(categoryIconProvider.notifier).state = category.symbol; ref.read(categoryColorProvider.notifier).state = category.color; } @@ -56,6 +59,12 @@ class AsyncCategoriesNotifier extends AsyncNotifier> { return _getCategories(); }); } + + void reset() { + ref.invalidate(selectedCategoryProvider); + ref.invalidate(categoryIconProvider); + ref.invalidate(categoryColorProvider); + } } final categoriesProvider = AsyncNotifierProvider>(() { diff --git a/lib/providers/dashboard_provider.dart b/lib/providers/dashboard_provider.dart new file mode 100644 index 0000000..aa7bc23 --- /dev/null +++ b/lib/providers/dashboard_provider.dart @@ -0,0 +1,32 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../model/transaction.dart'; + +final incomeProvider = StateProvider((ref) => 0); +final expenseProvider = StateProvider((ref) => 0); +final currentMonthListProvider = StateProvider>((ref) => const []); +final lastMonthListProvider = StateProvider>((ref) => const []); + +final dashboardProvider = FutureProvider((ref) async { + final currentMonth = await TransactionMethods().currentMonthTransactions(); + final lastMonth = await TransactionMethods().lastMonthTransactions(); + + ref.read(incomeProvider.notifier).state = + currentMonth.fold(0, (previousValue, element) => previousValue + element['income']); + ref.read(expenseProvider.notifier).state = + currentMonth.fold(0, (previousValue, element) => previousValue - element['expense']); + + double runningTotal = 0; + ref.read(currentMonthListProvider.notifier).state = currentMonth.map((e) { + runningTotal += e['income'] - e['expense']; + return FlSpot(double.parse(e['day'].substring(8)) - 1, double.parse(runningTotal.toStringAsFixed(2))); + }).toList(); + + runningTotal = 0; // Reset the running total for the next calculation + + ref.read(lastMonthListProvider.notifier).state = lastMonth.map((e) { + runningTotal += e['income'] - e['expense']; + return FlSpot(double.parse(e['day'].substring(8)) - 1, double.parse(runningTotal.toStringAsFixed(2))); + }).toList(); +}); diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart new file mode 100644 index 0000000..f98bafa --- /dev/null +++ b/lib/providers/settings_provider.dart @@ -0,0 +1,37 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +final transactionReminderSwitchProvider = StateProvider((ref) => false); +final transactionRecReminderSwitchProvider = StateProvider((ref) => false); +final transactionRecAddedSwitchProvider = StateProvider((ref) => false); + +class AsyncSettingsNotifier extends AsyncNotifier { + @override + Future build() async { + return _getSettings(); + } + + Future _getSettings() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + ref.read(transactionReminderSwitchProvider.notifier).state = prefs.getBool('transaction-reminder') ?? false; + ref.read(transactionRecReminderSwitchProvider.notifier).state = prefs.getBool('transaction-rec-reminder') ?? false; + ref.read(transactionRecAddedSwitchProvider.notifier).state = prefs.getBool('transaction-rec-added') ?? false; + } + + Future updateNotifications() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + final SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setBool('transaction-reminder', ref.read(transactionReminderSwitchProvider)); + await prefs.setBool( + 'transaction-rec-reminder', ref.read(transactionRecReminderSwitchProvider)); + await prefs.setBool('transaction-rec-added', ref.read(transactionRecAddedSwitchProvider)); + + return _getSettings(); + }); + } +} + +final settingsProvider = AsyncNotifierProvider(() { + return AsyncSettingsNotifier(); +}); diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart index ce81e01..10a5fa9 100644 --- a/lib/providers/theme_provider.dart +++ b/lib/providers/theme_provider.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sossoldi/constants/constants.dart'; + +import '../constants/constants.dart'; // import 'package:hooks_riverpod/hooks_riverpod.dart'; final appThemeStateNotifier = ChangeNotifierProvider( diff --git a/lib/providers/transactions_provider.dart b/lib/providers/transactions_provider.dart index f96888d..ad316cd 100644 --- a/lib/providers/transactions_provider.dart +++ b/lib/providers/transactions_provider.dart @@ -1,23 +1,27 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'categories_provider.dart'; -import 'accounts_provider.dart'; + import '../model/bank_account.dart'; -import '../model/transaction.dart'; import '../model/category_transaction.dart'; +import '../model/transaction.dart'; +import 'accounts_provider.dart'; +import 'dashboard_provider.dart'; -final transactionTypeList = - Provider>((ref) => [Type.income, Type.expense, Type.transfer]); +final lastTransactionsProvider = FutureProvider>((ref) async { + final transactions = await TransactionMethods().selectAll(limit: 5); + return transactions; +}); -final transactionTypesProvider = StateProvider>((ref) => [false, true, false]); +final transactionTypeList = Provider>( + (ref) => [TransactionType.income, TransactionType.expense, TransactionType.transfer]); + +final transactionTypeProvider = StateProvider((ref) => TransactionType.expense); final bankAccountTransferProvider = StateProvider((ref) => null); // Used as from account in transfer transactions final bankAccountProvider = StateProvider((ref) => ref.read(mainAccountProvider)); final dateProvider = StateProvider((ref) => DateTime.now()); final categoryProvider = StateProvider((ref) => null); -final amountProvider = StateProvider((ref) => 0); -final noteProvider = StateProvider((ref) => null); -//Recurring Payment +// Recurring Payment final selectedRecurringPayProvider = StateProvider((ref) => false); final intervalProvider = StateProvider((ref) => Recurrence.monthly); final repetitionProvider = StateProvider((ref) => null); @@ -25,34 +29,64 @@ final repetitionProvider = StateProvider((ref) => null); // Set when a transaction is selected for update final selectedTransactionUpdateProvider = StateProvider((ref) => null); -class AsyncTransactionsNotifier extends AsyncNotifier> { +// Amount total for the transactions filtered +final totalAmountProvider = StateProvider((ref) => 0); + +// Filters +final filterDateStartProvider = + StateProvider((ref) => DateTime(DateTime.now().year, DateTime.now().month, 1)); +final filterDateEndProvider = + StateProvider((ref) => DateTime(DateTime.now().year, DateTime.now().month + 1, 0)); + +class AsyncTransactionsNotifier extends AutoDisposeAsyncNotifier> { @override Future> build() async { return _getTransactions(); } - Future> _getTransactions() async { - final transaction = await TransactionMethods().selectAll(limit: 5); - return transaction; + Future> _getTransactions({int? limit, bool update = false}) async { + if (update) { + ref.invalidate(lastTransactionsProvider); + // ignore: unused_result + ref.refresh(dashboardProvider); + } + final dateStart = ref.watch(filterDateStartProvider); + final dateEnd = ref.watch(filterDateEndProvider); + final transactions = await TransactionMethods() + .selectAll(dateRangeStart: dateStart, dateRangeEnd: dateEnd, limit: limit); + + ref.read(totalAmountProvider.notifier).state = transactions.fold( + 0, + (prev, transaction) => transaction.type == TransactionType.transfer + ? prev + : transaction.type == TransactionType.expense + ? prev - transaction.amount + : prev + transaction.amount); + return transactions; } - Future addTransaction() async { + Future filterTransactions() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + return _getTransactions(); + }); + } + + Future addTransaction(num amount, String label) async { state = const AsyncValue.loading(); - final typeIndex = ref.read(transactionTypesProvider).indexOf(true); + final type = ref.read(transactionTypeProvider); final date = ref.read(dateProvider); - final amount = ref.read(amountProvider); final bankAccount = ref.read(bankAccountProvider)!; final bankAccountTransfer = ref.read(bankAccountTransferProvider); final category = ref.read(categoryProvider); - final note = ref.read(noteProvider); final recurring = ref.read(selectedRecurringPayProvider); Transaction transaction = Transaction( date: date, amount: amount, - type: ref.watch(transactionTypeList)[typeIndex], - note: note, + type: type, + note: label, idBankAccount: bankAccount.id!, idBankAccountTransfer: bankAccountTransfer?.id, idCategory: category?.id, @@ -61,25 +95,23 @@ class AsyncTransactionsNotifier extends AsyncNotifier> { state = await AsyncValue.guard(() async { await TransactionMethods().insert(transaction); - return _getTransactions(); + return _getTransactions(update: true); }); } - Future updateTransaction() async { - final typeIndex = ref.read(transactionTypesProvider).indexOf(true); + Future updateTransaction(num amount, String label) async { + final type = ref.read(transactionTypeProvider); final date = ref.read(dateProvider); - final amount = ref.read(amountProvider); final bankAccount = ref.read(bankAccountProvider)!; final bankAccountTransfer = ref.read(bankAccountTransferProvider); final category = ref.read(categoryProvider); - final note = ref.read(noteProvider); final recurring = ref.read(selectedRecurringPayProvider); Transaction transaction = ref.read(selectedTransactionUpdateProvider)!.copy( date: date, amount: amount, - type: ref.watch(transactionTypeList)[typeIndex], - note: note, + type: type, + note: label, idBankAccount: bankAccount.id!, idBankAccountTransfer: bankAccountTransfer?.id, idCategory: category?.id, @@ -89,37 +121,28 @@ class AsyncTransactionsNotifier extends AsyncNotifier> { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { await TransactionMethods().updateItem(transaction); - return _getTransactions(); + return _getTransactions(update: true); }); } - Future transactionUpdateState() async { - if (ref.read(selectedTransactionUpdateProvider) == null) return; - Transaction transaction = ref.read(selectedTransactionUpdateProvider)!; + Future transactionUpdateState(Transaction transaction) async { + ref.read(selectedTransactionUpdateProvider.notifier).state = transaction; final accountList = ref.watch(accountsProvider); - if (transaction.type != Type.transfer) { - final categories = ref - .watch(categoriesProvider) - .value! - .where((element) => element.id == transaction.idCategory); - if (categories.isNotEmpty) { - ref.read(categoryProvider.notifier).state = categories.first; + 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 == Type.transfer - ? accountList.value! - .firstWhere((element) => element.id == transaction.idBankAccountTransfer) - : null; - ref.read(transactionTypesProvider.notifier).state = [ - transaction.type == Type.income, - transaction.type == Type.expense, - transaction.type == Type.transfer, - ]; + 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(amountProvider.notifier).state = transaction.amount; - ref.read(noteProvider.notifier).state = transaction.note; ref.read(selectedRecurringPayProvider.notifier).state = transaction.recurring; } @@ -127,7 +150,7 @@ class AsyncTransactionsNotifier extends AsyncNotifier> { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { await TransactionMethods().deleteById(transactionId); - return _getTransactions(); + return _getTransactions(update: true); }); } @@ -139,9 +162,21 @@ class AsyncTransactionsNotifier extends AsyncNotifier> { ref.read(bankAccountTransferProvider.notifier).state = fromAccount; } } + + void reset() { + ref.invalidate(selectedTransactionUpdateProvider); + ref.invalidate(bankAccountProvider); + ref.invalidate(bankAccountTransferProvider); + ref.invalidate(dateProvider); + ref.invalidate(categoryProvider); + ref.invalidate(selectedRecurringPayProvider); + ref.invalidate(intervalProvider); + ref.invalidate(repetitionProvider); + ref.invalidate(transactionTypeProvider); + } } final transactionsProvider = - AsyncNotifierProvider>(() { + AsyncNotifierProvider.autoDispose>(() { return AsyncTransactionsNotifier(); }); diff --git a/lib/routes.dart b/lib/routes.dart index f674eab..86db717 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -7,11 +7,12 @@ import 'pages/accounts/add_account.dart'; import 'pages/add_page/add_page.dart'; import 'pages/categories/add_category.dart'; import 'pages/categories/category_list.dart'; -import 'pages/home_page.dart'; import 'pages/general_options/general_settings.dart'; +import 'pages/home_page.dart'; import 'pages/more_info_page/collaborators_page.dart'; import 'pages/more_info_page/more_info.dart'; import 'pages/more_info_page/privacy_policy.dart'; +import 'pages/notifications/notifications_settings.dart'; import 'pages/planning_page/planning_page.dart'; import 'pages/settings_page.dart'; import 'pages/statistics_page.dart'; @@ -51,7 +52,9 @@ Route makeRoute(RouteSettings settings) { case '/settings': return _noTransitionPageRoute(settings.name, const SettingsPage()); case '/general-settings': - return _noTransitionPageRoute(settings.name, const GeneralSettingsPage()); + return _cupertinoPageRoute(settings.name, const GeneralSettingsPage()); + case '/notifications-settings': + return _cupertinoPageRoute(settings.name, const NotificationsSettings()); default: throw 'Route is not defined'; } diff --git a/lib/utils/app_theme.dart b/lib/utils/app_theme.dart index 6d0cb21..a21f887 100644 --- a/lib/utils/app_theme.dart +++ b/lib/utils/app_theme.dart @@ -4,95 +4,103 @@ import '../constants/style.dart'; class AppTheme { static final lightTheme = ThemeData( - colorScheme: customColorScheme, - scaffoldBackgroundColor: blue7, - appBarTheme: const AppBarTheme( - color: grey3, - elevation: 0, - centerTitle: true, - iconTheme: IconThemeData(color: blue5), - titleTextStyle: TextStyle( - color: blue1, - fontSize: 18, - fontWeight: FontWeight.w700, - ), - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - backgroundColor: blue7, - unselectedItemColor: grey1, - ), - iconTheme: const IconThemeData( + colorScheme: customColorScheme, + scaffoldBackgroundColor: white, + useMaterial3: false, + appBarTheme: const AppBarTheme( + color: grey3, + elevation: 0, + centerTitle: true, + iconTheme: IconThemeData(color: blue5), + titleTextStyle: TextStyle( color: blue1, + fontSize: 24, + fontWeight: FontWeight.w700, + ), + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + backgroundColor: grey3, + unselectedItemColor: grey1, + ), + iconTheme: const IconThemeData( + color: blue1, + ), + listTileTheme: const ListTileThemeData( + tileColor: grey3, + contentPadding: EdgeInsets.all(16), + ), + disabledColor: grey2, + fontFamily: 'SF Pro Text', + textTheme: const TextTheme( + // display + displayLarge: TextStyle( + fontSize: 34.0, + fontWeight: FontWeight.w700, + ), + displayMedium: TextStyle( + fontSize: 34.0, + fontWeight: FontWeight.w600, + color: Colors.black, ), - fontFamily: 'SF Pro Text', - textTheme: const TextTheme( - // display - displayLarge: TextStyle( - fontSize: 34.0, - fontWeight: FontWeight.w700, - ), - displayMedium: TextStyle( - fontSize: 34.0, - fontWeight: FontWeight.w600, - color: Colors.black, - ), - // headline - headlineLarge: TextStyle( - fontSize: 24.0, - fontWeight: FontWeight.w700, - ), - headlineMedium: TextStyle( - fontSize: 24.0, - fontWeight: FontWeight.w600, - ), + // headline + headlineLarge: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.w700, + ), + headlineMedium: TextStyle( + fontSize: 24.0, + fontWeight: FontWeight.w600, + ), - // title - titleLarge: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.w700, - ), - titleMedium: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.w600, - ), - titleSmall: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.w400, - ), + // title + titleLarge: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w700, + ), + titleMedium: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w600, + ), + titleSmall: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.w400, + ), - // body - bodyLarge: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.w700, - ), - bodyMedium: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.w600, - ), - bodySmall: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.w400, - ), + // body + bodyLarge: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w700, + ), + bodyMedium: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w600, + ), + bodySmall: TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w400, + ), - // label - labelLarge: TextStyle( - fontSize: 10.0, - fontWeight: FontWeight.w700, - ), - labelMedium: TextStyle( - fontSize: 10.0, - fontWeight: FontWeight.w400, - ), - labelSmall: TextStyle( - fontSize: 8.0, - fontWeight: FontWeight.w700, - ), - )); + // label + labelLarge: TextStyle( + fontSize: 10.0, + fontWeight: FontWeight.w700, + ), + labelMedium: TextStyle( + fontSize: 10.0, + fontWeight: FontWeight.w400, + ), + labelSmall: TextStyle( + fontSize: 8.0, + fontWeight: FontWeight.w700, + ), + ), + ); static final darkTheme = ThemeData( colorScheme: darkCustomColorScheme, scaffoldBackgroundColor: darkBlue7, + useMaterial3: false, appBarTheme: const AppBarTheme( color: darkGrey3, elevation: 0, @@ -118,8 +126,7 @@ class AppTheme { //Text style fontFamily: 'SF Pro Text', textTheme: const TextTheme( - displayLarge: TextStyle( - fontSize: 34.0, fontWeight: FontWeight.w700, color: darkBlack), + displayLarge: TextStyle(fontSize: 34.0, fontWeight: FontWeight.w700, color: darkBlack), displayMedium: TextStyle( fontSize: 34.0, fontWeight: FontWeight.w600, @@ -195,7 +202,7 @@ ColorScheme customColorScheme = const ColorScheme( primary: blue1, primaryContainer: white, secondary: blue5, - tertiary: blue4, + tertiary: blue7, surface: grey3, background: white, error: red, @@ -211,7 +218,7 @@ ColorScheme darkCustomColorScheme = const ColorScheme( primary: darkBlue1, primaryContainer: darkGrey4, secondary: darkBlue5, - tertiary: darkBlack, + tertiary: darkBlue7, surface: darkBlue7, //darkBlue3 background: darkWhite, error: darkRed, diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b4e2147..6bbda39 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,13 @@ import FlutterMacOS import Foundation +import shared_preferences_foundation import sqflite import sqlite3_flutter_libs import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index a8afb76..6307e25 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -348,10 +348,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -384,6 +384,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" percent_indicator: dependency: "direct main" description: @@ -400,6 +424,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + url: "https://pub.dev" + source: hosted + version: "3.1.3" plugin_platform_interface: dependency: transitive description: @@ -440,6 +472,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.5" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" shelf: dependency: transitive description: @@ -545,10 +633,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -561,10 +649,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -593,26 +681,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f url: "https://pub.dev" source: hosted - version: "1.24.3" + version: "1.24.9" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" test_core: dependency: transitive description: name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.9" typed_data: dependency: transitive description: @@ -721,10 +809,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" web_socket_channel: dependency: transitive description: @@ -741,6 +829,22 @@ 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" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + url: "https://pub.dev" + source: hosted + version: "1.0.3" xml: dependency: transitive description: @@ -758,5 +862,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.5 <4.0.0" - flutter: ">=3.13.0" + dart: ">=3.2.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3b11b5d..e049369 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: percent_indicator: ^4.2.2 circular_menu: ^2.0.1 flutter_native_splash: ^2.2.18 + shared_preferences: ^2.2.2 dev_dependencies: flutter_test: diff --git a/test/model/transaction_test.dart b/test/model/transaction_test.dart index cc8baa3..c970ced 100644 --- a/test/model/transaction_test.dart +++ b/test/model/transaction_test.dart @@ -9,7 +9,7 @@ void main() { id: 2, date: DateTime.utc(2022), amount: 100, - type: Type.income, + type: TransactionType.income, note: "Note", idBankAccount: 0, idBankAccountTransfer: null, @@ -87,7 +87,7 @@ void main() { id: 2, date: DateTime.utc(2022), amount: 100, - type: Type.income, + type: TransactionType.income, note: "Note", idCategory: 0, idBankAccount: 0, diff --git a/test/widget/line_chart_test.dart b/test/widget/line_chart_test.dart index a8ceb91..5d4baf9 100644 --- a/test/widget/line_chart_test.dart +++ b/test/widget/line_chart_test.dart @@ -68,8 +68,6 @@ void main() { ], colorLine2Data: const Color(0xffffffff), colorBackground: const Color(0xff356CA3), - maxY: upper, - minY: lower, maxDays: 31.0, ), ), @@ -80,7 +78,7 @@ void main() { expect(find.text('11'), findsOneWidget); expect(find.text('18'), findsOneWidget); expect(find.text('25'), findsOneWidget); - + } ); } \ No newline at end of file