First effort towards nice charts in the budgeting overviews.

This commit is contained in:
James Cole 2014-08-23 08:34:22 +02:00
parent 1b1dc99bf7
commit 871509d4d3
7 changed files with 466 additions and 41 deletions

View File

@ -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

View File

@ -7,7 +7,7 @@ if($('#chart').length == 1) {
var options = {
chart: {
renderTo: 'chart',
type: 'line'
type: 'spline'
},
series: data.series,

View File

@ -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);
});
}
});

View File

@ -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
*

View File

@ -1,40 +1,41 @@
<?php
use Carbon\Carbon;
use LaravelBook\Ardent\Ardent;
/**
* TransactionJournal
*
* @property integer $id
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property integer $user_id
* @property integer $transaction_type_id
* @property integer $transaction_currency_id
* @property string $description
* @property boolean $completed
* @property \Carbon\Carbon $date
* @property integer $id
* @property Carbon $created_at
* @property Carbon $updated_at
* @property integer $user_id
* @property integer $transaction_type_id
* @property integer $transaction_currency_id
* @property string $description
* @property boolean $completed
* @property Carbon $date
* @property-read \Illuminate\Database\Eloquent\Collection|\
* 'Budget[] $budgets
* @property-read \Illuminate\Database\Eloquent\Collection|\
* 'Category[] $categories
* @property-read \Illuminate\Database\Eloquent\Collection|\Component[] $components
* @property-read \TransactionCurrency $transactionCurrency
* @property-read \TransactionType $transactionType
* @property-read \Illuminate\Database\Eloquent\Collection|\Component[] $components
* @property-read \TransactionCurrency $transactionCurrency
* @property-read \TransactionType $transactionType
* @property-read \Illuminate\Database\Eloquent\Collection|\Transaction[] $transactions
* @property-read \User $user
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereId($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereCreatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereUpdatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereUserId($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereTransactionTypeId($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereTransactionCurrencyId($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereDescription($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereCompleted($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereDate($value)
* @method static \TransactionJournal after($date)
* @method static \TransactionJournal before($date)
* @property-read \User $user
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereId($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereCreatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereUpdatedAt($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereUserId($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereTransactionTypeId($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereTransactionCurrencyId($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereDescription($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereCompleted($value)
* @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereDate($value)
* @method static \TransactionJournal after($date)
* @method static \TransactionJournal before($date)
*/
class TransactionJournal extends Ardent
{
@ -87,26 +88,37 @@ class TransactionJournal extends Ardent
/**
* @param $query
* @param \Carbon\Carbon $date
* @param Carbon $date
*
* @return mixed
*/
public function scopeAfter($query, \Carbon\Carbon $date)
public function scopeAfter($query, Carbon $date)
{
return $query->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
*/

View File

@ -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']);

View File

@ -9,11 +9,12 @@
<!-- warning for selected limit -->
@if(isset($filters[0]) && is_object($filters[0]) && get_class($filters[0]) == 'Limit')
<p class="bg-primary" style="padding:15px;">
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)}}.
</p>
<p class="bg-info" style="padding:15px;">
<a href="{{route('budgets.show',$budget->id)}}" class="text-info">Reset the filters.</a>
<a href="{{route('budgets.show',$budget->id)}}" class="text-info">Reset the filter(s).</a>
</p>
@endif
@ -23,7 +24,7 @@
This view is filtered to show transactions not in an envelope only.
</p>
<p class="bg-info" style="padding:15px;">
<a href="{{route('budgets.show',$budget->id)}}" class="text-info">Reset the filters.</a>
<a href="{{route('budgets.show',$budget->id)}}" class="text-info">Reset the filter(s).</a>
</p>
@endif
@ -35,7 +36,7 @@
</p>
<p class="bg-info" style="padding:15px;">
<a href="{{route('budgets.show',$budget->id)}}" class="text-info">Reset the filters.</a>
<a href="{{route('budgets.show',$budget->id)}}" class="text-info">Reset the filter(s).</a>
</p>
@endif
@ -44,23 +45,23 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<div id="chart" style="height:300px;"></div>
@if(isset($filters[0]) && is_object($filters[0]) && get_class($filters[0]) == 'Limit')
<p class="small text-center">
A chart showing the date-range of the selected envelope, all transactions
as bars and the amount left in the envelope as a line.
</p>
<div id="instr" data-type="envelope" data-envelope="{{$repetitions[0]['limitrepetition']->id}}"></div>
@elseif(isset($filters[0]) && $filters[0] == 'no_envelope')
<div id="instr" data-type="no_envelope" data-budget="{{$budget->id}}"></div>
<p class="small text-center">
A chart showing the date-range of all the not-enveloped stuff, and their amount.
</p>
@elseif($useSessionDates == true)
<div id="instr" data-type="session" data-budget="{{$budget->id}}"></div>
<p class="small text-center">
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).
</p>
@else
<div id="instr" data-type="default" data-budget="{{$budget->id}}"></div>
<p class="small text-center">(For each visible repetition, a sum of the expense as a bar. A line shows
the percentage spent for each rep.)</p>
@endif
@ -107,4 +108,7 @@
@endif
@endforeach
@stop
@section('scripts')
<?php echo javascript_include_tag('budgets'); ?>
@stop