From 844b8d48c4b33d456ded93b6b01bb78c17ce9eb9 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 15 Aug 2025 07:44:14 +0200 Subject: [PATCH] Update category overview to be multi-currency aware. --- .../V1/Controllers/Chart/BudgetController.php | 36 +++++----- .../Controllers/Chart/CategoryController.php | 65 ++++++++++++------- .../Http/Api/AccountBalanceGrouped.php | 2 + app/Support/Http/Api/CleansChartData.php | 5 +- routes/api.php | 4 +- 5 files changed, 71 insertions(+), 41 deletions(-) diff --git a/app/Api/V1/Controllers/Chart/BudgetController.php b/app/Api/V1/Controllers/Chart/BudgetController.php index e58d657d5b..59add4b2af 100644 --- a/app/Api/V1/Controllers/Chart/BudgetController.php +++ b/app/Api/V1/Controllers/Chart/BudgetController.php @@ -50,7 +50,7 @@ class BudgetController extends Controller use CleansChartData; use ValidatesUserGroupTrait; - protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; + protected array $acceptedRoles = [UserRoleEnum::READ_ONLY]; protected OperationsRepositoryInterface $opsRepository; private BudgetLimitRepositoryInterface $blRepository; @@ -81,15 +81,15 @@ class BudgetController extends Controller * * @throws FireflyException */ - public function dashboard(DateRequest $request): JsonResponse + public function overview(DateRequest $request): JsonResponse { - $params = $request->getAll(); + $params = $request->getAll(); /** @var Carbon $start */ - $start = $params['start']; + $start = $params['start']; /** @var Carbon $end */ - $end = $params['end']; + $end = $params['end']; // code from FrontpageChartGenerator, but not in separate class $budgets = $this->repository->getActiveBudgets(); @@ -116,12 +116,12 @@ class BudgetController extends Controller $expenses = $this->processExpenses($budget->id, $spent, $start, $end); /** - * @var int $currencyId + * @var int $currencyId * @var array $row */ foreach ($expenses as $currencyId => $row) { // budgeted, left and overspent are now 0. - $limit = $this->filterLimit($currencyId, $limits); + $limit = $this->filterLimit($currencyId, $limits); if (null !== $limit) { $row['budgeted'] = $limit->amount; $row['left'] = bcsub($row['budgeted'], bcmul($row['spent'], '-1')); @@ -140,7 +140,7 @@ class BudgetController extends Controller // } // is always an array - $return = []; + $return = []; foreach ($rows as $row) { $current = [ 'label' => $budget->name, @@ -149,14 +149,20 @@ class BudgetController extends Controller 'currency_name' => $row['currency_name'], 'currency_decimal_places' => $row['currency_decimal_places'], 'period' => null, - 'start' => $row['start'], - 'end' => $row['end'], + 'date' => $row['start'], + 'start_date' => $row['start'], + 'end_date' => $row['end'], + 'yAxisID' => 0, + 'type' => 'bar', 'entries' => [ 'budgeted' => $row['budgeted'], 'spent' => $row['spent'], 'left' => $row['left'], 'overspent' => $row['overspent'], ], + 'pc_entries' => [ + + ], ]; $return[] = $current; } @@ -191,7 +197,7 @@ class BudgetController extends Controller * This array contains the expenses in this budget. Grouped per currency. * The grouping is on the main currency only. * - * @var int $currencyId + * @var int $currencyId * @var array $block */ foreach ($spent as $currencyId => $block) { @@ -209,7 +215,7 @@ class BudgetController extends Controller 'left' => '0', 'overspent' => '0', ]; - $currentBudgetArray = $block['budgets'][$budgetId]; + $currentBudgetArray = $block['budgets'][$budgetId]; // var_dump($return); /** @var array $journal */ @@ -250,7 +256,7 @@ class BudgetController extends Controller private function processLimit(Budget $budget, BudgetLimit $limit): array { Log::debug(sprintf('Created new ExchangeRateConverter in %s', __METHOD__)); - $end = clone $limit->end_date; + $end = clone $limit->end_date; $end->endOfDay(); $spent = $this->opsRepository->listExpenses($limit->start_date, $end, null, new Collection([$budget])); $limitCurrencyId = $limit->transaction_currency_id; @@ -258,8 +264,8 @@ class BudgetController extends Controller /** @var array $entry */ // only spent the entry where the entry's currency matches the budget limit's currency // so $filtered will only have 1 or 0 entries - $filtered = array_filter($spent, fn ($entry) => $entry['currency_id'] === $limitCurrencyId); - $result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end); + $filtered = array_filter($spent, fn($entry) => $entry['currency_id'] === $limitCurrencyId); + $result = $this->processExpenses($budget->id, $filtered, $limit->start_date, $end); if (1 === count($result)) { $compare = bccomp($limit->amount, (string)app('steam')->positive($result[$limitCurrencyId]['spent'])); $result[$limitCurrencyId]['budgeted'] = $limit->amount; diff --git a/app/Api/V1/Controllers/Chart/CategoryController.php b/app/Api/V1/Controllers/Chart/CategoryController.php index 94968d09cc..986f2f4042 100644 --- a/app/Api/V1/Controllers/Chart/CategoryController.php +++ b/app/Api/V1/Controllers/Chart/CategoryController.php @@ -34,6 +34,7 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\GroupCollectorInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Support\Facades\Steam; use FireflyIII\Support\Http\Api\CleansChartData; use FireflyIII\Support\Http\Api\ExchangeRateConverter; use FireflyIII\Support\Http\Api\ValidatesUserGroupTrait; @@ -77,10 +78,10 @@ class CategoryController extends Controller * * @SuppressWarnings("PHPMD.UnusedFormalParameter") */ - public function dashboard(DateRequest $request): JsonResponse + public function overview(DateRequest $request): JsonResponse { /** @var Carbon $start */ - $start = $this->parameters->get('start'); + $start = $this->parameters->get('start'); /** @var Carbon $end */ $end = $this->parameters->get('end'); @@ -91,11 +92,11 @@ class CategoryController extends Controller // get journals for entire period: /** @var GroupCollectorInterface $collector */ - $collector = app(GroupCollectorInterface::class); + $collector = app(GroupCollectorInterface::class); $collector->setRange($start, $end)->withAccountInformation(); $collector->setXorAccounts($accounts)->withCategoryInformation(); $collector->setTypes([TransactionTypeEnum::WITHDRAWAL->value, TransactionTypeEnum::RECONCILIATION->value]); - $journals = $collector->getExtractedJournals(); + $journals = $collector->getExtractedJournals(); /** @var array $journal */ foreach ($journals as $journal) { @@ -108,44 +109,62 @@ class CategoryController extends Controller $currencyCode = (string)$currency->code; $currencySymbol = (string)$currency->symbol; $currencyDecimalPlaces = (int)$currency->decimal_places; - $amount = app('steam')->positive($journal['amount']); + $amount = Steam::positive($journal['amount']); + $pcAmount = null; // overrule if necessary: + if ($this->convertToPrimary && $journalCurrencyId === $this->primaryCurrency->id) { + $pcAmount = $amount; + } if ($this->convertToPrimary && $journalCurrencyId !== $this->primaryCurrency->id) { $currencyId = (int)$this->primaryCurrency->id; $currencyName = (string)$this->primaryCurrency->name; $currencyCode = (string)$this->primaryCurrency->code; $currencySymbol = (string)$this->primaryCurrency->symbol; $currencyDecimalPlaces = (int)$this->primaryCurrency->decimal_places; - $convertedAmount = $converter->convert($currency, $this->primaryCurrency, $journal['date'], $amount); - Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->primaryCurrency->code, $convertedAmount)); - $amount = $convertedAmount; + $pcAmount = $converter->convert($currency, $this->primaryCurrency, $journal['date'], $amount); + Log::debug(sprintf('Converted %s %s to %s %s', $journal['currency_code'], $amount, $this->primaryCurrency->code, $pcAmount)); } - $categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category'); - $key = sprintf('%s-%s', $categoryName, $currencyCode); + $categoryName = $journal['category_name'] ?? (string)trans('firefly.no_category'); + $key = sprintf('%s-%s', $categoryName, $currencyCode); // create arrays $return[$key] ??= [ - 'label' => $categoryName, - 'currency_id' => (string)$currencyId, - 'currency_code' => $currencyCode, - 'currency_name' => $currencyName, - 'currency_symbol' => $currencySymbol, - 'currency_decimal_places' => $currencyDecimalPlaces, - 'period' => null, - 'start' => $start->toAtomString(), - 'end' => $end->toAtomString(), - 'amount' => '0', + 'label' => $categoryName, + 'currency_id' => (string)$currencyId, + 'currency_name' => $currencyName, + 'currency_code' => $currencyCode, + 'currency_symbol' => $currencySymbol, + 'currency_decimal_places' => $currencyDecimalPlaces, + 'primary_currency_id' => (string)$this->primaryCurrency->id, + 'primary_currency_name' => (string)$this->primaryCurrency->name, + 'primary_currency_code' => (string)$this->primaryCurrency->code, + 'primary_currency_symbol' => (string)$this->primaryCurrency->symbol, + 'primary_currency_decimal_places' => (int)$this->primaryCurrency->decimal_places, + 'period' => null, + 'start_date' => $start->toAtomString(), + 'end_date' => $end->toAtomString(), + 'yAxisID' => 0, + 'type' => 'bar', + 'entries' => [ + 'spent' => '0' + ], + 'pc_entries' => [ + 'spent' => '0' + ], ]; // add monies - $return[$key]['amount'] = bcadd($return[$key]['amount'], (string)$amount); + $return[$key]['entries']['spent'] = bcadd($return[$key]['entries']['spent'], (string)$amount); + if (null !== $pcAmount) { + $return[$key]['pc_entries']['spent'] = bcadd($return[$key]['pc_entries']['spent'], (string)$pcAmount); + } } - $return = array_values($return); + $return = array_values($return); // order by amount - usort($return, static fn (array $a, array $b) => (float)$a['amount'] < (float)$b['amount'] ? 1 : -1); + usort($return, static fn(array $a, array $b) => (float)$a['entries']['spent'] < (float)$b['entries']['spent'] ? 1 : -1); return response()->json($this->clean($return)); } diff --git a/app/Support/Http/Api/AccountBalanceGrouped.php b/app/Support/Http/Api/AccountBalanceGrouped.php index 4363e05db8..6d258df38c 100644 --- a/app/Support/Http/Api/AccountBalanceGrouped.php +++ b/app/Support/Http/Api/AccountBalanceGrouped.php @@ -80,6 +80,7 @@ class AccountBalanceGrouped 'start_date' => $this->start->toAtomString(), 'end_date' => $this->end->toAtomString(), 'yAxisID' => 0, + 'type' => 'line', 'period' => $this->preferredRange, 'entries' => [], 'pc_entries' => [], @@ -97,6 +98,7 @@ class AccountBalanceGrouped 'date' => $this->start->toAtomString(), 'start_date' => $this->start->toAtomString(), 'end_date' => $this->end->toAtomString(), + 'type' => 'line', 'yAxisID' => 0, 'period' => $this->preferredRange, 'entries' => [], diff --git a/app/Support/Http/Api/CleansChartData.php b/app/Support/Http/Api/CleansChartData.php index 747edc5103..702926d35c 100644 --- a/app/Support/Http/Api/CleansChartData.php +++ b/app/Support/Http/Api/CleansChartData.php @@ -61,7 +61,10 @@ trait CleansChartData if (array_key_exists('primary_currency_id', $array)) { $array['primary_currency_id'] = (string)$array['primary_currency_id']; } - $required = ['start_date', 'end_date', 'period', 'yAxisID']; + $required = [ + 'start_date', 'end_date', 'period', 'yAxisID','type','entries','pc_entries', + 'currency_id', 'primary_currency_id' + ]; foreach ($required as $field) { if (!array_key_exists($field, $array)) { throw new FireflyException(sprintf('Data-set "%s" is missing the "%s"-variable.', $index, $field)); diff --git a/routes/api.php b/routes/api.php index 6d39318506..2d346dce5b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -128,7 +128,7 @@ Route::group( 'as' => 'api.v1.chart.budget.', ], static function (): void { - Route::get('dashboard', ['uses' => 'BudgetController@dashboard', 'as' => 'dashboard']); + Route::get('overview', ['uses' => 'BudgetController@overview', 'as' => 'overview']); } ); @@ -139,7 +139,7 @@ Route::group( 'as' => 'api.v1.chart.category.', ], static function (): void { - Route::get('dashboard', ['uses' => 'CategoryController@dashboard', 'as' => 'dashboard']); + Route::get('overview', ['uses' => 'CategoryController@overview', 'as' => 'overview']); } );