diff --git a/app/controllers/BudgetController.php b/app/controllers/BudgetController.php new file mode 100644 index 0000000000..1b3c466bd2 --- /dev/null +++ b/app/controllers/BudgetController.php @@ -0,0 +1,97 @@ +_budgets = $budgets; + View::share('menu', 'budgets'); + } + + public function index() + { + $budgets = $this->_budgets->get(); + $today = new \Carbon\Carbon; + + + return View::make('budgets.index')->with('budgets', $budgets)->with('today', $today); + } + + public function create() + { + + $periods = [ + 'weekly' => 'A week', + 'monthly' => 'A month', + 'quarterly' => 'A quarter', + 'half-year' => 'Six months', + 'yearly' => 'A year', + ]; + + return View::make('budgets.create')->with('periods', $periods); + } + + public function store() + { + + $data = [ + 'name' => Input::get('name'), + 'amount' => floatval(Input::get('amount')), + 'repeat_freq' => Input::get('period'), + 'repeats' => intval(Input::get('repeats')) + ]; + + $budget = $this->_budgets->create($data); + Session::flash('success', 'Budget created!'); + return Redirect::route('budgets.index'); + } + + public function show($budgetId) + { + $budget = $this->_budgets->find($budgetId); + + $list = $budget->transactionjournals()->get(); + $return = []; + /** @var \TransactionJournal $entry */ + foreach ($list as $entry) { + $month = $entry->date->format('F Y'); + $return[$month] = isset($return[$month]) ? $return[$month] : []; + + $return[$month][] = $entry; + + } + + foreach ($return as $month => $set) { + echo '

' . $month . '

'; + /** @var \TransactionJournal $tj */ + $sum = 0; + foreach ($set as $tj) { + echo '#' . $tj->id . ' ' . $tj->description . ': '; + + foreach ($tj->transactions as $index => $t) { + echo $t->amount . ', '; + if ($index == 0) { + $sum += $t->amount; + + } + } + echo '
'; + + } + echo 'sum: ' . $sum . '

'; + } + + + exit; + + return View::make('budgets.show'); + + + } + +} \ No newline at end of file diff --git a/app/controllers/HomeController.php b/app/controllers/HomeController.php index 948494bf1d..adaaf4fdaa 100644 --- a/app/controllers/HomeController.php +++ b/app/controllers/HomeController.php @@ -23,6 +23,9 @@ class HomeController extends BaseController $this->_preferences = $preferences; $this->_journal = $journal; View::share('menu', 'home'); + + + } /** @@ -30,10 +33,28 @@ class HomeController extends BaseController */ public function index() { - // get the accounts to display on the home screen: + // count, maybe we need some introductionary text to show: $count = $this->_accounts->count(); + + // get the preference for the home accounts to show: + $frontpage = $this->_preferences->get('frontpageAccounts', []); + + $accounts = $this->_accounts->getByIds($frontpage->data); + + $transactions = []; + foreach($accounts as $account) { + $transactions[] = [$this->_journal->getByAccount($account,15),$account]; + } + + if(count($transactions) % 2 == 0) { + $transactions = array_chunk($transactions, 2); + } elseif(count($transactions) == 1) { + $transactions = array_chunk($transactions, 3); + } else { + $transactions = array_chunk($transactions, 3); + } // build the home screen: - return View::make('index')->with('count', $count); + return View::make('index')->with('count', $count)->with('transactions',$transactions); } } \ No newline at end of file diff --git a/app/controllers/LimitController.php b/app/controllers/LimitController.php new file mode 100644 index 0000000000..de01c8d4a4 --- /dev/null +++ b/app/controllers/LimitController.php @@ -0,0 +1,49 @@ +_budgets = $budgets; + $this->_limits = $limits; + View::share('menu', 'budgets'); + + } + + public function create($budgetId = null) + { + $periods = [ + 'weekly' => 'A week', + 'monthly' => 'A month', + 'quarterly' => 'A quarter', + 'half-year' => 'Six months', + 'yearly' => 'A year', + ]; + + $budget = $this->_budgets->find($budgetId); + $budget_id = is_null($budget) ? null : $budget->id; + $budgets = $this->_budgets->getAsSelectList(); + return View::make('limits.create')->with('budgets', $budgets)->with('budget_id', $budget_id)->with( + 'periods', $periods + ); + } + + public function store() + { + // find a limit with these properties, as we might already have one: + $limit = $this->_limits->store(Input::all()); + if($limit->id) { + return Redirect::route('budgets.index'); + } else { + return Redirect::route('budgets.limits.create')->withInput(); + } + } + +} \ No newline at end of file diff --git a/app/controllers/MigrationController.php b/app/controllers/MigrationController.php index 4b87445824..2cf628ab98 100644 --- a/app/controllers/MigrationController.php +++ b/app/controllers/MigrationController.php @@ -1,5 +1,6 @@ home'; + echo 'home'; exit(); } + public function limit() + { + $user = User::find(1); + $budgets = []; + // new budget + for ($i = 0; $i < 7; $i++) { + $budget = new Budget(); + $budget->user()->associate($user); + $budget->name = 'Some budget #' . rand(1, 2000); + $budget->save(); + $budgets[] = $budget; + } + + // create a non-repeating limit for this week: + $today = new Carbon('01-07-2014'); + + $limit = new Limit; + $limit->budget()->associate($budgets[0]); + $limit->amount = 100; + $limit->startdate = $today; + $limit->amount = 100; + $limit->repeats = 0; + $limit->repeat_freq = 'weekly'; + + var_dump($limit->save()); + var_dump($limit->errors()->all()); + + + // create a repeating daily limit: + $day = new Limit; + $day->budget()->associate($budgets[1]); + $day->amount = 100; + $day->startdate = $today; + $day->amount = 100; + $day->repeats = 1; + $day->repeat_freq = 'daily'; + $day->save(); + + // repeating weekly limit. + $week = new Limit; + $week->budget()->associate($budgets[2]); + $week->amount = 100; + $week->startdate = $today; + $week->amount = 100; + $week->repeats = 1; + $week->repeat_freq = 'weekly'; + $week->save(); + + // repeating monthly limit + $month = new Limit; + $month->budget()->associate($budgets[3]); + $month->amount = 100; + $month->startdate = $today; + $month->amount = 100; + $month->repeats = 1; + $month->repeat_freq = 'monthly'; + $month->save(); + + // quarter + $quarter = new Limit; + $quarter->budget()->associate($budgets[4]); + $quarter->amount = 100; + $quarter->startdate = $today; + $quarter->amount = 100; + $quarter->repeats = 1; + $quarter->repeat_freq = 'quarterly'; + $quarter->save(); + + // six months + $six = new Limit; + $six->budget()->associate($budgets[5]); + $six->amount = 100; + $six->startdate = $today; + $six->amount = 100; + $six->repeats = 1; + $six->repeat_freq = 'half-year'; + $six->save(); + + // year + $yearly = new Limit; + $yearly->budget()->associate($budgets[6]); + $yearly->amount = 100; + $yearly->startdate = $today; + $yearly->amount = 100; + $yearly->repeats = 1; + $yearly->repeat_freq = 'yearly'; + $yearly->save(); + + + // create a repeating weekly limit: + // create a repeating monthly limit: + + foreach ($budgets as $budget) { + + echo '#' . $budget->id . ': ' . $budget->name . ':
'; + foreach ($budget->limits()->get() as $limit) { + echo '  Limit #' . $limit->id . ', amount: ' . $limit->amount . ', start: ' + . $limit->startdate->format('D d-m-Y') . ', repeats: ' + . $limit->repeats . ', repeat_freq: ' . $limit->repeat_freq . '
'; + + foreach ($limit->limitrepetitions()->get() as $rep) { + echo '    rep: #' . $rep->id . ', from ' . $rep->startdate->format('D d-m-Y') + . ' to ' + . $rep->enddate->format('D d-m-Y') . '
'; + + } + } + } + + + return ''; + } + /** * @return \Illuminate\View\View */ diff --git a/app/database/migrations/2014_07_17_183717_create_limits_table.php b/app/database/migrations/2014_07_17_183717_create_limits_table.php index 7a91836c49..fc23784b3f 100644 --- a/app/database/migrations/2014_07_17_183717_create_limits_table.php +++ b/app/database/migrations/2014_07_17_183717_create_limits_table.php @@ -18,8 +18,9 @@ class CreateLimitsTable extends Migration { $table->timestamps(); $table->integer('component_id')->unsigned(); $table->date('startdate'); - $table->date('enddate'); $table->decimal('amount',10,2); + $table->boolean('repeats'); + $table->enum('repeat_freq', ['daily', 'weekly','monthly','quarterly','half-year','yearly']); // connect component $table->foreign('component_id') diff --git a/app/database/migrations/2014_07_19_055011_create_limit_repeat_table.php b/app/database/migrations/2014_07_19_055011_create_limit_repeat_table.php new file mode 100644 index 0000000000..2fde86bfce --- /dev/null +++ b/app/database/migrations/2014_07_19_055011_create_limit_repeat_table.php @@ -0,0 +1,43 @@ +increments('id'); + $table->timestamps(); + $table->integer('limit_id')->unsigned(); + $table->date('startdate'); + $table->date('enddate'); + $table->decimal('amount',10,2); + + $table->unique(['limit_id','startdate','enddate']); + + // connect limit + $table->foreign('limit_id') + ->references('id')->on('limits') + ->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('limit_repetitions'); + } + +} diff --git a/app/filters.php b/app/filters.php index e0986863eb..8c8922959d 100644 --- a/app/filters.php +++ b/app/filters.php @@ -7,6 +7,7 @@ App::before( if (Auth::check()) { \Firefly\Helper\Toolkit\Toolkit::getDateRange(); } + Event::fire('app.before'); } ); diff --git a/app/lib/Firefly/Helper/Migration/MigrationHelper.php b/app/lib/Firefly/Helper/Migration/MigrationHelper.php index f370171720..5aaff61b8b 100644 --- a/app/lib/Firefly/Helper/Migration/MigrationHelper.php +++ b/app/lib/Firefly/Helper/Migration/MigrationHelper.php @@ -3,6 +3,8 @@ namespace Firefly\Helper\Migration; +use Firefly\Helper\MigrationException; + class MigrationHelper implements MigrationHelperInterface { protected $path; @@ -56,6 +58,9 @@ class MigrationHelper implements MigrationHelperInterface // create transfers: $this->_importTransfers(); + // create limits: + $this->_importLimits(); + } catch (\Firefly\Exception\FireflyException $e) { \DB::rollBack(); @@ -75,7 +80,7 @@ class MigrationHelper implements MigrationHelperInterface /** @var \Firefly\Storage\Account\AccountRepositoryInterface $accounts */ $accounts = \App::make('Firefly\Storage\Account\AccountRepositoryInterface'); $cash = $accounts->store(['name' => 'Cash account', 'account_type' => $cashAT, 'active' => 0]); - \Log::info('Created cash account (#'.$cash->id.')'); + \Log::info('Created cash account (#' . $cash->id . ')'); $this->map['cash'] = $cash; } @@ -149,6 +154,39 @@ class MigrationHelper implements MigrationHelperInterface return $components->store(['name' => $component->name, 'class' => 'Budget']); } + protected function _importLimits() + { + \Log::info('Importing limits'); + foreach ($this->JSON->limits as $entry) { + \Log::debug( + 'Now at #' . $entry->id . ': EUR ' . $entry->amount . ' for month ' . $entry->date + . ' and componentID: ' . $entry->component_id + ); + $budget = isset($this->map['budgets'][$entry->component_id]) ? $this->map['budgets'][$entry->component_id] + : null; + if (!is_null($budget)) { + \Log::debug('Found budget for this limit: #' . $budget->id . ', ' . $budget->name); + + $limit = new \Limit; + $limit->budget()->associate($budget); + $limit->startdate = new \Carbon\Carbon($entry->date); + $limit->amount = floatval($entry->amount); + $limit->repeats = 0; + $limit->repeat_freq = 'monthly'; + if (!$limit->save()) { + \Log::error('MigrationException!'); + throw new MigrationException('Importing limits failed: ' . $limit->errors()->first()); + } + } else { + \Log::warning('No budget for this limit!'); + } + + + // create repeat thing should not be necessary. + + } + } + protected function _importTransactions() { diff --git a/app/lib/Firefly/Storage/Account/EloquentAccountRepository.php b/app/lib/Firefly/Storage/Account/EloquentAccountRepository.php index 58ef637918..695ff609a5 100644 --- a/app/lib/Firefly/Storage/Account/EloquentAccountRepository.php +++ b/app/lib/Firefly/Storage/Account/EloquentAccountRepository.php @@ -13,7 +13,7 @@ class EloquentAccountRepository implements AccountRepositoryInterface public function get() { - return \Auth::user()->accounts()->with('accounttype')->orderBy('name','ASC')->get(); + return \Auth::user()->accounts()->with('accounttype')->orderBy('name', 'ASC')->get(); } public function getBeneficiaries() @@ -23,7 +23,7 @@ class EloquentAccountRepository implements AccountRepositoryInterface ) ->where('account_types.description', 'Beneficiary account')->where('accounts.active', 1) - ->orderBy('accounts.name','ASC')->get(['accounts.*']); + ->orderBy('accounts.name', 'ASC')->get(['accounts.*']); return $list; } @@ -34,7 +34,11 @@ class EloquentAccountRepository implements AccountRepositoryInterface public function getByIds($ids) { - return \Auth::user()->accounts()->with('accounttype')->whereIn('id', $ids)->orderBy('name','ASC')->get(); + if (count($ids) > 0) { + return \Auth::user()->accounts()->with('accounttype')->whereIn('id', $ids)->orderBy('name', 'ASC')->get(); + } else { + return []; + } } public function getDefault() @@ -42,7 +46,7 @@ class EloquentAccountRepository implements AccountRepositoryInterface return \Auth::user()->accounts()->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') ->where('account_types.description', 'Default account') - ->orderBy('accounts.name','ASC')->get(['accounts.*']); + ->orderBy('accounts.name', 'ASC')->get(['accounts.*']); } public function getActiveDefault() @@ -60,7 +64,7 @@ class EloquentAccountRepository implements AccountRepositoryInterface ) ->where('account_types.description', 'Default account')->where('accounts.active', 1) - ->orderBy('accounts.name','ASC')->get(['accounts.*']); + ->orderBy('accounts.name', 'ASC')->get(['accounts.*']); $return = []; foreach ($list as $entry) { $return[intval($entry->id)] = $entry->name; diff --git a/app/lib/Firefly/Storage/Budget/BudgetRepositoryInterface.php b/app/lib/Firefly/Storage/Budget/BudgetRepositoryInterface.php index e0896cf387..f15e406e74 100644 --- a/app/lib/Firefly/Storage/Budget/BudgetRepositoryInterface.php +++ b/app/lib/Firefly/Storage/Budget/BudgetRepositoryInterface.php @@ -6,6 +6,9 @@ namespace Firefly\Storage\Budget; interface BudgetRepositoryInterface { public function getAsSelectList(); + public function get(); + + public function create($data); public function find($id); diff --git a/app/lib/Firefly/Storage/Budget/EloquentBudgetRepository.php b/app/lib/Firefly/Storage/Budget/EloquentBudgetRepository.php index daafdddfb6..b5b241253d 100644 --- a/app/lib/Firefly/Storage/Budget/EloquentBudgetRepository.php +++ b/app/lib/Firefly/Storage/Budget/EloquentBudgetRepository.php @@ -8,7 +8,9 @@ class EloquentBudgetRepository implements BudgetRepositoryInterface public function getAsSelectList() { - $list = \Auth::user()->budgets()->get(); + $list = \Auth::user()->budgets()->with( + ['limits', 'limits.limitrepetitions'] + )->orderBy('name', 'ASC')->get(); $return = []; foreach ($list as $entry) { $return[intval($entry->id)] = $entry->name; @@ -16,6 +18,63 @@ class EloquentBudgetRepository implements BudgetRepositoryInterface return $return; } + public function create($data) + { + $budget = new \Budget; + $budget->name = $data['name']; + $budget->user()->associate(\Auth::user()); + $budget->save(); + + // if limit, create limit (repetition itself will be picked up elsewhere). + if ($data['amount'] > 0) { + $limit = new \Limit; + $limit->budget()->associate($budget); + $startDate = new \Carbon\Carbon; + switch ($data['repeat_freq']) { + case 'daily': + $startDate->startOfDay(); + break; + case 'weekly': + $startDate->startOfWeek(); + break; + case 'monthly': + $startDate->startOfMonth(); + break; + case 'quarterly': + $startDate->firstOfQuarter(); + break; + case 'half-year': + $startDate->startOfYear(); + if (intval($startDate->format('m')) >= 7) { + $startDate->addMonths(6); + } + break; + case 'yearly': + $startDate->startOfYear(); + break; + } + $limit->startdate = $startDate; + $limit->amount = $data['amount']; + $limit->repeats = $data['repeats']; + $limit->repeat_freq = $data['repeat_freq']; + $limit->save(); + } + + + return $budget; + } + + 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(); + } + public function find($id) { diff --git a/app/lib/Firefly/Storage/Limit/EloquentLimitRepository.php b/app/lib/Firefly/Storage/Limit/EloquentLimitRepository.php new file mode 100644 index 0000000000..6212c410ab --- /dev/null +++ b/app/lib/Firefly/Storage/Limit/EloquentLimitRepository.php @@ -0,0 +1,84 @@ +startOfDay(); + break; + case 'weekly': + $date->startOfWeek(); + break; + case 'monthly': + $date->startOfMonth(); + break; + case 'quarterly': + $date->firstOfQuarter(); + break; + case 'half-year': + + if (intval($date->format('m')) >= 7) { + $date->startOfYear(); + $date->addMonths(6); + } else { + $date->startOfYear(); + } + break; + case 'yearly': + $date->startOfYear(); + break; + } + // find existing: + $count = \Limit:: + leftJoin('components', 'components.id', '=', 'limits.component_id')->where( + 'components.user_id', \Auth::user()->id + )->where('startdate', $date->format('Y-m-d'))->where('component_id', $data['budget_id'])->where( + 'repeat_freq', $data['period'] + )->count(); + if ($count > 0) { + \Session::flash('error', 'There already is an entry for these parameters.'); + return new \Limit; + } + // create new limit: + $limit = new \Limit; + $limit->budget()->associate($budget); + $limit->startdate = $date; + $limit->amount = floatval($data['amount']); + $limit->repeats = isset($data['repeats']) ? intval($data['repeats']) : 0; + $limit->repeat_freq = $data['period']; + if (!$limit->save()) { + Session::flash('error', 'Could not save: ' . $limit->errors()->first()); + } + return $limit; + } + + public function getTJByBudgetAndDateRange(\Budget $budget, \Carbon\Carbon $start, \Carbon\Carbon $end) + { + $type = \TransactionType::where('type', 'Withdrawal')->first(); + + $result = $budget->transactionjournals()->after($start)-> + before($end)->get(); + + return $result; + + } + +} \ No newline at end of file diff --git a/app/lib/Firefly/Storage/Limit/LimitRepositoryInterface.php b/app/lib/Firefly/Storage/Limit/LimitRepositoryInterface.php new file mode 100644 index 0000000000..5d2b651348 --- /dev/null +++ b/app/lib/Firefly/Storage/Limit/LimitRepositoryInterface.php @@ -0,0 +1,12 @@ +app->bind( + 'Firefly\Storage\Limit\LimitRepositoryInterface', + 'Firefly\Storage\Limit\EloquentLimitRepository' + ); + $this->app->bind( 'Firefly\Storage\Budget\BudgetRepositoryInterface', 'Firefly\Storage\Budget\EloquentBudgetRepository' diff --git a/app/lib/Firefly/Trigger/Limits/EloquentLimitTrigger.php b/app/lib/Firefly/Trigger/Limits/EloquentLimitTrigger.php new file mode 100644 index 0000000000..05c8166603 --- /dev/null +++ b/app/lib/Firefly/Trigger/Limits/EloquentLimitTrigger.php @@ -0,0 +1,178 @@ +budgets() + ->with(['limits', 'limits.limitrepetitions']) + ->whereNotNull('limits.id') + ->leftJoin('limits', 'components.id', '=', 'limits.component_id')->get(['components.*']); + + // get todays date. + + foreach ($budgets as $budget) { + \Log::debug( + 'Now checking the ' . count($budget->limits) . ' limits in ' . $budget->name . ' (#' . $budget->id + . ').' + ); + // loop limits: + foreach ($budget->limits as $limit) { + \Log::debug( + 'Now at limit #' . $limit->id . ', which has ' . count($limit->limitrepetitions) . ' reps already' + ); + \Log::debug( + 'More: Amount: ' . $limit->amount . ', repeat: ' . $limit->repeats . ', freq: ' + . $limit->repeat_freq + ); + // should have a repetition, at the very least + // for the period it starts (startdate and onwards). + if (count($limit->limitrepetitions) == 0) { + \Log::debug('No reps, create one.'); + // create such a repetition: + $repetition = new \LimitRepetition(); + $start = clone $limit->startdate; + $end = clone $start; + + // go to end: + switch ($limit->repeat_freq) { + case 'daily': + $end->addDay(); + break; + case 'weekly': + $end->addWeek(); + break; + case 'monthly': + $end->addMonth(); + break; + case 'quarterly': + $end->addMonths(3); + break; + case 'half-year': + $end->addMonths(6); + break; + case 'yearly': + $end->addYear(); + break; + } + $end->subDay(); + $repetition->startdate = $start; + $repetition->enddate = $end; + $repetition->amount = $limit->amount; + $repetition->limit()->associate($limit); + \Log::debug('Created single rep for non-repeating limit, from ' . $start . ' until ' . $end); + + try { + $repetition->save(); + } catch (\Illuminate\Database\QueryException $e) { + // do nothing + \Log::error($e->getMessage()); + } + } else { + // there are limits already, do they + // fall into the range surrounding today? + $today = new \Carbon\Carbon; + $today->addMonths(2); + if ($limit->repeats == 1 && $today >= $limit->startdate) { + + /** @var \Carbon\Carbon $flowStart */ + $flowStart = clone $today; + /** @var \Carbon\Carbon $flowEnd */ + $flowEnd = clone $today; + + switch ($limit->repeat_freq) { + case 'daily': + $flowStart->startOfDay(); + $flowEnd->endOfDay(); + break; + case 'weekly': + $flowStart->startOfWeek(); + $flowEnd->endOfWeek(); + break; + case 'monthly': + $flowStart->startOfMonth(); + $flowEnd->endOfMonth(); + break; + case 'quarterly': + $flowStart->firstOfQuarter(); + $flowEnd->startOfMonth()->lastOfQuarter()->endOfDay(); + break; + case 'half-year': + + if (intval($flowStart->format('m')) >= 7) { + $flowStart->startOfYear(); + $flowStart->addMonths(6); + } else { + $flowStart->startOfYear(); + } + + $flowEnd->endOfYear(); + if (intval($start->format('m')) <= 6) { + $flowEnd->subMonths(6); + $flowEnd->subDay(); + + } + break; + case 'yearly': + $flowStart->startOfYear(); + $flowEnd->endOfYear(); + break; + } + + $inRange = false; + foreach ($limit->limitrepetitions as $rep) { + if ($rep->startdate->format('dmY') == $flowStart->format('dmY') + && $rep->enddate->format('dmY') == $flowEnd->format('dmY') + ) { + // falls in current range, do nothing? + $inRange = true; + } + } + // if there is none that fall in range, create! + if ($inRange === false) { + // create (but check first)! + $count = \LimitRepetition::where('limit_id', $limit->id)->where('startdate', $flowStart) + ->where('enddate', $flowEnd)->count(); + if ($count == 0) { + $repetition = new \LimitRepetition; + $repetition->startdate = $flowStart; + $repetition->enddate = $flowEnd; + $repetition->amount = $limit->amount; + $repetition->limit()->associate($limit); + try { + $repetition->save(); + } catch (\Illuminate\Database\QueryException $e) { + // do nothing + \Log::error($e->getMessage()); + } + } + } + } + } + } + \Log::debug('Done checking the budget!'); + } + } + + public function subscribe(\Illuminate\Events\Dispatcher $events) + { + $events->listen('app.before', 'Firefly\Trigger\Limits\EloquentLimitTrigger@updateLimitRepetitions'); + + } + +} + +\Limit::observe(new EloquentLimitTrigger); \ No newline at end of file diff --git a/app/models/Account.php b/app/models/Account.php index 8e5b834bb1..544f7f7c9f 100644 --- a/app/models/Account.php +++ b/app/models/Account.php @@ -14,13 +14,13 @@ use LaravelBook\Ardent\Ardent as Ardent; * @property-read \AccountType $accountType * @property-read \User $user * @property-read \Illuminate\Database\Eloquent\Collection|\Transaction[] $transactions - * @method static \Illuminate\Database\Query\Builder|\Account whereId($value) - * @method static \Illuminate\Database\Query\Builder|\Account whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Account whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Account whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\Account whereAccountTypeId($value) - * @method static \Illuminate\Database\Query\Builder|\Account whereName($value) - * @method static \Illuminate\Database\Query\Builder|\Account whereActive($value) + * @method static \Illuminate\Database\Query\Builder|\Account whereId($value) + * @method static \Illuminate\Database\Query\Builder|\Account whereCreatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Account whereUpdatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Account whereUserId($value) + * @method static \Illuminate\Database\Query\Builder|\Account whereAccountTypeId($value) + * @method static \Illuminate\Database\Query\Builder|\Account whereName($value) + * @method static \Illuminate\Database\Query\Builder|\Account whereActive($value) */ class Account extends Ardent { diff --git a/app/models/AccountType.php b/app/models/AccountType.php index 71b1098cdd..9a317c8315 100644 --- a/app/models/AccountType.php +++ b/app/models/AccountType.php @@ -9,10 +9,10 @@ * @property \Carbon\Carbon $updated_at * @property string $description * @property-read \Illuminate\Database\Eloquent\Collection|\Account[] $accounts - * @method static \Illuminate\Database\Query\Builder|\AccountType whereId($value) - * @method static \Illuminate\Database\Query\Builder|\AccountType whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\AccountType whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\AccountType whereDescription($value) + * @method static \Illuminate\Database\Query\Builder|\AccountType whereId($value) + * @method static \Illuminate\Database\Query\Builder|\AccountType whereCreatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\AccountType whereUpdatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\AccountType whereDescription($value) */ class AccountType extends Eloquent { diff --git a/app/models/Budget.php b/app/models/Budget.php index e9521a9199..c52404af0d 100644 --- a/app/models/Budget.php +++ b/app/models/Budget.php @@ -3,29 +3,40 @@ /** * Budget * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property string $name - * @property integer $user_id - * @property string $class - * @property-read \Illuminate\Database\Eloquent\Collection|\Transaction[] $transactions + * @property integer $id + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * @property string $name + * @property integer $user_id + * @property string $class + * @property-read \Illuminate\Database\Eloquent\Collection|\Transaction[] $transactions * @property-read \Illuminate\Database\Eloquent\Collection|\TransactionJournal[] $transactionjournals - * @property-read \User $user - * @method static \Illuminate\Database\Query\Builder|\Budget whereId($value) - * @method static \Illuminate\Database\Query\Builder|\Budget whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Budget whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Budget whereName($value) - * @method static \Illuminate\Database\Query\Builder|\Budget whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\Budget whereClass($value) + * @property-read \User $user + * @method static \Illuminate\Database\Query\Builder|\Budget whereId($value) + * @method static \Illuminate\Database\Query\Builder|\Budget whereCreatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Budget whereUpdatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Budget whereName($value) + * @method static \Illuminate\Database\Query\Builder|\Budget whereUserId($value) + * @method static \Illuminate\Database\Query\Builder|\Budget whereClass($value) + * @property-read \Illuminate\Database\Eloquent\Collection|\Limit[] $limits */ -class Budget extends Component { +class Budget extends Component +{ + public static $factory + = [ + 'name' => 'string', + 'user_id' => 'factory|User', + 'class' => 'Budget' + ]; protected $isSubclass = true; - public static $factory = [ - 'name' => 'string', - 'user_id' => 'factory|User', - 'class' => 'Budget' - ]; + public function limits() + { + return $this->hasMany('Limit', 'component_id'); + } + + public function transactionjournals() { + return $this->belongsToMany('TransactionJournal','component_transaction_journal','component_id'); + } } \ No newline at end of file diff --git a/app/models/Category.php b/app/models/Category.php index 403802dd0c..aeae8392b1 100644 --- a/app/models/Category.php +++ b/app/models/Category.php @@ -3,28 +3,30 @@ /** * Category * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property string $name - * @property integer $user_id - * @property string $class - * @property-read \Illuminate\Database\Eloquent\Collection|\Transaction[] $transactions + * @property integer $id + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * @property string $name + * @property integer $user_id + * @property string $class + * @property-read \Illuminate\Database\Eloquent\Collection|\Transaction[] $transactions * @property-read \Illuminate\Database\Eloquent\Collection|\TransactionJournal[] $transactionjournals - * @property-read \User $user - * @method static \Illuminate\Database\Query\Builder|\Category whereId($value) - * @method static \Illuminate\Database\Query\Builder|\Category whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Category whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Category whereName($value) - * @method static \Illuminate\Database\Query\Builder|\Category whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\Category whereClass($value) + * @property-read \User $user + * @method static \Illuminate\Database\Query\Builder|\Category whereId($value) + * @method static \Illuminate\Database\Query\Builder|\Category whereCreatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Category whereUpdatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Category whereName($value) + * @method static \Illuminate\Database\Query\Builder|\Category whereUserId($value) + * @method static \Illuminate\Database\Query\Builder|\Category whereClass($value) + * @property-read \Limit $limits */ class Category extends Component { + public static $factory + = [ + 'name' => 'string', + 'user_id' => 'factory|User', + 'class' => 'Category' + ]; protected $isSubclass = true; - public static $factory = [ - 'name' => 'string', - 'user_id' => 'factory|User', - 'class' => 'Category' - ]; } \ No newline at end of file diff --git a/app/models/Component.php b/app/models/Component.php index a06abc3215..09069f3ef3 100644 --- a/app/models/Component.php +++ b/app/models/Component.php @@ -4,45 +4,47 @@ /** * Component * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property string $name - * @property integer $user_id - * @property string $class - * @property-read \Illuminate\Database\Eloquent\Collection|\Transaction[] $transactions + * @property integer $id + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * @property string $name + * @property integer $user_id + * @property string $class + * @property-read \Illuminate\Database\Eloquent\Collection|\Transaction[] $transactions * @property-read \Illuminate\Database\Eloquent\Collection|\TransactionJournal[] $transactionjournals - * @property-read \User $user - * @method static \Illuminate\Database\Query\Builder|\Component whereId($value) - * @method static \Illuminate\Database\Query\Builder|\Component whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Component whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Component whereName($value) - * @method static \Illuminate\Database\Query\Builder|\Component whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\Component whereClass($value) + * @property-read \User $user + * @method static \Illuminate\Database\Query\Builder|\Component whereId($value) + * @method static \Illuminate\Database\Query\Builder|\Component whereCreatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Component whereUpdatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Component whereName($value) + * @method static \Illuminate\Database\Query\Builder|\Component whereUserId($value) + * @method static \Illuminate\Database\Query\Builder|\Component whereClass($value) + * @property-read \Limit $limits */ class Component extends Firefly\Database\SingleTableInheritanceEntity { public static $rules = [ - 'user_id' => 'exists:users,id|required', - 'name' => 'required|between:1,255', - 'class' => 'required', + 'user_id' => 'exists:users,id|required', + 'name' => 'required|between:1,255', + 'class' => 'required', + ]; + public static $factory + = [ + 'name' => 'string', + 'user_id' => 'factory|User', ]; protected $table = 'components'; protected $subclassField = 'class'; - public static $factory = [ - 'name' => 'string', - 'user_id' => 'factory|User', - ]; - public function transactions() { return $this->belongsToMany('Transaction'); } - public function limits() { + public function limits() + { return $this->belongsTo('Limit'); } diff --git a/app/models/Limit.php b/app/models/Limit.php index 5ffef56c58..ac0444131b 100644 --- a/app/models/Limit.php +++ b/app/models/Limit.php @@ -2,6 +2,29 @@ use LaravelBook\Ardent\Ardent as Ardent; +/** + * Limit + * + * @property integer $id + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * @property integer $component_id + * @property \Carbon\Carbon $startdate + * @property float $amount + * @property boolean $repeats + * @property string $repeat_freq + * @property-read \Component $component + * @property-read \Budget $budget + * @property-read \Illuminate\Database\Eloquent\Collection|\LimitRepetition[] $limitrepetitions + * @method static \Illuminate\Database\Query\Builder|\Limit whereId($value) + * @method static \Illuminate\Database\Query\Builder|\Limit whereCreatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Limit whereUpdatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Limit whereComponentId($value) + * @method static \Illuminate\Database\Query\Builder|\Limit whereStartdate($value) + * @method static \Illuminate\Database\Query\Builder|\Limit whereAmount($value) + * @method static \Illuminate\Database\Query\Builder|\Limit whereRepeats($value) + * @method static \Illuminate\Database\Query\Builder|\Limit whereRepeatFreq($value) + */ class Limit extends Ardent { @@ -9,8 +32,9 @@ class Limit extends Ardent = [ 'component_id' => 'required|exists:components,id', 'startdate' => 'required|date', - 'enddate' => 'required|date', - 'amount' => 'numeric|required|min:0.01' + 'amount' => 'numeric|required|min:0.01', + 'repeats' => 'required|between:0,1', + 'repeat_freq' => 'required|in:daily,weekly,monthly,quarterly,half-year,yearly' ]; @@ -24,7 +48,7 @@ class Limit extends Ardent public function component() { - return $this->belongsTo('Component'); + return $this->belongsTo('Component','component_id'); } public function budget() @@ -32,6 +56,10 @@ class Limit extends Ardent return $this->belongsTo('Budget', 'component_id'); } + public function limitrepetitions() { + return $this->hasMany('LimitRepetition'); + } + public function getDates() { return ['created_at', 'updated_at', 'startdate', 'enddate']; diff --git a/app/models/LimitRepetition.php b/app/models/LimitRepetition.php new file mode 100644 index 0000000000..5dfbd1c2ba --- /dev/null +++ b/app/models/LimitRepetition.php @@ -0,0 +1,84 @@ + 'required|exists:limits,id', + 'startdate' => 'required|date', + 'enddate' => 'required|date', + 'amount' => 'numeric|required|min:0.01', + ]; + + public static $factory + = [ + 'limit_id' => 'factory|Limit', + 'startdate' => 'date', + 'enddate' => 'date', + 'amount' => 'integer' + ]; + + public function limit() + { + return $this->belongsTo('Limit'); + } + + public function getDates() + { + return ['created_at', 'updated_at', 'startdate', 'enddate']; + } + + /** + * How much money is left in this? + */ + public function left() + { + $key = 'limit-rep-left-' . $this->id; + if (Cache::has($key)) { + return Cache::get($key); + } + $left = floatval($this->amount); + + // budget: + $budget = $this->limit->budget; + + /** @var \Firefly\Storage\Limit\EloquentLimitRepository $limits */ + $limits = App::make('Firefly\Storage\Limit\EloquentLimitRepository'); + $set = $limits->getTJByBudgetAndDateRange($budget, $this->startdate, $this->enddate); + + foreach ($set as $journal) { + foreach ($journal->transactions as $t) { + if ($t->amount < 0) { + $left += floatval($t->amount); + } + } + } + Cache::forever($key, $left); + + + return $left; + } + + +} \ No newline at end of file diff --git a/app/models/Preference.php b/app/models/Preference.php index 421707b429..b06ddd7771 100644 --- a/app/models/Preference.php +++ b/app/models/Preference.php @@ -13,12 +13,12 @@ use LaravelBook\Ardent\Ardent; * @property string $name * @property string $data * @property-read \User $user - * @method static \Illuminate\Database\Query\Builder|\Preference whereId($value) - * @method static \Illuminate\Database\Query\Builder|\Preference whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Preference whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Preference whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\Preference whereName($value) - * @method static \Illuminate\Database\Query\Builder|\Preference whereData($value) + * @method static \Illuminate\Database\Query\Builder|\Preference whereId($value) + * @method static \Illuminate\Database\Query\Builder|\Preference whereCreatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Preference whereUpdatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Preference whereUserId($value) + * @method static \Illuminate\Database\Query\Builder|\Preference whereName($value) + * @method static \Illuminate\Database\Query\Builder|\Preference whereData($value) */ class Preference extends Ardent { diff --git a/app/models/Transaction.php b/app/models/Transaction.php index 9eb9071213..1f44f6c7ad 100644 --- a/app/models/Transaction.php +++ b/app/models/Transaction.php @@ -18,13 +18,13 @@ use LaravelBook\Ardent\Ardent; * @property-read \Illuminate\Database\Eloquent\Collection|\Component[] $components * @property-read \Illuminate\Database\Eloquent\Collection|\Budget[] $budgets * @property-read \Illuminate\Database\Eloquent\Collection|\Category[] $categories - * @method static \Illuminate\Database\Query\Builder|\Transaction whereId($value) - * @method static \Illuminate\Database\Query\Builder|\Transaction whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Transaction whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\Transaction whereAccountId($value) - * @method static \Illuminate\Database\Query\Builder|\Transaction whereTransactionJournalId($value) - * @method static \Illuminate\Database\Query\Builder|\Transaction whereDescription($value) - * @method static \Illuminate\Database\Query\Builder|\Transaction whereAmount($value) + * @method static \Illuminate\Database\Query\Builder|\Transaction whereId($value) + * @method static \Illuminate\Database\Query\Builder|\Transaction whereCreatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Transaction whereUpdatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\Transaction whereAccountId($value) + * @method static \Illuminate\Database\Query\Builder|\Transaction whereTransactionJournalId($value) + * @method static \Illuminate\Database\Query\Builder|\Transaction whereDescription($value) + * @method static \Illuminate\Database\Query\Builder|\Transaction whereAmount($value) */ class Transaction extends Ardent { diff --git a/app/models/TransactionCurrency.php b/app/models/TransactionCurrency.php index 5fd3a3d4aa..4b5873c697 100644 --- a/app/models/TransactionCurrency.php +++ b/app/models/TransactionCurrency.php @@ -9,10 +9,10 @@ * @property \Carbon\Carbon $updated_at * @property string $code * @property-read \Illuminate\Database\Eloquent\Collection|\TransactionJournal[] $transactionJournals - * @method static \Illuminate\Database\Query\Builder|\TransactionCurrency whereId($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionCurrency whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionCurrency whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionCurrency whereCode($value) + * @method static \Illuminate\Database\Query\Builder|\TransactionCurrency whereId($value) + * @method static \Illuminate\Database\Query\Builder|\TransactionCurrency whereCreatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\TransactionCurrency whereUpdatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\TransactionCurrency whereCode($value) */ class TransactionCurrency extends Eloquent { diff --git a/app/models/TransactionJournal.php b/app/models/TransactionJournal.php index 5be5525ac8..3d28bb0398 100644 --- a/app/models/TransactionJournal.php +++ b/app/models/TransactionJournal.php @@ -32,6 +32,13 @@ use LaravelBook\Ardent\Ardent; * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereDate($value) * @method static \TransactionJournal after($date) * @method static \TransactionJournal before($date) + * @property integer $user_id + * @property-read \User $user + * @property-read \Illuminate\Database\Eloquent\Collection|\ + * 'Budget[] $budgets + * @property-read \Illuminate\Database\Eloquent\Collection|\ + * 'Category[] $categories + * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereUserId($value) */ class TransactionJournal extends Ardent { diff --git a/app/models/TransactionType.php b/app/models/TransactionType.php index dbee6bc992..77b1bd2b7f 100644 --- a/app/models/TransactionType.php +++ b/app/models/TransactionType.php @@ -9,10 +9,10 @@ * @property \Carbon\Carbon $updated_at * @property string $type * @property-read \Illuminate\Database\Eloquent\Collection|\TransactionJournal[] $transactionJournals - * @method static \Illuminate\Database\Query\Builder|\TransactionType whereId($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionType whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionType whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionType whereType($value) + * @method static \Illuminate\Database\Query\Builder|\TransactionType whereId($value) + * @method static \Illuminate\Database\Query\Builder|\TransactionType whereCreatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\TransactionType whereUpdatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\TransactionType whereType($value) */ class TransactionType extends Eloquent { public function transactionJournals() { diff --git a/app/models/User.php b/app/models/User.php index 75d0cf02e0..333bfc4143 100644 --- a/app/models/User.php +++ b/app/models/User.php @@ -23,14 +23,15 @@ use LaravelBook\Ardent\Ardent; * @property-read \Illuminate\Database\Eloquent\Collection|\Component[] $components * @property-read \Illuminate\Database\Eloquent\Collection|\Budget[] $budgets * @property-read \Illuminate\Database\Eloquent\Collection|\Category[] $categories - * @method static \Illuminate\Database\Query\Builder|\User whereId($value) - * @method static \Illuminate\Database\Query\Builder|\User whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\User whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\User whereEmail($value) - * @method static \Illuminate\Database\Query\Builder|\User wherePassword($value) - * @method static \Illuminate\Database\Query\Builder|\User whereReset($value) - * @method static \Illuminate\Database\Query\Builder|\User whereRememberToken($value) - * @method static \Illuminate\Database\Query\Builder|\User whereMigrated($value) + * @method static \Illuminate\Database\Query\Builder|\User whereId($value) + * @method static \Illuminate\Database\Query\Builder|\User whereCreatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\User whereUpdatedAt($value) + * @method static \Illuminate\Database\Query\Builder|\User whereEmail($value) + * @method static \Illuminate\Database\Query\Builder|\User wherePassword($value) + * @method static \Illuminate\Database\Query\Builder|\User whereReset($value) + * @method static \Illuminate\Database\Query\Builder|\User whereRememberToken($value) + * @method static \Illuminate\Database\Query\Builder|\User whereMigrated($value) + * @property-read \Illuminate\Database\Eloquent\Collection|\TransactionJournal[] $transactionjournals */ class User extends Ardent implements UserInterface, RemindableInterface { diff --git a/app/routes.php b/app/routes.php index c623f869e1..83df9a0cc5 100644 --- a/app/routes.php +++ b/app/routes.php @@ -26,8 +26,13 @@ Route::group(['before' => 'auth'], function () { Route::get('/accounts/create', ['uses' => 'AccountController@create', 'as' => 'accounts.create']); Route::get('/accounts/{account}', ['uses' => 'AccountController@show', 'as' => 'accounts.show']); - // budget controller - Route::get('/bugets',['uses' => 'BudgetController@index','as' => 'budgets.index']); + // budget controller: + Route::get('/budgets',['uses' => 'BudgetController@index','as' => 'budgets.index']); + Route::get('/budget/create',['uses' => 'BudgetController@create', 'as' => 'budgets.create']); + Route::get('/budget/show/{id}',['uses' => 'BudgetController@show', 'as' => 'budgets.show']); + + // limit controller: + Route::get('/budgets/limits/create/{id?}',['uses' => 'LimitController@create','as' => 'budgets.limits.create']); // JSON controller: Route::get('/json/beneficiaries', ['uses' => 'JsonController@beneficiaries', 'as' => 'json.beneficiaries']); @@ -53,6 +58,9 @@ Route::group(['before' => 'csrf|auth'], function () { // profile controller Route::post('/profile/change-password', ['uses' => 'ProfileController@postChangePassword']); + // budget controller: + Route::post('/budget/store',['uses' => 'BudgetController@store', 'as' => 'budgets.store']); + // migration controller Route::post('/migrate', ['uses' => 'MigrationController@postIndex']); @@ -62,6 +70,9 @@ Route::group(['before' => 'csrf|auth'], function () { // account controller: Route::get('/accounts/store', ['uses' => 'AccountController@store', 'as' => 'accounts.store']); + // limit controller: + Route::post('/limits/store', ['uses' => 'LimitController@store', 'as' => 'limits.store']); + // transaction controller: Route::post('/transactions/store/{what}', ['uses' => 'TransactionController@store', 'as' => 'transactions.store']) ->where(['what' => 'withdrawal|deposit|transfer']); @@ -81,6 +92,7 @@ Route::group(['before' => 'guest'], function () { // dev import route: Route::get('/dev',['uses' => 'MigrationController@dev']); + Route::get('/limit',['uses' => 'MigrationController@limit']); } ); diff --git a/app/views/budgets/create.blade.php b/app/views/budgets/create.blade.php new file mode 100644 index 0000000000..e25252c560 --- /dev/null +++ b/app/views/budgets/create.blade.php @@ -0,0 +1,87 @@ +@extends('layouts.default') +@section('content') +
+
+

Firefly + Create a budget +

+

+ Firefly uses the envelope system. Every budget + is an envelope in which you put money every [period]. Expenses allocated to each budget are paid from this + (virtual) envelope. +

+

+ When the envelope is empty, you must stop spending on the budget. If the envelope still has some money left at the + end of the [period], congratulations! You have saved money! +

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

Mandatory fields

+ +
+ +
+ + For example: groceries, bills +
+
+ +
+
+

Optional fields

+ +
+ +
+ + What's the most you're willing to spend in this budget? This amount is "put" in the virtual + envelope. +
+
+ +
+ +
+ {{Form::select('period',$periods,Input::old('period') ?: 'monthly',['class' => 'form-control'])}} + How long will the envelope last? A week, a month, or even longer? +
+
+ +
+ +
+
+ +
+ If you want, Firefly can automatically recreate the "envelope" and fill it again + when the timespan above has expired. Be careful with this option though. It makes it easier + to fall back to old habits. + Instead, you should recreate the envelope yourself each [period]. +
+
+
+
+ +
+
+ +



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

Firefly + Budgets and limits +

+

+ These are your budgets and if set, their "limits". Firefly uses an "envelope system" 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. + +

+
+
+
+
+ + + + + + + @foreach($budgets as $budget) + + + + + + @endforeach + +
BudgetCurrent envelope(s) 
+ {{{$budget->name}}} + + +
+
+ Envelope +
+
+ Left +
+
+ @foreach($budget->limits as $limit) + @foreach($limit->limitrepetitions as $index => $rep) +
+
+ + + {{mf($rep->amount,false)}} +
+
+ @if($rep->left() < 0) + + + {{mf($rep->left(),false)}} + @else + + + {{mf($rep->left(),false)}} + @endif +
+
+ + @if($limit->repeat_freq == 'monthly') + {{$rep->startdate->format('F Y')}} + @else + NO FORMAT + @endif + +
+ @if($limit->repeats == 1) +
+ auto repeats +
+ @endif +
+ + @if($limit->repeats == 0 || ($limit->repeats == 1 && $index == 0)) + + @endif +
+
+ @endforeach + @endforeach +

+ Add another limit +

+
+
+ + + +
+
+ +
+
+@stop \ No newline at end of file diff --git a/app/views/budgets/show.blade.php b/app/views/budgets/show.blade.php new file mode 100644 index 0000000000..52f124685a --- /dev/null +++ b/app/views/budgets/show.blade.php @@ -0,0 +1,4 @@ +@extends('layouts.default') +@section('content') + +@stop \ No newline at end of file diff --git a/app/views/index.blade.php b/app/views/index.blade.php index 3c71c84538..00a6b34f4d 100644 --- a/app/views/index.blade.php +++ b/app/views/index.blade.php @@ -7,6 +7,7 @@ What's playing? @endif + @if($count > 0)
@@ -23,6 +24,7 @@
+ @endif @@ -60,12 +62,35 @@ + + @if(count($transactions) > 0) + @foreach($transactions as $set) +
+ + @foreach($set as $data) +
+

{{{$data[1]->name}}}

+ @include('transactions.journals',['transactions' => $data[0],'account' => $data[1]]) +
+ @endforeach +
+ @endforeach + @endif + +
+
+ +
+
+
+ + @endif @stop diff --git a/app/views/limits/create.blade.php b/app/views/limits/create.blade.php new file mode 100644 index 0000000000..11ac4ca58b --- /dev/null +++ b/app/views/limits/create.blade.php @@ -0,0 +1,103 @@ +@extends('layouts.default') +@section('content') +
+
+

Firefly + Set a limit to a budget +

+

+ Firefly uses an "envelope + system" 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. +

+ +

+ Firefly gives you the opportunity to create such an envelope when you create a budget. However, you can + always add more of them. +

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

Mandatory fields

+ +
+ {{ Form::label('budget_id', 'Budget', ['class' => 'col-sm-3 control-label'])}} +
+ {{Form::select('budget_id',$budgets,Input::old('budget_id') ?: $budget_id, ['class' => + 'form-control'])}} + @if($errors->has('budget_id')) +

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

+ @else + Select one of your existing budgets. + @endif +
+
+ +
+ {{ Form::label('startdate', 'Start date', ['class' => 'col-sm-3 control-label'])}} +
+ + This date indicates when the envelope "starts". The date you select + here will correct itself to the nearest [period] you select below. +
+
+ +
+ + +
+ {{Form::select('period',$periods,Input::old('period') ?: 'monthly',['class' => 'form-control'])}} + How long will the envelope last? A week, a month, or even longer? +
+
+
+ + +
+
+ +
+ If you want, Firefly can automatically recreate the "envelope" and fill it again + when the timespan above has expired. Be careful with this option though. It makes it easier + to fall back to old habits. + Instead, you should recreate the envelope yourself each [period]. +
+
+ + +
+ {{ Form::label('amount', 'Amount', ['class' => 'col-sm-3 control-label'])}} +
+ + Of course, there needs to be money in the envelope. +
+
+ +
+ {{ Form::label('submit', ' ', ['class' => 'col-sm-3 control-label'])}} +
+ + +
+
+ +
+
+ +{{Form::open()}} + + +@stop +@section('scripts') + + +@stop \ No newline at end of file diff --git a/app/views/partials/menu/budgets.blade.php b/app/views/partials/menu/budgets.blade.php new file mode 100644 index 0000000000..b6e16f1c7f --- /dev/null +++ b/app/views/partials/menu/budgets.blade.php @@ -0,0 +1,28 @@ +getName(); +?> + \ No newline at end of file diff --git a/app/views/preferences/index.blade.php b/app/views/preferences/index.blade.php index 3ebd3b8a5a..a649ea0876 100644 --- a/app/views/preferences/index.blade.php +++ b/app/views/preferences/index.blade.php @@ -5,7 +5,6 @@

Firefly Preferences

- diff --git a/app/views/transactions/journals.blade.php b/app/views/transactions/journals.blade.php index ac551d8cc2..32259b19a6 100644 --- a/app/views/transactions/journals.blade.php +++ b/app/views/transactions/journals.blade.php @@ -5,7 +5,7 @@ Date Amount -@foreach($account->transactionList as $journal) +@foreach($transactions as $journal) diff --git a/bootstrap/start.php b/bootstrap/start.php index b97c1c495b..f6ee9f005c 100644 --- a/bootstrap/start.php +++ b/bootstrap/start.php @@ -94,4 +94,8 @@ require $framework . '/Illuminate/Foundation/start.php'; | */ +// do something with events: +Event::subscribe('Firefly\Trigger\Limits\EloquentLimitTrigger'); + + return $app; diff --git a/public/assets/javascript/date.js b/public/assets/javascript/date.js new file mode 100644 index 0000000000..2d52e9adfe --- /dev/null +++ b/public/assets/javascript/date.js @@ -0,0 +1,104 @@ +/** + * Version: 1.0 Alpha-1 + * Build Date: 13-Nov-2007 + * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved. + * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. + * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/ + */ +Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}}; +Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;idate)?1:(this=start.getTime()&&t<=end.getTime();};Date.prototype.addMilliseconds=function(value){this.setMilliseconds(this.getMilliseconds()+value);return this;};Date.prototype.addSeconds=function(value){return this.addMilliseconds(value*1000);};Date.prototype.addMinutes=function(value){return this.addMilliseconds(value*60000);};Date.prototype.addHours=function(value){return this.addMilliseconds(value*3600000);};Date.prototype.addDays=function(value){return this.addMilliseconds(value*86400000);};Date.prototype.addWeeks=function(value){return this.addMilliseconds(value*604800000);};Date.prototype.addMonths=function(value){var n=this.getDate();this.setDate(1);this.setMonth(this.getMonth()+value);this.setDate(Math.min(n,this.getDaysInMonth()));return this;};Date.prototype.addYears=function(value){return this.addMonths(value*12);};Date.prototype.add=function(config){if(typeof config=="number"){this._orient=config;return this;} +var x=config;if(x.millisecond||x.milliseconds){this.addMilliseconds(x.millisecond||x.milliseconds);} +if(x.second||x.seconds){this.addSeconds(x.second||x.seconds);} +if(x.minute||x.minutes){this.addMinutes(x.minute||x.minutes);} +if(x.hour||x.hours){this.addHours(x.hour||x.hours);} +if(x.month||x.months){this.addMonths(x.month||x.months);} +if(x.year||x.years){this.addYears(x.year||x.years);} +if(x.day||x.days){this.addDays(x.day||x.days);} +return this;};Date._validate=function(value,min,max,name){if(typeof value!="number"){throw new TypeError(value+" is not a Number.");}else if(valuemax){throw new RangeError(value+" is not a valid value for "+name+".");} +return true;};Date.validateMillisecond=function(n){return Date._validate(n,0,999,"milliseconds");};Date.validateSecond=function(n){return Date._validate(n,0,59,"seconds");};Date.validateMinute=function(n){return Date._validate(n,0,59,"minutes");};Date.validateHour=function(n){return Date._validate(n,0,23,"hours");};Date.validateDay=function(n,year,month){return Date._validate(n,1,Date.getDaysInMonth(year,month),"days");};Date.validateMonth=function(n){return Date._validate(n,0,11,"months");};Date.validateYear=function(n){return Date._validate(n,1,9999,"seconds");};Date.prototype.set=function(config){var x=config;if(!x.millisecond&&x.millisecond!==0){x.millisecond=-1;} +if(!x.second&&x.second!==0){x.second=-1;} +if(!x.minute&&x.minute!==0){x.minute=-1;} +if(!x.hour&&x.hour!==0){x.hour=-1;} +if(!x.day&&x.day!==0){x.day=-1;} +if(!x.month&&x.month!==0){x.month=-1;} +if(!x.year&&x.year!==0){x.year=-1;} +if(x.millisecond!=-1&&Date.validateMillisecond(x.millisecond)){this.addMilliseconds(x.millisecond-this.getMilliseconds());} +if(x.second!=-1&&Date.validateSecond(x.second)){this.addSeconds(x.second-this.getSeconds());} +if(x.minute!=-1&&Date.validateMinute(x.minute)){this.addMinutes(x.minute-this.getMinutes());} +if(x.hour!=-1&&Date.validateHour(x.hour)){this.addHours(x.hour-this.getHours());} +if(x.month!==-1&&Date.validateMonth(x.month)){this.addMonths(x.month-this.getMonth());} +if(x.year!=-1&&Date.validateYear(x.year)){this.addYears(x.year-this.getFullYear());} +if(x.day!=-1&&Date.validateDay(x.day,this.getFullYear(),this.getMonth())){this.addDays(x.day-this.getDate());} +if(x.timezone){this.setTimezone(x.timezone);} +if(x.timezoneOffset){this.setTimezoneOffset(x.timezoneOffset);} +return this;};Date.prototype.clearTime=function(){this.setHours(0);this.setMinutes(0);this.setSeconds(0);this.setMilliseconds(0);return this;};Date.prototype.isLeapYear=function(){var y=this.getFullYear();return(((y%4===0)&&(y%100!==0))||(y%400===0));};Date.prototype.isWeekday=function(){return!(this.is().sat()||this.is().sun());};Date.prototype.getDaysInMonth=function(){return Date.getDaysInMonth(this.getFullYear(),this.getMonth());};Date.prototype.moveToFirstDayOfMonth=function(){return this.set({day:1});};Date.prototype.moveToLastDayOfMonth=function(){return this.set({day:this.getDaysInMonth()});};Date.prototype.moveToDayOfWeek=function(day,orient){var diff=(day-this.getDay()+7*(orient||+1))%7;return this.addDays((diff===0)?diff+=7*(orient||+1):diff);};Date.prototype.moveToMonth=function(month,orient){var diff=(month-this.getMonth()+12*(orient||+1))%12;return this.addMonths((diff===0)?diff+=12*(orient||+1):diff);};Date.prototype.getDayOfYear=function(){return Math.floor((this-new Date(this.getFullYear(),0,1))/86400000);};Date.prototype.getWeekOfYear=function(firstDayOfWeek){var y=this.getFullYear(),m=this.getMonth(),d=this.getDate();var dow=firstDayOfWeek||Date.CultureInfo.firstDayOfWeek;var offset=7+1-new Date(y,0,1).getDay();if(offset==8){offset=1;} +var daynum=((Date.UTC(y,m,d,0,0,0)-Date.UTC(y,0,1,0,0,0))/86400000)+1;var w=Math.floor((daynum-offset+7)/7);if(w===dow){y--;var prevOffset=7+1-new Date(y,0,1).getDay();if(prevOffset==2||prevOffset==8){w=53;}else{w=52;}} +return w;};Date.prototype.isDST=function(){console.log('isDST');return this.toString().match(/(E|C|M|P)(S|D)T/)[2]=="D";};Date.prototype.getTimezone=function(){return Date.getTimezoneAbbreviation(this.getUTCOffset,this.isDST());};Date.prototype.setTimezoneOffset=function(s){var here=this.getTimezoneOffset(),there=Number(s)*-6/10;this.addMinutes(there-here);return this;};Date.prototype.setTimezone=function(s){return this.setTimezoneOffset(Date.getTimezoneOffset(s));};Date.prototype.getUTCOffset=function(){var n=this.getTimezoneOffset()*-10/6,r;if(n<0){r=(n-10000).toString();return r[0]+r.substr(2);}else{r=(n+10000).toString();return"+"+r.substr(1);}};Date.prototype.getDayName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedDayNames[this.getDay()]:Date.CultureInfo.dayNames[this.getDay()];};Date.prototype.getMonthName=function(abbrev){return abbrev?Date.CultureInfo.abbreviatedMonthNames[this.getMonth()]:Date.CultureInfo.monthNames[this.getMonth()];};Date.prototype._toString=Date.prototype.toString;Date.prototype.toString=function(format){var self=this;var p=function p(s){return(s.toString().length==1)?"0"+s:s;};return format?format.replace(/dd?d?d?|MM?M?M?|yy?y?y?|hh?|HH?|mm?|ss?|tt?|zz?z?/g,function(format){switch(format){case"hh":return p(self.getHours()<13?self.getHours():(self.getHours()-12));case"h":return self.getHours()<13?self.getHours():(self.getHours()-12);case"HH":return p(self.getHours());case"H":return self.getHours();case"mm":return p(self.getMinutes());case"m":return self.getMinutes();case"ss":return p(self.getSeconds());case"s":return self.getSeconds();case"yyyy":return self.getFullYear();case"yy":return self.getFullYear().toString().substring(2,4);case"dddd":return self.getDayName();case"ddd":return self.getDayName(true);case"dd":return p(self.getDate());case"d":return self.getDate().toString();case"MMMM":return self.getMonthName();case"MMM":return self.getMonthName(true);case"MM":return p((self.getMonth()+1));case"M":return self.getMonth()+1;case"t":return self.getHours()<12?Date.CultureInfo.amDesignator.substring(0,1):Date.CultureInfo.pmDesignator.substring(0,1);case"tt":return self.getHours()<12?Date.CultureInfo.amDesignator:Date.CultureInfo.pmDesignator;case"zzz":case"zz":case"z":return"";}}):this._toString();}; +Date.now=function(){return new Date();};Date.today=function(){return Date.now().clearTime();};Date.prototype._orient=+1;Date.prototype.next=function(){this._orient=+1;return this;};Date.prototype.last=Date.prototype.prev=Date.prototype.previous=function(){this._orient=-1;return this;};Date.prototype._is=false;Date.prototype.is=function(){this._is=true;return this;};Number.prototype._dateElement="day";Number.prototype.fromNow=function(){var c={};c[this._dateElement]=this;return Date.now().add(c);};Number.prototype.ago=function(){var c={};c[this._dateElement]=this*-1;return Date.now().add(c);};(function(){var $D=Date.prototype,$N=Number.prototype;var dx=("sunday monday tuesday wednesday thursday friday saturday").split(/\s/),mx=("january february march april may june july august september october november december").split(/\s/),px=("Millisecond Second Minute Hour Day Week Month Year").split(/\s/),de;var df=function(n){return function(){if(this._is){this._is=false;return this.getDay()==n;} +return this.moveToDayOfWeek(n,this._orient);};};for(var i=0;i0&&!last){try{q=d.call(this,r[1]);}catch(ex){last=true;}}else{last=true;} +if(!last&&q[1].length===0){last=true;} +if(!last){var qx=[];for(var j=0;j0){rx[0]=rx[0].concat(p[0]);rx[1]=p[1];}} +if(rx[1].length1){args=Array.prototype.slice.call(arguments);}else if(arguments[0]instanceof Array){args=arguments[0];} +if(args){for(var i=0,px=args.shift();i2)?n:(n+(((n+2000)Date.getDaysInMonth(this.year,this.month)){throw new RangeError(this.day+" is not a valid value for days.");} +var r=new Date(this.year,this.month,this.day,this.hour,this.minute,this.second);if(this.timezone){r.set({timezone:this.timezone});}else if(this.timezoneOffset){r.set({timezoneOffset:this.timezoneOffset});} +return r;},finish:function(x){x=(x instanceof Array)?flattenAndCompact(x):[x];if(x.length===0){return null;} +for(var i=0;i' + this.series.name + '', width: 250 } ) @@ -146,4 +146,8 @@ $(function () { }); }); + /** + * Get chart data for budget charts. + */ + }); \ No newline at end of file diff --git a/public/assets/javascript/limits.js b/public/assets/javascript/limits.js new file mode 100644 index 0000000000..4e5a2145b0 --- /dev/null +++ b/public/assets/javascript/limits.js @@ -0,0 +1,39 @@ + +console.log(moment().startOf('month').format('YYYY-MM-DD')); + +$(function () { + + $('#this-week').click(function (e) { + $('input[name="startdate"]').val(moment().startOf('isoWeek').format('YYYY-MM-DD')); + $('input[name="enddate"]').val(moment().endOf('isoWeek').format('YYYY-MM-DD')); + return false; + }); + + $('#this-month').click(function (e) { + $('input[name="startdate"]').val(moment().startOf('month').format('YYYY-MM-DD')); + $('input[name="enddate"]').val(moment().endOf('month').format('YYYY-MM-DD')); + return false; + }); + + $('#this-quarter').click(function (e) { + $('input[name="startdate"]').val(moment().startOf('quarter').format('YYYY-MM-DD')); + $('input[name="enddate"]').val(moment().endOf('quarter').format('YYYY-MM-DD')); + return false; + }); + + $('#this-year').click(function (e) { + $('input[name="startdate"]').val(moment().startOf('year').format('YYYY-MM-DD')); + $('input[name="enddate"]').val(moment().endOf('year').format('YYYY-MM-DD')); + return false; + }); + + +}); + + +function formatAsStr(dt) { + return dt.getFullYear() + '-' + + ('0' + (dt.getMonth() + 1)).slice(-2) + '-' + + ('0' + dt.getDate()).slice(-2); +} + diff --git a/public/assets/javascript/moment.min.js b/public/assets/javascript/moment.min.js new file mode 100644 index 0000000000..9c1f3ade9f --- /dev/null +++ b/public/assets/javascript/moment.min.js @@ -0,0 +1,6 @@ +//! moment.js +//! version : 2.7.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +(function(a){function b(a,b,c){switch(arguments.length){case 2:return null!=a?a:b;case 3:return null!=a?a:null!=b?b:c;default:throw new Error("Implement me")}}function c(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function d(a,b){function c(){mb.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+a)}var d=!0;return j(function(){return d&&(c(),d=!1),b.apply(this,arguments)},b)}function e(a,b){return function(c){return m(a.call(this,c),b)}}function f(a,b){return function(c){return this.lang().ordinal(a.call(this,c),b)}}function g(){}function h(a){z(a),j(this,a)}function i(a){var b=s(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+36e5*h,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._bubble()}function j(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return b.hasOwnProperty("toString")&&(a.toString=b.toString),b.hasOwnProperty("valueOf")&&(a.valueOf=b.valueOf),a}function k(a){var b,c={};for(b in a)a.hasOwnProperty(b)&&Ab.hasOwnProperty(b)&&(c[b]=a[b]);return c}function l(a){return 0>a?Math.ceil(a):Math.floor(a)}function m(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.lengthd;d++)(c&&a[d]!==b[d]||!c&&u(a[d])!==u(b[d]))&&g++;return g+f}function r(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=bc[a]||cc[b]||b}return a}function s(a){var b,c,d={};for(c in a)a.hasOwnProperty(c)&&(b=r(c),b&&(d[b]=a[c]));return d}function t(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}mb[b]=function(e,f){var g,h,i=mb.fn._lang[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=mb().utc().set(d,a);return i.call(mb.fn._lang,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function u(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function v(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function w(a,b,c){return bb(mb([a,11,31+b-c]),b,c).week}function x(a){return y(a)?366:365}function y(a){return a%4===0&&a%100!==0||a%400===0}function z(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[tb]<0||a._a[tb]>11?tb:a._a[ub]<1||a._a[ub]>v(a._a[sb],a._a[tb])?ub:a._a[vb]<0||a._a[vb]>23?vb:a._a[wb]<0||a._a[wb]>59?wb:a._a[xb]<0||a._a[xb]>59?xb:a._a[yb]<0||a._a[yb]>999?yb:-1,a._pf._overflowDayOfYear&&(sb>b||b>ub)&&(b=ub),a._pf.overflow=b)}function A(a){return null==a._isValid&&(a._isValid=!isNaN(a._d.getTime())&&a._pf.overflow<0&&!a._pf.empty&&!a._pf.invalidMonth&&!a._pf.nullInput&&!a._pf.invalidFormat&&!a._pf.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===a._pf.charsLeftOver&&0===a._pf.unusedTokens.length)),a._isValid}function B(a){return a?a.toLowerCase().replace("_","-"):a}function C(a,b){return b._isUTC?mb(a).zone(b._offset||0):mb(a).local()}function D(a,b){return b.abbr=a,zb[a]||(zb[a]=new g),zb[a].set(b),zb[a]}function E(a){delete zb[a]}function F(a){var b,c,d,e,f=0,g=function(a){if(!zb[a]&&Bb)try{require("./lang/"+a)}catch(b){}return zb[a]};if(!a)return mb.fn._lang;if(!o(a)){if(c=g(a))return c;a=[a]}for(;f0;){if(c=g(e.slice(0,b).join("-")))return c;if(d&&d.length>=b&&q(e,d,!0)>=b-1)break;b--}f++}return mb.fn._lang}function G(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function H(a){var b,c,d=a.match(Fb);for(b=0,c=d.length;c>b;b++)d[b]=hc[d[b]]?hc[d[b]]:G(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function I(a,b){return a.isValid()?(b=J(b,a.lang()),dc[b]||(dc[b]=H(b)),dc[b](a)):a.lang().invalidDate()}function J(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Gb.lastIndex=0;d>=0&&Gb.test(a);)a=a.replace(Gb,c),Gb.lastIndex=0,d-=1;return a}function K(a,b){var c,d=b._strict;switch(a){case"Q":return Rb;case"DDDD":return Tb;case"YYYY":case"GGGG":case"gggg":return d?Ub:Jb;case"Y":case"G":case"g":return Wb;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?Vb:Kb;case"S":if(d)return Rb;case"SS":if(d)return Sb;case"SSS":if(d)return Tb;case"DDD":return Ib;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Mb;case"a":case"A":return F(b._l)._meridiemParse;case"X":return Pb;case"Z":case"ZZ":return Nb;case"T":return Ob;case"SSSS":return Lb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?Sb:Hb;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Hb;case"Do":return Qb;default:return c=new RegExp(T(S(a.replace("\\","")),"i"))}}function L(a){a=a||"";var b=a.match(Nb)||[],c=b[b.length-1]||[],d=(c+"").match(_b)||["-",0,0],e=+(60*d[1])+u(d[2]);return"+"===d[0]?-e:e}function M(a,b,c){var d,e=c._a;switch(a){case"Q":null!=b&&(e[tb]=3*(u(b)-1));break;case"M":case"MM":null!=b&&(e[tb]=u(b)-1);break;case"MMM":case"MMMM":d=F(c._l).monthsParse(b),null!=d?e[tb]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[ub]=u(b));break;case"Do":null!=b&&(e[ub]=u(parseInt(b,10)));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=u(b));break;case"YY":e[sb]=mb.parseTwoDigitYear(b);break;case"YYYY":case"YYYYY":case"YYYYYY":e[sb]=u(b);break;case"a":case"A":c._isPm=F(c._l).isPM(b);break;case"H":case"HH":case"h":case"hh":e[vb]=u(b);break;case"m":case"mm":e[wb]=u(b);break;case"s":case"ss":e[xb]=u(b);break;case"S":case"SS":case"SSS":case"SSSS":e[yb]=u(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=L(b);break;case"dd":case"ddd":case"dddd":d=F(c._l).weekdaysParse(b),null!=d?(c._w=c._w||{},c._w.d=d):c._pf.invalidWeekday=b;break;case"w":case"ww":case"W":case"WW":case"d":case"e":case"E":a=a.substr(0,1);case"gggg":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=u(b));break;case"gg":case"GG":c._w=c._w||{},c._w[a]=mb.parseTwoDigitYear(b)}}function N(a){var c,d,e,f,g,h,i,j;c=a._w,null!=c.GG||null!=c.W||null!=c.E?(g=1,h=4,d=b(c.GG,a._a[sb],bb(mb(),1,4).year),e=b(c.W,1),f=b(c.E,1)):(j=F(a._l),g=j._week.dow,h=j._week.doy,d=b(c.gg,a._a[sb],bb(mb(),g,h).year),e=b(c.w,1),null!=c.d?(f=c.d,g>f&&++e):f=null!=c.e?c.e+g:g),i=cb(d,e,f,h,g),a._a[sb]=i.year,a._dayOfYear=i.dayOfYear}function O(a){var c,d,e,f,g=[];if(!a._d){for(e=Q(a),a._w&&null==a._a[ub]&&null==a._a[tb]&&N(a),a._dayOfYear&&(f=b(a._a[sb],e[sb]),a._dayOfYear>x(f)&&(a._pf._overflowDayOfYear=!0),d=Z(f,0,a._dayOfYear),a._a[tb]=d.getUTCMonth(),a._a[ub]=d.getUTCDate()),c=0;3>c&&null==a._a[c];++c)a._a[c]=g[c]=e[c];for(;7>c;c++)a._a[c]=g[c]=null==a._a[c]?2===c?1:0:a._a[c];a._d=(a._useUTC?Z:Y).apply(null,g),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()+a._tzm)}}function P(a){var b;a._d||(b=s(a._i),a._a=[b.year,b.month,b.day,b.hour,b.minute,b.second,b.millisecond],O(a))}function Q(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function R(a){if(a._f===mb.ISO_8601)return void V(a);a._a=[],a._pf.empty=!0;var b,c,d,e,f,g=F(a._l),h=""+a._i,i=h.length,j=0;for(d=J(a._f,g).match(Fb)||[],b=0;b0&&a._pf.unusedInput.push(f),h=h.slice(h.indexOf(c)+c.length),j+=c.length),hc[e]?(c?a._pf.empty=!1:a._pf.unusedTokens.push(e),M(e,c,a)):a._strict&&!c&&a._pf.unusedTokens.push(e);a._pf.charsLeftOver=i-j,h.length>0&&a._pf.unusedInput.push(h),a._isPm&&a._a[vb]<12&&(a._a[vb]+=12),a._isPm===!1&&12===a._a[vb]&&(a._a[vb]=0),O(a),z(a)}function S(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function T(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function U(a){var b,d,e,f,g;if(0===a._f.length)return a._pf.invalidFormat=!0,void(a._d=new Date(0/0));for(f=0;fg)&&(e=g,d=b));j(a,d||b)}function V(a){var b,c,d=a._i,e=Xb.exec(d);if(e){for(a._pf.iso=!0,b=0,c=Zb.length;c>b;b++)if(Zb[b][1].exec(d)){a._f=Zb[b][0]+(e[6]||" ");break}for(b=0,c=$b.length;c>b;b++)if($b[b][1].exec(d)){a._f+=$b[b][0];break}d.match(Nb)&&(a._f+="Z"),R(a)}else a._isValid=!1}function W(a){V(a),a._isValid===!1&&(delete a._isValid,mb.createFromInputFallback(a))}function X(b){var c=b._i,d=Cb.exec(c);c===a?b._d=new Date:d?b._d=new Date(+d[1]):"string"==typeof c?W(b):o(c)?(b._a=c.slice(0),O(b)):p(c)?b._d=new Date(+c):"object"==typeof c?P(b):"number"==typeof c?b._d=new Date(c):mb.createFromInputFallback(b)}function Y(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function Z(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function $(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function _(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function ab(a,b,c){var d=rb(Math.abs(a)/1e3),e=rb(d/60),f=rb(e/60),g=rb(f/24),h=rb(g/365),i=d0,i[4]=c,_.apply({},i)}function bb(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=mb(a).add("d",f),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function cb(a,b,c,d,e){var f,g,h=Z(a,0,1).getUTCDay();return h=0===h?7:h,c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:x(a-1)+g}}function db(b){var c=b._i,d=b._f;return null===c||d===a&&""===c?mb.invalid({nullInput:!0}):("string"==typeof c&&(b._i=c=F().preparse(c)),mb.isMoment(c)?(b=k(c),b._d=new Date(+c._d)):d?o(d)?U(b):R(b):X(b),new h(b))}function eb(a,b){var c,d;if(1===b.length&&o(b[0])&&(b=b[0]),!b.length)return mb();for(c=b[0],d=1;d=0?"+":"-";return b+m(Math.abs(a),6)},gg:function(){return m(this.weekYear()%100,2)},gggg:function(){return m(this.weekYear(),4)},ggggg:function(){return m(this.weekYear(),5)},GG:function(){return m(this.isoWeekYear()%100,2)},GGGG:function(){return m(this.isoWeekYear(),4)},GGGGG:function(){return m(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return u(this.milliseconds()/100)},SS:function(){return m(u(this.milliseconds()/10),2)},SSS:function(){return m(this.milliseconds(),3)},SSSS:function(){return m(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+m(u(a/60),2)+":"+m(u(a)%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+m(u(a/60),2)+m(u(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()},Q:function(){return this.quarter()}},ic=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];fc.length;)ob=fc.pop(),hc[ob+"o"]=f(hc[ob],ob);for(;gc.length;)ob=gc.pop(),hc[ob+ob]=e(hc[ob],2);for(hc.DDDD=e(hc.DDD,3),j(g.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=mb.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=mb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return bb(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),mb=function(b,d,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._i=b,g._f=d,g._l=e,g._strict=f,g._isUTC=!1,g._pf=c(),db(g)},mb.suppressDeprecationWarnings=!1,mb.createFromInputFallback=d("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i)}),mb.min=function(){var a=[].slice.call(arguments,0);return eb("isBefore",a)},mb.max=function(){var a=[].slice.call(arguments,0);return eb("isAfter",a)},mb.utc=function(b,d,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._useUTC=!0,g._isUTC=!0,g._l=e,g._i=b,g._f=d,g._strict=f,g._pf=c(),db(g).utc()},mb.unix=function(a){return mb(1e3*a)},mb.duration=function(a,b){var c,d,e,f=a,g=null;return mb.isDuration(a)?f={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(f={},b?f[b]=a:f.milliseconds=a):(g=Db.exec(a))?(c="-"===g[1]?-1:1,f={y:0,d:u(g[ub])*c,h:u(g[vb])*c,m:u(g[wb])*c,s:u(g[xb])*c,ms:u(g[yb])*c}):(g=Eb.exec(a))&&(c="-"===g[1]?-1:1,e=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*c},f={y:e(g[2]),M:e(g[3]),d:e(g[4]),h:e(g[5]),m:e(g[6]),s:e(g[7]),w:e(g[8])}),d=new i(f),mb.isDuration(a)&&a.hasOwnProperty("_lang")&&(d._lang=a._lang),d},mb.version=pb,mb.defaultFormat=Yb,mb.ISO_8601=function(){},mb.momentProperties=Ab,mb.updateOffset=function(){},mb.relativeTimeThreshold=function(b,c){return ec[b]===a?!1:(ec[b]=c,!0)},mb.lang=function(a,b){var c;return a?(b?D(B(a),b):null===b?(E(a),a="en"):zb[a]||F(a),c=mb.duration.fn._lang=mb.fn._lang=F(a),c._abbr):mb.fn._lang._abbr},mb.langData=function(a){return a&&a._lang&&a._lang._abbr&&(a=a._lang._abbr),F(a)},mb.isMoment=function(a){return a instanceof h||null!=a&&a.hasOwnProperty("_isAMomentObject")},mb.isDuration=function(a){return a instanceof i},ob=ic.length-1;ob>=0;--ob)t(ic[ob]);mb.normalizeUnits=function(a){return r(a)},mb.invalid=function(a){var b=mb.utc(0/0);return null!=a?j(b._pf,a):b._pf.userInvalidated=!0,b},mb.parseZone=function(){return mb.apply(null,arguments).parseZone()},mb.parseTwoDigitYear=function(a){return u(a)+(u(a)>68?1900:2e3)},j(mb.fn=h.prototype,{clone:function(){return mb(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=mb(this).utc();return 00:!1},parsingFlags:function(){return j({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(a){var b=I(this,a||mb.defaultFormat);return this.lang().postformat(b)},add:function(a,b){var c;return c="string"==typeof a&&"string"==typeof b?mb.duration(isNaN(+b)?+a:+b,isNaN(+b)?b:a):"string"==typeof a?mb.duration(+b,a):mb.duration(a,b),n(this,c,1),this},subtract:function(a,b){var c;return c="string"==typeof a&&"string"==typeof b?mb.duration(isNaN(+b)?+a:+b,isNaN(+b)?b:a):"string"==typeof a?mb.duration(+b,a):mb.duration(a,b),n(this,c,-1),this},diff:function(a,b,c){var d,e,f=C(a,this),g=6e4*(this.zone()-f.zone());return b=r(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+f.daysInMonth()),e=12*(this.year()-f.year())+(this.month()-f.month()),e+=(this-mb(this).startOf("month")-(f-mb(f).startOf("month")))/d,e-=6e4*(this.zone()-mb(this).startOf("month").zone()-(f.zone()-mb(f).startOf("month").zone()))/d,"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:l(e)},from:function(a,b){return mb.duration(this.diff(a)).lang(this.lang()._abbr).humanize(!b)},fromNow:function(a){return this.from(mb(),a)},calendar:function(a){var b=a||mb(),c=C(b,this).startOf("day"),d=this.diff(c,"days",!0),e=-6>d?"sameElse":-1>d?"lastWeek":0>d?"lastDay":1>d?"sameDay":2>d?"nextDay":7>d?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){return y(this.year())},isDST:function(){return this.zone()+mb(a).startOf(b)},isBefore:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)<+mb(a).startOf(b)},isSame:function(a,b){return b=b||"ms",+this.clone().startOf(b)===+C(a,this).startOf(b)},min:d("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(a){return a=mb.apply(null,arguments),this>a?this:a}),max:d("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(a){return a=mb.apply(null,arguments),a>this?this:a}),zone:function(a,b){var c=this._offset||0;return null==a?this._isUTC?c:this._d.getTimezoneOffset():("string"==typeof a&&(a=L(a)),Math.abs(a)<16&&(a=60*a),this._offset=a,this._isUTC=!0,c!==a&&(!b||this._changeInProgress?n(this,mb.duration(c-a,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,mb.updateOffset(this,!0),this._changeInProgress=null)),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.zone(this._tzm):"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(a){return a=a?mb(a).zone():0,(this.zone()-a)%60===0},daysInMonth:function(){return v(this.year(),this.month())},dayOfYear:function(a){var b=rb((mb(this).startOf("day")-mb(this).startOf("year"))/864e5)+1;return null==a?b:this.add("d",a-b)},quarter:function(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)},weekYear:function(a){var b=bb(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==a?b:this.add("y",a-b)},isoWeekYear:function(a){var b=bb(this,1,4).year;return null==a?b:this.add("y",a-b)},week:function(a){var b=this.lang().week(this);return null==a?b:this.add("d",7*(a-b))},isoWeek:function(a){var b=bb(this,1,4).week;return null==a?b:this.add("d",7*(a-b))},weekday:function(a){var b=(this.day()+7-this.lang()._week.dow)%7;return null==a?b:this.add("d",a-b)},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},isoWeeksInYear:function(){return w(this.year(),1,4)},weeksInYear:function(){var a=this._lang._week;return w(this.year(),a.dow,a.doy)},get:function(a){return a=r(a),this[a]()},set:function(a,b){return a=r(a),"function"==typeof this[a]&&this[a](b),this},lang:function(b){return b===a?this._lang:(this._lang=F(b),this)}}),mb.fn.millisecond=mb.fn.milliseconds=ib("Milliseconds",!1),mb.fn.second=mb.fn.seconds=ib("Seconds",!1),mb.fn.minute=mb.fn.minutes=ib("Minutes",!1),mb.fn.hour=mb.fn.hours=ib("Hours",!0),mb.fn.date=ib("Date",!0),mb.fn.dates=d("dates accessor is deprecated. Use date instead.",ib("Date",!0)),mb.fn.year=ib("FullYear",!0),mb.fn.years=d("years accessor is deprecated. Use year instead.",ib("FullYear",!0)),mb.fn.days=mb.fn.day,mb.fn.months=mb.fn.month,mb.fn.weeks=mb.fn.week,mb.fn.isoWeeks=mb.fn.isoWeek,mb.fn.quarters=mb.fn.quarter,mb.fn.toJSON=mb.fn.toISOString,j(mb.duration.fn=i.prototype,{_bubble:function(){var a,b,c,d,e=this._milliseconds,f=this._days,g=this._months,h=this._data;h.milliseconds=e%1e3,a=l(e/1e3),h.seconds=a%60,b=l(a/60),h.minutes=b%60,c=l(b/60),h.hours=c%24,f+=l(c/24),h.days=f%30,g+=l(f/30),h.months=g%12,d=l(g/12),h.years=d},weeks:function(){return l(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*u(this._months/12)},humanize:function(a){var b=+this,c=ab(b,!a,this.lang());return a&&(c=this.lang().pastFuture(b,c)),this.lang().postformat(c)},add:function(a,b){var c=mb.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=mb.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=r(a),this[a.toLowerCase()+"s"]()},as:function(a){return a=r(a),this["as"+a.charAt(0).toUpperCase()+a.slice(1)+"s"]()},lang:mb.fn.lang,toIsoString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"}});for(ob in ac)ac.hasOwnProperty(ob)&&(kb(ob,ac[ob]),jb(ob.toLowerCase()));kb("Weeks",6048e5),mb.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},mb.lang("en",{ordinal:function(a){var b=a%10,c=1===u(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),Bb?module.exports=mb:"function"==typeof define&&define.amd?(define("moment",function(a,b,c){return c.config&&c.config()&&c.config().noGlobal===!0&&(qb.moment=nb),mb}),lb(!0)):lb()}).call(this); \ No newline at end of file