New stuff! [skip ci]

This commit is contained in:
James Cole
2014-07-27 20:29:58 +02:00
parent b782bb8d93
commit 92f2e30ed1
22 changed files with 868 additions and 251 deletions

View File

@@ -0,0 +1,16 @@
// 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 highslide/highslide-full.min
//= require highslide/highslide.config
//= require_tree highcharts
//= require firefly/accounts

View File

@@ -0,0 +1,95 @@
$(function () {
if($('#chart').length == 1) {
/**
* get data from controller for home charts:
*/
$.getJSON('chart/home/account/' + accountID).success(function (data) {
var options = {
chart: {
renderTo: 'chart',
type: 'line'
},
series: data,
title: {
text: 'BETTER TITLE HERE'
},
yAxis: {
formatter: function () {
return '$' + Highcharts.numberFormat(this.y, 0);
}
},
subtitle: {
text: '<a href="#">View more</a>',
useHTML: true
},
xAxis: {
floor: 0,
type: 'datetime',
dateTimeLabelFormats: {
day: '%e %b',
year: '%b'
},
title: {
text: 'Date'
}
},
tooltip: {
shared: true,
crosshairs: false,
formatter: function () {
var str = '<span style="font-size:80%;">' + Highcharts.dateFormat("%A, %e %B", this.x) + '</span><br />';
for (x in this.points) {
var point = this.points[x];
var colour = point.point.pointAttr[''].fill;
str += '<span style="color:' + colour + '">' + point.series.name + '</span>: € ' + Highcharts.numberFormat(point.y, 2) + '<br />';
}
//console.log();
return str;
return '<span style="font-size:80%;">' + this.series.name + ' on ' + Highcharts.dateFormat("%e %B", this.x) + ':</span><br /> € ' + Highcharts.numberFormat(this.y, 2);
}
},
plotOptions: {
line: {
shadow: true
},
series: {
cursor: 'pointer',
negativeColor: '#FF0000',
threshold: 0,
lineWidth: 1,
marker: {
radius: 2
},
point: {
events: {
click: function (e) {
hs.htmlExpand(null, {
src: 'chart/home/info/' + this.series.name + '/' + Highcharts.dateFormat("%d/%m/%Y", this.x),
pageOrigin: {
x: e.pageX,
y: e.pageY
},
objectType: 'ajax',
headingText: '<a href="#">' + this.series.name + '</a>',
width: 250
}
)
;
}
}
}
}
},
credits: {
enabled: false
}
};
$('#chart').highcharts(options);
});
}
});

View File

@@ -0,0 +1,13 @@
/**
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any Css/Less files 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.css'
*
*= require highslide/highslide
*/

View File

@@ -51,13 +51,11 @@ class AccountController extends \BaseController
$result = $this->_repository->destroy(Input::get('id'));
if ($result === true) {
Session::flash('success', 'The account was deleted.');
return Redirect::route('accounts.index');
} else {
Session::flash('danger', 'Could not delete the account. Check the logs to be sure.');
return Redirect::route('accounts.index');
Session::flash('error', 'Could not delete the account. Check the logs to be sure.');
}
return Redirect::route('accounts.index');
}
@@ -93,7 +91,9 @@ class AccountController extends \BaseController
*/
public function show(Account $account)
{
return View::make('accounts.show')->with('account', $account);
$show = $this->_accounts->show($account, 40);
return View::make('accounts.show')->with('account', $account)->with('show',$show);
}
/**

View File

@@ -1,7 +1,8 @@
<?php
use Firefly\Helper\Controllers\BudgetInterface as BI;
use Firefly\Storage\Budget\BudgetRepositoryInterface as BRI;
use Carbon\Carbon;
/**
* Class BudgetController
*/
@@ -9,16 +10,40 @@ class BudgetController extends BaseController
{
protected $_budgets;
protected $_repository;
/**
* @param BRI $budgets
*/
public function __construct(BRI $budgets)
public function __construct(BI $budgets, BRI $repository)
{
$this->_budgets = $budgets;
$this->_repository = $repository;
View::share('menu', 'budgets');
}
/**
* @return $this|\Illuminate\View\View
*/
public function create()
{
$periods = \Config::get('firefly.periods_to_text');
return View::make('budgets.create')->with('periods', $periods);
}
/**
* @return $this|\Illuminate\View\View
*/
public function indexByBudget()
{
$budgets = $this->_repository->get();
$today = new Carbon;
return View::make('budgets.indexByBudget')->with('budgets', $budgets)->with('today', $today);
}
/**
* @return $this|\Illuminate\View\View
* @throws Firefly\Exception\FireflyException
@@ -26,76 +51,12 @@ class BudgetController extends BaseController
public function indexByDate()
{
// get a list of dates by getting all repetitions:
$budgets = $this->_budgets->get();
$reps = [];
foreach ($budgets as $budget) {
foreach ($budget->limits as $limit) {
$dateFormats = \Config::get('firefly.date_formats_by_period.' . $limit->repeat_freq);
if (is_null($dateFormats)) {
throw new \Firefly\Exception\FireflyException('No date formats for ' . $limit->repeat_freq);
}
$set = $this->_repository->get();
$budgets = $this->_budgets->organizeByDate($set);
foreach ($limit->limitrepetitions as $rep) {
$periodOrder = $rep->startdate->format($dateFormats['group_date']);
$period = $rep->startdate->format($dateFormats['display_date']);
$reps[$periodOrder] = isset($reps[$periodOrder]) ? $reps[$periodOrder] : ['date' => $period];
return View::make('budgets.indexByDate')->with('budgets', $budgets);
}
}
}
// put all the budgets under their respective date:
foreach ($budgets as $budget) {
foreach ($budget->limits as $limit) {
$dateFormats = \Config::get('firefly.date_formats_by_period.' . $limit->repeat_freq);
foreach ($limit->limitrepetitions as $rep) {
$month = $rep->startdate->format($dateFormats['group_date']);
$reps[$month]['limitrepetitions'][] = $rep;
}
}
}
krsort($reps);
return View::make('budgets.indexByDate')->with('reps', $reps);
}
/**
* @return $this|\Illuminate\View\View
*/
public function indexByBudget()
{
$budgets = $this->_budgets->get();
$today = new \Carbon\Carbon;
return View::make('budgets.indexByBudget')->with('budgets', $budgets)->with('today', $today);
}
/**
* @return $this|\Illuminate\View\View
*/
public function create()
{
$periods = \Config::get('firefly.periods_to_text');
return View::make('budgets.create')->with('periods', $periods);
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function store()
{
$data = [
'name' => Input::get('name'),
'amount' => floatval(Input::get('amount')),
'repeat_freq' => Input::get('period'),
'repeats' => intval(Input::get('repeats'))
];
$this->_budgets->store($data);
Session::flash('success', 'Budget created!');
return Redirect::route('budgets.index');
}
/**
@@ -146,5 +107,24 @@ class BudgetController extends BaseController
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function store()
{
$data = [
'name' => Input::get('name'),
'amount' => floatval(Input::get('amount')),
'repeat_freq' => Input::get('period'),
'repeats' => intval(Input::get('repeats'))
];
$this->_budgets->store($data);
Session::flash('success', 'Budget created!');
return Redirect::route('budgets.index');
}
}

View File

@@ -41,6 +41,7 @@ class Account implements AccountInterface
}
}
return $list;
}
@@ -53,6 +54,7 @@ class Account implements AccountInterface
public function openingBalanceTransaction(\Account $account)
{
$transactionType = \TransactionType::where('type', 'Opening balance')->first();
return \TransactionJournal::
with(
['transactions' => function ($q) {
@@ -63,4 +65,118 @@ class Account implements AccountInterface
->where('transactions.account_id', $account->id)->first(['transaction_journals.*']);
}
/**
* @param \Account $account
* @param $perPage
*
* @return mixed|void
*/
public function show(\Account $account, $perPage)
{
$start = \Session::get('start');
$end = \Session::get('end');
$stats = [
'budgets' => [],
'categories' => [],
'accounts' => []
];
$items = [];
// build a query:
$query = \TransactionJournal::with(
['transactions' => function ($q) {
$q->orderBy('amount', 'ASC');
}, 'transactiontype', 'components' => function ($q) {
$q->orderBy('class');
}, 'transactions.account.accounttype']
)->orderBy('date', 'DESC')->leftJoin(
'transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id'
)->where('transactions.account_id', $account->id)->where('date', '>=', $start->format('Y-m-d'))->where(
'date', '<=', $end->format('Y-m-d')
)->orderBy('transaction_journals.id', 'DESC');
// build paginator:
$totalItems = $query->count();
$page = intval(\Input::get('page')) > 1 ? intval(\Input::get('page')) : 1;
$skip = ($page - 1) * $perPage;
$result = $query->skip($skip)->take($perPage)->get(['transaction_journals.*']);
// in the mean time, build list of categories, budgets and other accounts:
/** @var $item \TransactionJournal */
foreach ($result as $item) {
$items[] = $item;
foreach ($item->components as $component) {
if ($component->class == 'Budget') {
$stats['budgets'][$component->id] = $component;
}
if ($component->class == 'Category') {
$stats['categories'][$component->id] = $component;
}
}
$fromAccount = $item->transactions[0]->account;
$toAccount = $item->transactions[1]->account;
$stats['accounts'][$fromAccount->id] = $fromAccount;
$stats['accounts'][$toAccount->id] = $toAccount;
}
unset($result, $page);
$paginator = \Paginator::make($items, $totalItems, $perPage);
// statistics
$stats['period']['in'] = floatval(
\Transaction::where('account_id', $account->id)->where('amount', '>', 0)->leftJoin(
'transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id'
)->leftJoin(
'transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'
)->whereIn('transaction_types.type', ['Deposit', 'Withdrawal'])->where(
'transaction_journals.date', '>=', $start->format('Y-m-d')
)->where('transaction_journals.date', '<=', $end->format('Y-m-d'))->sum('amount')
);
$stats['period']['out'] = floatval(
\Transaction::where('account_id', $account->id)->where('amount', '<', 0)->leftJoin(
'transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id'
)->leftJoin(
'transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'
)->whereIn('transaction_types.type', ['Deposit', 'Withdrawal'])->where(
'transaction_journals.date', '>=', $start->format('Y-m-d')
)->where('transaction_journals.date', '<=', $end->format('Y-m-d'))->sum('amount')
);
$stats['period']['diff'] = $stats['period']['in'] + $stats['period']['out'];
$stats['period']['t_in'] = floatval(
\Transaction::where('account_id', $account->id)->where('amount', '>', 0)->leftJoin(
'transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id'
)->leftJoin(
'transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'
)->where('transaction_types.type', 'Transfer')->where(
'transaction_journals.date', '>=', $start->format('Y-m-d')
)->where('transaction_journals.date', '<=', $end->format('Y-m-d'))->sum('amount')
);
$stats['period']['t_out'] = floatval(
\Transaction::where('account_id', $account->id)->where('amount', '<', 0)->leftJoin(
'transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id'
)->leftJoin(
'transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'
)->where('transaction_types.type', 'Transfer')->where(
'transaction_journals.date', '>=', $start->format('Y-m-d')
)->where('transaction_journals.date', '<=', $end->format('Y-m-d'))->sum('amount')
);
$stats['period']['t_diff'] = $stats['period']['t_in'] + $stats['period']['t_out'];
$return = [
'journals' => $paginator,
'statistics' => $stats
];
return $return;
}
}

View File

@@ -28,4 +28,12 @@ interface AccountInterface
*/
public function openingBalanceTransaction(\Account $account);
/**
* @param \Account $account
* @param $perPage
*
* @return mixed
*/
public function show(\Account $account, $perPage);
}

View File

@@ -0,0 +1,61 @@
<?php
/**
* Created by PhpStorm.
* User: sander
* Date: 27/07/14
* Time: 16:28
*/
namespace Firefly\Helper\Controllers;
use Illuminate\Database\Eloquent\Collection;
/**
* Class Budget
*
* @package Firefly\Helper\Controllers
*/
class Budget implements BudgetInterface
{
/**
* @param Collection $budgets
*
* @return mixed|void
*/
public function organizeByDate(Collection $budgets)
{
$return = [];
foreach ($budgets as $budget) {
foreach ($budget->limits as $limit) {
$dateFormats = \Config::get('firefly.date_formats_by_period.' . $limit->repeat_freq);
if (is_null($dateFormats)) {
throw new \Firefly\Exception\FireflyException('No date formats for ' . $limit->repeat_freq);
}
foreach ($limit->limitrepetitions as $rep) {
$periodOrder = $rep->startdate->format($dateFormats['group_date']);
$period = $rep->startdate->format($dateFormats['display_date']);
$return[$periodOrder] = isset($return[$periodOrder]) ? $return[$periodOrder] : ['date' => $period];
}
}
}
// put all the budgets under their respective date:
foreach ($budgets as $budget) {
foreach ($budget->limits as $limit) {
$dateFormats = \Config::get('firefly.date_formats_by_period.' . $limit->repeat_freq);
foreach ($limit->limitrepetitions as $rep) {
$rep->left = $rep->left();
$month = $rep->startdate->format($dateFormats['group_date']);
$return[$month]['limitrepetitions'][] = $rep;
}
}
}
krsort($return);
return $return;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Firefly\Helper\Controllers;
use Illuminate\Database\Eloquent\Collection;
/**
* Interface BudgetInterface
*
* @package Firefly\Helper\Controllers
*/
interface BudgetInterface {
/**
* @param Collection $budgets
*
* @return mixed
*/
public function organizeByDate(Collection $budgets);
}

View File

@@ -23,6 +23,11 @@ class HelperServiceProvider extends ServiceProvider
'Firefly\Helper\Controllers\Account'
);
$this->app->bind(
'Firefly\Helper\Controllers\BudgetInterface',
'Firefly\Helper\Controllers\Budget'
);
// mail:
$this->app->bind(
'Firefly\Helper\Email\EmailHelperInterface',

View File

@@ -12,6 +12,40 @@ use Carbon\Carbon;
class EloquentBudgetRepository implements BudgetRepositoryInterface
{
/**
* @param $budgetId
*
* @return mixed
*/
public function find($budgetId)
{
return \Auth::user()->budgets()->find($budgetId);
}
/**
* @return mixed
*/
public function get()
{
$set = \Auth::user()->budgets()->with(
['limits' => function ($q) {
$q->orderBy('limits.startdate', 'ASC');
}, 'limits.limitrepetitions' => function ($q) {
$q->orderBy('limit_repetitions.startdate', 'ASC');
}]
)->orderBy('name', 'ASC')->get();
foreach ($set as $budget) {
foreach ($budget->limits as $limit) {
foreach ($limit->limitrepetitions as $rep) {
$rep->left = $rep->left();
}
}
}
return $set;
}
/**
* @return array|mixed
*/
@@ -24,6 +58,7 @@ class EloquentBudgetRepository implements BudgetRepositoryInterface
foreach ($list as $entry) {
$return[intval($entry->id)] = $entry->name;
}
return $return;
}
@@ -69,6 +104,7 @@ class EloquentBudgetRepository implements BudgetRepositoryInterface
$budget->count += count($limit->limitrepetitions);
}
}
return $set;
}
@@ -123,30 +159,4 @@ class EloquentBudgetRepository implements BudgetRepositoryInterface
return $budget;
}
/**
* @return mixed
*/
public function get()
{
return \Auth::user()->budgets()->with(
['limits' => function ($q) {
$q->orderBy('limits.startdate', 'ASC');
}, 'limits.limitrepetitions' => function ($q) {
$q->orderBy('limit_repetitions.startdate', 'ASC');
}]
)->orderBy('name', 'ASC')->get();
}
/**
* @param $budgetId
*
* @return mixed
*/
public function find($budgetId)
{
return \Auth::user()->budgets()->find($budgetId);
}
}

View File

@@ -100,7 +100,7 @@ class EloquentLimitRepository implements LimitRepositoryInterface
*/
public function getTJByBudgetAndDateRange(\Budget $budget, Carbon $start, Carbon $end)
{
$result = $budget->transactionjournals()->after($start)->before($end)->get();
$result = $budget->transactionjournals()->with('transactions')->after($start)->before($end)->get();
return $result;

View File

@@ -37,6 +37,7 @@ class LimitRepetition extends Ardent
$start->startOfMonth();
$end = clone $start;
$end->endOfMonth();
return [
'limit_id' => 'factory|Limit',
'startdate' => $start,
@@ -45,11 +46,6 @@ class LimitRepetition extends Ardent
];
}
public function limit()
{
return $this->belongsTo('Limit');
}
public function getDates()
{
return ['created_at', 'updated_at', 'startdate', 'enddate'];
@@ -60,10 +56,6 @@ class LimitRepetition extends Ardent
*/
public function left()
{
$key = 'limit-rep-left-' . $this->id;
if (Cache::has($key)) {
return Cache::get($key);
}
$left = floatval($this->amount);
// budget:
@@ -80,11 +72,14 @@ class LimitRepetition extends Ardent
}
}
}
Cache::forever($key, $left);
return $left;
}
public function limit()
{
return $this->belongsTo('Limit');
}
}

View File

@@ -3,8 +3,23 @@
// models:
Route::bind('account', function($value, $route)
{
return Account::where('id', $value)->where('user_id',Auth::user()->id)->first();
if(Auth::check()) {
return Account::
where('id', $value)->
where('user_id',Auth::user()->id)->first();
}
return null;
});
Route::bind('budget', function($value, $route)
{
if(Auth::check()) {
return Budget::
where('id', $value)->
where('user_id',Auth::user()->id)->first();
}
return null;
});
// protected routes:
@@ -15,7 +30,7 @@ Route::group(['before' => 'auth'], function () {
Route::get('/flush', ['uses' => 'HomeController@flush', 'as' => 'flush']);
// chart controller
Route::get('/chart/home/account/{account?}', ['uses' => 'ChartController@homeAccount', 'as' => 'chart.home']);
Route::get('/chart/home/account/{accountname?}', ['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/{accountname}/{day}/{month}/{year}', ['uses' => 'ChartController@homeAccountInfo', 'as' => 'chart.info']);
@@ -42,7 +57,9 @@ Route::group(['before' => 'auth'], function () {
Route::get('/budget/create',['uses' => 'BudgetController@create', 'as' => 'budgets.create']);
Route::get('/budgets',['uses' => 'BudgetController@indexByDate','as' => 'budgets.index']);
Route::get('/budgets/budget',['uses' => 'BudgetController@indexByBudget','as' => 'budgets.index.budget']);
Route::get('/budget/show/{id}',['uses' => 'BudgetController@show', 'as' => 'budgets.show']);
Route::get('/budget/show/{budget}',['uses' => 'BudgetController@show', 'as' => 'budgets.show']);
Route::get('/budget/edit/{budget}',['uses' => 'BudgetController@edit', 'as' => 'budgets.edit']);
Route::get('/budget/delete/{budget}',['uses' => 'BudgetController@delete', 'as' => 'budgets.delete']);
// limit controller:
Route::get('/budgets/limits/create/{id?}',['uses' => 'LimitController@create','as' => 'budgets.limits.create']);

View File

@@ -1,102 +1,210 @@
<?php
use Mockery as m;
use \League\FactoryMuffin\Facade\FactoryMuffin as f;
use Illuminate\Database\Eloquent\Collection;
class AccountControllerTest extends TestCase
{
protected $_repository;
protected $_user;
protected $_accounts;
public function setUp()
{
parent::setUp();
Artisan::call('migrate');
Artisan::call('db:seed');
}
public function testIndex()
{
// mock account type(s):
$personal = $this->mock('AccountType');
$personal->shouldReceive('getAttribute', 'description')->andReturn('Default account');
$bene = $this->mock('AccountType');
$bene->shouldReceive('getAttribute', 'description')->andReturn('Beneficiary account');
$initial = $this->mock('AccountType');
$initial->shouldReceive('getAttribute', 'description')->andReturn('Initial balance account');
$cash = $this->mock('AccountType');
$cash->shouldReceive('getAttribute', 'description')->andReturn('Cash account');
// mock account(s)
$one = $this->mock('Account');
$one->shouldReceive('getAttribute')->andReturn($personal);
$two = $this->mock('Account');
$two->shouldReceive('getAttribute')->andReturn($bene);
$three = $this->mock('Account');
$three->shouldReceive('getAttribute')->andReturn($initial);
$four = $this->mock('Account');
$four->shouldReceive('getAttribute')->andReturn($cash);
$c = new \Illuminate\Database\Eloquent\Collection([$one, $two, $three, $four]);
// mock account repository:
$accounts = $this->mock('Firefly\Storage\Account\AccountRepositoryInterface');
$accounts->shouldReceive('get')->andReturn($c);
$list = [
'personal' => [$one],
'beneficiaries' => [$two],
'initial' => [$three],
'cash' => [$four]
];
// mock:
View::shouldReceive('share');
View::shouldReceive('make')->with('accounts.index')->once()->andReturn(\Mockery::self())
->shouldReceive('with')->once()->with('accounts', $list)->andReturn(\Mockery::self())
->shouldReceive('with')->once()->with('total', 4)->andReturn(\Mockery::self());
// call
$this->call('GET', '/accounts');
// test
$this->assertResponseOk();
$this->_repository = $this->mock('Firefly\Storage\Account\AccountRepositoryInterface');
$this->_accounts = $this->mock('Firefly\Helper\Controllers\AccountInterface');
$this->_user = m::mock('User','Eloquent');
$this->app->instance('User', $this->_user);
}
public function testCreate()
{
// mock:
View::shouldReceive('share');
View::shouldReceive('make')->with('accounts.create');
// call
$this->call('GET', '/accounts/create');
// test
$this->assertResponseOk();
}
public function testShow()
{
// mock account repository:
$accounts = $this->mock('Firefly\Storage\Account\AccountRepositoryInterface');
$accounts->shouldReceive('get')->with(1)->andReturn([]);
// call
$this->call('GET', '/accounts/1');
// test
$this->assertResponseOk();
}
public function tearDown()
{
Mockery::close();
}
public function testCreate()
{
$this->action('GET', 'AccountController@create');
$this->assertResponseOk();
}
public function testDelete()
{
$account = f::create('Account');
Auth::shouldReceive('user')->andReturn($this->_user);
Auth::shouldReceive('check')->andReturn(true);
$this->_user->shouldReceive('getAttribute')->with('id')->once()->andReturn($account->user_id);
$this->_user->shouldReceive('getAttribute')->with('email')->once()->andReturn($account->email);
$this->action('GET', 'AccountController@delete',$account->id);
$this->assertResponseOk();
}
public function testDestroy()
{
$account = f::create('Account');
Auth::shouldReceive('user')->andReturn($this->_user);
$this->_repository->shouldReceive('destroy')->once()->with("")->andReturn(true);
$this->action('POST', 'AccountController@destroy',$account->id);
$this->assertRedirectedToRoute('accounts.index');
$this->assertSessionHas('success');
}
public function testDestroyFails()
{
$account = f::create('Account');
$this->_repository->shouldReceive('destroy')->once()->with("")->andReturn(false);
$this->action('POST', 'AccountController@destroy',$account->id);
$this->assertRedirectedToRoute('accounts.index');
$this->assertSessionHas('error');
}
public function testEdit()
{
$account = f::create('Account');
Auth::shouldReceive('user')->andReturn($this->_user);
Auth::shouldReceive('check')->andReturn(true);
$this->_user->shouldReceive('getAttribute')->with('id')->once()->andReturn($account->user_id);
$this->_user->shouldReceive('getAttribute')->with('email')->once()->andReturn($account->email);
$this->_accounts->shouldReceive('openingBalanceTransaction')->once()->andReturn(null);
$this->action('GET', 'AccountController@edit',$account->id);
$this->assertResponseOk();
}
public function testIndex()
{
$account = f::create('Account');
$collection = new Collection();
$collection->add($account);
$list = [
'personal' => [],
'beneficiaries' => [],
'initial' => [],
'cash' => []
];
$this->_repository->shouldReceive('get')->with()->once()->andReturn($collection);
$this->_accounts->shouldReceive('index')->with($collection)->once()->andReturn($list);
$this->action('GET', 'AccountController@index');
$this->assertResponseOk();
}
public function testShow()
{
$account = f::create('Account');
Auth::shouldReceive('user')->andReturn($this->_user);
Auth::shouldReceive('check')->andReturn(true);
$this->_user->shouldReceive('getAttribute')->with('id')->once()->andReturn($account->user_id);
$this->_user->shouldReceive('getAttribute')->with('email')->once()->andReturn($account->email);
$this->_accounts->shouldReceive('paginate')->with($account,40)->once()->andReturn();
$this->action('GET', 'AccountController@show',$account->id);
$this->assertResponseOk();
}
public function testStore()
{
// $this->action('POST', 'AccountController@store');
// $this->assertResponseOk();
}
//
// public function testIndex()
// {
//// // mock account type(s):
//// $personal = $this->mock('AccountType');
//// $personal->shouldReceive('getAttribute', 'description')->andReturn('Default account');
////
//// $bene = $this->mock('AccountType');
//// $bene->shouldReceive('getAttribute', 'description')->andReturn('Beneficiary account');
////
//// $initial = $this->mock('AccountType');
//// $initial->shouldReceive('getAttribute', 'description')->andReturn('Initial balance account');
////
//// $cash = $this->mock('AccountType');
//// $cash->shouldReceive('getAttribute', 'description')->andReturn('Cash account');
////
////
//// // mock account(s)
//// $one = $this->mock('Account');
//// $one->shouldReceive('getAttribute')->andReturn($personal);
////
//// $two = $this->mock('Account');
//// $two->shouldReceive('getAttribute')->andReturn($bene);
////
//// $three = $this->mock('Account');
//// $three->shouldReceive('getAttribute')->andReturn($initial);
////
//// $four = $this->mock('Account');
//// $four->shouldReceive('getAttribute')->andReturn($cash);
//// $c = new \Illuminate\Database\Eloquent\Collection([$one, $two, $three, $four]);
////
//// // mock account repository:
//// $accounts = $this->mock('Firefly\Storage\Account\AccountRepositoryInterface');
//// $accounts->shouldReceive('get')->andReturn($c);
////
////
//// $list = [
//// 'personal' => [$one],
//// 'beneficiaries' => [$two],
//// 'initial' => [$three],
//// 'cash' => [$four]
//// ];
////
//// // mock:
//// View::shouldReceive('share');
//// View::shouldReceive('make')->with('accounts.index')->once()->andReturn(\Mockery::self())
//// ->shouldReceive('with')->once()->with('accounts', $list)->andReturn(\Mockery::self())
//// ->shouldReceive('with')->once()->with('total', 4)->andReturn(\Mockery::self());
////
//
// // call
// $this->call('GET', '/accounts');
//
// // test
// $this->assertResponseOk();
//
// }
////
//// public function testCreate()
//// {
//// // mock:
//// View::shouldReceive('share');
//// View::shouldReceive('make')->with('accounts.create');
////
//// // call
//// $this->call('GET', '/accounts/create');
////
//// // test
//// $this->assertResponseOk();
//// }
////
//// public function testShow()
//// {
//// // mock account repository:
//// $accounts = $this->mock('Firefly\Storage\Account\AccountRepositoryInterface');
//// $accounts->shouldReceive('get')->with(1)->andReturn([]);
////
//// // call
//// $this->call('GET', '/accounts/1');
////
//// // test
//// $this->assertResponseOk();
//// }
////
public function testUpdate()
{
}
}

View File

@@ -47,9 +47,4 @@
</div>
</div>
@stop
@section('scripts')
<script src="https://www.google.com/jsapi"></script>
<!-- <script src="assets/javascript/charts.js"></script>-->
<!-- <script src="assets/javascript/index.js"></script>-->
@stop

View File

@@ -18,18 +18,96 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<h4>Transactions</h4>
<h4>Summary <small>For selected account and period</small></h4>
</div>
</div>
<div class="row">
<div class="col-lg-6 col-md-6 col-sm-12">
<table class="table table-striped table-condensed">
<tr>
<th></th>
<th>Expense / income</th>
<th>Transfers</th>
</tr>
<tr>
<td>Out</td>
<td>
{{mf($show['statistics']['period']['out'])}}
<a href="#transactions-thisaccount-this-period-expensesonly"><span class="glyphicon glyphicon-circle-arrow-right"></span></a>
</td>
<td>
{{mf($show['statistics']['period']['t_out'])}}
<a href="#transactions-thisaccount-this-period-transfers-out-only"><span class="glyphicon glyphicon-circle-arrow-right"></span></a>
</td>
</tr>
<tr>
<td>In</td>
<td>
{{mf($show['statistics']['period']['in'])}}
<a href="#transactions-thisaccount-this-period-incomeonly"><span class="glyphicon glyphicon-circle-arrow-right"></span></a>
</td>
<td>
{{mf($show['statistics']['period']['t_in'])}}
<a href="#transactions-thisaccount-this-period-transfers-in-only"><span class="glyphicon glyphicon-circle-arrow-right"></span></a>
</td>
</tr>
<tr>
<td>Difference</td>
<td>{{mf($show['statistics']['period']['diff'])}}</td>
<td>{{mf($show['statistics']['period']['t_diff'])}}</td>
</tr>
</table>
</div>
<div class="col-lg-6 col-md-6 col-sm-12">
<table class="table table-striped table-condensed">
<tr>
<td style="width:30%;">Related accounts</td>
<td>
@foreach($show['statistics']['accounts'] as $acct)
<a href="{{route('accounts.show',$acct->id)}}" class="btn btn-default btn-xs">{{{$acct->name}}}</a>
@endforeach
</td>
</tr>
<tr>
<td>Related categories</td>
<td>
@foreach($show['statistics']['categories'] as $cat)
<a href="#category-overview" class="btn btn-default btn-xs">{{{$cat->name}}}</a>
@endforeach
</td>
</tr>
<tr>
<td>Related budgets</td>
<td>
@foreach($show['statistics']['budgets'] as $bud)
<a href="#budget-overview" class="btn btn-default btn-xs">{{{$bud->name}}}</a>
@endforeach
</td>
</tr>
</table>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<h4>Transactions <small> For selected account and period</small></h4>
@include('paginated.transactions',['journals' => $show['journals']])
</div>
</div>
@stop
@section('styles')
<?php echo stylesheet_link_tag('accounts'); ?>
@stop
@section('scripts')
<script type="text/javascript">
var accountID = {{$account->id}};
</script>
<script src="assets/javascript/highcharts.js"></script>
<script src="assets/javascript/highcharts-more.js"></script>
<script src="assets/javascript/highslide-full.min.js"></script>
<script src="assets/javascript/highslide.config.js"></script>
<script src="assets/javascript/accounts.js"></script>
<?php echo javascript_include_tag('accounts'); ?>
@stop

View File

@@ -3,18 +3,29 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<h1>Firefly
<small>Budgets and limits</small>
<small>Budgets and envelopes</small>
</h1>
<p class="lead">Use budgets to organize and limit your expenses.</p>
<p class="text-info">
These are your budgets and if set, their "limits". Firefly uses an "<a
href="http://en.wikipedia.org/wiki/Envelope_System" class="text-success">envelope system</a>" for your
budgets,
which means that for each period of time (for example a month) a virtual "envelope" can be created
containing a certain amount of money. Money spent within a budget is removed from the envelope.
Budgets are groups of expenses that reappear every [period]*. Examples could be "groceries", "bills" or
"drinks with friends". The table below lists all of the budgets you have, if any.
<a href="http://dictionary.reference.com/browse/budget">By definition</a>, budgets are an estimated amount
of money, ie. 400,-. Such an estimation can change over time but <em>should</em> always be set. Budgets
without an actual budget are fairly pointless.
</p>
<p>
<a class="btn btn-default" href ="{{route('budgets.index')}}"><span class="glyphicon glyphicon-th"></span> Group by date</a>
<a class="btn btn-default" href ="{{route('budgets.limits.create')}}"><span class="glyphicon glyphicon-plus-sign"></span> Create a limit</a>
<p class="text-info">
Use this page to create or change budgets and the estimated amount of money you think is wise. Pages further ahead
will explain what an "envelope" is in the context of budgeting.
</p>
<p class="text-info">
* <small>Every month, week, year, etc.</small>
</p>
<div class="btn-group">
<a class="btn btn-default" href ="{{route('budgets.index')}}"><span class="glyphicon glyphicon-th"></span> Group budgets by date</a>
<a class="btn btn-default" href ="{{route('budgets.limits.create')}}"><span class="glyphicon glyphicon-plus-sign"></span> Create a new envelope</a>
</div>
</p>
</div>
</div>
@@ -24,7 +35,7 @@
<tr>
<th>Budget</th>
<th>Current envelope(s)</th>
<th>&nbsp;</th>
<th>Update budget</th>
</tr>
@foreach($budgets as $budget)
<tr>
@@ -50,14 +61,14 @@
{{mf($rep->amount,false)}}</span>
</div>
<div class="col-sm-2">
@if($rep->left() < 0)
@if($rep->left < 0)
<span class="label label-danger">
<span class="glyphicon glyphicon-envelope"></span>
{{mf($rep->left(),false)}}</span>
{{mf($rep->left,false)}}</span>
@else
<span class="label label-success">
<span class="glyphicon glyphicon-envelope"></span>
{{mf($rep->left(),false)}}</span>
{{mf($rep->left,false)}}</span>
@endif
</div>
<div class="col-sm-3">
@@ -75,10 +86,12 @@
</div>
@endif
<div class="col-sm-2 @if($limit->repeats == 0) col-sm-offset-2 @endif">
<a href="#" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span></a>
@if($limit->repeats == 0 || ($limit->repeats == 1 && $index == 0))
<a href="#" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span></a>
@endif
<div class="btn-group btn-group-xs">
<a href="#" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span></a>
@if($limit->repeats == 0 || ($limit->repeats == 1 && $index == 0))
<a href="#" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span></a>
@endif
</div>
</div>
</div>
@endforeach

View File

@@ -3,64 +3,82 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<h1>Firefly
<small>Budgets and limits</small>
<small>Budgets and envelopes</small>
</h1>
<p class="lead">Use budgets to organize and limit your expenses.</p>
<p class="text-info">
These are your budgets and if set, their "limits". Firefly uses an "<a
href="http://en.wikipedia.org/wiki/Envelope_System" class="text-success">envelope system</a>" for your
budgets,
which means that for each period of time (for example a month) a virtual "envelope" can be created
containing a certain amount of money. Money spent within a budget is removed from the envelope.
Budgets are groups of expenses that reappear every [period]*. Examples could be "groceries", "bills" or
"drinks with friends". The table below lists all of the budgets you have, if any.
<a href="http://dictionary.reference.com/browse/budget">By definition</a>, budgets are an estimated amount
of money, ie. 400,-. Such an estimation can change over time but <em>should</em> always be set. Budgets
without an actual budget are fairly pointless.
</p>
<p class="text-info">
Use this page to create or change budgets and the estimated amount of money you think is wise. Pages further ahead
will explain what an "envelope" is in the context of budgeting.
</p>
<p class="text-info">
* <small>Every month, week, year, etc.</small>
</p>
<p>
<a class="btn btn-default" href ="{{route('budgets.index.budget')}}"><span class="glyphicon glyphicon-indent-left"></span> Group by budget</a>
<a class="btn btn-default" href ="{{route('budgets.limits.create')}}"><span class="glyphicon glyphicon-plus-sign"></span> Create a limit</a>
<a class="btn btn-default" href ="{{route('budgets.limits.create')}}"><span class="glyphicon glyphicon-plus-sign"></span> Create an envelope</a>
</p>
</div>
</div>
@foreach($reps as $date => $data)
<!-- count = zero! -->
@foreach($budgets as $date => $entry)
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<h3>{{$data['date']}}</h3>
<h3><a href="#transactions-in-this-period">{{$entry['date']}}</a>
<a class="btn btn-default btn-xs" href ="{{route('budgets.limits.create')}}#date-and-budget-selected"><span class="glyphicon glyphicon-plus-sign"></span> Create an envelope for {{$entry['date']}}</a>
</h3>
<table class="table table-bordered table-striped">
<tr>
<th style="width:45%;">Budget</th>
<th colspan="2" style="width:45%;">Budget</th>
<th style="width:15%;">Envelope</th>
<th style="width:15%;">Left</th>
<th>&nbsp;</th>
</tr>
@foreach($data['limitrepetitions'] as $index => $rep)
@foreach($entry['limitrepetitions'] as $index => $repetition)
<tr>
<td>
<a href="{{route('budgets.show',$rep->limit->budget->id)}}">{{{$rep->limit->budget->name}}}</a>
<div class="btn-group">
<a title="Edit budget {{{$repetition->limit->budget->name}}}" href="{{route('budgets.edit',$repetition->limit->budget->id)}}" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span></a>
<a title="Delete budget {{{$repetition->limit->budget->name}}}" href="{{route('budgets.delete',$repetition->limit->budget->id)}}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span></a>
</div>
</td>
<td>
<a href="{{route('budgets.show',$repetition->limit->budget->id)}}#transactions-in-this-period">
{{{$repetition->limit->budget->name}}}
</a>
</td>
<td>
<span class="label label-primary">
<span class="glyphicon glyphicon-envelope"></span> {{mf($rep->amount,false)}}</span>
<span class="glyphicon glyphicon-envelope"></span> {{mf($repetition->amount,false)}}</span>
</td>
<td>
@if($rep->left() < 0)
@if($repetition->left < 0)
<span class="label label-danger">
<span class="glyphicon glyphicon-envelope"></span>
{{mf($rep->left(),false)}}</span>
{{mf($repetition->left,false)}}</span>
@else
<span class="label label-success">
<span class="glyphicon glyphicon-envelope"></span>
{{mf($rep->left(),false)}}</span>
{{mf($repetition->left,false)}}</span>
@endif
</td>
<td>
<div class="btn-group">
<a href="{{route('budgets.limits.edit',$rep->limit->id)}}" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span></a>
<a href="{{route('budgets.limits.delete',$rep->limit->id)}}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span></a>
<a title="Edit envelope for {{{$repetition->limit->budget->name}}} in {{$entry['date']}}" href="{{route('budgets.limits.edit',$repetition->limit->id)}}" class="btn btn-default btn-xs"><span class="glyphicon glyphicon-pencil"></span></a>
<a title="Delete envelope for {{{$repetition->limit->budget->name}}} in {{$entry['date']}}" href="{{route('budgets.limits.delete',$repetition->limit->id)}}" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span></a>
</div>
@if($rep->limit->repeats == 1)
@if($repetition->limit->repeats == 1)
<span class="label label-warning">auto repeats</span>
@endif
<a href="{{route('budgets.limits.create',$rep->limit->budget->id)}}" class="btn btn-default btn-xs"><span
class="glyphicon-plus-sign glyphicon"></span> Add another limit</a>
</td>
</tr>
@endforeach

View File

@@ -56,7 +56,7 @@
@foreach($set as $data)
<div class="col-lg-{{$split}} col-md-{{$split}}">
<h4>
<a href="{{route('accounts.show',$data[1]->id)}}?range={{Config::get('firefly.range_to_text.'.Session::get('range'))}}&amp;startdate={{Session::get('start')->format('Y-m-d')}}">{{{$data[1]->name}}}</a>
<a href="{{route('accounts.show',$data[1]->id)}}">{{{$data[1]->name}}}</a>
</h4>
@include('transactions.journals-small',['transactions' => $data[0],'account' => $data[1]])

View File

@@ -0,0 +1,71 @@
<table class="table table-striped table-condensed">
<tr>
<th colspan="2"></th>
<th>Date</th>
<th>Description</th>
<th>Amount (&euro;)</th>
<th>From</th>
<th>To</th>
<th></th>
</tr>
@foreach($journals as $journal)
<tr>
<td>
@if($journal->transactiontype->type == 'Withdrawal')
<span class="glyphicon glyphicon-arrow-left" title="Withdrawal"></span>
@endif
@if($journal->transactiontype->type == 'Deposit')
<span class="glyphicon glyphicon-arrow-right" title="Deposit"></span>
@endif
@if($journal->transactiontype->type == 'Transfer')
<span class="glyphicon glyphicon-resize-full" title="Transfer"></span>
@endif
@if($journal->transactiontype->type == 'Opening balance')
<span class="glyphicon glyphicon-ban-circle" title="Opening balance"></span>
@endif
</td>
<td>
@foreach($journal->components as $component)
@if($component->class == 'Budget')
<a href="#budget-overview"><span class="glyphicon glyphicon-tasks" title="Budget: {{{$component->name}}}"></span></a>
@endif
@if($component->class == 'Category')
<a href="#category-overview"><span class="glyphicon glyphicon-tag" title="Category: {{{$component->name}}}"></span></a>
@endif
@endforeach
</td>
<td>
{{$journal->date->format('d F Y')}}
</td>
<td><a href="{{route('transactions.show',$journal->id)}}" title="{{{$journal->description}}}">{{{$journal->description}}}</a></td>
<td>
@if($journal->transactiontype->type == 'Withdrawal')
<span class="text-danger">{{mf($journal->transactions[1]->amount,false)}}</span>
@endif
@if($journal->transactiontype->type == 'Deposit')
<span class="text-success">{{mf($journal->transactions[1]->amount,false)}}</span>
@endif
@if($journal->transactiontype->type == 'Transfer')
<span class="text-info">{{mf($journal->transactions[1]->amount,false)}}</span>
@endif
</td>
<td>
<a href="{{route('accounts.show',$journal->transactions[0]->account_id)}}">{{{$journal->transactions[0]->account->name}}}</a>
</td>
<td>
<a href="{{route('accounts.show',$journal->transactions[1]->account_id)}}">{{{$journal->transactions[1]->account->name}}}</a>
</td>
<td>
<div class="btn-group btn-group-xs">
<a href="{{route('transactions.edit',$journal->id)}}" class="btn btn-default">
<span class="glyphicon glyphicon-pencil"></span>
<a href="{{route('transactions.delete',$journal->id)}}" class="btn btn-danger">
<span class="glyphicon glyphicon-trash"></span>
</a>
</div>
</td>
</tr>
@endforeach
</table>
{{$journals->links()}}