From 871509d4d33b8513939a7eff562ebde5c0848e6f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 23 Aug 2014 08:34:22 +0200 Subject: [PATCH] First effort towards nice charts in the budgeting overviews. --- app/assets/javascripts/budgets.js | 14 + app/assets/javascripts/firefly/accounts.js | 2 +- app/assets/javascripts/firefly/budgets.js | 93 +++++++ app/controllers/ChartController.php | 283 +++++++++++++++++++++ app/models/TransactionJournal.php | 68 +++-- app/routes.php | 23 +- app/views/budgets/show.blade.php | 24 +- 7 files changed, 466 insertions(+), 41 deletions(-) create mode 100644 app/assets/javascripts/budgets.js create mode 100644 app/assets/javascripts/firefly/budgets.js diff --git a/app/assets/javascripts/budgets.js b/app/assets/javascripts/budgets.js new file mode 100644 index 0000000000..c6da4f622d --- /dev/null +++ b/app/assets/javascripts/budgets.js @@ -0,0 +1,14 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear in whatever order it +// gets included (e.g. say you have require_tree . then the code will appear after all the directories +// but before any files alphabetically greater than 'application.js' +// +// The available directives right now are require, require_directory, and require_tree +// +//= require_tree highcharts +//= require firefly/budgets diff --git a/app/assets/javascripts/firefly/accounts.js b/app/assets/javascripts/firefly/accounts.js index 63efd1db42..e4b685fbe6 100644 --- a/app/assets/javascripts/firefly/accounts.js +++ b/app/assets/javascripts/firefly/accounts.js @@ -7,7 +7,7 @@ if($('#chart').length == 1) { var options = { chart: { renderTo: 'chart', - type: 'line' + type: 'spline' }, series: data.series, diff --git a/app/assets/javascripts/firefly/budgets.js b/app/assets/javascripts/firefly/budgets.js new file mode 100644 index 0000000000..26bac723e7 --- /dev/null +++ b/app/assets/javascripts/firefly/budgets.js @@ -0,0 +1,93 @@ +$(function () { + if ($('#chart').length == 1) { + chartType = $('#instr').data('type'); + + + + if(chartType == 'envelope') { + var envelopeId = $('#instr').data('envelope'); + var URL = 'chart/budget/envelope/' + envelopeId; + } + if(chartType == 'no_envelope') { + var budgetId = $('#instr').data('budget'); + var URL = 'chart/budget/'+budgetId+'/no_envelope'; + } + if(chartType == 'session') { + var budgetId = $('#instr').data('budget'); + var URL = 'chart/budget/'+budgetId+'/session'; + } + if(chartType == 'default') { + var budgetId = $('#instr').data('budget'); + var URL = 'chart/budget/'+budgetId+'/default'; + } + + // go do something with this URL. + $.getJSON(URL).success(function (data) { + var options = { + chart: { + renderTo: 'chart', + }, + + series: data.series, + title: { + text: data.chart_title + }, + yAxis: [{ // Primary yAxis + labels: { + format: '€ {value}', + style: { + color: Highcharts.getOptions().colors[1] + } + } + }, { // Secondary yAxis + title: { + style: { + color: Highcharts.getOptions().colors[0] + } + }, + opposite: true + }], + subtitle: { + text: data.subtitle, + useHTML: true + }, + + xAxis: { + floor: 0, + type: 'datetime', + dateTimeLabelFormats: { + day: '%e %b', + year: '%b' + }, + title: { + text: 'Date' + } + }, + plotOptions: { + line: { + shadow: true + }, + series: { + cursor: 'pointer', + negativeColor: '#FF0000', + threshold: 0, + lineWidth: 1, + marker: { + radius: 2 + }, + } + }, + credits: { + enabled: false + } + }; + $('#chart').highcharts(options); + + + + }); + + } + + +}); \ No newline at end of file diff --git a/app/controllers/ChartController.php b/app/controllers/ChartController.php index 48fff80efc..e93f9ef288 100644 --- a/app/controllers/ChartController.php +++ b/app/controllers/ChartController.php @@ -24,6 +24,289 @@ class ChartController extends BaseController $this->_accounts = $accounts; } + /** + * + */ + public function budgetDefault(\Budget $budget) + { + $expense = []; + $left = []; + // get all limit repetitions for this budget. + /** @var \Limit $limit */ + foreach ($budget->limits as $limit) { + /** @var \LimitRepetition $rep */ + foreach ($limit->limitrepetitions as $rep) { + $spentInRep = \Transaction:: + leftJoin( + 'transaction_journals', 'transaction_journals.id', '=', + 'transactions.transaction_journal_id' + ) + ->leftJoin( + 'component_transaction_journal', 'component_transaction_journal.transaction_journal_id', + '=', + 'transaction_journals.id' + )->where('component_transaction_journal.component_id', '=', $budget->id)->where( + 'transaction_journals.date', '>=', $rep->startdate->format('Y-m-d') + )->where('transaction_journals.date', '<=', $rep->enddate->format('Y-m-d'))->where( + 'amount', '>', 0 + )->sum('amount'); + + + $pct = round(($spentInRep / $limit->amount) * 100,2); + $expense[] = [$rep->startdate->timestamp * 1000, floatval($spentInRep)]; + $left[] = [$rep->startdate->timestamp * 1000, $pct]; + } + } + + $return = [ + 'chart_title' => 'Overview for budget ' . $budget->name, + 'subtitle' => 'Between something something', + 'series' => [ + [ + 'type' => 'column', + 'name' => 'Expenses in envelope', + 'data' => $expense + ], + [ + 'type' => 'line', + 'yAxis' => 1, + 'name' => 'Spent pct for envelope', + 'data' => $left + ] + + ] + ]; + return Response::json($return); + } + + /** + * @param LimitRepetition $rep + */ + public function budgetLimit(\LimitRepetition $rep) + { + $budget = $rep->limit->budget; + $current = clone $rep->startdate; + $expense = []; + $leftInLimit = []; + $currentLeftInLimit = floatval($rep->limit->amount); + while ($current <= $rep->enddate) { + $spent = \Transaction:: + leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->leftJoin( + 'component_transaction_journal', 'component_transaction_journal.transaction_journal_id', '=', + 'transaction_journals.id' + )->where('component_transaction_journal.component_id', '=', $budget->id)->where( + 'transaction_journals.date', $current->format('Y-m-d') + )->where('amount', '>', 0)->sum('amount'); + $spent = floatval($spent) == 0 ? null : floatval($spent); + $entry = [$current->timestamp * 1000, $spent]; + $expense[] = $entry; + $currentLeftInLimit = $currentLeftInLimit - $spent; + $leftInLimit[] = [$current->timestamp * 1000, $currentLeftInLimit]; + $current->addDay(); + } + + $return = [ + 'chart_title' => 'Overview for budget ' . $budget->name, + 'subtitle' => 'Between ' . $rep->startdate->format('d M Y') . ' and ' . $rep->startdate->format('d M Y'), + 'series' => [ + [ + 'type' => 'column', + 'name' => 'Expenses per day', + 'yAxis' => 1, + 'data' => $expense + ], + [ + 'type' => 'line', + 'name' => 'Left in envelope', + 'data' => $leftInLimit + ] + + ] + ]; + + return Response::json($return); + } + + /** + * + */ + public function budgetNoLimits(\Budget $budget) + { + $inRepetitions = []; + foreach ($budget->limits as $limit) { + foreach ($limit->limitrepetitions as $repetition) { + $set = $budget->transactionjournals()->leftJoin( + 'transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id' + )->where('transaction_types.type', 'Withdrawal')->where( + 'date', '>=', $repetition->startdate->format('Y-m-d') + )->where('date', '<=', $repetition->enddate->format('Y-m-d'))->orderBy('date', 'DESC')->get( + ['transaction_journals.id'] + ); + foreach ($set as $item) { + $inRepetitions[] = $item->id; + } + } + + } + + $query = $budget->transactionjournals()->whereNotIn( + 'transaction_journals.id', $inRepetitions + )->orderBy('date', 'DESC')->orderBy( + 'transaction_journals.id', 'DESC' + ); + + + $result = $query->get(['transaction_journals.id']); + $set = []; + foreach ($result as $entry) { + $set[] = $entry->id; + } + // all transactions for these journals, grouped by date and SUM + $transactions = \Transaction::whereIn('transaction_journal_id', $set)->leftJoin( + 'transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id' + ) + ->groupBy('transaction_journals.date')->where('amount', '>', 0)->get( + ['transaction_journals.date', DB::Raw('SUM(`amount`) as `aggregate`')] + ); + + + // this set builds the chart: + $expense = []; + + foreach ($transactions as $t) { + $date = new Carbon($t->date); + $expense[] = [$date->timestamp * 1000, floatval($t->aggregate)]; + } + $return = [ + 'chart_title' => 'Overview for ' . $budget->name, + 'subtitle' => 'Not organized by an envelope', + 'series' => [ + [ + 'type' => 'spline', + 'name' => 'Expenses per day', + 'data' => $expense + ] + + ] + ]; + + return Response::json($return); + } + + /** + * + */ + public function budgetSession(\Budget $budget) + { + $expense = []; + $repetitionSeries = []; + $current = clone Session::get('start'); + $end = clone Session::get('end'); + while ($current <= $end) { + $spent = \Transaction:: + leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->leftJoin( + 'component_transaction_journal', 'component_transaction_journal.transaction_journal_id', '=', + 'transaction_journals.id' + )->where('component_transaction_journal.component_id', '=', $budget->id)->where( + 'transaction_journals.date', $current->format('Y-m-d') + )->where('amount', '>', 0)->sum('amount'); + $spent = floatval($spent) == 0 ? null : floatval($spent); + if (!is_null($spent)) { + $expense[] = [$current->timestamp * 1000, $spent]; + } + + $current->addDay(); + } + + // find all limit repetitions (for this budget) between start and end. + $start = clone Session::get('start'); + $repetitionSeries[] = [ + 'type' => 'column', + 'name' => 'Something something expenses', + 'data' => $expense + ]; + + + /** @var \Limit $limit */ + foreach ($budget->limits as $limit) { + $reps = $limit->limitrepetitions()->where( + function ($q) use ($start, $end) { + // startdate is between range + $q->where( + function ($q) use ($start, $end) { + $q->where('startdate', '>=', $start->format('Y-m-d')); + $q->where('startdate', '<=', $end->format('Y-m-d')); + } + ); + + // or enddate is between range. + $q->orWhere( + function ($q) use ($start, $end) { + $q->where('enddate', '>=', $start->format('Y-m-d')); + $q->where('enddate', '<=', $end->format('Y-m-d')); + } + ); + } + ) + ->get(); + $currentLeftInLimit = floatval($limit->amount); + /** @var \LimitRepetition $repetition */ + foreach ($reps as $repetition) { + // create a serie for the repetition. + $currentSerie = [ + 'type' => 'spline', + 'id' => 'rep-' . $repetition->id, + 'yAxis' => 1, + 'name' => 'Serie ' . $repetition->startdate->format('Y-m-d') . ' to ' . + $repetition->enddate->format('Y-m-d') . '.', + 'data' => [] + ]; + $current = clone $repetition->startdate; + while ($current <= $repetition->enddate) { + if ($current >= Session::get('start') && $current <= Session::get('end')) { + // spent on limit: + $spentSoFar = \Transaction:: + leftJoin( + 'transaction_journals', 'transaction_journals.id', '=', + 'transactions.transaction_journal_id' + ) + ->leftJoin( + 'component_transaction_journal', 'component_transaction_journal.transaction_journal_id', + '=', + 'transaction_journals.id' + )->where('component_transaction_journal.component_id', '=', $budget->id)->where( + 'transaction_journals.date', '>=', $repetition->startdate->format('Y-m-d') + )->where('transaction_journals.date', '<=', $current->format('Y-m-d'))->where( + 'amount', '>', 0 + )->sum('amount'); + $spent = floatval($spent) == 0 ? null : floatval($spent); + $currentLeftInLimit = floatval($limit->amount) - floatval($spentSoFar); + + $currentSerie['data'][] = [$current->timestamp * 1000, $currentLeftInLimit]; + } + $current->addDay(); + } + + // do something here. + $repetitionSeries[] = $currentSerie; + + } + + } + + + $return = [ + 'chart_title' => 'Overview for budget ' . $budget->name, + 'subtitle' => 'Between Bla bla bla', + 'series' => $repetitionSeries + ]; + + return Response::json($return); + + } + /** * @param Category $category * diff --git a/app/models/TransactionJournal.php b/app/models/TransactionJournal.php index 8f7d0e9f69..53d648790f 100644 --- a/app/models/TransactionJournal.php +++ b/app/models/TransactionJournal.php @@ -1,40 +1,41 @@ where('date', '>=', $date->format('Y-m-d')); } /** * @param $query - * @param \Carbon\Carbon $date + * @param Carbon $date * * @return mixed */ - public function scopeBefore($query, \Carbon\Carbon $date) + public function scopeBefore($query, Carbon $date) { return $query->where('date', '<=', $date->format('Y-m-d')); } + /** + * @param $query + * @param Carbon $date + * + * @return mixed + */ + public function scopeOnDate($query, Carbon $date) + { + return $query->where('date', '=', $date->format('Y-m-d')); + } + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ diff --git a/app/routes.php b/app/routes.php index c5c6e3442d..59ec4b70a7 100644 --- a/app/routes.php +++ b/app/routes.php @@ -75,6 +75,19 @@ Route::bind('limit', function($value, $route) return null; }); +Route::bind('limitrepetition', function($value, $route) + { + if(Auth::check()) { + return LimitRepetition:: + where('limit_repetitions.id', $value)-> + leftjoin('limits','limits.id','=','limit_repetitions.limit_id')-> + leftJoin('components','components.id','=','limits.component_id')-> + where('components.class','Budget')-> + where('components.user_id',Auth::user()->id)->first(['limit_repetitions.*']); + } + return null; + }); + Route::bind('piggybank', function($value, $route) { if(Auth::check()) { @@ -116,9 +129,15 @@ Route::group(['before' => 'auth'], function () { Route::get('/chart/home/account/{account?}', ['uses' => 'ChartController@homeAccount', 'as' => 'chart.home']); Route::get('/chart/home/categories', ['uses' => 'ChartController@homeCategories', 'as' => 'chart.categories']); Route::get('/chart/home/budgets', ['uses' => 'ChartController@homeBudgets', 'as' => 'chart.budgets']); - Route::get('/chart/home/info/{accountnameA}/{day}/{month}/{year}', - ['uses' => 'ChartController@homeAccountInfo', 'as' => 'chart.info']); + Route::get('/chart/home/info/{accountnameA}/{day}/{month}/{year}', ['uses' => 'ChartController@homeAccountInfo', 'as' => 'chart.info']); Route::get('/chart/categories/show/{category}', ['uses' => 'ChartController@categoryShowChart','as' => 'chart.showcategory']); + // (new charts for budgets) + Route::get('/chart/budget/{budget}/default', ['uses' => 'ChartController@budgetDefault', 'as' => 'chart.budget.default']); + Route::get('chart/budget/{budget}/no_envelope', ['uses' => 'ChartController@budgetNoLimits', 'as' => 'chart.budget.nolimit']); + Route::get('chart/budget/{budget}/session', ['uses' => 'ChartController@budgetSession', 'as' => 'chart.budget.session']); + Route::get('chart/budget/envelope/{limitrepetition}', ['uses' => 'ChartController@budgetLimit', 'as' => 'chart.budget.limit']); + + // home controller Route::get('/', ['uses' => 'HomeController@index', 'as' => 'index']); diff --git a/app/views/budgets/show.blade.php b/app/views/budgets/show.blade.php index ab99bf0709..52f14a4d26 100644 --- a/app/views/budgets/show.blade.php +++ b/app/views/budgets/show.blade.php @@ -9,11 +9,12 @@ @if(isset($filters[0]) && is_object($filters[0]) && get_class($filters[0]) == 'Limit')

- This view is filtered to show only the envelope from {{{$repetitions[0]['limitrepetition']->periodShow()}}} - with a total amount of {{mf($repetitions[0]['limit']->amount,false)}}. + This view is filtered to show only the envelope from + {{{$repetitions[0]['limitrepetition']->periodShow()}}}, + which contains {{mf($repetitions[0]['limit']->amount,false)}}.

- Reset the filters. + Reset the filter(s).

@endif @@ -23,7 +24,7 @@ This view is filtered to show transactions not in an envelope only.

- Reset the filters. + Reset the filter(s).

@endif @@ -35,7 +36,7 @@

- Reset the filters. + Reset the filter(s).

@endif @@ -44,23 +45,23 @@
+
@if(isset($filters[0]) && is_object($filters[0]) && get_class($filters[0]) == 'Limit') - -

- A chart showing the date-range of the selected envelope, all transactions - as bars and the amount left in the envelope as a line. -

+
@elseif(isset($filters[0]) && $filters[0] == 'no_envelope') +

A chart showing the date-range of all the not-enveloped stuff, and their amount.

@elseif($useSessionDates == true) +

Date range of session, show chart with all expenses in bars find all limit repetitions, add them as individual lines and make them go down. same as the first but bigger range (potentially).

@else +

(For each visible repetition, a sum of the expense as a bar. A line shows the percentage spent for each rep.)

@endif @@ -107,4 +108,7 @@ @endif @endforeach +@stop +@section('scripts') + @stop \ No newline at end of file