From e1c829f4fa3ff85c67db2c720dcdf4af52087aef Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 28 Aug 2018 05:21:23 +0200 Subject: [PATCH] Make some charts multi-currency. --- .../Chart/Basic/ChartJsGenerator.php | 40 ++++++++++++ .../Chart/Basic/GeneratorInterface.php | 7 ++ .../Controllers/Chart/AccountController.php | 2 - app/Http/Controllers/Chart/BillController.php | 34 ++++++---- app/Repositories/Bill/BillRepository.php | 64 +++++++++++++++++++ .../Bill/BillRepositoryInterface.php | 21 ++++++ app/Support/Steam.php | 7 ++ public/js/ff/charts.defaults.js | 14 ++++ public/js/ff/charts.js | 17 +++++ public/js/ff/index.js | 2 +- resources/lang/en_US/firefly.php | 4 +- routes/breadcrumbs.php | 2 +- 12 files changed, 196 insertions(+), 18 deletions(-) diff --git a/app/Generator/Chart/Basic/ChartJsGenerator.php b/app/Generator/Chart/Basic/ChartJsGenerator.php index 60bce88e62..28862b1706 100644 --- a/app/Generator/Chart/Basic/ChartJsGenerator.php +++ b/app/Generator/Chart/Basic/ChartJsGenerator.php @@ -140,6 +140,46 @@ class ChartJsGenerator implements GeneratorInterface return $chartData; } + /** + * Expects data as:. + * + * key => [value => x, 'currency_symbol' => 'x'] + * + * @param array $data + * + * @return array + */ + public function multiCurrencyPieChart(array $data): array + { + $chartData = [ + 'datasets' => [ + 0 => [], + ], + 'labels' => [], + ]; + + $amounts = array_column($data, 'amount'); + $next = next($amounts); + $sortFlag = SORT_ASC; + if (!\is_bool($next) && 1 === bccomp((string)$next, '0')) { + $sortFlag = SORT_DESC; + } + array_multisort($amounts, $sortFlag, $data); + unset($next, $sortFlag, $amounts); + + $index = 0; + foreach ($data as $key => $valueArray) { + // make larger than 0 + $chartData['datasets'][0]['data'][] = (float)app('steam')->positive((string)$valueArray['amount']); + $chartData['datasets'][0]['backgroundColor'][] = ChartColour::getColour($index); + $chartData['datasets'][0]['currency_symbol'][] = $valueArray['currency_symbol']; + $chartData['labels'][] = $key; + ++$index; + } + + return $chartData; + } + /** * Will generate a (ChartJS) compatible array from the given input. Expects this format:. * diff --git a/app/Generator/Chart/Basic/GeneratorInterface.php b/app/Generator/Chart/Basic/GeneratorInterface.php index 5cfdd4d94f..cc7d55626c 100644 --- a/app/Generator/Chart/Basic/GeneratorInterface.php +++ b/app/Generator/Chart/Basic/GeneratorInterface.php @@ -27,6 +27,13 @@ namespace FireflyIII\Generator\Chart\Basic; */ interface GeneratorInterface { + /** + * @param array $data + * + * @return array + */ + public function multiCurrencyPieChart(array $data): array; + /** * Will generate a Chart JS compatible array from the given input. Expects this format. * diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index 65f9034301..423151b2fa 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -329,8 +329,6 @@ class AccountController extends Controller /** * Shows the balances for all the user's frontpage accounts. * - * TODO this chart is not multi-currency aware. - * * @param AccountRepositoryInterface $repository * * @return JsonResponse diff --git a/app/Http/Controllers/Chart/BillController.php b/app/Http/Controllers/Chart/BillController.php index 0287184d7b..3472335bec 100644 --- a/app/Http/Controllers/Chart/BillController.php +++ b/app/Http/Controllers/Chart/BillController.php @@ -29,6 +29,7 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Bill; use FireflyIII\Models\Transaction; use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Http\JsonResponse; use Illuminate\Support\Collection; @@ -54,8 +55,6 @@ class BillController extends Controller /** * Shows all bills and whether or not they've been paid this month (pie chart). * - * TODO this chart is not multi-currency aware. - * * @param BillRepositoryInterface $repository * * @return JsonResponse @@ -69,17 +68,28 @@ class BillController extends Controller $cache->addProperty($end); $cache->addProperty('chart.bill.frontpage'); if ($cache->has()) { - return response()->json($cache->get()); // @codeCoverageIgnore + //return response()->json($cache->get()); // @codeCoverageIgnore + } + /** @var CurrencyRepositoryInterface $currencyRepository */ + $currencyRepository = app(CurrencyRepositoryInterface::class); + + $chartData = []; + $currencies = []; + $paid = $repository->getBillsPaidInRangePerCurrency($start, $end); // will be a negative amount. + $unpaid = $repository->getBillsUnpaidInRangePerCurrency($start, $end); // will be a positive amount. + + foreach ($paid as $currencyId => $amount) { + $currencies[$currencyId] = $currencies[$currencyId] ?? $currencyRepository->findNull($currencyId); + $label = (string)trans('firefly.paid_in_currency', ['currency' => $currencies[$currencyId]->name]); + $chartData[$label] = ['amount' => $amount, 'currency_symbol' => $currencies[$currencyId]->symbol]; + } + foreach ($unpaid as $currencyId => $amount) { + $currencies[$currencyId] = $currencies[$currencyId] ?? $currencyRepository->findNull($currencyId); + $label = (string)trans('firefly.unpaid_in_currency', ['currency' => $currencies[$currencyId]->name]); + $chartData[$label] = ['amount' => $amount, 'currency_symbol' => $currencies[$currencyId]->symbol]; } - $paid = $repository->getBillsPaidInRange($start, $end); // will be a negative amount. - $unpaid = $repository->getBillsUnpaidInRange($start, $end); // will be a positive amount. - $chartData = [ - (string)trans('firefly.unpaid') => $unpaid, - (string)trans('firefly.paid') => $paid, - ]; - - $data = $this->generator->pieChart($chartData); + $data = $this->generator->multiCurrencyPieChart($chartData); $cache->store($data); return response()->json($data); @@ -93,8 +103,6 @@ class BillController extends Controller * @param Bill $bill * * @return JsonResponse - * - * TODO this chart is not multi-currency aware. */ public function single(TransactionCollectorInterface $collector, Bill $bill): JsonResponse { diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index a7587fff83..9ede87e449 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -199,6 +199,36 @@ class BillRepository implements BillRepositoryInterface return $sum; } + /** + * Get the total amount of money paid for the users active bills in the date range given, + * grouped per currency. + * + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function getBillsPaidInRangePerCurrency(Carbon $start, Carbon $end): array + { + $bills = $this->getActiveBills(); + $return = []; + /** @var Bill $bill */ + foreach ($bills as $bill) { + /** @var Collection $set */ + $set = $bill->transactionJournals()->after($start)->before($end)->get(['transaction_journals.*']); + $currencyId = (int)$bill->transaction_currency_id; + if ($set->count() > 0) { + $journalIds = $set->pluck('id')->toArray(); + $amount = (string)Transaction::whereIn('transaction_journal_id', $journalIds)->where('amount', '<', 0)->sum('amount'); + $return[$currencyId] = $return[$currencyId] ?? '0'; + $return[$currencyId] = bcadd($amount, $return[$currencyId]); + Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f (currency %d)', $amount, $return[$currencyId], $currencyId)); + } + } + + return $return; + } + /** * Get the total amount of money due for the users active bills in the date range given. This amount will be positive. * @@ -231,6 +261,40 @@ class BillRepository implements BillRepositoryInterface return $sum; } + /** + * Get the total amount of money due for the users active bills in the date range given. + * + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function getBillsUnpaidInRangePerCurrency(Carbon $start, Carbon $end): array + { + $bills = $this->getActiveBills(); + $return = []; + /** @var Bill $bill */ + foreach ($bills as $bill) { + Log::debug(sprintf('Now at bill #%d (%s)', $bill->id, $bill->name)); + $dates = $this->getPayDatesInRange($bill, $start, $end); + $count = $bill->transactionJournals()->after($start)->before($end)->count(); + $total = $dates->count() - $count; + $currencyId = (int)$bill->transaction_currency_id; + + Log::debug(sprintf('Dates = %d, journalCount = %d, total = %d', $dates->count(), $count, $total)); + + if ($total > 0) { + $average = bcdiv(bcadd($bill->amount_max, $bill->amount_min), '2'); + $multi = bcmul($average, (string)$total); + $return[$currencyId] = $return[$currencyId] ?? '0'; + $return[$currencyId] = bcadd($return[$currencyId], $multi); + Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f (for currency %d)', $multi, $return[$currencyId], $currencyId)); + } + } + + return $return; + } + /** * Get all bills with these ID's. * diff --git a/app/Repositories/Bill/BillRepositoryInterface.php b/app/Repositories/Bill/BillRepositoryInterface.php index 3c9e1fa8f9..a1a8e6dfcb 100644 --- a/app/Repositories/Bill/BillRepositoryInterface.php +++ b/app/Repositories/Bill/BillRepositoryInterface.php @@ -88,6 +88,17 @@ interface BillRepositoryInterface */ public function getBillsPaidInRange(Carbon $start, Carbon $end): string; + /** + * Get the total amount of money paid for the users active bills in the date range given, + * grouped per currency. + * + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function getBillsPaidInRangePerCurrency(Carbon $start, Carbon $end): array; + /** * Get the total amount of money due for the users active bills in the date range given. * @@ -98,6 +109,16 @@ interface BillRepositoryInterface */ public function getBillsUnpaidInRange(Carbon $start, Carbon $end): string; + /** + * Get the total amount of money due for the users active bills in the date range given. + * + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function getBillsUnpaidInRangePerCurrency(Carbon $start, Carbon $end): array; + /** * Get all bills with these ID's. * diff --git a/app/Support/Steam.php b/app/Support/Steam.php index a30bcc7778..1f7999722f 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -163,6 +163,13 @@ class Steam $balances[$formatted] = $startBalance; $currencyId = (int)$repository->getMetaValue($account, 'currency_id'); + + // use system default currency: + if (0 === $currencyId) { + $currency = app('amount')->getDefaultCurrencyByUser($account->user); + $currencyId = $currency->id; + } + $start->addDay(); // query! diff --git a/public/js/ff/charts.defaults.js b/public/js/ff/charts.defaults.js index d299625005..7b502f3419 100644 --- a/public/js/ff/charts.defaults.js +++ b/public/js/ff/charts.defaults.js @@ -117,6 +117,20 @@ var defaultChartOptions = { } }; +var pieOptionsWithCurrency = { + tooltips: { + callbacks: { + label: function (tooltipItem, data) { + "use strict"; + var value = data.datasets[0].data[tooltipItem.index]; + return data.labels[tooltipItem.index] + ': ' + accounting.formatMoney(value, data.datasets[tooltipItem.datasetIndex].currency_symbol[tooltipItem.index]); + } + } + }, + maintainAspectRatio: true, + responsive: true +}; + var defaultPieOptions = { tooltips: { callbacks: { diff --git a/public/js/ff/charts.js b/public/js/ff/charts.js index e420ac849f..ba98bb07f4 100644 --- a/public/js/ff/charts.js +++ b/public/js/ff/charts.js @@ -257,6 +257,23 @@ function pieChart(URI, container) { } +/** + * + * @param URI + * @param container + */ +function multiCurrencyPieChart(URI, container) { + "use strict"; + + var colorData = false; + var options = $.extend(true, {}, pieOptionsWithCurrency); + var chartType = 'pie'; + + drawAChart(URI, container, chartType, options, colorData); + +} + + /** * * @param URI diff --git a/public/js/ff/index.js b/public/js/ff/index.js index d9cc68cf54..1a615fc672 100644 --- a/public/js/ff/index.js +++ b/public/js/ff/index.js @@ -32,7 +32,7 @@ function drawChart() { lineChart(accountFrontpageUri, 'accounts-chart'); if (billCount > 0) { - pieChart('chart/bill/frontpage', 'bills-chart'); + multiCurrencyPieChart('chart/bill/frontpage', 'bills-chart'); } stackedColumnChart('chart/budget/frontpage', 'budgets-chart'); columnChart('chart/category/frontpage', 'categories-chart'); diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 054b2bba07..ded64314f0 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -183,7 +183,9 @@ return [ 'scopes_will_be_able' => 'This application will be able to:', 'button_authorize' => 'Authorize', 'none_in_select_list' => '(none)', - 'name_in_currency' => ':name in :currency', + 'name_in_currency' => ':name in :currency', + 'paid_in_currency' => 'Paid in :currency', + 'unpaid_in_currency' => 'Unpaid in :currency', // check for updates: 'update_check_title' => 'Check for updates', diff --git a/routes/breadcrumbs.php b/routes/breadcrumbs.php index 64fb338054..764071d342 100644 --- a/routes/breadcrumbs.php +++ b/routes/breadcrumbs.php @@ -104,7 +104,7 @@ try { $what = config('firefly.shortNamesByFullName.' . $account->accountType->type); $breadcrumbs->parent('accounts.index', $what); - $breadcrumbs->push(limitStringLength($account->name), route('accounts.show', [$account->id])); + $breadcrumbs->push(limitStringLength($account->name), route('accounts.show.all', [$account->id])); if (null !== $start && null !== $end) { $title = trans( 'firefly.between_dates_breadcrumb',