diff --git a/.env.example b/.env.example index 87540a4f1c..04528da7fc 100755 --- a/.env.example +++ b/.env.example @@ -43,6 +43,7 @@ CACHE_PREFIX=firefly GOOGLE_MAPS_API_KEY= ANALYTICS_ID= SITE_OWNER=mail@example.com +USE_ENCRYPTION=true PUSHER_KEY= PUSHER_SECRET= diff --git a/.github/CONTRIBUTING b/.github/CONTRIBUTING deleted file mode 100644 index 2a1338d94d..0000000000 --- a/.github/CONTRIBUTING +++ /dev/null @@ -1 +0,0 @@ -If you are requesting a new feature, please check out the list of [often requested features](https://firefly-iii.github.io/requested-features/) first. Thanks! \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000000..2d9bcc0192 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,7 @@ +## Hi there! + +Thank you for taking the time to report an issue or requesting a new feature. + +Please take note that there are NO rules or regulations when you submit an issue. + +If you are requesting a new feature, please check out the list of [often requested features](https://firefly-iii.github.io/requested-features/). diff --git a/.travis.yml b/.travis.yml index bcba948aa5..040654a669 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,18 @@ language: php -sudo: false php: - - '7.0' + - 7.0 + - 7.1 + +cache: + directories: + - vendor + - $HOME/.composer/cache install: - - phpenv config-rm xdebug.ini + - if [[ "$(php -v | grep 'PHP 7')" ]]; then phpenv config-rm xdebug.ini; fi - rm composer.lock - composer update --no-scripts + - cp .env.testing .env - php artisan clear-compiled - php artisan optimize - php artisan env diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e278c4994..5dc3f748c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [4.3.3] - 2017-01-30 + +_The 100th release of Firefly!_ + +### Added +- Add locales to Docker (#534) by @elohmeier. +- Optional database encryption. On by default. +- Datepicker for Firefox and other browsers. +- New instruction block for updating and installing. +- Ability to clone transactions. +- Use multi-select Bootstrap thing instead of massive lists of checkboxes. + +### Removed +- Lots of old Javascript + +### Fixed +- Missing sort broke various charts +- Bug in reports that made amounts behave weird +- Various bug fixes + +### Security +- Tested FF against the naughty string list. ## [4.3.2] - 2017-01-09 diff --git a/app/Console/Commands/EncryptFile.php b/app/Console/Commands/EncryptFile.php index 415649f9a4..1ff271a9e5 100644 --- a/app/Console/Commands/EncryptFile.php +++ b/app/Console/Commands/EncryptFile.php @@ -35,7 +35,7 @@ class EncryptFile extends Command * * @var string */ - protected $signature = 'firefly:encrypt {file} {key}'; + protected $signature = 'firefly:encrypt-file {file} {key}'; /** * Create a new command instance. diff --git a/app/Console/Commands/UpgradeFireflyInstructions.php b/app/Console/Commands/UpgradeFireflyInstructions.php index 866884de56..a88c8ab79c 100644 --- a/app/Console/Commands/UpgradeFireflyInstructions.php +++ b/app/Console/Commands/UpgradeFireflyInstructions.php @@ -33,7 +33,7 @@ class UpgradeFireflyInstructions extends Command * * @var string */ - protected $signature = 'firefly:upgrade-instructions'; + protected $signature = 'firefly:instructions {task}'; /** * Create a new command instance. @@ -49,11 +49,60 @@ class UpgradeFireflyInstructions extends Command */ public function handle() { - // + + if ($this->argument('task') == 'update') { + $this->updateInstructions(); + } + if ($this->argument('task') == 'install') { + $this->installInstructions(); + } + } + + /** + * Show a nice box + * + * @param string $text + */ + private function boxed(string $text) + { + $parts = explode("\n", wordwrap($text)); + foreach ($parts as $string) { + $this->line('| ' . sprintf('%-77s', $string) . '|'); + } + } + + /** + * Show a nice info box + * + * @param string $text + */ + private function boxedInfo(string $text) + { + $parts = explode("\n", wordwrap($text)); + foreach ($parts as $string) { + $this->info('| ' . sprintf('%-77s', $string) . '|'); + } + } + + /** + * Show a line + */ + private function showLine() + { + $line = '+'; + for ($i = 0; $i < 78; $i++) { + $line .= '-'; + } + $line .= '+'; + $this->line($line); + + } + + private function installInstructions() { /** @var string $version */ $version = config('firefly.version'); - $config = config('upgrade.text'); - $text = null; + $config = config('upgrade.text.install'); + $text = ''; foreach (array_keys($config) as $compare) { // if string starts with: $len = strlen($compare); @@ -62,22 +111,53 @@ class UpgradeFireflyInstructions extends Command } } - + $this->showLine(); + $this->boxed(''); if (is_null($text)) { - $this->line(sprintf('Thank you for installing Firefly III, v%s', $version)); - $this->info('There are no extra upgrade instructions.'); - $this->line('Firefly III should be ready for use.'); + $this->boxed(sprintf('Thank you for installin Firefly III, v%s!', $version)); + $this->boxedInfo('There are no extra installation instructions.'); + $this->boxed('Firefly III should be ready for use.'); + $this->boxed(''); + $this->showLine(); return; } - $this->line('+------------------------------------------------------------------------------+'); - $this->line(''); - $this->line(sprintf('Thank you for installing Firefly III, v%s', $version)); - $this->info(wordwrap($text)); - $this->line(''); - $this->line('+------------------------------------------------------------------------------+'); + $this->boxed(sprintf('Thank you for installing Firefly III, v%s!', $version)); + $this->boxedInfo($text); + $this->boxed(''); + $this->showLine(); + } + private function updateInstructions() + { + /** @var string $version */ + $version = config('firefly.version'); + $config = config('upgrade.text.upgrade'); + $text = ''; + foreach (array_keys($config) as $compare) { + // if string starts with: + $len = strlen($compare); + if (substr($version, 0, $len) === $compare) { + $text = $config[$compare]; + } + } + $this->showLine(); + $this->boxed(''); + if (is_null($text)) { + + $this->boxed(sprintf('Thank you for updating to Firefly III, v%s', $version)); + $this->boxedInfo('There are no extra upgrade instructions.'); + $this->boxed('Firefly III should be ready for use.'); + $this->boxed(''); + $this->showLine(); + return; + } + + $this->boxed(sprintf('Thank you for updating to Firefly III, v%s!', $version)); + $this->boxedInfo($text); + $this->boxed(''); + $this->showLine(); } } diff --git a/app/Console/Commands/UseEncryption.php b/app/Console/Commands/UseEncryption.php new file mode 100644 index 0000000000..d3c3e178c0 --- /dev/null +++ b/app/Console/Commands/UseEncryption.php @@ -0,0 +1,66 @@ +handleObjects('Account', 'name', 'encrypted'); + $this->handleObjects('Bill', 'name', 'name_encrypted'); + $this->handleObjects('Bill', 'match', 'match_encrypted'); + $this->handleObjects('Budget', 'name', 'encrypted'); + $this->handleObjects('Category', 'name', 'encrypted'); + $this->handleObjects('PiggyBank', 'name', 'encrypted'); + $this->handleObjects('TransactionJournal', 'description', 'encrypted'); + } + + /** + * @param string $class + * @param string $field + * @param string $indicator + */ + public function handleObjects(string $class, string $field, string $indicator) + { + $fqn = sprintf('FireflyIII\Models\%s', $class); + $encrypt = config('firefly.encryption') ? 0 : 1; + $set = $fqn::where($indicator, $encrypt)->get(); + + foreach ($set as $entry) { + $newName = $entry->$field; + $entry->$field = $newName; + $entry->save(); + } + + $this->line(sprintf('Updated %d %s.', $set->count(), strtolower(Str::plural($class)))); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 61537ce4e8..8ea0f88014 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -16,10 +16,10 @@ namespace FireflyIII\Console; use FireflyIII\Console\Commands\CreateImport; use FireflyIII\Console\Commands\EncryptFile; use FireflyIII\Console\Commands\Import; -use FireflyIII\Console\Commands\MoveRepository; use FireflyIII\Console\Commands\ScanAttachments; use FireflyIII\Console\Commands\UpgradeDatabase; use FireflyIII\Console\Commands\UpgradeFireflyInstructions; +use FireflyIII\Console\Commands\UseEncryption; use FireflyIII\Console\Commands\VerifyDatabase; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -63,6 +63,7 @@ class Kernel extends ConsoleKernel EncryptFile::class, ScanAttachments::class, UpgradeDatabase::class, + UseEncryption::class, ]; /** diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index c61dac5629..00b359d148 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -111,9 +111,9 @@ class Handler extends ExceptionHandler /** * Convert an authentication exception into an unauthenticated response. * - * @param \Illuminate\Http\Request $request + * @param $request * - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse */ protected function unauthenticated($request) { diff --git a/app/Export/Entry/Entry.php b/app/Export/Entry/Entry.php index 53ace9643b..0afc40ada4 100644 --- a/app/Export/Entry/Entry.php +++ b/app/Export/Entry/Entry.php @@ -51,7 +51,6 @@ final class Entry public $destination_account_id; public $destination_account_name; - public $budget_id; public $budget_name; public $category_id; @@ -74,15 +73,15 @@ final class Entry { $entry = new self; $entry->journal_id = $object->transaction_journal_id; - $entry->description = self::decrypt($object->journal_encrypted, $object->journal_description); + $entry->description = self::decrypt(intval($object->journal_encrypted), $object->journal_description); $entry->amount = $object->amount; $entry->date = $object->date; $entry->transaction_type = $object->transaction_type; $entry->currency_code = $object->transaction_currency_code; $entry->source_account_id = $object->account_id; - $entry->source_account_name = self::decrypt($object->account_name_encrypted, $object->account_name); + $entry->source_account_name = self::decrypt(intval($object->account_name_encrypted), $object->account_name); $entry->destination_account_id = $object->opposing_account_id; - $entry->destination_account_name = self::decrypt($object->opposing_account_encrypted, $object->opposing_account_name); + $entry->destination_account_name = self::decrypt(intval($object->opposing_account_encrypted), $object->opposing_account_name); $entry->category_id = $object->category_id ?? ''; $entry->category_name = $object->category_name ?? ''; $entry->budget_id = $object->budget_id ?? ''; diff --git a/app/Helpers/Attachments/AttachmentHelper.php b/app/Helpers/Attachments/AttachmentHelper.php index 501842a7ce..c7f7902109 100644 --- a/app/Helpers/Attachments/AttachmentHelper.php +++ b/app/Helpers/Attachments/AttachmentHelper.php @@ -32,9 +32,9 @@ class AttachmentHelper implements AttachmentHelperInterface /** @var MessageBag */ public $messages; /** @var array */ - protected $allowedMimes; + protected $allowedMimes = []; /** @var int */ - protected $maxUploadSize; + protected $maxUploadSize = 0; /** @var \Illuminate\Contracts\Filesystem\Filesystem */ protected $uploadDisk; @@ -44,8 +44,8 @@ class AttachmentHelper implements AttachmentHelperInterface */ public function __construct() { - $this->maxUploadSize = config('firefly.maxUploadSize'); - $this->allowedMimes = config('firefly.allowedMimes'); + $this->maxUploadSize = intval(config('firefly.maxUploadSize')); + $this->allowedMimes = (array) config('firefly.allowedMimes'); $this->errors = new MessageBag; $this->messages = new MessageBag; $this->uploadDisk = Storage::disk('upload'); diff --git a/app/Helpers/Collection/Account.php b/app/Helpers/Collection/Account.php deleted file mode 100644 index e3fc70e702..0000000000 --- a/app/Helpers/Collection/Account.php +++ /dev/null @@ -1,107 +0,0 @@ -accounts = new Collection; - } - - /** - * @return Collection - */ - public function getAccounts(): Collection - { - return $this->accounts; - } - - /** - * @param Collection $accounts - */ - public function setAccounts(Collection $accounts) - { - $this->accounts = $accounts; - } - - /** - * @return string - */ - public function getDifference(): string - { - return $this->difference; - } - - /** - * @param string $difference - */ - public function setDifference(string $difference) - { - $this->difference = $difference; - } - - /** - * @return string - */ - public function getEnd(): string - { - return $this->end; - } - - /** - * @param string $end - */ - public function setEnd(string $end) - { - $this->end = $end; - } - - /** - * @return string - */ - public function getStart(): string - { - return $this->start; - } - - /** - * @param string $start - */ - public function setStart(string $start) - { - $this->start = $start; - } - - -} diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/JournalCollector.php index d5d881196a..c198da7764 100644 --- a/app/Helpers/Collector/JournalCollector.php +++ b/app/Helpers/Collector/JournalCollector.php @@ -167,6 +167,7 @@ class JournalCollector implements JournalCollectorInterface public function getJournals(): Collection { $this->run = true; + /** @var Collection $set */ $set = $this->query->get(array_values($this->fields)); Log::debug(sprintf('Count of set is %d', $set->count())); $set = $this->filterTransfers($set); diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 42eec4e59b..b3b007cb9e 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -75,6 +75,11 @@ class AccountController extends Controller $defaultCurrency = Amount::getDefaultCurrency(); $subTitleIcon = config('firefly.subIconsByIdentifier.' . $what); $subTitle = trans('firefly.make_new_' . $what . '_account'); + $roles = []; + foreach (config('firefly.accountRoles') as $role) { + $roles[$role] = strval(trans('firefly.account_role_' . $role)); + } + // pre fill some data Session::flash('preFilled', ['currency_id' => $defaultCurrency->id,]); @@ -87,7 +92,7 @@ class AccountController extends Controller Session::flash('gaEventCategory', 'accounts'); Session::flash('gaEventAction', 'create-' . $what); - return view('accounts.create', compact('subTitleIcon', 'what', 'subTitle', 'currencies')); + return view('accounts.create', compact('subTitleIcon', 'what', 'subTitle', 'currencies', 'roles')); } @@ -155,6 +160,11 @@ class AccountController extends Controller /** @var CurrencyRepositoryInterface $repository */ $repository = app(CurrencyRepositoryInterface::class); $currencies = ExpandedForm::makeSelectList($repository->get()); + $roles = []; + foreach (config('firefly.accountRoles') as $role) { + $roles[$role] = strval(trans('firefly.account_role_' . $role)); + } + // put previous url in session if not redirect from store (not "return_to_edit"). if (session('accounts.edit.fromUpdate') !== true) { @@ -185,7 +195,7 @@ class AccountController extends Controller Session::flash('gaEventCategory', 'accounts'); Session::flash('gaEventAction', 'edit-' . $what); - return view('accounts.edit', compact('currencies', 'account', 'subTitle', 'subTitleIcon', 'openingBalance', 'what')); + return view('accounts.edit', compact('currencies', 'account', 'subTitle', 'subTitleIcon', 'openingBalance', 'what', 'roles')); } /** diff --git a/app/Http/Controllers/AttachmentController.php b/app/Http/Controllers/AttachmentController.php index 8febccf3e4..2b154defea 100644 --- a/app/Http/Controllers/AttachmentController.php +++ b/app/Http/Controllers/AttachmentController.php @@ -18,6 +18,7 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Requests\AttachmentFormRequest; use FireflyIII\Models\Attachment; use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface; +use Illuminate\Http\Response as LaravelResponse; use Preferences; use Response; use Session; @@ -100,7 +101,9 @@ class AttachmentController extends Controller $content = $repository->getContent($attachment); $quoted = sprintf('"%s"', addcslashes(basename($attachment->filename), '"\\')); - return response($content, 200) + /** @var LaravelResponse $response */ + $response = response($content, 200); + $response ->header('Content-Description', 'File Transfer') ->header('Content-Type', 'application/octet-stream') ->header('Content-Disposition', 'attachment; filename=' . $quoted) @@ -110,6 +113,8 @@ class AttachmentController extends Controller ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') ->header('Pragma', 'public') ->header('Content-Length', strlen($content)); + + return $response; } throw new FireflyException('Could not find the indicated attachment. The file is no longer there.'); } diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index d9b5cc2a27..0d8ce65937 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -278,8 +278,6 @@ class BudgetController extends Controller throw new FireflyException('This budget limit is not part of this budget.'); } - /** @var AccountRepositoryInterface $accountRepository */ - $accountRepository = app(AccountRepositoryInterface::class); $page = intval($request->get('page')) == 0 ? 1 : intval($request->get('page')); $pageSize = intval(Preferences::get('transactionPageSize', 50)->data); $subTitle = trans( @@ -289,7 +287,6 @@ class BudgetController extends Controller 'end' => $budgetLimit->end_date->formatLocalized($this->monthAndDayFormat), ] ); - $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH]); // collector: /** @var JournalCollectorInterface $collector */ diff --git a/app/Http/Controllers/ExportController.php b/app/Http/Controllers/ExportController.php index 7f9d29eb7f..1ace8118c1 100644 --- a/app/Http/Controllers/ExportController.php +++ b/app/Http/Controllers/ExportController.php @@ -24,6 +24,7 @@ use FireflyIII\Models\ExportJob; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\ExportJob\ExportJobRepositoryInterface; use FireflyIII\Repositories\ExportJob\ExportJobRepositoryInterface as EJRI; +use Illuminate\Http\Response as LaravelResponse; use Preferences; use Response; use View; @@ -73,8 +74,9 @@ class ExportController extends Controller $job->change('export_downloaded'); - - return response($content, 200) + /** @var LaravelResponse $response */ + $response = response($content, 200); + $response ->header('Content-Description', 'File Transfer') ->header('Content-Type', 'application/octet-stream') ->header('Content-Disposition', 'attachment; filename=' . $quoted) @@ -85,6 +87,8 @@ class ExportController extends Controller ->header('Pragma', 'public') ->header('Content-Length', strlen($content)); + return $response; + } /** @@ -128,8 +132,7 @@ class ExportController extends Controller * @param AccountRepositoryInterface $repository * @param EJRI $jobs * - * @return string - * + * @return \Illuminate\Http\JsonResponse */ public function postIndex(ExportFormRequest $request, AccountRepositoryInterface $repository, EJRI $jobs) { diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index 12fc16c3af..ef812a8a1b 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -21,6 +21,7 @@ use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use Illuminate\Http\Request; +use Illuminate\Http\Response as LaravelResponse; use Log; use Response; use Session; @@ -120,15 +121,18 @@ class ImportController extends Controller $result = json_encode($config, JSON_PRETTY_PRINT); $name = sprintf('"%s"', addcslashes('import-configuration-' . date('Y-m-d') . '.json', '"\\')); - return response($result, 200) - ->header('Content-disposition', 'attachment; filename=' . $name) - ->header('Content-Type', 'application/json') - ->header('Content-Description', 'File Transfer') - ->header('Connection', 'Keep-Alive') - ->header('Expires', '0') - ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - ->header('Pragma', 'public') - ->header('Content-Length', strlen($result)); + /** @var LaravelResponse $response */ + $response = response($result, 200); + $response->header('Content-disposition', 'attachment; filename=' . $name) + ->header('Content-Type', 'application/json') + ->header('Content-Description', 'File Transfer') + ->header('Connection', 'Keep-Alive') + ->header('Expires', '0') + ->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') + ->header('Pragma', 'public') + ->header('Content-Length', strlen($result)); + + return $response; } diff --git a/app/Http/Controllers/JavascriptController.php b/app/Http/Controllers/JavascriptController.php new file mode 100644 index 0000000000..67c3d268d7 --- /dev/null +++ b/app/Http/Controllers/JavascriptController.php @@ -0,0 +1,132 @@ +getDateRangePicker(); + $start = Session::get('start'); + $end = Session::get('end'); + $linkTitle = sprintf('%s - %s', $start->formatLocalized($this->monthAndDayFormat), $end->formatLocalized($this->monthAndDayFormat)); + $firstDate = session('first')->format('Y-m-d'); + $localeconv = localeconv(); + $accounting = Amount::getJsConfig($localeconv); + $localeconv = localeconv(); + $defaultCurrency = Amount::getDefaultCurrency(); + $localeconv['frac_digits'] = $defaultCurrency->decimal_places; + $pref = Preferences::get('language', config('firefly.default_language', 'en_US')); + $lang = $pref->data; + $data = [ + 'picker' => $picker, + 'linkTitle' => $linkTitle, + 'firstDate' => $firstDate, + 'currencyCode' => Amount::getCurrencyCode(), + 'currencySymbol' => Amount::getCurrencySymbol(), + 'accounting' => $accounting, + 'localeconv' => $localeconv, + 'language' => $lang, + ]; + + return response() + ->view('javascript.variables', $data, 200) + ->header('Content-Type', 'text/javascript'); + } + + /** + * @return array + * @throws FireflyException + */ + private function getDateRangePicker(): array + { + $viewRange = Preferences::get('viewRange', '1M')->data; + $start = Session::get('start'); + $end = Session::get('end'); + + $prevStart = clone $start; + $prevEnd = clone $start; + $nextStart = clone $end; + $nextEnd = clone $end; + if ($viewRange === 'custom') { + $days = $start->diffInDays($end); + $prevStart->subDays($days); + $nextEnd->addDays($days); + unset($days); + } + + if ($viewRange !== 'custom') { + $prevStart = Navigation::subtractPeriod($start, $viewRange);// subtract for previous period + $prevEnd = Navigation::endOfPeriod($prevStart, $viewRange); + $nextStart = Navigation::addPeriod($start, $viewRange, 0); // add for previous period + $nextEnd = Navigation::endOfPeriod($nextStart, $viewRange); + } + + $ranges = []; + $ranges['current'] = [$start->format('Y-m-d'), $end->format('Y-m-d')]; + $ranges['previous'] = [$prevStart->format('Y-m-d'), $prevEnd->format('Y-m-d')]; + $ranges['next'] = [$nextStart->format('Y-m-d'), $nextEnd->format('Y-m-d')]; + + switch ($viewRange) { + default: + throw new FireflyException('The date picker does not yet support "' . $viewRange . '".'); + case '1D': + case 'custom': + $format = (string)trans('config.month_and_day'); + break; + case '3M': + $format = (string)trans('config.quarter_in_year'); + break; + case '6M': + $format = (string)trans('config.half_year'); + break; + case '1Y': + $format = (string)trans('config.year'); + break; + case '1M': + $format = (string)trans('config.month'); + break; + case '1W': + $format = (string)trans('config.week_in_year'); + break; + } + + $current = $start->formatLocalized($format); + $next = $nextStart->formatLocalized($format); + $prev = $prevStart->formatLocalized($format); + + return [ + 'start' => $start->format('Y-m-d'), + 'end' => $end->format('Y-m-d'), + 'current' => $current, + 'previous' => $prev, + 'next' => $next, + 'ranges' => $ranges, + ]; + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/JsonController.php b/app/Http/Controllers/JsonController.php index d38012e6a2..4d95c51fc9 100644 --- a/app/Http/Controllers/JsonController.php +++ b/app/Http/Controllers/JsonController.php @@ -20,7 +20,9 @@ use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; -use FireflyIII\Repositories\Category\CategoryRepositoryInterface as CRI; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Http\Request; @@ -61,6 +63,43 @@ class JsonController extends Controller return Response::json(['html' => $view]); } + /** + * Returns a JSON list of all accounts. + * + * @param AccountRepositoryInterface $repository + * + * @return \Illuminate\Http\JsonResponse + * + */ + public function allAccounts(AccountRepositoryInterface $repository) + { + $return = array_unique( + $repository->getAccountsByType( + [AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET] + )->pluck('name')->toArray() + ); + sort($return); + + return Response::json($return); + + } + + /** + * @param JournalCollectorInterface $collector + * + * @return \Illuminate\Http\JsonResponse + */ + public function allTransactionJournals(JournalCollectorInterface $collector) + { + $collector->setLimit(100)->setPage(1); + $return = array_unique($collector->getJournals()->pluck('description')->toArray()); + sort($return); + + return Response::json($return); + + + } + /** * @param BillRepositoryInterface $repository * @@ -158,19 +197,29 @@ class JsonController extends Controller } /** - * Returns a list of categories. - * - * @param CRI $repository + * @param BudgetRepositoryInterface $repository * * @return \Illuminate\Http\JsonResponse */ - public function categories(CRI $repository) + public function budgets(BudgetRepositoryInterface $repository) { - $list = $repository->getCategories(); - $return = []; - foreach ($list as $entry) { - $return[] = $entry->name; - } + $return = array_unique($repository->getBudgets()->pluck('name')->toArray()); + sort($return); + + return Response::json($return); + } + + /** + * Returns a list of categories. + * + * @param CategoryRepositoryInterface $repository + * + * @return \Illuminate\Http\JsonResponse + */ + public function categories(CategoryRepositoryInterface $repository) + { + $return = array_unique($repository->getCategories()->pluck('name')->toArray()); + sort($return); return Response::json($return); } @@ -195,14 +244,10 @@ class JsonController extends Controller */ public function expenseAccounts(AccountRepositoryInterface $repository) { - $list = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]); - $return = []; - foreach ($list as $entry) { - $return[] = $entry->name; - } + $return = array_unique($repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY])->pluck('name')->toArray()); + sort($return); return Response::json($return); - } /** @@ -213,14 +258,10 @@ class JsonController extends Controller */ public function revenueAccounts(AccountRepositoryInterface $repository) { - $list = $repository->getAccountsByType([AccountType::REVENUE]); - $return = []; - foreach ($list as $entry) { - $return[] = $entry->name; - } + $return = array_unique($repository->getAccountsByType([AccountType::REVENUE])->pluck('name')->toArray()); + sort($return); return Response::json($return); - } /** @@ -232,11 +273,8 @@ class JsonController extends Controller */ public function tags(TagRepositoryInterface $tagRepository) { - $list = $tagRepository->get(); - $return = []; - foreach ($list as $entry) { - $return[] = $entry->tag; - } + $return = array_unique($tagRepository->get()->pluck('tag')->toArray()); + sort($return); return Response::json($return); @@ -278,25 +316,29 @@ class JsonController extends Controller */ public function transactionJournals(JournalCollectorInterface $collector, string $what) { - $descriptions = []; - $type = config('firefly.transactionTypesByWhat.' . $what); - $types = [$type]; + $type = config('firefly.transactionTypesByWhat.' . $what); + $types = [$type]; - // use journal collector instead: $collector->setTypes($types)->setLimit(100)->setPage(1); - $journals = $collector->getJournals(); - foreach ($journals as $j) { - $descriptions[] = $j->description; - } + $return = array_unique($collector->getJournals()->pluck('description')->toArray()); + sort($return); - $descriptions = array_unique($descriptions); - sort($descriptions); - - return Response::json($descriptions); + return Response::json($return); } + /** + * + */ + public function transactionTypes(JournalRepositoryInterface $repository) + { + $return = array_unique($repository->getTransactionTypes()->pluck('type')->toArray()); + sort($return); + + return Response::json($return); + } + /** * @param Request $request * diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index f155623fdc..260adc97e3 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -230,6 +230,8 @@ class TagController extends Controller public function show(Request $request, JournalCollectorInterface $collector, Tag $tag, string $moment = '') { $range = Preferences::get('viewRange', '1M')->data; + $start = new Carbon; + $end = new Carbon; if (strlen($moment) > 0) { try { diff --git a/app/Http/Controllers/Transaction/MassController.php b/app/Http/Controllers/Transaction/MassController.php index 5676e902a8..d139f0f414 100644 --- a/app/Http/Controllers/Transaction/MassController.php +++ b/app/Http/Controllers/Transaction/MassController.php @@ -163,11 +163,11 @@ class MassController extends Controller $journal->transaction_count = $journal->transactions()->count(); if (!is_null($sources->first())) { $journal->source_account_id = $sources->first()->id; - $journal->source_account_name = $sources->first()->name; + $journal->source_account_name = $sources->first()->editname; } if (!is_null($destinations->first())) { $journal->destination_account_id = $destinations->first()->id; - $journal->destination_account_name = $destinations->first()->name; + $journal->destination_account_name = $destinations->first()->editname; } } ); @@ -178,7 +178,7 @@ class MassController extends Controller $journals = $filtered; - return view('transactions.mass-edit', compact('journals', 'subTitle', 'accountList')); + return view('transactions.mass.edit', compact('journals', 'subTitle', 'accountList')); } /** diff --git a/app/Http/Controllers/Transaction/SingleController.php b/app/Http/Controllers/Transaction/SingleController.php index b1133686f3..9c006617bd 100644 --- a/app/Http/Controllers/Transaction/SingleController.php +++ b/app/Http/Controllers/Transaction/SingleController.php @@ -82,6 +82,42 @@ class SingleController extends Controller } + public function cloneTransaction(TransactionJournal $journal) + { + $source = TransactionJournal::sourceAccountList($journal)->first(); + $destination = TransactionJournal::destinationAccountList($journal)->first(); + $budget = $journal->budgets()->first(); + $budgetId = is_null($budget) ? 0 : $budget->id; + $category = $journal->categories()->first(); + $categoryName = is_null($category) ? '' : $category->name; + $tags = join(',', $journal->tags()->get()->pluck('tag')->toArray()); + + + $preFilled = [ + 'description' => $journal->description, + 'source_account_id' => $source->id, + 'source_account_name' => $source->name, + 'destination_account_id' => $destination->id, + 'destination_account_name' => $destination->name, + 'amount' => TransactionJournal::amountPositive($journal), + 'date' => $journal->date->format('Y-m-d'), + 'budget_id' => $budgetId, + 'category' => $categoryName, + 'tags' => $tags, + 'interest_date' => $journal->getMeta('interest_date'), + 'book_date' => $journal->getMeta('book_date'), + 'process_date' => $journal->getMeta('process_date'), + 'due_date' => $journal->getMeta('due_date'), + 'payment_date' => $journal->getMeta('payment_date'), + 'invoice_date' => $journal->getMeta('invoice_date'), + 'internal_reference' => $journal->getMeta('internal_reference'), + 'notes' => $journal->getMeta('notes'), + ]; + Session::flash('preFilled', $preFilled); + + return redirect(route('transactions.create', [strtolower($journal->transactionType->type)])); + } + /** * @param string $what * @@ -113,7 +149,10 @@ class SingleController extends Controller asort($piggies); - return view('transactions.create', compact('assetAccounts', 'subTitleIcon', 'uploadSize', 'budgets', 'what', 'piggies', 'subTitle', 'optionalFields')); + return view( + 'transactions.single.create', + compact('assetAccounts', 'subTitleIcon', 'uploadSize', 'budgets', 'what', 'piggies', 'subTitle', 'optionalFields', 'preFilled') + ); } /** @@ -137,7 +176,7 @@ class SingleController extends Controller Session::flash('gaEventCategory', 'transactions'); Session::flash('gaEventAction', 'delete-' . $what); - return view('transactions.delete', compact('journal', 'subTitle', 'what')); + return view('transactions.single.delete', compact('journal', 'subTitle', 'what')); } @@ -241,7 +280,7 @@ class SingleController extends Controller Session::forget('transactions.edit.fromUpdate'); return view( - 'transactions.edit', + 'transactions.single.edit', compact('journal', 'optionalFields', 'assetAccounts', 'what', 'budgetList', 'subTitle') )->with('data', $preFilled); } @@ -265,6 +304,7 @@ class SingleController extends Controller return redirect(route('transactions.create', [$request->input('what')]))->withInput(); } + /** @var array $files */ $files = $request->hasFile('attachments') ? $request->file('attachments') : null; $this->attachments->saveAttachmentsForModel($journal, $files); @@ -315,7 +355,8 @@ class SingleController extends Controller $data = $request->getJournalData(); $journal = $repository->update($journal, $data); - $files = $request->hasFile('attachments') ? $request->file('attachments') : null; + /** @var array $files */ + $files = $request->hasFile('attachments') ? $request->file('attachments') : null; $this->attachments->saveAttachmentsForModel($journal, $files); // flash errors diff --git a/app/Http/Controllers/Transaction/SplitController.php b/app/Http/Controllers/Transaction/SplitController.php index 7d4cd17761..91b35ddfce 100644 --- a/app/Http/Controllers/Transaction/SplitController.php +++ b/app/Http/Controllers/Transaction/SplitController.php @@ -112,7 +112,7 @@ class SplitController extends Controller Session::forget('transactions.edit-split.fromUpdate'); return view( - 'transactions.edit-split', + 'transactions.split.edit', compact( 'subTitleIcon', 'currencies', 'optionalFields', 'preFilled', 'subTitle', 'amount', 'sourceAccounts', 'uploadSize', 'destinationAccounts', 'assetAccounts', @@ -138,6 +138,7 @@ class SplitController extends Controller $data = $this->arrayFromInput($request); $journal = $repository->updateSplitJournal($journal, $data); + /** @var array $files */ $files = $request->hasFile('attachments') ? $request->file('attachments') : null; // save attachments: $this->attachments->saveAttachmentsForModel($journal, $files); diff --git a/app/Http/Middleware/Range.php b/app/Http/Middleware/Range.php index 4371d473d7..fdf5322a4d 100644 --- a/app/Http/Middleware/Range.php +++ b/app/Http/Middleware/Range.php @@ -72,9 +72,6 @@ class Range // set view variables. $this->configureView(); - // get variables for date range: - $this->datePicker(); - // set more view variables: $this->configureList(); } @@ -96,7 +93,6 @@ class Range { $pref = Preferences::get('language', config('firefly.default_language', 'en_US')); $lang = $pref->data; - App::setLocale($lang); Carbon::setLocale(substr($lang, 0, 2)); $locale = explode(',', trans('config.locale')); @@ -105,94 +101,15 @@ class Range setlocale(LC_TIME, $locale); setlocale(LC_MONETARY, $locale); + // save some formats: - $monthFormat = (string)trans('config.month'); $monthAndDayFormat = (string)trans('config.month_and_day'); $dateTimeFormat = (string)trans('config.date_time'); $defaultCurrency = Amount::getDefaultCurrency(); - $localeconv = localeconv(); - $accounting = Amount::getJsConfig($localeconv); - // decimal places is overruled by TransactionCurrency - $localeconv['frac_digits'] = $defaultCurrency->decimal_places; - - View::share('monthFormat', $monthFormat); View::share('monthAndDayFormat', $monthAndDayFormat); View::share('dateTimeFormat', $dateTimeFormat); - View::share('language', $lang); - View::share('localeconv', $localeconv); View::share('defaultCurrency', $defaultCurrency); - View::share('accountingConfig', $accounting); - } - - /** - * @throws FireflyException - */ - private function datePicker() - { - $viewRange = Preferences::get('viewRange', '1M')->data; - /** @var Carbon $start */ - $start = Session::get('start'); - /** @var Carbon $end */ - $end = Session::get('end'); - - $prevStart = clone $start; - $prevEnd = clone $start; - $nextStart = clone $end; - $nextEnd = clone $end; - if ($viewRange === 'custom') { - $days = $start->diffInDays($end); - $prevStart->subDays($days); - $nextEnd->addDays($days); - unset($days); - } - - if ($viewRange !== 'custom') { - $prevStart = Navigation::subtractPeriod($start, $viewRange);// subtract for previous period - $prevEnd = Navigation::endOfPeriod($prevStart, $viewRange); - $nextStart = Navigation::addPeriod($start, $viewRange, 0); // add for previous period - $nextEnd = Navigation::endOfPeriod($nextStart, $viewRange); - } - - $ranges = []; - $ranges['current'] = [$start->format('Y-m-d'), $end->format('Y-m-d')]; - $ranges['previous'] = [$prevStart->format('Y-m-d'), $prevEnd->format('Y-m-d')]; - $ranges['next'] = [$nextStart->format('Y-m-d'), $nextEnd->format('Y-m-d')]; - - switch ($viewRange) { - default: - throw new FireflyException('The date picker does not yet support "' . $viewRange . '".'); - case '1D': - case 'custom': - $format = (string)trans('config.month_and_day'); - break; - case '3M': - $format = (string)trans('config.quarter_in_year'); - break; - case '6M': - $format = (string)trans('config.half_year'); - break; - case '1Y': - $format = (string)trans('config.year'); - break; - case '1M': - $format = (string)trans('config.month'); - break; - case '1W': - $format = (string)trans('config.week_in_year'); - break; - } - - - $current = $start->formatLocalized($format); - $next = $nextStart->formatLocalized($format); - $prev = $prevStart->formatLocalized($format); - View::share('dpStart', $start->format('Y-m-d')); - View::share('dpEnd', $end->format('Y-m-d')); - View::share('dpCurrent', $current); - View::share('dpPrevious', $prev); - View::share('dpNext', $next); - View::share('dpRanges', $ranges); } /** diff --git a/app/Http/Requests/AccountFormRequest.php b/app/Http/Requests/AccountFormRequest.php index 364898614c..91b05abcb0 100644 --- a/app/Http/Requests/AccountFormRequest.php +++ b/app/Http/Requests/AccountFormRequest.php @@ -13,7 +13,6 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; -use Carbon\Carbon; use FireflyIII\Repositories\Account\AccountRepositoryInterface; /** @@ -39,21 +38,21 @@ class AccountFormRequest extends Request public function getAccountData(): array { return [ - 'name' => trim(strval($this->input('name'))), - 'active' => intval($this->input('active')) === 1, - 'accountType' => $this->input('what'), - 'currency_id' => intval($this->input('currency_id')), - 'virtualBalance' => round($this->input('virtualBalance'), 12), - 'virtualBalanceCurrency' => intval($this->input('amount_currency_id_virtualBalance')), - 'iban' => trim(strval($this->input('iban'))), - 'BIC' => trim(strval($this->input('BIC'))), - 'accountNumber' => trim(strval($this->input('accountNumber'))), - 'accountRole' => $this->input('accountRole'), - 'openingBalance' => round($this->input('openingBalance'), 12), - 'openingBalanceDate' => new Carbon((string)$this->input('openingBalanceDate')), - 'openingBalanceCurrency' => intval($this->input('amount_currency_id_openingBalance')), - 'ccType' => $this->input('ccType'), - 'ccMonthlyPaymentDate' => $this->input('ccMonthlyPaymentDate'), + 'name' => $this->string('name'), + 'active' => $this->boolean('active'), + 'accountType' => $this->string('what'), + 'currency_id' => $this->integer('currency_id'), + 'virtualBalance' => $this->float('virtualBalance'), + 'virtualBalanceCurrency' => $this->integer('amount_currency_id_virtualBalance'), + 'iban' => $this->string('iban'), + 'BIC' => $this->string('BIC'), + 'accountNumber' => $this->string('accountNumber'), + 'accountRole' => $this->string('accountRole'), + 'openingBalance' => $this->float('openingBalance'), + 'openingBalanceDate' => $this->date('openingBalanceDate'), + 'openingBalanceCurrency' => $this->integer('amount_currency_id_openingBalance'), + 'ccType' => $this->string('ccType'), + 'ccMonthlyPaymentDate' => $this->string('ccMonthlyPaymentDate'), ]; } @@ -72,7 +71,7 @@ class AccountFormRequest extends Request $idRule = ''; if (!is_null($repository->find(intval($this->get('id')))->id)) { $idRule = 'belongsToUser:accounts'; - $nameRule = 'required|min:1|uniqueAccountForUser:' . $this->get('id'); + $nameRule = 'required|min:1|uniqueAccountForUser:' . intval($this->get('id')); } return [ diff --git a/app/Http/Requests/AttachmentFormRequest.php b/app/Http/Requests/AttachmentFormRequest.php index 16028cab37..6f7608bc46 100644 --- a/app/Http/Requests/AttachmentFormRequest.php +++ b/app/Http/Requests/AttachmentFormRequest.php @@ -36,9 +36,9 @@ class AttachmentFormRequest extends Request public function getAttachmentData(): array { return [ - 'title' => trim($this->input('title')), - 'description' => trim($this->input('description')), - 'notes' => trim($this->input('notes')), + 'title' => $this->string('title'), + 'description' => $this->string('description'), + 'notes' => $this->string('notes'), ]; } diff --git a/app/Http/Requests/BillFormRequest.php b/app/Http/Requests/BillFormRequest.php index 0b0b85e29d..992542eca1 100644 --- a/app/Http/Requests/BillFormRequest.php +++ b/app/Http/Requests/BillFormRequest.php @@ -13,8 +13,6 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; -use Carbon\Carbon; - /** * Class BillFormRequest * @@ -38,17 +36,17 @@ class BillFormRequest extends Request public function getBillData() { return [ - 'name' => $this->get('name'), - 'match' => $this->get('match'), - 'amount_min' => round($this->get('amount_min'), 12), - 'amount_currency_id_amount_min' => intval($this->get('amount_currency_id_amount_min')), - 'amount_currency_id_amount_max' => intval($this->get('amount_currency_id_amount_max')), - 'amount_max' => round($this->get('amount_max'), 12), - 'date' => new Carbon($this->get('date')), - 'repeat_freq' => $this->get('repeat_freq'), - 'skip' => intval($this->get('skip')), - 'automatch' => intval($this->get('automatch')) === 1, - 'active' => intval($this->get('active')) === 1, + 'name' => $this->string('name'), + 'match' => $this->string('match'), + 'amount_min' => $this->float('amount_min'), + 'amount_currency_id_amount_min' => $this->integer('amount_currency_id_amount_min'), + 'amount_currency_id_amount_max' => $this->integer('amount_currency_id_amount_max'), + 'amount_max' => $this->float('amount_max'), + 'date' => $this->date('date'), + 'repeat_freq' => $this->string('repeat_freq'), + 'skip' => $this->integer('skip'), + 'automatch' => $this->boolean('automatch'), + 'active' => $this->boolean('active'), ]; } diff --git a/app/Http/Requests/BudgetFormRequest.php b/app/Http/Requests/BudgetFormRequest.php index 3ca9d0fb37..c7d9d0de63 100644 --- a/app/Http/Requests/BudgetFormRequest.php +++ b/app/Http/Requests/BudgetFormRequest.php @@ -37,8 +37,8 @@ class BudgetFormRequest extends Request public function getBudgetData(): array { return [ - 'name' => trim($this->input('name')), - 'active' => intval($this->input('active')) == 1, + 'name' => $this->string('name'), + 'active' => $this->boolean('active'), ]; } diff --git a/app/Http/Requests/CategoryFormRequest.php b/app/Http/Requests/CategoryFormRequest.php index 34347608af..b7b5e94a5e 100644 --- a/app/Http/Requests/CategoryFormRequest.php +++ b/app/Http/Requests/CategoryFormRequest.php @@ -38,7 +38,7 @@ class CategoryFormRequest extends Request public function getCategoryData(): array { return [ - 'name' => trim($this->input('name')), + 'name' => $this->string('name'), ]; } diff --git a/app/Http/Requests/ConfigurationRequest.php b/app/Http/Requests/ConfigurationRequest.php index d6bd5d7ca2..452e613e73 100644 --- a/app/Http/Requests/ConfigurationRequest.php +++ b/app/Http/Requests/ConfigurationRequest.php @@ -36,8 +36,8 @@ class ConfigurationRequest extends Request public function getConfigurationData(): array { return [ - 'single_user_mode' => intval($this->get('single_user_mode')) === 1, - 'is_demo_site' => intval($this->get('is_demo_site')) === 1, + 'single_user_mode' => $this->boolean('single_user_mode'), + 'is_demo_site' => $this->boolean('is_demo_site'), ]; } diff --git a/app/Http/Requests/CurrencyFormRequest.php b/app/Http/Requests/CurrencyFormRequest.php index ffbfa7e399..0449f9b4ab 100644 --- a/app/Http/Requests/CurrencyFormRequest.php +++ b/app/Http/Requests/CurrencyFormRequest.php @@ -36,10 +36,10 @@ class CurrencyFormRequest extends Request public function getCurrencyData() { return [ - 'name' => $this->get('name'), - 'code' => $this->get('code'), - 'symbol' => $this->get('symbol'), - 'decimal_places' => intval($this->get('decimal_places')), + 'name' => $this->string('name'), + 'code' => $this->string('code'), + 'symbol' => $this->string('symbol'), + 'decimal_places' => $this->integer('decimal_places'), ]; } diff --git a/app/Http/Requests/JournalFormRequest.php b/app/Http/Requests/JournalFormRequest.php index 2ecc96af87..62dc5656d2 100644 --- a/app/Http/Requests/JournalFormRequest.php +++ b/app/Http/Requests/JournalFormRequest.php @@ -13,7 +13,6 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; -use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\TransactionType; @@ -43,30 +42,30 @@ class JournalFormRequest extends Request { $data = [ 'what' => $this->get('what'), // type. can be 'deposit', 'withdrawal' or 'transfer' - 'date' => new Carbon($this->get('date')), - 'tags' => explode(',', $this->getFieldOrEmptyString('tags')), - 'currency_id' => intval($this->get('amount_currency_id_amount')), + 'date' => $this->date('date'), + 'tags' => explode(',', $this->string('tags')), + 'currency_id' => $this->integer('amount_currency_id_amount'), // all custom fields: - 'interest_date' => $this->getDateOrNull('interest_date'), - 'book_date' => $this->getDateOrNull('book_date'), - 'process_date' => $this->getDateOrNull('process_date'), - 'due_date' => $this->getDateOrNull('due_date'), - 'payment_date' => $this->getDateOrNull('payment_date'), - 'invoice_date' => $this->getDateOrNull('invoice_date'), - 'internal_reference' => trim(strval($this->get('internal_reference'))), - 'notes' => trim(strval($this->get('notes'))), + 'interest_date' => $this->date('interest_date'), + 'book_date' => $this->date('book_date'), + 'process_date' => $this->date('process_date'), + 'due_date' => $this->date('due_date'), + 'payment_date' => $this->date('payment_date'), + 'invoice_date' => $this->date('invoice_date'), + 'internal_reference' => $this->string('internal_reference'), + 'notes' => $this->string('notes'), // transaction / journal data: - 'description' => $this->getFieldOrEmptyString('description'), - 'amount' => round($this->get('amount'), 12), - 'budget_id' => intval($this->get('budget_id')), - 'category' => $this->getFieldOrEmptyString('category'), - 'source_account_id' => intval($this->get('source_account_id')), - 'source_account_name' => $this->getFieldOrEmptyString('source_account_name'), - 'destination_account_id' => $this->getFieldOrEmptyString('destination_account_id'), - 'destination_account_name' => $this->getFieldOrEmptyString('destination_account_name'), - 'piggy_bank_id' => intval($this->get('piggy_bank_id')), + 'description' => $this->string('description'), + 'amount' => $this->float('amount'), + 'budget_id' => $this->integer('budget_id'), + 'category' => $this->string('category'), + 'source_account_id' => $this->integer('source_account_id'), + 'source_account_name' => $this->string('source_account_name'), + 'destination_account_id' => $this->string('destination_account_id'), + 'destination_account_name' => $this->string('destination_account_name'), + 'piggy_bank_id' => $this->integer('piggy_bank_id'), ]; @@ -142,26 +141,4 @@ class JournalFormRequest extends Request return $rules; } - - /** - * @param string $field - * - * @return Carbon|null - */ - private function getDateOrNull(string $field) - { - return $this->get($field) ? new Carbon($this->get($field)) : null; - } - - /** - * @param string $field - * - * @return string - */ - private function getFieldOrEmptyString(string $field): string - { - $string = $this->get($field) ?? ''; - - return trim($string); - } } diff --git a/app/Http/Requests/PiggyBankFormRequest.php b/app/Http/Requests/PiggyBankFormRequest.php index d0e6889d62..ba969dcf64 100644 --- a/app/Http/Requests/PiggyBankFormRequest.php +++ b/app/Http/Requests/PiggyBankFormRequest.php @@ -38,12 +38,12 @@ class PiggyBankFormRequest extends Request public function getPiggyBankData(): array { return [ - 'name' => trim($this->get('name')), + 'name' => $this->string('name'), 'startdate' => new Carbon, - 'account_id' => intval($this->get('account_id')), - 'targetamount' => round($this->get('targetamount'), 12), - 'targetdate' => strlen(strval($this->get('targetdate'))) > 0 ? new Carbon($this->get('targetdate')) : null, - 'note' => trim(strval($this->get('note'))), + 'account_id' => $this->integer('account_id'), + 'targetamount' => $this->float('targetamount'), + 'targetdate' => $this->date('targetdate'), + 'note' => $this->string('note'), ]; } diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index 84cb318d3e..e73b17ac60 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -13,6 +13,7 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; +use Carbon\Carbon; use Illuminate\Foundation\Http\FormRequest; /** @@ -20,7 +21,123 @@ use Illuminate\Foundation\Http\FormRequest; * * @package FireflyIII\Http\Requests */ -abstract class Request extends FormRequest +class Request extends FormRequest { - // + /** + * @param string $field + * + * @return bool + */ + protected function boolean(string $field): bool + { + return intval($this->input($field)) === 1; + } + + /** + * @param string $field + * + * @return Carbon|null + */ + protected function date(string $field) + { + return $this->get($field) ? new Carbon($this->get($field)) : null; + } + + /** + * @param string $field + * + * @return float + */ + protected function float(string $field): float + { + return round($this->input($field), 12); + } + + /** + * @param string $field + * @param string $type + * + * @return array + */ + protected function getArray(string $field, string $type): array + { + $original = $this->get($field); + $return = []; + foreach ($original as $index => $value) { + $return[$index] = $this->$type($value); + } + + return $return; + } + + /** + * @param string $field + * + * @return int + */ + protected function integer(string $field): int + { + return intval($this->get($field)); + } + + /** + * @param string $field + * + * @return string + */ + protected function string(string $field): string + { + $string = $this->get($field) ?? ''; + $search = [ + "\u{0001}", // start of heading + "\u{0002}", // start of text + "\u{0003}", // end of text + "\u{0004}", // end of transmission + "\u{0005}", // enquiry + "\u{0006}", // ACK + "\u{0007}", // BEL + "\u{0008}", // backspace + "\u{000E}", // shift out + "\u{000F}", // shift in + "\u{0010}", // data link escape + "\u{0011}", // DC1 + "\u{0012}", // DC2 + "\u{0013}", // DC3 + "\u{0014}", // DC4 + "\u{0015}", // NAK + "\u{0016}", // SYN + "\u{0017}", // ETB + "\u{0018}", // CAN + "\u{0019}", // EM + "\u{001A}", // SUB + "\u{001B}", // escape + "\u{001C}", // file separator + "\u{001D}", // group separator + "\u{001E}", // record separator + "\u{001F}", // unit separator + "\u{007F}", // DEL + "\u{00A0}", // non-breaking space + "\u{1680}", // ogham space mark + "\u{180E}", // mongolian vowel separator + "\u{2000}", // en quad + "\u{2001}", // em quad + "\u{2002}", // en space + "\u{2003}", // em space + "\u{2004}", // three-per-em space + "\u{2005}", // four-per-em space + "\u{2006}", // six-per-em space + "\u{2007}", // figure space + "\u{2008}", // punctuation space + "\u{2009}", // thin space + "\u{200A}", // hair space + "\u{200B}", // zero width space + "\u{202F}", // narrow no-break space + "\u{3000}", // ideographic space + "\u{FEFF}", // zero width no -break space + ]; + $replace = "\x20"; // plain old normal space + $string = str_replace($search, $replace, $string); + + return trim($string); + } } diff --git a/app/Http/Requests/RuleFormRequest.php b/app/Http/Requests/RuleFormRequest.php index 814f0c560d..6295bd9e3f 100644 --- a/app/Http/Requests/RuleFormRequest.php +++ b/app/Http/Requests/RuleFormRequest.php @@ -38,17 +38,17 @@ class RuleFormRequest extends Request public function getRuleData(): array { return [ - 'title' => trim($this->get('title')), - 'active' => intval($this->get('active')) == 1, - 'trigger' => trim($this->get('trigger')), - 'description' => trim($this->get('description')), + 'title' => $this->string('title'), + 'active' => $this->boolean('active'), + 'trigger' => $this->string('trigger'), + 'description' => $this->string('description'), 'rule-triggers' => $this->get('rule-trigger'), 'rule-trigger-values' => $this->get('rule-trigger-value'), 'rule-trigger-stop' => $this->get('rule-trigger-stop'), 'rule-actions' => $this->get('rule-action'), 'rule-action-values' => $this->get('rule-action-value'), 'rule-action-stop' => $this->get('rule-action-stop'), - 'stop_processing' => intval($this->get('stop_processing')) === 1, + 'stop_processing' => $this->boolean('stop_processing'), ]; } diff --git a/app/Http/Requests/RuleGroupFormRequest.php b/app/Http/Requests/RuleGroupFormRequest.php index b5420c5ce5..fcf92d0105 100644 --- a/app/Http/Requests/RuleGroupFormRequest.php +++ b/app/Http/Requests/RuleGroupFormRequest.php @@ -38,8 +38,8 @@ class RuleGroupFormRequest extends Request public function getRuleGroupData(): array { return [ - 'title' => trim($this->input('title')), - 'description' => trim($this->input('description')), + 'title' => $this->string('title'), + 'description' => $this->string('description'), ]; } diff --git a/app/Http/Requests/SplitJournalFormRequest.php b/app/Http/Requests/SplitJournalFormRequest.php index 2bf7100677..5efba69243 100644 --- a/app/Http/Requests/SplitJournalFormRequest.php +++ b/app/Http/Requests/SplitJournalFormRequest.php @@ -13,8 +13,6 @@ declare(strict_types = 1); namespace FireflyIII\Http\Requests; -use Carbon\Carbon; - /** * Class SplitJournalFormRequest @@ -38,18 +36,18 @@ class SplitJournalFormRequest extends Request public function getSplitData(): array { $data = [ - 'id' => $this->get('id') ?? 0, - 'journal_description' => $this->get('journal_description'), - 'journal_currency_id' => intval($this->get('journal_currency_id')), - 'journal_source_account_id' => intval($this->get('journal_source_account_id')), - 'journal_source_account_name' => $this->get('journal_source_account_name'), - 'journal_destination_account_id' => intval($this->get('journal_destination_account_id')), - 'journal_destination_account_name' => $this->get('journal_source_destination_name'), - 'date' => new Carbon($this->get('date')), - 'what' => $this->get('what'), - 'interest_date' => $this->get('interest_date') ? new Carbon($this->get('interest_date')) : null, - 'book_date' => $this->get('book_date') ? new Carbon($this->get('book_date')) : null, - 'process_date' => $this->get('process_date') ? new Carbon($this->get('process_date')) : null, + 'id' => $this->integer('id'), + 'journal_description' => $this->string('journal_description'), + 'journal_currency_id' => $this->integer('journal_currency_id'), + 'journal_source_account_id' => $this->integer('journal_source_account_id'), + 'journal_source_account_name' => $this->string('journal_source_account_name'), + 'journal_destination_account_id' => $this->integer('journal_destination_account_id'), + 'journal_destination_account_name' => $this->string('journal_source_destination_name'), + 'date' => $this->date('date'), + 'what' => $this->string('what'), + 'interest_date' => $this->date('interest_date'), + 'book_date' => $this->date('book_date'), + 'process_date' => $this->date('process_date'), 'transactions' => $this->getTransactionData(), ]; @@ -87,28 +85,30 @@ class SplitJournalFormRequest extends Request */ private function getTransactionData(): array { + $descriptions = $this->getArray('description', 'string'); + $categories = $this->getArray('category', 'string'); + $amounts = $this->getArray('amount', 'float'); + $budgets = $this->getArray('amount', 'integer'); + $srcAccountIds = $this->getArray('source_account_id', 'integer'); + $srcAccountNames = $this->getArray('source_account_name', 'string'); + $dstAccountIds = $this->getArray('destination_account_id', 'integer'); + $dstAccountNames = $this->getArray('destination_account_name', 'string'); + $piggyBankIds = $this->getArray('piggy_bank_id', 'integer'); + $return = []; // description is leading because it is one of the mandatory fields. - foreach ($this->get('description') as $index => $description) { - $category = $this->get('category')[$index] ?? ''; + foreach ($descriptions as $index => $description) { + $category = $categories[$index] ?? ''; $transaction = [ 'description' => $description, - 'amount' => round($this->get('amount')[$index], 12), - 'budget_id' => $this->get('budget_id')[$index] ? intval($this->get('budget_id')[$index]) : 0, - 'category' => trim($category), - 'source_account_id' => isset($this->get('source_account_id')[$index]) - ? intval($this->get('source_account_id')[$index]) - : intval( - $this->get('journal_source_account_id') - ), - 'source_account_name' => $this->get('source_account_name')[$index] ?? '', - 'piggy_bank_id' => isset($this->get('piggy_bank_id')[$index]) ? intval($this->get('piggy_bank_id')[$index]) : 0, - 'destination_account_id' => isset($this->get('destination_account_id')[$index]) - ? intval($this->get('destination_account_id')[$index]) - : intval( - $this->get('journal_destination_account_id') - ), - 'destination_account_name' => $this->get('destination_account_name')[$index] ?? '', + 'amount' => $amounts[$index], + 'budget_id' => $budgets[$index] ?? 0, + 'category' => $category, + 'source_account_id' => $srcAccountIds[$index] ?? $this->get('journal_source_account_id'), + 'source_account_name' => $srcAccountNames[$index] ?? '', + 'piggy_bank_id' => $piggyBankIds[$index] ?? 0, + 'destination_account_id' => $dstAccountIds[$index] ?? $this->get('journal_destination_account_id'), + 'destination_account_name' => $dstAccountNames[$index] ?? '', ]; $return[] = $transaction; } diff --git a/app/Http/Requests/TagFormRequest.php b/app/Http/Requests/TagFormRequest.php index d7bdb63f54..544b53c0b0 100644 --- a/app/Http/Requests/TagFormRequest.php +++ b/app/Http/Requests/TagFormRequest.php @@ -38,9 +38,9 @@ class TagFormRequest extends Request public function collectTagData(): array { if ($this->get('setTag') == 'true') { - $latitude = $this->get('latitude'); - $longitude = $this->get('longitude'); - $zoomLevel = $this->get('zoomLevel'); + $latitude = $this->string('latitude'); + $longitude = $this->string('longitude'); + $zoomLevel = $this->integer('zoomLevel'); } else { $latitude = null; $longitude = null; @@ -49,13 +49,13 @@ class TagFormRequest extends Request $date = $this->get('date') ?? ''; $data = [ - 'tag' => $this->get('tag'), - 'date' => strlen($date) > 0 ? new Carbon($date) : null, - 'description' => $this->get('description') ?? '', + 'tag' => $this->string('tag'), + 'date' => $this->date($date), + 'description' => $this->string('description'), 'latitude' => $latitude, 'longitude' => $longitude, 'zoomLevel' => $zoomLevel, - 'tagMode' => $this->get('tagMode'), + 'tagMode' => $this->string('tagMode'), ]; return $data; diff --git a/app/Http/Requests/UserFormRequest.php b/app/Http/Requests/UserFormRequest.php index faf12136e1..436e3c9d33 100644 --- a/app/Http/Requests/UserFormRequest.php +++ b/app/Http/Requests/UserFormRequest.php @@ -36,11 +36,10 @@ class UserFormRequest extends Request public function getUserData(): array { return [ - 'email' => trim($this->get('email')), - 'blocked' => intval($this->get('blocked')), - 'blocked_code' => trim($this->get('blocked_code')), - 'password' => trim($this->get('password')), - + 'email' => $this->string('email'), + 'blocked' => $this->integer('blocked'), + 'blocked_code' => $this->string('blocked_code'), + 'password' => $this->string('password'), ]; } diff --git a/app/Models/Account.php b/app/Models/Account.php index 4810c6006e..11b337ba10 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -85,7 +85,7 @@ class Account extends Model foreach ($search as $name => $value) { $query->where($name, $value); } - $set = $query->get(['accounts.*']); + $set = $query->get(['accounts.*']); // account must have a name. If not set, use IBAN. if (!isset($fields['name'])) { @@ -93,7 +93,6 @@ class Account extends Model } - /** @var Account $account */ foreach ($set as $account) { if ($account->name == $fields['name']) { @@ -316,8 +315,9 @@ class Account extends Model */ public function setNameAttribute($value) { - $this->attributes['name'] = $value; - $this->attributes['encrypted'] = false; + $encrypt = config('firefly.encryption'); + $this->attributes['name'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['encrypted'] = $encrypt; } /** diff --git a/app/Models/Bill.php b/app/Models/Bill.php index bf67e0f730..54a7429746 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -35,7 +35,7 @@ class Bill extends Model * @var array */ protected $casts - = [ + = [ 'created_at' => 'date', 'updated_at' => 'date', 'deleted_at' => 'date', @@ -47,7 +47,7 @@ class Bill extends Model 'match_encrypted' => 'boolean', ]; /** @var array */ - protected $dates = ['created_at', 'updated_at', 'deleted_at']; + protected $dates = ['created_at', 'updated_at', 'deleted_at']; protected $fillable = ['name', 'match', 'amount_min', 'match_encrypted', 'name_encrypted', 'user_id', 'amount_max', 'date', 'repeat_freq', 'skip', 'automatch', 'active',]; @@ -120,8 +120,9 @@ class Bill extends Model */ public function setMatchAttribute($value) { - $this->attributes['match'] = Crypt::encrypt($value); - $this->attributes['match_encrypted'] = true; + $encrypt = config('firefly.encryption'); + $this->attributes['match'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['match_encrypted'] = $encrypt; } /** @@ -129,8 +130,9 @@ class Bill extends Model */ public function setNameAttribute($value) { - $this->attributes['name'] = Crypt::encrypt($value); - $this->attributes['name_encrypted'] = true; + $encrypt = config('firefly.encryption'); + $this->attributes['name'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['name_encrypted'] = $encrypt; } /** diff --git a/app/Models/Budget.php b/app/Models/Budget.php index d62e195352..504802e05a 100644 --- a/app/Models/Budget.php +++ b/app/Models/Budget.php @@ -121,8 +121,9 @@ class Budget extends Model */ public function setNameAttribute($value) { - $this->attributes['name'] = $value; - $this->attributes['encrypted'] = false; + $encrypt = config('firefly.encryption'); + $this->attributes['name'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['encrypted'] = $encrypt; } /** diff --git a/app/Models/Category.php b/app/Models/Category.php index e8e665bff2..50349e9b44 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -115,8 +115,9 @@ class Category extends Model */ public function setNameAttribute($value) { - $this->attributes['name'] = $value; - $this->attributes['encrypted'] = false; + $encrypt = config('firefly.encryption'); + $this->attributes['name'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['encrypted'] = $encrypt; } /** diff --git a/app/Models/PiggyBank.php b/app/Models/PiggyBank.php index 34c742bd7f..d5b2b00187 100644 --- a/app/Models/PiggyBank.php +++ b/app/Models/PiggyBank.php @@ -159,8 +159,9 @@ class PiggyBank extends Model */ public function setNameAttribute($value) { - $this->attributes['name'] = $value; - $this->attributes['encrypted'] = false; + $encrypt = config('firefly.encryption'); + $this->attributes['name'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['encrypted'] = $encrypt; } /** diff --git a/app/Models/PiggyBankRepetition.php b/app/Models/PiggyBankRepetition.php index 1d9dca1eb0..00c8221582 100644 --- a/app/Models/PiggyBankRepetition.php +++ b/app/Models/PiggyBankRepetition.php @@ -89,7 +89,7 @@ class PiggyBankRepetition extends Model */ public function setCurrentamountAttribute($value) { - $this->attributes['currentamount'] = strval(round($value, 2)); + $this->attributes['currentamount'] = strval(round($value, 12)); } } diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index 1cf5faf17f..1808955271 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -371,8 +371,9 @@ class TransactionJournal extends TransactionJournalSupport */ public function setDescriptionAttribute($value) { - $this->attributes['description'] = $value; - $this->attributes['encrypted'] = false; + $encrypt = config('firefly.encryption'); + $this->attributes['description'] = $encrypt ? Crypt::encrypt($value) : $value; + $this->attributes['encrypted'] = $encrypt; } /** diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index ab2cc761bd..d740291f68 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -283,6 +283,29 @@ class AccountRepository implements AccountRepositoryInterface return $last; } + /** + * Returns the date of the very first transaction in this account. + * + * @param Account $account + * + * @return TransactionJournal + */ + public function oldestJournal(Account $account): TransactionJournal + { + $first = $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->orderBy('transaction_journals.date', 'ASC') + ->orderBy('transaction_journals.order', 'DESC') + ->where('transaction_journals.user_id', $this->user->id) + ->orderBy('transaction_journals.id', 'ASC') + ->first(['transaction_journals.id']); + if (!is_null($first)) { + return TransactionJournal::find(intval($first->id)); + } + + return new TransactionJournal(); + } + /** * Returns the date of the very first transaction in this account. * @@ -292,18 +315,12 @@ class AccountRepository implements AccountRepositoryInterface */ public function oldestJournalDate(Account $account): Carbon { - $first = new Carbon; - $date = $account->transactions() - ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->orderBy('transaction_journals.date', 'ASC') - ->orderBy('transaction_journals.order', 'DESC') - ->orderBy('transaction_journals.id', 'ASC') - ->first(['transaction_journals.date']); - if (!is_null($date)) { - $first = new Carbon($date->date); + $journal = $this->oldestJournal($account); + if (is_null($journal->id)) { + return new Carbon; } - return $first; + return $journal->date; } /** @@ -477,7 +494,6 @@ class AccountRepository implements AccountRepositoryInterface } /** - * @param float $amount * @param string $name * * @return Account diff --git a/app/Repositories/Account/AccountRepositoryInterface.php b/app/Repositories/Account/AccountRepositoryInterface.php index 6aca02047e..428e48144d 100644 --- a/app/Repositories/Account/AccountRepositoryInterface.php +++ b/app/Repositories/Account/AccountRepositoryInterface.php @@ -15,6 +15,7 @@ namespace FireflyIII\Repositories\Account; use Carbon\Carbon; use FireflyIII\Models\Account; +use FireflyIII\Models\TransactionJournal; use Illuminate\Support\Collection; /** @@ -105,6 +106,15 @@ interface AccountRepositoryInterface */ public function newestJournalDate(Account $account): Carbon; + /** + * Returns the date of the very first transaction in this account. + * + * @param Account $account + * + * @return TransactionJournal + */ + public function oldestJournal(Account $account): TransactionJournal; + /** * Returns the date of the very first transaction in this account. * diff --git a/app/Repositories/Account/AccountTasker.php b/app/Repositories/Account/AccountTasker.php index 5a85a93201..126d084e8d 100644 --- a/app/Repositories/Account/AccountTasker.php +++ b/app/Repositories/Account/AccountTasker.php @@ -14,8 +14,6 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Account; use Carbon\Carbon; -use FireflyIII\Helpers\Collection\Account as AccountCollection; -use FireflyIII\Models\Account; use FireflyIII\Models\Transaction; use FireflyIII\User; use Illuminate\Database\Query\JoinClause; @@ -106,55 +104,55 @@ class AccountTasker implements AccountTaskerInterface * @param Carbon $start * @param Carbon $end * - * @return AccountCollection + * @return array */ - public function getAccountReport(Collection $accounts, Carbon $start, Carbon $end): AccountCollection + public function getAccountReport(Collection $accounts, Carbon $start, Carbon $end): array { - $startAmount = '0'; - $endAmount = '0'; - $diff = '0'; $ids = $accounts->pluck('id')->toArray(); $yesterday = clone $start; $yesterday->subDay(); - $startSet = Steam::balancesById($ids, $yesterday); - $backupSet = Steam::balancesById($ids, $start); - $endSet = Steam::balancesById($ids, $end); + $startSet = Steam::balancesById($ids, $yesterday); + $endSet = Steam::balancesById($ids, $end); - Log::debug( - sprintf( - 'getAccountReport from %s to %s for %d accounts.', - $start->format('Y-m-d'), - $end->format('Y-m-d'), - $accounts->count() - ) - ); - $accounts->each( - function (Account $account) use ($startSet, $endSet, $backupSet) { - $account->startBalance = $startSet[$account->id] ?? '0'; - $account->endBalance = $endSet[$account->id] ?? '0'; + Log::debug('Start of accountreport'); - // check backup set just in case: - if ($account->startBalance === '0' && isset($backupSet[$account->id])) { - $account->startBalance = $backupSet[$account->id]; - } - } - ); + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + + $return = [ + 'start' => '0', + 'end' => '0', + 'difference' => '0', + 'accounts' => [], + ]; - // summarize: foreach ($accounts as $account) { - $startAmount = bcadd($startAmount, $account->startBalance); - $endAmount = bcadd($endAmount, $account->endBalance); - $diff = bcadd($diff, bcsub($account->endBalance, $account->startBalance)); + $id = $account->id; + $entry = [ + 'name' => $account->name, + 'id' => $account->id, + 'start_balance' => '0', + 'end_balance' => '0', + ]; + + // get first journal date: + $first = $repository->oldestJournal($account); + $entry['start_balance'] = $startSet[$account->id] ?? '0'; + $entry['end_balance'] = $endSet[$account->id] ?? '0'; + if (!is_null($first->id) && $yesterday < $first->date && $end >= $first->date) { + // something about balance? + $entry['start_balance'] = $first->transactions()->where('account_id', $account->id)->first()->amount; + Log::debug(sprintf('Account was opened before %s, so opening balance is %f', $yesterday->format('Y-m-d'), $entry['start_balance'])); + } + $return['start'] = bcadd($return['start'], $entry['start_balance']); + $return['end'] = bcadd($return['end'], $entry['end_balance']); + + $return['accounts'][$id] = $entry; } - $object = new AccountCollection; - $object->setStart($startAmount); - $object->setEnd($endAmount); - $object->setDifference($diff); - $object->setAccounts($accounts); + $return['difference'] = bcsub($return['end'], $return['start']); - - return $object; + return $return; } /** diff --git a/app/Repositories/Account/AccountTaskerInterface.php b/app/Repositories/Account/AccountTaskerInterface.php index 67ab246ed9..57fbebaa79 100644 --- a/app/Repositories/Account/AccountTaskerInterface.php +++ b/app/Repositories/Account/AccountTaskerInterface.php @@ -14,7 +14,6 @@ declare(strict_types = 1); namespace FireflyIII\Repositories\Account; use Carbon\Carbon; -use FireflyIII\Helpers\Collection\Account as AccountCollection; use Illuminate\Support\Collection; /** @@ -54,8 +53,8 @@ interface AccountTaskerInterface * @param Carbon $start * @param Carbon $end * - * @return AccountCollection + * @return array */ - public function getAccountReport(Collection $accounts, Carbon $start, Carbon $end): AccountCollection; + public function getAccountReport(Collection $accounts, Carbon $start, Carbon $end): array; } diff --git a/app/Repositories/ExportJob/ExportJobRepositoryInterface.php b/app/Repositories/ExportJob/ExportJobRepositoryInterface.php index dbb4a04418..6f3c7a30b1 100644 --- a/app/Repositories/ExportJob/ExportJobRepositoryInterface.php +++ b/app/Repositories/ExportJob/ExportJobRepositoryInterface.php @@ -50,7 +50,7 @@ interface ExportJobRepositoryInterface /** * @param string $key * - * @return ExportJob|null + * @return ExportJob */ public function findByKey(string $key): ExportJob; diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 2093793c8d..9ee336cddf 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -137,6 +137,14 @@ class JournalRepository implements JournalRepositoryInterface return $entry; } + /** + * @return Collection + */ + public function getTransactionTypes(): Collection + { + return TransactionType::orderBy('type', 'ASC')->get(); + } + /** * @param array $data * @@ -208,40 +216,6 @@ class JournalRepository implements JournalRepositoryInterface } - /** - * Store journal only, uncompleted, with attachments if necessary. - * - * @param array $data - * - * @return TransactionJournal - */ - public function storeJournal(array $data): TransactionJournal - { - // find transaction type. - $transactionType = TransactionType::where('type', ucfirst($data['what']))->first(); - - // store actual journal. - $journal = new TransactionJournal( - [ - 'user_id' => $this->user->id, - 'transaction_type_id' => $transactionType->id, - 'transaction_currency_id' => $data['amount_currency_id_amount'], - 'description' => $data['description'], - 'completed' => 0, - 'date' => $data['date'], - ] - ); - - $result = $journal->save(); - if ($result) { - return $journal; - } - - return new TransactionJournal(); - - - } - /** * @param TransactionJournal $journal * @param array $data diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php index fb9779eec3..0b62a5729a 100644 --- a/app/Repositories/Journal/JournalRepositoryInterface.php +++ b/app/Repositories/Journal/JournalRepositoryInterface.php @@ -16,6 +16,7 @@ namespace FireflyIII\Repositories\Journal; use FireflyIII\Models\Account; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; +use Illuminate\Support\Collection; use Illuminate\Support\MessageBag; /** @@ -36,6 +37,11 @@ interface JournalRepositoryInterface */ public function convert(TransactionJournal $journal, TransactionType $type, Account $source, Account $destination): MessageBag; + /** + * @return Collection + */ + public function getTransactionTypes(): Collection; + /** * Deletes a journal. * @@ -69,15 +75,6 @@ interface JournalRepositoryInterface */ public function store(array $data): TransactionJournal; - /** - * Store journal only, uncompleted, with attachments if necessary. - * - * @param array $data - * - * @return TransactionJournal - */ - public function storeJournal(array $data): TransactionJournal; - /** * @param TransactionJournal $journal * @param array $data diff --git a/app/Repositories/RuleGroup/RuleGroupRepository.php b/app/Repositories/RuleGroup/RuleGroupRepository.php index 247078221e..b294e19937 100644 --- a/app/Repositories/RuleGroup/RuleGroupRepository.php +++ b/app/Repositories/RuleGroup/RuleGroupRepository.php @@ -49,8 +49,8 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface } /** - * @param RuleGroup $ruleGroup - * @param RuleGroup $moveTo + * @param RuleGroup $ruleGroup + * @param RuleGroup|null $moveTo * * @return bool */ diff --git a/app/Repositories/RuleGroup/RuleGroupRepositoryInterface.php b/app/Repositories/RuleGroup/RuleGroupRepositoryInterface.php index a321b04e98..985a121b92 100644 --- a/app/Repositories/RuleGroup/RuleGroupRepositoryInterface.php +++ b/app/Repositories/RuleGroup/RuleGroupRepositoryInterface.php @@ -35,8 +35,8 @@ interface RuleGroupRepositoryInterface public function count(): int; /** - * @param RuleGroup $ruleGroup - * @param RuleGroup $moveTo + * @param RuleGroup $ruleGroup + * @param RuleGroup|null $moveTo * * @return bool */ diff --git a/app/Rules/Triggers/AbstractTrigger.php b/app/Rules/Triggers/AbstractTrigger.php index f3be138f8a..0fd9a3bfd8 100644 --- a/app/Rules/Triggers/AbstractTrigger.php +++ b/app/Rules/Triggers/AbstractTrigger.php @@ -74,6 +74,8 @@ class AbstractTrigger return $self; } + + /** * @param RuleTrigger $trigger * @param TransactionJournal $journal diff --git a/app/Rules/Triggers/BudgetIs.php b/app/Rules/Triggers/BudgetIs.php new file mode 100644 index 0000000000..fa444f3ffc --- /dev/null +++ b/app/Rules/Triggers/BudgetIs.php @@ -0,0 +1,95 @@ +budgets()->first(); + if (!is_null($budget)) { + $name = strtolower($budget->name); + // match on journal: + if ($name === strtolower($this->triggerValue)) { + Log::debug(sprintf('RuleTrigger BudgetIs for journal #%d: "%s" is "%s", return true.', $journal->id, $name, $this->triggerValue)); + + return true; + } + } + + if (is_null($budget)) { + // perhaps transactions have this budget? + /** @var Transaction $transaction */ + foreach ($journal->transactions as $transaction) { + $budget = $transaction->budgets()->first(); + if (!is_null($budget)) { + $name = strtolower($budget->name); + if ($name === strtolower($this->triggerValue)) { + Log::debug( + sprintf( + 'RuleTrigger BudgetIs for journal #%d (transaction #%d): "%s" is "%s", return true.', + $journal->id, $transaction->id, $name, $this->triggerValue + ) + ); + + return true; + } + } + } + } + + Log::debug(sprintf('RuleTrigger BudgetIs for journal #%d: does not have budget "%s", return false.', $journal->id, $this->triggerValue)); + + return false; + } +} diff --git a/app/Rules/Triggers/CategoryIs.php b/app/Rules/Triggers/CategoryIs.php new file mode 100644 index 0000000000..28dcddb086 --- /dev/null +++ b/app/Rules/Triggers/CategoryIs.php @@ -0,0 +1,95 @@ +categories()->first(); + if (!is_null($category)) { + $name = strtolower($category->name); + // match on journal: + if ($name === strtolower($this->triggerValue)) { + Log::debug(sprintf('RuleTrigger CategoryIs for journal #%d: "%s" is "%s", return true.', $journal->id, $name, $this->triggerValue)); + + return true; + } + } + + if (is_null($category)) { + // perhaps transactions have this category? + /** @var Transaction $transaction */ + foreach ($journal->transactions as $transaction) { + $category = $transaction->categories()->first(); + if (!is_null($category)) { + $name = strtolower($category->name); + if ($name === strtolower($this->triggerValue)) { + Log::debug( + sprintf( + 'RuleTrigger CategoryIs for journal #%d (transaction #%d): "%s" is "%s", return true.', + $journal->id, $transaction->id, $name, $this->triggerValue + ) + ); + + return true; + } + } + } + } + + Log::debug(sprintf('RuleTrigger CategoryIs for journal #%d: does not have category "%s", return false.', $journal->id, $this->triggerValue)); + + return false; + } +} diff --git a/app/Rules/Triggers/TagIs.php b/app/Rules/Triggers/TagIs.php new file mode 100644 index 0000000000..684d806760 --- /dev/null +++ b/app/Rules/Triggers/TagIs.php @@ -0,0 +1,74 @@ +tags()->get(); + /** @var Tag $tag */ + foreach ($tags as $tag) { + $name = strtolower($tag->tag); + // match on journal: + if ($name === strtolower($this->triggerValue)) { + Log::debug(sprintf('RuleTrigger TagIs for journal #%d: is tagged with "%s", return true.', $journal->id, $name)); + + return true; + } + } + Log::debug(sprintf('RuleTrigger TagIs for journal #%d: is not tagged with "%s", return false.', $journal->id, $this->triggerValue)); + + return false; + } +} diff --git a/app/Support/Amount.php b/app/Support/Amount.php index de7fcce12f..9434352b80 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -90,11 +90,11 @@ class Amount $pos_c = $sign; } - // default: (amount before currency) + // default is amount before currency $format = $pos_a . $pos_d . '%v' . $space . $pos_b . '%s' . $pos_c . $pos_e; if ($csPrecedes) { - // (currency before amount) + // alternative is currency before amount $format = $pos_a . $pos_b . '%s' . $pos_c . $space . $pos_d . '%v' . $pos_e; } Log::debug(sprintf('Final format: "%s"', $format)); diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index 28b6972ed7..85b85fd5d2 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -285,7 +285,6 @@ class ExpandedForm /** * @param $name - * @param null $value * @param array $options * * @return string diff --git a/app/Support/Steam.php b/app/Support/Steam.php index 65e4b148a6..0185a93caf 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -124,6 +124,8 @@ class Steam ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) ->groupBy('transaction_journals.date') + ->orderBy('transaction_journals.date', 'ASC') + ->whereNull('transaction_journals.deleted_at') ->get(['transaction_journals.date', DB::raw('SUM(transactions.amount) AS modified')]); $currentBalance = $startBalance; foreach ($set as $entry) { @@ -150,7 +152,7 @@ class Steam public function balancesById(array $ids, Carbon $date): array { - // abuse chart properties: + // cache this property. $cache = new CacheProperties; $cache->addProperty($ids); $cache->addProperty('balances'); @@ -163,6 +165,7 @@ class Steam ->where('transaction_journals.date', '<=', $date->format('Y-m-d')) ->groupBy('transactions.account_id') ->whereIn('transactions.account_id', $ids) + ->whereNull('transaction_journals.deleted_at') ->get(['transactions.account_id', DB::raw('sum(transactions.amount) AS aggregate')]); $result = []; diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php index 04336c3163..cfd98fcafc 100644 --- a/app/Support/Twig/General.php +++ b/app/Support/Twig/General.php @@ -14,7 +14,6 @@ declare(strict_types = 1); namespace FireflyIII\Support\Twig; use Carbon\Carbon; -use Config; use FireflyIII\Models\Account; use FireflyIII\Models\TransactionJournal; use Route; @@ -42,7 +41,6 @@ class General extends Twig_Extension $this->formatAmountPlain(), $this->formatJournal(), $this->balance(), - $this->getAccountRole(), $this->formatFilesize(), $this->mimeIcon(), ]; @@ -234,18 +232,6 @@ class General extends Twig_Extension ); } - /** - * @return Twig_SimpleFilter - */ - protected function getAccountRole(): Twig_SimpleFilter - { - return new Twig_SimpleFilter( - 'getAccountRole', function (string $name): string { - return Config::get('firefly.accountRoles.' . $name); - } - ); - } - /** * @return Twig_SimpleFunction */ diff --git a/bootstrap/app.php b/bootstrap/app.php index 88ef952626..b026728e73 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -25,7 +25,6 @@ declare(strict_types = 1); bcscale(12); - $app = new Illuminate\Foundation\Application( realpath(__DIR__.'/../') ); diff --git a/composer.json b/composer.json index 222bdc6cf0..6f7bea2597 100755 --- a/composer.json +++ b/composer.json @@ -5,16 +5,36 @@ "finance", "finances", "manager", + "management", "euro", + "dollar", "laravel", "money", + "currency", "financials", + "financial", "budgets", + "administration", + "tool", + "tooling", + "help", + "helper", + "assistant", + "planning", + "organizing", + "bills", + "personal finance", + "budgets", + "budgeting", + "budgeting tool", + "budgeting application", "transactions", + "self hosted", + "self-hosted", "transfers", "management" ], - "license": "MIT", + "license": "Creative Commons Attribution-ShareAlike 4.0 International License", "homepage": "https://github.com/firefly-iii/firefly-iii", "type": "project", "authors": [ @@ -28,7 +48,7 @@ "require": { "php": ">=7.0.0", "ext-intl": "*", - "laravel/framework": "5.3.28", + "laravel/framework": "5.3.29", "davejamesmiller/laravel-breadcrumbs": "^3.0", "watson/validating": "3.*", "doctrine/dbal": "^2.5", @@ -73,13 +93,14 @@ ], "post-install-cmd": [ "Illuminate\\Foundation\\ComposerScripts::postInstall", - "php artisan optimize" + "php artisan optimize", + "php artisan firefly:instructions install" ], "post-update-cmd": [ "Illuminate\\Foundation\\ComposerScripts::postUpdate", - "php artisan firefly:upgrade-instructions", "php artisan firefly:upgrade-database", "php artisan firefly:verify", + "php artisan firefly:instructions update", "php artisan optimize" ] }, diff --git a/composer.lock b/composer.lock index 7cc131d34a..ae477e0989 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "c1354d0797f44315708cc46642aca068", + "content-hash": "db26ae145d3656fe05d8a222fc21e263", "packages": [ { "name": "bacon/bacon-qr-code", @@ -378,28 +378,29 @@ }, { "name": "doctrine/collections", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a" + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/6c1e4eef75f310ea1b3e30945e9f06e652128b8a", - "reference": "6c1e4eef75f310ea1b3e30945e9f06e652128b8a", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "doctrine/coding-standard": "~0.1@dev", + "phpunit/phpunit": "^5.7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { @@ -440,20 +441,20 @@ "collections", "iterator" ], - "time": "2015-04-14T22:21:58+00:00" + "time": "2017-01-03T10:49:41+00:00" }, { "name": "doctrine/common", - "version": "v2.6.2", + "version": "v2.7.2", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "7bce00698899aa2c06fe7365c76e4d78ddb15fa3" + "reference": "930297026c8009a567ac051fd545bf6124150347" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/7bce00698899aa2c06fe7365c76e4d78ddb15fa3", - "reference": "7bce00698899aa2c06fe7365c76e4d78ddb15fa3", + "url": "https://api.github.com/repos/doctrine/common/zipball/930297026c8009a567ac051fd545bf6124150347", + "reference": "930297026c8009a567ac051fd545bf6124150347", "shasum": "" }, "require": { @@ -462,10 +463,10 @@ "doctrine/collections": "1.*", "doctrine/inflector": "1.*", "doctrine/lexer": "1.*", - "php": "~5.5|~7.0" + "php": "~5.6|~7.0" }, "require-dev": { - "phpunit/phpunit": "~4.8|~5.0" + "phpunit/phpunit": "^5.4.6" }, "type": "library", "extra": { @@ -513,24 +514,24 @@ "persistence", "spl" ], - "time": "2016-11-30T16:50:46+00:00" + "time": "2017-01-13T14:02:13+00:00" }, { "name": "doctrine/dbal", - "version": "v2.5.5", + "version": "v2.5.10", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "9f8c05cd5225a320d56d4bfdb4772f10d045a0c9" + "reference": "fc376f7a61498e18520cd6fa083752a4ca08072b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/9f8c05cd5225a320d56d4bfdb4772f10d045a0c9", - "reference": "9f8c05cd5225a320d56d4bfdb4772f10d045a0c9", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/fc376f7a61498e18520cd6fa083752a4ca08072b", + "reference": "fc376f7a61498e18520cd6fa083752a4ca08072b", "shasum": "" }, "require": { - "doctrine/common": ">=2.4,<2.7-dev", + "doctrine/common": ">=2.4,<2.8-dev", "php": ">=5.3.2" }, "require-dev": { @@ -584,7 +585,7 @@ "persistence", "queryobject" ], - "time": "2016-09-09T19:13:33+00:00" + "time": "2017-01-23T23:17:10+00:00" }, { "name": "doctrine/inflector", @@ -854,16 +855,16 @@ }, { "name": "laravel/framework", - "version": "v5.3.28", + "version": "v5.3.29", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "a64fc4f8958091ca39623b2e8c8f173cb34fa47a" + "reference": "6fd76dec90466dc3f703d8df72e38130f2ee6a32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/a64fc4f8958091ca39623b2e8c8f173cb34fa47a", - "reference": "a64fc4f8958091ca39623b2e8c8f173cb34fa47a", + "url": "https://api.github.com/repos/laravel/framework/zipball/6fd76dec90466dc3f703d8df72e38130f2ee6a32", + "reference": "6fd76dec90466dc3f703d8df72e38130f2ee6a32", "shasum": "" }, "require": { @@ -880,7 +881,7 @@ "php": ">=5.6.4", "psy/psysh": "0.7.*|0.8.*", "ramsey/uuid": "~3.0", - "swiftmailer/swiftmailer": "~5.1", + "swiftmailer/swiftmailer": "~5.4", "symfony/console": "3.1.*", "symfony/debug": "3.1.*", "symfony/finder": "3.1.*", @@ -978,20 +979,20 @@ "framework", "laravel" ], - "time": "2016-12-15T18:03:17+00:00" + "time": "2017-01-06T14:33:56+00:00" }, { "name": "laravelcollective/html", - "version": "v5.3.0", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/LaravelCollective/html.git", - "reference": "961ce141c16c6b085128f209496c26efd3e681ca" + "reference": "2f7f2e127c6fed47f269ea29ab5efeb8f65e9d35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/LaravelCollective/html/zipball/961ce141c16c6b085128f209496c26efd3e681ca", - "reference": "961ce141c16c6b085128f209496c26efd3e681ca", + "url": "https://api.github.com/repos/LaravelCollective/html/zipball/2f7f2e127c6fed47f269ea29ab5efeb8f65e9d35", + "reference": "2f7f2e127c6fed47f269ea29ab5efeb8f65e9d35", "shasum": "" }, "require": { @@ -1032,7 +1033,7 @@ ], "description": "HTML and Form Builders for the Laravel Framework", "homepage": "http://laravelcollective.com", - "time": "2016-08-27T23:52:43+00:00" + "time": "2016-12-13T14:23:36+00:00" }, { "name": "league/commonmark", @@ -1105,16 +1106,16 @@ }, { "name": "league/csv", - "version": "8.1.2", + "version": "8.2.0", "source": { "type": "git", "url": "https://github.com/thephpleague/csv.git", - "reference": "33447984f7a7038fefaa5a6177e8407b66bc85b4" + "reference": "ef7eef710810c8bd0cf9371582ccd0123ff96d4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/csv/zipball/33447984f7a7038fefaa5a6177e8407b66bc85b4", - "reference": "33447984f7a7038fefaa5a6177e8407b66bc85b4", + "url": "https://api.github.com/repos/thephpleague/csv/zipball/ef7eef710810c8bd0cf9371582ccd0123ff96d4b", + "reference": "ef7eef710810c8bd0cf9371582ccd0123ff96d4b", "shasum": "" }, "require": { @@ -1128,7 +1129,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "8.1-dev" + "dev-master": "8.2-dev" } }, "autoload": { @@ -1158,20 +1159,20 @@ "read", "write" ], - "time": "2016-10-27T11:21:24+00:00" + "time": "2017-01-25T13:32:07+00:00" }, { "name": "league/flysystem", - "version": "1.0.32", + "version": "1.0.33", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "1b5c4a0031697f46e779a9d1b309c2e1b24daeab" + "reference": "5c7f98498b12d47f9de90ec9186a90000125777c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1b5c4a0031697f46e779a9d1b309c2e1b24daeab", - "reference": "1b5c4a0031697f46e779a9d1b309c2e1b24daeab", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5c7f98498b12d47f9de90ec9186a90000125777c", + "reference": "5c7f98498b12d47f9de90ec9186a90000125777c", "shasum": "" }, "require": { @@ -1241,7 +1242,7 @@ "sftp", "storage" ], - "time": "2016-10-19T20:38:46+00:00" + "time": "2017-01-23T10:32:09+00:00" }, { "name": "monolog/monolog", @@ -1323,16 +1324,16 @@ }, { "name": "mtdowling/cron-expression", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/mtdowling/cron-expression.git", - "reference": "c9ee7886f5a12902b225a1a12f36bb45f9ab89e5" + "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/c9ee7886f5a12902b225a1a12f36bb45f9ab89e5", - "reference": "c9ee7886f5a12902b225a1a12f36bb45f9ab89e5", + "url": "https://api.github.com/repos/mtdowling/cron-expression/zipball/9504fa9ea681b586028adaaa0877db4aecf32bad", + "reference": "9504fa9ea681b586028adaaa0877db4aecf32bad", "shasum": "" }, "require": { @@ -1343,8 +1344,8 @@ }, "type": "library", "autoload": { - "psr-0": { - "Cron": "src/" + "psr-4": { + "Cron\\": "src/Cron/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1363,30 +1364,36 @@ "cron", "schedule" ], - "time": "2016-01-26T21:23:30+00:00" + "time": "2017-01-23T04:29:33+00:00" }, { "name": "nesbot/carbon", - "version": "1.21.0", + "version": "1.22.1", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "7b08ec6f75791e130012f206e3f7b0e76e18e3d7" + "reference": "7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/7b08ec6f75791e130012f206e3f7b0e76e18e3d7", - "reference": "7b08ec6f75791e130012f206e3f7b0e76e18e3d7", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc", + "reference": "7cdf42c0b1cc763ab7e4c33c47a24e27c66bfccc", "shasum": "" }, "require": { "php": ">=5.3.0", - "symfony/translation": "~2.6|~3.0" + "symfony/translation": "~2.6 || ~3.0" }, "require-dev": { - "phpunit/phpunit": "~4.0|~5.0" + "friendsofphp/php-cs-fixer": "~2", + "phpunit/phpunit": "~4.0 || ~5.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.23-dev" + } + }, "autoload": { "psr-4": { "Carbon\\": "src/Carbon/" @@ -1410,7 +1417,7 @@ "datetime", "time" ], - "time": "2015-11-04T20:07:17+00:00" + "time": "2017-01-16T07:55:07+00:00" }, { "name": "nikic/php-parser", @@ -1621,16 +1628,16 @@ }, { "name": "psy/psysh", - "version": "v0.8.0", + "version": "v0.8.1", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "4a8860e13aa68a4bbf2476c014f8a1f14f1bf991" + "reference": "701e8a1cc426ee170f1296f5d9f6b8a26ad25c4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4a8860e13aa68a4bbf2476c014f8a1f14f1bf991", - "reference": "4a8860e13aa68a4bbf2476c014f8a1f14f1bf991", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/701e8a1cc426ee170f1296f5d9f6b8a26ad25c4a", + "reference": "701e8a1cc426ee170f1296f5d9f6b8a26ad25c4a", "shasum": "" }, "require": { @@ -1660,7 +1667,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-develop": "0.8.x-dev" + "dev-develop": "0.9.x-dev" } }, "autoload": { @@ -1690,7 +1697,7 @@ "interactive", "shell" ], - "time": "2016-12-07T17:15:07+00:00" + "time": "2017-01-15T17:54:13+00:00" }, { "name": "ramsey/uuid", @@ -1776,23 +1783,23 @@ }, { "name": "rcrowe/twigbridge", - "version": "v0.9.3", + "version": "v0.9.4", "source": { "type": "git", "url": "https://github.com/rcrowe/TwigBridge.git", - "reference": "6226d33331bbb1cdf64593a786f7efd1670200a7" + "reference": "effda159c436b08eae1a9d9ba3d28bee8f7b0f3f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/6226d33331bbb1cdf64593a786f7efd1670200a7", - "reference": "6226d33331bbb1cdf64593a786f7efd1670200a7", + "url": "https://api.github.com/repos/rcrowe/TwigBridge/zipball/effda159c436b08eae1a9d9ba3d28bee8f7b0f3f", + "reference": "effda159c436b08eae1a9d9ba3d28bee8f7b0f3f", "shasum": "" }, "require": { - "illuminate/support": "5.0.*|5.1.*|5.2.*|5.3.*", - "illuminate/view": "5.0.*|5.1.*|5.2.*|5.3.*", + "illuminate/support": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*", + "illuminate/view": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*", "php": ">=5.4.0", - "twig/twig": "~1.15|~2.0" + "twig/twig": "~1.30" }, "require-dev": { "laravel/framework": "5.0.*", @@ -1836,7 +1843,7 @@ "laravel", "twig" ], - "time": "2016-05-01T16:43:38+00:00" + "time": "2017-01-21T14:33:47+00:00" }, { "name": "rmccue/requests", @@ -1943,16 +1950,16 @@ }, { "name": "symfony/console", - "version": "v3.1.8", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "221a60fb2f369a065eea1ed96b61183219fdfa6e" + "reference": "047f16485d68c083bd5d9b73ff16f9cb9c1a9f52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/221a60fb2f369a065eea1ed96b61183219fdfa6e", - "reference": "221a60fb2f369a065eea1ed96b61183219fdfa6e", + "url": "https://api.github.com/repos/symfony/console/zipball/047f16485d68c083bd5d9b73ff16f9cb9c1a9f52", + "reference": "047f16485d68c083bd5d9b73ff16f9cb9c1a9f52", "shasum": "" }, "require": { @@ -2000,20 +2007,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2016-12-08T14:58:14+00:00" + "time": "2017-01-08T20:43:43+00:00" }, { "name": "symfony/debug", - "version": "v3.1.8", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "c058661c32f5b462722e36d120905940089cbd9a" + "reference": "c6661361626b3cf5cf2089df98b3b5006a197e85" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/c058661c32f5b462722e36d120905940089cbd9a", - "reference": "c058661c32f5b462722e36d120905940089cbd9a", + "url": "https://api.github.com/repos/symfony/debug/zipball/c6661361626b3cf5cf2089df98b3b5006a197e85", + "reference": "c6661361626b3cf5cf2089df98b3b5006a197e85", "shasum": "" }, "require": { @@ -2057,20 +2064,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2016-11-15T12:55:20+00:00" + "time": "2017-01-28T00:04:57+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.2.1", + "version": "v3.2.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "e8f47a327c2f0fd5aa04fa60af2b693006ed7283" + "reference": "9137eb3a3328e413212826d63eeeb0217836e2b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e8f47a327c2f0fd5aa04fa60af2b693006ed7283", - "reference": "e8f47a327c2f0fd5aa04fa60af2b693006ed7283", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9137eb3a3328e413212826d63eeeb0217836e2b6", + "reference": "9137eb3a3328e413212826d63eeeb0217836e2b6", "shasum": "" }, "require": { @@ -2117,20 +2124,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2016-10-13T06:29:04+00:00" + "time": "2017-01-02T20:32:22+00:00" }, { "name": "symfony/finder", - "version": "v3.1.8", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "74dcd370c8d057882575e535616fde935e411b19" + "reference": "59687a255d1562f2c17b012418273862083d85f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/74dcd370c8d057882575e535616fde935e411b19", - "reference": "74dcd370c8d057882575e535616fde935e411b19", + "url": "https://api.github.com/repos/symfony/finder/zipball/59687a255d1562f2c17b012418273862083d85f7", + "reference": "59687a255d1562f2c17b012418273862083d85f7", "shasum": "" }, "require": { @@ -2166,20 +2173,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2016-12-13T09:38:21+00:00" + "time": "2017-01-02T20:31:54+00:00" }, { "name": "symfony/http-foundation", - "version": "v3.1.8", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "88a1d3cee2dbd06f7103ff63938743b903b65a92" + "reference": "cef0ad49a2e90455cfc649522025b5a2929648c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/88a1d3cee2dbd06f7103ff63938743b903b65a92", - "reference": "88a1d3cee2dbd06f7103ff63938743b903b65a92", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/cef0ad49a2e90455cfc649522025b5a2929648c0", + "reference": "cef0ad49a2e90455cfc649522025b5a2929648c0", "shasum": "" }, "require": { @@ -2219,20 +2226,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2016-11-27T04:21:07+00:00" + "time": "2017-01-08T20:43:43+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.1.8", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "d7a4671a6f8e4174127770263dcd95bee5713f76" + "reference": "c830387dec1b48c100473d10a6a356c3c3ae2a13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/d7a4671a6f8e4174127770263dcd95bee5713f76", - "reference": "d7a4671a6f8e4174127770263dcd95bee5713f76", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/c830387dec1b48c100473d10a6a356c3c3ae2a13", + "reference": "c830387dec1b48c100473d10a6a356c3c3ae2a13", "shasum": "" }, "require": { @@ -2301,7 +2308,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2016-12-13T12:52:10+00:00" + "time": "2017-01-28T02:53:17+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -2472,16 +2479,16 @@ }, { "name": "symfony/process", - "version": "v3.1.8", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d23427a7f97a373129f61bc3b876fe4c66e2b3c7" + "reference": "2605753c5f8c531623d24d002825ebb1d6a22248" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d23427a7f97a373129f61bc3b876fe4c66e2b3c7", - "reference": "d23427a7f97a373129f61bc3b876fe4c66e2b3c7", + "url": "https://api.github.com/repos/symfony/process/zipball/2605753c5f8c531623d24d002825ebb1d6a22248", + "reference": "2605753c5f8c531623d24d002825ebb1d6a22248", "shasum": "" }, "require": { @@ -2517,20 +2524,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2016-11-24T01:08:05+00:00" + "time": "2017-01-21T17:13:55+00:00" }, { "name": "symfony/routing", - "version": "v3.1.8", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "4beb3dceb14cf2dd63dd222d1825ca981a2952cb" + "reference": "f25581d4eb0a82962c291917f826166f0dcd8a9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/4beb3dceb14cf2dd63dd222d1825ca981a2952cb", - "reference": "4beb3dceb14cf2dd63dd222d1825ca981a2952cb", + "url": "https://api.github.com/repos/symfony/routing/zipball/f25581d4eb0a82962c291917f826166f0dcd8a9a", + "reference": "f25581d4eb0a82962c291917f826166f0dcd8a9a", "shasum": "" }, "require": { @@ -2592,20 +2599,20 @@ "uri", "url" ], - "time": "2016-11-25T12:27:14+00:00" + "time": "2017-01-28T00:04:57+00:00" }, { "name": "symfony/translation", - "version": "v3.1.8", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "2f4b6114b75c506dd1ee7eb485b35facbcb2d873" + "reference": "d5a20fab5f63f44c233c69b3041c3cb1d4945e45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/2f4b6114b75c506dd1ee7eb485b35facbcb2d873", - "reference": "2f4b6114b75c506dd1ee7eb485b35facbcb2d873", + "url": "https://api.github.com/repos/symfony/translation/zipball/d5a20fab5f63f44c233c69b3041c3cb1d4945e45", + "reference": "d5a20fab5f63f44c233c69b3041c3cb1d4945e45", "shasum": "" }, "require": { @@ -2656,20 +2663,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2016-11-18T21:15:08+00:00" + "time": "2017-01-21T17:01:39+00:00" }, { "name": "symfony/var-dumper", - "version": "v3.1.8", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "5ccbd23a97035886e595ce497dbe94bc88ac0b57" + "reference": "16df11647e5b992d687cb4eeeb9a882d5f5c26b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5ccbd23a97035886e595ce497dbe94bc88ac0b57", - "reference": "5ccbd23a97035886e595ce497dbe94bc88ac0b57", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/16df11647e5b992d687cb4eeeb9a882d5f5c26b9", + "reference": "16df11647e5b992d687cb4eeeb9a882d5f5c26b9", "shasum": "" }, "require": { @@ -2719,7 +2726,7 @@ "debug", "dump" ], - "time": "2016-12-08T14:58:14+00:00" + "time": "2017-01-24T13:02:38+00:00" }, { "name": "twig/twig", @@ -2886,16 +2893,16 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v2.3.1", + "version": "v2.3.2", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "65b0465e38a9524c9d5eb2dfc0389aba23090625" + "reference": "24e4f0261e352d3fd86d0447791b56ae49398674" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/65b0465e38a9524c9d5eb2dfc0389aba23090625", - "reference": "65b0465e38a9524c9d5eb2dfc0389aba23090625", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/24e4f0261e352d3fd86d0447791b56ae49398674", + "reference": "24e4f0261e352d3fd86d0447791b56ae49398674", "shasum": "" }, "require": { @@ -2936,27 +2943,27 @@ "profiler", "webprofiler" ], - "time": "2017-01-05T08:53:44+00:00" + "time": "2017-01-19T08:19:49+00:00" }, { "name": "barryvdh/laravel-ide-helper", - "version": "v2.2.2", + "version": "v2.2.3", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "105f14a50d0959a0e80004a15b3350fdf78f9623" + "reference": "a7fc2ec489aada6062d3a63ddc915004a21e38af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/105f14a50d0959a0e80004a15b3350fdf78f9623", - "reference": "105f14a50d0959a0e80004a15b3350fdf78f9623", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/a7fc2ec489aada6062d3a63ddc915004a21e38af", + "reference": "a7fc2ec489aada6062d3a63ddc915004a21e38af", "shasum": "" }, "require": { "barryvdh/reflection-docblock": "^2.0.4", - "illuminate/console": "^5.0,<5.4", - "illuminate/filesystem": "^5.0,<5.4", - "illuminate/support": "^5.0,<5.4", + "illuminate/console": "^5.0,<5.5", + "illuminate/filesystem": "^5.0,<5.5", + "illuminate/support": "^5.0,<5.5", "php": ">=5.4.0", "symfony/class-loader": "^2.3|^3.0" }, @@ -3002,7 +3009,7 @@ "phpstorm", "sublime" ], - "time": "2016-11-15T08:21:23+00:00" + "time": "2017-01-05T21:20:42+00:00" }, { "name": "barryvdh/reflection-docblock", @@ -3378,16 +3385,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.5.5", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108" + "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/399c1f9781e222f6eb6cc238796f5200d1b7f108", - "reference": "399c1f9781e222f6eb6cc238796f5200d1b7f108", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/5a5a9fc8025a08d8919be87d6884d5a92520cefe", + "reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe", "shasum": "" }, "require": { @@ -3416,7 +3423,7 @@ "object", "object graph" ], - "time": "2016-10-31T17:19:45+00:00" + "time": "2017-01-26T22:05:40+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -3629,16 +3636,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "c14196e64a78570034afd0b7a9f3757ba71c2a0a" + "reference": "c19cfc7cbb0e9338d8c469c7eedecc2a428b0971" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c14196e64a78570034afd0b7a9f3757ba71c2a0a", - "reference": "c14196e64a78570034afd0b7a9f3757ba71c2a0a", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c19cfc7cbb0e9338d8c469c7eedecc2a428b0971", + "reference": "c19cfc7cbb0e9338d8c469c7eedecc2a428b0971", "shasum": "" }, "require": { @@ -3688,7 +3695,7 @@ "testing", "xunit" ], - "time": "2016-12-20T15:22:42+00:00" + "time": "2017-01-20T15:06:43+00:00" }, { "name": "phpunit/php-file-iterator", @@ -3873,16 +3880,16 @@ }, { "name": "phpunit/phpunit", - "version": "5.7.5", + "version": "5.7.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "50fd2be8f3e23e91da825f36f08e5f9633076ffe" + "reference": "69f832b87c731d5cacad7f91948778fe98335fdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/50fd2be8f3e23e91da825f36f08e5f9633076ffe", - "reference": "50fd2be8f3e23e91da825f36f08e5f9633076ffe", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/69f832b87c731d5cacad7f91948778fe98335fdd", + "reference": "69f832b87c731d5cacad7f91948778fe98335fdd", "shasum": "" }, "require": { @@ -3894,7 +3901,7 @@ "myclabs/deep-copy": "~1.3", "php": "^5.6 || ^7.0", "phpspec/prophecy": "^1.6.2", - "phpunit/php-code-coverage": "^4.0.3", + "phpunit/php-code-coverage": "^4.0.4", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", "phpunit/php-timer": "^1.0.6", @@ -3951,7 +3958,7 @@ "testing", "xunit" ], - "time": "2016-12-28T07:18:51+00:00" + "time": "2017-01-28T06:14:33+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -4059,16 +4066,16 @@ }, { "name": "sebastian/comparator", - "version": "1.2.2", + "version": "1.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "6a1ed12e8b2409076ab22e3897126211ff8b1f7f" + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a1ed12e8b2409076ab22e3897126211ff8b1f7f", - "reference": "6a1ed12e8b2409076ab22e3897126211ff8b1f7f", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", "shasum": "" }, "require": { @@ -4119,7 +4126,7 @@ "compare", "equality" ], - "time": "2016-11-19T09:18:40+00:00" + "time": "2017-01-29T09:50:25+00:00" }, { "name": "sebastian/diff", @@ -4527,16 +4534,16 @@ }, { "name": "symfony/class-loader", - "version": "v3.2.1", + "version": "v3.2.2", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "87cd4e69435d98de01d0162c5f9c0ac017075c63" + "reference": "0152f7a47acd564ca62c652975c2b32ac6d613a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/87cd4e69435d98de01d0162c5f9c0ac017075c63", - "reference": "87cd4e69435d98de01d0162c5f9c0ac017075c63", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/0152f7a47acd564ca62c652975c2b32ac6d613a6", + "reference": "0152f7a47acd564ca62c652975c2b32ac6d613a6", "shasum": "" }, "require": { @@ -4579,20 +4586,20 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2016-11-29T08:26:13+00:00" + "time": "2017-01-10T14:14:38+00:00" }, { "name": "symfony/css-selector", - "version": "v3.1.8", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "a37b3359566415a91cba55a2d95820b3fa1a9658" + "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/a37b3359566415a91cba55a2d95820b3fa1a9658", - "reference": "a37b3359566415a91cba55a2d95820b3fa1a9658", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d", + "reference": "722a87478a72d95dc2a3bcf41dc9c2d13fd4cb2d", "shasum": "" }, "require": { @@ -4632,20 +4639,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2016-11-03T08:04:31+00:00" + "time": "2017-01-02T20:31:54+00:00" }, { "name": "symfony/dom-crawler", - "version": "v3.1.8", + "version": "v3.1.10", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "51e979357eba65b1e6aac7cba75cf5aa6379b8f3" + "reference": "7eede2a901a19928494194f7d1815a77b9a473a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/51e979357eba65b1e6aac7cba75cf5aa6379b8f3", - "reference": "51e979357eba65b1e6aac7cba75cf5aa6379b8f3", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/7eede2a901a19928494194f7d1815a77b9a473a0", + "reference": "7eede2a901a19928494194f7d1815a77b9a473a0", "shasum": "" }, "require": { @@ -4688,20 +4695,20 @@ ], "description": "Symfony DomCrawler Component", "homepage": "https://symfony.com", - "time": "2016-12-10T14:24:45+00:00" + "time": "2017-01-21T17:13:55+00:00" }, { "name": "symfony/yaml", - "version": "v3.2.1", + "version": "v3.2.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "a7095af4b97a0955f85c8989106c249fa649011f" + "reference": "50eadbd7926e31842893c957eca362b21592a97d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/a7095af4b97a0955f85c8989106c249fa649011f", - "reference": "a7095af4b97a0955f85c8989106c249fa649011f", + "url": "https://api.github.com/repos/symfony/yaml/zipball/50eadbd7926e31842893c957eca362b21592a97d", + "reference": "50eadbd7926e31842893c957eca362b21592a97d", "shasum": "" }, "require": { @@ -4743,7 +4750,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-12-10T10:07:06+00:00" + "time": "2017-01-03T13:51:32+00:00" }, { "name": "webmozart/assert", diff --git a/config/firefly.php b/config/firefly.php index 73ae9563e8..7d0627c636 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -22,8 +22,9 @@ return [ 'single_user_mode' => true, 'is_demo_site' => false, ], + 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), 'chart' => 'chartjs', - 'version' => '4.3.2', + 'version' => '4.3.3', 'csv_import_enabled' => true, 'maxUploadSize' => 5242880, 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'], @@ -40,12 +41,7 @@ return [ 'default_export_format' => 'csv', 'default_import_format' => 'csv', 'bill_periods' => ['weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], - 'accountRoles' => [ - 'defaultAsset' => 'Default asset account', - 'sharedAsset' => 'Shared asset account', - 'savingAsset' => 'Savings account', - 'ccAsset' => 'Credit card', - ], + 'accountRoles' => ['defaultAsset', 'sharedAsset', 'savingAsset', 'ccAsset',], 'ccTypes' => [ 'monthlyFull' => 'Full payment every month', ], @@ -170,7 +166,6 @@ return [ 'to_account_ends' => 'FireflyIII\Rules\Triggers\ToAccountEnds', 'to_account_is' => 'FireflyIII\Rules\Triggers\ToAccountIs', 'to_account_contains' => 'FireflyIII\Rules\Triggers\ToAccountContains', - 'transaction_type' => 'FireflyIII\Rules\Triggers\TransactionType', 'amount_less' => 'FireflyIII\Rules\Triggers\AmountLess', 'amount_exactly' => 'FireflyIII\Rules\Triggers\AmountExactly', 'amount_more' => 'FireflyIII\Rules\Triggers\AmountMore', @@ -178,6 +173,10 @@ return [ 'description_ends' => 'FireflyIII\Rules\Triggers\DescriptionEnds', 'description_contains' => 'FireflyIII\Rules\Triggers\DescriptionContains', 'description_is' => 'FireflyIII\Rules\Triggers\DescriptionIs', + 'transaction_type' => 'FireflyIII\Rules\Triggers\TransactionType', + 'category_is' => 'FireflyIII\Rules\Triggers\CategoryIs', + 'budget_is' => 'FireflyIII\Rules\Triggers\BudgetIs', + 'tag_is' => 'FireflyIII\Rules\Triggers\TagIs', ], 'rule-actions' => [ 'set_category' => 'FireflyIII\Rules\Actions\SetCategory', diff --git a/config/upgrade.php b/config/upgrade.php index e88f92c76e..199596e690 100644 --- a/config/upgrade.php +++ b/config/upgrade.php @@ -12,5 +12,14 @@ declare(strict_types = 1); return [ - 'text' => [], + 'text' => [ + 'upgrade' => + [ + '4.3' => 'Make sure you run the migrations and clear your cache. If you need more help, please check Github or the Firefly III website.', + ], + 'install' => + [ + '4.3' => 'Welcome to Firefly! Make sure you follow the installation guide. If you need more help, please check Github or the Firefly III website. The installation guide has a FAQ which you should check out as well.', + ], + ], ]; diff --git a/public/css/bootstrap-multiselect.css b/public/css/bootstrap-multiselect.css new file mode 100755 index 0000000000..5acaf9f7ab --- /dev/null +++ b/public/css/bootstrap-multiselect.css @@ -0,0 +1 @@ +span.multiselect-native-select{position:relative}span.multiselect-native-select select{border:0!important;clip:rect(0 0 0 0)!important;height:1px!important;margin:-1px -1px -1px -3px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;left:50%;top:30px}.multiselect-container{position:absolute;list-style-type:none;margin:0;padding:0}.multiselect-container .input-group{margin:5px}.multiselect-container>li{padding:0}.multiselect-container>li>a.multiselect-all label{font-weight:700}.multiselect-container>li.multiselect-group label{margin:0;padding:3px 20px 3px 20px;height:100%;font-weight:700}.multiselect-container>li.multiselect-group-clickable label{cursor:pointer}.multiselect-container>li>a{padding:0}.multiselect-container>li>a>label{margin:0;height:100%;cursor:pointer;font-weight:400;padding:3px 20px 3px 40px}.multiselect-container>li>a>label.radio,.multiselect-container>li>a>label.checkbox{margin:0}.multiselect-container>li>a>label>input[type=checkbox]{margin-bottom:5px}.btn-group>.btn-group:nth-child(2)>.multiselect.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.form-inline .multiselect-container label.checkbox,.form-inline .multiselect-container label.radio{padding:3px 20px 3px 40px}.form-inline .multiselect-container li a label.checkbox input[type=checkbox],.form-inline .multiselect-container li a label.radio input[type=radio]{margin-left:-20px;margin-right:0} diff --git a/public/css/firefly.css b/public/css/firefly.css index d7958dabc0..cc051e6442 100644 --- a/public/css/firefly.css +++ b/public/css/firefly.css @@ -31,7 +31,7 @@ body.waiting * { } .preferences-box { - border:1px #ddd solid; + border: 1px #ddd solid; border-radius: 4px 4px 0 0; padding: 15px; margin: 15px; @@ -101,4 +101,13 @@ body.waiting * { a[href]:after { content: none !important; } -} \ No newline at end of file +} + +.edit_tr_buttons { + white-space: nowrap; +} + +.edit_tr_buttons .btn { + float: none; + display: inline-block; +} diff --git a/public/css/jquery-ui/images/ui-icons_444444_256x240.png b/public/css/jquery-ui/images/ui-icons_444444_256x240.png new file mode 100644 index 0000000000..19f664d970 Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_444444_256x240.png differ diff --git a/public/css/jquery-ui/images/ui-icons_555555_256x240.png b/public/css/jquery-ui/images/ui-icons_555555_256x240.png new file mode 100644 index 0000000000..e965f6d97c Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_555555_256x240.png differ diff --git a/public/css/jquery-ui/images/ui-icons_777620_256x240.png b/public/css/jquery-ui/images/ui-icons_777620_256x240.png new file mode 100644 index 0000000000..9785948a29 Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_777620_256x240.png differ diff --git a/public/css/jquery-ui/images/ui-icons_777777_256x240.png b/public/css/jquery-ui/images/ui-icons_777777_256x240.png new file mode 100644 index 0000000000..323c4564a7 Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_777777_256x240.png differ diff --git a/public/css/jquery-ui/images/ui-icons_cc0000_256x240.png b/public/css/jquery-ui/images/ui-icons_cc0000_256x240.png new file mode 100644 index 0000000000..45ac7787cd Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_cc0000_256x240.png differ diff --git a/public/css/jquery-ui/images/ui-icons_ffffff_256x240.png b/public/css/jquery-ui/images/ui-icons_ffffff_256x240.png new file mode 100644 index 0000000000..fe41d2d0fd Binary files /dev/null and b/public/css/jquery-ui/images/ui-icons_ffffff_256x240.png differ diff --git a/public/css/jquery-ui/jquery-ui.structure.min.css b/public/css/jquery-ui/jquery-ui.structure.min.css new file mode 100644 index 0000000000..882d445bde --- /dev/null +++ b/public/css/jquery-ui/jquery-ui.structure.min.css @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.12.1 - 2017-01-15 +* http://jqueryui.com +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em} \ No newline at end of file diff --git a/public/css/jquery-ui/jquery-ui.theme.min.css b/public/css/jquery-ui/jquery-ui.theme.min.css new file mode 100644 index 0000000000..4c88d3c36b --- /dev/null +++ b/public/css/jquery-ui/jquery-ui.theme.min.css @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.12.1 - 2017-01-15 +* http://jqueryui.com +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +.ui-widget{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Arial,Helvetica,sans-serif;font-size:1em}.ui-widget.ui-widget-content{border:1px solid #c5c5c5}.ui-widget-content{border:1px solid #ddd;background:#fff;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #ddd;background:#e9e9e9;color:#333;font-weight:bold}.ui-widget-header a{color:#333}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default,.ui-button,html .ui-button.ui-state-disabled:hover,html .ui-button.ui-state-disabled:active{border:1px solid #c5c5c5;background:#f6f6f6;font-weight:normal;color:#454545}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited,a.ui-button,a:link.ui-button,a:visited.ui-button,.ui-button{color:#454545;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus,.ui-button:hover,.ui-button:focus{border:1px solid #ccc;background:#ededed;font-weight:normal;color:#2b2b2b}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,a.ui-button:hover,a.ui-button:focus{color:#2b2b2b;text-decoration:none}.ui-visual-focus{box-shadow:0 0 3px 1px rgb(94,158,214)}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active,a.ui-button:active,.ui-button:active,.ui-button.ui-state-active:hover{border:1px solid #003eff;background:#007fff;font-weight:normal;color:#fff}.ui-icon-background,.ui-state-active .ui-icon-background{border:#003eff;background-color:#fff}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#fff;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #dad55e;background:#fffa90;color:#777620}.ui-state-checked{border:1px solid #dad55e;background:#fffa90}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#777620}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #f1a899;background:#fddfdf;color:#5f3f3f}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#5f3f3f}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#5f3f3f}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_444444_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon,.ui-button:hover .ui-icon,.ui-button:focus .ui-icon{background-image:url("images/ui-icons_555555_256x240.png")}.ui-state-active .ui-icon,.ui-button:active .ui-icon{background-image:url("images/ui-icons_ffffff_256x240.png")}.ui-state-highlight .ui-icon,.ui-button .ui-state-highlight.ui-icon{background-image:url("images/ui-icons_777620_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cc0000_256x240.png")}.ui-button .ui-icon{background-image:url("images/ui-icons_777777_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-caret-1-n{background-position:0 0}.ui-icon-caret-1-ne{background-position:-16px 0}.ui-icon-caret-1-e{background-position:-32px 0}.ui-icon-caret-1-se{background-position:-48px 0}.ui-icon-caret-1-s{background-position:-65px 0}.ui-icon-caret-1-sw{background-position:-80px 0}.ui-icon-caret-1-w{background-position:-96px 0}.ui-icon-caret-1-nw{background-position:-112px 0}.ui-icon-caret-2-n-s{background-position:-128px 0}.ui-icon-caret-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-65px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-65px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:1px -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:3px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:3px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:3px}.ui-widget-overlay{background:#aaa;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{-webkit-box-shadow:0 0 5px #666;box-shadow:0 0 5px #666} \ No newline at end of file diff --git a/public/js/ff/accounts/create.js b/public/js/ff/accounts/create.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/accounts/create.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/accounts/edit.js b/public/js/ff/accounts/edit.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/accounts/edit.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/accounts/show.js b/public/js/ff/accounts/show.js index a1db23e92a..b3b1093c7a 100644 --- a/public/js/ff/accounts/show.js +++ b/public/js/ff/accounts/show.js @@ -54,7 +54,7 @@ $(function () { ui.placeholder.html(' '); } } - ).disableSelection(); + ); } }); diff --git a/public/js/ff/bills/create.js b/public/js/ff/bills/create.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/bills/create.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/bills/edit.js b/public/js/ff/bills/edit.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/bills/edit.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/export/index.js b/public/js/ff/export/index.js index 006afca9ba..e7af126f51 100644 --- a/public/js/ff/export/index.js +++ b/public/js/ff/export/index.js @@ -8,7 +8,7 @@ * See the LICENSE file for details. */ -/** global: jobKey */ +/** global: jobKey, Modernizr */ var intervalId = 0; @@ -21,6 +21,14 @@ $(function () { // - return false, $('#export').submit(startExport); + + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } } ); diff --git a/public/js/ff/piggy-banks/create.js b/public/js/ff/piggy-banks/create.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/piggy-banks/create.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/piggy-banks/edit.js b/public/js/ff/piggy-banks/edit.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/piggy-banks/edit.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/preferences/index.js b/public/js/ff/preferences/index.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/preferences/index.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/reports/index.js b/public/js/ff/reports/index.js index 77c15f0578..b57faf8b04 100644 --- a/public/js/ff/reports/index.js +++ b/public/js/ff/reports/index.js @@ -8,7 +8,17 @@ * See the LICENSE file for details. */ -/** global: minDate */ +/** global: minDate, nonSelectedText, allSelectedText, filterPlaceholder */ + +var defaultMultiSelect = { + disableIfEmpty: true, + nonSelectedText: nonSelectedText, + allSelectedText: allSelectedText, + includeSelectAllOption: true, + enableFiltering: true, + enableCaseInsensitiveFiltering: true, + filterPlaceholder: filterPlaceholder +}; $(function () { "use strict"; @@ -26,19 +36,24 @@ $(function () { } ); - // set values from cookies, if any: + + // set report type from cookie, if any: if (!(readCookie('report-type') === null)) { $('select[name="report_type"]').val(readCookie('report-type')); } + // set accounts from cookie if ((readCookie('report-accounts') !== null)) { var arr = readCookie('report-accounts').split(','); arr.forEach(function (val) { - $('input[class="account-checkbox"][type="checkbox"][value="' + val + '"]').prop('checked', true); + $('#inputAccounts').find('option[value="' + val + '"]').prop('selected', true); }); } - // set date: + // make account select a hip new bootstrap multi-select thing. + $('#inputAccounts').multiselect(defaultMultiSelect); + + // set date from cookie var startStr = readCookie('report-start'); var endStr = readCookie('report-end'); if (startStr !== null && endStr !== null && startStr.length == 8 && endStr.length == 8) { @@ -81,16 +96,18 @@ function setOptionalFromCookies() { if ((readCookie('report-categories') !== null)) { arr = readCookie('report-categories').split(','); arr.forEach(function (val) { - $('input[class="category-checkbox"][type="checkbox"][value="' + val + '"]').prop('checked', true); + $('#inputCategories').find('option[value="' + val + '"]').prop('selected', true); }); + $('#inputCategories').multiselect(defaultMultiSelect); } // and budgets! if ((readCookie('report-budgets') !== null)) { arr = readCookie('report-budgets').split(','); arr.forEach(function (val) { - $('input[class="budget-checkbox"][type="checkbox"][value="' + val + '"]').prop('checked', true); + $('#inputBudgets').find('option[value="' + val + '"]').prop('selected', true); }); + $('#inputBudgets').multiselect(defaultMultiSelect); } } diff --git a/public/js/ff/rules/create-edit.js b/public/js/ff/rules/create-edit.js index cd36a7b1ad..9100050012 100644 --- a/public/js/ff/rules/create-edit.js +++ b/public/js/ff/rules/create-edit.js @@ -8,47 +8,99 @@ * See the LICENSE file for details. */ -var triggerCount = 0; -var actionCount = 0; +/** global: triggerCount, actionCount */ +$(function () { + "use strict"; + if (triggerCount === 0) { + addNewTrigger(); + } + if (actionCount === 0) { + addNewAction(); + } + if (triggerCount > 0) { + onAddNewTrigger(); + } + + if (actionCount > 0) { + onAddNewAction(); + } + + $('.add_rule_trigger').click(addNewTrigger); + $('.add_rule_action').click(addNewAction); + $('.test_rule_triggers').click(testRuleTriggers); + $('.remove-trigger').unbind('click').click(removeTrigger); + $('.remove-action').unbind('click').click(removeAction); +}); + +/** + * This method triggers when a new trigger must be added to the form. + */ function addNewTrigger() { "use strict"; triggerCount++; + // disable the button + $('.add_rule_trigger').attr('disabled', 'disabled'); + + // get the HTML for the new trigger $.getJSON('json/trigger', {count: triggerCount}).done(function (data) { + + // append it to the other triggers $('tbody.rule-trigger-tbody').append(data.html); + $('.remove-trigger').unbind('click').click(removeTrigger); - $('.remove-trigger').unbind('click').click(function (e) { - removeTrigger(e); + // update all "select trigger type" dropdowns + // so the accompanying text-box has the correct autocomplete. + onAddNewTrigger(); + + $('.add_rule_trigger').removeAttr('disabled'); - return false; - }); }).fail(function () { alert('Cannot get a new trigger.'); + $('.add_rule_trigger').removeAttr('disabled'); }); + return false; } +/** + * Method triggers when a new action must be added to the form.. + */ function addNewAction() { "use strict"; actionCount++; + // disable the button + $('.add_rule_action').attr('disabled', 'disabled'); + + $.getJSON('json/action', {count: actionCount}).done(function (data) { $('tbody.rule-action-tbody').append(data.html); // add action things. - $('.remove-action').unbind('click').click(function (e) { - removeAction(e); + $('.remove-action').unbind('click').click(removeAction); - return false; - }); + // update all "select trigger type" dropdowns + // so the accompanying text-box has the correct autocomplete. + onAddNewAction(); + + $('.add_rule_action').removeAttr('disabled'); }).fail(function () { alert('Cannot get a new action.'); + + $('.add_rule_action').removeAttr('disabled'); }); + return false; } +/** + * Method fires when a trigger must be removed from the form. + * + * @param e + */ function removeTrigger(e) { "use strict"; var target = $(e.target); @@ -62,8 +114,14 @@ function removeTrigger(e) { if ($('.rule-trigger-tbody tr').length == 0) { addNewTrigger(); } + return false; } +/** + * Method fires when an action must be removed from the form. + * + * @param e + */ function removeAction(e) { "use strict"; var target = $(e.target); @@ -77,6 +135,147 @@ function removeAction(e) { if ($('.rule-action-tbody tr').length == 0) { addNewAction(); } + return false; + +} + +/** + * Method fires when a new action is added. It will update ALL action value input fields. + */ +function onAddNewAction() { + "use strict"; + + // update all "select action type" dropdown buttons so they will respond correctly + $('select[name^="rule-action["]').unbind('change').change(function (e) { + var target = $(e.target); + updateActionInput(target) + }); + + $.each($('.rule-action-holder'), function (i, v) { + var holder = $(v); + var select = holder.find('select'); + updateActionInput(select); + }); +} + +/** + * Method fires when a new trigger is added. It will update ALL trigger value input fields. + */ +function onAddNewTrigger() { + "use strict"; + + // update all "select trigger type" dropdown buttons so they will respond correctly + $('select[name^="rule-trigger["]').unbind('change').change(function (e) { + var target = $(e.target); + updateTriggerInput(target) + }); + + $.each($('.rule-trigger-holder'), function (i, v) { + var holder = $(v); + var select = holder.find('select'); + updateTriggerInput(select); + }); +} + +/** + * Creates a nice auto complete action depending on the type of the select list value thing. + * + * @param selectList + */ +function updateActionInput(selectList) { + // the actual row this select list is in: + var parent = selectList.parent().parent(); + // the text input we're looking for: + var input = parent.find('input[name^="rule-action-value["]'); + input.removeAttr('disabled'); + switch (selectList.val()) { + case 'set_category': + createAutoComplete(input, 'json/categories'); + break; + case 'clear_category': + case 'clear_budget': + case 'remove_all_tags': + input.attr('disabled', 'disabled'); + break; + case 'set_budget': + createAutoComplete(input, 'json/budgets'); + break; + case 'add_tag': + case 'remove_tag': + createAutoComplete(input, 'json/tags'); + break; + case 'set_description': + createAutoComplete(input, 'json/transaction-journals/all'); + break; + case 'set_source_account': + createAutoComplete(input, 'json/all-accounts'); + break; + case 'set_destination_account': + createAutoComplete(input, 'json/all-accounts'); + break; + default: + input.typeahead('destroy'); + break; + } +} + +/** + * Creates a nice auto complete trigger depending on the type of the select list value thing. + * + * @param selectList + */ +function updateTriggerInput(selectList) { + // the actual row this select list is in: + var parent = selectList.parent().parent(); + // the text input we're looking for: + var input = parent.find('input[name^="rule-trigger-value["]'); + switch (selectList.val()) { + case 'from_account_starts': + case 'from_account_ends': + case 'from_account_is': + case 'from_account_contains': + case 'to_account_starts': + case 'to_account_ends': + case 'to_account_is': + case 'to_account_contains': + createAutoComplete(input, 'json/all-accounts'); + break; + case 'tag_is': + // also make tag thing? + createAutoComplete(input, 'json/tags'); + break; + case 'budget_is': + createAutoComplete(input, 'json/budgets'); + break; + case 'category_is': + createAutoComplete(input, 'json/categories'); + break; + case 'transaction_type': + createAutoComplete(input, 'json/transaction-types'); + break; + case 'description_starts': + case 'description_ends': + case 'description_contains': + case 'description_is': + createAutoComplete(input, 'json/transaction-journals/all'); + break; + default: + input.typeahead('destroy'); + break; + } +} + +/** + * Create actual autocomplete + * @param input + * @param URI + */ +function createAutoComplete(input, URI) { + input.typeahead('destroy'); + $.getJSON(URI).done(function (data) { + input.typeahead({source: data}); + }); + } function testRuleTriggers() { @@ -105,4 +304,5 @@ function testRuleTriggers() { }).fail(function () { alert('Cannot get transactions for given triggers.'); }); + return false; } \ No newline at end of file diff --git a/public/js/ff/rules/create.js b/public/js/ff/rules/create.js deleted file mode 100644 index ffa65be20e..0000000000 --- a/public/js/ff/rules/create.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * create.js - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms of the - * Creative Commons Attribution-ShareAlike 4.0 International License. - * - * See the LICENSE file for details. - */ - -/** global: triggerCount, actionCount */ - - -$(function () { - "use strict"; - if (triggerCount === 0) { - addNewTrigger(); - } - if (actionCount === 0) { - addNewAction(); - } - - - $('.add_rule_trigger').click(function () { - addNewTrigger(); - - return false; - }); - - $('.add_rule_action').click(function () { - addNewAction(); - - return false; - }); - - $('.test_rule_triggers').click(function () { - testRuleTriggers(); - - return false; - }); -}); diff --git a/public/js/ff/rules/edit.js b/public/js/ff/rules/edit.js deleted file mode 100644 index 893feff138..0000000000 --- a/public/js/ff/rules/edit.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * edit.js - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms of the - * Creative Commons Attribution-ShareAlike 4.0 International License. - * - * See the LICENSE file for details. - */ - -/** global: triggerCount, actionCount */ - -$(function () { - "use strict"; - if (triggerCount === 0) { - addNewTrigger(); - } - if (actionCount === 0) { - addNewAction(); - } - - - $('.add_rule_trigger').click(function () { - addNewTrigger(); - - return false; - }); - - $('.test_rule_triggers').click(function () { - testRuleTriggers(); - - return false; - }); - - $('.add_rule_action').click(function () { - addNewAction(); - - return false; - }); - - $('.remove-trigger').unbind('click').click(function (e) { - removeTrigger(e); - - return false; - }); - - // add action things. - $('.remove-action').unbind('click').click(function (e) { - removeAction(e); - - return false; - }); -}); \ No newline at end of file diff --git a/public/js/ff/rules/select-transactions.js b/public/js/ff/rules/select-transactions.js new file mode 100644 index 0000000000..675a5fe730 --- /dev/null +++ b/public/js/ff/rules/select-transactions.js @@ -0,0 +1,20 @@ +/* + * create.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: Modernizr */ + +$(document).ready(function () { + "use strict"; + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } +}); diff --git a/public/js/ff/tags/create-edit.js b/public/js/ff/tags/create-edit.js index 397f49ccef..7184b17ae2 100644 --- a/public/js/ff/tags/create-edit.js +++ b/public/js/ff/tags/create-edit.js @@ -5,12 +5,19 @@ * * See the LICENSE file for details. */ -/** global: zoomLevel, latitude, longitude, google, apiKey, doPlaceMarker */ +/** global: zoomLevel, latitude, longitude, google, apiKey, doPlaceMarker, Modernizr */ $(function () { "use strict"; $('#clearLocation').click(clearLocation); + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } }); diff --git a/public/js/ff/transactions/create-edit.js b/public/js/ff/transactions/create-edit.js deleted file mode 100644 index 4684f71a45..0000000000 --- a/public/js/ff/transactions/create-edit.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * create-edit.js - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms of the - * Creative Commons Attribution-ShareAlike 4.0 International License. - * - * See the LICENSE file for details. - */ - -/** global: what */ - -$(document).ready(function () { - "use strict"; - - // the destination account name is always an expense account name. - if ($('input[name="destination_account_name"]').length > 0) { - $.getJSON('json/expense-accounts').done(function (data) { - $('input[name="destination_account_name"]').typeahead({source: data}); - }); - } - - // also for multi input - if ($('input[name="destination_account_name[]"]').length > 0) { - $.getJSON('json/expense-accounts').done(function (data) { - $('input[name="destination_account_name[]"]').typeahead({source: data}); - }); - } - - if ($('input[name="tags"]').length > 0) { - $.getJSON('json/tags').done(function (data) { - - var opt = { - typeahead: { - source: data, - afterSelect: function(val) { this.$element.val(""); } - } - }; - $('input[name="tags"]').tagsinput( - opt - ); - }); - } - - // the source account name is always a revenue account name. - if ($('input[name="source_account_name"]').length > 0) { - $.getJSON('json/revenue-accounts').done(function (data) { - $('input[name="source_account_name"]').typeahead({source: data}); - }); - } - // also for multi-input: - if ($('input[name="source_account_name[]"]').length > 0) { - $.getJSON('json/revenue-accounts').done(function (data) { - $('input[name="source_account_name[]"]').typeahead({source: data}); - }); - } - // and for split: - if ($('input[name="journal_source_account_name"]').length > 0) { - $.getJSON('json/revenue-accounts').done(function (data) { - $('input[name="journal_source_account_name"]').typeahead({source: data}); - }); - } - - - if ($('input[name="description"]').length > 0 && !(typeof what === "undefined")) { - $.getJSON('json/transaction-journals/' + what).done(function (data) { - $('input[name="description"]').typeahead({source: data}); - }); - } - // also for multi input: - if ($('input[name="description[]"]').length > 0 && !(typeof what === "undefined")) { - $.getJSON('json/transaction-journals/' + what).done(function (data) { - $('input[name="description[]"]').typeahead({source: data}); - }); - } - // and for the (rare) journal_description: - if ($('input[name="journal_description"]').length > 0 && !(typeof what === "undefined")) { - $.getJSON('json/transaction-journals/' + what).done(function (data) { - $('input[name="journal_description"]').typeahead({source: data}); - }); - } - - if ($('input[name="category"]').length > 0) { - $.getJSON('json/categories').done(function (data) { - $('input[name="category"]').typeahead({source: data}); - }); - } - - // also for multi input: - if ($('input[name^="category["]').length > 0) { - $.getJSON('json/categories').done(function (data) { - $('input[name^="category["]').typeahead({source: data}); - }); - } - - - - -}); \ No newline at end of file diff --git a/public/js/ff/transactions/edit.js b/public/js/ff/transactions/edit.js deleted file mode 100644 index 63390bc3e6..0000000000 --- a/public/js/ff/transactions/edit.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * edit.js - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms of the - * Creative Commons Attribution-ShareAlike 4.0 International License. - * - * See the LICENSE file for details. - */ - -$(document).ready(function () { - "use strict"; - // no special JS for edit transaction. -}); diff --git a/public/js/ff/transactions/mass/edit.js b/public/js/ff/transactions/mass/edit.js new file mode 100644 index 0000000000..0c2ec4a3ba --- /dev/null +++ b/public/js/ff/transactions/mass/edit.js @@ -0,0 +1,31 @@ +/* + * edit.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: what */ + +$(document).ready(function () { + "use strict"; + + // destination account names: + if ($('input[name^="destination_account_name["]').length > 0) { + $.getJSON('json/expense-accounts').done(function (data) { + $('input[name^="destination_account_name["]').typeahead({source: data}); + }); + } + + // source account name + if ($('input[name^="source_account_name["]').length > 0) { + $.getJSON('json/revenue-accounts').done(function (data) { + $('input[name^="source_account_name["]').typeahead({source: data}); + }); + } + + $.getJSON('json/categories').done(function (data) { + $('input[name^="category["]').typeahead({source: data}); + }); +}); \ No newline at end of file diff --git a/public/js/ff/transactions/create.js b/public/js/ff/transactions/single/create.js similarity index 68% rename from public/js/ff/transactions/create.js rename to public/js/ff/transactions/single/create.js index aaed65207a..7a38946984 100644 --- a/public/js/ff/transactions/create.js +++ b/public/js/ff/transactions/single/create.js @@ -1,14 +1,12 @@ /* * create.js - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms of the - * Creative Commons Attribution-ShareAlike 4.0 International License. + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. * * See the LICENSE file for details. */ -/** global: what, title, breadcrumbs, middleCrumbName, button, piggiesLength, txt, doSwitch, middleCrumbUrl */ +/** global: what,Modernizr, title, breadcrumbs, middleCrumbName, button, piggiesLength, txt, doSwitch, middleCrumbUrl */ $(document).ready(function () { "use strict"; @@ -19,11 +17,65 @@ $(document).ready(function () { updateButtons(); updateForm(); updateLayout(); + updateDescription(); } + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } + + // get JSON things: + getJSONautocomplete(); }); +function updateDescription() { + $.getJSON('json/transaction-journals/' + what).done(function (data) { + $('input[name="description"]').typeahead('destroy'); + $('input[name="description"]').typeahead({source: data}); + }); +} + +function getJSONautocomplete() { + + // for withdrawals + $.getJSON('json/expense-accounts').done(function (data) { + $('input[name="destination_account_name"]').typeahead({source: data}); + }); + + // for tags: + if ($('input[name="tags"]').length > 0) { + $.getJSON('json/tags').done(function (data) { + + var opt = { + typeahead: { + source: data, + afterSelect: function () { + this.$element.val(""); + } + } + }; + $('input[name="tags"]').tagsinput( + opt + ); + }); + } + + // for deposits + $.getJSON('json/revenue-accounts').done(function (data) { + $('input[name="source_account_name"]').typeahead({source: data}); + }); + + $.getJSON('json/categories').done(function (data) { + $('input[name="category"]').typeahead({source: data}); + }); + +} + function updateLayout() { "use strict"; $('#subTitle').text(title[what]); @@ -131,6 +183,7 @@ function clickButton(e) { updateButtons(); updateForm(); updateLayout(); + updateDescription(); } return false; } \ No newline at end of file diff --git a/public/js/ff/transactions/single/edit.js b/public/js/ff/transactions/single/edit.js new file mode 100644 index 0000000000..a025212d43 --- /dev/null +++ b/public/js/ff/transactions/single/edit.js @@ -0,0 +1,62 @@ +/* + * edit.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms of the + * Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: what, Modernizr */ + +$(document).ready(function () { + "use strict"; + // give date a datepicker if not natively supported. + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } + + // the destination account name is always an expense account name. + if ($('input[name="destination_account_name"]').length > 0) { + $.getJSON('json/expense-accounts').done(function (data) { + $('input[name="destination_account_name"]').typeahead({source: data}); + }); + } + + $.getJSON('json/tags').done(function (data) { + + var opt = { + typeahead: { + source: data, + afterSelect: function () { + this.$element.val(""); + } + } + }; + $('input[name="tags"]').tagsinput( + opt + ); + }); + + // the source account name is always a revenue account name. + if ($('input[name="source_account_name"]').length > 0) { + $.getJSON('json/revenue-accounts').done(function (data) { + $('input[name="source_account_name"]').typeahead({source: data}); + }); + } + + $.getJSON('json/transaction-journals/' + what).done(function (data) { + $('input[name="description"]').typeahead({source: data}); + }); + + + $.getJSON('json/categories').done(function (data) { + $('input[name="category"]').typeahead({source: data}); + }); + +}); diff --git a/public/js/ff/split/journal/from-store.js b/public/js/ff/transactions/split/edit.js similarity index 80% rename from public/js/ff/split/journal/from-store.js rename to public/js/ff/transactions/split/edit.js index a7323a12a6..19fffbdf4b 100644 --- a/public/js/ff/split/journal/from-store.js +++ b/public/js/ff/transactions/split/edit.js @@ -1,19 +1,20 @@ /* - * from-store.js - * Copyright (C) 2016 thegrumpydictator@gmail.com - * - * This software may be modified and distributed under the terms of the - * Creative Commons Attribution-ShareAlike 4.0 International License. + * edit.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. * * See the LICENSE file for details. */ -/** global: originalSum, accounting */ + +/** global: originalSum, accounting, what, Modernizr */ var destAccounts = {}; var srcAccounts = {}; var categories = {}; -$(function () { +var descriptions = {}; + +$(document).ready(function () { "use strict"; $('.btn-do-split').click(cloneRow); $('.remove-current-split').click(removeRow); @@ -33,13 +34,40 @@ $(function () { $('input[name$="category]"]').typeahead({source: categories}); }); + $.getJSON('json/transaction-journals/' + what).done(function (data) { + descriptions = data; + $('input[name="journal_description"]').typeahead({source: descriptions}); + $('input[name$="description]"]').typeahead({source: descriptions}); + }); + + $.getJSON('json/tags').done(function (data) { + + var opt = { + typeahead: { + source: data, + afterSelect: function () { + this.$element.val(""); + } + } + }; + $('input[name="tags"]').tagsinput( + opt + ); + }); + + $('input[name$="][amount]"]').on('input', calculateSum); - // add auto complete: - - + if (!Modernizr.inputtypes.date) { + $('input[type="date"]').datepicker( + { + dateFormat: 'yy-mm-dd' + } + ); + } }); + function removeRow(e) { "use strict"; var rows = $('table.split-table tbody tr'); @@ -75,6 +103,9 @@ function cloneRow() { if (categories.length > 0) { source.find('input[name$="category]"]').typeahead({source: categories}); } + if (descriptions.length > 0) { + source.find('input[name$="description]"]').typeahead({source: descriptions}); + } $('.split-table tbody').append(source); diff --git a/public/js/lib/bootstrap-multiselect.js b/public/js/lib/bootstrap-multiselect.js new file mode 100755 index 0000000000..9a50a18a0d --- /dev/null +++ b/public/js/lib/bootstrap-multiselect.js @@ -0,0 +1,1716 @@ +/** + * Bootstrap Multiselect (https://github.com/davidstutz/bootstrap-multiselect) + * + * Apache License, Version 2.0: + * Copyright (c) 2012 - 2015 David Stutz + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * BSD 3-Clause License: + * Copyright (c) 2012 - 2015 David Stutz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of David Stutz nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +!function ($) { + "use strict";// jshint ;_; + + if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) { + ko.bindingHandlers.multiselect = { + after: ['options', 'value', 'selectedOptions', 'enable', 'disable'], + + init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { + var $element = $(element); + var config = ko.toJS(valueAccessor()); + + $element.multiselect(config); + + if (allBindings.has('options')) { + var options = allBindings.get('options'); + if (ko.isObservable(options)) { + ko.computed({ + read: function() { + options(); + setTimeout(function() { + var ms = $element.data('multiselect'); + if (ms) + ms.updateOriginalOptions();//Not sure how beneficial this is. + $element.multiselect('rebuild'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }); + } + } + + //value and selectedOptions are two-way, so these will be triggered even by our own actions. + //It needs some way to tell if they are triggered because of us or because of outside change. + //It doesn't loop but it's a waste of processing. + if (allBindings.has('value')) { + var value = allBindings.get('value'); + if (ko.isObservable(value)) { + ko.computed({ + read: function() { + value(); + setTimeout(function() { + $element.multiselect('refresh'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } + } + + //Switched from arrayChange subscription to general subscription using 'refresh'. + //Not sure performance is any better using 'select' and 'deselect'. + if (allBindings.has('selectedOptions')) { + var selectedOptions = allBindings.get('selectedOptions'); + if (ko.isObservable(selectedOptions)) { + ko.computed({ + read: function() { + selectedOptions(); + setTimeout(function() { + $element.multiselect('refresh'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } + } + + var setEnabled = function (enable) { + setTimeout(function () { + if (enable) + $element.multiselect('enable'); + else + $element.multiselect('disable'); + }); + }; + + if (allBindings.has('enable')) { + var enable = allBindings.get('enable'); + if (ko.isObservable(enable)) { + ko.computed({ + read: function () { + setEnabled(enable()); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } else { + setEnabled(enable); + } + } + + if (allBindings.has('disable')) { + var disable = allBindings.get('disable'); + if (ko.isObservable(disable)) { + ko.computed({ + read: function () { + setEnabled(!disable()); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } else { + setEnabled(!disable); + } + } + + ko.utils.domNodeDisposal.addDisposeCallback(element, function() { + $element.multiselect('destroy'); + }); + }, + + update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { + var $element = $(element); + var config = ko.toJS(valueAccessor()); + + $element.multiselect('setOptions', config); + $element.multiselect('rebuild'); + } + }; + } + + function forEach(array, callback) { + for (var index = 0; index < array.length; ++index) { + callback(array[index], index); + } + } + + /** + * Constructor to create a new multiselect using the given select. + * + * @param {jQuery} select + * @param {Object} options + * @returns {Multiselect} + */ + function Multiselect(select, options) { + + this.$select = $(select); + this.options = this.mergeOptions($.extend({}, options, this.$select.data())); + + // Placeholder via data attributes + if (this.$select.attr("data-placeholder")) { + this.options.nonSelectedText = this.$select.data("placeholder"); + } + + // Initialization. + // We have to clone to create a new reference. + this.originalOptions = this.$select.clone()[0].options; + this.query = ''; + this.searchTimeout = null; + this.lastToggledInput = null; + + this.options.multiple = this.$select.attr('multiple') === "multiple"; + this.options.onChange = $.proxy(this.options.onChange, this); + this.options.onSelectAll = $.proxy(this.options.onSelectAll, this); + this.options.onDeselectAll = $.proxy(this.options.onDeselectAll, this); + this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this); + this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this); + this.options.onDropdownShown = $.proxy(this.options.onDropdownShown, this); + this.options.onDropdownHidden = $.proxy(this.options.onDropdownHidden, this); + this.options.onInitialized = $.proxy(this.options.onInitialized, this); + this.options.onFiltering = $.proxy(this.options.onFiltering, this); + + // Build select all if enabled. + this.buildContainer(); + this.buildButton(); + this.buildDropdown(); + this.buildSelectAll(); + this.buildDropdownOptions(); + this.buildFilter(); + + this.updateButtonText(); + this.updateSelectAll(true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + this.options.wasDisabled = this.$select.prop('disabled'); + if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { + this.disable(); + } + + this.$select.wrap('').after(this.$container); + this.options.onInitialized(this.$select, this.$container); + } + + Multiselect.prototype = { + + defaults: { + /** + * Default text function will either print 'None selected' in case no + * option is selected or a list of the selected options up to a length + * of 3 selected options. + * + * @param {jQuery} options + * @param {jQuery} select + * @returns {String} + */ + buttonText: function(options, select) { + if (this.disabledText.length > 0 + && (select.prop('disabled') || (options.length == 0 && this.disableIfEmpty))) { + + return this.disabledText; + } + else if (options.length === 0) { + return this.nonSelectedText; + } + else if (this.allSelectedText + && options.length === $('option', $(select)).length + && $('option', $(select)).length !== 1 + && this.multiple) { + + if (this.selectAllNumber) { + return this.allSelectedText + ' (' + options.length + ')'; + } + else { + return this.allSelectedText; + } + } + else if (options.length > this.numberDisplayed) { + return options.length + ' ' + this.nSelectedText; + } + else { + var selected = ''; + var delimiter = this.delimiterText; + + options.each(function() { + var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); + selected += label + delimiter; + }); + + return selected.substr(0, selected.length - this.delimiterText.length); + } + }, + /** + * Updates the title of the button similar to the buttonText function. + * + * @param {jQuery} options + * @param {jQuery} select + * @returns {@exp;selected@call;substr} + */ + buttonTitle: function(options, select) { + if (options.length === 0) { + return this.nonSelectedText; + } + else { + var selected = ''; + var delimiter = this.delimiterText; + + options.each(function () { + var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); + selected += label + delimiter; + }); + return selected.substr(0, selected.length - this.delimiterText.length); + } + }, + checkboxName: function(option) { + return false; // no checkbox name + }, + /** + * Create a label. + * + * @param {jQuery} element + * @returns {String} + */ + optionLabel: function(element){ + return $(element).attr('label') || $(element).text(); + }, + /** + * Create a class. + * + * @param {jQuery} element + * @returns {String} + */ + optionClass: function(element) { + return $(element).attr('class') || ''; + }, + /** + * Triggered on change of the multiselect. + * + * Not triggered when selecting/deselecting options manually. + * + * @param {jQuery} option + * @param {Boolean} checked + */ + onChange : function(option, checked) { + + }, + /** + * Triggered when the dropdown is shown. + * + * @param {jQuery} event + */ + onDropdownShow: function(event) { + + }, + /** + * Triggered when the dropdown is hidden. + * + * @param {jQuery} event + */ + onDropdownHide: function(event) { + + }, + /** + * Triggered after the dropdown is shown. + * + * @param {jQuery} event + */ + onDropdownShown: function(event) { + + }, + /** + * Triggered after the dropdown is hidden. + * + * @param {jQuery} event + */ + onDropdownHidden: function(event) { + + }, + /** + * Triggered on select all. + */ + onSelectAll: function() { + + }, + /** + * Triggered on deselect all. + */ + onDeselectAll: function() { + + }, + /** + * Triggered after initializing. + * + * @param {jQuery} $select + * @param {jQuery} $container + */ + onInitialized: function($select, $container) { + + }, + /** + * Triggered on filtering. + * + * @param {jQuery} $filter + */ + onFiltering: function($filter) { + + }, + enableHTML: false, + buttonClass: 'btn btn-default', + inheritClass: false, + buttonWidth: 'auto', + buttonContainer: '
', + dropRight: false, + dropUp: false, + selectedClass: 'active', + // Maximum height of the dropdown menu. + // If maximum height is exceeded a scrollbar will be displayed. + maxHeight: false, + includeSelectAllOption: false, + includeSelectAllIfMoreThan: 0, + selectAllText: ' Select all', + selectAllValue: 'multiselect-all', + selectAllName: false, + selectAllNumber: true, + selectAllJustVisible: true, + enableFiltering: false, + enableCaseInsensitiveFiltering: false, + enableFullValueFiltering: false, + enableClickableOptGroups: false, + enableCollapsibleOptGroups: false, + filterPlaceholder: 'Search', + // possible options: 'text', 'value', 'both' + filterBehavior: 'text', + includeFilterClearBtn: true, + preventInputChangeEvent: false, + nonSelectedText: 'None selected', + nSelectedText: 'selected', + allSelectedText: 'All selected', + numberDisplayed: 3, + disableIfEmpty: false, + disabledText: '', + delimiterText: ', ', + templates: { + button: '', + ul: '', + filter: '
  • ', + filterClearBtn: '', + li: '
  • ', + divider: '
  • ', + liGroup: '
  • ' + } + }, + + constructor: Multiselect, + + /** + * Builds the container of the multiselect. + */ + buildContainer: function() { + this.$container = $(this.options.buttonContainer); + this.$container.on('show.bs.dropdown', this.options.onDropdownShow); + this.$container.on('hide.bs.dropdown', this.options.onDropdownHide); + this.$container.on('shown.bs.dropdown', this.options.onDropdownShown); + this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden); + }, + + /** + * Builds the button of the multiselect. + */ + buildButton: function() { + this.$button = $(this.options.templates.button).addClass(this.options.buttonClass); + if (this.$select.attr('class') && this.options.inheritClass) { + this.$button.addClass(this.$select.attr('class')); + } + // Adopt active state. + if (this.$select.prop('disabled')) { + this.disable(); + } + else { + this.enable(); + } + + // Manually add button width if set. + if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') { + this.$button.css({ + 'width' : '100%', //this.options.buttonWidth, + 'overflow' : 'hidden', + 'text-overflow' : 'ellipsis' + }); + this.$container.css({ + 'width': this.options.buttonWidth + }); + } + + // Keep the tab index from the select. + var tabindex = this.$select.attr('tabindex'); + if (tabindex) { + this.$button.attr('tabindex', tabindex); + } + + this.$container.prepend(this.$button); + }, + + /** + * Builds the ul representing the dropdown menu. + */ + buildDropdown: function() { + + // Build ul. + this.$ul = $(this.options.templates.ul); + + if (this.options.dropRight) { + this.$ul.addClass('pull-right'); + } + + // Set max height of dropdown menu to activate auto scrollbar. + if (this.options.maxHeight) { + // TODO: Add a class for this option to move the css declarations. + this.$ul.css({ + 'max-height': this.options.maxHeight + 'px', + 'overflow-y': 'auto', + 'overflow-x': 'hidden' + }); + } + + if (this.options.dropUp) { + + var height = Math.min(this.options.maxHeight, $('option[data-role!="divider"]', this.$select).length*26 + $('option[data-role="divider"]', this.$select).length*19 + (this.options.includeSelectAllOption ? 26 : 0) + (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering ? 44 : 0)); + var moveCalc = height + 34; + + this.$ul.css({ + 'max-height': height + 'px', + 'overflow-y': 'auto', + 'overflow-x': 'hidden', + 'margin-top': "-" + moveCalc + 'px' + }); + } + + this.$container.append(this.$ul); + }, + + /** + * Build the dropdown options and binds all necessary events. + * + * Uses createDivider and createOptionValue to create the necessary options. + */ + buildDropdownOptions: function() { + + this.$select.children().each($.proxy(function(index, element) { + + var $element = $(element); + // Support optgroups and options without a group simultaneously. + var tag = $element.prop('tagName') + .toLowerCase(); + + if ($element.prop('value') === this.options.selectAllValue) { + return; + } + + if (tag === 'optgroup') { + this.createOptgroup(element); + } + else if (tag === 'option') { + + if ($element.data('role') === 'divider') { + this.createDivider(); + } + else { + this.createOptionValue(element); + } + + } + + // Other illegal tags will be ignored. + }, this)); + + // Bind the change event on the dropdown elements. + $('li:not(.multiselect-group) input', this.$ul).on('change', $.proxy(function(event) { + var $target = $(event.target); + + var checked = $target.prop('checked') || false; + var isSelectAllOption = $target.val() === this.options.selectAllValue; + + // Apply or unapply the configured selected class. + if (this.options.selectedClass) { + if (checked) { + $target.closest('li') + .addClass(this.options.selectedClass); + } + else { + $target.closest('li') + .removeClass(this.options.selectedClass); + } + } + + // Get the corresponding option. + var value = $target.val(); + var $option = this.getOptionByValue(value); + + var $optionsNotThis = $('option', this.$select).not($option); + var $checkboxesNotThis = $('input', this.$container).not($target); + + if (isSelectAllOption) { + + if (checked) { + this.selectAll(this.options.selectAllJustVisible, true); + } + else { + this.deselectAll(this.options.selectAllJustVisible, true); + } + } + else { + if (checked) { + $option.prop('selected', true); + + if (this.options.multiple) { + // Simply select additional option. + $option.prop('selected', true); + } + else { + // Unselect all other options and corresponding checkboxes. + if (this.options.selectedClass) { + $($checkboxesNotThis).closest('li').removeClass(this.options.selectedClass); + } + + $($checkboxesNotThis).prop('checked', false); + $optionsNotThis.prop('selected', false); + + // It's a single selection, so close. + this.$button.click(); + } + + if (this.options.selectedClass === "active") { + $optionsNotThis.closest("a").css("outline", ""); + } + } + else { + // Unselect option. + $option.prop('selected', false); + } + + // To prevent select all from firing onChange: #575 + this.options.onChange($option, checked); + + // Do not update select all or optgroups on select all change! + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + } + + this.$select.change(); + this.updateButtonText(); + + if(this.options.preventInputChangeEvent) { + return false; + } + }, this)); + + $('li a', this.$ul).on('mousedown', function(e) { + if (e.shiftKey) { + // Prevent selecting text by Shift+click + return false; + } + }); + + $('li a', this.$ul).on('touchstart click', $.proxy(function(event) { + event.stopPropagation(); + + var $target = $(event.target); + + if (event.shiftKey && this.options.multiple) { + if($target.is("label")){ // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431) + event.preventDefault(); + $target = $target.find("input"); + $target.prop("checked", !$target.prop("checked")); + } + var checked = $target.prop('checked') || false; + + if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range + var from = $target.closest("li").index(); + var to = this.lastToggledInput.closest("li").index(); + + if (from > to) { // Swap the indices + var tmp = to; + to = from; + from = tmp; + } + + // Make sure we grab all elements since slice excludes the last index + ++to; + + // Change the checkboxes and underlying options + var range = this.$ul.find("li").slice(from, to).find("input"); + + range.prop('checked', checked); + + if (this.options.selectedClass) { + range.closest('li') + .toggleClass(this.options.selectedClass, checked); + } + + for (var i = 0, j = range.length; i < j; i++) { + var $checkbox = $(range[i]); + + var $option = this.getOptionByValue($checkbox.val()); + + $option.prop('selected', checked); + } + } + + // Trigger the select "change" event + $target.trigger("change"); + } + + // Remembers last clicked option + if($target.is("input") && !$target.closest("li").is(".multiselect-item")){ + this.lastToggledInput = $target; + } + + $target.blur(); + }, this)); + + // Keyboard support. + this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) { + if ($('input[type="text"]', this.$container).is(':focus')) { + return; + } + + if (event.keyCode === 9 && this.$container.hasClass('open')) { + this.$button.click(); + } + else { + var $items = $(this.$container).find("li:not(.divider):not(.disabled) a").filter(":visible"); + + if (!$items.length) { + return; + } + + var index = $items.index($items.filter(':focus')); + + // Navigation up. + if (event.keyCode === 38 && index > 0) { + index--; + } + // Navigate down. + else if (event.keyCode === 40 && index < $items.length - 1) { + index++; + } + else if (!~index) { + index = 0; + } + + var $current = $items.eq(index); + $current.focus(); + + if (event.keyCode === 32 || event.keyCode === 13) { + var $checkbox = $current.find('input'); + + $checkbox.prop("checked", !$checkbox.prop("checked")); + $checkbox.change(); + } + + event.stopPropagation(); + event.preventDefault(); + } + }, this)); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + $("li.multiselect-group input", this.$ul).on("change", $.proxy(function(event) { + event.stopPropagation(); + + var $target = $(event.target); + var checked = $target.prop('checked') || false; + + var $li = $(event.target).closest('li'); + var $group = $li.nextUntil("li.multiselect-group") + .not('.multiselect-filter-hidden') + .not('.disabled'); + + var $inputs = $group.find("input"); + + var values = []; + var $options = []; + + if (this.options.selectedClass) { + if (checked) { + $li.addClass(this.options.selectedClass); + } + else { + $li.removeClass(this.options.selectedClass); + } + } + + $.each($inputs, $.proxy(function(index, input) { + var value = $(input).val(); + var $option = this.getOptionByValue(value); + + if (checked) { + $(input).prop('checked', true); + $(input).closest('li') + .addClass(this.options.selectedClass); + + $option.prop('selected', true); + } + else { + $(input).prop('checked', false); + $(input).closest('li') + .removeClass(this.options.selectedClass); + + $option.prop('selected', false); + } + + $options.push(this.getOptionByValue(value)); + }, this)) + + // Cannot use select or deselect here because it would call updateOptGroups again. + + this.options.onChange($options, checked); + + this.updateButtonText(); + this.updateSelectAll(); + }, this)); + } + + if (this.options.enableCollapsibleOptGroups && this.options.multiple) { + $("li.multiselect-group .caret-container", this.$ul).on("click", $.proxy(function(event) { + var $li = $(event.target).closest('li'); + var $inputs = $li.nextUntil("li.multiselect-group") + .not('.multiselect-filter-hidden'); + + var visible = true; + $inputs.each(function() { + visible = visible && $(this).is(':visible'); + }); + + if (visible) { + $inputs.hide() + .addClass('multiselect-collapsible-hidden'); + } + else { + $inputs.show() + .removeClass('multiselect-collapsible-hidden'); + } + }, this)); + + $("li.multiselect-all", this.$ul).css('background', '#f3f3f3').css('border-bottom', '1px solid #eaeaea'); + $("li.multiselect-all > a > label.checkbox", this.$ul).css('padding', '3px 20px 3px 35px'); + $("li.multiselect-group > a > input", this.$ul).css('margin', '4px 0px 5px -20px'); + } + }, + + /** + * Create an option using the given select option. + * + * @param {jQuery} element + */ + createOptionValue: function(element) { + var $element = $(element); + if ($element.is(':selected')) { + $element.prop('selected', true); + } + + // Support the label attribute on options. + var label = this.options.optionLabel(element); + var classes = this.options.optionClass(element); + var value = $element.val(); + var inputType = this.options.multiple ? "checkbox" : "radio"; + + var $li = $(this.options.templates.li); + var $label = $('label', $li); + $label.addClass(inputType); + $li.addClass(classes); + + if (this.options.enableHTML) { + $label.html(" " + label); + } + else { + $label.text(" " + label); + } + + var $checkbox = $('').attr('type', inputType); + + var name = this.options.checkboxName($element); + if (name) { + $checkbox.attr('name', name); + } + + $label.prepend($checkbox); + + var selected = $element.prop('selected') || false; + $checkbox.val(value); + + if (value === this.options.selectAllValue) { + $li.addClass("multiselect-item multiselect-all"); + $checkbox.parent().parent() + .addClass('multiselect-all'); + } + + $label.attr('title', $element.attr('title')); + + this.$ul.append($li); + + if ($element.is(':disabled')) { + $checkbox.attr('disabled', 'disabled') + .prop('disabled', true) + .closest('a') + .attr("tabindex", "-1") + .closest('li') + .addClass('disabled'); + } + + $checkbox.prop('checked', selected); + + if (selected && this.options.selectedClass) { + $checkbox.closest('li') + .addClass(this.options.selectedClass); + } + }, + + /** + * Creates a divider using the given select option. + * + * @param {jQuery} element + */ + createDivider: function(element) { + var $divider = $(this.options.templates.divider); + this.$ul.append($divider); + }, + + /** + * Creates an optgroup. + * + * @param {jQuery} group + */ + createOptgroup: function(group) { + var label = $(group).attr("label"); + var value = $(group).attr("value"); + var $li = $('
  • '); + + var classes = this.options.optionClass(group); + $li.addClass(classes); + + if (this.options.enableHTML) { + $('label b', $li).html(" " + label); + } + else { + $('label b', $li).text(" " + label); + } + + if (this.options.enableCollapsibleOptGroups && this.options.multiple) { + $('a', $li).append(''); + } + + if (this.options.enableClickableOptGroups && this.options.multiple) { + $('a label', $li).prepend(''); + } + + if ($(group).is(':disabled')) { + $li.addClass('disabled'); + } + + this.$ul.append($li); + + $("option", group).each($.proxy(function($, group) { + this.createOptionValue(group); + }, this)) + }, + + /** + * Build the select all. + * + * Checks if a select all has already been created. + */ + buildSelectAll: function() { + if (typeof this.options.selectAllValue === 'number') { + this.options.selectAllValue = this.options.selectAllValue.toString(); + } + + var alreadyHasSelectAll = this.hasSelectAll(); + + if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple + && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) { + + // Check whether to add a divider after the select all. + if (this.options.includeSelectAllDivider) { + this.$ul.prepend($(this.options.templates.divider)); + } + + var $li = $(this.options.templates.li); + $('label', $li).addClass("checkbox"); + + if (this.options.enableHTML) { + $('label', $li).html(" " + this.options.selectAllText); + } + else { + $('label', $li).text(" " + this.options.selectAllText); + } + + if (this.options.selectAllName) { + $('label', $li).prepend(''); + } + else { + $('label', $li).prepend(''); + } + + var $checkbox = $('input', $li); + $checkbox.val(this.options.selectAllValue); + + $li.addClass("multiselect-item multiselect-all"); + $checkbox.parent().parent() + .addClass('multiselect-all'); + + this.$ul.prepend($li); + + $checkbox.prop('checked', false); + } + }, + + /** + * Builds the filter. + */ + buildFilter: function() { + + // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength. + if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) { + var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering); + + if (this.$select.find('option').length >= enableFilterLength) { + + this.$filter = $(this.options.templates.filter); + $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder); + + // Adds optional filter clear button + if(this.options.includeFilterClearBtn) { + var clearBtn = $(this.options.templates.filterClearBtn); + clearBtn.on('click', $.proxy(function(event){ + clearTimeout(this.searchTimeout); + + this.$filter.find('.multiselect-search').val(''); + $('li', this.$ul).show().removeClass('multiselect-filter-hidden'); + + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + }, this)); + this.$filter.find('.input-group').append(clearBtn); + } + + this.$ul.prepend(this.$filter); + + this.$filter.val(this.query).on('click', function(event) { + event.stopPropagation(); + }).on('input keydown', $.proxy(function(event) { + // Cancel enter key default behaviour + if (event.which === 13) { + event.preventDefault(); + } + + // This is useful to catch "keydown" events after the browser has updated the control. + clearTimeout(this.searchTimeout); + + this.searchTimeout = this.asyncFunction($.proxy(function() { + + if (this.query !== event.target.value) { + this.query = event.target.value; + + var currentGroup, currentGroupVisible; + $.each($('li', this.$ul), $.proxy(function(index, element) { + var value = $('input', element).length > 0 ? $('input', element).val() : ""; + var text = $('label', element).text(); + + var filterCandidate = ''; + if ((this.options.filterBehavior === 'text')) { + filterCandidate = text; + } + else if ((this.options.filterBehavior === 'value')) { + filterCandidate = value; + } + else if (this.options.filterBehavior === 'both') { + filterCandidate = text + '\n' + value; + } + + if (value !== this.options.selectAllValue && text) { + + // By default lets assume that element is not + // interesting for this search. + var showElement = false; + + if (this.options.enableCaseInsensitiveFiltering) { + filterCandidate = filterCandidate.toLowerCase(); + this.query = this.query.toLowerCase(); + } + + if (this.options.enableFullValueFiltering && this.options.filterBehavior !== 'both') { + var valueToMatch = filterCandidate.trim().substring(0, this.query.length); + if (this.query.indexOf(valueToMatch) > -1) { + showElement = true; + } + } + else if (filterCandidate.indexOf(this.query) > -1) { + showElement = true; + } + + // Toggle current element (group or group item) according to showElement boolean. + $(element).toggle(showElement) + .toggleClass('multiselect-filter-hidden', !showElement); + + // Differentiate groups and group items. + if ($(element).hasClass('multiselect-group')) { + // Remember group status. + currentGroup = element; + currentGroupVisible = showElement; + } + else { + // Show group name when at least one of its items is visible. + if (showElement) { + $(currentGroup).show() + .removeClass('multiselect-filter-hidden'); + } + + // Show all group items when group name satisfies filter. + if (!showElement && currentGroupVisible) { + $(element).show() + .removeClass('multiselect-filter-hidden'); + } + } + } + }, this)); + } + + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + this.options.onFiltering(event.target); + + }, this), 300, this); + }, this)); + } + } + }, + + /** + * Unbinds the whole plugin. + */ + destroy: function() { + this.$container.remove(); + this.$select.show(); + + // reset original state + this.$select.prop('disabled', this.options.wasDisabled); + + this.$select.data('multiselect', null); + }, + + /** + * Refreshs the multiselect based on the selected options of the select. + */ + refresh: function () { + var inputs = $.map($('li input', this.$ul), $); + + $('option', this.$select).each($.proxy(function (index, element) { + var $elem = $(element); + var value = $elem.val(); + var $input; + for (var i = inputs.length; 0 < i--; /**/) { + if (value !== ($input = inputs[i]).val()) + continue; // wrong li + + if ($elem.is(':selected')) { + $input.prop('checked', true); + + if (this.options.selectedClass) { + $input.closest('li') + .addClass(this.options.selectedClass); + } + } + else { + $input.prop('checked', false); + + if (this.options.selectedClass) { + $input.closest('li') + .removeClass(this.options.selectedClass); + } + } + + if ($elem.is(":disabled")) { + $input.attr('disabled', 'disabled') + .prop('disabled', true) + .closest('li') + .addClass('disabled'); + } + else { + $input.prop('disabled', false) + .closest('li') + .removeClass('disabled'); + } + break; // assumes unique values + } + }, this)); + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Select all options of the given values. + * + * If triggerOnChange is set to true, the on change event is triggered if + * and only if one value is passed. + * + * @param {Array} selectValues + * @param {Boolean} triggerOnChange + */ + select: function(selectValues, triggerOnChange) { + if(!$.isArray(selectValues)) { + selectValues = [selectValues]; + } + + for (var i = 0; i < selectValues.length; i++) { + var value = selectValues[i]; + + if (value === null || value === undefined) { + continue; + } + + var $option = this.getOptionByValue(value); + var $checkbox = this.getInputByValue(value); + + if($option === undefined || $checkbox === undefined) { + continue; + } + + if (!this.options.multiple) { + this.deselectAll(false); + } + + if (this.options.selectedClass) { + $checkbox.closest('li') + .addClass(this.options.selectedClass); + } + + $checkbox.prop('checked', true); + $option.prop('selected', true); + + if (triggerOnChange) { + this.options.onChange($option, true); + } + } + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Clears all selected items. + */ + clearSelection: function () { + this.deselectAll(false); + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Deselects all options of the given values. + * + * If triggerOnChange is set to true, the on change event is triggered, if + * and only if one value is passed. + * + * @param {Array} deselectValues + * @param {Boolean} triggerOnChange + */ + deselect: function(deselectValues, triggerOnChange) { + if(!$.isArray(deselectValues)) { + deselectValues = [deselectValues]; + } + + for (var i = 0; i < deselectValues.length; i++) { + var value = deselectValues[i]; + + if (value === null || value === undefined) { + continue; + } + + var $option = this.getOptionByValue(value); + var $checkbox = this.getInputByValue(value); + + if($option === undefined || $checkbox === undefined) { + continue; + } + + if (this.options.selectedClass) { + $checkbox.closest('li') + .removeClass(this.options.selectedClass); + } + + $checkbox.prop('checked', false); + $option.prop('selected', false); + + if (triggerOnChange) { + this.options.onChange($option, false); + } + } + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Selects all enabled & visible options. + * + * If justVisible is true or not specified, only visible options are selected. + * + * @param {Boolean} justVisible + * @param {Boolean} triggerOnSelectAll + */ + selectAll: function (justVisible, triggerOnSelectAll) { + + var justVisible = typeof justVisible === 'undefined' ? true : justVisible; + var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); + var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); + + if(justVisible) { + $('input:enabled' , visibleLis).prop('checked', true); + visibleLis.addClass(this.options.selectedClass); + + $('input:enabled' , visibleLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', true); + }, this)); + } + else { + $('input:enabled' , allLis).prop('checked', true); + allLis.addClass(this.options.selectedClass); + + $('input:enabled' , allLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', true); + }, this)); + } + + $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (triggerOnSelectAll) { + this.options.onSelectAll(); + } + }, + + /** + * Deselects all options. + * + * If justVisible is true or not specified, only visible options are deselected. + * + * @param {Boolean} justVisible + */ + deselectAll: function (justVisible, triggerOnDeselectAll) { + + var justVisible = typeof justVisible === 'undefined' ? true : justVisible; + var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); + var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); + + if(justVisible) { + $('input[type="checkbox"]:enabled' , visibleLis).prop('checked', false); + visibleLis.removeClass(this.options.selectedClass); + + $('input[type="checkbox"]:enabled' , visibleLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', false); + }, this)); + } + else { + $('input[type="checkbox"]:enabled' , allLis).prop('checked', false); + allLis.removeClass(this.options.selectedClass); + + $('input[type="checkbox"]:enabled' , allLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', false); + }, this)); + } + + $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', false); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (triggerOnDeselectAll) { + this.options.onDeselectAll(); + } + }, + + /** + * Rebuild the plugin. + * + * Rebuilds the dropdown, the filter and the select all option. + */ + rebuild: function() { + this.$ul.html(''); + + // Important to distinguish between radios and checkboxes. + this.options.multiple = this.$select.attr('multiple') === "multiple"; + + this.buildSelectAll(); + this.buildDropdownOptions(); + this.buildFilter(); + + this.updateButtonText(); + this.updateSelectAll(true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { + this.disable(); + } + else { + this.enable(); + } + + if (this.options.dropRight) { + this.$ul.addClass('pull-right'); + } + }, + + /** + * The provided data will be used to build the dropdown. + */ + dataprovider: function(dataprovider) { + + var groupCounter = 0; + var $select = this.$select.empty(); + + $.each(dataprovider, function (index, option) { + var $tag; + + if ($.isArray(option.children)) { // create optiongroup tag + groupCounter++; + + $tag = $('').attr({ + label: option.label || 'Group ' + groupCounter, + disabled: !!option.disabled + }); + + forEach(option.children, function(subOption) { // add children option tags + var attributes = { + value: subOption.value, + label: subOption.label || subOption.value, + title: subOption.title, + selected: !!subOption.selected, + disabled: !!subOption.disabled + }; + + //Loop through attributes object and add key-value for each attribute + for (var key in subOption.attributes) { + attributes['data-' + key] = subOption.attributes[key]; + } + //Append original attributes + new data attributes to option + $tag.append($('
    diff --git a/resources/views/export/index.twig b/resources/views/export/index.twig index f5fcd3dadf..afe83f4c9c 100644 --- a/resources/views/export/index.twig +++ b/resources/views/export/index.twig @@ -100,5 +100,11 @@ + + {% endblock %} +{% block styles %} + + +{% endblock %} \ No newline at end of file diff --git a/resources/views/javascript/variables.twig b/resources/views/javascript/variables.twig new file mode 100644 index 0000000000..8d188af209 --- /dev/null +++ b/resources/views/javascript/variables.twig @@ -0,0 +1,29 @@ +// date range picker configuration: +var dateRangeConfig = { + startDate: moment("{{ picker.start }}"), + endDate: moment("{{ picker.end }}"), + linkTitle: "{{ linkTitle }}", + URL: "{{ route('daterange') }}", + firstDate: moment("{{ firstDate }}"), + currentPeriod: "{{ picker.current }}", + previousPeriod: "{{ picker.previous }}", + nextPeriod: "{{ picker.next }}", + everything: '{{ 'everything'|_|escape }}', + customRangeLabel: '{{ 'customRange'|_|escape }}', + applyLabel: '{{ 'apply'|_|escape }}', + cancelLabel: '{{ 'cancel'|_|escape }}', + fromLabel: '{{ 'from'|_|escape }}', + toLabel: '{{ 'to'|_|escape }}', + ranges: {{ picker.ranges|json_encode|raw }} +}; + +var language = "{{ language|escape }}"; +var currencyCode = '{{ currencyCode|escape('js') }}'; +var currencySymbol = '{{ currencySymbol|escape('js') }}'; +var mon_decimal_point = "{{ localeconv.mon_decimal_point|escape('js') }}"; +var mon_thousands_sep = "{{ localeconv.mon_thousands_sep|escape('js') }}"; +var frac_digits = {{ localeconv.frac_digits }}; +var noDataForChart = '{{ trans('firefly.no_data_for_chart')|escape }}'; +var showFullList = '{{ trans('firefly.show_full_list') }}'; +var showOnlyTop = '{{ trans('firefly.show_only_top',{number:listLength}) }}'; +var accountingConfig = {{ accounting|json_encode|raw }}; \ No newline at end of file diff --git a/resources/views/layout/default.twig b/resources/views/layout/default.twig index 85b8f95e3b..16a8c2f4e9 100644 --- a/resources/views/layout/default.twig +++ b/resources/views/layout/default.twig @@ -177,42 +177,7 @@ - - - + {% block scripts %}{% endblock %} diff --git a/resources/views/list/accounts.twig b/resources/views/list/accounts.twig index dbf3d7436c..baa896e1b7 100644 --- a/resources/views/list/accounts.twig +++ b/resources/views/list/accounts.twig @@ -28,7 +28,7 @@ {% for entry in account.accountmeta %} {% if entry.name == 'accountRole' %} - {{ entry.data|getAccountRole }} + {{ ('account_role_'~entry.data)|_ }} {% endif %} {% endfor %} diff --git a/resources/views/list/journals-tasker.twig b/resources/views/list/journals-tasker.twig index a52df725be..184ab9431f 100644 --- a/resources/views/list/journals-tasker.twig +++ b/resources/views/list/journals-tasker.twig @@ -33,13 +33,7 @@ -
    - {% if sorting %} - - {% endif %} - - -
    +
    {% if sorting %}{% endif %}
    diff --git a/resources/views/list/piggy-banks.twig b/resources/views/list/piggy-banks.twig index 7fa3188f14..c754498b8f 100644 --- a/resources/views/list/piggy-banks.twig +++ b/resources/views/list/piggy-banks.twig @@ -4,14 +4,17 @@   {{ 'piggy_bank'|_ }} {{ 'saved_so_far'|_ }} -   - {{ 'target_amount'|_ }} - {{ 'left_to_save'|_ }} +   + {{ 'target_amount'|_ }} + {{ 'left_to_save'|_ }} {% for piggyBank in piggyBanks %} + +   +
    diff --git a/resources/views/partials/control-bar.twig b/resources/views/partials/control-bar.twig index de0eaeed02..813549c0aa 100644 --- a/resources/views/partials/control-bar.twig +++ b/resources/views/partials/control-bar.twig @@ -63,7 +63,7 @@
  • - +
  • - +
  • - + {% endblock %} +{% block scripts %} + + + +{% endblock %} + +{% block styles %} + + +{% endblock %} \ No newline at end of file diff --git a/resources/views/piggy-banks/edit.twig b/resources/views/piggy-banks/edit.twig index 35f8f26cc5..df25b8aebc 100644 --- a/resources/views/piggy-banks/edit.twig +++ b/resources/views/piggy-banks/edit.twig @@ -56,3 +56,13 @@
  • {{ Form.close|raw }} {% endblock %} +{% block scripts %} + + + +{% endblock %} + +{% block styles %} + + +{% endblock %} \ No newline at end of file diff --git a/resources/views/preferences/index.twig b/resources/views/preferences/index.twig index 65080962ba..5432d9887f 100644 --- a/resources/views/preferences/index.twig +++ b/resources/views/preferences/index.twig @@ -24,7 +24,7 @@
    - + {# language #}

    {{ 'pref_languages'|_ }}

    {{ 'pref_languages_help'|_ }}

    @@ -272,3 +272,13 @@
    {% endblock %} +{% block scripts %} + + + +{% endblock %} + +{% block styles %} + + +{% endblock %} \ No newline at end of file diff --git a/resources/views/reports/index.twig b/resources/views/reports/index.twig index 9d3ddeffe9..85771558c0 100644 --- a/resources/views/reports/index.twig +++ b/resources/views/reports/index.twig @@ -35,17 +35,15 @@
    - {% for account in accounts %} -
    - -
    - {% endfor %} +
    @@ -121,22 +119,38 @@

    {{ ('quick_link_default_report')|_ }}

    {{ ('quick_link_audit_report')|_ }}

    {#

    {{ ('quick_link_category_report')|_ }}

    #} @@ -191,11 +205,18 @@ {% endblock %} +{% block styles %} + +{% endblock %} + {% block scripts %} - + {% endblock %} diff --git a/resources/views/reports/options/budget.twig b/resources/views/reports/options/budget.twig index b2e0e7862e..85d9bac773 100644 --- a/resources/views/reports/options/budget.twig +++ b/resources/views/reports/options/budget.twig @@ -1,8 +1,8 @@

    {{ 'select_budget'|_ }}

    -{% for budget in budgets %} - -{% endfor %} + diff --git a/resources/views/reports/options/category.twig b/resources/views/reports/options/category.twig index 1487c42ed0..126467ca26 100644 --- a/resources/views/reports/options/category.twig +++ b/resources/views/reports/options/category.twig @@ -1,8 +1,8 @@

    {{ 'select_category'|_ }}

    -{% for category in categories %} - -{% endfor %} + \ No newline at end of file diff --git a/resources/views/reports/partials/accounts.twig b/resources/views/reports/partials/accounts.twig index 3468f4c1d0..285e2a5d06 100644 --- a/resources/views/reports/partials/accounts.twig +++ b/resources/views/reports/partials/accounts.twig @@ -8,23 +8,23 @@ - {% for account in accountReport.getAccounts %} + {% for account in accountReport.accounts %} {{ account.name }} - {{ account.startBalance|formatAmount }} - {{ account.endBalance|formatAmount }} - {{ (account.endBalance - account.startBalance)|formatAmount }} + {{ account.start_balance|formatAmount }} + {{ account.end_balance|formatAmount }} + {{ (account.end_balance - account.start_balance)|formatAmount }} {% endfor %} {{ 'sumOfSums'|_ }} - {{ accountReport.getStart|formatAmount }} - {{ accountReport.getEnd|formatAmount }} - {{ accountReport.getDifference|formatAmount }} + {{ accountReport.start|formatAmount }} + {{ accountReport.end|formatAmount }} + {{ accountReport.difference|formatAmount }} diff --git a/resources/views/rules/partials/action.twig b/resources/views/rules/partials/action.twig index d3b215ee6b..f7e7a0886d 100644 --- a/resources/views/rules/partials/action.twig +++ b/resources/views/rules/partials/action.twig @@ -1,4 +1,4 @@ - + diff --git a/resources/views/rules/partials/trigger.twig b/resources/views/rules/partials/trigger.twig index 2f60699ea4..ea9ea7b9e6 100644 --- a/resources/views/rules/partials/trigger.twig +++ b/resources/views/rules/partials/trigger.twig @@ -1,4 +1,4 @@ - + diff --git a/resources/views/rules/rule-group/select-transactions.twig b/resources/views/rules/rule-group/select-transactions.twig index 6fc61bf15e..e33b186cd9 100644 --- a/resources/views/rules/rule-group/select-transactions.twig +++ b/resources/views/rules/rule-group/select-transactions.twig @@ -40,3 +40,13 @@
    {% endblock %} +{% block scripts %} + + + +{% endblock %} + +{% block styles %} + + +{% endblock %} \ No newline at end of file diff --git a/resources/views/rules/rule/create.twig b/resources/views/rules/rule/create.twig index 481473712c..dd17381052 100644 --- a/resources/views/rules/rule/create.twig +++ b/resources/views/rules/rule/create.twig @@ -63,7 +63,7 @@


    - {{ 'add_rule_trigger'|_ }} + {{ 'test_rule_triggers'|_ }}

    @@ -127,10 +127,13 @@ {% endblock %} {% block scripts %} - + - + + +{% endblock %} +{% block styles %} {% endblock %} diff --git a/resources/views/rules/rule/edit.twig b/resources/views/rules/rule/edit.twig index 0887615600..84f56a72a3 100644 --- a/resources/views/rules/rule/edit.twig +++ b/resources/views/rules/rule/edit.twig @@ -126,10 +126,11 @@ {% endblock %} {% block scripts %} - + - + + {% endblock %} diff --git a/resources/views/search/partials/accounts.twig b/resources/views/search/partials/accounts.twig index 45c4a2466a..c86b89c345 100644 --- a/resources/views/search/partials/accounts.twig +++ b/resources/views/search/partials/accounts.twig @@ -24,7 +24,7 @@ {% for entry in account.accountmeta %} {% if entry.name == 'accountRole' %} - {{ trans('firefly.'~entry.data|getAccountRole) }} + {{ ('account_role_'~entry.data)|_ }} {% endif %} {% endfor %} diff --git a/resources/views/tags/create.twig b/resources/views/tags/create.twig index 1864f85843..8d85dcc86a 100644 --- a/resources/views/tags/create.twig +++ b/resources/views/tags/create.twig @@ -84,5 +84,11 @@ + + {% endblock %} +{% block styles %} + + +{% endblock %} \ No newline at end of file diff --git a/resources/views/tags/edit.twig b/resources/views/tags/edit.twig index 99366b5546..4012de4137 100644 --- a/resources/views/tags/edit.twig +++ b/resources/views/tags/edit.twig @@ -87,5 +87,11 @@ + + {% endblock %} +{% block styles %} + + +{% endblock %} \ No newline at end of file diff --git a/resources/views/transactions/mass-edit.twig b/resources/views/transactions/mass/edit.twig similarity index 97% rename from resources/views/transactions/mass-edit.twig rename to resources/views/transactions/mass/edit.twig index 1399aea9d4..f4dfb503a9 100644 --- a/resources/views/transactions/mass-edit.twig +++ b/resources/views/transactions/mass/edit.twig @@ -101,6 +101,9 @@ {% endblock %} {% block scripts %} + - + {% endblock %} diff --git a/resources/views/transactions/show.twig b/resources/views/transactions/show.twig index f20e920b98..c3a0ab7079 100644 --- a/resources/views/transactions/show.twig +++ b/resources/views/transactions/show.twig @@ -82,6 +82,15 @@ {{ 'budgets'|_ }} {{ journalBudgets(journal)|raw }} + + {% if journal.hasMeta('interest_date') %} + + {{ trans('list.interest_date') }} + {{ journal.getMeta('interest_date').formatLocalized(monthAndDayFormat) }} + + + {% endif %} + {% if journal.hasMeta('book_date') %} {{ trans('list.book_date') }} @@ -95,13 +104,7 @@ {% endif %} - {% if journal.hasMeta('interest_date') %} - - {{ trans('list.interest_date') }} - {{ journal.getMeta('interest_date').formatLocalized(monthAndDayFormat) }} - - {% endif %} {% if journal.hasMeta('due_date') %} {{ trans('list.due_date') }} @@ -211,7 +214,7 @@ {% if transactions|length == 1 %}
    -

    {{ 'transaction_journal_convert_options'|_ }}

    +

    {{ 'transaction_journal_other_options'|_ }}

    {% if journal.transactionType.type != "Withdrawal" %} @@ -239,6 +242,14 @@

    {% endif %} + {% if transactions|length == 1 %} +

    + + + {{ ('clone_'~journal.transactionType.type|lower)|_ }} + +

    + {% endif %}
    diff --git a/resources/views/transactions/create.twig b/resources/views/transactions/single/create.twig similarity index 94% rename from resources/views/transactions/create.twig rename to resources/views/transactions/single/create.twig index ac232f8b0b..baa6544194 100644 --- a/resources/views/transactions/create.twig +++ b/resources/views/transactions/single/create.twig @@ -48,7 +48,7 @@ {{ ExpandedForm.amount('amount') }} - {{ ExpandedForm.date('date', phpdate('Y-m-d')) }} + {{ ExpandedForm.date('date', preFilled.date|default(phpdate('Y-m-d'))) }} - #} @@ -224,10 +223,13 @@ - - + + + {% endblock %} {% block styles %} + + {% endblock %} diff --git a/resources/views/transactions/delete.twig b/resources/views/transactions/single/delete.twig similarity index 100% rename from resources/views/transactions/delete.twig rename to resources/views/transactions/single/delete.twig diff --git a/resources/views/transactions/edit.twig b/resources/views/transactions/single/edit.twig similarity index 96% rename from resources/views/transactions/edit.twig rename to resources/views/transactions/single/edit.twig index b3e3a37326..82149d1d50 100644 --- a/resources/views/transactions/edit.twig +++ b/resources/views/transactions/single/edit.twig @@ -233,9 +233,13 @@ - - + + + + {% endblock %} {% block styles %} + + {% endblock %} diff --git a/resources/views/transactions/edit-split.twig b/resources/views/transactions/split/edit.twig similarity index 97% rename from resources/views/transactions/edit-split.twig rename to resources/views/transactions/split/edit.twig index f47ec11af3..e78dffb4ab 100644 --- a/resources/views/transactions/edit-split.twig +++ b/resources/views/transactions/split/edit.twig @@ -299,6 +299,8 @@ {% endblock %} {% block styles %} + + {% endblock %} {% block scripts %} - - + + + {% endblock %} diff --git a/routes/web.php b/routes/web.php index 71793df647..bd438f5f59 100755 --- a/routes/web.php +++ b/routes/web.php @@ -49,6 +49,7 @@ Route::group( /** * For the two factor routes, the user must be logged in, but NOT 2FA. Account confirmation does not matter here. + * * @deprecated */ Route::group( @@ -364,25 +365,38 @@ Route::group( } ); +/** + * Budget Controller + */ +Route::group( + ['middleware' => 'user-full-auth', 'prefix' => 'javascript', 'as' => 'javascript.'], function () { + Route::get('variables.js', ['uses' => 'JavascriptController@variables', 'as' => 'variables']); +} +); + /** * JSON Controller */ Route::group( ['middleware' => 'user-full-auth', 'prefix' => 'json', 'as' => 'json.'], function () { Route::get('expense-accounts', ['uses' => 'JsonController@expenseAccounts', 'as' => 'expense-accounts']); + Route::get('all-accounts', ['uses' => 'JsonController@allAccounts', 'as' => 'all-accounts']); Route::get('revenue-accounts', ['uses' => 'JsonController@revenueAccounts', 'as' => 'revenue-accounts']); Route::get('categories', ['uses' => 'JsonController@categories', 'as' => 'categories']); + Route::get('budgets', ['uses' => 'JsonController@budgets', 'as' => 'budgets']); Route::get('tags', ['uses' => 'JsonController@tags', 'as' => 'tags']); Route::get('tour', ['uses' => 'JsonController@tour', 'as' => 'tour']); Route::get('box/in', ['uses' => 'JsonController@boxIn', 'as' => 'box.in']); Route::get('box/out', ['uses' => 'JsonController@boxOut', 'as' => 'box.out']); Route::get('box/bills-unpaid', ['uses' => 'JsonController@boxBillsUnpaid', 'as' => 'box.paid']); Route::get('box/bills-paid', ['uses' => 'JsonController@boxBillsPaid', 'as' => 'box.unpaid']); + Route::get('transaction-journals/all', ['uses' => 'JsonController@allTransactionJournals', 'as' => 'all-transaction-journals']); Route::get('transaction-journals/{what}', ['uses' => 'JsonController@transactionJournals', 'as' => 'transaction-journals']); + Route::get('transaction-types', ['uses' => 'JsonController@transactionTypes', 'as' => 'transaction-types']); Route::get('trigger', ['uses' => 'JsonController@trigger', 'as' => 'trigger']); Route::get('action', ['uses' => 'JsonController@action', 'as' => 'action']); - Route::post('end-tour', ['uses' => 'JsonController@endTour','as' => 'end-tour']); + Route::post('end-tour', ['uses' => 'JsonController@endTour', 'as' => 'end-tour']); } ); @@ -624,6 +638,7 @@ Route::group( Route::post('store/{what}', ['uses' => 'SingleController@store', 'as' => 'store'])->where(['what' => 'withdrawal|deposit|transfer']); Route::post('update/{tj}', ['uses' => 'SingleController@update', 'as' => 'update']); Route::post('destroy/{tj}', ['uses' => 'SingleController@destroy', 'as' => 'destroy']); + Route::get('clone/{tj}', ['uses' => 'SingleController@cloneTransaction', 'as' => 'clone']); } ); @@ -657,7 +672,6 @@ Route::group( ['middleware' => 'user-full-auth', 'namespace' => 'Transaction', 'prefix' => 'transactions/convert', 'as' => 'transactions.convert.'], function () { Route::get('{transaction_type}/{tj}', ['uses' => 'ConvertController@index', 'as' => 'index']); Route::post('{transaction_type}/{tj}', ['uses' => 'ConvertController@postIndex', 'as' => 'index.post']); - } ); diff --git a/test.sh b/test.sh index 3c06c5acec..a88423359e 100755 --- a/test.sh +++ b/test.sh @@ -70,7 +70,7 @@ then php artisan migrate:refresh --seed # call test data generation script - $(which php) /sites/FF3/test-data/artisan generate:data testing sqlite + $(which php) /sites/FF3/test-data/artisan generate:data local sqlite # copy new database over backup (resets backup) cp $DATABASE $DATABASECOPY fi diff --git a/tests/TestCase.php b/tests/TestCase.php index f0aed4568b..c2a57c8e43 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -84,16 +84,6 @@ abstract class TestCase extends Illuminate\Foundation\Testing\TestCase ]; } - /** - * Sets up the fixture, for example, opens a network connection. - * This method is called before a test is executed. - */ - public function setUp() - { - parent::setUp(); - - } - /** * @return User */ @@ -104,6 +94,39 @@ abstract class TestCase extends Illuminate\Foundation\Testing\TestCase return $user; } + /** + * @return array + */ + public function naughtyStringProvider() + { + /* + * If on Travis, return very small set. + */ + if (getenv('TRAVIS') == 'true') { + return [['Default value']]; + + } + $path = realpath(__DIR__ . '/../resources/tests/blns.base64.json'); + $content = file_get_contents($path); + $array = json_decode($content); + $return = []; + foreach ($array as $entry) { + $return[] = [base64_decode($entry)]; + } + + return $return; + } + + /** + * Sets up the fixture, for example, opens a network connection. + * This method is called before a test is executed. + */ + public function setUp() + { + parent::setUp(); + + } + /** * @return User */ diff --git a/tests/acceptance/Controllers/Transaction/SingleControllerTest.php b/tests/acceptance/Controllers/Transaction/SingleControllerTest.php index beae215fcd..81e67a4e21 100644 --- a/tests/acceptance/Controllers/Transaction/SingleControllerTest.php +++ b/tests/acceptance/Controllers/Transaction/SingleControllerTest.php @@ -85,7 +85,7 @@ class SingleControllerTest extends TestCase } /** - * @covers \FireflyIII\Http\Controllers\Transaction\SingleController::store + * @covers \FireflyIII\Http\Controllers\Transaction\SingleController::store */ public function testStore() { @@ -99,7 +99,7 @@ class SingleControllerTest extends TestCase 'source_account_id' => 1, 'destination_account_name' => 'Some destination', 'date' => '2016-01-01', - 'description' => 'Some description', + 'description' => 'Test descr', ]; $this->call('post', route('transactions.store', ['withdrawal']), $data); $this->assertResponseStatus(302); @@ -138,5 +138,4 @@ class SingleControllerTest extends TestCase $this->see('