diff --git a/README.md b/README.md index 35ed614ab1..d44acfefd3 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,6 @@ Everything is organised: - Clear views that should show you how you're doing; - Easy navigation through your records; - Browse back and forth to see previous months or even years; -- Lots of help text in case you don't get it; - Lots of charts because we all love them. ## Changes @@ -36,6 +35,7 @@ Everything is organised: Firefly III will feature, but does not feature yet: - Financial reporting showing you how well you are doing; +- Lots of help text in case you don't get it; - More control over other resources outside of personal finance - Accounts shared with a partner (household accounts) - Debts @@ -51,6 +51,10 @@ Some stuff has been removed: - The nesting of budgets, categories and beneficiaries is removed because it was pretty pointless. - Firefly will not encrypt the content of the (MySQL) tables. Old versions of Firefly had this capability but it sucks when searching, sorting and organizing entries. +## Screenshots + +![Index](http://i.imgur.com/oUQ1UhU.png) + ## Current state I have the basics up and running. Test coverage is currently non-existent. diff --git a/app/config/app.php b/app/config/app.php index 66190ce4e2..b997979dd7 100644 --- a/app/config/app.php +++ b/app/config/app.php @@ -43,7 +43,7 @@ return [ 'Firefly\Helper\HelperServiceProvider', 'Firefly\Validation\ValidationServiceProvider', 'DaveJamesMiller\Breadcrumbs\ServiceProvider', - 'TwigBridge\ServiceProvider' + 'Grumpydictator\Gchart\GchartServiceProvider', ], 'manifest' => storage_path() . '/meta', 'aliases' => [ diff --git a/app/config/firefly.php b/app/config/firefly.php index 61ccb7820e..bd7db200f6 100644 --- a/app/config/firefly.php +++ b/app/config/firefly.php @@ -2,10 +2,14 @@ use Carbon\Carbon; return [ - 'index_periods' => ['1D', '1W', '1M', '3M', '6M','1Y', 'custom'], - 'budget_periods' => ['daily', 'weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], - 'piggybank_periods' => ['day', 'week', 'month', 'year'], - 'periods_to_text' => [ + 'index_periods' => ['1D', '1W', '1M', '3M', '6M', '1Y', 'custom'], + 'budget_periods' => ['daily', 'weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], + 'piggybank_periods' => [ + 'week' => 'Week', + 'month' => 'Month', + 'year' => 'Year' + ], + 'periods_to_text' => [ 'weekly' => 'A week', 'monthly' => 'A month', 'quarterly' => 'A quarter', @@ -13,7 +17,7 @@ return [ 'yearly' => 'A year', ], - 'range_to_text' => [ + 'range_to_text' => [ '1D' => 'day', '1W' => 'week', '1M' => 'month', @@ -21,15 +25,15 @@ return [ '6M' => 'half year', 'custom' => '(custom)' ], - 'range_to_name' => [ - '1D' => 'one day', - '1W' => 'one week', - '1M' => 'one month', - '3M' => 'three months', - '6M' => 'six months', - '1Y' => 'one year', + 'range_to_name' => [ + '1D' => 'one day', + '1W' => 'one week', + '1M' => 'one month', + '3M' => 'three months', + '6M' => 'six months', + '1Y' => 'one year', ], - 'range_to_repeat_freq' => [ + 'range_to_repeat_freq' => [ '1D' => 'weekly', '1W' => 'weekly', '1M' => 'monthly', diff --git a/app/controllers/AccountController.php b/app/controllers/AccountController.php index 2ddcd82e42..ba394611ae 100644 --- a/app/controllers/AccountController.php +++ b/app/controllers/AccountController.php @@ -1,31 +1,116 @@ _accounts = $accounts; - $this->_repository = $repository; View::share('mainTitleIcon', 'fa-credit-card'); View::share('title', 'Accounts'); } + /** + * @param string $what + * + * @return View + * @throws FireflyException + */ + public function index($what = 'default') + { + switch ($what) { + default: + throw new FireflyException('Cannot handle account type "' . e($what) . '".'); + break; + case 'asset': + View::share('subTitleIcon', 'fa-money'); + View::share('subTitle', 'Asset accounts'); + break; + case 'expense': + View::share('subTitleIcon', 'fa-shopping-cart'); + View::share('subTitle', 'Expense accounts'); + break; + case 'revenue': + View::share('subTitleIcon', 'fa-download'); + View::share('subTitle', 'Revenue accounts'); + break; + } + return View::make('accounts.index')->with('what', $what); + } + + + /** + * @param string $what + * + * @return \Illuminate\Http\JsonResponse + * @throws FireflyException + */ + public function json($what = 'default') + { + /** @var \FireflyIII\Database\Account $acct */ + $acct = App::make('FireflyIII\Database\Account'); + + /** @var \FireflyIII\Shared\Json\Json $json */ + $json = App::make('FireflyIII\Shared\Json\Json'); + + $parameters = $json->dataTableParameters(); + + switch ($what) { + default: + throw new FireflyException('Cannot handle account type "' . e($what) . '".'); + break; + case 'asset': + $accounts = $acct->getAssetAccounts($parameters); + $count = $acct->countAssetAccounts(); + break; + case 'expense': + $accounts = $acct->getExpenseAccounts($parameters); + $count = $acct->countExpenseAccounts(); + break; + case 'revenue': + $accounts = $acct->getRevenueAccounts($parameters); + $count = $acct->countRevenueAccounts(); + break; + } + + /* + * Output the set compatible with data tables: + */ + $return = [ + 'draw' => intval(Input::get('draw')), + 'recordsTotal' => $count, + 'recordsFiltered' => $accounts->count(), + 'data' => [], + ]; + + /* + * Loop the accounts: + */ + /** @var \Account $account */ + foreach ($accounts as $account) { + $entry = [ + 'name' => ['name' => $account->name, 'url' => route('accounts.show', $account->id)], + 'balance' => $account->balance(), + 'id' => [ + 'edit' => route('accounts.edit', $account->id), + 'delete' => route('accounts.delete', $account->id), + ] + ]; + $return['data'][] = $entry; + } + + + return Response::jsoN($return); + } + /** * @return \Illuminate\View\View */ @@ -43,52 +128,7 @@ class AccountController extends \BaseController break; } - - return View::make('accounts.create')->with('subTitle', 'Create a new ' . $what . ' account')->with( - 'what', $what - ); - } - - /** - * @return $this - */ - public function asset() - { - View::share('subTitleIcon', 'fa-money'); - - $accounts = $this->_repository->getOfTypes(['Asset account', 'Default account']); - - return View::make('accounts.asset')->with('subTitle', 'Asset accounts')->with( - 'accounts', $accounts - ); - } - - /** - * @return $this - */ - public function expense() - { - View::share('subTitleIcon', 'fa-shopping-cart'); - - $accounts = $this->_repository->getOfTypes(['Expense account', 'Beneficiary account']); - - return View::make('accounts.expense')->with('subTitle', 'Expense accounts')->with( - 'accounts', $accounts - ); - } - - /** - * @return $this - */ - public function revenue() - { - View::share('subTitleIcon', 'fa-download'); - - $accounts = $this->_repository->getOfTypes(['Revenue account']); - - return View::make('accounts.revenue')->with('subTitle', 'Revenue accounts')->with( - 'accounts', $accounts - ); + return View::make('accounts.create')->with('subTitle', 'Create a new ' . $what . ' account')->with('what', $what); } /** @@ -99,9 +139,9 @@ class AccountController extends \BaseController public function delete(Account $account) { return View::make('accounts.delete')->with('account', $account) - ->with( - 'subTitle', 'Delete ' . strtolower($account->accountType->type) . ' "' . $account->name . '"' - ); + ->with( + 'subTitle', 'Delete ' . strtolower($account->accountType->type) . ' "' . $account->name . '"' + ); } /** @@ -111,20 +151,67 @@ class AccountController extends \BaseController */ public function destroy(Account $account) { + $type = $account->accountType->type; - $this->_repository->destroy($account); + + /** @var \FireflyIII\Database\Account $acct */ + $acct = App::make('FireflyIII\Database\Account'); + + /** @var \FireflyIII\Database\TransactionJournal $jrnls */ + $jrnls = App::make('FireflyIII\Database\TransactionJournal'); + + /* + * Find the "initial balance account", should it exist: + */ + $initialBalance = $acct->findInitialBalanceAccount($account); + + /* + * Get all the transaction journals that are part of this/these account(s): + */ + $journals = []; + if ($initialBalance) { + $transactions = $initialBalance->transactions()->get(); + /** @var \Transaction $transaction */ + foreach ($transactions as $transaction) { + $journals[] = $transaction->transaction_journal_id; + } + } + /** @var \Transaction $transaction */ + foreach ($account->transactions() as $transaction) { + $journals[] = $transaction->transaction_journal_id; + } + + $journals = array_unique($journals); + + /* + * Delete the journals. Should get rid of the transactions as well. + */ + foreach ($journals as $id) { + $journal = $jrnls->find($id); + $journal->delete(); + } + + /* + * Delete it + */ + if ($initialBalance) { + $acct->destroy($initialBalance); + } + + $acct->destroy($account); + Session::flash('success', 'The account was deleted.'); switch ($type) { case 'Asset account': case 'Default account': - return Redirect::route('accounts.asset'); + return Redirect::route('accounts.index', 'asset'); break; case 'Expense account': case 'Beneficiary account': - return Redirect::route('accounts.expense'); + return Redirect::route('accounts.index', 'expense'); break; case 'Revenue account': - return Redirect::route('accounts.revenue'); + return Redirect::route('accounts.index', 'revenue'); break; } @@ -153,18 +240,23 @@ class AccountController extends \BaseController break; } - $openingBalance = $this->_accounts->openingBalanceTransaction($account); - return View::make('accounts.edit')->with('account', $account)->with('openingBalance', $openingBalance) + /** @var \FireflyIII\Database\Account $acct */ + $acct = App::make('FireflyIII\Database\Account'); - ->with('subTitle', 'Edit ' . strtolower($account->accountType->type) . ' "' . $account->name . '"'); - } + $openingBalance = $acct->openingBalanceTransaction($account); + Session::forget('prefilled'); + if (!is_null($openingBalance)) { + $prefilled['openingbalancedate'] = $openingBalance->date->format('Y-m-d'); + $prefilled['openingbalance'] = floatval($openingBalance->transactions()->where('account_id', $account->id)->first()->amount); + Session::flash('prefilled', $prefilled); + } - /** - * @return $this - */ - public function index() - { - return View::make('error')->with('message', 'This view has been disabled'); + + return View::make('accounts.edit')->with('account', $account)->with('openingBalance', $openingBalance)->with( + 'subTitle', 'Edit ' . strtolower( + $account->accountType->type + ) . ' "' . $account->name . '"' + ); } /** @@ -189,86 +281,116 @@ class AccountController extends \BaseController } - $data = $this->_accounts->show($account, 40); - - return View::make('accounts.show')->with('account', $account)->with('show', $data)->with( - 'subTitle', - 'Details for ' . strtolower($account->accountType->type) . ' "' . $account->name . '"' - ); + //$data = $this->_accounts->show($account, 40); + return View::make('accounts.show') + ->with('account', $account) + ->with('subTitle', 'Details for ' . strtolower($account->accountType->type) . ' "' . $account->name . '"'); } /** * @return $this|\Illuminate\Http\RedirectResponse + * @throws FireflyException */ public function store() { $data = Input::all(); $data['what'] = isset($data['what']) && $data['what'] != '' ? $data['what'] : 'asset'; + /** @var \FireflyIII\Database\Account $acct */ + $acct = App::make('FireflyIII\Database\Account'); - - switch ($data['what']) { + switch ($data['post_submit_action']) { default: - case 'asset': - $data['account_type'] = 'Asset account'; - break; - case 'expense': - $data['account_type'] = 'Expense account'; - break; - case 'revenue': - $data['account_type'] = 'Revenue account'; + throw new FireflyException('Cannot handle post_submit_action "' . e($data['post_submit_action']) . '"'); break; + case 'create_another': + case 'store': + $messages = $acct->validate($data); + /** @var MessageBag $messages ['errors'] */ + if ($messages['errors']->count() > 0) { + Session::flash('warnings', $messages['warnings']); + Session::flash('successes', $messages['successes']); + Session::flash('error', 'Could not save account: ' . $messages['errors']->first()); + return Redirect::route('accounts.create', $data['what'])->withInput()->withErrors($messages['errors']); + } + // store! + $acct->store($data); + Session::flash('success', 'New account stored!'); - } - $account = $this->_repository->store($data); - - if ($account->validate()) { - // saved! return to wherever. - Session::flash('success', 'Account "' . $account->name . '" created!'); - if (intval(Input::get('create')) === 1) { + if ($data['post_submit_action'] == 'create_another') { + return Redirect::route('accounts.create', $data['what']); + } else { + return Redirect::route('accounts.index', $data['what']); + } + break; + case 'validate_only': + $messageBags = $acct->validate($data); + Session::flash('warnings', $messageBags['warnings']); + Session::flash('successes', $messageBags['successes']); + Session::flash('errors', $messageBags['errors']); return Redirect::route('accounts.create', $data['what'])->withInput(); - } else { - - return Redirect::route('accounts.' . e($data['what'])); - } - } else { - // did not save, return with error: - Session::flash('error', 'Could not save the new account: ' . $account->errors()->first()); - - return Redirect::route('accounts.create', $data['what'])->withErrors($account->errors())->withInput(); - + break; } } /** * @param Account $account * - * @return $this|\Illuminate\Http\RedirectResponse + * @return $this + * @throws FireflyException */ public function update(Account $account) { - /** @var \Account $account */ - $account = $this->_repository->update($account, Input::all()); - if ($account->validate()) { - Session::flash('success', 'Account "' . $account->name . '" updated.'); - switch ($account->accountType->type) { - case 'Asset account': - case 'Default account': - return Redirect::route('accounts.asset'); - break; - case 'Expense account': - case 'Beneficiary account': - return Redirect::route('accounts.expense'); - break; - case 'Revenue account': - return Redirect::route('accounts.revenue'); - break; - } - } else { - Session::flash('error', 'Could not update account: ' . $account->errors()->first()); + /** @var \FireflyIII\Database\Account $acct */ + $acct = App::make('FireflyIII\Database\Account'); + $data = Input::except('_token'); - return Redirect::route('accounts.edit', $account->id)->withInput()->withErrors($account->errors()); + switch ($account->accountType->type) { + default: + throw new FireflyException('Cannot handle account type "' . e($account->accountType->type) . '"'); + break; + case 'Default account': + $data['what'] = 'asset'; + break; + case 'Beneficiary account': + $data['what'] = 'expense'; + break; + case 'Revenue account': + $data['what'] = 'revenue'; + break; + } + + switch (Input::get('post_submit_action')) { + default: + throw new FireflyException('Cannot handle post_submit_action "' . e(Input::get('post_submit_action')) . '"'); + break; + case 'create_another': + case 'update': + $messages = $acct->validate($data); + /** @var MessageBag $messages ['errors'] */ + if ($messages['errors']->count() > 0) { + Session::flash('warnings', $messages['warnings']); + Session::flash('successes', $messages['successes']); + Session::flash('error', 'Could not save account: ' . $messages['errors']->first()); + return Redirect::route('accounts.edit', $account->id)->withInput()->withErrors($messages['errors']); + } + // store! + $acct->update($account, $data); + Session::flash('success', 'Account updated!'); + + if ($data['post_submit_action'] == 'create_another') { + return Redirect::route('accounts.edit', $account->id); + } else { + return Redirect::route('accounts.index',$data['what']); + } + case 'validate_only': + $messageBags = $acct->validate($data); + Session::flash('warnings', $messageBags['warnings']); + Session::flash('successes', $messageBags['successes']); + Session::flash('errors', $messageBags['errors']); + return Redirect::route('accounts.edit', $account->id)->withInput(); + break; } } diff --git a/app/controllers/BudgetController.php b/app/controllers/BudgetController.php index 6ed90c18f5..78f7dc2dd8 100644 --- a/app/controllers/BudgetController.php +++ b/app/controllers/BudgetController.php @@ -4,72 +4,175 @@ use Carbon\Carbon; use Firefly\Exception\FireflyException; use Firefly\Helper\Controllers\BudgetInterface as BI; use Firefly\Storage\Budget\BudgetRepositoryInterface as BRI; +use FireflyIII\Exception\NotImplementedException; +use Illuminate\Support\MessageBag; + /** * Class BudgetController - * - * @SuppressWarnings(PHPMD.CamelCasePropertyName) - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * */ class BudgetController extends BaseController { - protected $_budgets; - protected $_repository; - /** - * @param BI $budgets - * @param BRI $repository - */ - public function __construct(BI $budgets, BRI $repository) + public function __construct() { - $this->_budgets = $budgets; - $this->_repository = $repository; View::share('title', 'Budgets'); View::share('mainTitleIcon', 'fa-tasks'); } - public function nobudget($view = 'session') { - switch($view) { - default: - throw new FireflyException('Cannot show transactions without a budget for view "'.$view.'".'); - break; - case 'session': - $start = Session::get('start'); - $end = Session::get('end'); - break; - } + /** + * @return \Illuminate\Http\RedirectResponse + */ + public function postUpdateIncome() + { + /** @var \Firefly\Helper\Preferences\PreferencesHelperInterface $preferences */ + $preferences = App::make('Firefly\Helper\Preferences\PreferencesHelperInterface'); + $date = Session::get('start'); - // Add expenses that have no budget: - $set = \Auth::user()->transactionjournals()->whereNotIn( - 'transaction_journals.id', function ($query) use ($start, $end) { - $query->select('transaction_journals.id')->from('transaction_journals') - ->leftJoin( - 'component_transaction_journal', 'component_transaction_journal.transaction_journal_id', '=', - 'transaction_journals.id' - ) - ->leftJoin('components', 'components.id', '=', 'component_transaction_journal.component_id') - ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) - ->where('components.class', 'Budget'); - } - )->before($end)->after($start)->get(); - - return View::make('budgets.nobudget') - ->with('view', $view) - ->with('transactions',$set) - ->with('subTitle', 'Transactions without a budget'); + $value = intval(Input::get('amount')); + $preferences->set('budgetIncomeTotal' . $date->format('FY'), $value); + return Redirect::route('budgets.index'); } /** - * @return $this|\Illuminate\View\View + * Update the amount for a budget's limitrepetition and/or create it. + * + * @param Budget $budget + */ + public function amount(Budget $budget) + { + $amount = intval(Input::get('amount')); + $date = Session::get('start'); + /** @var \Limit $limit */ + $limit = $budget->limits()->where('startdate', $date->format('Y-m-d'))->first(); + if (!$limit) { + // create one! + $limit = new Limit; + $limit->budget()->associate($budget); + $limit->startdate = $date; + $limit->amount = $amount; + $limit->repeat_freq = 'monthly'; + $limit->repeats = 0; + $limit->save(); + Event::fire('limits.store', [$limit]); + + } else { + if ($amount > 0) { + $limit->amount = $amount; + $limit->save(); + Event::fire('limits.update', [$limit]); + } else { + $limit->delete(); + } + } + // try to find the limit repetition for this limit: + $repetition = $limit->limitrepetitions()->first(); + if ($repetition) { + return Response::json(['name' => $budget->name, 'repetition' => $repetition->id]); + } else { + return Response::json(['name' => $budget->name, 'repetition' => null]); + } + + } + + public function index() + { + + /** @var \Firefly\Helper\Preferences\PreferencesHelperInterface $preferences */ + $preferences = App::make('Firefly\Helper\Preferences\PreferencesHelperInterface'); + $date = Session::get('start'); + + /** @var \FireflyIII\Database\Budget $repos */ + $repos = App::make('FireflyIII\Database\Budget'); + $budgets = $repos->get(); + + // get the limits for the current month. + $date = \Session::get('start'); + $spent = 0; + /** @var \Budget $budget */ + foreach ($budgets as $budget) { + + $budget->spent = $repos->spentInMonth($budget, $date); + $spent += $budget->spent; + $budget->pct = 0; + $budget->limit = 0; + + /** @var \Limit $limit */ + foreach ($budget->limits as $limit) { + /** @var \LimitRepetition $repetition */ + foreach ($limit->limitrepetitions as $repetition) { + if ($repetition->startdate == $date) { + $budget->currentRep = $repetition; + $budget->limit = floatval($repetition->amount); + if ($budget->limit > $budget->spent) { + // not overspent: + $budget->pct = 30; + } else { + $budget->pct = 50; + } + + } + } + } + } + + $budgetAmount = $preferences->get('budgetIncomeTotal' . $date->format('FY'), 1000); + $amount = floatval($budgetAmount->data); + $overspent = $spent > $amount; + if($overspent) { + // overspent on total amount + $spentPCT = ceil($amount / $spent * 100); + } else { + // not overspent on total amount. + $spentPCT = ceil($spent / $amount * 100); + } + + return View::make('budgets.index', compact('budgets','spent','spentPCT','overspent'))->with('budgetAmount', $budgetAmount); + } + + /** + * @return $this + */ + public function updateIncome() + { + $date = Session::get('start'); + /** @var \Firefly\Helper\Preferences\PreferencesHelperInterface $preferences */ + $preferences = App::make('Firefly\Helper\Preferences\PreferencesHelperInterface'); + $budgetAmount = $preferences->get('budgetIncomeTotal' . $date->format('FY'), 1000); + return View::make('budgets.income')->with('amount', $budgetAmount)->with('date', $date); + } + + /** + * @param Budget $budget + * @param LimitRepetition $repetition + * + * @return \Illuminate\View\View + */ + public function show(Budget $budget, LimitRepetition $repetition = null) + { + if (!is_null($repetition) && $repetition->limit->budget->id != $budget->id) { + App::abort(500); + } + + if (is_null($repetition)) { + // get all other repetitions: + $limits = $budget->limits()->orderBy('startdate', 'DESC')->get(); + + } else { + // get nothing? i dunno + $limits = [$repetition->limit]; + } + + return View::make('budgets.show', compact('limits', 'budget', 'repetition')); + } + + /** + * @return $this */ public function create() { - $periods = \Config::get('firefly.periods_to_text'); - - return View::make('budgets.create')->with('periods', $periods)->with('subTitle', 'Create a new budget'); + return View::make('budgets.create')->with('subTitle', 'Create a new budget'); } /** @@ -79,27 +182,17 @@ class BudgetController extends BaseController */ public function delete(Budget $budget) { - return View::make('budgets.delete')->with('budget', $budget) - ->with('subTitle', 'Delete budget "' . $budget->name . '"'); + return View::make('budgets.delete')->with('budget', $budget)->with('subTitle', 'Delete budget "' . $budget->name . '"'); } - /** - * @param Budget $budget - * - * @return \Illuminate\Http\RedirectResponse - */ public function destroy(Budget $budget) { + /** @var \FireflyIII\Database\Budget $repos */ + $repos = App::make('FireflyIII\Database\Budget'); // remove budget - Event::fire('budgets.destroy', [$budget]); // just before deletion. - $this->_repository->destroy($budget); + $repos->destroy($budget); Session::flash('success', 'The budget was deleted.'); - - // redirect: - if (Input::get('from') == 'date') { - return Redirect::route('budgets.index'); - } - return Redirect::route('budgets.index.budget'); + return Redirect::route('budgets.index'); } @@ -110,142 +203,96 @@ class BudgetController extends BaseController */ public function edit(Budget $budget) { - return View::make('budgets.edit')->with('budget', $budget) - ->with('subTitle', 'Edit budget "' . $budget->name . '"'); + Session::flash('prefilled', ['name' => $budget->name]); + return View::make('budgets.edit')->with('budget', $budget)->with('subTitle', 'Edit budget "' . $budget->name . '"'); } - /** - * @return $this|\Illuminate\View\View - */ - public function indexByBudget() - { - View::share('subTitleIcon', 'fa-folder-open'); - - $budgets = $this->_repository->get(); - - return View::make('budgets.indexByBudget')->with('budgets', $budgets)->with('today', new Carbon) - ->with('subTitle', 'Grouped by budget'); - - } - - /** - * @return $this - */ - public function indexByDate() - { - View::share('subTitleIcon', 'fa-calendar'); - - // get a list of dates by getting all repetitions: - $set = $this->_repository->get(); - $budgets = $this->_budgets->organizeByDate($set); - - return View::make('budgets.indexByDate')->with('budgets', $budgets) - ->with('subTitle', 'Grouped by date'); - - - } - - /** - * Three use cases for this view: - * - * - Show everything. - * - Show a specific repetition. - * - Show everything shows NO repetition. - * - * @param Budget $budget - * @param LimitRepetition $repetition - * - * @return int - */ - public function show(Budget $budget, \LimitRepetition $repetition = null) - { - $useSessionDates = Input::get('useSession') == 'true' ? true : false; - $view = null; - $title = null; - \Log::debug('Is envelope true? ' . (Input::get('noenvelope') == 'true')); - switch (true) { - case (!is_null($repetition)): - $data = $this->_budgets->organizeRepetition($repetition); - $view = 1; - $title = $budget->name . ', ' . $repetition->periodShow() . ', ' . mf( - $repetition->limit->amount, - false - ); - break; - case (Input::get('noenvelope') == 'true'): - $data = $this->_budgets->outsideRepetitions($budget); - $view = 2; - $title = $budget->name . ', transactions outside an envelope'; - break; - default: - $data = $this->_budgets->organizeRepetitions($budget, $useSessionDates); - $view = $useSessionDates ? 3 : 4; - $title = $useSessionDates ? $budget->name . ' in session period' : $budget->name; - break; - } - - return View::make('budgets.show') - ->with('budget', $budget) - ->with('repetitions', $data) - ->with('view', $view) - ->with('highlight', Input::get('highlight')) - ->with('useSessionDates', $useSessionDates) - ->with('subTitle', 'Overview for ' . $title); - } - /** * @return \Illuminate\Http\RedirectResponse */ public function store() { + /** @var \FireflyIII\Database\Budget $repos */ + $repos = App::make('FireflyIII\Database\Budget'); + $data = Input::except('_token'); - $budget = $this->_repository->store(Input::all()); - if ($budget->validate()) { - Event::fire('budgets.store', [$budget]); - Session::flash('success', 'Budget created!'); + switch ($data['post_submit_action']) { + default: + throw new FireflyException('Cannot handle post_submit_action "' . e($data['post_submit_action']) . '"'); + break; + case 'create_another': + case 'store': + $messages = $repos->validate($data); + /** @var MessageBag $messages ['errors'] */ + if ($messages['errors']->count() > 0) { + Session::flash('warnings', $messages['warnings']); + Session::flash('successes', $messages['successes']); + Session::flash('error', 'Could not save budget: ' . $messages['errors']->first()); + return Redirect::route('budgets.create')->withInput()->withErrors($messages['errors']); + } + // store! + $repos->store($data); + Session::flash('success', 'New budget stored!'); - if (Input::get('create') == '1') { - return Redirect::route('budgets.create', ['from' => Input::get('from')]); - } - - if (Input::get('from') == 'date') { - return Redirect::route('budgets.index'); - } else { - return Redirect::route('budgets.index.budget'); - } - } else { - Session::flash('error', 'Could not save the new budget'); - - return Redirect::route('budgets.create')->withInput()->withErrors($budget->errors()); + if ($data['post_submit_action'] == 'create_another') { + return Redirect::route('budgets.create'); + } else { + return Redirect::route('budgets.index'); + } + break; + case 'validate_only': + $messageBags = $repos->validate($data); + Session::flash('warnings', $messageBags['warnings']); + Session::flash('successes', $messageBags['successes']); + Session::flash('errors', $messageBags['errors']); + return Redirect::route('budgets.create')->withInput(); + break; } - } /** * @param Budget $budget * - * @return $this|\Illuminate\Http\RedirectResponse + * @throws FireflyException */ public function update(Budget $budget) { - $budget = $this->_repository->update($budget, Input::all()); - if ($budget->validate()) { - Event::fire('budgets.update', [$budget]); - Session::flash('success', 'Budget "' . $budget->name . '" updated.'); - if (Input::get('from') == 'date') { - return Redirect::route('budgets.index'); - } else { - return Redirect::route('budgets.index.budget'); - } - } else { - Session::flash('error', 'Could not update budget: ' . $budget->errors()->first()); + /** @var \FireflyIII\Database\Budget $repos */ + $repos = App::make('FireflyIII\Database\Budget'); + $data = Input::except('_token'); - return Redirect::route('budgets.edit', $budget->id)->withInput()->withErrors($budget->errors()); + switch (Input::get('post_submit_action')) { + default: + throw new FireflyException('Cannot handle post_submit_action "' . e(Input::get('post_submit_action')) . '"'); + break; + case 'create_another': + case 'update': + $messages = $repos->validate($data); + /** @var MessageBag $messages ['errors'] */ + if ($messages['errors']->count() > 0) { + Session::flash('warnings', $messages['warnings']); + Session::flash('successes', $messages['successes']); + Session::flash('error', 'Could not save budget: ' . $messages['errors']->first()); + return Redirect::route('budgets.edit', $budget->id)->withInput()->withErrors($messages['errors']); + } + // store! + $repos->update($budget, $data); + Session::flash('success', 'Budget updated!'); + + if ($data['post_submit_action'] == 'create_another') { + return Redirect::route('budgets.edit', $budget->id); + } else { + return Redirect::route('budgets.index'); + } + case 'validate_only': + $messageBags = $repos->validate($data); + Session::flash('warnings', $messageBags['warnings']); + Session::flash('successes', $messageBags['successes']); + Session::flash('errors', $messageBags['errors']); + return Redirect::route('budgets.edit', $budget->id)->withInput(); + break; } - } - - -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/controllers/GoogleChartController.php b/app/controllers/GoogleChartController.php new file mode 100644 index 0000000000..daeedcfddd --- /dev/null +++ b/app/controllers/GoogleChartController.php @@ -0,0 +1,600 @@ +addColumn('Day of the month', 'date'); + + /** @var \FireflyIII\Shared\Preferences\Preferences $preferences */ + $preferences = App::make('FireflyIII\Shared\Preferences\Preferences'); + $pref = $preferences->get('frontpageAccounts'); + + /** @var \FireflyIII\Database\Account $acct */ + $acct = App::make('FireflyIII\Database\Account'); + $accounts = $acct->getByIds($pref->data); + + + /* + * Add a column for each account. + */ + /** @var Account $account */ + foreach ($accounts as $account) { + $chart->addColumn('Balance for ' . $account->name, 'number'); + } + /* + * Loop the date, then loop the accounts, then add balance. + */ + $start = Session::get('start'); + $end = Session::get('end'); + $current = clone $start; + + while ($end >= $current) { + $row = [clone $current]; + + foreach ($accounts as $account) { + //if ($current > Carbon::now()) { + // $row[] = 0; + //} else { + $row[] = $account->balance($current); + //} + + } + + $chart->addRowArray($row); + $current->addDay(); + } + + $chart->generate(); + return Response::json($chart->getData()); + + } + + /** + * @param $year + * + * @return \Illuminate\Http\JsonResponse + */ + public function yearInExp($year) + { + try { + $start = new Carbon('01-01-' . $year); + } catch (Exception $e) { + App::abort(500); + } + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + $chart->addColumn('Month', 'date'); + $chart->addColumn('Income', 'number'); + $chart->addColumn('Expenses', 'number'); + + /** @var \FireflyIII\Database\TransactionJournal $tj */ + $tj = App::make('FireflyIII\Database\TransactionJournal'); + + $end = clone $start; + $end->endOfYear(); + while ($start < $end) { + + // total income: + $income = $tj->getSumOfIncomesByMonth($start); + $expense = $tj->getSumOfExpensesByMonth($start); + + $chart->addRow(clone $start, $income, $expense); + $start->addMonth(); + } + + + $chart->generate(); + return Response::json($chart->getData()); + + } + + /** + * @param $year + * + * @return \Illuminate\Http\JsonResponse + */ + public function yearInExpSum($year) + { + try { + $start = new Carbon('01-01-' . $year); + } catch (Exception $e) { + App::abort(500); + } + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + $chart->addColumn('Summary', 'string'); + $chart->addColumn('Income', 'number'); + $chart->addColumn('Expenses', 'number'); + + /** @var \FireflyIII\Database\TransactionJournal $tj */ + $tj = App::make('FireflyIII\Database\TransactionJournal'); + + $end = clone $start; + $end->endOfYear(); + $income = 0; + $expense = 0; + $count = 0; + while ($start < $end) { + + // total income: + $income += $tj->getSumOfIncomesByMonth($start); + $expense += $tj->getSumOfExpensesByMonth($start); + $count++; + + $start->addMonth(); + } + $chart->addRow('Sum', $income, $expense); + $count = $count > 0 ? $count : 1; + $chart->addRow('Average', ($income / $count), ($expense / $count)); + + + $chart->generate(); + return Response::json($chart->getData()); + + } + + + /** + * @return \Illuminate\Http\JsonResponse + */ + public function budgetsReportChart($year) + { + + try { + $start = new Carbon('01-01-' . $year); + } catch (Exception $e) { + App::abort(500); + } + + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + + /** @var \FireflyIII\Database\Budget $bdt */ + $bdt = App::make('FireflyIII\Database\Budget'); + $budgets = $bdt->get(); + + $chart->addColumn('Month', 'date'); + /** @var \Budget $budget */ + foreach ($budgets as $budget) { + $chart->addColumn($budget->name, 'number'); + } + $chart->addColumn('No budget','number'); + + /* + * Loop budgets this year. + */ + $end = clone $start; + $end->endOfYear(); + while ($start <= $end) { + $row = [clone $start]; + + foreach($budgets as $budget) { + $row[] = $bdt->spentInMonth($budget, $start); + } + + /* + * Without a budget: + */ + $endOfMonth = clone $start; + $endOfMonth->endOfMonth(); + $set = $bdt->transactionsWithoutBudgetInDateRange($start, $endOfMonth); + $row[] = floatval($set->sum('amount')) * -1; + + $chart->addRowArray($row); + $start->addMonth(); + } + + + $chart->generate(); + return Response::json($chart->getData()); + } + + public function budgetsAndSpending(Budget $budget, $year) { + try { + $start = new Carbon('01-01-' . $year); + } catch (Exception $e) { + App::abort(500); + } + + /** @var \FireflyIII\Database\Budget $bdt */ + $bdt = App::make('FireflyIII\Database\Budget'); + + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + $chart->addColumn('Month', 'date'); + $chart->addColumn('Budgeted', 'number'); + $chart->addColumn('Spent', 'number'); + + $end = clone $start; + $end->endOfYear(); + while($start <= $end) { + + $spent = $bdt->spentInMonth($budget, $start); + $repetition = $bdt->repetitionOnStartingOnDate($budget, $start); + if($repetition) { + $budgeted = floatval($repetition->amount); + } else { + $budgeted = 0; + } + + $chart->addRow(clone $start, $budgeted, $spent); + + $start->addMonth(); + } + + + + $chart->generate(); + return Response::json($chart->getData()); + + + } + + /** + * @return \Illuminate\Http\JsonResponse + */ + public function allBudgetsHomeChart() + { + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + $chart->addColumn('Budget', 'string'); + $chart->addColumn('Budgeted', 'number'); + $chart->addColumn('Spent', 'number'); + + /** @var \FireflyIII\Database\Budget $bdt */ + $bdt = App::make('FireflyIII\Database\Budget'); + $budgets = $bdt->get(); + + /* + * Loop budgets: + */ + /** @var Budget $budget */ + foreach ($budgets as $budget) { + + /* + * Is there a repetition starting on this particular date? We can use that. + */ + /** @var \LimitRepetition $repetition */ + $repetition = $bdt->repetitionOnStartingOnDate($budget, Session::get('start')); + + /* + * If there is, use it. Otherwise, forget it. + */ + if (is_null($repetition)) { + // use the session start and end for our search query + $searchStart = Session::get('start'); + $searchEnd = Session::get('end'); + // the limit is zero: + $limit = 0; + + } else { + // use the limit's start and end for our search query + $searchStart = $repetition->startdate; + $searchEnd = $repetition->enddate; + // the limit is the repetitions limit: + $limit = floatval($repetition->amount); + } + + /* + * No matter the result of the search for the repetition, get all the transactions associated + * with the budget, and sum up the expenses made. + */ + $expenses = floatval($budget->transactionjournals()->before($searchEnd)->after($searchStart)->lessThan(0)->sum('amount')) * -1; + if ($expenses > 0) { + $chart->addRow($budget->name, $limit, $expenses); + } + } + + /* + * Finally, get all transactions WITHOUT a budget and add those as well. + * (yes this method is oddly specific). + */ + $noBudgetSet = $bdt->transactionsWithoutBudgetInDateRange(Session::get('start'), Session::get('end')); + $sum = $noBudgetSet->sum('amount') * -1; + $chart->addRow('No budget', 0, $sum); + + + $chart->generate(); + return Response::json($chart->getData()); + } + + /** + * @return \Illuminate\Http\JsonResponse + */ + public function allCategoriesHomeChart() + { + $data = []; + + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + $chart->addColumn('Category', 'string'); + $chart->addColumn('Spent', 'number'); + + /** @var \FireflyIII\Database\TransactionJournal $tj */ + $tj = App::make('FireflyIII\Database\TransactionJournal'); + + /* + * Get the journals: + */ + $journals = $tj->getInDateRange(Session::get('start'), Session::get('end')); + + /** @var \TransactionJournal $journal */ + foreach ($journals as $journal) { + if ($journal->transactionType->type == 'Withdrawal') { + $amount = $journal->getAmount(); + $category = $journal->categories()->first(); + if (!is_null($category)) { + if (isset($data[$category->name])) { + $data[$category->name] += $amount; + } else { + $data[$category->name] = $amount; + } + } + } + } + arsort($data); + foreach ($data as $key => $entry) { + $chart->addRow($key, $entry); + } + + + $chart->generate(); + return Response::json($chart->getData()); + + } + + /** + * @return \Illuminate\Http\JsonResponse + * @throws \Firefly\Exception\FireflyException + */ + public function recurringTransactionsOverview() + { + + /* + * Set of paid transaction journals. + * Set of unpaid recurring transactions. + */ + $paid = [ + 'items' => [], + 'amount' => 0 + ]; + $unpaid = [ + 'items' => [], + 'amount' => 0 + ]; + + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + $chart->addColumn('Name', 'string'); + $chart->addColumn('Amount', 'number'); + + /** @var \FireflyIII\Database\Recurring $rcr */ + $rcr = App::make('FireflyIII\Database\Recurring'); + + /** @var \FireflyIII\Shared\Toolkit\Date $dateKit */ + $dateKit = App::make('FireflyIII\Shared\Toolkit\Date'); + + $recurring = $rcr->get(); + + /** @var \RecurringTransaction $entry */ + foreach ($recurring as $entry) { + /* + * Start another loop starting at the $date. + */ + $start = clone $entry->date; + $end = Carbon::now(); + + /* + * The jump we make depends on the $repeat_freq + */ + $current = clone $start; + + while ($current <= $end) { + /* + * Get end of period for $current: + */ + $currentEnd = clone $current; + $dateKit->endOfPeriod($currentEnd, $entry->repeat_freq); + + /* + * In the current session range? + */ + if (\Session::get('end') >= $current and $currentEnd >= \Session::get('start')) { + /* + * Lets see if we've already spent money on this recurring transaction (it hath recurred). + */ + /** @var TransactionJournal $set */ + $journal = $rcr->getJournalForRecurringInRange($entry, $current, $currentEnd); + + if (is_null($journal)) { + $unpaid['items'][] = $entry->name; + $unpaid['amount'] += (($entry->amount_max + $entry->amount_min) / 2); + } else { + $amount = $journal->getAmount(); + $paid['items'][] = $journal->description; + $paid['amount'] += $amount; + } + } + + /* + * Add some time for the next loop! + */ + $dateKit->addPeriod($current, $entry->repeat_freq, intval($entry->skip)); + + } + + } + /** @var \RecurringTransaction $entry */ + $chart->addRow('Unpaid: ' . join(', ', $unpaid['items']), $unpaid['amount']); + $chart->addRow('Paid: ' . join(', ', $paid['items']), $paid['amount']); + + $chart->generate(); + return Response::json($chart->getData()); + + } + + /** + * @param Account $account + */ + public function accountBalanceChart(Account $account) + { + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + $chart->addColumn('Day of month', 'date'); + $chart->addColumn('Balance for ' . $account->name, 'number'); + + /* + * Loop the date, then loop the accounts, then add balance. + */ + $start = Session::get('start'); + $end = Session::get('end'); + $current = clone $start; + + while ($end >= $current) { + $row = [clone $current]; + if ($current > Carbon::now()) { + $row[] = null; + } else { + $row[] = $account->balance($current); + } + + $chart->addRowArray($row); + $current->addDay(); + } + + + $chart->generate(); + return Response::json($chart->getData()); + } + + /** + * @param Account $account + * + * @return \Illuminate\Http\JsonResponse + */ + public function accountSankeyOutChart(Account $account) + { + // collect all relevant entries. + $set = []; + + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + $chart->addColumn('From', 'string'); + $chart->addColumn('To', 'string', 'domain'); + $chart->addColumn('Weight', 'number'); + + $transactions = $account->transactions()->with( + ['transactionjournal', 'transactionjournal.transactions', 'transactionjournal.budgets', 'transactionjournal.transactiontype', + 'transactionjournal.categories'] + )->before(Session::get('end'))->after( + Session::get('start') + )->get(); + + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $amount = floatval($transaction->amount); + $type = $transaction->transactionJournal->transactionType->type; + + if ($amount < 0 && $type != 'Transfer') { + + // from account to a budget (if present). + $budgetName = isset($transaction->transactionJournal->budgets[0]) ? $transaction->transactionJournal->budgets[0]->name : '(no budget)'; + $set[] = [$account->name, $budgetName, $amount * -1]; + + // from budget to category. + $categoryName = isset($transaction->transactionJournal->categories[0]) ? ' ' . $transaction->transactionJournal->categories[0]->name + : '(no cat)'; + $set[] = [$budgetName, $categoryName, $amount * -1]; + } + } + // loop the set, group everything together: + $grouped = []; + foreach ($set as $entry) { + $key = $entry[0] . $entry[1]; + if (isset($grouped[$key])) { + $grouped[$key][2] += $entry[2]; + } else { + $grouped[$key] = $entry; + } + } + + // add rows to the chart: + foreach ($grouped as $entry) { + $chart->addRow($entry[0], $entry[1], $entry[2]); + } + + $chart->generate(); + return Response::json($chart->getData()); + + } + + /** + * @param Account $account + * + * @return \Illuminate\Http\JsonResponse + */ + public function accountSankeyInChart(Account $account) + { + // collect all relevant entries. + $set = []; + + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + $chart->addColumn('From', 'string'); + $chart->addColumn('To', 'string', 'domain'); + $chart->addColumn('Weight', 'number'); + + $transactions = $account->transactions()->with( + ['transactionjournal', 'transactionjournal.transactions' => function ($q) { + $q->where('amount', '<', 0); + }, 'transactionjournal.budgets', 'transactionjournal.transactiontype', 'transactionjournal.categories'] + )->before(Session::get('end'))->after( + Session::get('start') + )->get(); + + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $amount = floatval($transaction->amount); + $type = $transaction->transactionJournal->transactionType->type; + + if ($amount > 0 && $type != 'Transfer') { + + $otherAccount = $transaction->transactionJournal->transactions[0]->account->name; + $categoryName = isset($transaction->transactionJournal->categories[0]) ? $transaction->transactionJournal->categories[0]->name + : '(no cat)'; + $set[] = [$otherAccount, $categoryName, $amount]; + $set[] = [$categoryName, $account->name, $amount]; + } + } + // loop the set, group everything together: + $grouped = []; + foreach ($set as $entry) { + $key = $entry[0] . $entry[1]; + if (isset($grouped[$key])) { + $grouped[$key][2] += $entry[2]; + } else { + $grouped[$key] = $entry; + } + } + + // add rows to the chart: + foreach ($grouped as $entry) { + $chart->addRow($entry[0], $entry[1], $entry[2]); + } + + $chart->generate(); + return Response::json($chart->getData()); + + } +} \ No newline at end of file diff --git a/app/controllers/GoogleTableController.php b/app/controllers/GoogleTableController.php new file mode 100644 index 0000000000..8f51270363 --- /dev/null +++ b/app/controllers/GoogleTableController.php @@ -0,0 +1,240 @@ +getAssetAccounts(); + break; + case 'expense': + $list = $acct->getExpenseAccounts(); + break; + case 'revenue': + $list = $acct->getRevenueAccounts(); + break; + } + + + $chart = App::make('gchart'); + $chart->addColumn('ID', 'number'); + $chart->addColumn('ID_Edit', 'string'); + $chart->addColumn('ID_Delete', 'string'); + $chart->addColumn('Name_URL', 'string'); + $chart->addColumn('Name', 'string'); + $chart->addColumn('Balance', 'number'); + + /** @var \Account $entry */ + foreach ($list as $entry) { + $edit = route('accounts.edit', $entry->id); + $delete = route('accounts.delete', $entry->id); + $show = route('accounts.show', $entry->id); + $chart->addRow($entry->id, $edit, $delete, $show, $entry->name, $entry->balance()); + } + + $chart->generate(); + return Response::json($chart->getData()); + + + } + + /** + * @param Budget $budget + * @param LimitRepetition $repetition + */ + public function transactionsByBudget(Budget $budget, LimitRepetition $repetition = null) + { + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + $chart->addColumn('ID', 'number'); + $chart->addColumn('ID_Edit', 'string'); + $chart->addColumn('ID_Delete', 'string'); + $chart->addColumn('Date', 'date'); + $chart->addColumn('Description_URL', 'string'); + $chart->addColumn('Description', 'string'); + $chart->addColumn('Amount', 'number'); + $chart->addColumn('From_URL', 'string'); + $chart->addColumn('From', 'string'); + $chart->addColumn('To_URL', 'string'); + $chart->addColumn('To', 'string'); + $chart->addColumn('Budget_URL', 'string'); + $chart->addColumn('Budget', 'string'); + $chart->addColumn('Category_URL', 'string'); + $chart->addColumn('Category', 'string'); + + if (is_null($repetition)) { + $journals = $budget->transactionjournals()->with(['budgets', 'categories', 'transactions', 'transactions.account'])->orderBy('date', 'DESC')->get(); + } else { + $journals = $budget->transactionjournals()->with(['budgets', 'categories', 'transactions', 'transactions.account'])-> + after($repetition->startdate)->before($repetition->enddate)->orderBy('date', 'DESC')->get(); + } + /** @var TransactionJournal $transaction */ + foreach ($journals as $journal) { + $date = $journal->date; + $descriptionURL = route('transactions.show', $journal->id); + $description = $journal->description; + /** @var Transaction $transaction */ + foreach ($journal->transactions as $transaction) { + if (floatval($transaction->amount) > 0) { + $amount = floatval($transaction->amount); + $to = $transaction->account->name; + $toURL = route('accounts.show', $transaction->account->id); + } else { + $from = $transaction->account->name; + $fromURL = route('accounts.show', $transaction->account->id); + } + + } + if (isset($journal->budgets[0])) { + $budgetURL = route('budgets.show', $journal->budgets[0]->id); + $budget = $journal->budgets[0]->name; + } else { + $budgetURL = ''; + $budget = ''; + } + + if (isset($journal->categories[0])) { + $categoryURL = route('categories.show', $journal->categories[0]->id); + $category = $journal->categories[0]->name; + } else { + $categoryURL = ''; + $category = ''; + } + + + $id = $journal->id; + $edit = route('transactions.edit', $journal->id); + $delete = route('transactions.delete', $journal->id); + $chart->addRow( + $id, $edit, $delete, $date, $descriptionURL, $description, $amount, $fromURL, $from, $toURL, $to, $budgetURL, $budget, $categoryURL, + $category + ); + } + + + $chart->generate(); + return Response::json($chart->getData()); + + } + + /** + * @param Account $account + */ + public function transactionsByAccount(Account $account) + { + /** @var \Grumpydictator\Gchart\GChart $chart */ + $chart = App::make('gchart'); + $chart->addColumn('ID', 'number'); + $chart->addColumn('ID_Edit', 'string'); + $chart->addColumn('ID_Delete', 'string'); + $chart->addColumn('Date', 'date'); + $chart->addColumn('Description_URL', 'string'); + $chart->addColumn('Description', 'string'); + $chart->addColumn('Amount', 'number'); + $chart->addColumn('From_URL', 'string'); + $chart->addColumn('From', 'string'); + $chart->addColumn('To_URL', 'string'); + $chart->addColumn('To', 'string'); + $chart->addColumn('Budget_URL', 'string'); + $chart->addColumn('Budget', 'string'); + $chart->addColumn('Category_URL', 'string'); + $chart->addColumn('Category', 'string'); + + /* + * Find transactions: + */ + $accountID = $account->id; + $transactions = $account->transactions()->with( + ['transactionjournal', 'transactionjournal.transactions' => function ($q) use ($accountID) { + $q->where('account_id', '!=', $accountID); + }, 'transactionjournal.budgets', 'transactionjournal.transactiontype', + 'transactionjournal.categories'] + )->before(Session::get('end'))->after( + Session::get('start') + )->orderBy('date', 'DESC')->get(); + + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $date = $transaction->transactionJournal->date; + $descriptionURL = route('transactions.show', $transaction->transaction_journal_id); + $description = $transaction->transactionJournal->description; + $amount = floatval($transaction->amount); + + if ($transaction->transactionJournal->transactions[0]->account->id == $account->id) { + $opposingAccountURI = route('accounts.show', $transaction->transactionJournal->transactions[1]->account->id); + $opposingAccountName = $transaction->transactionJournal->transactions[1]->account->name; + } else { + $opposingAccountURI = route('accounts.show', $transaction->transactionJournal->transactions[0]->account->id); + $opposingAccountName = $transaction->transactionJournal->transactions[0]->account->name; + } + if (isset($transaction->transactionJournal->budgets[0])) { + $budgetURL = route('budgets.show', $transaction->transactionJournal->budgets[0]->id); + $budget = $transaction->transactionJournal->budgets[0]->name; + } else { + $budgetURL = ''; + $budget = ''; + } + + if (isset($transaction->transactionJournal->categories[0])) { + $categoryURL = route('categories.show', $transaction->transactionJournal->categories[0]->id); + $category = $transaction->transactionJournal->categories[0]->name; + } else { + $categoryURL = ''; + $category = ''; + } + + + if ($amount < 0) { + $from = $account->name; + $fromURL = route('accounts.show', $account->id); + + $to = $opposingAccountName; + $toURL = $opposingAccountURI; + } else { + $to = $account->name; + $toURL = route('accounts.show', $account->id); + + $from = $opposingAccountName; + $fromURL = $opposingAccountURI; + } + + $id = $transaction->transactionJournal->id; + $edit = route('transactions.edit', $transaction->transactionJournal->id); + $delete = route('transactions.delete', $transaction->transactionJournal->id); + $chart->addRow( + $id, $edit, $delete, $date, $descriptionURL, $description, $amount, $fromURL, $from, $toURL, $to, $budgetURL, $budget, $categoryURL, $category + ); + } + +// Date +// Description +// Amount (€) +// From +// To +// Budget / category +// ID + + + $chart->generate(); + return Response::json($chart->getData()); + } +} \ No newline at end of file diff --git a/app/controllers/HomeController.php b/app/controllers/HomeController.php index 0f2a049c9d..ffd33b86cd 100644 --- a/app/controllers/HomeController.php +++ b/app/controllers/HomeController.php @@ -15,8 +15,8 @@ class HomeController extends BaseController protected $_journal; /** - * @param ARI $accounts - * @param PHI $preferences + * @param ARI $accounts + * @param PHI $preferences * @param TJRI $journal */ public function __construct(ARI $accounts, PHI $preferences, TJRI $journal) @@ -115,4 +115,59 @@ class HomeController extends BaseController return View::make('index')->with('count', $count)->with('transactions', $transactions)->with('title', 'Firefly') ->with('subTitle', 'What\'s playing?')->with('mainTitleIcon', 'fa-fire'); } + + public function cleanup() + { + /** @var \FireflyIII\Database\TransactionJournal $jrnls */ + $jrnls = App::make('FireflyIII\Database\TransactionJournal'); + + /** @var \FireflyIII\Database\Account $acct */ + $acct = \App::make('FireflyIII\Database\Account'); + + /** @var \FireflyIII\Database\AccountType $acctType */ + $acctType = \App::make('FireflyIII\Database\AccountType'); + $rightAcctType = $acctType->findByWhat('revenue'); + + $all = $jrnls->get(); + + /** @var \TransactionJournal $entry */ + foreach ($all as $entry) { + $wrongFromType = false; + $wrongToType = false; + $transactions = $entry->transactions; + if (count($transactions) == 2) { + switch ($entry->transactionType->type) { + case 'Deposit': + /** @var \Transaction $transaction */ + foreach ($transactions as $transaction) { + if (floatval($transaction->amount) < 0) { + $accountType = $transaction->account->accountType; + if ($accountType->type == 'Beneficiary account') { + // should be a Revenue account! + $name = $transaction->account->name; + /** @var \Account $account */ + $account = \Auth::user()->accounts()->where('name', $name)->where('account_type_id', $rightAcctType->id)->first(); + if (!$account) { + $new = [ + 'name' => $name, + 'what' => 'revenue' + ]; + $account = $acct->store($new); + } + $transaction->account()->associate($account); + $transaction->save(); + } + + echo 'Paid by: ' . $transaction->account->name . ' (' . $transaction->account->accountType->type . ')
'; + } + } + break; + } + + + } + } + + + } } \ No newline at end of file diff --git a/app/controllers/PiggybankController.php b/app/controllers/PiggybankController.php index 6f08e87c1f..78b6c49ec0 100644 --- a/app/controllers/PiggybankController.php +++ b/app/controllers/PiggybankController.php @@ -1,31 +1,42 @@ _repository = $repository; - $this->_accounts = $accounts; + + /** @var \FireflyIII\Database\Account $acct */ + $acct = App::make('FireflyIII\Database\Account'); + + /** @var \FireflyIII\Shared\Toolkit\Form $toolkit */ + $toolkit = App::make('FireflyIII\Shared\Toolkit\Form'); + + $periods = Config::get('firefly.piggybank_periods'); + + + $accounts = $toolkit->makeSelectList($acct->getAssetAccounts()); + return View::make('piggybanks.create', compact('accounts', 'periods'))->with('title', 'Piggy banks')->with('mainTitleIcon', 'fa-sort-amount-asc') + ->with('subTitle', 'Create new piggy bank')->with('subTitleIcon', 'fa-plus'); } /** @@ -33,74 +44,13 @@ class PiggybankController extends BaseController * * @return $this */ - public function addMoney(Piggybank $piggyBank) + public function delete(Piggybank $piggybank) { - $what = 'add'; - $maxAdd = $this->_repository->leftOnAccount($piggyBank->account); - $maxRemove = null; - - return View::make('piggybanks.modifyAmount')->with('what', $what)->with('maxAdd', $maxAdd)->with( - 'maxRemove', $maxRemove - )->with('piggybank', $piggyBank); - } - - /** - * @return $this - */ - public function createPiggybank() - { - /** @var \Firefly\Helper\Toolkit\Toolkit $toolkit */ - $toolkit = App::make('Firefly\Helper\Toolkit\Toolkit'); - - - $periods = Config::get('firefly.piggybank_periods'); - $list = $this->_accounts->getActiveDefault(); - $accounts = $toolkit->makeSelectList($list); - + View::share('subTitle', 'Delete "' . $piggybank->name . '"'); View::share('title', 'Piggy banks'); - View::share('subTitle', 'Create new'); View::share('mainTitleIcon', 'fa-sort-amount-asc'); - return View::make('piggybanks.create-piggybank')->with('accounts', $accounts) - ->with('periods', $periods); - } - - /** - * @return $this - */ - public function createRepeated() - { - /** @var \Firefly\Helper\Toolkit\Toolkit $toolkit */ - $toolkit = App::make('Firefly\Helper\Toolkit\Toolkit'); - - $periods = Config::get('firefly.piggybank_periods'); - $list = $this->_accounts->getActiveDefault(); - $accounts = $toolkit->makeSelectList($list); - - View::share('title', 'Repeated expenses'); - View::share('subTitle', 'Create new'); - View::share('mainTitleIcon', 'fa-rotate-right'); - - return View::make('piggybanks.create-repeated')->with('accounts', $accounts)->with('periods', $periods); - } - - /** - * @param Piggybank $piggyBank - * - * @return $this - */ - public function delete(Piggybank $piggyBank) - { - View::share('subTitle', 'Delete "' . $piggyBank->name . '"'); - if ($piggyBank->repeats == 1) { - View::share('title', 'Repeated expenses'); - View::share('mainTitleIcon', 'fa-rotate-right'); - } else { - View::share('title', 'Piggy banks'); - View::share('mainTitleIcon', 'fa-sort-amount-asc'); - } - - return View::make('piggybanks.delete')->with('piggybank', $piggyBank); + return View::make('piggybanks.delete')->with('piggybank', $piggybank); } /** @@ -111,18 +61,13 @@ class PiggybankController extends BaseController public function destroy(Piggybank $piggyBank) { Event::fire('piggybanks.destroy', [$piggyBank]); - if ($piggyBank->repeats == 1) { - $route = 'piggybanks.index.repeated'; - $message = 'Repeated expense'; - } else { - $route = 'piggybanks.index.piggybanks'; - $message = 'Piggybank'; - } - $this->_repository->destroy($piggyBank); - Session::flash('success', $message . ' deleted.'); + /** @var \FireflyIII\Database\Piggybank $acct */ + $repos = App::make('FireflyIII\Database\Piggybank'); + $repos->destroy($piggyBank); + Session::flash('success', 'Piggy bank deleted.'); - return Redirect::route($route); + return Redirect::route('piggybanks.index'); } /** @@ -130,245 +75,203 @@ class PiggybankController extends BaseController * * @return $this */ - public function edit(Piggybank $piggyBank) + public function edit(Piggybank $piggybank) { - /** @var \Firefly\Helper\Toolkit\Toolkit $toolkit */ - $toolkit = App::make('Firefly\Helper\Toolkit\Toolkit'); - $list = $this->_accounts->getActiveDefault(); - $accounts = $toolkit->makeSelectList($list); - $periods = Config::get('firefly.piggybank_periods'); + /** @var \FireflyIII\Database\Account $acct */ + $acct = App::make('FireflyIII\Database\Account'); + /** @var \FireflyIII\Shared\Toolkit\Form $toolkit */ + $toolkit = App::make('FireflyIII\Shared\Toolkit\Form'); - View::share('subTitle', 'Edit "' . $piggyBank->name . '"'); + $periods = Config::get('firefly.piggybank_periods'); + $accounts = $toolkit->makeSelectList($acct->getAssetAccounts()); - if ($piggyBank->repeats == 1) { - View::share('title', 'Repeated expenses'); - View::share('mainTitleIcon', 'fa-rotate-left'); - - return View::make('piggybanks.edit-repeated')->with('piggybank', $piggyBank)->with('accounts', $accounts) - ->with('periods', $periods); - } else { - // piggy bank. - View::share('title', 'Piggy banks'); - View::share('mainTitleIcon', 'fa-sort-amount-asc'); - - return View::make('piggybanks.edit-piggybank')->with('piggybank', $piggyBank)->with('accounts', $accounts) - ->with('periods', $periods); - } - + /* + * Flash some data to fill the form. + */ + $prefilled = [ + 'name' => $piggybank->name, + 'account_id' => $piggybank->account_id, + 'targetamount' => $piggybank->targetamount, + 'targetdate' => $piggybank->targetdate, + 'remind_me' => intval($piggybank->remind_me) == 1 ? true : false + ]; + Session::flash('prefilled', $prefilled); + return View::make('piggybanks.edit', compact('piggybank', 'accounts', 'periods', 'prefilled'))->with('title', 'Piggybanks')->with( + 'mainTitleIcon', 'fa-sort-amount-asc' + ) + ->with('subTitle', 'Edit piggy bank "' . e($piggybank->name) . '"')->with('subTitleIcon', 'fa-pencil'); } /** - * @param Piggybank $piggyBank + * @param Piggybank $piggybank + * + * @return $this + */ + public function add(Piggybank $piggybank) + { + /** @var \FireflyIII\Database\Piggybank $acct */ + $repos = App::make('FireflyIII\Database\Piggybank'); + + $leftOnAccount = $repos->leftOnAccount($piggybank->account); + $savedSoFar = $piggybank->currentRelevantRep()->currentamount; + $leftToSave = $piggybank->targetamount - $savedSoFar; + $amount = min($leftOnAccount, $leftToSave); + + + return View::make('piggybanks.add', compact('piggybank'))->with('maxAmount', $amount); + } + + /** + * @param Piggybank $piggybank * * @return \Illuminate\Http\RedirectResponse - * @throws Firefly\Exception\FireflyException */ - public function modMoney(Piggybank $piggyBank) + public function postAdd(Piggybank $piggybank) + { + $amount = round(floatval(Input::get('amount')), 2); + + /** @var \FireflyIII\Database\Piggybank $acct */ + $repos = App::make('FireflyIII\Database\Piggybank'); + + $leftOnAccount = $repos->leftOnAccount($piggybank->account); + $savedSoFar = $piggybank->currentRelevantRep()->currentamount; + $leftToSave = $piggybank->targetamount - $savedSoFar; + $maxAmount = round(min($leftOnAccount, $leftToSave), 2); + + if ($amount <= $maxAmount) { + $repetition = $piggybank->currentRelevantRep(); + $repetition->currentamount += $amount; + $repetition->save(); + Session::flash('success', 'Added ' . mf($amount, false) . ' to "' . e($piggybank->name) . '".'); + } else { + Session::flash('error', 'Could not add ' . mf($amount, false) . ' to "' . e($piggybank->name) . '".'); + } + return Redirect::route('piggybanks.index'); + } + + /** + * @param Piggybank $piggybank + * + * @return \Illuminate\View\View + */ + public function remove(Piggybank $piggybank) + { + return View::make('piggybanks.remove', compact('piggybank')); + } + + /** + * @param Piggybank $piggybank + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postRemove(Piggybank $piggybank) { $amount = floatval(Input::get('amount')); - switch (Input::get('what')) { - default: - throw new FireflyException('No such action'); - break; - case 'add': - $maxAdd = $this->_repository->leftOnAccount($piggyBank->account); - if (round($amount, 2) <= round(min($maxAdd, $piggyBank->targetamount), 2)) { - Session::flash('success', 'Amount updated!'); - $this->_repository->modifyAmount($piggyBank, $amount); - Event::fire('piggybanks.modifyAmountAdd', [$piggyBank, $amount]); - } else { - Session::flash('warning', 'Could not!'); - } - break; - case 'remove': - $rep = $piggyBank->currentRelevantRep(); - $maxRemove = $rep->currentamount; - if (round($amount, 2) <= round($maxRemove, 2)) { - Session::flash('success', 'Amount updated!'); - $this->_repository->modifyAmount($piggyBank, ($amount * -1)); - Event::fire('piggybanks.modifyAmountRemove', [$piggyBank, ($amount * -1)]); - } else { - Session::flash('warning', 'Could not!'); - } - break; - } - if($piggyBank->repeats == 1) { - $route = 'piggybanks.index.repeated'; + $savedSoFar = $piggybank->currentRelevantRep()->currentamount; + + if ($amount <= $savedSoFar) { + $repetition = $piggybank->currentRelevantRep(); + $repetition->currentamount -= $amount; + $repetition->save(); + Session::flash('success', 'Removed ' . mf($amount, false) . ' from "' . e($piggybank->name) . '".'); } else { - $route = 'piggybanks.index.piggybanks'; + Session::flash('error', 'Could not remove ' . mf($amount, false) . ' from "' . e($piggybank->name) . '".'); } - return Redirect::route($route); + return Redirect::route('piggybanks.index'); } - /** - * @return $this - */ - public function piggybanks() + public function index() { - $countRepeating = $this->_repository->countRepeating(); - $countNonRepeating = $this->_repository->countNonrepeating(); + /** @var \FireflyIII\Database\Piggybank $repos */ + $repos = App::make('FireflyIII\Database\Piggybank'); - $piggybanks = $this->_repository->get(); - - // get the accounts with each piggy bank and check their balance; Fireflyy might needs to - // show the user a correction. + /** @var Collection $piggybanks */ + $piggybanks = $repos->get(); $accounts = []; - /** @var \Piggybank $piggybank */ + /** @var Piggybank $piggybank */ foreach ($piggybanks as $piggybank) { + $piggybank->savedSoFar = floatval($piggybank->currentRelevantRep()->currentamount); + $piggybank->percentage = intval($piggybank->savedSoFar / $piggybank->targetamount * 100); + $piggybank->leftToSave = $piggybank->targetamount - $piggybank->savedSoFar; + + /* + * Fill account information: + */ $account = $piggybank->account; - $id = $account->id; - if (!isset($accounts[$id])) { - $account->leftOnAccount = $this->_repository->leftOnAccount($account); - $accounts[$id] = ['account' => $account, 'left' => $this->_repository->leftOnAccount($account)]; + if (!isset($accounts[$account->id])) { + $accounts[$account->id] = [ + 'name' => $account->name, + 'balance' => $account->balance(), + 'leftForPiggybanks' => $repos->leftOnAccount($account), + 'sumOfSaved' => $piggybank->savedSoFar, + 'sumOfTargets' => floatval($piggybank->targetamount), + 'leftToSave' => $piggybank->leftToSave + ]; + } else { + $accounts[$account->id]['sumOfSaved'] += $piggybank->savedSoFar; + $accounts[$account->id]['sumOfTargets'] += floatval($piggybank->targetamount); + $accounts[$account->id]['leftToSave'] += $piggybank->leftToSave; } } - - View::share('title', 'Piggy banks'); - View::share('subTitle', 'Save for big expenses'); - View::share('mainTitleIcon', 'fa-sort-amount-asc'); - - return View::make('piggybanks.index')->with('piggybanks', $piggybanks) - ->with('countRepeating', $countRepeating) - ->with('countNonRepeating', $countNonRepeating) - ->with('accounts', $accounts); + return View::make('piggybanks.index', compact('piggybanks', 'accounts'))->with('title', 'Piggy banks')->with('mainTitleIcon', 'fa-sort-amount-asc'); } - /** - * @param Piggybank $piggyBank - * - * @return $this - */ - public function removeMoney(Piggybank $piggyBank) - { - $what = 'remove'; - $maxAdd = $this->_repository->leftOnAccount($piggyBank->account); - $maxRemove = $piggyBank->currentRelevantRep()->currentamount; - return View::make('piggybanks.modifyAmount')->with('what', $what)->with('maxAdd', $maxAdd)->with( - 'maxRemove', $maxRemove - )->with('piggybank', $piggyBank); - } - - /** - * @return $this - */ - public function repeated() - { - $countRepeating = $this->_repository->countRepeating(); - $countNonRepeating = $this->_repository->countNonrepeating(); - - $piggybanks = $this->_repository->get(); - - // get the accounts with each piggy bank and check their balance; Fireflyy might needs to - // show the user a correction. - - $accounts = []; - /** @var \Piggybank $piggybank */ - foreach ($piggybanks as $piggybank) { - $account = $piggybank->account; - $id = $account->id; - if (!isset($accounts[$id])) { - $account->leftOnAccount = $this->_repository->leftOnAccount($account); - $accounts[$id] = ['account' => $account, 'left' => $this->_repository->leftOnAccount($account)]; - } - } - - View::share('title', 'Repeated expenses'); - View::share('subTitle', 'Save for returning bills'); - View::share('mainTitleIcon', 'fa-rotate-left'); - - - return View::make('piggybanks.index')->with('piggybanks', $piggybanks) - ->with('countRepeating', $countRepeating) - ->with('countNonRepeating', $countNonRepeating) - ->with('accounts', $accounts); - } - - /** - * - */ public function show(Piggybank $piggyBank) { - $leftOnAccount = $this->_repository->leftOnAccount($piggyBank->account); - $balance = $piggyBank->account->balance(); - - View::share('subTitle', $piggyBank->name); - - if ($piggyBank->repeats == 1) { - // repeated expense. - View::share('title', 'Repeated expenses'); - View::share('mainTitleIcon', 'fa-rotate-left'); - } else { - // piggy bank. - View::share('title', 'Piggy banks'); - View::share('mainTitleIcon', 'fa-sort-amount-asc'); - } - - return View::make('piggybanks.show')->with('piggyBank', $piggyBank)->with('leftOnAccount', $leftOnAccount) - ->with('balance', $balance); - } - - /** - * @return $this|\Illuminate\Http\RedirectResponse - */ - public function storePiggybank() - { - $data = Input::all(); - unset($data['_token']); - - // extend the data array with the settings needed to create a piggy bank: - $data['repeats'] = 0; - $data['rep_times'] = 1; - $data['rep_every'] = 1; - $data['order'] = 0; - - $piggyBank = $this->_repository->store($data); - if (!is_null($piggyBank->id)) { - Session::flash('success', 'New piggy bank "' . $piggyBank->name . '" created!'); - Event::fire('piggybanks.store', [$piggyBank]); - - return Redirect::route('piggybanks.index.piggybanks'); - - - } else { - Session::flash('error', 'Could not save piggy bank: ' . $piggyBank->errors()->first()); - - return Redirect::route('piggybanks.create.piggybank')->withInput()->withErrors($piggyBank->errors()); - } + throw new NotImplementedException; } /** - * @return $this|\Illuminate\Http\RedirectResponse + * */ - public function storeRepeated() + public function store() { + $data = Input::all(); + $data['repeats'] = 0; + /** @var \FireflyIII\Database\Piggybank $repos */ + $repos = App::make('FireflyIII\Database\Piggybank'); - $data = Input::all(); - unset($data['_token']); + switch ($data['post_submit_action']) { + default: + throw new FireflyException('Cannot handle post_submit_action "' . e($data['post_submit_action']) . '"'); + break; + case 'create_another': + case 'store': + $messages = $repos->validate($data); + /** @var MessageBag $messages ['errors'] */ + if ($messages['errors']->count() > 0) { + Session::flash('warnings', $messages['warnings']); + Session::flash('successes', $messages['successes']); + Session::flash('error', 'Could not save piggy bank: ' . $messages['errors']->first()); + return Redirect::route('piggybanks.create')->withInput()->withErrors($messages['errors']); + } + // store! + $repos->store($data); + Session::flash('success', 'New piggy bank stored!'); - // extend the data array with the settings needed to create a repeated: - $data['repeats'] = 1; - $data['order'] = 0; + if ($data['post_submit_action'] == 'create_another') { + return Redirect::route('piggybanks.create'); + } else { + return Redirect::route('piggybanks.index'); + } + break; + case 'validate_only': + $messageBags = $repos->validate($data); + Session::flash('warnings', $messageBags['warnings']); + Session::flash('successes', $messageBags['successes']); + Session::flash('errors', $messageBags['errors']); - $piggyBank = $this->_repository->store($data); - if ($piggyBank->id) { - Session::flash('success', 'New piggy bank "' . $piggyBank->name . '" created!'); - Event::fire('piggybanks.store', [$piggyBank]); - return Redirect::route('piggybanks.index.repeated'); - - } else { - Session::flash('error', 'Could not save piggy bank: ' . $piggyBank->errors()->first()); - - return Redirect::route('piggybanks.create.repeated')->withInput()->withErrors($piggyBank->errors()); + return Redirect::route('piggybanks.create')->withInput(); + break; } - } /** @@ -378,25 +281,41 @@ class PiggybankController extends BaseController */ public function update(Piggybank $piggyBank) { - $piggyBank = $this->_repository->update($piggyBank, Input::all()); - if ($piggyBank->validate()) { - if ($piggyBank->repeats == 1) { - $route = 'piggybanks.index.repeated'; - $message = 'Repeated expense'; - } else { - $route = 'piggybanks.index.piggybanks'; - $message = 'Piggy bank'; - } + /** @var \FireflyIII\Database\Piggybank $repos */ + $repos = App::make('FireflyIII\Database\Piggybank'); + $data = Input::except('_token'); - Session::flash('success', $message . ' "' . $piggyBank->name . '" updated.'); - Event::fire('piggybanks.update', [$piggyBank]); + switch (Input::get('post_submit_action')) { + default: + throw new FireflyException('Cannot handle post_submit_action "' . e(Input::get('post_submit_action')) . '"'); + break; + case 'create_another': + case 'update': + $messages = $repos->validate($data); + /** @var MessageBag $messages ['errors'] */ + if ($messages['errors']->count() > 0) { + Session::flash('warnings', $messages['warnings']); + Session::flash('successes', $messages['successes']); + Session::flash('error', 'Could not save piggy bank: ' . $messages['errors']->first()); + return Redirect::route('piggybanks.edit', $piggyBank->id)->withInput()->withErrors($messages['errors']); + } + // store! + $repos->update($piggyBank, $data); + Session::flash('success', 'Piggy bank updated!'); - return Redirect::route($route); - } else { - Session::flash('error', 'Could not update piggy bank: ' . $piggyBank->errors()->first()); - - return Redirect::route('piggybanks.edit', $piggyBank->id)->withErrors($piggyBank->errors())->withInput(); + if ($data['post_submit_action'] == 'create_another') { + return Redirect::route('piggybanks.edit', $piggyBank->id); + } else { + return Redirect::route('piggybanks.index'); + } + case 'validate_only': + $messageBags = $repos->validate($data); + Session::flash('warnings', $messageBags['warnings']); + Session::flash('successes', $messageBags['successes']); + Session::flash('errors', $messageBags['errors']); + return Redirect::route('piggybanks.edit', $piggyBank->id)->withInput(); + break; } diff --git a/app/controllers/ReportController.php b/app/controllers/ReportController.php index a313d89564..baab2c53a0 100644 --- a/app/controllers/ReportController.php +++ b/app/controllers/ReportController.php @@ -1,4 +1,5 @@ with('title','Reports')->with('mainTitleIcon','fa-line-chart'); + /** @var \FireflyIII\Database\TransactionJournal $journals */ + $journals = App::make('FireflyIII\Database\TransactionJournal'); + $journal = $journals->first(); + + $date = $journal->date; + $years = []; + while ($date <= Carbon::now()) { + $years[] = $date->format('Y'); + $date->addYear(); + } + + + return View::make('reports.index', compact('years'))->with('title', 'Reports')->with('mainTitleIcon', 'fa-line-chart'); + } + + /** + * @param $year + */ + public function year($year) + { + try { + $date = new Carbon('01-01-' . $year); + } catch (Exception $e) { + App::abort(500); + } + + /** @var \FireflyIII\Database\TransactionJournal $tj */ + $tj = App::make('FireflyIII\Database\TransactionJournal'); + + // get some sums going + $summary = []; + + + $end = clone $date; + $end->endOfYear(); + while ($date < $end) { + $summary[] = [ + 'month' => $date->format('F'), + 'income' => $tj->getSumOfIncomesByMonth($date), + 'expense' => $tj->getSumOfExpensesByMonth($date), + ]; + $date->addMonth(); + } + + + // draw some charts etc. + return View::make('reports.year', compact('summary'))->with('title', 'Reports')->with('mainTitleIcon', 'fa-line-chart')->with('subTitle', $year)->with( + 'subTitleIcon', 'fa-bar-chart' + )->with('year', $year); } } \ No newline at end of file diff --git a/app/database/migrations/2014_06_27_163818_create_piggybanks_table.php b/app/database/migrations/2014_06_27_163818_create_piggybanks_table.php index b30cae3406..74ff3e20c8 100644 --- a/app/database/migrations/2014_06_27_163818_create_piggybanks_table.php +++ b/app/database/migrations/2014_06_27_163818_create_piggybanks_table.php @@ -33,6 +33,7 @@ class CreatePiggybanksTable extends Migration $table->smallInteger('rep_times')->unsigned()->nullable(); $table->enum('reminder', ['day', 'week', 'month', 'year'])->nullable(); $table->smallInteger('reminder_skip')->unsigned(); + $table->boolean('remind_me'); $table->integer('order')->unsigned(); // connect account to piggybank. diff --git a/app/lib/Firefly/Form/Form.php b/app/lib/Firefly/Form/Form.php index 6414fd983e..a06c3184ec 100644 --- a/app/lib/Firefly/Form/Form.php +++ b/app/lib/Firefly/Form/Form.php @@ -10,9 +10,10 @@ class Form { /** - * @param $name - * @param null $value + * @param $name + * @param null $value * @param array $options + * * @return string * @throws FireflyException */ @@ -29,18 +30,42 @@ class Form return self::ffInput('checkbox', $name, $value, $options); } + /** + * @param $name + * @param null $value + * @param array $options + * + * @return string + * @throws FireflyException + */ public static function ffAmount($name, $value = null, array $options = []) { $options['step'] = 'any'; - $options['min'] = '0.01'; + $options['min'] = '0.01'; return self::ffInput('amount', $name, $value, $options); } /** - * @param $name - * @param null $value + * @param $name + * @param null $value * @param array $options + * + * @return string + * @throws FireflyException + */ + public static function ffBalance($name, $value = null, array $options = []) + { + $options['step'] = 'any'; + return self::ffInput('amount', $name, $value, $options); + + } + + /** + * @param $name + * @param null $value + * @param array $options + * * @return string * @throws FireflyException */ @@ -50,9 +75,10 @@ class Form } /** - * @param $name - * @param null $value + * @param $name + * @param null $value * @param array $options + * * @return string * @throws FireflyException */ @@ -63,10 +89,11 @@ class Form } /** - * @param $name + * @param $name * @param array $list - * @param null $selected + * @param null $selected * @param array $options + * * @return string * @throws FireflyException */ @@ -76,9 +103,10 @@ class Form } /** - * @param $name - * @param null $value + * @param $name + * @param null $value * @param array $options + * * @return string * @throws FireflyException */ @@ -88,16 +116,25 @@ class Form } - public static function label($name) + /** + * @param $name + * @param $options + * + * @return string + */ + public static function label($name, $options) { + if (isset($options['label'])) { + return $options['label']; + } $labels = [ - 'amount_min' => 'Amount (min)', - 'amount_max' => 'Amount (max)', - 'match' => 'Matches on', - 'repeat_freq' => 'Repetition', + 'amount_min' => 'Amount (min)', + 'amount_max' => 'Amount (max)', + 'match' => 'Matches on', + 'repeat_freq' => 'Repetition', 'account_from_id' => 'Account from', - 'account_to_id' => 'Account to', - 'account_id' => 'Asset account' + 'account_to_id' => 'Account to', + 'account_id' => 'Asset account' ]; return isset($labels[$name]) ? $labels[$name] : str_replace('_', ' ', ucfirst($name)); @@ -151,28 +188,29 @@ class Form case 'create': $return = '
'; break; case 'update': $return = '
'; break; default: throw new FireflyException('Cannot create ffOptionsList for option (store+return) ' . $type); break; } - return $store.$validate.$return; + return $store . $validate . $return; } /** - * @param $type - * @param $name - * @param null $value + * @param $type + * @param $name + * @param null $value * @param array $options * @param array $list + * * @return string * @throws FireflyException */ @@ -181,10 +219,10 @@ class Form /* * add some defaults to this method: */ - $options['class'] = 'form-control'; - $options['id'] = 'ffInput_' . $name; + $options['class'] = 'form-control'; + $options['id'] = 'ffInput_' . $name; $options['autocomplete'] = 'off'; - $label = self::label($name); + $label = self::label($name, $options); /* * Make label and placeholder look nice. */ @@ -193,9 +231,9 @@ class Form /* * Get prefilled value: */ - if(\Session::has('prefilled')) { + if (\Session::has('prefilled')) { $prefilled = \Session::get('prefilled'); - $value = isset($prefilled[$name]) && is_null($value) ? $prefilled[$name] : $value; + $value = isset($prefilled[$name]) && is_null($value) ? $prefilled[$name] : $value; } /* diff --git a/app/lib/Firefly/Helper/Controllers/Account.php b/app/lib/Firefly/Helper/Controllers/Account.php index a7fb95a02b..93dd3b12d7 100644 --- a/app/lib/Firefly/Helper/Controllers/Account.php +++ b/app/lib/Firefly/Helper/Controllers/Account.php @@ -12,14 +12,17 @@ class Account implements AccountInterface /** * @param \Account $account + * * @return \TransactionJournal|null */ public function openingBalanceTransaction(\Account $account) { return \TransactionJournal::withRelevantData() ->accountIs($account) - ->leftJoin('transaction_types', 'transaction_types.id', '=', - 'transaction_journals.transaction_type_id') + ->leftJoin( + 'transaction_types', 'transaction_types.id', '=', + 'transaction_journals.transaction_type_id' + ) ->where('transaction_types.type', 'Opening balance') ->first(['transaction_journals.*']); } @@ -36,7 +39,7 @@ class Account implements AccountInterface * For now, Firefly simply warns the user of this. * * @param \Account $account - * @param $perPage + * @param $perPage * * @return array|mixed * @throws \Firefly\Exception\FireflyException @@ -110,17 +113,25 @@ class Account implements AccountInterface // statistics (transactions) - $trIn = floatval(\Transaction::before($end)->after($start)->accountIs($account)->moreThan(0) - ->transactionTypes(['Deposit', 'Withdrawal'])->sum('transactions.amount')); - $trOut = floatval(\Transaction::before($end)->after($start)->accountIs($account)->lessThan(0) - ->transactionTypes(['Deposit', 'Withdrawal'])->sum('transactions.amount')); + $trIn = floatval( + \Transaction::before($end)->after($start)->accountIs($account)->moreThan(0) + ->transactionTypes(['Deposit', 'Withdrawal'])->sum('transactions.amount') + ); + $trOut = floatval( + \Transaction::before($end)->after($start)->accountIs($account)->lessThan(0) + ->transactionTypes(['Deposit', 'Withdrawal'])->sum('transactions.amount') + ); $trDiff = $trIn + $trOut; // statistics (transfers) - $trfIn = floatval(\Transaction::before($end)->after($start)->accountIs($account)->moreThan(0) - ->transactionTypes(['Transfer'])->sum('transactions.amount')); - $trfOut = floatval(\Transaction::before($end)->after($start)->accountIs($account)->lessThan(0) - ->transactionTypes(['Transfer'])->sum('transactions.amount')); + $trfIn = floatval( + \Transaction::before($end)->after($start)->accountIs($account)->moreThan(0) + ->transactionTypes(['Transfer'])->sum('transactions.amount') + ); + $trfOut = floatval( + \Transaction::before($end)->after($start)->accountIs($account)->lessThan(0) + ->transactionTypes(['Transfer'])->sum('transactions.amount') + ); $trfDiff = $trfIn + $trfOut; $stats['period'] = [ diff --git a/app/lib/Firefly/Helper/Controllers/Json.php b/app/lib/Firefly/Helper/Controllers/Json.php index 80b87286f7..34e4f1f11f 100644 --- a/app/lib/Firefly/Helper/Controllers/Json.php +++ b/app/lib/Firefly/Helper/Controllers/Json.php @@ -1,11 +1,4 @@ limitrepetitions()->get() as $l) { - $l->delete(); - } $limit->createRepetition($limit->startdate); return true; diff --git a/app/lib/Firefly/Trigger/Piggybanks/EloquentPiggybankTrigger.php b/app/lib/Firefly/Trigger/Piggybanks/EloquentPiggybankTrigger.php index 80023eb48e..09e3943099 100644 --- a/app/lib/Firefly/Trigger/Piggybanks/EloquentPiggybankTrigger.php +++ b/app/lib/Firefly/Trigger/Piggybanks/EloquentPiggybankTrigger.php @@ -193,7 +193,7 @@ class EloquentPiggybankTrigger 'Firefly\Trigger\Piggybanks\EloquentPiggybankTrigger@updateRelatedTransfer' ); $events->listen( - 'piggybanks.check', 'Firefly\Trigger\Piggybanks\EloquentPiggybankTrigger@checkRepeatingPiggies' + 'piggybanks.storepiggybanks.check', 'Firefly\Trigger\Piggybanks\EloquentPiggybankTrigger@checkRepeatingPiggies' ); } diff --git a/app/lib/FireflyIII/Database/Account.php b/app/lib/FireflyIII/Database/Account.php new file mode 100644 index 0000000000..60726af53c --- /dev/null +++ b/app/lib/FireflyIII/Database/Account.php @@ -0,0 +1,469 @@ +setUser(\Auth::user()); + } + + /** + * @param Ardent $model + * @param array $data + * + * @return bool + */ + public function update(Ardent $model, array $data) + { + $model->name = $data['name']; + $model->active = isset($data['active']) ? intval($data['active']) : 0; + $model->save(); + + if (isset($data['openingbalance']) && isset($data['openingbalancedate'])) { + $openingBalance = $this->openingBalanceTransaction($model); + + $openingBalance->date = new Carbon($data['openingbalancedate']); + $openingBalance->save(); + $amount = floatval($data['openingbalance']); + /** @var \Transaction $transaction */ + foreach ($openingBalance->transactions as $transaction) { + if ($transaction->account_id == $model->id) { + $transaction->amount = $amount; + } else { + $transaction->amount = $amount * -1; + } + $transaction->save(); + } + } + return true; + } + + /** + * Get all asset accounts. Optional JSON based parameters. + * + * @param array $parameters + * + * @return Collection + */ + public function getAssetAccounts(array $parameters = []) + { + return $this->getAccountsByType(['Default account', 'Asset account'], $parameters); + + } + + /** + * @param \Account $account + * + * @return \Account|null + */ + public function findInitialBalanceAccount(\Account $account) + { + /** @var \FireflyIII\Database\AccountType $acctType */ + $acctType = \App::make('FireflyIII\Database\AccountType'); + + $accountType = $acctType->findByWhat('initial'); + + return $this->getUser()->accounts()->where('account_type_id', $accountType->id)->where('name', 'LIKE', $account->name . '%')->first(); + } + + /** + * @param array $types + * @param array $parameters + * + * @return Collection + */ + public function getAccountsByType(array $types, array $parameters = []) + { + /* + * Basic query: + */ + $query = $this->getUser()->accounts()->accountTypeIn($types); + + + /* + * Without an opening balance, the rest of these queries will fail. + */ + + $query->leftJoin('transactions', 'transactions.account_id', '=', 'accounts.id'); + $query->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id'); + + /* + * Not used, but useful for the balance within a certain month / year. + */ + $balanceOnDate = isset($parameters['date']) ? $parameters['date'] : Carbon::now(); + $query->where( + function ($q) use ($balanceOnDate) { + $q->where('transaction_journals.date', '<=', $balanceOnDate->format('Y-m-d')); + $q->orWhereNull('transaction_journals.date'); + } + ); + + $query->groupBy('accounts.id'); + + /* + * If present, process parameters for sorting: + */ + $query->orderBy('name', 'ASC'); + + /* + * If present, process parameters for searching. + */ + if (isset($parameters['search'])) { + $query->where('name', 'LIKE', '%' . e($parameters['search']['value'] . '%')); + } + + /* + * If present, start at $start: + */ + if (isset($parameters['start'])) { + $query->skip(intval($parameters['start'])); + } + if (isset($parameters['length'])) { + $query->take(intval($parameters['length'])); + } + + return $query->get(['accounts.*', \DB::Raw('SUM(`transactions`.`amount`) as `balance`')]); + } + + /** + * @return int + */ + public function countAssetAccounts() + { + return $this->countAccountsByType(['Default account', 'Asset account']); + } + + /** + * @return int + */ + public function countExpenseAccounts() + { + return $this->countAccountsByType(['Expense account', 'Beneficiary account']); + } + + /** + * @param array $types + * + * @return int + */ + public function countAccountsByType(array $types) + { + return $this->getUser()->accounts()->accountTypeIn($types)->count(); + } + + /** + * @param array $parameters + * + * @return Collection + */ + public function getExpenseAccounts(array $parameters = []) + { + return $this->getAccountsByType(['Expense account', 'Beneficiary account'], $parameters); + } + + /** + * Get all default accounts. + * + * @return Collection + */ + public function getDefaultAccounts() + { + // TODO: Implement getDefaultAccounts() method. + } + + /** + * Returns an object with id $id. + * + * @param int $id + * + * @return Ardent + */ + public function find($id) + { + // TODO: Implement find() method. + } + + /** + * Returns all objects. + * + * @return Collection + */ + public function get() + { + // TODO: Implement get() method. + } + + /** + * Counts the number of total revenue accounts. Useful for DataTables. + * + * @return int + */ + public function countRevenueAccounts() + { + return $this->countAccountsByType(['Revenue account']); + } + + /** + * Get all revenue accounts. + * + * @param array $parameters + * + * @return Collection + */ + public function getRevenueAccounts(array $parameters = []) + { + return $this->getAccountsByType(['Revenue account'], $parameters); + } + + /** + * @param Ardent $model + * + * @return bool + */ + public function destroy(Ardent $model) + { + $model->delete(); + return true; + + } + + /** + * @param \Account $account + * + * @return \TransactionJournal|null + */ + public function openingBalanceTransaction(\Account $account) + { + return \TransactionJournal::withRelevantData() + ->accountIs($account) + ->leftJoin( + 'transaction_types', 'transaction_types.id', '=', + 'transaction_journals.transaction_type_id' + ) + ->where('transaction_types.type', 'Opening balance') + ->first(['transaction_journals.*']); + } + + /** + * Validates a model. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param Ardent $model + * + * @return array + */ + public function validateObject(Ardent $model) + { + die('No impl'); + } + + /** + * Validates a model. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param array $model + * + * @return array + */ + public function validate(array $model) + { + $warnings = new MessageBag; + $successes = new MessageBag; + $errors = new MessageBag; + + /* + * Name validation: + */ + if (!isset($model['name'])) { + $errors->add('name', 'Name is mandatory'); + } + + if (isset($model['name']) && strlen($model['name']) == 0) { + $errors->add('name', 'Name is too short'); + } + if (isset($model['name']) && strlen($model['name']) > 100) { + $errors->add('name', 'Name is too long'); + } + $validator = \Validator::make([$model], \Account::$rules); + if ($validator->invalid()) { + $errors->merge($errors); + } + + /* + * type validation. + */ + if (!isset($model['what'])) { + $errors->add('name', 'Internal error: need to know type of account!'); + } + + /* + * Opening balance and opening balance date. + */ + if (isset($model['what']) && $model['what'] == 'asset') { + if (isset($model['openingbalance']) && strlen($model['openingbalance']) > 0 && !is_numeric($model['openingbalance'])) { + $errors->add('openingbalance', 'This is not a number.'); + } + if (isset($model['openingbalancedate']) && strlen($model['openingbalancedate']) > 0) { + try { + new Carbon($model['openingbalancedate']); + } catch (\Exception $e) { + $errors->add('openingbalancedate', 'This date is invalid.'); + } + } + } + + + if (!$errors->has('name')) { + $successes->add('name', 'OK'); + } + if (!$errors->has('openingbalance')) { + $successes->add('openingbalance', 'OK'); + } + if (!$errors->has('openingbalancedate')) { + $successes->add('openingbalancedate', 'OK'); + } + return [ + 'errors' => $errors, + 'warnings' => $warnings, + 'successes' => $successes + ]; + } + + /** + * @param array $data + * + * @return Ardent + */ + public function store(array $data) + { + + /* + * Find account type. + */ + /** @var \FireflyIII\Database\AccountType $acctType */ + $acctType = \App::make('FireflyIII\Database\AccountType'); + + $accountType = $acctType->findByWhat($data['what']); + + $data['user_id'] = $this->getUser()->id; + $data['account_type_id'] = $accountType->id; + $data['active'] = isset($data['active']) && $data['active'] === '1' ? 1 : 0; + + + $data = array_except($data, array('_token', 'what')); + $account = new \Account($data); + if (!$account->validate()) { + var_dump($account->errors()->all()); + exit; + } + $account->save(); + if (isset($data['openingbalance']) && floatval($data['openingbalance']) != 0) { + $this->storeInitialBalance($account, $data); + } + + + /* Tell transaction journal to store a new one.*/ + + + return $account; + + } + + /** + * @param \Account $account + * @param array $data + * + * @return bool + */ + public function storeInitialBalance(\Account $account, array $data) + { + $opposingData = [ + 'name' => $account->name . ' Initial Balance', + 'active' => 0, + 'what' => 'initial' + ]; + $opposingAccount = $this->store($opposingData); + + /* + * Create a journal from opposing to account or vice versa. + */ + $balance = floatval($data['openingbalance']); + $date = new Carbon($data['openingbalancedate']); + /** @var \FireflyIII\Database\TransactionJournal $tj */ + $tj = \App::make('FireflyIII\Database\TransactionJournal'); + if ($balance < 0) { + // first transaction draws money from the new account to the opposing + $from = $account; + $to = $opposingAccount; + } else { + // first transaction puts money into account + $from = $opposingAccount; + $to = $account; + } + + // data for transaction journal: + $balance = $balance < 0 ? $balance * -1 : $balance; + $opening = [ + 'what' => 'opening', + 'currency' => 'EUR', + 'amount' => $balance, + 'from' => $from, + 'to' => $to, + 'date' => $date, + 'description' => 'Opening balance for new account ' . $account->name, + ]; + + + $validation = $tj->validate($opening); + if ($validation['errors']->count() == 0) { + $tj->store($opening); + return true; + } else { + var_dump($validation['errors']); + exit; + } + return false; + } + + /** + * Finds an account type using one of the "$what"'s: expense, asset, revenue, opening, etc. + * + * @param $what + * + * @return \AccountType|null + */ + public function findByWhat($what) + { + // TODO: Implement findByWhat() method. + } + + /** + * @param array $ids + * + * @return Collection + */ + public function getByIds(array $ids) + { + return $this->getUser()->accounts()->whereIn('id', $ids)->get(); + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Database/AccountType.php b/app/lib/FireflyIII/Database/AccountType.php new file mode 100644 index 0000000000..8a4e13f1dc --- /dev/null +++ b/app/lib/FireflyIII/Database/AccountType.php @@ -0,0 +1,139 @@ +first(); + break; + case 'asset': + return \AccountType::whereType('Asset account')->first(); + break; + case 'revenue': + return \AccountType::whereType('Revenue account')->first(); + break; + case 'initial': + return \AccountType::whereType('Initial balance account')->first(); + break; + default: + throw new FireflyException('Cannot find account type described as "' . e($what) . '".'); + break; + + } + return null; + } + + /** + * @param Ardent $model + * + * @return bool + */ + public function destroy(Ardent $model) + { + // TODO: Implement destroy() method. + } + + /** + * Validates a model. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param Ardent $model + * + * @return array + */ + public function validateObject(Ardent $model) + { + // TODO: Implement validateObject() method. + } + + /** + * Validates an array. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param array $model + * + * @return array + */ + public function validate(array $model) + { + // TODO: Implement validate() method. + } + + /** + * @param array $data + * + * @return Ardent + */ + public function store(array $data) + { + // TODO: Implement store() method. + } + + /** + * Returns an object with id $id. + * + * @param int $id + * + * @return Ardent + */ + public function find($id) + { + // TODO: Implement find() method. + } + + /** + * Returns all objects. + * + * @return Collection + */ + public function get() + { + // TODO: Implement get() method. + } + + /** + * @param array $ids + * + * @return Collection + */ + public function getByIds(array $ids) + { + // TODO: Implement getByIds() method. + } + + /** + * @param Ardent $model + * @param array $data + * + * @return bool + */ + public function update(Ardent $model, array $data) + { + // TODO: Implement update() method. + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Database/Budget.php b/app/lib/FireflyIII/Database/Budget.php new file mode 100644 index 0000000000..ba8d22fcf4 --- /dev/null +++ b/app/lib/FireflyIII/Database/Budget.php @@ -0,0 +1,234 @@ +setUser(\Auth::user()); + } + + /** + * @param \Budget $budget + * @param Carbon $date + * + * @return \LimitRepetition|null + */ + public function repetitionOnStartingOnDate(\Budget $budget, Carbon $date) + { + return \LimitRepetition:: + leftJoin('limits', 'limit_repetitions.limit_id', '=', 'limits.id')->leftJoin( + 'components', 'limits.component_id', '=', 'components.id' + )->where('limit_repetitions.startdate', $date->format('Y-m-d'))->where( + 'components.id', $budget->id + )->first(['limit_repetitions.*']); + } + + /** + * @param Ardent $model + * + * @return bool + */ + public function destroy(Ardent $model) + { + $model->delete(); + return true; + } + + /** + * Validates a model. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param Ardent $model + * + * @return array + */ + public function validateObject(Ardent $model) + { + // TODO: Implement validateObject() method. + } + + /** + * Validates an array. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param array $model + * + * @return array + */ + public function validate(array $model) + { + $warnings = new MessageBag; + $successes = new MessageBag; + $errors = new MessageBag; + + if(isset($model['name'])) { + if(strlen($model['name']) < 1) { + $errors->add('name', 'Name is too short'); + } + if(strlen($model['name']) > 200) { + $errors->add('name', 'Name is too long'); + + } + } else { + $errors->add('name', 'Name is mandatory'); + } + $validator = \Validator::make($model, \Component::$rules); + + if ($validator->invalid()) { + $errors->merge($validator->errors()); + } + + + if(!$errors->has('name')) { + $successes->add('name','OK'); + } + + return [ + 'errors' => $errors, + 'warnings' => $warnings, + 'successes' => $successes + ]; + } + + /** + * @param array $data + * + * @return Ardent + */ + public function store(array $data) + { + $data['user_id'] = $this->getUser()->id; + + $budget = new \Budget($data); + $budget->class = 'Budget'; + + if (!$budget->validate()) { + var_dump($budget->errors()->all()); + exit; + } + $budget->save(); + return $budget; + } + + /** + * Returns an object with id $id. + * + * @param int $id + * + * @return Ardent + */ + public function find($id) + { + // TODO: Implement find() method. + } + + /** + * Returns all objects. + * + * @return Collection + */ + public function get() + { + $budgets = $this->getUser()->budgets()->get(); + + return $budgets; + } + + /** + * @param \Budget $budget + * @param Carbon $date + * @return float + */ + public function spentInMonth(\Budget $budget, Carbon $date) { + $end = clone $date; + $date->startOfMonth(); + $end->endOfMonth(); + $sum = floatval($budget->transactionjournals()->before($end)->after($date)->lessThan(0)->sum('amount')) * -1; + return $sum; + } + + /** + * @param array $ids + * + * @return Collection + */ + public function getByIds(array $ids) + { + // TODO: Implement getByIds() method. + } + + /** + * Finds an account type using one of the "$what"'s: expense, asset, revenue, opening, etc. + * + * @param $what + * + * @return \AccountType|null + */ + public function findByWhat($what) + { + // TODO: Implement findByWhat() method. + } + + /** + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function transactionsWithoutBudgetInDateRange(Carbon $start, Carbon $end) + { + // Add expenses that have no budget: + return \Auth::user()->transactionjournals()->whereNotIn( + 'transaction_journals.id', function ($query) use ($start, $end) { + $query->select('transaction_journals.id')->from('transaction_journals') + ->leftJoin( + 'component_transaction_journal', 'component_transaction_journal.transaction_journal_id', '=', + 'transaction_journals.id' + ) + ->leftJoin('components', 'components.id', '=', 'component_transaction_journal.component_id') + ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) + ->where('components.class', 'Budget'); + } + )->before($end)->after($start)->lessThan(0)->transactionTypes(['Withdrawal'])->get(); + } + + /** + * @param Ardent $model + * @param array $data + * + * @return bool + */ + public function update(Ardent $model, array $data) + { + $model->name = $data['name']; + if (!$model->validate()) { + var_dump($model->errors()->all()); + exit; + } + + + $model->save(); + + return true; + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Database/Category.php b/app/lib/FireflyIII/Database/Category.php new file mode 100644 index 0000000000..47349ac021 --- /dev/null +++ b/app/lib/FireflyIII/Database/Category.php @@ -0,0 +1,129 @@ +setUser(\Auth::user()); + } + + /** + * @param Ardent $model + * + * @return bool + */ + public function destroy(Ardent $model) + { + // TODO: Implement destroy() method. + } + + /** + * Validates a model. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param Ardent $model + * + * @return array + */ + public function validateObject(Ardent $model) + { + // TODO: Implement validateObject() method. + } + + /** + * Validates an array. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param array $model + * + * @return array + */ + public function validate(array $model) + { + // TODO: Implement validate() method. + } + + /** + * @param array $data + * + * @return Ardent + */ + public function store(array $data) + { + // TODO: Implement store() method. + } + + /** + * Returns an object with id $id. + * + * @param int $id + * + * @return Ardent + */ + public function find($id) + { + // TODO: Implement find() method. + } + + /** + * Returns all objects. + * + * @return Collection + */ + public function get() + { + // TODO: Implement get() method. + } + + /** + * @param array $ids + * + * @return Collection + */ + public function getByIds(array $ids) + { + // TODO: Implement getByIds() method. + } + + /** + * Finds an account type using one of the "$what"'s: expense, asset, revenue, opening, etc. + * + * @param $what + * + * @return \AccountType|null + */ + public function findByWhat($what) + { + // TODO: Implement findByWhat() method. + } + + /** + * @param Ardent $model + * @param array $data + * + * @return bool + */ + public function update(Ardent $model, array $data) + { + // TODO: Implement update() method. + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Database/Ifaces/AccountInterface.php b/app/lib/FireflyIII/Database/Ifaces/AccountInterface.php new file mode 100644 index 0000000000..a24fa07421 --- /dev/null +++ b/app/lib/FireflyIII/Database/Ifaces/AccountInterface.php @@ -0,0 +1,109 @@ +balance(); + /** @var \Piggybank $p */ + foreach ($account->piggybanks()->get() as $p) { + $balance -= $p->currentRelevantRep()->currentamount; + } + + return $balance; + + } + + /** + * + */ + public function __construct() + { + $this->setUser(\Auth::user()); + } + + /** + * @param Ardent $model + * + * @return bool + */ + public function destroy(Ardent $model) + { + $model->delete(); + } + + /** + * Validates a model. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param Ardent $model + * + * @return array + */ + public function validateObject(Ardent $model) + { + // TODO: Implement validateObject() method. + } + + /** + * Validates an array. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param array $model + * + * @return array + */ + public function validate(array $model) + { + $warnings = new MessageBag; + $successes = new MessageBag; + $errors = new MessageBag; + + /* + * Name validation: + */ + if (!isset($model['name'])) { + $errors->add('name', 'Name is mandatory'); + } + + if (isset($model['name']) && strlen($model['name']) == 0) { + $errors->add('name', 'Name is too short'); + } + if (isset($model['name']) && strlen($model['name']) > 100) { + $errors->add('name', 'Name is too long'); + } + + if (intval($model['account_id']) == 0) { + $errors->add('account_id', 'Account is mandatory'); + } + if ($model['targetdate'] == '' && isset($model['remind_me']) && intval($model['remind_me']) == 1) { + $errors->add('targetdate', 'Target date is mandatory when setting reminders.'); + } + if ($model['targetdate'] != '') { + try { + new Carbon($model['targetdate']); + } catch (\Exception $e) { + $errors->add('date', 'Invalid date.'); + } + } + if (floatval($model['targetamount']) < 0.01) { + $errors->add('targetamount', 'Amount should be above 0.01.'); + } + if (!in_array(ucfirst($model['reminder']), \Config::get('firefly.piggybank_periods'))) { + $errors->add('reminder', 'Invalid reminder period (' . $model['reminder'] . ')'); + } + // check period. + if (!$errors->has('reminder') && !$errors->has('targetdate') && isset($model['remind_me']) && intval($model['remind_me']) == 1) { + $today = new Carbon; + $target = new Carbon($model['targetdate']); + switch ($model['reminder']) { + case 'week': + $today->addWeek(); + break; + case 'month': + $today->addMonth(); + break; + case 'year': + $today->addYear(); + break; + } + if ($today > $target) { + $errors->add('reminder', 'Target date is too close to today to set reminders.'); + } + } + + $validator = \Validator::make($model, \Piggybank::$rules); + if ($validator->invalid()) { + $errors->merge($errors); + } + + // add ok messages. + $list = ['name', 'account_id', 'targetamount', 'targetdate', 'remind_me', 'reminder']; + foreach ($list as $entry) { + if (!$errors->has($entry) && !$warnings->has($entry)) { + $successes->add($entry, 'OK'); + } + } + + return [ + 'errors' => $errors, + 'warnings' => $warnings, + 'successes' => $successes + ]; + } + + /** + * @param array $data + * + * @return Ardent + */ + public function store(array $data) + { + $data['rep_every'] = isset($data['rep_every']) ? $data['rep_every'] : 0; + $data['reminder_skip'] = isset($data['reminder_skip']) ? $data['reminder_skip'] : 0; + $data['order'] = isset($data['order']) ? $data['order'] : 0; + $data['remind_me'] = isset($data['remind_me']) ? intval($data['remind_me']) : 0; + $data['startdate'] = isset($data['startdate']) ? $data['startdate'] : Carbon::now()->format('Y-m-d'); + $data['targetdate'] = isset($data['targetdate']) && $data['targetdate'] != '' ? $data['targetdate'] : null; + + + $piggybank = new \Piggybank($data); + if (!$piggybank->validate()) { + var_dump($piggybank->errors()->all()); + exit; + } + $piggybank->save(); + \Event::fire('piggybanks.store', [$piggybank]); + $piggybank->save(); + } + + /** + * Returns an object with id $id. + * + * @param int $id + * + * @return Ardent + */ + public function find($id) + { + // TODO: Implement find() method. + } + + /** + * Returns all objects. + * + * @return Collection + */ + public function get() + { + return $this->getUser()->piggybanks()->where('repeats', 0)->get(); + } + + /** + * @param array $ids + * + * @return Collection + */ + public function getByIds(array $ids) + { + // TODO: Implement getByIds() method. + } + + /** + * Finds an account type using one of the "$what"'s: expense, asset, revenue, opening, etc. + * + * @param $what + * + * @return \AccountType|null + */ + public function findByWhat($what) + { + // TODO: Implement findByWhat() method. + } + + /** + * @param Ardent $model + * @param array $data + * + * @return bool + */ + public function update(Ardent $model, array $data) + { + /** @var \Piggybank $model */ + $model->name = $data['name']; + $model->account_id = intval($data['account_id']); + $model->targetamount = floatval($data['targetamount']); + $model->targetdate = isset($data['targetdate']) && $data['targetdate'] != '' ? $data['targetdate'] : null; + $model->rep_every = isset($data['rep_every']) ? $data['rep_every'] : 0; + $model->reminder_skip = isset($data['reminder_skip']) ? $data['reminder_skip'] : 0; + $model->order = isset($data['order']) ? $data['order'] : 0; + $model->remind_me = isset($data['remind_me']) ? intval($data['remind_me']) : 0; + if(!$model->validate()) { + var_dump($model->errors()); + exit(); + } + $model->save(); + return true; + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Database/Recurring.php b/app/lib/FireflyIII/Database/Recurring.php new file mode 100644 index 0000000000..b8cb6ba29f --- /dev/null +++ b/app/lib/FireflyIII/Database/Recurring.php @@ -0,0 +1,143 @@ +setUser(\Auth::user()); + } + + /** + * @param \RecurringTransaction $recurring + * @param Carbon $start + * @param Carbon $end + * + * @return \TransactionJournal|null + */ + public function getJournalForRecurringInRange(\RecurringTransaction $recurring, Carbon $start, Carbon $end) + { + return $this->getUser()->transactionjournals()->where('recurring_transaction_id', $recurring->id)->after($start)->before($end)->first(); + + } + + /** + * @param Ardent $model + * + * @return bool + */ + public function destroy(Ardent $model) + { + // TODO: Implement destroy() method. + } + + /** + * Validates a model. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param Ardent $model + * + * @return array + */ + public function validateObject(Ardent $model) + { + // TODO: Implement validateObject() method. + } + + /** + * Validates an array. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param array $model + * + * @return array + */ + public function validate(array $model) + { + // TODO: Implement validate() method. + } + + /** + * @param array $data + * + * @return Ardent + */ + public function store(array $data) + { + // TODO: Implement store() method. + } + + /** + * Returns an object with id $id. + * + * @param int $id + * + * @return Ardent + */ + public function find($id) + { + // TODO: Implement find() method. + } + + /** + * Returns all objects. + * + * @return Collection + */ + public function get() + { + return $this->getUser()->recurringtransactions()->get(); + } + + /** + * @param array $ids + * + * @return Collection + */ + public function getByIds(array $ids) + { + // TODO: Implement getByIds() method. + } + + /** + * Finds an account type using one of the "$what"'s: expense, asset, revenue, opening, etc. + * + * @param $what + * + * @return \AccountType|null + */ + public function findByWhat($what) + { + // TODO: Implement findByWhat() method. + } + + /** + * @param Ardent $model + * @param array $data + * + * @return bool + */ + public function update(Ardent $model, array $data) + { + // TODO: Implement update() method. + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Database/SwitchUser.php b/app/lib/FireflyIII/Database/SwitchUser.php new file mode 100644 index 0000000000..3fa27d3f84 --- /dev/null +++ b/app/lib/FireflyIII/Database/SwitchUser.php @@ -0,0 +1,27 @@ +_user; + } + + /** + * @param $user + */ + public function setUser($user) + { + $this->_user = $user; + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Database/Transaction.php b/app/lib/FireflyIII/Database/Transaction.php new file mode 100644 index 0000000000..744880dd73 --- /dev/null +++ b/app/lib/FireflyIII/Database/Transaction.php @@ -0,0 +1,195 @@ +add('account', 'No account present'); + } + if (isset($model['account']) && !($model['account'] instanceof \Account)) { + $errors->add('account', 'No valid account present'); + } + if (isset($model['account_id']) && intval($model['account_id']) < 0) { + $errors->add('account', 'No valid account_id present'); + } + + if (isset($model['piggybank_id']) && intval($model['piggybank_id']) < 0) { + $errors->add('piggybank', 'No valid piggybank_id present'); + } + + if (!isset($model['transaction_journal_id']) && !isset($model['transaction_journal'])) { + $errors->add('transaction_journal', 'No TJ present'); + } + if (isset($model['transaction_journal']) && !($model['transaction_journal'] instanceof \TransactionJournal)) { + $errors->add('transaction_journal', 'No valid transaction_journal present'); + } + if (isset($model['transaction_journal_id']) && intval($model['transaction_journal_id']) < 0) { + $errors->add('account', 'No valid transaction_journal_id present'); + } + + if (isset($model['description']) && strlen($model['description']) > 255) { + $errors->add('account', 'Description too long'); + } + + if (!isset($model['amount'])) { + $errors->add('amount', 'No amount present.'); + } + if (isset($model['amount']) && floatval($model['amount']) == 0) { + $errors->add('amount', 'Invalid amount.'); + } + + if (!$errors->has('account')) { + $successes->add('account', 'OK'); + } + if (!$errors->has('')) { + $successes->add('piggybank', 'OK'); + } + if (!$errors->has('transaction_journal')) { + $successes->add('transaction_journal', 'OK'); + } + if (!$errors->has('amount')) { + $successes->add('amount', 'OK'); + } + + return [ + 'errors' => $errors, + 'warnings' => $warnings, + 'successes' => $successes + ]; + } + + /** + * @param array $data + * + * @return Ardent + */ + public function store(array $data) + { + // TODO: Implement store() method. + $transaction = new \Transaction; + $transaction->account()->associate($data['account']); + $transaction->transactionJournal()->associate($data['transaction_journal']); + $transaction->amount = floatval($data['amount']); + if (isset($data['piggybank'])) { + $transaction->piggybank()->associate($data['piggybank']); + } + if (isset($data['description'])) { + $transaction->description = $data['description']; + } + if ($transaction->validate()) { + $transaction->save(); + } else { + throw new FireflyException($transaction->errors()->first()); + } + return $transaction; + } + + /** + * Returns an object with id $id. + * + * @param int $id + * + * @return Ardent + */ + public function find($id) + { + // TODO: Implement find() method. + } + + /** + * Returns all objects. + * + * @return Collection + */ + public function get() + { + // TODO: Implement get() method. + } + + /** + * Finds an account type using one of the "$what"'s: expense, asset, revenue, opening, etc. + * + * @param $what + * + * @return \AccountType|null + */ + public function findByWhat($what) + { + // TODO: Implement findByWhat() method. + } + + /** + * @param Ardent $model + * @param array $data + * + * @return bool + */ + public function update(Ardent $model, array $data) + { + // TODO: Implement update() method. + } + + /** + * @param array $ids + * + * @return Collection + */ + public function getByIds(array $ids) + { + // TODO: Implement getByIds() method. + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Database/TransactionCurrency.php b/app/lib/FireflyIII/Database/TransactionCurrency.php new file mode 100644 index 0000000000..9e46efb207 --- /dev/null +++ b/app/lib/FireflyIII/Database/TransactionCurrency.php @@ -0,0 +1,130 @@ +first(); + } + + /** + * @param array $ids + * + * @return Collection + */ + public function getByIds(array $ids) + { + // TODO: Implement getByIds() method. + } + + /** + * @param Ardent $model + * @param array $data + * + * @return bool + */ + public function update(Ardent $model, array $data) + { + // TODO: Implement update() method. + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Database/TransactionJournal.php b/app/lib/FireflyIII/Database/TransactionJournal.php new file mode 100644 index 0000000000..3355f458a8 --- /dev/null +++ b/app/lib/FireflyIII/Database/TransactionJournal.php @@ -0,0 +1,334 @@ +setUser(\Auth::user()); + } + + /** + * @param Carbon $date + * + * @return float + */ + public function getSumOfIncomesByMonth(Carbon $date) + { + $end = clone $date; + $date->startOfMonth(); + $end->endOfMonth(); + + $sum = \DB::table('transactions') + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id') + ->where('amount', '>', 0) + ->where('transaction_types.type', '=', 'Deposit') + ->where('transaction_journals.date', '>=', $date->format('Y-m-d')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d'))->sum('transactions.amount'); + $sum = floatval($sum); + return $sum; + } + + /** + * @param Carbon $date + * + * @return float + */ + public function getSumOfExpensesByMonth(Carbon $date) + { + $end = clone $date; + $date->startOfMonth(); + $end->endOfMonth(); + + $sum = \DB::table('transactions') + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id') + ->where('amount', '>', 0) + ->where('transaction_types.type', '=', 'Withdrawal') + ->where('transaction_journals.date', '>=', $date->format('Y-m-d')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d'))->sum('transactions.amount'); + $sum = floatval($sum); + return $sum; + } + + /** + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function getInDateRange(Carbon $start, Carbon $end) + { + return $this->getuser()->transactionjournals()->withRelevantData()->before($end)->after($start)->get(); + } + + /** + * @return TransactionJournal + */ + public function first() + { + return $this->getUser()->transactionjournals()->orderBy('date', 'ASC')->first(); + } + + + /** + * @param Ardent $model + * + * @return bool + */ + public function destroy(Ardent $model) + { + // TODO: Implement destroy() method. + } + + /** + * Validates a model. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param Ardent $model + * + * @return array + */ + public function validateObject(Ardent $model) + { + // TODO: Implement validateObject() method. + } + + /** + * Validates an array. Returns an array containing MessageBags + * errors/warnings/successes. + * + * @param array $model + * + * @return array + */ + public function validate(array $model) + { + + $warnings = new MessageBag; + $successes = new MessageBag; + $errors = new MessageBag; + + + if (!isset($model['what'])) { + $errors->add('description', 'Internal error: need to know type of transaction!'); + } + if (isset($model['recurring_transaction_id']) && intval($model['recurring_transaction_id']) < 0) { + $errors->add('recurring_transaction_id', 'Recurring transaction is invalid.'); + } + if (!isset($model['description'])) { + $errors->add('description', 'This field is mandatory.'); + } + if (isset($model['description']) && strlen($model['description']) == 0) { + $errors->add('description', 'This field is mandatory.'); + } + if (isset($model['description']) && strlen($model['description']) > 255) { + $errors->add('description', 'Description is too long.'); + } + + if (!isset($model['currency'])) { + $errors->add('description', 'Internal error: currency is mandatory!'); + } + if (isset($model['date']) && !($model['date'] instanceof Carbon) && strlen($model['date']) > 0) { + try { + new Carbon($model['date']); + } catch (\Exception $e) { + $errors->add('date', 'This date is invalid.'); + } + } + if (!isset($model['date'])) { + $errors->add('date', 'This date is invalid.'); + } + + if (isset($model['to_id']) && intval($model['to_id']) < 0) { + $errors->add('account_to', 'Invalid to-account'); + } + if (isset($model['from_id']) && intval($model['from_id']) < 0) { + $errors->add('account_from', 'Invalid from-account'); + } + if (isset($model['to']) && !($model['to'] instanceof \Account)) { + $errors->add('account_to', 'Invalid to-account'); + } + if (isset($model['from']) && !($model['from'] instanceof \Account)) { + $errors->add('account_from', 'Invalid from-account'); + } + if (!isset($model['amount']) || (isset($model['amount']) && floatval($model['amount']) < 0)) { + $errors->add('amount', 'Invalid amount'); + } + if (!isset($model['from']) && !isset($model['to'])) { + $errors->add('account_to', 'No accounts found!'); + } + + $validator = \Validator::make([$model], \Transaction::$rules); + if ($validator->invalid()) { + $errors->merge($errors); + } + + + /* + * Add "OK" + */ + if (!$errors->has('description')) { + $successes->add('description', 'OK'); + } + if (!$errors->has('date')) { + $successes->add('date', 'OK'); + } + return [ + 'errors' => $errors, + 'warnings' => $warnings, + 'successes' => $successes + ]; + + + } + + /** + * @param array $data + * + * @return Ardent + */ + public function store(array $data) + { + + /** @var \FireflyIII\Database\TransactionType $typeRepository */ + $typeRepository = \App::make('FireflyIII\Database\TransactionType'); + + /** @var \FireflyIII\Database\TransactionCurrency $currencyRepository */ + $currencyRepository = \App::make('FireflyIII\Database\TransactionCurrency'); + + /** @var \FireflyIII\Database\Transaction $transactionRepository */ + $transactionRepository = \App::make('FireflyIII\Database\Transaction'); + + $journalType = $typeRepository->findByWhat($data['what']); + $currency = $currencyRepository->findByCode($data['currency']); + + $journal = new \TransactionJournal; + $journal->transactionType()->associate($journalType); + $journal->transactionCurrency()->associate($currency); + $journal->user()->associate($this->getUser()); + $journal->description = $data['description']; + $journal->date = $data['date']; + $journal->completed = 0; + //$journal->user_id = $this->getUser()->id; + + /* + * This must be enough to store the journal: + */ + if (!$journal->validate()) { + \Log::error($journal->errors()->all()); + throw new FireflyException('store() transactionjournal failed, but it should not!'); + } + $journal->save(); + + /* + * Then store both transactions. + */ + $first = [ + 'account' => $data['from'], + 'transaction_journal' => $journal, + 'amount' => ($data['amount'] * -1), + ]; + $validate = $transactionRepository->validate($first); + if ($validate['errors']->count() == 0) { + $transactionRepository->store($first); + } else { + throw new FireflyException($validate['errors']->first()); + } + + $second = [ + 'account' => $data['to'], + 'transaction_journal' => $journal, + 'amount' => floatval($data['amount']), + ]; + + $validate = $transactionRepository->validate($second); + if ($validate['errors']->count() == 0) { + $transactionRepository->store($second); + } else { + throw new FireflyException($validate['errors']->first()); + } + + $journal->completed = 1; + $journal->save(); + return $journal; + } + + /** + * Returns an object with id $id. + * + * @param int $id + * + * @return Ardent + */ + public function find($id) + { + return $this->getUser()->transactionjournals()->find($id); + } + + /** + * Returns all objects. + * + * @return Collection + */ + public function get() + { + return $this->getUser()->transactionjournals()->get(); + } + + /** + * Finds an account type using one of the "$what"'s: expense, asset, revenue, opening, etc. + * + * @param $what + * + * @return \AccountType|null + */ + public function findByWhat($what) + { + // TODO: Implement findByWhat() method. + } + + /** + * @param array $ids + * + * @return Collection + */ + public function getByIds(array $ids) + { + // TODO: Implement getByIds() method. + } + + /** + * @param Ardent $model + * @param array $data + * + * @return bool + */ + public function update(Ardent $model, array $data) + { + // TODO: Implement update() method. + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Database/TransactionType.php b/app/lib/FireflyIII/Database/TransactionType.php new file mode 100644 index 0000000000..0022ec36fb --- /dev/null +++ b/app/lib/FireflyIII/Database/TransactionType.php @@ -0,0 +1,129 @@ +first(); + break; + default: + throw new FireflyException('Cannot find transaction type described as "' . e($what) . '".'); + break; + + } + return null; + } + + /** + * @param array $ids + * + * @return Collection + */ + public function getByIds(array $ids) + { + // TODO: Implement getByIds() method. + } + + /** + * @param Ardent $model + * @param array $data + * + * @return bool + */ + public function update(Ardent $model, array $data) + { + // TODO: Implement update() method. + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Exception/NotImplementedException.php b/app/lib/FireflyIII/Exception/NotImplementedException.php new file mode 100644 index 0000000000..208566204f --- /dev/null +++ b/app/lib/FireflyIII/Exception/NotImplementedException.php @@ -0,0 +1,13 @@ + intval(\Input::get('start')), + 'length' => $length, + 'draw' => intval(\Input::get('draw')), + ]; + + + /* + * Columns: + */ + if (!is_null(\Input::get('columns')) && is_array(\Input::get('columns'))) { + foreach (\Input::get('columns') as $column) { + $parameters['columns'][] = [ + 'data' => $column['data'], + 'name' => $column['name'], + 'searchable' => $column['searchable'] == 'true' ? true : false, + 'orderable' => $column['orderable'] == 'true' ? true : false, + 'search' => [ + 'value' => $column['search']['value'], + 'regex' => $column['search']['regex'] == 'true' ? true : false, + ] + ]; + } + } + + + /* + * Sorting. + */ + $parameters['orderOnAccount'] = false; + if (!is_null(\Input::get('order')) && is_array(\Input::get('order'))) { + foreach (\Input::get('order') as $order) { + $columnIndex = intval($order['column']); + $columnName = $parameters['columns'][$columnIndex]['name']; + $parameters['order'][] = [ + 'name' => $columnName, + 'dir' => strtoupper($order['dir']) + ]; + if ($columnName == 'to' || $columnName == 'from') { + $parameters['orderOnAccount'] = true; + } + } + } + /* + * Search parameters: + */ + $parameters['search'] = [ + 'value' => '', + 'regex' => false + ]; + if (!is_null(\Input::get('search')) && is_array(\Input::get('search'))) { + $search = \Input::get('search'); + $parameters['search'] = [ + 'value' => $search['value'], + 'regex' => $search['regex'] == 'true' ? true : false + ]; + } + return $parameters; + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Shared/Preferences/Preferences.php b/app/lib/FireflyIII/Shared/Preferences/Preferences.php new file mode 100644 index 0000000000..0fa5bfda7a --- /dev/null +++ b/app/lib/FireflyIII/Shared/Preferences/Preferences.php @@ -0,0 +1,58 @@ +id)->where('name', $name)->first(); + if (is_null($default) && is_null($pref)) { + // return NULL + return null; + } + if (!is_null($pref)) { + return $pref; + } + if (!is_null($default) && is_null($pref)) { + // create preference, return that: + return $this->set($name, $default); + } + + return null; + + } + + /** + * @param $name + * @param $value + * + * @return \Preference + */ + public function set($name, $value) + { + $pref = \Preference::where('user_id', \Auth::user()->id)->where('name', $name)->first(); + if (is_null($pref)) { + $pref = new \Preference; + $pref->name = $name; + $pref->user()->associate(\Auth::user()); + + } + $pref->data = $value; + $pref->save(); + + + return $pref; + + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Shared/Preferences/PreferencesInterface.php b/app/lib/FireflyIII/Shared/Preferences/PreferencesInterface.php new file mode 100644 index 0000000000..4e7389695f --- /dev/null +++ b/app/lib/FireflyIII/Shared/Preferences/PreferencesInterface.php @@ -0,0 +1,29 @@ +addDay(); + break; + case 'weekly': + $currentEnd->addWeek()->subDay(); + break; + case 'monthly': + $currentEnd->addMonth()->subDay(); + break; + case 'quarterly': + $currentEnd->addMonths(3)->subDay(); + break; + case 'half-year': + $currentEnd->addMonths(6)->subDay(); + break; + case 'yearly': + $currentEnd->addYear()->subDay(); + break; + } + } + + /** + * @param Carbon $date + * @param $repeatFreq + * @param $skip + * + * @return Carbon + * @throws FireflyException + */ + public function addPeriod(Carbon $date, $repeatFreq, $skip) + { + $add = ($skip + 1); + switch ($repeatFreq) { + default: + throw new FireflyException('Cannot do addPeriod for $repeat_freq ' . $repeatFreq); + break; + case 'daily': + $date->addDays($add); + break; + case 'weekly': + $date->addWeeks($add); + break; + case 'monthly': + $date->addMonths($add); + break; + case 'quarterly': + $months = $add * 3; + $date->addMonths($months); + break; + case 'half-year': + $months = $add * 6; + $date->addMonths($months); + break; + case 'yearly': + $date->addYears($add); + break; + } + return $date; + } +} \ No newline at end of file diff --git a/app/lib/FireflyIII/Shared/Toolkit/Form.php b/app/lib/FireflyIII/Shared/Toolkit/Form.php new file mode 100644 index 0000000000..7383a0e7a2 --- /dev/null +++ b/app/lib/FireflyIII/Shared/Toolkit/Form.php @@ -0,0 +1,50 @@ +id); + $title = null; + if (is_null($titleField)) { + // try 'title' field. + if (isset($entry->title)) { + $title = $entry->title; + } + // try 'name' field + if (is_null($title)) { + $title = $entry->name; + } + + // try 'description' field + if (is_null($title)) { + $title = $entry->description; + } + } else { + $title = $entry->$titleField; + } + $selectList[$id] = $title; + } + return $selectList; + } + +} \ No newline at end of file diff --git a/app/models/Limit.php b/app/models/Limit.php index 5c11896933..49571e2d25 100644 --- a/app/models/Limit.php +++ b/app/models/Limit.php @@ -89,9 +89,9 @@ class Limit extends Ardent break; } $end->subDay(); - $count = $this->limitrepetitions()->where('startdate', $start->format('Y-m-d'))->where( - 'enddate', $start->format('Y-m-d') - )->count(); + $count = $this->limitrepetitions()->where('startdate', $start->format('Y-m-d'))->where('enddate', $end->format('Y-m-d'))->count(); + \Log::debug('All: '.$this->limitrepetitions()->count().' (#'.$this->id.')'); + \Log::debug('Found ' . $count.' limit-reps for limit #' . $this->id.' with start '.$start->format('Y-m-d') .' and end ' . $end->format('Y-m-d')); if ($count == 0) { @@ -115,6 +115,12 @@ class Limit extends Ardent if (isset($repetition->id)) { \Event::fire('limits.repetition', [$repetition]); } + } else if($count == 1) { + // update this one: + $repetition = $this->limitrepetitions()->where('startdate', $start->format('Y-m-d'))->where('enddate', $end->format('Y-m-d'))->first(); + $repetition->amount = $this->amount; + $repetition->save(); + } } diff --git a/app/models/LimitRepetition.php b/app/models/LimitRepetition.php index 943512aed4..0c4188bbb7 100644 --- a/app/models/LimitRepetition.php +++ b/app/models/LimitRepetition.php @@ -5,14 +5,14 @@ use LaravelBook\Ardent\Ardent as Ardent; /** * LimitRepetition * - * @property integer $id + * @property integer $id * @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $updated_at - * @property integer $limit_id + * @property integer $limit_id * @property \Carbon\Carbon $startdate * @property \Carbon\Carbon $enddate - * @property float $amount - * @property-read \Limit $limit + * @property float $amount + * @property-read \Limit $limit * @method static \Illuminate\Database\Query\Builder|\LimitRepetition whereId($value) * @method static \Illuminate\Database\Query\Builder|\LimitRepetition whereCreatedAt($value) * @method static \Illuminate\Database\Query\Builder|\LimitRepetition whereUpdatedAt($value) @@ -39,29 +39,27 @@ class LimitRepetition extends Ardent return ['created_at', 'updated_at', 'startdate', 'enddate']; } + public function spentInRepetition() { + $sum = \DB::table('transactions') + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->leftJoin('component_transaction_journal', 'component_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('components', 'components.id', '=', 'component_transaction_journal.component_id') + ->leftJoin('limits', 'limits.component_id', '=', 'components.id') + ->leftJoin('limit_repetitions', 'limit_repetitions.limit_id', '=', 'limits.id') + ->where('transaction_journals.date', '>=', $this->startdate->format('Y-m-d')) + ->where('transaction_journals.date', '<=', $this->enddate->format('Y-m-d')) + ->where('transactions.amount', '>', 0) + ->where('limit_repetitions.id', '=', $this->id)->sum('transactions.amount'); + return floatval($sum); + } + /** * How much money is left in this? */ public function leftInRepetition() { - $left = floatval($this->amount); + return floatval($this->amount - $this->spentInRepetition()); - // 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); - } - } - } - - return $left; } /** @@ -85,8 +83,10 @@ class LimitRepetition extends Ardent } switch ($this->repeat_freq) { default: - throw new \Firefly\Exception\FireflyException('No date formats for frequency "' . $this->repeat_freq - . '"!'); + throw new \Firefly\Exception\FireflyException( + 'No date formats for frequency "' . $this->repeat_freq + . '"!' + ); break; case 'daily': return $this->startdate->format('Ymd') . '-5'; @@ -119,8 +119,10 @@ class LimitRepetition extends Ardent } switch ($this->repeat_freq) { default: - throw new \Firefly\Exception\FireflyException('No date formats for frequency "' . $this->repeat_freq - . '"!'); + throw new \Firefly\Exception\FireflyException( + 'No date formats for frequency "' . $this->repeat_freq + . '"!' + ); break; case 'daily': return $this->startdate->format('j F Y'); diff --git a/app/models/Piggybank.php b/app/models/Piggybank.php index b754e133f2..143f900543 100644 --- a/app/models/Piggybank.php +++ b/app/models/Piggybank.php @@ -56,6 +56,7 @@ class Piggybank extends Ardent 'rep_times' => 'min:1|max:100', // how many times do you want to save this amount? eg. 3 times 'reminder' => 'in:day,week,month,year', // want a reminder to put money in this? 'reminder_skip' => 'required|min:0|max:100', // every week? every 2 months? + 'remind_me' => 'required|boolean', 'order' => 'required:min:1', // not yet used. ]; public $fillable @@ -71,6 +72,7 @@ class Piggybank extends Ardent 'rep_times', 'reminder', 'reminder_skip', + 'remind_me', 'order' ]; @@ -90,7 +92,6 @@ class Piggybank extends Ardent $rep->targetdate = $target; $rep->currentamount = 0; $rep->save(); - \Event::fire('piggybanks.repetition', [$rep]); return $rep; diff --git a/app/models/TransactionJournal.php b/app/models/TransactionJournal.php index 41c8f79805..fb760e6782 100644 --- a/app/models/TransactionJournal.php +++ b/app/models/TransactionJournal.php @@ -4,242 +4,6 @@ use Carbon\Carbon; use LaravelBook\Ardent\Ardent; use LaravelBook\Ardent\Builder; -/** - * TransactionJournal - * - * @property integer $id - * @property \Carbon\Carbon $created_at - * @property \Carbon\Carbon $updated_at - * @property integer $user_id - * @property integer $transaction_type_id - * @property integer $transaction_currency_id - * @property string $description - * @property boolean $completed - * @property \Carbon\Carbon $date - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\Component[] $components - * @property-read \TransactionCurrency $transactionCurrency - * @property-read \TransactionType $transactionType - * @property-read \Illuminate\Database\Eloquent\Collection|\Transaction[] $transactions - * @property-read \User $user - * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereId($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereCreatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereUpdatedAt($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereUserId($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereTransactionTypeId($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereTransactionCurrencyId($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereDescription($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereCompleted($value) - * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereDate($value) - * @method static \TransactionJournal account($account) - * @method static \TransactionJournal after($date) - * @method static \TransactionJournal before($date) - * @method static \TransactionJournal defaultSorting() - * @method static \TransactionJournal moreThan($amount) - * @method static \TransactionJournal lessThan($amount) - * @method static \TransactionJournal onDate($date) - * @method static \TransactionJournal transactionTypes($types) - * @method static \TransactionJournal withRelevantData() - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property integer $recurring_transaction_id - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \RecurringTransaction $recurringTransaction - * @method static \Illuminate\Database\Query\Builder|\TransactionJournal whereRecurringTransactionId($value) - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @method static \TransactionJournal accountIs($account) - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Budget[] $budgets - * @property-read \Illuminate\Database\Eloquent\Collection|\ - * 'Category[] $categories - */ class TransactionJournal extends Ardent { @@ -259,7 +23,7 @@ class TransactionJournal extends Ardent public function budgets() { return $this->belongsToMany( - 'Budget', 'component_transaction_journal', 'transaction_journal_id', 'component_id' + 'Budget', 'component_transaction_journal', 'transaction_journal_id', 'component_id' ); } @@ -269,7 +33,7 @@ class TransactionJournal extends Ardent public function categories() { return $this->belongsToMany( - 'Category', 'component_transaction_journal', 'transaction_journal_id', 'component_id' + 'Category', 'component_transaction_journal', 'transaction_journal_id', 'component_id' ); } @@ -281,6 +45,20 @@ class TransactionJournal extends Ardent return $this->belongsToMany('Component'); } + /** + * @return float + */ + public function getAmount() + { + + foreach ($this->transactions as $t) { + if (floatval($t->amount) > 0) { + return floatval($t->amount); + } + } + return -0.01; + } + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ @@ -312,7 +90,7 @@ class TransactionJournal extends Ardent /** * @param $query - * @param Carbon $date + * @param Carbon $date * * @return mixed */ @@ -323,7 +101,7 @@ class TransactionJournal extends Ardent /** * @param $query - * @param Carbon $date + * @param Carbon $date * * @return mixed */ @@ -340,8 +118,10 @@ class TransactionJournal extends Ardent public function scopeMoreThan(Builder $query, $amount) { if (is_null($this->joinedTransactions)) { - $query->leftJoin('transactions', 'transactions.transaction_journal_id', '=', - 'transaction_journals.id'); + $query->leftJoin( + 'transactions', 'transactions.transaction_journal_id', '=', + 'transaction_journals.id' + ); $this->joinedTransactions = true; } @@ -351,8 +131,10 @@ class TransactionJournal extends Ardent public function scopeLessThan(Builder $query, $amount) { if (is_null($this->joinedTransactions)) { - $query->leftJoin('transactions', 'transactions.transaction_journal_id', '=', - 'transaction_journals.id'); + $query->leftJoin( + 'transactions', 'transactions.transaction_journal_id', '=', + 'transaction_journals.id' + ); $this->joinedTransactions = true; } @@ -373,8 +155,10 @@ class TransactionJournal extends Ardent public function scopeTransactionTypes(Builder $query, array $types) { if (is_null($this->joinedTransactionTypes)) { - $query->leftJoin('transaction_types', 'transaction_types.id', '=', - 'transaction_journals.transaction_type_id'); + $query->leftJoin( + 'transaction_types', 'transaction_types.id', '=', + 'transaction_journals.transaction_type_id' + ); $this->joinedTransactionTypes = true; } $query->whereIn('transaction_types.type', $types); @@ -389,11 +173,11 @@ class TransactionJournal extends Ardent public function scopeWithRelevantData(Builder $query) { $query->with( - ['transactions' => function ($q) { - $q->orderBy('amount', 'ASC'); - }, 'transactiontype', 'components' => function ($q) { - $q->orderBy('class'); - }, 'transactions.account.accounttype','recurringTransaction'] + ['transactions' => function ($q) { + $q->orderBy('amount', 'ASC'); + }, 'transactiontype', 'components' => function ($q) { + $q->orderBy('class'); + }, 'transactions.account.accounttype', 'recurringTransaction'] ); } diff --git a/app/routes.php b/app/routes.php index 535b161678..4d85b37703 100644 --- a/app/routes.php +++ b/app/routes.php @@ -3,174 +3,183 @@ //use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - // models: -Route::bind('account', function($value, $route) - { - if(Auth::check()) { +Route::bind( + 'account', function ($value, $route) { + if (Auth::check()) { $account = Account:: - leftJoin('account_types','account_types.id','=','accounts.account_type_id')-> - where('account_types.editable',1)-> - where('accounts.id', $value)-> - where('user_id',Auth::user()->id)-> - first(['accounts.*']); - if($account) { + leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')->where('account_types.editable', 1)->where('accounts.id', $value) + ->where('user_id', Auth::user()->id)->first(['accounts.*']); + if ($account) { return $account; } } App::abort(404); - }); - -Route::bind('accountname', function($value, $route) - { - if(Auth::check()) { - return Account:: - leftJoin('account_types','account_types.id','=','accounts.account_type_id')-> - where('account_types.editable',1)-> - where('name', $value)-> - where('user_id',Auth::user()->id)->first(); - } - 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()) { - return Budget:: - where('id', $value)-> - where('user_id',Auth::user()->id)->first(); - } - return null; - }); - -Route::bind('reminder', function($value, $route) - { - if(Auth::check()) { - return Reminder:: - where('id', $value)-> - where('user_id',Auth::user()->id)->first(); - } - return null; - }); - -Route::bind('category', function($value, $route) -{ - if(Auth::check()) { - return Category:: - where('id', $value)-> - where('user_id',Auth::user()->id)->first(); } - return null; -}); +); -Route::bind('tj', function($value, $route) - { - if(Auth::check()) { +Route::bind( + 'accountname', function ($value, $route) { + if (Auth::check()) { + return Account:: + leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')->where('account_types.editable', 1)->where('name', $value)->where( + 'user_id', Auth::user()->id + )->first(); + } + 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()) { + return Budget:: + where('id', $value)->where('user_id', Auth::user()->id)->first(); + } + return null; + } +); + +Route::bind( + 'reminder', function ($value, $route) { + if (Auth::check()) { + return Reminder:: + where('id', $value)->where('user_id', Auth::user()->id)->first(); + } + return null; + } +); + +Route::bind( + 'category', function ($value, $route) { + if (Auth::check()) { + return Category:: + where('id', $value)->where('user_id', Auth::user()->id)->first(); + } + return null; + } +); + +Route::bind( + 'tj', function ($value, $route) { + if (Auth::check()) { return TransactionJournal:: - where('id', $value)-> - where('user_id',Auth::user()->id)->first(); + where('id', $value)->where('user_id', Auth::user()->id)->first(); } return null; - }); + } +); -Route::bind('limit', function($value, $route) - { - if(Auth::check()) { +Route::bind( + 'limit', function ($value, $route) { + if (Auth::check()) { return Limit:: - where('limits.id', $value)-> - leftJoin('components','components.id','=','limits.component_id')-> - where('components.class','Budget')-> - where('components.user_id',Auth::user()->id)->first(['limits.*']); + where('limits.id', $value)->leftJoin('components', 'components.id', '=', 'limits.component_id')->where('components.class', 'Budget')->where( + 'components.user_id', Auth::user()->id + )->first(['limits.*']); } return null; - }); + } +); -Route::bind('limitrepetition', function($value, $route) - { - if(Auth::check()) { +Route::bind( + 'limitrepetition', function ($value, $route) { + if (Auth::check()) { return LimitRepetition:: - where('limit_repetitions.id', $value)-> - leftjoin('limits','limits.id','=','limit_repetitions.limit_id')-> - leftJoin('components','components.id','=','limits.component_id')-> - where('components.class','Budget')-> - where('components.user_id',Auth::user()->id)->first(['limit_repetitions.*']); + where('limit_repetitions.id', $value)->leftjoin('limits', 'limits.id', '=', 'limit_repetitions.limit_id')->leftJoin( + 'components', 'components.id', '=', 'limits.component_id' + )->where('components.class', 'Budget')->where('components.user_id', Auth::user()->id)->first(['limit_repetitions.*']); } return null; - }); + } +); -Route::bind('piggybank', function($value, $route) - { - if(Auth::check()) { +Route::bind( + 'piggybank', function ($value, $route) { + if (Auth::check()) { return Piggybank:: - where('piggybanks.id', $value)-> - leftJoin('accounts','accounts.id','=','piggybanks.account_id')-> - where('accounts.user_id',Auth::user()->id)->first(['piggybanks.*']); + where('piggybanks.id', $value)->leftJoin('accounts', 'accounts.id', '=', 'piggybanks.account_id')->where('accounts.user_id', Auth::user()->id) + ->first(['piggybanks.*']); } return null; - }); - + } +); // a development route: Route::get('/dev', ['uses' => 'HomeController@jobDev']); // protected routes: -Route::group(['before' => 'auth'], function () { - - +Route::group( + ['before' => 'auth'], function () { // some date routes: - Route::get('/prev',['uses' => 'HomeController@sessionPrev', 'as' => 'sessionPrev']); - Route::get('/next',['uses' => 'HomeController@sessionNext', 'as' => 'sessionNext']); - Route::get('/jump/{range}',['uses' => 'HomeController@rangeJump','as' => 'rangeJump']); + Route::get('/prev', ['uses' => 'HomeController@sessionPrev', 'as' => 'sessionPrev']); + Route::get('/next', ['uses' => 'HomeController@sessionNext', 'as' => 'sessionNext']); + Route::get('/jump/{range}', ['uses' => 'HomeController@rangeJump', 'as' => 'rangeJump']); + Route::get('/cleanup', ['uses' => 'HomeController@cleanup', 'as' => 'cleanup']); // account controller: - Route::get('/accounts', ['uses' => 'AccountController@index', 'as' => 'accounts.index']); - Route::get('/accounts/asset', ['uses' => 'AccountController@asset', 'as' => 'accounts.asset']); - Route::get('/accounts/expense', ['uses' => 'AccountController@expense', 'as' => 'accounts.expense']); - Route::get('/accounts/revenue', ['uses' => 'AccountController@revenue', 'as' => 'accounts.revenue']); - - Route::get('/accounts/create/{what}', ['uses' => 'AccountController@create', 'as' => 'accounts.create'])->where('what','revenue|asset|expense'); - Route::get('/accounts/{account}', ['uses' => 'AccountController@show', 'as' => 'accounts.show']); - Route::get('/accounts/{account}/edit', ['uses' => 'AccountController@edit', 'as' => 'accounts.edit']); - Route::get('/accounts/{account}/delete', ['uses' => 'AccountController@delete', 'as' => 'accounts.delete']); + Route::get('/accounts/json/{what}', ['uses' => 'AccountController@json', 'as' => 'accounts.json'])->where('what', 'revenue|asset|expense'); + Route::get('/accounts/{what}', ['uses' => 'AccountController@index', 'as' => 'accounts.index'])->where('what', 'revenue|asset|expense'); + Route::get('/accounts/create/{what}', ['uses' => 'AccountController@create', 'as' => 'accounts.create'])->where('what', 'revenue|asset|expense'); + Route::get('/accounts/edit/{account}', ['uses' => 'AccountController@edit', 'as' => 'accounts.edit']); + Route::get('/accounts/delete/{account}', ['uses' => 'AccountController@delete', 'as' => 'accounts.delete']); + Route::get('/accounts/show/{account}', ['uses' => 'AccountController@show', 'as' => 'accounts.show']); // budget controller: - Route::get('/budgets/date',['uses' => 'BudgetController@indexByDate','as' => 'budgets.index.date']); - Route::get('/budgets/budget',['uses' => 'BudgetController@indexByBudget','as' => 'budgets.index.budget']); - Route::get('/budgets/create',['uses' => 'BudgetController@create', 'as' => 'budgets.create']); + Route::get('/budgets', ['uses' => 'BudgetController@index', 'as' => 'budgets.index']); + Route::get('/budgets/income', ['uses' => 'BudgetController@updateIncome', 'as' => 'budgets.income']); + Route::get('/budgets/show/{budget}/{limitrepetition?}', ['uses' => 'BudgetController@show', 'as' => 'budgets.show']); - Route::get('/budgets/nobudget/{period}',['uses' => 'BudgetController@nobudget', 'as' => 'budgets.nobudget']); + #Route::get('/budgets/date', ['uses' => 'BudgetController@indexByDate', 'as' => 'budgets.index.date']); + #Route::get('/budgets/budget', ['uses' => 'BudgetController@indexByBudget', 'as' => 'budgets.index.budget']); + Route::get('/budgets/create', ['uses' => 'BudgetController@create', 'as' => 'budgets.create']); + #Route::get('/budgets/nobudget/{period}', ['uses' => 'BudgetController@nobudget', 'as' => 'budgets.nobudget']); - Route::get('/budgets/show/{budget}/{limitrepetition?}',['uses' => 'BudgetController@show', 'as' => 'budgets.show']); - Route::get('/budgets/edit/{budget}',['uses' => 'BudgetController@edit', 'as' => 'budgets.edit']); - Route::get('/budgets/delete/{budget}',['uses' => 'BudgetController@delete', 'as' => 'budgets.delete']); + Route::get('/budgets/edit/{budget}', ['uses' => 'BudgetController@edit', 'as' => 'budgets.edit']); + Route::get('/budgets/delete/{budget}', ['uses' => 'BudgetController@delete', 'as' => 'budgets.delete']); // category controller: - Route::get('/categories',['uses' => 'CategoryController@index','as' => 'categories.index']); - Route::get('/categories/create',['uses' => 'CategoryController@create','as' => 'categories.create']); - Route::get('/categories/show/{category}',['uses' => 'CategoryController@show','as' => 'categories.show']); - Route::get('/categories/edit/{category}',['uses' => 'CategoryController@edit','as' => 'categories.edit']); - Route::get('/categories/delete/{category}',['uses' => 'CategoryController@delete','as' => 'categories.delete']); + Route::get('/categories', ['uses' => 'CategoryController@index', 'as' => 'categories.index']); + Route::get('/categories/create', ['uses' => 'CategoryController@create', 'as' => 'categories.create']); + Route::get('/categories/show/{category}', ['uses' => 'CategoryController@show', 'as' => 'categories.show']); + Route::get('/categories/edit/{category}', ['uses' => 'CategoryController@edit', 'as' => 'categories.edit']); + Route::get('/categories/delete/{category}', ['uses' => 'CategoryController@delete', 'as' => 'categories.delete']); + + // google chart controller + Route::get('/chart/home/account', ['uses' => 'GoogleChartController@allAccountsBalanceChart']); + Route::get('/chart/home/budgets', ['uses' => 'GoogleChartController@allBudgetsHomeChart']); + Route::get('/chart/home/categories', ['uses' => 'GoogleChartController@allCategoriesHomeChart']); + Route::get('/chart/home/recurring', ['uses' => 'GoogleChartController@recurringTransactionsOverview']); + Route::get('/chart/account/{account}', ['uses' => 'GoogleChartController@accountBalanceChart']); + Route::get('/chart/sankey/{account}/out', ['uses' => 'GoogleChartController@accountSankeyOutChart']); + Route::get('/chart/sankey/{account}/in', ['uses' => 'GoogleChartController@accountSankeyInChart']); + Route::get('/chart/reports/income-expenses/{year}', ['uses' => 'GoogleChartController@yearInExp']); + Route::get('/chart/reports/income-expenses-sum/{year}', ['uses' => 'GoogleChartController@yearInExpSum']); + Route::get('/chart/reports/budgets/{year}', ['uses' => 'GoogleChartController@budgetsReportChart']); + Route::get('/chart/budgets/{budget}/spending/{year}', ['uses' => 'GoogleChartController@budgetsAndSpending']); + + // google table controller + Route::get('/table/account/{account}/transactions', ['uses' => 'GoogleTableController@transactionsByAccount']); + Route::get('/table/accounts/{what}', ['uses' => 'GoogleTableController@accountList']); + Route::get('/table/budget/{budget}/{limitrepetition?}/transactions', ['uses' => 'GoogleTableController@transactionsByBudget']); + - // chart controller - Route::get('/chart/home/account/{account?}', ['uses' => 'ChartController@homeAccount', 'as' => 'chart.home']); - Route::get('/chart/home/categories', ['uses' => 'ChartController@homeCategories', 'as' => 'chart.categories']); - Route::get('/chart/home/budgets', ['uses' => 'ChartController@homeBudgets', 'as' => 'chart.budgets']); Route::get('/chart/home/info/{accountnameA}/{day}/{month}/{year}', ['uses' => 'ChartController@homeAccountInfo', 'as' => 'chart.info']); - Route::get('/chart/categories/show/{category}', ['uses' => 'ChartController@categoryShowChart','as' => 'chart.showcategory']); - Route::get('/chart/home/recurring', ['uses' => 'ChartController@homeRecurring', 'as' => 'chart.recurring']); + Route::get('/chart/categories/show/{category}', ['uses' => 'ChartController@categoryShowChart', 'as' => 'chart.showcategory']); + // (new charts for budgets) Route::get('/chart/budget/{budget}/default', ['uses' => 'ChartController@budgetDefault', 'as' => 'chart.budget.default']); Route::get('chart/budget/{budget}/no_envelope', ['uses' => 'ChartController@budgetNoLimits', 'as' => 'chart.budget.nolimit']); @@ -192,96 +201,111 @@ Route::group(['before' => 'auth'], function () { Route::get('/json/recurringjournals/{recurring}', ['uses' => 'JsonController@recurringjournals', 'as' => 'json.recurringjournals']); // limit controller: - Route::get('/budgets/limits/create/{budget?}',['uses' => 'LimitController@create','as' => 'budgets.limits.create']); - Route::get('/budgets/limits/delete/{limit}',['uses' => 'LimitController@delete','as' => 'budgets.limits.delete']); - Route::get('/budgets/limits/edit/{limit}',['uses' => 'LimitController@edit','as' => 'budgets.limits.edit']); + Route::get('/budgets/limits/create/{budget?}', ['uses' => 'LimitController@create', 'as' => 'budgets.limits.create']); + Route::get('/budgets/limits/delete/{limit}', ['uses' => 'LimitController@delete', 'as' => 'budgets.limits.delete']); + Route::get('/budgets/limits/edit/{limit}', ['uses' => 'LimitController@edit', 'as' => 'budgets.limits.edit']); - Route::get('/migrate',['uses' => 'MigrateController@index', 'as' => 'migrate.index']); + Route::get('/migrate', ['uses' => 'MigrateController@index', 'as' => 'migrate.index']); // piggy bank controller - Route::get('/piggybanks',['uses' => 'PiggybankController@piggybanks','as' => 'piggybanks.index.piggybanks']); - Route::get('/repeated',['uses' => 'PiggybankController@repeated','as' => 'piggybanks.index.repeated']); - Route::get('/piggybanks/create/piggybank', ['uses' => 'PiggybankController@createPiggybank','as' => 'piggybanks.create.piggybank']); - Route::get('/piggybanks/create/repeated', ['uses' => 'PiggybankController@createRepeated','as' => 'piggybanks.create.repeated']); - Route::get('/piggybanks/addMoney/{piggybank}', ['uses' => 'PiggybankController@addMoney','as' => 'piggybanks.amount.add']); - Route::get('/piggybanks/removeMoney/{piggybank}', ['uses' => 'PiggybankController@removeMoney','as' => 'piggybanks.amount.remove']); - Route::get('/piggybanks/show/{piggybank}', ['uses' => 'PiggybankController@show','as' => 'piggybanks.show']); - Route::get('/piggybanks/edit/{piggybank}', ['uses' => 'PiggybankController@edit','as' => 'piggybanks.edit']); - Route::get('/piggybanks/delete/{piggybank}', ['uses' => 'PiggybankController@delete','as' => 'piggybanks.delete']); - Route::post('/piggybanks/updateAmount/{piggybank}',['uses' => 'PiggybankController@updateAmount','as' => 'piggybanks.updateAmount']); + Route::get('/piggybanks', ['uses' => 'PiggybankController@index', 'as' => 'piggybanks.index']); + Route::get('/piggybanks/add/{piggybank}', ['uses' => 'PiggybankController@add']); + Route::get('/piggybanks/remove/{piggybank}', ['uses' => 'PiggybankController@remove']); + Route::get('/piggybanks/edit/{piggybank}', ['uses' => 'PiggybankController@edit', 'as' => 'piggybanks.edit']); + Route::get('/piggybanks/create', ['uses' => 'PiggybankController@create', 'as' => 'piggybanks.create']); + Route::get('/piggybanks/delete/{piggybank}', ['uses' => 'PiggybankController@delete', 'as' => 'piggybanks.delete']); + + +// Route::get('/repeated',['uses' => 'PiggybankController@repeated','as' => 'piggybanks.index.repeated']); +// Route::get('/piggybanks/create/repeated', ['uses' => 'PiggybankController@createRepeated','as' => 'piggybanks.create.repeated']); +// Route::get('/piggybanks/addMoney/{piggybank}', ['uses' => 'PiggybankController@addMoney','as' => 'piggybanks.amount.add']); +// Route::get('/piggybanks/removeMoney/{piggybank}', ['uses' => 'PiggybankController@removeMoney','as' => 'piggybanks.amount.remove']); +// Route::get('/piggybanks/show/{piggybank}', ['uses' => 'PiggybankController@show','as' => 'piggybanks.show']); +// Route::get('/piggybanks/delete/{piggybank}', ['uses' => 'PiggybankController@delete','as' => 'piggybanks.delete']); +// Route::post('/piggybanks/updateAmount/{piggybank}',['uses' => 'PiggybankController@updateAmount','as' => 'piggybanks.updateAmount']); // preferences controller Route::get('/preferences', ['uses' => 'PreferencesController@index', 'as' => 'preferences']); //profile controller Route::get('/profile', ['uses' => 'ProfileController@index', 'as' => 'profile']); - Route::get('/profile/change-password',['uses' => 'ProfileController@changePassword', 'as' => 'change-password']); + Route::get('/profile/change-password', ['uses' => 'ProfileController@changePassword', 'as' => 'change-password']); // recurring transactions controller - Route::get('/recurring',['uses' => 'RecurringController@index', 'as' => 'recurring.index']); - Route::get('/recurring/show/{recurring}',['uses' => 'RecurringController@show', 'as' => 'recurring.show']); - Route::get('/recurring/rescan/{recurring}',['uses' => 'RecurringController@rescan', 'as' => 'recurring.rescan']); - 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']); + Route::get('/recurring', ['uses' => 'RecurringController@index', 'as' => 'recurring.index']); + Route::get('/recurring/show/{recurring}', ['uses' => 'RecurringController@show', 'as' => 'recurring.show']); + Route::get('/recurring/rescan/{recurring}', ['uses' => 'RecurringController@rescan', 'as' => 'recurring.rescan']); + 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']); // report controller: - Route::get('/reports',['uses' => 'ReportController@index','as' => 'reports.index']); + Route::get('/reports', ['uses' => 'ReportController@index', 'as' => 'reports.index']); + Route::get('/reports/{year}', ['uses' => 'ReportController@year', 'as' => 'reports.year']); // search controller: - Route::get('/search',['uses' => 'SearchController@index','as' => 'search']); + Route::get('/search', ['uses' => 'SearchController@index', 'as' => 'search']); // transaction controller: - Route::get('/transactions/create/{what}', ['uses' => 'TransactionController@create', 'as' => 'transactions.create'])->where(['what' => 'withdrawal|deposit|transfer']); - Route::get('/transaction/show/{tj}',['uses' => 'TransactionController@show','as' => 'transactions.show']); - Route::get('/transaction/edit/{tj}',['uses' => 'TransactionController@edit','as' => 'transactions.edit']); - Route::get('/transaction/delete/{tj}',['uses' => 'TransactionController@delete','as' => 'transactions.delete']); - Route::get('/transactions/index',['uses' => 'TransactionController@index','as' => 'transactions.index']); - Route::get('/transactions/expenses',['uses' => 'TransactionController@expenses','as' => 'transactions.expenses']); - Route::get('/transactions/revenue',['uses' => 'TransactionController@revenue','as' => 'transactions.revenue']); - Route::get('/transactions/transfers',['uses' => 'TransactionController@transfers','as' => 'transactions.transfers']); + Route::get('/transactions/create/{what}', ['uses' => 'TransactionController@create', 'as' => 'transactions.create'])->where( + ['what' => 'withdrawal|deposit|transfer'] + ); + Route::get('/transaction/show/{tj}', ['uses' => 'TransactionController@show', 'as' => 'transactions.show']); + Route::get('/transaction/edit/{tj}', ['uses' => 'TransactionController@edit', 'as' => 'transactions.edit']); + Route::get('/transaction/delete/{tj}', ['uses' => 'TransactionController@delete', 'as' => 'transactions.delete']); + Route::get('/transactions/index', ['uses' => 'TransactionController@index', 'as' => 'transactions.index']); + Route::get('/transactions/expenses', ['uses' => 'TransactionController@expenses', 'as' => 'transactions.expenses']); + Route::get('/transactions/revenue', ['uses' => 'TransactionController@revenue', 'as' => 'transactions.revenue']); + Route::get('/transactions/transfers', ['uses' => 'TransactionController@transfers', 'as' => 'transactions.transfers']); - Route::get('/transactions/expenses',['uses' => 'TransactionController@expenses','as' => 'transactions.index.withdrawal']); - Route::get('/transactions/revenue',['uses' => 'TransactionController@revenue','as' => 'transactions.index.deposit']); - Route::get('/transactions/transfers',['uses' => 'TransactionController@transfers','as' => 'transactions.index.transfer']); + Route::get('/transactions/expenses', ['uses' => 'TransactionController@expenses', 'as' => 'transactions.index.withdrawal']); + Route::get('/transactions/revenue', ['uses' => 'TransactionController@revenue', 'as' => 'transactions.index.deposit']); + Route::get('/transactions/transfers', ['uses' => 'TransactionController@transfers', 'as' => 'transactions.index.transfer']); // user controller Route::get('/logout', ['uses' => 'UserController@logout', 'as' => 'logout']); + Route::post('budgets/amount/{budget}', ['uses' => 'BudgetController@amount']); + + } ); // protected + csrf routes (POST) -Route::group(['before' => 'csrf|auth'], function () { +Route::group( + ['before' => 'csrf|auth'], function () { // account controller: Route::post('/accounts/store', ['uses' => 'AccountController@store', 'as' => 'accounts.store']); Route::post('/accounts/update/{account}', ['uses' => 'AccountController@update', 'as' => 'accounts.update']); Route::post('/accounts/destroy/{account}', ['uses' => 'AccountController@destroy', 'as' => 'accounts.destroy']); // budget controller: - Route::post('/budgets/store',['uses' => 'BudgetController@store', 'as' => 'budgets.store']); + Route::post('/budgets/income', ['uses' => 'BudgetController@postUpdateIncome', 'as' => 'budgets.postIncome']); Route::post('/budgets/update/{budget}', ['uses' => 'BudgetController@update', 'as' => 'budgets.update']); + Route::post('/budgets/store', ['uses' => 'BudgetController@store', 'as' => 'budgets.store']); Route::post('/budgets/destroy/{budget}', ['uses' => 'BudgetController@destroy', 'as' => 'budgets.destroy']); // category controller - Route::post('/categories/store',['uses' => 'CategoryController@store', 'as' => 'categories.store']); + Route::post('/categories/store', ['uses' => 'CategoryController@store', 'as' => 'categories.store']); Route::post('/categories/update/{category}', ['uses' => 'CategoryController@update', 'as' => 'categories.update']); Route::post('/categories/destroy/{category}', ['uses' => 'CategoryController@destroy', 'as' => 'categories.destroy']); // limit controller: Route::post('/budgets/limits/store/{budget?}', ['uses' => 'LimitController@store', 'as' => 'budgets.limits.store']); - Route::post('/budgets/limits/destroy/{limit}',['uses' => 'LimitController@destroy','as' => 'budgets.limits.destroy']); - Route::post('/budgets/limits/update/{limit}',['uses' => 'LimitController@update','as' => 'budgets.limits.update']); + Route::post('/budgets/limits/destroy/{limit}', ['uses' => 'LimitController@destroy', 'as' => 'budgets.limits.destroy']); + Route::post('/budgets/limits/update/{limit}', ['uses' => 'LimitController@update', 'as' => 'budgets.limits.update']); - Route::post('/migrate/upload',['uses' => 'MigrateController@upload', 'as' => 'migrate.upload']); + Route::post('/migrate/upload', ['uses' => 'MigrateController@upload', 'as' => 'migrate.upload']); // piggy bank controller - Route::post('/piggybanks/store/piggybank',['uses' => 'PiggybankController@storePiggybank','as' => 'piggybanks.store.piggybank']); - Route::post('/piggybanks/store/repeated',['uses' => 'PiggybankController@storeRepeated','as' => 'piggybanks.store.repeated']); - Route::post('/piggybanks/update/{piggybank}', ['uses' => 'PiggybankController@update','as' => 'piggybanks.update']); - Route::post('/piggybanks/destroy/{piggybank}', ['uses' => 'PiggybankController@destroy','as' => 'piggybanks.destroy']); - Route::post('/piggybanks/mod/{piggybank}', ['uses' => 'PiggybankController@modMoney','as' => 'piggybanks.modMoney']); + Route::post('/piggybanks/store', ['uses' => 'PiggybankController@store', 'as' => 'piggybanks.store']); + #Route::post('/piggybanks/store/repeated', ['uses' => 'PiggybankController@storeRepeated', 'as' => 'piggybanks.store.repeated']); + Route::post('/piggybanks/update/{piggybank}', ['uses' => 'PiggybankController@update', 'as' => 'piggybanks.update']); + Route::post('/piggybanks/destroy/{piggybank}', ['uses' => 'PiggybankController@destroy', 'as' => 'piggybanks.destroy']); + #Route::post('/piggybanks/mod/{piggybank}', ['uses' => 'PiggybankController@modMoney', 'as' => 'piggybanks.modMoney']); + Route::post('/piggybanks/add/{piggybank}', ['uses' => 'PiggybankController@postAdd', 'as' => 'piggybanks.add']); + Route::post('/piggybanks/remove/{piggybank}', ['uses' => 'PiggybankController@postRemove', 'as' => 'piggybanks.remove']); // preferences controller @@ -291,20 +315,23 @@ Route::group(['before' => 'csrf|auth'], function () { 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']); + 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']); - Route::post('/transaction/destroy/{tj}',['uses' => 'TransactionController@destroy','as' => 'transactions.destroy']); + 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']); + Route::post('/transaction/destroy/{tj}', ['uses' => 'TransactionController@destroy', 'as' => 'transactions.destroy']); } ); // guest routes: -Route::group(['before' => 'guest'], function () { +Route::group( + ['before' => 'guest'], function () { // user controller Route::get('/login', ['uses' => 'UserController@login', 'as' => 'login']); Route::get('/register', ['uses' => 'UserController@register', 'as' => 'register']); @@ -317,7 +344,8 @@ Route::group(['before' => 'guest'], function () { ); // guest + csrf routes: -Route::group(['before' => 'csrf|guest'], function () { +Route::group( + ['before' => 'csrf|guest'], function () { // user controller Route::post('/login', ['uses' => 'UserController@postLogin']); diff --git a/app/start/global.php b/app/start/global.php index c5098db09d..10109df498 100644 --- a/app/start/global.php +++ b/app/start/global.php @@ -72,30 +72,51 @@ App::down( ); // forms: -\Form::macro('ffText', function ($name, $value = null, array $options = []) { - return \Firefly\Form\Form::ffText($name, $value, $options); -}); -\Form::macro('ffSelect', function ($name, array $list = [], $selected = null, array $options = []) { - return \Firefly\Form\Form::ffSelect($name, $list, $selected, $options); -}); -\Form::macro('ffInteger', function ($name, $value = null, array $options = []) { - return \Firefly\Form\Form::ffInteger($name, $value, $options); -}); -\Form::macro('ffAmount', function ($name, $value = null, array $options = []) { - return \Firefly\Form\Form::ffAmount($name, $value, $options); -}); -\Form::macro('ffDate', function ($name, $value = null, array $options = []) { - return \Firefly\Form\Form::ffDate($name, $value, $options); -}); -\Form::macro('ffTags', function ($name, $value = null, array $options = []) { - return \Firefly\Form\Form::ffTags($name, $value, $options); -}); -\Form::macro('ffCheckbox',function ($name, $value = 1, $checked = null, $options = []) { - return \Firefly\Form\Form::ffCheckbox($name, $value, $checked, $options); -}); -\Form::macro('ffOptionsList',function ($type, $name) { - return \Firefly\Form\Form::ffOptionsList($type, $name); -}); +\Form::macro( + 'ffText', function ($name, $value = null, array $options = []) { + return \Firefly\Form\Form::ffText($name, $value, $options); + } +); +\Form::macro( + 'ffSelect', function ($name, array $list = [], $selected = null, array $options = []) { + return \Firefly\Form\Form::ffSelect($name, $list, $selected, $options); + } +); +\Form::macro( + 'ffInteger', function ($name, $value = null, array $options = []) { + return \Firefly\Form\Form::ffInteger($name, $value, $options); + } +); +\Form::macro( + 'ffAmount', function ($name, $value = null, array $options = []) { + return \Firefly\Form\Form::ffAmount($name, $value, $options); + } +); +\Form::macro( + 'ffBalance', function ($name, $value = null, array $options = []) { + return \Firefly\Form\Form::ffBalance($name, $value, $options); + } +); +\Form::macro( + 'ffDate', function ($name, $value = null, array $options = []) { + return \Firefly\Form\Form::ffDate($name, $value, $options); + } +); +\Form::macro( + 'ffTags', function ($name, $value = null, array $options = []) { + return \Firefly\Form\Form::ffTags($name, $value, $options); + } +); +\Form::macro( + 'ffCheckbox', function ($name, $value = 1, $checked = null, $options = []) { + return \Firefly\Form\Form::ffCheckbox($name, $value, $checked, $options); + } +); +\Form::macro( + 'ffOptionsList', function ($type, $name) { + return \Firefly\Form\Form::ffOptionsList($type, $name); + } +); /* diff --git a/app/views/accounts/asset.blade.php b/app/views/accounts/asset.blade.php deleted file mode 100644 index 5cc72b31c9..0000000000 --- a/app/views/accounts/asset.blade.php +++ /dev/null @@ -1,18 +0,0 @@ -@extends('layouts.default') -@section('content') -
-
-

- Create a new asset account -

- @if(count($accounts) > 0) - @include('accounts.list') -

- Create a new asset account -

- @endif -
- -
- -@stop \ No newline at end of file diff --git a/app/views/accounts/create.blade.php b/app/views/accounts/create.blade.php index 6e3c7d2128..edf174bce6 100644 --- a/app/views/accounts/create.blade.php +++ b/app/views/accounts/create.blade.php @@ -4,98 +4,50 @@ {{Form::hidden('what',$what)}}
-

Mandatory fields

- -
- {{ Form::label('name', 'Account name', ['class' => 'col-sm-4 control-label'])}} -
- {{ Form::text('name', Input::old('name'), ['class' => 'form-control']) }} - @if($errors->has('name')) -

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

- @else - @if($what == 'asset') - - Use something descriptive such as "checking account" or "My Bank Main Account". - - @endif - @if($what == 'expense') - - Use something descriptive such as "Albert Heijn" or "Amazon". - - @endif - @if($what == 'revenue') - - Use something descriptive such as "my mom" or "my job". - - @endif - @endif - +
+
+ Mandatory fields +
+
+ {{Form::ffText('name')}}
- +

+ +

+
- @if($what == 'asset') -

Optional fields

-
- {{ Form::label('openingbalance', 'Opening balance', ['class' => 'col-sm-4 control-label'])}} -
-
- - {{Form::input('number','openingbalance', Input::old('openingbalance'), ['step' => 'any', 'class' => 'form-control'])}} -
- @if($errors->has('openingbalance')) -

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

- @else - What's the current balance of this new account? - @endif +
+
+ Optional fields +
+
+ @if($what == 'asset') + {{Form::ffBalance('openingbalance')}} + {{Form::ffDate('openingbalancedate', date('Y-m-d'))}} + @endif + {{Form::ffCheckbox('active','1',true)}}
-
- {{ Form::label('openingbalancedate', 'Opening balance date', ['class' => 'col-sm-4 control-label'])}} -
- {{ Form::input('date','openingbalancedate', Input::old('openingbalancedate') ?: date('Y-m-d'), ['class' - => 'form-control']) }} - @if($errors->has('openingbalancedate')) -

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

- @else - When was this the balance of the new account? Since your bank statements may lag behind, update this date to match the date of the last known balance of the account. - @endif + + + +
+
+ Options +
+
+ {{Form::ffOptionsList('create','account')}}
- @endif
- -
-
-
- - -
- -
-
- -
-
-
- -
-
- -
-
-
-
- - {{Form::close()}} @stop \ No newline at end of file diff --git a/app/views/accounts/delete.blade.php b/app/views/accounts/delete.blade.php index 7174cb310f..6f2059a15e 100644 --- a/app/views/accounts/delete.blade.php +++ b/app/views/accounts/delete.blade.php @@ -1,44 +1,38 @@ @extends('layouts.default') @section('content') -
-
-

- Remember that deleting something is permanent. -

-
-
- {{Form::open(['class' => 'form-horizontal','url' => route('accounts.destroy',$account->id)])}}
-
- @if($account->transactions()->count() > 0) -

- Account "{{{$account->name}}}" still has {{$account->transactions()->count()}} transaction(s) associated to it. - These will be deleted as well. -

- @endif +
+
+
+ Delete account "{{{$account->name}}}" +
+
+

+ Are you sure? +

-

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

+ @if($account->transactions()->count() > 0) +

+ Account "{{{$account->name}}}" still has {{$account->transactions()->count()}} transaction(s) associated to it. + These will be deleted as well. +

+ @endif + +

+ + Cancel +

+
+
-
- - @if($account->accountType->type == 'Asset account' || $account->accountType->type == 'Default account') - Cancel - @endif - @if($account->accountType->type == 'Expense account' || $account->accountType->type == 'Beneficiary account') - Cancel - @endif - @if($account->accountType->type == 'Revenue account') - Cancel - @endif +
diff --git a/app/views/accounts/edit.blade.php b/app/views/accounts/edit.blade.php index 3393570a0b..39e14486ca 100644 --- a/app/views/accounts/edit.blade.php +++ b/app/views/accounts/edit.blade.php @@ -11,79 +11,46 @@ {{Form::model($account, ['class' => 'form-horizontal','url' => route('accounts.update',$account->id)])}}
-

Mandatory fields

- -
- {{ Form::label('name', 'Account name', ['class' => 'col-sm-4 control-label'])}} -
- {{ Form::text('name', Input::old('name'), ['class' => 'form-control']) }} - @if($errors->has('name')) -

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

- @else - Use something descriptive such as "checking account" or "Albert Heijn". - @endif - +
+
+ Mandatory fields +
+
+ {{Form::ffText('name')}}
- +

+ +

- @if($account->accounttype->type == 'Default account' || $account->accounttype->type == 'Asset account') -

Optional fields

- -
- {{ Form::label('openingbalance', 'Opening balance', ['class' => 'col-sm-4 control-label'])}} -
-
- - @if(!is_null($openingBalance)) - {{Form::input('number','openingbalance', Input::old('openingbalance') ?: $openingBalance->transactions[1]->amount, ['step' => 'any', 'class' => 'form-control'])}} - @else - {{Form::input('number','openingbalance', Input::old('openingbalance'), ['step' => 'any', 'class' => 'form-control'])}} - @endif - -
- - @if($errors->has('openingbalance')) -

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

- @else - What's the current balance of this new account? +
+
+ Optional fields +
+
+ {{Form::ffCheckbox('active','1')}} + @if($account->accounttype->type == 'Default account' || $account->accounttype->type == 'Asset account') + {{Form::ffBalance('openingbalance')}} + {{Form::ffDate('openingbalancedate')}} @endif
-
- {{ Form::label('openingbalancedate', 'Opening balance date', ['class' => 'col-sm-4 control-label'])}} -
- @if(!is_null($openingBalance)) - {{ Form::input('date','openingbalancedate', Input::old('openingbalancedate') ?: $openingBalance->date->format('Y-m-d'), ['class' => 'form-control']) }} - @else - {{ Form::input('date','openingbalancedate', Input::old('openingbalancedate') ?: '', ['class' => 'form-control']) }} - @endif - @if($errors->has('openingbalancedate')) -

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

- @else - When was this the balance of the new account? Since your bank statements may lag behind, update this date to match the date of the last known balance of the account. - @endif + + +
+
+ Options +
+
+ {{Form::ffOptionsList('update','account')}}
- @endif
- -
-
-
-
-
- -
-
-
-
- - {{Form::close()}} @stop \ No newline at end of file diff --git a/app/views/accounts/expense.blade.php b/app/views/accounts/expense.blade.php deleted file mode 100644 index d64ef24f63..0000000000 --- a/app/views/accounts/expense.blade.php +++ /dev/null @@ -1,28 +0,0 @@ -@extends('layouts.default') -@section('content') -
-
-

- Bla bla expense -

-

- Bla bla bla expense -

-
-
-
-
-

- Create a new expense account -

- @if(count($accounts) > 0) - @include('accounts.list') -

- Create a new expense account -

- @endif -
- -
- -@stop \ No newline at end of file diff --git a/app/views/accounts/index.blade.php b/app/views/accounts/index.blade.php index 545d3f6fc6..2410bc6c8e 100644 --- a/app/views/accounts/index.blade.php +++ b/app/views/accounts/index.blade.php @@ -2,42 +2,45 @@ @section('content')
-

Firefly - Accounts -

-

- Accounts are the record holders for transactions and transfers. Money moves from one account to another. -

-

- In a double-entry bookkeeping system almost everything is an account. Your own personal - bank accounts are representated as accounts (naturally), but the stores you buy stuff at are also - represented as accounts. Likewise, if you have a job, your salary is drawn from their account. -

-

- Create a new account -

+
+
+ {{{$subTitle}}} + + +
+
+ + +
+
+ + +
+
+
+
+
-
-
- @if(count($accounts['personal']) > 0) -

Your accounts

-

- These are your personal accounts. -

+@stop +@section('scripts') + - @include('accounts.list',['accounts' => $accounts['personal']]) - @endif + + +{{HTML::script('assets/javascript/firefly/gcharts.options.js')}} +{{HTML::script('assets/javascript/firefly/gcharts.js')}} - @if(count($accounts['beneficiaries']) > 0) -

Beneficiaries

-

- These are beneficiaries; places where you spend money or people who pay you. -

- @include('accounts.list',['accounts' => $accounts['beneficiaries']]) - @endif -
-
+ +@stop -@stop \ No newline at end of file +@section('styles') +@endsection \ No newline at end of file diff --git a/app/views/accounts/list.blade.php b/app/views/accounts/list.blade.php deleted file mode 100644 index 7ba7ceed8d..0000000000 --- a/app/views/accounts/list.blade.php +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - @foreach($accounts as $account) - - - - - - - @endforeach -
 NameCurrent balance
- @if($account->active == 0) - - @endif - - {{{$account->name}}}{{mf($account->balance())}} - - - - -
\ No newline at end of file diff --git a/app/views/accounts/revenue.blade.php b/app/views/accounts/revenue.blade.php deleted file mode 100644 index d32e2e5655..0000000000 --- a/app/views/accounts/revenue.blade.php +++ /dev/null @@ -1,28 +0,0 @@ -@extends('layouts.default') -@section('content') -
-
-

- Bla bla revenue -

-

- Bla bla bla revenue -

-
-
-
-
-

- Create a new revenue account -

- @if(count($accounts) > 0) - @include('accounts.list') -

- Create a new revenue account -

- @endif -
- -
- -@stop \ No newline at end of file diff --git a/app/views/accounts/show.blade.php b/app/views/accounts/show.blade.php index f3148e26f8..8d5842fd67 100644 --- a/app/views/accounts/show.blade.php +++ b/app/views/accounts/show.blade.php @@ -4,10 +4,10 @@
- {{{$account->name}}} + {{{$account->name}}}
-
+
@@ -16,88 +16,26 @@ @include('partials.date_nav')
+ +
-
+
- Summary + Out
- - - - - - - - - - - - - - - - - - - - - -
Expense / incomeTransfers
Out - {{mf($show['statistics']['period']['out'])}} - - - {{mf($show['statistics']['period']['t_out'])}} - -
In - {{mf($show['statistics']['period']['in'])}} - - - {{mf($show['statistics']['period']['t_in'])}} - -
Difference{{mf($show['statistics']['period']['diff'])}}{{mf($show['statistics']['period']['t_diff'])}}
+
-
+
- Related + In
- - @if(count($show['statistics']['accounts']) > 0) - - - - - @endif - @if(isset($show['statistics']['Category']) && count($show['statistics']['Category']) > 0) - - - - - @endif - @if(isset($show['statistics']['Budget']) && count($show['statistics']['Budget']) > 0) - - - - - @endif -
Related accounts - @foreach($show['statistics']['accounts'] as $acct) - {{{$acct->name}}} - @endforeach -
Related categories - @foreach($show['statistics']['Category'] as $cat) - {{{$cat->name}}} - @endforeach -
Related budgets - @foreach($show['statistics']['Budget'] as $bud) - {{{$bud->name}}} - @endforeach -
+
@@ -105,23 +43,26 @@
- - -

Transactions For selected account and period

- @include('paginated.transactions',['journals' => $show['journals'],'sum' => true]) +
+
+ Transactions +
+
+
+
-@stop -@section('styles') -{{HTML::style('assets/stylesheets/highslide/highslide.css')}} -@stop +@stop @section('scripts') -{{HTML::script('assets/javascript/highcharts/highcharts.js')}} + + +{{HTML::script('assets/javascript/firefly/gcharts.options.js')}} +{{HTML::script('assets/javascript/firefly/gcharts.js')}} {{HTML::script('assets/javascript/firefly/accounts.js')}} @stop \ No newline at end of file diff --git a/app/views/budgets/create.blade.php b/app/views/budgets/create.blade.php index 96d6664410..0b20da4663 100644 --- a/app/views/budgets/create.blade.php +++ b/app/views/budgets/create.blade.php @@ -1,118 +1,39 @@ @extends('layouts.default') @section('content') -
-
-

Use budgets to organize and limit your expenses.

-

- 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')])}} - -{{Form::hidden('from',e(Input::get('from')))}} -
-
-

Mandatory fields

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

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

- @else - For example: groceries, bills - @endif +
+
+
+ Mandatory fields +
+
+ {{Form::ffText('name')}}
+

+ +

-
-

Optional fields

- -
- -
-
- - +
+ +
+
+ Options
- - @if($errors->has('amount')) -

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

- @else - What's the most you're willing to spend in this budget? This amount is "put" in the virtual - envelope. - @endif -
-
- -
- -
- {{Form::select('repeat_freq',$periods,Input::old('repeat_freq') ?: 'monthly',['class' => 'form-control'])}} - @if($errors->has('repeat_freq')) -

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

- @else - How long will the envelope last? A week, a month, or even longer? - @endif -
-
- -
- -
-
- +
+ {{Form::ffOptionsList('create','budget')}}
- @if($errors->has('repeats')) -

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

- @else - 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]. - @endif
+
-
+
-
-
- -
- -
-
- -
-
-
- -
-
- -
-
-
-
{{Form::close()}} diff --git a/app/views/budgets/delete.blade.php b/app/views/budgets/delete.blade.php index 78abbcc3f7..96775a8357 100644 --- a/app/views/budgets/delete.blade.php +++ b/app/views/budgets/delete.blade.php @@ -1,42 +1,31 @@ @extends('layouts.default') @section('content') -
-
-

- Remember that deleting something is permanent. -

-
-
- {{Form::open(['class' => 'form-horizontal','url' => route('budgets.destroy',$budget->id)])}} -{{Form::hidden('from',e(Input::get('from')))}}
-
- @if($budget->transactionjournals()->count() > 0) -

+

+
+
+ Delete budget "{{{$budget->name}}}" +
+
+

+ Are you sure? +

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

- @endif - -

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

+

+ + Cancel +

+
+
-
- - @if(Input::get('from') == 'date') - Cancel - @else - Cancel - @endif +
diff --git a/app/views/budgets/edit.blade.php b/app/views/budgets/edit.blade.php index ecf8564944..baeeb1cc8c 100644 --- a/app/views/budgets/edit.blade.php +++ b/app/views/budgets/edit.blade.php @@ -7,40 +7,34 @@
{{Form::open(['class' => 'form-horizontal','url' => route('budgets.update',$budget->id)])}} - -{{Form::hidden('from',e(Input::get('from')))}} -
-

Mandatory fields

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

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

- @else - For example: groceries, bills - @endif +
+
+ Mandatory fields +
+
+ {{Form::ffText('name')}}
- +

+ +

- -
- -
- -
-
- + +
+
+ Options +
+
+ {{Form::ffOptionsList('update','budget')}}
- {{Form::close()}} diff --git a/app/views/budgets/income.blade.php b/app/views/budgets/income.blade.php new file mode 100644 index 0000000000..4c6e3761df --- /dev/null +++ b/app/views/budgets/income.blade.php @@ -0,0 +1,21 @@ +
+{{Form::token()}} + +
\ 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..9dd76b21e6 --- /dev/null +++ b/app/views/budgets/index.blade.php @@ -0,0 +1,190 @@ +@extends('layouts.default') +@section('content') +
+
+
+
+ {{\Session::get('start')->format('F Y')}} +
+
+
+
+ Budgeted: {{mf(300)}} +
+
+ Income {{\Session::get('start')->format('F Y')}}: + {{mf($budgetAmount->data)}} +
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Spent: {{mf($spent)}} +
+
+
+
+
+ @if($overspent) +
+
+ @else +
+ @endif +
+
+
+ +
+
+
+
+ + @include('partials.date_nav') +
+
+ +
+@foreach($budgets as $budget) +
+
+
+ @if($budget->currentRep) + {{{$budget->name}}} + @else + {{{$budget->name}}} + @endif + + + +
+
+ + +
+
+ +
+
+ +

+ @if($budget->currentRep) + + @else + + @endif +

+ +

+ + + @if($budget->currentRep) + + Budgeted: + + + @if($budget->limit > $budget->spent) + + @else + + @endif + + @else + No budget + + + + @endif + +

+

+ Spent: {{mf($budget->spent)}} +

+
+
+
+@endforeach +
+
+
+ Create budget +
+ +
+
+ + + +@foreach($budgets as $budget) +{{-- +
+
+
+
+ {{$budget->name}} +
+
+ +
+
+ +
+
+
+
+ @if($budget->pct > 0) + +
+ +
+ @else + + + @endif + +
+
+
+
+--}} +@endforeach + + + + + +@stop +@section('scripts') +{{HTML::script('assets/javascript/firefly/budgets.js')}} +@stop \ No newline at end of file diff --git a/app/views/budgets/indexByBudget.blade.php b/app/views/budgets/indexByBudget.blade.php deleted file mode 100644 index c4596e92a6..0000000000 --- a/app/views/budgets/indexByBudget.blade.php +++ /dev/null @@ -1,113 +0,0 @@ -@extends('layouts.default') -@section('content') -
-
-

Use budgets to organize and limit your expenses.

- -

- Budgets are groups of expenses that reappear every [period]*. Examples could be "groceries", "bills" or - "drinks with friends". The table below lists all of the budgets you have, if any. - By definition, budgets are an estimated amount - of money, ie. € 400,-. Such an estimation can change over time but should always be set. Budgets - without an actual budget are fairly pointless. -

-

- Use this page to create or change budgets and the estimated amount of money you think is wise. Pages further ahead - will explain what an "envelope" is in the context of budgeting. -

-

- * Every month, week, year, etc. -

- - -

-
-
-
-
- - - - - - - @foreach($budgets as $budget) - - - - - - @endforeach - -
BudgetCurrent envelope(s)Update budget
- {{{$budget->name}}} - - -
-
- Envelope -
-
- Left -
-
- @foreach($budget->limits as $limit) - @foreach($limit->limitrepetitions as $index => $rep) -
-
- - - {{mf($rep->amount,false)}} -
-
- @if($rep->leftInRepetition() < 0) - - - {{mf($rep->leftInRepetition(),false)}} - @else - - - {{mf($rep->leftInRepetition(),false)}} - @endif -
- - @if($limit->repeats == 1) -
- auto repeats -
- @endif -
-
- - @if($limit->repeats == 0 || ($limit->repeats == 1 && $index == 0)) - - @endif -
-
-
- @endforeach - @endforeach -

- Add another envelope -

-
-
- - - -
-
- -
-
-@stop \ No newline at end of file diff --git a/app/views/budgets/indexByDate.blade.php b/app/views/budgets/indexByDate.blade.php deleted file mode 100644 index a953a8a633..0000000000 --- a/app/views/budgets/indexByDate.blade.php +++ /dev/null @@ -1,87 +0,0 @@ -@extends('layouts.default') -@section('content') -
-
-

Use budgets to organize and limit your expenses.

- -

- Budgets are groups of expenses that reappear every [period]*. Examples could be "groceries", "bills" or - "drinks with friends". The table below lists all of the budgets you have, if any. - By definition, budgets are an estimated amount - of money, ie. € 400,-. Such an estimation can change over time but should always be set. Budgets - without an actual budget are fairly pointless. -

-

- Use this page to create or change budgets and the estimated amount of money you think is wise. Pages further ahead - will explain what an "envelope" is in the context of budgeting. -

-

- * Every month, week, year, etc. -

- -
-
- - - -@foreach($budgets as $date => $entry) -
-
-

{{$entry['date']}} - Create a new envelope for {{$entry['date']}} -

- - - - - - - - @foreach($entry['limitrepetitions'] as $index => $repetition) - - - - - - - -@endforeach -
BudgetEnvelopeLeft 
-
- - -
-
- - {{{$repetition->limit->budget->name}}} - - - - {{mf($repetition->amount,false)}} - - @if($repetition->left < 0) - - - {{mf($repetition->left,false)}} - @else - - - {{mf($repetition->left,false)}} - @endif - -
- - -
- @if($repetition->limit->repeats == 1) - auto repeats - @endif -
-
-
-@endforeach - -@stop \ No newline at end of file diff --git a/app/views/budgets/nobudget.blade.php b/app/views/budgets/nobudget.blade.php deleted file mode 100644 index 293d8cdab1..0000000000 --- a/app/views/budgets/nobudget.blade.php +++ /dev/null @@ -1,30 +0,0 @@ -@extends('layouts.default') -@section('content') -
-
- @if($view == 'session') - -

- This view is filtered to only show transactions between {{Session::get('start')->format('d M Y')}} - and {{Session::get('end')->format('d M Y')}}. -

- @endif -
-
-@if($transactions->count() > 0) -
-
- @include('lists.transactions',['journals' => $transactions,'sum' => true]) -
-
-@else -
-
-

{{$repetition['date']}} -

-

No transactions

-
-
-@endif - -@stop \ No newline at end of file diff --git a/app/views/budgets/show.blade.php b/app/views/budgets/show.blade.php index 5be4b68fa6..2cdc0f3e14 100644 --- a/app/views/budgets/show.blade.php +++ b/app/views/budgets/show.blade.php @@ -1,120 +1,92 @@ @extends('layouts.default') @section('content') +
-
-

Budgets can help you cut back on spending.

+
+
+
+ Some stuff? +
+
+
+
+
- @if($view == 1) - -

- This view is filtered to show only the envelope from - {{{$repetitions[0]['limitrepetition']->periodShow()}}}, - which contains {{mf($repetitions[0]['limit']->amount,false)}}. -

+
+
+ Transactions +
+
+
+
+
+
+
+ @foreach($limits as $limit) + @foreach($limit->limitrepetitions as $rep) +
+ +
+
+
+ Amount: {{mf($rep->amount)}} +
+
+ Spent: {{mf($rep->spentInRepetition())}} +
+
+
+
+ spentInRepetition() > $rep->amount; + ?> + @if($overspent) + amount / $rep->spentInRepetition()*100; + ?> +
+
+
+
+ @else + spentInRepetition() / $rep->amount*100; + ?> +
+
+
+
+ @endif +
+
+
+
+ @endforeach + @endforeach - @endif - - - @if($view == 2) - -

- This view is filtered to show transactions not in an envelope only. -

- @endif - - @if($view == 3) - -

- This view is filtered to only show transactions between {{Session::get('start')->format('d M Y')}} - and {{Session::get('end')->format('d M Y')}}. -

- @endif - @if($view != 4) -

- Reset the filter -

- @endif
-
-
-
- @if($view == 1) -
- @endif - - - @if($view == 2) -
- @endif - - @if($view == 3) -
- @endif - - @if($view == 4) -
- @endif - -
-
- -@foreach($repetitions as $repetition) -@if(isset($repetition['journals']) && count($repetition['journals']) > 0) -
-
- - - @if($repetition['paginated'] == true) -

- - {{$repetition['date']}} paginated

- @else -

- - {{$repetition['date']}} - -

- {{mf($repetition['limit']->amount,false)}} - (left: {{mf($repetition['limitrepetition']->leftInRepetition(),false)}}) - @endif - - @if($repetition['paginated'] == true) - @include('paginated.transactions',['journals' => $repetition['journals'],'highlight' => $highlight]) - @else - @include('lists.transactions',['journals' => $repetition['journals'],'sum' => true,'highlight' => $highlight]) - @endif -
-
-@else -
-
-

{{$repetition['date']}} -

-

No transactions

-
-
-@endif -@endforeach - @stop @section('scripts') -{{HTML::script('assets/javascript/highcharts/highcharts.js')}} -@if($view == 1) -{{HTML::script('assets/javascript/firefly/budgets/limit.js')}} -@endif + -@if($view == 3) -{{HTML::script('assets/javascript/firefly/budgets/session.js')}} -@endif -@if($view == 4) -{{HTML::script('assets/javascript/firefly/budgets/default.js')}} -@endif + + +{{HTML::script('assets/javascript/firefly/gcharts.options.js')}} +{{HTML::script('assets/javascript/firefly/gcharts.js')}} +{{HTML::script('assets/javascript/firefly/budgets.js')}} @stop \ No newline at end of file diff --git a/app/views/categories/create.blade.php b/app/views/categories/create.blade.php index f97cab4f67..bf8c855596 100644 --- a/app/views/categories/create.blade.php +++ b/app/views/categories/create.blade.php @@ -8,7 +8,7 @@ Expenses grouped in categories do not have to reoccur every month or every week, like budgets.

-
+
{{Form::open(['class' => 'form-horizontal','url' => route('categories.store')])}} diff --git a/app/views/categories/delete.blade.php b/app/views/categories/delete.blade.php index fb262aedea..9524245af1 100644 --- a/app/views/categories/delete.blade.php +++ b/app/views/categories/delete.blade.php @@ -6,7 +6,7 @@ Remember that deleting something is permanent.

-
+
{{Form::open(['class' => 'form-horizontal','url' => route('categories.destroy',$category->id)])}}
diff --git a/app/views/categories/edit.blade.php b/app/views/categories/edit.blade.php index f245bf4757..d747576b34 100644 --- a/app/views/categories/edit.blade.php +++ b/app/views/categories/edit.blade.php @@ -4,7 +4,7 @@

Use categories to group your expenses

-
+
{{Form::open(['class' => 'form-horizontal','url' => route('categories.update',$category->id)])}} diff --git a/app/views/categories/index.blade.php b/app/views/categories/index.blade.php index f6bced7af0..78a2451a1f 100644 --- a/app/views/categories/index.blade.php +++ b/app/views/categories/index.blade.php @@ -11,7 +11,7 @@ Create a new category

-
+
diff --git a/app/views/categories/show.blade.php b/app/views/categories/show.blade.php index cb5c54418f..f6f4678b6d 100644 --- a/app/views/categories/show.blade.php +++ b/app/views/categories/show.blade.php @@ -15,13 +15,13 @@ transactions for the currently selected period.

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

(Some chart here)

+
@@ -40,6 +40,5 @@ -{{HTML::script('assets/javascript/highcharts/highcharts.js')}} {{HTML::script('assets/javascript/firefly/categories.js')}} @stop \ No newline at end of file diff --git a/app/views/charts/info.blade.php b/app/views/charts/info.blade.php index dbe16970f1..00e590e129 100644 --- a/app/views/charts/info.blade.php +++ b/app/views/charts/info.blade.php @@ -9,4 +9,4 @@ {{mf($entry['amount']*-1)}} @endforeach - \ No newline at end of file + \ No newline at end of file diff --git a/app/views/emails/auth/reminder.blade.php b/app/views/emails/auth/reminder.blade.php index aebea9e364..276f8c59f2 100644 --- a/app/views/emails/auth/reminder.blade.php +++ b/app/views/emails/auth/reminder.blade.php @@ -12,3 +12,4 @@
+ \ No newline at end of file diff --git a/app/views/emails/user/remindme-text.php b/app/views/emails/user/remindme-text.blade.php similarity index 100% rename from app/views/emails/user/remindme-text.php rename to app/views/emails/user/remindme-text.blade.php diff --git a/app/views/index.blade.php b/app/views/index.blade.php index c9137158cc..66857d7cfd 100644 --- a/app/views/index.blade.php +++ b/app/views/index.blade.php @@ -30,102 +30,107 @@ -
-
- -
- -
-
-
+
+
+ +
+ - -
- -
-
-
+
+
- -
- -
-
-
-
-
-
- Savings -
-
- Bla bla -
-
- - -
-
- - @include('partials.date_nav') - - -
-
- Recurring transactions -
-
-
-
+ +
+ - - - @foreach($transactions as $data) -
-
- - {{{$data[1]->name}}} - - - -
-
- - -
-
- - - -
-
- @include('transactions.journals-small-index',['transactions' => $data[0],'account' => $data[1]]) -
+
+
- @endforeach
+ +
+ +
+
+
+
+
+
+ Savings +
+
+ (todo) +
+
+ + +
+
+ + @include('partials.date_nav') - @endif + +
+
+ Recurring transactions +
+
+
+
+
- @stop - @section('scripts') - {{HTML::script('assets/javascript/highcharts/highcharts.js')}} - {{HTML::script('assets/javascript/firefly/index.js')}} - @stop - @section('styles') - {{HTML::style('assets/stylesheets/highslide/highslide.css')}} - @stop \ No newline at end of file + + @foreach($transactions as $data) +
+
+ + {{{$data[1]->name}}} + + + +
+
+ + +
+
+ + + +
+
+ @include('transactions.journals-small-index',['transactions' => $data[0],'account' => $data[1]]) +
+
+ @endforeach +
+
+ +@endif + +@stop +@section('scripts') + + +{{HTML::script('assets/javascript/firefly/gcharts.options.js')}} +{{HTML::script('assets/javascript/firefly/gcharts.js')}} + + + +{{HTML::script('assets/javascript/firefly/index.js')}} +@stop +@section('styles') +@stop \ No newline at end of file diff --git a/app/views/layouts/default.blade.php b/app/views/layouts/default.blade.php index 76b2c54814..346742569b 100644 --- a/app/views/layouts/default.blade.php +++ b/app/views/layouts/default.blade.php @@ -17,6 +17,7 @@ {{HTML::style('assets/stylesheets/metisMenu/metisMenu.min.css')}} {{HTML::style('assets/stylesheets/sbadmin/sb.css')}} {{HTML::style('assets/stylesheets/fa/css/font-awesome.min.css')}} + {{HTML::style('http://fonts.googleapis.com/css?family=Roboto2')}} @yield('styles') \ No newline at end of file diff --git a/app/views/limits/create.blade.php b/app/views/limits/create.blade.php index 6b660ead47..a3a13c4dcf 100644 --- a/app/views/limits/create.blade.php +++ b/app/views/limits/create.blade.php @@ -14,7 +14,7 @@ always add more of them.

-
+
{{Form::open(['class' => 'form-horizontal','url' => route('budgets.limits.store',$prefilled['budget_id'])])}} {{Form::hidden('from',e(Input::get('from')))}} diff --git a/app/views/limits/delete.blade.php b/app/views/limits/delete.blade.php index 631405dd93..4c3bd9b8b7 100644 --- a/app/views/limits/delete.blade.php +++ b/app/views/limits/delete.blade.php @@ -6,7 +6,7 @@
-
+
{{Form::open(['class' => 'form-horizontal','url' => route('budgets.limits.destroy',$limit->id)])}} @@ -31,9 +31,9 @@
@if(Input::get('from') == 'date') - Cancel + Cancel @else - Cancel + Cancel @endif
diff --git a/app/views/limits/edit.blade.php b/app/views/limits/edit.blade.php index e9f2fb90b3..7ba95b132b 100644 --- a/app/views/limits/edit.blade.php +++ b/app/views/limits/edit.blade.php @@ -17,7 +17,7 @@ @endif

-
+
{{Form::open(['class' => 'form-horizontal','url' => route('budgets.limits.update',$limit->id)])}} {{Form::hidden('from',e(Input::get('from')))}} diff --git a/app/views/lists/transactions.blade.php b/app/views/lists/transactions.blade.php index 8ce89718cd..ceb0f54757 100644 --- a/app/views/lists/transactions.blade.php +++ b/app/views/lists/transactions.blade.php @@ -2,7 +2,7 @@ A - Date + Date Description Amount (€) From diff --git a/app/views/paginated/transactions.blade.php b/app/views/paginated/transactions.blade.php index 31980183f4..223541781e 100644 --- a/app/views/paginated/transactions.blade.php +++ b/app/views/paginated/transactions.blade.php @@ -1,3 +1,3 @@ {{$journals->links()}} @include('lists.transactions') -{{$journals->links()}} \ No newline at end of file +{{$journals->links()}} \ No newline at end of file diff --git a/app/views/partials/menu.blade.php b/app/views/partials/menu.blade.php index 8bbbbae2bc..21a5068f89 100644 --- a/app/views/partials/menu.blade.php +++ b/app/views/partials/menu.blade.php @@ -48,7 +48,7 @@ diff --git a/app/views/piggybanks/add.blade.php b/app/views/piggybanks/add.blade.php new file mode 100644 index 0000000000..4320379029 --- /dev/null +++ b/app/views/piggybanks/add.blade.php @@ -0,0 +1,24 @@ +
+{{Form::token()}} + +
\ No newline at end of file diff --git a/app/views/piggybanks/create-piggybank.blade.php b/app/views/piggybanks/create-piggybank.blade.old.php similarity index 99% rename from app/views/piggybanks/create-piggybank.blade.php rename to app/views/piggybanks/create-piggybank.blade.old.php index 8dbbd137db..5fb1eb65d4 100644 --- a/app/views/piggybanks/create-piggybank.blade.php +++ b/app/views/piggybanks/create-piggybank.blade.old.php @@ -4,7 +4,7 @@

Use piggy banks to save for a one-time goal.

-
+ {{Form::open(['class' => 'form-horizontal','url' => route('piggybanks.store.piggybank')])}} diff --git a/app/views/piggybanks/create-repeated.blade.php b/app/views/piggybanks/create-repeated.blade.old.php similarity index 99% rename from app/views/piggybanks/create-repeated.blade.php rename to app/views/piggybanks/create-repeated.blade.old.php index df51394373..cd55d693a0 100644 --- a/app/views/piggybanks/create-repeated.blade.php +++ b/app/views/piggybanks/create-repeated.blade.old.php @@ -3,7 +3,7 @@

Create repeated expenses to keep track of long-term planned expenses

-
+
{{Form::open(['class' => 'form-horizontal','url' => route('piggybanks.store.repeated')])}} diff --git a/app/views/piggybanks/create.blade.php b/app/views/piggybanks/create.blade.php new file mode 100644 index 0000000000..6c05dcc7fe --- /dev/null +++ b/app/views/piggybanks/create.blade.php @@ -0,0 +1,93 @@ +@extends('layouts.default') +@section('content') +{{Form::open(['class' => 'form-horizontal','url' => route('piggybanks.store')])}} + +
+
+
+
+ Mandatory fields +
+
+ {{Form::ffText('name')}} + {{Form::ffSelect('account_id',$accounts,null,['label' => 'Save on account'])}} + {{Form::ffAmount('targetamount')}} + +
+
+

+ +

+
+
+ +
+
+ Optional fields +
+
+ {{Form::ffDate('targetdate')}} + {{Form::ffCheckbox('remind_me','1',false,['label' => 'Remind me'])}} + {{Form::ffSelect('reminder',$periods,'month',['label' => 'Remind every'])}} +
+
+ + +
+
+ Options +
+
+ {{Form::ffOptionsList('create','piggy bank')}} +
+
+ +
+
+{{-- + +

Mandatory fields

+ +

Optional fields

+ + +
+ {{ Form::label('reminder', 'Remind you every', ['class' => 'col-sm-4 control-label'])}} +
+ + + + @if($errors->has('reminder')) +

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

+ @else + Enter a number and a period and Firefly will remind you to add money + to this piggy bank every now and then. + @endif +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+--}} + +{{Form::close()}} +@stop diff --git a/app/views/piggybanks/delete.blade.old.php b/app/views/piggybanks/delete.blade.old.php new file mode 100644 index 0000000000..4541ea09d7 --- /dev/null +++ b/app/views/piggybanks/delete.blade.old.php @@ -0,0 +1,42 @@ +@extends('layouts.default') +@section('content') +
+
+

Remember that deleting something is permanent.

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

 

+

+ This form allows you to delete the piggy bank "{{{$piggybank->name}}}". +

+

+ Destroying an envelope does not remove any transactions or accounts. +

+

+ Are you sure? +

+ +
+
+ + @if($piggybank->repeats == 1) + Cancel + @else + Cancel + @endif +
+
+
+
+ + +{{Form::close()}} + +@stop diff --git a/app/views/piggybanks/delete.blade.php b/app/views/piggybanks/delete.blade.php index 29255e2188..52695c20b0 100644 --- a/app/views/piggybanks/delete.blade.php +++ b/app/views/piggybanks/delete.blade.php @@ -1,36 +1,31 @@ @extends('layouts.default') @section('content') -
-
-

Remember that deleting something is permanent.

- -
- -
- {{Form::open(['class' => 'form-horizontal','url' => route('piggybanks.destroy',$piggybank->id)])}} +
+
+
+
+ Delete piggy bank "{{{$piggybank->name}}}" +
+
+

+ Are you sure? +

+ +

+ + Cancel +

+
+
+
+
-

 

-

- This form allows you to delete the piggy bank "{{{$piggybank->name}}}". -

-

- Destroying an envelope does not remove any transactions or accounts. -

-

- Are you sure? -

-
- - @if($piggybank->repeats == 1) - Cancel - @else - Cancel - @endif +
@@ -38,5 +33,4 @@ {{Form::close()}} - -@stop +@stop \ No newline at end of file diff --git a/app/views/piggybanks/edit-piggybank.blade.php b/app/views/piggybanks/edit-piggybank.blade.old.php similarity index 99% rename from app/views/piggybanks/edit-piggybank.blade.php rename to app/views/piggybanks/edit-piggybank.blade.old.php index 876aac0204..ec572047a3 100644 --- a/app/views/piggybanks/edit-piggybank.blade.php +++ b/app/views/piggybanks/edit-piggybank.blade.old.php @@ -4,7 +4,7 @@

Use piggy banks to save for a one-time goal.

-
+
{{Form::open(['class' => 'form-horizontal','url' => route('piggybanks.update',$piggybank->id)])}} diff --git a/app/views/piggybanks/edit-repeated.blade.php b/app/views/piggybanks/edit-repeated.blade.old.php similarity index 99% rename from app/views/piggybanks/edit-repeated.blade.php rename to app/views/piggybanks/edit-repeated.blade.old.php index 6caa2a4312..a16fe00add 100644 --- a/app/views/piggybanks/edit-repeated.blade.php +++ b/app/views/piggybanks/edit-repeated.blade.old.php @@ -5,7 +5,7 @@

Create repeated expenses to keep track of long-term planned expenses

- + {{Form::open(['class' => 'form-horizontal','url' => route('piggybanks.update',$piggybank->id)])}}
diff --git a/app/views/piggybanks/edit.blade.php b/app/views/piggybanks/edit.blade.php new file mode 100644 index 0000000000..e8ae4435ac --- /dev/null +++ b/app/views/piggybanks/edit.blade.php @@ -0,0 +1,93 @@ +@extends('layouts.default') +@section('content') +{{Form::open(['class' => 'form-horizontal','url' => route('piggybanks.update',$piggybank->id)])}} + +
+
+
+
+ Mandatory fields +
+
+ {{Form::ffText('name')}} + {{Form::ffSelect('account_id',$accounts,null,['label' => 'Save on account'])}} + {{Form::ffAmount('targetamount')}} + +
+
+

+ +

+
+
+ +
+
+ Optional fields +
+
+ {{Form::ffDate('targetdate')}} + {{Form::ffCheckbox('remind_me','1',$prefilled['remind_me'],['label' => 'Remind me'])}} + {{Form::ffSelect('reminder',$periods,'month',['label' => 'Remind every'])}} +
+
+ + +
+
+ Options +
+
+ {{Form::ffOptionsList('update','piggy bank')}} +
+
+ +
+
+{{-- + +

Mandatory fields

+ +

Optional fields

+ + +
+ {{ Form::label('reminder', 'Remind you every', ['class' => 'col-sm-4 control-label'])}} +
+ + + + @if($errors->has('reminder')) +

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

+ @else + Enter a number and a period and Firefly will remind you to add money + to this piggy bank every now and then. + @endif +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+--}} + +{{Form::close()}} +@stop diff --git a/app/views/piggybanks/index.blade.old.php b/app/views/piggybanks/index.blade.old.php new file mode 100644 index 0000000000..981d7bc1a2 --- /dev/null +++ b/app/views/piggybanks/index.blade.old.php @@ -0,0 +1,223 @@ +@extends('layouts.default') +@section('content') + +@if($countNonRepeating > 0) +
+@foreach($piggybanks as $piggyBank) + @if($piggyBank->repeats == 0) +
+
+ +
+
+
+
+
+

+ {{mf($piggyBank->currentRelevantRep()->currentamount)}} of {{mf($piggyBank->targetamount)}}
+ @if($piggyBank->targetamount-$piggyBank->currentRelevantRep()->currentamount > 0) + {{mf($piggyBank->targetamount-$piggyBank->currentRelevantRep()->currentamount)}} to go. + @endif +

+ +
+ + + @if($accounts[$piggyBank->account_id]['account']->leftOnAccount > 0) + + @endif + @if($piggyBank->currentRelevantRep()->currentamount > 0) + + @endif +
+ + +
+
+
+ @endif +@endforeach +
+
+
+   +
+ +
+
+
+@endif +{{-- + + +

Current piggy banks

+ @if($countNonRepeating == 0) +

No piggy banks found.

+ @else + @foreach($piggybanks as $piggyBank) + @if($piggyBank->repeats == 0) +

+ + + + + + + + + + + + +
{{mf($piggyBank->currentRelevantRep()->currentamount)}} +
+
+ {{$piggyBank->currentRelevantRep()->pct()}}% +
+
+
{{mf($piggyBank->targetamount)}}
+ +
+ @if($accounts[$piggyBank->account_id]['account']->leftOnAccount > 0) + Add money + @endif + @if($piggyBank->currentRelevantRep()->currentamount > 0) + Remove money + @endif +
+
+

+ @if(!is_null($piggyBank->targetdate)) + Target date: {{$piggyBank->targetdate->format('M jS, Y')}}
+ @endif + @if(!is_null($piggyBank->reminder)) + Next reminder: TODO + @endif +

+ +
+
+ + +
+
+ @endif + @endforeach + @endif +
+
+
+
+

Current repeated expenses

+ @if($countRepeating == 0) +

No repeated expenses found.

+ @else + @foreach($piggybanks as $repeated) + @if($repeated->repeats == 1) +

{{{$repeated->name}}}

+ + + + + + + + + + + + + +
{{mf($repeated->currentRelevantRep()->currentamount)}} +
+
+ {{$repeated->currentRelevantRep()->pct()}}% +
+
+
{{mf($repeated->targetamount)}}
+ +
+ @if($accounts[$repeated->account_id]['account']->leftOnAccount > 0) + Add money + @endif + @if($repeated->currentRelevantRep()->currentamount > 0) + Remove money + @endif + +
+
+ + @if(!is_null($repeated->reminder)) + + Next reminder: TODO + + @endif + + +
+ + +
+
+ @endif + @endforeach +@endif + +
+
+ + + + + + + +--}} +
+
+

Account information

+ + + + + + + + + @foreach($accounts as $account) + + + + + + + + @endforeach +
AccountLeft for piggy banksTotal planned savingsSaved so farLeft to save
{{{$account['account']->name}}}{{mf($account['left'])}}{{mf($account['tosave'])}}{{mf($account['saved'])}}{{mf($account['tosave']-$account['saved'])}}
+
+
+ + + + + + +@stop diff --git a/app/views/piggybanks/index.blade.php b/app/views/piggybanks/index.blade.php index bb4af677cf..4adaf1f253 100644 --- a/app/views/piggybanks/index.blade.php +++ b/app/views/piggybanks/index.blade.php @@ -1,173 +1,112 @@ @extends('layouts.default') @section('content') - -
-
-

Current piggy banks

- @if($countNonRepeating == 0) -

No piggy banks found.

- @else - @foreach($piggybanks as $piggyBank) - @if($piggyBank->repeats == 0) -

{{{$piggyBank->name}}}

- - - - - - - - - - - - -
{{mf($piggyBank->currentRelevantRep()->currentamount)}} -
-
- {{$piggyBank->currentRelevantRep()->pct()}}% -
-
-
{{mf($piggyBank->targetamount)}}
- -
- @if($accounts[$piggyBank->account_id]['account']->leftOnAccount > 0) - Add money - @endif - @if($piggyBank->currentRelevantRep()->currentamount > 0) - Remove money - @endif -
-
-

- @if(!is_null($piggyBank->targetdate)) - Target date: {{$piggyBank->targetdate->format('M jS, Y')}}
+ @foreach($piggybanks as $piggybank) +

+
+ +
+
+
+ {{mf($piggybank->savedSoFar,true)}} +
+
+
+
percentage == 100) + class="progress-bar progress-bar-success" + @else + class="progress-bar progress-bar-info" @endif - @if(!is_null($piggyBank->reminder)) - Next reminder: TODO - @endif -

- -
-
- - -
-
- @endif - @endforeach - @endif -
-
-
-
-

Current repeated expenses

- @if($countRepeating == 0) -

No repeated expenses found.

- @else - @foreach($piggybanks as $repeated) - @if($repeated->repeats == 1) -

{{{$repeated->name}}}

- - - - - - - - - - - - - -
{{mf($repeated->currentRelevantRep()->currentamount)}} -
-
- {{$repeated->currentRelevantRep()->pct()}}% + role="progressbar" aria-valuenow="{{$piggybank->percentage}}" aria-valuemin="0" aria-valuemax="100" style="width: {{$piggybank->percentage}}%;"> + {{$piggybank->percentage}}%
-
{{mf($repeated->targetamount)}}
- -
- @if($accounts[$repeated->account_id]['account']->leftOnAccount > 0) - Add money - @endif - @if($repeated->currentRelevantRep()->currentamount > 0) - Remove money - @endif - -
-
- - @if(!is_null($repeated->reminder)) - - Next reminder: TODO - - @endif - - + +
+ {{mf($piggybank->targetamount,true)}} +
+ +
+
- - + @if($piggybank->leftToSave > 0) + + @endif +
-
- @endif +
+
+
+ + +
+
+
+ @if($piggybank->leftToSave > 0) + {{mf($piggybank->leftToSave)}} + @endif +
+
+
+
+
@endforeach -@endif - -
-
- -
-
-

Account information

- - - - - - @foreach($accounts as $account) - - - - - @endforeach -
AccountLeft for piggy banks
{{{$account['account']->name}}}{{mf($account['left'])}}
-
-
- - - - -
{{Form::token()}} diff --git a/app/views/piggybanks/remove.blade.php b/app/views/piggybanks/remove.blade.php new file mode 100644 index 0000000000..c64ae65d12 --- /dev/null +++ b/app/views/piggybanks/remove.blade.php @@ -0,0 +1,24 @@ + +{{Form::token()}} + +
\ No newline at end of file diff --git a/app/views/piggybanks/show.blade.php b/app/views/piggybanks/show.blade.old.php similarity index 98% rename from app/views/piggybanks/show.blade.php rename to app/views/piggybanks/show.blade.old.php index 90c84704e2..1f5faf4aa9 100644 --- a/app/views/piggybanks/show.blade.php +++ b/app/views/piggybanks/show.blade.old.php @@ -15,7 +15,7 @@ @endif - +

General information

diff --git a/app/views/preferences/index.blade.php b/app/views/preferences/index.blade.php index f9afac5501..b9b273a565 100644 --- a/app/views/preferences/index.blade.php +++ b/app/views/preferences/index.blade.php @@ -3,7 +3,7 @@ {{Form::open(['class' => 'form-horizontal'])}} - +
diff --git a/app/views/recurring/edit.blade.php b/app/views/recurring/edit.blade.php index f4be05b370..d9680c93f1 100644 --- a/app/views/recurring/edit.blade.php +++ b/app/views/recurring/edit.blade.php @@ -21,7 +21,7 @@

diff --git a/app/views/reports/index.blade.php b/app/views/reports/index.blade.php index ff79240f6f..4511ce7b3c 100644 --- a/app/views/reports/index.blade.php +++ b/app/views/reports/index.blade.php @@ -1,10 +1,19 @@ @extends('layouts.default') @section('content')
-
-

- Here be content. -

+
+
+
+ Yearly reports +
+
+
    + @foreach($years as $year) +
  • {{$year}}
  • + @endforeach +
+
+
@stop \ No newline at end of file diff --git a/app/views/reports/year.blade.php b/app/views/reports/year.blade.php new file mode 100644 index 0000000000..ffbd8ab0cc --- /dev/null +++ b/app/views/reports/year.blade.php @@ -0,0 +1,97 @@ +@extends('layouts.default') +@section('content') +
+
+
+
+ Income vs. expenses +
+
+
+
+
+
+
+
+
+ Income vs. expenses +
+
+
+
+
+
+
+ +
+
+
+
+ Summary +
+
+ + + + @foreach($summary as $entry) + + @endforeach + + + + + + @foreach($summary as $entry) + + + @endforeach + + + + + @foreach($summary as $entry) + + + @endforeach + + + + @foreach($summary as $entry) + + @endforeach + + +
{{$entry['month']}}Sum
In{{mf($entry['income'])}}{{mf($inSum)}}
Out{{mf($entry['expense']*-1)}}{{mf($outSum)}}
Difference{{mf($entry['income']- $entry['expense'])}}{{mf($inSum + $outSum)}}
+
+
+
+
+ +
+
+
+
+ Budgets +
+
+
+
+
+
+
+ +@stop +@section('scripts') + + +{{HTML::script('assets/javascript/firefly/gcharts.options.js')}} +{{HTML::script('assets/javascript/firefly/gcharts.js')}} + + + +{{HTML::script('assets/javascript/firefly/reports.js')}} + +@stop \ No newline at end of file diff --git a/app/views/search/index.blade.php b/app/views/search/index.blade.php index 5ac821defb..c8940ac822 100644 --- a/app/views/search/index.blade.php +++ b/app/views/search/index.blade.php @@ -1,7 +1,7 @@ @extends('layouts.default') @section('content') @if(!is_null($query)) -
+
@if(isset($result['transactions']) && $result['transactions']->count() > 0)
diff --git a/app/views/start.blade.php b/app/views/start.blade.php index 2f1821e78c..907a489bd8 100644 --- a/app/views/start.blade.php +++ b/app/views/start.blade.php @@ -12,7 +12,7 @@

Welcome to Firefly! To get started, choose either of the two options below. -

+

diff --git a/bootstrap/start.php b/bootstrap/start.php index 7edb58f2c9..1e76ce97dd 100644 --- a/bootstrap/start.php +++ b/bootstrap/start.php @@ -9,16 +9,16 @@ if (!function_exists('mf')) { $string = number_format($n, 2, ',', '.'); if ($coloured === true && $n === 0.0) { - return '€ ' . $string . ''; + return '€ ' . $string . ''; } if ($coloured === true && $n > 0) { - return '€ ' . $string . ''; + return '€ ' . $string . ''; } if ($coloured === true && $n < 0) { - return '€ ' . $string . ''; + return '€ ' . $string . ''; } - return '€ ' . $string; + return '€ ' . $string; } } diff --git a/composer.json b/composer.json index dbbe1678bf..df668992fe 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ "laravel/framework": "4.2.*", "laravelbook/ardent": "~2.4", "davejamesmiller/laravel-breadcrumbs": "2.*", - "rcrowe/twigbridge": "0.6.*" + "rcrowe/twigbridge": "0.6.*", + "grumpydictator/gchart": "dev-master" }, "require-dev": { "barryvdh/laravel-debugbar": "@stable", diff --git a/public/assets/images/error.png b/public/assets/images/error.png new file mode 100755 index 0000000000..628cf2dae3 Binary files /dev/null and b/public/assets/images/error.png differ diff --git a/public/assets/javascript/firefly/accounts.js b/public/assets/javascript/firefly/accounts.js index a5fc3b60f3..2387946845 100644 --- a/public/assets/javascript/firefly/accounts.js +++ b/public/assets/javascript/firefly/accounts.js @@ -1,77 +1,20 @@ $(function () { -if($('#chart').length == 1) { - /** - * get data from controller for home charts: - */ - $.getJSON('chart/home/account/' + accountID).success(function (data) { - var options = { - chart: { - renderTo: 'chart', - type: 'spline' - }, - - series: data.series, - title: { - text: null - }, - yAxis: { - allowDecimals: false, - labels: { - formatter: function () { - if(this.value >= 1000 || this.value <= -1000) { - return '\u20AC ' + (this.value / 1000) + 'k'; - } - return '\u20AC ' + this.value; - - } - }, - title: {text: null} - }, - xAxis: { - type: 'datetime', - dateTimeLabelFormats: { - day: '%e %b', - week: '%e %b' - }, - title: { - text: null - } - }, - legend: {enabled:false}, - tooltip: { - formatter: function () { - return this.series.name + ': \u20AC ' + Highcharts.numberFormat(this.y,2); - } - }, - plotOptions: { - line: { - shadow: true - }, - series: { - cursor: 'pointer', - negativeColor: '#FF0000', - threshold: 0, - lineWidth: 1, - marker: { - radius: 0 - }, - point: { - events: { - click: function (e) { - alert('click!'); - } - } - } - } - }, - credits: { - enabled: false - } - }; - $('#chart').highcharts(options); - }); -} - + if (typeof(googleLineChart) == "function" && typeof accountID != 'undefined') { + googleLineChart('chart/account/' + accountID, 'overview-chart'); + } + // + if (typeof(googleSankeyChart) == 'function' && typeof accountID != 'undefined') { + googleSankeyChart('chart/sankey/' + accountID + '/out', 'account-out-sankey'); + googleSankeyChart('chart/sankey/' + accountID + '/in', 'account-in-sankey'); + } + if (typeof(googleTable) == 'function') { + if (typeof accountID != 'undefined') { + googleTable('table/account/' + accountID + '/transactions', 'account-transactions'); + } + if (typeof what != 'undefined') { + googleTable('table/accounts/' + what, 'account-list'); + } + } }); \ No newline at end of file diff --git a/public/assets/javascript/firefly/budgets.js b/public/assets/javascript/firefly/budgets.js new file mode 100644 index 0000000000..a6de8b79cc --- /dev/null +++ b/public/assets/javascript/firefly/budgets.js @@ -0,0 +1,175 @@ +$(function () { + updateRanges(); + $('input[type="range"]').change(updateSingleRange); + $('input[type="range"]').on('input', updateSingleRange); + $('input[type="number"]').on('change', updateSingleRange); + $('input[type="number"]').on('input', updateSingleRange); + $('.updateIncome').on('click', updateIncome); + + + if (typeof(googleTable) == 'function') { + if (typeof budgetID != 'undefined' && typeof repetitionID == 'undefined') { + googleTable('table/budget/' + budgetID + '/0/transactions', 'transactions'); + googleColumnChart('chart/budgets/'+budgetID+'/spending/2014','budgetOverview'); + + } else if (typeof budgetID != 'undefined' && typeof repetitionID != 'undefined') { + googleTable('table/budget/' + budgetID + '/' + repetitionID + '/transactions', 'transactions'); + } + } + +}); + + +function updateSingleRange(e) { + // get some values: + var input = $(e.target); + var id = input.data('id'); + var value = parseInt(input.val()); + + var spent = parseFloat($('#spent-' + id).data('value')); + console.log('Spent vs budgeted: ' + spent + ' vs ' + value) + + // update small display: + if (value > 0) { + // show the input: + $('#budget-info-' + id + ' span').show(); + $('#budget-info-' + id + ' input').show(); + + // update the text: + $('#budget-description-' + id).text('Budgeted: '); + + //if(value < spent) { + // $('#budgeted-' + id).html('Budgeted: \u20AC ' + value.toFixed(2) + ''); + //} else { + // $('#budgeted-' + id).html('Budgeted: \u20AC ' + value.toFixed(2) + ''); + //} + } else { + console.log('Set to zero!'); + // hide the input: + $('#budget-info-' + id + ' span').hide(); + $('#budget-info-' + id + ' input').hide(); + + // update the text: + $('#budget-description-' + id).html('No budget'); + + //$('#budgeted-' + id).html('No budget'); + } + // update the range display text: + $('#budget-range-display-' + id).text('\u20AC ' + value.toFixed(2)); + + // send a post to Firefly to update the amount: + console.log('Value is: ' + value); + $.post('budgets/amount/' + id, {amount: value}).success(function (data) { + console.log('Budget ' + data.name + ' updated!'); + // update the link if relevant: + $('#budget-link-' + id).attr('href', 'budgets/show/' + id + '/' + data.repetition); + }); + if (input.attr('type') == 'number') { + // update the range-input: + $('#budget-range-' + id).val(value); + } else { + // update the number-input: + $('#budget-info-' + id + ' input').val(value); + } + + // update or hide the bar, whichever is necessary. + + updateTotal(); + return value; +} + +function updateTotal() { + var sum = 0; + $('input[type="range"]').each(function (i, v) { + // get some values: + sum += parseInt($(v).val()); + }); + + /** + * Update total sum: + */ + var totalAmount = parseInt($('#totalAmount').data('value')); + if (sum <= totalAmount) { + var pct = sum / totalAmount * 100; + $('#progress-bar-default').css('width', pct + '%'); + $('#progress-bar-warning').css('width', '0'); + $('#progress-bar-danger').css('width', '0'); + $('#budgetedAmount').text('\u20AC ' + sum.toFixed(2)).addClass('text-success').removeClass('text-danger'); + } else { + // we gaan er X overheen, + + var pct = totalAmount / sum * 100; + console.log(pct) + var danger = 100 - pct; + var err = 100 - danger; + $('#progress-bar-default').css('width', 0); + $('#progress-bar-warning').css('width', err + '%'); + $('#progress-bar-danger').css('width', danger + '%'); + $('#budgetedAmount').text('\u20AC ' + sum.toFixed(2)).addClass('text-danger').removeClass('text-success'); + } + +} + +function updateIncome(e) { + $('#monthlyBudgetModal').empty().load('budgets/income').modal('show'); + + return false; +} + +function updateRanges() { + /** + * Update all ranges. + */ + var sum = 0; + $('input[type="range"]').each(function (i, v) { + // get some values: + var input = $(v); + var id = input.data('id'); + var value = parseInt(input.val()); + + // calculate sum: + sum += value + + // update small display: + $('#budget-range-display-' + id).text('\u20AC ' + value.toFixed(2)); + + // update progress bar (if relevant) + var barHolder = $('#budget-progress-' + id); + var spent = parseFloat(barHolder.data('spent')); + if (value > 0 && spent > 0) { + console.log('Add bar') + //barHolder.append($('
')); + } + + // send a post to Firefly to update the amount: + $.post('budgets/amount/' + id, {amount: value}).success(function (data) { + console.log('Budget ' + data.name + ' updated!'); + }); + + }); + + /** + * Update total sum: + */ + var totalAmount = parseInt($('#totalAmount').data('value')); + if (sum <= totalAmount) { + var pct = sum / totalAmount * 100; + $('#progress-bar-default').css('width', pct + '%'); + $('#progress-bar-warning').css('width', '0'); + $('#progress-bar-danger').css('width', '0'); + $('#budgetedAmount').text('\u20AC ' + sum.toFixed(2)).addClass('text-success').removeClass('text-danger'); + } else { + // we gaan er X overheen, + + var pct = totalAmount / sum * 100; + console.log(pct) + var danger = 100 - pct; + var err = 100 - danger; + $('#progress-bar-default').css('width', 0); + $('#progress-bar-warning').css('width', err + '%'); + $('#progress-bar-danger').css('width', danger + '%'); + $('#budgetedAmount').text('\u20AC ' + sum.toFixed(2)).addClass('text-danger').removeClass('text-success'); + } + + +} \ No newline at end of file diff --git a/public/assets/javascript/firefly/gcharts.js b/public/assets/javascript/firefly/gcharts.js new file mode 100644 index 0000000000..b17ab935e1 --- /dev/null +++ b/public/assets/javascript/firefly/gcharts.js @@ -0,0 +1,263 @@ +google.load('visualization', '1.1', {'packages': ['corechart', 'bar', 'sankey', 'table']}); + +function googleLineChart(URL, container) { + $.getJSON(URL).success(function (data) { + /* + Get the data from the JSON + */ + gdata = new google.visualization.DataTable(data); + + /* + Format as money + */ + var money = new google.visualization.NumberFormat({decimalSymbol: ',', groupingSymbol: '.', prefix: '\u20AC '}); + for (i = 1; i < gdata.getNumberOfColumns(); i++) { + money.format(gdata, i); + } + + /* + Create a new google charts object. + */ + var chart = new google.visualization.LineChart(document.getElementById(container)); + + /* + Draw it: + */ + chart.draw(gdata, defaultLineChartOptions); + + }).fail(function () { + $('#' + container).addClass('google-chart-error'); + }); +} + +function googleBarChart(URL, container) { + $.getJSON(URL).success(function (data) { + /* + Get the data from the JSON + */ + gdata = new google.visualization.DataTable(data); + + /* + Format as money + */ + var money = new google.visualization.NumberFormat({decimalSymbol: ',', groupingSymbol: '.', prefix: '\u20AC '}); + for (i = 1; i < gdata.getNumberOfColumns(); i++) { + money.format(gdata, i); + } + + /* + Create a new google charts object. + */ + var chart = new google.charts.Bar(document.getElementById(container)); + + /* + Draw it: + */ + chart.draw(gdata, defaultBarChartOptions); + + }).fail(function () { + $('#' + container).addClass('google-chart-error'); + }); +} + +function googleColumnChart(URL, container) { + $.getJSON(URL).success(function (data) { + /* + Get the data from the JSON + */ + gdata = new google.visualization.DataTable(data); + + /* + Format as money + */ + var money = new google.visualization.NumberFormat({decimalSymbol: ',', groupingSymbol: '.', prefix: '\u20AC '}); + for (i = 1; i < gdata.getNumberOfColumns(); i++) { + money.format(gdata, i); + } + + /* + Create a new google charts object. + */ + var chart = new google.charts.Bar(document.getElementById(container)); + /* + Draw it: + */ + chart.draw(gdata, defaultColumnChartOptions); + + }).fail(function () { + $('#' + container).addClass('google-chart-error'); + }); +} + +function googleStackedColumnChart(URL, container) { + $.getJSON(URL).success(function (data) { + /* + Get the data from the JSON + */ + gdata = new google.visualization.DataTable(data); + + /* + Format as money + */ + var money = new google.visualization.NumberFormat({decimalSymbol: ',', groupingSymbol: '.', prefix: '\u20AC '}); + for (i = 1; i < gdata.getNumberOfColumns(); i++) { + money.format(gdata, i); + } + + /* + Create a new google charts object. + */ + var chart = new google.visualization.ColumnChart(document.getElementById(container)); + /* + Draw it: + */ + chart.draw(gdata, defaultStackedColumnChartOptions); + + }).fail(function () { + $('#' + container).addClass('google-chart-error'); + }); +} + +function googlePieChart(URL, container) { + $.getJSON(URL).success(function (data) { + /* + Get the data from the JSON + */ + gdata = new google.visualization.DataTable(data); + + /* + Format as money + */ + var money = new google.visualization.NumberFormat({decimalSymbol: ',', groupingSymbol: '.', prefix: '\u20AC '}); + for (i = 1; i < gdata.getNumberOfColumns(); i++) { + money.format(gdata, i); + } + + /* + Create a new google charts object. + */ + var chart = new google.visualization.PieChart(document.getElementById(container)); + + /* + Draw it: + */ + chart.draw(gdata, defaultPieChartOptions); + + }).fail(function () { + $('#' + container).addClass('google-chart-error'); + }); +} + +function googleSankeyChart(URL, container) { + $.getJSON(URL).success(function (data) { + /* + Get the data from the JSON + */ + gdata = new google.visualization.DataTable(data); + + /* + Format as money + */ + + console.log(gdata.getNumberOfRows()) + if (gdata.getNumberOfRows() < 1) { + console.log('remove'); + $('#' + container).parent().parent().remove(); + return; + } else if (gdata.getNumberOfRows() < 6) { + defaultSankeyChartOptions.height = 100 + } else { + defaultSankeyChartOptions.height = 400 + } + + + /* + Create a new google charts object. + */ + var chart = new google.visualization.Sankey(document.getElementById(container)); + + /* + Draw it: + */ + chart.draw(gdata, defaultSankeyChartOptions); + + }).fail(function () { + $('#' + container).addClass('google-chart-error'); + }); +} + +function googleTable(URL, container) { + $.getJSON(URL).success(function (data) { + /* + Get the data from the JSON + */ + var gdata = new google.visualization.DataTable(data); + + /* + Create a new google charts object. + */ + var chart = new google.visualization.Table(document.getElementById(container)); + + /* + Do something with formatters: + */ + var x = gdata.getNumberOfColumns(); + var columnsToHide = new Array; + var URLFormatter = new google.visualization.PatternFormat('{1}'); + + var EditButtonFormatter = new google.visualization.PatternFormat('
'); + + var money = new google.visualization.NumberFormat({decimalSymbol: ',', groupingSymbol: '.', prefix: '\u20AC '}); + + + for (var i = 0; i < x; i++) { + var label = gdata.getColumnLabel(i); + console.log('Column ' + i + ':' + label); + /* + Format a string using the previous column as URL. + */ + if (label == 'Description' || label == 'From' || label == 'Name' || label == 'To' || label == 'Budget' || label == 'Category') { + URLFormatter.format(gdata, [i - 1, i], i); + columnsToHide.push(i - 1); + } + if (label == 'ID') { + EditButtonFormatter.format(gdata, [i + 1, i + 2], i); + columnsToHide.push(i + 1, i + 2); + } + + /* + Format with buttons: + */ + + + /* + Format as money + */ + if (label == 'Amount' || label == 'Balance') { + money.format(gdata, i); + } + + } + + + //var formatter = new google.visualization.PatternFormat('{1}'); + + //formatter.format(gdata, [5, 6], 6); + //formatter.format(gdata, [7, 8], 8); + + + var view = new google.visualization.DataView(gdata); + // hide certain columns: + + view.hideColumns(columnsToHide); + + + /* + Draw it: + */ + chart.draw(view, defaultTableOptions); + + }).fail(function () { + $('#' + container).addClass('google-chart-error'); + }); +} \ No newline at end of file diff --git a/public/assets/javascript/firefly/gcharts.options.js b/public/assets/javascript/firefly/gcharts.options.js new file mode 100644 index 0000000000..acba8a81bb --- /dev/null +++ b/public/assets/javascript/firefly/gcharts.options.js @@ -0,0 +1,123 @@ +var defaultLineChartOptions = { + curveType: 'function', + legend: { + position: 'none' + }, + interpolateNulls: true, + lineWidth: 1, + chartArea: { + left: 50, + top: 10, + width: '85%', + height: '80%' + }, + height: 400, + colors: ["#4285f4", "#db4437", "#f4b400", "#0f9d58", "#ab47bc", "#00acc1", "#ff7043", "#9e9d24", "#5c6bc0", "#f06292", "#00796b", "#c2185b"], + hAxis: { + textStyle: { + color: '#838383', + fontName: 'Roboto2', + fontSize: '12' + }, + gridlines: { + color: 'transparent' + } + }, + vAxis: { + textStyle: { + color: '#838383', + fontName: 'Roboto2', + fontSize: '12' + }, + format: '\u20AC #' + } + + +}; + +var defaultBarChartOptions = { + height: 400, + bars: 'horizontal', + hAxis: {format: '\u20AC #'}, + chartArea: { + left: 75, + top: 10, + width: '100%', + height: '90%' + }, + + legend: { + position: 'none' + }, +}; + +var defaultColumnChartOptions = { + height: 400, + chartArea: { + left: 50, + top: 10, + width: '85%', + height: '80%' + }, + vAxis: {format: '\u20AC #'}, + legend: { + position: 'none' + }, +}; + +var defaultStackedColumnChartOptions = { + height: 400, + chartArea: { + left: 50, + top: 10, + width: '85%', + height: '80%' + }, + vAxis: {format: '\u20AC #'}, + legend: { + position: 'none' + }, + isStacked: true, + colors: ["#4285f4", "#db4437", "#f4b400", "#0f9d58", "#ab47bc", "#00acc1", "#ff7043", "#9e9d24", "#5c6bc0", "#f06292", "#00796b", "#c2185b"], + vAxis: { + textStyle: { + color: '#838383', + fontName: 'Roboto2', + fontSize: '12' + }, + format: '\u20AC #' + }, + hAxis: { + textStyle: { + color: '#838383', + fontName: 'Roboto2', + fontSize: '12' + }, + gridlines: { + color: 'transparent' + } + }, +}; + +var defaultPieChartOptions = { + chartArea: { + left: 0, + top: 0, + width: '100%', + height: '100%' + }, + height: 200, + legend: { + position: 'none' + }, + colors: ["#4285f4", "#db4437", "#f4b400", "#0f9d58", "#ab47bc", "#00acc1", "#ff7043", "#9e9d24", "#5c6bc0", "#f06292", "#00796b", "#c2185b"] +}; + +var defaultSankeyChartOptions = { + height: 400 +} +var defaultTableOptions = { + allowHtml: true, + page: 'enable', + pageSize: 50 +}; \ No newline at end of file diff --git a/public/assets/javascript/firefly/index.js b/public/assets/javascript/firefly/index.js index b8d5efda98..1b2831aafe 100644 --- a/public/assets/javascript/firefly/index.js +++ b/public/assets/javascript/firefly/index.js @@ -1,256 +1,43 @@ +google.setOnLoadCallback(drawChart); + + +function drawChart() { + googleLineChart('chart/home/account', 'accounts-chart'); + googleBarChart('chart/home/budgets','budgets-chart'); + googleColumnChart('chart/home/categories','categories-chart'); + googlePieChart('chart/home/recurring','recurring-chart') +} + + + $(function () { - /** - * get data from controller for home charts: - */ - $.getJSON('chart/home/account').success(function (data) { - var options = { - chart: { - renderTo: 'accounts-chart', - type: 'line' - }, - series: data.series, - title: { - text: null - }, - yAxis: { - allowDecimals: false, - labels: { - formatter: function () { - if (this.value >= 1000 || this.value <= -1000) { - return '\u20AC ' + (this.value / 1000) + 'k'; - } - return '\u20AC ' + this.value; - } - }, - title: {text: null} - }, - xAxis: { - type: 'datetime', - dateTimeLabelFormats: { - day: '%e %b', - week: '%e %b' - }, - title: { - text: null - } - }, - legend: {enabled: false}, - tooltip: { - formatter: function () { - return this.series.name + ': \u20AC ' + Highcharts.numberFormat(this.y, 2); - } - }, - plotOptions: { - line: { - shadow: true - }, - series: { - cursor: 'pointer', - negativeColor: '#FF0000', - threshold: 0, - lineWidth: 1, - marker: { - radius: 0 - }, - point: { - events: { - click: function (e) { - alert('click!'); - } - } - } - } - }, - credits: { - enabled: false - } - }; - $('#accounts-chart').highcharts(options); - }); - - /** - * Get chart data for categories chart: - */ - $.getJSON('chart/home/categories').success(function (data) { - $('#categories').highcharts({ - chart: { - type: 'column' - }, - title: { - text: null - }, - credits: { - enabled: false - }, - xAxis: { - type: 'category', - labels: { - rotation: -45, - style: { - fontSize: '12px', - fontFamily: 'Verdana, sans-serif' - } - } - }, - yAxis: { - min: 0, - title: { - text: null - }, - labels: { - formatter: function () { - if (this.value >= 1000 || this.value <= -1000) { - return '\u20AC ' + (this.value / 1000) + 'k'; - } - return '\u20AC ' + this.value; - - } - }, - }, - legend: { - enabled: false - }, - tooltip: { - pointFormat: 'Total expense: \u20AC {point.y:.2f}', - }, - plotOptions: { - column: { - cursor: 'pointer' - } - }, - series: [ - { - name: 'Population', - data: data, - - events: { - click: function (e) { - alert('klik!'); - } - }, - dataLabels: { - enabled: false - } - } - ] - }); - }); - - /** - * Get chart data for budget charts. - */ - $.getJSON('chart/home/budgets').success(function (data) { - $('#budgets').highcharts({ - chart: { - type: 'bar' - }, - title: { - text: null - }, - subtitle: { - text: null - }, - xAxis: { - categories: data.labels, - title: { - text: null - }, - labels: { - style: { - fontSize: '11px', - } - } - }, - tooltip: { - formatter: function () { - return this.series.name + ': \u20AC ' + Highcharts.numberFormat(this.y, 2); - } - }, - yAxis: { - min: 0, - title: {text: null}, - - labels: { - overflow: 'justify', - formatter: function () { - if (this.value >= 1000 || this.value <= -1000) { - return '\u20AC ' + (this.value / 1000) + 'k'; - } - return '\u20AC ' + this.value; - - } - } - }, - plotOptions: { - bar: { - cursor: 'pointer', - events: { - click: function (e) { - if (e.point.url != null) { - window.location = e.point.url; - } - } - }, - dataLabels: { - enabled: true, - formatter: function () { - return '\u20AC ' + Highcharts.numberFormat(this.y, 2); - } - } - } - }, - legend: { - enabled: false - }, - credits: { - enabled: false - }, - series: data.series - }); - }); - - $.getJSON('chart/home/recurring').success(function (data) { - if (data[0].data.length == 0) { - $('#recurring').parent().parent().remove(); - } else { - $('#recurring').highcharts({ - title: { - text: null - }, - credits: { - enabled: false - }, - tooltip: { - formatter: function () { - if (this.point.objType == 'paid') { - return this.key + ': \u20AC ' + Highcharts.numberFormat(this.y, 2); - } else { - return this.key + ': ~\u20AC ' + Highcharts.numberFormat(this.y, 2); - } - - } - }, - plotOptions: { - pie: { - events: { - click: function (e) { - if (e.point.url != null) { - window.location = e.point.url; - } - } - }, - allowPointSelect: true, - cursor: 'pointer', - dataLabels: { - enabled: false - } - } - }, - series: data - }); - } - }); + ////googleLineChart(); + ///** + // * get data from controller for home charts: + // */ + //$.getJSON('chart/home/account').success(function (data) { + // //$('#accounts-chart').highcharts(options); + //}); + // + ///** + // * Get chart data for categories chart: + // */ + //$.getJSON('chart/home/categories').success(function (data) { + // //$('#categories-chart'); + //}); + // + ///** + // * Get chart data for budget charts. + // */ + //$.getJSON('chart/home/budgets').success(function (data) { + // //$('#budgets-chart'); + // + //}); + // + //$.getJSON('chart/home/recurring').success(function (data) { + // //$('#recurring-chart'); + //}); }); \ No newline at end of file diff --git a/public/assets/javascript/firefly/piggybanks.js b/public/assets/javascript/firefly/piggybanks.js new file mode 100644 index 0000000000..3d4d4739c0 --- /dev/null +++ b/public/assets/javascript/firefly/piggybanks.js @@ -0,0 +1,18 @@ +$(function () { + $('.addMoney').on('click',addMoney); + $('.removeMoney').on('click',removeMoney); +}); + +function addMoney(e) { + var pigID = parseInt($(e.target).data('id')); + $('#moneyManagementModal').empty().load('piggybanks/add/' + pigID).modal('show'); + + return false; +} + +function removeMoney(e) { + var pigID = parseInt($(e.target).data('id')); + $('#moneyManagementModal').empty().load('piggybanks/remove/' + pigID).modal('show'); + + return false; +} \ No newline at end of file diff --git a/public/assets/javascript/firefly/reports.js b/public/assets/javascript/firefly/reports.js new file mode 100644 index 0000000000..6977f38f57 --- /dev/null +++ b/public/assets/javascript/firefly/reports.js @@ -0,0 +1,8 @@ +google.setOnLoadCallback(drawChart); + + +function drawChart() { + googleColumnChart('chart/reports/income-expenses/' + year, 'income-expenses-chart'); + googleColumnChart('chart/reports/income-expenses-sum/' + year, 'income-expenses-sum-chart') + googleStackedColumnChart('chart/reports/budgets/' + year, 'budgets'); +} \ No newline at end of file diff --git a/public/assets/javascript/highcharts/highcharts.js b/public/assets/javascript/highcharts/highcharts.js deleted file mode 100644 index d37670c432..0000000000 --- a/public/assets/javascript/highcharts/highcharts.js +++ /dev/null @@ -1,307 +0,0 @@ -/* - Highcharts JS v4.0.3 (2014-07-03) - - (c) 2009-2014 Torstein Honsi - - License: www.highcharts.com/license -*/ -(function(){function r(a,b){var c;a||(a={});for(c in b)a[c]=b[c];return a}function w(){var a,b=arguments,c,d={},e=function(a,b){var c,d;typeof a!=="object"&&(a={});for(d in b)b.hasOwnProperty(d)&&(c=b[d],a[d]=c&&typeof c==="object"&&Object.prototype.toString.call(c)!=="[object Array]"&&d!=="renderTo"&&typeof c.nodeType!=="number"?e(a[d]||{},c):b[d]);return a};b[0]===!0&&(d=b[1],b=Array.prototype.slice.call(b,2));c=b.length;for(a=0;a3?c.length%3:0;return e+(g?c.substr(0,g)+d:"")+c.substr(g).replace(/(\d{3})(?=\d)/g,"$1"+d)+(f?b+Q(a-c).toFixed(f).slice(2):"")}function Ha(a,b){return Array((b||2)+1-String(a).length).join(0)+a}function Ma(a,b,c){var d=a[b];a[b]=function(){var a=Array.prototype.slice.call(arguments);a.unshift(d); -return c.apply(this,a)}}function Ia(a,b){for(var c="{",d=!1,e,f,g,h,i,j=[];(c=a.indexOf(c))!==-1;){e=a.slice(0,c);if(d){f=e.split(":");g=f.shift().split(".");i=g.length;e=b;for(h=0;h-1?h.thousandsSep:""))):e=bb(f,e)}j.push(e);a=a.slice(c+1);c=(d=!d)?"}":"{"}j.push(a);return j.join("")}function lb(a){return V.pow(10,U(V.log(a)/V.LN10))} -function mb(a,b,c,d){var e,c=p(c,1);e=a/c;b||(b=[1,2,2.5,5,10],d&&d.allowDecimals===!1&&(c===1?b=[1,2,5,10]:c<=0.1&&(b=[1/c])));for(d=0;dc&&(c=a[b]); -return c}function Oa(a,b){for(var c in a)a[c]&&a[c]!==b&&a[c].destroy&&a[c].destroy(),delete a[c]}function Pa(a){cb||(cb=$(Ja));a&&cb.appendChild(a);cb.innerHTML=""}function ea(a){return parseFloat(a.toPrecision(14))}function Qa(a,b){va=p(a,b.animation)}function Ab(){var a=L.global.useUTC,b=a?"getUTC":"get",c=a?"setUTC":"set";Ra=(a&&L.global.timezoneOffset||0)*6E4;db=a?Date.UTC:function(a,b,c,g,h,i){return(new Date(a,b,p(c,1),p(g,0),p(h,0),p(i,0))).getTime()};ob=b+"Minutes";pb=b+"Hours";qb=b+"Day"; -Wa=b+"Date";eb=b+"Month";fb=b+"FullYear";Bb=c+"Minutes";Cb=c+"Hours";rb=c+"Date";Db=c+"Month";Eb=c+"FullYear"}function G(){}function Sa(a,b,c,d){this.axis=a;this.pos=b;this.type=c||"";this.isNew=!0;!c&&!d&&this.addLabel()}function ma(){this.init.apply(this,arguments)}function Xa(){this.init.apply(this,arguments)}function Fb(a,b,c,d,e){var f=a.chart.inverted;this.axis=a;this.isNegative=c;this.options=b;this.x=d;this.total=null;this.points={};this.stack=e;this.alignOptions={align:b.align||(f?c?"left": -"right":"center"),verticalAlign:b.verticalAlign||(f?"middle":c?"bottom":"top"),y:p(b.y,f?4:c?14:-6),x:p(b.x,f?c?-6:6:0)};this.textAlign=b.textAlign||(f?c?"right":"left":"center")}var t,x=document,H=window,V=Math,v=V.round,U=V.floor,Ka=V.ceil,u=V.max,C=V.min,Q=V.abs,aa=V.cos,fa=V.sin,na=V.PI,Ca=na*2/360,wa=navigator.userAgent,Gb=H.opera,Aa=/msie/i.test(wa)&&!Gb,gb=x.documentMode===8,sb=/AppleWebKit/.test(wa),Ta=/Firefox/.test(wa),Hb=/(Mobile|Android|Windows Phone)/.test(wa),xa="http://www.w3.org/2000/svg", -ba=!!x.createElementNS&&!!x.createElementNS(xa,"svg").createSVGRect,Nb=Ta&&parseInt(wa.split("Firefox/")[1],10)<4,ga=!ba&&!Aa&&!!x.createElement("canvas").getContext,Ya,Za,Ib={},tb=0,cb,L,bb,va,ub,B,oa,sa=function(){return t},W=[],$a=0,Ja="div",P="none",Ob=/^[0-9]+$/,Pb="stroke-width",db,Ra,ob,pb,qb,Wa,eb,fb,Bb,Cb,rb,Db,Eb,J={},S;H.Highcharts?oa(16,!0):S=H.Highcharts={};bb=function(a,b,c){if(!s(b)||isNaN(b))return"Invalid date";var a=p(a,"%Y-%m-%d %H:%M:%S"),d=new Date(b-Ra),e,f=d[pb](),g=d[qb](), -h=d[Wa](),i=d[eb](),j=d[fb](),k=L.lang,l=k.weekdays,d=r({a:l[g].substr(0,3),A:l[g],d:Ha(h),e:h,b:k.shortMonths[i],B:k.months[i],m:Ha(i+1),y:j.toString().substr(2,2),Y:j,H:Ha(f),I:Ha(f%12||12),l:f%12||12,M:Ha(d[ob]()),p:f<12?"AM":"PM",P:f<12?"am":"pm",S:Ha(d.getSeconds()),L:Ha(v(b%1E3),3)},S.dateFormats);for(e in d)for(;a.indexOf("%"+e)!==-1;)a=a.replace("%"+e,typeof d[e]==="function"?d[e](b):d[e]);return c?a.substr(0,1).toUpperCase()+a.substr(1):a};oa=function(a,b){var c="Highcharts error #"+a+": www.highcharts.com/errors/"+ -a;if(b)throw c;H.console&&console.log(c)};B={millisecond:1,second:1E3,minute:6E4,hour:36E5,day:864E5,week:6048E5,month:26784E5,year:31556952E3};ub={init:function(a,b,c){var b=b||"",d=a.shift,e=b.indexOf("C")>-1,f=e?7:3,g,b=b.split(" "),c=[].concat(c),h,i,j=function(a){for(g=a.length;g--;)a[g]==="M"&&a.splice(g+1,0,a[g+1],a[g+2],a[g+1],a[g+2])};e&&(j(b),j(c));a.isArea&&(h=b.splice(b.length-6,6),i=c.splice(c.length-6,6));if(d<=c.length/f&&b.length===c.length)for(;d--;)c=[].concat(c).splice(0,f).concat(c); -a.shift=0;if(b.length)for(a=c.length;b.length{point.key}
',pointFormat:' {series.name}: {point.y}
',shadow:!0,snap:Hb?25:10,style:{color:"#333333",cursor:"default",fontSize:"12px",padding:"8px",whiteSpace:"nowrap"}},credits:{enabled:!0,text:"Highcharts.com",href:"http://www.highcharts.com",position:{align:"right",x:-10,verticalAlign:"bottom",y:-5},style:{cursor:"pointer", -color:"#909090",fontSize:"9px"}}};var ca=L.plotOptions,T=ca.line;Ab();var Tb=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/,Ub=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/,Vb=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/,ya=function(a){var b=[],c,d;(function(a){a&&a.stops?d=Ua(a.stops,function(a){return ya(a[1])}):(c=Tb.exec(a))?b=[z(c[1]),z(c[2]),z(c[3]),parseFloat(c[4],10)]:(c=Ub.exec(a))?b=[z(c[1],16),z(c[2],16),z(c[3], -16),1]:(c=Vb.exec(a))&&(b=[z(c[1]),z(c[2]),z(c[3]),1])})(a);return{get:function(c){var f;d?(f=w(a),f.stops=[].concat(f.stops),q(d,function(a,b){f.stops[b]=[f.stops[b][0],a.get(c)]})):f=b&&!isNaN(b[0])?c==="rgb"?"rgb("+b[0]+","+b[1]+","+b[2]+")":c==="a"?b[3]:"rgba("+b.join(",")+")":a;return f},brighten:function(a){if(d)q(d,function(b){b.brighten(a)});else if(ia(a)&&a!==0){var c;for(c=0;c<3;c++)b[c]+=z(a*255),b[c]<0&&(b[c]=0),b[c]>255&&(b[c]=255)}return this},rgba:b,setOpacity:function(a){b[3]=a;return this}}}; -G.prototype={opacity:1,textProps:"fontSize,fontWeight,fontFamily,color,lineHeight,width,textDecoration,textShadow,HcTextStroke".split(","),init:function(a,b){this.element=b==="span"?$(b):x.createElementNS(xa,b);this.renderer=a},animate:function(a,b,c){b=p(b,va,!0);ab(this);if(b){b=w(b,{});if(c)b.complete=c;ib(this,a,b)}else this.attr(a),c&&c();return this},colorGradient:function(a,b,c){var d=this.renderer,e,f,g,h,i,j,k,l,m,n,o=[];a.linearGradient?f="linearGradient":a.radialGradient&&(f="radialGradient"); -if(f){g=a[f];h=d.gradients;j=a.stops;m=c.radialReference;La(g)&&(a[f]=g={x1:g[0],y1:g[1],x2:g[2],y2:g[3],gradientUnits:"userSpaceOnUse"});f==="radialGradient"&&m&&!s(g.gradientUnits)&&(g=w(g,{cx:m[0]-m[2]/2+g.cx*m[2],cy:m[1]-m[2]/2+g.cy*m[2],r:g.r*m[2],gradientUnits:"userSpaceOnUse"}));for(n in g)n!=="id"&&o.push(n,g[n]);for(n in j)o.push(j[n]);o=o.join(",");h[o]?a=h[o].attr("id"):(g.id=a="highcharts-"+tb++,h[o]=i=d.createElement(f).attr(g).add(d.defs),i.stops=[],q(j,function(a){a[1].indexOf("rgba")=== -0?(e=ya(a[1]),k=e.get("rgb"),l=e.get("a")):(k=a[1],l=1);a=d.createElement("stop").attr({offset:a[0],"stop-color":k,"stop-opacity":l}).add(i);i.stops.push(a)}));c.setAttribute(b,"url("+d.url+"#"+a+")")}},attr:function(a,b){var c,d,e=this.element,f,g=this,h;typeof a==="string"&&b!==t&&(c=a,a={},a[c]=b);if(typeof a==="string")g=(this[a+"Getter"]||this._defaultGetter).call(this,a,e);else{for(c in a){d=a[c];h=!1;this.symbolName&&/^(x|y|width|height|r|start|end|innerR|anchorX|anchorY)/.test(c)&&(f||(this.symbolAttr(a), -f=!0),h=!0);if(this.rotation&&(c==="x"||c==="y"))this.doTransform=!0;h||(this[c+"Setter"]||this._defaultSetter).call(this,d,c,e);this.shadows&&/^(width|height|visibility|x|y|d|transform|cx|cy|r)$/.test(c)&&this.updateShadows(c,d)}if(this.doTransform)this.updateTransform(),this.doTransform=!1}return g},updateShadows:function(a,b){for(var c=this.shadows,d=c.length;d--;)c[d].setAttribute(a,a==="height"?u(b-(c[d].cutHeight||0),0):a==="d"?this.d:b)},addClass:function(a){var b=this.element,c=F(b,"class")|| -"";c.indexOf(a)===-1&&F(b,"class",c+" "+a);return this},symbolAttr:function(a){var b=this;q("x,y,r,start,end,width,height,innerR,anchorX,anchorY".split(","),function(c){b[c]=p(a[c],b[c])});b.attr({d:b.renderer.symbols[b.symbolName](b.x,b.y,b.width,b.height,b)})},clip:function(a){return this.attr("clip-path",a?"url("+this.renderer.url+"#"+a.id+")":P)},crisp:function(a){var b,c={},d,e=a.strokeWidth||this.strokeWidth||0;d=v(e)%2/2;a.x=U(a.x||this.x||0)+d;a.y=U(a.y||this.y||0)+d;a.width=U((a.width||this.width|| -0)-2*d);a.height=U((a.height||this.height||0)-2*d);a.strokeWidth=e;for(b in a)this[b]!==a[b]&&(this[b]=c[b]=a[b]);return c},css:function(a){var b=this.styles,c={},d=this.element,e,f,g="";e=!b;if(a&&a.color)a.fill=a.color;if(b)for(f in a)a[f]!==b[f]&&(c[f]=a[f],e=!0);if(e){e=this.textWidth=a&&a.width&&d.nodeName.toLowerCase()==="text"&&z(a.width);b&&(a=r(b,c));this.styles=a;e&&(ga||!ba&&this.renderer.forExport)&&delete a.width;if(Aa&&!ba)A(this.element,a);else{b=function(a,b){return"-"+b.toLowerCase()}; -for(f in a)g+=f.replace(/([A-Z])/g,b)+":"+a[f]+";";F(d,"style",g)}e&&this.added&&this.renderer.buildText(this)}return this},on:function(a,b){var c=this,d=c.element;Za&&a==="click"?(d.ontouchstart=function(a){c.touchEventFired=Date.now();a.preventDefault();b.call(d,a)},d.onclick=function(a){(wa.indexOf("Android")===-1||Date.now()-(c.touchEventFired||0)>1100)&&b.call(d,a)}):d["on"+a]=b;return this},setRadialReference:function(a){this.element.radialReference=a;return this},translate:function(a,b){return this.attr({translateX:a, -translateY:b})},invert:function(){this.inverted=!0;this.updateTransform();return this},updateTransform:function(){var a=this.translateX||0,b=this.translateY||0,c=this.scaleX,d=this.scaleY,e=this.inverted,f=this.rotation,g=this.element;e&&(a+=this.attr("width"),b+=this.attr("height"));a=["translate("+a+","+b+")"];e?a.push("rotate(90) scale(-1,1)"):f&&a.push("rotate("+f+" "+(g.getAttribute("x")||0)+" "+(g.getAttribute("y")||0)+")");(s(c)||s(d))&&a.push("scale("+p(c,1)+" "+p(d,1)+")");a.length&&g.setAttribute("transform", -a.join(" "))},toFront:function(){var a=this.element;a.parentNode.appendChild(a);return this},align:function(a,b,c){var d,e,f,g,h={};e=this.renderer;f=e.alignedObjects;if(a){if(this.alignOptions=a,this.alignByTranslate=b,!c||Fa(c))this.alignTo=d=c||"renderer",ka(f,this),f.push(this),c=null}else a=this.alignOptions,b=this.alignByTranslate,d=this.alignTo;c=p(c,e[d],e);d=a.align;e=a.verticalAlign;f=(c.x||0)+(a.x||0);g=(c.y||0)+(a.y||0);if(d==="right"||d==="center")f+=(c.width-(a.width||0))/{right:1,center:2}[d]; -h[b?"translateX":"x"]=v(f);if(e==="bottom"||e==="middle")g+=(c.height-(a.height||0))/({bottom:1,middle:2}[e]||1);h[b?"translateY":"y"]=v(g);this[this.placed?"animate":"attr"](h);this.placed=!0;this.alignAttr=h;return this},getBBox:function(){var a=this.bBox,b=this.renderer,c,d,e=this.rotation;c=this.element;var f=this.styles,g=e*Ca;d=this.textStr;var h;if(d===""||Ob.test(d))h="num."+d.toString().length+(f?"|"+f.fontSize+"|"+f.fontFamily:"");h&&(a=b.cache[h]);if(!a){if(c.namespaceURI===xa||b.forExport){try{a= -c.getBBox?r({},c.getBBox()):{width:c.offsetWidth,height:c.offsetHeight}}catch(i){}if(!a||a.width<0)a={width:0,height:0}}else a=this.htmlGetBBox();if(b.isSVG){c=a.width;d=a.height;if(Aa&&f&&f.fontSize==="11px"&&d.toPrecision(3)==="16.9")a.height=d=14;if(e)a.width=Q(d*fa(g))+Q(c*aa(g)),a.height=Q(d*aa(g))+Q(c*fa(g))}this.bBox=a;h&&(b.cache[h]=a)}return a},show:function(a){return a&&this.element.namespaceURI===xa?(this.element.removeAttribute("visibility"),this):this.attr({visibility:a?"inherit":"visible"})}, -hide:function(){return this.attr({visibility:"hidden"})},fadeOut:function(a){var b=this;b.animate({opacity:0},{duration:a||150,complete:function(){b.hide()}})},add:function(a){var b=this.renderer,c=a||b,d=c.element||b.box,e=this.element,f=this.zIndex,g,h;if(a)this.parentGroup=a;this.parentInverted=a&&a.inverted;this.textStr!==void 0&&b.buildText(this);if(f)c.handleZ=!0,f=z(f);if(c.handleZ){a=d.childNodes;for(g=0;gf||!s(f)&&s(c))){d.insertBefore(e, -b);h=!0;break}}h||d.appendChild(e);this.added=!0;if(this.onAdd)this.onAdd();return this},safeRemoveChild:function(a){var b=a.parentNode;b&&b.removeChild(a)},destroy:function(){var a=this,b=a.element||{},c=a.shadows,d=a.renderer.isSVG&&b.nodeName==="SPAN"&&a.parentGroup,e,f;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=b.point=null;ab(a);if(a.clipPath)a.clipPath=a.clipPath.destroy();if(a.stops){for(f=0;f/,i=/<.*href="(http[^"]+)".*>/,l&&!a.added&&this.box.appendChild(b),e=f?e.replace(/<(b|strong)>/g,'').replace(/<(i|em)>/g,'').replace(//g,"").split(//g):[e],e[e.length-1]===""&&e.pop(),q(e,function(e,f){var g,m=0,e=e.replace(//g,"|||");g=e.split("|||");q(g,function(e){if(e!== -""||g.length===1){var n={},o=x.createElementNS(xa,"tspan"),p;h.test(e)&&(p=e.match(h)[1].replace(/(;| |^)color([ :])/,"$1fill$2"),F(o,"style",p));i.test(e)&&!d&&(F(o,"onclick",'location.href="'+e.match(i)[1]+'"'),A(o,{cursor:"pointer"}));e=(e.replace(/<(.|\n)*?>/g,"")||" ").replace(/</g,"<").replace(/>/g,">");if(e!==" "){o.appendChild(x.createTextNode(e));if(m)n.dx=0;else if(f&&j!==null)n.x=j;F(o,n);b.appendChild(o);!m&&f&&(!ba&&d&&A(o,{display:"block"}),F(o,"dy",Y(o)));if(l)for(var e=e.replace(/([^\^])-/g, -"$1- ").split(" "),n=g.length>1||e.length>1&&k.whiteSpace!=="nowrap",q,E,s=k.HcHeight,u=[],t=Y(o),Kb=1;n&&(e.length||u.length);)delete a.bBox,q=a.getBBox(),E=q.width,!ba&&c.forExport&&(E=c.measureSpanWidth(o.firstChild.data,a.styles)),q=E>l,!q||e.length===1?(e=u,u=[],e.length&&(Kb++,s&&Kb*t>s?(e=["..."],a.attr("title",a.textStr)):(o=x.createElementNS(xa,"tspan"),F(o,{dy:t,x:j}),p&&F(o,"style",p),b.appendChild(o))),E>l&&(l=E)):(o.removeChild(o.firstChild),u.unshift(e.pop())),e.length&&o.appendChild(x.createTextNode(e.join(" ").replace(/- /g, -"-")));m++}}})}))},button:function(a,b,c,d,e,f,g,h,i){var j=this.label(a,b,c,i,null,null,null,null,"button"),k=0,l,m,n,o,p,q,a={x1:0,y1:0,x2:0,y2:1},e=w({"stroke-width":1,stroke:"#CCCCCC",fill:{linearGradient:a,stops:[[0,"#FEFEFE"],[1,"#F6F6F6"]]},r:2,padding:5,style:{color:"black"}},e);n=e.style;delete e.style;f=w(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#FFF"],[1,"#ACF"]]}},f);o=f.style;delete f.style;g=w(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#9BD"],[1,"#CDF"]]}},g);p=g.style; -delete g.style;h=w(e,{style:{color:"#CCC"}},h);q=h.style;delete h.style;N(j.element,Aa?"mouseover":"mouseenter",function(){k!==3&&j.attr(f).css(o)});N(j.element,Aa?"mouseout":"mouseleave",function(){k!==3&&(l=[e,f,g][k],m=[n,o,p][k],j.attr(l).css(m))});j.setState=function(a){(j.state=k=a)?a===2?j.attr(g).css(p):a===3&&j.attr(h).css(q):j.attr(e).css(n)};return j.on("click",function(){k!==3&&d.call(j)}).attr(e).css(r({cursor:"default"},n))},crispLine:function(a,b){a[1]===a[4]&&(a[1]=a[4]=v(a[1])-b% -2/2);a[2]===a[5]&&(a[2]=a[5]=v(a[2])+b%2/2);return a},path:function(a){var b={fill:P};La(a)?b.d=a:da(a)&&r(b,a);return this.createElement("path").attr(b)},circle:function(a,b,c){a=da(a)?a:{x:a,y:b,r:c};b=this.createElement("circle");b.xSetter=function(a){this.element.setAttribute("cx",a)};b.ySetter=function(a){this.element.setAttribute("cy",a)};return b.attr(a)},arc:function(a,b,c,d,e,f){if(da(a))b=a.y,c=a.r,d=a.innerR,e=a.start,f=a.end,a=a.x;a=this.symbol("arc",a||0,b||0,c||0,c||0,{innerR:d||0,start:e|| -0,end:f||0});a.r=c;return a},rect:function(a,b,c,d,e,f){var e=da(a)?a.r:e,g=this.createElement("rect"),a=da(a)?a:a===t?{}:{x:a,y:b,width:u(c,0),height:u(d,0)};if(f!==t)a.strokeWidth=f,a=g.crisp(a);if(e)a.r=e;g.rSetter=function(a){F(this.element,{rx:a,ry:a})};return g.attr(a)},setSize:function(a,b,c){var d=this.alignedObjects,e=d.length;this.width=a;this.height=b;for(this.boxWrapper[p(c,!0)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b=this.createElement("g");return s(a)? -b.attr({"class":"highcharts-"+a}):b},image:function(a,b,c,d,e){var f={preserveAspectRatio:P};arguments.length>1&&r(f,{x:b,y:c,width:d,height:e});f=this.createElement("image").attr(f);f.element.setAttributeNS?f.element.setAttributeNS("http://www.w3.org/1999/xlink","href",a):f.element.setAttribute("hc-svg-href",a);return f},symbol:function(a,b,c,d,e,f){var g,h=this.symbols[a],h=h&&h(v(b),v(c),d,e,f),i=/^url\((.*?)\)$/,j,k;if(h)g=this.path(h),r(g,{symbolName:a,x:b,y:c,width:d,height:e}),f&&r(g,f);else if(i.test(a))k= -function(a,b){a.element&&(a.attr({width:b[0],height:b[1]}),a.alignByTranslate||a.translate(v((d-b[0])/2),v((e-b[1])/2)))},j=a.match(i)[1],a=Ib[j],g=this.image(j).attr({x:b,y:c}),g.isImg=!0,a?k(g,a):(g.attr({width:0,height:0}),$("img",{onload:function(){k(g,Ib[j]=[this.width,this.height])},src:j}));return g},symbols:{circle:function(a,b,c,d){var e=0.166*c;return["M",a+c/2,b,"C",a+c+e,b,a+c+e,b+d,a+c/2,b+d,"C",a-e,b+d,a-e,b,a+c/2,b,"Z"]},square:function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c,b+d,a,b+ -d,"Z"]},triangle:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d,a,b+d,"Z"]},"triangle-down":function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c/2,b+d,"Z"]},diamond:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d/2,a+c/2,b+d,a,b+d/2,"Z"]},arc:function(a,b,c,d,e){var f=e.start,c=e.r||c||d,g=e.end-0.001,d=e.innerR,h=e.open,i=aa(f),j=fa(f),k=aa(g),g=fa(g),e=e.end-fc&&i>b+g&&ib+g&&id&&h>a+g&&ha+g&&hl&&/[ \-]/.test(b.textContent||b.innerText))A(b,{width:l+"px",display:"block",whiteSpace:"normal"}),i=l;this.getSpanCorrection(i,k,h,j,g)}A(b,{left:e+ -(this.xCorr||0)+"px",top:f+(this.yCorr||0)+"px"});if(sb)k=b.offsetHeight;this.cTT=m}}else this.alignOnAdd=!0},setSpanRotation:function(a,b,c){var d={},e=Aa?"-ms-transform":sb?"-webkit-transform":Ta?"MozTransform":Gb?"-o-transform":"";d[e]=d.transform="rotate("+a+"deg)";d[e+(Ta?"Origin":"-origin")]=d.transformOrigin=b*100+"% "+c+"px";A(this.element,d)},getSpanCorrection:function(a,b,c){this.xCorr=-a*c;this.yCorr=-b}});r(ta.prototype,{html:function(a,b,c){var d=this.createElement("span"),e=d.element, -f=d.renderer;d.textSetter=function(a){a!==e.innerHTML&&delete this.bBox;e.innerHTML=this.textStr=a};d.xSetter=d.ySetter=d.alignSetter=d.rotationSetter=function(a,b){b==="align"&&(b="textAlign");d[b]=a;d.htmlUpdateTransform()};d.attr({text:a,x:v(b),y:v(c)}).css({position:"absolute",whiteSpace:"nowrap",fontFamily:this.style.fontFamily,fontSize:this.style.fontSize});d.css=d.htmlCss;if(f.isSVG)d.add=function(a){var b,c=f.box.parentNode,j=[];if(this.parentGroup=a){if(b=a.div,!b){for(;a;)j.push(a),a=a.parentGroup; -q(j.reverse(),function(a){var d;b=a.div=a.div||$(Ja,{className:F(a.element,"class")},{position:"absolute",left:(a.translateX||0)+"px",top:(a.translateY||0)+"px"},b||c);d=b.style;r(a,{translateXSetter:function(b,c){d.left=b+"px";a[c]=b;a.doTransform=!0},translateYSetter:function(b,c){d.top=b+"px";a[c]=b;a.doTransform=!0},visibilitySetter:function(a,b){d[b]=a}})})}}else b=c;b.appendChild(e);d.added=!0;d.alignOnAdd&&d.htmlUpdateTransform();return d};return d}});var Z;if(!ba&&!ga){Z={init:function(a, -b){var c=["<",b,' filled="f" stroked="f"'],d=["position: ","absolute",";"],e=b===Ja;(b==="shape"||e)&&d.push("left:0;top:0;width:1px;height:1px;");d.push("visibility: ",e?"hidden":"visible");c.push(' style="',d.join(""),'"/>');if(b)c=e||b==="span"||b==="img"?c.join(""):a.prepVML(c),this.element=$(c);this.renderer=a},add:function(a){var b=this.renderer,c=this.element,d=b.box,d=a?a.element||a:d;a&&a.inverted&&b.invertChild(c,d);d.appendChild(c);this.added=!0;this.alignOnAdd&&!this.deferUpdateTransform&& -this.updateTransform();if(this.onAdd)this.onAdd();return this},updateTransform:G.prototype.htmlUpdateTransform,setSpanRotation:function(){var a=this.rotation,b=aa(a*Ca),c=fa(a*Ca);A(this.element,{filter:a?["progid:DXImageTransform.Microsoft.Matrix(M11=",b,", M12=",-c,", M21=",c,", M22=",b,", sizingMethod='auto expand')"].join(""):P})},getSpanCorrection:function(a,b,c,d,e){var f=d?aa(d*Ca):1,g=d?fa(d*Ca):0,h=p(this.elemHeight,this.element.offsetHeight),i;this.xCorr=f<0&&-a;this.yCorr=g<0&&-h;i=f*g< -0;this.xCorr+=g*b*(i?1-c:c);this.yCorr-=f*b*(d?i?c:1-c:1);e&&e!=="left"&&(this.xCorr-=a*c*(f<0?-1:1),d&&(this.yCorr-=h*c*(g<0?-1:1)),A(this.element,{textAlign:e}))},pathToVML:function(a){for(var b=a.length,c=[];b--;)if(ia(a[b]))c[b]=v(a[b]*10)-5;else if(a[b]==="Z")c[b]="x";else if(c[b]=a[b],a.isArc&&(a[b]==="wa"||a[b]==="at"))c[b+5]===c[b+7]&&(c[b+7]+=a[b+7]>a[b+5]?1:-1),c[b+6]===c[b+8]&&(c[b+8]+=a[b+8]>a[b+6]?1:-1);return c.join(" ")||"x"},clip:function(a){var b=this,c;a?(c=a.members,ka(c,b),c.push(b), -b.destroyClip=function(){ka(c,b)},a=a.getCSS(b)):(b.destroyClip&&b.destroyClip(),a={clip:gb?"inherit":"rect(auto)"});return b.css(a)},css:G.prototype.htmlCss,safeRemoveChild:function(a){a.parentNode&&Pa(a)},destroy:function(){this.destroyClip&&this.destroyClip();return G.prototype.destroy.apply(this)},on:function(a,b){this.element["on"+a]=function(){var a=H.event;a.target=a.srcElement;b(a)};return this},cutOffPath:function(a,b){var c,a=a.split(/[ ,]/);c=a.length;if(c===9||c===11)a[c-4]=a[c-2]=z(a[c- -2])-10*b;return a.join(" ")},shadow:function(a,b,c){var d=[],e,f=this.element,g=this.renderer,h,i=f.style,j,k=f.path,l,m,n,o;k&&typeof k.value!=="string"&&(k="x");m=k;if(a){n=p(a.width,3);o=(a.opacity||0.15)/n;for(e=1;e<=3;e++){l=n*2+1-2*e;c&&(m=this.cutOffPath(k.value,l+0.5));j=[''];h=$(g.prepVML(j),null,{left:z(i.left)+p(a.offsetX,1),top:z(i.top)+p(a.offsetY,1)});if(c)h.cutOff= -l+1;j=[''];$(g.prepVML(j),null,null,h);b?b.element.appendChild(h):f.parentNode.insertBefore(h,f);d.push(h)}this.shadows=d}return this},updateShadows:sa,setAttr:function(a,b){gb?this.element[a]=b:this.element.setAttribute(a,b)},classSetter:function(a){this.element.className=a},dashstyleSetter:function(a,b,c){(c.getElementsByTagName("stroke")[0]||$(this.renderer.prepVML([""]),null,null,c))[b]=a||"solid";this[b]=a},dSetter:function(a,b, -c){var d=this.shadows,a=a||[];this.d=a.join&&a.join(" ");c.path=a=this.pathToVML(a);if(d)for(c=d.length;c--;)d[c].path=d[c].cutOff?this.cutOffPath(a,d[c].cutOff):a;this.setAttr(b,a)},fillSetter:function(a,b,c){var d=c.nodeName;if(d==="SPAN")c.style.color=a;else if(d!=="IMG")c.filled=a!==P,this.setAttr("fillcolor",this.renderer.color(a,c,b,this))},opacitySetter:sa,rotationSetter:function(a,b,c){c=c.style;this[b]=c[b]=a;c.left=-v(fa(a*Ca)+1)+"px";c.top=v(aa(a*Ca))+"px"},strokeSetter:function(a,b,c){this.setAttr("strokecolor", -this.renderer.color(a,c,b))},"stroke-widthSetter":function(a,b,c){c.stroked=!!a;this[b]=a;ia(a)&&(a+="px");this.setAttr("strokeweight",a)},titleSetter:function(a,b){this.setAttr(b,a)},visibilitySetter:function(a,b,c){a==="inherit"&&(a="visible");this.shadows&&q(this.shadows,function(c){c.style[b]=a});c.nodeName==="DIV"&&(a=a==="hidden"?"-999em":0,gb||(c.style[b]=a?"visible":"hidden"),b="top");c.style[b]=a},xSetter:function(a,b,c){this[b]=a;b==="x"?b="left":b==="y"&&(b="top");this.updateClipping?(this[b]= -a,this.updateClipping()):c.style[b]=a},zIndexSetter:function(a,b,c){c.style[b]=a}};S.VMLElement=Z=la(G,Z);Z.prototype.ySetter=Z.prototype.widthSetter=Z.prototype.heightSetter=Z.prototype.xSetter;var ha={Element:Z,isIE8:wa.indexOf("MSIE 8.0")>-1,init:function(a,b,c,d){var e;this.alignedObjects=[];d=this.createElement(Ja).css(r(this.getStyle(d),{position:"relative"}));e=d.element;a.appendChild(d.element);this.isVML=!0;this.box=e;this.boxWrapper=d;this.cache={};this.setSize(b,c,!1);if(!x.namespaces.hcv){x.namespaces.add("hcv", -"urn:schemas-microsoft-com:vml");try{x.createStyleSheet().cssText="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "}catch(f){x.styleSheets[0].cssText+="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "}}},isHidden:function(){return!this.box.offsetWidth},clipRect:function(a,b,c,d){var e=this.createElement(),f=da(a);return r(e,{members:[],left:(f?a.x:a)+1,top:(f?a.y:b)+1,width:(f?a.width: -c)-1,height:(f?a.height:d)-1,getCSS:function(a){var b=a.element,c=b.nodeName,a=a.inverted,d=this.top-(c==="shape"?b.offsetTop:0),e=this.left,b=e+this.width,f=d+this.height,d={clip:"rect("+v(a?e:d)+"px,"+v(a?f:b)+"px,"+v(a?b:f)+"px,"+v(a?d:e)+"px)"};!a&&gb&&c==="DIV"&&r(d,{width:b+"px",height:f+"px"});return d},updateClipping:function(){q(e.members,function(a){a.element&&a.css(e.getCSS(a))})}})},color:function(a,b,c,d){var e=this,f,g=/^rgba/,h,i,j=P;a&&a.linearGradient?i="gradient":a&&a.radialGradient&& -(i="pattern");if(i){var k,l,m=a.linearGradient||a.radialGradient,n,o,p,E,I,D="",a=a.stops,u,s=[],t=function(){h=[''];$(e.prepVML(h),null,null,b)};n=a[0];u=a[a.length-1];n[0]>0&&a.unshift([0,n[1]]);u[0]<1&&a.push([1,u[1]]);q(a,function(a,b){g.test(a[1])?(f=ya(a[1]),k=f.get("rgb"),l=f.get("a")):(k=a[1],l=1);s.push(a[0]*100+"% "+k);b?(p=l,E=k):(o=l,I=k)});if(c==="fill")if(i==="gradient")c= -m.x1||m[0]||0,a=m.y1||m[1]||0,n=m.x2||m[2]||0,m=m.y2||m[3]||0,D='angle="'+(90-V.atan((m-a)/(n-c))*180/na)+'"',t();else{var j=m.r,r=j*2,v=j*2,x=m.cx,y=m.cy,R=b.radialReference,w,j=function(){R&&(w=d.getBBox(),x+=(R[0]-w.x)/w.width-0.5,y+=(R[1]-w.y)/w.height-0.5,r*=R[2]/w.width,v*=R[2]/w.height);D='src="'+L.global.VMLRadialGradientURL+'" size="'+r+","+v+'" origin="0.5,0.5" position="'+x+","+y+'" color2="'+I+'" ';t()};d.added?j():d.onAdd=j;j=E}else j=k}else if(g.test(a)&&b.tagName!=="IMG")f=ya(a),h= -["<",c,' opacity="',f.get("a"),'"/>'],$(this.prepVML(h),null,null,b),j=f.get("rgb");else{j=b.getElementsByTagName(c);if(j.length)j[0].opacity=1,j[0].type="solid";j=a}return j},prepVML:function(a){var b=this.isIE8,a=a.join("");b?(a=a.replace("/>",' xmlns="urn:schemas-microsoft-com:vml" />'),a=a.indexOf('style="')===-1?a.replace("/>",' style="display:inline-block;behavior:url(#default#VML);" />'):a.replace('style="','style="display:inline-block;behavior:url(#default#VML);')):a=a.replace("<","1&&f.attr({x:b,y:c,width:d, -height:e});return f},createElement:function(a){return a==="rect"?this.symbol(a):ta.prototype.createElement.call(this,a)},invertChild:function(a,b){var c=this,d=b.style,e=a.tagName==="IMG"&&a.style;A(a,{flip:"x",left:z(d.width)-(e?z(e.top):1),top:z(d.height)-(e?z(e.left):1),rotation:-90});q(a.childNodes,function(b){c.invertChild(b,a)})},symbols:{arc:function(a,b,c,d,e){var f=e.start,g=e.end,h=e.r||c||d,c=e.innerR,d=aa(f),i=fa(f),j=aa(g),k=fa(g);if(g-f===0)return["x"];f=["wa",a-h,b-h,a+h,b+h,a+h*d, -b+h*i,a+h*j,b+h*k];e.open&&!c&&f.push("e","M",a,b);f.push("at",a-c,b-c,a+c,b+c,a+c*j,b+c*k,a+c*d,b+c*i,"x","e");f.isArc=!0;return f},circle:function(a,b,c,d,e){e&&(c=d=2*e.r);e&&e.isCircle&&(a-=c/2,b-=d/2);return["wa",a,b,a+c,b+d,a+c,b+d/2,a+c,b+d/2,"e"]},rect:function(a,b,c,d,e){return ta.prototype.symbols[!s(e)||!e.r?"square":"callout"].call(0,a,b,c,d,e)}}};S.VMLRenderer=Z=function(){this.init.apply(this,arguments)};Z.prototype=w(ta.prototype,ha);Ya=Z}ta.prototype.measureSpanWidth=function(a,b){var c= -x.createElement("span"),d;d=x.createTextNode(a);c.appendChild(d);A(c,b);this.box.appendChild(c);d=c.offsetWidth;Pa(c);return d};var Lb;if(ga)S.CanVGRenderer=Z=function(){xa="http://www.w3.org/1999/xhtml"},Z.prototype.symbols={},Lb=function(){function a(){var a=b.length,d;for(d=0;dl[o]?l[o]=g+j:m||(c=!1);if(m){l=(m=d.justifyToPlot)?d.pos:0;m=m?l+d.len: -d.chart.chartWidth;do a+=e?1:-1,n=d.ticks[i[a]];while(i[a]&&(!n||!n.label||n.label.line!==o));d=n&&n.label.xy&&n.label.xy.x+n.getLabelSides()[e?0:1];e&&!h||f&&h?g+kd&&(c=!1)):g+j>m&&(g=m-j,n&&g+k0&&b.height>0){f=w({align:c&&k&&"center",x:c?!k&&4:10,verticalAlign:!c&&k&&"middle",y:c?k?16:10:k?6:-4,rotation:c&&!k&&90},f);if(!g){r={align:f.textAlign||f.align,rotation:f.rotation};if(s(I))r.zIndex=I;a.label=g=t.text(f.text,0,0,f.useHTML).attr(r).css(f.style).add()}b=[o[1],o[4],k?o[6]:o[1]];k=[o[2],o[5],k?o[7]:o[2]];o=Na(b);c=Na(k);g.align(f,!1,{x:o,y:c,width:Ba(b)-o,height:Ba(k)-c});g.show()}else g&&g.hide();return a},destroy:function(){ka(this.axis.plotLinesAndBands, -this);delete this.axis;Oa(this)}};ma.prototype={defaultOptions:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L",second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%e. %b",week:"%e. %b",month:"%b '%y",year:"%Y"},endOnTick:!1,gridLineColor:"#C0C0C0",labels:M,lineColor:"#C0D0E0",lineWidth:1,minPadding:0.01,maxPadding:0.01,minorGridLineColor:"#E0E0E0",minorGridLineWidth:1,minorTickColor:"#A0A0A0",minorTickLength:2,minorTickPosition:"outside",startOfWeek:1,startOnTick:!1,tickColor:"#C0D0E0",tickLength:10, -tickmarkPlacement:"between",tickPixelInterval:100,tickPosition:"outside",tickWidth:1,title:{align:"middle",style:{color:"#707070"}},type:"linear"},defaultYAxisOptions:{endOnTick:!0,gridLineWidth:1,tickPixelInterval:72,showLastLabel:!0,labels:{x:-8,y:3},lineWidth:0,maxPadding:0.05,minPadding:0.05,startOnTick:!0,tickWidth:0,title:{rotation:270,text:"Values"},stackLabels:{enabled:!1,formatter:function(){return Ga(this.total,-1)},style:M.style}},defaultLeftAxisOptions:{labels:{x:-15,y:null},title:{rotation:270}}, -defaultRightAxisOptions:{labels:{x:15,y:null},title:{rotation:90}},defaultBottomAxisOptions:{labels:{x:0,y:null},title:{rotation:0}},defaultTopAxisOptions:{labels:{x:0,y:-15},title:{rotation:0}},init:function(a,b){var c=b.isX;this.horiz=a.inverted?!c:c;this.coll=(this.isXAxis=c)?"xAxis":"yAxis";this.opposite=b.opposite;this.side=b.side||(this.horiz?this.opposite?0:2:this.opposite?1:3);this.setOptions(b);var d=this.options,e=d.type;this.labelFormatter=d.labels.formatter||this.defaultLabelFormatter; -this.userOptions=b;this.minPixelPadding=0;this.chart=a;this.reversed=d.reversed;this.zoomEnabled=d.zoomEnabled!==!1;this.categories=d.categories||e==="category";this.names=[];this.isLog=e==="logarithmic";this.isDatetimeAxis=e==="datetime";this.isLinked=s(d.linkedTo);this.tickmarkOffset=this.categories&&d.tickmarkPlacement==="between"?0.5:0;this.ticks={};this.labelEdge=[];this.minorTicks={};this.plotLinesAndBands=[];this.alternateBands={};this.len=0;this.minRange=this.userMinRange=d.minRange||d.maxZoom; -this.range=d.range;this.offset=d.offset||0;this.stacks={};this.oldStacks={};this.min=this.max=null;this.crosshair=p(d.crosshair,ra(a.options.tooltip.crosshairs)[c?0:1],!1);var f,d=this.options.events;Da(this,a.axes)===-1&&(c&&!this.isColorAxis?a.axes.splice(a.xAxis.length,0,this):a.axes.push(this),a[this.coll].push(this));this.series=this.series||[];if(a.inverted&&c&&this.reversed===t)this.reversed=!0;this.removePlotLine=this.removePlotBand=this.removePlotBandOrLine;for(f in d)N(this,f,d[f]);if(this.isLog)this.val2lin= -za,this.lin2val=ja},setOptions:function(a){this.options=w(this.defaultOptions,this.isXAxis?{}:this.defaultYAxisOptions,[this.defaultTopAxisOptions,this.defaultRightAxisOptions,this.defaultBottomAxisOptions,this.defaultLeftAxisOptions][this.side],w(L[this.coll],a))},defaultLabelFormatter:function(){var a=this.axis,b=this.value,c=a.categories,d=this.dateTimeLabelFormat,e=L.lang.numericSymbols,f=e&&e.length,g,h=a.options.labels.format,a=a.isLog?b:a.tickInterval;if(h)g=Ia(h,this);else if(c)g=b;else if(d)g= -bb(d,b);else if(f&&a>=1E3)for(;f--&&g===t;)c=Math.pow(1E3,f+1),a>=c&&e[f]!==null&&(g=Ga(b/c,-1)+e[f]);g===t&&(g=Q(b)>=1E4?Ga(b,0):Ga(b,-1,t,""));return g},getSeriesExtremes:function(){var a=this,b=a.chart;a.hasVisibleSeries=!1;a.dataMin=a.dataMax=null;a.buildStacks&&a.buildStacks();q(a.series,function(c){if(c.visible||!b.options.chart.ignoreHiddenSeries){var d;d=c.options.threshold;var e;a.hasVisibleSeries=!0;a.isLog&&d<=0&&(d=null);if(a.isXAxis){if(d=c.xData,d.length)a.dataMin=C(p(a.dataMin,d[0]), -Na(d)),a.dataMax=u(p(a.dataMax,d[0]),Ba(d))}else{c.getExtremes();e=c.dataMax;c=c.dataMin;if(s(c)&&s(e))a.dataMin=C(p(a.dataMin,c),c),a.dataMax=u(p(a.dataMax,e),e);if(s(d))if(a.dataMin>=d)a.dataMin=d,a.ignoreMinPadding=!0;else if(a.dataMaxg+this.width)m=!0}else if(a=g,c=l-this.right,ih+this.height)m=!0;return m&&!d?null:f.renderer.crispLine(["M",a,i,"L",c,j],b||1)},getLinearTickPositions:function(a,b,c){var d,e=ea(U(b/a)*a),f=ea(Ka(c/a)*a),g=[];if(b===c&&ia(b))return[b];for(b=e;b<=f;){g.push(b);b=ea(b+a);if(b===d)break;d=b}return g},getMinorTickPositions:function(){var a= -this.options,b=this.tickPositions,c=this.minorTickInterval,d=[],e;if(this.isLog){e=b.length;for(a=1;a=this.minRange,f,g,h,i,j;if(this.isXAxis&&this.minRange===t&&!this.isLog)s(a.min)||s(a.max)?this.minRange=null:(q(this.series,function(a){i=a.xData;for(g=j=a.xIncrement?1:i.length-1;g>0;g--)if(h=i[g]-i[g-1],f===t||hc&&(h=0);d=u(d,h);f=u(f,Fa(j)?0:h/2);g=u(g,j==="on"?0:h);!a.noSharedTooltip&&s(n)&&(e=s(e)?C(e,n):n)}),h=b.ordinalSlope&&e?b.ordinalSlope/e:1,b.minPointOffset=f*=h,b.pointRangePadding= -g*=h,b.pointRange=C(d,c),b.closestPointRange=e;if(a)b.oldTransA=j;b.translationSlope=b.transA=j=b.len/(c+g||1);b.transB=b.horiz?b.left:b.bottom;b.minPixelPadding=j*f},setTickPositions:function(a){var b=this,c=b.chart,d=b.options,e=d.startOnTick,f=d.endOnTick,g=b.isLog,h=b.isDatetimeAxis,i=b.isXAxis,j=b.isLinked,k=b.options.tickPositioner,l=d.maxPadding,m=d.minPadding,n=d.tickInterval,o=d.minTickInterval,Y=d.tickPixelInterval,E,I=b.categories;j?(b.linkedParent=c[b.coll][d.linkedTo],c=b.linkedParent.getExtremes(), -b.min=p(c.min,c.dataMin),b.max=p(c.max,c.dataMax),d.type!==b.linkedParent.options.type&&oa(11,1)):(b.min=p(b.userMin,d.min,b.dataMin),b.max=p(b.userMax,d.max,b.dataMax));if(g)!a&&C(b.min,p(b.dataMin,b.min))<=0&&oa(10,1),b.min=ea(za(b.min)),b.max=ea(za(b.max));if(b.range&&s(b.max))b.userMin=b.min=u(b.min,b.max-b.range),b.userMax=b.max,b.range=null;b.beforePadding&&b.beforePadding();b.adjustForMinRange();if(!I&&!b.axisPointRange&&!b.usePercentage&&!j&&s(b.min)&&s(b.max)&&(c=b.max-b.min)){if(!s(d.min)&& -!s(b.userMin)&&m&&(b.dataMin<0||!b.ignoreMinPadding))b.min-=c*m;if(!s(d.max)&&!s(b.userMax)&&l&&(b.dataMax>0||!b.ignoreMaxPadding))b.max+=c*l}if(ia(d.floor))b.min=u(b.min,d.floor);if(ia(d.ceiling))b.max=C(b.max,d.ceiling);b.min===b.max||b.min===void 0||b.max===void 0?b.tickInterval=1:j&&!n&&Y===b.linkedParent.options.tickPixelInterval?b.tickInterval=b.linkedParent.tickInterval:(b.tickInterval=p(n,I?1:(b.max-b.min)*Y/u(b.len,Y)),!s(n)&&b.lenu(2*b.len,200)&&oa(19,!0),a=h?b.getTimeTicks(b.normalizeTimeTickInterval(b.tickInterval,d.units),b.min,b.max,d.startOfWeek,b.ordinalPositions,b.closestPointRange,!0):g?b.getLogTickPositions(b.tickInterval,b.min,b.max):b.getLinearTickPositions(b.tickInterval,b.min,b.max),E&&a.splice(1,a.length-2),b.tickPositions=a;if(!j)d=a[0],g=a[a.length- -1],h=b.minPointOffset||0,!e&&!f&&!I&&a.length===2&&a.splice(1,0,(g+d)/2),e?b.min=d:b.min-h>d&&a.shift(),f?b.max=g:b.max+h1E13?1:0.001,b.min-=e,b.max+=e)},setMaxTicks:function(){var a=this.chart,b=a.maxTicks||{},c=this.tickPositions,d=this._maxTicksKey=[this.coll,this.pos,this.len].join("-");if(!this.isLinked&&!this.isDatetimeAxis&&c&&c.length>(b[d]||0)&&this.options.alignTicks!==!1)b[d]=c.length;a.maxTicks=b},adjustTickAmount:function(){var a=this._maxTicksKey, -b=this.tickPositions,c=this.chart.maxTicks;if(c&&c[a]&&!this.isDatetimeAxis&&!this.categories&&!this.isLinked&&this.options.alignTicks!==!1&&this.min!==t){var d=this.tickAmount,e=b.length;this.tickAmount=a=c[a];if(e=u(d,p(e.max,d))&&(b=t));this.displayBtn=a!==t||b!==t;this.setExtremes(a,b,!1,t, -{trigger:"zoom"});return!0},setAxisSize:function(){var a=this.chart,b=this.options,c=b.offsetLeft||0,d=this.horiz,e=p(b.width,a.plotWidth-c+(b.offsetRight||0)),f=p(b.height,a.plotHeight),g=p(b.top,a.plotTop),b=p(b.left,a.plotLeft+c),c=/%$/;c.test(f)&&(f=parseInt(f,10)/100*a.plotHeight);c.test(g)&&(g=parseInt(g,10)/100*a.plotHeight+a.plotTop);this.left=b;this.top=g;this.width=e;this.height=f;this.bottom=a.chartHeight-f-g;this.right=a.chartWidth-e-b;this.len=u(d?e:f,0);this.pos=d?b:g},getExtremes:function(){var a= -this.isLog;return{min:a?ea(ja(this.min)):this.min,max:a?ea(ja(this.max)):this.max,dataMin:this.dataMin,dataMax:this.dataMax,userMin:this.userMin,userMax:this.userMax}},getThreshold:function(a){var b=this.isLog,c=b?ja(this.min):this.min,b=b?ja(this.max):this.max;c>a||a===null?a=c:b15&&a<165?"right":a>195&&a<345?"left":"center"},getOffset:function(){var a=this,b=a.chart,c=b.renderer,d=a.options, -e=a.tickPositions,f=a.ticks,g=a.horiz,h=a.side,i=b.inverted?[1,0,3,2][h]:h,j,k,l=0,m,n=0,o=d.title,Y=d.labels,E=0,I=b.axisOffset,b=b.clipOffset,D=[-1,1,1,-1][h],r,v=1,w=p(Y.maxStaggerLines,5),x,z,C,y,R;a.hasData=j=a.hasVisibleSeries||s(a.min)&&s(a.max)&&!!e;a.showAxis=k=j||p(d.showEmpty,!0);a.staggerLines=a.horiz&&Y.staggerLines;if(!a.axisGroup)a.gridGroup=c.g("grid").attr({zIndex:d.gridZIndex||1}).add(),a.axisGroup=c.g("axis").attr({zIndex:d.zIndex||2}).add(),a.labelGroup=c.g("axis-labels").attr({zIndex:Y.zIndex|| -7}).addClass("highcharts-"+a.coll.toLowerCase()+"-labels").add();if(j||a.isLinked){a.labelAlign=p(Y.align||a.autoLabelAlign(Y.rotation));q(e,function(b){f[b]?f[b].addLabel():f[b]=new Sa(a,b)});if(a.horiz&&!a.staggerLines&&w&&!Y.rotation){for(j=a.reversed?[].concat(e).reverse():e;v1)a.staggerLines=v}q(e,function(b){if(h=== -0||h===2||{1:"left",3:"right"}[h]===a.labelAlign)E=u(f[b].getLabelSize(),E)});if(a.staggerLines)E*=a.staggerLines,a.labelOffset=E}else for(r in f)f[r].destroy(),delete f[r];if(o&&o.text&&o.enabled!==!1){if(!a.axisTitle)a.axisTitle=c.text(o.text,0,0,o.useHTML).attr({zIndex:7,rotation:o.rotation||0,align:o.textAlign||{low:"left",middle:"center",high:"right"}[o.align]}).addClass("highcharts-"+this.coll.toLowerCase()+"-title").css(o.style).add(a.axisGroup),a.axisTitle.isNew=!0;if(k)l=a.axisTitle.getBBox()[g? -"height":"width"],m=o.offset,n=s(m)?0:p(o.margin,g?5:10);a.axisTitle[k?"show":"hide"]()}a.offset=D*p(d.offset,I[h]);c=h===2?a.tickBaseline:0;g=E+n+(E&&D*(g?p(Y.y,a.tickBaseline+8):Y.x)-c);a.axisTitleMargin=p(m,g);I[h]=u(I[h],a.axisTitleMargin+l+D*a.offset,g);b[i]=u(b[i],U(d.lineWidth/2)*2)},getLinePath:function(a){var b=this.chart,c=this.opposite,d=this.offset,e=this.horiz,f=this.left+(c?this.width:0)+d,d=b.chartHeight-this.bottom-(c?this.height:0)+d;c&&(a*=-1);return b.renderer.crispLine(["M",e? -this.left:f,e?d:this.top,"L",e?b.chartWidth-this.right:f,e?d:b.chartHeight-this.bottom],a)},getTitlePosition:function(){var a=this.horiz,b=this.left,c=this.top,d=this.len,e=this.options.title,f=a?b:c,g=this.opposite,h=this.offset,i=z(e.style.fontSize||12),d={low:f+(a?0:d),middle:f+d/2,high:f+(a?d:0)}[e.align],b=(a?c+this.height:b)+(a?1:-1)*(g?-1:1)*this.axisTitleMargin+(this.side===2?i:0);return{x:a?d:b+(g?this.width:0)+h+(e.x||0),y:a?b-(g?this.height:0)+h:d+(e.y||0)}},render:function(){var a=this, -b=a.horiz,c=a.reversed,d=a.chart,e=d.renderer,f=a.options,g=a.isLog,h=a.isLinked,i=a.tickPositions,j,k=a.axisTitle,l=a.ticks,m=a.minorTicks,n=a.alternateBands,o=f.stackLabels,p=f.alternateGridColor,E=a.tickmarkOffset,I=f.lineWidth,D=d.hasRendered&&s(a.oldMin)&&!isNaN(a.oldMin),r=a.hasData,u=a.showAxis,v,w=f.labels.overflow,x=a.justifyLabels=b&&w!==!1,z;a.labelEdge.length=0;a.justifyToPlot=w==="justify";q([l,m,n],function(a){for(var b in a)a[b].isActive=!1});if(r||h)if(a.minorTickInterval&&!a.categories&& -q(a.getMinorTickPositions(),function(b){m[b]||(m[b]=new Sa(a,b,"minor"));D&&m[b].isNew&&m[b].render(null,!0);m[b].render(null,!1,1)}),i.length&&(j=i.slice(),(b&&c||!b&&!c)&&j.reverse(),x&&(j=j.slice(1).concat([j[0]])),q(j,function(b,c){x&&(c=c===j.length-1?0:c+1);if(!h||b>=a.min&&b<=a.max)l[b]||(l[b]=new Sa(a,b)),D&&l[b].isNew&&l[b].render(c,!0,0.1),l[b].render(c)}),E&&a.min===0&&(l[-1]||(l[-1]=new Sa(a,-1,null,!0)),l[-1].render(-1))),p&&q(i,function(b,c){if(c%2===0&&b=B.second&&(i.setMilliseconds(0),i.setSeconds(j>=B.minute?0:k*U(i.getSeconds()/k)));if(j>=B.minute)i[Bb](j>=B.hour?0:k*U(i[ob]()/k));if(j>=B.hour)i[Cb](j>= -B.day?0:k*U(i[pb]()/k));if(j>=B.day)i[rb](j>=B.month?1:k*U(i[Wa]()/k));j>=B.month&&(i[Db](j>=B.year?0:k*U(i[eb]()/k)),h=i[fb]());j>=B.year&&(h-=h%k,i[Eb](h));if(j===B.week)i[rb](i[Wa]()-i[qb]()+p(d,1));b=1;Ra&&(i=new Date(i.getTime()+Ra));h=i[fb]();for(var d=i.getTime(),l=i[eb](),m=i[Wa](),n=g?Ra:(864E5+i.getTimezoneOffset()*6E4)%864E5;d=0.5)a=v(a),g=this.getLinearTickPositions(a,b,c);else if(a>=0.08)for(var f=U(b),h,i,j,k,l,e=a>0.3?[1,2,4]:a>0.15?[1,2,4,6,8]:[1,2,3,4,5,6,7,8,9];fb&&(!d||k<=c)&&k!==t&&g.push(k),k>c&&(l=!0),k=j}else if(b=ja(b), -c=ja(c),a=e[d?"minorTickInterval":"tickInterval"],a=p(a==="auto"?null:a,this._minorAutoInterval,(c-b)*(e.tickPixelInterval/(d?5:1))/((d?f/this.tickPositions.length:f)||1)),a=mb(a,null,lb(a)),g=Ua(this.getLinearTickPositions(a,b,c),za),!d)this._minorAutoInterval=a/5;if(!d)this.tickInterval=a;return g};var Mb=S.Tooltip=function(){this.init.apply(this,arguments)};Mb.prototype={init:function(a,b){var c=b.borderWidth,d=b.style,e=z(d.padding);this.chart=a;this.options=b;this.crosshairs=[];this.now={x:0, -y:0};this.isHidden=!0;this.label=a.renderer.label("",0,0,b.shape||"callout",null,null,b.useHTML,null,"tooltip").attr({padding:e,fill:b.backgroundColor,"stroke-width":c,r:b.borderRadius,zIndex:8}).css(d).css({padding:0}).add().attr({y:-9999});ga||this.label.shadow(b.shadow);this.shared=b.shared},destroy:function(){if(this.label)this.label=this.label.destroy();clearTimeout(this.hideTimer);clearTimeout(this.tooltipTimeout)},move:function(a,b,c,d){var e=this,f=e.now,g=e.options.animation!==!1&&!e.isHidden&& -(Q(a-f.x)>1||Q(b-f.y)>1),h=e.followPointer||e.len>1;r(f,{x:g?(2*f.x+a)/3:a,y:g?(f.y+b)/2:b,anchorX:h?t:g?(2*f.anchorX+c)/3:c,anchorY:h?t:g?(f.anchorY+d)/2:d});e.label.attr(f);if(g)clearTimeout(this.tooltipTimeout),this.tooltipTimeout=setTimeout(function(){e&&e.move(a,b,c,d)},32)},hide:function(){var a=this,b;clearTimeout(this.hideTimer);if(!this.isHidden)b=this.chart.hoverPoints,this.hideTimer=setTimeout(function(){a.label.fadeOut();a.isHidden=!0},p(this.options.hideDelay,500)),b&&q(b,function(a){a.setState()}), -this.chart.hoverPoints=null},getAnchor:function(a,b){var c,d=this.chart,e=d.inverted,f=d.plotTop,g=0,h=0,i,a=ra(a);c=a[0].tooltipPos;this.followPointer&&b&&(b.chartX===t&&(b=d.pointer.normalize(b)),c=[b.chartX-d.plotLeft,b.chartY-f]);c||(q(a,function(a){i=a.series.yAxis;g+=a.plotX;h+=(a.plotLow?(a.plotLow+a.plotHigh)/2:a.plotY)+(!e&&i?i.top-f:0)}),g/=a.length,h/=a.length,c=[e?d.plotWidth-h:g,this.shared&&!e&&a.length>1&&b?b.chartY-f:e?d.plotHeight-g:h]);return Ua(c,v)},getPosition:function(a,b,c){var d= -this.chart,e=this.distance,f={},g,h=["y",d.chartHeight,b,c.plotY+d.plotTop],i=["x",d.chartWidth,a,c.plotX+d.plotLeft],j=c.ttBelow||d.inverted&&!c.negative||!d.inverted&&c.negative,k=function(a,b,c,d){var g=cb-e)return!1;else f[a]=db-c/2?b-c-2:d-c/2},m=function(a){var b=h;h=i;i=b;g=a},n=function(){k.apply(0,h)!==!1?l.apply(0,i)===!1&&!g&&(m(!0),n()): -g?f.x=f.y=0:(m(!0),n())};(d.inverted||this.len>1)&&m();n();return f},defaultFormatter:function(a){var b=this.points||ra(this),c=b[0].series,d;d=[a.tooltipHeaderFormatter(b[0])];q(b,function(a){c=a.series;d.push(c.tooltipFormatter&&c.tooltipFormatter(a)||a.point.tooltipFormatter(c.tooltipOptions.pointFormat))});d.push(a.options.footerFormat||"");return d.join("")},refresh:function(a,b){var c=this.chart,d=this.label,e=this.options,f,g,h={},i,j=[];i=e.formatter||this.defaultFormatter;var h=c.hoverPoints, -k,l=this.shared;clearTimeout(this.hideTimer);this.followPointer=ra(a)[0].series.tooltipOptions.followPointer;g=this.getAnchor(a,b);f=g[0];g=g[1];l&&(!a.series||!a.series.noSharedTooltip)?(c.hoverPoints=a,h&&q(h,function(a){a.setState()}),q(a,function(a){a.setState("hover");j.push(a.getLabelConfig())}),h={x:a[0].category,y:a[0].y},h.points=j,this.len=j.length,a=a[0]):h=a.getLabelConfig();i=i.call(h,this);h=a.series;this.distance=p(h.tooltipOptions.distance,16);i===!1?this.hide():(this.isHidden&&(ab(d), -d.attr("opacity",1).show()),d.attr({text:i}),k=e.borderColor||a.color||h.color||"#606060",d.attr({stroke:k}),this.updatePosition({plotX:f,plotY:g,negative:a.negative,ttBelow:a.ttBelow}),this.isHidden=!1);K(c,"tooltipRefresh",{text:i,x:f+c.plotLeft,y:g+c.plotTop,borderColor:k})},updatePosition:function(a){var b=this.chart,c=this.label,c=(this.options.positioner||this.getPosition).call(this,c.width,c.height,a);this.move(v(c.x),v(c.y),a.plotX+b.plotLeft,a.plotY+b.plotTop)},tooltipHeaderFormatter:function(a){var b= -a.series,c=b.tooltipOptions,d=c.dateTimeLabelFormats,e=c.xDateFormat,f=b.xAxis,g=f&&f.options.type==="datetime"&&ia(a.key),c=c.headerFormat,f=f&&f.closestPointRange,h;if(g&&!e){if(f)for(h in B){if(B[h]>=f||B[h]<=B.day&&a.key%B[h]>0){e=d[h];break}}else e=d.day;e=e||d.year}g&&e&&(c=c.replace("{point.key}","{point.key:"+e+"}"));return Ia(c,{point:a,series:b})}};var pa;Za=x.documentElement.ontouchstart!==t;var Va=S.Pointer=function(a,b){this.init(a,b)};Va.prototype={init:function(a,b){var c=b.chart,d= -c.events,e=ga?"":c.zoomType,c=a.inverted,f;this.options=b;this.chart=a;this.zoomX=f=/x/.test(e);this.zoomY=e=/y/.test(e);this.zoomHor=f&&!c||e&&c;this.zoomVert=e&&!c||f&&c;this.hasZoom=f||e;this.runChartClick=d&&!!d.click;this.pinchDown=[];this.lastValidTouch={};if(S.Tooltip&&b.tooltip.enabled)a.tooltip=new Mb(a,b.tooltip),this.followTouchMove=b.tooltip.followTouchMove;this.setDOMEvents()},normalize:function(a,b){var c,d,a=a||window.event,a=Sb(a);if(!a.target)a.target=a.srcElement;d=a.touches?a.touches.length? -a.touches.item(0):a.changedTouches[0]:a;if(!b)this.chartPosition=b=Rb(this.chart.container);d.pageX===t?(c=u(a.x,a.clientX-b.left),d=a.y):(c=d.pageX-b.left,d=d.pageY-b.top);return r(a,{chartX:v(c),chartY:v(d)})},getCoordinates:function(a){var b={xAxis:[],yAxis:[]};q(this.chart.axes,function(c){b[c.isXAxis?"xAxis":"yAxis"].push({axis:c,value:c.toValue(a[c.horiz?"chartX":"chartY"])})});return b},getIndex:function(a){var b=this.chart;return b.inverted?b.plotHeight+b.plotTop-a.chartY:a.chartX-b.plotLeft}, -runPointActions:function(a){var b=this.chart,c=b.series,d=b.tooltip,e,f,g=b.hoverPoint,h=b.hoverSeries,i,j,k=b.chartWidth,l=this.getIndex(a);if(d&&this.options.tooltip.shared&&(!h||!h.noSharedTooltip)){f=[];i=c.length;for(j=0;jk&&f.splice(i,1);if(f.length&& -f[0].clientX!==this.hoverX)d.refresh(f,a),this.hoverX=f[0].clientX}c=h&&h.tooltipOptions.followPointer;if(h&&h.tracker&&!c){if((e=h.tooltipPoints[l])&&e!==g)e.onMouseOver(a)}else d&&c&&!d.isHidden&&(h=d.getAnchor([{}],a),d.updatePosition({plotX:h[0],plotY:h[1]}));if(d&&!this._onDocumentMouseMove)this._onDocumentMouseMove=function(a){if(W[pa])W[pa].pointer.onDocumentMouseMove(a)},N(x,"mousemove",this._onDocumentMouseMove);q(b.axes,function(b){b.drawCrosshair(a,p(e,g))})},reset:function(a){var b=this.chart, -c=b.hoverSeries,d=b.hoverPoint,e=b.tooltip,f=e&&e.shared?b.hoverPoints:d;(a=a&&e&&f)&&ra(f)[0].plotX===t&&(a=!1);if(a)e.refresh(f),d&&d.setState(d.state,!0);else{if(d)d.onMouseOut();if(c)c.onMouseOut();e&&e.hide();if(this._onDocumentMouseMove)X(x,"mousemove",this._onDocumentMouseMove),this._onDocumentMouseMove=null;q(b.axes,function(a){a.hideCrosshair()});this.hoverX=null}},scaleGroups:function(a,b){var c=this.chart,d;q(c.series,function(e){d=a||e.getPlotBox();e.xAxis&&e.xAxis.zoomEnabled&&(e.group.attr(d), -e.markerGroup&&(e.markerGroup.attr(d),e.markerGroup.clip(b?c.clipRect:null)),e.dataLabelsGroup&&e.dataLabelsGroup.attr(d))});c.clipRect.attr(b||c.clipBox)},dragStart:function(a){var b=this.chart;b.mouseIsDown=a.type;b.cancelClick=!1;b.mouseDownX=this.mouseDownX=a.chartX;b.mouseDownY=this.mouseDownY=a.chartY},drag:function(a){var b=this.chart,c=b.options.chart,d=a.chartX,e=a.chartY,f=this.zoomHor,g=this.zoomVert,h=b.plotLeft,i=b.plotTop,j=b.plotWidth,k=b.plotHeight,l,m=this.mouseDownX,n=this.mouseDownY, -o=c.panKey&&a[c.panKey+"Key"];dh+j&&(d=h+j);ei+k&&(e=i+k);this.hasDragged=Math.sqrt(Math.pow(m-d,2)+Math.pow(n-e,2));if(this.hasDragged>10){l=b.isInsidePlot(m-h,n-i);if(b.hasCartesianSeries&&(this.zoomX||this.zoomY)&&l&&!o&&!this.selectionMarker)this.selectionMarker=b.renderer.rect(h,i,f?1:j,g?1:k,0).attr({fill:c.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add();this.selectionMarker&&f&&(d-=m,this.selectionMarker.attr({width:Q(d),x:(d>0?0:d)+m}));this.selectionMarker&& -g&&(d=e-n,this.selectionMarker.attr({height:Q(d),y:(d>0?0:d)+n}));l&&!this.selectionMarker&&c.panning&&b.pan(a,c.panning)}},drop:function(a){var b=this.chart,c=this.hasPinched;if(this.selectionMarker){var d={xAxis:[],yAxis:[],originalEvent:a.originalEvent||a},e=this.selectionMarker,f=e.attr?e.attr("x"):e.x,g=e.attr?e.attr("y"):e.y,h=e.attr?e.attr("width"):e.width,i=e.attr?e.attr("height"):e.height,j;if(this.hasDragged||c)q(b.axes,function(b){if(b.zoomEnabled){var c=b.horiz,e=a.type==="touchend"?b.minPixelPadding: -0,n=b.toValue((c?f:g)+e),c=b.toValue((c?f+h:g+i)-e);!isNaN(n)&&!isNaN(c)&&(d[b.coll].push({axis:b,min:C(n,c),max:u(n,c)}),j=!0)}}),j&&K(b,"selection",d,function(a){b.zoom(r(a,c?{animation:!1}:null))});this.selectionMarker=this.selectionMarker.destroy();c&&this.scaleGroups()}if(b)A(b.container,{cursor:b._cursor}),b.cancelClick=this.hasDragged>10,b.mouseIsDown=this.hasDragged=this.hasPinched=!1,this.pinchDown=[]},onContainerMouseDown:function(a){a=this.normalize(a);a.preventDefault&&a.preventDefault(); -this.dragStart(a)},onDocumentMouseUp:function(a){W[pa]&&W[pa].pointer.drop(a)},onDocumentMouseMove:function(a){var b=this.chart,c=this.chartPosition,d=b.hoverSeries,a=this.normalize(a,c);c&&d&&!this.inClass(a.target,"highcharts-tracker")&&!b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&&this.reset()},onContainerMouseLeave:function(){var a=W[pa];if(a)a.pointer.reset(),a.pointer.chartPosition=null},onContainerMouseMove:function(a){var b=this.chart;pa=b.index;a=this.normalize(a);a.returnValue= -!1;b.mouseIsDown==="mousedown"&&this.drag(a);(this.inClass(a.target,"highcharts-tracker")||b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop))&&!b.openMenu&&this.runPointActions(a)},inClass:function(a,b){for(var c;a;){if(c=F(a,"class"))if(c.indexOf(b)!==-1)return!0;else if(c.indexOf("highcharts-container")!==-1)return!1;a=a.parentNode}},onTrackerMouseOut:function(a){var b=this.chart.hoverSeries,c=(a=a.relatedTarget||a.toElement)&&a.point&&a.point.series;if(b&&!b.options.stickyTracking&&!this.inClass(a, -"highcharts-tooltip")&&c!==b)b.onMouseOut()},onContainerClick:function(a){var b=this.chart,c=b.hoverPoint,d=b.plotLeft,e=b.plotTop,a=this.normalize(a);a.cancelBubble=!0;b.cancelClick||(c&&this.inClass(a.target,"highcharts-tracker")?(K(c.series,"click",r(a,{point:c})),b.hoverPoint&&c.firePointEvent("click",a)):(r(a,this.getCoordinates(a)),b.isInsidePlot(a.chartX-d,a.chartY-e)&&K(b,"click",a)))},setDOMEvents:function(){var a=this,b=a.chart.container;b.onmousedown=function(b){a.onContainerMouseDown(b)}; -b.onmousemove=function(b){a.onContainerMouseMove(b)};b.onclick=function(b){a.onContainerClick(b)};N(b,"mouseleave",a.onContainerMouseLeave);$a===1&&N(x,"mouseup",a.onDocumentMouseUp);if(Za)b.ontouchstart=function(b){a.onContainerTouchStart(b)},b.ontouchmove=function(b){a.onContainerTouchMove(b)},$a===1&&N(x,"touchend",a.onDocumentTouchEnd)},destroy:function(){var a;X(this.chart.container,"mouseleave",this.onContainerMouseLeave);$a||(X(x,"mouseup",this.onDocumentMouseUp),X(x,"touchend",this.onDocumentTouchEnd)); -clearInterval(this.tooltipTimeout);for(a in this)this[a]=null}};r(S.Pointer.prototype,{pinchTranslate:function(a,b,c,d,e,f){(this.zoomHor||this.pinchHor)&&this.pinchTranslateDirection(!0,a,b,c,d,e,f);(this.zoomVert||this.pinchVert)&&this.pinchTranslateDirection(!1,a,b,c,d,e,f)},pinchTranslateDirection:function(a,b,c,d,e,f,g,h){var i=this.chart,j=a?"x":"y",k=a?"X":"Y",l="chart"+k,m=a?"width":"height",n=i["plot"+(a?"Left":"Top")],o,p,q=h||1,r=i.inverted,D=i.bounds[a?"h":"v"],u=b.length===1,s=b[0][l], -v=c[0][l],t=!u&&b[1][l],w=!u&&c[1][l],x,c=function(){!u&&Q(s-t)>20&&(q=h||Q(v-w)/Q(s-t));p=(n-v)/q+s;o=i["plot"+(a?"Width":"Height")]/q};c();b=p;bD.max&&(b=D.max-o,x=!0);x?(v-=0.8*(v-g[j][0]),u||(w-=0.8*(w-g[j][1])),c()):g[j]=[v,w];r||(f[j]=p-n,f[m]=o);f=r?1/q:q;e[m]=o;e[j]=b;d[r?a?"scaleY":"scaleX":"scale"+k]=q;d["translate"+k]=f*n+(v-f*s)},pinch:function(a){var b=this,c=b.chart,d=b.pinchDown,e=b.followTouchMove,f=a.touches,g=f.length,h=b.lastValidTouch,i=b.hasZoom,j=b.selectionMarker, -k={},l=g===1&&(b.inClass(a.target,"highcharts-tracker")&&c.runTrackerClick||c.runChartClick),m={};(i||e)&&!l&&a.preventDefault();Ua(f,function(a){return b.normalize(a)});if(a.type==="touchstart")q(f,function(a,b){d[b]={chartX:a.chartX,chartY:a.chartY}}),h.x=[d[0].chartX,d[1]&&d[1].chartX],h.y=[d[0].chartY,d[1]&&d[1].chartY],q(c.axes,function(a){if(a.zoomEnabled){var b=c.bounds[a.horiz?"h":"v"],d=a.minPixelPadding,e=a.toPixels(p(a.options.min,a.dataMin)),f=a.toPixels(p(a.options.max,a.dataMax)),g= -C(e,f),e=u(e,f);b.min=C(a.pos,g-d);b.max=u(a.pos+a.len,e+d)}});else if(d.length){if(!j)b.selectionMarker=j=r({destroy:sa},c.plotBox);b.pinchTranslate(d,f,k,j,m,h);b.hasPinched=i;b.scaleGroups(k,m);!i&&e&&g===1&&this.runPointActions(b.normalize(a))}},onContainerTouchStart:function(a){var b=this.chart;pa=b.index;a.touches.length===1?(a=this.normalize(a),b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)?(this.runPointActions(a),this.pinch(a)):this.reset()):a.touches.length===2&&this.pinch(a)},onContainerTouchMove:function(a){(a.touches.length=== -1||a.touches.length===2)&&this.pinch(a)},onDocumentTouchEnd:function(a){W[pa]&&W[pa].pointer.drop(a)}});if(H.PointerEvent||H.MSPointerEvent){var ua={},yb=!!H.PointerEvent,Wb=function(){var a,b=[];b.item=function(a){return this[a]};for(a in ua)ua.hasOwnProperty(a)&&b.push({pageX:ua[a].pageX,pageY:ua[a].pageY,target:ua[a].target});return b},zb=function(a,b,c,d){a=a.originalEvent||a;if((a.pointerType==="touch"||a.pointerType===a.MSPOINTER_TYPE_TOUCH)&&W[pa])d(a),d=W[pa].pointer,d[b]({type:c,target:a.currentTarget, -preventDefault:sa,touches:Wb()})};r(Va.prototype,{onContainerPointerDown:function(a){zb(a,"onContainerTouchStart","touchstart",function(a){ua[a.pointerId]={pageX:a.pageX,pageY:a.pageY,target:a.currentTarget}})},onContainerPointerMove:function(a){zb(a,"onContainerTouchMove","touchmove",function(a){ua[a.pointerId]={pageX:a.pageX,pageY:a.pageY};if(!ua[a.pointerId].target)ua[a.pointerId].target=a.currentTarget})},onDocumentPointerUp:function(a){zb(a,"onContainerTouchEnd","touchend",function(a){delete ua[a.pointerId]})}, -batchMSEvents:function(a){a(this.chart.container,yb?"pointerdown":"MSPointerDown",this.onContainerPointerDown);a(this.chart.container,yb?"pointermove":"MSPointerMove",this.onContainerPointerMove);a(x,yb?"pointerup":"MSPointerUp",this.onDocumentPointerUp)}});Ma(Va.prototype,"init",function(a,b,c){a.call(this,b,c);(this.hasZoom||this.followTouchMove)&&A(b.container,{"-ms-touch-action":P,"touch-action":P})});Ma(Va.prototype,"setDOMEvents",function(a){a.apply(this);(this.hasZoom||this.followTouchMove)&& -this.batchMSEvents(N)});Ma(Va.prototype,"destroy",function(a){this.batchMSEvents(X);a.call(this)})}var kb=S.Legend=function(a,b){this.init(a,b)};kb.prototype={init:function(a,b){var c=this,d=b.itemStyle,e=p(b.padding,8),f=b.itemMarginTop||0;this.options=b;if(b.enabled)c.itemStyle=d,c.itemHiddenStyle=w(d,b.itemHiddenStyle),c.itemMarginTop=f,c.padding=e,c.initialItemX=e,c.initialItemY=e-5,c.maxItemWidth=0,c.chart=a,c.itemHeight=0,c.lastLineHeight=0,c.symbolWidth=p(b.symbolWidth,16),c.pages=[],c.render(), -N(c.chart,"endResize",function(){c.positionCheckboxes()})},colorizeItem:function(a,b){var c=this.options,d=a.legendItem,e=a.legendLine,f=a.legendSymbol,g=this.itemHiddenStyle.color,c=b?c.itemStyle.color:g,h=b?a.legendColor||a.color||"#CCC":g,g=a.options&&a.options.marker,i={fill:h},j;d&&d.css({fill:c,color:c});e&&e.attr({stroke:h});if(f){if(g&&f.isMarker)for(j in i.stroke=h,g=a.convertAttribs(g),g)d=g[j],d!==t&&(i[j]=d);f.attr(i)}},positionItem:function(a){var b=this.options,c=b.symbolPadding,b=!b.rtl, -d=a._legendItemPos,e=d[0],d=d[1],f=a.checkbox;a.legendGroup&&a.legendGroup.translate(b?e:this.legendWidth-e-2*c-4,d);if(f)f.x=e,f.y=d},destroyItem:function(a){var b=a.checkbox;q(["legendItem","legendLine","legendSymbol","legendGroup"],function(b){a[b]&&(a[b]=a[b].destroy())});b&&Pa(a.checkbox)},destroy:function(){var a=this.group,b=this.box;if(b)this.box=b.destroy();if(a)this.group=a.destroy()},positionCheckboxes:function(a){var b=this.group.alignAttr,c,d=this.clipHeight||this.legendHeight;if(b)c= -b.translateY,q(this.allItems,function(e){var f=e.checkbox,g;f&&(g=c+f.y+(a||0)+3,A(f,{left:b.translateX+e.checkboxOffset+f.x-20+"px",top:g+"px",display:g>c-6&&g(m||b.chartWidth-2*j-q-d.x))this.itemX=q,this.itemY+=o+this.lastLineHeight+n,this.lastLineHeight=0;this.maxItemWidth=u(this.maxItemWidth,f);this.lastItemY=o+this.itemY+n;this.lastLineHeight=u(g,this.lastLineHeight);a._legendItemPos=[this.itemX,this.itemY];e?this.itemX+=f:(this.itemY+=o+g+n,this.lastLineHeight=g);this.offsetWidth=m||u((e?this.itemX-q-k:f)+j,this.offsetWidth)},getAllItems:function(){var a=[];q(this.chart.series, -function(b){var c=b.options;if(p(c.showInLegend,!s(c.linkedTo)?t:!1,!0))a=a.concat(b.legendItems||(c.legendType==="point"?b.data:b))});return a},render:function(){var a=this,b=a.chart,c=b.renderer,d=a.group,e,f,g,h,i=a.box,j=a.options,k=a.padding,l=j.borderWidth,m=j.backgroundColor;a.itemX=a.initialItemX;a.itemY=a.initialItemY;a.offsetWidth=0;a.lastItemY=0;if(!d)a.group=d=c.g("legend").attr({zIndex:7}).add(),a.contentGroup=c.g().attr({zIndex:1}).add(d),a.scrollGroup=c.g().add(a.contentGroup);a.renderTitle(); -e=a.getAllItems();nb(e,function(a,b){return(a.options&&a.options.legendIndex||0)-(b.options&&b.options.legendIndex||0)});j.reversed&&e.reverse();a.allItems=e;a.display=f=!!e.length;q(e,function(b){a.renderItem(b)});g=j.width||a.offsetWidth;h=a.lastItemY+a.lastLineHeight+a.titleHeight;h=a.handleOverflow(h);if(l||m){g+=k;h+=k;if(i){if(g>0&&h>0)i[i.isNew?"attr":"animate"](i.crisp({width:g,height:h})),i.isNew=!1}else a.box=i=c.rect(0,0,g,h,j.borderRadius,l||0).attr({stroke:j.borderColor,"stroke-width":l|| -0,fill:m||P}).add(d).shadow(j.shadow),i.isNew=!0;i[f?"show":"hide"]()}a.legendWidth=g;a.legendHeight=h;q(e,function(b){a.positionItem(b)});f&&d.align(r({width:g,height:h},j),!0,"spacingBox");b.isResizing||this.positionCheckboxes()},handleOverflow:function(a){var b=this,c=this.chart,d=c.renderer,e=this.options,f=e.y,f=c.spacingBox.height+(e.verticalAlign==="top"?-f:f)-this.padding,g=e.maxHeight,h,i=this.clipRect,j=e.navigation,k=p(j.animation,!0),l=j.arrowSize||12,m=this.nav,n=this.pages,o,r=this.allItems; -e.layout==="horizontal"&&(f/=2);g&&(f=C(f,g));n.length=0;if(a>f&&!e.useHTML){this.clipHeight=h=u(f-20-this.titleHeight-this.padding,0);this.currentPage=p(this.currentPage,1);this.fullHeight=a;q(r,function(a,b){var c=a._legendItemPos[1],d=v(a.legendItem.getBBox().height),e=n.length;if(!e||c-n[e-1]>h&&(o||c)!==n[e-1])n.push(o||c),e++;b===r.length-1&&c+d-n[e-1]>h&&n.push(c);c!==o&&(o=c)});if(!i)i=b.clipRect=d.clipRect(0,this.padding,9999,0),b.contentGroup.clip(i);i.attr({height:h});if(!m)this.nav=m= -d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle",0,0,l,l).on("click",function(){b.scroll(-1,k)}).add(m),this.pager=d.text("",15,10).css(j.style).add(m),this.down=d.symbol("triangle-down",0,0,l,l).on("click",function(){b.scroll(1,k)}).add(m);b.scroll(0);a=f}else if(m)i.attr({height:c.chartHeight}),m.hide(),this.scrollGroup.attr({translateY:1}),this.clipHeight=0;return a},scroll:function(a,b){var c=this.pages,d=c.length,e=this.currentPage+a,f=this.clipHeight,g=this.options.navigation, -h=g.activeColor,g=g.inactiveColor,i=this.pager,j=this.padding;e>d&&(e=d);if(e>0)b!==t&&Qa(b,this.chart),this.nav.attr({translateX:j,translateY:f+this.padding+7+this.titleHeight,visibility:"visible"}),this.up.attr({fill:e===1?g:h}).css({cursor:e===1?"default":"pointer"}),i.attr({text:e+"/"+d}),this.down.attr({x:18+this.pager.getBBox().width,fill:e===d?g:h}).css({cursor:e===d?"default":"pointer"}),c=-c[e-1]+this.initialItemY,this.scrollGroup.animate({translateY:c}),this.currentPage=e,this.positionCheckboxes(c)}}; -M=S.LegendSymbolMixin={drawRectangle:function(a,b){var c=a.options.symbolHeight||12;b.legendSymbol=this.chart.renderer.rect(0,a.baseline-5-c/2,a.symbolWidth,c,a.options.symbolRadius||0).attr({zIndex:3}).add(b.legendGroup)},drawLineMarker:function(a){var b=this.options,c=b.marker,d;d=a.symbolWidth;var e=this.chart.renderer,f=this.legendGroup,a=a.baseline-v(e.fontMetrics(a.options.itemStyle.fontSize,this.legendItem).b*0.3),g;if(b.lineWidth){g={"stroke-width":b.lineWidth};if(b.dashStyle)g.dashstyle= -b.dashStyle;this.legendLine=e.path(["M",0,a,"L",d,a]).attr(g).add(f)}if(c&&c.enabled!==!1)b=c.radius,this.legendSymbol=d=e.symbol(this.symbol,d/2-b,a-b,2*b,2*b).add(f),d.isMarker=!0}};(/Trident\/7\.0/.test(wa)||Ta)&&Ma(kb.prototype,"positionItem",function(a,b){var c=this,d=function(){b._legendItemPos&&a.call(c,b)};d();setTimeout(d)});Xa.prototype={init:function(a,b){var c,d=a.series;a.series=null;c=w(L,a);c.series=a.series=d;this.userOptions=a;d=c.chart;this.margin=this.splashArray("margin",d);this.spacing= -this.splashArray("spacing",d);var e=d.events;this.bounds={h:{},v:{}};this.callback=b;this.isResizing=0;this.options=c;this.axes=[];this.series=[];this.hasCartesianSeries=d.showAxes;var f=this,g;f.index=W.length;W.push(f);$a++;d.reflow!==!1&&N(f,"load",function(){f.initReflow()});if(e)for(g in e)N(f,g,e[g]);f.xAxis=[];f.yAxis=[];f.animation=ga?!1:p(d.animation,!0);f.pointCount=f.colorCounter=f.symbolCounter=0;f.firstRender()},initSeries:function(a){var b=this.options.chart;(b=J[a.type||b.type||b.defaultSeriesType])|| -oa(17,!0);b=new b;b.init(this,a);return b},isInsidePlot:function(a,b,c){var d=c?b:a,a=c?a:b;return d>=0&&d<=this.plotWidth&&a>=0&&a<=this.plotHeight},adjustTickAmounts:function(){this.options.chart.alignTicks!==!1&&q(this.axes,function(a){a.adjustTickAmount()});this.maxTicks=null},redraw:function(a){var b=this.axes,c=this.series,d=this.pointer,e=this.legend,f=this.isDirtyLegend,g,h,i=this.hasCartesianSeries,j=this.isDirtyBox,k=c.length,l=k,m=this.renderer,n=m.isHidden(),o=[];Qa(a,this);n&&this.cloneRenderTo(); -for(this.layOutTitles();l--;)if(a=c[l],a.options.stacking&&(g=!0,a.isDirty)){h=!0;break}if(h)for(l=k;l--;)if(a=c[l],a.options.stacking)a.isDirty=!0;q(c,function(a){a.isDirty&&a.options.legendType==="point"&&(f=!0)});if(f&&e.options.enabled)e.render(),this.isDirtyLegend=!1;g&&this.getStacks();if(i){if(!this.isResizing)this.maxTicks=null,q(b,function(a){a.setScale()});this.adjustTickAmounts()}this.getMargins();i&&(q(b,function(a){a.isDirty&&(j=!0)}),q(b,function(a){if(a.isDirtyExtremes)a.isDirtyExtremes= -!1,o.push(function(){K(a,"afterSetExtremes",r(a.eventArgs,a.getExtremes()));delete a.eventArgs});(j||g)&&a.redraw()}));j&&this.drawChartBox();q(c,function(a){a.isDirty&&a.visible&&(!a.isCartesian||a.xAxis)&&a.redraw()});d&&d.reset(!0);m.draw();K(this,"redraw");n&&this.cloneRenderTo(!0);q(o,function(a){a.call()})},get:function(a){var b=this.axes,c=this.series,d,e;for(d=0;d -19?this.containerHeight:400))},cloneRenderTo:function(a){var b=this.renderToClone,c=this.container;a?b&&(this.renderTo.appendChild(c),Pa(b),delete this.renderToClone):(c&&c.parentNode===this.renderTo&&this.renderTo.removeChild(c),this.renderToClone=b=this.renderTo.cloneNode(0),A(b,{position:"absolute",top:"-9999px",display:"block"}),b.style.setProperty&&b.style.setProperty("display","block","important"),x.body.appendChild(b),c&&b.appendChild(c))},getContainer:function(){var a,b=this.options.chart, -c,d,e;this.renderTo=a=b.renderTo;e="highcharts-"+tb++;if(Fa(a))this.renderTo=a=x.getElementById(a);a||oa(13,!0);c=z(F(a,"data-highcharts-chart"));!isNaN(c)&&W[c]&&W[c].hasRendered&&W[c].destroy();F(a,"data-highcharts-chart",this.index);a.innerHTML="";!b.skipClone&&!a.offsetWidth&&this.cloneRenderTo();this.getChartSize();c=this.chartWidth;d=this.chartHeight;this.container=a=$(Ja,{className:"highcharts-container"+(b.className?" "+b.className:""),id:e},r({position:"relative",overflow:"hidden",width:c+ -"px",height:d+"px",textAlign:"left",lineHeight:"normal",zIndex:0,"-webkit-tap-highlight-color":"rgba(0,0,0,0)"},b.style),this.renderToClone||a);this._cursor=a.style.cursor;this.renderer=b.forExport?new ta(a,c,d,b.style,!0):new Ya(a,c,d,b.style);ga&&this.renderer.create(this,a,c,d)},getMargins:function(){var a=this.spacing,b,c=this.legend,d=this.margin,e=this.options.legend,f=p(e.margin,20),g=e.x,h=e.y,i=e.align,j=e.verticalAlign,k=this.titleOffset;this.resetMargins();b=this.axisOffset;if(k&&!s(d[0]))this.plotTop= -u(this.plotTop,k+this.options.title.margin+a[0]);if(c.display&&!e.floating)if(i==="right"){if(!s(d[1]))this.marginRight=u(this.marginRight,c.legendWidth-g+f+a[1])}else if(i==="left"){if(!s(d[3]))this.plotLeft=u(this.plotLeft,c.legendWidth+g+f+a[3])}else if(j==="top"){if(!s(d[0]))this.plotTop=u(this.plotTop,c.legendHeight+h+f+a[0])}else if(j==="bottom"&&!s(d[2]))this.marginBottom=u(this.marginBottom,c.legendHeight-h+f+a[2]);this.extraBottomMargin&&(this.marginBottom+=this.extraBottomMargin);this.extraTopMargin&& -(this.plotTop+=this.extraTopMargin);this.hasCartesianSeries&&q(this.axes,function(a){a.getOffset()});s(d[3])||(this.plotLeft+=b[3]);s(d[0])||(this.plotTop+=b[0]);s(d[2])||(this.marginBottom+=b[2]);s(d[1])||(this.marginRight+=b[1]);this.setChartSize()},reflow:function(a){var b=this,c=b.options.chart,d=b.renderTo,e=c.width||hb(d,"width"),f=c.height||hb(d,"height"),c=a?a.target:H,d=function(){if(b.container)b.setSize(e,f,!1),b.hasUserSize=null};if(!b.hasUserSize&&e&&f&&(c===H||c===x)){if(e!==b.containerWidth|| -f!==b.containerHeight)clearTimeout(b.reflowTimeout),a?b.reflowTimeout=setTimeout(d,100):d();b.containerWidth=e;b.containerHeight=f}},initReflow:function(){var a=this,b=function(b){a.reflow(b)};N(H,"resize",b);N(a,"destroy",function(){X(H,"resize",b)})},setSize:function(a,b,c){var d=this,e,f,g;d.isResizing+=1;g=function(){d&&K(d,"endResize",null,function(){d.isResizing-=1})};Qa(c,d);d.oldChartHeight=d.chartHeight;d.oldChartWidth=d.chartWidth;if(s(a))d.chartWidth=e=u(0,v(a)),d.hasUserSize=!!e;if(s(b))d.chartHeight= -f=u(0,v(b));(va?ib:A)(d.container,{width:e+"px",height:f+"px"},va);d.setChartSize(!0);d.renderer.setSize(e,f,c);d.maxTicks=null;q(d.axes,function(a){a.isDirty=!0;a.setScale()});q(d.series,function(a){a.isDirty=!0});d.isDirtyLegend=!0;d.isDirtyBox=!0;d.layOutTitles();d.getMargins();d.redraw(c);d.oldChartHeight=null;K(d,"resize");va===!1?g():setTimeout(g,va&&va.duration||500)},setChartSize:function(a){var b=this.inverted,c=this.renderer,d=this.chartWidth,e=this.chartHeight,f=this.options.chart,g=this.spacing, -h=this.clipOffset,i,j,k,l;this.plotLeft=i=v(this.plotLeft);this.plotTop=j=v(this.plotTop);this.plotWidth=k=u(0,v(d-i-this.marginRight));this.plotHeight=l=u(0,v(e-j-this.marginBottom));this.plotSizeX=b?l:k;this.plotSizeY=b?k:l;this.plotBorderWidth=f.plotBorderWidth||0;this.spacingBox=c.spacingBox={x:g[3],y:g[0],width:d-g[3]-g[1],height:e-g[0]-g[2]};this.plotBox=c.plotBox={x:i,y:j,width:k,height:l};d=2*U(this.plotBorderWidth/2);b=Ka(u(d,h[3])/2);c=Ka(u(d,h[0])/2);this.clipBox={x:b,y:c,width:U(this.plotSizeX- -u(d,h[1])/2-b),height:u(0,U(this.plotSizeY-u(d,h[2])/2-c))};a||q(this.axes,function(a){a.setAxisSize();a.setAxisTranslation()})},resetMargins:function(){var a=this.spacing,b=this.margin;this.plotTop=p(b[0],a[0]);this.marginRight=p(b[1],a[1]);this.marginBottom=p(b[2],a[2]);this.plotLeft=p(b[3],a[3]);this.axisOffset=[0,0,0,0];this.clipOffset=[0,0,0,0]},drawChartBox:function(){var a=this.options.chart,b=this.renderer,c=this.chartWidth,d=this.chartHeight,e=this.chartBackground,f=this.plotBackground,g= -this.plotBorder,h=this.plotBGImage,i=a.borderWidth||0,j=a.backgroundColor,k=a.plotBackgroundColor,l=a.plotBackgroundImage,m=a.plotBorderWidth||0,n,o=this.plotLeft,p=this.plotTop,q=this.plotWidth,r=this.plotHeight,u=this.plotBox,s=this.clipRect,v=this.clipBox;n=i+(a.shadow?8:0);if(i||j)if(e)e.animate(e.crisp({width:c-n,height:d-n}));else{e={fill:j||P};if(i)e.stroke=a.borderColor,e["stroke-width"]=i;this.chartBackground=b.rect(n/2,n/2,c-n,d-n,a.borderRadius,i).attr(e).addClass("highcharts-background").add().shadow(a.shadow)}if(k)f? -f.animate(u):this.plotBackground=b.rect(o,p,q,r,0).attr({fill:k}).add().shadow(a.plotShadow);if(l)h?h.animate(u):this.plotBGImage=b.image(l,o,p,q,r).add();s?s.animate({width:v.width,height:v.height}):this.clipRect=b.clipRect(v);if(m)g?g.animate(g.crisp({x:o,y:p,width:q,height:r})):this.plotBorder=b.rect(o,p,q,r,0,-m).attr({stroke:a.plotBorderColor,"stroke-width":m,fill:P,zIndex:1}).add();this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart,c,d=a.options.series,e,f;q(["inverted", -"angular","polar"],function(g){c=J[b.type||b.defaultSeriesType];f=a[g]||b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&&e--;)(c=J[d[e].type])&&c.prototype[g]&&(f=!0);a[g]=f})},linkSeries:function(){var a=this,b=a.series;q(b,function(a){a.linkedSeries.length=0});q(b,function(b){var d=b.options.linkedTo;if(Fa(d)&&(d=d===":previous"?a.series[b.index-1]:a.get(d)))d.linkedSeries.push(b),b.linkedParent=d})},renderSeries:function(){q(this.series,function(a){a.translate();a.setTooltipPoints&&a.setTooltipPoints(); -a.render()})},renderLabels:function(){var a=this,b=a.options.labels;b.items&&q(b.items,function(c){var d=r(b.style,c.style),e=z(d.left)+a.plotLeft,f=z(d.top)+a.plotTop+12;delete d.left;delete d.top;a.renderer.text(c.html,e,f).attr({zIndex:2}).css(d).add()})},render:function(){var a=this.axes,b=this.renderer,c=this.options;this.setTitle();this.legend=new kb(this,c.legend);this.getStacks();q(a,function(a){a.setScale()});this.getMargins();this.maxTicks=null;q(a,function(a){a.setTickPositions(!0);a.setMaxTicks()}); -this.adjustTickAmounts();this.getMargins();this.drawChartBox();this.hasCartesianSeries&&q(a,function(a){a.render()});if(!this.seriesGroup)this.seriesGroup=b.g("series-group").attr({zIndex:3}).add();this.renderSeries();this.renderLabels();this.showCredits(c.credits);this.hasRendered=!0},showCredits:function(a){if(a.enabled&&!this.credits)this.credits=this.renderer.text(a.text,0,0).on("click",function(){if(a.href)location.href=a.href}).attr({align:a.position.align,zIndex:8}).css(a.style).add().align(a.position)}, -destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;K(a,"destroy");W[a.index]=t;$a--;a.renderTo.removeAttribute("data-highcharts-chart");X(a);for(e=b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();q("title,subtitle,chartBackground,plotBackground,plotBGImage,plotBorder,seriesGroup,clipRect,credits,pointer,scroller,rangeSelector,legend,resetZoomButton,tooltip,renderer".split(","),function(b){var c=a[b];c&&c.destroy&&(a[b]=c.destroy())});if(d)d.innerHTML= -"",X(d),f&&Pa(d);for(e in a)delete a[e]},isReadyToRender:function(){var a=this;return!ba&&H==H.top&&x.readyState!=="complete"||ga&&!H.canvg?(ga?Lb.push(function(){a.firstRender()},a.options.global.canvasToolsURL):x.attachEvent("onreadystatechange",function(){x.detachEvent("onreadystatechange",a.firstRender);x.readyState==="complete"&&a.firstRender()}),!1):!0},firstRender:function(){var a=this,b=a.options,c=a.callback;if(a.isReadyToRender()){a.getContainer();K(a,"init");a.resetMargins();a.setChartSize(); -a.propFromSeries();a.getAxes();q(b.series||[],function(b){a.initSeries(b)});a.linkSeries();K(a,"beforeRender");if(S.Pointer)a.pointer=new Va(a,b);a.render();a.renderer.draw();c&&c.apply(a,[a]);q(a.callbacks,function(b){b.apply(a,[a])});a.cloneRenderTo(!0);K(a,"load")}},splashArray:function(a,b){var c=b[a],c=da(c)?c:[c,c,c,c];return[p(b[a+"Top"],c[0]),p(b[a+"Right"],c[1]),p(b[a+"Bottom"],c[2]),p(b[a+"Left"],c[3])]}};Xa.prototype.callbacks=[];Z=S.CenteredSeriesMixin={getCenter:function(){var a=this.options, -b=this.chart,c=2*(a.slicedOffset||0),d,e=b.plotWidth-2*c,f=b.plotHeight-2*c,b=a.center,a=[p(b[0],"50%"),p(b[1],"50%"),a.size||"100%",a.innerSize||0],g=C(e,f),h;return Ua(a,function(a,b){h=/%$/.test(a);d=b<2||b===2&&h;return(h?[e,f,g,g][b]*z(a)/100:a)+(d?c:0)})}};var Ea=function(){};Ea.prototype={init:function(a,b,c){this.series=a;this.applyOptions(b,c);this.pointAttr={};if(a.options.colorByPoint&&(b=a.options.colors||a.chart.options.colors,this.color=this.color||b[a.colorCounter++],a.colorCounter=== -b.length))a.colorCounter=0;a.chart.pointCount++;return this},applyOptions:function(a,b){var c=this.series,d=c.options.pointValKey||c.pointValKey,a=Ea.prototype.optionsToObject.call(this,a);r(this,a);this.options=this.options?r(this.options,a):a;if(d)this.y=this[d];if(this.x===t&&c)this.x=b===t?c.autoIncrement():b;return this},optionsToObject:function(a){var b={},c=this.series,d=c.pointArrayMap||["y"],e=d.length,f=0,g=0;if(typeof a==="number"||a===null)b[d[0]]=a;else if(La(a)){if(a.length>e){c=typeof a[0]; -if(c==="string")b.name=a[0];else if(c==="number")b.x=a[0];f++}for(;ga+1&&b.push(d.slice(a+1,g)),a=g):g===e-1&&b.push(d.slice(a+1,g+1))});this.segments=b},setOptions:function(a){var b=this.chart,c=b.options.plotOptions,b=b.userOptions||{},d=b.plotOptions||{},e=c[this.type];this.userOptions=a;c=w(e,c.series, -a);this.tooltipOptions=w(L.tooltip,L.plotOptions[this.type].tooltip,b.tooltip,d.series&&d.series.tooltip,d[this.type]&&d[this.type].tooltip,a.tooltip);e.marker===null&&delete c.marker;return c},getCyclic:function(a,b,c){var d=this.userOptions,e="_"+a+"Index",f=a+"Counter";b||(s(d[e])?b=d[e]:(d[e]=b=this.chart[f]%c.length,this.chart[f]+=1),b=c[b]);this[a]=b},getColor:function(){this.options.colorByPoint||this.getCyclic("color",this.options.color||ca[this.type].color,this.chart.options.colors)},getSymbol:function(){var a= -this.options.marker;this.getCyclic("symbol",a.symbol,this.chart.options.symbols);if(/^url/.test(this.symbol))a.radius=0},drawLegendSymbol:M.drawLineMarker,setData:function(a,b,c,d){var e=this,f=e.points,g=f&&f.length||0,h,i=e.options,j=e.chart,k=null,l=e.xAxis,m=l&&!!l.categories,n=e.tooltipPoints,o=i.turboThreshold,r=this.xData,u=this.yData,s=(h=e.pointArrayMap)&&h.length,a=a||[];h=a.length;b=p(b,!0);if(d!==!1&&h&&g===h&&!e.cropped&&!e.hasGroupedData)q(a,function(a,b){f[b].update(a,!1)});else{e.xIncrement= -null;e.pointRange=m?1:i.pointRange;e.colorCounter=0;q(this.parallelArrays,function(a){e[a+"Data"].length=0});if(o&&h>o){for(c=0;k===null&&cj||this.forceCrop))if(m= -h.getExtremes(),n=m.min,m=m.max,b[d-1]m)b=[],c=[];else if(b[0]m)e=this.cropData(this.xData,this.yData,n,m),b=e.xData,c=e.yData,e=e.start,f=!0,k=b.length;for(a=b.length-1;a>=0;a--)d=b[a]-b[a-1],!f&&b[a]>n&&b[a]0&&(g===t||d=c){f=u(0,i-h);break}for(;id){g=i+h;break}return{xData:a.slice(f,g),yData:b.slice(f,g),start:f,end:g}},generatePoints:function(){var a=this.options.data,b=this.data,c,d=this.processedXData,e=this.processedYData,f=this.pointClass,g=d.length,h=this.cropStart||0,i,j=this.hasGroupedData,k,l=[],m;if(!b&&!j)b=[],b.length=a.length,b=this.data=b;for(m=0;m0),j=this.getExtremesFromAll||this.cropped||(c[l+1]||j)>=g&& -(c[l-1]||j)<=h,i&&j)if(i=k.length)for(;i--;)k[i]!==null&&(e[f++]=k[i]);else e[f++]=k;this.dataMin=p(void 0,Na(e));this.dataMax=p(void 0,Ba(e))},translate:function(){this.processedXData||this.processData();this.generatePoints();for(var a=this.options,b=a.stacking,c=this.xAxis,d=c.categories,e=this.yAxis,f=this.points,g=f.length,h=!!this.modifyValue,i=a.pointPlacement,j=i==="between"||ia(i),k=a.threshold,a=0;a0||j))g.graphic=c.renderer.symbol(i,d-h,e-h,2*h,2*h).attr(a).add(n)}else if(k)g.graphic=k.destroy()},convertAttribs:function(a,b,c,d){var e=this.pointAttrToOptions,f,g,h={},a=a||{},b=b||{},c=c||{},d=d||{};for(f in e)g=e[f],h[f]=p(a[g],b[f],c[f],d[f]);return h},getAttribs:function(){var a=this,b=a.options,c=ca[a.type].marker?b.marker:b,d=c.states,e=d.hover,f,g=a.color;f={stroke:g,fill:g};var h=a.points||[],i,j=[],k,l=a.pointAttrToOptions; -k=a.hasPointSpecificOptions;var m=b.negativeColor,n=c.lineColor,o=c.fillColor;i=b.turboThreshold;var p;b.marker?(e.radius=e.radius||c.radius+e.radiusPlus,e.lineWidth=e.lineWidth||c.lineWidth+e.lineWidthPlus):e.color=e.color||ya(e.color||g).brighten(e.brightness).get();j[""]=a.convertAttribs(c,f);q(["hover","select"],function(b){j[b]=a.convertAttribs(d[b],j[""])});a.pointAttr=j;g=h.length;if(!i||g -1?b=b.concat(c):d.push(e[0])});a.singlePoints=d;return a.graphPath=b},drawGraph:function(){var a=this,b=this.options,c=[["graph",b.lineColor||this.color]],d=b.lineWidth,e=b.dashStyle,f=b.linecap!=="square",g=this.getGraphPath(),h=b.negativeColor;h&&c.push(["graphNeg",h]);q(c,function(c,h){var k=c[0],l=a[k];if(l)ab(l),l.animate({d:g});else if(d&&g.length)l={stroke:c[1],"stroke-width":d,fill:P,zIndex:1},e?l.dashstyle=e:f&&(l["stroke-linecap"]=l["stroke-linejoin"]="round"),a[k]=a.chart.renderer.path(g).attr(l).add(a.group).shadow(!h&& -b.shadow)})},clipNeg:function(){var a=this.options,b=this.chart,c=b.renderer,d=a.negativeColor||a.negativeFillColor,e,f=this.graph,g=this.area,h=this.posClip,i=this.negClip;e=b.chartWidth;var j=b.chartHeight,k=u(e,j),l=this.yAxis;if(d&&(f||g)){d=v(l.toPixels(a.threshold||0,!0));d<0&&(k-=d);a={x:0,y:0,width:k,height:d};k={x:0,y:d,width:k,height:k};if(b.inverted)a.height=k.y=b.plotWidth-d,c.isVML&&(a={x:b.plotWidth-d-b.plotLeft,y:0,width:e,height:j},k={x:d+b.plotLeft-e,y:0,width:b.plotLeft+d,height:e}); -l.reversed?(b=k,e=a):(b=a,e=k);h?(h.animate(b),i.animate(e)):(this.posClip=h=c.clipRect(b),this.negClip=i=c.clipRect(e),f&&this.graphNeg&&(f.clip(h),this.graphNeg.clip(i)),g&&(g.clip(h),this.areaNeg.clip(i)))}},invertGroups:function(){function a(){var a={width:b.yAxis.len,height:b.xAxis.len};q(["group","markerGroup"],function(c){b[c]&&b[c].attr(a).invert()})}var b=this,c=b.chart;if(b.xAxis)N(c,"resize",a),N(b,"destroy",function(){X(c,"resize",a)}),a(),b.invertGroups=a},plotGroup:function(a,b,c,d, -e){var f=this[a],g=!f;g&&(this[a]=f=this.chart.renderer.g(b).attr({visibility:c,zIndex:d||0.1}).add(e));f[g?"attr":"animate"](this.getPlotBox());return f},getPlotBox:function(){var a=this.chart,b=this.xAxis,c=this.yAxis;if(a.inverted)b=c,c=this.xAxis;return{translateX:b?b.left:a.plotLeft,translateY:c?c.top:a.plotTop,scaleX:1,scaleY:1}},render:function(){var a=this,b=a.chart,c,d=a.options,e=(c=d.animation)&&!!a.animate&&b.renderer.isSVG&&p(c.duration,500)||0,f=a.visible?"visible":"hidden",g=d.zIndex, -h=a.hasRendered,i=b.seriesGroup;c=a.plotGroup("group","series",f,g,i);a.markerGroup=a.plotGroup("markerGroup","markers",f,g,i);e&&a.animate(!0);a.getAttribs();c.inverted=a.isCartesian?b.inverted:!1;a.drawGraph&&(a.drawGraph(),a.clipNeg());a.drawDataLabels&&a.drawDataLabels();a.visible&&a.drawPoints();a.drawTracker&&a.options.enableMouseTracking!==!1&&a.drawTracker();b.inverted&&a.invertGroups();d.clip!==!1&&!a.sharedClipKey&&!h&&c.clip(b.clipRect);e&&a.animate();if(!h)e?a.animationTimeout=setTimeout(function(){a.afterAnimate()}, -e):a.afterAnimate();a.isDirty=a.isDirtyData=!1;a.hasRendered=!0},redraw:function(){var a=this.chart,b=this.isDirtyData,c=this.group,d=this.xAxis,e=this.yAxis;c&&(a.inverted&&c.attr({width:a.plotWidth,height:a.plotHeight}),c.animate({translateX:p(d&&d.left,a.plotLeft),translateY:p(e&&e.top,a.plotTop)}));this.translate();this.setTooltipPoints&&this.setTooltipPoints(!0);this.render();b&&K(this,"updatedData")}};Fb.prototype={destroy:function(){Oa(this,this.axis)},render:function(a){var b=this.options, -c=b.format,c=c?Ia(c,this):b.formatter.call(this);this.label?this.label.attr({text:c,visibility:"hidden"}):this.label=this.axis.chart.renderer.text(c,null,null,b.useHTML).css(b.style).attr({align:this.textAlign,rotation:b.rotation,visibility:"hidden"}).add(a)},setOffset:function(a,b){var c=this.axis,d=c.chart,e=d.inverted,f=this.isNegative,g=c.translate(c.usePercentage?100:this.total,0,0,0,1),c=c.translate(0),c=Q(g-c),h=d.xAxis[0].translate(this.x)+a,i=d.plotHeight,f={x:e?f?g:g-c:h,y:e?i-h-b:f?i-g- -c:i-g,width:e?c:b,height:e?b:c};if(e=this.label)e.align(this.alignOptions,null,f),f=e.alignAttr,e[this.options.crop===!1||d.isInsidePlot(f.x,f.y)?"show":"hide"](!0)}};ma.prototype.buildStacks=function(){var a=this.series,b=p(this.options.reversedStacks,!0),c=a.length;if(!this.isXAxis){for(this.usePercentage=!1;c--;)a[b?c:a.length-c-1].setStackedPoints();if(this.usePercentage)for(c=0;cg;)h--;this.updateParallelArrays(d,"splice",h,0,0);this.updateParallelArrays(d,h);if(j)j[g]=d.name;l.splice(h,0,a);m&&(this.data.splice(h,0,null),this.processData());e.legendType==="point"&&this.generatePoints();c&&(f[0]&&f[0].remove?f[0].remove(!1):(f.shift(),this.updateParallelArrays(d,"shift"),l.shift()));this.isDirtyData=this.isDirty= -!0;b&&(this.getAttribs(),i.redraw())},remove:function(a,b){var c=this,d=c.chart,a=p(a,!0);if(!c.isRemoving)c.isRemoving=!0,K(c,"remove",null,function(){c.destroy();d.isDirtyLegend=d.isDirtyBox=!0;d.linkSeries();a&&d.redraw(b)});c.isRemoving=!1},update:function(a,b){var c=this,d=this.chart,e=this.userOptions,f=this.type,g=J[f].prototype,h=["group","markerGroup","dataLabelsGroup"],i;q(h,function(a){h[a]=c[a];delete c[a]});a=w(e,{animation:!1,index:this.index,pointStart:this.xData[0]},{data:this.options.data}, -a);this.remove(!1);for(i in g)g.hasOwnProperty(i)&&(this[i]=t);r(this,J[a.type||f].prototype);q(h,function(a){c[a]=h[a]});this.init(d,a);d.linkSeries();p(b,!0)&&d.redraw(!1)}});r(ma.prototype,{update:function(a,b){var c=this.chart,a=c.options[this.coll][this.options.index]=w(this.userOptions,a);this.destroy(!0);this._addedPlotLB=t;this.init(c,r(a,{events:t}));c.isDirtyBox=!0;p(b,!0)&&c.redraw()},remove:function(a){for(var b=this.chart,c=this.coll,d=this.series,e=d.length;e--;)d[e]&&d[e].remove(!1); -ka(b.axes,this);ka(b[c],this);b.options[c].splice(this.options.index,1);q(b[c],function(a,b){a.options.index=b});this.destroy();b.isDirtyBox=!0;p(a,!0)&&b.redraw()},setTitle:function(a,b){this.update({title:a},b)},setCategories:function(a,b){this.update({categories:a},b)}});ha=la(O);J.line=ha;ca.area=w(T,{threshold:0});var qa=la(O,{type:"area",getSegments:function(){var a=this,b=[],c=[],d=[],e=this.xAxis,f=this.yAxis,g=f.stacks[this.stackKey],h={},i,j,k=this.points,l=this.options.connectNulls,m,n; -if(this.options.stacking&&!this.cropped){for(m=0;m=0;d--)g=p(a[d].yBottom,f),da&&i>e?(i=u(a,e),k=2*e-i):ig&&k>e?(k=u(g,e),i=2*e-k):k0.5*a.xAxis.len?0:1),e=a.yAxis,f=a.translatedThreshold=e.getThreshold(c.threshold),g=p(c.minPointLength,5),h=a.getColumnMetrics(),i=h.width,j=a.barW=u(i,1+2*d),k=a.pointXOffset=h.offset,l=-(d%2?0.5:0),m=d%2?0.5:1;b.renderer.isVML&&b.inverted&&(m+=1);c.pointPadding&&(j=Ka(j));O.prototype.translate.apply(a);q(a.points,function(c){var d=p(c.yBottom,f),h=C(u(-999-d,c.plotY),e.len+999+d),q=c.plotX+k,r=j,s=C(h,d),t;t=u(h,d)-s;Q(t)g?d-g:f-(e.translate(c.y, -0,1,0,1)<=f?g:0)));c.barX=q;c.pointWidth=i;c.tooltipPos=b.inverted?[e.len-h,a.xAxis.len-q-r/2]:[q+r/2,h];r=v(q+r)+l;q=v(q)+l;r-=q;d=Q(s)<0.5;t=v(s+t)+m;s=v(s)+m;t-=s;d&&(s-=1,t+=1);c.shapeType="rect";c.shapeArgs={x:q,y:s,width:r,height:t}})},getSymbol:sa,drawLegendSymbol:M.drawRectangle,drawGraph:sa,drawPoints:function(){var a=this,b=this.chart,c=a.options,d=b.renderer,e=c.animationLimit||250,f,g;q(a.points,function(h){var i=h.plotY,j=h.graphic;if(i!==t&&!isNaN(i)&&h.y!==null)f=h.shapeArgs,i=s(a.borderWidth)? -{"stroke-width":a.borderWidth}:{},g=h.pointAttr[h.selected?"select":""]||a.pointAttr[""],j?(ab(j),j.attr(i)[b.pointCount {series.name}
',pointFormat:"x: {point.x}
y: {point.y}
"}, -stickyTracking:!1});qa=la(O,{type:"scatter",sorted:!1,requireSorting:!1,noSharedTooltip:!0,trackerGroups:["markerGroup","dataLabelsGroup"],takeOrdinalPosition:!1,singularTooltips:!0,drawGraph:function(){this.options.lineWidth&&O.prototype.drawGraph.call(this)}});J.scatter=qa;ca.pie=w(T,{borderColor:"#FFFFFF",borderWidth:1,center:[null,null],clip:!1,colorByPoint:!0,dataLabels:{distance:30,enabled:!0,formatter:function(){return this.point.name}},ignoreHiddenPoint:!0,legendType:"point",marker:null,size:null, -showInLegend:!1,slicedOffset:10,states:{hover:{brightness:0.1,shadow:!1}},stickyTracking:!1,tooltip:{followPointer:!0}});T={type:"pie",isCartesian:!1,pointClass:la(Ea,{init:function(){Ea.prototype.init.apply(this,arguments);var a=this,b;if(a.y<0)a.y=null;r(a,{visible:a.visible!==!1,name:p(a.name,"Slice")});b=function(b){a.slice(b.type==="select")};N(a,"select",b);N(a,"unselect",b);return a},setVisible:function(a){var b=this,c=b.series,d=c.chart;b.visible=b.options.visible=a=a===t?!b.visible:a;c.options.data[Da(b, -c.data)]=b.options;q(["graphic","dataLabel","connector","shadowGroup"],function(c){if(b[c])b[c][a?"show":"hide"](!0)});b.legendItem&&d.legend.colorizeItem(b,a);if(!c.isDirty&&c.options.ignoreHiddenPoint)c.isDirty=!0,d.redraw()},slice:function(a,b,c){var d=this.series;Qa(c,d.chart);p(b,!0);this.sliced=this.options.sliced=a=s(a)?a:!this.sliced;d.options.data[Da(this,d.data)]=this.options;a=a?this.slicedTranslation:{translateX:0,translateY:0};this.graphic.animate(a);this.shadowGroup&&this.shadowGroup.animate(a)}, -haloPath:function(a){var b=this.shapeArgs,c=this.series.chart;return this.sliced||!this.visible?[]:this.series.chart.renderer.symbols.arc(c.plotLeft+b.x,c.plotTop+b.y,b.r+a,b.r+a,{innerR:this.shapeArgs.r,start:b.start,end:b.end})}}),requireSorting:!1,noSharedTooltip:!0,trackerGroups:["group","dataLabelsGroup"],axisTypes:[],pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color"},singularTooltips:!0,getColor:sa,animate:function(a){var b=this,c=b.points,d=b.startAngleRad; -if(!a)q(c,function(a){var c=a.graphic,a=a.shapeArgs;c&&(c.attr({r:b.center[3]/2,start:d,end:d}),c.animate({r:a.r,start:a.start,end:a.end},b.options.animation))}),b.animate=null},setData:function(a,b,c,d){O.prototype.setData.call(this,a,!1,c,d);this.processData();this.generatePoints();p(b,!0)&&this.chart.redraw(c)},generatePoints:function(){var a,b=0,c,d,e,f=this.options.ignoreHiddenPoint;O.prototype.generatePoints.call(this);c=this.points;d=c.length;for(a=0;a0?e.y/b*100:0,e.total=b},translate:function(a){this.generatePoints();var b=0,c=this.options,d=c.slicedOffset,e=d+c.borderWidth,f,g,h,i=c.startAngle||0,j=this.startAngleRad=na/180*(i-90),i=(this.endAngleRad=na/180*(p(c.endAngle,i+360)-90))-j,k=this.points,l=c.dataLabels.distance,c=c.ignoreHiddenPoint,m,n=k.length,o;if(!a)this.center=a=this.getCenter();this.getX=function(b,c){h=V.asin(C((b-a[1])/(a[2]/2+l),1));return a[0]+(c?-1:1)*aa(h)*(a[2]/2+l)};for(m=0;m< -n;m++){o=k[m];f=j+b*i;if(!c||o.visible)b+=o.percentage/100;g=j+b*i;o.shapeType="arc";o.shapeArgs={x:a[0],y:a[1],r:a[2]/2,innerR:a[3]/2,start:v(f*1E3)/1E3,end:v(g*1E3)/1E3};h=(g+f)/2;h>1.5*na?h-=2*na:h<-na/2&&(h+=2*na);o.slicedTranslation={translateX:v(aa(h)*d),translateY:v(fa(h)*d)};f=aa(h)*a[2]/2;g=fa(h)*a[2]/2;o.tooltipPos=[a[0]+f*0.7,a[1]+g*0.7];o.half=h<-na/2||h>na/2?1:0;o.angle=h;e=C(e,l/2);o.labelPos=[a[0]+f+aa(h)*l,a[1]+g+fa(h)*l,a[0]+f+aa(h)*e,a[1]+g+fa(h)*e,a[0]+f,a[1]+g,l<0?"center":o.half? -"right":"left",h]}},drawGraph:null,drawPoints:function(){var a=this,b=a.chart.renderer,c,d,e=a.options.shadow,f,g;if(e&&!a.shadowGroup)a.shadowGroup=b.g("shadow").add(a.group);q(a.points,function(h){d=h.graphic;g=h.shapeArgs;f=h.shadowGroup;if(e&&!f)f=h.shadowGroup=b.g("shadow").add(a.shadowGroup);c=h.sliced?h.slicedTranslation:{translateX:0,translateY:0};f&&f.attr(c);d?d.animate(r(g,c)):h.graphic=d=b[h.shapeType](g).setRadialReference(a.center).attr(h.pointAttr[h.selected?"select":""]).attr({"stroke-linejoin":"round"}).attr(c).add(a.group).shadow(e, -f);h.visible!==void 0&&h.setVisible(h.visible)})},sortByAngle:function(a,b){a.sort(function(a,d){return a.angle!==void 0&&(d.angle-a.angle)*b})},drawLegendSymbol:M.drawRectangle,getCenter:Z.getCenter,getSymbol:sa};T=la(O,T);J.pie=T;O.prototype.drawDataLabels=function(){var a=this,b=a.options,c=b.cursor,d=b.dataLabels,e=a.points,f,g,h,i;if(d.enabled||a._hasPointLabels)a.dlProcessOptions&&a.dlProcessOptions(d),i=a.plotGroup("dataLabelsGroup","data-labels",d.defer?"hidden":"visible",d.zIndex||6),!a.hasRendered&& -p(d.defer,!0)&&(i.attr({opacity:0}),N(a,"afterAnimate",function(){a.visible&&i.show();i[b.animation?"animate":"attr"]({opacity:1},{duration:200})})),g=d,q(e,function(b){var e,l=b.dataLabel,m,n,o=b.connector,q=!0;f=b.options&&b.options.dataLabels;e=p(f&&f.enabled,g.enabled);if(l&&!e)b.dataLabel=l.destroy();else if(e){d=w(g,f);e=d.rotation;m=b.getLabelConfig();h=d.format?Ia(d.format,m):d.formatter.call(m,d);d.style.color=p(d.color,d.style.color,a.color,"black");if(l)if(s(h))l.attr({text:h}),q=!1;else{if(b.dataLabel= -l=l.destroy(),o)b.connector=o.destroy()}else if(s(h)){l={fill:d.backgroundColor,stroke:d.borderColor,"stroke-width":d.borderWidth,r:d.borderRadius||0,rotation:e,padding:d.padding,zIndex:1};for(n in l)l[n]===t&&delete l[n];l=b.dataLabel=a.chart.renderer[e?"text":"label"](h,0,-999,null,null,null,d.useHTML).attr(l).css(r(d.style,c&&{cursor:c})).add(i).shadow(d.shadow)}l&&a.alignDataLabel(b,l,d,null,q)}})};O.prototype.alignDataLabel=function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=p(a.plotX,-999), -i=p(a.plotY,-999),j=b.getBBox();if(a=this.visible&&(a.series.forceDL||f.isInsidePlot(h,v(i),g)||d&&f.isInsidePlot(h,g?d.x+1:d.y+d.height-1,g)))d=r({x:g?f.plotWidth-i:h,y:v(g?f.plotHeight-h:i),width:0,height:0},d),r(c,{width:j.width,height:j.height}),c.rotation?b[e?"attr":"animate"]({x:d.x+c.x+d.width/2,y:d.y+c.y+d.height/2}).attr({align:c.align}):(b.align(c,null,d),g=b.alignAttr,p(c.overflow,"justify")==="justify"?this.justifyDataLabel(b,c,g,j,d,e):p(c.crop,!0)&&(a=f.isInsidePlot(g.x,g.y)&&f.isInsidePlot(g.x+ -j.width,g.y+j.height)));if(!a)b.attr({y:-999}),b.placed=!1};O.prototype.justifyDataLabel=function(a,b,c,d,e,f){var g=this.chart,h=b.align,i=b.verticalAlign,j,k;j=c.x;if(j<0)h==="right"?b.align="left":b.x=-j,k=!0;j=c.x+d.width;if(j>g.plotWidth)h==="left"?b.align="right":b.x=g.plotWidth-j,k=!0;j=c.y;if(j<0)i==="bottom"?b.verticalAlign="top":b.y=-j,k=!0;j=c.y+d.height;if(j>g.plotHeight)i==="top"?b.verticalAlign="bottom":b.y=g.plotHeight-j,k=!0;if(k)a.placed=!f,a.align(b,null,e)};if(J.pie)J.pie.prototype.drawDataLabels= -function(){var a=this,b=a.data,c,d=a.chart,e=a.options.dataLabels,f=p(e.connectorPadding,10),g=p(e.connectorWidth,1),h=d.plotWidth,i=d.plotHeight,j,k,l=p(e.softConnector,!0),m=e.distance,n=a.center,o=n[2]/2,r=n[1],s=m>0,t,w,x,z=[[],[]],B,A,K,J,y,R=[0,0,0,0],N=function(a,b){return b.y-a.y};if(a.visible&&(e.enabled||a._hasPointLabels)){O.prototype.drawDataLabels.apply(a);q(b,function(a){a.dataLabel&&a.visible&&z[a.half].push(a)});for(J=2;J--;){var H=[],M=[],F=z[J],L=F.length,G;if(L){a.sortByAngle(F, -J-0.5);for(y=b=0;!b&&F[y];)b=F[y]&&F[y].dataLabel&&(F[y].dataLabel.getBBox().height||21),y++;if(m>0){w=C(r+o+m,d.plotHeight);for(y=u(0,r-o-m);y<=w;y+=b)H.push(y);w=H.length;if(L>w){c=[].concat(F);c.sort(N);for(y=L;y--;)c[y].rank=y;for(y=L;y--;)F[y].rank>=w&&F.splice(y,1);L=F.length}for(y=0;y0){if(w=M.pop(),G=w.i,A=w.y,c>A&&H[G+1]!==null||ch-f&&(R[1]=u(v(B+w-h+f),R[1])),A-b/2<0?R[0]=u(v(-A+b/2),R[0]): -A+b/2>i&&(R[2]=u(v(A+b/2-i),R[2]))}}}if(Ba(R)===0||this.verifyDataLabelOverflow(R))this.placeDataLabels(),s&&g&&q(this.points,function(b){j=b.connector;x=b.labelPos;if((t=b.dataLabel)&&t._pos)K=t._attr.visibility,B=t.connX,A=t.connY,k=l?["M",B+(x[6]==="left"?5:-5),A,"C",B,A,2*x[2]-x[4],2*x[3]-x[5],x[2],x[3],"L",x[4],x[5]]:["M",B+(x[6]==="left"?5:-5),A,"L",x[2],x[3],"L",x[4],x[5]],j?(j.animate({d:k}),j.attr("visibility",K)):b.connector=j=a.chart.renderer.path(k).attr({"stroke-width":g,stroke:e.connectorColor|| -b.color||"#606060",visibility:K}).add(a.dataLabelsGroup);else if(j)b.connector=j.destroy()})}},J.pie.prototype.placeDataLabels=function(){q(this.points,function(a){var a=a.dataLabel,b;if(a)(b=a._pos)?(a.attr(a._attr),a[a.moved?"animate":"attr"](b),a.moved=!0):a&&a.attr({y:-999})})},J.pie.prototype.alignDataLabel=sa,J.pie.prototype.verifyDataLabelOverflow=function(a){var b=this.center,c=this.options,d=c.center,e=c=c.minSize||80,f;d[0]!==null?e=u(b[2]-u(a[1],a[3]),c):(e=u(b[2]-a[1]-a[3],c),b[0]+=(a[3]- -a[1])/2);d[1]!==null?e=u(C(e,b[2]-u(a[0],a[2])),c):(e=u(C(e,b[2]-a[0]-a[2]),c),b[1]+=(a[0]-a[2])/2);ep(this.translatedThreshold,f.plotSizeY),j=p(c.inside,!!this.options.stacking);if(h&&(d=w(h),g&&(d={x:f.plotWidth- -d.y-d.height,y:f.plotHeight-d.x-d.width,width:d.height,height:d.width}),!j))g?(d.x+=i?0:d.width,d.width=0):(d.y+=i?d.height:0,d.height=0);c.align=p(c.align,!g||j?"center":i?"right":"left");c.verticalAlign=p(c.verticalAlign,g||j?"middle":i?"top":"bottom");O.prototype.alignDataLabel.call(this,a,b,c,d,e)};T=S.TrackerMixin={drawTrackerPoint:function(){var a=this,b=a.chart,c=b.pointer,d=a.options.cursor,e=d&&{cursor:d},f=function(c){var d=c.target,e;if(b.hoverSeries!==a)a.onMouseOver();for(;d&&!e;)e=d.point, -d=d.parentNode;if(e!==t&&e!==b.hoverPoint)e.onMouseOver(c)};q(a.points,function(a){if(a.graphic)a.graphic.element.point=a;if(a.dataLabel)a.dataLabel.element.point=a});if(!a._hasTracking)q(a.trackerGroups,function(b){if(a[b]&&(a[b].addClass("highcharts-tracker").on("mouseover",f).on("mouseout",function(a){c.onTrackerMouseOut(a)}).css(e),Za))a[b].on("touchstart",f)}),a._hasTracking=!0},drawTrackerGraph:function(){var a=this,b=a.options,c=b.trackByArea,d=[].concat(c?a.areaPath:a.graphPath),e=d.length, -f=a.chart,g=f.pointer,h=f.renderer,i=f.options.tooltip.snap,j=a.tracker,k=b.cursor,l=k&&{cursor:k},k=a.singlePoints,m,n=function(){if(f.hoverSeries!==a)a.onMouseOver()},o="rgba(192,192,192,"+(ba?1.0E-4:0.002)+")";if(e&&!c)for(m=e+1;m--;)d[m]==="M"&&d.splice(m+1,0,d[m+1]-i,d[m+2],"L"),(m&&d[m]==="M"||m===e)&&d.splice(m,0,"L",d[m-2]+i,d[m-1]);for(m=0;mC(k.dataMin,k.min)&&i=f.min&&c<=f.max){h=b[i+1];c=d===t?0:d+1;for(d=b[i+1]?C(u(0, -U((e.clientX+(h?h.wrappedClientX||h.clientX:g))/2)),g):g;c>=0&&c<=d;)j[c++]=e}this.tooltipPoints=j}},show:function(){this.setVisible(!0)},hide:function(){this.setVisible(!1)},select:function(a){this.selected=a=a===t?!this.selected:a;if(this.checkbox)this.checkbox.checked=a;K(this,a?"select":"unselect")},drawTracker:T.drawTrackerGraph});r(S,{Axis:ma,Chart:Xa,Color:ya,Point:Ea,Tick:Sa,Renderer:Ya,Series:O,SVGElement:G,SVGRenderer:ta,arrayMin:Na,arrayMax:Ba,charts:W,dateFormat:bb,format:Ia,pathAnim:ub, -getOptions:function(){return L},hasBidiBug:Nb,isTouchDevice:Hb,numberFormat:Ga,seriesTypes:J,setOptions:function(a){L=w(!0,L,a);Ab();return L},addEvent:N,removeEvent:X,createElement:$,discardElement:Pa,css:A,each:q,extend:r,map:Ua,merge:w,pick:p,splat:ra,extendClass:la,pInt:z,wrap:Ma,svg:ba,canvas:ga,vml:!ba&&!ga,product:"Highcharts",version:"4.0.3"})})(); diff --git a/public/assets/stylesheets/highslide/highslide.css b/public/assets/stylesheets/highslide/highslide.css deleted file mode 100755 index c882a43145..0000000000 --- a/public/assets/stylesheets/highslide/highslide.css +++ /dev/null @@ -1,889 +0,0 @@ -/** -* @file: highslide.css -* @version: 4.1.13 -*/ -.highslide-container div { - font-family: Verdana, Helvetica; - font-size: 10pt; -} -.highslide-container table { - background: none; -} -.highslide { - outline: none; - text-decoration: none; -} -.highslide img { - border: 2px solid silver; -} -.highslide:hover img { - border-color: gray; -} -.highslide-active-anchor img { - visibility: hidden; -} -.highslide-gallery .highslide-active-anchor img { - border-color: black; - visibility: visible; - cursor: default; -} -.highslide-image { - border-width: 2px; - border-style: solid; - border-color: white; -} -.highslide-wrapper, .highslide-outline { - background: white; -} -.glossy-dark { - background: #111; -} - -.highslide-image-blur { -} -.highslide-number { - font-weight: bold; - color: gray; - font-size: .9em; -} -.highslide-caption { - display: none; - font-size: 1em; - padding: 5px; - /*background: white;*/ -} -.highslide-heading { - display: none; - font-weight: bold; - margin: 0.4em; -} -.highslide-dimming { - /*position: absolute;*/ - background: black; -} -a.highslide-full-expand { - background: url(highslide/fullexpand.gif) no-repeat; - display: block; - margin: 0 10px 10px 0; - width: 34px; - height: 34px; -} -.highslide-loading { - display: block; - color: black; - font-size: 9px; - font-weight: bold; - text-transform: uppercase; - text-decoration: none; - padding: 3px; - border: 1px solid white; - background-color: white; - padding-left: 22px; - background-image: url(highslide/loader.white.gif); - background-repeat: no-repeat; - background-position: 3px 1px; -} -a.highslide-credits, -a.highslide-credits i { - padding: 2px; - color: silver; - text-decoration: none; - font-size: 10px; -} -a.highslide-credits:hover, -a.highslide-credits:hover i { - color: white; - background-color: gray; -} -.highslide-move, .highslide-move * { - cursor: move; -} - -.highslide-viewport { - display: none; - position: fixed; - width: 100%; - height: 100%; - z-index: 1; - background: none; - left: 0; - top: 0; -} -.highslide-overlay { - display: none; -} -.hidden-container { - display: none; -} -/* Example of a semitransparent, offset closebutton */ -.closebutton { - position: relative; - top: -15px; - left: 15px; - width: 30px; - height: 30px; - cursor: pointer; - background: url(highslide/close.png); - /* NOTE! For IE6, you also need to update the highslide-ie6.css file. */ -} - -/*****************************************************************************/ -/* Thumbnail boxes for the galleries. */ -/* Remove these if you are not using a gallery. */ -/*****************************************************************************/ -.highslide-gallery ul { - list-style-type: none; - margin: 0; - padding: 0; -} -.highslide-gallery ul li { - display: block; - position: relative; - float: left; - width: 106px; - height: 106px; - border: 1px solid silver; - background: #ededed; - margin: 2px; - padding: 0; - line-height: 0; - overflow: hidden; -} -.highslide-gallery ul a { - position: absolute; - top: 50%; - left: 50%; -} -.highslide-gallery ul img { - position: relative; - top: -50%; - left: -50%; -} -html>/**/body .highslide-gallery ul li { - display: table; - text-align: center; -} -html>/**/body .highslide-gallery ul li { - text-align: center; -} -html>/**/body .highslide-gallery ul a { - position: static; - display: table-cell; - vertical-align: middle; -} -html>/**/body .highslide-gallery ul img { - position: static; -} - -/*****************************************************************************/ -/* Controls for the galleries. */ -/* Remove these if you are not using a gallery */ -/*****************************************************************************/ -.highslide-controls { - width: 195px; - height: 40px; - background: url(highslide/controlbar-white.gif) 0 -90px no-repeat; - margin: 20px 15px 10px 0; -} -.highslide-controls ul { - position: relative; - left: 15px; - height: 40px; - list-style: none; - margin: 0; - padding: 0; - background: url(highslide/controlbar-white.gif) right -90px no-repeat; - -} -.highslide-controls li { - float: left; - padding: 5px 0; - margin:0; - list-style: none; -} -.highslide-controls a { - background-image: url(highslide/controlbar-white.gif); - display: block; - float: left; - height: 30px; - width: 30px; - outline: none; -} -.highslide-controls a.disabled { - cursor: default; -} -.highslide-controls a.disabled span { - cursor: default; -} -.highslide-controls a span { - /* hide the text for these graphic buttons */ - display: none; - cursor: pointer; -} - - -/* The CSS sprites for the controlbar - see http://www.google.com/search?q=css+sprites */ -.highslide-controls .highslide-previous a { - background-position: 0 0; -} -.highslide-controls .highslide-previous a:hover { - background-position: 0 -30px; -} -.highslide-controls .highslide-previous a.disabled { - background-position: 0 -60px !important; -} -.highslide-controls .highslide-play a { - background-position: -30px 0; -} -.highslide-controls .highslide-play a:hover { - background-position: -30px -30px; -} -.highslide-controls .highslide-play a.disabled { - background-position: -30px -60px !important; -} -.highslide-controls .highslide-pause a { - background-position: -60px 0; -} -.highslide-controls .highslide-pause a:hover { - background-position: -60px -30px; -} -.highslide-controls .highslide-next a { - background-position: -90px 0; -} -.highslide-controls .highslide-next a:hover { - background-position: -90px -30px; -} -.highslide-controls .highslide-next a.disabled { - background-position: -90px -60px !important; -} -.highslide-controls .highslide-move a { - background-position: -120px 0; -} -.highslide-controls .highslide-move a:hover { - background-position: -120px -30px; -} -.highslide-controls .highslide-full-expand a { - background-position: -150px 0; -} -.highslide-controls .highslide-full-expand a:hover { - background-position: -150px -30px; -} -.highslide-controls .highslide-full-expand a.disabled { - background-position: -150px -60px !important; -} -.highslide-controls .highslide-close a { - background-position: -180px 0; -} -.highslide-controls .highslide-close a:hover { - background-position: -180px -30px; -} - -/*****************************************************************************/ -/* Styles for the HTML popups */ -/* Remove these if you are not using Highslide HTML */ -/*****************************************************************************/ -.highslide-maincontent { - display: none; -} -.highslide-html { - background-color: white; -} -.mobile .highslide-html { - border: 1px solid silver; -} -.highslide-html-content { - display: none; - width: 400px; - padding: 0 5px 5px 5px; -} -.highslide-header { - padding-bottom: 5px; -} -.highslide-header ul { - margin: 0; - padding: 0; - text-align: right; -} -.highslide-header ul li { - display: inline; - padding-left: 1em; -} -.highslide-header ul li.highslide-previous, .highslide-header ul li.highslide-next { - display: none; -} -.highslide-header a { - font-weight: bold; - color: gray; - text-transform: uppercase; - text-decoration: none; -} -.highslide-header a:hover { - color: black; -} -.highslide-header .highslide-move a { - cursor: move; -} -.highslide-footer { - height: 16px; -} -.highslide-footer .highslide-resize { - display: block; - float: right; - margin-top: 5px; - height: 11px; - width: 11px; - background: url(highslide/resize.gif) no-repeat; -} -.highslide-footer .highslide-resize span { - display: none; -} -.highslide-body { -} -.highslide-resize { - cursor: nw-resize; -} - -/*****************************************************************************/ -/* Styles for the Individual wrapper class names. */ -/* See www.highslide.com/ref/hs.wrapperClassName */ -/* You can safely remove the class name themes you don't use */ -/*****************************************************************************/ - -/* hs.wrapperClassName = 'draggable-header' */ -.draggable-header .highslide-header { - height: 18px; - border-bottom: 1px solid #dddddd; -} -.draggable-header .highslide-heading { - position: absolute; - margin: 2px 0.4em; -} - -.draggable-header .highslide-header .highslide-move { - cursor: move; - display: block; - height: 16px; - position: absolute; - right: 24px; - top: 0; - width: 100%; - z-index: 1; -} -.draggable-header .highslide-header .highslide-move * { - display: none; -} -.draggable-header .highslide-header .highslide-close { - position: absolute; - right: 2px; - top: 2px; - z-index: 5; - padding: 0; -} -.draggable-header .highslide-header .highslide-close a { - display: block; - height: 16px; - width: 16px; - background-image: url(highslide/closeX.png); -} -.draggable-header .highslide-header .highslide-close a:hover { - background-position: 0 16px; -} -.draggable-header .highslide-header .highslide-close span { - display: none; -} -.draggable-header .highslide-maincontent { - padding-top: 1em; -} - -/* hs.wrapperClassName = 'titlebar' */ -.titlebar .highslide-header { - height: 18px; - border-bottom: 1px solid #dddddd; -} -.titlebar .highslide-heading { - position: absolute; - width: 90%; - margin: 1px 0 1px 5px; - color: #666666; -} - -.titlebar .highslide-header .highslide-move { - cursor: move; - display: block; - height: 16px; - position: absolute; - right: 24px; - top: 0; - width: 100%; - z-index: 1; -} -.titlebar .highslide-header .highslide-move * { - display: none; -} -.titlebar .highslide-header li { - position: relative; - top: 3px; - z-index: 2; - padding: 0 0 0 1em; -} -.titlebar .highslide-maincontent { - padding-top: 1em; -} - -/* hs.wrapperClassName = 'no-footer' */ -.no-footer .highslide-footer { - display: none; -} - -/* hs.wrapperClassName = 'wide-border' */ -.wide-border { - background: white; -} -.wide-border .highslide-image { - border-width: 10px; -} -.wide-border .highslide-caption { - padding: 0 10px 10px 10px; -} - -/* hs.wrapperClassName = 'borderless' */ -.borderless .highslide-image { - border: none; -} -.borderless .highslide-caption { - border-bottom: 1px solid white; - border-top: 1px solid white; - background: silver; -} - -/* hs.wrapperClassName = 'outer-glow' */ -.outer-glow { - background: #444; -} -.outer-glow .highslide-image { - border: 5px solid #444444; -} -.outer-glow .highslide-caption { - border: 5px solid #444444; - border-top: none; - padding: 5px; - background-color: gray; -} - -/* hs.wrapperClassName = 'colored-border' */ -.colored-border { - background: white; -} -.colored-border .highslide-image { - border: 2px solid green; -} -.colored-border .highslide-caption { - border: 2px solid green; - border-top: none; -} - -/* hs.wrapperClassName = 'dark' */ -.dark { - background: #111; -} -.dark .highslide-image { - border-color: black black #202020 black; - background: gray; -} -.dark .highslide-caption { - color: white; - background: #111; -} -.dark .highslide-controls, -.dark .highslide-controls ul, -.dark .highslide-controls a { - background-image: url(highslide/controlbar-black-border.gif); -} - -/* hs.wrapperClassName = 'floating-caption' */ -.floating-caption .highslide-caption { - position: absolute; - padding: 1em 0 0 0; - background: none; - color: white; - border: none; - font-weight: bold; -} - -/* hs.wrapperClassName = 'controls-in-heading' */ -.controls-in-heading .highslide-heading { - color: gray; - font-weight: bold; - height: 20px; - overflow: hidden; - cursor: default; - padding: 0 0 0 22px; - margin: 0; - background: url(highslide/icon.gif) no-repeat 0 1px; -} -.controls-in-heading .highslide-controls { - width: 105px; - height: 20px; - position: relative; - margin: 0; - top: -23px; - left: 7px; - background: none; -} -.controls-in-heading .highslide-controls ul { - position: static; - height: 20px; - background: none; -} -.controls-in-heading .highslide-controls li { - padding: 0; -} -.controls-in-heading .highslide-controls a { - background-image: url(highslide/controlbar-white-small.gif); - height: 20px; - width: 20px; -} - -.controls-in-heading .highslide-controls .highslide-move { - display: none; -} - -.controls-in-heading .highslide-controls .highslide-previous a { - background-position: 0 0; -} -.controls-in-heading .highslide-controls .highslide-previous a:hover { - background-position: 0 -20px; -} -.controls-in-heading .highslide-controls .highslide-previous a.disabled { - background-position: 0 -40px !important; -} -.controls-in-heading .highslide-controls .highslide-play a { - background-position: -20px 0; -} -.controls-in-heading .highslide-controls .highslide-play a:hover { - background-position: -20px -20px; -} -.controls-in-heading .highslide-controls .highslide-play a.disabled { - background-position: -20px -40px !important; -} -.controls-in-heading .highslide-controls .highslide-pause a { - background-position: -40px 0; -} -.controls-in-heading .highslide-controls .highslide-pause a:hover { - background-position: -40px -20px; -} -.controls-in-heading .highslide-controls .highslide-next a { - background-position: -60px 0; -} -.controls-in-heading .highslide-controls .highslide-next a:hover { - background-position: -60px -20px; -} -.controls-in-heading .highslide-controls .highslide-next a.disabled { - background-position: -60px -40px !important; -} -.controls-in-heading .highslide-controls .highslide-full-expand a { - background-position: -100px 0; -} -.controls-in-heading .highslide-controls .highslide-full-expand a:hover { - background-position: -100px -20px; -} -.controls-in-heading .highslide-controls .highslide-full-expand a.disabled { - background-position: -100px -40px !important; -} -.controls-in-heading .highslide-controls .highslide-close a { - background-position: -120px 0; -} -.controls-in-heading .highslide-controls .highslide-close a:hover { - background-position: -120px -20px; -} - -/*****************************************************************************/ -/* Styles for text based controls. */ -/* You can safely remove this if you don't use text based controls */ -/*****************************************************************************/ - -.text-controls .highslide-controls { - width: auto; - height: auto; - margin: 0; - text-align: center; - background: none; -} -.text-controls ul { - position: static; - background: none; - height: auto; - left: 0; -} -.text-controls .highslide-move { - display: none; -} -.text-controls li { - background-image: url(highslide/controlbar-text-buttons.png); - background-position: right top !important; - padding: 0; - margin-left: 15px; - display: block; - width: auto; -} -.text-controls a { - background: url(highslide/controlbar-text-buttons.png) no-repeat; - background-position: left top !important; - position: relative; - left: -10px; - display: block; - width: auto; - height: auto; - text-decoration: none !important; -} -.text-controls a span { - background: url(highslide/controlbar-text-buttons.png) no-repeat; - margin: 1px 2px 1px 10px; - display: block; - min-width: 4em; - height: 18px; - line-height: 18px; - padding: 1px 0 1px 18px; - color: #333; - font-family: "Trebuchet MS", Arial, sans-serif; - font-size: 12px; - font-weight: bold; - white-space: nowrap; -} -.text-controls .highslide-next { - margin-right: 1em; -} -.text-controls .highslide-full-expand a span { - min-width: 0; - margin: 1px 0; - padding: 1px 0 1px 10px; -} -.text-controls .highslide-close a span { - min-width: 0; -} -.text-controls a:hover span { - color: black; -} -.text-controls a.disabled span { - color: #999; -} - -.text-controls .highslide-previous span { - background-position: 0 -40px; -} -.text-controls .highslide-previous a.disabled { - background-position: left top !important; -} -.text-controls .highslide-previous a.disabled span { - background-position: 0 -140px; -} -.text-controls .highslide-play span { - background-position: 0 -60px; -} -.text-controls .highslide-play a.disabled { - background-position: left top !important; -} -.text-controls .highslide-play a.disabled span { - background-position: 0 -160px; -} -.text-controls .highslide-pause span { - background-position: 0 -80px; -} -.text-controls .highslide-next span { - background-position: 0 -100px; -} -.text-controls .highslide-next a.disabled { - background-position: left top !important; -} -.text-controls .highslide-next a.disabled span { - background-position: 0 -200px; -} -.text-controls .highslide-full-expand span { - background: none; -} -.text-controls .highslide-full-expand a.disabled { - background-position: left top !important; -} -.text-controls .highslide-close span { - background-position: 0 -120px; -} - - -/*****************************************************************************/ -/* Styles for the thumbstrip. */ -/* See www.highslide.com/ref/hs.addSlideshow */ -/* You can safely remove this if you don't use a thumbstrip */ -/*****************************************************************************/ - -.highslide-thumbstrip { - height: 100%; - direction: ltr; -} -.highslide-thumbstrip div { - overflow: hidden; -} -.highslide-thumbstrip table { - position: relative; - padding: 0; - border-collapse: collapse; -} -.highslide-thumbstrip td { - padding: 1px; - /*text-align: center;*/ -} -.highslide-thumbstrip a { - outline: none; -} -.highslide-thumbstrip img { - display: block; - border: 1px solid gray; - margin: 0 auto; -} -.highslide-thumbstrip .highslide-active-anchor img { - visibility: visible; -} -.highslide-thumbstrip .highslide-marker { - position: absolute; - width: 0; - height: 0; - border-width: 0; - border-style: solid; - border-color: transparent; /* change this to actual background color in highslide-ie6.css */ -} -.highslide-thumbstrip-horizontal div { - width: auto; - /* width: 100% breaks in small strips in IE */ -} -.highslide-thumbstrip-horizontal .highslide-scroll-up { - display: none; - position: absolute; - top: 3px; - left: 3px; - width: 25px; - height: 42px; -} -.highslide-thumbstrip-horizontal .highslide-scroll-up div { - margin-bottom: 10px; - cursor: pointer; - background: url(highslide/scrollarrows.png) left center no-repeat; - height: 42px; -} -.highslide-thumbstrip-horizontal .highslide-scroll-down { - display: none; - position: absolute; - top: 3px; - right: 3px; - width: 25px; - height: 42px; -} -.highslide-thumbstrip-horizontal .highslide-scroll-down div { - margin-bottom: 10px; - cursor: pointer; - background: url(highslide/scrollarrows.png) center right no-repeat; - height: 42px; -} -.highslide-thumbstrip-horizontal table { - margin: 2px 0 10px 0; -} -.highslide-viewport .highslide-thumbstrip-horizontal table { - margin-left: 10px; -} -.highslide-thumbstrip-horizontal img { - width: auto; - height: 40px; -} -.highslide-thumbstrip-horizontal .highslide-marker { - top: 47px; - border-left-width: 6px; - border-right-width: 6px; - border-bottom: 6px solid gray; -} -.highslide-viewport .highslide-thumbstrip-horizontal .highslide-marker { - margin-left: 10px; -} -.dark .highslide-thumbstrip-horizontal .highslide-marker, .highslide-viewport .highslide-thumbstrip-horizontal .highslide-marker { - border-bottom-color: white !important; -} - -.highslide-thumbstrip-vertical-overlay { - overflow: hidden !important; -} -.highslide-thumbstrip-vertical div { - height: 100%; -} -.highslide-thumbstrip-vertical a { - display: block; -} -.highslide-thumbstrip-vertical .highslide-scroll-up { - display: none; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 25px; -} -.highslide-thumbstrip-vertical .highslide-scroll-up div { - margin-left: 10px; - cursor: pointer; - background: url(highslide/scrollarrows.png) top center no-repeat; - height: 25px; -} -.highslide-thumbstrip-vertical .highslide-scroll-down { - display: none; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 25px; -} -.highslide-thumbstrip-vertical .highslide-scroll-down div { - margin-left: 10px; - cursor: pointer; - background: url(highslide/scrollarrows.png) bottom center no-repeat; - height: 25px; -} -.highslide-thumbstrip-vertical table { - margin: 10px 0 0 10px; -} -.highslide-thumbstrip-vertical img { - width: 60px; /* t=5481 */ -} -.highslide-thumbstrip-vertical .highslide-marker { - left: 0; - margin-top: 8px; - border-top-width: 6px; - border-bottom-width: 6px; - border-left: 6px solid gray; -} -.dark .highslide-thumbstrip-vertical .highslide-marker, .highslide-viewport .highslide-thumbstrip-vertical .highslide-marker { - border-left-color: white; -} - -.highslide-viewport .highslide-thumbstrip-float { - overflow: auto; -} -.highslide-thumbstrip-float ul { - margin: 2px 0; - padding: 0; -} -.highslide-thumbstrip-float li { - display: block; - height: 60px; - margin: 0 2px; - list-style: none; - float: left; -} -.highslide-thumbstrip-float img { - display: inline; - border-color: silver; - max-height: 56px; -} -.highslide-thumbstrip-float .highslide-active-anchor img { - border-color: black; -} -.highslide-thumbstrip-float .highslide-scroll-up div, .highslide-thumbstrip-float .highslide-scroll-down div { - display: none; -} -.highslide-thumbstrip-float .highslide-marker { - display: none; -} \ No newline at end of file diff --git a/public/assets/stylesheets/sbadmin/sb.css b/public/assets/stylesheets/sbadmin/sb.css index 151ee76cfe..7317b94229 100755 --- a/public/assets/stylesheets/sbadmin/sb.css +++ b/public/assets/stylesheets/sbadmin/sb.css @@ -8,6 +8,11 @@ body { background-color: #f8f8f8; } +.google-chart-error { + height:30px; + background:url('../../images/error.png') no-repeat center center +} + #wrapper { width: 100%; }