diff --git a/app/Helpers/Report/BalanceReportHelper.php b/app/Helpers/Report/BalanceReportHelper.php index fdd7563ef5..3fd6c5417a 100644 --- a/app/Helpers/Report/BalanceReportHelper.php +++ b/app/Helpers/Report/BalanceReportHelper.php @@ -67,8 +67,10 @@ class BalanceReportHelper implements BalanceReportHelperInterface /** @var BudgetLimit $budgetLimit */ foreach ($budgetLimits as $budgetLimit) { - $line = $this->createBalanceLine($budgetLimit, $accounts); - $balance->addBalanceLine($line); + if (!is_null($budgetLimit->budget)) { + $line = $this->createBalanceLine($budgetLimit, $accounts); + $balance->addBalanceLine($line); + } } $noBudgetLine = $this->createNoBudgetLine($accounts, $start, $end); diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index 6f58fc6c13..20dc8e3311 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -198,7 +198,7 @@ class BudgetController extends Controller $inactive = $this->repository->getInactiveBudgets(); $periodStart = $start->formatLocalized($this->monthAndDayFormat); $periodEnd = $end->formatLocalized($this->monthAndDayFormat); - $budgetInformation = $this->collectBudgetInformation($budgets, $start, $end); + $budgetInformation = $this->repository->collectBudgetInformation($budgets, $start, $end); $defaultCurrency = Amount::getDefaultCurrency(); $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end); $spent = array_sum(array_column($budgetInformation, 'spent')); @@ -252,8 +252,8 @@ class BudgetController extends Controller * * @return View * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function noBudget(Request $request, JournalRepositoryInterface $repository, string $moment = '') { @@ -457,50 +457,6 @@ class BudgetController extends Controller return view('budgets.income', compact('available', 'start', 'end')); } - /** - * @param Collection $budgets - * @param Carbon $start - * @param Carbon $end - * - * @return array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - private function collectBudgetInformation(Collection $budgets, Carbon $start, Carbon $end): array - { - // get account information - /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); - $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH]); - $return = []; - /** @var Budget $budget */ - foreach ($budgets as $budget) { - $budgetId = $budget->id; - $return[$budgetId] = [ - 'spent' => $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end), - 'budgeted' => '0', - 'currentRep' => false, - ]; - $budgetLimits = $this->repository->getBudgetLimits($budget, $start, $end); - $otherLimits = new Collection; - - // get all the budget limits relevant between start and end and examine them: - /** @var BudgetLimit $limit */ - foreach ($budgetLimits as $limit) { - if ($limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end) - ) { - $return[$budgetId]['currentLimit'] = $limit; - $return[$budgetId]['budgeted'] = $limit->amount; - continue; - } - // otherwise it's just one of the many relevant repetitions: - $otherLimits->push($limit); - } - $return[$budgetId]['otherLimits'] = $otherLimits; - } - - return $return; - } - /** * @param Budget $budget * @param Carbon $start diff --git a/app/Http/Controllers/Json/BoxController.php b/app/Http/Controllers/Json/BoxController.php new file mode 100644 index 0000000000..7a40f50e81 --- /dev/null +++ b/app/Http/Controllers/Json/BoxController.php @@ -0,0 +1,194 @@ +startOfMonth()); + $end = session('end', Carbon::now()->endOfMonth()); + $today = new Carbon; + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty($today); + $cache->addProperty('box-available'); + if ($cache->has()) { + return Response::json($cache->get()); // @codeCoverageIgnore + } + // get available amount + $currency = app('amount')->getDefaultCurrency(); + $available = $repository->getAvailableBudget($currency, $start, $end); + + + // get spent amount: + $budgets = $repository->getActiveBudgets(); + $budgetInformation = $repository->collectBudgetInformation($budgets, $start, $end); + $spent = strval(array_sum(array_column($budgetInformation, 'spent'))); + $left = bcadd($available, $spent); + // left less than zero? then it's zero: + if (bccomp($left, '0') === -1) { + $left = '0'; + } + $days = $today->diffInDays($end); + $perDay = '0'; + if ($days !== 0) { + $perDay = bcdiv($left, strval($days)); + } + + $return = [ + 'perDay' => app('amount')->formatAnything($currency, $perDay, false), + 'left' => app('amount')->formatAnything($currency, $left, false), + ]; + + $cache->store($return); + + return Response::json($return); + } + + /** + * @return \Illuminate\Http\JsonResponse + */ + public function balance() + { + $start = session('start', Carbon::now()->startOfMonth()); + $end = session('end', Carbon::now()->endOfMonth()); + + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('box-balance'); + if ($cache->has()) { + return Response::json($cache->get()); // @codeCoverageIgnore + } + + // try a collector for income: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($start, $end) + ->setTypes([TransactionType::DEPOSIT]) + ->withOpposingAccount(); + $income = strval($collector->getJournals()->sum('transaction_amount')); + $currency = app('amount')->getDefaultCurrency(); + + // expense: + // try a collector for expenses: + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setAllAssetAccounts()->setRange($start, $end) + ->setTypes([TransactionType::WITHDRAWAL]) + ->withOpposingAccount(); + $expense = strval($collector->getJournals()->sum('transaction_amount')); + + $response = [ + 'income' => app('amount')->formatAnything($currency, $income, false), + 'expense' => app('amount')->formatAnything($currency, $expense, false), + 'combined' => app('amount')->formatAnything($currency, bcadd($income, $expense), false), + ]; + + $cache->store($response); + + return Response::json($response); + } + + /** + * @param BillRepositoryInterface $repository + * + * @return \Illuminate\Http\JsonResponse + */ + public function bills(BillRepositoryInterface $repository) + { + $start = session('start', Carbon::now()->startOfMonth()); + $end = session('end', Carbon::now()->endOfMonth()); + + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('box-bills'); + if ($cache->has()) { + return Response::json($cache->get()); // @codeCoverageIgnore + } + + /* + * Since both this method and the chart use the exact same data, we can suffice + * with calling the one method in the bill repository that will get this amount. + */ + $paidAmount = bcmul($repository->getBillsPaidInRange($start, $end), '-1'); + $unpaidAmount = $repository->getBillsUnpaidInRange($start, $end); // will be a positive amount. + $currency = app('amount')->getDefaultCurrency(); + + $return = [ + 'paid' => app('amount')->formatAnything($currency, $paidAmount, false), + 'unpaid' => app('amount')->formatAnything($currency, $unpaidAmount, false), + ]; + $cache->store($return); + + return Response::json($return); + } + + /** + * @param AccountRepositoryInterface $repository + * + * @return \Illuminate\Http\JsonResponse + */ + public function netWorth(AccountRepositoryInterface $repository) + { + $start = session('start', Carbon::now()->startOfMonth()); + $end = session('end', Carbon::now()->endOfMonth()); + + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('box-net-worth'); + if ($cache->has()) { + return Response::json($cache->get()); // @codeCoverageIgnore + } + $accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH]); + $currency = app('amount')->getDefaultCurrency(); + $balances = app('steam')->balancesByAccounts($accounts, new Carbon); + $sum = '0'; + foreach ($balances as $entry) { + $sum = bcadd($sum, $entry); + } + + $return = [ + 'net_worth' => app('amount')->formatAnything($currency, $sum, false), + ]; + + $cache->store($return); + + return Response::json($return); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/JsonController.php b/app/Http/Controllers/JsonController.php index 4805e620eb..4058e30cce 100644 --- a/app/Http/Controllers/JsonController.php +++ b/app/Http/Controllers/JsonController.php @@ -60,114 +60,6 @@ class JsonController extends Controller return Response::json(['html' => $view]); } - /** - * @param BillRepositoryInterface $repository - * - * @return \Symfony\Component\HttpFoundation\Response - */ - public function boxBillsPaid(BillRepositoryInterface $repository) - { - $start = session('start', Carbon::now()->startOfMonth()); - $end = session('end', Carbon::now()->endOfMonth()); - - /* - * Since both this method and the chart use the exact same data, we can suffice - * with calling the one method in the bill repository that will get this amount. - */ - $amount = $repository->getBillsPaidInRange($start, $end); // will be a negative amount. - $amount = bcmul($amount, '-1'); - $currency = Amount::getDefaultCurrency(); - - $data = ['box' => 'bills-paid', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; - - return Response::json($data); - } - - /** - * @param BillRepositoryInterface $repository - * - * @return \Illuminate\Http\JsonResponse - */ - public function boxBillsUnpaid(BillRepositoryInterface $repository) - { - $start = session('start', Carbon::now()->startOfMonth()); - $end = session('end', Carbon::now()->endOfMonth()); - $amount = $repository->getBillsUnpaidInRange($start, $end); // will be a positive amount. - $currency = Amount::getDefaultCurrency(); - $data = ['box' => 'bills-unpaid', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; - - return Response::json($data); - } - - /** - * @return \Illuminate\Http\JsonResponse - * @internal param AccountTaskerInterface $accountTasker - * @internal param AccountRepositoryInterface $repository - * - */ - public function boxIn() - { - $start = session('start', Carbon::now()->startOfMonth()); - $end = session('end', Carbon::now()->endOfMonth()); - - // works for json too! - $cache = new CacheProperties; - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('box-in'); - if ($cache->has()) { - return Response::json($cache->get()); // @codeCoverageIgnore - } - - // try a collector for income: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAllAssetAccounts()->setRange($start, $end) - ->setTypes([TransactionType::DEPOSIT]) - ->withOpposingAccount(); - - $amount = strval($collector->getJournals()->sum('transaction_amount')); - $currency = Amount::getDefaultCurrency(); - $data = ['box' => 'in', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; - $cache->store($data); - - return Response::json($data); - } - - /** - * @return \Symfony\Component\HttpFoundation\Response - * @internal param AccountTaskerInterface $accountTasker - * @internal param AccountRepositoryInterface $repository - * - */ - public function boxOut() - { - $start = session('start', Carbon::now()->startOfMonth()); - $end = session('end', Carbon::now()->endOfMonth()); - - // works for json too! - $cache = new CacheProperties; - $cache->addProperty($start); - $cache->addProperty($end); - $cache->addProperty('box-out'); - if ($cache->has()) { - return Response::json($cache->get()); // @codeCoverageIgnore - } - - // try a collector for expenses: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); - $collector->setAllAssetAccounts()->setRange($start, $end) - ->setTypes([TransactionType::WITHDRAWAL]) - ->withOpposingAccount(); - $amount = strval($collector->getJournals()->sum('transaction_amount')); - $currency = Amount::getDefaultCurrency(); - $data = ['box' => 'out', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; - $cache->store($data); - - return Response::json($data); - } - /** * @param BudgetRepositoryInterface $repository * diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 8754c70ca8..6d4db0c635 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -15,6 +15,7 @@ namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Models\AccountType; use FireflyIII\Models\AvailableBudget; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; @@ -22,6 +23,7 @@ use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; @@ -220,6 +222,53 @@ class BudgetRepository implements BudgetRepositoryInterface return $set; } + /** + * This method collects various info on budgets, used on the budget page and on the index. + * + * @param Collection $budgets + * @param Carbon $start + * @param Carbon $end + * + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function collectBudgetInformation(Collection $budgets, Carbon $start, Carbon $end): array + { + // get account information + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH]); + $return = []; + /** @var Budget $budget */ + foreach ($budgets as $budget) { + $budgetId = $budget->id; + $return[$budgetId] = [ + 'spent' => $this->spentInPeriod(new Collection([$budget]), $accounts, $start, $end), + 'budgeted' => '0', + 'currentRep' => false, + ]; + $budgetLimits = $this->getBudgetLimits($budget, $start, $end); + $otherLimits = new Collection; + + // get all the budget limits relevant between start and end and examine them: + /** @var BudgetLimit $limit */ + foreach ($budgetLimits as $limit) { + if ($limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end) + ) { + $return[$budgetId]['currentLimit'] = $limit; + $return[$budgetId]['budgeted'] = $limit->amount; + continue; + } + // otherwise it's just one of the many relevant repetitions: + $otherLimits->push($limit); + } + $return[$budgetId]['otherLimits'] = $otherLimits; + } + + return $return; + } + + /** * @param TransactionCurrency $currency * @param Carbon $start diff --git a/app/Repositories/Budget/BudgetRepositoryInterface.php b/app/Repositories/Budget/BudgetRepositoryInterface.php index eb522deb77..18520faf54 100644 --- a/app/Repositories/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetRepositoryInterface.php @@ -32,6 +32,18 @@ interface BudgetRepositoryInterface */ public function cleanupBudgets(): bool; + /** + * This method collects various info on budgets, used on the budget page and on the index. + * + * @param Collection $budgets + * @param Carbon $start + * @param Carbon $end + * + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function collectBudgetInformation(Collection $budgets, Carbon $start, Carbon $end): array; + /** * @param Budget $budget * diff --git a/public/js/ff/index.js b/public/js/ff/index.js index 58005bac67..97ea192975 100644 --- a/public/js/ff/index.js +++ b/public/js/ff/index.js @@ -28,10 +28,62 @@ function drawChart() { columnChart(accountExpenseUri, 'expense-accounts-chart'); columnChart(accountRevenueUri, 'revenue-accounts-chart'); + // get balance box: + getBalanceBox(); + getBillsBox(); + getAvailableBox(); + getNetWorthBox(); - getBoxAmounts(); + //getBoxAmounts(); } +function getNetWorthBox() { + // box-net-worth + $.getJSON('json/box/net-worth').done(function(data) { + $('#box-net-worth').html(data.net_worth); + }); +} + +/** + * + */ +function getAvailableBox() { + // box-left-to-spend + // box-left-per-day + $.getJSON('json/box/available').done(function(data) { + $('#box-left-to-spend').html(data.left); + $('#box-left-per-day').html(data.perDay); + }); +} + +/** + * + */ +function getBillsBox() { + // box-bills-unpaid + // box-bills-paid + $.getJSON('json/box/bills').done(function(data) { + $('#box-bills-paid').html(data.paid); + $('#box-bills-unpaid').html(data.unpaid); + }); +} + +/** + * + */ +function getBalanceBox() { + // box-balance-total + // box-balance-out + // box-balance-in + $.getJSON('json/box/balance').done(function(data) { + $('#box-balance-total').html(data.combined); + $('#box-balance-in').html(data.income); + $('#box-balance-out').html(data.expense); + }); +} + + + function getBoxAmounts() { "use strict"; var boxes = ['in', 'out', 'bills-unpaid', 'bills-paid']; diff --git a/resources/views/partials/boxes.twig b/resources/views/partials/boxes.twig index d7d27e2a11..e61840b9a9 100644 --- a/resources/views/partials/boxes.twig +++ b/resources/views/partials/boxes.twig @@ -6,8 +6,90 @@ {% set boxClasses = 'col-lg-4 col-md-4 col-sm-6 col-xs-12' %} {% endif %} - + + #} + + {# box that shows amount earned #} + {#
@@ -31,9 +117,43 @@
+ #} + + {# box for total net worth #} + {# +
+
+ + + + + +
+
+ #} + {# box for left to spend#} + {# +
+
+ + + + + +
+
+ #} + {# {% if billCount > 0 %}
+
@@ -46,6 +166,8 @@
+ +
@@ -59,4 +181,5 @@
{% endif %} + #} diff --git a/routes/web.php b/routes/web.php index c7e42eaccd..a6cbcdfc63 100755 --- a/routes/web.php +++ b/routes/web.php @@ -444,10 +444,12 @@ Route::group( Route::get('categories', ['uses' => 'JsonController@categories', 'as' => 'categories']); Route::get('budgets', ['uses' => 'JsonController@budgets', 'as' => 'budgets']); Route::get('tags', ['uses' => 'JsonController@tags', 'as' => 'tags']); - Route::get('box/in', ['uses' => 'JsonController@boxIn', 'as' => 'box.in']); - Route::get('box/out', ['uses' => 'JsonController@boxOut', 'as' => 'box.out']); - Route::get('box/bills-unpaid', ['uses' => 'JsonController@boxBillsUnpaid', 'as' => 'box.unpaid']); - Route::get('box/bills-paid', ['uses' => 'JsonController@boxBillsPaid', 'as' => 'box.paid']); + + Route::get('box/balance', ['uses' => 'Json\BoxController@balance', 'as' => 'box.balance']); + Route::get('box/bills', ['uses' => 'Json\BoxController@bills', 'as' => 'box.bills']); + Route::get('box/available', ['uses' => 'Json\BoxController@available', 'as' => 'box.available']); + Route::get('box/net-worth', ['uses' => 'Json\BoxController@netWorth', 'as' => 'box.net-worth']); + Route::get('transaction-journals/all', ['uses' => 'Json\AutoCompleteController@allTransactionJournals', 'as' => 'all-transaction-journals']); Route::get('transaction-journals/with-id/{tj}', ['uses' => 'Json\AutoCompleteController@journalsWithId', 'as' => 'journals-with-id']); Route::get('transaction-journals/{what}', ['uses' => 'Json\AutoCompleteController@transactionJournals', 'as' => 'transaction-journals']);