diff --git a/lib/domains/account/account_bloc.dart b/lib/domains/account/account_bloc.dart index fa5d254..3436307 100644 --- a/lib/domains/account/account_bloc.dart +++ b/lib/domains/account/account_bloc.dart @@ -41,8 +41,13 @@ class AccountBloc extends Bloc { super(const AccountState()) { on(_onAccountImportJSON); on(_onAccountImportCSV); + on(_onSubAccountLoad); // on(_onAccountImportJSON); // on(_onAccountImportJSON); + + _accountRepository + .getSubAccountsStream() + .listen((subAccounts) => add(SubAccountLoad(subAccounts))); } double _universalConvertToDouble(dynamic value) { @@ -69,6 +74,7 @@ class AccountBloc extends Bloc { final List> csvList = const CsvToListConverter(fieldDelimiter: '|', eol: '\n').convert(csvFileContent); final Map categoriesMap = {}; + final Set subAccounts = {}; final transactions = csvList .map((line) { @@ -88,6 +94,8 @@ class AccountBloc extends Bloc { } } + subAccounts.add(line[3]); + return Transaction( uuid: const Uuid().v8(), date: DateTime.parse(line[0]), @@ -99,7 +107,7 @@ class AccountBloc extends Bloc { .toList(); await _accountRepository.deleteAccount(); - final account = Account(transactions: transactions, categories: categoriesMap.values.toList()); + final account = Account(transactions: transactions, categories: categoriesMap.values.toList(), subAccounts: subAccounts); await _accountRepository.saveAccount(account); } } @@ -119,4 +127,12 @@ class AccountBloc extends Bloc { await _accountRepository.saveTransactions(transactions); } } + + _onSubAccountLoad( + SubAccountLoad event, Emitter emit + ) { + emit( + state.copyWith(event.subAccounts) + ); + } } diff --git a/lib/domains/account/account_event.dart b/lib/domains/account/account_event.dart index f1d0327..f871cf3 100644 --- a/lib/domains/account/account_event.dart +++ b/lib/domains/account/account_event.dart @@ -22,3 +22,11 @@ final class AccountExportJSON extends AccountEvent { final class AccountExportCSV extends AccountEvent { const AccountExportCSV(); } + +final class SubAccountLoad extends AccountEvent { + final Set subAccounts; + const SubAccountLoad(this.subAccounts); + + @override + List get props => [subAccounts]; +} diff --git a/lib/domains/account/account_state.dart b/lib/domains/account/account_state.dart index 3eebdbe..11fcd29 100644 --- a/lib/domains/account/account_state.dart +++ b/lib/domains/account/account_state.dart @@ -1,12 +1,18 @@ part of 'account_bloc.dart'; final class AccountState extends Equatable { - const AccountState(); + final Set subAccounts; - AccountState copyWith() { - return const AccountState(); + const AccountState({ + this.subAccounts = const {}, + }); + + AccountState copyWith(Set? subAccounts) { + return AccountState( + subAccounts: subAccounts ?? this.subAccounts, + ); } @override - List get props => []; + List get props => [subAccounts]; } diff --git a/lib/domains/charts/chart_bloc.dart b/lib/domains/charts/chart_bloc.dart index 69fab03..75c3ded 100644 --- a/lib/domains/charts/chart_bloc.dart +++ b/lib/domains/charts/chart_bloc.dart @@ -96,22 +96,21 @@ class ChartBloc extends Bloc { ChartState _computeStateScopedStats(ChartState state) { double globalTotal = 0; + double scoppedTotal = 0; List scopedCategoriesPositiveTotals = []; List scopedCategoriesNegativeTotals = []; Map scopedMonthlyTotals = {}; Map> scopedCategoriesMonthlyTotals = {}; for(var transaction in state.transactions) { - double subTotal = globalTotal + transaction.value; - globalTotal = subTotal; + globalTotal += transaction.value; + double subTotal = globalTotal; if (transaction.date.year != state.currentYear) { continue; } - if (state.accountsTotals.containsKey(transaction.category)) { - continue; - } + scoppedTotal += transaction.value; TransactionLine transactionLine = TransactionLine(transaction: transaction, subTotal: subTotal); @@ -155,7 +154,6 @@ class ChartBloc extends Bloc { a?[transaction.category] = transaction.value.abs() + (a[transaction.category] ?? 0); scopedCategoriesMonthlyTotals[transaction.date.month] = a!; } - } List scopedCategoriesPositiveTotalsPercents = []; @@ -205,6 +203,7 @@ class ChartBloc extends Bloc { scopedSimplifiedCategoriesNegativeTotalsPercents.sort((a, b) => a.value.compareTo(b.value)); return state.copyWith( + scoppedProfit: scoppedTotal, scopedCategoriesPositiveTotals: scopedCategoriesPositiveTotals, scopedCategoriesPositiveTotalsPercents: scopedCategoriesPositiveTotalsPercents, scopedCategoriesNegativeTotals: scopedCategoriesNegativeTotals, diff --git a/lib/domains/charts/chart_state.dart b/lib/domains/charts/chart_state.dart index 78e989e..23a6042 100644 --- a/lib/domains/charts/chart_state.dart +++ b/lib/domains/charts/chart_state.dart @@ -29,6 +29,8 @@ final class ChartState extends Equatable { final List scopedMonthlyTotals; final Map> scopedCategoriesMonthlyTotals; + final double scoppedProfit; + const ChartState({ this.transactions = const [], this.transactionsLines = const [], @@ -50,6 +52,7 @@ final class ChartState extends Equatable { this.scopedSimplifiedCategoriesNegativeTotalsPercents = const [], this.scopedMonthlyTotals = const [], this.scopedCategoriesMonthlyTotals = const {}, + this.scoppedProfit = 0, }); ChartState copyWith({ @@ -73,6 +76,7 @@ final class ChartState extends Equatable { List? scopedSimplifiedCategoriesNegativeTotalsPercents, List? scopedMonthlyTotals, Map>? scopedCategoriesMonthlyTotals, + double? scoppedProfit, }) { 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, + scoppedProfit: scoppedProfit ?? this.scoppedProfit, ); } @@ -118,6 +123,7 @@ final class ChartState extends Equatable { scopedSimplifiedCategoriesNegativeTotalsPercents, scopedMonthlyTotals, scopedCategoriesMonthlyTotals, + scoppedProfit, ]; } diff --git a/lib/pages/budgets/budgets_page.dart b/lib/pages/budgets/budgets_page.dart index 2557794..54920ce 100644 --- a/lib/pages/budgets/budgets_page.dart +++ b/lib/pages/budgets/budgets_page.dart @@ -6,16 +6,17 @@ class BudgetsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return const Flex( - direction: Axis.horizontal, - children: [ - Expanded( - child: Column( + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 1000 + ), + child: const Column( children: [ BudgetsActions(), ], - )) - ], + ) + ) ); } } \ No newline at end of file diff --git a/lib/pages/data/data_page.dart b/lib/pages/data/data_page.dart index 7efa548..3d670af 100644 --- a/lib/pages/data/data_page.dart +++ b/lib/pages/data/data_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:tunas/pages/data/widgets/account_settings.dart'; import 'package:tunas/pages/data/widgets/categories_settings.dart'; import 'package:tunas/pages/data/widgets/import_settings.dart'; @@ -7,27 +8,33 @@ class DataPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 10), - margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 10), - child: const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Settings', - style: TextStyle( - fontWeight: FontWeight.w900, - fontSize: 35, + return Center( + child: Container( + constraints: const BoxConstraints( + maxWidth: 1000 + ), + padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 10), + margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 10), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Settings', + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 35, + ), ), - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ImportSettings(), - CategoriesSettings() - ], - ) - ] + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ImportSettings(), + CategoriesSettings(), + AccountSettings(), + ], + ) + ] + ) ) ); } diff --git a/lib/pages/data/widgets/account_settings.dart b/lib/pages/data/widgets/account_settings.dart new file mode 100644 index 0000000..76785a6 --- /dev/null +++ b/lib/pages/data/widgets/account_settings.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tunas/domains/account/account_bloc.dart'; + +class AccountSettings extends StatelessWidget { + const AccountSettings({super.key}); + + List _computeCategoryList(Set subAccounts) { + return subAccounts.map((subAccount) => Row( + children: [ + Text(subAccount), + ], + )).toList(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) => Container( + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(5), + ), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15), + margin: const EdgeInsets.all(5), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Accounts", + style: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 20, + ), + ), + const SizedBox(height: 10), + SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _computeCategoryList(state.subAccounts), + ), + ), + ], + ) + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 9a089aa..2218f35 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -25,23 +25,36 @@ class HomePage extends StatelessWidget { child: DefaultTabController( length: 4, child: Scaffold( - appBar: AppBar( - title: const Text('Tunas'), - bottom: const TabBar( - tabs: [ - Tab(icon: Icon(Icons.insights)), - Tab(icon: Icon(Icons.receipt_long)), - Tab(icon: Icon(Icons.pie_chart)), - Tab(icon: Icon(Icons.settings)), - ], - ), - ), - body: const TabBarView( + body: Stack( children: [ - StatsPage(), - TransactionsPage(), - BudgetsPage(), - DataPage() + const TabBarView( + children: [ + StatsPage(), + TransactionsPage(), + BudgetsPage(), + DataPage() + ], + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + margin: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: const Color.fromARGB(255, 41, 49, 56), + borderRadius: BorderRadius.circular(25) + ), + child: TabBar( + tabAlignment: TabAlignment.center, + splashBorderRadius: BorderRadius.circular(25), + tabs: const [ + 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 26762e6..d7cfbb4 100644 --- a/lib/pages/stats/stats_page.dart +++ b/lib/pages/stats/stats_page.dart @@ -20,24 +20,35 @@ class StatsPage extends StatelessWidget { child: BlocBuilder( builder: (context, state) => ListView( children: [ - Row( - children: [ - Expanded( - flex: 2, - child: MainCounter(value: state.globalTotal) + Center ( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 1000 ), - Expanded( - flex: 1, - child: AccountCounter(accountsTotals: state.accountsTotals) - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const YearSelector(), - ProfitIndicator(monthlyTotals: state.scopedMonthlyTotals) - ], + child: Column( + children: [ + Row( + children: [ + Expanded( + flex: 2, + child: MainCounter(value: state.globalTotal) + ), + Expanded( + flex: 1, + child: AccountCounter(accountsTotals: state.accountsTotals) + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const YearSelector(), + ProfitIndicator(profit: state.scoppedProfit) + ], + ), + ] + ) + ) ), SizedBox( height: 200, @@ -47,14 +58,20 @@ class StatsPage extends StatelessWidget { height: 500, child: MonthlyCategoriesTotalChart(categoriesMonthlyTotals: state.scopedCategoriesMonthlyTotals) ), - SizedBox( - height: 450, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CategoriesTotalsChart(categoriesTotals: state.scopedCategoriesPositiveTotals, categoriesTotalsPercents: state.scopedSimplifiedCategoriesPositiveTotalsPercents), - CategoriesTotalsChart(categoriesTotals: state.scopedCategoriesNegativeTotals, categoriesTotalsPercents: state.scopedSimplifiedCategoriesNegativeTotalsPercents), - ], + Center ( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 1500 + ), + child: SizedBox( + height: 450, + child: OverflowBar( + children: [ + CategoriesTotalsChart(categoriesTotals: state.scopedCategoriesPositiveTotals, categoriesTotalsPercents: state.scopedSimplifiedCategoriesPositiveTotalsPercents), + CategoriesTotalsChart(categoriesTotals: state.scopedCategoriesNegativeTotals, categoriesTotalsPercents: state.scopedSimplifiedCategoriesNegativeTotalsPercents), + ], + ) + ) ) ), ], diff --git a/lib/pages/stats/widgets/account_counters.dart b/lib/pages/stats/widgets/account_counters.dart index a9f7b85..a42bef9 100644 --- a/lib/pages/stats/widgets/account_counters.dart +++ b/lib/pages/stats/widgets/account_counters.dart @@ -10,7 +10,12 @@ class AccountCounter extends StatelessWidget { return accountsTotals.entries.toList().map((entry) => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(entry.key), + Flexible( + child: Text( + entry.key, + overflow: TextOverflow.ellipsis, + ) + ), Text( NumberFormat('#######.00 €', 'fr_FR').format(entry.value), style: TextStyle( diff --git a/lib/pages/stats/widgets/categories_totals_chart.dart b/lib/pages/stats/widgets/categories_totals_chart.dart index d6fd54e..6ad0815 100644 --- a/lib/pages/stats/widgets/categories_totals_chart.dart +++ b/lib/pages/stats/widgets/categories_totals_chart.dart @@ -60,7 +60,7 @@ class CategoriesTotalsChart extends StatelessWidget { return BlocBuilder( builder: (context, state) => Container( height: 320, - width: 560, + width: 600, padding: const EdgeInsets.all(10), margin: const EdgeInsets.all(20), decoration: BoxDecoration( diff --git a/lib/pages/stats/widgets/profit_indicator.dart b/lib/pages/stats/widgets/profit_indicator.dart index 3e2a65b..b57f4e5 100644 --- a/lib/pages/stats/widgets/profit_indicator.dart +++ b/lib/pages/stats/widgets/profit_indicator.dart @@ -1,25 +1,14 @@ -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class ProfitIndicator extends StatelessWidget { - final List monthlyTotals; + final double profit; - const ProfitIndicator({super.key, required this.monthlyTotals}); + const ProfitIndicator({super.key, required this.profit}); - double _profit() { - if (monthlyTotals.isEmpty) { - return 0; - } - final maxDateSpot = monthlyTotals.reduce((value, element) => value.x > element.x ? value : element); - final minDateSpot = monthlyTotals.reduce((value, element) => value.x < element.x ? value : element); - - return maxDateSpot.y - minDateSpot.y; - } @override Widget build(BuildContext context) { - final profit = _profit(); return Container( margin: const EdgeInsets.fromLTRB(0, 0, 20, 0), padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), diff --git a/lib/pages/transactions/transactions_page.dart b/lib/pages/transactions/transactions_page.dart index 4eafd42..9e14964 100644 --- a/lib/pages/transactions/transactions_page.dart +++ b/lib/pages/transactions/transactions_page.dart @@ -8,18 +8,19 @@ class TransactionsPage extends StatelessWidget { @override Widget build(BuildContext context) { - return const Flex( - direction: Axis.horizontal, - children: [ - Expanded( - child: Column( + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 1000 + ), + child: const 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 deleted file mode 100644 index 9d471ad..0000000 --- a/lib/pages/transactions/widgets/autocomplete_input.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; - -class AutocompleteInput extends StatelessWidget { - final List options; - final String hintText; - final String? errorText; - final String? initialValue; - final ValueChanged? onChanged; - - const AutocompleteInput({ - super.key, - 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, - TextEditingController textEditingController, - FocusNode focusNode, - VoidCallback onFieldSubmitted, - ) { - return TextFormField( - controller: textEditingController, - focusNode: focusNode, - decoration: InputDecoration( - hintText: hintText, - errorText: errorText - ), - onFieldSubmitted: (String value) { - onFieldSubmitted(); - }, - onChanged: onChanged, - ); - }, - optionsViewBuilder: ( - BuildContext context, - AutocompleteOnSelected onSelected, - Iterable options, - ) { - return Align( - alignment: Alignment.topLeft, - child: Material( - elevation: 4.0, - child: SizedBox( - height: 200.0, - child: ListView.builder( - padding: const EdgeInsets.all(8.0), - itemCount: options.length, - itemBuilder: (BuildContext context, int index) { - final String option = options.elementAt(index); - return GestureDetector( - onTap: () { - onSelected(option); - }, - child: ListTile( - title: Text(option), - ), - ); - }, - ), - ), - ), - ); - }, - ); - } -} \ No newline at end of file diff --git a/lib/pages/transactions/widgets/transaction_add_dialog.dart b/lib/pages/transactions/widgets/transaction_add_dialog.dart index 7aca030..f7e25bf 100644 --- a/lib/pages/transactions/widgets/transaction_add_dialog.dart +++ b/lib/pages/transactions/widgets/transaction_add_dialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tunas/domains/account/account_bloc.dart'; import 'package:tunas/domains/category/category_bloc.dart'; import 'package:tunas/domains/transaction/transaction_bloc.dart'; import 'package:tunas/pages/transactions/widgets/transaction_form.dart'; @@ -18,6 +19,7 @@ class TransactionAddDialog extends StatelessWidget { providers: [ BlocProvider.value(value: BlocProvider.of(context)), BlocProvider.value(value: BlocProvider.of(context)), + BlocProvider.value(value: BlocProvider.of(context)), ], child: const TransactionAddDialog() ) @@ -55,10 +57,7 @@ class TransactionAddDialog extends StatelessWidget { 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(), - ) + content: const TransactionForm() ) ); } diff --git a/lib/pages/transactions/widgets/transaction_form.dart b/lib/pages/transactions/widgets/transaction_form.dart index 64e1680..7311ce9 100644 --- a/lib/pages/transactions/widgets/transaction_form.dart +++ b/lib/pages/transactions/widgets/transaction_form.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; +import 'package:tunas/domains/account/account_bloc.dart'; import 'package:tunas/domains/category/category_bloc.dart'; import 'package:tunas/domains/transaction/transaction_bloc.dart'; -import 'autocomplete_input.dart'; - class TransactionForm extends StatelessWidget { const TransactionForm({super.key}); @@ -13,11 +12,16 @@ class TransactionForm extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + mainAxisSize: MainAxisSize.min, children: [ _TransactionDateInput(), + const SizedBox(height: 10,), _TransactionCategoryInput(), + const SizedBox(height: 10,), _TransactionDescriptionInput(), + const SizedBox(height: 10,), _TransactionAccountInput(), + const SizedBox(height: 10,), _TransactionValueInput() ], ); @@ -41,11 +45,19 @@ class _TransactionDateInput extends StatelessWidget { context: context, firstDate: DateTime.fromMicrosecondsSinceEpoch(0), lastDate: DateTime.now() - ).then((value) => context.read().add(TransactionDateChange(value))); + ).then((value) { + if (value != null) { + context.read().add(TransactionDateChange(value)); + } + }); }, decoration: InputDecoration( + icon: const Icon(Icons.calendar_month), hintText: 'Date', - errorText: state.transactionDate.isNotValid ? state.transactionDate.error?.message : null + errorText: state.transactionDate.isNotValid ? state.transactionDate.error?.message : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + ), ), ) ) @@ -61,12 +73,18 @@ class _TransactionCategoryInput extends StatelessWidget { buildWhen: (previous, current) => previous.transactionCategory != current.transactionCategory, builder: (context, state) => SizedBox( width: 500, - child: AutocompleteInput( - options: categoryState.categories.map((e) => e.label).toList(), - hintText: 'Category', - initialValue: state.transactionCategory.value, - errorText: state.transactionCategory.isNotValid ? state.transactionCategory.error?.message : null, - onChanged: (value) => context.read().add(TransactionCategoryChange(value)), + child: DropdownButtonFormField( + value: state.transactionCategory.value.toString() == '' ? null : state.transactionCategory.value.toString(), + onChanged: (value) => context.read().add(TransactionCategoryChange(value!)), + items: categoryState.categories.map((e) => DropdownMenuItem(value: e.label, child: Text(e.label))).toList(), + decoration: InputDecoration( + icon: const Icon(Icons.category), + hintText: 'Category', + errorText: state.transactionCategory.isNotValid ? state.transactionCategory.error?.message : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + ), + ), ), ), ); @@ -80,11 +98,16 @@ class _TransactionDescriptionInput extends StatelessWidget { buildWhen: (previous, current) => previous.transactionDescription != current.transactionDescription, builder: (context, state) => SizedBox( width: 500, - child: TextField( + child: TextFormField( decoration: InputDecoration( + icon: const Icon(Icons.description), hintText: 'Description', - errorText: state.transactionDescription.isNotValid ? state.transactionDescription.error?.message : null + errorText: state.transactionDescription.isNotValid ? state.transactionDescription.error?.message : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + ), ), + initialValue: state.transactionDescription.value, onChanged: (value) => context.read().add(TransactionDescriptionChange(value)) ) ), @@ -95,16 +118,23 @@ class _TransactionDescriptionInput extends StatelessWidget { class _TransactionAccountInput extends StatelessWidget { @override Widget build(BuildContext context) { + final accountState = context.watch().state; return BlocBuilder( buildWhen: (previous, current) => previous.transactionAccount != current.transactionAccount, builder: (context, state) => SizedBox( width: 500, - 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)), + child: DropdownButtonFormField( + value: state.transactionAccount.value.toString() == '' ? null : state.transactionAccount.value.toString(), + onChanged: (value) => context.read().add(TransactionAccountChange(value!)), + items: accountState.subAccounts.map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(), + decoration: InputDecoration( + icon: const Icon(Icons.account_box), + hintText: 'Account', + errorText: state.transactionAccount.isNotValid ? state.transactionAccount.error?.message : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + ), + ), ), ), ); @@ -118,12 +148,17 @@ class _TransactionValueInput extends StatelessWidget { buildWhen: (previous, current) => previous.transactionValue != current.transactionValue, builder: (context, state) => SizedBox( width: 500, - child: TextField( + child: TextFormField( keyboardType: TextInputType.number, decoration: InputDecoration( + icon: const Icon(Icons.euro), hintText: '\$\$\$', - errorText: state.transactionValue.isNotValid ? state.transactionValue.error?.message : null + errorText: state.transactionValue.isNotValid ? state.transactionValue.error?.message : null, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + ), ), + initialValue: state.transactionValue.value.toString(), onChanged: (value) => context.read().add(TransactionValueChange(value)) ) ), diff --git a/lib/repositories/account/account_repository.dart b/lib/repositories/account/account_repository.dart index ad54b37..9c54df4 100644 --- a/lib/repositories/account/account_repository.dart +++ b/lib/repositories/account/account_repository.dart @@ -16,6 +16,7 @@ class AccountRepository { final _transactionsController = BehaviorSubject>.seeded(const []); final _categoriesController = BehaviorSubject>.seeded(const []); final _budgetController = BehaviorSubject>.seeded(const []); + final _subAccountController = BehaviorSubject>.seeded(const {}); AccountRepository({ required storageClient, @@ -25,9 +26,7 @@ class AccountRepository { init() async { final account = await getAccount(); - _transactionsController.add(account.transactions); - _categoriesController.add(account.categories); - _budgetController.add(account.budgets); + _broadcastAccountData(account); } Stream> getTransactionsStream() { @@ -42,6 +41,10 @@ class AccountRepository { return _budgetController.asBroadcastStream(); } + Stream> getSubAccountsStream() { + return _subAccountController.asBroadcastStream(); + } + Future getAccount() async { String json = await _storageClient.load(accountFile); Map accountJson = jsonDecode(json); @@ -72,5 +75,6 @@ class AccountRepository { _transactionsController.add(account.transactions); _categoriesController.add(account.categories); _budgetController.add(account.budgets); + _subAccountController.add(account.subAccounts); } } diff --git a/lib/repositories/account/models/account.dart b/lib/repositories/account/models/account.dart index c892106..69f84b4 100644 --- a/lib/repositories/account/models/account.dart +++ b/lib/repositories/account/models/account.dart @@ -9,11 +9,13 @@ class Account { List transactions; List budgets; List categories; + Set subAccounts; Account({ this.transactions = const [], this.budgets = const [], this.categories = const [], + this.subAccounts = const {}, }); factory Account.fromJson(Map json) { @@ -21,6 +23,7 @@ class Account { transactions: (jsonDecode(json['transactions']) as List).map((transaction) => Transaction.fromJson(transaction)).toList(), budgets: (jsonDecode(json['budgets']) as List).map((budget) => Budget.fromJson(budget)).toList(), categories: (jsonDecode(json['categories']) as List).map((category) => Category.fromJson(category)).toList(), + subAccounts: Set.from(jsonDecode(json['subAccounts'])), ); } @@ -28,5 +31,6 @@ class Account { 'transactions': jsonEncode(transactions.map((transaction) => transaction.toJson()).toList()), 'budgets': jsonEncode(budgets.map((budget) => budget.toJson()).toList()), 'categories': jsonEncode(categories.map((category) => category.toJson()).toList()), + 'subAccounts': jsonEncode(subAccounts.toList()), }; } diff --git a/pubspec.lock b/pubspec.lock index f08bb4a..042d880 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -134,6 +134,54 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.3" + flutter_keyboard_visibility: + dependency: transitive + description: + name: flutter_keyboard_visibility + sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_lints: dependency: "direct dev" description: @@ -160,6 +208,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_typeahead: + dependency: "direct main" + description: + name: flutter_typeahead + sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d + url: "https://pub.dev" + source: hosted + version: "5.2.0" flutter_web_plugins: dependency: transitive description: flutter @@ -301,6 +357,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.7" + pointer_interceptor: + dependency: transitive + description: + name: pointer_interceptor + sha256: bd18321519718678d5fa98ad3a3359cbc7a31f018554eab80b73d08a7f0c165a + url: "https://pub.dev" + source: hosted + version: "0.10.1" + pointer_interceptor_ios: + dependency: transitive + description: + name: pointer_interceptor_ios + sha256: "2e73c39452830adc4695757130676a39412a3b7f3c34e3f752791b5384770877" + url: "https://pub.dev" + source: hosted + version: "0.10.0+2" + pointer_interceptor_platform_interface: + dependency: transitive + description: + name: pointer_interceptor_platform_interface + sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" + url: "https://pub.dev" + source: hosted + version: "0.10.0+1" + pointer_interceptor_web: + dependency: transitive + description: + name: pointer_interceptor_web + sha256: "9386e064097fd16419e935c23f08f35b58e6aaec155dd39bd6a003b88f9c14b4" + url: "https://pub.dev" + source: hosted + version: "0.10.1+2" provider: dependency: transitive description: @@ -428,4 +516,4 @@ packages: version: "1.0.3" sdks: dart: ">=3.2.2 <4.0.0" - flutter: ">=3.7.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4ac5579..89c62a1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: intl: any formz: ^0.6.1 uuid: ^4.3.2 + flutter_typeahead: ^5.2.0 dev_dependencies: flutter_test: