From cdd5a6c2254c8d8af3a3ed2fd9793b834ef674c5 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 7 Aug 2014 07:44:37 +0200 Subject: [PATCH] Stuff for recurring transactions [skip ci] --- app/assets/javascripts/firefly/recurring.js | 0 app/assets/javascripts/recurring.js | 14 ++ .../tagsinput/bootstrap-tagsinput.css | 48 +++++ .../tagsinput/bootstrap-tagsinput.min.js | 7 + app/assets/stylesheets/recurring.css | 13 ++ app/controllers/RecurringController.php | 37 +++- app/lib/Firefly/Helper/Controllers/Budget.php | 24 ++- .../Budget/EloquentBudgetRepository.php | 2 +- ...EloquentRecurringTransactionRepository.php | 34 ++- ...ecurringTransactionRepositoryInterface.php | 4 + .../EloquentTransactionJournalRepository.php | 4 +- app/models/RecurringTransaction.php | 60 ++++-- app/routes.php | 19 ++ app/views/accounts/show.blade.php | 2 +- app/views/budgets/indexByBudget.blade.php | 2 + app/views/lists/transactions.blade.php | 2 +- app/views/recurring/create.blade.php | 196 ++++++++++++++++++ app/views/recurring/delete.blade.php | 37 ++++ app/views/recurring/index.blade.php | 47 ++++- 19 files changed, 523 insertions(+), 29 deletions(-) create mode 100644 app/assets/javascripts/firefly/recurring.js create mode 100644 app/assets/javascripts/recurring.js create mode 100755 app/assets/javascripts/tagsinput/bootstrap-tagsinput.css create mode 100755 app/assets/javascripts/tagsinput/bootstrap-tagsinput.min.js create mode 100644 app/assets/stylesheets/recurring.css create mode 100644 app/views/recurring/create.blade.php create mode 100644 app/views/recurring/delete.blade.php diff --git a/app/assets/javascripts/firefly/recurring.js b/app/assets/javascripts/firefly/recurring.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/assets/javascripts/recurring.js b/app/assets/javascripts/recurring.js new file mode 100644 index 0000000000..123611d7e9 --- /dev/null +++ b/app/assets/javascripts/recurring.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 tagsinput/bootstrap-tagsinput.min +//= require firefly/recurring \ No newline at end of file diff --git a/app/assets/javascripts/tagsinput/bootstrap-tagsinput.css b/app/assets/javascripts/tagsinput/bootstrap-tagsinput.css new file mode 100755 index 0000000000..54f3f24f1f --- /dev/null +++ b/app/assets/javascripts/tagsinput/bootstrap-tagsinput.css @@ -0,0 +1,48 @@ +.bootstrap-tagsinput { + background-color: #fff; + border: 1px solid #ccc; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + display: inline-block; + padding: 4px 6px; + margin-bottom: 10px; + color: #555; + vertical-align: middle; + border-radius: 4px; + max-width: 100%; + line-height: 22px; + cursor: text; +} +.bootstrap-tagsinput input { + border: none; + box-shadow: none; + outline: none; + background-color: transparent; + padding: 0; + margin: 0; + width: auto !important; + max-width: inherit; +} +.bootstrap-tagsinput input:focus { + border: none; + box-shadow: none; +} +.bootstrap-tagsinput .tag { + margin-right: 2px; + color: white; +} +.bootstrap-tagsinput .tag [data-role="remove"] { + margin-left: 8px; + cursor: pointer; +} +.bootstrap-tagsinput .tag [data-role="remove"]:after { + content: "x"; + padding: 0px 2px; +} +.bootstrap-tagsinput .tag [data-role="remove"]:hover { + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} +.bootstrap-tagsinput .tag [data-role="remove"]:hover:active { + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.bootstrap-tagsinput {width:100%;} \ No newline at end of file diff --git a/app/assets/javascripts/tagsinput/bootstrap-tagsinput.min.js b/app/assets/javascripts/tagsinput/bootstrap-tagsinput.min.js new file mode 100755 index 0000000000..16be0abb93 --- /dev/null +++ b/app/assets/javascripts/tagsinput/bootstrap-tagsinput.min.js @@ -0,0 +1,7 @@ +/* + * bootstrap-tagsinput v0.4.2 by Tim Schlechter + * + */ + +!function(a){"use strict";function b(b,c){this.itemsArray=[],this.$element=a(b),this.$element.hide(),this.isSelect="SELECT"===b.tagName,this.multiple=this.isSelect&&b.hasAttribute("multiple"),this.objectItems=c&&c.itemValue,this.placeholderText=b.hasAttribute("placeholder")?this.$element.attr("placeholder"):"",this.inputSize=Math.max(1,this.placeholderText.length),this.$container=a('
'),this.$input=a('').appendTo(this.$container),this.$element.after(this.$container);var d=(this.inputSize<3?3:this.inputSize)+"em";this.$input.get(0).style.cssText="width: "+d+" !important;",this.build(c)}function c(a,b){if("function"!=typeof a[b]){var c=a[b];a[b]=function(a){return a[c]}}}function d(a,b){if("function"!=typeof a[b]){var c=a[b];a[b]=function(){return c}}}function e(a){return a?i.text(a).html():""}function f(a){var b=0;if(document.selection){a.focus();var c=document.selection.createRange();c.moveStart("character",-a.value.length),b=c.text.length}else(a.selectionStart||"0"==a.selectionStart)&&(b=a.selectionStart);return b}function g(b,c){var d=!1;return a.each(c,function(a,c){if("number"==typeof c&&b.which===c)return d=!0,!1;if(b.which===c.which){var e=!c.hasOwnProperty("altKey")||b.altKey===c.altKey,f=!c.hasOwnProperty("shiftKey")||b.shiftKey===c.shiftKey,g=!c.hasOwnProperty("ctrlKey")||b.ctrlKey===c.ctrlKey;if(e&&f&&g)return d=!0,!1}}),d}var h={tagClass:function(){return"label label-info"},itemValue:function(a){return a?a.toString():a},itemText:function(a){return this.itemValue(a)},freeInput:!0,addOnBlur:!0,maxTags:void 0,maxChars:void 0,confirmKeys:[13,44],onTagExists:function(a,b){b.hide().fadeIn()},trimValue:!1,allowDuplicates:!1};b.prototype={constructor:b,add:function(b,c){var d=this;if(!(d.options.maxTags&&d.itemsArray.length>=d.options.maxTags||b!==!1&&!b)){if("string"==typeof b&&d.options.trimValue&&(b=a.trim(b)),"object"==typeof b&&!d.objectItems)throw"Can't add objects when itemValue option is not set";if(!b.toString().match(/^\s*$/)){if(d.isSelect&&!d.multiple&&d.itemsArray.length>0&&d.remove(d.itemsArray[0]),"string"==typeof b&&"INPUT"===this.$element[0].tagName){var f=b.split(",");if(f.length>1){for(var g=0;gd.options.maxInputLength)){var l=a.Event("beforeItemAdd",{item:b,cancel:!1});if(d.$element.trigger(l),!l.cancel){d.itemsArray.push(b);var m=a(''+e(i)+'');if(m.data("item",b),d.findInputWrapper().before(m),m.after(" "),d.isSelect&&!a('option[value="'+encodeURIComponent(h)+'"]',d.$element)[0]){var n=a("");n.data("item",b),n.attr("value",h),d.$element.append(n)}c||d.pushVal(),(d.options.maxTags===d.itemsArray.length||d.items().toString().length===d.options.maxInputLength)&&d.$container.addClass("bootstrap-tagsinput-max"),d.$element.trigger(a.Event("itemAdded",{item:b}))}}}else if(d.options.onTagExists){var o=a(".tag",d.$container).filter(function(){return a(this).data("item")===k});d.options.onTagExists(b,o)}}}},remove:function(b,c){var d=this;if(d.objectItems&&(b="object"==typeof b?a.grep(d.itemsArray,function(a){return d.options.itemValue(a)==d.options.itemValue(b)}):a.grep(d.itemsArray,function(a){return d.options.itemValue(a)==b}),b=b[b.length-1]),b){var e=a.Event("beforeItemRemove",{item:b,cancel:!1});if(d.$element.trigger(e),e.cancel)return;a(".tag",d.$container).filter(function(){return a(this).data("item")===b}).remove(),a("option",d.$element).filter(function(){return a(this).data("item")===b}).remove(),-1!==a.inArray(b,d.itemsArray)&&d.itemsArray.splice(a.inArray(b,d.itemsArray),1)}c||d.pushVal(),d.options.maxTags>d.itemsArray.length&&d.$container.removeClass("bootstrap-tagsinput-max"),d.$element.trigger(a.Event("itemRemoved",{item:b}))},removeAll:function(){var b=this;for(a(".tag",b.$container).remove(),a("option",b.$element).remove();b.itemsArray.length>0;)b.itemsArray.pop();b.pushVal()},refresh:function(){var b=this;a(".tag",b.$container).each(function(){var c=a(this),d=c.data("item"),f=b.options.itemValue(d),g=b.options.itemText(d),h=b.options.tagClass(d);if(c.attr("class",null),c.addClass("tag "+e(h)),c.contents().filter(function(){return 3==this.nodeType})[0].nodeValue=e(g),b.isSelect){var i=a("option",b.$element).filter(function(){return a(this).data("item")===d});i.attr("value",f)}})},items:function(){return this.itemsArray},pushVal:function(){var b=this,c=a.map(b.items(),function(a){return b.options.itemValue(a).toString()});b.$element.val(c,!0).trigger("change")},build:function(b){var e=this;if(e.options=a.extend({},h,b),e.objectItems&&(e.options.freeInput=!1),c(e.options,"itemValue"),c(e.options,"itemText"),d(e.options,"tagClass"),e.options.typeahead){var i=e.options.typeahead||{};d(i,"source"),e.$input.typeahead(a.extend({},i,{source:function(b,c){function d(a){for(var b=[],d=0;d$1")}}))}if(e.options.typeaheadjs){var j=e.options.typeaheadjs||{};e.$input.typeahead(null,j).on("typeahead:selected",a.proxy(function(a,b){e.add(j.valueKey?b[j.valueKey]:b),e.$input.typeahead("val","")},e))}e.$container.on("click",a.proxy(function(){e.$element.attr("disabled")||e.$input.removeAttr("disabled"),e.$input.focus()},e)),e.options.addOnBlur&&e.options.freeInput&&e.$input.on("focusout",a.proxy(function(){0===a(".typeahead, .twitter-typeahead",e.$container).length&&(e.add(e.$input.val()),e.$input.val(""))},e)),e.$container.on("keydown","input",a.proxy(function(b){var c=a(b.target),d=e.findInputWrapper();if(e.$element.attr("disabled"))return void e.$input.attr("disabled","disabled");switch(b.which){case 8:if(0===f(c[0])){var g=d.prev();g&&e.remove(g.data("item"))}break;case 46:if(0===f(c[0])){var h=d.next();h&&e.remove(h.data("item"))}break;case 37:var i=d.prev();0===c.val().length&&i[0]&&(i.before(d),c.focus());break;case 39:var j=d.next();0===c.val().length&&j[0]&&(j.after(d),c.focus())}{var k=c.val().length;Math.ceil(k/5)}c.attr("size",Math.max(this.inputSize,c.val().length))},e)),e.$container.on("keypress","input",a.proxy(function(b){var c=a(b.target);if(e.$element.attr("disabled"))return void e.$input.attr("disabled","disabled");var d=c.val(),f=e.options.maxChars&&d.length>=e.options.maxChars;e.options.freeInput&&(g(b,e.options.confirmKeys)||f)&&(e.add(f?d.substr(0,e.options.maxChars):d),c.val(""),b.preventDefault());{var h=c.val().length;Math.ceil(h/5)}c.attr("size",Math.max(this.inputSize,c.val().length))},e)),e.$container.on("click","[data-role=remove]",a.proxy(function(b){e.$element.attr("disabled")||e.remove(a(b.target).closest(".tag").data("item"))},e)),e.options.itemValue===h.itemValue&&("INPUT"===e.$element[0].tagName?e.add(e.$element.val()):a("option",e.$element).each(function(){e.add(a(this).attr("value"),!0)}))},destroy:function(){var a=this;a.$container.off("keypress","input"),a.$container.off("click","[role=remove]"),a.$container.remove(),a.$element.removeData("tagsinput"),a.$element.show()},focus:function(){this.$input.focus()},input:function(){return this.$input},findInputWrapper:function(){for(var b=this.$input[0],c=this.$container[0];b&&b.parentNode!==c;)b=b.parentNode;return a(b)}},a.fn.tagsinput=function(c,d){var e=[];return this.each(function(){var f=a(this).data("tagsinput");if(f)if(c||d){if(void 0!==f[c]){var g=f[c](d);void 0!==g&&e.push(g)}}else e.push(f);else f=new b(this,c),a(this).data("tagsinput",f),e.push(f),"SELECT"===this.tagName&&a("option",a(this)).attr("selected","selected"),a(this).val(a(this).val())}),"string"==typeof c?e.length>1?e:e[0]:e},a.fn.tagsinput.Constructor=b;var i=a("
");a(function(){a("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput()})}(window.jQuery); +//# sourceMappingURL=bootstrap-tagsinput.min.js.map \ No newline at end of file diff --git a/app/assets/stylesheets/recurring.css b/app/assets/stylesheets/recurring.css new file mode 100644 index 0000000000..84680f30d6 --- /dev/null +++ b/app/assets/stylesheets/recurring.css @@ -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 tagsinput/bootstrap-tagsinput + */ \ No newline at end of file diff --git a/app/controllers/RecurringController.php b/app/controllers/RecurringController.php index 5b01dae6f3..a70e92561e 100644 --- a/app/controllers/RecurringController.php +++ b/app/controllers/RecurringController.php @@ -5,21 +5,36 @@ use Firefly\Storage\RecurringTransaction\RecurringTransactionRepositoryInterface class RecurringController extends BaseController { protected $_repository; + public function __construct(RTR $repository) { $this->_repository = $repository; View::share('menu', 'home'); } + public function create() { + $periods = \Config::get('firefly.periods_to_text'); + + return View::make('recurring.create')->with('periods', $periods); } - public function delete() + public function delete(RecurringTransaction $recurringTransaction) { + return View::make('recurring.delete')->with('recurringTransaction', $recurringTransaction); } - public function destroy() + public function destroy(RecurringTransaction $recurringTransaction) { + $result = $this->_repository->destroy($recurringTransaction); + if ($result === true) { + Session::flash('success', 'The recurring transaction was deleted.'); + } else { + Session::flash('error', 'Could not delete the recurring transaction. Check the logs to be sure.'); + } + + return Redirect::route('recurring.index'); + } public function edit() @@ -29,7 +44,8 @@ class RecurringController extends BaseController public function index() { $list = $this->_repository->get(); - return View::make('recurring.index'); + + return View::make('recurring.index')->with('list', $list); } public function show() @@ -38,6 +54,21 @@ class RecurringController extends BaseController public function store() { + $recurringTransaction = $this->_repository->store(Input::all()); + if ($recurringTransaction->id) { + Session::flash('success', 'Recurring transaction "' . $recurringTransaction->name . '" saved!'); + if (Input::get('create') == '1') { + return Redirect::route('recurring.create')->withInput(); + } else { + return Redirect::route('recurring.index'); + } + } else { + Session::flash( + 'error', 'Could not save the recurring transaction: ' . $recurringTransaction->errors()->first() + ); + + return Redirect::route('recurring.create')->withInput()->withErrors($recurringTransaction->errors()); + } } public function update() diff --git a/app/lib/Firefly/Helper/Controllers/Budget.php b/app/lib/Firefly/Helper/Controllers/Budget.php index b698c12b47..d2a9884718 100644 --- a/app/lib/Firefly/Helper/Controllers/Budget.php +++ b/app/lib/Firefly/Helper/Controllers/Budget.php @@ -134,15 +134,23 @@ class Budget implements BudgetInterface } - - $query = $budget->transactionjournals()->with( - 'transactions', 'transactions.account', 'components', 'transactiontype', - 'transactions.account.accounttype' - )->whereNotIn( - 'transaction_journals.id', $inRepetition + if (count($inRepetition) > 0) { + $query = $budget->transactionjournals()->with( + 'transactions', 'transactions.account', 'components', 'transactiontype', + 'transactions.account.accounttype' + )->whereNotIn( + 'transaction_journals.id', $inRepetition + )->orderBy('date', 'DESC')->orderBy( + 'transaction_journals.id', 'DESC' + ); + } else { + $query = $budget->transactionjournals()->with( + 'transactions', 'transactions.account', 'components', 'transactiontype', + 'transactions.account.accounttype' )->orderBy('date', 'DESC')->orderBy( - 'transaction_journals.id', 'DESC' - ); + 'transaction_journals.id', 'DESC' + ); + } // build paginator: $perPage = 25; diff --git a/app/lib/Firefly/Storage/Budget/EloquentBudgetRepository.php b/app/lib/Firefly/Storage/Budget/EloquentBudgetRepository.php index f1ce5e3fe8..8baf9580a9 100644 --- a/app/lib/Firefly/Storage/Budget/EloquentBudgetRepository.php +++ b/app/lib/Firefly/Storage/Budget/EloquentBudgetRepository.php @@ -124,7 +124,7 @@ class EloquentBudgetRepository implements BudgetRepositoryInterface } $limit->startdate = $startDate; $limit->amount = $data['amount']; - $limit->repeats = $data['repeats']; + $limit->repeats = isset($data['repeats']) ? $data['repeats'] : 0; $limit->repeat_freq = $data['repeat_freq']; if ($limit->validate()) { $limit->save(); diff --git a/app/lib/Firefly/Storage/RecurringTransaction/EloquentRecurringTransactionRepository.php b/app/lib/Firefly/Storage/RecurringTransaction/EloquentRecurringTransactionRepository.php index 3348a3c6db..fe13c980f0 100644 --- a/app/lib/Firefly/Storage/RecurringTransaction/EloquentRecurringTransactionRepository.php +++ b/app/lib/Firefly/Storage/RecurringTransaction/EloquentRecurringTransactionRepository.php @@ -7,9 +7,41 @@ use Carbon\Carbon; class EloquentRecurringTransactionRepository implements RecurringTransactionRepositoryInterface { + public function destroy(\RecurringTransaction $recurringTransaction) { + $recurringTransaction->delete(); + return true; + } - public function get() { + public function get() + { return \Auth::user()->recurringtransactions()->get(); } + public function store($data) + { + $recurringTransaction = new \RecurringTransaction; + $recurringTransaction->user()->associate(\Auth::user()); + $recurringTransaction->name = $data['name']; + $recurringTransaction->match = join(' ', explode(',', $data['match'])); + $recurringTransaction->amount_max = floatval($data['amount_max']); + $recurringTransaction->amount_min = floatval($data['amount_min']); + + // both amounts zero: + if($recurringTransaction->amount_max == 0 && $recurringTransaction->amount_min == 0) { + $recurringTransaction->errors()->add('amount_max','Amount max and min cannot both be zero.'); + return $recurringTransaction; + } + + $recurringTransaction->date = new Carbon($data['date']); + $recurringTransaction->active = isset($data['active']) ? intval($data['active']) : 0; + $recurringTransaction->automatch = isset($data['automatch']) ? intval($data['automatch']) : 0; + $recurringTransaction->skip = isset($data['skip']) ? intval($data['skip']) : 0; + $recurringTransaction->repeat_freq = $data['repeat_freq']; + + if($recurringTransaction->validate()) { + $recurringTransaction->save(); + } + return $recurringTransaction; + } + } \ No newline at end of file diff --git a/app/lib/Firefly/Storage/RecurringTransaction/RecurringTransactionRepositoryInterface.php b/app/lib/Firefly/Storage/RecurringTransaction/RecurringTransactionRepositoryInterface.php index 2706c62172..ce1e40d311 100644 --- a/app/lib/Firefly/Storage/RecurringTransaction/RecurringTransactionRepositoryInterface.php +++ b/app/lib/Firefly/Storage/RecurringTransaction/RecurringTransactionRepositoryInterface.php @@ -9,5 +9,9 @@ interface RecurringTransactionRepositoryInterface public function get(); + public function store($data); + + public function destroy(\RecurringTransaction $recurringTransaction); + } \ No newline at end of file diff --git a/app/lib/Firefly/Storage/TransactionJournal/EloquentTransactionJournalRepository.php b/app/lib/Firefly/Storage/TransactionJournal/EloquentTransactionJournalRepository.php index fc04b223fb..aad984cf59 100644 --- a/app/lib/Firefly/Storage/TransactionJournal/EloquentTransactionJournalRepository.php +++ b/app/lib/Firefly/Storage/TransactionJournal/EloquentTransactionJournalRepository.php @@ -392,7 +392,9 @@ class EloquentTransactionJournalRepository implements TransactionJournalReposito // do budget: $budget = $budRepository->find($data['budget_id']); - $journal->budgets()->attach($budget); + if(!is_null($budget)) { + $journal->budgets()->attach($budget); + } break; case 'Deposit': diff --git a/app/models/RecurringTransaction.php b/app/models/RecurringTransaction.php index e60938e722..72cf81a330 100644 --- a/app/models/RecurringTransaction.php +++ b/app/models/RecurringTransaction.php @@ -6,28 +6,64 @@ class RecurringTransaction extends Ardent public static $rules = [ - 'user_id' => 'required|exists:users,id', - 'name' => 'required|between:1,255', - 'match' => 'required', - 'amount_max' => 'required|between:0,65536', - 'amount_min' => 'required|between:0,65536', - 'date' => 'required|date', - 'active' => 'required|between:0,1', - 'automatch' => 'required|between:0,1', + 'user_id' => 'required|exists:users,id', + 'name' => 'required|between:1,255', + 'match' => 'required', + 'amount_max' => 'required|between:0,65536', + 'amount_min' => 'required|between:0,65536', + 'date' => 'required|date', + 'active' => 'required|between:0,1', + 'automatch' => 'required|between:0,1', 'repeat_freq' => 'required|in:daily,weekly,monthly,quarterly,half-year,yearly', - 'skip' => 'required|between:0,31', + 'skip' => 'required|between:0,31', ]; public static $factory = [ 'user_id' => 'factory|User', - 'name' => 'string', - 'data' => 'string' + 'name' => 'string', + 'data' => 'string' ]; + public function getDates() + { + return ['created_at', 'updated_at', 'date']; + } + + public function next() + { + $start = clone $this->date; + $skip = $this->skip == 0 ? 1 : $this->skip; + + while ($start <= $this->date) { + switch ($this->repeat_freq) { + case 'daily': + $start->addDays($skip); + break; + case 'weekly': + $start->addWeeks($skip); + break; + case 'monthly': + $start->addMonths($skip); + break; + case 'quarterly': + $start->addMonths($skip); + break; + case 'half-year': + $start->addMonths($skip * 6); + break; + case 'yearly': + $this->addYears($skip); + break; + + } + } + + return $start; + } + public function user() { return $this->belongsTo('User'); } - } \ No newline at end of file diff --git a/app/routes.php b/app/routes.php index d843ce83e3..ee14a44cb2 100644 --- a/app/routes.php +++ b/app/routes.php @@ -20,6 +20,17 @@ Route::bind('accountname', function($value, $route) } return null; }); + + +Route::bind('recurring', function($value, $route) + { + if(Auth::check()) { + return RecurringTransaction:: + where('id', $value)-> + where('user_id',Auth::user()->id)->first(); + } + return null; + }); Route::bind('budget', function($value, $route) { if(Auth::check()) { @@ -136,6 +147,9 @@ Route::group(['before' => 'auth'], function () { // recurring transactions controller Route::get('/recurring',['uses' => 'RecurringController@index', 'as' => 'recurring.index']); + Route::get('/recurring/create',['uses' => 'RecurringController@create', 'as' => 'recurring.create']); + Route::get('/recurring/edit/{recurring}',['uses' => 'RecurringController@edit','as' => 'recurring.edit']); + Route::get('/recurring/delete/{recurring}',['uses' => 'RecurringController@delete','as' => 'recurring.delete']); // transaction controller: Route::get('/transactions/create/{what}', ['uses' => 'TransactionController@create', 'as' => 'transactions.create'])->where(['what' => 'withdrawal|deposit|transfer']); @@ -187,6 +201,11 @@ Route::group(['before' => 'csrf|auth'], function () { // profile controller Route::post('/profile/change-password', ['uses' => 'ProfileController@postChangePassword']); + // recurring controller + Route::post('/recurring/store',['uses' => 'RecurringController@store', 'as' => 'recurring.store']); + Route::post('/recurring/update/{recurring}',['uses' => 'RecurringController@update','as' => 'recurring.update']); + Route::post('/recurring/destroy/{recurring}',['uses' => 'RecurringController@destroy','as' => 'recurring.destroy']); + // transaction controller: Route::post('/transactions/store/{what}', ['uses' => 'TransactionController@store', 'as' => 'transactions.store'])->where(['what' => 'withdrawal|deposit|transfer']); Route::post('/transaction/update/{tj}',['uses' => 'TransactionController@update','as' => 'transactions.update']); diff --git a/app/views/accounts/show.blade.php b/app/views/accounts/show.blade.php index d81d40fb93..b1408b5e9b 100644 --- a/app/views/accounts/show.blade.php +++ b/app/views/accounts/show.blade.php @@ -35,7 +35,7 @@ Out {{mf($show['statistics']['period']['out'])}} - + {{mf($show['statistics']['period']['t_out'])}} diff --git a/app/views/budgets/indexByBudget.blade.php b/app/views/budgets/indexByBudget.blade.php index 479bfdb019..ecc5af452c 100644 --- a/app/views/budgets/indexByBudget.blade.php +++ b/app/views/budgets/indexByBudget.blade.php @@ -74,7 +74,9 @@
@if($limit->repeats == 1) diff --git a/app/views/lists/transactions.blade.php b/app/views/lists/transactions.blade.php index b41db9db0d..322802caf2 100644 --- a/app/views/lists/transactions.blade.php +++ b/app/views/lists/transactions.blade.php @@ -28,7 +28,7 @@ @foreach($journal->components as $component) @if($component->class == 'Budget') - + @endif @if($component->class == 'Category') diff --git a/app/views/recurring/create.blade.php b/app/views/recurring/create.blade.php new file mode 100644 index 0000000000..4b143f4ec8 --- /dev/null +++ b/app/views/recurring/create.blade.php @@ -0,0 +1,196 @@ +@extends('layouts.default') +@section('content') +
+
+

Firefly + Create a recurring transaction +

+

Use recurring transactions to track repeated expenses

+

+ Bla bla. +

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

Mandatory fields

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

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

+ @else + For example: rent, gas, insurance + @endif +
+
+
+ +
+ + @if($errors->has('match')) +

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

+ @else + For example: rent, [company name]. All matches need to + be present for the recurring transaction to be recognized. This field is not case-sensitive. + @endif +
+
+ +
+ {{ Form::label('amount_min', 'Minimum amount', ['class' => 'col-sm-4 control-label'])}} +
+
+ + {{Form::input('number','amount_min', Input::old('amount_min'), ['step' => 'any', 'class' => 'form-control'])}} +
+ + @if($errors->has('amount_min')) +

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

+ @else + Firefly will only include transactions with a higher amount than this. If your rent + is usually around € 500,-, enter 450 to be safe. + @endif +
+
+ +
+ {{ Form::label('amount_max', 'Maximum amount', ['class' => 'col-sm-4 control-label'])}} +
+
+ + {{Form::input('number','amount_max', Input::old('amount_max'), ['step' => 'any', 'class' => 'form-control'])}} +
+ + @if($errors->has('amount_max')) +

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

+ @else + Firefly will only include transactions with a lower amount than this. If your rent + is usually around € 500,-, enter 550 to be safe. + @endif +
+
+ +
+ {{ Form::label('date', 'Date', ['class' => 'col-sm-4 control-label'])}} +
+ {{ Form::input('date','date', Input::old('date') ?: date('Y-m-d'), ['class' + => 'form-control']) }} + @if($errors->has('date')) +

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

+ @else + Select the next date you expect the transaction to occur. + @endif +
+
+ +
+ +
+ {{Form::select('repeat_freq',$periods,Input::old('repeat_freq') ?: 'monthly',['class' => 'form-control'])}} + @if($errors->has('repeat_freq')) +

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

+ @else + Select the period over which this transaction repeats + @endif +
+
+ +
+
+

Optional fields

+ +
+ {{ Form::label('skip', 'Skip', ['class' => 'col-sm-4 control-label'])}} +
+ {{Form::input('number','skip', Input::old('skip') ?: 0, ['class' => 'form-control'])}} + + @if($errors->has('skip')) +

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

+ @else + Make Firefly skip every n times. Fill in 2, and Firefly + will match, skip, skip and match a transaction. + @endif +
+
+ + + + + + + + + +
+ +
+
+ +
+ Firefly will automatically match transactions. +
+
+ +
+ +
+
+ +
+ This recurring transaction is actually active. +
+
+ + + +
+
+ +
+
+ + +
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+ +{{Form::close()}} + + +@stop +@section('styles') + +@stop +@section('scripts') + + +@stop \ No newline at end of file diff --git a/app/views/recurring/delete.blade.php b/app/views/recurring/delete.blade.php new file mode 100644 index 0000000000..e94b5b6ac3 --- /dev/null +++ b/app/views/recurring/delete.blade.php @@ -0,0 +1,37 @@ +@extends('layouts.default') +@section('content') +
+
+

Firefly + Delete recurring transaction "{{{$recurringTransaction->name}}}" +

+

+ Remember that deleting something is permanent. +

+
+
+ +{{Form::open(['class' => 'form-horizontal','url' => route('recurring.destroy',$recurringTransaction->id)])}} +
+
+

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

+
+ +
+ +
+
+
+
+ + Cancel +
+
+
+
+ + +{{Form::close()}} +@stop \ No newline at end of file diff --git a/app/views/recurring/index.blade.php b/app/views/recurring/index.blade.php index c99c31ae74..e0f94a7a62 100644 --- a/app/views/recurring/index.blade.php +++ b/app/views/recurring/index.blade.php @@ -5,6 +5,7 @@

Firefly Recurring transactions

+

Use recurring transactions to track repeated expenses

We all have bills to pay. Firefly can help you organize those bills into recurring transactions, which are exactly what the name suggests. Firefly can match new (and existing) transactions to such a recurring transaction and help you organize these expenses into manageable groups. The front page of Firefly will show you which recurring @@ -22,10 +23,54 @@ Amount between Expected every Next expected match - Automatch + Auto-match Active + + @foreach($list as $entry) + + {{{$entry->name}}} + + @foreach(explode(' ',$entry->match) as $word) + {{{$word}}} + @endforeach + + + {{mf($entry->amount_min)}} – + {{mf($entry->amount_max)}} + + + {{$entry->repeat_freq}} + + + {{$entry->next()->format('d-m-Y')}} + + + @if($entry->automatch) + + @else + + @endif + + + @if($entry->active) + + @else + + @endif + + +

+ + +
+ + + @endforeach +

+ Create new recurring transaction +

@stop \ No newline at end of file