diff --git a/lib/app.dart b/lib/app.dart index c5e0396..614bdfe 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -2,7 +2,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:tunas/clients/storage/storage_client.dart'; +import 'package:tunas/clients/storage/json_storage_client.dart'; import 'package:tunas/pages/home/home_page.dart'; import 'package:tunas/repositories/json/json_repository.dart'; import 'package:tunas/repositories/metadata/metadata_repository.dart'; @@ -30,7 +30,7 @@ class AppView extends StatefulWidget { } class _AppViewState extends State { - late final StorageClient _storageClient; + late final JsonStorageClient _storageClient; late final JsonRepository _jsonRepository; late final TransactionsRepository _transactionsRepository; late final MetadataRepository _metadataRepository; @@ -39,7 +39,7 @@ class _AppViewState extends State { void initState() { super.initState(); - _storageClient = StorageClient(); + _storageClient = JsonStorageClient(); _jsonRepository = JsonRepository(storageClient: _storageClient); _transactionsRepository = TransactionsRepository(jsonRepository: _jsonRepository); _metadataRepository = MetadataRepository(jsonRepository: _jsonRepository); diff --git a/lib/clients/storage/storage_client.dart b/lib/clients/storage/json_storage_client.dart similarity index 96% rename from lib/clients/storage/storage_client.dart rename to lib/clients/storage/json_storage_client.dart index 1539a49..14623c2 100644 --- a/lib/clients/storage/storage_client.dart +++ b/lib/clients/storage/json_storage_client.dart @@ -2,7 +2,8 @@ import 'dart:io'; import 'package:path_provider/path_provider.dart'; -class StorageClient { +class JsonStorageClient { + save(String filename, String data) async { File file = await _getJson(filename); await file.writeAsString(data); diff --git a/lib/domains/account/account_bloc.dart b/lib/domains/account/account_bloc.dart index 2e65717..f08e7f3 100644 --- a/lib/domains/account/account_bloc.dart +++ b/lib/domains/account/account_bloc.dart @@ -123,13 +123,13 @@ class AccountBloc extends Bloc { }) .toList(); - await _metadataRepository.deleteMetadata(); - await _transactionsRepository.deleteTransactions(); + _metadataRepository.deleteMetadata(); + _transactionsRepository.deleteTransactions(); - await _metadataRepository.saveAccounts(accounts.values.toList()); - await _metadataRepository.saveBudgets([]); - await _metadataRepository.saveCategories(categoriesMap.values.toList()); - await _transactionsRepository.saveTransactions(transactions); + _metadataRepository.saveAccounts(accounts.values.toList()); + _metadataRepository.saveBudgets([]); + _metadataRepository.saveCategories(categoriesMap.values.toList()); + _transactionsRepository.saveTransactions(transactions); } } @@ -141,7 +141,7 @@ class AccountBloc extends Bloc { ); } - _onAccountAdd(AccountAdd event, Emitter emit) async { + _onAccountAdd(AccountAdd event, Emitter emit) { String uuid = const Uuid().v8(); Account account = Account( label: 'Account $uuid', @@ -149,11 +149,11 @@ class AccountBloc extends Bloc { saving: false ); emit( - state.copyWith(await _saveAccount(account)) + state.copyWith(_saveAccount(account)) ); } - _onAcountRemove(AccountRemove event, Emitter emit) async { + _onAcountRemove(AccountRemove event, Emitter emit) { Account accountToRemove = event.account; List accounts = state.accounts; List transactions = _transactionsRepository.getTransactions(); @@ -165,33 +165,33 @@ class AccountBloc extends Bloc { accounts.removeWhere((account) => account.label == accountToRemove.label); emit(AccountRemoveSucess()); emit( - state.copyWith(await _metadataRepository.saveAccounts(accounts)) + state.copyWith(_metadataRepository.saveAccounts(accounts)) ); } } - _onAccountEditLabel(AccountEditLabel event, Emitter emit) async { + _onAccountEditLabel(AccountEditLabel event, Emitter emit) { Account account = event.account; // TODO check for existance, rename every transaction } - _onAccountEditSaving(AccountEditSaving event, Emitter emit) async { + _onAccountEditSaving(AccountEditSaving event, Emitter emit) { Account account = event.account; account.saving = event.saving; emit( - state.copyWith(await _saveAccount(account)) + state.copyWith(_saveAccount(account)) ); } - _onAccountEditColor(AccountEditColor event, Emitter emit) async { + _onAccountEditColor(AccountEditColor event, Emitter emit) { Account account = event.account; account.color = event.color; emit( - state.copyWith(await _saveAccount(account)) + state.copyWith(_saveAccount(account)) ); } - Future> _saveAccount(Account accountToSave) async { + List _saveAccount(Account accountToSave) { List accounts = _metadataRepository.getAccounts(); try { @@ -206,7 +206,7 @@ class AccountBloc extends Bloc { } } - await _metadataRepository.saveAccounts(accounts); + _metadataRepository.saveAccounts(accounts); return accounts; } } diff --git a/lib/domains/budget/budget_bloc.dart b/lib/domains/budget/budget_bloc.dart index 1a80aff..1f714d2 100644 --- a/lib/domains/budget/budget_bloc.dart +++ b/lib/domains/budget/budget_bloc.dart @@ -4,55 +4,66 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:tunas/repositories/metadata/models/budget.dart'; import 'package:tunas/repositories/metadata/metadata_repository.dart'; +import 'package:tunas/repositories/transactions/transactions_repository.dart'; part 'budget_event.dart'; part 'budget_state.dart'; class BudgetBloc extends Bloc { final MetadataRepository _metadataRepository; + final TransactionsRepository _transactionsRepository; + Timer? setValueTimer; - BudgetBloc({required MetadataRepository metadataRepository}) : _metadataRepository = metadataRepository, super(const BudgetState()) { + BudgetBloc({required MetadataRepository metadataRepository, required TransactionsRepository transactionsRepository}) : _metadataRepository = metadataRepository, _transactionsRepository = transactionsRepository, super(const BudgetState()) { on(_onBudgetsLoad); on(_onBudgetAdd); on(_onBudgetRemove); on(_onBudgetSetValue); + on(_onBudgetSetLabel); on(_onBudgetCompareNext); on(_onBudgetComparePrevious); + on(_onBudgetSetCompare); + on(_onBudgetSetInitial); _metadataRepository .getBudgetsStream() .listen((budgets) => add(BudgetsLoad(budgets))); + + _transactionsRepository + .getTransactionsStream() + .listen((transactions) => add(BudgetSetCompare())); } _onBudgetsLoad( BudgetsLoad event, Emitter emit ) { - emit(state.copyWith( - budgets: event.budgets, - )); + emit(_computeState(event.budgets, null)); } - FutureOr _onBudgetAdd(BudgetAdd event, Emitter emit) async { + FutureOr _onBudgetAdd(BudgetAdd event, Emitter emit) { Budget budget = Budget( label: event.label, ); - emit(_computeState(await _saveBudget(budget))); + List budgets = _computeBudgets(budget); + _saveBudget(budgets); + emit(_computeState(budgets, null)); } - FutureOr _onBudgetRemove(BudgetRemove event, Emitter emit) async { + FutureOr _onBudgetRemove(BudgetRemove event, Emitter emit) { List budgets = _metadataRepository.getBudgets(); Budget budgetToRemove = event.budget; budgets.removeWhere((budget) => budget.label == budgetToRemove.label); - emit(_computeState(await _metadataRepository.saveBudgets(budgets))); + emit(_computeState(_metadataRepository.saveBudgets(budgets), null)); } - FutureOr _onBudgetSetValue(BudgetSetValue event, Emitter emit) async { + void _onBudgetSetValue(BudgetSetValue event, Emitter emit) { Budget budgetToUpdate = event.budget; + double newValue = event.value; if (state.remainingBudget - (event.value - budgetToUpdate.value) < 0) { - return; + newValue = event.budget.value + state.remainingBudget; } // if (state.remainingBudget - event.value < 0 && state.remainingBudget < 10) { @@ -60,18 +71,112 @@ class BudgetBloc extends Bloc { // } else { // budgetToUpdate.value = event.value; // } - budgetToUpdate.value = event.value; + budgetToUpdate.value = newValue; + List budgets = _computeBudgets(budgetToUpdate); + emit(_computeState(budgets, null)); - emit(_computeState(await _saveBudget(budgetToUpdate))); + setValueTimer?.cancel(); + setValueTimer = Timer( + const Duration(milliseconds: 100), + () { + _saveBudget(budgets); + } + ); } - FutureOr _onBudgetCompareNext(BudgetCompareNext event, Emitter emit) { + void _onBudgetSetLabel(BudgetSetLabel event, Emitter emit) { + Budget budgetToUpdate = event.budget; + + budgetToUpdate.label = event.label; + List budgets = _computeBudgets(budgetToUpdate); + _saveBudget(budgets); + emit(_computeState(budgets, null)); } - FutureOr _onBudgetComparePrevious(BudgetComparePrevious event, Emitter emit) { + void _onBudgetCompareNext(BudgetCompareNext event, Emitter emit) { + num compareMonth = state.compareMonth; + num compareYear = state.compareYear; + if (state.compareMonth == 12) { + compareMonth = 1; + compareYear++; + } else { + compareMonth++; + } + + if (state.lastDate != null && state.lastDate!.isBefore(DateTime(compareYear.toInt(), compareMonth.toInt()))) { + return; + } + + final compareResult = _computeCompareBudget(state.budgets, compareYear, compareMonth); + emit(state.copyWith( + compareBudgets: compareResult.$1, + otherBudgets: compareResult.$2, + compareMonth: compareMonth, + compareYear: compareYear, + )); } - Future> _saveBudget(Budget budgetToSave) async { + void _onBudgetComparePrevious(BudgetComparePrevious event, Emitter emit) { + num compareMonth = state.compareMonth; + num compareYear = state.compareYear; + if (state.compareMonth == 1) { + compareMonth = 12; + compareYear--; + } else { + compareMonth--; + } + + if (state.firstDate != null && state.firstDate!.isAfter(DateTime(compareYear.toInt(), compareMonth.toInt()))) { + return; + } + + final compareResult = _computeCompareBudget(state.budgets, compareYear, compareMonth); + emit(state.copyWith( + compareBudgets: compareResult.$1, + otherBudgets: compareResult.$2, + compareMonth: compareMonth, + compareYear: compareYear, + )); + } + + _onBudgetSetCompare(BudgetSetCompare event, Emitter emit) { + DateTime firstDate = DateTime.now(); + DateTime lastDate = DateTime.fromMicrosecondsSinceEpoch(0); + + for (var transaction in _transactionsRepository.getTransactions()) { + if (firstDate.compareTo(transaction.date) > 0) { + firstDate = transaction.date; + } + + if (lastDate.compareTo(transaction.date) < 0) { + lastDate = transaction.date; + } + } + + num compareMonth = state.compareMonth; + num compareYear = state.compareYear; + + if (compareMonth == 1 && compareYear == 2000) { + compareMonth = lastDate.month; + compareYear = lastDate.year; + } + + final compareResult = _computeCompareBudget(state.budgets, compareYear, compareMonth); + emit(state.copyWith( + firstDate: firstDate, + lastDate: lastDate, + compareMonth: compareMonth, + compareYear: compareYear, + compareBudgets: compareResult.$1, + otherBudgets: compareResult.$2, + )); + } + + void _onBudgetSetInitial(BudgetSetInitial event, Emitter emit) { + emit(_computeState(state.budgets, event.value)); + } + + List _computeBudgets(Budget budgetToSave) { List budgets = _metadataRepository.getBudgets(); try { @@ -85,14 +190,48 @@ class BudgetBloc extends Bloc { } } - // _metadataRepository.saveBudgets(budgets); return budgets; } - BudgetState _computeState(List budgets) { + List _saveBudget(List budgets) { + return _metadataRepository.saveBudgets(budgets); + } + + (List compareBudgets, List otherBudgets) _computeCompareBudget(List budgets, num year, num month) { + Map compareBudgetMap = { for (var budget in budgets) budget.label : Budget(label: budget.label)}; + Budget otherBudget = Budget(label: 'Hors budget'); + Map otherBudgetMap = {}; + List compareBudgets = []; + _transactionsRepository.getTransactions() + .where((transaction) => transaction.value < 0 && transaction.date.year == year && transaction.date.month == month) + .forEach((transaction) { + Budget? budget = compareBudgetMap[transaction.category]; + if (budget == null) { + otherBudget.value += transaction.value.abs(); + Budget? otherBudgetFromMap = otherBudgetMap[transaction.category]; + if (otherBudgetFromMap == null) { + otherBudgetMap[transaction.category] = Budget(label: transaction.category, value: transaction.value.abs()); + } else { + otherBudgetFromMap.value += transaction.value.abs(); + } + } else { + budget.value += transaction.value.abs(); + } + }); + + compareBudgets.addAll(compareBudgetMap.values); + compareBudgets.add(otherBudget); + return (compareBudgets, otherBudgetMap.values.toList()..sort((a, b) => b.value.compareTo(a.value))); + } + + BudgetState _computeState(List budgets, double? initialBudget) { + final compareResult = _computeCompareBudget(state.budgets, state.compareYear, state.compareMonth); return state.copyWith( budgets: budgets, - remainingBudget: state.initialBudget - budgets.map((budget) => budget.value).reduce((value, element) => value + element), + initialBudget: (initialBudget ?? state.initialBudget), + remainingBudget: (initialBudget ?? state.initialBudget) - budgets.map((budget) => budget.value).reduce((value, element) => value + element), + compareBudgets: compareResult.$1, + otherBudgets: compareResult.$2, ); } } \ No newline at end of file diff --git a/lib/domains/budget/budget_event.dart b/lib/domains/budget/budget_event.dart index 8176df7..97695bd 100644 --- a/lib/domains/budget/budget_event.dart +++ b/lib/domains/budget/budget_event.dart @@ -43,5 +43,24 @@ final class BudgetSetValue extends BudgetEvent { List get props => [budget, value]; } +final class BudgetSetLabel extends BudgetEvent { + final Budget budget; + final String label; + + const BudgetSetLabel(this.budget, this.label); + + @override + List get props => [budget, label]; +} + final class BudgetCompareNext extends BudgetEvent {} -final class BudgetComparePrevious extends BudgetEvent {} \ No newline at end of file +final class BudgetComparePrevious extends BudgetEvent {} +final class BudgetSetCompare extends BudgetEvent {} +final class BudgetSetInitial extends BudgetEvent { + final double value; + + const BudgetSetInitial(this.value); + + @override + List get props => [value]; +} \ No newline at end of file diff --git a/lib/domains/budget/budget_state.dart b/lib/domains/budget/budget_state.dart index cfcfb76..36fcab4 100644 --- a/lib/domains/budget/budget_state.dart +++ b/lib/domains/budget/budget_state.dart @@ -5,21 +5,46 @@ final class BudgetState { final double initialBudget; final double remainingBudget; + final List compareBudgets; + final List otherBudgets; + final num compareYear; + final num compareMonth; + final DateTime? firstDate; + final DateTime? lastDate; + const BudgetState({ this.budgets = const [], this.initialBudget = 2300.0, this.remainingBudget = 2300.0, + this.compareBudgets = const [], + this.otherBudgets = const [], + this.compareYear = 2000, + this.compareMonth = 1, + this.firstDate, + this.lastDate, }); BudgetState copyWith({ List? budgets, double? initialBudget, double? remainingBudget, + List? compareBudgets, + List? otherBudgets, + num? compareYear, + num? compareMonth, + DateTime? firstDate, + DateTime? lastDate, }) { return BudgetState( budgets: budgets ?? this.budgets, initialBudget: initialBudget ?? this.initialBudget, remainingBudget: remainingBudget ?? this.remainingBudget, + compareBudgets: compareBudgets ?? this.compareBudgets, + otherBudgets: otherBudgets ?? this.otherBudgets, + compareYear: compareYear ?? this.compareYear, + compareMonth: compareMonth ?? this.compareMonth, + firstDate: firstDate ?? this.firstDate, + lastDate: lastDate ?? this.lastDate, ); } } \ No newline at end of file diff --git a/lib/domains/category/category_bloc.dart b/lib/domains/category/category_bloc.dart index 4ec5a7a..2342a12 100644 --- a/lib/domains/category/category_bloc.dart +++ b/lib/domains/category/category_bloc.dart @@ -42,29 +42,29 @@ class CategoryBloc extends Bloc { emit(_computeState(event.categories)); } - FutureOr _onCategoryEditColor(CategoryEditColor event, Emitter emit) async { + FutureOr _onCategoryEditColor(CategoryEditColor event, Emitter emit) { Category category = event.category; category.color = event.color; - emit(_computeState(await _saveCategory(category))); + emit(_computeState(_saveCategory(category))); } - FutureOr _onCategoryEditTransfert(CategoryEditTransfert event, Emitter emit) async { + FutureOr _onCategoryEditTransfert(CategoryEditTransfert event, Emitter emit) { Category category = event.category; category.transfert = event.transfert; - emit(_computeState(await _saveCategory(category))); + emit(_computeState(_saveCategory(category))); } - FutureOr _onCategoryEditEssential(CategoryEditEssential event, Emitter emit) async { + FutureOr _onCategoryEditEssential(CategoryEditEssential event, Emitter emit) { Category category = event.category; category.essential = event.essential; - emit(_computeState(await _saveCategory(category))); + emit(_computeState(_saveCategory(category))); } - FutureOr _onCategoryEditLabel(CategoryEditLabel event, Emitter emit) async { + FutureOr _onCategoryEditLabel(CategoryEditLabel event, Emitter emit) { // TODO check for existance, rename every transaction } - FutureOr _onCategoryRemove(CategoryRemove event, Emitter emit) async { + FutureOr _onCategoryRemove(CategoryRemove event, Emitter emit) { CategoryState originalCategoryState = state.copyWith(); Category categoryToRemove = event.category; List categories = state.categories; @@ -76,21 +76,21 @@ class CategoryBloc extends Bloc { } else { categories.removeWhere((category) => category.label == categoryToRemove.label); emit(CategoryRemoveSucess()); - emit(_computeState(await _metadataRepository.saveCategories(categories))); + emit(_computeState(_metadataRepository.saveCategories(categories))); } } - FutureOr _onCategoryAdd(CategoryAdd event, Emitter emit) async { + FutureOr _onCategoryAdd(CategoryAdd event, Emitter emit) { String uuid = const Uuid().v8(); Category category = Category( label: 'Category $uuid', color: 'FF74feff', ); - emit(_computeState(await _saveCategory(category))); + emit(_computeState(_saveCategory(category))); } - _saveCategory(Category categoryToSave) async { + List _saveCategory(Category categoryToSave) { List categories = _metadataRepository.getCategories(); try { @@ -106,7 +106,7 @@ class CategoryBloc extends Bloc { } } - await _metadataRepository.saveCategories(categories); + _metadataRepository.saveCategories(categories); return categories; } diff --git a/lib/domains/transaction/transaction_bloc.dart b/lib/domains/transaction/transaction_bloc.dart index 4cf2f7c..472e53f 100644 --- a/lib/domains/transaction/transaction_bloc.dart +++ b/lib/domains/transaction/transaction_bloc.dart @@ -145,7 +145,7 @@ class TransactionBloc extends Bloc { _onTransactionAddDialog( TransactionAdd event, Emitter emit - ) async { + ) { if (state.isValid) { List transactions = state.transactions; Transaction? currentTransaction = state.currentTransaction; @@ -163,7 +163,7 @@ class TransactionBloc extends Bloc { )); final computeResult = _computeTransactionLine(transactions); - await _transactionsRepository.saveTransactions(transactions); + _transactionsRepository.saveTransactions(transactions); emit(state.copyWith( currentTransaction: null, @@ -208,13 +208,13 @@ class TransactionBloc extends Bloc { _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 _transactionsRepository.saveTransactions(transactions); + _transactionsRepository.saveTransactions(transactions); emit(state.copyWith( currentTransaction: null, transactionDate: const TransactionDate.pure(), diff --git a/lib/pages/budgets/widgets/budget_comparator.dart b/lib/pages/budgets/widgets/budget_comparator.dart new file mode 100644 index 0000000..73a1260 --- /dev/null +++ b/lib/pages/budgets/widgets/budget_comparator.dart @@ -0,0 +1,135 @@ +import 'package:fl_chart/fl_chart.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'; + +class BudgetComparator extends StatelessWidget { + const BudgetComparator({super.key}); + + List _computeDataSet(BuildContext context, List targetBudgets, List realBudgets) { + RadarDataSet targetDataSet = RadarDataSet( + fillColor: Theme.of(context).colorScheme.primary.withAlpha(100), + borderColor: Theme.of(context).colorScheme.primary, + entryRadius: 5, + dataEntries: targetBudgets.map((budget) => RadarEntry(value: budget.value)).toList(), + ); + if (realBudgets.isNotEmpty) { + targetDataSet.dataEntries.add(const RadarEntry(value: 0)); + } + RadarDataSet realDataSet = RadarDataSet( + fillColor: Theme.of(context).colorScheme.onPrimary.withAlpha(100), + borderColor: Theme.of(context).colorScheme.onPrimary, + entryRadius: 8, + dataEntries: realBudgets.map((budget) => RadarEntry(value: budget.value)).toList(), + ); + return realBudgets.isEmpty ? [RadarDataSet(dataEntries: [const RadarEntry(value: 1), const RadarEntry(value: 2), const RadarEntry(value: 3)])] : [realDataSet, targetDataSet]; + } + + RadarChartTitle _computeDataSetTitle(int index, List realBudgets) { + return realBudgets.isEmpty ? const RadarChartTitle(text: 'No data') : RadarChartTitle(text: realBudgets[index].label); + } + + List _computeOtherBudgets(BuildContext context, List otherBudgets) { + List list = [ + const Text('Hors budget'), + Container( + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 2, + color: Theme.of(context).colorScheme.onPrimaryContainer + ) + ) + ), + ) + ]; + + list.addAll(otherBudgets.map((budget) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container(width: 5), + Text(budget.label), + ], + ), + Text( + NumberFormat("#00 €").format(budget.value.abs()), + style: const TextStyle( + fontFamily: 'NovaMono', + ) + ) + ], + ))); + return list; + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) => TitledContainer( + title: 'Compare', + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + onPressed: () => context.read().add(BudgetComparePrevious()), + icon: const Icon(Icons.skip_previous) + ), + Text('${NumberFormat('00', 'fr_FR').format(state.compareMonth)} - ${state.compareYear}'), + IconButton( + onPressed: () => context.read().add(BudgetCompareNext()), + icon: const Icon(Icons.skip_next) + ), + ], + ), + Row( + children: [ + Container( + height: 300, + width: 250, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: Theme.of(context).colorScheme.secondaryContainer + ), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: _computeOtherBudgets(context, state.otherBudgets), + ), + ) + ), + Expanded( + child: AspectRatio( + aspectRatio: 1.3, + child: RadarChart( + RadarChartData( + titlePositionPercentageOffset: 0.15, + tickBorderData: BorderSide(color: Theme.of(context).colorScheme.secondary, width: 1), + gridBorderData: BorderSide(color: Theme.of(context).colorScheme.secondary, width: 2), + radarBorderData: BorderSide(color: Theme.of(context).colorScheme.secondary, width: 3), + radarBackgroundColor: Theme.of(context).colorScheme.secondaryContainer.withAlpha(100), + radarShape: RadarShape.circle, + dataSets: _computeDataSet(context, state.budgets, state.compareBudgets), + getTitle: (index, angle) => _computeDataSetTitle(index, state.compareBudgets), + ) + ), + ) + ), + ], + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/pages/budgets/widgets/budget_maker.dart b/lib/pages/budgets/widgets/budget_maker.dart new file mode 100644 index 0000000..50fc3fa --- /dev/null +++ b/lib/pages/budgets/widgets/budget_maker.dart @@ -0,0 +1,133 @@ +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/domains/category/category_bloc.dart'; +import 'package:tunas/pages/common/titled_container.dart'; +import 'package:tunas/repositories/metadata/models/budget.dart'; + +class BudgetMaker extends StatelessWidget { + const BudgetMaker({super.key}); + + List _computeBudgetLines(BuildContext context, List budgets, double initialBudget, double remainingBudget) { + final categoryState = context.watch().state; + List list = []; + + list.add( + Container( + margin: const EdgeInsets.only(top: 5, bottom: 5), + padding: const EdgeInsets.only(bottom: 10), + child: Flex( + direction: Axis.horizontal, + children: [Expanded( + child: + TextFormField( + keyboardType: const TextInputType.numberWithOptions(decimal: false), + decoration: InputDecoration( + icon: const Icon(Icons.euro), + hintText: '\$\$\$', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + ), + ), + initialValue: initialBudget.toString(), + onChanged: (value) => context.read().add(BudgetSetInitial(double.parse(value))) + ) + + )] + )) + ); + + list.addAll(budgets.map((budget) => Row( + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: DropdownButtonFormField( + value: budget.label, + onChanged: (value) => context.read().add(BudgetSetLabel(budget, value!)), + items: categoryState.categories.map((e) => DropdownMenuItem(value: e.label, child: Text(e.label))).toList(), + ), + ), + const SizedBox(width: 20), + Text( + NumberFormat('####000 €', 'fr_FR').format(budget.value), + style: const TextStyle( + fontFamily: 'NovaMono', + fontWeight: FontWeight.bold, + fontSize: 15, + ) + ), + ], + ) + ), + Expanded( + child: SliderTheme( + data: const 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.round().toDouble())), + ), + ) + ), + IconButton( + onPressed: () => context.read().add(BudgetRemove(budget)), + icon: const Icon(Icons.delete), + ), + ], + )).toList()); + + list.add( + Container( + margin: const EdgeInsets.only(top: 25, bottom: 5), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 2, + color: Theme.of(context).colorScheme.onPrimaryContainer + ) + ) + ), + ) + ); + + list.add( + Row( + children: [ + Text( + ' Remaining ${NumberFormat('#####00 €', 'fr_FR').format(remainingBudget)} out of ${NumberFormat('#####00 €', 'fr_FR').format(initialBudget)}', + style: const TextStyle( + fontFamily: 'NovaMono', + fontWeight: FontWeight.bold, + fontSize: 15, + ) + ) + ], + ) + ); + return list; + } + + @override + Widget build(BuildContext context) { + final categoryState = context.watch().state; + return BlocBuilder( + builder: (context, state) => TitledContainer( + title: 'Prepare', + action: IconButton( + onPressed: () => context.read().add(BudgetAdd(categoryState.categories.first.label)), + icon: const Icon(Icons.add), + ), + child: Column( + children: _computeBudgetLines(context, state.budgets, state.initialBudget, state.remainingBudget), + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/pages/budgets/widgets/month_distribution.dart b/lib/pages/budgets/widgets/month_distribution.dart index 5e4fbe8..ff070a9 100644 --- a/lib/pages/budgets/widgets/month_distribution.dart +++ b/lib/pages/budgets/widgets/month_distribution.dart @@ -1,102 +1,20 @@ -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'; +import 'package:tunas/pages/budgets/widgets/budget_comparator.dart'; +import 'package:tunas/pages/budgets/widgets/budget_maker.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 BlocBuilder( - builder: (context, state) => Column( + builder: (context, state) => const 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'), - ], - ), - ), - 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) - ), - ], - ), - ], - ) - ) - ], - ), - ), + BudgetMaker(), + BudgetComparator(), ], ) ); diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 5c15120..31fe6a9 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -93,7 +93,7 @@ class HomePage extends StatelessWidget { BlocProvider(create: (context) => AccountBloc(metadataRepository: RepositoryProvider.of(context), transactionsRepository: RepositoryProvider.of(context))), BlocProvider(create: (context) => TransactionBloc(transactionsRepository: RepositoryProvider.of(context))), BlocProvider(create: (context) => CategoryBloc(metadataRepository: RepositoryProvider.of(context), transactionsRepository: RepositoryProvider.of(context))), - BlocProvider(create: (context) => BudgetBloc(metadataRepository: RepositoryProvider.of(context))), + BlocProvider(create: (context) => BudgetBloc(metadataRepository: RepositoryProvider.of(context), transactionsRepository: RepositoryProvider.of(context))), BlocProvider(create: (context) => ChartBloc(metadataRepository: RepositoryProvider.of(context), transactionsRepository: RepositoryProvider.of(context))), ], child: DefaultTabController( diff --git a/lib/pages/transactions/widgets/transactions_header.dart b/lib/pages/transactions/widgets/transactions_header.dart index cf0b5e5..ef744db 100644 --- a/lib/pages/transactions/widgets/transactions_header.dart +++ b/lib/pages/transactions/widgets/transactions_header.dart @@ -60,6 +60,7 @@ class TransactionsHeader extends StatelessWidget { decoration: BoxDecoration( border: Border( bottom: BorderSide( + width: 2, color: Theme.of(context).colorScheme.onPrimaryContainer ) ) diff --git a/lib/repositories/json/json_repository.dart b/lib/repositories/json/json_repository.dart index 0876bc6..f1516ab 100644 --- a/lib/repositories/json/json_repository.dart +++ b/lib/repositories/json/json_repository.dart @@ -1,19 +1,31 @@ +import 'dart:async'; import 'dart:convert'; -import 'package:tunas/clients/storage/storage_client.dart'; +import 'package:tunas/clients/storage/json_storage_client.dart'; import 'package:tunas/repositories/json/models/json.dart'; class JsonRepository { String accountFile = 'tunas_main_account.json'; - final StorageClient _storageClient; + final JsonStorageClient _storageClient; + + Map saveTimerMap = {}; JsonRepository({ required storageClient, }) : _storageClient = storageClient; - saveJson(Json json) async { - await _storageClient.save(json.getJsonFileName(), jsonEncode(json.toJson())); + void saveJson(Json json) { + Timer? saveTimer = saveTimerMap[json.getJsonFileName()]; + + if (saveTimer != null) { + saveTimer.cancel(); + } + + saveTimer = Timer(const Duration(milliseconds: 500), () { + saveTimerMap.remove(json.getJsonFileName()); + _storageClient.save(json.getJsonFileName(), jsonEncode(json.toJson())); + }); } Future loadJson(Json json, JsonFactory jsonFactory) async { diff --git a/lib/repositories/metadata/metadata_repository.dart b/lib/repositories/metadata/metadata_repository.dart index 445e7c5..876d9d4 100644 --- a/lib/repositories/metadata/metadata_repository.dart +++ b/lib/repositories/metadata/metadata_repository.dart @@ -45,37 +45,37 @@ class MetadataRepository { _broadcastMetadata(metadata); } - Future> saveCategories(List categories) async { + List saveCategories(List categories) { Metadata metadata = _constructMetadataFromControllers(); metadata.categories = categories; - await _jsonRepository.saveJson(metadata); + _jsonRepository.saveJson(metadata); _categoriesController.add(categories); return categories; } - Future> saveBudgets(List budgets) async { + List saveBudgets(List budgets) { Metadata metadata = _constructMetadataFromControllers(); metadata.budgets = budgets; - await _jsonRepository.saveJson(metadata); + _jsonRepository.saveJson(metadata); _budgetController.add(budgets); return budgets; } - Future> saveAccounts(List accounts) async { + List saveAccounts(List accounts) { Metadata metadata = _constructMetadataFromControllers(); metadata.accounts = accounts; - await _jsonRepository.saveJson(metadata); + _jsonRepository.saveJson(metadata); _accountController.add(accounts); return accounts; } - deleteMetadata() async { + void deleteMetadata() { Metadata metadata = Metadata(); - await _jsonRepository.saveJson(metadata); + _jsonRepository.saveJson(metadata); _broadcastMetadata(metadata); } - _broadcastMetadata(Metadata metadata) { + void _broadcastMetadata(Metadata metadata) { _categoriesController.add(metadata.categories); _budgetController.add(metadata.budgets); _accountController.add(metadata.accounts); diff --git a/lib/repositories/transactions/transactions_repository.dart b/lib/repositories/transactions/transactions_repository.dart index 3e9ac0f..741e10f 100644 --- a/lib/repositories/transactions/transactions_repository.dart +++ b/lib/repositories/transactions/transactions_repository.dart @@ -20,20 +20,20 @@ class TransactionsRepository { return _transactionsController.asBroadcastStream(); } - loadTransactions() async { + Future loadTransactions() async { Transactions transactions = await _jsonRepository.loadJson(Transactions(), TransactionsFactory()); _transactionsController.add(transactions.transactions); } - saveTransactions(List transactionsList) async { + void saveTransactions(List transactionsList) { Transactions transactions = Transactions(transactions: transactionsList); - await _jsonRepository.saveJson(transactions); + _jsonRepository.saveJson(transactions); _transactionsController.add(transactionsList); } - deleteTransactions() async { + void deleteTransactions() { Transactions transactions = Transactions(); - await _jsonRepository.saveJson(transactions); + _jsonRepository.saveJson(transactions); _transactionsController.add([]); } } \ No newline at end of file