From 3610c466d2295b00619ded4625f73a760738aaeb Mon Sep 17 00:00:00 2001 From: gltron Date: Tue, 6 Feb 2024 23:58:29 +0100 Subject: [PATCH] stacked bar graph, edit / remove transaction & budget page base --- README.md | 17 +--- lib/domains/account/account_bloc.dart | 87 ++++++++++++++--- lib/domains/account/account_event.dart | 17 ++-- lib/domains/account/account_state.dart | 6 ++ lib/domains/budget/budget_bloc.dart | 10 ++ lib/domains/budget/budget_event.dart | 8 ++ lib/domains/budget/budget_state.dart | 6 ++ lib/domains/charts/chart_bloc.dart | 45 ++++++++- lib/domains/charts/chart_state.dart | 10 +- lib/pages/budgets/budgets_page.dart | 22 ++++- .../budgets/widgets/budgets_actions.dart | 35 +++++++ lib/pages/home/home_page.dart | 8 +- lib/pages/stats/stats_page.dart | 6 +- lib/pages/stats/widgets/account_counters.dart | 4 +- .../widgets/categories_totals_chart.dart | 46 ++++----- .../monthly_categories_total_chart.dart | 67 ++++++++++++- lib/pages/transactions/transactions_page.dart | 39 ++++---- .../widgets/autocomplete_input.dart | 3 + .../widgets/transaction_add_dialog.dart | 70 +++++++++----- ...on_add_form.dart => transaction_form.dart} | 7 +- .../widgets/transaction_line.dart | 96 ++++++++++--------- .../widgets/transactions_actions.dart | 7 +- .../account/models/transaction.dart | 6 ++ pubspec.lock | 40 ++++++++ pubspec.yaml | 1 + 25 files changed, 483 insertions(+), 180 deletions(-) create mode 100644 lib/domains/budget/budget_bloc.dart create mode 100644 lib/domains/budget/budget_event.dart create mode 100644 lib/domains/budget/budget_state.dart create mode 100644 lib/pages/budgets/widgets/budgets_actions.dart rename lib/pages/transactions/widgets/{transaction_add_form.dart => transaction_form.dart} (95%) diff --git a/README.md b/README.md index f985b9b..fff6f4b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,3 @@ -# tunas +# Tunas -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +A very complicated tool that should have been an excel sheet. \ No newline at end of file diff --git a/lib/domains/account/account_bloc.dart b/lib/domains/account/account_bloc.dart index 3f10d4e..ca63039 100644 --- a/lib/domains/account/account_bloc.dart +++ b/lib/domains/account/account_bloc.dart @@ -14,6 +14,7 @@ import 'package:tunas/domains/account/models/transaction_line.dart'; import 'package:tunas/domains/account/models/transaction_value.dart'; import 'package:tunas/repositories/account/account_repository.dart'; import 'package:tunas/repositories/account/models/transaction.dart'; +import 'package:uuid/uuid.dart'; part 'account_event.dart'; part 'account_state.dart'; @@ -24,7 +25,6 @@ class AccountBloc extends Bloc { AccountBloc({required AccountRepository accountRepository}) : _accountRepository = accountRepository, super(const AccountState()) { - on(_onTransactionAdd); on(_onAccountLoad); on(_onAccountImportJSON); on(_onAccountImportCSV); @@ -38,23 +38,14 @@ class AccountBloc extends Bloc { on(_onTransactionOpenAddDialog); on(_onTransactionHideAddDialog); on(_onTransactionAddDialog); + on(_onTransactionSetCurrent); + on(_onTransactionDeleteCurrent); _accountRepository .getTransactionsStream() .listen((transactions) => add(AccountLoad(transactions))); } - _onTransactionAdd(AccountAddTransaction event, Emitter emit) { - state.transactions.add(event.transaction); - var computeResult = _computeTransactionLine(state.transactions); - - emit(state.copyWith( - transactionsLines: computeResult.list, - globalTotal: computeResult.globalTotal, - accountsTotals: computeResult.accountsTotals, - )); - } - _onAccountLoad(AccountLoad event, Emitter emit) { var computeResult = _computeTransactionLine(event.transactions); emit(state.copyWith( @@ -78,6 +69,7 @@ class AccountBloc extends Bloc { final transactions = csvList .map((line) => Transaction( + uuid: const Uuid().v8(), date: DateTime.parse(line[0]), category: line[1], description: line[2], @@ -200,18 +192,83 @@ class AccountBloc extends Bloc { _onTransactionAddDialog( TransactionAdd event, Emitter emit - ) { + ) async { if (state.isValid) { - state.transactions.add(Transaction( + List transactions = state.transactions; + Transaction? currentTransaction = state.currentTransaction; + if (currentTransaction != null) { + transactions.removeWhere((transaction) => transaction.uuid == currentTransaction.uuid); + } + + transactions.add(Transaction( + uuid: const Uuid().v8(), date: state.transactionDate.value ?? DateTime.now(), category: state.transactionCategory.value, description: state.transactionDescription.value, account: state.transactionAccount.value, value: state.transactionValue.value )); - var computeResult = _computeTransactionLine(state.transactions); + final computeResult = _computeTransactionLine(transactions); + + await _accountRepository.saveTransactions(transactions); emit(state.copyWith( + currentTransaction: null, + transactionDate: const TransactionDate.pure(), + transactionCategory: const TransactionCategory.pure(), + transactionDescription: const TransactionDescription.pure(), + transactionAccount: const TransactionAccount.pure(), + transactionValue: const TransactionValue.pure(), + transactions: transactions, + transactionsLines: computeResult.list, + globalTotal: computeResult.globalTotal, + accountsTotals: computeResult.accountsTotals, + )); + } + } + + _onTransactionSetCurrent( + TransactionSetCurrent event, Emitter emit + ) { + Transaction? transaction = event.transaction; + if (transaction == null) { + emit(state.copyWith( + currentTransaction: event.transaction, + transactionDate: const TransactionDate.pure(), + transactionCategory: const TransactionCategory.pure(), + transactionDescription: const TransactionDescription.pure(), + transactionAccount: const TransactionAccount.pure(), + transactionValue: const TransactionValue.pure(), + )); + } else { + emit(state.copyWith( + currentTransaction: transaction, + transactionDate: TransactionDate.dirty(transaction.date), + transactionCategory: TransactionCategory.dirty(transaction.category), + transactionDescription: TransactionDescription.dirty(transaction.description), + transactionAccount: TransactionAccount.dirty(transaction.account), + transactionValue: TransactionValue.dirty(transaction.value), + )); + } + } + + _onTransactionDeleteCurrent( + TransactionDeleteCurrent event, Emitter emit + ) async { + Transaction? currentTransaction = state.currentTransaction; + if (currentTransaction != null) { + List transactions = state.transactions; + transactions.removeWhere((transaction) => transaction.uuid == currentTransaction.uuid); + final computeResult = _computeTransactionLine(transactions); + await _accountRepository.saveTransactions(transactions); + emit(state.copyWith( + currentTransaction: null, + transactionDate: const TransactionDate.pure(), + transactionCategory: const TransactionCategory.pure(), + transactionDescription: const TransactionDescription.pure(), + transactionAccount: const TransactionAccount.pure(), + transactionValue: const TransactionValue.pure(), + transactions: transactions, transactionsLines: computeResult.list, globalTotal: computeResult.globalTotal, accountsTotals: computeResult.accountsTotals, diff --git a/lib/domains/account/account_event.dart b/lib/domains/account/account_event.dart index 3bc8545..5402d80 100644 --- a/lib/domains/account/account_event.dart +++ b/lib/domains/account/account_event.dart @@ -7,14 +7,6 @@ sealed class AccountEvent extends Equatable { List get props => []; } -final class AccountAddTransaction extends AccountEvent { - final Transaction transaction; - const AccountAddTransaction(this.transaction); - - @override - List get props => [transaction]; -} - final class AccountLoad extends AccountEvent { final List transactions; const AccountLoad(this.transactions); @@ -86,4 +78,13 @@ final class TransactionOpenAddDialog extends AccountEvent { final class TransactionHideAddDialog extends AccountEvent { const TransactionHideAddDialog(); +} + +final class TransactionSetCurrent extends AccountEvent { + final Transaction? transaction; + const TransactionSetCurrent(this.transaction); +} + +final class TransactionDeleteCurrent extends AccountEvent { + const TransactionDeleteCurrent(); } \ No newline at end of file diff --git a/lib/domains/account/account_state.dart b/lib/domains/account/account_state.dart index fd968ec..4bff110 100644 --- a/lib/domains/account/account_state.dart +++ b/lib/domains/account/account_state.dart @@ -16,6 +16,8 @@ final class AccountState extends Equatable { final bool isValid; final bool showAddDialog; + final Transaction? currentTransaction; + const AccountState({ this.transactions = const [], this.transactionsLines = const [], @@ -29,6 +31,7 @@ final class AccountState extends Equatable { this.transactionValue = const TransactionValue.pure(), this.isValid = false, this.showAddDialog = false, + this.currentTransaction }); AccountState copyWith({ @@ -44,6 +47,7 @@ final class AccountState extends Equatable { TransactionValue? transactionValue, bool? isValid, bool? showAddDialog, + Transaction? currentTransaction, }) { return AccountState( transactions: transactions ?? this.transactions, @@ -58,6 +62,7 @@ final class AccountState extends Equatable { transactionValue: transactionValue ?? this.transactionValue, isValid: isValid ?? this.isValid, showAddDialog: showAddDialog ?? this.showAddDialog, + currentTransaction: currentTransaction ?? this.currentTransaction, ); } @@ -75,5 +80,6 @@ final class AccountState extends Equatable { transactionValue, isValid, showAddDialog, + currentTransaction, ]; } diff --git a/lib/domains/budget/budget_bloc.dart b/lib/domains/budget/budget_bloc.dart new file mode 100644 index 0000000..1590c3a --- /dev/null +++ b/lib/domains/budget/budget_bloc.dart @@ -0,0 +1,10 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'budget_event.dart'; +part 'budget_state.dart'; + +class BudgetBloc extends Bloc { + BudgetBloc(super.initialState); + +} \ No newline at end of file diff --git a/lib/domains/budget/budget_event.dart b/lib/domains/budget/budget_event.dart new file mode 100644 index 0000000..bbf2a4c --- /dev/null +++ b/lib/domains/budget/budget_event.dart @@ -0,0 +1,8 @@ +part of 'budget_bloc.dart'; + +sealed class BudgetEvent extends Equatable { + const BudgetEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/domains/budget/budget_state.dart b/lib/domains/budget/budget_state.dart new file mode 100644 index 0000000..5cf1355 --- /dev/null +++ b/lib/domains/budget/budget_state.dart @@ -0,0 +1,6 @@ +part of 'budget_bloc.dart'; + +final class BudgetState extends Equatable { + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/domains/charts/chart_bloc.dart b/lib/domains/charts/chart_bloc.dart index ddc37c4..1ac42b6 100644 --- a/lib/domains/charts/chart_bloc.dart +++ b/lib/domains/charts/chart_bloc.dart @@ -1,5 +1,8 @@ +import 'dart:ui'; + import 'package:equatable/equatable.dart'; import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:tunas/domains/account/models/transaction_line.dart'; import 'package:tunas/domains/charts/models/chart_item.dart'; @@ -9,6 +12,29 @@ import 'package:tunas/repositories/account/models/transaction.dart'; part 'chart_event.dart'; part 'chart_state.dart'; +final colors = [ + Colors.purple.shade300, + Colors.purple.shade500, + Colors.purple.shade700, + Colors.purple.shade900, + Colors.blue.shade300, + Colors.blue.shade500, + Colors.blue.shade700, + Colors.blue.shade900, + Colors.green.shade300, + Colors.green.shade500, + Colors.green.shade700, + Colors.green.shade900, + Colors.yellow.shade300, + Colors.yellow.shade500, + Colors.yellow.shade700, + Colors.yellow.shade900, + Colors.red.shade300, + Colors.red.shade500, + Colors.red.shade700, + Colors.red.shade900, +]; + class ChartBloc extends Bloc { final AccountRepository _accountRepository; @@ -95,7 +121,9 @@ class ChartBloc extends Bloc { List scopedCategoriesPositiveTotals = []; List scopedCategoriesNegativeTotals = []; Map scopedMonthlyTotals = {}; - Map> scopedCategoriesMonthlyTotals = {}; + Map> scopedCategoriesMonthlyTotals = {}; + Map categoriesColors = {}; + int colorIndex = 0; for(var transaction in state.transactions) { double subTotal = globalTotal + transaction.value; @@ -120,6 +148,11 @@ class ChartBloc extends Bloc { continue; } + if (categoriesColors[transaction.category] == null) { + categoriesColors[transaction.category] = colors[colorIndex]; + colorIndex++; + } + if (transaction.value >= 0) { ChartItem? chartItem = scopedCategoriesPositiveTotals.firstWhere( (item) => item.label == transaction.category, @@ -142,7 +175,16 @@ class ChartBloc extends Bloc { } ); chartItem.value += transaction.value; + + Map? a = scopedCategoriesMonthlyTotals[transaction.date.month]; + if (scopedCategoriesMonthlyTotals[transaction.date.month] == null) { + a = {}; + } + + a?[transaction.category] = transaction.value.abs() + (a[transaction.category] ?? 0); + scopedCategoriesMonthlyTotals[transaction.date.month] = a!; } + } List scopedCategoriesPositiveTotalsPercents = []; @@ -202,6 +244,7 @@ class ChartBloc extends Bloc { scopedSimplifiedCategoriesNegativeTotalsPercents: scopedSimplifiedCategoriesNegativeTotalsPercents, scopedMonthlyTotals: scopedMonthlyTotals.values.toList(), scopedCategoriesMonthlyTotals: scopedCategoriesMonthlyTotals, + categoriesColors: categoriesColors, ); } } diff --git a/lib/domains/charts/chart_state.dart b/lib/domains/charts/chart_state.dart index 910fe07..2a9b2e2 100644 --- a/lib/domains/charts/chart_state.dart +++ b/lib/domains/charts/chart_state.dart @@ -27,7 +27,9 @@ final class ChartState extends Equatable { final List scopedSimplifiedCategoriesNegativeTotalsPercents; final List scopedMonthlyTotals; - final Map> scopedCategoriesMonthlyTotals; + final Map> scopedCategoriesMonthlyTotals; + + final Map categoriesColors; const ChartState({ this.transactions = const [], @@ -50,6 +52,7 @@ final class ChartState extends Equatable { this.scopedSimplifiedCategoriesNegativeTotalsPercents = const [], this.scopedMonthlyTotals = const [], this.scopedCategoriesMonthlyTotals = const {}, + this.categoriesColors = const {}, }); ChartState copyWith({ @@ -72,7 +75,8 @@ final class ChartState extends Equatable { List? scopedSimplifiedCategoriesNegativeTotals, List? scopedSimplifiedCategoriesNegativeTotalsPercents, List? scopedMonthlyTotals, - Map>? scopedCategoriesMonthlyTotals, + Map>? scopedCategoriesMonthlyTotals, + Map? categoriesColors, }) { return ChartState( transactions: transactions ?? this.transactions, @@ -95,6 +99,7 @@ final class ChartState extends Equatable { scopedSimplifiedCategoriesNegativeTotalsPercents: scopedSimplifiedCategoriesNegativeTotalsPercents ?? this.scopedSimplifiedCategoriesNegativeTotalsPercents, scopedMonthlyTotals: scopedMonthlyTotals ?? this.scopedMonthlyTotals, scopedCategoriesMonthlyTotals: scopedCategoriesMonthlyTotals ?? this.scopedCategoriesMonthlyTotals, + categoriesColors: categoriesColors ?? this.categoriesColors, ); } @@ -118,6 +123,7 @@ final class ChartState extends Equatable { scopedSimplifiedCategoriesNegativeTotalsPercents, scopedMonthlyTotals, scopedCategoriesMonthlyTotals, + categoriesColors, ]; } diff --git a/lib/pages/budgets/budgets_page.dart b/lib/pages/budgets/budgets_page.dart index 1125527..e54e239 100644 --- a/lib/pages/budgets/budgets_page.dart +++ b/lib/pages/budgets/budgets_page.dart @@ -1,10 +1,30 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tunas/domains/account/account_bloc.dart'; +import 'package:tunas/pages/budgets/widgets/budgets_actions.dart'; class BudgetsPage extends StatelessWidget { const BudgetsPage({super.key}); @override Widget build(BuildContext context) { - return const Text('Budgets'); + return BlocListener( + listener: (context, state) { + if (state.showAddDialog) { + // TransactionAddDialog.show(context); + } + }, + child: const Flex( + direction: Axis.horizontal, + children: [ + Expanded( + child: Column( + children: [ + BudgetsActions(), + ], + )) + ], + ), + ); } } \ No newline at end of file diff --git a/lib/pages/budgets/widgets/budgets_actions.dart b/lib/pages/budgets/widgets/budgets_actions.dart new file mode 100644 index 0000000..6b2b515 --- /dev/null +++ b/lib/pages/budgets/widgets/budgets_actions.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tunas/domains/account/account_bloc.dart'; + +class BudgetsActions extends StatelessWidget { + const BudgetsActions({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) => Container( + padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 10), + margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Budgets', + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 35, + ), + ), + IconButton( + onPressed: () => null, + icon: const Icon( + Icons.add + ) + ), + ], + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index eafa73c..a66c72f 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -21,10 +21,10 @@ class HomePage extends StatelessWidget { title: const Text('Tunas'), bottom: const TabBar( tabs: [ - Tab(text: 'Dashboard'), - Tab(text: 'Transactions'), - Tab(text: 'Budgets'), - Tab(text: 'Data'), + Tab(icon: Icon(Icons.insights)), + Tab(icon: Icon(Icons.receipt_long)), + Tab(icon: Icon(Icons.pie_chart)), + Tab(icon: Icon(Icons.settings)), ], ), ), diff --git a/lib/pages/stats/stats_page.dart b/lib/pages/stats/stats_page.dart index ce31af0..ffa689b 100644 --- a/lib/pages/stats/stats_page.dart +++ b/lib/pages/stats/stats_page.dart @@ -45,15 +45,15 @@ class StatsPage extends StatelessWidget { ), SizedBox( height: 500, - child: MonthlyCategoriesTotalChart(categoriesMonthlyTotals: state.scopedCategoriesMonthlyTotals) + child: MonthlyCategoriesTotalChart(categoriesMonthlyTotals: state.scopedCategoriesMonthlyTotals, categoriesColors: state.categoriesColors) ), SizedBox( height: 450, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - CategoriesTotalsChart(categoriesTotals: state.scopedCategoriesPositiveTotals, categoriesTotalsPercents: state.scopedCategoriesPositiveTotalsPercents,), - CategoriesTotalsChart(categoriesTotals: state.scopedCategoriesNegativeTotals, categoriesTotalsPercents: state.scopedSimplifiedCategoriesNegativeTotalsPercents,), + CategoriesTotalsChart(categoriesTotals: state.scopedCategoriesPositiveTotals, categoriesTotalsPercents: state.scopedCategoriesPositiveTotalsPercents, categoriesColors: state.categoriesColors), + CategoriesTotalsChart(categoriesTotals: state.scopedCategoriesNegativeTotals, categoriesTotalsPercents: state.scopedSimplifiedCategoriesNegativeTotalsPercents, categoriesColors: state.categoriesColors), ], ) ), diff --git a/lib/pages/stats/widgets/account_counters.dart b/lib/pages/stats/widgets/account_counters.dart index c896678..fe96c41 100644 --- a/lib/pages/stats/widgets/account_counters.dart +++ b/lib/pages/stats/widgets/account_counters.dart @@ -25,8 +25,8 @@ class AccountCounter extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: EdgeInsets.all(10), - margin: EdgeInsets.all(20), + padding: const EdgeInsets.all(10), + margin: const EdgeInsets.all(20), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), color: Colors.blue diff --git a/lib/pages/stats/widgets/categories_totals_chart.dart b/lib/pages/stats/widgets/categories_totals_chart.dart index 88a4075..f0d495d 100644 --- a/lib/pages/stats/widgets/categories_totals_chart.dart +++ b/lib/pages/stats/widgets/categories_totals_chart.dart @@ -6,33 +6,11 @@ import 'package:tunas/domains/charts/models/chart_item.dart'; class CategoriesTotalsChart extends StatelessWidget { final List categoriesTotals; final List categoriesTotalsPercents; + final Map categoriesColors; - const CategoriesTotalsChart({super.key, required this.categoriesTotals, required this.categoriesTotalsPercents}); + const CategoriesTotalsChart({super.key, required this.categoriesTotals, required this.categoriesTotalsPercents, required this.categoriesColors}); List _convertDataForChart() { - var count = 1; - var colors = [ - Colors.purple.shade300, - Colors.purple.shade500, - Colors.purple.shade700, - Colors.purple.shade900, - Colors.blue.shade300, - Colors.blue.shade500, - Colors.blue.shade700, - Colors.blue.shade900, - Colors.green.shade300, - Colors.green.shade500, - Colors.green.shade700, - Colors.green.shade900, - Colors.yellow.shade300, - Colors.yellow.shade500, - Colors.yellow.shade700, - Colors.yellow.shade900, - Colors.red.shade300, - Colors.red.shade500, - Colors.red.shade700, - Colors.red.shade900, - ]; return categoriesTotalsPercents .map((item) => PieChartSectionData( @@ -45,7 +23,7 @@ class CategoriesTotalsChart extends StatelessWidget { titlePositionPercentageOffset: 0.8, borderSide: const BorderSide(width: 0), radius: 150, - color: colors[count++] + color: categoriesColors[item.label] )) .toList(); } @@ -55,9 +33,19 @@ class CategoriesTotalsChart extends StatelessWidget { .map((item) => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text("${item.label}: "), + Row( + children: [ + Container( + height: 10, + width: 10, + color: categoriesColors[item.label], + ), + Container(width: 5), + Text(item.label), + ], + ), Text( - NumberFormat("#00 €").format(item.value), + NumberFormat("#00 €").format(item.value.abs()), style: const TextStyle( fontFamily: 'NovaMono', ) @@ -70,7 +58,7 @@ class CategoriesTotalsChart extends StatelessWidget { Widget build(BuildContext context) { return Container( height: 320, - width: 550, + width: 560, padding: const EdgeInsets.all(10), margin: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -99,12 +87,14 @@ class CategoriesTotalsChart extends StatelessWidget { ), Container( height: 300, + width: 250, padding: const EdgeInsets.all(10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5), color: Colors.blueGrey ), child: SingleChildScrollView( + scrollDirection: Axis.vertical, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/pages/stats/widgets/monthly_categories_total_chart.dart b/lib/pages/stats/widgets/monthly_categories_total_chart.dart index 1679903..416302c 100644 --- a/lib/pages/stats/widgets/monthly_categories_total_chart.dart +++ b/lib/pages/stats/widgets/monthly_categories_total_chart.dart @@ -2,14 +2,73 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; class MonthlyCategoriesTotalChart extends StatelessWidget { - final Map> categoriesMonthlyTotals; + final Map> categoriesMonthlyTotals; + final Map categoriesColors; - const MonthlyCategoriesTotalChart({super.key, required this.categoriesMonthlyTotals}); + const MonthlyCategoriesTotalChart({super.key, required this.categoriesMonthlyTotals, required this.categoriesColors}); + + BarChartRodData _computeStack(double barsWidth, MapEntry> entry) { + var subcounter = 0.0; + var a = entry.value.entries.map((subEntry) => BarChartRodStackItem(subcounter, subcounter += subEntry.value, categoriesColors[subEntry.key] ?? Colors.red)).toList(); + return BarChartRodData( + fromY: 0, + toY: subcounter, + width: barsWidth, + borderRadius: BorderRadius.zero, + rodStackItems: a + ); + } + + List _computeBarGroups(double barsSpace, double barsWidth) { + var a = categoriesMonthlyTotals.entries + .map((entry) { + return BarChartGroupData( + x: entry.key, + barsSpace: barsSpace, + barRods: [_computeStack(barsWidth, entry)] + ); + }) + .toList(); + return a; + } + + SideTitleWidget _computeBottomTitles(double value, TitleMeta meta) { + const titles = ['Jan', 'Fev', 'Mar', 'Avr', 'Mai', 'Juin', 'Jui', 'Aou', 'Sep', 'Oct', 'Nov', 'Dec']; + String title = titles[value.toInt() - 1]; + + return SideTitleWidget( + axisSide: meta.axisSide, + child: Text(title) + ); + } @override Widget build(BuildContext context) { - return BarChart( - BarChartData() + return AspectRatio( + aspectRatio: 1.66, + child: LayoutBuilder( + builder: (context, constraints) { + final barsSpace = 4.0 * constraints.maxWidth / 400; + final barsWidth = 8.0 * constraints.maxWidth / 100; + + return BarChart( + BarChartData( + barGroups: _computeBarGroups(barsSpace, barsWidth), + titlesData: FlTitlesData( + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false) + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: _computeBottomTitles + ) + ) + ) + ) + ); + }, + ) ); } } \ No newline at end of file diff --git a/lib/pages/transactions/transactions_page.dart b/lib/pages/transactions/transactions_page.dart index daf86da..796b5e7 100644 --- a/lib/pages/transactions/transactions_page.dart +++ b/lib/pages/transactions/transactions_page.dart @@ -5,31 +5,30 @@ import 'package:tunas/pages/transactions/widgets/transactions_actions.dart'; import 'package:tunas/pages/transactions/widgets/transactions_header.dart'; import 'package:tunas/pages/transactions/widgets/transactions_list.dart'; - class TransactionsPage extends StatelessWidget { const TransactionsPage({super.key}); @override Widget build(BuildContext context) { return BlocListener( - listener: (context, state) { - if (state.showAddDialog) { - // TransactionAddDialog.show(context); - } - }, - child: const Flex( - direction: Axis.horizontal, - children: [ - Expanded( - child: Column( - children: [ - TransactionsActions(), - TransactionsHeader(), - TransactionsList(), - ], - )) - ], - ), - ); + listener: (context, state) { + if (state.showAddDialog) { + // TransactionAddDialog.show(context); + } + }, + child: const Flex( + direction: Axis.horizontal, + children: [ + Expanded( + child: Column( + children: [ + TransactionsActions(), + TransactionsHeader(), + TransactionsList(), + ], + )) + ], + ), + ); } } \ No newline at end of file diff --git a/lib/pages/transactions/widgets/autocomplete_input.dart b/lib/pages/transactions/widgets/autocomplete_input.dart index 0a9a97a..9d471ad 100644 --- a/lib/pages/transactions/widgets/autocomplete_input.dart +++ b/lib/pages/transactions/widgets/autocomplete_input.dart @@ -4,6 +4,7 @@ class AutocompleteInput extends StatelessWidget { final List options; final String hintText; final String? errorText; + final String? initialValue; final ValueChanged? onChanged; const AutocompleteInput({ @@ -11,12 +12,14 @@ class AutocompleteInput extends StatelessWidget { required this.options, required this.hintText, required this.errorText, + required this.initialValue, required this.onChanged, }); @override Widget build(BuildContext context) { return RawAutocomplete( + initialValue: TextEditingValue(text: initialValue ?? ''), optionsBuilder: (TextEditingValue textEditingValue) => options.where((String option) =>option.contains(textEditingValue.text.toLowerCase())), fieldViewBuilder: ( BuildContext context, diff --git a/lib/pages/transactions/widgets/transaction_add_dialog.dart b/lib/pages/transactions/widgets/transaction_add_dialog.dart index 4fbd1b9..fd6ef20 100644 --- a/lib/pages/transactions/widgets/transaction_add_dialog.dart +++ b/lib/pages/transactions/widgets/transaction_add_dialog.dart @@ -1,40 +1,60 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:tunas/domains/account/account_bloc.dart'; -import 'package:tunas/pages/transactions/widgets/transaction_add_form.dart'; +import 'package:tunas/pages/transactions/widgets/transaction_form.dart'; +import 'package:tunas/repositories/account/models/transaction.dart'; class TransactionAddDialog extends StatelessWidget { const TransactionAddDialog({super.key}); - static void show(BuildContext context) => showDialog( - context: context, - barrierDismissible: false, - useRootNavigator: false, - builder: (_) => BlocProvider.value( - value: BlocProvider.of(context), - child: const TransactionAddDialog() - ) - ); + static void show(BuildContext context, Transaction? transaction) { + context.read().add(TransactionSetCurrent(transaction)); + showDialog( + context: context, + barrierDismissible: false, + useRootNavigator: false, + builder: (_) => BlocProvider.value( + value: BlocProvider.of(context), + child: const TransactionAddDialog() + ) + ); + } static void hide(BuildContext context) => Navigator.pop(context); + _computeActions(BuildContext context, Transaction? currentTransaction) { + final actions = [ + IconButton( + onPressed: () => TransactionAddDialog.hide(context), + icon: const Icon(Icons.close) + ), + IconButton( + onPressed: () => context.read().add(const TransactionAdd()), + icon: const Icon(Icons.save) + ), + ]; + + if (currentTransaction != null) { + actions.add(IconButton( + onPressed: () => context.read().add(const TransactionDeleteCurrent()), + icon: const Icon(Icons.delete) + )); + } + + return actions; + } + @override Widget build(BuildContext context) { - return AlertDialog( - title: Text('Add transaction'), - actions: [ - TextButton( - onPressed: () => TransactionAddDialog.hide(context), - child: Text('Close') - ), - TextButton( - onPressed: () => context.read().add(TransactionAdd()), - child: Text('Add') - ), - ], - content: SizedBox( - height: 400, - child: TransactionAddForm(), + return BlocBuilder( + buildWhen: (previous, current) => previous.currentTransaction != current.currentTransaction, + builder: (context, state) => AlertDialog( + title: Text(state.currentTransaction == null ? 'Add Transaction' : 'Edit Transaction'), + actions: _computeActions(context, state.currentTransaction), + content: const SizedBox( + height: 400, + child: TransactionForm(), + ) ) ); } diff --git a/lib/pages/transactions/widgets/transaction_add_form.dart b/lib/pages/transactions/widgets/transaction_form.dart similarity index 95% rename from lib/pages/transactions/widgets/transaction_add_form.dart rename to lib/pages/transactions/widgets/transaction_form.dart index 5cfc4f9..0c2654d 100644 --- a/lib/pages/transactions/widgets/transaction_add_form.dart +++ b/lib/pages/transactions/widgets/transaction_form.dart @@ -5,8 +5,9 @@ import 'package:tunas/domains/account/account_bloc.dart'; import 'autocomplete_input.dart'; -class TransactionAddForm extends StatelessWidget { - const TransactionAddForm({super.key}); +class TransactionForm extends StatelessWidget { + + const TransactionForm({super.key}); @override Widget build(BuildContext context) { @@ -61,6 +62,7 @@ class _TransactionCategoryInput extends StatelessWidget { child: AutocompleteInput( options: state.categories, hintText: 'Category', + initialValue: state.transactionCategory.value, errorText: state.transactionCategory.isNotValid ? state.transactionCategory.error?.message : null, onChanged: (value) => context.read().add(TransactionCategoryChange(value)), ), @@ -98,6 +100,7 @@ class _TransactionAccountInput extends StatelessWidget { child: AutocompleteInput( options: state.accountsTotals.keys.toList(), hintText: 'Account', + initialValue: state.transactionAccount.value, errorText: state.transactionAccount.isNotValid ? state.transactionAccount.error?.message : null, onChanged: (value) => context.read().add(TransactionAccountChange(value)), ), diff --git a/lib/pages/transactions/widgets/transaction_line.dart b/lib/pages/transactions/widgets/transaction_line.dart index 9507c47..8b10561 100644 --- a/lib/pages/transactions/widgets/transaction_line.dart +++ b/lib/pages/transactions/widgets/transaction_line.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:tunas/pages/transactions/widgets/transaction_add_dialog.dart'; import 'package:tunas/repositories/account/models/transaction.dart'; class TransactionLine extends StatelessWidget { @@ -10,54 +11,57 @@ class TransactionLine extends StatelessWidget { @override Widget build(BuildContext context) { - return MergeSemantics( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 10), - margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: 100, - child: Text(DateFormat('dd-MM-yyyy', 'fr_FR').format(transaction.date)) - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - transaction.category, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18) - ), - Text(transaction.description), - ], + return InkWell( + onTap: () => TransactionAddDialog.show(context, transaction), + child: MergeSemantics( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 10), + margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 100, + child: Text(DateFormat('dd-MM-yyyy', 'fr_FR').format(transaction.date)) ), - ), - SizedBox( - width: 100, - child: Text(transaction.account), - ), - SizedBox( - width: 120, - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - NumberFormat(transaction.value > 0 ? '+#######.00 €' : '#######.00 €', 'fr_FR').format(transaction.value), - style: TextStyle( - color: transaction.value > 0 ? Colors.green : Colors.red - ) - ), - Text( - NumberFormat('#######.00 €', 'fr_FR').format(subTotal), - style: TextStyle( - color: subTotal > 0 ? Colors.green : Colors.red - ) - ), - ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + transaction.category, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18) + ), + Text(transaction.description), + ], + ), ), - ) - ], + SizedBox( + width: 100, + child: Text(transaction.account), + ), + SizedBox( + width: 120, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + NumberFormat(transaction.value > 0 ? '+#######.00 €' : '#######.00 €', 'fr_FR').format(transaction.value), + style: TextStyle( + color: transaction.value > 0 ? Colors.green : Colors.red + ) + ), + Text( + NumberFormat('#######.00 €', 'fr_FR').format(subTotal), + style: TextStyle( + color: subTotal > 0 ? Colors.green : Colors.red + ) + ), + ], + ), + ) + ], + ) ) ) ); diff --git a/lib/pages/transactions/widgets/transactions_actions.dart b/lib/pages/transactions/widgets/transactions_actions.dart index 2b49698..c8bf7aa 100644 --- a/lib/pages/transactions/widgets/transactions_actions.dart +++ b/lib/pages/transactions/widgets/transactions_actions.dart @@ -22,12 +22,11 @@ class TransactionsActions extends StatelessWidget { fontSize: 35, ), ), - ElevatedButton.icon( - onPressed: () => TransactionAddDialog.show(context), + IconButton( + onPressed: () => TransactionAddDialog.show(context, null), icon: const Icon( Icons.add - ), - label: const Text('Add'), + ) ), ], ) diff --git a/lib/repositories/account/models/transaction.dart b/lib/repositories/account/models/transaction.dart index 6320f81..b0d3bea 100644 --- a/lib/repositories/account/models/transaction.dart +++ b/lib/repositories/account/models/transaction.dart @@ -1,4 +1,7 @@ +import 'package:uuid/uuid.dart'; + class Transaction { + String uuid; DateTime date; String category; String description; @@ -6,6 +9,7 @@ class Transaction { double value; Transaction({ + required this.uuid, required this.date, required this.category, required this.description, @@ -15,6 +19,7 @@ class Transaction { factory Transaction.fromJson(Map json) { return Transaction( + uuid: json['uuid'] ?? const Uuid().v8(), date: DateTime.parse(json['date']), category: json['category'], description: json['description'], @@ -24,6 +29,7 @@ class Transaction { } Map toJson() => { + 'uuid': uuid, 'date': date.toIso8601String(), 'category': category, 'description': description, diff --git a/pubspec.lock b/pubspec.lock index 5339cbb..f08bb4a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" csv: dependency: "direct main" description: @@ -97,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" fl_chart: dependency: "direct main" description: @@ -314,6 +330,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -354,6 +378,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + url: "https://pub.dev" + source: hosted + version: "4.3.3" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a8e7682..4ac5579 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: sdk: flutter intl: any formz: ^0.6.1 + uuid: ^4.3.2 dev_dependencies: flutter_test: