diff --git a/app/Http/Controllers/Chart/BudgetController.php b/app/Http/Controllers/Chart/BudgetController.php index b68058ead3..fdb8f6b368 100644 --- a/app/Http/Controllers/Chart/BudgetController.php +++ b/app/Http/Controllers/Chart/BudgetController.php @@ -36,6 +36,7 @@ use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\NoBudgetRepositoryInterface; use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; use FireflyIII\Support\CacheProperties; +use FireflyIII\Support\Chart\Budget\FrontpageChartGenerator; use FireflyIII\Support\Http\Controllers\AugumentData; use FireflyIII\Support\Http\Controllers\DateCalculation; use Illuminate\Http\JsonResponse; @@ -411,6 +412,7 @@ class BudgetController extends Controller { $start = session('start', Carbon::now()->startOfMonth()); $end = session('end', Carbon::now()->endOfMonth()); + // chart properties for cache: $cache = new CacheProperties(); $cache->addProperty($start); @@ -419,59 +421,14 @@ class BudgetController extends Controller if ($cache->has()) { return response()->json($cache->get()); // @codeCoverageIgnore } - $budgets = $this->repository->getActiveBudgets(); - $chartData = [ - ['label' => (string) trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'], - ['label' => (string) trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'], - ['label' => (string) trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'], - ]; - /** @var Budget $budget */ - foreach ($budgets as $budget) { - $limits = $this->blRepository->getBudgetLimits($budget, $start, $end); - if (0 === $limits->count()) { - $spent = $this->opsRepository->sumExpenses($start, $end, null, new Collection([$budget]), null); - /** @var array $entry */ - foreach ($spent as $entry) { - $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); - $chartData[0]['entries'][$title] = bcmul($entry['sum'], '-1'); // spent - $chartData[1]['entries'][$title] = 0; // left to spend - $chartData[2]['entries'][$title] = 0; // overspent - } - } - if (0 !== $limits->count()) { - /** @var BudgetLimit $limit */ - foreach ($limits as $limit) { - $spent = $this->opsRepository->sumExpenses( - $limit->start_date, - $limit->end_date, - null, - new Collection([$budget]), - $limit->transactionCurrency - ); - /** @var array $entry */ - foreach ($spent as $entry) { - $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); - if ($limit->start_date->startOfDay()->ne($start->startOfDay()) || $limit->end_date->startOfDay()->ne($end->startOfDay())) { - $title = sprintf( - '%s (%s) (%s - %s)', - $budget->name, - $entry['currency_name'], - $limit->start_date->formatLocalized($this->monthAndDayFormat), - $limit->end_date->formatLocalized($this->monthAndDayFormat) - ); - } - $sumSpent = bcmul($entry['sum'], '-1'); // spent - $chartData[0]['entries'][$title] = 1 === bccomp($sumSpent, $limit->amount) ? $limit->amount : $sumSpent; - $chartData[1]['entries'][$title] = 1 === bccomp($limit->amount, $sumSpent) ? bcadd($entry['sum'], $limit->amount) - : '0'; - $chartData[2]['entries'][$title] = 1 === bccomp($limit->amount, $sumSpent) ? - '0' : bcmul(bcadd($entry['sum'], $limit->amount), '-1'); - } - } - } - } - $data = $this->generator->multiSet($chartData); + $generator = app(FrontpageChartGenerator::class); + $generator->setUser(auth()->user()); + $generator->setStart($start); + $generator->setEnd($end); + + $chartData = $generator->generate(); + $data = $this->generator->multiSet($chartData); $cache->store($data); return response()->json($data); diff --git a/app/Support/Chart/Budget/FrontpageChartGenerator.php b/app/Support/Chart/Budget/FrontpageChartGenerator.php new file mode 100644 index 0000000000..3cdaff86a4 --- /dev/null +++ b/app/Support/Chart/Budget/FrontpageChartGenerator.php @@ -0,0 +1,229 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Support\Chart\Budget; + +use Carbon\Carbon; +use FireflyIII\Models\Budget; +use FireflyIII\Models\BudgetLimit; +use FireflyIII\Repositories\Budget\BudgetLimitRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Budget\OperationsRepositoryInterface; +use FireflyIII\User; +use Illuminate\Support\Collection; + +/** + * Class FrontpageChartGenerator + */ +class FrontpageChartGenerator +{ + private User $user; + private Carbon $start; + private Carbon $end; + private BudgetRepositoryInterface $budgetRepository; + private BudgetLimitRepositoryInterface $blRepository; + protected OperationsRepositoryInterface $opsRepository; + private string $monthAndDayFormat; + + /** + * FrontpageChartGenerator constructor. + */ + public function __construct() + { + $this->budgetRepository = app(BudgetRepositoryInterface::class); + $this->blRepository = app(BudgetLimitRepositoryInterface::class); + $this->opsRepository = app(OperationsRepositoryInterface::class); + $this->monthAndDayFormat = ''; + } + + /** + * Generate the data for a budget chart. Collect all budgets and process each budget. + * + * @return array[] + */ + public function generate(): array + { + $budgets = $this->budgetRepository->getActiveBudgets(); + $data = [ + ['label' => (string) trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'], + ['label' => (string) trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'], + ['label' => (string) trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'], + ]; + + // loop al budgets: + /** @var Budget $budget */ + foreach ($budgets as $budget) { + $data = $this->processBudget($data, $budget); + } + + return $data; + } + + /** + * A basic setter for the user. Also updates the repositories with the right user. + * + * @param User $user + */ + public function setUser(User $user): void + { + $this->user = $user; + $this->budgetRepository->setUser($user); + $this->blRepository->setUser($user); + $this->opsRepository->setUser($user); + + $locale = app('steam')->getLocale(); + $this->monthAndDayFormat = (string) trans('config.month_and_day', [], $locale); + } + + /** + * @param Carbon $start + */ + public function setStart(Carbon $start): void + { + $this->start = $start; + } + + /** + * @param Carbon $end + */ + public function setEnd(Carbon $end): void + { + $this->end = $end; + } + + /** + * For each budget, gets all budget limits for the current time range. + * When no limits are present, the time range is used to collect information on money spent. + * If limits are present, each limit is processed individually. + * + * @param array $data + * @param Budget $budget + * @return array + */ + private function processBudget(array $data, Budget $budget): array + { + // get all limits: + $limits = $this->blRepository->getBudgetLimits($budget, $this->start, $this->end); + + // if no limits + if (0 === $limits->count()) { + return $this->noBudgetLimits($data, $budget); + } + + // if limits: + if (0 !== $limits->count()) { + return $this->budgetLimits($data, $budget, $limits); + } + return $data; + } + + /** + * When no limits are present, the expenses of the whole period are collected and grouped. + * This is grouped per currency. Because there is no limit set, "left to spend" and "overspent" are empty. + * + * @param array $data + * @param Budget $budget + * @return array + */ + private function noBudgetLimits(array $data, Budget $budget): array + { + $spent = $this->opsRepository->sumExpenses($this->start, $this->end, null, new Collection([$budget]), null); + /** @var array $entry */ + foreach ($spent as $entry) { + $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); + $data[0]['entries'][$title] = bcmul($entry['sum'], '-1'); // spent + $data[1]['entries'][$title] = 0; // left to spend + $data[2]['entries'][$title] = 0; // overspent + } + return $data; + } + + /** + * If a budget has budget limit, each limit is processed individually. + * + * @param array $data + * @param Budget $budget + * @param Collection $limits + * @return array + */ + private function budgetLimits(array $data, Budget $budget, Collection $limits): array + { + /** @var BudgetLimit $limit */ + foreach ($limits as $limit) { + $data = $this->processLimit($data, $budget, $limit); + } + return $data; + } + + /** + * For each limit, the expenses from the time range of the limit are collected. Each row from the result is processed individually. + * + * @param array $data + * @param Budget $budget + * @param BudgetLimit $limit + * @return array + */ + private function processLimit(array $data, Budget $budget, BudgetLimit $limit): array + { + $spent = $this->opsRepository->sumExpenses($limit->start_date, $limit->end_date, null, new Collection([$budget]), $limit->transactionCurrency); + /** @var array $entry */ + foreach ($spent as $entry) { + $data = $this->processRow($data, $budget, $limit, $entry); + } + return $data; + } + + /** + * Each row of expenses from a budget limit is in another currency (note $entry['currency_name']). + * + * Each one is added to the $data array. If the limit's date range is different from the global $start and $end dates, + * for example when a limit only partially falls into this month, the title is expanded to clarify. + * + * @param array $data + * @param Budget $budget + * @param BudgetLimit $limit + * @param array $entry + * @return array + */ + private function processRow(array $data, Budget $budget, BudgetLimit $limit, array $entry): array + { + $title = sprintf('%s (%s)', $budget->name, $entry['currency_name']); + if ($limit->start_date->startOfDay()->ne($this->start->startOfDay()) || $limit->end_date->startOfDay()->ne($this->end->startOfDay())) { + $title = sprintf( + '%s (%s) (%s - %s)', + $budget->name, + $entry['currency_name'], + $limit->start_date->formatLocalized($this->monthAndDayFormat), + $limit->end_date->formatLocalized($this->monthAndDayFormat) + ); + } + $sumSpent = bcmul($entry['sum'], '-1'); // spent + + $data[0]['entries'][$title] = 1 === bccomp($sumSpent, $limit->amount) ? $limit->amount : $sumSpent; // spent + $data[1]['entries'][$title] = 1 === bccomp($limit->amount, $sumSpent) ? bcadd($entry['sum'], $limit->amount) : '0'; // left to spent + $data[2]['entries'][$title] = 1 === bccomp($limit->amount, $sumSpent) ? '0' : bcmul(bcadd($entry['sum'], $limit->amount), '-1'); // overspent + + return $data; + } + + +} \ No newline at end of file