diff --git a/.coveralls.yml b/.coveralls.yml index 8951ac7b73..0c2d3ece9c 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1,3 +1,3 @@ src_dir: . -coverage_clover: tests/_output/coverage.xml -json_path: tests/_output/coveralls-upload.json +coverage_clover: storage/coverage/clover.xml +json_path: storage/coverage/coveralls-upload.json \ No newline at end of file diff --git a/.gitignore b/.gitignore index ddec920a30..60d6836759 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ c3.php db.sqlite-journal tests/_output/* .env +clover.xml diff --git a/.travis.yml b/.travis.yml index e96ea998f8..702afba662 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,12 +15,7 @@ install: - mv -v .env.testing .env script: - - ./tests/_data/db.sh - - php vendor/bin/codecept build - - php vendor/bin/codecept run --coverage --coverage-xml --no-exit + - phpunit after_script: - - cp -v tests/_output/coverage.xml build/logs/clover.xml - php vendor/bin/coveralls - - vendor/bin/test-reporter --stdout > codeclimate.json - - "curl -X POST -d @codeclimate.json -H 'Content-Type: application/json' -H 'User-Agent: Code Climate (PHP Test Reporter v0.1.1)' https://codeclimate.com/test_reports" \ No newline at end of file diff --git a/README.md b/README.md index 21c1857d84..a8a488a6ca 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Firefly III (v3.3.1) +Firefly III (v3.3.2) =========== [![Build Status](https://travis-ci.org/JC5/firefly-iii.svg?branch=develop)](https://travis-ci.org/JC5/firefly-iii) diff --git a/app/Events/JournalCreated.php b/app/Events/JournalCreated.php new file mode 100644 index 0000000000..d7af994329 --- /dev/null +++ b/app/Events/JournalCreated.php @@ -0,0 +1,31 @@ +journal = $journal; + $this->piggyBankId = $piggyBankId; + + + + + } + +} diff --git a/app/Events/JournalSaved.php b/app/Events/JournalSaved.php new file mode 100644 index 0000000000..e618861169 --- /dev/null +++ b/app/Events/JournalSaved.php @@ -0,0 +1,25 @@ +journal = $journal; + } + +} diff --git a/app/Handlers/Events/ConnectJournalToPiggyBank.php b/app/Handlers/Events/ConnectJournalToPiggyBank.php new file mode 100644 index 0000000000..5747aeba49 --- /dev/null +++ b/app/Handlers/Events/ConnectJournalToPiggyBank.php @@ -0,0 +1,85 @@ +journal; + $piggyBankId = $event->piggyBankId; + + Log::debug('JournalCreated event: ' . $journal->id . ', ' . $piggyBankId); + + /** @var PiggyBank $piggyBank */ + $piggyBank = Auth::user()->piggybanks()->where('piggy_banks.id', $piggyBankId)->first(['piggy_banks.*']); + + if (is_null($piggyBank) || $journal->transactionType->type != 'Transfer') { + return; + } + Log::debug('Found a piggy bank'); + $amount = 0; + /** @var Transaction $transaction */ + foreach ($journal->transactions()->get() as $transaction) { + if ($transaction->account_id === $piggyBank->account_id) { + // this transaction is the relevant one. + $amount = floatval($transaction->amount); + } + } + Log::debug('Amount: ' . $amount); + if ($amount == 0) { + return; + } + // update piggy bank rep for date of transaction journal. + $repetition = $piggyBank->piggyBankRepetitions()->relevantOnDate($journal->date)->first(); + if (is_null($repetition)) { + return; + } + + Log::debug('Found rep! ' . $repetition->id); + $repetition->currentamount += $amount; + $repetition->save(); + + PiggyBankEvent::create( + [ + 'piggy_bank_id' => $piggyBank->id, + 'transaction_journal_id' => $journal->id, + 'date' => $journal->date, + 'amount' => $amount + ] + ); + + } + + +} diff --git a/app/Handlers/Events/RescanJournal.php b/app/Handlers/Events/RescanJournal.php new file mode 100644 index 0000000000..9b74e3bb44 --- /dev/null +++ b/app/Handlers/Events/RescanJournal.php @@ -0,0 +1,53 @@ +journal; + + Log::debug('Triggered saved event for journal #' . $journal->id . ' (' . $journal->description . ')'); + + /** @var \FireflyIII\Repositories\Bill\BillRepositoryInterface $repository */ + $repository = App::make('FireflyIII\Repositories\Bill\BillRepositoryInterface'); + $list = $journal->user->bills()->where('active', 1)->where('automatch', 1)->get(); + + Log::debug('Found ' . $list->count() . ' bills to check.'); + + /** @var Bill $bill */ + foreach ($list as $bill) { + Log::debug('Now calling bill #' . $bill->id . ' (' . $bill->name . ')'); + $repository->scan($bill, $journal); + } + + Log::debug('Done!'); + } + +} diff --git a/app/Handlers/Events/UpdateJournalConnection.php b/app/Handlers/Events/UpdateJournalConnection.php new file mode 100644 index 0000000000..346a806abc --- /dev/null +++ b/app/Handlers/Events/UpdateJournalConnection.php @@ -0,0 +1,64 @@ +journal; + + // get the event connected to this journal: + /** @var PiggyBankEvent $event */ + $event = PiggyBankEvent::where('transaction_journal_id', $journal->id)->first(); + if(is_null($event)) { + return; + } + $piggyBank = $event->piggyBank()->first(); + $repetition = $piggyBank->piggyBankRepetitions()->relevantOnDate($journal->date)->first(); + + if (is_null($repetition)) { + return; + } + $amount = 0; + /** @var Transaction $transaction */ + foreach ($journal->transactions()->get() as $transaction) { + if ($transaction->account_id === $piggyBank->account_id) { + // this transaction is the relevant one. + $amount = floatval($transaction->amount); + } + } + + // update current repetition: + $diff = $amount - $event->amount; + + $repetition->currentamount += $diff; + $repetition->save(); + + + $event->amount = $amount; + $event->save(); + } + +} diff --git a/app/Helpers/Reminders/ReminderHelper.php b/app/Helpers/Reminders/ReminderHelper.php new file mode 100644 index 0000000000..ccfdc2d57a --- /dev/null +++ b/app/Helpers/Reminders/ReminderHelper.php @@ -0,0 +1,148 @@ +reminders() + ->where('remindersable_id', $piggyBank->id) + ->onDates($start, $end) + ->first(); + if (is_null($reminder)) { + + if (!is_null($piggyBank->targetdate)) { + // get ranges again, but now for the start date + $ranges = $this->getReminderRanges($piggyBank, $start); + $currentRep = $piggyBank->currentRelevantRep(); + $left = $piggyBank->targetamount - $currentRep->currentamount; + $perReminder = $left / count($ranges); + } else { + $perReminder = null; + $ranges = []; + $left = 0; + } + $metaData = [ + 'perReminder' => $perReminder, + 'rangesCount' => count($ranges), + 'ranges' => $ranges, + 'leftToSave' => $left, + ]; + + + // create one: + $reminder = new Reminder; + $reminder->user()->associate(Auth::user()); + $reminder->startdate = $start; + $reminder->enddate = $end; + $reminder->active = true; + $reminder->metadata = $metaData; + $reminder->notnow = false; + $reminder->remindersable()->associate($piggyBank); + $reminder->save(); + + return $reminder; + + } else { + return $reminder; + } + } + + /** + * This routine will return an array consisting of two dates which indicate the start + * and end date for each reminder that this piggy bank will have, if the piggy bank has + * any reminders. For example: + * + * [12 mar - 15 mar] + * [15 mar - 18 mar] + * + * etcetera. + * + * Array is filled with tiny arrays with Carbon objects in them. + * + * @param PiggyBank $piggyBank + * @param Carbon $date ; + * + * @return array + */ + public function getReminderRanges(PiggyBank $piggyBank, Carbon $date = null) + { + $ranges = []; + if (is_null($date)) { + $date = new Carbon; + } + + if ($piggyBank->remind_me === false) { + return $ranges; + } + + if (!is_null($piggyBank->targetdate)) { + // count back until now. + // echo 'Count back!
'; + $start = $piggyBank->targetdate; + $end = $piggyBank->startdate; + + while ($start > $end) { + $currentEnd = clone $start; + $start = Navigation::subtractPeriod($start, $piggyBank->reminder, 1); + $currentStart = clone $start; + $ranges[] = ['start' => clone $currentStart, 'end' => clone $currentEnd]; + } + } else { + $start = clone $piggyBank->startdate; + while ($start < $date) { + $currentStart = clone $start; + $start = Navigation::addPeriod($start, $piggyBank->reminder, 0); + $currentEnd = clone $start; + $ranges[] = ['start' => clone $currentStart, 'end' => clone $currentEnd]; + } + } + + return $ranges; + } + + /** + * Takes a reminder, finds the piggy bank and tells you what to do now. + * Aka how much money to put in. + * + * + * @param Reminder $reminder + * + * @return string + */ + public function getReminderText(Reminder $reminder) + { + /** @var PiggyBank $piggyBank */ + $piggyBank = $reminder->remindersable; + if(is_null($piggyBank)) { + return 'Piggy bank no longer exists.'; + } + + if (is_null($piggyBank->targetdate)) { + return 'Add money to this piggy bank to reach your target of ' . Amount::format($piggyBank->targetamount); + } + + return 'Add ' . Amount::format($reminder->metadata->perReminder) . ' to fill this piggy bank on ' . $piggyBank->targetdate->format('jS F Y'); + + } +} \ No newline at end of file diff --git a/app/Helpers/Reminders/ReminderHelperInterface.php b/app/Helpers/Reminders/ReminderHelperInterface.php new file mode 100644 index 0000000000..cc381e1b04 --- /dev/null +++ b/app/Helpers/Reminders/ReminderHelperInterface.php @@ -0,0 +1,51 @@ +transactionjournals()->orderBy('date', 'ASC')->first(); - if ($journal) { - return $journal->date; - } - - return Carbon::now(); - } - /** * This method gets some kind of list for a monthly overview. * @@ -62,7 +50,7 @@ class ReportHelper implements ReportHelperInterface $end = clone $date; $end->endOfMonth(); // all budgets - $set = \Auth::user()->budgets() + $set = Auth::user()->budgets() ->leftJoin( 'budget_limits', function (JoinClause $join) use ($date) { $join->on('budget_limits.budget_id', '=', 'budgets.id')->where('budget_limits.startdate', '=', $date->format('Y-m-d')); @@ -99,7 +87,8 @@ class ReportHelper implements ReportHelperInterface $end = Carbon::now(); $months = []; while ($start <= $end) { - $months[] = [ + $year = $start->format('Y'); + $months[$year][] = [ 'formatted' => $start->format('F Y'), 'month' => intval($start->format('m')), 'year' => intval($start->format('Y')), @@ -130,43 +119,47 @@ class ReportHelper implements ReportHelperInterface /** * @param Carbon $date + * @param bool $showSharedReports * * @return array */ - public function yearBalanceReport(Carbon $date) + public function yearBalanceReport(Carbon $date, $showSharedReports = false) { - $start = clone $date; - $end = clone $date; - $sharedAccounts = []; - $sharedCollection = \Auth::user()->accounts() - ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id') - ->where('account_meta.name', '=', 'accountRole') - ->where('account_meta.data', '=', json_encode('sharedExpense')) - ->get(['accounts.id']); + $start = clone $date; + $end = clone $date; + $sharedAccounts = []; + if ($showSharedReports === false) { + $sharedCollection = \Auth::user()->accounts() + ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id') + ->where('account_meta.name', '=', 'accountRole') + ->where('account_meta.data', '=', json_encode('sharedAsset')) + ->get(['accounts.id']); - foreach ($sharedCollection as $account) { - $sharedAccounts[] = $account->id; + foreach ($sharedCollection as $account) { + $sharedAccounts[] = $account->id; + } } - $accounts = \Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*'])->filter( - function (Account $account) use ($sharedAccounts) { - if (!in_array($account->id, $sharedAccounts)) { - return $account; - } + $accounts = Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->orderBy('accounts.name', 'ASC')->get(['accounts.*']) + ->filter( + function (Account $account) use ($sharedAccounts) { + if (!in_array($account->id, $sharedAccounts)) { + return $account; + } - return null; - } - ); + return null; + } + ); $report = []; $start->startOfYear()->subDay(); $end->endOfYear(); foreach ($accounts as $account) { $report[] = [ - 'start' => \Steam::balance($account, $start), - 'end' => \Steam::balance($account, $end), + 'start' => Steam::balance($account, $start), + 'end' => Steam::balance($account, $end), 'account' => $account, - 'shared' => $account->accountRole == 'sharedExpense' + 'shared' => $account->accountRole == 'sharedAsset' ]; } diff --git a/app/Helpers/Report/ReportHelperInterface.php b/app/Helpers/Report/ReportHelperInterface.php index 99fbad8742..dedc6b38cb 100644 --- a/app/Helpers/Report/ReportHelperInterface.php +++ b/app/Helpers/Report/ReportHelperInterface.php @@ -14,32 +14,6 @@ interface ReportHelperInterface { - /** - * @return Carbon - */ - public function firstDate(); - - /** - * @param Carbon $date - * - * @return array - */ - public function listOfMonths(Carbon $date); - - /** - * @param Carbon $date - * - * @return array - */ - public function listOfYears(Carbon $date); - - /** - * @param Carbon $date - * - * @return array - */ - public function yearBalanceReport(Carbon $date); - /** * This methods fails to take in account transfers FROM shared accounts. * @@ -59,4 +33,26 @@ interface ReportHelperInterface * @return Collection */ public function getBudgetsForMonth(Carbon $date); + + /** + * @param Carbon $date + * + * @return array + */ + public function listOfMonths(Carbon $date); + + /** + * @param Carbon $date + * + * @return array + */ + public function listOfYears(Carbon $date); + + /** + * @param Carbon $date + * @param bool $showSharedReports + * + * @return array + */ + public function yearBalanceReport(Carbon $date, $showSharedReports = false); } \ No newline at end of file diff --git a/app/Helpers/Report/ReportQuery.php b/app/Helpers/Report/ReportQuery.php index fc26592564..9d0a5aab8f 100644 --- a/app/Helpers/Report/ReportQuery.php +++ b/app/Helpers/Report/ReportQuery.php @@ -1,10 +1,4 @@ accounts() - ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') - ->leftJoin( - 'account_meta', function (JoinClause $join) { - $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', "accountRole"); - } - ) - ->whereIn('account_types.type', ['Default account', 'Cash account', 'Asset account']) - ->where('active', 1) - ->where( - function (Builder $query) { - $query->where('account_meta.data', '!=', '"sharedExpense"'); - $query->orWhereNull('account_meta.data'); - } - ) - ->get(['accounts.*']); + $query = Auth::user()->accounts(); + if ($showSharedReports === false) { + + $query->leftJoin( + 'account_meta', function (JoinClause $join) { + $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', "accountRole"); + } + )->where( + function (Builder $query) { + $query->where('account_meta.data', '!=', '"sharedAsset"'); + $query->orWhereNull('account_meta.data'); + } + ); + + } + $query->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->whereIn('account_types.type', ['Default account', 'Cash account', 'Asset account']) + ->where('active', 1) + ->orderBy('accounts.name', 'ASC'); + + return $query->get(['accounts.*']); } /** @@ -82,19 +83,37 @@ class ReportQuery implements ReportQueryInterface ->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'otherJournals.id') ->before($end)->after($start) ->where('transaction_types.type', 'Withdrawal') - ->where('transaction_journals.user_id', \Auth::user()->id) + ->where('transaction_journals.user_id', Auth::user()->id) ->whereNull('budget_transaction_journal.budget_id')->whereNull('transaction_journals.deleted_at') ->whereNull('otherJournals.deleted_at') ->where('transactions.account_id', $account->id) ->whereNotNull('transaction_group_transaction_journal.transaction_group_id') - ->first( + ->get( [ - DB::Raw('SUM(`transactions`.`amount`) as `amount`') + 'transaction_journals.*', + 'transactions.amount' ] ); + + return $set; + } + + /** + * This method will get the sum of all expenses in a certain time period that have no budget + * and are balanced by a transfer to make up for it. + * + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function balancedTransactionsSum(Account $account, Carbon $start, Carbon $end) + { + $set = $this->balancedTransactionsList($account, $start, $end); $sum = 0; - if (!is_null($set)) { - $sum = floatval($set->amount); + foreach ($set as $entry) { + $sum += floatval($entry->amount); } return $sum; @@ -105,25 +124,31 @@ class ReportQuery implements ReportQueryInterface * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function getAllAccounts(Carbon $start, Carbon $end) + public function getAllAccounts(Carbon $start, Carbon $end, $showSharedReports = false) { - $set = Auth::user()->accounts()->orderBy('accounts.name', 'ASC') - ->accountTypeIn(['Default account', 'Asset account', 'Cash account']) - ->leftJoin( - 'account_meta', function (JoinClause $join) { - $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); - } - ) - ->where( - function (Builder $query) { - $query->where('account_meta.data', '!=', '"sharedExpense"'); - $query->orWhereNull('account_meta.data'); - } - ) - ->get(['accounts.*']); + $query = Auth::user()->accounts()->orderBy('accounts.name', 'ASC') + ->accountTypeIn(['Default account', 'Asset account', 'Cash account']); + if ($showSharedReports === false) { + $query->leftJoin( + 'account_meta', function (JoinClause $join) { + $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); + } + ) + ->orderBy('accounts.name', 'ASC') + ->where( + function (Builder $query) use ($showSharedReports) { + + $query->where('account_meta.data', '!=', '"sharedAsset"'); + $query->orWhereNull('account_meta.data'); + + } + ); + } + $set = $query->get(['accounts.*']); $set->each( function (Account $account) use ($start, $end) { /** @noinspection PhpParamsInspection */ @@ -167,7 +192,40 @@ class ReportQuery implements ReportQueryInterface return $set; + } + /** + * Get a list of transaction journals that have no budget, filtered for the specified account + * and the specified date range. + * + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function getTransactionsWithoutBudget(Account $account, Carbon $start, Carbon $end) + { + $set = TransactionJournal:: + leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('budgets', 'budgets.id', '=', 'budget_transaction_journal.budget_id') + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->leftJoin( + 'transactions', function (JoinClause $join) { + $join->on('transactions.transaction_journal_id', '=', 'transaction_journals.id')->where('transactions.amount', '<', 0); + } + ) + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') + ->before($end) + ->after($start) + ->where('accounts.id', $account->id) + ->where('transaction_journals.user_id', Auth::user()->id) + ->where('transaction_types.type', 'Withdrawal') + ->whereNull('budgets.id') + ->orderBy('transaction_journals.date', 'ASC') + ->get(['budgets.name', 'transactions.amount', 'transaction_journals.*']); + + return $set; } /** @@ -177,62 +235,73 @@ class ReportQuery implements ReportQueryInterface * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function incomeByPeriod(Carbon $start, Carbon $end) + public function incomeByPeriod(Carbon $start, Carbon $end, $showSharedReports = false) { - return TransactionJournal:: + $query = TransactionJournal:: leftJoin( 'transactions as t_from', function (JoinClause $join) { $join->on('t_from.transaction_journal_id', '=', 'transaction_journals.id')->where('t_from.amount', '<', 0); } ) - ->leftJoin('accounts as ac_from', 't_from.account_id', '=', 'ac_from.id') - ->leftJoin( - 'account_meta as acm_from', function (JoinClause $join) { - $join->on('ac_from.id', '=', 'acm_from.account_id')->where('acm_from.name', '=', 'accountRole'); - } - ) - ->leftJoin( - 'transactions as t_to', function (JoinClause $join) { - $join->on('t_to.transaction_journal_id', '=', 'transaction_journals.id')->where('t_to.amount', '>', 0); - } - ) - ->leftJoin('accounts as ac_to', 't_to.account_id', '=', 'ac_to.id') - ->leftJoin( - 'account_meta as acm_to', function (JoinClause $join) { - $join->on('ac_to.id', '=', 'acm_to.account_id')->where('acm_to.name', '=', 'accountRole'); - } - ) - ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->where( - function ($query) { - $query->where( - function ($q) { - $q->where('transaction_types.type', 'Deposit'); - $q->where('acm_to.data', '!=', '"sharedExpense"'); - } - ); - $query->orWhere( - function ($q) { - $q->where('transaction_types.type', 'Transfer'); - $q->where('acm_from.data', '=', '"sharedExpense"'); - } - ); - } - ) - ->before($end)->after($start) - ->where('transaction_journals.user_id', Auth::user()->id) - ->groupBy('t_from.account_id')->orderBy('transaction_journals.date') - ->get( - ['transaction_journals.id', - 'transaction_journals.description', - 'transaction_journals.encrypted', - 'transaction_types.type', - 't_to.amount', 'transaction_journals.date', 't_from.account_id as account_id', - 'ac_from.name as name'] - ); + ->leftJoin('accounts as ac_from', 't_from.account_id', '=', 'ac_from.id') + ->leftJoin( + 'account_meta as acm_from', function (JoinClause $join) { + $join->on('ac_from.id', '=', 'acm_from.account_id')->where('acm_from.name', '=', 'accountRole'); + } + ) + ->leftJoin( + 'transactions as t_to', function (JoinClause $join) { + $join->on('t_to.transaction_journal_id', '=', 'transaction_journals.id')->where('t_to.amount', '>', 0); + } + ) + ->leftJoin('accounts as ac_to', 't_to.account_id', '=', 'ac_to.id') + ->leftJoin( + 'account_meta as acm_to', function (JoinClause $join) { + $join->on('ac_to.id', '=', 'acm_to.account_id')->where('acm_to.name', '=', 'accountRole'); + } + ) + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); + if ($showSharedReports === false) { + // only get deposits not to a shared account + // and transfers to a shared account. + $query->where( + function ($query) { + $query->where( + function ($q) { + $q->where('transaction_types.type', 'Deposit'); + $q->where('acm_to.data', '!=', '"sharedAsset"'); + } + ); + $query->orWhere( + function ($q) { + $q->where('transaction_types.type', 'Transfer'); + $q->where('acm_from.data', '=', '"sharedAsset"'); + } + ); + } + ); + } else { + // any deposit is fine. + $query->where('transaction_types.type', 'Deposit'); + } + $query->before($end)->after($start) + ->where('transaction_journals.user_id', Auth::user()->id) + ->groupBy('t_from.account_id')->orderBy('transaction_journals.date'); + + return $query->get( + ['transaction_journals.id', + 'transaction_journals.description', + 'transaction_journals.encrypted', + 'transaction_types.type', + DB::Raw('SUM(`t_to`.`amount`) as `amount`'), + 'transaction_journals.date', + 't_from.account_id as account_id', + 'ac_from.name as name'] + ); } /** @@ -240,33 +309,37 @@ class ReportQuery implements ReportQueryInterface * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function journalsByBudget(Carbon $start, Carbon $end) + public function journalsByBudget(Carbon $start, Carbon $end, $showSharedReports = false) { - return Auth::user()->transactionjournals() - ->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('budgets', 'budget_transaction_journal.budget_id', '=', 'budgets.id') - ->leftJoin( - 'transactions', function (JoinClause $join) { - $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', '<', 0); - } - ) - ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') - ->leftJoin( - 'account_meta', function (JoinClause $join) { - $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); - } - ) - ->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id') - ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) - ->where('account_meta.data', '!=', '"sharedExpense"') - ->where('transaction_types.type', 'Withdrawal') - ->groupBy('budgets.id') - ->orderBy('budgets.name', 'ASC') - ->get(['budgets.id', 'budgets.name', DB::Raw('SUM(`transactions`.`amount`) AS `spent`')]); + $query = Auth::user()->transactionjournals() + ->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('budgets', 'budget_transaction_journal.budget_id', '=', 'budgets.id') + ->leftJoin( + 'transactions', function (JoinClause $join) { + $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', '<', 0); + } + ) + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id'); + if ($showSharedReports === false) { + + $query->leftJoin( + 'account_meta', function (JoinClause $join) { + $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); + } + )->where('account_meta.data', '!=', '"sharedAsset"'); + } + $query->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id') + ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) + ->where('transaction_types.type', 'Withdrawal') + ->groupBy('budgets.id') + ->orderBy('budgets.name', 'ASC'); + + return $query->get(['budgets.id', 'budgets.name', DB::Raw('SUM(`transactions`.`amount`) AS `spent`')]); } /** @@ -275,35 +348,38 @@ class ReportQuery implements ReportQueryInterface * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function journalsByCategory(Carbon $start, Carbon $end) + public function journalsByCategory(Carbon $start, Carbon $end, $showSharedReports = false) { - return Auth::user()->transactionjournals() - ->leftJoin( - 'category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id' - ) - ->leftJoin('categories', 'category_transaction_journal.category_id', '=', 'categories.id') - ->leftJoin( - 'transactions', function (JoinClause $join) { - $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', '<', 0); - } - ) - ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') - ->leftJoin( - 'account_meta', function (JoinClause $join) { - $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); - } - ) - ->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id') - ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) - ->where('account_meta.data', '!=', '"sharedExpense"') - ->where('transaction_types.type', 'Withdrawal') - ->groupBy('categories.id') - ->orderBy('amount') - ->get(['categories.id', 'categories.name', DB::Raw('SUM(`transactions`.`amount`) AS `amount`')]); + $query = Auth::user()->transactionjournals() + ->leftJoin( + 'category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id' + ) + ->leftJoin('categories', 'category_transaction_journal.category_id', '=', 'categories.id') + ->leftJoin( + 'transactions', function (JoinClause $join) { + $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', '<', 0); + } + ) + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id'); + if ($showSharedReports === false) { + $query->leftJoin( + 'account_meta', function (JoinClause $join) { + $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); + } + )->where('account_meta.data', '!=', '"sharedAsset"'); + } + $query->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id') + ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) + ->where('transaction_types.type', 'Withdrawal') + ->groupBy('categories.id') + ->orderBy('amount'); + + return $query->get(['categories.id', 'categories.name', DB::Raw('SUM(`transactions`.`amount`) AS `amount`')]); } @@ -315,57 +391,65 @@ class ReportQuery implements ReportQueryInterface * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function journalsByExpenseAccount(Carbon $start, Carbon $end) + public function journalsByExpenseAccount(Carbon $start, Carbon $end, $showSharedReports = false) { - return TransactionJournal:: - leftJoin( + $query = TransactionJournal::leftJoin( 'transactions as t_from', function (JoinClause $join) { $join->on('t_from.transaction_journal_id', '=', 'transaction_journals.id')->where('t_from.amount', '<', 0); } - ) - ->leftJoin('accounts as ac_from', 't_from.account_id', '=', 'ac_from.id') - ->leftJoin( - 'account_meta as acm_from', function (JoinClause $join) { - $join->on('ac_from.id', '=', 'acm_from.account_id')->where('acm_from.name', '=', 'accountRole'); - } - ) - ->leftJoin( - 'transactions as t_to', function (JoinClause $join) { - $join->on('t_to.transaction_journal_id', '=', 'transaction_journals.id')->where('t_to.amount', '>', 0); - } - ) - ->leftJoin('accounts as ac_to', 't_to.account_id', '=', 'ac_to.id') - ->leftJoin( - 'account_meta as acm_to', function (JoinClause $join) { - $join->on('ac_to.id', '=', 'acm_to.account_id')->where('acm_to.name', '=', 'accountRole'); - } - ) - ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->where( - function ($query) { - $query->where( - function ($q) { - $q->where('transaction_types.type', 'Withdrawal'); - $q->where('acm_from.data', '!=', '"sharedExpense"'); - } - ); - $query->orWhere( - function ($q) { - $q->where('transaction_types.type', 'Transfer'); - $q->where('acm_to.data', '=', '"sharedExpense"'); - } - ); - } - ) - ->before($end) - ->after($start) - ->where('transaction_journals.user_id', Auth::user()->id) - ->groupBy('t_to.account_id') - ->orderBy('amount', 'DESC') - ->get(['t_to.account_id as id', 'ac_to.name as name', DB::Raw('SUM(t_to.amount) as `amount`')]); + )->leftJoin('accounts as ac_from', 't_from.account_id', '=', 'ac_from.id') + ->leftJoin( + 'account_meta as acm_from', function (JoinClause $join) { + $join->on('ac_from.id', '=', 'acm_from.account_id')->where('acm_from.name', '=', 'accountRole'); + } + ) + ->leftJoin( + 'transactions as t_to', function (JoinClause $join) { + $join->on('t_to.transaction_journal_id', '=', 'transaction_journals.id')->where('t_to.amount', '>', 0); + } + ) + ->leftJoin('accounts as ac_to', 't_to.account_id', '=', 'ac_to.id') + ->leftJoin( + 'account_meta as acm_to', function (JoinClause $join) { + $join->on('ac_to.id', '=', 'acm_to.account_id')->where('acm_to.name', '=', 'accountRole'); + } + ) + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); + + if ($showSharedReports === false) { + // get all withdrawals not from a shared accounts + // and all transfers to a shared account + $query->where( + function ($query) { + $query->where( + function ($q) { + $q->where('transaction_types.type', 'Withdrawal'); + $q->where('acm_from.data', '!=', '"sharedAsset"'); + } + ); + $query->orWhere( + function ($q) { + $q->where('transaction_types.type', 'Transfer'); + $q->where('acm_to.data', '=', '"sharedAsset"'); + } + ); + } + ); + } else { + // any withdrawal goes: + $query->where('transaction_types.type', 'Withdrawal'); + } + $query->before($end) + ->after($start) + ->where('transaction_journals.user_id', Auth::user()->id) + ->groupBy('t_to.account_id') + ->orderBy('amount', 'DESC'); + + return $query->get(['t_to.account_id as id', 'ac_to.name as name', DB::Raw('SUM(t_to.amount) as `amount`')]); } /** @@ -373,55 +457,65 @@ class ReportQuery implements ReportQueryInterface * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function journalsByRevenueAccount(Carbon $start, Carbon $end) + public function journalsByRevenueAccount(Carbon $start, Carbon $end, $showSharedReports = false) { - return TransactionJournal:: + $query = TransactionJournal:: leftJoin( 'transactions as t_from', function (JoinClause $join) { $join->on('t_from.transaction_journal_id', '=', 'transaction_journals.id')->where('t_from.amount', '<', 0); } ) - ->leftJoin('accounts as ac_from', 't_from.account_id', '=', 'ac_from.id') - ->leftJoin( - 'account_meta as acm_from', function (JoinClause $join) { - $join->on('ac_from.id', '=', 'acm_from.account_id')->where('acm_from.name', '=', 'accountRole'); - } - ) - ->leftJoin( - 'transactions as t_to', function (JoinClause $join) { - $join->on('t_to.transaction_journal_id', '=', 'transaction_journals.id')->where('t_to.amount', '>', 0); - } - ) - ->leftJoin('accounts as ac_to', 't_to.account_id', '=', 'ac_to.id') - ->leftJoin( - 'account_meta as acm_to', function (JoinClause $join) { - $join->on('ac_to.id', '=', 'acm_to.account_id')->where('acm_to.name', '=', 'accountRole'); - } - ) - ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') - ->where( - function ($query) { - $query->where( - function ($q) { - $q->where('transaction_types.type', 'Deposit'); - $q->where('acm_to.data', '!=', '"sharedExpense"'); - } - ); - $query->orWhere( - function ($q) { - $q->where('transaction_types.type', 'Transfer'); - $q->where('acm_from.data', '=', '"sharedExpense"'); - } - ); - } - ) - ->before($end)->after($start) - ->where('transaction_journals.user_id', Auth::user()->id) - ->groupBy('t_from.account_id')->orderBy('amount') - ->get(['t_from.account_id as account_id', 'ac_from.name as name', DB::Raw('SUM(t_from.amount) as `amount`')]); + ->leftJoin('accounts as ac_from', 't_from.account_id', '=', 'ac_from.id') + ->leftJoin( + 'account_meta as acm_from', function (JoinClause $join) { + $join->on('ac_from.id', '=', 'acm_from.account_id')->where('acm_from.name', '=', 'accountRole'); + } + ) + ->leftJoin( + 'transactions as t_to', function (JoinClause $join) { + $join->on('t_to.transaction_journal_id', '=', 'transaction_journals.id')->where('t_to.amount', '>', 0); + } + ) + ->leftJoin('accounts as ac_to', 't_to.account_id', '=', 'ac_to.id') + ->leftJoin( + 'account_meta as acm_to', function (JoinClause $join) { + $join->on('ac_to.id', '=', 'acm_to.account_id')->where('acm_to.name', '=', 'accountRole'); + } + ) + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); + if ($showSharedReports === false) { + + // show queries where transfer type is deposit, and its not to a shared account + // or where its a transfer and its from a shared account (both count as incomes) + $query->where( + function ($query) { + $query->where( + function ($q) { + $q->where('transaction_types.type', 'Deposit'); + $q->where('acm_to.data', '!=', '"sharedAsset"'); + } + ); + $query->orWhere( + function ($q) { + $q->where('transaction_types.type', 'Transfer'); + $q->where('acm_from.data', '=', '"sharedAsset"'); + } + ); + } + ); + } else { + // any deposit goes: + $query->where('transaction_types.type', 'Deposit'); + } + $query->before($end)->after($start) + ->where('transaction_journals.user_id', Auth::user()->id) + ->groupBy('t_from.account_id')->orderBy('amount'); + + return $query->get(['t_from.account_id as account_id', 'ac_from.name as name', DB::Raw('SUM(t_from.amount) as `amount`')]); } /** @@ -450,7 +544,7 @@ class ReportQuery implements ReportQueryInterface $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); } ) - ->where('account_meta.data', '"sharedExpense"') + ->where('account_meta.data', '"sharedAsset"') ->after($start) ->before($end) ->where('transaction_types.type', 'Transfer') @@ -492,7 +586,7 @@ class ReportQuery implements ReportQueryInterface 'category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id' ) ->leftJoin('categories', 'category_transaction_journal.category_id', '=', 'categories.id') - ->where('account_meta.data', '"sharedExpense"') + ->where('account_meta.data', '"sharedAsset"') ->after($start) ->before($end) ->where('transaction_types.type', 'Transfer') diff --git a/app/Helpers/Report/ReportQueryInterface.php b/app/Helpers/Report/ReportQueryInterface.php index 1bce968341..e67581a047 100644 --- a/app/Helpers/Report/ReportQueryInterface.php +++ b/app/Helpers/Report/ReportQueryInterface.php @@ -17,19 +17,69 @@ interface ReportQueryInterface /** * This query retrieves a list of accounts that are active and not shared. * + * @param bool $showSharedReports + * * @return Collection */ - public function accountList(); + public function accountList($showSharedReports = false); + + /** + * This method will get a list of all expenses in a certain time period that have no budget + * and are balanced by a transfer to make up for it. + * + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function balancedTransactionsList(Account $account, Carbon $start, Carbon $end); + + /** + * This method will get the sum of all expenses in a certain time period that have no budget + * and are balanced by a transfer to make up for it. + * + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function balancedTransactionsSum(Account $account, Carbon $start, Carbon $end); /** * Get a users accounts combined with various meta-data related to the start and end date. * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function getAllAccounts(Carbon $start, Carbon $end); + public function getAllAccounts(Carbon $start, Carbon $end, $showSharedReports = false); + + /** + * Grabs a summary of all expenses grouped by budget, related to the account. + * + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return mixed + */ + public function getBudgetSummary(Account $account, Carbon $start, Carbon $end); + + /** + * Get a list of transaction journals that have no budget, filtered for the specified account + * and the specified date range. + * + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function getTransactionsWithoutBudget(Account $account, Carbon $start, Carbon $end); /** * This method returns all "income" journals in a certain period, which are both transfers from a shared account @@ -38,20 +88,22 @@ interface ReportQueryInterface * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function incomeByPeriod(Carbon $start, Carbon $end); + public function incomeByPeriod(Carbon $start, Carbon $end, $showSharedReports = false); /** * Gets a list of expenses grouped by the budget they were filed under. * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function journalsByBudget(Carbon $start, Carbon $end); + public function journalsByBudget(Carbon $start, Carbon $end, $showSharedReports = false); /** * Gets a list of categories and the expenses therein, grouped by the relevant category. @@ -59,10 +111,11 @@ interface ReportQueryInterface * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function journalsByCategory(Carbon $start, Carbon $end); + public function journalsByCategory(Carbon $start, Carbon $end, $showSharedReports = false); /** * Gets a list of expense accounts and the expenses therein, grouped by that expense account. @@ -72,20 +125,22 @@ interface ReportQueryInterface * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function journalsByExpenseAccount(Carbon $start, Carbon $end); + public function journalsByExpenseAccount(Carbon $start, Carbon $end, $showSharedReports = false); /** * This method returns all deposits into asset accounts, grouped by the revenue account, * * @param Carbon $start * @param Carbon $end + * @param bool $showSharedReports * * @return Collection */ - public function journalsByRevenueAccount(Carbon $start, Carbon $end); + public function journalsByRevenueAccount(Carbon $start, Carbon $end, $showSharedReports = false); /** * With an equally misleading name, this query returns are transfers to shared accounts. These are considered @@ -108,27 +163,4 @@ interface ReportQueryInterface * @return Collection */ public function sharedExpensesByCategory(Carbon $start, Carbon $end); - - /** - * Grabs a summary of all expenses grouped by budget, related to the account. - * - * @param Account $account - * @param Carbon $start - * @param Carbon $end - * - * @return mixed - */ - public function getBudgetSummary(Account $account, Carbon $start, Carbon $end); - - /** - * This method will get a list of all expenses in a certain time period that have no budget - * and are balanced by a transfer to make up for it. - * - * @param Account $account - * @param Carbon $start - * @param Carbon $end - * - * @return Collection - */ - public function balancedTransactionsList(Account $account, Carbon $start, Carbon $end); } \ No newline at end of file diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index c3a79b49ca..e5812d513c 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -1,6 +1,5 @@ accountType->type]; @@ -110,29 +117,59 @@ class AccountController extends Controller $subTitle = Config::get('firefly.subTitlesByIdentifier.' . $what); $subTitleIcon = Config::get('firefly.subIconsByIdentifier.' . $what); $types = Config::get('firefly.accountTypesByIdentifier.' . $what); - $accounts = Auth::user()->accounts()->accountTypeIn($types)->get(['accounts.*']); + $size = 50; + $page = intval(Input::get('page')) == 0 ? 1 : intval(Input::get('page')); + $offset = ($page - 1) * $size; + + + // move to repository: + $set = Auth::user()->accounts()->with( + ['accountmeta' => function ($query) { + $query->where('name', 'accountRole'); + }] + )->accountTypeIn($types)->take($size)->offset($offset)->orderBy('accounts.name', 'ASC')->get(['accounts.*']); + $total = Auth::user()->accounts()->accountTypeIn($types)->count(); + + // last activity: + $start = clone Session::get('start'); + $start->subDay(); + $set->each( + function (Account $account) use ($start) { + $lastTransaction = $account->transactions()->leftJoin( + 'transaction_journals', 'transactions.transaction_journal_id', '=', 'transaction_journals.id' + )->orderBy('transaction_journals.date', 'DESC')->first(['transactions.*', 'transaction_journals.date']); + if ($lastTransaction) { + $account->lastActivityDate = $lastTransaction->transactionjournal->date; + } else { + $account->lastActivityDate = null; + } + $account->startBalance = Steam::balance($account, $start); + $account->endBalance = Steam::balance($account, Session::get('end')); + } + ); + + $accounts = new LengthAwarePaginator($set, $total, $size, $page); + $accounts->setPath(route('accounts.index', $what)); + return view('accounts.index', compact('what', 'subTitleIcon', 'subTitle', 'accounts')); } /** * @param Account $account - * @param string $range * @param AccountRepositoryInterface $repository * - * @return \Illuminate\View\View + * @return View */ - public function show(Account $account, $range = 'session') + public function show(Account $account, AccountRepositoryInterface $repository) { - /** @var \FireflyIII\Repositories\Account\AccountRepositoryInterface $repository */ - $repository = App::make('FireflyIII\Repositories\Account\AccountRepositoryInterface'); $page = intval(Input::get('page')) == 0 ? 1 : intval(Input::get('page')); $subTitleIcon = Config::get('firefly.subTitlesByIdentifier.' . $account->accountType->type); $what = Config::get('firefly.shortNamesByFullName.' . $account->accountType->type); - $journals = $repository->getJournals($account, $page, $range); + $journals = $repository->getJournals($account, $page); $subTitle = 'Details for ' . strtolower(e($account->accountType->type)) . ' "' . e($account->name) . '"'; - return view('accounts.show', compact('account', 'what', 'range', 'subTitleIcon', 'journals', 'subTitle')); + return view('accounts.show', compact('account', 'what', 'subTitleIcon', 'journals', 'subTitle')); } /** @@ -158,6 +195,10 @@ class AccountController extends Controller Session::flash('success', 'New account "' . $account->name . '" stored!'); + if (intval(Input::get('create_another')) === 1) { + return Redirect::route('accounts.create', $request->input('what')); + } + return Redirect::route('accounts.index', $request->input('what')); } @@ -186,6 +227,10 @@ class AccountController extends Controller Session::flash('success', 'Account "' . $account->name . '" updated.'); + if (intval(Input::get('return_to_edit')) === 1) { + return Redirect::route('accounts.edit', $account->id); + } + return Redirect::route('accounts.index', $what); } diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index d79cc2c56e..b4c92cf941 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -4,6 +4,9 @@ use FireflyIII\Http\Controllers\Controller; use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Auth\Registrar; use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers; +use Illuminate\Http\Request; +use Mail; +use Session; /** * Class AuthController @@ -43,4 +46,40 @@ class AuthController extends Controller $this->middleware('guest', ['except' => 'getLogout']); } + /** + * Handle a registration request for the application. + * + * @param Request $request + * + * @return \Illuminate\Http\Response + */ + public function postRegister(Request $request) + { + $validator = $this->registrar->validator($request->all()); + + if ($validator->fails()) { + $this->throwValidationException( + $request, $validator + ); + } + + $this->auth->login($this->registrar->create($request->all())); + + // get the email address + $email = $this->auth->user()->email; + + // send email. + Mail::send( + 'emails.registered', [], function ($message) use ($email) { + $message->to($email, $email)->subject('Welcome to Firefly III!'); + } + ); + + // set flash message + Session::flash('success', 'You have registered successfully!'); + + + return redirect($this->redirectPath()); + } + } diff --git a/app/Http/Controllers/BillController.php b/app/Http/Controllers/BillController.php index 139fb63887..59bd29dea2 100644 --- a/app/Http/Controllers/BillController.php +++ b/app/Http/Controllers/BillController.php @@ -8,6 +8,7 @@ use FireflyIII\Models\Bill; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use Input; use Redirect; use Session; use URL; @@ -80,7 +81,7 @@ class BillController extends Controller */ public function index(BillRepositoryInterface $repository) { - $bills = Auth::user()->bills()->get(); + $bills = Auth::user()->bills()->orderBy('name', 'ASC')->get(); $bills->each( function (Bill $bill) use ($repository) { $bill->nextExpectedMatch = $repository->nextExpectedMatch($bill); @@ -108,7 +109,9 @@ class BillController extends Controller return Redirect::intended('/'); } - $set = \DB::table('transactions')->where('amount', '>', 0)->where('amount', '>=', $bill->amount_min)->where('amount', '<=', $bill->amount_max)->get(['transaction_journal_id']); + $set = \DB::table('transactions')->where('amount', '>', 0)->where('amount', '>=', $bill->amount_min)->where('amount', '<=', $bill->amount_max)->get( + ['transaction_journal_id'] + ); $ids = []; /** @var Transaction $entry */ @@ -116,7 +119,7 @@ class BillController extends Controller $ids[] = intval($entry->transaction_journal_id); } if (count($ids) > 0) { - $journals = Auth::user()->transactionjournals()->whereIn('id',$ids)->get(); + $journals = Auth::user()->transactionjournals()->whereIn('id', $ids)->get(); /** @var TransactionJournal $journal */ foreach ($journals as $journal) { $repository->scan($bill, $journal); @@ -168,6 +171,10 @@ class BillController extends Controller $bill = $repository->store($billData); Session::flash('success', 'Bill "' . e($bill->name) . '" stored.'); + if (intval(Input::get('create_another')) === 1) { + return Redirect::route('bills.create')->withInput(); + } + return Redirect::route('bills.index'); } @@ -195,6 +202,10 @@ class BillController extends Controller $bill = $repository->update($bill, $billData); + if (intval(Input::get('return_to_edit')) === 1) { + return Redirect::route('bills.edit', $bill->id); + } + Session::flash('success', 'Bill "' . e($bill->name) . '" updated.'); return Redirect::route('bills.index'); diff --git a/app/Http/Controllers/CategoryController.php b/app/Http/Controllers/CategoryController.php index 8bc05e7bc8..ff93171f9e 100644 --- a/app/Http/Controllers/CategoryController.php +++ b/app/Http/Controllers/CategoryController.php @@ -7,10 +7,10 @@ use FireflyIII\Http\Requests\CategoryFormRequest; use FireflyIII\Models\Category; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use Illuminate\Pagination\LengthAwarePaginator; +use Input; use Redirect; use Session; use View; -use Input; /** @@ -38,44 +38,6 @@ class CategoryController extends Controller return view('categories.create')->with('subTitle', 'Create a new category'); } - /** - * @param Category $category - * - * @return $this - */ - public function show(Category $category, CategoryRepositoryInterface $repository) - { - $hideCategory = true; // used in list. - $page = intval(Input::get('page')); - $offset = $page > 0 ? $page * 50 : 0; - $set = $category->transactionJournals()->withRelevantData()->take(50)->offset($offset)->orderBy('date', 'DESC')->get(['transaction_journals.*']); - $count = $category->transactionJournals()->count(); - - $journals = new LengthAwarePaginator($set, $count, 50, $page); - - return view('categories.show', compact('category', 'journals', 'hideCategory')); - } - - /** - * @return \Illuminate\View\View - */ - public function noCategory() - { - $start = Session::get('start', Carbon::now()->startOfMonth()); - $end = Session::get('end', Carbon::now()->startOfMonth()); - $list = Auth::user() - ->transactionjournals() - ->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') - ->whereNull('category_transaction_journal.id') - ->before($end) - ->after($start) - ->orderBy('transaction_journals.date') - ->get(['transaction_journals.*']); - $subTitle = 'Transactions without a category in ' . $start->format('F Y'); - - return view('categories.noCategory', compact('list', 'subTitle')); - } - /** * @param Category $category * @@ -122,11 +84,61 @@ class CategoryController extends Controller */ public function index() { - $categories = Auth::user()->categories()->get(); + $categories = Auth::user()->categories()->orderBy('name', 'ASC')->get(); + + $categories->each( + function (Category $category) { + $latest = $category->transactionjournals()->orderBy('date', 'DESC')->first(); + if ($latest) { + $category->lastActivity = $latest->date; + } + } + ); return view('categories.index', compact('categories')); } + /** + * @return \Illuminate\View\View + */ + public function noCategory() + { + $start = Session::get('start', Carbon::now()->startOfMonth()); + $end = Session::get('end', Carbon::now()->startOfMonth()); + $list = Auth::user() + ->transactionjournals() + ->leftJoin('category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') + ->whereNull('category_transaction_journal.id') + ->before($end) + ->after($start) + ->orderBy('transaction_journals.date') + ->get(['transaction_journals.*']); + + $subTitle = 'Transactions without a category between ' . $start->format('jS F Y') . ' and ' . $end->format('jS F Y'); + + return view('categories.noCategory', compact('list', 'subTitle')); + } + + /** + * @param Category $category + * + * @return $this + */ + public function show(Category $category, CategoryRepositoryInterface $repository) + { + $hideCategory = true; // used in list. + $page = intval(Input::get('page')); + $offset = $page > 0 ? $page * 50 : 0; + $set = $category->transactionJournals()->withRelevantData()->take(50)->offset($offset)->orderBy('date', 'DESC')->get( + ['transaction_journals.*'] + ); + $count = $category->transactionJournals()->count(); + + $journals = new LengthAwarePaginator($set, $count, 50, $page); + + return view('categories.show', compact('category', 'journals', 'hideCategory')); + } + /** * @param CategoryFormRequest $request * @param CategoryRepositoryInterface $repository diff --git a/app/Http/Controllers/CurrencyController.php b/app/Http/Controllers/CurrencyController.php index 318e2736d3..50e58ef418 100644 --- a/app/Http/Controllers/CurrencyController.php +++ b/app/Http/Controllers/CurrencyController.php @@ -1,14 +1,16 @@ name . '" created'); + if (intval(Input::get('create_another')) === 1) { + return Redirect::route('currency.create'); + } + return Redirect::route('currency.index'); @@ -163,7 +169,12 @@ class CurrencyController extends Controller $currency->name = $request->get('name'); $currency->save(); - Session::flash('success', 'Currency "' . e($currency->namename) . '" updated.'); + Session::flash('success', 'Currency "' . e($currency->name) . '" updated.'); + + + if (intval(Input::get('return_to_edit')) === 1) { + return Redirect::route('currency.edit', $currency->id); + } return Redirect::route('currency.index'); diff --git a/app/Http/Controllers/GoogleChartController.php b/app/Http/Controllers/GoogleChartController.php index 55e90b4979..ff0280b1e8 100644 --- a/app/Http/Controllers/GoogleChartController.php +++ b/app/Http/Controllers/GoogleChartController.php @@ -3,7 +3,7 @@ use App; use Auth; use Carbon\Carbon; -use Crypt; +use DB; use Exception; use FireflyIII\Helpers\Report\ReportQueryInterface; use FireflyIII\Http\Requests; @@ -13,17 +13,19 @@ use FireflyIII\Models\Budget; use FireflyIII\Models\Category; use FireflyIII\Models\LimitRepetition; use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\Preference; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use Grumpydictator\Gchart\GChart; use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\JoinClause; +use Illuminate\Support\Collection; use Navigation; use Preferences; use Response; use Session; -use DB; use Steam; /** @@ -41,27 +43,14 @@ class GoogleChartController extends Controller * * @return \Illuminate\Http\JsonResponse */ - public function accountBalanceChart(Account $account, $view = 'session', GChart $chart) + public function accountBalanceChart(Account $account, GChart $chart) { $chart->addColumn('Day of month', 'date'); $chart->addColumn('Balance for ' . $account->name, 'number'); $chart->addCertainty(1); - $start = Session::get('start', Carbon::now()->startOfMonth()); - $end = Session::get('end', Carbon::now()->endOfMonth()); - $count = $account->transactions()->count(); - - if ($view == 'all' && $count > 0) { - $first = $account->transactions()->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')->orderBy( - 'date', 'ASC' - )->first(['transaction_journals.date']); - $last = $account->transactions()->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')->orderBy( - 'date', 'DESC' - )->first(['transaction_journals.date']); - $start = new Carbon($first->date); - $end = new Carbon($last->date); - } - + $start = Session::get('start', Carbon::now()->startOfMonth()); + $end = Session::get('end', Carbon::now()->endOfMonth()); $current = clone $start; while ($end >= $current) { @@ -89,9 +78,9 @@ class GoogleChartController extends Controller $end = Session::get('end', Carbon::now()->endOfMonth()); if ($frontPage->data == []) { - $accounts = Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*']); + $accounts = Auth::user()->accounts()->orderBy('accounts.name', 'ASC')->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*']); } else { - $accounts = Auth::user()->accounts()->whereIn('id', $frontPage->data)->get(['accounts.*']); + $accounts = Auth::user()->accounts()->whereIn('id', $frontPage->data)->orderBy('accounts.name', 'ASC')->get(['accounts.*']); } $index = 1; /** @var Account $account */ @@ -177,27 +166,35 @@ class GoogleChartController extends Controller /** @var Budget $budget */ foreach ($budgets as $budget) { - /** @var \LimitRepetition $repetition */ - $repetition = LimitRepetition:: + /** @var Collection $repetitions */ + $repetitions = LimitRepetition:: leftJoin('budget_limits', 'limit_repetitions.budget_limit_id', '=', 'budget_limits.id') - ->where('limit_repetitions.startdate', $start->format('Y-m-d 00:00:00')) - ->where('budget_limits.budget_id', $budget->id) - ->first(['limit_repetitions.*']); - if (is_null($repetition)) { // use the session start and end for our search query - $searchStart = $start; - $searchEnd = $end; - $limit = 0; // the limit is zero: + ->where('limit_repetitions.startdate', '<=', $end->format('Y-m-d 00:00:00')) + ->where('limit_repetitions.startdate', '>=', $start->format('Y-m-d 00:00:00')) + ->where('budget_limits.budget_id', $budget->id) + ->get(['limit_repetitions.*']); + + // no results? search entire range for expenses and list those. + if ($repetitions->count() == 0) { + $expenses = floatval($budget->transactionjournals()->before($end)->after($start)->lessThan(0)->sum('amount')) * -1; + if ($expenses > 0) { + $chart->addRow($budget->name, 0, $expenses); + } } else { - // use the limit's start and end for our search query - $searchStart = $repetition->startdate; - $searchEnd = $repetition->enddate; - $limit = floatval($repetition->amount); // the limit is the repetitions limit: + // add with foreach: + /** @var LimitRepetition $repetition */ + foreach ($repetitions as $repetition) { + + $expenses + = + floatval($budget->transactionjournals()->before($repetition->enddate)->after($repetition->startdate)->lessThan(0)->sum('amount')) * -1; + if ($expenses > 0) { + $chart->addRow($budget->name . ' (' . $repetition->startdate->format('j M Y') . ')', floatval($repetition->amount), $expenses); + } + } } - $expenses = floatval($budget->transactionjournals()->before($searchEnd)->after($searchStart)->lessThan(0)->sum('amount')) * -1; - if ($expenses > 0) { - $chart->addRow($budget->name, $limit, $expenses); - } + } $noBudgetSet = Auth::user() @@ -209,7 +206,8 @@ class GoogleChartController extends Controller ->from('transaction_journals') ->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') ->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d 00:00:00')); + ->where('transaction_journals.date', '<=', $end->format('Y-m-d 00:00:00')) + ->whereNotNull('budget_transaction_journal.budget_id'); } ) ->before($end) @@ -315,49 +313,45 @@ class GoogleChartController extends Controller * * @return \Symfony\Component\HttpFoundation\Response */ - public function billsOverview(GChart $chart) + public function billsOverview(GChart $chart, BillRepositoryInterface $repository) { + $chart->addColumn('Name', 'string'); + $chart->addColumn('Amount', 'number'); + + $paid = ['items' => [], 'amount' => 0]; $unpaid = ['items' => [], 'amount' => 0]; $start = Session::get('start', Carbon::now()->startOfMonth()); $end = Session::get('end', Carbon::now()->endOfMonth()); - $chart->addColumn('Name', 'string'); - $chart->addColumn('Amount', 'number'); + $bills = Auth::user()->bills()->where('active', 1)->get(); - $set = Bill:: - leftJoin( - 'transaction_journals', function (JoinClause $join) use ($start, $end) { - $join->on('bills.id', '=', 'transaction_journals.bill_id') - ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) - ->where('transaction_journals.date', '<=', $end->format('Y-m-d')); - } - ) - ->leftJoin( - 'transactions', function (JoinClause $join) { - $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', '>', 0); - } - ) - ->where('active', 1) - ->groupBy('bills.id') - ->get( - ['bills.id', 'bills.name', 'transaction_journals.description', - 'transaction_journals.encrypted', - 'transaction_journals.id as journalId', - \DB::Raw('SUM(`bills`.`amount_min` + `bills`.`amount_max`) / 2 as `averageAmount`'), - 'transactions.amount AS actualAmount'] - ); + /** @var Bill $bill */ + foreach ($bills as $bill) { + $ranges = $repository->getRanges($bill, $start, $end); + + foreach ($ranges as $range) { + // paid a bill in this range? + $count = $bill->transactionjournals()->before($range['end'])->after($range['start'])->count(); + if ($count == 0) { + $unpaid['items'][] = $bill->name . ' (' . $range['start']->format('jS M Y') . ')'; + $unpaid['amount'] += ($bill->amount_max + $bill->amount_min / 2); + + } else { + $journal = $bill->transactionjournals()->with('transactions')->before($range['end'])->after($range['start'])->first(); + $paid['items'][] = $journal->description; + $amount = 0; + foreach ($journal->transactions as $t) { + if (floatval($t->amount) > 0) { + $amount = floatval($t->amount); + } + } + $paid['amount'] += $amount; + } - foreach ($set as $entry) { - if (intval($entry->journalId) == 0) { - $unpaid['items'][] = $entry->name; - $unpaid['amount'] += floatval($entry->averageAmount); - } else { - $description = intval($entry->encrypted) == 1 ? Crypt::decrypt($entry->description) : $entry->description; - $paid['items'][] = $description; - $paid['amount'] += floatval($entry->actualAmount); } } + $chart->addRow('Unpaid: ' . join(', ', $unpaid['items']), $unpaid['amount']); $chart->addRow('Paid: ' . join(', ', $paid['items']), $paid['amount']); $chart->generate(); @@ -463,36 +457,32 @@ class GoogleChartController extends Controller /** * - * @param Category $category - * @param $year + * @param Category $category * * @return \Illuminate\Http\JsonResponse */ - public function categoriesAndSpending(Category $category, $year, GChart $chart) + public function categoryOverviewChart(Category $category, GChart $chart) { - try { - new Carbon('01-01-' . $year); - } catch (Exception $e) { - return view('error')->with('message', 'Invalid year.'); - } + // oldest transaction in category: + /** @var TransactionJournal $first */ + $first = $category->transactionjournals()->orderBy('date', 'ASC')->first(); + $start = $first->date; + /** @var Preference $range */ + $range = Preferences::get('viewRange', '1M'); + // jump to start of week / month / year / etc (TODO). + $start = Navigation::startOfPeriod($start, $range->data); - $chart->addColumn('Month', 'date'); - $chart->addColumn('Budgeted', 'number'); + $chart->addColumn('Period', 'date'); $chart->addColumn('Spent', 'number'); - $start = new Carbon('01-01-' . $year); - $end = clone $start; - $end->endOfYear(); + $end = new Carbon; while ($start <= $end) { - $currentEnd = clone $start; - $currentEnd->endOfMonth(); - $spent = floatval($category->transactionjournals()->before($end)->after($start)->lessThan(0)->sum('amount')) * -1; - $budgeted = null; + $currentEnd = Navigation::endOfPeriod($start, $range->data); + $spent = floatval($category->transactionjournals()->before($currentEnd)->after($start)->lessThan(0)->sum('amount')) * -1; + $chart->addRow(clone $start, $spent); - $chart->addRow(clone $start, $budgeted, $spent); - - $start->addMonth(); + $start = Navigation::addPeriod($start, $range->data, 0); } @@ -503,6 +493,35 @@ class GoogleChartController extends Controller } + /** + * + * @param Category $category + * + * @return \Illuminate\Http\JsonResponse + */ + public function categoryPeriodChart(Category $category, GChart $chart) + { + // oldest transaction in category: + /** @var TransactionJournal $first */ + $start = Session::get('start'); + $chart->addColumn('Period', 'date'); + $chart->addColumn('Spent', 'number'); + + $end = Session::get('end'); + while ($start <= $end) { + $spent = floatval($category->transactionjournals()->onDate($start)->lessThan(0)->sum('amount')) * -1; + $chart->addRow(clone $start, $spent); + $start->addDay(); + } + + $chart->generate(); + + return Response::json($chart->getData()); + + + } + + /** * @param PiggyBank $piggyBank * @@ -542,6 +561,9 @@ class GoogleChartController extends Controller $chart->addColumn('Income', 'number'); $chart->addColumn('Expenses', 'number'); + $pref = Preferences::get('showSharedReports', false); + $showSharedReports = $pref->data; + // get report query interface. $end = clone $start; @@ -550,14 +572,14 @@ class GoogleChartController extends Controller $currentEnd = clone $start; $currentEnd->endOfMonth(); // total income: - $income = $query->incomeByPeriod($start, $currentEnd); + $income = $query->incomeByPeriod($start, $currentEnd, $showSharedReports); $incomeSum = 0; foreach ($income as $entry) { $incomeSum += floatval($entry->amount); } // total expenses: - $expense = $query->journalsByExpenseAccount($start, $currentEnd); + $expense = $query->journalsByExpenseAccount($start, $currentEnd, $showSharedReports); $expenseSum = 0; foreach ($expense as $entry) { $expenseSum += floatval($entry->amount); @@ -591,6 +613,9 @@ class GoogleChartController extends Controller $chart->addColumn('Income', 'number'); $chart->addColumn('Expenses', 'number'); + $pref = Preferences::get('showSharedReports', false); + $showSharedReports = $pref->data; + $income = 0; $expense = 0; $count = 0; @@ -601,14 +626,14 @@ class GoogleChartController extends Controller $currentEnd = clone $start; $currentEnd->endOfMonth(); // total income: - $incomeResult = $query->incomeByPeriod($start, $currentEnd); + $incomeResult = $query->incomeByPeriod($start, $currentEnd, $showSharedReports); $incomeSum = 0; foreach ($incomeResult as $entry) { $incomeSum += floatval($entry->amount); } // total expenses: - $expenseResult = $query->journalsByExpenseAccount($start, $currentEnd); + $expenseResult = $query->journalsByExpenseAccount($start, $currentEnd, $showSharedReports); $expenseSum = 0; foreach ($expenseResult as $entry) { $expenseSum += floatval($entry->amount); diff --git a/app/Http/Controllers/HelpController.php b/app/Http/Controllers/HelpController.php index 19457e4786..fe3194ea61 100644 --- a/app/Http/Controllers/HelpController.php +++ b/app/Http/Controllers/HelpController.php @@ -1,21 +1,19 @@ There is no help for this route.

'; } - $converter = new CommonMarkConverter(); + $converter = new CommonMarkConverter(); $content['text'] = $converter->convertToHtml($content['text']); return $content; diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 0d6056761f..a2926001fb 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -3,11 +3,11 @@ use Auth; use Cache; use Carbon\Carbon; -use Navigation; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use Input; use Preferences; use Redirect; use Session; -use URL; /** * Class HomeController @@ -24,6 +24,21 @@ class HomeController extends Controller { } + public function dateRange() + { + $start = new Carbon(Input::get('start')); + $end = new Carbon(Input::get('end')); + + $diff = $start->diffInDays($end); + + if ($diff > 50) { + Session::flash('warning', $diff . ' days of data may take a while to load.'); + } + + Session::put('start', $start); + Session::put('end', $end); + } + /** * @return \Illuminate\Http\RedirectResponse */ @@ -37,9 +52,10 @@ class HomeController extends Controller /** * @return \Illuminate\View\View */ - public function index() + public function index(AccountRepositoryInterface $repository) { - $count = Auth::user()->accounts()->accountTypeIn(['Asset account', 'Default account'])->count(); + + $count = $repository->countAssetAccounts(); $title = 'Firefly'; $subTitle = 'What\'s playing?'; $mainTitleIcon = 'fa-fire'; @@ -47,25 +63,10 @@ class HomeController extends Controller $frontPage = Preferences::get('frontPageAccounts', []); $start = Session::get('start', Carbon::now()->startOfMonth()); $end = Session::get('end', Carbon::now()->endOfMonth()); - - if ($frontPage->data == []) { - $accounts = Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*']); - } else { - $accounts = Auth::user()->accounts()->whereIn('id', $frontPage->data)->get(['accounts.*']); - } + $accounts = $repository->getFrontpageAccounts($frontPage); foreach ($accounts as $account) { - $set = Auth::user() - ->transactionjournals() - ->with(['transactions', 'transactioncurrency', 'transactiontype']) - ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') - ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')->where('accounts.id', $account->id) - ->where('date', '>=', $start->format('Y-m-d')) - ->where('date', '<=', $end->format('Y-m-d')) - ->orderBy('transaction_journals.date', 'DESC') - ->orderBy('transaction_journals.id', 'DESC') - ->take(10) - ->get(['transaction_journals.*']); + $set = $repository->getFrontpageTransactions($account, $start, $end); if (count($set) > 0) { $transactions[] = [$set, $account]; } @@ -76,49 +77,5 @@ class HomeController extends Controller return view('index', compact('count', 'title', 'subTitle', 'mainTitleIcon', 'transactions')); } - /** - * @param string $range - * - * @return mixed - */ - public function rangeJump($range) - { - - $valid = ['1D', '1W', '1M', '3M', '6M', '1Y',]; - - if (in_array($range, $valid)) { - Preferences::set('viewRange', $range); - Session::forget('range'); - } - - return Redirect::to(URL::previous()); - } - - /** - * @return \Illuminate\Http\RedirectResponse - */ - public function sessionNext() - { - $range = Session::get('range'); - $start = Session::get('start'); - - Session::put('start', Navigation::jumpToNext($range, clone $start)); - - return Redirect::to(URL::previous()); - - } - - /** - * @return \Illuminate\Http\RedirectResponse - */ - public function sessionPrev() - { - $range = Session::get('range'); - $start = Session::get('start'); - - Session::put('start', Navigation::jumpToPrevious($range, clone $start)); - - return Redirect::to(URL::previous()); - } } diff --git a/app/Http/Controllers/JsonController.php b/app/Http/Controllers/JsonController.php index f4583a826f..5f1bb43549 100644 --- a/app/Http/Controllers/JsonController.php +++ b/app/Http/Controllers/JsonController.php @@ -1,20 +1,113 @@ transactionjournals() + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->before($end) + ->after($start) + ->transactionTypes(['Deposit']) + ->where('transactions.amount', '>', 0) + ->first([DB::Raw('SUM(transactions.amount) as `amount`')]); + if (!is_null($in)) { + $amount = floatval($in->amount); + } + + break; + case 'out': + $box = Input::get('box'); + $in = Auth::user()->transactionjournals() + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->before($end) + ->after($start) + ->transactionTypes(['Withdrawal']) + ->where('transactions.amount', '>', 0) + ->first([DB::Raw('SUM(transactions.amount) as `amount`')]); + if (!is_null($in)) { + $amount = floatval($in->amount); + } + + break; + case 'bills-unpaid': + $box = 'bills-unpaid'; + $bills = Auth::user()->bills()->where('active', 1)->get(); + + /** @var Bill $bill */ + foreach ($bills as $bill) { + $ranges = $repository->getRanges($bill, $start, $end); + + foreach ($ranges as $range) { + // paid a bill in this range? + $count = $bill->transactionjournals()->before($range['end'])->after($range['start'])->count(); + if ($count == 0) { + $amount += floatval($bill->amount_max + $bill->amount_min / 2); + + } + + } + } + break; + case 'bills-paid': + $box = 'bills-paid'; + // these two functions are the same as the chart TODO + $bills = Auth::user()->bills()->where('active', 1)->get(); + + /** @var Bill $bill */ + foreach ($bills as $bill) { + $ranges = $repository->getRanges($bill, $start, $end); + + foreach ($ranges as $range) { + // paid a bill in this range? + $count = $bill->transactionjournals()->before($range['end'])->after($range['start'])->count(); + if ($count != 0) { + $journal = $bill->transactionjournals()->with('transactions')->before($range['end'])->after($range['start'])->first(); + $paid['items'][] = $journal->description; + $currentAmount = 0; + foreach ($journal->transactions as $t) { + if (floatval($t->amount) > 0) { + $currentAmount = floatval($t->amount); + } + } + $amount += $currentAmount; + } + + } + } + } + + return Response::json(['box' => $box, 'amount' => Amount::format($amount, false), 'amount_raw' => $amount]); + } + /** * Returns a list of categories. * @@ -22,15 +115,13 @@ class JsonController extends Controller { */ public function categories() { - $list = Auth::user()->categories()->orderBy('name','ASC')->get(); - $return = []; + $list = Auth::user()->categories()->orderBy('name', 'ASC')->get(); + $return = []; foreach ($list as $entry) { $return[] = $entry->name; } return Response::json($return); - - } /** @@ -40,8 +131,8 @@ class JsonController extends Controller { */ public function expenseAccounts() { - $list = Auth::user()->accounts()->accountTypeIn(['Expense account', 'Beneficiary account'])->get(); - $return = []; + $list = Auth::user()->accounts()->orderBy('accounts.name', 'ASC')->accountTypeIn(['Expense account', 'Beneficiary account'])->get(); + $return = []; foreach ($list as $entry) { $return[] = $entry->name; } @@ -55,8 +146,8 @@ class JsonController extends Controller { */ public function revenueAccounts() { - $list = Auth::user()->accounts()->accountTypeIn(['Revenue account'])->get(); - $return = []; + $list = Auth::user()->accounts()->accountTypeIn(['Revenue account'])->orderBy('accounts.name', 'ASC')->get(['accounts.*']); + $return = []; foreach ($list as $entry) { $return[] = $entry->name; } @@ -65,4 +156,27 @@ class JsonController extends Controller { } + /** + * @return \Symfony\Component\HttpFoundation\Response + */ + public function showSharedReports() + { + $pref = Preferences::get('showSharedReports', false); + + return Response::json(['value' => $pref->data]); + } + + /** + * @return \Symfony\Component\HttpFoundation\Response + */ + public function setSharedReports() + { + $pref = Preferences::get('showSharedReports', false); + $new = !$pref->data; + Preferences::set('showSharedReports', $new); + + + return Response::json(['value' => $new]); + } + } diff --git a/app/Http/Controllers/PiggyBankController.php b/app/Http/Controllers/PiggyBankController.php index 9e2a7ef9f5..010f36e2f9 100644 --- a/app/Http/Controllers/PiggyBankController.php +++ b/app/Http/Controllers/PiggyBankController.php @@ -9,6 +9,7 @@ use FireflyIII\Http\Requests; use FireflyIII\Http\Requests\PiggyBankFormRequest; use FireflyIII\Models\Account; use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\PiggyBankEvent; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use Illuminate\Support\Collection; @@ -62,7 +63,9 @@ class PiggyBankController extends Controller { $periods = Config::get('firefly.piggy_bank_periods'); - $accounts = ExpandedForm::makeSelectList(Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*'])); + $accounts = ExpandedForm::makeSelectList( + Auth::user()->accounts()->orderBy('accounts.name', 'ASC')->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*']) + ); $subTitle = 'Create new piggy bank'; $subTitleIcon = 'fa-plus'; @@ -78,7 +81,7 @@ class PiggyBankController extends Controller { $subTitle = 'Delete "' . e($piggyBank->name) . '"'; - return view('piggy_banks.delete', compact('piggyBank', 'subTitle')); + return view('piggy-banks.delete', compact('piggyBank', 'subTitle')); } /** @@ -90,9 +93,9 @@ class PiggyBankController extends Controller { Session::flash('success', 'Piggy bank "' . e($piggyBank->name) . '" deleted.'); - $this->_repository->destroy($piggyBank); + $piggyBank->delete(); - return Redirect::route('piggy_banks.index'); + return Redirect::route('piggy-banks.index'); } /** @@ -106,7 +109,9 @@ class PiggyBankController extends Controller { $periods = Config::get('firefly.piggy_bank_periods'); - $accounts = ExpandedForm::makeSelectList(Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*'])); + $accounts = ExpandedForm::makeSelectList( + Auth::user()->accounts()->orderBy('accounts.name', 'ASC')->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*']) + ); $subTitle = 'Edit piggy bank "' . e($piggyBank->name) . '"'; $subTitleIcon = 'fa-pencil'; @@ -124,7 +129,7 @@ class PiggyBankController extends Controller 'targetamount' => $piggyBank->targetamount, 'targetdate' => $targetDate, 'reminder' => $piggyBank->reminder, - 'remind_me' => intval($piggyBank->remind_me) == 1 || !is_null($piggyBank->reminder) ? true : false + 'remind_me' => intval($piggyBank->remind_me) == 1 && !is_null($piggyBank->reminder) ? true : false ]; Session::flash('preFilled', $preFilled); @@ -191,6 +196,9 @@ class PiggyBankController extends Controller $repetition->currentamount += $amount; $repetition->save(); + // create event. + PiggyBankEvent::create(['date' => Carbon::now(), 'amount' => $amount, 'piggy_bank_id' => $piggyBank->id]); + /* * Create event! */ @@ -220,6 +228,8 @@ class PiggyBankController extends Controller $repetition->currentamount -= $amount; $repetition->save(); + PiggyBankEvent::create(['date' => Carbon::now(), 'amount' => $amount * -1, 'piggy_bank_id' => $piggyBank->id]); + /* * Create event! */ @@ -278,12 +288,18 @@ class PiggyBankController extends Controller 'targetamount' => floatval($request->get('targetamount')), 'targetdate' => strlen($request->get('targetdate')) > 0 ? new Carbon($request->get('targetdate')) : null, 'reminder' => $request->get('reminder'), + 'remind_me' => $request->get('remind_me'), ]; $piggyBank = $repository->store($piggyBankData); Session::flash('success', 'Stored piggy bank "' . e($piggyBank->name) . '".'); + if (intval(Input::get('create_another')) === 1) { + return Redirect::route('piggy-banks.create')->withInput(); + } + + return Redirect::route('piggy-banks.index'); } @@ -299,21 +315,28 @@ class PiggyBankController extends Controller $piggyBankData = [ 'repeats' => false, 'name' => $request->get('name'), + 'startdate' => is_null($piggyBank->startdate) ? $piggyBank->created_at : $piggyBank->startdate, 'account_id' => intval($request->get('account_id')), 'targetamount' => floatval($request->get('targetamount')), 'targetdate' => strlen($request->get('targetdate')) > 0 ? new Carbon($request->get('targetdate')) : null, 'reminder' => $request->get('reminder'), + 'remind_me' => $request->get('remind_me') ]; + $piggyBank = $repository->update($piggyBank, $piggyBankData); Session::flash('success', 'Updated piggy bank "' . e($piggyBank->name) . '".'); + if (intval(Input::get('return_to_edit')) === 1) { + return Redirect::route('piggy-banks.edit', $piggyBank->id); + } + + return Redirect::route('piggy-banks.index'); } - } diff --git a/app/Http/Controllers/PreferencesController.php b/app/Http/Controllers/PreferencesController.php index c8c1379149..b21e81c0bc 100644 --- a/app/Http/Controllers/PreferencesController.php +++ b/app/Http/Controllers/PreferencesController.php @@ -1,21 +1,20 @@ accounts()->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*']); + $accounts = Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->orderBy('accounts.name', 'ASC')->get(['accounts.*']); $viewRange = Preferences::get('viewRange', '1M'); $viewRangeValue = $viewRange->data; $frontPage = Preferences::get('frontPageAccounts', []); diff --git a/app/Http/Controllers/RelatedController.php b/app/Http/Controllers/RelatedController.php index 9ea88a1352..6e5102bf52 100644 --- a/app/Http/Controllers/RelatedController.php +++ b/app/Http/Controllers/RelatedController.php @@ -1,15 +1,16 @@ 0) { - $set = Auth::user()->transactionjournals()->whereIn('id', $unique)->get(); - $set->each( + $journals = Auth::user()->transactionjournals()->whereIn('id', $unique)->get(); + $journals->each( function (TransactionJournal $journal) { /** @var Transaction $t */ foreach ($journal->transactions()->get() as $t) { @@ -52,11 +54,38 @@ class RelatedController extends Controller } ); - - return Response::json($set->toArray()); - } else { - return Response::json((new Collection)->toArray()); } + $parent = $journal; + + return view('related.alreadyRelated', compact('parent', 'journals')); + } + + /** + * @SuppressWarnings("CyclomaticComplexity") // It's exactly 5. So I don't mind. + * + * @param TransactionJournal $parentJournal + * @param TransactionJournal $childJournal + * + * @return \Illuminate\Http\JsonResponse + * @throws Exception + */ + public function getRemoveRelation(TransactionJournal $parentJournal, TransactionJournal $childJournal) + { + $groups = $parentJournal->transactiongroups()->get(); + /** @var TransactionGroup $group */ + foreach ($groups as $group) { + foreach ($group->transactionjournals()->get() as $loopJournal) { + if ($loopJournal->id == $childJournal->id) { + // remove from group: + $group->transactionjournals()->detach($childJournal); + } + } + if ($group->transactionjournals()->count() == 1) { + $group->delete(); + } + } + + return Redirect::to(URL::previous()); } /** @@ -137,20 +166,12 @@ class RelatedController extends Controller { $search = e(trim(Input::get('searchValue'))); + $parent = $journal; - $result = $repository->searchRelated($search, $journal); - $result->each( - function (TransactionJournal $journal) { - /** @var Transaction $t */ - foreach ($journal->transactions()->get() as $t) { - if ($t->amount > 0) { - $journal->amount = $t->amount; - } - } - } - ); + $journals = $repository->searchRelated($search, $journal); + + return view('related.searchResult', compact('journals', 'search', 'parent')); - return Response::json($result->toArray()); } } diff --git a/app/Http/Controllers/ReminderController.php b/app/Http/Controllers/ReminderController.php new file mode 100644 index 0000000000..379274a3af --- /dev/null +++ b/app/Http/Controllers/ReminderController.php @@ -0,0 +1,128 @@ + 'Money for piggy bank "' . $reminder->remindersable->name . '"', + 'amount' => round($reminder->metadata->perReminder, 2), + 'account_to_id' => $reminder->remindersable->account_id, + 'piggy_bank_id' => $reminder->remindersable_id, + 'reminder_id' => $reminder->id, + ]; + Session::flash('_old_input', $data); + + return Redirect::route('transactions.create', 'transfer'); + } + + /** + * @param Reminder $reminder + */ + public function dismiss(Reminder $reminder) + { + $reminder->notnow = true; + $reminder->save(); + + return Redirect::to(URL::previous()); + + + } + + /** + * + */ + public function index(ReminderHelperInterface $helper) + { + + $reminders = Auth::user()->reminders()->get(); + + $reminders->each( + function (Reminder $reminder) use ($helper) { + $reminder->description = $helper->getReminderText($reminder); + } + ); + + $today = new Carbon; + // active reminders: + $active = $reminders->filter( + function (Reminder $reminder) use ($today) { + if ($reminder->notnow === false && $reminder->active === true && $reminder->startdate <= $today && $reminder->enddate >= $today) { + return $reminder; + } + } + ); + + // expired reminders: + $expired = $reminders->filter( + function (Reminder $reminder) use ($today) { + if ($reminder->notnow === false && $reminder->active === true && $reminder->startdate > $today || $reminder->enddate < $today) { + return $reminder; + } + } + ); + + // inactive reminders + $inactive = $reminders->filter( + function (Reminder $reminder) use ($today) { + if ($reminder->active === false) { + return $reminder; + } + } + ); + + // dismissed reminders + $dismissed = $reminders->filter( + function (Reminder $reminder) use ($today) { + if ($reminder->notnow === true) { + return $reminder; + } + } + ); + + $title = 'Reminders'; + $mainTitleIcon = 'fa-clock-o'; + + return view('reminders.index', compact('dismissed', 'expired', 'inactive', 'active', 'title', 'mainTitleIcon')); + } + + /** + * @param Reminder $reminder + */ + public function show(Reminder $reminder) + { + $title = 'Reminder'; + $mainTitleIcon = 'fa-clock-o'; + if ($reminder->notnow === true) { + $subTitle = 'Dismissed reminder'; + } else { + $subTitle = 'Reminder'; + } + $subTitle .= ' for piggy bank "' . $reminder->remindersable->name . '"'; + + + return view('reminders.show', compact('reminder', 'title', 'subTitle', 'mainTitleIcon')); + + + } + +} diff --git a/app/Http/Controllers/RepeatedExpenseController.php b/app/Http/Controllers/RepeatedExpenseController.php index 8569b067a9..64f2e63c45 100644 --- a/app/Http/Controllers/RepeatedExpenseController.php +++ b/app/Http/Controllers/RepeatedExpenseController.php @@ -36,7 +36,9 @@ class RepeatedExpenseController extends Controller public function create() { $periods = Config::get('firefly.piggy_bank_periods'); - $accounts = ExpandedForm::makeSelectList(Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*'])); + $accounts = ExpandedForm::makeSelectList( + Auth::user()->accounts()->orderBy('accounts.name', 'ASC')->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*']) + ); return view('repeatedExpense.create', compact('accounts', 'periods'))->with('subTitle', 'Create new repeated expense')->with( 'subTitleIcon', 'fa-plus' @@ -79,7 +81,9 @@ class RepeatedExpenseController extends Controller { $periods = Config::get('firefly.piggy_bank_periods'); - $accounts = ExpandedForm::makeSelectList(Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*'])); + $accounts = ExpandedForm::makeSelectList( + Auth::user()->accounts()->orderBy('accounts.name', 'ASC')->accountTypeIn(['Default account', 'Asset account'])->get(['accounts.*']) + ); $subTitle = 'Edit repeated expense "' . e($repeatedExpense->name) . '"'; $subTitleIcon = 'fa-pencil'; @@ -183,7 +187,7 @@ class RepeatedExpenseController extends Controller 'rep_length' => $request->get('rep_length'), 'rep_every' => intval($request->get('rep_every')), 'rep_times' => intval($request->get('rep_times')), - 'remind_me' => intval($request->get('remind_me')) == 1 ? true : false , + 'remind_me' => intval($request->get('remind_me')) == 1 ? true : false, 'reminder' => $request->get('reminder'), ]; diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 5349059c90..7c239116b2 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -7,7 +7,10 @@ use FireflyIII\Helpers\Report\ReportHelperInterface; use FireflyIII\Helpers\Report\ReportQueryInterface; use FireflyIII\Http\Requests; use FireflyIII\Models\Account; +use FireflyIII\Models\TransactionJournal; use Illuminate\Database\Query\JoinClause; +use Preferences; +use Session; use Steam; use View; @@ -49,17 +52,22 @@ class ReportController extends Controller $end->endOfMonth(); $start->subDay(); + // shared accounts preference: + $pref = Preferences::get('showSharedReports', false); + $showSharedReports = $pref->data; + + $dayEarly = clone $date; $subTitle = 'Budget report for ' . $date->format('F Y'); $subTitleIcon = 'fa-calendar'; $dayEarly = $dayEarly->subDay(); - $accounts = $query->getAllAccounts($start, $end); + $accounts = $query->getAllAccounts($start, $end, $showSharedReports); $start->addDay(); $accounts->each( function (Account $account) use ($start, $end, $query) { $budgets = $query->getBudgetSummary($account, $start, $end); - $balancedAmount = $query->balancedTransactionsList($account, $start, $end); + $balancedAmount = $query->balancedTransactionsSum($account, $start, $end); $array = []; foreach ($budgets as $budget) { $id = intval($budget->id); @@ -87,24 +95,27 @@ class ReportController extends Controller ) ->get(['budgets.*', 'budget_limits.amount as amount']); $budgets = Steam::makeArray($set); - $amountSet = $query->journalsByBudget($start, $end); + $amountSet = $query->journalsByBudget($start, $end, $showSharedReports); $amounts = Steam::makeArray($amountSet); $budgets = Steam::mergeArrays($budgets, $amounts); $budgets[0]['spent'] = isset($budgets[0]['spent']) ? $budgets[0]['spent'] : 0.0; $budgets[0]['amount'] = isset($budgets[0]['amount']) ? $budgets[0]['amount'] : 0.0; $budgets[0]['name'] = 'No budget'; - // find transactions to shared expense accounts, which are without a budget by default: - $transfers = $query->sharedExpenses($start, $end); - foreach ($transfers as $transfer) { - $budgets[0]['spent'] += floatval($transfer->amount) * -1; + // find transactions to shared asset accounts, which are without a budget by default: + // which is only relevant when shared asset accounts are hidden. + if ($showSharedReports === false) { + $transfers = $query->sharedExpenses($start, $end); + foreach ($transfers as $transfer) { + $budgets[0]['spent'] += floatval($transfer->amount) * -1; + } } /** * End getBudgetsForMonth DONE */ - return view('reports.budget', compact('subTitle', 'subTitleIcon', 'date', 'accounts', 'budgets', 'dayEarly')); + return view('reports.budget', compact('subTitle', 'year', 'month', 'subTitleIcon', 'date', 'accounts', 'budgets', 'dayEarly')); } @@ -115,7 +126,7 @@ class ReportController extends Controller */ public function index(ReportHelperInterface $helper) { - $start = $helper->firstDate(); + $start = Session::get('first'); $months = $helper->listOfMonths($start); $years = $helper->listOfYears($start); $title = 'Reports'; @@ -124,6 +135,87 @@ class ReportController extends Controller return view('reports.index', compact('years', 'months', 'title', 'mainTitleIcon')); } + /** + * @param Account $account + * @param string $year + * @param string $month + * + * @return \Illuminate\View\View + */ + public function modalBalancedTransfers(Account $account, $year = '2014', $month = '1', ReportQueryInterface $query) + { + + try { + new Carbon($year . '-' . $month . '-01'); + } catch (Exception $e) { + return view('error')->with('message', 'Invalid date'); + } + $start = new Carbon($year . '-' . $month . '-01'); + $end = clone $start; + $end->endOfMonth(); + + $journals = $query->balancedTransactionsList($account, $start, $end); + + return view('reports.modal-journal-list', compact('journals')); + + + } + + /** + * @param Account $account + * @param string $year + * @param string $month + * @param ReportQueryInterface $query + * + * @return View + */ + public function modalLeftUnbalanced(Account $account, $year = '2014', $month = '1', ReportQueryInterface $query) + { + try { + new Carbon($year . '-' . $month . '-01'); + } catch (Exception $e) { + return view('error')->with('message', 'Invalid date'); + } + $start = new Carbon($year . '-' . $month . '-01'); + $end = clone $start; + $end->endOfMonth(); + $set = $query->getTransactionsWithoutBudget($account, $start, $end); + + $journals = $set->filter( + function (TransactionJournal $journal) { + $count = $journal->transactiongroups()->where('relation', 'balance')->count(); + if ($count == 0) { + return $journal; + } + } + ); + + return view('reports.modal-journal-list', compact('journals')); + } + + /** + * @param Account $account + * @param string $year + * @param string $month + * + * @return \Illuminate\View\View + */ + public function modalNoBudget(Account $account, $year = '2014', $month = '1', ReportQueryInterface $query) + { + try { + new Carbon($year . '-' . $month . '-01'); + } catch (Exception $e) { + return view('error')->with('message', 'Invalid date'); + } + $start = new Carbon($year . '-' . $month . '-01'); + $end = clone $start; + $end->endOfMonth(); + $journals = $query->getTransactionsWithoutBudget($account, $start, $end); + + return view('reports.modal-journal-list', compact('journals')); + + } + /** * @param string $year * @param string $month @@ -142,6 +234,9 @@ class ReportController extends Controller $subTitleIcon = 'fa-calendar'; $displaySum = true; // to show sums in report. + $pref = Preferences::get('showSharedReports', false); + $showSharedReports = $pref->data; + /** * @@ -157,14 +252,14 @@ class ReportController extends Controller /** * Start getIncomeForMonth DONE */ - $income = $query->incomeByPeriod($start, $end); + $income = $query->incomeByPeriod($start, $end, $showSharedReports); /** * End getIncomeForMonth DONE */ /** * Start getExpenseGroupedForMonth DONE */ - $set = $query->journalsByExpenseAccount($start, $end); + $set = $query->journalsByExpenseAccount($start, $end, $showSharedReports); $expenses = Steam::makeArray($set); $expenses = Steam::sortArray($expenses); $expenses = Steam::limitArray($expenses, 10); @@ -182,7 +277,7 @@ class ReportController extends Controller ) ->get(['budgets.*', 'budget_limits.amount as amount']); $budgets = Steam::makeArray($set); - $amountSet = $query->journalsByBudget($start, $end); + $amountSet = $query->journalsByBudget($start, $end, $showSharedReports); $amounts = Steam::makeArray($amountSet); $budgets = Steam::mergeArrays($budgets, $amounts); $budgets[0]['spent'] = isset($budgets[0]['spent']) ? $budgets[0]['spent'] : 0.0; @@ -190,9 +285,11 @@ class ReportController extends Controller $budgets[0]['name'] = 'No budget'; // find transactions to shared expense accounts, which are without a budget by default: - $transfers = $query->sharedExpenses($start, $end); - foreach ($transfers as $transfer) { - $budgets[0]['spent'] += floatval($transfer->amount) * -1; + if ($showSharedReports === false) { + $transfers = $query->sharedExpenses($start, $end); + foreach ($transfers as $transfer) { + $budgets[0]['spent'] += floatval($transfer->amount) * -1; + } } /** @@ -206,9 +303,13 @@ class ReportController extends Controller $categories = Steam::makeArray($result); // all transfers - $result = $query->sharedExpensesByCategory($start, $end); - $transfers = Steam::makeArray($result); - $merged = Steam::mergeArrays($categories, $transfers); + if ($showSharedReports === false) { + $result = $query->sharedExpensesByCategory($start, $end); + $transfers = Steam::makeArray($result); + $merged = Steam::mergeArrays($categories, $transfers); + } else { + $merged = $categories; + } // sort. $sorted = Steam::sortNegativeArray($merged); @@ -221,7 +322,7 @@ class ReportController extends Controller /** * Start getAccountsForMonth */ - $list = $query->accountList(); + $list = $query->accountList($showSharedReports); $accounts = []; /** @var Account $account */ foreach ($list as $account) { @@ -262,16 +363,19 @@ class ReportController extends Controller } catch (Exception $e) { return view('error')->with('message', 'Invalid date.'); } - $date = new Carbon('01-01-' . $year); - $end = clone $date; + + $pref = Preferences::get('showSharedReports', false); + $showSharedReports = $pref->data; + $date = new Carbon('01-01-' . $year); + $end = clone $date; $end->endOfYear(); $title = 'Reports'; $subTitle = $year; $subTitleIcon = 'fa-bar-chart'; $mainTitleIcon = 'fa-line-chart'; - $balances = $helper->yearBalanceReport($date); - $groupedIncomes = $query->journalsByRevenueAccount($date, $end); - $groupedExpenses = $query->journalsByExpenseAccount($date, $end); + $balances = $helper->yearBalanceReport($date, $showSharedReports); + $groupedIncomes = $query->journalsByRevenueAccount($date, $end, $showSharedReports); + $groupedExpenses = $query->journalsByExpenseAccount($date, $end, $showSharedReports); //$groupedExpenses = $helper-> expensesGroupedByAccount($date, $end, 15); diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index aa56c5e993..8c54a3170e 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -1,10 +1,7 @@ accounts()->accountTypeIn(['Default account', 'Asset account'])->where('active', 1)->orderBy('name', 'DESC')->get(['accounts.*']) + Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->orderBy('accounts.name', 'ASC')->orderBy('name', 'ASC')->where( + 'active', 1 + )->orderBy('name', 'DESC')->get(['accounts.*']) ); $budgets = ExpandedForm::makeSelectList(Auth::user()->budgets()->get()); $budgets[0] = '(no budget)'; @@ -118,7 +121,9 @@ class TransactionController extends Controller { $what = strtolower($journal->transactiontype->type); $accounts = ExpandedForm::makeSelectList( - Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->where('active', 1)->orderBy('name', 'DESC')->get(['accounts.*']) + Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->orderBy('accounts.name', 'ASC')->where('active', 1)->orderBy( + 'name', 'DESC' + )->get(['accounts.*']) ); $budgets = ExpandedForm::makeSelectList(Auth::user()->budgets()->get()); $budgets[0] = '(no budget)'; @@ -143,7 +148,7 @@ class TransactionController extends Controller } if ($journal->piggyBankEvents()->count() > 0) { - $preFilled['piggy_bank_id'] = $journal->piggyBankEvents()->first()->piggy_bank_id; + $preFilled['piggy_bank_id'] = $journal->piggyBankEvents()->orderBy('date', 'DESC')->first()->piggy_bank_id; } $preFilled['amount'] = 0; @@ -227,25 +232,15 @@ class TransactionController extends Controller $t->after = $t->before + $t->amount; } ); - $members = new Collection; - /** @var TransactionGroup $group */ - foreach ($journal->transactiongroups()->get() as $group) { - /** @var TransactionJournal $loopJournal */ - foreach ($group->transactionjournals()->get() as $loopJournal) { - if ($loopJournal->id != $journal->id) { - $members->push($loopJournal); - } - } - } - return view('transactions.show', compact('journal', 'members'))->with( + + return view('transactions.show', compact('journal'))->with( 'subTitle', e($journal->transactiontype->type) . ' "' . e($journal->description) . '"' ); } public function store(JournalFormRequest $request, JournalRepositoryInterface $repository) { - $journalData = [ 'what' => $request->get('what'), 'description' => $request->get('description'), @@ -264,8 +259,21 @@ class TransactionController extends Controller $journal = $repository->store($journalData); + event(new JournalSaved($journal)); + event(new JournalCreated($journal, intval($request->get('piggy_bank_id')))); + + if (intval($request->get('reminder_id')) > 0) { + $reminder = Auth::user()->reminders()->find($request->get('reminder_id')); + $reminder->active = 0; + $reminder->save(); + } + Session::flash('success', 'New transaction "' . $journal->description . '" stored!'); + if (intval(Input::get('create_another')) === 1) { + return Redirect::route('transactions.create', $request->input('what'))->withInput(); + } + return Redirect::route('transactions.index', $request->input('what')); } @@ -281,6 +289,7 @@ class TransactionController extends Controller public function update(TransactionJournal $journal, JournalFormRequest $request, JournalRepositoryInterface $repository) { + $journalData = [ 'what' => $request->get('what'), 'description' => $request->get('description'), @@ -298,8 +307,16 @@ class TransactionController extends Controller ]; $repository->update($journal, $journalData); + + event(new JournalSaved($journal)); + // update, get events by date and sort DESC + Session::flash('success', 'Transaction "' . e($journalData['description']) . '" updated.'); + if (intval(Input::get('return_to_edit')) === 1) { + return Redirect::route('transactions.edit', $journal->id); + } + return Redirect::route('transactions.index', $journalData['what']); } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 67ce5757e4..970e795492 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -36,6 +36,7 @@ class Kernel extends HttpKernel 'auth.basic' => 'Illuminate\Auth\Middleware\AuthenticateWithBasicAuth', 'guest' => 'FireflyIII\Http\Middleware\RedirectIfAuthenticated', 'range' => 'FireflyIII\Http\Middleware\Range', + 'reminders' => 'FireflyIII\Http\Middleware\Reminders', ]; } diff --git a/app/Http/Middleware/Range.php b/app/Http/Middleware/Range.php index 49c3708b93..b37db6b6b4 100644 --- a/app/Http/Middleware/Range.php +++ b/app/Http/Middleware/Range.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Auth\Guard; use Navigation; use Preferences; use Session; +use View; /** * Class SessionFilter @@ -46,25 +47,36 @@ class Range public function handle($request, Closure $theNext) { if ($this->auth->check()) { - // user's view range comes from preferences, gets set in session: - /** @var \FireflyIII\Models\Preference $viewRange */ - $viewRange = Preferences::get('viewRange', '1M'); + // ignore preference. set the range to be the current month: + if (!Session::has('start') && !Session::has('end')) { - // the start and end date are checked and stored: - $start = Session::has('start') ? Session::get('start') : new Carbon; - $start = Navigation::updateStartDate($viewRange->data, $start); - $end = Navigation::updateEndDate($viewRange->data, $start); - $period = Navigation::periodName($viewRange->data, $start); - $prev = Navigation::jumpToPrevious($viewRange->data, clone $start); - $next = Navigation::jumpToNext($viewRange->data, clone $start); + /** @var \FireflyIII\Models\Preference $viewRange */ + $viewRange = Preferences::get('viewRange', '1M'); + $start = Session::has('start') ? Session::get('start') : new Carbon; + $start = Navigation::updateStartDate($viewRange->data, $start); + $end = Navigation::updateEndDate($viewRange->data, $start); + + Session::put('start', $start); + Session::put('end', $end); + } + if (!Session::has('first')) { + $journal = $this->auth->user()->transactionjournals()->orderBy('date', 'ASC')->first(['transaction_journals.*']); + if ($journal) { + Session::put('first', $journal->date); + } else { + Session::put('first', Carbon::now()); + } + } + + // set current / next / prev month. + $current = Carbon::now()->format('F Y'); + $next = Carbon::now()->endOfMonth()->addDay()->format('F Y'); + $prev = Carbon::now()->startOfMonth()->subDay()->format('F Y'); + View::share('currentMonthName', $current); + View::share('previousMonthName', $prev); + View::share('nextMonthName', $next); - Session::put('range', $viewRange->data); - Session::put('start', $start); - Session::put('end', $end); - Session::put('period', $period); - Session::put('prev', Navigation::periodName($viewRange->data, $prev)); - Session::put('next', Navigation::periodName($viewRange->data, $next)); } diff --git a/app/Http/Middleware/Reminders.php b/app/Http/Middleware/Reminders.php new file mode 100644 index 0000000000..4310bdfac9 --- /dev/null +++ b/app/Http/Middleware/Reminders.php @@ -0,0 +1,91 @@ +auth = $auth; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * + * @return mixed + */ + public function handle($request, Closure $next) + { + if ($this->auth->check()) { + // do reminders stuff. + $piggyBanks = $this->auth->user()->piggyBanks()->where('remind_me', 1)->get(); + $today = new Carbon; + /** @var \FireflyIII\Helpers\Reminders\ReminderHelperInterface $helper */ + $helper = App::make('FireflyIII\Helpers\Reminders\ReminderHelperInterface'); + + /** @var PiggyBank $piggyBank */ + foreach ($piggyBanks as $piggyBank) { + $ranges = $helper->getReminderRanges($piggyBank); + + foreach ($ranges as $range) { + if ($today < $range['end'] && $today > $range['start']) { + // create a reminder here! + $helper->createReminder($piggyBank, $range['start'], $range['end']); + // stop looping, we're done. + break; + } + + } + } + // delete invalid reminders + $reminders = $this->auth->user()->reminders()->get(); + foreach($reminders as $reminder) { + if(is_null($reminder->remindersable)) { + $reminder->delete(); + } + } + + + + // get and list active reminders: + $reminders = $this->auth->user()->reminders()->today()->get(); + $reminders->each( + function (Reminder $reminder) use ($helper) { + $reminder->description = $helper->getReminderText($reminder); + } + ); + View::share('reminders', $reminders); + } + + return $next($request); + } +} \ No newline at end of file diff --git a/app/Http/Requests/BillFormRequest.php b/app/Http/Requests/BillFormRequest.php index 2c9452cccb..2c1cfdc45a 100644 --- a/app/Http/Requests/BillFormRequest.php +++ b/app/Http/Requests/BillFormRequest.php @@ -1,10 +1,4 @@ 0) { - $nameRule .= ','.intval(Input::get('id')); + if (intval(Input::get('id')) > 0) { + $nameRule = 'required|between:1,255'; } $rules = [ diff --git a/app/Http/Requests/CurrencyFormRequest.php b/app/Http/Requests/CurrencyFormRequest.php index 75e0f7dd6f..48fe2146d3 100644 --- a/app/Http/Requests/CurrencyFormRequest.php +++ b/app/Http/Requests/CurrencyFormRequest.php @@ -1,10 +1,4 @@ 'required|in:withdrawal,deposit,transfer|exists:transaction_types,type', 'amount' => 'numeric|required|min:0.01', 'date' => 'required|date', + 'reminder_id' => 'numeric|exists:reminders,id', 'amount_currency_id' => 'required|exists:transaction_currencies,id', ]; diff --git a/app/Http/Requests/PiggyBankFormRequest.php b/app/Http/Requests/PiggyBankFormRequest.php index 74fc66efdb..6917c117d9 100644 --- a/app/Http/Requests/PiggyBankFormRequest.php +++ b/app/Http/Requests/PiggyBankFormRequest.php @@ -51,7 +51,7 @@ class PiggyBankFormRequest extends Request 'rep_times' => 'integer|min:0|max:99', 'reminder' => 'in:day,week,quarter,month,year', 'reminder_skip' => 'integer|min:0|max:99', - 'remind_me' => 'boolean', + 'remind_me' => 'boolean|piggyBankReminder', 'order' => 'integer|min:1', ]; diff --git a/app/Http/breadcrumbs.php b/app/Http/breadcrumbs.php index e4f73da2e3..1c167e7dd4 100644 --- a/app/Http/breadcrumbs.php +++ b/app/Http/breadcrumbs.php @@ -7,6 +7,7 @@ use FireflyIII\Models\Budget; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\Bill; use FireflyIII\Models\Category; +use FireflyIII\Models\Reminder; use FireflyIII\Models\LimitRepetition; use FireflyIII\Models\PiggyBank; @@ -239,8 +240,17 @@ Breadcrumbs::register( // reminders Breadcrumbs::register( - 'reminders.show', function (Generator $breadcrumbs, Reminder $reminder) { + 'reminders.index', function (Generator $breadcrumbs) { $breadcrumbs->parent('home'); + $breadcrumbs->push('Reminders', route('reminders.index')); + +} +); + +// reminders +Breadcrumbs::register( + 'reminders.show', function (Generator $breadcrumbs, Reminder $reminder) { + $breadcrumbs->parent('reminders.index'); $breadcrumbs->push('Reminder #' . $reminder->id, route('reminders.show', $reminder->id)); } diff --git a/app/Http/routes.php b/app/Http/routes.php index 84ee9dd19c..928e3a54ec 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -4,9 +4,10 @@ use FireflyIII\Models\Bill; use FireflyIII\Models\Budget; use FireflyIII\Models\Category; use FireflyIII\Models\LimitRepetition; +use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\Reminder; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; -use FireflyIII\Models\PiggyBank; // models @@ -89,6 +90,16 @@ Route::bind( } ); +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( 'limitrepetition', function ($value, $route) { if (Auth::check()) { @@ -135,7 +146,7 @@ Route::get('/register', ['uses' => 'Auth\AuthController@getRegister', 'as' => 'r Route::controllers( [ - 'auth' => 'Auth\AuthController', + 'auth' => 'Auth\AuthController', 'password' => 'Auth\PasswordController', ] ); @@ -145,12 +156,10 @@ Route::controllers( * Home Controller */ Route::group( - ['middleware' => ['auth', 'range']], function () { + ['middleware' => ['auth', 'range', 'reminders']], function () { Route::get('/', ['uses' => 'HomeController@index', 'as' => 'index']); Route::get('/home', ['uses' => 'HomeController@index', 'as' => 'home']); - 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::post('/daterange', ['uses' => 'HomeController@dateRange', 'as' => 'daterange']); Route::get('/flush', ['uses' => 'HomeController@flush', 'as' => 'flush']); /** * Account Controller @@ -232,11 +241,12 @@ Route::group( Route::get('/chart/budget/{budget}/spending/{year?}', ['uses' => 'GoogleChartController@budgetsAndSpending']); Route::get('/chart/budgets/spending/{year?}', ['uses' => 'GoogleChartController@allBudgetsAndSpending']); Route::get('/chart/budget/{budget}/{limitrepetition}', ['uses' => 'GoogleChartController@budgetLimitSpending']); - Route::get('/chart/category/{category}/spending/{year}', ['uses' => 'GoogleChartController@categoriesAndSpending']); Route::get('/chart/reports/income-expenses/{year}', ['uses' => 'GoogleChartController@yearInExp']); Route::get('/chart/reports/income-expenses-sum/{year}', ['uses' => 'GoogleChartController@yearInExpSum']); Route::get('/chart/bills/{bill}', ['uses' => 'GoogleChartController@billOverview']); Route::get('/chart/piggy-history/{piggyBank}', ['uses' => 'GoogleChartController@piggyBankHistory']); + Route::get('/chart/category/{category}/period', ['uses' => 'GoogleChartController@categoryPeriodChart']); + Route::get('/chart/category/{category}/overview', ['uses' => 'GoogleChartController@categoryOverviewChart']); /** * Help Controller @@ -249,14 +259,17 @@ Route::group( Route::get('/json/expense-accounts', ['uses' => 'JsonController@expenseAccounts', 'as' => 'json.expense-accounts']); Route::get('/json/revenue-accounts', ['uses' => 'JsonController@revenueAccounts', 'as' => 'json.revenue-accounts']); Route::get('/json/categories', ['uses' => 'JsonController@categories', 'as' => 'json.categories']); + Route::get('/json/box', ['uses' => 'JsonController@box', 'as' => 'json.box']); + Route::get('/json/show-shared-reports', 'JsonController@showSharedReports'); + Route::get('/json/show-shared-reports/set', 'JsonController@setSharedReports'); /** * Piggy Bank Controller */ Route::get('/piggy-banks', ['uses' => 'PiggyBankController@index', 'as' => 'piggy-banks.index']); - Route::get('/piggy-banks/add/{piggyBank}', ['uses' => 'PiggyBankController@add']); # add money - Route::get('/piggy-banks/remove/{piggyBank}', ['uses' => 'PiggyBankController@remove']); #remove money + Route::get('/piggy-banks/add/{piggyBank}', ['uses' => 'PiggyBankController@add', 'as' => 'piggy-banks.addMoney']); # add money + Route::get('/piggy-banks/remove/{piggyBank}', ['uses' => 'PiggyBankController@remove', 'as' => 'piggy-banks.removeMoney']); #remove money Route::get('/piggy-banks/create', ['uses' => 'PiggyBankController@create', 'as' => 'piggy-banks.create']); Route::get('/piggy-banks/edit/{piggyBank}', ['uses' => 'PiggyBankController@edit', 'as' => 'piggy-banks.edit']); Route::get('/piggy-banks/delete/{piggyBank}', ['uses' => 'PiggyBankController@delete', 'as' => 'piggy-banks.delete']); @@ -278,7 +291,7 @@ Route::group( */ Route::get('/profile', ['uses' => 'ProfileController@index', 'as' => 'profile']); Route::get('/profile/change-password', ['uses' => 'ProfileController@changePassword', 'as' => 'change-password']); - Route::post('/profile/change-password', ['uses' => 'ProfileController@postChangePassword','as' => 'change-password-post']); + Route::post('/profile/change-password', ['uses' => 'ProfileController@postChangePassword', 'as' => 'change-password-post']); /** * Related transactions controller @@ -286,9 +299,19 @@ Route::group( Route::get('/related/alreadyRelated/{tj}', ['uses' => 'RelatedController@alreadyRelated', 'as' => 'related.alreadyRelated']); Route::post('/related/relate/{tj}/{tjSecond}', ['uses' => 'RelatedController@relate', 'as' => 'related.relate']); Route::post('/related/removeRelation/{tj}/{tjSecond}', ['uses' => 'RelatedController@removeRelation', 'as' => 'related.removeRelation']); + Route::get('/related/remove/{tj}/{tjSecond}', ['uses' => 'RelatedController@getRemoveRelation', 'as' => 'related.getRemoveRelation']); Route::get('/related/related/{tj}', ['uses' => 'RelatedController@related', 'as' => 'related.related']); Route::post('/related/search/{tj}', ['uses' => 'RelatedController@search', 'as' => 'related.search']); + /** + * Reminder Controller + */ + Route::get('/reminders', ['uses' => 'ReminderController@index', 'as' => 'reminders.index']); + Route::get('/reminder/dismiss/{reminder}', ['uses' => 'ReminderController@dismiss', 'as' => 'reminders.dismiss']); + Route::get('/reminder/act/{reminder}', ['uses' => 'ReminderController@act', 'as' => 'reminders.act']); + Route::get('/reminder/{reminder}', ['uses' => 'ReminderController@show', 'as' => 'reminders.show']); + + /** * Repeated Expenses Controller */ @@ -309,6 +332,16 @@ Route::group( Route::get('/reports/{year}/{month}', ['uses' => 'ReportController@month', 'as' => 'reports.month']); Route::get('/reports/budget/{year}/{month}', ['uses' => 'ReportController@budget', 'as' => 'reports.budget']); + // pop ups for budget report: + Route::get('/reports/modal/{account}/{year}/{month}/no-budget', ['uses' => 'ReportController@modalNoBudget', 'as' => 'reports.no-budget']); + Route::get( + '/reports/modal/{account}/{year}/{month}/balanced-transfers', + ['uses' => 'ReportController@modalBalancedTransfers', 'as' => 'reports.balanced-transfers'] + ); + Route::get( + '/reports/modal/{account}/{year}/{month}/left-unbalanced', ['uses' => 'ReportController@modalLeftUnbalanced', 'as' => 'reports.left-unbalanced'] + ); + /** * Search Controller */ diff --git a/app/Models/Account.php b/app/Models/Account.php index 2c6c21002e..9c91fa403d 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Watson\Validating\ValidatingTrait; - +use Crypt; /** * Class Account * @@ -18,7 +18,7 @@ class Account extends Model = [ 'user_id' => 'required|exists:users,id', 'account_type_id' => 'required|exists:account_types,id', - 'name' => 'required|between:1,100|uniqueForUser:accounts,name', + 'name' => 'required|between:1,1024|uniqueForUser:accounts,name', 'active' => 'required|boolean' ]; @@ -41,6 +41,22 @@ class Account extends Model } + /** + * @param $value + * + * @return string + */ + public function getNameAttribute($value) + { + if ($this->encrypted) { + return Crypt::decrypt($value); + } + + // @codeCoverageIgnoreStart + return $value; + // @codeCoverageIgnoreEnd + } + /** * @return \Illuminate\Database\Eloquent\Relations\HasMany */ diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index 305a8eb076..38f5fa5690 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -14,7 +14,7 @@ class PiggyBank extends Model { use SoftDeletes; - protected $fillable = ['repeats', 'name', 'account_id','rep_every', 'rep_times', 'reminder_skip', 'targetamount', 'startdate', 'targetdate', 'reminder',]; + protected $fillable = ['repeats', 'name', 'account_id','rep_every', 'rep_times', 'reminder_skip', 'targetamount', 'startdate', 'targetdate', 'reminder','remind_me']; /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo @@ -24,6 +24,15 @@ class PiggyBank extends Model return $this->belongsTo('FireflyIII\Models\Account'); } + /** + * @param $value + * + * @return int + */ + public function getRemindMeAttribute($value) { + return intval($value) == 1; + } + /** * Grabs the PiggyBankRepetition that's currently relevant / active * diff --git a/app/Models/PiggyBankEvent.php b/app/Models/PiggyBankEvent.php index f9a94a0286..775a4e310d 100644 --- a/app/Models/PiggyBankEvent.php +++ b/app/Models/PiggyBankEvent.php @@ -10,6 +10,8 @@ use Illuminate\Database\Eloquent\Model; class PiggyBankEvent extends Model { + protected $fillable = ['piggy_bank_id', 'transaction_journal_id', 'date', 'amount']; + /** * @return array */ diff --git a/app/Models/PiggyBankRepetition.php b/app/Models/PiggyBankRepetition.php index fe4d72a6f2..6bb9d2d6e2 100644 --- a/app/Models/PiggyBankRepetition.php +++ b/app/Models/PiggyBankRepetition.php @@ -1,7 +1,8 @@ belongsTo('FireflyIII\Models\PiggyBank'); } + /** + * @param EloquentBuilder $query + * @param Carbon $date + * + * @return mixed + */ + public function scopeRelevantOnDate(EloquentBuilder $query, Carbon $date) + { + return $query->where( + function($q) use ($date) { + $q->where('startdate', '>=', $date->format('Y-m-d 00:00:00')); + $q->orWhereNull('startdate'); + }) + + ->where(function($q) use ($date) { + + $q->where('targetdate', '<=', $date->format('Y-m-d 00:00:00')); + $q->orWhereNull('targetdate'); + }); + } + } diff --git a/app/Models/Reminder.php b/app/Models/Reminder.php index 98e75a0a4c..7a39b59a54 100644 --- a/app/Models/Reminder.php +++ b/app/Models/Reminder.php @@ -1,5 +1,7 @@ morphTo(); } + /** + * @param EloquentBuilder $query + * @param Carbon $start + * @param Carbon $end + * + * @return $this + */ + public function scopeOnDates(EloquentBuilder $query, Carbon $start, Carbon $end) + { + return $query->where('reminders.startdate', '=', $start->format('Y-m-d 00:00:00'))->where('reminders.enddate', '=', $end->format('Y-m-d 00:00:00')); + } + + public function scopeToday(EloquentBuilder $query) + { + $today = new Carbon; + + return $query->where('startdate', '<=', $today->format('Y-m-d 00:00:00'))->where('enddate', '>=', $today->format('Y-m-d 00:00:00'))->where('active', 1) + ->where('notnow', 0); + } + + /** + * @param $value + */ + public function setMetadataAttribute($value) + { + $this->attributes['metadata'] = json_encode($value); + } + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 2a98c7656e..043e9ad7e1 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -1,17 +1,19 @@ [ - 'EventListener', - ], - 'App\Events\JournalDeleted' => [ - 'App\Handlers\Events\JournalDeletedHandler@handle', + 'FireflyIII\Events\JournalSaved' => [ + 'FireflyIII\Handlers\Events\RescanJournal', + 'FireflyIII\Handlers\Events\UpdateJournalConnection', + ], + 'FireflyIII\Events\JournalCreated' => [ + 'FireflyIII\Handlers\Events\ConnectJournalToPiggyBank', + ] ]; /** @@ -57,6 +61,8 @@ class EventServiceProvider extends ServiceProvider } ); + + Account::deleted( function (Account $account) { @@ -68,14 +74,16 @@ class EventServiceProvider extends ServiceProvider } ); - PiggyBank::created(function(PiggyBank $piggyBank) { + PiggyBank::created( + function (PiggyBank $piggyBank) { $repetition = new PiggyBankRepetition; $repetition->piggyBank()->associate($piggyBank); - $repetition->startdate = $piggyBank->startdate; - $repetition->targetdate = $piggyBank->targetdate; + $repetition->startdate = is_null($piggyBank->startdate) ? null : $piggyBank->startdate; + $repetition->targetdate = is_null($piggyBank->targetdate) ? null : $piggyBank->targetdate; $repetition->currentamount = 0; $repetition->save(); - }); + } + ); BudgetLimit::saved( function (BudgetLimit $budgetLimit) { diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index 549df90c5a..432da63ab8 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -64,6 +64,8 @@ class FireflyServiceProvider extends ServiceProvider $this->app->bind('FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface', 'FireflyIII\Repositories\PiggyBank\PiggyBankRepository'); $this->app->bind('FireflyIII\Support\Search\SearchInterface', 'FireflyIII\Support\Search\Search'); + + $this->app->bind('FireflyIII\Helpers\Reminders\ReminderHelperInterface', 'FireflyIII\Helpers\Reminders\ReminderHelper'); $this->app->bind('FireflyIII\Helpers\Report\ReportHelperInterface', 'FireflyIII\Helpers\Report\ReportHelper'); $this->app->bind('FireflyIII\Helpers\Report\ReportQueryInterface', 'FireflyIII\Helpers\Report\ReportQuery'); diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index aff7831674..506f0b9ddd 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -10,10 +10,12 @@ use FireflyIII\Models\Account; use FireflyIII\Models\AccountMeta; use FireflyIII\Models\AccountType; use FireflyIII\Models\PiggyBank; +use FireflyIII\Models\Preference; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; use Log; use Session; @@ -25,6 +27,14 @@ use Session; class AccountRepository implements AccountRepositoryInterface { + /** + * @return int + */ + public function countAssetAccounts() + { + return Auth::user()->accounts()->accountTypeIn(['Asset account', 'Default account'])->count(); + } + /** * @param Account $account * @@ -37,16 +47,53 @@ class AccountRepository implements AccountRepositoryInterface return true; } + /** + * @param Preference $preference + * + * @return Collection + */ + public function getFrontpageAccounts(Preference $preference) + { + if ($preference->data == []) { + $accounts = Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->orderBy('accounts.name', 'ASC')->get(['accounts.*']); + } else { + $accounts = Auth::user()->accounts()->whereIn('id', $preference->data)->orderBy('accounts.name', 'ASC')->get(['accounts.*']); + } + + return $accounts; + } + /** * @param Account $account - * @param int $page - * @param string $range + * @param Carbon $start + * @param Carbon $end * * @return mixed */ - public function getJournals(Account $account, $page, $range = 'session') + public function getFrontpageTransactions(Account $account, Carbon $start, Carbon $end) { - $offset = $page * 50; + return Auth::user() + ->transactionjournals() + ->with(['transactions', 'transactioncurrency', 'transactiontype']) + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')->where('accounts.id', $account->id) + ->before($end) + ->after($start) + ->orderBy('transaction_journals.date', 'DESC') + ->orderBy('transaction_journals.id', 'DESC') + ->take(10) + ->get(['transaction_journals.*']); + } + + /** + * @param Account $account + * @param int $page + * + * @return mixed + */ + public function getJournals(Account $account, $page) + { + $offset = ($page - 1) * 50; $query = Auth::user() ->transactionJournals() ->withRelevantData() @@ -54,10 +101,8 @@ class AccountRepository implements AccountRepositoryInterface ->where('transactions.account_id', $account->id) ->orderBy('date', 'DESC'); - if ($range == 'session') { - $query->before(Session::get('end', Carbon::now()->startOfMonth())); - $query->after(Session::get('start', Carbon::now()->startOfMonth())); - } + $query->before(Session::get('end', Carbon::now()->endOfMonth())); + $query->after(Session::get('start', Carbon::now()->startOfMonth())); $count = $query->count(); $set = $query->take(50)->offset($offset)->get(['transaction_journals.*']); $paginator = new LengthAwarePaginator($set, $count, 50, $page); @@ -69,6 +114,23 @@ class AccountRepository implements AccountRepositoryInterface } + /** + * @param Account $account + * + * @return float + */ + public function leftOnAccount(Account $account) + { + $balance = \Steam::balance($account); + /** @var PiggyBank $p */ + foreach ($account->piggybanks()->get() as $p) { + $balance -= $p->currentRelevantRep()->currentamount; + } + + return $balance; + + } + /** * @param Account $account * @@ -123,13 +185,7 @@ class AccountRepository implements AccountRepositoryInterface $account->save(); // update meta data: - /** @var AccountMeta $meta */ - foreach ($account->accountMeta()->get() as $meta) { - if ($meta->name == 'accountRole') { - $meta->data = $data['accountRole']; - $meta->save(); - } - } + $this->_updateMetadata($account, $data); $openingBalance = $this->openingBalanceTransaction($account); @@ -285,6 +341,40 @@ class AccountRepository implements AccountRepositoryInterface } + /** + * @param Account $account + * @param array $data + */ + protected function _updateMetadata(Account $account, array $data) + { + $metaEntries = $account->accountMeta()->get(); + $updated = false; + + /** @var AccountMeta $entry */ + foreach ($metaEntries as $entry) { + if ($entry->name == 'accountRole') { + $entry->data = $data['accountRole']; + $updated = true; + $entry->save(); + } + } + + if ($updated === false) { + $metaData = new AccountMeta( + [ + 'account_id' => $account->id, + 'name' => 'accountRole', + 'data' => $data['accountRole'] + ] + ); + if (!$metaData->isValid()) { + App::abort(500); + } + $metaData->save(); + } + + } + /** * @param Account $account * @param TransactionJournal $journal @@ -310,21 +400,4 @@ class AccountRepository implements AccountRepositoryInterface return $journal; } - - /** - * @param Account $account - * - * @return float - */ - public function leftOnAccount(Account $account) - { - $balance = \Steam::balance($account); - /** @var PiggyBank $p */ - foreach ($account->piggybanks()->get() as $p) { - $balance -= $p->currentRelevantRep()->currentamount; - } - - return $balance; - - } } \ No newline at end of file diff --git a/app/Repositories/Account/AccountRepositoryInterface.php b/app/Repositories/Account/AccountRepositoryInterface.php index 619309d762..020ae41b69 100644 --- a/app/Repositories/Account/AccountRepositoryInterface.php +++ b/app/Repositories/Account/AccountRepositoryInterface.php @@ -4,7 +4,9 @@ namespace FireflyIII\Repositories\Account; use FireflyIII\Models\Account; use FireflyIII\Models\TransactionJournal; - +use FireflyIII\Models\Preference; +use Illuminate\Support\Collection; +use Carbon\Carbon; /** * Interface AccountRepositoryInterface * @@ -19,14 +21,34 @@ interface AccountRepositoryInterface */ public function destroy(Account $account); + /** + * @return int + */ + public function countAssetAccounts(); + + /** + * @param Preference $preference + * + * @return Collection + */ + public function getFrontpageAccounts(Preference $preference); + + /** + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return mixed + */ + public function getFrontpageTransactions(Account $account, Carbon $start, Carbon $end); + /** * @param Account $account - * @param int $page * @param string $range * * @return mixed */ - public function getJournals(Account $account, $page, $range = 'session'); + public function getJournals(Account $account, $page); /** * @param Account $account diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index 3a5a2abf67..5c12f99b0b 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -1,18 +1,12 @@ date; + $startOfBill = Navigation::startOfPeriod($startOfBill, $bill->repeat_freq); + + + // all periods of this bill up until the current period: + $billStarts = []; + while ($startOfBill < $end) { + + $endOfBill = Navigation::endOfPeriod($startOfBill, $bill->repeat_freq); + + $billStarts[] = [ + 'start' => clone $startOfBill, + 'end' => clone $endOfBill, + ]; + // actually the next one: + $startOfBill = Navigation::addPeriod($startOfBill, $bill->repeat_freq, $bill->skip); + + } + // for each + $validRanges = []; + foreach ($billStarts as $dateEntry) { + if ($dateEntry['end'] > $start && $dateEntry['start'] < $end) { + // count transactions for bill in this range (not relevant yet!): + // $count = $bill->transactionjournals()->before($dateEntry['end'])->after($dateEntry['start'])->count(); + // if ($count == 0) { + $validRanges[] = $dateEntry; + // } + } + } + + return $validRanges; + // echo $bill->name; + // var_dump($validRanges); + } + /** * @param Bill $bill * @@ -28,6 +70,7 @@ class BillRepository implements BillRepositoryInterface */ public function nextExpectedMatch(Bill $bill) { + $finalDate = null; if ($bill->active == 0) { return $finalDate; @@ -113,7 +156,7 @@ class BillRepository implements BillRepositoryInterface $wordMatch = true; Log::debug('word match is true'); } else { - Log::debug('Count: ' . $count.', count(matches): ' . count($matches)); + Log::debug('Count: ' . $count . ', count(matches): ' . count($matches)); } diff --git a/app/Repositories/Bill/BillRepositoryInterface.php b/app/Repositories/Bill/BillRepositoryInterface.php index ead38877b1..66ce00cf4a 100644 --- a/app/Repositories/Bill/BillRepositoryInterface.php +++ b/app/Repositories/Bill/BillRepositoryInterface.php @@ -1,13 +1,8 @@ transactions()->get() as $transaction) { - if ($transaction->account_id === $from->id) { + if (floatval($transaction->amount) < 0) { // this is the from transaction, negative amount: - $transaction->amount = $data['amount'] * -1; + $transaction->amount = $data['amount'] * -1; + $transaction->account_id = $from->id; $transaction->save(); } - if ($transaction->account_id === $to->id) { + if (floatval($transaction->amount) > 0) { $transaction->amount = $data['amount']; + $transaction->account_id = $to->id; $transaction->save(); } } diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index a7eaf8f265..54a8608335 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -2,8 +2,12 @@ namespace FireflyIII\Repositories\PiggyBank; +use Amount; +use Auth; +use Carbon\Carbon; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBankRepetition; +use FireflyIII\Models\Reminder; use Illuminate\Support\Collection; use Navigation; @@ -15,49 +19,6 @@ use Navigation; class PiggyBankRepository implements PiggyBankRepositoryInterface { - /** - * @param array $data - * - * @return PiggyBank - */ - public function store(array $data) - { - - $piggyBank = PiggyBank::create($data); - - return $piggyBank; - } - - /** - * @param PiggyBank $account - * @param array $data - * - * @return PiggyBank - */ - public function update(PiggyBank $piggyBank, array $data) - { - /** - 'rep_length' => $request->get('rep_length'), - 'rep_every' => intval($request->get('rep_every')), - 'rep_times' => intval($request->get('rep_times')), - 'remind_me' => intval($request->get('remind_me')) == 1 ? true : false , - 'reminder' => $request->get('reminder'), - */ - - $piggyBank->name = $data['name']; - $piggyBank->account_id = intval($data['account_id']); - $piggyBank->targetamount = floatval($data['targetamount']); - $piggyBank->targetdate = $data['targetdate']; - $piggyBank->reminder = $data['reminder']; - $piggyBank->rep_length = isset($data['rep_length']) ? $data['rep_length'] : null; - $piggyBank->rep_every =isset($data['rep_every']) ? $data['rep_every'] : null; - $piggyBank->rep_times = isset($data['rep_times']) ? $data['rep_times'] : null; - $piggyBank->remind_me = isset($data['remind_me']) ? $data['remind_me'] : null; - - $piggyBank->save(); - return $piggyBank; - } - /** * @SuppressWarnings("CyclomaticComplexity") // It's exactly 5. So I don't mind. * @@ -126,4 +87,50 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface return $part; } + + /** + * @param array $data + * + * @return PiggyBank + */ + public function store(array $data) + { + $data['remind_me'] = isset($data['remind_me']) && $data['remind_me'] == '1' ? true : false; + $piggyBank = PiggyBank::create($data); + + return $piggyBank; + } + + /** + * @param PiggyBank $account + * @param array $data + * + * @return PiggyBank + */ + public function update(PiggyBank $piggyBank, array $data) + { + /** + * 'rep_length' => $request->get('rep_length'), + * 'rep_every' => intval($request->get('rep_every')), + * 'rep_times' => intval($request->get('rep_times')), + * 'remind_me' => intval($request->get('remind_me')) == 1 ? true : false , + * 'reminder' => $request->get('reminder'), + */ + + $piggyBank->name = $data['name']; + $piggyBank->account_id = intval($data['account_id']); + $piggyBank->targetamount = floatval($data['targetamount']); + $piggyBank->targetdate = $data['targetdate']; + $piggyBank->reminder = $data['reminder']; + $piggyBank->startdate = $data['startdate']; + $piggyBank->rep_length = isset($data['rep_length']) ? $data['rep_length'] : null; + $piggyBank->rep_every = isset($data['rep_every']) ? $data['rep_every'] : null; + $piggyBank->rep_times = isset($data['rep_times']) ? $data['rep_times'] : null; + $piggyBank->remind_me = isset($data['remind_me']) && $data['remind_me'] == '1' ? 1 : 0; + + $piggyBank->save(); + + return $piggyBank; + } + } \ No newline at end of file diff --git a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php index a06ed505be..c65d002f1e 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php +++ b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php @@ -4,31 +4,19 @@ namespace FireflyIII\Repositories\PiggyBank; use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBankRepetition; +use FireflyIII\Models\Reminder; use Illuminate\Support\Collection; +use Carbon\Carbon; /** * Interface PiggyBankRepositoryInterface * * @package FireflyIII\Repositories\PiggyBank */ -interface PiggyBankRepositoryInterface { +interface PiggyBankRepositoryInterface +{ - /** - * @param array $data - * - * @return PiggyBank - */ - public function store(array $data); - - /** - * @param PiggyBank $account - * @param array $data - * - * @return PiggyBank - */ - public function update(PiggyBank $piggyBank, array $data); - /** * @SuppressWarnings("CyclomaticComplexity") // It's exactly 5. So I don't mind. * @@ -48,4 +36,21 @@ interface PiggyBankRepositoryInterface { * @return PiggyBankPart */ public function createPiggyBankPart(array $data); + + + + /** + * @param array $data + * + * @return PiggyBank + */ + public function store(array $data); + + /** + * @param PiggyBank $account + * @param array $data + * + * @return PiggyBank + */ + public function update(PiggyBank $piggyBank, array $data); } \ No newline at end of file diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index 94f462e6fe..2e5ee2b16a 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -8,6 +8,7 @@ use Illuminate\Support\MessageBag; use Input; use Session; use View; +use Amount as Amt; /** * Class ExpandedForm @@ -17,46 +18,6 @@ use View; class ExpandedForm { - /** - * @param $name - * @param null $value - * @param array $options - * - * @return string - */ - public function integer($name, $value = null, array $options = []) - { - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = $this->fillFieldValue($name, $value); - $options['step'] = '1'; - $html = \View::make('form.integer', compact('classes', 'name', 'label', 'value', 'options'))->render(); - - return $html; - - } - - - /** - * @param $name - * @param null $value - * @param array $options - * - * @return string - */ - public function tags($name, $value = null, array $options = []) - { - $label = $this->label($name, $options); - $options = $this->expandOptionArray($name, $label, $options); - $classes = $this->getHolderClasses($name); - $value = $this->fillFieldValue($name, $value); - $options['data-role'] = 'tagsinput'; - $html = \View::make('form.tags', compact('classes', 'name', 'label', 'value', 'options'))->render(); - - return $html; - } - /** * @param $name * @param null $value @@ -72,7 +33,7 @@ class ExpandedForm $value = $this->fillFieldValue($name, $value); $options['step'] = 'any'; $options['min'] = '0.01'; - $defaultCurrency = isset($options['currency']) ? $options['currency'] : \Amount::getDefaultCurrency(); + $defaultCurrency = isset($options['currency']) ? $options['currency'] : Amt::getDefaultCurrency(); $currencies = TransactionCurrency::orderBy('code', 'ASC')->get(); $html = View::make('form.amount', compact('defaultCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); @@ -91,9 +52,19 @@ class ExpandedForm if (isset($options['label'])) { return $options['label']; } - $labels = ['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', 'budget_id' => 'Budget' - , 'piggy_bank_id' => 'Piggy bank']; + $labels = [ + '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', + 'budget_id' => 'Budget', + 'openingBalance' => 'Opening balance', + 'accountRole' => 'Account role', + 'openingBalanceDate' => 'Opening balance date', + 'piggy_bank_id' => 'Piggy bank']; return isset($labels[$name]) ? $labels[$name] : str_replace('_', ' ', ucfirst($name)); @@ -157,7 +128,7 @@ class ExpandedForm public function fillFieldValue($name, $value) { if (Session::has('preFilled')) { - $preFilled = \Session::get('preFilled'); + $preFilled = Session::get('preFilled'); $value = isset($preFilled[$name]) && is_null($value) ? $preFilled[$name] : $value; } if (!is_null(Input::old($name))) { @@ -181,7 +152,7 @@ class ExpandedForm $classes = $this->getHolderClasses($name); $value = $this->fillFieldValue($name, $value); $options['step'] = 'any'; - $defaultCurrency = isset($options['currency']) ? $options['currency'] : Amount::getDefaultCurrency(); + $defaultCurrency = isset($options['currency']) ? $options['currency'] : Amt::getDefaultCurrency(); $currencies = TransactionCurrency::orderBy('code', 'ASC')->get(); $html = View::make('form.balance', compact('defaultCurrency', 'currencies', 'classes', 'name', 'label', 'value', 'options'))->render(); @@ -229,6 +200,26 @@ class ExpandedForm return $html; } + /** + * @param $name + * @param null $value + * @param array $options + * + * @return string + */ + public function integer($name, $value = null, array $options = []) + { + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); + $options['step'] = '1'; + $html = View::make('form.integer', compact('classes', 'name', 'label', 'value', 'options'))->render(); + + return $html; + + } + /** * @SuppressWarnings("CyclomaticComplexity") // It's exactly 5. So I don't mind. * @@ -246,7 +237,7 @@ class ExpandedForm $selectList[0] = '(none)'; } $fields = ['title', 'name', 'description']; - /** @var \Eloquent $entry */ + /** @var Eloquent $entry */ foreach ($set as $entry) { $id = intval($entry->id); $title = null; @@ -270,9 +261,9 @@ class ExpandedForm */ public function optionsList($type, $name) { - $previousValue = \Input::old('post_submit_action'); + $previousValue = Input::old('post_submit_action'); $previousValue = is_null($previousValue) ? 'store' : $previousValue; - $html = \View::make('form.options', compact('type', 'name', 'previousValue'))->render(); + $html = View::make('form.options', compact('type', 'name', 'previousValue'))->render(); return $html; } @@ -291,7 +282,26 @@ class ExpandedForm $options = $this->expandOptionArray($name, $label, $options); $classes = $this->getHolderClasses($name); $selected = $this->fillFieldValue($name, $selected); - $html = \View::make('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render(); + $html = View::make('form.select', compact('classes', 'name', 'label', 'selected', 'options', 'list'))->render(); + + return $html; + } + + /** + * @param $name + * @param null $value + * @param array $options + * + * @return string + */ + public function tags($name, $value = null, array $options = []) + { + $label = $this->label($name, $options); + $options = $this->expandOptionArray($name, $label, $options); + $classes = $this->getHolderClasses($name); + $value = $this->fillFieldValue($name, $value); + $options['data-role'] = 'tagsinput'; + $html = View::make('form.tags', compact('classes', 'name', 'label', 'value', 'options'))->render(); return $html; } diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index 786ffa39cf..7c734e3b9d 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -28,20 +28,27 @@ class Navigation $add = ($skip + 1); $functionMap = [ + '1D' => 'addDays', 'daily' => 'addDays', + '1W' => 'addWeeks', 'weekly' => 'addWeeks', 'week' => 'addWeeks', + '1M' => 'addMonths', 'month' => 'addMonths', 'monthly' => 'addMonths', + '3M' => 'addMonths', 'quarter' => 'addMonths', 'quarterly' => 'addMonths', + '6M' => 'addMonths', 'half-year' => 'addMonths', 'year' => 'addYears', 'yearly' => 'addYears', ]; $modifierMap = [ 'quarter' => 3, + '3M' => 3, 'quarterly' => 3, + '6M' => 6, 'half-year' => 6, ]; if (!isset($functionMap[$repeatFreq])) { @@ -68,24 +75,31 @@ class Navigation $currentEnd = clone $theCurrentEnd; $functionMap = [ + '1D' => 'addDay', 'daily' => 'addDay', + '1W' => 'addWeek', 'week' => 'addWeek', 'weekly' => 'addWeek', + '1M' => 'addMonth', 'month' => 'addMonth', 'monthly' => 'addMonth', + '3M' => 'addMonths', 'quarter' => 'addMonths', 'quarterly' => 'addMonths', + '6M' => 'addMonths', 'half-year' => 'addMonths', 'year' => 'addYear', 'yearly' => 'addYear', ]; $modifierMap = [ 'quarter' => 3, + '3M' => 3, 'quarterly' => 3, 'half-year' => 6, + '6M' => 6, ]; - $subDay = ['week', 'weekly', 'month', 'monthly', 'quarter', 'quarterly', 'half-year', 'year', 'yearly']; + $subDay = ['week', 'weekly', '1W', 'month', 'monthly', '1M', '3M', 'quarter', 'quarterly', '6M', 'half-year', 'year', 'yearly']; if (!isset($functionMap[$repeatFreq])) { throw new FireflyException('Cannot do endOfPeriod for $repeat_freq ' . $repeatFreq); @@ -298,15 +312,19 @@ class Navigation $date = clone $theDate; $functionMap = [ - 'daily' => 'startOfDay', - 'week' => 'startOfWeek', - 'weekly' => 'startOfWeek', - 'month' => 'startOfMonth', - 'monthly' => 'startOfMonth', - 'quarter' => 'firstOfQuarter', - 'quartly' => 'firstOfQuarter', - 'year' => 'startOfYear', - 'yearly' => 'startOfYear', + '1D' => 'startOfDay', + 'daily' => 'startOfDay', + '1W' => 'startOfWeek', + 'week' => 'startOfWeek', + 'weekly' => 'startOfWeek', + 'month' => 'startOfMonth', + '1M' => 'startOfMonth', + 'monthly' => 'startOfMonth', + '3M' => 'firstOfQuarter', + 'quarter' => 'firstOfQuarter', + 'quarterly' => 'firstOfQuarter', + 'year' => 'startOfYear', + 'yearly' => 'startOfYear', ]; if (isset($functionMap[$repeatFreq])) { $function = $functionMap[$repeatFreq]; @@ -314,7 +332,7 @@ class Navigation return $date; } - if ($repeatFreq == 'half-year') { + if ($repeatFreq == 'half-year' || $repeatFreq == '6M') { $month = intval($date->format('m')); $date->startOfYear(); if ($month >= 7) { @@ -396,5 +414,47 @@ class Navigation throw new FireflyException('updateStartDate cannot handle $range ' . $range); } + /** + * @param Carbon $theDate + * @param $repeatFreq + * @param int $subtract + * + * @return Carbon + * @throws FireflyException + */ + public function subtractPeriod(Carbon $theDate, $repeatFreq, $subtract = 1) + { + $date = clone $theDate; + + $functionMap = [ + 'daily' => 'subDays', + 'week' => 'subWeeks', + 'weekly' => 'subWeeks', + 'month' => 'subMonths', + 'monthly' => 'subMonths', + 'year' => 'subYears', + 'yearly' => 'subYears', + ]; + $modifierMap = [ + 'quarter' => 3, + 'quarterly' => 3, + 'half-year' => 6, + ]; + if (isset($functionMap[$repeatFreq])) { + $function = $functionMap[$repeatFreq]; + $date->$function($subtract); + + return $date; + } + if (isset($modifierMap[$repeatFreq])) { + $subtract = $subtract * $modifierMap[$repeatFreq]; + $date->subMonths($subtract); + + return $date; + } + + throw new FireflyException('Cannot do subtractPeriod for $repeat_freq ' . $repeatFreq); + } + } \ No newline at end of file diff --git a/app/Support/Preferences.php b/app/Support/Preferences.php index 98cab769c9..185fea9512 100644 --- a/app/Support/Preferences.php +++ b/app/Support/Preferences.php @@ -45,12 +45,12 @@ class Preferences if (is_null($pref)) { $pref = new Preference; $pref->name = $name; - $pref->user()->associate(Auth::user()); - } $pref->data = $value; - $pref->save(); - + if (!is_null(Auth::user()->id)) { + $pref->user()->associate(Auth::user()); + $pref->save(); + } return $pref; diff --git a/app/User.php b/app/User.php index 4e09c591f0..659d236c1d 100644 --- a/app/User.php +++ b/app/User.php @@ -99,12 +99,4 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return $this->hasMany('FireflyIII\Models\TransactionJournal'); } - /** - * @param $value - */ - public function setPasswordAttribute($value) - { - $this->attributes['password'] = \Hash::make($value); - } - } diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index 1d06ba7c38..46b39b0b1d 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -3,8 +3,10 @@ namespace FireflyIII\Validation; use Auth; +use Carbon\Carbon; use DB; use Illuminate\Validation\Validator; +use Navigation; /** * Class FireflyValidator @@ -32,6 +34,31 @@ class FireflyValidator extends Validator } + public function validatePiggyBankReminder($attribute, $value, $parameters) + { + $array = $this->data; + // no reminder? dont care. + if (!isset($array['remind_me'])) { + return true; + } + + // get or set start date & target date: + $startDate = isset($array['startdate']) ? new Carbon($array['startdate']) : new Carbon; + $targetDate = isset($array['targetdate']) && strlen($array['targetdate']) > 0 ? new Carbon($array['targetdate']) : null; + + // target date is null? reminder period is always good. + if ($array['remind_me'] == '1' && is_null($targetDate)) { + return true; + } + + $nextReminder = Navigation::addPeriod($startDate, $array['reminder'],0); + // reminder is beyond target? + if($nextReminder > $targetDate) { + return false; + } + return true; + } + /** * @param $attribute * @param $value @@ -41,7 +68,11 @@ class FireflyValidator extends Validator */ public function validateUniqueForUser($attribute, $value, $parameters) { - $count = DB::table($parameters[0])->where($parameters[1], $value)->count(); + $query = DB::table($parameters[0])->where($parameters[1], $value); + if (isset($paramers[2])) { + $query->where('id', '!=', $parameters[2]); + } + $count = $query->count(); if ($count == 0) { return true; } diff --git a/codeception.yml b/codeception.yml deleted file mode 100644 index 3284792656..0000000000 --- a/codeception.yml +++ /dev/null @@ -1,18 +0,0 @@ -actor: Tester -paths: - tests: tests - log: tests/_output - data: tests/_data - helpers: tests/_support -settings: - bootstrap: _bootstrap.php - colors: true - memory_limit: 1024M -modules: - config: -coverage: - enabled: true - remote: false - whitelist: - include: - - app/* \ No newline at end of file diff --git a/composer.json b/composer.json index 59a4fbdd9c..f214d73c10 100644 --- a/composer.json +++ b/composer.json @@ -26,23 +26,14 @@ "watson/validating": "dev-master", "doctrine/dbal": "~2.5", "illuminate/html": "~5.0", - "barryvdh/laravel-ide-helper": "~2.0", "league/commonmark": "0.7.*" - }, "require-dev": { "barryvdh/laravel-debugbar": "@stable", + "barryvdh/laravel-ide-helper": "~2.0", "phpunit/phpunit": "~4.0", "phpspec/phpspec": "~2.1", - "codeception/codeception": "@stable", - "codeception/c3": "@stable", - "league/factory-muffin": "~2.1", - "codeception/phpbuiltinserver": "*", - "codeception/specify": "*", - "codeception/verify": "*", - "fzaninotto/faker": "1.*", - "codeclimate/php-test-reporter": "dev-master" - + "satooshi/php-coveralls": "0.6.1" }, "autoload": { "classmap": [ @@ -60,14 +51,11 @@ "scripts": { "post-install-cmd": [ "php artisan clear-compiled", - "php artisan optimize", - "Codeception\\c3\\Installer::copyC3ToRoot" - + "php artisan optimize" ], "post-update-cmd": [ "php artisan clear-compiled", - "php artisan optimize", - "Codeception\\c3\\Installer::copyC3ToRoot" + "php artisan optimize" ], "post-create-project-cmd": [ "php -r \"copy('.env.example', '.env');\"", diff --git a/composer.lock b/composer.lock index 95b03522ad..50678e27a2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,71 +4,8 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "d78d46376975c1bfab9823b66a30ed5f", + "hash": "34e0c28c0f8f4c04b545b2cbb0f2f24b", "packages": [ - { - "name": "barryvdh/laravel-ide-helper", - "version": "v2.0.1", - "source": { - "type": "git", - "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "81b7febfc64168ea1af57261aa4dfc9acefd5429" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/81b7febfc64168ea1af57261aa4dfc9acefd5429", - "reference": "81b7febfc64168ea1af57261aa4dfc9acefd5429", - "shasum": "" - }, - "require": { - "illuminate/console": "5.0.x", - "illuminate/filesystem": "5.0.x", - "illuminate/support": "5.0.x", - "php": ">=5.4.0", - "phpdocumentor/reflection-docblock": "2.0.x", - "symfony/class-loader": "~2.3" - }, - "require-dev": { - "doctrine/dbal": "~2.3" - }, - "suggest": { - "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "Barryvdh\\LaravelIdeHelper\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" - } - ], - "description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.", - "keywords": [ - "autocomplete", - "codeintel", - "helper", - "ide", - "laravel", - "netbeans", - "phpdoc", - "phpstorm", - "sublime" - ], - "time": "2015-02-23 15:55:54" - }, { "name": "classpreloader/classpreloader", "version": "1.2.0", @@ -955,16 +892,16 @@ }, { "name": "jeremeamia/SuperClosure", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/jeremeamia/super_closure.git", - "reference": "ac4394c7e21777ac48543190fb0872b2fc7f17af" + "reference": "b712f39c671e5ead60c7ebfe662545456aade833" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jeremeamia/super_closure/zipball/ac4394c7e21777ac48543190fb0872b2fc7f17af", - "reference": "ac4394c7e21777ac48543190fb0872b2fc7f17af", + "url": "https://api.github.com/repos/jeremeamia/super_closure/zipball/b712f39c671e5ead60c7ebfe662545456aade833", + "reference": "b712f39c671e5ead60c7ebfe662545456aade833", "shasum": "" }, "require": { @@ -978,7 +915,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -1009,20 +946,20 @@ "serialize", "tokenizer" ], - "time": "2015-01-26 23:07:39" + "time": "2015-03-11 20:06:43" }, { "name": "laravel/framework", - "version": "v5.0.13", + "version": "v5.0.16", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "c263240daa383e6b9bd979600dc61dcb2c436d53" + "reference": "861a1e78c84dca82fe4bd85d00349c52304eea77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/c263240daa383e6b9bd979600dc61dcb2c436d53", - "reference": "c263240daa383e6b9bd979600dc61dcb2c436d53", + "url": "https://api.github.com/repos/laravel/framework/zipball/861a1e78c84dca82fe4bd85d00349c52304eea77", + "reference": "861a1e78c84dca82fe4bd85d00349c52304eea77", "shasum": "" }, "require": { @@ -1130,24 +1067,25 @@ } ], "description": "The Laravel Framework.", + "homepage": "http://laravel.com", "keywords": [ "framework", "laravel" ], - "time": "2015-02-27 00:32:38" + "time": "2015-03-13 13:27:55" }, { "name": "league/commonmark", - "version": "0.7.0", + "version": "0.7.2", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "5f5137889b2aec36f8a1009ebe8673dac45f004e" + "reference": "7fecb7bdef265e45c80c53e1000e2056a9463401" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/5f5137889b2aec36f8a1009ebe8673dac45f004e", - "reference": "5f5137889b2aec36f8a1009ebe8673dac45f004e", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/7fecb7bdef265e45c80c53e1000e2056a9463401", + "reference": "7fecb7bdef265e45c80c53e1000e2056a9463401", "shasum": "" }, "require": { @@ -1159,14 +1097,14 @@ }, "require-dev": { "erusev/parsedown": "~1.0", - "jgm/commonmark": "0.17", + "jgm/commonmark": "0.18", "michelf/php-markdown": "~1.4", "phpunit/phpunit": "~4.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.7-dev" + "dev-master": "0.8-dev" } }, "autoload": { @@ -1193,20 +1131,20 @@ "markdown", "parser" ], - "time": "2015-02-16 23:59:27" + "time": "2015-03-08 17:48:53" }, { "name": "league/flysystem", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "efa1d394bb45b0984a863854c608c607b9c2a9a4" + "reference": "51cd7cd7ee0defbaafc6ec0d3620110a5d71e11a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/efa1d394bb45b0984a863854c608c607b9c2a9a4", - "reference": "efa1d394bb45b0984a863854c608c607b9c2a9a4", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/51cd7cd7ee0defbaafc6ec0d3620110a5d71e11a", + "reference": "51cd7cd7ee0defbaafc6ec0d3620110a5d71e11a", "shasum": "" }, "require": { @@ -1218,21 +1156,21 @@ "mockery/mockery": "~0.9", "phpspec/phpspec": "~2.0.0", "phpspec/prophecy-phpunit": "~1.0", - "phpunit/phpunit": "~4.0", + "phpunit/phpunit": "~4.1", "predis/predis": "~1.0", "tedivm/stash": "~0.12.0" }, "suggest": { "ext-fileinfo": "Required for MimeType", - "league/flysystem-aws-s3-v2": "Use S3 storage with AWS SDK v2", - "league/flysystem-aws-s3-v3": "Use S3 storage with AWS SDK v3", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", "league/flysystem-copy": "Allows you to use Copy.com storage", - "league/flysystem-dropbox": "Use Dropbox storage", + "league/flysystem-dropbox": "Allows you to use Dropbox storage", "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", - "league/flysystem-sftp": "Allows SFTP server storage via phpseclib", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", "league/flysystem-webdav": "Allows you to use WebDAV storage", "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", "predis/predis": "Allows you to use Predis for caching" @@ -1276,20 +1214,20 @@ "sftp", "storage" ], - "time": "2015-01-23 09:43:34" + "time": "2015-03-10 11:04:14" }, { "name": "monolog/monolog", - "version": "1.12.0", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "1fbe8c2641f2b163addf49cc5e18f144bec6b19f" + "reference": "c31a2c4e8db5da8b46c74cf275d7f109c0f249ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1fbe8c2641f2b163addf49cc5e18f144bec6b19f", - "reference": "1fbe8c2641f2b163addf49cc5e18f144bec6b19f", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c31a2c4e8db5da8b46c74cf275d7f109c0f249ac", + "reference": "c31a2c4e8db5da8b46c74cf275d7f109c0f249ac", "shasum": "" }, "require": { @@ -1306,6 +1244,7 @@ "phpunit/phpunit": "~4.0", "raven/raven": "~0.5", "ruflin/elastica": "0.90.*", + "swiftmailer/swiftmailer": "~5.3", "videlalvaro/php-amqplib": "~2.4" }, "suggest": { @@ -1322,7 +1261,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12.x-dev" + "dev-master": "1.13.x-dev" } }, "autoload": { @@ -1348,7 +1287,7 @@ "logging", "psr-3" ], - "time": "2014-12-29 21:29:35" + "time": "2015-03-09 09:58:04" }, { "name": "mtdowling/cron-expression", @@ -1396,16 +1335,16 @@ }, { "name": "nesbot/carbon", - "version": "1.14.0", + "version": "1.17.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "bb87460c995d97fe55b39e65f6ffb7f64b0a941e" + "reference": "a1dd1ad9abfc8b3c4d8768068e6c71d293424e86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/bb87460c995d97fe55b39e65f6ffb7f64b0a941e", - "reference": "bb87460c995d97fe55b39e65f6ffb7f64b0a941e", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/a1dd1ad9abfc8b3c4d8768068e6c71d293424e86", + "reference": "a1dd1ad9abfc8b3c4d8768068e6c71d293424e86", "shasum": "" }, "require": { @@ -1432,13 +1371,13 @@ } ], "description": "A simple API extension for DateTime.", - "homepage": "https://github.com/briannesbitt/Carbon", + "homepage": "http://carbon.nesbot.com", "keywords": [ "date", "datetime", "time" ], - "time": "2015-02-06 05:07:29" + "time": "2015-03-08 14:05:44" }, { "name": "nikic/php-parser", @@ -1485,55 +1424,6 @@ ], "time": "2015-01-18 11:29:59" }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", - "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "dflydev/markdown": "~1.0", - "erusev/parsedown": "~1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "phpDocumentor": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" - } - ], - "time": "2015-02-03 12:10:50" - }, { "name": "psr/log", "version": "1.0.0", @@ -1574,16 +1464,16 @@ }, { "name": "psy/psysh", - "version": "v0.4.1", + "version": "v0.4.2", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "3787f3436f4fd898e0ac1eb6f5abd2ab6b24085b" + "reference": "e50a63b4e4971041fda993b0dd6977fc60bc39d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/3787f3436f4fd898e0ac1eb6f5abd2ab6b24085b", - "reference": "3787f3436f4fd898e0ac1eb6f5abd2ab6b24085b", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/e50a63b4e4971041fda993b0dd6977fc60bc39d4", + "reference": "e50a63b4e4971041fda993b0dd6977fc60bc39d4", "shasum": "" }, "require": { @@ -1594,10 +1484,10 @@ "symfony/console": "~2.3.10|~2.4.2|~2.5" }, "require-dev": { - "fabpot/php-cs-fixer": "~1.3", + "fabpot/php-cs-fixer": "~1.5", "phpunit/phpunit": "~3.7|~4.0", "squizlabs/php_codesniffer": "~2.0", - "symfony/finder": "~2.1" + "symfony/finder": "~2.1|~3.0" }, "suggest": { "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", @@ -1611,7 +1501,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-develop": "0.3.x-dev" + "dev-develop": "0.4.x-dev" } }, "autoload": { @@ -1641,20 +1531,20 @@ "interactive", "shell" ], - "time": "2015-02-25 20:35:54" + "time": "2015-03-14 17:29:14" }, { "name": "swiftmailer/swiftmailer", - "version": "v5.3.1", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "c5f963e7f9d6f6438fda4f22d5cc2db296ec621a" + "reference": "31454f258f10329ae7c48763eb898a75c39e0a9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/c5f963e7f9d6f6438fda4f22d5cc2db296ec621a", - "reference": "c5f963e7f9d6f6438fda4f22d5cc2db296ec621a", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/31454f258f10329ae7c48763eb898a75c39e0a9f", + "reference": "31454f258f10329ae7c48763eb898a75c39e0a9f", "shasum": "" }, "require": { @@ -1666,7 +1556,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.3-dev" + "dev-master": "5.4-dev" } }, "autoload": { @@ -1693,57 +1583,7 @@ "mail", "mailer" ], - "time": "2014-12-05 14:17:14" - }, - { - "name": "symfony/class-loader", - "version": "v2.6.4", - "target-dir": "Symfony/Component/ClassLoader", - "source": { - "type": "git", - "url": "https://github.com/symfony/ClassLoader.git", - "reference": "deac802f76910708ab50d039806cfd1866895b52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/ClassLoader/zipball/deac802f76910708ab50d039806cfd1866895b52", - "reference": "deac802f76910708ab50d039806cfd1866895b52", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "symfony/finder": "~2.0,>=2.0.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.6-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\ClassLoader\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "Symfony ClassLoader Component", - "homepage": "http://symfony.com", - "time": "2015-01-05 14:28:40" + "time": "2015-03-14 06:06:39" }, { "name": "symfony/console", @@ -2494,15 +2334,16 @@ "source": { "type": "git", "url": "https://github.com/dwightwatson/validating.git", - "reference": "359a2a76666635f8ae122ece31a5da5e798005de" + "reference": "b85ca3550a66f31685fad78b3ae085a8cea6fcdf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dwightwatson/validating/zipball/359a2a76666635f8ae122ece31a5da5e798005de", - "reference": "359a2a76666635f8ae122ece31a5da5e798005de", + "url": "https://api.github.com/repos/dwightwatson/validating/zipball/b85ca3550a66f31685fad78b3ae085a8cea6fcdf", + "reference": "b85ca3550a66f31685fad78b3ae085a8cea6fcdf", "shasum": "" }, "require": { + "illuminate/contracts": "~5.0", "illuminate/database": "~5.0", "illuminate/events": "~5.0", "illuminate/support": "~5.0", @@ -2540,7 +2381,7 @@ "laravel", "validation" ], - "time": "2014-12-10 18:32:24" + "time": "2015-03-13 05:19:15" } ], "packages-dev": [ @@ -2599,103 +2440,42 @@ "time": "2015-02-19 10:26:39" }, { - "name": "codeception/c3", - "version": "2.0.3", + "name": "barryvdh/laravel-ide-helper", + "version": "v2.0.1", "source": { "type": "git", - "url": "https://github.com/Codeception/c3.git", - "reference": "30321efb2421c5d201d02e2cb8da1a1ca96e4a38" + "url": "https://github.com/barryvdh/laravel-ide-helper.git", + "reference": "81b7febfc64168ea1af57261aa4dfc9acefd5429" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/c3/zipball/30321efb2421c5d201d02e2cb8da1a1ca96e4a38", - "reference": "30321efb2421c5d201d02e2cb8da1a1ca96e4a38", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/81b7febfc64168ea1af57261aa4dfc9acefd5429", + "reference": "81b7febfc64168ea1af57261aa4dfc9acefd5429", "shasum": "" }, "require": { - "php": ">=5.4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Codeception\\c3\\": "." - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert.php@resend.cc", - "homepage": "http://codegyre.com" - } - ], - "description": "CodeCoverage collector for Codeception", - "homepage": "http://codeception.com/", - "keywords": [ - "code coverage", - "codecoverage" - ], - "time": "2014-11-18 22:06:45" - }, - { - "name": "codeception/codeception", - "version": "2.0.11", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Codeception.git", - "reference": "9c7f23eff3e607225e9f43277c6d9cdb03d30b84" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/9c7f23eff3e607225e9f43277c6d9cdb03d30b84", - "reference": "9c7f23eff3e607225e9f43277c6d9cdb03d30b84", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-mbstring": "*", - "facebook/webdriver": "~0.4|~0.5", - "guzzlehttp/guzzle": "~4.0|~5.0", + "illuminate/console": "5.0.x", + "illuminate/filesystem": "5.0.x", + "illuminate/support": "5.0.x", "php": ">=5.4.0", - "phpunit/phpunit": "~4.5.0", - "symfony/browser-kit": "~2.4", - "symfony/console": "~2.4", - "symfony/css-selector": "~2.4", - "symfony/dom-crawler": "~2.4,!=2.4.5", - "symfony/event-dispatcher": "~2.4", - "symfony/finder": "~2.4", - "symfony/yaml": "~2.4" + "phpdocumentor/reflection-docblock": "2.0.x", + "symfony/class-loader": "~2.3" }, "require-dev": { - "codeception/specify": "~0.3", - "facebook/php-sdk": "~3.2", - "flow/jsonpath": "~0.2", - "monolog/monolog": "~1.8", - "pda/pheanstalk": "~2.0", - "videlalvaro/php-amqplib": "~2.4" + "doctrine/dbal": "~2.3" }, "suggest": { - "codeception/phpbuiltinserver": "Extension to start and stop PHP built-in web server for your tests", - "codeception/specify": "BDD-style code blocks", - "codeception/verify": "BDD-style assertions", - "monolog/monolog": "Log test steps", - "phpseclib/phpseclib": "Extension required to use the SFTP option in the FTP Module." + "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)" }, - "bin": [ - "codecept" - ], "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "2.0-dev" } }, "autoload": { - "psr-0": { - "Codeception": "src" + "psr-4": { + "Barryvdh\\LaravelIdeHelper\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2704,190 +2484,23 @@ ], "authors": [ { - "name": "Michael Bodnarchuk", - "email": "davert@mail.ua", - "homepage": "http://codegyre.com" + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" } ], - "description": "BDD-style testing framework", - "homepage": "http://codeception.com/", + "description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.", "keywords": [ - "BDD", - "TDD", - "acceptance testing", - "functional testing", - "unit testing" + "autocomplete", + "codeintel", + "helper", + "ide", + "laravel", + "netbeans", + "phpdoc", + "phpstorm", + "sublime" ], - "time": "2015-02-23 23:10:03" - }, - { - "name": "codeception/phpbuiltinserver", - "version": "v1.2.1", - "source": { - "type": "git", - "url": "https://github.com/tiger-seo/PhpBuiltinServer.git", - "reference": "730206313b7e85d9ed4838ba02a0aee24fce1239" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/tiger-seo/PhpBuiltinServer/zipball/730206313b7e85d9ed4838ba02a0aee24fce1239", - "reference": "730206313b7e85d9ed4838ba02a0aee24fce1239", - "shasum": "" - }, - "require": { - "codeception/codeception": ">=2.0.2", - "php": ">=5.4.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Codeception": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "tiger-seo", - "email": "tiger.seo@gmail.com" - } - ], - "description": "PhpBuiltinServer extension for Codeception", - "keywords": [ - "codeception" - ], - "time": "2014-09-19 10:14:07" - }, - { - "name": "codeception/specify", - "version": "0.4.1", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Specify.git", - "reference": "0c0ae07adfc231115b3b72ade22f44c23c199ded" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Specify/zipball/0c0ae07adfc231115b3b72ade22f44c23c199ded", - "reference": "0c0ae07adfc231115b3b72ade22f44c23c199ded", - "shasum": "" - }, - "require": { - "myclabs/deep-copy": "~1.1", - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Codeception\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert.php@mailican.com" - } - ], - "description": "BDD code blocks for PHPUnit and Codeception", - "time": "2014-10-17 00:06:51" - }, - { - "name": "codeception/verify", - "version": "0.2.7", - "source": { - "type": "git", - "url": "https://github.com/Codeception/Verify.git", - "reference": "66e5074905f4d9590ddb805d123fe632f4baa488" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Codeception/Verify/zipball/66e5074905f4d9590ddb805d123fe632f4baa488", - "reference": "66e5074905f4d9590ddb805d123fe632f4baa488", - "shasum": "" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*" - }, - "type": "library", - "autoload": { - "files": [ - "src/Codeception/function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "authors": [ - { - "name": "Michael Bodnarchuk", - "email": "davert.php@mailican.com", - "homepage": "http://codeception.com" - } - ], - "description": "BDD assertion library for PHPUnit", - "time": "2014-01-22 14:40:33" - }, - { - "name": "codeclimate/php-test-reporter", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/codeclimate/php-test-reporter.git", - "reference": "2a67d5d940e175fddba15f29c81a646ace26dc38" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/codeclimate/php-test-reporter/zipball/2a67d5d940e175fddba15f29c81a646ace26dc38", - "reference": "2a67d5d940e175fddba15f29c81a646ace26dc38", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "php": ">=5.3", - "satooshi/php-coveralls": "0.6.*", - "symfony/console": ">=2.0" - }, - "require-dev": { - "ext-xdebug": "*", - "phpunit/phpunit": "3.7.*@stable" - }, - "bin": [ - "composer/bin/test-reporter" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.1.x-dev" - } - }, - "autoload": { - "psr-0": { - "CodeClimate\\Component": "src/", - "CodeClimate\\Bundle": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Code Climate", - "email": "hello@codeclimate.com", - "homepage": "https://codeclimate.com" - } - ], - "description": "PHP client for reporting test coverage to Code Climate", - "homepage": "https://github.com/codeclimate/php-test-reporter", - "keywords": [ - "codeclimate", - "coverage" - ], - "time": "2015-02-20 22:40:35" + "time": "2015-02-23 15:55:54" }, { "name": "doctrine/instantiator", @@ -2943,95 +2556,6 @@ ], "time": "2014-10-13 12:58:55" }, - { - "name": "facebook/webdriver", - "version": "v0.5.1", - "source": { - "type": "git", - "url": "https://github.com/facebook/php-webdriver.git", - "reference": "bbcb697efb394d17bd9ec3d467e7da847cde4509" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/facebook/php-webdriver/zipball/bbcb697efb394d17bd9ec3d467e7da847cde4509", - "reference": "bbcb697efb394d17bd9ec3d467e7da847cde4509", - "shasum": "" - }, - "require": { - "php": ">=5.3.19" - }, - "require-dev": { - "phpdocumentor/phpdocumentor": "2.*", - "phpunit/phpunit": "3.7.*" - }, - "type": "library", - "autoload": { - "classmap": [ - "lib/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "description": "A php client for WebDriver", - "homepage": "https://github.com/facebook/php-webdriver", - "keywords": [ - "facebook", - "php", - "selenium", - "webdriver" - ], - "time": "2014-11-05 20:53:09" - }, - { - "name": "fzaninotto/faker", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/fzaninotto/Faker.git", - "reference": "010c7efedd88bf31141a02719f51fb44c732d5a0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/010c7efedd88bf31141a02719f51fb44c732d5a0", - "reference": "010c7efedd88bf31141a02719f51fb44c732d5a0", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~1.5" - }, - "type": "library", - "extra": { - "branch-alias": [] - }, - "autoload": { - "psr-0": { - "Faker": "src/", - "Faker\\PHPUnit": "test/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "François Zaninotto" - } - ], - "description": "Faker is a PHP library that generates fake data for you.", - "keywords": [ - "data", - "faker", - "fixtures" - ], - "time": "2014-06-04 14:43:02" - }, { "name": "guzzle/guzzle", "version": "v3.9.2", @@ -3124,226 +2648,6 @@ ], "time": "2014-08-11 04:32:36" }, - { - "name": "guzzlehttp/guzzle", - "version": "5.2.0", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "475b29ccd411f2fa8a408e64576418728c032cfa" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/475b29ccd411f2fa8a408e64576418728c032cfa", - "reference": "475b29ccd411f2fa8a408e64576418728c032cfa", - "shasum": "" - }, - "require": { - "guzzlehttp/ringphp": "~1.0", - "php": ">=5.4.0" - }, - "require-dev": { - "ext-curl": "*", - "phpunit/phpunit": "~4.0", - "psr/log": "~1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "time": "2015-01-28 01:03:29" - }, - { - "name": "guzzlehttp/ringphp", - "version": "1.0.6", - "source": { - "type": "git", - "url": "https://github.com/guzzle/RingPHP.git", - "reference": "f43ab34aad69ca0ba04172cf2c3cd5c12fc0e5a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/RingPHP/zipball/f43ab34aad69ca0ba04172cf2c3cd5c12fc0e5a4", - "reference": "f43ab34aad69ca0ba04172cf2c3cd5c12fc0e5a4", - "shasum": "" - }, - "require": { - "guzzlehttp/streams": "~3.0", - "php": ">=5.4.0", - "react/promise": "~2.0" - }, - "require-dev": { - "ext-curl": "*", - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "ext-curl": "Guzzle will use specific adapters if cURL is present" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Ring\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.", - "time": "2015-02-26 20:43:09" - }, - { - "name": "guzzlehttp/streams", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/guzzle/streams.git", - "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/streams/zipball/47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", - "reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Stream\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - } - ], - "description": "Provides a simple abstraction over streams of data", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "Guzzle", - "stream" - ], - "time": "2014-10-12 19:18:40" - }, - { - "name": "league/factory-muffin", - "version": "v2.1.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/factory-muffin.git", - "reference": "91f0adcdac6b5f7bf2277ac2c90f94352afe65de" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/factory-muffin/zipball/91f0adcdac6b5f7bf2277ac2c90f94352afe65de", - "reference": "91f0adcdac6b5f7bf2277ac2c90f94352afe65de", - "shasum": "" - }, - "require": { - "fzaninotto/faker": "1.4.*", - "php": ">=5.3.3" - }, - "replace": { - "zizaco/factory-muff": "self.version" - }, - "require-dev": { - "illuminate/database": "~4.1", - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "illuminate/database": "Factory Muffin works well with eloquent models." - }, - "type": "library", - "autoload": { - "psr-4": { - "League\\FactoryMuffin\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "graham@mineuk.com" - }, - { - "name": "Zizaco Zizuini", - "email": "zizaco@gmail.com" - }, - { - "name": "Scott Robertson", - "email": "scottymeuk@gmail.com" - } - ], - "description": "The goal of this package is to enable the rapid creation of objects for the purpose of testing.", - "homepage": "http://factory-muffin.thephpleague.com/", - "keywords": [ - "factory", - "laravel", - "testing" - ], - "time": "2014-09-18 18:29:06" - }, { "name": "maximebf/debugbar", "version": "v1.10.4", @@ -3401,46 +2705,53 @@ "time": "2015-02-05 07:51:20" }, { - "name": "myclabs/deep-copy", - "version": "1.2.1", + "name": "phpdocumentor/reflection-docblock", + "version": "2.0.4", "source": { "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "d93c485e71bcd22df0a994e9e3e03a3ef3a3e3f3" + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/d93c485e71bcd22df0a994e9e3e03a3ef3a3e3f3", - "reference": "d93c485e71bcd22df0a994e9e3e03a3ef3a3e3f3", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=5.3.3" }, "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" + "psr-0": { + "phpDocumentor": [ + "src/" + ] } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } ], - "time": "2014-11-20 05:11:17" + "time": "2015-02-03 12:10:50" }, { "name": "phpspec/php-diff", @@ -3982,50 +3293,6 @@ ], "time": "2014-10-03 05:12:11" }, - { - "name": "react/promise", - "version": "v2.2.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "365fcee430dfa4ace1fbc75737ca60ceea7eeeef" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/365fcee430dfa4ace1fbc75737ca60ceea7eeeef", - "reference": "365fcee430dfa4ace1fbc75737ca60ceea7eeeef", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "React\\Promise\\": "src/" - }, - "files": [ - "src/functions_include.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@googlemail.com" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "time": "2014-12-30 13:32:42" - }, { "name": "satooshi/php-coveralls", "version": "v0.6.1", @@ -4466,30 +3733,25 @@ "time": "2014-12-15 14:25:24" }, { - "name": "symfony/browser-kit", + "name": "symfony/class-loader", "version": "v2.6.4", - "target-dir": "Symfony/Component/BrowserKit", + "target-dir": "Symfony/Component/ClassLoader", "source": { "type": "git", - "url": "https://github.com/symfony/BrowserKit.git", - "reference": "2ecec44ed5047020c65dd6e4a4b2f3cf13ae3c04" + "url": "https://github.com/symfony/ClassLoader.git", + "reference": "deac802f76910708ab50d039806cfd1866895b52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/BrowserKit/zipball/2ecec44ed5047020c65dd6e4a4b2f3cf13ae3c04", - "reference": "2ecec44ed5047020c65dd6e4a4b2f3cf13ae3c04", + "url": "https://api.github.com/repos/symfony/ClassLoader/zipball/deac802f76910708ab50d039806cfd1866895b52", + "reference": "deac802f76910708ab50d039806cfd1866895b52", "shasum": "" }, "require": { - "php": ">=5.3.3", - "symfony/dom-crawler": "~2.0,>=2.0.5" + "php": ">=5.3.3" }, "require-dev": { - "symfony/css-selector": "~2.0,>=2.0.5", - "symfony/process": "~2.0,>=2.0.5" - }, - "suggest": { - "symfony/process": "" + "symfony/finder": "~2.0,>=2.0.5" }, "type": "library", "extra": { @@ -4499,7 +3761,7 @@ }, "autoload": { "psr-0": { - "Symfony\\Component\\BrowserKit\\": "" + "Symfony\\Component\\ClassLoader\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -4516,9 +3778,9 @@ "email": "fabien@symfony.com" } ], - "description": "Symfony BrowserKit Component", + "description": "Symfony ClassLoader Component", "homepage": "http://symfony.com", - "time": "2015-01-03 08:01:59" + "time": "2015-01-05 14:28:40" }, { "name": "symfony/config", @@ -4568,110 +3830,6 @@ "homepage": "http://symfony.com", "time": "2015-01-21 20:57:55" }, - { - "name": "symfony/css-selector", - "version": "v2.6.4", - "target-dir": "Symfony/Component/CssSelector", - "source": { - "type": "git", - "url": "https://github.com/symfony/CssSelector.git", - "reference": "3f80ecc614fec68d5b4a84a0703db3fdf5ce8584" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/CssSelector/zipball/3f80ecc614fec68d5b4a84a0703db3fdf5ce8584", - "reference": "3f80ecc614fec68d5b4a84a0703db3fdf5ce8584", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.6-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\CssSelector\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - }, - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "Symfony CssSelector Component", - "homepage": "http://symfony.com", - "time": "2015-01-03 08:01:59" - }, - { - "name": "symfony/dom-crawler", - "version": "v2.6.4", - "target-dir": "Symfony/Component/DomCrawler", - "source": { - "type": "git", - "url": "https://github.com/symfony/DomCrawler.git", - "reference": "26a9eb302decd828990e1015afaa11b78b016073" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/DomCrawler/zipball/26a9eb302decd828990e1015afaa11b78b016073", - "reference": "26a9eb302decd828990e1015afaa11b78b016073", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "symfony/css-selector": "~2.3" - }, - "suggest": { - "symfony/css-selector": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.6-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\DomCrawler\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "Symfony DomCrawler Component", - "homepage": "http://symfony.com", - "time": "2015-01-03 08:01:59" - }, { "name": "symfony/stopwatch", "version": "v2.6.4", @@ -4772,10 +3930,7 @@ "stability-flags": { "grumpydictator/gchart": 20, "watson/validating": 20, - "barryvdh/laravel-debugbar": 0, - "codeception/codeception": 0, - "codeception/c3": 0, - "codeclimate/php-test-reporter": 20 + "barryvdh/laravel-debugbar": 0 }, "prefer-stable": false, "prefer-lowest": false, diff --git a/config/firefly.php b/config/firefly.php index 004b9803bf..a52e464077 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -18,8 +18,8 @@ return [ ], 'accountRoles' => [ - 'defaultExpense' => 'Default expense account', - 'sharedExpense' => 'Shared expense account' + 'defaultAsset' => 'Default asset account', + 'sharedAsset' => 'Shared asset account' ], 'range_to_text' => [ diff --git a/config/mail.php b/config/mail.php index 76d5eff892..3841db29c9 100644 --- a/config/mail.php +++ b/config/mail.php @@ -15,7 +15,7 @@ return [ | */ - 'driver' => 'smtp', + 'driver' => env('EMAIL_DRIVER', 'smtp'), /* |-------------------------------------------------------------------------- diff --git a/database/migrations/2014_12_13_190730_changes_for_v321.php b/database/migrations/2014_12_13_190730_changes_for_v321.php index c5a2be3f0c..84a59ab82b 100644 --- a/database/migrations/2014_12_13_190730_changes_for_v321.php +++ b/database/migrations/2014_12_13_190730_changes_for_v321.php @@ -1,5 +1,7 @@ dropForeign('piggy_bank_events_piggy_bank_id_foreign'); $table->renameColumn('piggy_bank_id', 'piggybank_id'); + $table->foreign('piggybank_id')->references('id')->on('piggybanks')->onDelete('cascade'); } ); Schema::table( 'piggybank_repetitions', function (Blueprint $table) { + $table->dropForeign('piggy_bank_repetitions_piggy_bank_id_foreign'); $table->renameColumn('piggy_bank_id', 'piggybank_id'); + $table->foreign('piggybank_id')->references('id')->on('piggybanks')->onDelete('cascade'); } ); @@ -55,11 +59,12 @@ class ChangesForV322 extends Migration } ); - // drop foreign key from budget_limits: + // drop unique constraint from budget_limits: Schema::table( 'budget_limits', function (Blueprint $table) { $table->dropForeign('bid_foreign'); $table->dropUnique('unique_bl_combi'); + $table->foreign('budget_id', 'bid_foreign')->references('id')->on('budgets')->onDelete('cascade'); } ); diff --git a/database/migrations/2015_02_27_210653_changes_for_v332.php b/database/migrations/2015_02_27_210653_changes_for_v332.php new file mode 100644 index 0000000000..3621857c2f --- /dev/null +++ b/database/migrations/2015_02_27_210653_changes_for_v332.php @@ -0,0 +1,46 @@ +boolean('encrypted'); + + } + ); + + Schema::table( + 'reminders', function (Blueprint $table) { + $table->text('metadata'); + + } + ); + + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } + +} diff --git a/database/seeds/TestDataSeeder.php b/database/seeds/TestDataSeeder.php index 35bd3d8d9f..90b5726889 100644 --- a/database/seeds/TestDataSeeder.php +++ b/database/seeds/TestDataSeeder.php @@ -138,9 +138,9 @@ class TestDataSeeder extends Seeder $acc_c = Account::create(['user_id' => $user->id, 'account_type_id' => $assetType->id, 'name' => 'Delete me', 'active' => 1]); // create account meta: - $meta_a = AccountMeta::create(['account_id' => $acc_a->id, 'name' => 'accountRole', 'data' => 'defaultExpense']); - $meta_b = AccountMeta::create(['account_id' => $acc_b->id, 'name' => 'accountRole', 'data' => 'defaultExpense']); - $meta_c = AccountMeta::create(['account_id' => $acc_c->id, 'name' => 'accountRole', 'data' => 'defaultExpense']); + $meta_a = AccountMeta::create(['account_id' => $acc_a->id, 'name' => 'accountRole', 'data' => 'defaultAsset']); + $meta_b = AccountMeta::create(['account_id' => $acc_b->id, 'name' => 'accountRole', 'data' => 'defaultAsset']); + $meta_c = AccountMeta::create(['account_id' => $acc_c->id, 'name' => 'accountRole', 'data' => 'defaultAsset']); // var_dump($meta_a->toArray()); // var_dump($meta_b->toArray()); // var_dump($meta_c->toArray()); @@ -279,7 +279,6 @@ class TestDataSeeder extends Seeder ); // and some events! PiggyBankEvent::create(['piggy_bank_id' => $newCamera->id, 'date' => $this->som, 'amount' => 100]); - PiggyBankRepetition::create(['piggy_bank_id' => $newCamera->id, 'startdate' => $this->som, 'targetdate' => null, 'currentamount' => 100]); $newClothes = PiggyBank::create( @@ -301,27 +300,51 @@ class TestDataSeeder extends Seeder ); PiggyBankEvent::create(['piggy_bank_id' => $newClothes->id, 'date' => $this->som, 'amount' => 100]); - PiggyBankRepetition::create(['piggy_bank_id' => $newClothes->id, 'startdate' => $this->som, 'targetdate' => $end, 'currentamount' => 100]); - // weekly reminder piggy bank - $weekly = PiggyBank::create( - [ - 'account_id' => $savings->id, - 'name' => 'Weekly reminder for clothes', - 'targetamount' => 2000, - 'startdate' => $this->som, - 'targetdate' => $next, - 'repeats' => 0, - 'rep_length' => null, - 'rep_every' => 0, - 'rep_times' => null, - 'reminder' => 'week', - 'reminder_skip' => 0, - 'remind_me' => 1, - 'order' => 0, - ] - ); - PiggyBankRepetition::create(['piggy_bank_id' => $weekly->id, 'startdate' => $this->som, 'targetdate' => $next, 'currentamount' => 0]); + /* + * New: create no less than eight piggy banks that + * create all sorts of reminders + */ + $list = ['week','quarter','month','year']; + $nextYear = clone $this->_startOfMonth; + $nextYear->addYear(); + foreach($list as $entry) { + + PiggyBank::create( + [ + 'account_id' => $savings->id, + 'name' => $entry.' piggy bank with target date.', + 'targetamount' => 1000, + 'startdate' => $this->som, + 'targetdate' => $nextYear, + 'repeats' => 0, + 'rep_length' => null, + 'rep_every' => 0, + 'rep_times' => null, + 'reminder' => $entry, + 'reminder_skip' => 0, + 'remind_me' => 1, + 'order' => 0, + ] + ); + PiggyBank::create( + [ + 'account_id' => $savings->id, + 'name' => $entry.' piggy bank without target date.', + 'targetamount' => 1000, + 'startdate' => $this->som, + 'targetdate' => null, + 'repeats' => 0, + 'rep_length' => null, + 'rep_every' => 0, + 'rep_times' => null, + 'reminder' => $entry, + 'reminder_skip' => 0, + 'remind_me' => 1, + 'order' => 0, + ] + ); + } } /** @@ -331,21 +354,7 @@ class TestDataSeeder extends Seeder { $user = User::whereEmail('thegrumpydictator@gmail.com')->first(); // for weekly piggy bank (clothes) - $nextWeek = clone $this->_startOfMonth; - $piggyBank = PiggyBank::whereName('New clothes')->orderBy('id', 'DESC')->first(); - $nextWeek->addWeek(); - $week = $nextWeek->format('Y-m-d'); - Reminder::create( - ['user_id' => $user->id, 'startdate' => $this->som, 'enddate' => $week, 'active' => 1, 'notnow' => 0, - 'remindersable_id' => $piggyBank->id, 'remindersable_type' => 'PiggyBank'] - ); - - // a fake reminder:: - Reminder::create( - ['user_id' => $user->id, 'startdate' => $this->som, 'enddate' => $week, 'active' => 0, 'notnow' => 0, 'remindersable_id' => 40, - 'remindersable_type' => 'Transaction'] - ); } /** @@ -374,14 +383,6 @@ class TestDataSeeder extends Seeder 'order' => 0, ] ); - PiggyBankRepetition::create(['piggy_bank_id' => $recurring->id, 'startdate' => $this->som, 'targetdate' => $this->eom, 'currentamount' => 0]); - PiggyBankRepetition::create( - ['piggy_bank_id' => $recurring->id, 'startdate' => $this->nsom, 'targetdate' => $this->neom, 'currentamount' => 0] - ); - Reminder::create( - ['user_id' => $user->id, 'startdate' => $this->som, 'enddate' => $this->neom, 'active' => 1, 'notnow' => 0, - 'remindersable_id' => $recurring->id, 'remindersable_type' => 'PiggyBank'] - ); } /** diff --git a/phpunit.xml b/phpunit.xml index 08522be980..7fd14d7b0c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -11,12 +11,26 @@ syntaxCheck="false"> - ./tests/ + ./tests + + + ./app + + + + + + + - + \ No newline at end of file diff --git a/public/css/daterangepicker-bs3.css b/public/css/daterangepicker-bs3.css new file mode 100755 index 0000000000..c9c8fedf47 --- /dev/null +++ b/public/css/daterangepicker-bs3.css @@ -0,0 +1,319 @@ +/*! + * Stylesheet for the Date Range Picker, for use with Bootstrap 3.x + * + * Copyright 2013-2015 Dan Grossman ( http://www.dangrossman.info ) + * Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php + * + * Built for http://www.improvely.com + */ + + .daterangepicker.dropdown-menu { + max-width: none; + z-index: 3000; +} + +.daterangepicker.opensleft .ranges, .daterangepicker.opensleft .calendar { + float: left; + margin: 4px; +} + +.daterangepicker.opensright .ranges, .daterangepicker.opensright .calendar, +.daterangepicker.openscenter .ranges, .daterangepicker.openscenter .calendar { + float: right; + margin: 4px; +} + +.daterangepicker.single .ranges, .daterangepicker.single .calendar { + float: none; +} + +.daterangepicker .ranges { + width: 160px; + text-align: left; +} + +.daterangepicker .ranges .range_inputs>div { + float: left; +} + +.daterangepicker .ranges .range_inputs>div:nth-child(2) { + padding-left: 11px; +} + +.daterangepicker .calendar { + display: none; + max-width: 270px; +} + +.daterangepicker.show-calendar .calendar { + display: block; +} + +.daterangepicker .calendar.single .calendar-date { + border: none; +} + +.daterangepicker .calendar th, .daterangepicker .calendar td { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + white-space: nowrap; + text-align: center; + min-width: 32px; +} + +.daterangepicker .daterangepicker_start_input label, +.daterangepicker .daterangepicker_end_input label { + color: #333; + display: block; + font-size: 11px; + font-weight: normal; + height: 20px; + line-height: 20px; + margin-bottom: 2px; + text-shadow: #fff 1px 1px 0px; + text-transform: uppercase; + width: 74px; +} + +.daterangepicker .ranges input { + font-size: 11px; +} + +.daterangepicker .ranges .input-mini { + border: 1px solid #ccc; + border-radius: 4px; + color: #555; + display: block; + font-size: 11px; + height: 30px; + line-height: 30px; + vertical-align: middle; + margin: 0 0 10px 0; + padding: 0 6px; + width: 74px; +} + +.daterangepicker .ranges ul { + list-style: none; + margin: 0; + padding: 0; +} + +.daterangepicker .ranges li { + font-size: 13px; + background: #f5f5f5; + border: 1px solid #f5f5f5; + color: #08c; + padding: 3px 12px; + margin-bottom: 8px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + cursor: pointer; +} + +.daterangepicker .ranges li.active, .daterangepicker .ranges li:hover { + background: #08c; + border: 1px solid #08c; + color: #fff; +} + +.daterangepicker .calendar-date { + border: 1px solid #ddd; + padding: 4px; + border-radius: 4px; + background: #fff; +} + +.daterangepicker .calendar-time { + text-align: center; + margin: 8px auto 0 auto; + line-height: 30px; +} + +.daterangepicker { + position: absolute; + background: #fff; + top: 100px; + left: 20px; + padding: 4px; + margin-top: 1px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.daterangepicker.opensleft:before { + position: absolute; + top: -7px; + right: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.daterangepicker.opensleft:after { + position: absolute; + top: -6px; + right: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; + content: ''; +} + +.daterangepicker.openscenter:before { + position: absolute; + top: -7px; + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.daterangepicker.openscenter:after { + position: absolute; + top: -6px; + left: 0; + right: 0; + width: 0; + margin-left: auto; + margin-right: auto; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; + content: ''; +} + +.daterangepicker.opensright:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.daterangepicker.opensright:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; + content: ''; +} + +.daterangepicker table { + width: 100%; + margin: 0; +} + +.daterangepicker td, .daterangepicker th { + text-align: center; + width: 20px; + height: 20px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + cursor: pointer; + white-space: nowrap; +} + +.daterangepicker td.off { + color: #999; +} + +.daterangepicker td.disabled, .daterangepicker option.disabled { + color: #999; +} + +.daterangepicker td.available:hover, .daterangepicker th.available:hover { + background: #eee; +} + +.daterangepicker td.in-range { + background: #ebf4f8; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.daterangepicker td.start-date { + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.daterangepicker td.end-date { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} + +.daterangepicker td.start-date.end-date { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.daterangepicker td.active, .daterangepicker td.active:hover { + background-color: #357ebd; + border-color: #3071a9; + color: #fff; +} + +.daterangepicker td.week, .daterangepicker th.week { + font-size: 80%; + color: #ccc; +} + +.daterangepicker select.monthselect, .daterangepicker select.yearselect { + font-size: 12px; + padding: 1px; + height: auto; + margin: 0; + cursor: default; +} + +.daterangepicker select.monthselect { + margin-right: 2%; + width: 56%; +} + +.daterangepicker select.yearselect { + width: 40%; +} + +.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect { + width: 50px; + margin-bottom: 0; +} + +.daterangepicker_start_input { + float: left; +} + +.daterangepicker_end_input { + float: left; + padding-left: 11px +} + +.daterangepicker th.month { + width: auto; +} diff --git a/public/css/firefly.css b/public/css/firefly.css new file mode 100644 index 0000000000..15f7ef3e7a --- /dev/null +++ b/public/css/firefly.css @@ -0,0 +1,2 @@ +#daterange {cursor:pointer;} +.google-chart-error {height:30px;background:url('/images/error.png') no-repeat center center;} \ No newline at end of file diff --git a/public/css/sb-admin-2.css b/public/css/sb-admin-2.css index e8be396917..4a2cd36ca3 100755 --- a/public/css/sb-admin-2.css +++ b/public/css/sb-admin-2.css @@ -298,6 +298,9 @@ table.dataTable thead .sorting:after { .huge { font-size: 40px; } +.large { + font-size: 30px; +} .panel-green { border-color: #5cb85c; diff --git a/public/js/accounts.js b/public/js/accounts.js index d0830eecef..9301f828e6 100644 --- a/public/js/accounts.js +++ b/public/js/accounts.js @@ -1,7 +1,7 @@ $(function () { - if (typeof(googleLineChart) === "function" && typeof accountID !== 'undefined' && typeof view !== 'undefined') { - googleLineChart('chart/account/' + accountID + '/' + view, 'overview-chart'); + if (typeof(googleLineChart) === "function" && typeof accountID !== 'undefined') { + googleLineChart('chart/account/' + accountID, 'overview-chart'); } }); \ No newline at end of file diff --git a/public/js/categories.js b/public/js/categories.js index 694754351b..01c72143d9 100644 --- a/public/js/categories.js +++ b/public/js/categories.js @@ -1,7 +1,8 @@ $(function () { - if (typeof componentID !== 'undefined' && typeof repetitionID === 'undefined') { - googleColumnChart('chart/category/' + componentID + '/spending/' + year, 'componentOverview'); + if (typeof categoryID !== 'undefined') { + googleColumnChart('chart/category/' + categoryID + '/overview', 'componentOverview'); + googleColumnChart('chart/category/' + categoryID + '/period', 'periodOverview'); } diff --git a/public/js/daterangepicker.js b/public/js/daterangepicker.js new file mode 100755 index 0000000000..e54fd5c9f7 --- /dev/null +++ b/public/js/daterangepicker.js @@ -0,0 +1,1280 @@ +/** +* @version: 1.3.18 +* @author: Dan Grossman http://www.dangrossman.info/ +* @copyright: Copyright (c) 2012-2015 Dan Grossman. All rights reserved. +* @license: Licensed under the MIT license. See http://www.opensource.org/licenses/mit-license.php +* @website: https://www.improvely.com/ +*/ + +(function(root, factory) { + + if (typeof define === 'function' && define.amd) { + define(['moment', 'jquery', 'exports'], function(momentjs, $, exports) { + root.daterangepicker = factory(root, exports, momentjs, $); + }); + + } else if (typeof exports !== 'undefined') { + var momentjs = require('moment'); + var jQuery; + try { + jQuery = require('jquery'); + } catch (err) { + jQuery = window.jQuery; + if (!jQuery) throw new Error('jQuery dependency not found'); + } + + factory(root, exports, momentjs, jQuery); + + // Finally, as a browser global. + } else { + root.daterangepicker = factory(root, {}, root.moment, (root.jQuery || root.Zepto || root.ender || root.$)); + } + +}(this, function(root, daterangepicker, moment, $) { + + var DateRangePicker = function (element, options, cb) { + + // by default, the daterangepicker element is placed at the bottom of HTML body + this.parentEl = 'body'; + + //element that triggered the date range picker + this.element = $(element); + + //tracks visible state + this.isShowing = false; + + //create the picker HTML object + var DRPTemplate = ''; + + //custom options + if (typeof options !== 'object' || options === null) + options = {}; + + this.parentEl = (typeof options === 'object' && options.parentEl && $(options.parentEl).length) ? $(options.parentEl) : $(this.parentEl); + this.container = $(DRPTemplate).appendTo(this.parentEl); + + this.setOptions(options, cb); + + //apply CSS classes and labels to buttons + var c = this.container; + $.each(this.buttonClasses, function (idx, val) { + c.find('button').addClass(val); + }); + this.container.find('.daterangepicker_start_input label').html(this.locale.fromLabel); + this.container.find('.daterangepicker_end_input label').html(this.locale.toLabel); + if (this.applyClass.length) + this.container.find('.applyBtn').addClass(this.applyClass); + if (this.cancelClass.length) + this.container.find('.cancelBtn').addClass(this.cancelClass); + this.container.find('.applyBtn').html(this.locale.applyLabel); + this.container.find('.cancelBtn').html(this.locale.cancelLabel); + + //event listeners + + this.container.find('.calendar') + .on('click.daterangepicker', '.prev', $.proxy(this.clickPrev, this)) + .on('click.daterangepicker', '.next', $.proxy(this.clickNext, this)) + .on('click.daterangepicker', 'td.available', $.proxy(this.clickDate, this)) + .on('mouseenter.daterangepicker', 'td.available', $.proxy(this.hoverDate, this)) + .on('mouseleave.daterangepicker', 'td.available', $.proxy(this.updateFormInputs, this)) + .on('change.daterangepicker', 'select.yearselect', $.proxy(this.updateMonthYear, this)) + .on('change.daterangepicker', 'select.monthselect', $.proxy(this.updateMonthYear, this)) + .on('change.daterangepicker', 'select.hourselect,select.minuteselect,select.secondselect,select.ampmselect', $.proxy(this.updateTime, this)); + + this.container.find('.ranges') + .on('click.daterangepicker', 'button.applyBtn', $.proxy(this.clickApply, this)) + .on('click.daterangepicker', 'button.cancelBtn', $.proxy(this.clickCancel, this)) + .on('click.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.showCalendars, this)) + .on('change.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsChanged, this)) + .on('keydown.daterangepicker', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.inputsKeydown, this)) + .on('click.daterangepicker', 'li', $.proxy(this.clickRange, this)) + .on('mouseenter.daterangepicker', 'li', $.proxy(this.enterRange, this)) + .on('mouseleave.daterangepicker', 'li', $.proxy(this.updateFormInputs, this)); + + if (this.element.is('input')) { + this.element.on({ + 'click.daterangepicker': $.proxy(this.show, this), + 'focus.daterangepicker': $.proxy(this.show, this), + 'keyup.daterangepicker': $.proxy(this.updateFromControl, this) + }); + } else { + this.element.on('click.daterangepicker', $.proxy(this.toggle, this)); + } + + }; + + DateRangePicker.prototype = { + + constructor: DateRangePicker, + + setOptions: function(options, callback) { + + this.startDate = moment().startOf('day'); + this.endDate = moment().endOf('day'); + this.timeZone = moment().utcOffset(); + this.minDate = false; + this.maxDate = false; + this.dateLimit = false; + + this.showDropdowns = false; + this.showWeekNumbers = false; + this.timePicker = false; + this.timePickerSeconds = false; + this.timePickerIncrement = 30; + this.timePicker12Hour = true; + this.singleDatePicker = false; + this.ranges = {}; + + this.opens = 'right'; + if (this.element.hasClass('pull-right')) + this.opens = 'left'; + + this.buttonClasses = ['btn', 'btn-small btn-sm']; + this.applyClass = 'btn-success'; + this.cancelClass = 'btn-default'; + + this.format = 'MM/DD/YYYY'; + this.separator = ' - '; + + this.locale = { + applyLabel: 'Apply', + cancelLabel: 'Cancel', + fromLabel: 'From', + toLabel: 'To', + weekLabel: 'W', + customRangeLabel: 'Custom Range', + daysOfWeek: moment.weekdaysMin(), + monthNames: moment.monthsShort(), + firstDay: moment.localeData()._week.dow + }; + + this.cb = function () { }; + + if (typeof options.format === 'string') + this.format = options.format; + + if (typeof options.separator === 'string') + this.separator = options.separator; + + if (typeof options.startDate === 'string') + this.startDate = moment(options.startDate, this.format); + + if (typeof options.endDate === 'string') + this.endDate = moment(options.endDate, this.format); + + if (typeof options.minDate === 'string') + this.minDate = moment(options.minDate, this.format); + + if (typeof options.maxDate === 'string') + this.maxDate = moment(options.maxDate, this.format); + + if (typeof options.startDate === 'object') + this.startDate = moment(options.startDate); + + if (typeof options.endDate === 'object') + this.endDate = moment(options.endDate); + + if (typeof options.minDate === 'object') + this.minDate = moment(options.minDate); + + if (typeof options.maxDate === 'object') + this.maxDate = moment(options.maxDate); + + if (typeof options.applyClass === 'string') + this.applyClass = options.applyClass; + + if (typeof options.cancelClass === 'string') + this.cancelClass = options.cancelClass; + + if (typeof options.dateLimit === 'object') + this.dateLimit = options.dateLimit; + + if (typeof options.locale === 'object') { + + if (typeof options.locale.daysOfWeek === 'object') { + // Create a copy of daysOfWeek to avoid modification of original + // options object for reusability in multiple daterangepicker instances + this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); + } + + if (typeof options.locale.monthNames === 'object') { + this.locale.monthNames = options.locale.monthNames.slice(); + } + + if (typeof options.locale.firstDay === 'number') { + this.locale.firstDay = options.locale.firstDay; + } + + if (typeof options.locale.applyLabel === 'string') { + this.locale.applyLabel = options.locale.applyLabel; + } + + if (typeof options.locale.cancelLabel === 'string') { + this.locale.cancelLabel = options.locale.cancelLabel; + } + + if (typeof options.locale.fromLabel === 'string') { + this.locale.fromLabel = options.locale.fromLabel; + } + + if (typeof options.locale.toLabel === 'string') { + this.locale.toLabel = options.locale.toLabel; + } + + if (typeof options.locale.weekLabel === 'string') { + this.locale.weekLabel = options.locale.weekLabel; + } + + if (typeof options.locale.customRangeLabel === 'string') { + this.locale.customRangeLabel = options.locale.customRangeLabel; + } + } + + if (typeof options.opens === 'string') + this.opens = options.opens; + + if (typeof options.showWeekNumbers === 'boolean') { + this.showWeekNumbers = options.showWeekNumbers; + } + + if (typeof options.buttonClasses === 'string') { + this.buttonClasses = [options.buttonClasses]; + } + + if (typeof options.buttonClasses === 'object') { + this.buttonClasses = options.buttonClasses; + } + + if (typeof options.showDropdowns === 'boolean') { + this.showDropdowns = options.showDropdowns; + } + + if (typeof options.singleDatePicker === 'boolean') { + this.singleDatePicker = options.singleDatePicker; + if (this.singleDatePicker) { + this.endDate = this.startDate.clone(); + } + } + + if (typeof options.timePicker === 'boolean') { + this.timePicker = options.timePicker; + } + + if (typeof options.timePickerSeconds === 'boolean') { + this.timePickerSeconds = options.timePickerSeconds; + } + + if (typeof options.timePickerIncrement === 'number') { + this.timePickerIncrement = options.timePickerIncrement; + } + + if (typeof options.timePicker12Hour === 'boolean') { + this.timePicker12Hour = options.timePicker12Hour; + } + + // update day names order to firstDay + if (this.locale.firstDay != 0) { + var iterator = this.locale.firstDay; + while (iterator > 0) { + this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); + iterator--; + } + } + + var start, end, range; + + //if no start/end dates set, check if an input element contains initial values + if (typeof options.startDate === 'undefined' && typeof options.endDate === 'undefined') { + if ($(this.element).is('input[type=text]')) { + var val = $(this.element).val(), + split = val.split(this.separator); + + start = end = null; + + if (split.length == 2) { + start = moment(split[0], this.format); + end = moment(split[1], this.format); + } else if (this.singleDatePicker && val !== "") { + start = moment(val, this.format); + end = moment(val, this.format); + } + if (start !== null && end !== null) { + this.startDate = start; + this.endDate = end; + } + } + } + + // bind the time zone used to build the calendar to either the timeZone passed in through the options or the zone of the startDate (which will be the local time zone by default) + if (typeof options.timeZone === 'string' || typeof options.timeZone === 'number') { + this.timeZone = options.timeZone; + this.startDate.utcOffset(this.timeZone); + this.endDate.utcOffset(this.timeZone); + } else { + this.timeZone = moment(this.startDate).utcOffset(); + } + + if (typeof options.ranges === 'object') { + for (range in options.ranges) { + + if (typeof options.ranges[range][0] === 'string') + start = moment(options.ranges[range][0], this.format); + else + start = moment(options.ranges[range][0]); + + if (typeof options.ranges[range][1] === 'string') + end = moment(options.ranges[range][1], this.format); + else + end = moment(options.ranges[range][1]); + + // If we have a min/max date set, bound this range + // to it, but only if it would otherwise fall + // outside of the min/max. + if (this.minDate && start.isBefore(this.minDate)) + start = moment(this.minDate); + + if (this.maxDate && end.isAfter(this.maxDate)) + end = moment(this.maxDate); + + // If the end of the range is before the minimum (if min is set) OR + // the start of the range is after the max (also if set) don't display this + // range option. + if ((this.minDate && end.isBefore(this.minDate)) || (this.maxDate && start.isAfter(this.maxDate))) { + continue; + } + + this.ranges[range] = [start, end]; + } + + var list = ''; + this.container.find('.ranges ul').remove(); + this.container.find('.ranges').prepend(list); + } + + if (typeof callback === 'function') { + this.cb = callback; + } + + if (!this.timePicker) { + this.startDate = this.startDate.startOf('day'); + this.endDate = this.endDate.endOf('day'); + } + + if (this.singleDatePicker) { + this.opens = 'right'; + this.container.addClass('single'); + this.container.find('.calendar.right').show(); + this.container.find('.calendar.left').hide(); + if (!this.timePicker) { + this.container.find('.ranges').hide(); + } else { + this.container.find('.ranges .daterangepicker_start_input, .ranges .daterangepicker_end_input').hide(); + } + if (!this.container.find('.calendar.right').hasClass('single')) + this.container.find('.calendar.right').addClass('single'); + } else { + this.container.removeClass('single'); + this.container.find('.calendar.right').removeClass('single'); + this.container.find('.ranges').show(); + } + + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + this.oldChosenLabel = this.chosenLabel; + + this.leftCalendar = { + month: moment([this.startDate.year(), this.startDate.month(), 1, this.startDate.hour(), this.startDate.minute(), this.startDate.second()]), + calendar: [] + }; + + this.rightCalendar = { + month: moment([this.endDate.year(), this.endDate.month(), 1, this.endDate.hour(), this.endDate.minute(), this.endDate.second()]), + calendar: [] + }; + + if (this.opens == 'right' || this.opens == 'center') { + //swap calendar positions + var first = this.container.find('.calendar.first'); + var second = this.container.find('.calendar.second'); + + if (second.hasClass('single')) { + second.removeClass('single'); + first.addClass('single'); + } + + first.removeClass('left').addClass('right'); + second.removeClass('right').addClass('left'); + + if (this.singleDatePicker) { + first.show(); + second.hide(); + } + } + + if (typeof options.ranges === 'undefined' && !this.singleDatePicker) { + this.container.addClass('show-calendar'); + } + + this.container.addClass('opens' + this.opens); + + this.updateView(); + this.updateCalendars(); + + }, + + setStartDate: function(startDate) { + if (typeof startDate === 'string') + this.startDate = moment(startDate, this.format).utcOffset(this.timeZone); + + if (typeof startDate === 'object') + this.startDate = moment(startDate); + + if (!this.timePicker) + this.startDate = this.startDate.startOf('day'); + + this.oldStartDate = this.startDate.clone(); + + this.updateView(); + this.updateCalendars(); + this.updateInputText(); + }, + + setEndDate: function(endDate) { + if (typeof endDate === 'string') + this.endDate = moment(endDate, this.format).utcOffset(this.timeZone); + + if (typeof endDate === 'object') + this.endDate = moment(endDate); + + if (!this.timePicker) + this.endDate = this.endDate.endOf('day'); + + this.oldEndDate = this.endDate.clone(); + + this.updateView(); + this.updateCalendars(); + this.updateInputText(); + }, + + updateView: function () { + this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute()); + this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()).hour(this.endDate.hour()).minute(this.endDate.minute()); + this.updateFormInputs(); + }, + + updateFormInputs: function () { + this.container.find('input[name=daterangepicker_start]').val(this.startDate.format(this.format)); + this.container.find('input[name=daterangepicker_end]').val(this.endDate.format(this.format)); + + if (this.startDate.isSame(this.endDate) || this.startDate.isBefore(this.endDate)) { + this.container.find('button.applyBtn').removeAttr('disabled'); + } else { + this.container.find('button.applyBtn').attr('disabled', 'disabled'); + } + }, + + updateFromControl: function () { + if (!this.element.is('input')) return; + if (!this.element.val().length) return; + + var dateString = this.element.val().split(this.separator), + start = null, + end = null; + + if(dateString.length === 2) { + start = moment(dateString[0], this.format).utcOffset(this.timeZone); + end = moment(dateString[1], this.format).utcOffset(this.timeZone); + } + + if (this.singleDatePicker || start === null || end === null) { + start = moment(this.element.val(), this.format).utcOffset(this.timeZone); + end = start; + } + + if (end.isBefore(start)) return; + + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + + this.startDate = start; + this.endDate = end; + + if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) + this.notify(); + + this.updateCalendars(); + }, + + notify: function () { + this.updateView(); + this.cb(this.startDate, this.endDate, this.chosenLabel); + }, + + move: function () { + var parentOffset = { top: 0, left: 0 }; + var parentRightEdge = $(window).width(); + if (!this.parentEl.is('body')) { + parentOffset = { + top: this.parentEl.offset().top - this.parentEl.scrollTop(), + left: this.parentEl.offset().left - this.parentEl.scrollLeft() + }; + parentRightEdge = this.parentEl[0].clientWidth + this.parentEl.offset().left; + } + + if (this.opens == 'left') { + this.container.css({ + top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, + right: parentRightEdge - this.element.offset().left - this.element.outerWidth(), + left: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } + } else if (this.opens == 'center') { + this.container.css({ + top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, + left: this.element.offset().left - parentOffset.left + this.element.outerWidth() / 2 + - this.container.outerWidth() / 2, + right: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } + } else { + this.container.css({ + top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, + left: this.element.offset().left - parentOffset.left, + right: 'auto' + }); + if (this.container.offset().left + this.container.outerWidth() > $(window).width()) { + this.container.css({ + left: 'auto', + right: 0 + }); + } + } + }, + + toggle: function (e) { + if (this.element.hasClass('active')) { + this.hide(); + } else { + this.show(); + } + }, + + show: function (e) { + if (this.isShowing) return; + + this.element.addClass('active'); + this.container.show(); + this.move(); + + // Create a click proxy that is private to this instance of datepicker, for unbinding + this._outsideClickProxy = $.proxy(function (e) { this.outsideClick(e); }, this); + // Bind global datepicker mousedown for hiding and + $(document) + .on('mousedown.daterangepicker', this._outsideClickProxy) + // also support mobile devices + .on('touchend.daterangepicker', this._outsideClickProxy) + // also explicitly play nice with Bootstrap dropdowns, which stopPropagation when clicking them + .on('click.daterangepicker', '[data-toggle=dropdown]', this._outsideClickProxy) + // and also close when focus changes to outside the picker (eg. tabbing between controls) + .on('focusin.daterangepicker', this._outsideClickProxy); + + this.isShowing = true; + this.element.trigger('show.daterangepicker', this); + }, + + outsideClick: function (e) { + var target = $(e.target); + // if the page is clicked anywhere except within the daterangerpicker/button + // itself then call this.hide() + if ( + // ie modal dialog fix + e.type == "focusin" || + target.closest(this.element).length || + target.closest(this.container).length || + target.closest('.calendar-date').length + ) return; + this.hide(); + }, + + hide: function (e) { + if (!this.isShowing) return; + + $(document) + .off('.daterangepicker'); + + this.element.removeClass('active'); + this.container.hide(); + + if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) + this.notify(); + + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + + this.isShowing = false; + this.element.trigger('hide.daterangepicker', this); + }, + + enterRange: function (e) { + // mouse pointer has entered a range label + var label = e.target.innerHTML; + if (label == this.locale.customRangeLabel) { + this.updateView(); + } else { + var dates = this.ranges[label]; + this.container.find('input[name=daterangepicker_start]').val(dates[0].format(this.format)); + this.container.find('input[name=daterangepicker_end]').val(dates[1].format(this.format)); + } + }, + + showCalendars: function() { + this.container.addClass('show-calendar'); + this.move(); + this.element.trigger('showCalendar.daterangepicker', this); + }, + + hideCalendars: function() { + this.container.removeClass('show-calendar'); + this.element.trigger('hideCalendar.daterangepicker', this); + }, + + // when a date is typed into the start to end date textboxes + inputsChanged: function (e) { + var el = $(e.target); + var date = moment(el.val(), this.format); + if (!date.isValid()) return; + + var startDate, endDate; + if (el.attr('name') === 'daterangepicker_start') { + startDate = (false !== this.minDate && date.isBefore(this.minDate)) ? this.minDate : date; + endDate = this.endDate; + } else { + startDate = this.startDate; + endDate = (false !== this.maxDate && date.isAfter(this.maxDate)) ? this.maxDate : date; + } + this.setCustomDates(startDate, endDate); + }, + + inputsKeydown: function(e) { + if (e.keyCode === 13) { + this.inputsChanged(e); + this.notify(); + } + }, + + updateInputText: function() { + if (this.element.is('input') && !this.singleDatePicker) { + this.element.val(this.startDate.format(this.format) + this.separator + this.endDate.format(this.format)); + this.element.trigger('change'); + } else if (this.element.is('input')) { + this.element.val(this.endDate.format(this.format)); + this.element.trigger('change'); + } + }, + + clickRange: function (e) { + var label = e.target.innerHTML; + this.chosenLabel = label; + if (label == this.locale.customRangeLabel) { + this.showCalendars(); + } else { + var dates = this.ranges[label]; + + this.startDate = dates[0]; + this.endDate = dates[1]; + + if (!this.timePicker) { + this.startDate.startOf('day'); + this.endDate.endOf('day'); + } + + this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute()); + this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()).hour(this.endDate.hour()).minute(this.endDate.minute()); + this.updateCalendars(); + + this.updateInputText(); + + this.hideCalendars(); + this.hide(); + this.element.trigger('apply.daterangepicker', this); + } + }, + + clickPrev: function (e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.subtract(1, 'month'); + } else { + this.rightCalendar.month.subtract(1, 'month'); + } + this.updateCalendars(); + }, + + clickNext: function (e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.add(1, 'month'); + } else { + this.rightCalendar.month.add(1, 'month'); + } + this.updateCalendars(); + }, + + hoverDate: function (e) { + var title = $(e.target).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + + if (cal.hasClass('left')) { + this.container.find('input[name=daterangepicker_start]').val(this.leftCalendar.calendar[row][col].format(this.format)); + } else { + this.container.find('input[name=daterangepicker_end]').val(this.rightCalendar.calendar[row][col].format(this.format)); + } + }, + + setCustomDates: function(startDate, endDate) { + this.chosenLabel = this.locale.customRangeLabel; + if (startDate.isAfter(endDate)) { + var difference = this.endDate.diff(this.startDate); + endDate = moment(startDate).add(difference, 'ms'); + if (this.maxDate && endDate.isAfter(this.maxDate)) { + endDate = this.maxDate; + } + } + this.startDate = startDate; + this.endDate = endDate; + + this.updateView(); + this.updateCalendars(); + }, + + clickDate: function (e) { + var title = $(e.target).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + + var startDate, endDate; + if (cal.hasClass('left')) { + startDate = this.leftCalendar.calendar[row][col]; + endDate = this.endDate; + if (typeof this.dateLimit === 'object') { + var maxDate = moment(startDate).add(this.dateLimit).startOf('day'); + if (endDate.isAfter(maxDate)) { + endDate = maxDate; + } + } + } else { + startDate = this.startDate; + endDate = this.rightCalendar.calendar[row][col]; + if (typeof this.dateLimit === 'object') { + var minDate = moment(endDate).subtract(this.dateLimit).startOf('day'); + if (startDate.isBefore(minDate)) { + startDate = minDate; + } + } + } + + if (this.singleDatePicker && cal.hasClass('left')) { + endDate = startDate.clone(); + } else if (this.singleDatePicker && cal.hasClass('right')) { + startDate = endDate.clone(); + } + + cal.find('td').removeClass('active'); + + $(e.target).addClass('active'); + + this.setCustomDates(startDate, endDate); + + if (!this.timePicker) + endDate.endOf('day'); + + if (this.singleDatePicker && !this.timePicker) + this.clickApply(); + }, + + clickApply: function (e) { + this.updateInputText(); + this.hide(); + this.element.trigger('apply.daterangepicker', this); + }, + + clickCancel: function (e) { + this.startDate = this.oldStartDate; + this.endDate = this.oldEndDate; + this.chosenLabel = this.oldChosenLabel; + this.updateView(); + this.updateCalendars(); + this.hide(); + this.element.trigger('cancel.daterangepicker', this); + }, + + updateMonthYear: function (e) { + var isLeft = $(e.target).closest('.calendar').hasClass('left'), + leftOrRight = isLeft ? 'left' : 'right', + cal = this.container.find('.calendar.'+leftOrRight); + + // Month must be Number for new moment versions + var month = parseInt(cal.find('.monthselect').val(), 10); + var year = cal.find('.yearselect').val(); + + if (!isLeft) { + if (year < this.startDate.year() || (year == this.startDate.year() && month < this.startDate.month())) { + month = this.startDate.month(); + year = this.startDate.year(); + } + } + + if (this.minDate) { + if (year < this.minDate.year() || (year == this.minDate.year() && month < this.minDate.month())) { + month = this.minDate.month(); + year = this.minDate.year(); + } + } + + if (this.maxDate) { + if (year > this.maxDate.year() || (year == this.maxDate.year() && month > this.maxDate.month())) { + month = this.maxDate.month(); + year = this.maxDate.year(); + } + } + + + this[leftOrRight+'Calendar'].month.month(month).year(year); + this.updateCalendars(); + }, + + updateTime: function(e) { + + var cal = $(e.target).closest('.calendar'), + isLeft = cal.hasClass('left'); + + var hour = parseInt(cal.find('.hourselect').val(), 10); + var minute = parseInt(cal.find('.minuteselect').val(), 10); + var second = 0; + + if (this.timePickerSeconds) { + second = parseInt(cal.find('.secondselect').val(), 10); + } + + if (this.timePicker12Hour) { + var ampm = cal.find('.ampmselect').val(); + if (ampm === 'PM' && hour < 12) + hour += 12; + if (ampm === 'AM' && hour === 12) + hour = 0; + } + + if (isLeft) { + var start = this.startDate.clone(); + start.hour(hour); + start.minute(minute); + start.second(second); + this.startDate = start; + this.leftCalendar.month.hour(hour).minute(minute).second(second); + if (this.singleDatePicker) + this.endDate = start.clone(); + } else { + var end = this.endDate.clone(); + end.hour(hour); + end.minute(minute); + end.second(second); + this.endDate = end; + if (this.singleDatePicker) + this.startDate = end.clone(); + this.rightCalendar.month.hour(hour).minute(minute).second(second); + } + + this.updateView(); + this.updateCalendars(); + }, + + updateCalendars: function () { + this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.month(), this.leftCalendar.month.year(), this.leftCalendar.month.hour(), this.leftCalendar.month.minute(), this.leftCalendar.month.second(), 'left'); + this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.month(), this.rightCalendar.month.year(), this.rightCalendar.month.hour(), this.rightCalendar.month.minute(), this.rightCalendar.month.second(), 'right'); + this.container.find('.calendar.left').empty().html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.maxDate, 'left')); + this.container.find('.calendar.right').empty().html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.singleDatePicker ? this.minDate : this.startDate, this.maxDate, 'right')); + + this.container.find('.ranges li').removeClass('active'); + var customRange = true; + var i = 0; + for (var range in this.ranges) { + if (this.timePicker) { + if (this.startDate.isSame(this.ranges[range][0]) && this.endDate.isSame(this.ranges[range][1])) { + customRange = false; + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')') + .addClass('active').html(); + } + } else { + //ignore times when comparing dates if time picker is not enabled + if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) { + customRange = false; + this.chosenLabel = this.container.find('.ranges li:eq(' + i + ')') + .addClass('active').html(); + } + } + i++; + } + if (customRange) { + this.chosenLabel = this.container.find('.ranges li:last').addClass('active').html(); + this.showCalendars(); + } + }, + + buildCalendar: function (month, year, hour, minute, second, side) { + var daysInMonth = moment([year, month]).daysInMonth(); + var firstDay = moment([year, month, 1]); + var lastDay = moment([year, month, daysInMonth]); + var lastMonth = moment(firstDay).subtract(1, 'month').month(); + var lastYear = moment(firstDay).subtract(1, 'month').year(); + + var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth(); + + var dayOfWeek = firstDay.day(); + + var i; + + //initialize a 6 rows x 7 columns array for the calendar + var calendar = []; + calendar.firstDay = firstDay; + calendar.lastDay = lastDay; + + for (i = 0; i < 6; i++) { + calendar[i] = []; + } + + //populate the calendar with date objects + var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1; + if (startDay > daysInLastMonth) + startDay -= 7; + + if (dayOfWeek == this.locale.firstDay) + startDay = daysInLastMonth - 6; + + var curDate = moment([lastYear, lastMonth, startDay, 12, minute, second]).utcOffset(this.timeZone); + + var col, row; + for (i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add(24, 'hour')) { + if (i > 0 && col % 7 === 0) { + col = 0; + row++; + } + calendar[row][col] = curDate.clone().hour(hour); + curDate.hour(12); + + if (this.minDate && calendar[row][col].format('YYYY-MM-DD') == this.minDate.format('YYYY-MM-DD') && calendar[row][col].isBefore(this.minDate) && side == 'left') { + calendar[row][col] = this.minDate.clone(); + } + + if (this.maxDate && calendar[row][col].format('YYYY-MM-DD') == this.maxDate.format('YYYY-MM-DD') && calendar[row][col].isAfter(this.maxDate) && side == 'right') { + calendar[row][col] = this.maxDate.clone(); + } + + } + + return calendar; + }, + + renderDropdowns: function (selected, minDate, maxDate) { + var currentMonth = selected.month(); + var currentYear = selected.year(); + var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); + var minYear = (minDate && minDate.year()) || (currentYear - 50); + + var monthHtml = '"; + + var yearHtml = ''; + + return monthHtml + yearHtml; + }, + + renderCalendar: function (calendar, selected, minDate, maxDate, side) { + + var html = '
'; + html += ''; + html += ''; + html += ''; + + // add empty cell for week number + if (this.showWeekNumbers) + html += ''; + + if (!minDate || minDate.isBefore(calendar.firstDay)) { + html += ''; + } else { + html += ''; + } + + var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY"); + + if (this.showDropdowns) { + dateHtml = this.renderDropdowns(calendar[1][1], minDate, maxDate); + } + + html += ''; + if (!maxDate || maxDate.isAfter(calendar.lastDay)) { + html += ''; + } else { + html += ''; + } + + html += ''; + html += ''; + + // add week number label + if (this.showWeekNumbers) + html += ''; + + $.each(this.locale.daysOfWeek, function (index, dayOfWeek) { + html += ''; + }); + + html += ''; + html += ''; + html += ''; + + for (var row = 0; row < 6; row++) { + html += ''; + + // add week number + if (this.showWeekNumbers) + html += ''; + + for (var col = 0; col < 7; col++) { + var cname = 'available '; + cname += (calendar[row][col].month() == calendar[1][1].month()) ? '' : 'off'; + + if ((minDate && calendar[row][col].isBefore(minDate, 'day')) || (maxDate && calendar[row][col].isAfter(maxDate, 'day'))) { + cname = ' off disabled '; + } else if (calendar[row][col].format('YYYY-MM-DD') == selected.format('YYYY-MM-DD')) { + cname += ' active '; + if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD')) { + cname += ' start-date '; + } + if (calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD')) { + cname += ' end-date '; + } + } else if (calendar[row][col] >= this.startDate && calendar[row][col] <= this.endDate) { + cname += ' in-range '; + if (calendar[row][col].isSame(this.startDate)) { cname += ' start-date '; } + if (calendar[row][col].isSame(this.endDate)) { cname += ' end-date '; } + } + + var title = 'r' + row + 'c' + col; + html += ''; + } + html += ''; + } + + html += ''; + html += '
' + dateHtml + '
' + this.locale.weekLabel + '' + dayOfWeek + '
' + calendar[row][0].week() + '' + calendar[row][col].date() + '
'; + html += '
'; + + var i; + if (this.timePicker) { + + html += '
'; + html += ' : '; + + html += ' '; + + if (this.timePickerSeconds) { + html += ': '; + } + + if (this.timePicker12Hour) { + html += ''; + } + + html += '
'; + + } + + return html; + + }, + + remove: function() { + + this.container.remove(); + this.element.off('.daterangepicker'); + this.element.removeData('daterangepicker'); + + } + + }; + + $.fn.daterangepicker = function (options, cb) { + this.each(function () { + var el = $(this); + if (el.data('daterangepicker')) + el.data('daterangepicker').remove(); + el.data('daterangepicker', new DateRangePicker(el, options, cb)); + }); + return this; + }; + +})); diff --git a/public/js/firefly.js b/public/js/firefly.js index 871eb221b4..e0a35830ea 100644 --- a/public/js/firefly.js +++ b/public/js/firefly.js @@ -1,6 +1,46 @@ $(function () { - $('.currencySelect').click(currencySelect) + $('.currencySelect').click(currencySelect); + + ranges = {}; + ranges[currentMonthName] = [moment().startOf('month'), moment().endOf('month')]; + ranges[previousMonthName] = [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]; + ranges[nextMonthName] = [moment().add(1, 'month').startOf('month'), moment().add(1, 'month').endOf('month')]; + ranges['Everything'] = [firstDate, moment()]; + + $('#daterange').daterangepicker( + { + //View::share('currentMonthName', $current); + //View::share('previousMonthName', $prev); + //View::share('nextMonthName', $next); + + + ranges: ranges + , + opens: 'left', + + format: 'DD-MM-YYYY', + startDate: start, + endDate: end + }, + function (start, end, label) { + + // send post. + $.post(dateRangeURL, { + start: start.format('YYYY-MM-DD'), + end: end.format('YYYY-MM-DD'), + label: label, + _token: token + }).success(function () { + window.location.reload(true); + }).fail(function () { + alert('Could not change date range'); + + }); + + //alert('A date range was chosen: ' + start.format('YYYY-MM-DD') + ' to ' + end.format('YYYY-MM-DD')); + } + ); }); @@ -21,4 +61,5 @@ function currencySelect(e) { return false; -} \ No newline at end of file +} + diff --git a/public/js/index.js b/public/js/index.js index 3328f23d6f..bc4148eac5 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -3,7 +3,22 @@ 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/bills','bills-chart') + googleBarChart('chart/home/budgets', 'budgets-chart'); + googleColumnChart('chart/home/categories', 'categories-chart'); + googlePieChart('chart/home/bills', 'bills-chart'); + getBoxAmounts(); +} + +function getBoxAmounts() { + var boxes = ['in', 'out','bills-unpaid','bills-paid']; + for (x in boxes) { + var box = boxes[x]; + $.getJSON('/json/box', {box: box}).success(function (data) { + if(data.amount_raw != 0) { + $('#box-' + data.box).html(data.amount); + } + }).fail(function () { + console.log('Failed to get box!') + }); + } } diff --git a/public/js/moment.min.js b/public/js/moment.min.js new file mode 100644 index 0000000000..024d488fbc --- /dev/null +++ b/public/js/moment.min.js @@ -0,0 +1,7 @@ +//! moment.js +//! version : 2.9.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +(function(a){function b(a,b,c){switch(arguments.length){case 2:return null!=a?a:b;case 3:return null!=a?a:null!=b?b:c;default:throw new Error("Implement me")}}function c(a,b){return Bb.call(a,b)}function d(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function e(a){vb.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+a)}function f(a,b){var c=!0;return o(function(){return c&&(e(a),c=!1),b.apply(this,arguments)},b)}function g(a,b){sc[a]||(e(b),sc[a]=!0)}function h(a,b){return function(c){return r(a.call(this,c),b)}}function i(a,b){return function(c){return this.localeData().ordinal(a.call(this,c),b)}}function j(a,b){var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),f=a.clone().add(e,"months");return 0>b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)}function k(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function l(){}function m(a,b){b!==!1&&H(a),p(this,a),this._d=new Date(+a._d),uc===!1&&(uc=!0,vb.updateOffset(this),uc=!1)}function n(a){var b=A(a),c=b.year||0,d=b.quarter||0,e=b.month||0,f=b.week||0,g=b.day||0,h=b.hour||0,i=b.minute||0,j=b.second||0,k=b.millisecond||0;this._milliseconds=+k+1e3*j+6e4*i+36e5*h,this._days=+g+7*f,this._months=+e+3*d+12*c,this._data={},this._locale=vb.localeData(),this._bubble()}function o(a,b){for(var d in b)c(b,d)&&(a[d]=b[d]);return c(b,"toString")&&(a.toString=b.toString),c(b,"valueOf")&&(a.valueOf=b.valueOf),a}function p(a,b){var c,d,e;if("undefined"!=typeof b._isAMomentObject&&(a._isAMomentObject=b._isAMomentObject),"undefined"!=typeof b._i&&(a._i=b._i),"undefined"!=typeof b._f&&(a._f=b._f),"undefined"!=typeof b._l&&(a._l=b._l),"undefined"!=typeof b._strict&&(a._strict=b._strict),"undefined"!=typeof b._tzm&&(a._tzm=b._tzm),"undefined"!=typeof b._isUTC&&(a._isUTC=b._isUTC),"undefined"!=typeof b._offset&&(a._offset=b._offset),"undefined"!=typeof b._pf&&(a._pf=b._pf),"undefined"!=typeof b._locale&&(a._locale=b._locale),Kb.length>0)for(c in Kb)d=Kb[c],e=b[d],"undefined"!=typeof e&&(a[d]=e);return a}function q(a){return 0>a?Math.ceil(a):Math.floor(a)}function r(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.lengthd;d++)(c&&a[d]!==b[d]||!c&&C(a[d])!==C(b[d]))&&g++;return g+f}function z(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=lc[a]||mc[b]||b}return a}function A(a){var b,d,e={};for(d in a)c(a,d)&&(b=z(d),b&&(e[b]=a[d]));return e}function B(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}vb[b]=function(e,f){var g,h,i=vb._locale[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=vb().utc().set(d,a);return i.call(vb._locale,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function C(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function D(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function E(a,b,c){return jb(vb([a,11,31+b-c]),b,c).week}function F(a){return G(a)?366:365}function G(a){return a%4===0&&a%100!==0||a%400===0}function H(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[Db]<0||a._a[Db]>11?Db:a._a[Eb]<1||a._a[Eb]>D(a._a[Cb],a._a[Db])?Eb:a._a[Fb]<0||a._a[Fb]>24||24===a._a[Fb]&&(0!==a._a[Gb]||0!==a._a[Hb]||0!==a._a[Ib])?Fb:a._a[Gb]<0||a._a[Gb]>59?Gb:a._a[Hb]<0||a._a[Hb]>59?Hb:a._a[Ib]<0||a._a[Ib]>999?Ib:-1,a._pf._overflowDayOfYear&&(Cb>b||b>Eb)&&(b=Eb),a._pf.overflow=b)}function I(b){return null==b._isValid&&(b._isValid=!isNaN(b._d.getTime())&&b._pf.overflow<0&&!b._pf.empty&&!b._pf.invalidMonth&&!b._pf.nullInput&&!b._pf.invalidFormat&&!b._pf.userInvalidated,b._strict&&(b._isValid=b._isValid&&0===b._pf.charsLeftOver&&0===b._pf.unusedTokens.length&&b._pf.bigHour===a)),b._isValid}function J(a){return a?a.toLowerCase().replace("_","-"):a}function K(a){for(var b,c,d,e,f=0;f0;){if(d=L(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&y(e,c,!0)>=b-1)break;b--}f++}return null}function L(a){var b=null;if(!Jb[a]&&Lb)try{b=vb.locale(),require("./locale/"+a),vb.locale(b)}catch(c){}return Jb[a]}function M(a,b){var c,d;return b._isUTC?(c=b.clone(),d=(vb.isMoment(a)||x(a)?+a:+vb(a))-+c,c._d.setTime(+c._d+d),vb.updateOffset(c,!1),c):vb(a).local()}function N(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function O(a){var b,c,d=a.match(Pb);for(b=0,c=d.length;c>b;b++)d[b]=rc[d[b]]?rc[d[b]]:N(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function P(a,b){return a.isValid()?(b=Q(b,a.localeData()),nc[b]||(nc[b]=O(b)),nc[b](a)):a.localeData().invalidDate()}function Q(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Qb.lastIndex=0;d>=0&&Qb.test(a);)a=a.replace(Qb,c),Qb.lastIndex=0,d-=1;return a}function R(a,b){var c,d=b._strict;switch(a){case"Q":return _b;case"DDDD":return bc;case"YYYY":case"GGGG":case"gggg":return d?cc:Tb;case"Y":case"G":case"g":return ec;case"YYYYYY":case"YYYYY":case"GGGGG":case"ggggg":return d?dc:Ub;case"S":if(d)return _b;case"SS":if(d)return ac;case"SSS":if(d)return bc;case"DDD":return Sb;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return Wb;case"a":case"A":return b._locale._meridiemParse;case"x":return Zb;case"X":return $b;case"Z":case"ZZ":return Xb;case"T":return Yb;case"SSSS":return Vb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"ww":case"WW":return d?ac:Rb;case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"W":case"e":case"E":return Rb;case"Do":return d?b._locale._ordinalParse:b._locale._ordinalParseLenient;default:return c=new RegExp($(Z(a.replace("\\","")),"i"))}}function S(a){a=a||"";var b=a.match(Xb)||[],c=b[b.length-1]||[],d=(c+"").match(jc)||["-",0,0],e=+(60*d[1])+C(d[2]);return"+"===d[0]?e:-e}function T(a,b,c){var d,e=c._a;switch(a){case"Q":null!=b&&(e[Db]=3*(C(b)-1));break;case"M":case"MM":null!=b&&(e[Db]=C(b)-1);break;case"MMM":case"MMMM":d=c._locale.monthsParse(b,a,c._strict),null!=d?e[Db]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[Eb]=C(b));break;case"Do":null!=b&&(e[Eb]=C(parseInt(b.match(/\d{1,2}/)[0],10)));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=C(b));break;case"YY":e[Cb]=vb.parseTwoDigitYear(b);break;case"YYYY":case"YYYYY":case"YYYYYY":e[Cb]=C(b);break;case"a":case"A":c._meridiem=b;break;case"h":case"hh":c._pf.bigHour=!0;case"H":case"HH":e[Fb]=C(b);break;case"m":case"mm":e[Gb]=C(b);break;case"s":case"ss":e[Hb]=C(b);break;case"S":case"SS":case"SSS":case"SSSS":e[Ib]=C(1e3*("0."+b));break;case"x":c._d=new Date(C(b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=S(b);break;case"dd":case"ddd":case"dddd":d=c._locale.weekdaysParse(b),null!=d?(c._w=c._w||{},c._w.d=d):c._pf.invalidWeekday=b;break;case"w":case"ww":case"W":case"WW":case"d":case"e":case"E":a=a.substr(0,1);case"gggg":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=C(b));break;case"gg":case"GG":c._w=c._w||{},c._w[a]=vb.parseTwoDigitYear(b)}}function U(a){var c,d,e,f,g,h,i;c=a._w,null!=c.GG||null!=c.W||null!=c.E?(g=1,h=4,d=b(c.GG,a._a[Cb],jb(vb(),1,4).year),e=b(c.W,1),f=b(c.E,1)):(g=a._locale._week.dow,h=a._locale._week.doy,d=b(c.gg,a._a[Cb],jb(vb(),g,h).year),e=b(c.w,1),null!=c.d?(f=c.d,g>f&&++e):f=null!=c.e?c.e+g:g),i=kb(d,e,f,h,g),a._a[Cb]=i.year,a._dayOfYear=i.dayOfYear}function V(a){var c,d,e,f,g=[];if(!a._d){for(e=X(a),a._w&&null==a._a[Eb]&&null==a._a[Db]&&U(a),a._dayOfYear&&(f=b(a._a[Cb],e[Cb]),a._dayOfYear>F(f)&&(a._pf._overflowDayOfYear=!0),d=fb(f,0,a._dayOfYear),a._a[Db]=d.getUTCMonth(),a._a[Eb]=d.getUTCDate()),c=0;3>c&&null==a._a[c];++c)a._a[c]=g[c]=e[c];for(;7>c;c++)a._a[c]=g[c]=null==a._a[c]?2===c?1:0:a._a[c];24===a._a[Fb]&&0===a._a[Gb]&&0===a._a[Hb]&&0===a._a[Ib]&&(a._nextDay=!0,a._a[Fb]=0),a._d=(a._useUTC?fb:eb).apply(null,g),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[Fb]=24)}}function W(a){var b;a._d||(b=A(a._i),a._a=[b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],V(a))}function X(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function Y(b){if(b._f===vb.ISO_8601)return void ab(b);b._a=[],b._pf.empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=Q(b._f,b._locale).match(Pb)||[],c=0;c0&&b._pf.unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length),rc[f]?(d?b._pf.empty=!1:b._pf.unusedTokens.push(f),T(f,d,b)):b._strict&&!d&&b._pf.unusedTokens.push(f);b._pf.charsLeftOver=i-j,h.length>0&&b._pf.unusedInput.push(h),b._pf.bigHour===!0&&b._a[Fb]<=12&&(b._pf.bigHour=a),b._a[Fb]=k(b._locale,b._a[Fb],b._meridiem),V(b),H(b)}function Z(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function $(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function _(a){var b,c,e,f,g;if(0===a._f.length)return a._pf.invalidFormat=!0,void(a._d=new Date(0/0));for(f=0;fg)&&(e=g,c=b));o(a,c||b)}function ab(a){var b,c,d=a._i,e=fc.exec(d);if(e){for(a._pf.iso=!0,b=0,c=hc.length;c>b;b++)if(hc[b][1].exec(d)){a._f=hc[b][0]+(e[6]||" ");break}for(b=0,c=ic.length;c>b;b++)if(ic[b][1].exec(d)){a._f+=ic[b][0];break}d.match(Xb)&&(a._f+="Z"),Y(a)}else a._isValid=!1}function bb(a){ab(a),a._isValid===!1&&(delete a._isValid,vb.createFromInputFallback(a))}function cb(a,b){var c,d=[];for(c=0;ca&&h.setFullYear(a),h}function fb(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function gb(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function hb(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function ib(a,b,c){var d=vb.duration(a).abs(),e=Ab(d.as("s")),f=Ab(d.as("m")),g=Ab(d.as("h")),h=Ab(d.as("d")),i=Ab(d.as("M")),j=Ab(d.as("y")),k=e0,k[4]=c,hb.apply({},k)}function jb(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=vb(a).add(f,"d"),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function kb(a,b,c,d,e){var f,g,h=fb(a,0,1).getUTCDay();return h=0===h?7:h,c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:F(a-1)+g}}function lb(b){var c,d=b._i,e=b._f;return b._locale=b._locale||vb.localeData(b._l),null===d||e===a&&""===d?vb.invalid({nullInput:!0}):("string"==typeof d&&(b._i=d=b._locale.preparse(d)),vb.isMoment(d)?new m(d,!0):(e?w(e)?_(b):Y(b):db(b),c=new m(b),c._nextDay&&(c.add(1,"d"),c._nextDay=a),c))}function mb(a,b){var c,d;if(1===b.length&&w(b[0])&&(b=b[0]),!b.length)return vb();for(c=b[0],d=1;d=0?"+":"-";return b+r(Math.abs(a),6)},gg:function(){return r(this.weekYear()%100,2)},gggg:function(){return r(this.weekYear(),4)},ggggg:function(){return r(this.weekYear(),5)},GG:function(){return r(this.isoWeekYear()%100,2)},GGGG:function(){return r(this.isoWeekYear(),4)},GGGGG:function(){return r(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.localeData().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return C(this.milliseconds()/100)},SS:function(){return r(C(this.milliseconds()/10),2)},SSS:function(){return r(this.milliseconds(),3)},SSSS:function(){return r(this.milliseconds(),3)},Z:function(){var a=this.utcOffset(),b="+";return 0>a&&(a=-a,b="-"),b+r(C(a/60),2)+":"+r(C(a)%60,2)},ZZ:function(){var a=this.utcOffset(),b="+";return 0>a&&(a=-a,b="-"),b+r(C(a/60),2)+r(C(a)%60,2)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},x:function(){return this.valueOf()},X:function(){return this.unix()},Q:function(){return this.quarter()}},sc={},tc=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"],uc=!1;pc.length;)xb=pc.pop(),rc[xb+"o"]=i(rc[xb],xb);for(;qc.length;)xb=qc.pop(),rc[xb+xb]=h(rc[xb],2);rc.DDDD=h(rc.DDD,3),o(l.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a,b,c){var d,e,f;for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),d=0;12>d;d++){if(e=vb.utc([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=vb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY LT",LLLL:"dddd, MMMM D, YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b,c){var d=this._calendar[a];return"function"==typeof d?d.apply(b,[c]):d},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",_ordinalParse:/\d{1,2}/,preparse:function(a){return a},postformat:function(a){return a},week:function(a){return jb(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},firstDayOfWeek:function(){return this._week.dow},firstDayOfYear:function(){return this._week.doy},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),vb=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._i=b,g._f=c,g._l=e,g._strict=f,g._isUTC=!1,g._pf=d(),lb(g)},vb.suppressDeprecationWarnings=!1,vb.createFromInputFallback=f("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),vb.min=function(){var a=[].slice.call(arguments,0);return mb("isBefore",a)},vb.max=function(){var a=[].slice.call(arguments,0);return mb("isAfter",a)},vb.utc=function(b,c,e,f){var g;return"boolean"==typeof e&&(f=e,e=a),g={},g._isAMomentObject=!0,g._useUTC=!0,g._isUTC=!0,g._l=e,g._i=b,g._f=c,g._strict=f,g._pf=d(),lb(g).utc()},vb.unix=function(a){return vb(1e3*a)},vb.duration=function(a,b){var d,e,f,g,h=a,i=null;return vb.isDuration(a)?h={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(h={},b?h[b]=a:h.milliseconds=a):(i=Nb.exec(a))?(d="-"===i[1]?-1:1,h={y:0,d:C(i[Eb])*d,h:C(i[Fb])*d,m:C(i[Gb])*d,s:C(i[Hb])*d,ms:C(i[Ib])*d}):(i=Ob.exec(a))?(d="-"===i[1]?-1:1,f=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*d},h={y:f(i[2]),M:f(i[3]),d:f(i[4]),h:f(i[5]),m:f(i[6]),s:f(i[7]),w:f(i[8])}):null==h?h={}:"object"==typeof h&&("from"in h||"to"in h)&&(g=t(vb(h.from),vb(h.to)),h={},h.ms=g.milliseconds,h.M=g.months),e=new n(h),vb.isDuration(a)&&c(a,"_locale")&&(e._locale=a._locale),e},vb.version=yb,vb.defaultFormat=gc,vb.ISO_8601=function(){},vb.momentProperties=Kb,vb.updateOffset=function(){},vb.relativeTimeThreshold=function(b,c){return oc[b]===a?!1:c===a?oc[b]:(oc[b]=c,!0)},vb.lang=f("moment.lang is deprecated. Use moment.locale instead.",function(a,b){return vb.locale(a,b)}),vb.locale=function(a,b){var c;return a&&(c="undefined"!=typeof b?vb.defineLocale(a,b):vb.localeData(a),c&&(vb.duration._locale=vb._locale=c)),vb._locale._abbr},vb.defineLocale=function(a,b){return null!==b?(b.abbr=a,Jb[a]||(Jb[a]=new l),Jb[a].set(b),vb.locale(a),Jb[a]):(delete Jb[a],null)},vb.langData=f("moment.langData is deprecated. Use moment.localeData instead.",function(a){return vb.localeData(a)}),vb.localeData=function(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return vb._locale;if(!w(a)){if(b=L(a))return b;a=[a]}return K(a)},vb.isMoment=function(a){return a instanceof m||null!=a&&c(a,"_isAMomentObject")},vb.isDuration=function(a){return a instanceof n};for(xb=tc.length-1;xb>=0;--xb)B(tc[xb]);vb.normalizeUnits=function(a){return z(a)},vb.invalid=function(a){var b=vb.utc(0/0);return null!=a?o(b._pf,a):b._pf.userInvalidated=!0,b},vb.parseZone=function(){return vb.apply(null,arguments).parseZone()},vb.parseTwoDigitYear=function(a){return C(a)+(C(a)>68?1900:2e3)},vb.isDate=x,o(vb.fn=m.prototype,{clone:function(){return vb(this)},valueOf:function(){return+this._d-6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){var a=vb(this).utc();return 00:!1},parsingFlags:function(){return o({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(a){return this.utcOffset(0,a)},local:function(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(this._dateUtcOffset(),"m")),this},format:function(a){var b=P(this,a||vb.defaultFormat);return this.localeData().postformat(b)},add:u(1,"add"),subtract:u(-1,"subtract"),diff:function(a,b,c){var d,e,f=M(a,this),g=6e4*(f.utcOffset()-this.utcOffset());return b=z(b),"year"===b||"month"===b||"quarter"===b?(e=j(this,f),"quarter"===b?e/=3:"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:q(e)},from:function(a,b){return vb.duration({to:this,from:a}).locale(this.locale()).humanize(!b)},fromNow:function(a){return this.from(vb(),a)},calendar:function(a){var b=a||vb(),c=M(b,this).startOf("day"),d=this.diff(c,"days",!0),e=-6>d?"sameElse":-1>d?"lastWeek":0>d?"lastDay":1>d?"sameDay":2>d?"nextDay":7>d?"nextWeek":"sameElse";return this.format(this.localeData().calendar(e,this,vb(b)))},isLeapYear:function(){return G(this.year())},isDST:function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},day:function(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=gb(a,this.localeData()),this.add(a-b,"d")):b},month:qb("Month",!0),startOf:function(a){switch(a=z(a)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a?this.weekday(0):"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this},endOf:function(b){return b=z(b),b===a||"millisecond"===b?this:this.startOf(b).add(1,"isoWeek"===b?"week":b).subtract(1,"ms")},isAfter:function(a,b){var c;return b=z("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=vb.isMoment(a)?a:vb(a),+this>+a):(c=vb.isMoment(a)?+a:+vb(a),c<+this.clone().startOf(b))},isBefore:function(a,b){var c;return b=z("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=vb.isMoment(a)?a:vb(a),+a>+this):(c=vb.isMoment(a)?+a:+vb(a),+this.clone().endOf(b)a?this:a}),max:f("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(a){return a=vb.apply(null,arguments),a>this?this:a}),zone:f("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",function(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}),utcOffset:function(a,b){var c,d=this._offset||0;return null!=a?("string"==typeof a&&(a=S(a)),Math.abs(a)<16&&(a=60*a),!this._isUTC&&b&&(c=this._dateUtcOffset()),this._offset=a,this._isUTC=!0,null!=c&&this.add(c,"m"),d!==a&&(!b||this._changeInProgress?v(this,vb.duration(a-d,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,vb.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?d:this._dateUtcOffset()},isLocal:function(){return!this._isUTC},isUtcOffset:function(){return this._isUTC},isUtc:function(){return this._isUTC&&0===this._offset},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(S(this._i)),this},hasAlignedHourOffset:function(a){return a=a?vb(a).utcOffset():0,(this.utcOffset()-a)%60===0},daysInMonth:function(){return D(this.year(),this.month())},dayOfYear:function(a){var b=Ab((vb(this).startOf("day")-vb(this).startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")},quarter:function(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)},weekYear:function(a){var b=jb(this,this.localeData()._week.dow,this.localeData()._week.doy).year;return null==a?b:this.add(a-b,"y")},isoWeekYear:function(a){var b=jb(this,1,4).year;return null==a?b:this.add(a-b,"y")},week:function(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")},isoWeek:function(a){var b=jb(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")},weekday:function(a){var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},isoWeeksInYear:function(){return E(this.year(),1,4)},weeksInYear:function(){var a=this.localeData()._week;return E(this.year(),a.dow,a.doy)},get:function(a){return a=z(a),this[a]()},set:function(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else a=z(a),"function"==typeof this[a]&&this[a](b);return this},locale:function(b){var c;return b===a?this._locale._abbr:(c=vb.localeData(b),null!=c&&(this._locale=c),this)},lang:f("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(b){return b===a?this.localeData():this.locale(b)}),localeData:function(){return this._locale},_dateUtcOffset:function(){return 15*-Math.round(this._d.getTimezoneOffset()/15)}}),vb.fn.millisecond=vb.fn.milliseconds=qb("Milliseconds",!1),vb.fn.second=vb.fn.seconds=qb("Seconds",!1),vb.fn.minute=vb.fn.minutes=qb("Minutes",!1),vb.fn.hour=vb.fn.hours=qb("Hours",!0),vb.fn.date=qb("Date",!0),vb.fn.dates=f("dates accessor is deprecated. Use date instead.",qb("Date",!0)),vb.fn.year=qb("FullYear",!0),vb.fn.years=f("years accessor is deprecated. Use year instead.",qb("FullYear",!0)),vb.fn.days=vb.fn.day,vb.fn.months=vb.fn.month,vb.fn.weeks=vb.fn.week,vb.fn.isoWeeks=vb.fn.isoWeek,vb.fn.quarters=vb.fn.quarter,vb.fn.toJSON=vb.fn.toISOString,vb.fn.isUTC=vb.fn.isUtc,o(vb.duration.fn=n.prototype,{_bubble:function(){var a,b,c,d=this._milliseconds,e=this._days,f=this._months,g=this._data,h=0;g.milliseconds=d%1e3,a=q(d/1e3),g.seconds=a%60,b=q(a/60),g.minutes=b%60,c=q(b/60),g.hours=c%24,e+=q(c/24),h=q(rb(e)),e-=q(sb(h)),f+=q(e/30),e%=30,h+=q(f/12),f%=12,g.days=e,g.months=f,g.years=h},abs:function(){return this._milliseconds=Math.abs(this._milliseconds),this._days=Math.abs(this._days),this._months=Math.abs(this._months),this._data.milliseconds=Math.abs(this._data.milliseconds),this._data.seconds=Math.abs(this._data.seconds),this._data.minutes=Math.abs(this._data.minutes),this._data.hours=Math.abs(this._data.hours),this._data.months=Math.abs(this._data.months),this._data.years=Math.abs(this._data.years),this},weeks:function(){return q(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*C(this._months/12) +},humanize:function(a){var b=ib(this,!a,this.localeData());return a&&(b=this.localeData().pastFuture(+this,b)),this.localeData().postformat(b)},add:function(a,b){var c=vb.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=vb.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=z(a),this[a.toLowerCase()+"s"]()},as:function(a){var b,c;if(a=z(a),"month"===a||"year"===a)return b=this._days+this._milliseconds/864e5,c=this._months+12*rb(b),"month"===a?c:c/12;switch(b=this._days+Math.round(sb(this._months/12)),a){case"week":return b/7+this._milliseconds/6048e5;case"day":return b+this._milliseconds/864e5;case"hour":return 24*b+this._milliseconds/36e5;case"minute":return 24*b*60+this._milliseconds/6e4;case"second":return 24*b*60*60+this._milliseconds/1e3;case"millisecond":return Math.floor(24*b*60*60*1e3)+this._milliseconds;default:throw new Error("Unknown unit "+a)}},lang:vb.fn.lang,locale:vb.fn.locale,toIsoString:f("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",function(){return this.toISOString()}),toISOString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"},localeData:function(){return this._locale},toJSON:function(){return this.toISOString()}}),vb.duration.fn.toString=vb.duration.fn.toISOString;for(xb in kc)c(kc,xb)&&tb(xb.toLowerCase());vb.duration.fn.asMilliseconds=function(){return this.as("ms")},vb.duration.fn.asSeconds=function(){return this.as("s")},vb.duration.fn.asMinutes=function(){return this.as("m")},vb.duration.fn.asHours=function(){return this.as("h")},vb.duration.fn.asDays=function(){return this.as("d")},vb.duration.fn.asWeeks=function(){return this.as("weeks")},vb.duration.fn.asMonths=function(){return this.as("M")},vb.duration.fn.asYears=function(){return this.as("y")},vb.locale("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===C(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),Lb?module.exports=vb:"function"==typeof define&&define.amd?(define(function(a,b,c){return c.config&&c.config()&&c.config().noGlobal===!0&&(zb.moment=wb),vb}),ub(!0)):ub()}).call(this); \ No newline at end of file diff --git a/public/js/piggy-banks.js b/public/js/piggy-banks.js index 0b603306cd..c6cdac3c94 100644 --- a/public/js/piggy-banks.js +++ b/public/js/piggy-banks.js @@ -9,14 +9,14 @@ $(function () { function addMoney(e) { var pigID = parseInt($(e.target).data('id')); - $('#moneyManagementModal').empty().load('piggy-banks/add/' + pigID).modal('show'); + $('#moneyManagementModal').empty().load('piggy-banks/add/' + pigID, function() {$('#moneyManagementModal').modal('show');}); return false; } function removeMoney(e) { var pigID = parseInt($(e.target).data('id')); - $('#moneyManagementModal').empty().load('piggy-banks/remove/' + pigID).modal('show'); + $('#moneyManagementModal').empty().load('piggy-banks/remove/' + pigID, function() {$('#moneyManagementModal').modal('show');}); return false; } \ No newline at end of file diff --git a/public/js/related-manager.js b/public/js/related-manager.js index 55cdd19431..276532d77f 100644 --- a/public/js/related-manager.js +++ b/public/js/related-manager.js @@ -1,23 +1,40 @@ $(document).ready(function () { - $('.relateTransaction').click(relateTransaction); - $('.unrelate-checkbox').click(unrelateTransaction); + $('.relateTransaction').click(relateTransactionDialog); + //$('.unrelate-checkbox').click(unrelateTransaction); }); function unrelateTransaction(e) { var target = $(e.target); var id = target.data('id'); - var relatedTo = target.data('relatedto'); + var parent = target.data('parent'); - $.post('related/removeRelation/' + id + '/' + relatedTo, {_token:token}).success(function (data) { + if(typeof id == "undefined" && typeof parent == "undefined") { + target = target.parent(); + id = target.data('id'); + parent = target.data('parent'); + } + console.log('unlink ' + id + ' from ' + parent); + + $.post('related/removeRelation/' + id + '/' + parent, {_token: token}).success(function (data) { target.parent().parent().remove(); }).fail(function () { alert('Could not!'); }); + + return false; + + + //$.post('related/removeRelation/' + id + '/' + relatedTo, {_token: token}).success(function (data) { + // target.parent().parent().remove(); + //}).fail(function () { + // alert('Could not!'); + //}); + } -function relateTransaction(e) { +function relateTransactionDialog(e) { var target = $(e.target); var ID = target.data('id'); @@ -41,21 +58,12 @@ function relateTransaction(e) { function searchRelatedTransactions(e, ID) { var searchValue = $('#relatedSearchValue').val(); if (searchValue != '') { - $.post('related/search/' + ID, {searchValue: searchValue,_token:token}).success(function (data) { - // post each result to some div. - $('#relatedSearchResults').empty(); - - $.each(data, function (i, row) { - var tr = $(''); - - var checkBox = $('').append($('').attr('type', 'checkbox').data('relateto', ID).data('id', row.id).click(doRelateNewTransaction)); - var description = $('').text(row.description); - var amount = $('').html(row.amount); - tr.append(checkBox).append(description).append(amount); - $('#relatedSearchResults').append(tr); - //$('#relatedSearchResults').append($('
').text(row.id)); - }); - + $.post('related/search/' + ID, {searchValue: searchValue, _token: token}).success(function (data) { + // post the results to some div. + $('#relatedSearchResultsTitle').show(); + $('#relatedSearchResults').empty().html(data); + // remove any clicks. + $('.relate').unbind('click').on('click', doRelateNewTransaction); }).fail(function () { alert('Could not search. Sorry.'); @@ -69,38 +77,35 @@ function doRelateNewTransaction(e) { // remove the row from the table: var target = $(e.target); var id = target.data('id'); - var relateToId = target.data('relateto'); - if (!target.checked) { - var relateID = target.data('id'); - $.post('related/relate/' + id + '/' + relateToId,{_token:token}).success(function (data) { - // success! - target.parent().parent().remove(); - getAlreadyRelatedTransactions(null, relateToId); - }).fail(function () { - // could not relate. - alert('Error!'); - }); + var parent = target.data('parent'); - - } else { - alert('remove again!'); + if (typeof id == "undefined" && typeof parent == "undefined") { + target = target.parent(); + console.log(target); + id = target.data('id'); + parent = target.data('parent'); } + + console.log('Relate ' + id + ' to ' + parent); + $.post('related/relate/' + parent + '/' + id, {_token: token}).success(function (data) { + // success! remove entry: + target.parent().parent().remove(); + // get related stuff (again). + getAlreadyRelatedTransactions(null, parent); + }).fail(function () { + // could not relate. + alert('Could not relate this transaction to the intended target.'); + }); + return false; } function getAlreadyRelatedTransactions(e, ID) { //#alreadyRelated $.get('related/alreadyRelated/' + ID).success(function (data) { - $('#alreadyRelated').empty(); - $.each(data, function (i, row) { - var tr = $(''); + $('#alreadyRelated').empty().html(data); + // some event triggers. + $('.unrelate').unbind('click').on('click', unrelateTransaction); - var checkBox = $('').append($('').attr('type', 'checkbox').data('relateto', ID).data('id', row.id).click(doRelateNewTransaction)); - var description = $('').text(row.description); - var amount = $('').html(row.amount); - tr.append(checkBox).append(description).append(amount); - $('#alreadyRelated').append(tr); - //$('#relatedSearchResults').append($('
').text(row.id)); - }); }).fail(function () { alert('Cannot get related stuff.'); }); diff --git a/public/js/reports.js b/public/js/reports.js index 4dee6cbbdd..c8b46cfcbb 100644 --- a/public/js/reports.js +++ b/public/js/reports.js @@ -6,4 +6,55 @@ if (typeof(google) != 'undefined') { googleStackedColumnChart('chart/budgets/spending/' + year, 'budgets'); } +} + + +$(function () { + $('.openModal').on('click', openModal); + includeSharedToggle(); + $('#includeShared').click(includeSharedSet); +}); + +function openModal(e) { + "use strict"; + var target = $(e.target).parent(); + var URL = target.attr('href'); + + $.get(URL).success(function (data) { + $('#defaultModal').empty().html(data).modal('show'); + + }).fail(function () { + alert('Could not load data.'); + }); + + return false; +} + +function includeSharedToggle() { + // get setting from JSON. + $.getJSON('json/show-shared-reports').success(function (data) { + console.log('GO'); + if (data.value == true) { + // show shared data, update button: + // + $('#includeShared').empty().addClass('btn-info').append($('').addClass('state-icon glyphicon glyphicon-check')).append(' Include shared asset accounts').show(); + console.log('true'); + } else { + $('#includeShared').empty().removeClass('btn-info').append($('').addClass('state-icon glyphicon glyphicon-unchecked')).append(' Include shared asset accounts').show(); + console.log('false'); + } + }).fail(function () { + console.log('fail'); + }); +} + +function includeSharedSet() { + // get setting from JSON. + $.getJSON('json/show-shared-reports/set').success(function (data) { + console.log('Value is now: ' + data.value); + includeSharedToggle(); + }).fail(function () { + console.log('fail'); + }); + return false; } \ No newline at end of file diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index db49422d54..e0e00f3efb 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -20,7 +20,8 @@ return [ "alpha_dash" => "The :attribute may only contain letters, numbers, and dashes.", "alpha_num" => "The :attribute may only contain letters and numbers.", "array" => "The :attribute must be an array.", - "unique_for_user" => "There already is an entry with this :attribute.", + "unique_for_user" => "There already is an entry with this :attribute.", + 'piggy_bank_reminder' => 'The target date is too close to today to allow reminders.', "before" => "The :attribute must be a date before :date.", "between" => [ "numeric" => "The :attribute must be between :min and :max.", diff --git a/resources/views/accounts/index.blade.php b/resources/views/accounts/index.blade.php index c0f34287a8..09cdafce86 100644 --- a/resources/views/accounts/index.blade.php +++ b/resources/views/accounts/index.blade.php @@ -22,7 +22,9 @@
+
@include('list.accounts') +
diff --git a/resources/views/accounts/show.blade.php b/resources/views/accounts/show.blade.php index 2fbd79614d..462cbe5a9e 100644 --- a/resources/views/accounts/show.blade.php +++ b/resources/views/accounts/show.blade.php @@ -2,7 +2,7 @@ @section('content') {!! Breadcrumbs::renderIfExists(Route::getCurrentRoute()->getName(), $account) !!}
-
+
{{{$account->name}}} @@ -27,24 +27,6 @@
-
- - @include('partials.date_nav') -
-
- View options for {{{$account->name}}} -
-
-

- @if($range == 'all') - Stick to date-range - @else - Show all transactions - @endif -

-
-
-
@@ -66,7 +48,6 @@ @section('scripts') diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php deleted file mode 100644 index b0b406e2a7..0000000000 --- a/resources/views/app.blade.php +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - Laravel - - - - - - - - - - - - - - @yield('content') - - - - - - diff --git a/resources/views/bills/index.blade.php b/resources/views/bills/index.blade.php index c9a465cc51..98f0e96cd3 100644 --- a/resources/views/bills/index.blade.php +++ b/resources/views/bills/index.blade.php @@ -20,10 +20,8 @@
-
@include('list.bills')
- @stop diff --git a/resources/views/budgets/income.blade.php b/resources/views/budgets/income.blade.php index d01fea7eea..f4573bda88 100644 --- a/resources/views/budgets/income.blade.php +++ b/resources/views/budgets/income.blade.php @@ -5,7 +5,7 @@