diff --git a/app/assets/javascripts/categories.js b/app/assets/javascripts/categories.js new file mode 100644 index 0000000000..f2c72e3b14 --- /dev/null +++ b/app/assets/javascripts/categories.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/categories diff --git a/app/assets/javascripts/firefly/categories.js b/app/assets/javascripts/firefly/categories.js new file mode 100644 index 0000000000..d338be7540 --- /dev/null +++ b/app/assets/javascripts/firefly/categories.js @@ -0,0 +1,90 @@ +$(function () { +if($('#chart').length == 1) { + /** + * get data from controller for home charts: + */ + $.getJSON('chart/categories/show/' + categoryID).success(function (data) { + var options = { + chart: { + renderTo: 'chart', + type: 'column' + }, + series: [data.series], + title: { + text: data.chart_title + }, + yAxis: { + formatter: function () { + return '$' + Highcharts.numberFormat(this.y, 0); + } + }, + subtitle: { + text: data.subtitle, + useHTML: true + }, + + xAxis: { + floor: 0, + type: 'category', + title: { + text: 'Period' + } + }, + tooltip: { + shared: true, + crosshairs: false, + formatter: function () { + var str = '' + Highcharts.dateFormat("%A, %e %B", this.x) + '
'; + for (x in this.points) { + var point = this.points[x]; + var colour = point.point.pointAttr[''].fill; + str += '' + point.series.name + ': € ' + Highcharts.numberFormat(point.y, 2) + '
'; + } + //console.log(); + return str; + return '' + this.series.name + ' on ' + Highcharts.dateFormat("%e %B", this.x) + ':
€ ' + 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: '' + this.series.name + '', + width: 250 + } + ) + ; + } + } + } + } + }, + credits: { + enabled: false + } + }; + $('#chart').highcharts(options); + }); +} + + + +}); \ No newline at end of file diff --git a/app/controllers/BudgetController.php b/app/controllers/BudgetController.php index 9e720e9a27..b6ddac3363 100644 --- a/app/controllers/BudgetController.php +++ b/app/controllers/BudgetController.php @@ -158,7 +158,6 @@ class BudgetController extends BaseController return Redirect::route('budgets.index.budget'); } - return Redirect::route('budgets.index'); } diff --git a/app/controllers/CategoryController.php b/app/controllers/CategoryController.php index 787835a4ad..b098fbefca 100644 --- a/app/controllers/CategoryController.php +++ b/app/controllers/CategoryController.php @@ -1,6 +1,7 @@ _repository = $repository; + $this->_category = $category; View::share('menu', 'categories'); } public function create() { + return View::make('categories.create'); } public function delete(Category $category) { - return View::make('categories.delete')->with('category',$category); + return View::make('categories.delete')->with('category', $category); } public function destroy() { + $result = $this->_repository->destroy(Input::get('id')); + if ($result === true) { + Session::flash('success', 'The category was deleted.'); + } else { + Session::flash('error', 'Could not delete the category. Check the logs to be sure.'); + } + return Redirect::route('categories.index'); } public function edit(Category $category) { - return View::make('categories.edit')->with('category',$category); + return View::make('categories.edit')->with('category', $category); } public function index() { $categories = $this->_repository->get(); - return View::make('categories.index')->with('categories',$categories); + return View::make('categories.index')->with('categories', $categories); } public function show(Category $category) { - return View::make('categories.show')->with('category',$category); + $start = \Session::get('start'); + $end = \Session::get('end'); + + + $journals = $this->_category->journalsInRange($category, $start, $end); + + return View::make('categories.show')->with('category', $category)->with('journals',$journals); } public function store() { + $category = $this->_repository->store(Input::all()); + if ($category->id) { + Session::flash('success', 'Category created!'); + + if (Input::get('create') == '1') { + return Redirect::route('categories.create'); + } + return Redirect::route('categories.index'); + } else { + Session::flash('error', 'Could not save the new category!'); + return Redirect::route('categories.create')->withInput(); + } } public function update() { + $category = $this->_repository->update(Input::all()); + Session::flash('success', 'Category "' . $category->name . '" updated.'); + + return Redirect::route('categories.index'); } diff --git a/app/controllers/ChartController.php b/app/controllers/ChartController.php index 20abf95400..6e8388241c 100644 --- a/app/controllers/ChartController.php +++ b/app/controllers/ChartController.php @@ -110,5 +110,21 @@ class ChartController extends BaseController return Response::json($this->_chart->categories($start, $end)); + } + public function categoryShowChart(Category $category) { + $start = Session::get('start'); + $end = Session::get('end'); + $range = Session::get('range'); + + $serie = $this->_chart->categoryShowChart($category, $range, $start, $end); + $data = [ + 'chart_title' => $category->name, + 'subtitle' => 'View more', + 'series' => $serie + ]; + return Response::json($data); + + + } } \ No newline at end of file diff --git a/app/lib/Firefly/Helper/Controllers/Category.php b/app/lib/Firefly/Helper/Controllers/Category.php new file mode 100644 index 0000000000..1f31dd9c04 --- /dev/null +++ b/app/lib/Firefly/Helper/Controllers/Category.php @@ -0,0 +1,24 @@ +transactionjournals()-> + with(['transactions','transactions.account','transactiontype','components'])-> + + orderBy('date','DESC')->orderBy('id','DESC')->before($end)->after($start)->get(); + + } +} \ No newline at end of file diff --git a/app/lib/Firefly/Helper/Controllers/CategoryInterface.php b/app/lib/Firefly/Helper/Controllers/CategoryInterface.php new file mode 100644 index 0000000000..621c6a6600 --- /dev/null +++ b/app/lib/Firefly/Helper/Controllers/CategoryInterface.php @@ -0,0 +1,18 @@ + [], - 'sum' => 0 + 'sum' => 0 ]; if ($account) { // get journals in range: @@ -85,7 +85,7 @@ class Chart implements ChartInterface $data = []; $budgets = \Auth::user()->budgets()->with( - ['limits' => function ($q) { + ['limits' => function ($q) { $q->orderBy('limits.startdate', 'ASC'); }, 'limits.limitrepetitions' => function ($q) use ($start) { $q->orderBy('limit_repetitions.startdate', 'ASC'); @@ -154,6 +154,136 @@ class Chart implements ChartInterface return $data; } + public function categoryShowChart(\Category $category, $range, Carbon $start, Carbon $end) + { + $data = ['name' => $category->name . ' per ' . $range, 'data' => []]; + // go back twelve periods. Skip if empty. + $beginning = clone $start; + switch ($range) { + default: + throw new FireflyException('No beginning for range ' . $range); + break; + case '1D': + $beginning->subDays(12); + break; + case '1W': + $beginning->subWeeks(12); + break; + case '1M': + $beginning->subYear(); + break; + case '3M': + $beginning->subYears(3); + break; + case '6M': + $beginning->subYears(6); + break; + } + // loop over the periods: + while ($beginning <= $start) { + // increment currentEnd to fit beginning: + $currentEnd = clone $beginning; + // increase beginning for next round: + switch ($range) { + default: + throw new FireflyException('No currentEnd incremental for range ' . $range); + break; + case '1D': + break; + case '1W': + $currentEnd->addWeek()->subDay(); + break; + case '1M': + $currentEnd->addMonth()->subDay(); + break; + case '3M': + $currentEnd->addMonths(3)->subDay(); + break; + case '6M': + $currentEnd->addMonths(6)->subDay(); + + } + + // now format the current range: + $title = ''; + switch ($range) { + default: + throw new \Firefly\Exception\FireflyException('No date formats for frequency "' . $range . '"!'); + break; + case '1D': + $title = $beginning->format('j F Y'); + break; + case '1W': + $title = $beginning->format('\W\e\e\k W, Y'); + break; + case '1M': + $title = $beginning->format('F Y'); + break; + case '3M': + case '6M': + $title = $beginning->format('M Y') . ' - ' . $currentEnd->format('M Y'); + break; + case 'yearly': +// return $this->startdate->format('Y'); + break; + } + + // get sum for current range: + $journals = \TransactionJournal:: + with( + ['transactions' => function ($q) { + $q->where('amount', '>', 0); + }] + ) + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->where('transaction_types.type', 'Withdrawal') + ->leftJoin('component_transaction_journal', 'component_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('components', 'components.id', '=', 'component_transaction_journal.component_id') + ->where('components.id', '=', $category->id) + //->leftJoin() + ->after($beginning)->before($currentEnd) + ->where('completed', 1) + ->get(['transaction_journals.*']); + $currentSum = 0; + foreach ($journals as $journal) { + if (!isset($journal->transactions[0])) { + throw new FireflyException('Journal #' . $journal->id . ' has ' . count($journal->transactions) + . ' transactions!'); + } + $transaction = $journal->transactions[0]; + $amount = floatval($transaction->amount); + $currentSum += $amount; + + } + $data['data'][] = [$title, $currentSum]; + + // increase beginning for next round: + switch ($range) { + default: + throw new FireflyException('No incremental for range ' . $range); + break; + case '1D': + $beginning->addDay(); + break; + case '1W': + $beginning->addWeek(); + break; + case '1M': + $beginning->addMonth(); + break; + case '3M': + $beginning->addMonths(3); + break; + case '6M': + $beginning->addMonths(6); + break; + } + } + return $data; + + + } + public function categories(Carbon $start, Carbon $end) { @@ -192,8 +322,7 @@ class Chart implements ChartInterface // sort arsort($result); - $chartData = [ - ]; + $chartData = []; foreach ($result as $name => $value) { $chartData[] = [$name, $value]; } @@ -202,15 +331,4 @@ class Chart implements ChartInterface return $chartData; } - public function accountXX(\Account $account) - { - $data = [ - 'chart_title' => $account->name, - 'subtitle' => 'View more', - 'series' => [$this->_account($account)] - ]; - - return $data; - } - } \ No newline at end of file diff --git a/app/lib/Firefly/Helper/Controllers/ChartInterface.php b/app/lib/Firefly/Helper/Controllers/ChartInterface.php index 3839b546f9..c5e9f9e1bb 100644 --- a/app/lib/Firefly/Helper/Controllers/ChartInterface.php +++ b/app/lib/Firefly/Helper/Controllers/ChartInterface.php @@ -21,4 +21,6 @@ interface ChartInterface public function budgets(Carbon $start); public function accountDailySummary(\Account $account, Carbon $date); + + public function categoryShowChart(\Category $category, $range, Carbon $start, Carbon $end); } \ No newline at end of file diff --git a/app/lib/Firefly/Helper/HelperServiceProvider.php b/app/lib/Firefly/Helper/HelperServiceProvider.php index c60f39553a..2b812009c4 100644 --- a/app/lib/Firefly/Helper/HelperServiceProvider.php +++ b/app/lib/Firefly/Helper/HelperServiceProvider.php @@ -26,6 +26,10 @@ class HelperServiceProvider extends ServiceProvider 'Firefly\Helper\Controllers\ChartInterface', 'Firefly\Helper\Controllers\Chart' ); + $this->app->bind( + 'Firefly\Helper\Controllers\CategoryInterface', + 'Firefly\Helper\Controllers\Category' + ); $this->app->bind( 'Firefly\Helper\Controllers\BudgetInterface', diff --git a/app/lib/Firefly/Storage/Budget/BudgetRepositoryInterface.php b/app/lib/Firefly/Storage/Budget/BudgetRepositoryInterface.php index 9decd1ca5b..88debbad6b 100644 --- a/app/lib/Firefly/Storage/Budget/BudgetRepositoryInterface.php +++ b/app/lib/Firefly/Storage/Budget/BudgetRepositoryInterface.php @@ -40,7 +40,7 @@ interface BudgetRepositoryInterface * * @return mixed */ - public function destroy($data); + public function destroy($budgetId); /** * @param $budgetId diff --git a/app/lib/Firefly/Storage/Category/CategoryRepositoryInterface.php b/app/lib/Firefly/Storage/Category/CategoryRepositoryInterface.php index efffd51d64..f6ab03d581 100644 --- a/app/lib/Firefly/Storage/Category/CategoryRepositoryInterface.php +++ b/app/lib/Firefly/Storage/Category/CategoryRepositoryInterface.php @@ -14,6 +14,7 @@ interface CategoryRepositoryInterface * @return mixed */ public function get(); + public function find($categoryId); /** * @param $name @@ -30,10 +31,19 @@ interface CategoryRepositoryInterface public function findByName($name); /** - * @param $name + * @param $data * * @return mixed */ - public function store($name); + public function store($data); + + public function update($data); + + /** + * @param $data + * + * @return mixed + */ + public function destroy($categoryId); } \ No newline at end of file diff --git a/app/lib/Firefly/Storage/Category/EloquentCategoryRepository.php b/app/lib/Firefly/Storage/Category/EloquentCategoryRepository.php index d3b727edff..c703200dbd 100644 --- a/app/lib/Firefly/Storage/Category/EloquentCategoryRepository.php +++ b/app/lib/Firefly/Storage/Category/EloquentCategoryRepository.php @@ -14,7 +14,12 @@ class EloquentCategoryRepository implements CategoryRepositoryInterface */ public function get() { - return \Auth::user()->categories()->orderBy('name','ASC')->get(); + return \Auth::user()->categories()->orderBy('name', 'ASC')->get(); + } + + public function find($categoryId) + { + return \Auth::user()->categories()->find($categoryId); } /** @@ -26,7 +31,7 @@ class EloquentCategoryRepository implements CategoryRepositoryInterface { $category = $this->findByName($name); if (!$category) { - return $this->store($name); + return $this->store(['name' => $name]); } return $category; @@ -54,14 +59,39 @@ class EloquentCategoryRepository implements CategoryRepositoryInterface * * @return \Category|mixed */ - public function store($name) + public function store($data) { - $category = new \Category(); - $category->name = $name; + $category = new \Category; + $category->name = $data['name']; + $category->user()->associate(\Auth::user()); $category->save(); + return $category; + } + + public function update($data) + { + $category = $this->find($data['id']); + if ($category) { + // update account accordingly: + $category->name = $data['name']; + if ($category->validate()) { + $category->save(); + } + } return $category; } + public function destroy($categoryId) + { + $category = $this->find($categoryId); + if ($category) { + $category->delete(); + + return true; + } + + return false; + } } \ No newline at end of file diff --git a/app/lib/Firefly/Storage/StorageServiceProvider.php b/app/lib/Firefly/Storage/StorageServiceProvider.php index 9d032a4e52..2fa2f0f518 100644 --- a/app/lib/Firefly/Storage/StorageServiceProvider.php +++ b/app/lib/Firefly/Storage/StorageServiceProvider.php @@ -27,6 +27,8 @@ class StorageServiceProvider extends ServiceProvider ); + + $this->app->bind( 'Firefly\Storage\Account\AccountRepositoryInterface', 'Firefly\Storage\Account\EloquentAccountRepository' diff --git a/app/models/Category.php b/app/models/Category.php index 545f2b9ad8..067f795bc0 100644 --- a/app/models/Category.php +++ b/app/models/Category.php @@ -29,4 +29,9 @@ class Category extends Component 'class' => 'Category' ]; protected $isSubclass = true; + + public function transactionjournals() + { + return $this->belongsToMany('TransactionJournal', 'component_transaction_journal', 'component_id'); + } } \ No newline at end of file diff --git a/app/routes.php b/app/routes.php index 680485e96f..edadc1de26 100644 --- a/app/routes.php +++ b/app/routes.php @@ -20,6 +20,15 @@ Route::bind('budget', function($value, $route) return null; }); +Route::bind('category', function($value, $route) +{ + if(Auth::check()) { + return Category:: + where('id', $value)-> + where('user_id',Auth::user()->id)->first(); + } + return null; +}); // protected routes: @@ -34,6 +43,7 @@ Route::group(['before' => 'auth'], function () { 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']); + Route::get('/chart/categories/show/{category}', ['uses' => 'ChartController@categoryShowChart','as' => 'chart.showcategory']); // Categories controller: Route::get('/categories',['uses' => 'CategoryController@index','as' => 'categories.index']); @@ -100,6 +110,11 @@ Route::group(['before' => 'csrf|auth'], function () { Route::post('/budgets/update', ['uses' => 'BudgetController@update', 'as' => 'budgets.update']); Route::post('/budgets/destroy', ['uses' => 'BudgetController@destroy', 'as' => 'budgets.destroy']); + // category controller + Route::post('/categories/store',['uses' => 'CategoryController@store', 'as' => 'categories.store']); + Route::post('/categories/update', ['uses' => 'CategoryController@update', 'as' => 'categories.update']); + Route::post('/categories/destroy', ['uses' => 'CategoryController@destroy', 'as' => 'categories.destroy']); + // migration controller Route::post('/migrate', ['uses' => 'MigrationController@postIndex']); diff --git a/app/views/categories/create.blade.php b/app/views/categories/create.blade.php new file mode 100644 index 0000000000..464445d4db --- /dev/null +++ b/app/views/categories/create.blade.php @@ -0,0 +1,64 @@ +@extends('layouts.default') +@section('content') +
+
+

Firefly + Create a category +

+

Use categories to group your expenses

+

+ Use categories to group expenses by hobby, for certain types of groceries or what bills are for. + Expenses grouped in categories do not have to reoccur every month or every week, like budgets. +

+
+
+ +{{Form::open(['class' => 'form-horizontal','url' => route('categories.store')])}} + +
+
+

Mandatory fields

+ +
+ +
+ + @if($errors->has('name')) +

{{$errors->first('name')}}

+ @else + For example: bike, utilities, daily groceries + @endif +
+
+ +
+
+ +
+
+ + +
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ +{{Form::close()}} + + +@stop diff --git a/app/views/categories/delete.blade.php b/app/views/categories/delete.blade.php new file mode 100644 index 0000000000..4931e0c267 --- /dev/null +++ b/app/views/categories/delete.blade.php @@ -0,0 +1,46 @@ +@extends('layouts.default') +@section('content') +
+
+

Firefly + Delete "{{{$category->name}}}" +

+

+ Remember that deleting something is permanent. +

+
+
+ +{{Form::open(['class' => 'form-horizontal','url' => route('categories.destroy')])}} +{{Form::hidden('id',$category->id)}} +
+
+ @if($category->transactionjournals()->count() > 0) +

+ + Account "{{{$category->name}}}" still has {{$category->transactionjournals()->count()}} transaction(s) associated to it. + These will NOT be deleted but will lose their connection to the category. +

+ @endif + +

+ Press "Delete permanently" If you are sure you want to delete "{{{$category->name}}}". +

+
+ +
+ +
+
+
+
+ + Cancel +
+
+
+
+ + +{{Form::close()}} +@stop \ No newline at end of file diff --git a/app/views/categories/edit.blade.php b/app/views/categories/edit.blade.php new file mode 100644 index 0000000000..400dc67d55 --- /dev/null +++ b/app/views/categories/edit.blade.php @@ -0,0 +1,50 @@ +@extends('layouts.default') +@section('content') +
+
+

Firefly + Edit category "{{{$category->name}}}" +

+

Use categories to group your expenses

+
+
+ +{{Form::open(['class' => 'form-horizontal','url' => route('categories.update')])}} + +{{Form::hidden('id',$category->id)}} + +
+
+

Mandatory fields

+ +
+ +
+ + @if($errors->has('name')) +

{{$errors->first('name')}}

+ @else + For example: bike, utilities, daily groceries + @endif +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+
+ +{{Form::close()}} + + +@stop diff --git a/app/views/categories/index.blade.php b/app/views/categories/index.blade.php index a99f39d25f..e035ecafa8 100644 --- a/app/views/categories/index.blade.php +++ b/app/views/categories/index.blade.php @@ -6,6 +6,10 @@ Categories

Use categories to group your expenses

+

+ Use categories to group expenses by hobby, for certain types of groceries or what bills are for. + Expenses grouped in categories do not have to reoccur every month or every week, like budgets. +

diff --git a/app/views/categories/show.blade.php b/app/views/categories/show.blade.php new file mode 100644 index 0000000000..a92de10f34 --- /dev/null +++ b/app/views/categories/show.blade.php @@ -0,0 +1,47 @@ +@extends('layouts.default') +@section('content') + + + +
+
+

Firefly + Category "{{{$category->name}}}" +

+

Use categories to group your expenses

+

+ Use categories to group expenses by hobby, for certain types of groceries or what bills are for. + Expenses grouped in categories do not have to reoccur every month or every week, like budgets. +

+

+ This overview will show you the expenses you've made in each [period] and show you the actual + transactions for the currently selected period. +

+
+
+ + +@include('partials.date_nav') +
+
+

(Some chart here)

+
+
+ + +
+
+

Transactions in current range

+ @include('lists.transactions',['journals' => $journals,'sum' => true]) +
+
+ + + +@stop +@section('scripts') + + +@stop \ No newline at end of file diff --git a/app/views/layouts/default.blade.php b/app/views/layouts/default.blade.php index 5af643dd8f..56a7c66f1d 100644 --- a/app/views/layouts/default.blade.php +++ b/app/views/layouts/default.blade.php @@ -32,13 +32,6 @@ @include('partials.flashes') @yield('content') - - - @yield('scripts')