From 979fecb60a0436c21f2b8623c43c1604fc500a26 Mon Sep 17 00:00:00 2001 From: gltron Date: Sun, 25 Feb 2024 19:20:18 +0100 Subject: [PATCH] Basic budget sliders --- lib/domains/budget/budget_bloc.dart | 71 +++++++++ lib/domains/budget/budget_event.dart | 33 +++- lib/domains/budget/budget_state.dart | 15 +- lib/domains/charts/chart_bloc.dart | 9 +- lib/pages/budgets/budgets_page.dart | 2 +- .../budgets/widgets/month_distribution.dart | 143 +++++++++++------- lib/pages/data/data_page.dart | 1 + lib/pages/stats/stats_page.dart | 2 - .../widgets/transactions_header.dart | 8 +- lib/repositories/metadata/models/budget.dart | 6 +- 10 files changed, 219 insertions(+), 71 deletions(-) diff --git a/lib/domains/budget/budget_bloc.dart b/lib/domains/budget/budget_bloc.dart index 0bf74d5..1a80aff 100644 --- a/lib/domains/budget/budget_bloc.dart +++ b/lib/domains/budget/budget_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:tunas/repositories/metadata/models/budget.dart'; @@ -11,6 +13,11 @@ class BudgetBloc extends Bloc { BudgetBloc({required MetadataRepository metadataRepository}) : _metadataRepository = metadataRepository, super(const BudgetState()) { on(_onBudgetsLoad); + on(_onBudgetAdd); + on(_onBudgetRemove); + on(_onBudgetSetValue); + on(_onBudgetCompareNext); + on(_onBudgetComparePrevious); _metadataRepository .getBudgetsStream() @@ -24,4 +31,68 @@ class BudgetBloc extends Bloc { budgets: event.budgets, )); } + + FutureOr _onBudgetAdd(BudgetAdd event, Emitter emit) async { + Budget budget = Budget( + label: event.label, + ); + + emit(_computeState(await _saveBudget(budget))); + } + + FutureOr _onBudgetRemove(BudgetRemove event, Emitter emit) async { + List budgets = _metadataRepository.getBudgets(); + Budget budgetToRemove = event.budget; + + budgets.removeWhere((budget) => budget.label == budgetToRemove.label); + emit(_computeState(await _metadataRepository.saveBudgets(budgets))); + } + + FutureOr _onBudgetSetValue(BudgetSetValue event, Emitter emit) async { + Budget budgetToUpdate = event.budget; + + if (state.remainingBudget - (event.value - budgetToUpdate.value) < 0) { + return; + } + + // if (state.remainingBudget - event.value < 0 && state.remainingBudget < 10) { + // budgetToUpdate.value = + // } else { + // budgetToUpdate.value = event.value; + // } + budgetToUpdate.value = event.value; + + emit(_computeState(await _saveBudget(budgetToUpdate))); + } + + FutureOr _onBudgetCompareNext(BudgetCompareNext event, Emitter emit) { + } + + FutureOr _onBudgetComparePrevious(BudgetComparePrevious event, Emitter emit) { + } + + Future> _saveBudget(Budget budgetToSave) async { + List budgets = _metadataRepository.getBudgets(); + + try { + Budget budgetFound = budgets.firstWhere((category) => category.label == budgetToSave.label); + budgetFound.value = budgetToSave.value; + } catch (e) { + if (budgets.isEmpty) { + budgets = [budgetToSave]; + } else { + budgets.add(budgetToSave); + } + } + + // _metadataRepository.saveBudgets(budgets); + return budgets; + } + + BudgetState _computeState(List budgets) { + return state.copyWith( + budgets: budgets, + remainingBudget: state.initialBudget - budgets.map((budget) => budget.value).reduce((value, element) => value + element), + ); + } } \ No newline at end of file diff --git a/lib/domains/budget/budget_event.dart b/lib/domains/budget/budget_event.dart index 83f15fe..8176df7 100644 --- a/lib/domains/budget/budget_event.dart +++ b/lib/domains/budget/budget_event.dart @@ -13,4 +13,35 @@ final class BudgetsLoad extends BudgetEvent { @override List get props => [budgets]; -} \ No newline at end of file +} + +final class BudgetAdd extends BudgetEvent { + final String label; + + const BudgetAdd(this.label); + + @override + List get props => [label]; +} + +final class BudgetRemove extends BudgetEvent { + final Budget budget; + + const BudgetRemove(this.budget); + + @override + List get props => [budget]; +} + +final class BudgetSetValue extends BudgetEvent { + final Budget budget; + final double value; + + const BudgetSetValue(this.budget, this.value); + + @override + List get props => [budget, value]; +} + +final class BudgetCompareNext extends BudgetEvent {} +final class BudgetComparePrevious extends BudgetEvent {} \ No newline at end of file diff --git a/lib/domains/budget/budget_state.dart b/lib/domains/budget/budget_state.dart index ce4bbde..cfcfb76 100644 --- a/lib/domains/budget/budget_state.dart +++ b/lib/domains/budget/budget_state.dart @@ -1,20 +1,25 @@ part of 'budget_bloc.dart'; -final class BudgetState extends Equatable { +final class BudgetState { final List budgets; + final double initialBudget; + final double remainingBudget; const BudgetState({ this.budgets = const [], + this.initialBudget = 2300.0, + this.remainingBudget = 2300.0, }); BudgetState copyWith({ List? budgets, + double? initialBudget, + double? remainingBudget, }) { return BudgetState( budgets: budgets ?? this.budgets, + initialBudget: initialBudget ?? this.initialBudget, + remainingBudget: remainingBudget ?? this.remainingBudget, ); } - - @override - List get props => [budgets]; -} +} \ No newline at end of file diff --git a/lib/domains/charts/chart_bloc.dart b/lib/domains/charts/chart_bloc.dart index 4f7ae92..7f4724d 100644 --- a/lib/domains/charts/chart_bloc.dart +++ b/lib/domains/charts/chart_bloc.dart @@ -113,7 +113,7 @@ class ChartBloc extends Bloc { double scoppedTotal = 0; List scopedCategoriesPositiveTotals = []; List scopedCategoriesNegativeTotals = []; - Map scopedMonthlyTotals = {}; + Map scopedMonthlyTotalsMap = {}; Map scopedCategoriesMonthlyTotals = {}; Map scopedMonthlyPostitiveTotals = {}; @@ -134,7 +134,7 @@ class ChartBloc extends Bloc { DateTime transactionDate = transactionLine.transaction.date; int transactionDateDay = transactionDate.difference(DateTime(transactionDate.year,1,1)).inDays + 1; - scopedMonthlyTotals[transactionDateDay] = FlSpot(transactionDateDay.toDouble(), transactionLine.subTotal); + scopedMonthlyTotalsMap[transactionDateDay] = FlSpot(transactionDateDay.toDouble(), transactionLine.subTotal); final category = state.categories[transaction.category]; if (category == null || category.transfert) { @@ -229,6 +229,9 @@ class ChartBloc extends Bloc { _sortMapByValues(monthTotals.negatives); } + List scopedMonthlyTotals = scopedMonthlyTotalsMap.values.toList(); + scopedMonthlyTotals.sort((a, b) => a.x.compareTo(b.x)); + return state.copyWith( scoppedProfit: scoppedTotal, scopedCategoriesPositiveTotals: scopedCategoriesPositiveTotals, @@ -239,7 +242,7 @@ class ChartBloc extends Bloc { scopedSimplifiedCategoriesPositiveTotalsPercents: scopedSimplifiedCategoriesPositiveTotalsPercents, scopedSimplifiedCategoriesNegativeTotals: scopedSimplifiedCategoriesNegativeTotals, scopedSimplifiedCategoriesNegativeTotalsPercents: scopedSimplifiedCategoriesNegativeTotalsPercents, - scopedMonthlyTotals: scopedMonthlyTotals.values.toList(), + scopedMonthlyTotals: scopedMonthlyTotals, scopedCategoriesMonthlyTotals: scopedCategoriesMonthlyTotals, scopedMonthlyPostitiveTotals: scopedMonthlyPostitiveTotals, scopedMonthlyNegativeTotals: scopedMonthlyNegativeTotals, diff --git a/lib/pages/budgets/budgets_page.dart b/lib/pages/budgets/budgets_page.dart index 5ec38d2..92ea095 100644 --- a/lib/pages/budgets/budgets_page.dart +++ b/lib/pages/budgets/budgets_page.dart @@ -11,7 +11,7 @@ class BudgetsPage extends StatelessWidget { return Center( child: Container( constraints: const BoxConstraints( - maxWidth: 1000 + maxWidth: 1000, ), child: ListView( padding: mediaQuery.padding.copyWith(left: 10.0, right: 10.0, bottom: 100.0), diff --git a/lib/pages/budgets/widgets/month_distribution.dart b/lib/pages/budgets/widgets/month_distribution.dart index 072842b..5e4fbe8 100644 --- a/lib/pages/budgets/widgets/month_distribution.dart +++ b/lib/pages/budgets/widgets/month_distribution.dart @@ -1,73 +1,104 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:tunas/domains/budget/budget_bloc.dart'; import 'package:tunas/pages/common/titled_container.dart'; +import 'package:tunas/repositories/metadata/models/budget.dart'; +import 'package:uuid/uuid.dart'; class MonthDistribution extends StatelessWidget { const MonthDistribution({super.key}); + List _computeBudgetLines(BuildContext context, List budgets, double initialBudget, double remainingBudget) { + List list = [ + Text('Money to spare: ${NumberFormat('#####00.00 €', 'fr_FR').format(remainingBudget)} € / $initialBudget €'), + ]; + + list.addAll(budgets.map((budget) => Row( + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(budget.label), + Text(NumberFormat('#####00.00 €', 'fr_FR').format(budget.value)), + ], + ) + ), + Expanded( + child: SliderTheme( + data: SliderThemeData( + + ), + child: Slider( + min: 0, + max: initialBudget, + label: budget.value.round().toString(), + value: budget.value, + secondaryTrackValue: remainingBudget + budget.value, + onChanged: (value) => context.read().add(BudgetSetValue(budget, value)), + ), + ) + ) + ], + )).toList()); + return list; + } + @override Widget build(BuildContext context) { - return Column( - children: [ - TitledContainer( - title: 'Prepare', - child: Column( - children: [ - Text('Money to spare: 2300 €'), - Text('Loyer'), - Slider( - min: 0, - max: 2300, - value: 200, - onChanged: (value) => {}, - ), - Text('Loyer'), - Slider( - min: 0, - max: 2300, - value: 200, - onChanged: (value) => {}, - ), - ], + return BlocBuilder( + builder: (context, state) => Column( + children: [ + TitledContainer( + title: 'Prepare', + action: IconButton( + onPressed: () => context.read().add(BudgetAdd(const Uuid().v8())), + icon: const Icon(Icons.add), + ), + child: Column( + children: _computeBudgetLines(context, state.budgets, state.initialBudget, state.remainingBudget), + ), ), - ), - TitledContainer( - title: 'Compare', - height: 500, - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Budget'), - ], + TitledContainer( + title: 'Compare', + height: 500, + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Budget'), + ], + ), ), - ), - Expanded( - child: Column( - children: [ - Row( - children: [ - IconButton( - onPressed: () => {}, - icon: const Icon(Icons.skip_previous) - ), - Text('Fev 2024'), - IconButton( - onPressed: () => {}, - icon: const Icon(Icons.skip_next) - ), - ], - ), - ], + Expanded( + child: Column( + children: [ + Row( + children: [ + IconButton( + onPressed: () => {}, + icon: const Icon(Icons.skip_previous) + ), + Text('Fev 2024'), + IconButton( + onPressed: () => {}, + icon: const Icon(Icons.skip_next) + ), + ], + ), + ], + ) ) - ) - ], + ], + ), ), - ), - ], + ], + ) ); } } \ No newline at end of file diff --git a/lib/pages/data/data_page.dart b/lib/pages/data/data_page.dart index ddd8cdd..54c1650 100644 --- a/lib/pages/data/data_page.dart +++ b/lib/pages/data/data_page.dart @@ -11,6 +11,7 @@ class DataPage extends StatelessWidget { MediaQueryData mediaQuery = MediaQuery.of(context); return Center( child: Container( + margin: const EdgeInsets.symmetric(vertical: 11, horizontal: 0), constraints: const BoxConstraints( maxWidth: 1000 ), diff --git a/lib/pages/stats/stats_page.dart b/lib/pages/stats/stats_page.dart index 6b77463..a3da180 100644 --- a/lib/pages/stats/stats_page.dart +++ b/lib/pages/stats/stats_page.dart @@ -8,8 +8,6 @@ import 'package:tunas/pages/stats/widgets/monthly_categories_total_chart.dart'; import 'package:tunas/pages/stats/widgets/global_total_chart.dart'; import 'package:tunas/pages/stats/widgets/profit_indicator.dart'; import 'package:tunas/pages/stats/widgets/year_selector.dart'; -import 'package:tunas/repositories/metadata/metadata_repository.dart'; -import 'package:tunas/repositories/transactions/transactions_repository.dart'; class StatsPage extends StatelessWidget { const StatsPage({super.key}); diff --git a/lib/pages/transactions/widgets/transactions_header.dart b/lib/pages/transactions/widgets/transactions_header.dart index 43f4a0e..cf0b5e5 100644 --- a/lib/pages/transactions/widgets/transactions_header.dart +++ b/lib/pages/transactions/widgets/transactions_header.dart @@ -57,8 +57,12 @@ class TransactionsHeader extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 10), margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 10), - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: Colors.black)) + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).colorScheme.onPrimaryContainer + ) + ) ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/repositories/metadata/models/budget.dart b/lib/repositories/metadata/models/budget.dart index 85e0bb9..e9d6bc2 100644 --- a/lib/repositories/metadata/models/budget.dart +++ b/lib/repositories/metadata/models/budget.dart @@ -1,20 +1,24 @@ class Budget { + String label; bool monthly; double value; Budget({ + this.label = '', this.monthly = false, this.value = 0.0, }); factory Budget.fromJson(Map json) { return Budget( - monthly: json['monthly'], + label: json['label'], + monthly: bool.parse(json['monthly']), value: double.parse(json['value']), ); } Map toJson() => { + 'label': label, 'monthly': monthly.toString(), 'value': value.toString(), };