This commit is contained in:
James Cole 2016-11-20 18:31:29 +01:00
parent c56f937521
commit 8baea2feb9
No known key found for this signature in database
GPG Key ID: C16961E655E74B5E
12 changed files with 433 additions and 67 deletions

View File

@ -24,6 +24,15 @@ use Illuminate\Support\Collection;
*/
interface AccountChartGeneratorInterface
{
/**
* @param array $values
* @param array $names
*
* @return array
*/
public function pieChart(array $values, array $names): array;
/**
* @param Collection $accounts
* @param Carbon $start

View File

@ -14,6 +14,7 @@ namespace FireflyIII\Generator\Chart\Account;
use Carbon\Carbon;
use FireflyIII\Models\Account;
use FireflyIII\Support\ChartColour;
use Illuminate\Support\Collection;
/**
@ -83,6 +84,37 @@ class ChartJsAccountChartGenerator implements AccountChartGeneratorInterface
return $data;
}
/**
* @param array $values
* @param array $names
*
* @return array
*/
public function pieChart(array $values, array $names): array
{
$data = [
'datasets' => [
0 => [],
],
'labels' => [],
];
$index = 0;
foreach ($values as $categoryId => $value) {
// make larger than 0
if (bccomp($value, '0') === -1) {
$value = bcmul($value, '-1');
}
$data['datasets'][0]['data'][] = round($value, 2);
$data['datasets'][0]['backgroundColor'][] = ChartColour::getColour($index);
$data['labels'][] = $names[$categoryId];
$index++;
}
return $data;
}
/**
* @param Collection $accounts
* @param Carbon $start

View File

@ -259,8 +259,10 @@ class JournalCollector implements JournalCollectorInterface
$this->query->where(
function (EloquentBuilder $q) use ($categoryIds) {
$q->whereIn('category_transaction.category_id', $categoryIds);
$q->orWhereIn('category_transaction_journal.category_id', $categoryIds);
if (count($categoryIds) > 0) {
$q->whereIn('category_transaction.category_id', $categoryIds);
$q->orWhereIn('category_transaction_journal.category_id', $categoryIds);
}
}
);
@ -383,6 +385,27 @@ class JournalCollector implements JournalCollectorInterface
return $this;
}
/**
* @return JournalCollectorInterface
*/
public function withBudgetInformation(): JournalCollectorInterface
{
$this->joinBudgetTables();
return $this;
}
/**
* @return JournalCollectorInterface
*/
public function withCategoryInformation(): JournalCollectorInterface
{
$this->joinCategoryTables();
return $this;
}
/**
* @return JournalCollectorInterface
*/
@ -516,6 +539,8 @@ class JournalCollector implements JournalCollectorInterface
$this->joinedBudget = true;
$this->query->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id');
$this->query->leftJoin('budget_transaction', 'budget_transaction.transaction_id', '=', 'transactions.id');
$this->fields[] = 'budget_transaction_journal.budget_id as transaction_journal_budget_id';
$this->fields[] = 'budget_transaction.budget_id as transaction_budget_id';
}
}

View File

@ -131,6 +131,16 @@ interface JournalCollectorInterface
*/
public function setTypes(array $types): JournalCollectorInterface;
/**
* @return JournalCollectorInterface
*/
public function withBudgetInformation(): JournalCollectorInterface;
/**
* @return JournalCollectorInterface
*/
public function withCategoryInformation(): JournalCollectorInterface;
/**
* @return JournalCollectorInterface
*/

View File

@ -17,6 +17,7 @@ use Carbon\Carbon;
use ExpandedForm;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\JournalCollector;
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Http\Requests\AccountFormRequest;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
@ -209,7 +210,7 @@ class AccountController extends Controller
*
* @return View
*/
public function show(AccountTaskerInterface $tasker, ARI $repository, Account $account)
public function show(JournalCollectorInterface $collector, Account $account)
{
if ($account->accountType->type === AccountType::INITIAL_BALANCE) {
return $this->redirectToOriginalAccount($account);
@ -218,62 +219,20 @@ class AccountController extends Controller
$subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type);
$subTitle = $account->name;
$range = Preferences::get('viewRange', '1M')->data;
/** @var Carbon $start */
$start = session('start', Navigation::startOfPeriod(new Carbon, $range));
/** @var Carbon $end */
$end = session('end', Navigation::endOfPeriod(new Carbon, $range));
$page = intval(Input::get('page')) === 0 ? 1 : intval(Input::get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
$start = session('start', Navigation::startOfPeriod(new Carbon, $range));
$end = session('end', Navigation::endOfPeriod(new Carbon, $range));
$page = intval(Input::get('page')) === 0 ? 1 : intval(Input::get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
// replace with journal collector:
$collector = new JournalCollector(auth()->user());
// grab those journals:
$collector->setAccounts(new Collection([$account]))->setRange($start, $end)->setLimit($pageSize)->setPage($page);
$journals = $collector->getPaginatedJournals();
$journals->setPath('accounts/show/' . $account->id);
// grouped other months thing:
// oldest transaction in account:
$start = $repository->oldestJournalDate($account);
$range = Preferences::get('viewRange', '1M')->data;
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfX(new Carbon, $range);
$entries = new Collection;
// generate entries for each period (and cache those)
$entries = $this->periodEntries($account);
// chart properties for cache:
$cache = new CacheProperties;
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('account-show');
$cache->addProperty($account->id);
if ($cache->has()) {
$entries = $cache->get();
Log::debug('Entries are cached, return cache.');
return view('accounts.show', compact('account', 'what', 'entries', 'subTitleIcon', 'journals', 'subTitle'));
}
// only include asset accounts when this account is an asset:
$assets = new Collection;
if (in_array($account->accountType->type, [AccountType::ASSET, AccountType::DEFAULT])) {
$assets = $repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]);
}
Log::debug('Going to get period expenses and incomes.');
while ($end >= $start) {
$end = Navigation::startOfPeriod($end, $range);
$currentEnd = Navigation::endOfPeriod($end, $range);
$spent = $tasker->amountOutInPeriod(new Collection([$account]), $assets, $end, $currentEnd);
$earned = $tasker->amountInInPeriod(new Collection([$account]), $assets, $end, $currentEnd);
$dateStr = $end->format('Y-m-d');
$dateName = Navigation::periodShow($end, $range);
$entries->push([$dateStr, $dateName, $spent, $earned]);
$end = Navigation::subtractPeriod($end, $range, 1);
}
$cache->store($entries);
return view('accounts.show', compact('account', 'what', 'entries', 'subTitleIcon', 'journals', 'subTitle'));
return view('accounts.show', compact('account', 'what', 'entries', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end'));
}
/**
@ -318,7 +277,7 @@ class AccountController extends Controller
$journals = $collector->getPaginatedJournals();
$journals->setPath('accounts/show/' . $account->id . '/' . $date);
return view('accounts.show_with_date', compact('category', 'date', 'account', 'journals', 'subTitle', 'carbon'));
return view('accounts.show_with_date', compact('category', 'date', 'account', 'journals', 'subTitle', 'carbon', 'start', 'end'));
}
/**
@ -397,6 +356,63 @@ class AccountController extends Controller
return '';
}
/**
* This method returns "period entries", so nov-2015, dec-2015, etc etc (this depends on the users session range)
* and for each period, the amount of money spent and earned. This is a complex operation which is cached for
* performance reasons.
*
* @param Account $account The account involved.
*
* @return Collection
*/
private function periodEntries(Account $account): Collection
{
/** @var ARI $repository */
$repository = app(ARI::class);
/** @var AccountTaskerInterface $tasker */
$tasker = app(AccountTaskerInterface::class);
$start = $repository->oldestJournalDate($account);
$range = Preferences::get('viewRange', '1M')->data;
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfX(new Carbon, $range);
$entries = new Collection;
// properties for cache
$cache = new CacheProperties;
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('account-show-period-entries');
$cache->addProperty($account->id);
if ($cache->has()) {
Log::debug('Entries are cached, return cache.');
return $cache->get();
}
// only include asset accounts when this account is an asset:
$assets = new Collection;
if (in_array($account->accountType->type, [AccountType::ASSET, AccountType::DEFAULT])) {
$assets = $repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]);
}
Log::debug('Going to get period expenses and incomes.');
while ($end >= $start) {
$end = Navigation::startOfPeriod($end, $range);
$currentEnd = Navigation::endOfPeriod($end, $range);
$spent = $tasker->amountOutInPeriod(new Collection([$account]), $assets, $end, $currentEnd);
$earned = $tasker->amountInInPeriod(new Collection([$account]), $assets, $end, $currentEnd);
$dateStr = $end->format('Y-m-d');
$dateName = Navigation::periodShow($end, $range);
$entries->push([$dateStr, $dateName, $spent, $earned]);
$end = Navigation::subtractPeriod($end, $range, 1);
}
$cache->store($entries);
return $entries;
}
/**
* @param Account $account
*

View File

@ -17,10 +17,15 @@ use Carbon\Carbon;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Generator\Chart\Account\AccountChartGeneratorInterface;
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use Illuminate\Support\Collection;
use Log;
@ -99,6 +104,87 @@ class AccountController extends Controller
return Response::json($data);
}
/**
* @param JournalCollectorInterface $collector
* @param Account $account
* @param Carbon $start
* @param Carbon $end
*
* @return \Illuminate\Http\JsonResponse
*/
public function expenseByBudget(JournalCollectorInterface $collector, Account $account, Carbon $start, Carbon $end)
{
$cache = new CacheProperties;
$cache->addProperty($account->id);
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('expenseByBudget');
if ($cache->has()) {
return Response::json($cache->get());
}
// grab all journals:
$collector->setAccounts(new Collection([$account]))->setRange($start, $end)->withBudgetInformation()->setTypes([TransactionType::WITHDRAWAL]);
$transactions = $collector->getJournals();
$result = [];
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$jrnlBudgetId = intval($transaction->transaction_journal_budget_id);
$transBudgetId = intval($transaction->transaction_budget_id);
$budgetId = max($jrnlBudgetId, $transBudgetId);
$result[$budgetId] = $result[$budgetId] ?? '0';
$result[$budgetId] = bcadd($transaction->transaction_amount, $result[$budgetId]);
}
$names = $this->getBudgetNames(array_keys($result));
$data = $this->generator->pieChart($result, $names);
$cache->store($data);
return Response::json($data);
}
/**
* @param JournalCollectorInterface $collector
* @param Account $account
* @param Carbon $start
* @param Carbon $end
*
* @return \Illuminate\Http\JsonResponse
*/
public function expenseByCategory(JournalCollectorInterface $collector, Account $account, Carbon $start, Carbon $end)
{
$cache = new CacheProperties;
$cache->addProperty($account->id);
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('expenseByCategory');
if ($cache->has()) {
return Response::json($cache->get());
}
// grab all journals:
$collector->setAccounts(new Collection([$account]))->setRange($start, $end)->withCategoryInformation()->setTypes([TransactionType::WITHDRAWAL]);
$transactions = $collector->getJournals();
$result = [];
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$jrnlCatId = intval($transaction->transaction_journal_category_id);
$transCatId = intval($transaction->transaction_category_id);
$categoryId = max($jrnlCatId, $transCatId);
$result[$categoryId] = $result[$categoryId] ?? '0';
$result[$categoryId] = bcadd($transaction->transaction_amount, $result[$categoryId]);
}
$names = $this->getCategoryNames(array_keys($result));
$data = $this->generator->pieChart($result, $names);
$cache->store($data);
return Response::json($data);
}
/**
* Shows the balances for all the user's frontpage accounts.
*
@ -116,6 +202,43 @@ class AccountController extends Controller
return Response::json($this->accountBalanceChart($start, $end, $accounts));
}
/**
* @param Account $account
* @param Carbon $start
* @param Carbon $end
*/
public function incomeByCategory(JournalCollectorInterface $collector, Account $account, Carbon $start, Carbon $end)
{
$cache = new CacheProperties;
$cache->addProperty($account->id);
$cache->addProperty($start);
$cache->addProperty($end);
$cache->addProperty('incomeByCategory');
if ($cache->has()) {
return Response::json($cache->get());
}
// grab all journals:
$collector->setAccounts(new Collection([$account]))->setRange($start, $end)->withCategoryInformation()->setTypes([TransactionType::DEPOSIT]);
$transactions = $collector->getJournals();
$result = [];
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$jrnlCatId = intval($transaction->transaction_journal_category_id);
$transCatId = intval($transaction->transaction_category_id);
$categoryId = max($jrnlCatId, $transCatId);
$result[$categoryId] = $result[$categoryId] ?? '0';
$result[$categoryId] = bcadd($transaction->transaction_amount, $result[$categoryId]);
}
$names = $this->getCategoryNames(array_keys($result));
$data = $this->generator->pieChart($result, $names);
$cache->store($data);
return Response::json($data);
}
/**
* Shows the balances for a given set of dates and accounts.
*
@ -227,7 +350,6 @@ class AccountController extends Controller
return Response::json($data);
}
/**
* @param Account $account
* @param string $date
@ -319,4 +441,51 @@ class AccountController extends Controller
return $data;
}
/**
* @param array $budgetIds
*
* @return array
*/
private function getBudgetNames(array $budgetIds): array
{
/** @var BudgetRepositoryInterface $repository */
$repository = app(BudgetRepositoryInterface::class);
$budgets = $repository->getBudgets();
$grouped = $budgets->groupBy('id')->toArray();
$return = [];
foreach ($budgetIds as $budgetId) {
if (isset($grouped[$budgetId])) {
$return[$budgetId] = $grouped[$budgetId][0]['name'];
}
}
$return[0] = trans('firefly.no_budget');
return $return;
}
/**
* Small helper function for some of the charts.
*
* @param array $categoryIds
*
* @return array
*/
private function getCategoryNames(array $categoryIds): array
{
/** @var CategoryRepositoryInterface $repository */
$repository = app(CategoryRepositoryInterface::class);
$categories = $repository->getCategories();
$grouped = $categories->groupBy('id')->toArray();
$return = [];
foreach ($categoryIds as $categoryId) {
if (isset($grouped[$categoryId])) {
$return[$categoryId] = $grouped[$categoryId][0]['name'];
}
}
$return[0] = trans('firefly.noCategory');
return $return;
}
}

View File

@ -1,4 +1,4 @@
/* global $, lineChart, accountID, token */
/* global $, lineChart, accountID, token, incomeByCategoryUri, expenseByCategoryUri, expenseByBudgetUri */
// Return a helper with preserved width of cells
@ -15,10 +15,11 @@ var fixHelper = function (e, tr) {
$(function () {
"use strict";
if (typeof(lineChart) === "function" && typeof accountID !== 'undefined') {
lineChart('chart/account/' + accountID, 'overview-chart');
}
pieChart(incomeByCategoryUri, 'account-cat-in');
pieChart(expenseByCategoryUri, 'account-cat-out');
pieChart(expenseByBudgetUri, 'account-budget-out');
// sortable!
if (typeof $(".sortable-table tbody").sortable !== "undefined") {

View File

@ -6,7 +6,7 @@
* of the MIT license. See the LICENSE file for details.
*/
/* global $, lineChart, dateString, accountID, token */
/* global $, lineChart, dateString, accountID, token, incomeByCategoryUri, expenseByCategoryUri, expenseByBudgetUri */
// Return a helper with preserved width of cells
@ -23,13 +23,13 @@ var fixHelper = function (e, tr) {
$(function () {
"use strict";
if (typeof(lineChart) === "function" &&
typeof accountID !== 'undefined' &&
typeof dateString !== 'undefined'
) {
lineChart('chart/account/' + accountID + '/' + dateString, 'period-specific-account');
}
lineChart('chart/account/' + accountID + '/' + dateString, 'period-specific-account');
pieChart(incomeByCategoryUri, 'account-cat-in');
pieChart(expenseByCategoryUri, 'account-cat-out');
pieChart(expenseByBudgetUri, 'account-budget-out');
// sortable!
if (typeof $(".sortable-table tbody").sortable !== "undefined") {

View File

@ -87,6 +87,9 @@ return [
'need_more_help' => 'If you need more help using Firefly III, please <a href="https://github.com/JC5/firefly-iii/issues">open a ticker on Github</a>.',
'nothing_to_display' => 'There are no transactions to show you',
'show_all_no_filter' => 'Show all transactions without grouping them by date.',
'expenses_by_category' => 'Expenses by category',
'expenses_by_budget' => 'Expenses by budget',
'income_by_category' => 'Income by category',
// repeat frequencies:
'repeat_freq_yearly' => 'yearly',

View File

@ -28,7 +28,44 @@
</div>
</div>
</div>
<div class="row">
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'expenses_by_category'|_ }}</h3>
</div>
<div class="box-body">
<div style="width:60%;margin:0 auto;">
<canvas id="account-cat-out" style="margin:0 auto;" height="150" width="150"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'expenses_by_budget'|_ }}</h3>
</div>
<div class="box-body">
<div style="width:60%;margin:0 auto;">
<canvas id="account-budget-out" style="margin:0 auto;" height="150" width="150"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'income_by_category'|_ }}</h3>
</div>
<div class="box-body">
<div style="width:60%;margin:0 auto;">
<canvas id="account-cat-in" style="margin:0 auto;" height="150" width="150"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-10 col-md-10 col-sm-12">
@ -85,7 +122,13 @@
{% block scripts %}
<script type="text/javascript">
var accountID = {{ account.id }};
// uri's for charts:
var incomeByCategoryUri = '{{ route('chart.account.incomeByCategory', [account.id, start.format('Ymd'), end.format('Ymd')]) }}';
var expenseByCategoryUri = '{{ route('chart.account.expenseByCategory', [account.id, start.format('Ymd'), end.format('Ymd')]) }}';
var expenseByBudgetUri = '{{ route('chart.account.expenseByBudget', [account.id, start.format('Ymd'), end.format('Ymd')]) }}';
</script>
<script type="text/javascript" src="js/lib/Chart.bundle.min.js"></script>
<script type="text/javascript" src="js/ff/charts.defaults.js"></script>
<script type="text/javascript" src="js/ff/charts.js"></script>

View File

@ -32,6 +32,45 @@
</div>
</div>
<div class="row">
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'expenses_by_category'|_ }}</h3>
</div>
<div class="box-body">
<div style="width:60%;margin:0 auto;">
<canvas id="account-cat-out" style="margin:0 auto;" height="150" width="150"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'expenses_by_budget'|_ }}</h3>
</div>
<div class="box-body">
<div style="width:60%;margin:0 auto;">
<canvas id="account-budget-out" style="margin:0 auto;" height="150" width="150"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 col-sm-12 col-xs-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'income_by_category'|_ }}</h3>
</div>
<div class="box-body">
<div style="width:60%;margin:0 auto;">
<canvas id="account-cat-in" style="margin:0 auto;" height="150" width="150"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<div class="box">
@ -53,6 +92,12 @@
<script type="text/javascript">
var accountID = {{ account.id }};
var dateString = "{{ date }}";
// uri's for charts:
var incomeByCategoryUri = '{{ route('chart.account.incomeByCategory', [account.id, start.format('Ymd'), end.format('Ymd')]) }}';
var expenseByCategoryUri = '{{ route('chart.account.expenseByCategory', [account.id, start.format('Ymd'), end.format('Ymd')]) }}';
var expenseByBudgetUri = '{{ route('chart.account.expenseByBudget', [account.id, start.format('Ymd'), end.format('Ymd')]) }}';
</script>
<script type="text/javascript" src="js/lib/Chart.bundle.min.js"></script>
<script type="text/javascript" src="js/ff/charts.defaults.js"></script>

View File

@ -190,6 +190,19 @@ Route::group(
Route::get('/chart/account/{account}', ['uses' => 'Chart\AccountController@single']);
Route::get('/chart/account/{account}/{date}', ['uses' => 'Chart\AccountController@specificPeriod']);
Route::get(
'/chart/account/income-by-category/{account}/{start_date}/{end_date}',
['uses' => 'Chart\AccountController@incomeByCategory', 'as' => 'chart.account.incomeByCategory']
);
Route::get(
'/chart/account/expense-by-category/{account}/{start_date}/{end_date}',
['uses' => 'Chart\AccountController@expenseByCategory', 'as' => 'chart.account.expenseByCategory']
);
Route::get(
'/chart/account/expense-by-budget/{account}/{start_date}/{end_date}',
['uses' => 'Chart\AccountController@expenseByBudget', 'as' => 'chart.account.expenseByBudget']
);
// bills:
Route::get('/chart/bill/frontpage', ['uses' => 'Chart\BillController@frontpage']);